1
# -*- test-case-name: twisted.words.test -*-
2
# Copyright (c) 2001-2005 Twisted Matrix Laboratories.
3
# See LICENSE for details.
8
MSNP8 Protocol (client only) - semi-experimental
12
This module provides support for clients using the MSN Protocol (MSNP8).
13
There are basically 3 servers involved in any MSN session:
17
The DispatchClient class handles connections to the
18
dispatch server, which basically delegates users to a
19
suitable notification server.
21
You will want to subclass this and handle the gotNotificationReferral
24
I{Notification Server}
26
The NotificationClient class handles connections to the
27
notification server, which acts as a session server
28
(state updates, message negotiation etc...)
32
The SwitchboardClient handles connections to switchboard
33
servers which are used to conduct conversations with other users.
35
There are also two classes (FileSend and FileReceive) used
38
Clients handle events in two ways.
40
- each client request requiring a response will return a Deferred,
41
the callback for same will be fired when the server sends the
43
- Events which are not in response to any client request have
44
respective methods which should be overridden and handled in
47
Most client request callbacks require more than one argument,
48
and since Deferreds can only pass the callback one result,
49
most of the time the callback argument will be a tuple of
50
values (documented in the respective request method).
51
To make reading/writing code easier, callbacks can be defined in
52
a number of ways to handle this 'cleanly'. One way would be to
53
define methods like: def callBack(self, (arg1, arg2, arg)): ...
54
another way would be to do something like:
55
d.addCallback(lambda result: myCallback(*result)).
57
If the server sends an error response to a client request,
58
the errback of the corresponding Deferred will be called,
59
the argument being the corresponding error code.
62
Due to the lack of an official spec for MSNP8, extra checking
63
than may be deemed necessary often takes place considering the
64
server is never 'wrong'. Thus, if gotBadLine (in any of the 3
65
main clients) is called, or an MSNProtocolError is raised, it's
66
probably a good idea to submit a bug report. ;)
67
Use of this module requires that PyOpenSSL is installed.
71
- check message hooks with invalid x-msgsinvite messages.
75
@author: U{Sam Jordan<mailto:sam@twistedmatrix.com>}
78
from __future__ import nested_scopes
82
from twisted.internet import reactor
83
from twisted.internet.defer import Deferred
84
from twisted.internet.protocol import ClientFactory
85
from twisted.internet.ssl import ClientContextFactory
86
from twisted.python import failure, log
88
from twisted.protocols.basic import LineReceiver
89
from twisted.web.http import HTTPClient
92
import types, operator, os, md5
93
from random import randint
94
from urllib import quote, unquote
96
MSN_PROTOCOL_VERSION = "MSNP8 CVR0" # protocol version
97
MSN_PORT = 1863 # default dispatch server port
98
MSN_MAX_MESSAGE = 1664 # max message length
99
MSN_CHALLENGE_STR = "Q1P7W2E4J9R8U3S5" # used for server challenges
100
MSN_CVR_STR = "0x0409 win 4.10 i386 MSNMSGR 5.0.0544 MSMSGS" # :(
120
STATUS_ONLINE = 'NLN'
121
STATUS_OFFLINE = 'FLN'
122
STATUS_HIDDEN = 'HDN'
133
def checkParamLen(num, expected, cmd, error=None):
135
error = "Invalid Number of Parameters for %s" % cmd
137
raise MSNProtocolError, error
139
def _parseHeader(h, v):
141
Split a certin number of known
142
header values with the format:
143
field1=val,field2=val,field3=val into
144
a dict mapping fields to values.
145
@param h: the header's key
146
@param v: the header's value as a string
149
if h in ('passporturls','authentication-info','www-authenticate'):
150
v = v.replace('Passport1.4','').lstrip()
152
for fieldPair in v.split(','):
154
field,value = fieldPair.split('=',1)
155
fields[field.lower()] = value
157
fields[field.lower()] = ''
162
def _parsePrimitiveHost(host):
164
h,p = host.replace('https://','').split('/',1)
168
def _login(userHandle, passwd, nexusServer, cached=0, authData=''):
170
This function is used internally and should not ever be called
174
def _cb(server, auth):
175
loginFac = ClientFactory()
176
loginFac.protocol = lambda : PassportLogin(cb, userHandle, passwd, server, auth)
177
reactor.connectSSL(_parsePrimitiveHost(server)[0], 443, loginFac, ClientContextFactory())
180
_cb(nexusServer, authData)
182
fac = ClientFactory()
184
d.addCallbacks(_cb, callbackArgs=(authData,))
185
d.addErrback(lambda f: cb.errback(f))
186
fac.protocol = lambda : PassportNexus(d, nexusServer)
187
reactor.connectSSL(_parsePrimitiveHost(nexusServer)[0], 443, fac, ClientContextFactory())
191
class PassportNexus(HTTPClient):
194
Used to obtain the URL of a valid passport
197
This class is used internally and should
198
not be instantiated directly -- that is,
199
The passport logging in process is handled
200
transparantly by NotificationClient.
203
def __init__(self, deferred, host):
204
self.deferred = deferred
205
self.host, self.path = _parsePrimitiveHost(host)
207
def connectionMade(self):
208
HTTPClient.connectionMade(self)
209
self.sendCommand('GET', self.path)
210
self.sendHeader('Host', self.host)
214
def handleHeader(self, header, value):
216
self.headers[h] = _parseHeader(h, value)
218
def handleEndHeaders(self):
220
self.transport.loseConnection()
221
if not self.headers.has_key('passporturls') or not self.headers['passporturls'].has_key('dalogin'):
222
self.deferred.errback(failure.Failure(failure.DefaultException("Invalid Nexus Reply")))
223
self.deferred.callback('https://' + self.headers['passporturls']['dalogin'])
225
def handleResponse(self, r):
228
class PassportLogin(HTTPClient):
230
This class is used internally to obtain
231
a login ticket from a passport HTTPS
232
server -- it should not be used directly.
237
def __init__(self, deferred, userHandle, passwd, host, authData):
238
self.deferred = deferred
239
self.userHandle = userHandle
241
self.authData = authData
242
self.host, self.path = _parsePrimitiveHost(host)
244
def connectionMade(self):
245
self.sendCommand('GET', self.path)
246
self.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
247
'sign-in=%s,pwd=%s,%s' % (quote(self.userHandle), self.passwd,self.authData))
248
self.sendHeader('Host', self.host)
252
def handleHeader(self, header, value):
254
self.headers[h] = _parseHeader(h, value)
256
def handleEndHeaders(self):
259
self._finished = 1 # I think we need this because of HTTPClient
261
self.transport.loseConnection()
262
authHeader = 'authentication-info'
263
_interHeader = 'www-authenticate'
264
if self.headers.has_key(_interHeader):
265
authHeader = _interHeader
267
info = self.headers[authHeader]
268
status = info['da-status']
269
handler = getattr(self, 'login_%s' % (status,), None)
275
self.deferred.errback(failure.Failure(e))
277
def handleResponse(self, r):
280
def login_success(self, info):
281
ticket = info['from-pp']
282
ticket = ticket[1:len(ticket)-1]
283
self.deferred.callback((LOGIN_SUCCESS, ticket))
285
def login_failed(self, info):
286
self.deferred.callback((LOGIN_FAILURE, unquote(info['cbtxt'])))
288
def login_redir(self, info):
289
self.deferred.callback((LOGIN_REDIRECT, self.headers['location'], self.authData))
292
class MSNProtocolError(Exception):
294
This Exception is basically used for debugging
295
purposes, as the official MSN server should never
296
send anything _wrong_ and nobody in their right
297
mind would run their B{own} MSN server.
298
If it is raised by default command handlers
299
(handle_BLAH) the error will be logged.
304
class MSNCommandFailed(Exception):
306
The server said that the command failed.
309
def __init__(self, errorCode):
310
self.errorCode = errorCode
313
return ("Command failed: %s (error code %d)"
314
% (errorCodes[self.errorCode], self.errorCode))
319
I am the class used to represent an 'instant' message.
321
@ivar userHandle: The user handle (passport) of the sender
322
(this is only used when receiving a message)
323
@ivar screenName: The screen name of the sender (this is only used
324
when receiving a message)
325
@ivar message: The message
326
@ivar headers: The message headers
328
@ivar length: The message length (including headers and line endings)
329
@ivar ack: This variable is used to tell the server how to respond
330
once the message has been sent. If set to MESSAGE_ACK
331
(default) the server will respond with an ACK upon receiving
332
the message, if set to MESSAGE_NACK the server will respond
333
with a NACK upon failure to receive the message.
334
If set to MESSAGE_ACK_NONE the server will do nothing.
335
This is relevant for the return value of
336
SwitchboardClient.sendMessage (which will return
337
a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
338
and will fire when the respective ACK or NACK is received).
339
If set to MESSAGE_ACK_NONE sendMessage will return None.
343
MESSAGE_ACK_NONE = 'U'
347
def __init__(self, length=0, userHandle="", screenName="", message=""):
348
self.userHandle = userHandle
349
self.screenName = screenName
350
self.message = message
351
self.headers = {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
355
def _calcMessageLen(self):
357
used to calculte the number to send
358
as the message length when sending a message.
360
return reduce(operator.add, [len(x[0]) + len(x[1]) + 4 for x in self.headers.items()]) + len(self.message) + 2
362
def setHeader(self, header, value):
363
""" set the desired header """
364
self.headers[header] = value
366
def getHeader(self, header):
368
get the desired header value
369
@raise KeyError: if no such header exists.
371
return self.headers[header]
373
def hasHeader(self, header):
374
""" check to see if the desired header exists """
375
return self.headers.has_key(header)
377
def getMessage(self):
378
""" return the message - not including headers """
381
def setMessage(self, message):
382
""" set the message text """
383
self.message = message
388
This class represents a contact (user).
390
@ivar userHandle: The contact's user handle (passport).
391
@ivar screenName: The contact's screen name.
392
@ivar groups: A list of all the group IDs which this
394
@ivar lists: An integer representing the sum of all lists
395
that this contact belongs to.
396
@ivar status: The contact's status code.
397
@type status: str if contact's status is known, None otherwise.
399
@ivar homePhone: The contact's home phone number.
400
@type homePhone: str if known, otherwise None.
401
@ivar workPhone: The contact's work phone number.
402
@type workPhone: str if known, otherwise None.
403
@ivar mobilePhone: The contact's mobile phone number.
404
@type mobilePhone: str if known, otherwise None.
405
@ivar hasPager: Whether or not this user has a mobile pager
409
def __init__(self, userHandle="", screenName="", lists=0, groups=[], status=None):
410
self.userHandle = userHandle
411
self.screenName = screenName
413
self.groups = [] # if applicable
414
self.status = status # current status
417
self.homePhone = None
418
self.workPhone = None
419
self.mobilePhone = None
422
def setPhone(self, phoneType, value):
424
set phone numbers/values for this specific user.
425
for phoneType check the *_PHONE constants and HAS_PAGER
428
t = phoneType.upper()
430
self.homePhone = value
431
elif t == WORK_PHONE:
432
self.workPhone = value
433
elif t == MOBILE_PHONE:
434
self.mobilePhone = value
436
self.hasPager = value
438
raise ValueError, "Invalid Phone Type"
440
def addToList(self, listType):
442
Update the lists attribute to
443
reflect being part of the
446
self.lists |= listType
448
def removeFromList(self, listType):
450
Update the lists attribute to
451
reflect being removed from the
454
self.lists ^= listType
456
class MSNContactList:
458
This class represents a basic MSN contact list.
460
@ivar contacts: All contacts on my various lists
461
@type contacts: dict (mapping user handles to MSNContact objects)
462
@ivar version: The current contact list version (used for list syncing)
463
@ivar groups: a mapping of group ids to group names
464
(groups can only exist on the forward list)
468
This is used only for storage and doesn't effect the
469
server's contact list.
479
def _getContactsFromList(self, listType):
481
Obtain all contacts which belong
482
to the given list type.
484
return dict([(uH,obj) for uH,obj in self.contacts.items() if obj.lists & listType])
486
def addContact(self, contact):
490
self.contacts[contact.userHandle] = contact
492
def remContact(self, userHandle):
497
del self.contacts[userHandle]
501
def getContact(self, userHandle):
503
Obtain the MSNContact object
504
associated with the given
506
@return: the MSNContact object if
507
the user exists, or None.
510
return self.contacts[userHandle]
514
def getBlockedContacts(self):
516
Obtain all the contacts on my block list
518
return self._getContactsFromList(BLOCK_LIST)
520
def getAuthorizedContacts(self):
522
Obtain all the contacts on my auth list.
523
(These are contacts which I have verified
524
can view my state changes).
526
return self._getContactsFromList(ALLOW_LIST)
528
def getReverseContacts(self):
530
Get all contacts on my reverse list.
531
(These are contacts which have added me
532
to their forward list).
534
return self._getContactsFromList(REVERSE_LIST)
536
def getContacts(self):
538
Get all contacts on my forward list.
539
(These are the contacts which I have added
542
return self._getContactsFromList(FORWARD_LIST)
544
def setGroup(self, id, name):
546
Keep a mapping from the given id
549
self.groups[id] = name
551
def remGroup(self, id):
553
Removed the stored group
554
mapping for the given id.
560
for c in self.contacts:
565
class MSNEventBase(LineReceiver):
567
This class provides support for handling / dispatching events and is the
568
base class of the three main client protocols (DispatchClient,
569
NotificationClient, SwitchboardClient)
573
self.ids = {} # mapping of ids to Deferreds
577
self.currentMessage = None
579
def connectionLost(self, reason):
583
def connectionMade(self):
586
def _fireCallback(self, id, *args):
588
Fire the callback for the given id
589
if one exists and return 1, else return false
591
if self.ids.has_key(id):
592
self.ids[id][0].callback(args)
597
def _nextTransactionID(self):
598
""" return a usable transaction ID """
600
if self.currentID > 1000:
602
return self.currentID
604
def _createIDMapping(self, data=None):
606
return a unique transaction ID that is mapped internally to a
607
deferred .. also store arbitrary data if it is needed
609
id = self._nextTransactionID()
611
self.ids[id] = (d, data)
614
def checkMessage(self, message):
616
process received messages to check for file invitations and
617
typing notifications and other control type messages
619
raise NotImplementedError
621
def lineReceived(self, line):
622
if self.currentMessage:
623
self.currentMessage.readPos += len(line+CR+LF)
626
if self.currentMessage.readPos == self.currentMessage.length:
627
self.rawDataReceived("") # :(
630
header, value = line.split(':')
632
raise MSNProtocolError, "Invalid Message Header"
633
self.currentMessage.setHeader(header, unquote(value).lstrip())
636
cmd, params = line.split(' ', 1)
638
raise MSNProtocolError, "Invalid Message, %s" % repr(line)
641
raise MSNProtocolError, "Invalid Command, %s" % repr(cmd)
644
id = int(params.split()[0])
646
self.ids[id][0].errback(MSNCommandFailed(errorCode))
649
else: # we received an error which doesn't map to a sent command
650
self.gotError(errorCode)
653
handler = getattr(self, "handle_%s" % cmd.upper(), None)
656
handler(params.split())
657
except MSNProtocolError, why:
658
self.gotBadLine(line, why)
660
self.handle_UNKNOWN(cmd, params.split())
662
def rawDataReceived(self, data):
664
self.currentMessage.readPos += len(data)
665
diff = self.currentMessage.readPos - self.currentMessage.length
667
self.currentMessage.message += data[:-diff]
670
self.currentMessage.message += data
672
self.currentMessage += data
674
del self.currentMessage.readPos
675
m = self.currentMessage
676
self.currentMessage = None
677
self.setLineMode(extra)
678
if not self.checkMessage(m):
682
### protocol command handlers - no need to override these.
684
def handle_MSG(self, params):
685
checkParamLen(len(params), 3, 'MSG')
687
messageLen = int(params[2])
689
raise MSNProtocolError, "Invalid Parameter for MSG length argument"
690
self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName=unquote(params[1]))
692
def handle_UNKNOWN(self, cmd, params):
693
""" implement me in subclasses if you want to handle unknown events """
694
log.msg("Received unknown command (%s), params: %s" % (cmd, params))
698
def gotMessage(self, message):
700
called when we receive a message - override in notification
701
and switchboard clients
703
raise NotImplementedError
705
def gotBadLine(self, line, why):
706
""" called when a handler notifies me that this line is broken """
707
log.msg('Error in line: %s (%s)' % (line, why))
709
def gotError(self, errorCode):
711
called when the server sends an error which is not in
712
response to a sent command (ie. it has no matching transaction ID)
714
log.msg('Error %s' % (errorCodes[errorCode]))
716
class DispatchClient(MSNEventBase):
718
This class provides support for clients connecting to the dispatch server
719
@ivar userHandle: your user handle (passport) needed before connecting.
722
# eventually this may become an attribute of the
726
def connectionMade(self):
727
MSNEventBase.connectionMade(self)
728
self.sendLine('VER %s %s' % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
730
### protocol command handlers ( there is no need to override these )
732
def handle_VER(self, params):
733
versions = params[1:]
734
if versions is None or ' '.join(versions) != MSN_PROTOCOL_VERSION:
735
self.transport.loseConnection()
736
raise MSNProtocolError, "Invalid version response"
737
id = self._nextTransactionID()
738
self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.userHandle))
740
def handle_CVR(self, params):
741
self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.userHandle))
743
def handle_XFR(self, params):
745
raise MSNProtocolError, "Invalid number of parameters for XFR"
746
id, refType, addr = params[:3]
747
# was addr a host:port pair?
749
host, port = addr.split(':')
754
self.gotNotificationReferral(host, int(port))
758
def gotNotificationReferral(self, host, port):
760
called when we get a referral to the notification server.
762
@param host: the notification server's hostname
763
@param port: the port to connect to
768
class NotificationClient(MSNEventBase):
770
This class provides support for clients connecting
771
to the notification server.
774
factory = None # sssh pychecker
776
def __init__(self, currentID=0):
777
MSNEventBase.__init__(self)
778
self.currentID = currentID
779
self._state = ['DISCONNECTED', {}]
781
def _setState(self, state):
782
self._state[0] = state
785
return self._state[0]
787
def _getStateData(self, key):
788
return self._state[1][key]
790
def _setStateData(self, key, value):
791
self._state[1][key] = value
793
def _remStateData(self, *args):
795
del self._state[1][key]
797
def connectionMade(self):
798
MSNEventBase.connectionMade(self)
799
self._setState('CONNECTED')
800
self.sendLine("VER %s %s" % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
802
def connectionLost(self, reason):
803
self._setState('DISCONNECTED')
805
MSNEventBase.connectionLost(self, reason)
807
def checkMessage(self, message):
808
""" hook used for detecting specific notification messages """
809
cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
810
if 'text/x-msmsgsprofile' in cTypes:
811
self.gotProfile(message)
815
### protocol command handlers - no need to override these
817
def handle_VER(self, params):
818
versions = params[1:]
819
if versions is None or ' '.join(versions) != MSN_PROTOCOL_VERSION:
820
self.transport.loseConnection()
821
raise MSNProtocolError, "Invalid version response"
822
self.sendLine("CVR %s %s %s" % (self._nextTransactionID(), MSN_CVR_STR, self.factory.userHandle))
824
def handle_CVR(self, params):
825
self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.factory.userHandle))
827
def handle_USR(self, params):
828
if len(params) != 4 and len(params) != 6:
829
raise MSNProtocolError, "Invalid Number of Parameters for USR"
831
mechanism = params[1]
832
if mechanism == "OK":
833
self.loggedIn(params[2], unquote(params[3]), int(params[4]))
834
elif params[2].upper() == "S":
835
# we need to obtain auth from a passport server
837
d = _login(f.userHandle, f.password, f.passportServer, authData=params[3])
838
d.addCallback(self._passportLogin)
839
d.addErrback(self._passportError)
841
def _passportLogin(self, result):
842
if result[0] == LOGIN_REDIRECT:
843
d = _login(self.factory.userHandle, self.factory.password,
844
result[1], cached=1, authData=result[2])
845
d.addCallback(self._passportLogin)
846
d.addErrback(self._passportError)
847
elif result[0] == LOGIN_SUCCESS:
848
self.sendLine("USR %s TWN S %s" % (self._nextTransactionID(), result[1]))
849
elif result[0] == LOGIN_FAILURE:
850
self.loginFailure(result[1])
852
def _passportError(self, failure):
853
self.loginFailure("Exception while authenticating: %s" % failure)
855
def handle_CHG(self, params):
856
checkParamLen(len(params), 3, 'CHG')
858
if not self._fireCallback(id, params[1]):
859
self.statusChanged(params[1])
861
def handle_ILN(self, params):
862
checkParamLen(len(params), 5, 'ILN')
863
self.gotContactStatus(params[1], params[2], unquote(params[3]))
865
def handle_CHL(self, params):
866
checkParamLen(len(params), 2, 'CHL')
867
self.sendLine("QRY %s msmsgs@msnmsgr.com 32" % self._nextTransactionID())
868
self.transport.write(md5.md5(params[1] + MSN_CHALLENGE_STR).hexdigest())
870
def handle_QRY(self, params):
873
def handle_NLN(self, params):
874
checkParamLen(len(params), 4, 'NLN')
875
self.contactStatusChanged(params[0], params[1], unquote(params[2]))
877
def handle_FLN(self, params):
878
checkParamLen(len(params), 1, 'FLN')
879
self.contactOffline(params[0])
881
def handle_LST(self, params):
882
# support no longer exists for manually
883
# requesting lists - why do I feel cleaner now?
884
if self._getState() != 'SYNC':
886
contact = MSNContact(userHandle=params[0], screenName=unquote(params[1]),
887
lists=int(params[2]))
888
if contact.lists & FORWARD_LIST:
889
contact.groups.extend(map(int, params[3].split(',')))
890
self._getStateData('list').addContact(contact)
891
self._setStateData('last_contact', contact)
892
sofar = self._getStateData('lst_sofar') + 1
893
if sofar == self._getStateData('lst_reply'):
894
# this is the best place to determine that
895
# a syn realy has finished - msn _may_ send
896
# BPR information for the last contact
897
# which is unfortunate because it means
898
# that the real end of a syn is non-deterministic.
899
# to handle this we'll keep 'last_contact' hanging
900
# around in the state data and update it if we need
902
self._setState('SESSION')
903
contacts = self._getStateData('list')
904
phone = self._getStateData('phone')
905
id = self._getStateData('synid')
906
self._remStateData('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
907
self._fireCallback(id, contacts, phone)
909
self._setStateData('lst_sofar',sofar)
911
def handle_BLP(self, params):
912
# check to see if this is in response to a SYN
913
if self._getState() == 'SYNC':
914
self._getStateData('list').privacy = listCodeToID[params[0].lower()]
917
self._fireCallback(id, int(params[1]), listCodeToID[params[2].lower()])
919
def handle_GTC(self, params):
920
# check to see if this is in response to a SYN
921
if self._getState() == 'SYNC':
922
if params[0].lower() == "a":
923
self._getStateData('list').autoAdd = 0
924
elif params[0].lower() == "n":
925
self._getStateData('list').autoAdd = 1
927
raise MSNProtocolError, "Invalid Paramater for GTC" # debug
930
if params[1].lower() == "a":
931
self._fireCallback(id, 0)
932
elif params[1].lower() == "n":
933
self._fireCallback(id, 1)
935
raise MSNProtocolError, "Invalid Paramater for GTC" # debug
937
def handle_SYN(self, params):
940
self._setState('SESSION')
941
self._fireCallback(id, None, None)
943
contacts = MSNContactList()
944
contacts.version = int(params[1])
945
self._setStateData('list', contacts)
946
self._setStateData('lst_reply', int(params[2]))
947
self._setStateData('lsg_reply', int(params[3]))
948
self._setStateData('lst_sofar', 0)
949
self._setStateData('phone', [])
951
def handle_LSG(self, params):
952
if self._getState() == 'SYNC':
953
self._getStateData('list').groups[int(params[0])] = unquote(params[1])
955
# Please see the comment above the requestListGroups / requestList methods
956
# regarding support for this
959
# self._getStateData('groups').append((int(params[4]), unquote(params[5])))
960
# if params[3] == params[4]: # this was the last group
961
# self._fireCallback(int(params[0]), self._getStateData('groups'), int(params[1]))
962
# self._remStateData('groups')
964
def handle_PRP(self, params):
965
if self._getState() == 'SYNC':
966
self._getStateData('phone').append((params[0], unquote(params[1])))
968
self._fireCallback(int(params[0]), int(params[1]), unquote(params[3]))
970
def handle_BPR(self, params):
971
numParams = len(params)
972
if numParams == 2: # part of a syn
973
self._getStateData('last_contact').setPhone(params[0], unquote(params[1]))
975
self.gotPhoneNumber(int(params[0]), params[1], params[2], unquote(params[3]))
977
def handle_ADG(self, params):
978
checkParamLen(len(params), 5, 'ADG')
980
if not self._fireCallback(id, int(params[1]), unquote(params[2]), int(params[3])):
981
raise MSNProtocolError, "ADG response does not match up to a request" # debug
983
def handle_RMG(self, params):
984
checkParamLen(len(params), 3, 'RMG')
986
if not self._fireCallback(id, int(params[1]), int(params[2])):
987
raise MSNProtocolError, "RMG response does not match up to a request" # debug
989
def handle_REG(self, params):
990
checkParamLen(len(params), 5, 'REG')
992
if not self._fireCallback(id, int(params[1]), int(params[2]), unquote(params[3])):
993
raise MSNProtocolError, "REG response does not match up to a request" # debug
995
def handle_ADD(self, params):
996
numParams = len(params)
997
if numParams < 5 or params[1].upper() not in ('AL','BL','RL','FL'):
998
raise MSNProtocolError, "Invalid Paramaters for ADD" # debug
1000
listType = params[1].lower()
1001
listVer = int(params[2])
1002
userHandle = params[3]
1004
if numParams == 6: # they sent a group id
1005
if params[1].upper() != "FL":
1006
raise MSNProtocolError, "Only forward list can contain groups" # debug
1007
groupID = int(params[5])
1008
if not self._fireCallback(id, listCodeToID[listType], userHandle, listVer, groupID):
1009
self.userAddedMe(userHandle, unquote(params[4]), listVer)
1011
def handle_REM(self, params):
1012
numParams = len(params)
1013
if numParams < 4 or params[1].upper() not in ('AL','BL','FL','RL'):
1014
raise MSNProtocolError, "Invalid Paramaters for REM" # debug
1016
listType = params[1].lower()
1017
listVer = int(params[2])
1018
userHandle = params[3]
1021
if params[1] != "FL":
1022
raise MSNProtocolError, "Only forward list can contain groups" # debug
1023
groupID = int(params[4])
1024
if not self._fireCallback(id, listCodeToID[listType], userHandle, listVer, groupID):
1025
if listType.upper() == "RL":
1026
self.userRemovedMe(userHandle, listVer)
1028
def handle_REA(self, params):
1029
checkParamLen(len(params), 4, 'REA')
1031
self._fireCallback(id, int(params[1]), unquote(params[3]))
1033
def handle_XFR(self, params):
1034
checkParamLen(len(params), 5, 'XFR')
1036
# check to see if they sent a host/port pair
1038
host, port = params[2].split(':')
1043
if not self._fireCallback(id, host, int(port), params[4]):
1044
raise MSNProtocolError, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
1046
def handle_RNG(self, params):
1047
checkParamLen(len(params), 6, 'RNG')
1048
# check for host:port pair
1050
host, port = params[1].split(":")
1055
self.gotSwitchboardInvitation(int(params[0]), host, port, params[3], params[4],
1058
def handle_OUT(self, params):
1059
checkParamLen(len(params), 1, 'OUT')
1060
if params[0] == "OTH":
1061
self.multipleLogin()
1062
elif params[0] == "SSD":
1063
self.serverGoingDown()
1065
raise MSNProtocolError, "Invalid Parameters received for OUT" # debug
1069
def loggedIn(self, userHandle, screenName, verified):
1071
Called when the client has logged in.
1072
The default behaviour of this method is to
1073
update the factory with our screenName and
1074
to sync the contact list (factory.contacts).
1075
When this is complete self.listSynchronized
1078
@param userHandle: our userHandle
1079
@param screenName: our screenName
1080
@param verified: 1 if our passport has been (verified), 0 if not.
1081
(i'm not sure of the significace of this)
1084
self.factory.screenName = screenName
1085
if not self.factory.contacts:
1088
listVersion = self.factory.contacts.version
1089
self.syncList(listVersion).addCallback(self.listSynchronized)
1091
def loginFailure(self, message):
1093
Called when the client fails to login.
1095
@param message: a message indicating the problem that was encountered
1099
def gotProfile(self, message):
1101
Called after logging in when the server sends an initial
1102
message with MSN/passport specific profile information
1103
such as country, number of kids, etc.
1104
Check the message headers for the specific values.
1106
@param message: The profile message
1110
def listSynchronized(self, *args):
1112
Lists are now synchronized by default upon logging in, this
1113
method is called after the synchronization has finished
1114
and the factory now has the up-to-date contacts.
1118
def statusChanged(self, statusCode):
1120
Called when our status changes and it isn't in response to
1121
a client command. By default we will update the status
1122
attribute of the factory.
1124
@param statusCode: 3-letter status code
1126
self.factory.status = statusCode
1128
def gotContactStatus(self, statusCode, userHandle, screenName):
1130
Called after loggin in when the server sends status of online contacts.
1131
By default we will update the status attribute of the contact stored
1134
@param statusCode: 3-letter status code
1135
@param userHandle: the contact's user handle (passport)
1136
@param screenName: the contact's screen name
1138
self.factory.contacts.getContact(userHandle).status = statusCode
1140
def contactStatusChanged(self, statusCode, userHandle, screenName):
1142
Called when we're notified that a contact's status has changed.
1143
By default we will update the status attribute of the contact
1144
stored on the factory.
1146
@param statusCode: 3-letter status code
1147
@param userHandle: the contact's user handle (passport)
1148
@param screenName: the contact's screen name
1150
self.factory.contacts.getContact(userHandle).status = statusCode
1152
def contactOffline(self, userHandle):
1154
Called when a contact goes offline. By default this method
1155
will update the status attribute of the contact stored
1158
@param userHandle: the contact's user handle
1160
self.factory.contacts.getContact(userHandle).status = STATUS_OFFLINE
1162
def gotPhoneNumber(self, listVersion, userHandle, phoneType, number):
1164
Called when the server sends us phone details about
1165
a specific user (for example after a user is added
1166
the server will send their status, phone details etc.
1167
By default we will update the list version for the
1168
factory's contact list and update the phone details
1169
for the specific user.
1171
@param listVersion: the new list version
1172
@param userHandle: the contact's user handle (passport)
1173
@param phoneType: the specific phoneType
1174
(*_PHONE constants or HAS_PAGER)
1175
@param number: the value/phone number.
1177
self.factory.contacts.version = listVersion
1178
self.factory.contacts.getContact(userHandle).setPhone(phoneType, number)
1180
def userAddedMe(self, userHandle, screenName, listVersion):
1182
Called when a user adds me to their list. (ie. they have been added to
1183
the reverse list. By default this method will update the version of
1184
the factory's contact list -- that is, if the contact already exists
1185
it will update the associated lists attribute, otherwise it will create
1186
a new MSNContact object and store it.
1188
@param userHandle: the userHandle of the user
1189
@param screenName: the screen name of the user
1190
@param listVersion: the new list version
1191
@type listVersion: int
1193
self.factory.contacts.version = listVersion
1194
c = self.factory.contacts.getContact(userHandle)
1196
c = MSNContact(userHandle=userHandle, screenName=screenName)
1197
self.factory.contacts.addContact(c)
1198
c.addToList(REVERSE_LIST)
1200
def userRemovedMe(self, userHandle, listVersion):
1202
Called when a user removes us from their contact list
1203
(they are no longer on our reverseContacts list.
1204
By default this method will update the version of
1205
the factory's contact list -- that is, the user will
1206
be removed from the reverse list and if they are no longer
1207
part of any lists they will be removed from the contact
1210
@param userHandle: the contact's user handle (passport)
1211
@param listVersion: the new list version
1213
self.factory.contacts.version = listVersion
1214
c = self.factory.contacts.getContact(userHandle)
1215
c.removeFromList(REVERSE_LIST)
1217
self.factory.contacts.remContact(c.userHandle)
1219
def gotSwitchboardInvitation(self, sessionID, host, port,
1220
key, userHandle, screenName):
1222
Called when we get an invitation to a switchboard server.
1223
This happens when a user requests a chat session with us.
1225
@param sessionID: session ID number, must be remembered for logging in
1226
@param host: the hostname of the switchboard server
1227
@param port: the port to connect to
1228
@param key: used for authorization when connecting
1229
@param userHandle: the user handle of the person who invited us
1230
@param screenName: the screen name of the person who invited us
1234
def multipleLogin(self):
1236
Called when the server says there has been another login
1237
under our account, the server should disconnect us right away.
1241
def serverGoingDown(self):
1243
Called when the server has notified us that it is going down for
1250
def changeStatus(self, status):
1252
Change my current status. This method will add
1253
a default callback to the returned Deferred
1254
which will update the status attribute of the
1257
@param status: 3-letter status code (as defined by
1258
the STATUS_* constants)
1259
@return: A Deferred, the callback of which will be
1260
fired when the server confirms the change
1261
of status. The callback argument will be
1262
a tuple with the new status code as the
1266
id, d = self._createIDMapping()
1267
self.sendLine("CHG %s %s" % (id, status))
1269
self.factory.status = r[0]
1271
return d.addCallback(_cb)
1273
# I am no longer supporting the process of manually requesting
1274
# lists or list groups -- as far as I can see this has no use
1275
# if lists are synchronized and updated correctly, which they
1276
# should be. If someone has a specific justified need for this
1277
# then please contact me and i'll re-enable/fix support for it.
1279
#def requestList(self, listType):
1281
# request the desired list type
1283
# @param listType: (as defined by the *_LIST constants)
1284
# @return: A Deferred, the callback of which will be
1285
# fired when the list has been retrieved.
1286
# The callback argument will be a tuple with
1287
# the only element being a list of MSNContact
1290
# # this doesn't need to ever be used if syncing of the lists takes place
1291
# # i.e. please don't use it!
1292
# warnings.warn("Please do not use this method - use the list syncing process instead")
1293
# id, d = self._createIDMapping()
1294
# self.sendLine("LST %s %s" % (id, listIDToCode[listType].upper()))
1295
# self._setStateData('list',[])
1298
def setPrivacyMode(self, privLevel):
1300
Set my privacy mode on the server.
1303
This only keeps the current privacy setting on
1304
the server for later retrieval, it does not
1305
effect the way the server works at all.
1307
@param privLevel: This parameter can be true, in which
1308
case the server will keep the state as
1309
'al' which the official client interprets
1310
as -> allow messages from only users on
1311
the allow list. Alternatively it can be
1312
false, in which case the server will keep
1313
the state as 'bl' which the official client
1314
interprets as -> allow messages from all
1315
users except those on the block list.
1317
@return: A Deferred, the callback of which will be fired when
1318
the server replies with the new privacy setting.
1319
The callback argument will be a tuple, the 2 elements
1320
of which being the list version and either 'al'
1321
or 'bl' (the new privacy setting).
1324
id, d = self._createIDMapping()
1326
self.sendLine("BLP %s AL" % id)
1328
self.sendLine("BLP %s BL" % id)
1331
def syncList(self, version):
1333
Used for keeping an up-to-date contact list.
1334
A callback is added to the returned Deferred
1335
that updates the contact list on the factory
1336
and also sets my state to STATUS_ONLINE.
1339
This is called automatically upon signing
1340
in using the version attribute of
1341
factory.contacts, so you may want to persist
1342
this object accordingly. Because of this there
1343
is no real need to ever call this method
1346
@param version: The current known list version
1348
@return: A Deferred, the callback of which will be
1349
fired when the server sends an adequate reply.
1350
The callback argument will be a tuple with two
1351
elements, the new list (MSNContactList) and
1352
your current state (a dictionary). If the version
1353
you sent _was_ the latest list version, both elements
1354
will be None. To just request the list send a version of 0.
1357
self._setState('SYNC')
1358
id, d = self._createIDMapping(data=str(version))
1359
self._setStateData('synid',id)
1360
self.sendLine("SYN %s %s" % (id, version))
1362
self.changeStatus(STATUS_ONLINE)
1363
if r[0] is not None:
1364
self.factory.contacts = r[0]
1366
return d.addCallback(_cb)
1369
# I am no longer supporting the process of manually requesting
1370
# lists or list groups -- as far as I can see this has no use
1371
# if lists are synchronized and updated correctly, which they
1372
# should be. If someone has a specific justified need for this
1373
# then please contact me and i'll re-enable/fix support for it.
1375
#def requestListGroups(self):
1377
# Request (forward) list groups.
1379
# @return: A Deferred, the callback for which will be called
1380
# when the server responds with the list groups.
1381
# The callback argument will be a tuple with two elements,
1382
# a dictionary mapping group IDs to group names and the
1383
# current list version.
1386
# # this doesn't need to be used if syncing of the lists takes place (which it SHOULD!)
1387
# # i.e. please don't use it!
1388
# warnings.warn("Please do not use this method - use the list syncing process instead")
1389
# id, d = self._createIDMapping()
1390
# self.sendLine("LSG %s" % id)
1391
# self._setStateData('groups',{})
1394
def setPhoneDetails(self, phoneType, value):
1396
Set/change my phone numbers stored on the server.
1398
@param phoneType: phoneType can be one of the following
1399
constants - HOME_PHONE, WORK_PHONE,
1400
MOBILE_PHONE, HAS_PAGER.
1401
These are pretty self-explanatory, except
1402
maybe HAS_PAGER which refers to whether or
1403
not you have a pager.
1404
@param value: for all of the *_PHONE constants the value is a
1405
phone number (str), for HAS_PAGER accepted values
1406
are 'Y' (for yes) and 'N' (for no).
1408
@return: A Deferred, the callback for which will be fired when
1409
the server confirms the change has been made. The
1410
callback argument will be a tuple with 2 elements, the
1411
first being the new list version (int) and the second
1412
being the new phone number value (str).
1414
# XXX: Add a default callback which updates
1415
# factory.contacts.version and the relevant phone
1417
id, d = self._createIDMapping()
1418
self.sendLine("PRP %s %s %s" % (id, phoneType, quote(value)))
1421
def addListGroup(self, name):
1423
Used to create a new list group.
1424
A default callback is added to the
1425
returned Deferred which updates the
1426
contacts attribute of the factory.
1428
@param name: The desired name of the new group.
1430
@return: A Deferred, the callbacck for which will be called
1431
when the server clarifies that the new group has been
1432
created. The callback argument will be a tuple with 3
1433
elements: the new list version (int), the new group name
1434
(str) and the new group ID (int).
1437
id, d = self._createIDMapping()
1438
self.sendLine("ADG %s %s 0" % (id, quote(name)))
1440
self.factory.contacts.version = r[0]
1441
self.factory.contacts.setGroup(r[1], r[2])
1443
return d.addCallback(_cb)
1445
def remListGroup(self, groupID):
1447
Used to remove a list group.
1448
A default callback is added to the
1449
returned Deferred which updates the
1450
contacts attribute of the factory.
1452
@param groupID: the ID of the desired group to be removed.
1454
@return: A Deferred, the callback for which will be called when
1455
the server clarifies the deletion of the group.
1456
The callback argument will be a tuple with 2 elements:
1457
the new list version (int) and the group ID (int) of
1461
id, d = self._createIDMapping()
1462
self.sendLine("RMG %s %s" % (id, groupID))
1464
self.factory.contacts.version = r[0]
1465
self.factory.contacts.remGroup(r[1])
1467
return d.addCallback(_cb)
1469
def renameListGroup(self, groupID, newName):
1471
Used to rename an existing list group.
1472
A default callback is added to the returned
1473
Deferred which updates the contacts attribute
1476
@param groupID: the ID of the desired group to rename.
1477
@param newName: the desired new name for the group.
1479
@return: A Deferred, the callback for which will be called
1480
when the server clarifies the renaming.
1481
The callback argument will be a tuple of 3 elements,
1482
the new list version (int), the group id (int) and
1483
the new group name (str).
1486
id, d = self._createIDMapping()
1487
self.sendLine("REG %s %s %s 0" % (id, groupID, quote(newName)))
1489
self.factory.contacts.version = r[0]
1490
self.factory.contacts.setGroup(r[1], r[2])
1492
return d.addCallback(_cb)
1494
def addContact(self, listType, userHandle, groupID=0):
1496
Used to add a contact to the desired list.
1497
A default callback is added to the returned
1498
Deferred which updates the contacts attribute of
1499
the factory with the new contact information.
1500
If you are adding a contact to the forward list
1501
and you want to associate this contact with multiple
1502
groups then you will need to call this method for each
1503
group you would like to add them to, changing the groupID
1504
parameter. The default callback will take care of updating
1505
the group information on the factory's contact list.
1507
@param listType: (as defined by the *_LIST constants)
1508
@param userHandle: the user handle (passport) of the contact
1510
@param groupID: the group ID for which to associate this contact
1511
with. (default 0 - default group). Groups are only
1512
valid for FORWARD_LIST.
1514
@return: A Deferred, the callback for which will be called when
1515
the server has clarified that the user has been added.
1516
The callback argument will be a tuple with 4 elements:
1517
the list type, the contact's user handle, the new list
1518
version, and the group id (if relevant, otherwise it
1522
id, d = self._createIDMapping()
1523
listType = listIDToCode[listType].upper()
1524
if listType == "FL":
1525
self.sendLine("ADD %s FL %s %s %s" % (id, userHandle, userHandle, groupID))
1527
self.sendLine("ADD %s %s %s %s" % (id, listType, userHandle, userHandle))
1530
self.factory.contacts.version = r[2]
1531
c = self.factory.contacts.getContact(r[1])
1533
c = MSNContact(userHandle=r[1])
1535
c.groups.append(r[3])
1538
return d.addCallback(_cb)
1540
def remContact(self, listType, userHandle, groupID=0):
1542
Used to remove a contact from the desired list.
1543
A default callback is added to the returned deferred
1544
which updates the contacts attribute of the factory
1545
to reflect the new contact information. If you are
1546
removing from the forward list then you will need to
1547
supply a groupID, if the contact is in more than one
1548
group then they will only be removed from this group
1549
and not the entire forward list, but if this is their
1550
only group they will be removed from the whole list.
1552
@param listType: (as defined by the *_LIST constants)
1553
@param userHandle: the user handle (passport) of the
1554
contact being removed
1555
@param groupID: the ID of the group to which this contact
1556
belongs (only relevant for FORWARD_LIST,
1559
@return: A Deferred, the callback for which will be called when
1560
the server has clarified that the user has been removed.
1561
The callback argument will be a tuple of 4 elements:
1562
the list type, the contact's user handle, the new list
1563
version, and the group id (if relevant, otherwise it will
1567
id, d = self._createIDMapping()
1568
listType = listIDToCode[listType].upper()
1569
if listType == "FL":
1570
self.sendLine("REM %s FL %s %s" % (id, userHandle, groupID))
1572
self.sendLine("REM %s %s %s" % (id, listType, userHandle))
1575
l = self.factory.contacts
1577
c = l.getContact(r[1])
1580
if group: # they may not have been removed from the list
1581
c.groups.remove(group)
1585
c.removeFromList(r[0])
1587
l.remContact(c.userHandle)
1589
return d.addCallback(_cb)
1591
def changeScreenName(self, newName):
1593
Used to change your current screen name.
1594
A default callback is added to the returned
1595
Deferred which updates the screenName attribute
1596
of the factory and also updates the contact list
1599
@param newName: the new screen name
1601
@return: A Deferred, the callback for which will be called
1602
when the server sends an adequate reply.
1603
The callback argument will be a tuple of 2 elements:
1604
the new list version and the new screen name.
1607
id, d = self._createIDMapping()
1608
self.sendLine("REA %s %s %s" % (id, self.factory.userHandle, quote(newName)))
1610
self.factory.contacts.version = r[0]
1611
self.factory.screenName = r[1]
1613
return d.addCallback(_cb)
1615
def requestSwitchboardServer(self):
1617
Used to request a switchboard server to use for conversations.
1619
@return: A Deferred, the callback for which will be called when
1620
the server responds with the switchboard information.
1621
The callback argument will be a tuple with 3 elements:
1622
the host of the switchboard server, the port and a key
1623
used for logging in.
1626
id, d = self._createIDMapping()
1627
self.sendLine("XFR %s SB" % id)
1632
Used to log out of the notification server.
1633
After running the method the server is expected
1634
to close the connection.
1637
self.sendLine("OUT")
1639
class NotificationFactory(ClientFactory):
1641
Factory for the NotificationClient protocol.
1642
This is basically responsible for keeping
1643
the state of the client and thus should be used
1644
in a 1:1 situation with clients.
1646
@ivar contacts: An MSNContactList instance reflecting
1647
the current contact list -- this is
1648
generally kept up to date by the default
1650
@ivar userHandle: The client's userHandle, this is expected
1651
to be set by the client and is used by the
1652
protocol (for logging in etc).
1653
@ivar screenName: The client's current screen-name -- this is
1654
generally kept up to date by the default
1656
@ivar password: The client's password -- this is (obviously)
1657
expected to be set by the client.
1658
@ivar passportServer: This must point to an msn passport server
1659
(the whole URL is required)
1660
@ivar status: The status of the client -- this is generally kept
1661
up to date by the default command handlers
1668
passportServer = 'https://nexus.passport.com/rdr/pprdr.asp'
1670
protocol = NotificationClient
1673
# XXX: A lot of the state currently kept in
1674
# instances of SwitchboardClient is likely to
1675
# be moved into a factory at some stage in the
1678
class SwitchboardClient(MSNEventBase):
1680
This class provides support for clients connecting to a switchboard server.
1682
Switchboard servers are used for conversations with other people
1683
on the MSN network. This means that the number of conversations at
1684
any given time will be directly proportional to the number of
1685
connections to varioius switchboard servers.
1687
MSN makes no distinction between single and group conversations,
1688
so any number of users may be invited to join a specific conversation
1689
taking place on a switchboard server.
1691
@ivar key: authorization key, obtained when receiving
1692
invitation / requesting switchboard server.
1693
@ivar userHandle: your user handle (passport)
1694
@ivar sessionID: unique session ID, used if you are replying
1695
to a switchboard invitation
1696
@ivar reply: set this to 1 in connectionMade or before to signifiy
1697
that you are replying to a switchboard invitation.
1708
MSNEventBase.__init__(self)
1709
self.pendingUsers = {}
1710
self.cookies = {'iCookies' : {}, 'external' : {}} # will maybe be moved to a factory in the future
1712
def connectionMade(self):
1713
MSNEventBase.connectionMade(self)
1714
print 'sending initial stuff'
1717
def connectionLost(self, reason):
1718
self.cookies['iCookies'] = {}
1719
self.cookies['external'] = {}
1720
MSNEventBase.connectionLost(self, reason)
1722
def _sendInit(self):
1724
send initial data based on whether we are replying to an invitation
1727
id = self._nextTransactionID()
1729
self.sendLine("USR %s %s %s" % (id, self.userHandle, self.key))
1731
self.sendLine("ANS %s %s %s %s" % (id, self.userHandle, self.key, self.sessionID))
1733
def _newInvitationCookie(self):
1735
if self._iCookie > 1000:
1737
return self._iCookie
1739
def _checkTyping(self, message, cTypes):
1740
""" helper method for checkMessage """
1741
if 'text/x-msmsgscontrol' in cTypes and message.hasHeader('TypingUser'):
1742
self.userTyping(message)
1745
def _checkFileInvitation(self, message, info):
1746
""" helper method for checkMessage """
1747
guid = info.get('Application-GUID', '').lower()
1748
name = info.get('Application-Name', '').lower()
1750
# Both fields are required, but we'll let some lazy clients get away
1751
# with only sending a name, if it is easy for us to recognize the
1752
# name (the name is localized, so this check might fail for lazy,
1753
# non-english clients, but I'm not about to include "file transfer"
1754
# in 80 different languages here).
1756
if name != "file transfer" and guid != classNameToGUID["file transfer"]:
1759
cookie = int(info['Invitation-Cookie'])
1760
fileName = info['Application-File']
1761
fileSize = int(info['Application-FileSize'])
1763
log.msg('Received munged file transfer request ... ignoring.')
1765
self.gotSendRequest(fileName, fileSize, cookie, message)
1768
def _checkFileResponse(self, message, info):
1769
""" helper method for checkMessage """
1771
cmd = info['Invitation-Command'].upper()
1772
cookie = int(info['Invitation-Cookie'])
1775
accept = (cmd == 'ACCEPT') and 1 or 0
1776
requested = self.cookies['iCookies'].get(cookie)
1779
requested[0].callback((accept, cookie, info))
1780
del self.cookies['iCookies'][cookie]
1783
def _checkFileInfo(self, message, info):
1784
""" helper method for checkMessage """
1786
ip = info['IP-Address']
1787
iCookie = int(info['Invitation-Cookie'])
1788
aCookie = int(info['AuthCookie'])
1789
cmd = info['Invitation-Command'].upper()
1790
port = int(info['Port'])
1793
accept = (cmd == 'ACCEPT') and 1 or 0
1794
requested = self.cookies['external'].get(iCookie)
1796
return 1 # we didn't ask for this
1797
requested[0].callback((accept, ip, port, aCookie, info))
1798
del self.cookies['external'][iCookie]
1801
def checkMessage(self, message):
1803
hook for detecting any notification type messages
1804
(e.g. file transfer)
1806
cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
1807
if self._checkTyping(message, cTypes):
1809
if 'text/x-msmsgsinvite' in cTypes:
1810
# header like info is sent as part of the message body.
1812
for line in message.message.split('\r\n'):
1814
key, val = line.split(':')
1815
info[key] = val.lstrip()
1818
if self._checkFileInvitation(message, info) or self._checkFileInfo(message, info) or self._checkFileResponse(message, info):
1820
elif 'text/x-clientcaps' in cTypes:
1821
# do something with capabilities
1826
def handle_USR(self, params):
1827
checkParamLen(len(params), 4, 'USR')
1828
if params[1] == "OK":
1832
def handle_CAL(self, params):
1833
checkParamLen(len(params), 3, 'CAL')
1835
if params[1].upper() == "RINGING":
1836
self._fireCallback(id, int(params[2])) # session ID as parameter
1839
def handle_JOI(self, params):
1840
checkParamLen(len(params), 2, 'JOI')
1841
self.userJoined(params[0], unquote(params[1]))
1843
# users participating in the current chat
1844
def handle_IRO(self, params):
1845
checkParamLen(len(params), 5, 'IRO')
1846
self.pendingUsers[params[3]] = unquote(params[4])
1847
if params[1] == params[2]:
1848
self.gotChattingUsers(self.pendingUsers)
1849
self.pendingUsers = {}
1851
# finished listing users
1852
def handle_ANS(self, params):
1853
checkParamLen(len(params), 2, 'ANS')
1854
if params[1] == "OK":
1857
def handle_ACK(self, params):
1858
checkParamLen(len(params), 1, 'ACK')
1859
self._fireCallback(int(params[0]), None)
1861
def handle_NAK(self, params):
1862
checkParamLen(len(params), 1, 'NAK')
1863
self._fireCallback(int(params[0]), None)
1865
def handle_BYE(self, params):
1866
#checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
1867
self.userLeft(params[0])
1873
called when all login details have been negotiated.
1874
Messages can now be sent, or new users invited.
1878
def gotChattingUsers(self, users):
1880
called after connecting to an existing chat session.
1882
@param users: A dict mapping user handles to screen names
1883
(current users taking part in the conversation)
1887
def userJoined(self, userHandle, screenName):
1889
called when a user has joined the conversation.
1891
@param userHandle: the user handle (passport) of the user
1892
@param screenName: the screen name of the user
1896
def userLeft(self, userHandle):
1898
called when a user has left the conversation.
1900
@param userHandle: the user handle (passport) of the user.
1904
def gotMessage(self, message):
1906
called when we receive a message.
1908
@param message: the associated MSNMessage object
1912
def userTyping(self, message):
1914
called when we receive the special type of message notifying
1915
us that a user is typing a message.
1917
@param message: the associated MSNMessage object
1921
def gotSendRequest(self, fileName, fileSize, iCookie, message):
1923
called when a contact is trying to send us a file.
1924
To accept or reject this transfer see the
1925
fileInvitationReply method.
1927
@param fileName: the name of the file
1928
@param fileSize: the size of the file
1929
@param iCookie: the invitation cookie, used so the client can
1930
match up your reply with this request.
1931
@param message: the MSNMessage object which brought about this
1932
invitation (it may contain more information)
1938
def inviteUser(self, userHandle):
1940
used to invite a user to the current switchboard server.
1942
@param userHandle: the user handle (passport) of the desired user.
1944
@return: A Deferred, the callback for which will be called
1945
when the server notifies us that the user has indeed
1946
been invited. The callback argument will be a tuple
1947
with 1 element, the sessionID given to the invited user.
1948
I'm not sure if this is useful or not.
1951
id, d = self._createIDMapping()
1952
self.sendLine("CAL %s %s" % (id, userHandle))
1955
def sendMessage(self, message):
1957
used to send a message.
1959
@param message: the corresponding MSNMessage object.
1961
@return: Depending on the value of message.ack.
1962
If set to MSNMessage.MESSAGE_ACK or
1963
MSNMessage.MESSAGE_NACK a Deferred will be returned,
1964
the callback for which will be fired when an ACK or
1965
NACK is received - the callback argument will be
1966
(None,). If set to MSNMessage.MESSAGE_ACK_NONE then
1967
the return value is None.
1970
if message.ack not in ('A','N'):
1971
id, d = self._nextTransactionID(), None
1973
id, d = self._createIDMapping()
1974
if message.length == 0:
1975
message.length = message._calcMessageLen()
1976
self.sendLine("MSG %s %s %s" % (id, message.ack, message.length))
1977
# apparently order matters with at least MIME-Version and Content-Type
1978
self.sendLine('MIME-Version: %s' % message.getHeader('MIME-Version'))
1979
self.sendLine('Content-Type: %s' % message.getHeader('Content-Type'))
1980
# send the rest of the headers
1981
for header in [h for h in message.headers.items() if h[0].lower() not in ('mime-version','content-type')]:
1982
self.sendLine("%s: %s" % (header[0], header[1]))
1983
self.transport.write(CR+LF)
1984
self.transport.write(message.message)
1987
def sendTypingNotification(self):
1989
used to send a typing notification. Upon receiving this
1990
message the official client will display a 'user is typing'
1991
message to all other users in the chat session for 10 seconds.
1992
The official client sends one of these every 5 seconds (I think)
1993
as long as you continue to type.
1996
m.ack = m.MESSAGE_ACK_NONE
1997
m.setHeader('Content-Type', 'text/x-msmsgscontrol')
1998
m.setHeader('TypingUser', self.userHandle)
2002
def sendFileInvitation(self, fileName, fileSize):
2004
send an notification that we want to send a file.
2006
@param fileName: the file name
2007
@param fileSize: the file size
2009
@return: A Deferred, the callback of which will be fired
2010
when the user responds to this invitation with an
2011
appropriate message. The callback argument will be
2012
a tuple with 3 elements, the first being 1 or 0
2013
depending on whether they accepted the transfer
2014
(1=yes, 0=no), the second being an invitation cookie
2015
to identify your follow-up responses and the third being
2016
the message 'info' which is a dict of information they
2017
sent in their reply (this doesn't really need to be used).
2018
If you wish to proceed with the transfer see the
2019
sendTransferInfo method.
2021
cookie = self._newInvitationCookie()
2024
m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2025
m.message += 'Application-Name: File Transfer\r\n'
2026
m.message += 'Application-GUID: %s\r\n' % (classNameToGUID["file transfer"],)
2027
m.message += 'Invitation-Command: INVITE\r\n'
2028
m.message += 'Invitation-Cookie: %s\r\n' % str(cookie)
2029
m.message += 'Application-File: %s\r\n' % fileName
2030
m.message += 'Application-FileSize: %s\r\n\r\n' % str(fileSize)
2031
m.ack = m.MESSAGE_ACK_NONE
2033
self.cookies['iCookies'][cookie] = (d, m)
2036
def fileInvitationReply(self, iCookie, accept=1):
2038
used to reply to a file transfer invitation.
2040
@param iCookie: the invitation cookie of the initial invitation
2041
@param accept: whether or not you accept this transfer,
2042
1 = yes, 0 = no, default = 1.
2044
@return: A Deferred, the callback for which will be fired when
2045
the user responds with the transfer information.
2046
The callback argument will be a tuple with 5 elements,
2047
whether or not they wish to proceed with the transfer
2048
(1=yes, 0=no), their ip, the port, the authentication
2049
cookie (see FileReceive/FileSend) and the message
2050
info (dict) (in case they send extra header-like info
2051
like Internal-IP, this doesn't necessarily need to be
2052
used). If you wish to proceed with the transfer see
2057
m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2058
m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
2059
m.message += 'Invitation-Cookie: %s\r\n' % str(iCookie)
2061
m.message += 'Cancel-Code: REJECT\r\n'
2062
m.message += 'Launch-Application: FALSE\r\n'
2063
m.message += 'Request-Data: IP-Address:\r\n'
2065
m.ack = m.MESSAGE_ACK_NONE
2067
self.cookies['external'][iCookie] = (d, m)
2070
def sendTransferInfo(self, accept, iCookie, authCookie, ip, port):
2072
send information relating to a file transfer session.
2074
@param accept: whether or not to go ahead with the transfer
2076
@param iCookie: the invitation cookie of previous replies
2077
relating to this transfer
2078
@param authCookie: the authentication cookie obtained from
2079
an FileSend instance
2081
@param port: the port on which an FileSend protocol is listening.
2084
m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2085
m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
2086
m.message += 'Invitation-Cookie: %s\r\n' % iCookie
2087
m.message += 'IP-Address: %s\r\n' % ip
2088
m.message += 'Port: %s\r\n' % port
2089
m.message += 'AuthCookie: %s\r\n' % authCookie
2091
m.ack = m.MESSAGE_NACK
2094
class FileReceive(LineReceiver):
2096
This class provides support for receiving files from contacts.
2098
@ivar fileSize: the size of the receiving file. (you will have to set this)
2099
@ivar connected: true if a connection has been established.
2100
@ivar completed: true if the transfer is complete.
2101
@ivar bytesReceived: number of bytes (of the file) received.
2102
This does not include header data.
2105
def __init__(self, auth, myUserHandle, file, directory="", overwrite=0):
2107
@param auth: auth string received in the file invitation.
2108
@param myUserHandle: your userhandle.
2109
@param file: A string or file object represnting the file
2111
@param directory: optional parameter specifiying the directory.
2112
Defaults to the current directory.
2113
@param overwrite: if true and a file of the same name exists on
2114
your system, it will be overwritten. (0 by default)
2117
self.myUserHandle = myUserHandle
2121
self.directory = directory
2122
self.bytesReceived = 0
2123
self.overwrite = overwrite
2125
# used for handling current received state
2126
self.state = 'CONNECTING'
2127
self.segmentLength = 0
2130
if isinstance(file, types.StringType):
2131
path = os.path.join(directory, file)
2132
if os.path.exists(path) and not self.overwrite:
2133
log.msg('File already exists...')
2134
raise IOError, "File Exists" # is this all we should do here?
2135
self.file = open(os.path.join(directory, file), 'wb')
2139
def connectionMade(self):
2141
self.state = 'INHEADER'
2142
self.sendLine('VER MSNFTP')
2144
def connectionLost(self, reason):
2148
def parseHeader(self, header):
2149
""" parse the header of each 'message' to obtain the segment length """
2151
if ord(header[0]) != 0: # they requested that we close the connection
2152
self.transport.loseConnection()
2155
extra, factor = header[1:]
2157
# munged header, ending transfer
2158
self.transport.loseConnection()
2161
factor = ord(factor)
2162
return factor * 256 + extra
2164
def lineReceived(self, line):
2171
handler = getattr(self, "handle_%s" % cmd.upper(), None)
2173
handler(params) # try/except
2175
self.handle_UNKNOWN(cmd, params)
2177
def rawDataReceived(self, data):
2178
bufferLen = len(self.buffer)
2179
if self.state == 'INHEADER':
2181
self.buffer += data[:delim]
2182
if len(self.buffer) == 3:
2183
self.segmentLength = self.parseHeader(self.buffer)
2184
if not self.segmentLength:
2187
self.state = 'INSEGMENT'
2188
extra = data[delim:]
2190
self.rawDataReceived(extra)
2193
elif self.state == 'INSEGMENT':
2194
dataSeg = data[:(self.segmentLength-bufferLen)]
2195
self.buffer += dataSeg
2196
self.bytesReceived += len(dataSeg)
2197
if len(self.buffer) == self.segmentLength:
2198
self.gotSegment(self.buffer)
2200
if self.bytesReceived == self.fileSize:
2204
self.sendLine("BYE 16777989")
2206
self.state = 'INHEADER'
2207
extra = data[(self.segmentLength-bufferLen):]
2209
self.rawDataReceived(extra)
2212
def handle_VER(self, params):
2213
checkParamLen(len(params), 1, 'VER')
2214
if params[0].upper() == "MSNFTP":
2215
self.sendLine("USR %s %s" % (self.myUserHandle, self.auth))
2217
log.msg('they sent the wrong version, time to quit this transfer')
2218
self.transport.loseConnection()
2220
def handle_FIL(self, params):
2221
checkParamLen(len(params), 1, 'FIL')
2223
self.fileSize = int(params[0])
2224
except ValueError: # they sent the wrong file size - probably want to log this
2225
self.transport.loseConnection()
2228
self.sendLine("TFR")
2230
def handle_UNKNOWN(self, cmd, params):
2231
log.msg('received unknown command (%s), params: %s' % (cmd, params))
2233
def gotSegment(self, data):
2234
""" called when a segment (block) of data arrives. """
2235
self.file.write(data)
2237
class FileSend(LineReceiver):
2239
This class provides support for sending files to other contacts.
2241
@ivar bytesSent: the number of bytes that have currently been sent.
2242
@ivar completed: true if the send has completed.
2243
@ivar connected: true if a connection has been established.
2244
@ivar targetUser: the target user (contact).
2245
@ivar segmentSize: the segment (block) size.
2246
@ivar auth: the auth cookie (number) to use when sending the
2250
def __init__(self, file):
2252
@param file: A string or file object represnting the file to send.
2255
if isinstance(file, types.StringType):
2256
self.file = open(file, 'rb')
2264
self.targetUser = None
2265
self.segmentSize = 2045
2266
self.auth = randint(0, 2**30)
2267
self._pendingSend = None # :(
2269
def connectionMade(self):
2272
def connectionLost(self, reason):
2273
if self._pendingSend.active():
2274
self._pendingSend.cancel()
2275
self._pendingSend = None
2276
if self.bytesSent == self.fileSize:
2281
def lineReceived(self, line):
2288
handler = getattr(self, "handle_%s" % cmd.upper(), None)
2292
self.handle_UNKNOWN(cmd, params)
2294
def handle_VER(self, params):
2295
checkParamLen(len(params), 1, 'VER')
2296
if params[0].upper() == "MSNFTP":
2297
self.sendLine("VER MSNFTP")
2298
else: # they sent some weird version during negotiation, i'm quitting.
2299
self.transport.loseConnection()
2301
def handle_USR(self, params):
2302
checkParamLen(len(params), 2, 'USR')
2303
self.targetUser = params[0]
2304
if self.auth == int(params[1]):
2305
self.sendLine("FIL %s" % (self.fileSize))
2306
else: # they failed the auth test, disconnecting.
2307
self.transport.loseConnection()
2309
def handle_TFR(self, params):
2310
checkParamLen(len(params), 0, 'TFR')
2311
# they are ready for me to start sending
2314
def handle_BYE(self, params):
2315
self.completed = (self.bytesSent == self.fileSize)
2316
self.transport.loseConnection()
2318
def handle_CCL(self, params):
2319
self.completed = (self.bytesSent == self.fileSize)
2320
self.transport.loseConnection()
2322
def handle_UNKNOWN(self, cmd, params):
2323
log.msg('received unknown command (%s), params: %s' % (cmd, params))
2325
def makeHeader(self, size):
2326
""" make the appropriate header given a specific segment size. """
2327
quotient, remainder = divmod(size, 256)
2328
return chr(0) + chr(remainder) + chr(quotient)
2331
""" send a segment of data """
2332
if not self.connected:
2333
self._pendingSend = None
2334
return # may be buggy (if handle_CCL/BYE is called but self.connected is still 1)
2335
data = self.file.read(self.segmentSize)
2337
dataSize = len(data)
2338
header = self.makeHeader(dataSize)
2339
self.bytesSent += dataSize
2340
self.transport.write(header + data)
2341
self._pendingSend = reactor.callLater(0, self.sendPart)
2343
self._pendingSend = None
2346
# mapping of error codes to error messages
2349
200 : "Syntax error",
2350
201 : "Invalid parameter",
2351
205 : "Invalid user",
2352
206 : "Domain name missing",
2353
207 : "Already logged in",
2354
208 : "Invalid username",
2355
209 : "Invalid screen name",
2356
210 : "User list full",
2357
215 : "User already there",
2358
216 : "User already on list",
2359
217 : "User not online",
2360
218 : "Already in mode",
2361
219 : "User is in the opposite list",
2362
223 : "Too many groups",
2363
224 : "Invalid group",
2364
225 : "User not in group",
2365
229 : "Group name too long",
2366
230 : "Cannot remove group 0",
2367
231 : "Invalid group",
2368
280 : "Switchboard failed",
2369
281 : "Transfer to switchboard failed",
2371
300 : "Required field missing",
2372
301 : "Too many FND responses",
2373
302 : "Not logged in",
2375
500 : "Internal server error",
2376
501 : "Database server error",
2377
502 : "Command disabled",
2378
510 : "File operation failed",
2379
520 : "Memory allocation failed",
2380
540 : "Wrong CHL value sent to server",
2382
600 : "Server is busy",
2383
601 : "Server is unavaliable",
2384
602 : "Peer nameserver is down",
2385
603 : "Database connection failed",
2386
604 : "Server is going down",
2387
605 : "Server unavailable",
2389
707 : "Could not create connection",
2390
710 : "Invalid CVR parameters",
2391
711 : "Write is blocking",
2392
712 : "Session is overloaded",
2393
713 : "Too many active users",
2394
714 : "Too many sessions",
2395
715 : "Not expected",
2396
717 : "Bad friend file",
2397
731 : "Not expected",
2399
800 : "Requests too rapid",
2401
910 : "Server too busy",
2402
911 : "Authentication failed",
2403
912 : "Server too busy",
2404
913 : "Not allowed when offline",
2405
914 : "Server too busy",
2406
915 : "Server too busy",
2407
916 : "Server too busy",
2408
917 : "Server too busy",
2409
918 : "Server too busy",
2410
919 : "Server too busy",
2411
920 : "Not accepting new users",
2412
921 : "Server too busy",
2413
922 : "Server too busy",
2414
923 : "No parent consent",
2415
924 : "Passport account not yet verified"
2419
# mapping of status codes to readable status format
2422
STATUS_ONLINE : "Online",
2423
STATUS_OFFLINE : "Offline",
2424
STATUS_HIDDEN : "Appear Offline",
2425
STATUS_IDLE : "Idle",
2426
STATUS_AWAY : "Away",
2427
STATUS_BUSY : "Busy",
2428
STATUS_BRB : "Be Right Back",
2429
STATUS_PHONE : "On the Phone",
2430
STATUS_LUNCH : "Out to Lunch"
2434
# mapping of list ids to list codes
2437
FORWARD_LIST : 'fl',
2444
# mapping of list codes to list ids
2446
for id,code in listIDToCode.items():
2447
listCodeToID[code] = id
2451
# Mapping of class GUIDs to simple english names
2453
"{5D3E02AB-6190-11d3-BBBB-00C04F795683}": "file transfer",
2456
# Reverse of the above
2457
classNameToGUID = {}
2458
for guid, name in guidToClassName.iteritems():
2459
classNameToGUID[name] = guid