]> code.delx.au - pymsnt/blob - src/tlib/msn.py
Reimporting (0.9.5)
[pymsnt] / src / tlib / msn.py
1 # Twisted, the Framework of Your Internet
2 # Copyright (C) 2001-2002 Matthew W. Lefkowitz
3 #
4 # This library is free software; you can redistribute it and/or
5 # modify it under the terms of version 2.1 of the GNU Lesser General Public
6 # License as published by the Free Software Foundation.
7 #
8 # This library is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 # Lesser General Public License for more details.
12 #
13 # You should have received a copy of the GNU Lesser General Public
14 # License along with this library; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16 #
17
18 """
19 MSNP8 Protocol (client only) - semi-experimental
20
21 Stability: unstable.
22
23 This module provides support for clients using the MSN Protocol (MSNP8).
24 There are basically 3 servers involved in any MSN session:
25
26 I{Dispatch server}
27
28 The DispatchClient class handles connections to the
29 dispatch server, which basically delegates users to a
30 suitable notification server.
31
32 You will want to subclass this and handle the gotNotificationReferral
33 method appropriately.
34
35 I{Notification Server}
36
37 The NotificationClient class handles connections to the
38 notification server, which acts as a session server
39 (state updates, message negotiation etc...)
40
41 I{Switcboard Server}
42
43 The SwitchboardClient handles connections to switchboard
44 servers which are used to conduct conversations with other users.
45
46 There are also two classes (FileSend and FileReceive) used
47 for file transfers.
48
49 Clients handle events in two ways.
50
51 - each client request requiring a response will return a Deferred,
52 the callback for same will be fired when the server sends the
53 required response
54 - Events which are not in response to any client request have
55 respective methods which should be overridden and handled in
56 an adequate manner
57
58 Most client request callbacks require more than one argument,
59 and since Deferreds can only pass the callback one result,
60 most of the time the callback argument will be a tuple of
61 values (documented in the respective request method).
62 To make reading/writing code easier, callbacks can be defined in
63 a number of ways to handle this 'cleanly'. One way would be to
64 define methods like: def callBack(self, (arg1, arg2, arg)): ...
65 another way would be to do something like:
66 d.addCallback(lambda result: myCallback(*result)).
67
68 If the server sends an error response to a client request,
69 the errback of the corresponding Deferred will be called,
70 the argument being the corresponding error code.
71
72 B{NOTE}:
73 Due to the lack of an official spec for MSNP8, extra checking
74 than may be deemed necessary often takes place considering the
75 server is never 'wrong'. Thus, if gotBadLine (in any of the 3
76 main clients) is called, or an MSNProtocolError is raised, it's
77 probably a good idea to submit a bug report. ;)
78 Use of this module requires that PyOpenSSL is installed.
79
80 TODO
81 ====
82 - check message hooks with invalid x-msgsinvite messages.
83 - font handling
84 - switchboard factory
85
86 @author: U{Sam Jordan<mailto:sam@twistedmatrix.com>}
87 """
88
89 from __future__ import nested_scopes
90
91 # Sibling imports
92 from twisted.protocols.basic import LineReceiver
93 #FLAG
94 import utils
95 if(utils.checkTwisted()):
96 from twisted.web.http import HTTPClient
97 else:
98 from twisted.protocols.http import HTTPClient
99 from proxy import proxy_connect_ssl
100
101 # Twisted imports
102 from twisted.internet import reactor, task
103 from twisted.internet.defer import Deferred
104 from twisted.internet.protocol import ClientFactory
105 from twisted.internet.ssl import ClientContextFactory
106 from twisted.python import failure, log
107
108 # System imports
109 import types, operator, os, md5
110 from random import randint
111 from urllib import quote, unquote
112
113 MSN_PROTOCOL_VERSION = "MSNP8 CVR0" # protocol version
114 MSN_PORT = 1863 # default dispatch server port
115 MSN_MAX_MESSAGE = 1664 # max message length
116 MSN_CHALLENGE_STR = "Q1P7W2E4J9R8U3S5" # used for server challenges
117 MSN_CVR_STR = "0x0409 win 4.10 i386 MSNMSGR 5.0.0544 MSMSGS" # :(
118
119 # auth constants
120 LOGIN_SUCCESS = 1
121 LOGIN_FAILURE = 2
122 LOGIN_REDIRECT = 3
123
124 # list constants
125 FORWARD_LIST = 1
126 ALLOW_LIST = 2
127 BLOCK_LIST = 4
128 REVERSE_LIST = 8
129
130 # phone constants
131 HOME_PHONE = "PHH"
132 WORK_PHONE = "PHW"
133 MOBILE_PHONE = "PHM"
134 HAS_PAGER = "MOB"
135
136 # status constants
137 STATUS_ONLINE = 'NLN'
138 STATUS_OFFLINE = 'FLN'
139 STATUS_HIDDEN = 'HDN'
140 STATUS_IDLE = 'IDL'
141 STATUS_AWAY = 'AWY'
142 STATUS_BUSY = 'BSY'
143 STATUS_BRB = 'BRB'
144 STATUS_PHONE = 'PHN'
145 STATUS_LUNCH = 'LUN'
146
147 CR = "\r"
148 LF = "\n"
149
150 LINEDEBUG = False
151
152 def checkParamLen(num, expected, cmd, error=None):
153 if error == None: error = "Invalid Number of Parameters for %s" % cmd
154 if num != expected: raise MSNProtocolError, error
155
156 def _parseHeader(h, v):
157 """
158 Split a certin number of known
159 header values with the format:
160 field1=val,field2=val,field3=val into
161 a dict mapping fields to values.
162 @param h: the header's key
163 @param v: the header's value as a string
164 """
165
166 if h in ('passporturls','authentication-info','www-authenticate'):
167 v = v.replace('Passport1.4','').lstrip()
168 fields = {}
169 for fieldPair in v.split(','):
170 try:
171 field,value = fieldPair.split('=',1)
172 fields[field.lower()] = value
173 except ValueError:
174 fields[field.lower()] = ''
175 return fields
176 else: return v
177
178 def _parsePrimitiveHost(host):
179 # Ho Ho Ho
180 h,p = host.replace('https://','').split('/',1)
181 p = '/' + p
182 return h,p
183
184 def _login(userHandle, passwd, nexusServer, cached=0, authData='', proxy=None, proxyport=None):
185 """
186 This function is used internally and should not ever be called
187 directly.
188 """
189 cb = Deferred()
190 def _cb(server, auth):
191 loginFac = ClientFactory()
192 loginFac.protocol = lambda : PassportLogin(cb, userHandle, passwd, server, auth)
193 if(proxy and proxyport):
194 proxy_connect_ssl(proxy, proxyport, _parsePrimitiveHost(server)[0], 443, loginFac)
195 else:
196 reactor.connectSSL(_parsePrimitiveHost(server)[0], 443, loginFac, ClientContextFactory())
197
198 if cached:
199 _cb(nexusServer, authData)
200 else:
201 fac = ClientFactory()
202 d = Deferred()
203 d.addCallbacks(_cb, callbackArgs=(authData,))
204 d.addErrback(lambda f: cb.errback(f))
205 fac.protocol = lambda : PassportNexus(d, nexusServer)
206 if(proxy and proxyport):
207 proxy_connect_ssl(proxy, proxyport, _parsePrimitiveHost(nexusServer)[0], 443, fac)
208 else:
209 reactor.connectSSL(_parsePrimitiveHost(nexusServer)[0], 443, fac, ClientContextFactory())
210 return cb
211
212
213 class PassportNexus(HTTPClient):
214
215 """
216 Used to obtain the URL of a valid passport
217 login HTTPS server.
218
219 This class is used internally and should
220 not be instantiated directly -- that is,
221 The passport logging in process is handled
222 transparantly by NotificationClient.
223 """
224
225 def __init__(self, deferred, host):
226 self.deferred = deferred
227 self.host, self.path = _parsePrimitiveHost(host)
228
229 def connectionMade(self):
230 HTTPClient.connectionMade(self)
231 self.sendCommand('GET', self.path)
232 self.sendHeader('Host', self.host)
233 self.endHeaders()
234 self.headers = {}
235
236 def handleHeader(self, header, value):
237 h = header.lower()
238 self.headers[h] = _parseHeader(h, value)
239
240 def handleEndHeaders(self):
241 if self.connected: self.transport.loseConnection()
242 if not self.headers.has_key('passporturls') or not self.headers['passporturls'].has_key('dalogin'):
243 self.deferred.errback(failure.Failure(failure.DefaultException("Invalid Nexus Reply")))
244 else:
245 self.deferred.callback('https://' + self.headers['passporturls']['dalogin'])
246
247 def handleResponse(self, r): pass
248
249 class PassportLogin(HTTPClient):
250 """
251 This class is used internally to obtain
252 a login ticket from a passport HTTPS
253 server -- it should not be used directly.
254 """
255
256 _finished = 0
257
258 def __init__(self, deferred, userHandle, passwd, host, authData):
259 self.deferred = deferred
260 self.userHandle = userHandle
261 self.passwd = passwd
262 self.authData = authData
263 self.host, self.path = _parsePrimitiveHost(host)
264
265 def connectionMade(self):
266 self.sendCommand('GET', self.path)
267 self.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
268 'sign-in=%s,pwd=%s,%s' % (quote(self.userHandle), self.passwd,self.authData))
269 self.sendHeader('Host', self.host)
270 self.endHeaders()
271 self.headers = {}
272
273 def handleHeader(self, header, value):
274 h = header.lower()
275 self.headers[h] = _parseHeader(h, value)
276
277 def handleEndHeaders(self):
278 if self._finished: return
279 self._finished = 1 # I think we need this because of HTTPClient
280 if self.connected: self.transport.loseConnection()
281 authHeader = 'authentication-info'
282 _interHeader = 'www-authenticate'
283 if self.headers.has_key(_interHeader): authHeader = _interHeader
284 try:
285 info = self.headers[authHeader]
286 status = info['da-status']
287 handler = getattr(self, 'login_%s' % (status,), None)
288 if handler:
289 handler(info)
290 else: raise Exception()
291 except Exception, e:
292 self.deferred.errback(failure.Failure(e))
293
294 def handleResponse(self, r): pass
295
296 def login_success(self, info):
297 ticket = info['from-pp']
298 ticket = ticket[1:len(ticket)-1]
299 self.deferred.callback((LOGIN_SUCCESS, ticket))
300
301 def login_failed(self, info):
302 self.deferred.callback((LOGIN_FAILURE, unquote(info['cbtxt'])))
303
304 def login_redir(self, info):
305 self.deferred.callback((LOGIN_REDIRECT, self.headers['location'], self.authData))
306
307 class MSNProtocolError(Exception):
308 """
309 This Exception is basically used for debugging
310 purposes, as the official MSN server should never
311 send anything _wrong_ and nobody in their right
312 mind would run their B{own} MSN server.
313 If it is raised by default command handlers
314 (handle_BLAH) the error will be logged.
315 """
316 pass
317
318 class MSNMessage:
319
320 """
321 I am the class used to represent an 'instant' message.
322
323 @ivar userHandle: The user handle (passport) of the sender
324 (this is only used when receiving a message)
325 @ivar screenName: The screen name of the sender (this is only used
326 when receiving a message)
327 @ivar message: The message
328 @ivar headers: The message headers
329 @type headers: dict
330 @ivar length: The message length (including headers and line endings)
331 @ivar ack: This variable is used to tell the server how to respond
332 once the message has been sent. If set to MESSAGE_ACK
333 (default) the server will respond with an ACK upon receiving
334 the message, if set to MESSAGE_NACK the server will respond
335 with a NACK upon failure to receive the message.
336 If set to MESSAGE_ACK_NONE the server will do nothing.
337 This is relevant for the return value of
338 SwitchboardClient.sendMessage (which will return
339 a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
340 and will fire when the respective ACK or NACK is received).
341 If set to MESSAGE_ACK_NONE sendMessage will return None.
342 """
343 MESSAGE_ACK = 'A'
344 MESSAGE_NACK = 'N'
345 MESSAGE_ACK_NONE = 'U'
346
347 ack = MESSAGE_ACK
348
349 def __init__(self, length=0, userHandle="", screenName="", message=""):
350 self.userHandle = userHandle
351 self.screenName = screenName
352 self.message = message
353 self.headers = {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
354 self.length = length
355 self.readPos = 0
356
357 def _calcMessageLen(self):
358 """
359 used to calculte the number to send
360 as the message length when sending a message.
361 """
362 return reduce(operator.add, [len(x[0]) + len(x[1]) + 4 for x in self.headers.items()]) + len(self.message) + 2
363
364 def setHeader(self, header, value):
365 """ set the desired header """
366 self.headers[header] = value
367
368 def getHeader(self, header):
369 """
370 get the desired header value
371 @raise KeyError: if no such header exists.
372 """
373 return self.headers[header]
374
375 def hasHeader(self, header):
376 """ check to see if the desired header exists """
377 return self.headers.has_key(header)
378
379 def getMessage(self):
380 """ return the message - not including headers """
381 return self.message
382
383 def setMessage(self, message):
384 """ set the message text """
385 self.message = message
386
387 class MSNContact:
388
389 """
390 This class represents a contact (user).
391
392 @ivar userHandle: The contact's user handle (passport).
393 @ivar screenName: The contact's screen name.
394 @ivar groups: A list of all the group IDs which this
395 contact belongs to.
396 @ivar lists: An integer representing the sum of all lists
397 that this contact belongs to.
398 @ivar status: The contact's status code.
399 @type status: str if contact's status is known, None otherwise.
400
401 @ivar homePhone: The contact's home phone number.
402 @type homePhone: str if known, otherwise None.
403 @ivar workPhone: The contact's work phone number.
404 @type workPhone: str if known, otherwise None.
405 @ivar mobilePhone: The contact's mobile phone number.
406 @type mobilePhone: str if known, otherwise None.
407 @ivar hasPager: Whether or not this user has a mobile pager
408 (true=yes, false=no)
409 """
410
411 def __init__(self, userHandle="", screenName="", lists=0, groups=[], status=None):
412 self.userHandle = userHandle
413 self.screenName = screenName
414 self.lists = lists
415 self.groups = [] # if applicable
416 self.status = status # current status
417
418 # phone details
419 self.homePhone = None
420 self.workPhone = None
421 self.mobilePhone = None
422 self.hasPager = None
423
424 def setPhone(self, phoneType, value):
425 """
426 set phone numbers/values for this specific user.
427 for phoneType check the *_PHONE constants and HAS_PAGER
428 """
429
430 t = phoneType.upper()
431 if t == HOME_PHONE: self.homePhone = value
432 elif t == WORK_PHONE: self.workPhone = value
433 elif t == MOBILE_PHONE: self.mobilePhone = value
434 elif t == HAS_PAGER: self.hasPager = value
435 else: raise ValueError, "Invalid Phone Type"
436
437 def addToList(self, listType):
438 """
439 Update the lists attribute to
440 reflect being part of the
441 given list.
442 """
443 self.lists |= listType
444
445 def removeFromList(self, listType):
446 """
447 Update the lists attribute to
448 reflect being removed from the
449 given list.
450 """
451 self.lists ^= listType
452
453 class MSNContactList:
454 """
455 This class represents a basic MSN contact list.
456
457 @ivar contacts: All contacts on my various lists
458 @type contacts: dict (mapping user handles to MSNContact objects)
459 @ivar version: The current contact list version (used for list syncing)
460 @ivar groups: a mapping of group ids to group names
461 (groups can only exist on the forward list)
462 @type groups: dict
463
464 B{Note}:
465 This is used only for storage and doesn't effect the
466 server's contact list.
467 """
468
469 def __init__(self):
470 self.contacts = {}
471 self.version = 0
472 self.groups = {}
473 self.autoAdd = 0
474 self.privacy = 0
475
476 def _getContactsFromList(self, listType):
477 """
478 Obtain all contacts which belong
479 to the given list type.
480 """
481 return dict([(uH,obj) for uH,obj in self.contacts.items() if obj.lists & listType])
482
483 def addContact(self, contact):
484 """
485 Add a contact
486 """
487 self.contacts[contact.userHandle] = contact
488
489 def remContact(self, userHandle):
490 """
491 Remove a contact
492 """
493 try:
494 del self.contacts[userHandle]
495 except KeyError: pass
496
497 def getContact(self, userHandle):
498 """
499 Obtain the MSNContact object
500 associated with the given
501 userHandle.
502 @return: the MSNContact object if
503 the user exists, or None.
504 """
505 try:
506 return self.contacts[userHandle]
507 except KeyError:
508 return None
509
510 def getBlockedContacts(self):
511 """
512 Obtain all the contacts on my block list
513 """
514 return self._getContactsFromList(BLOCK_LIST)
515
516 def getAuthorizedContacts(self):
517 """
518 Obtain all the contacts on my auth list.
519 (These are contacts which I have verified
520 can view my state changes).
521 """
522 return self._getContactsFromList(ALLOW_LIST)
523
524 def getReverseContacts(self):
525 """
526 Get all contacts on my reverse list.
527 (These are contacts which have added me
528 to their forward list).
529 """
530 return self._getContactsFromList(REVERSE_LIST)
531
532 def getContacts(self):
533 """
534 Get all contacts on my forward list.
535 (These are the contacts which I have added
536 to my list).
537 """
538 return self._getContactsFromList(FORWARD_LIST)
539
540 def setGroup(self, id, name):
541 """
542 Keep a mapping from the given id
543 to the given name.
544 """
545 self.groups[id] = name
546
547 def remGroup(self, id):
548 """
549 Removed the stored group
550 mapping for the given id.
551 """
552 try:
553 del self.groups[id]
554 except KeyError: pass
555 for c in self.contacts:
556 if id in c.groups: c.groups.remove(id)
557
558
559 class MSNEventBase(LineReceiver):
560 """
561 This class provides support for handling / dispatching events and is the
562 base class of the three main client protocols (DispatchClient,
563 NotificationClient, SwitchboardClient)
564 """
565
566 def __init__(self):
567 self.ids = {} # mapping of ids to Deferreds
568 self.currentID = 0
569 self.connected = 0
570 self.setLineMode()
571 self.currentMessage = None
572
573 def connectionLost(self, reason):
574 self.ids = {}
575 self.connected = 0
576
577 def connectionMade(self):
578 self.connected = 1
579
580 def _fireCallback(self, id, *args):
581 """
582 Fire the callback for the given id
583 if one exists and return 1, else return false
584 """
585 if self.ids.has_key(id):
586 self.ids[id][0].callback(args)
587 del self.ids[id]
588 return 1
589 return 0
590
591 def _nextTransactionID(self):
592 """ return a usable transaction ID """
593 self.currentID += 1
594 if self.currentID > 1000: self.currentID = 1
595 return self.currentID
596
597 def _createIDMapping(self, data=None):
598 """
599 return a unique transaction ID that is mapped internally to a
600 deferred .. also store arbitrary data if it is needed
601 """
602 id = self._nextTransactionID()
603 d = Deferred()
604 self.ids[id] = (d, data)
605 return (id, d)
606
607 def checkMessage(self, message):
608 """
609 process received messages to check for file invitations and
610 typing notifications and other control type messages
611 """
612 raise NotImplementedError
613
614 def sendLine(self, line):
615 if(LINEDEBUG): print ">> " + line
616 LineReceiver.sendLine(self, line)
617
618 def lineReceived(self, line):
619 if(LINEDEBUG): print "<< " + line
620 if self.currentMessage:
621 self.currentMessage.readPos += len(line+CR+LF)
622 try:
623 header, value = line.split(':')
624 self.currentMessage.setHeader(header, unquote(value).lstrip())
625 return
626 except ValueError:
627 #raise MSNProtocolError, "Invalid Message Header"
628 line = ""
629 if line == "" or self.currentMessage.userHandle == "NOTIFICATION":
630 self.setRawMode()
631 if self.currentMessage.readPos == self.currentMessage.length: self.rawDataReceived("") # :(
632 return
633 try:
634 cmd, params = line.split(' ', 1)
635 except ValueError:
636 #raise MSNProtocolError, "Invalid Message, %s" % repr(line)
637 cmd = line.strip() # The QNG command has no parameters.
638 params = ""
639
640 if len(cmd) != 3: raise MSNProtocolError, "Invalid Command, %s" % repr(cmd)
641 if cmd.isdigit():
642 if self.ids.has_key(params.split(' ')[0]):
643 self.ids[id].errback(int(cmd))
644 del self.ids[id]
645 return
646 else: # we received an error which doesn't map to a sent command
647 self.gotError(int(cmd))
648 return
649
650 handler = getattr(self, "handle_%s" % cmd.upper(), None)
651 if handler:
652 try: handler(params.split(' '))
653 except MSNProtocolError, why: self.gotBadLine(line, why)
654 else:
655 self.handle_UNKNOWN(cmd, params.split(' '))
656
657 def rawDataReceived(self, data):
658 extra = ""
659 self.currentMessage.readPos += len(data)
660 diff = self.currentMessage.readPos - self.currentMessage.length
661 if diff > 0:
662 self.currentMessage.message += data[:-diff]
663 extra = data[-diff:]
664 elif diff == 0:
665 self.currentMessage.message += data
666 else:
667 self.currentMessage.message += data
668 return
669 del self.currentMessage.readPos
670 m = self.currentMessage
671 self.currentMessage = None
672 if not self.checkMessage(m):
673 self.setLineMode(extra)
674 return
675 self.setLineMode(extra)
676 self.gotMessage(m)
677
678 ### protocol command handlers - no need to override these.
679
680 def handle_MSG(self, params):
681 checkParamLen(len(params), 3, 'MSG')
682 try:
683 messageLen = int(params[2])
684 except ValueError: raise MSNProtocolError, "Invalid Parameter for MSG length argument"
685 self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName=unquote(params[1]))
686
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))
690
691 ### callbacks
692
693 def gotMessage(self, message):
694 """
695 called when we receive a message - override in notification
696 and switchboard clients
697 """
698 raise NotImplementedError
699
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))
703
704 def gotError(self, errorCode):
705 """
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)
708 """
709 log.msg('Error %s' % (errorCodes[errorCode]))
710
711 class DispatchClient(MSNEventBase):
712 """
713 This class provides support for clients connecting to the dispatch server
714 @ivar userHandle: your user handle (passport) needed before connecting.
715 """
716
717 # eventually this may become an attribute of the
718 # factory.
719 userHandle = ""
720
721 def connectionMade(self):
722 MSNEventBase.connectionMade(self)
723 self.sendLine('VER %s %s' % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
724
725 ### protocol command handlers ( there is no need to override these )
726
727 def handle_VER(self, params):
728 versions = params[1:]
729 if versions is None or ' '.join(versions) != MSN_PROTOCOL_VERSION:
730 self.transport.loseConnection()
731 raise MSNProtocolError, "Invalid version response"
732 id = self._nextTransactionID()
733 self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.userHandle))
734
735 def handle_CVR(self, params):
736 self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.userHandle))
737
738 def handle_XFR(self, params):
739 if len(params) < 4: raise MSNProtocolError, "Invalid number of parameters for XFR"
740 id, refType, addr = params[:3]
741 # was addr a host:port pair?
742 try:
743 host, port = addr.split(':')
744 except ValueError:
745 host = addr
746 port = MSN_PORT
747 if refType == "NS":
748 self.gotNotificationReferral(host, int(port))
749
750 ### callbacks
751
752 def gotNotificationReferral(self, host, port):
753 """
754 called when we get a referral to the notification server.
755
756 @param host: the notification server's hostname
757 @param port: the port to connect to
758 """
759 pass
760
761
762 class NotificationClient(MSNEventBase):
763 """
764 This class provides support for clients connecting
765 to the notification server.
766 """
767
768 factory = None # sssh pychecker
769
770 def __init__(self, currentID=0, proxy=None, proxyport=None):
771 MSNEventBase.__init__(self)
772 self.currentID = currentID
773 self._state = ['DISCONNECTED', {}]
774 self.proxy, self.proxyport = proxy, proxyport
775 self.pingCounter = 0
776 self.pingCheckTask = None
777
778 def _setState(self, state):
779 self._state[0] = state
780
781 def _getState(self):
782 return self._state[0]
783
784 def _getStateData(self, key):
785 return self._state[1][key]
786
787 def _setStateData(self, key, value):
788 self._state[1][key] = value
789
790 def _remStateData(self, *args):
791 for key in args: del self._state[1][key]
792
793 def connectionMade(self):
794 MSNEventBase.connectionMade(self)
795 self._setState('CONNECTED')
796 self.sendLine("VER %s %s" % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
797
798 def connectionLost(self, reason):
799 self._setState('DISCONNECTED')
800 self._state[1] = {}
801 if(self.pingCheckTask):
802 self.pingCheckTask.stop()
803 self.pingCheckTask = None
804 MSNEventBase.connectionLost(self, reason)
805
806 def checkMessage(self, message):
807 """ hook used for detecting specific notification messages """
808 cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
809 if 'text/x-msmsgsprofile' in cTypes:
810 self.gotProfile(message)
811 return 0
812 return 1
813
814 ### protocol command handlers - no need to override these
815
816 def handle_VER(self, params):
817 versions = params[1:]
818 if versions is None or ' '.join(versions) != MSN_PROTOCOL_VERSION:
819 self.transport.loseConnection()
820 raise MSNProtocolError, "Invalid version response"
821 self.sendLine("CVR %s %s %s" % (self._nextTransactionID(), MSN_CVR_STR, self.factory.userHandle))
822
823 def handle_CVR(self, params):
824 self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.factory.userHandle))
825
826 def handle_USR(self, params):
827 if len(params) != 4 and len(params) != 6:
828 raise MSNProtocolError, "Invalid Number of Parameters for USR"
829
830 mechanism = params[1]
831 if mechanism == "OK":
832 self.loggedIn(params[2], unquote(params[3]), int(params[4]))
833 elif params[2].upper() == "S":
834 # we need to obtain auth from a passport server
835 f = self.factory
836 d = _login(f.userHandle, f.password, f.passportServer, authData=params[3], proxy=self.proxy, proxyport=self.proxyport)
837 d.addCallback(self._passportLogin)
838 d.addErrback(self._passportError)
839
840 def _passportLogin(self, result):
841 if result[0] == LOGIN_REDIRECT:
842 d = _login(self.factory.userHandle, self.factory.password,
843 result[1], cached=1, authData=result[2], proxy=self.proxy, proxyport=self.proxyport)
844 d.addCallback(self._passportLogin)
845 d.addErrback(self._passportError)
846 elif result[0] == LOGIN_SUCCESS:
847 self.sendLine("USR %s TWN S %s" % (self._nextTransactionID(), result[1]))
848 elif result[0] == LOGIN_FAILURE:
849 self.loginFailure(result[1])
850
851 def _passportError(self, failure):
852 self.loginFailure("Exception while authenticating: %s" % failure)
853
854 def handle_CHG(self, params):
855 checkParamLen(len(params), 3, 'CHG')
856 id = int(params[0])
857 if not self._fireCallback(id, params[1]):
858 self.statusChanged(params[1])
859
860 def handle_ILN(self, params):
861 checkParamLen(len(params), 5, 'ILN')
862 self.gotContactStatus(params[1], params[2], unquote(params[3]))
863
864 def handle_CHL(self, params):
865 checkParamLen(len(params), 2, 'CHL')
866 self.sendLine("QRY %s msmsgs@msnmsgr.com 32" % self._nextTransactionID())
867 self.transport.write(md5.md5(params[1] + MSN_CHALLENGE_STR).hexdigest())
868
869 def handle_QRY(self, params):
870 pass
871
872 def handle_NLN(self, params):
873 checkParamLen(len(params), 4, 'NLN')
874 self.contactStatusChanged(params[0], params[1], unquote(params[2]))
875
876 def handle_FLN(self, params):
877 checkParamLen(len(params), 1, 'FLN')
878 self.contactOffline(params[0])
879
880 def handle_LST(self, params):
881 # support no longer exists for manually
882 # requesting lists - why do I feel cleaner now?
883 if self._getState() != 'SYNC': return
884 contact = MSNContact(userHandle=params[0], screenName=unquote(params[1]),
885 lists=int(params[2]))
886 if contact.lists & FORWARD_LIST:
887 contact.groups.extend(map(int, params[3].split(',')))
888 self._getStateData('list').addContact(contact)
889 self._setStateData('last_contact', contact)
890 sofar = self._getStateData('lst_sofar') + 1
891 if sofar == self._getStateData('lst_reply'):
892 # this is the best place to determine that
893 # a syn realy has finished - msn _may_ send
894 # BPR information for the last contact
895 # which is unfortunate because it means
896 # that the real end of a syn is non-deterministic.
897 # to handle this we'll keep 'last_contact' hanging
898 # around in the state data and update it if we need
899 # to later.
900 self._setState('SESSION')
901 contacts = self._getStateData('list')
902 phone = self._getStateData('phone')
903 id = self._getStateData('synid')
904 self._remStateData('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
905 self._fireCallback(id, contacts, phone)
906 else:
907 self._setStateData('lst_sofar',sofar)
908
909 def handle_BLP(self, params):
910 # check to see if this is in response to a SYN
911 if self._getState() == 'SYNC':
912 self._getStateData('list').privacy = listCodeToID[params[0].lower()]
913 else:
914 id = int(params[0])
915 self._fireCallback(id, int(params[1]), listCodeToID[params[2].lower()])
916
917 def handle_GTC(self, params):
918 # check to see if this is in response to a SYN
919 if self._getState() == 'SYNC':
920 if params[0].lower() == "a": self._getStateData('list').autoAdd = 0
921 elif params[0].lower() == "n": self._getStateData('list').autoAdd = 1
922 else: raise MSNProtocolError, "Invalid Paramater for GTC" # debug
923 else:
924 id = int(params[0])
925 if params[1].lower() == "a": self._fireCallback(id, 0)
926 elif params[1].lower() == "n": self._fireCallback(id, 1)
927 else: raise MSNProtocolError, "Invalid Paramater for GTC" # debug
928
929 def handle_SYN(self, params):
930 id = int(params[0])
931 if len(params) == 2:
932 self._setState('SESSION')
933 self._fireCallback(id, None, None)
934 else:
935 contacts = MSNContactList()
936 contacts.version = int(params[1])
937 self._setStateData('list', contacts)
938 self._setStateData('lst_reply', int(params[2]))
939 self._setStateData('lsg_reply', int(params[3]))
940 self._setStateData('lst_sofar', 0)
941 self._setStateData('phone', [])
942
943 def handle_LSG(self, params):
944 if self._getState() == 'SYNC':
945 self._getStateData('list').groups[int(params[0])] = unquote(params[1])
946
947 # Please see the comment above the requestListGroups / requestList methods
948 # regarding support for this
949 #
950 #else:
951 # self._getStateData('groups').append((int(params[4]), unquote(params[5])))
952 # if params[3] == params[4]: # this was the last group
953 # self._fireCallback(int(params[0]), self._getStateData('groups'), int(params[1]))
954 # self._remStateData('groups')
955
956 def handle_PRP(self, params):
957 if self._getState() == 'SYNC':
958 self._getStateData('phone').append((params[0], unquote(params[1])))
959 else:
960 self._fireCallback(int(params[0]), int(params[1]), unquote(params[3]))
961
962 def handle_BPR(self, params):
963 numParams = len(params)
964 if numParams == 2: # part of a syn
965 self._getStateData('last_contact').setPhone(params[0], unquote(params[1]))
966 elif numParams == 4:
967 self.gotPhoneNumber(int(params[0]), params[1], params[2], unquote(params[3]))
968
969 def handle_ADG(self, params):
970 checkParamLen(len(params), 5, 'ADG')
971 id = int(params[0])
972 if not self._fireCallback(id, int(params[1]), unquote(params[2]), int(params[3])):
973 raise MSNProtocolError, "ADG response does not match up to a request" # debug
974
975 def handle_RMG(self, params):
976 checkParamLen(len(params), 3, 'RMG')
977 id = int(params[0])
978 if not self._fireCallback(id, int(params[1]), int(params[2])):
979 raise MSNProtocolError, "RMG response does not match up to a request" # debug
980
981 def handle_REG(self, params):
982 checkParamLen(len(params), 5, 'REG')
983 id = int(params[0])
984 if not self._fireCallback(id, int(params[1]), int(params[2]), unquote(params[3])):
985 raise MSNProtocolError, "REG response does not match up to a request" # debug
986
987 def handle_ADD(self, params):
988 numParams = len(params)
989 if numParams < 5 or params[1].upper() not in ('AL','BL','RL','FL'):
990 raise MSNProtocolError, "Invalid Paramaters for ADD" # debug
991 id = int(params[0])
992 listType = params[1].lower()
993 listVer = int(params[2])
994 userHandle = params[3]
995 groupID = None
996 if numParams == 6: # they sent a group id
997 if params[1].upper() != "FL": raise MSNProtocolError, "Only forward list can contain groups" # debug
998 groupID = int(params[5])
999 if not self._fireCallback(id, listCodeToID[listType], userHandle, listVer, groupID):
1000 self.userAddedMe(userHandle, unquote(params[4]), listVer)
1001
1002 def handle_REM(self, params):
1003 numParams = len(params)
1004 if numParams < 4 or params[1].upper() not in ('AL','BL','FL','RL'):
1005 raise MSNProtocolError, "Invalid Paramaters for REM" # debug
1006 id = int(params[0])
1007 listType = params[1].lower()
1008 listVer = int(params[2])
1009 userHandle = params[3]
1010 groupID = None
1011 if numParams == 5:
1012 if params[1] != "FL": raise MSNProtocolError, "Only forward list can contain groups" # debug
1013 groupID = int(params[4])
1014 if not self._fireCallback(id, listCodeToID[listType], userHandle, listVer, groupID):
1015 if listType.upper() == "RL": self.userRemovedMe(userHandle, listVer)
1016
1017 def handle_REA(self, params):
1018 checkParamLen(len(params), 4, 'REA')
1019 id = int(params[0])
1020 self._fireCallback(id, int(params[1]), unquote(params[3]))
1021
1022 def handle_XFR(self, params):
1023 checkParamLen(len(params), 5, 'XFR')
1024 id = int(params[0])
1025 # check to see if they sent a host/port pair
1026 try:
1027 host, port = params[2].split(':')
1028 except ValueError:
1029 host = params[2]
1030 port = MSN_PORT
1031
1032 if not self._fireCallback(id, host, int(port), params[4]):
1033 raise MSNProtocolError, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
1034
1035 def handle_RNG(self, params):
1036 checkParamLen(len(params), 6, 'RNG')
1037 # check for host:port pair
1038 try:
1039 host, port = params[1].split(":")
1040 port = int(port)
1041 except ValueError:
1042 host = params[1]
1043 port = MSN_PORT
1044 self.gotSwitchboardInvitation(int(params[0]), host, port, params[3], params[4],
1045 unquote(params[5]))
1046
1047 def handle_NOT(self, params):
1048 checkParamLen(len(params), 1, 'NOT')
1049 try:
1050 messageLen = int(params[0])
1051 except ValueError: raise MSNProtocolError, "Invalid Parameter for NOT length argument"
1052 self.currentMessage = MSNMessage(length=messageLen, userHandle="NOTIFICATION", screenName="NOTIFICATION")
1053 self.setRawMode()
1054
1055
1056 def handle_OUT(self, params):
1057 checkParamLen(len(params), 1, 'OUT')
1058 if params[0] == "OTH": self.multipleLogin()
1059 elif params[0] == "SSD": self.serverGoingDown()
1060 else: raise MSNProtocolError, "Invalid Parameters received for OUT" # debug
1061
1062 def handle_QNG(self, params):
1063 self.pingCounter = 0 # They replied to a ping. We'll forgive them for any they may have missed, because they're alive again now
1064
1065 # callbacks
1066
1067 def pingChecker(self):
1068 if(self.pingCounter > 5):
1069 # The server has ignored 5 pings, lets kill the connection
1070 self.transport.loseConnection()
1071 else:
1072 self.sendLine("PNG")
1073 self.pingCounter += 1
1074
1075 def pingCheckerStart(self, *args):
1076 self.pingCheckTask = task.LoopingCall(self.pingChecker)
1077 self.pingCheckTask.start(50.0)
1078
1079 def loggedIn(self, userHandle, screenName, verified):
1080 """
1081 Called when the client has logged in.
1082 The default behaviour of this method is to
1083 update the factory with our screenName and
1084 to sync the contact list (factory.contacts).
1085 When this is complete self.listSynchronized
1086 will be called.
1087
1088 @param userHandle: our userHandle
1089 @param screenName: our screenName
1090 @param verified: 1 if our passport has been (verified), 0 if not.
1091 (i'm not sure of the significace of this)
1092 @type verified: int
1093 """
1094 self.factory.screenName = screenName
1095 listVersion = self.factory.initialListVersion
1096 if self.factory.contacts: listVersion = self.factory.contacts.version
1097 d = self.syncList(listVersion)
1098 d.addCallback(self.listSynchronized)
1099 d.addCallback(self.pingCheckerStart)
1100
1101 def loginFailure(self, message):
1102 """
1103 Called when the client fails to login.
1104
1105 @param message: a message indicating the problem that was encountered
1106 """
1107 pass
1108
1109 def gotProfile(self, message):
1110 """
1111 Called after logging in when the server sends an initial
1112 message with MSN/passport specific profile information
1113 such as country, number of kids, etc.
1114 Check the message headers for the specific values.
1115
1116 @param message: The profile message
1117 """
1118 pass
1119
1120 def listSynchronized(self, *args):
1121 """
1122 Lists are now synchronized by default upon logging in, this
1123 method is called after the synchronization has finished
1124 and the factory now has the up-to-date contacts.
1125 """
1126 pass
1127
1128 def statusChanged(self, statusCode):
1129 """
1130 Called when our status changes and it isn't in response to
1131 a client command. By default we will update the status
1132 attribute of the factory.
1133
1134 @param statusCode: 3-letter status code
1135 """
1136 self.factory.status = statusCode
1137
1138 def gotContactStatus(self, statusCode, userHandle, screenName):
1139 """
1140 Called after loggin in when the server sends status of online contacts.
1141 By default we will update the status attribute and screenName of the
1142 contact stored on the factory.
1143
1144 @param statusCode: 3-letter status code
1145 @param userHandle: the contact's user handle (passport)
1146 @param screenName: the contact's screen name
1147 """
1148 msnContact = self.factory.contacts.getContact(userHandle)
1149 if(not msnContact):
1150 msnContact = MSNContact()
1151 msnContact.addToList(FORWARD_LIST)
1152 self.factory.contacts.addContact(msnContact)
1153 msnContact.status = statusCode
1154 msnContact.screenName = screenName
1155
1156 def contactStatusChanged(self, statusCode, userHandle, screenName):
1157 """
1158 Called when we're notified that a contact's status has changed.
1159 By default we will update the status attribute and screenName
1160 of the contact stored on the factory.
1161
1162 @param statusCode: 3-letter status code
1163 @param userHandle: the contact's user handle (passport)
1164 @param screenName: the contact's screen name
1165 """
1166 msnContact = self.factory.contacts.getContact(userHandle)
1167 if(not msnContact):
1168 msnContact = MSNContact()
1169 self.factory.contacts.addContact(msnContact)
1170 msnContact.status = statusCode
1171 msnContact.screenName = screenName
1172
1173 def contactOffline(self, userHandle):
1174 """
1175 Called when a contact goes offline. By default this method
1176 will update the status attribute of the contact stored
1177 on the factory.
1178
1179 @param userHandle: the contact's user handle
1180 """
1181 msnContact = self.factory.contacts.getContact(userHandle)
1182 if(msnContact):
1183 msnContact.status = STATUS_OFFLINE
1184
1185 def gotPhoneNumber(self, listVersion, userHandle, phoneType, number):
1186 """
1187 Called when the server sends us phone details about
1188 a specific user (for example after a user is added
1189 the server will send their status, phone details etc.
1190 By default we will update the list version for the
1191 factory's contact list and update the phone details
1192 for the specific user.
1193
1194 @param listVersion: the new list version
1195 @param userHandle: the contact's user handle (passport)
1196 @param phoneType: the specific phoneType
1197 (*_PHONE constants or HAS_PAGER)
1198 @param number: the value/phone number.
1199 """
1200 if not self.factory.contacts: return
1201 self.factory.contacts.version = listVersion
1202 self.factory.contacts.getContact(userHandle).setPhone(phoneType, number)
1203
1204 def userAddedMe(self, userHandle, screenName, listVersion):
1205 """
1206 Called when a user adds me to their list. (ie. they have been added to
1207 the reverse list. By default this method will update the version of
1208 the factory's contact list -- that is, if the contact already exists
1209 it will update the associated lists attribute, otherwise it will create
1210 a new MSNContact object and store it.
1211
1212 @param userHandle: the userHandle of the user
1213 @param screenName: the screen name of the user
1214 @param listVersion: the new list version
1215 @type listVersion: int
1216 """
1217 if not self.factory.contacts: return
1218 self.factory.contacts.version = listVersion
1219 c = self.factory.contacts.getContact(userHandle)
1220 if not c:
1221 c = MSNContact(userHandle=userHandle, screenName=screenName)
1222 self.factory.contacts.addContact(c)
1223 c.addToList(REVERSE_LIST)
1224
1225 def userRemovedMe(self, userHandle, listVersion):
1226 """
1227 Called when a user removes us from their contact list
1228 (they are no longer on our reverseContacts list.
1229 By default this method will update the version of
1230 the factory's contact list -- that is, the user will
1231 be removed from the reverse list and if they are no longer
1232 part of any lists they will be removed from the contact
1233 list entirely.
1234
1235 @param userHandle: the contact's user handle (passport)
1236 @param listVersion: the new list version
1237 """
1238 if not self.factory.contacts: return
1239 self.factory.contacts.version = listVersion
1240 c = self.factory.contacts.getContact(userHandle)
1241 if not c: return
1242 c.removeFromList(REVERSE_LIST)
1243 if c.lists == 0: self.factory.contacts.remContact(c.userHandle)
1244
1245 def gotSwitchboardInvitation(self, sessionID, host, port,
1246 key, userHandle, screenName):
1247 """
1248 Called when we get an invitation to a switchboard server.
1249 This happens when a user requests a chat session with us.
1250
1251 @param sessionID: session ID number, must be remembered for logging in
1252 @param host: the hostname of the switchboard server
1253 @param port: the port to connect to
1254 @param key: used for authorization when connecting
1255 @param userHandle: the user handle of the person who invited us
1256 @param screenName: the screen name of the person who invited us
1257 """
1258 pass
1259
1260 def multipleLogin(self):
1261 """
1262 Called when the server says there has been another login
1263 under our account, the server should disconnect us right away.
1264 """
1265 pass
1266
1267 def serverGoingDown(self):
1268 """
1269 Called when the server has notified us that it is going down for
1270 maintenance.
1271 """
1272 pass
1273
1274 # api calls
1275
1276 def changeStatus(self, status):
1277 """
1278 Change my current status. This method will add
1279 a default callback to the returned Deferred
1280 which will update the status attribute of the
1281 factory.
1282
1283 @param status: 3-letter status code (as defined by
1284 the STATUS_* constants)
1285 @return: A Deferred, the callback of which will be
1286 fired when the server confirms the change
1287 of status. The callback argument will be
1288 a tuple with the new status code as the
1289 only element.
1290 """
1291
1292 id, d = self._createIDMapping()
1293 self.sendLine("CHG %s %s" % (id, status))
1294 def _cb(r):
1295 if self.factory: self.factory.status = r[0]
1296 return r
1297 return d.addCallback(_cb)
1298
1299 # I am no longer supporting the process of manually requesting
1300 # lists or list groups -- as far as I can see this has no use
1301 # if lists are synchronized and updated correctly, which they
1302 # should be. If someone has a specific justified need for this
1303 # then please contact me and i'll re-enable/fix support for it.
1304
1305 #def requestList(self, listType):
1306 # """
1307 # request the desired list type
1308 #
1309 # @param listType: (as defined by the *_LIST constants)
1310 # @return: A Deferred, the callback of which will be
1311 # fired when the list has been retrieved.
1312 # The callback argument will be a tuple with
1313 # the only element being a list of MSNContact
1314 # objects.
1315 # """
1316 # # this doesn't need to ever be used if syncing of the lists takes place
1317 # # i.e. please don't use it!
1318 # warnings.warn("Please do not use this method - use the list syncing process instead")
1319 # id, d = self._createIDMapping()
1320 # self.sendLine("LST %s %s" % (id, listIDToCode[listType].upper()))
1321 # self._setStateData('list',[])
1322 # return d
1323
1324 def setPrivacyMode(self, privLevel):
1325 """
1326 Set my privacy mode on the server.
1327
1328 B{Note}:
1329 This only keeps the current privacy setting on
1330 the server for later retrieval, it does not
1331 effect the way the server works at all.
1332
1333 @param privLevel: This parameter can be true, in which
1334 case the server will keep the state as
1335 'al' which the official client interprets
1336 as -> allow messages from only users on
1337 the allow list. Alternatively it can be
1338 false, in which case the server will keep
1339 the state as 'bl' which the official client
1340 interprets as -> allow messages from all
1341 users except those on the block list.
1342
1343 @return: A Deferred, the callback of which will be fired when
1344 the server replies with the new privacy setting.
1345 The callback argument will be a tuple, the 2 elements
1346 of which being the list version and either 'al'
1347 or 'bl' (the new privacy setting).
1348 """
1349
1350 id, d = self._createIDMapping()
1351 if privLevel: self.sendLine("BLP %s AL" % id)
1352 else: self.sendLine("BLP %s BL" % id)
1353 return d
1354
1355 def syncList(self, version):
1356 """
1357 Used for keeping an up-to-date contact list.
1358 A callback is added to the returned Deferred
1359 that updates the contact list on the factory
1360 and also sets my state to STATUS_ONLINE.
1361
1362 B{Note}:
1363 This is called automatically upon signing
1364 in using the version attribute of
1365 factory.contacts, so you may want to persist
1366 this object accordingly. Because of this there
1367 is no real need to ever call this method
1368 directly.
1369
1370 @param version: The current known list version
1371
1372 @return: A Deferred, the callback of which will be
1373 fired when the server sends an adequate reply.
1374 The callback argument will be a tuple with two
1375 elements, the new list (MSNContactList) and
1376 your current state (a dictionary). If the version
1377 you sent _was_ the latest list version, both elements
1378 will be None. To just request the list send a version of 0.
1379 """
1380
1381 self._setState('SYNC')
1382 id, d = self._createIDMapping(data=str(version))
1383 self._setStateData('synid',id)
1384 self.sendLine("SYN %s %s" % (id, version))
1385 def _cb(r):
1386 self.changeStatus(STATUS_ONLINE)
1387 if r[0] is not None:
1388 self.factory.contacts = r[0]
1389 return r
1390 return d.addCallback(_cb)
1391
1392
1393 # I am no longer supporting the process of manually requesting
1394 # lists or list groups -- as far as I can see this has no use
1395 # if lists are synchronized and updated correctly, which they
1396 # should be. If someone has a specific justified need for this
1397 # then please contact me and i'll re-enable/fix support for it.
1398
1399 #def requestListGroups(self):
1400 # """
1401 # Request (forward) list groups.
1402 #
1403 # @return: A Deferred, the callback for which will be called
1404 # when the server responds with the list groups.
1405 # The callback argument will be a tuple with two elements,
1406 # a dictionary mapping group IDs to group names and the
1407 # current list version.
1408 # """
1409 #
1410 # # this doesn't need to be used if syncing of the lists takes place (which it SHOULD!)
1411 # # i.e. please don't use it!
1412 # warnings.warn("Please do not use this method - use the list syncing process instead")
1413 # id, d = self._createIDMapping()
1414 # self.sendLine("LSG %s" % id)
1415 # self._setStateData('groups',{})
1416 # return d
1417
1418 def setPhoneDetails(self, phoneType, value):
1419 """
1420 Set/change my phone numbers stored on the server.
1421
1422 @param phoneType: phoneType can be one of the following
1423 constants - HOME_PHONE, WORK_PHONE,
1424 MOBILE_PHONE, HAS_PAGER.
1425 These are pretty self-explanatory, except
1426 maybe HAS_PAGER which refers to whether or
1427 not you have a pager.
1428 @param value: for all of the *_PHONE constants the value is a
1429 phone number (str), for HAS_PAGER accepted values
1430 are 'Y' (for yes) and 'N' (for no).
1431
1432 @return: A Deferred, the callback for which will be fired when
1433 the server confirms the change has been made. The
1434 callback argument will be a tuple with 2 elements, the
1435 first being the new list version (int) and the second
1436 being the new phone number value (str).
1437 """
1438 # XXX: Add a default callback which updates
1439 # factory.contacts.version and the relevant phone
1440 # number
1441 id, d = self._createIDMapping()
1442 self.sendLine("PRP %s %s %s" % (id, phoneType, quote(value)))
1443 return d
1444
1445 def addListGroup(self, name):
1446 """
1447 Used to create a new list group.
1448 A default callback is added to the
1449 returned Deferred which updates the
1450 contacts attribute of the factory.
1451
1452 @param name: The desired name of the new group.
1453
1454 @return: A Deferred, the callbacck for which will be called
1455 when the server clarifies that the new group has been
1456 created. The callback argument will be a tuple with 3
1457 elements: the new list version (int), the new group name
1458 (str) and the new group ID (int).
1459 """
1460
1461 id, d = self._createIDMapping()
1462 self.sendLine("ADG %s %s 0" % (id, quote(name)))
1463 def _cb(r):
1464 self.factory.contacts.version = r[0]
1465 self.factory.contacts.setGroup(r[1], r[2])
1466 return r
1467 return d.addCallback(_cb)
1468
1469 def remListGroup(self, groupID):
1470 """
1471 Used to remove a list group.
1472 A default callback is added to the
1473 returned Deferred which updates the
1474 contacts attribute of the factory.
1475
1476 @param groupID: the ID of the desired group to be removed.
1477
1478 @return: A Deferred, the callback for which will be called when
1479 the server clarifies the deletion of the group.
1480 The callback argument will be a tuple with 2 elements:
1481 the new list version (int) and the group ID (int) of
1482 the removed group.
1483 """
1484
1485 id, d = self._createIDMapping()
1486 self.sendLine("RMG %s %s" % (id, groupID))
1487 def _cb(r):
1488 self.factory.contacts.version = r[0]
1489 self.factory.contacts.remGroup(r[1])
1490 return r
1491 return d.addCallback(_cb)
1492
1493 def renameListGroup(self, groupID, newName):
1494 """
1495 Used to rename an existing list group.
1496 A default callback is added to the returned
1497 Deferred which updates the contacts attribute
1498 of the factory.
1499
1500 @param groupID: the ID of the desired group to rename.
1501 @param newName: the desired new name for the group.
1502
1503 @return: A Deferred, the callback for which will be called
1504 when the server clarifies the renaming.
1505 The callback argument will be a tuple of 3 elements,
1506 the new list version (int), the group id (int) and
1507 the new group name (str).
1508 """
1509
1510 id, d = self._createIDMapping()
1511 self.sendLine("REG %s %s %s 0" % (id, groupID, quote(newName)))
1512 def _cb(r):
1513 self.factory.contacts.version = r[0]
1514 self.factory.contacts.setGroup(r[1], r[2])
1515 return r
1516 return d.addCallback(_cb)
1517
1518 def addContact(self, listType, userHandle, groupID=0):
1519 """
1520 Used to add a contact to the desired list.
1521 A default callback is added to the returned
1522 Deferred which updates the contacts attribute of
1523 the factory with the new contact information.
1524 If you are adding a contact to the forward list
1525 and you want to associate this contact with multiple
1526 groups then you will need to call this method for each
1527 group you would like to add them to, changing the groupID
1528 parameter. The default callback will take care of updating
1529 the group information on the factory's contact list.
1530
1531 @param listType: (as defined by the *_LIST constants)
1532 @param userHandle: the user handle (passport) of the contact
1533 that is being added
1534 @param groupID: the group ID for which to associate this contact
1535 with. (default 0 - default group). Groups are only
1536 valid for FORWARD_LIST.
1537
1538 @return: A Deferred, the callback for which will be called when
1539 the server has clarified that the user has been added.
1540 The callback argument will be a tuple with 4 elements:
1541 the list type, the contact's user handle, the new list
1542 version, and the group id (if relevant, otherwise it
1543 will be None)
1544 """
1545
1546 id, d = self._createIDMapping()
1547 listType = listIDToCode[listType].upper()
1548 if listType == "FL":
1549 self.sendLine("ADD %s FL %s %s %s" % (id, userHandle, userHandle, groupID))
1550 else:
1551 self.sendLine("ADD %s %s %s %s" % (id, listType, userHandle, userHandle))
1552
1553 def _cb(r):
1554 self.factory.contacts.version = r[2]
1555 c = self.factory.contacts.getContact(r[1])
1556 if not c:
1557 c = MSNContact(userHandle=r[1])
1558 if r[3]: c.groups.append(r[3])
1559 c.addToList(r[0])
1560 return r
1561 return d.addCallback(_cb)
1562
1563 def remContact(self, listType, userHandle, groupID=0):
1564 """
1565 Used to remove a contact from the desired list.
1566 A default callback is added to the returned deferred
1567 which updates the contacts attribute of the factory
1568 to reflect the new contact information. If you are
1569 removing from the forward list then you will need to
1570 supply a groupID, if the contact is in more than one
1571 group then they will only be removed from this group
1572 and not the entire forward list, but if this is their
1573 only group they will be removed from the whole list.
1574
1575 @param listType: (as defined by the *_LIST constants)
1576 @param userHandle: the user handle (passport) of the
1577 contact being removed
1578 @param groupID: the ID of the group to which this contact
1579 belongs (only relevant for FORWARD_LIST,
1580 default is 0)
1581
1582 @return: A Deferred, the callback for which will be called when
1583 the server has clarified that the user has been removed.
1584 The callback argument will be a tuple of 4 elements:
1585 the list type, the contact's user handle, the new list
1586 version, and the group id (if relevant, otherwise it will
1587 be None)
1588 """
1589
1590 id, d = self._createIDMapping()
1591 listType = listIDToCode[listType].upper()
1592 if listType == "FL":
1593 self.sendLine("REM %s FL %s %s" % (id, userHandle, groupID))
1594 else:
1595 self.sendLine("REM %s %s %s" % (id, listType, userHandle))
1596
1597 def _cb(r):
1598 l = self.factory.contacts
1599 l.version = r[2]
1600 c = l.getContact(r[1])
1601 if not c: return
1602 group = r[3]
1603 shouldRemove = 1
1604 if group: # they may not have been removed from the list
1605 c.groups.remove(group)
1606 if c.groups: shouldRemove = 0
1607 if shouldRemove:
1608 c.removeFromList(r[0])
1609 if c.lists == 0: l.remContact(c.userHandle)
1610 return r
1611 return d.addCallback(_cb)
1612
1613 def changeScreenName(self, newName):
1614 """
1615 Used to change your current screen name.
1616 A default callback is added to the returned
1617 Deferred which updates the screenName attribute
1618 of the factory and also updates the contact list
1619 version.
1620
1621 @param newName: the new screen name
1622
1623 @return: A Deferred, the callback for which will be called
1624 when the server sends an adequate reply.
1625 The callback argument will be a tuple of 2 elements:
1626 the new list version and the new screen name.
1627 """
1628
1629 id, d = self._createIDMapping()
1630 self.sendLine("REA %s %s %s" % (id, self.factory.userHandle, quote(newName)))
1631 def _cb(r):
1632 if(self.factory.contacts): self.factory.contacts.version = r[0]
1633 self.factory.screenName = r[1]
1634 return r
1635 return d.addCallback(_cb)
1636
1637 def requestSwitchboardServer(self):
1638 """
1639 Used to request a switchboard server to use for conversations.
1640
1641 @return: A Deferred, the callback for which will be called when
1642 the server responds with the switchboard information.
1643 The callback argument will be a tuple with 3 elements:
1644 the host of the switchboard server, the port and a key
1645 used for logging in.
1646 """
1647
1648 id, d = self._createIDMapping()
1649 self.sendLine("XFR %s SB" % id)
1650 return d
1651
1652 def logOut(self):
1653 """
1654 Used to log out of the notification server.
1655 After running the method the server is expected
1656 to close the connection.
1657 """
1658
1659 if(self.pingCheckTask):
1660 self.pingCheckTask.stop()
1661 self.pingCheckTask = None
1662 self.sendLine("OUT")
1663
1664 class NotificationFactory(ClientFactory):
1665 """
1666 Factory for the NotificationClient protocol.
1667 This is basically responsible for keeping
1668 the state of the client and thus should be used
1669 in a 1:1 situation with clients.
1670
1671 @ivar contacts: An MSNContactList instance reflecting
1672 the current contact list -- this is
1673 generally kept up to date by the default
1674 command handlers.
1675 @ivar userHandle: The client's userHandle, this is expected
1676 to be set by the client and is used by the
1677 protocol (for logging in etc).
1678 @ivar screenName: The client's current screen-name -- this is
1679 generally kept up to date by the default
1680 command handlers.
1681 @ivar password: The client's password -- this is (obviously)
1682 expected to be set by the client.
1683 @ivar passportServer: This must point to an msn passport server
1684 (the whole URL is required)
1685 @ivar status: The status of the client -- this is generally kept
1686 up to date by the default command handlers
1687 """
1688
1689 contacts = None
1690 userHandle = ''
1691 screenName = ''
1692 password = ''
1693 passportServer = 'https://nexus.passport.com/rdr/pprdr.asp'
1694 status = 'FLN'
1695 protocol = NotificationClient
1696 initialListVersion = 0
1697
1698
1699 # XXX: A lot of the state currently kept in
1700 # instances of SwitchboardClient is likely to
1701 # be moved into a factory at some stage in the
1702 # future
1703
1704 class SwitchboardClient(MSNEventBase):
1705 """
1706 This class provides support for clients connecting to a switchboard server.
1707
1708 Switchboard servers are used for conversations with other people
1709 on the MSN network. This means that the number of conversations at
1710 any given time will be directly proportional to the number of
1711 connections to varioius switchboard servers.
1712
1713 MSN makes no distinction between single and group conversations,
1714 so any number of users may be invited to join a specific conversation
1715 taking place on a switchboard server.
1716
1717 @ivar key: authorization key, obtained when receiving
1718 invitation / requesting switchboard server.
1719 @ivar userHandle: your user handle (passport)
1720 @ivar sessionID: unique session ID, used if you are replying
1721 to a switchboard invitation
1722 @ivar reply: set this to 1 in connectionMade or before to signifiy
1723 that you are replying to a switchboard invitation.
1724 """
1725
1726 key = 0
1727 userHandle = ""
1728 sessionID = ""
1729 reply = 0
1730
1731 _iCookie = 0
1732
1733 def __init__(self):
1734 MSNEventBase.__init__(self)
1735 self.pendingUsers = {}
1736 self.cookies = {'iCookies' : {}, 'external' : {}} # will maybe be moved to a factory in the future
1737
1738 def connectionMade(self):
1739 MSNEventBase.connectionMade(self)
1740 self._sendInit()
1741
1742 def connectionLost(self, reason):
1743 self.cookies['iCookies'] = {}
1744 self.cookies['external'] = {}
1745 MSNEventBase.connectionLost(self, reason)
1746
1747 def _sendInit(self):
1748 """
1749 send initial data based on whether we are replying to an invitation
1750 or starting one.
1751 """
1752 id = self._nextTransactionID()
1753 if not self.reply:
1754 self.sendLine("USR %s %s %s" % (id, self.userHandle, self.key))
1755 else:
1756 self.sendLine("ANS %s %s %s %s" % (id, self.userHandle, self.key, self.sessionID))
1757
1758 def _newInvitationCookie(self):
1759 self._iCookie += 1
1760 if self._iCookie > 1000: self._iCookie = 1
1761 return self._iCookie
1762
1763 def _checkTyping(self, message, cTypes):
1764 """ helper method for checkMessage """
1765 if 'text/x-msmsgscontrol' in cTypes and message.hasHeader('TypingUser'):
1766 self.userTyping(message)
1767 return 1
1768
1769 def _checkFileInvitation(self, message, info):
1770 """ helper method for checkMessage """
1771 if not info.get('Application-Name', '').lower() == 'file transfer': return 0
1772 try:
1773 cookie = info['Invitation-Cookie']
1774 fileName = info['Application-File']
1775 fileSize = int(info['Application-FileSize'])
1776 except KeyError:
1777 log.msg('Received munged file transfer request ... ignoring.')
1778 return 0
1779 self.gotSendRequest(fileName, fileSize, cookie, message)
1780 return 1
1781
1782 def _checkFileResponse(self, message, info):
1783 """ helper method for checkMessage """
1784 try:
1785 cmd = info['Invitation-Command'].upper()
1786 cookie = info['Invitation-Cookie']
1787 except KeyError: return 0
1788 accept = (cmd == 'ACCEPT') and 1 or 0
1789 requested = self.cookies['iCookies'].get(cookie)
1790 if not requested: return 1
1791 requested[0].callback((accept, cookie, info))
1792 del self.cookies['iCookies'][cookie]
1793 return 1
1794
1795 def _checkFileInfo(self, message, info):
1796 """ helper method for checkMessage """
1797 try:
1798 ip = info['IP-Address']
1799 iCookie = info['Invitation-Cookie']
1800 aCookie = info['AuthCookie']
1801 cmd = info['Invitation-Command'].upper()
1802 port = int(info['Port'])
1803 except KeyError: return 0
1804 accept = (cmd == 'ACCEPT') and 1 or 0
1805 requested = self.cookies['external'].get(iCookie)
1806 if not requested: return 1 # we didn't ask for this
1807 requested[0].callback((accept, ip, port, aCookie, info))
1808 del self.cookies['external'][iCookie]
1809 return 1
1810
1811 def checkMessage(self, message):
1812 """
1813 hook for detecting any notification type messages
1814 (e.g. file transfer)
1815 """
1816 cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
1817 if self._checkTyping(message, cTypes): return 0
1818 if 'text/x-msmsgsinvite' in cTypes:
1819 # header like info is sent as part of the message body.
1820 info = {}
1821 for line in message.message.split('\r\n'):
1822 try:
1823 key, val = line.split(':')
1824 info[key] = val.lstrip()
1825 except ValueError: continue
1826 if self._checkFileInvitation(message, info) or self._checkFileInfo(message, info) or self._checkFileResponse(message, info): return 0
1827 return 1
1828
1829 # negotiation
1830 def handle_USR(self, params):
1831 checkParamLen(len(params), 4, 'USR')
1832 if params[1] == "OK":
1833 self.loggedIn()
1834
1835 # invite a user
1836 def handle_CAL(self, params):
1837 checkParamLen(len(params), 3, 'CAL')
1838 id = int(params[0])
1839 if params[1].upper() == "RINGING":
1840 self._fireCallback(id, int(params[2])) # session ID as parameter
1841
1842 # user joined
1843 def handle_JOI(self, params):
1844 checkParamLen(len(params), 2, 'JOI')
1845 self.userJoined(params[0], unquote(params[1]))
1846
1847 # users participating in the current chat
1848 def handle_IRO(self, params):
1849 checkParamLen(len(params), 5, 'IRO')
1850 self.pendingUsers[params[3]] = unquote(params[4])
1851 if params[1] == params[2]:
1852 self.gotChattingUsers(self.pendingUsers)
1853 self.pendingUsers = {}
1854
1855 # finished listing users
1856 def handle_ANS(self, params):
1857 checkParamLen(len(params), 2, 'ANS')
1858 if params[1] == "OK":
1859 self.loggedIn()
1860
1861 def handle_ACK(self, params):
1862 checkParamLen(len(params), 1, 'ACK')
1863 self._fireCallback(int(params[0]), None)
1864
1865 def handle_NAK(self, params):
1866 checkParamLen(len(params), 1, 'NAK')
1867 self._fireCallback(int(params[0]), None)
1868
1869 def handle_BYE(self, params):
1870 #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
1871 self.userLeft(params[0])
1872
1873 # callbacks
1874
1875 def loggedIn(self):
1876 """
1877 called when all login details have been negotiated.
1878 Messages can now be sent, or new users invited.
1879 """
1880 pass
1881
1882 def gotChattingUsers(self, users):
1883 """
1884 called after connecting to an existing chat session.
1885
1886 @param users: A dict mapping user handles to screen names
1887 (current users taking part in the conversation)
1888 """
1889 pass
1890
1891 def userJoined(self, userHandle, screenName):
1892 """
1893 called when a user has joined the conversation.
1894
1895 @param userHandle: the user handle (passport) of the user
1896 @param screenName: the screen name of the user
1897 """
1898 pass
1899
1900 def userLeft(self, userHandle):
1901 """
1902 called when a user has left the conversation.
1903
1904 @param userHandle: the user handle (passport) of the user.
1905 """
1906 pass
1907
1908 def gotMessage(self, message):
1909 """
1910 called when we receive a message.
1911
1912 @param message: the associated MSNMessage object
1913 """
1914 pass
1915
1916 def userTyping(self, message):
1917 """
1918 called when we receive the special type of message notifying
1919 us that a user is typing a message.
1920
1921 @param message: the associated MSNMessage object
1922 """
1923 pass
1924
1925 def gotSendRequest(self, fileName, fileSize, iCookie, message):
1926 """
1927 called when a contact is trying to send us a file.
1928 To accept or reject this transfer see the
1929 fileInvitationReply method.
1930
1931 @param fileName: the name of the file
1932 @param fileSize: the size of the file
1933 @param iCookie: the invitation cookie, used so the client can
1934 match up your reply with this request.
1935 @param message: the MSNMessage object which brought about this
1936 invitation (it may contain more information)
1937 """
1938 pass
1939
1940 # api calls
1941
1942 def inviteUser(self, userHandle):
1943 """
1944 used to invite a user to the current switchboard server.
1945
1946 @param userHandle: the user handle (passport) of the desired user.
1947
1948 @return: A Deferred, the callback for which will be called
1949 when the server notifies us that the user has indeed
1950 been invited. The callback argument will be a tuple
1951 with 1 element, the sessionID given to the invited user.
1952 I'm not sure if this is useful or not.
1953 """
1954
1955 id, d = self._createIDMapping()
1956 self.sendLine("CAL %s %s" % (id, userHandle))
1957 return d
1958
1959 def sendMessage(self, message):
1960 """
1961 used to send a message.
1962
1963 @param message: the corresponding MSNMessage object.
1964
1965 @return: Depending on the value of message.ack.
1966 If set to MSNMessage.MESSAGE_ACK or
1967 MSNMessage.MESSAGE_NACK a Deferred will be returned,
1968 the callback for which will be fired when an ACK or
1969 NACK is received - the callback argument will be
1970 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
1971 the return value is None.
1972 """
1973
1974 if message.ack not in ('A','N'): id, d = self._nextTransactionID(), None
1975 else: id, d = self._createIDMapping()
1976 if message.length == 0: message.length = message._calcMessageLen()
1977 self.sendLine("MSG %s %s %s" % (id, message.ack, message.length))
1978 # apparently order matters with at least MIME-Version and Content-Type
1979 self.sendLine('MIME-Version: %s' % message.getHeader('MIME-Version'))
1980 self.sendLine('Content-Type: %s' % message.getHeader('Content-Type'))
1981 # send the rest of the headers
1982 for header in [h for h in message.headers.items() if h[0].lower() not in ('mime-version','content-type')]:
1983 self.sendLine("%s: %s" % (header[0], header[1]))
1984 self.transport.write(CR+LF)
1985 self.transport.write(message.message)
1986 return d
1987
1988 def sendTypingNotification(self):
1989 """
1990 used to send a typing notification. Upon receiving this
1991 message the official client will display a 'user is typing'
1992 message to all other users in the chat session for 10 seconds.
1993 The official client sends one of these every 5 seconds (I think)
1994 as long as you continue to type.
1995 """
1996 m = MSNMessage()
1997 m.ack = m.MESSAGE_ACK_NONE
1998 m.setHeader('Content-Type', 'text/x-msmsgscontrol')
1999 m.setHeader('TypingUser', self.userHandle)
2000 m.message = "\r\n"
2001 self.sendMessage(m)
2002
2003 def sendFileInvitation(self, fileName, fileSize):
2004 """
2005 send an notification that we want to send a file.
2006
2007 @param fileName: the file name
2008 @param fileSize: the file size
2009
2010 @return: A Deferred, the callback of which will be fired
2011 when the user responds to this invitation with an
2012 appropriate message. The callback argument will be
2013 a tuple with 3 elements, the first being 1 or 0
2014 depending on whether they accepted the transfer
2015 (1=yes, 0=no), the second being an invitation cookie
2016 to identify your follow-up responses and the third being
2017 the message 'info' which is a dict of information they
2018 sent in their reply (this doesn't really need to be used).
2019 If you wish to proceed with the transfer see the
2020 sendTransferInfo method.
2021 """
2022 cookie = self._newInvitationCookie()
2023 d = Deferred()
2024 m = MSNMessage()
2025 m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2026 m.message += 'Application-Name: File Transfer\r\n'
2027 m.message += 'Application-GUID: {5D3E02AB-6190-11d3-BBBB-00C04F795683}\r\n'
2028 m.message += 'Invitation-Command: INVITE\r\n'
2029 m.message += 'Invitation-Cookie: %s\r\n' % str(cookie)
2030 m.message += 'Application-File: %s\r\n' % fileName
2031 m.message += 'Application-FileSize: %s\r\n\r\n' % str(fileSize)
2032 m.ack = m.MESSAGE_ACK_NONE
2033 self.sendMessage(m)
2034 self.cookies['iCookies'][cookie] = (d, m)
2035 return d
2036
2037 def fileInvitationReply(self, iCookie, accept=1):
2038 """
2039 used to reply to a file transfer invitation.
2040
2041 @param iCookie: the invitation cookie of the initial invitation
2042 @param accept: whether or not you accept this transfer,
2043 1 = yes, 0 = no, default = 1.
2044
2045 @return: A Deferred, the callback for which will be fired when
2046 the user responds with the transfer information.
2047 The callback argument will be a tuple with 5 elements,
2048 whether or not they wish to proceed with the transfer
2049 (1=yes, 0=no), their ip, the port, the authentication
2050 cookie (see FileReceive/FileSend) and the message
2051 info (dict) (in case they send extra header-like info
2052 like Internal-IP, this doesn't necessarily need to be
2053 used). If you wish to proceed with the transfer see
2054 FileReceive.
2055 """
2056 d = Deferred()
2057 m = MSNMessage()
2058 m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2059 m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
2060 m.message += 'Invitation-Cookie: %s\r\n' % str(iCookie)
2061 if not accept: m.message += 'Cancel-Code: REJECT\r\n'
2062 m.message += 'Launch-Application: FALSE\r\n'
2063 m.message += 'Request-Data: IP-Address:\r\n'
2064 m.message += '\r\n'
2065 m.ack = m.MESSAGE_ACK_NONE
2066 self.sendMessage(m)
2067 self.cookies['external'][iCookie] = (d, m)
2068 return d
2069
2070 def sendTransferInfo(self, accept, iCookie, authCookie, ip, port):
2071 """
2072 send information relating to a file transfer session.
2073
2074 @param accept: whether or not to go ahead with the transfer
2075 (1=yes, 0=no)
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
2080 @param ip: your ip
2081 @param port: the port on which an FileSend protocol is listening.
2082 """
2083 m = MSNMessage()
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
2090 m.message += '\r\n'
2091 m.ack = m.MESSAGE_NACK
2092 self.sendMessage(m)
2093
2094 class FileReceive(LineReceiver):
2095 """
2096 This class provides support for receiving files from contacts.
2097
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.
2103 """
2104
2105 def __init__(self, auth, myUserHandle, file, directory="", overwrite=0):
2106 """
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
2110 to save data to.
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)
2115 """
2116 self.auth = auth
2117 self.myUserHandle = myUserHandle
2118 self.fileSize = 0
2119 self.connected = 0
2120 self.completed = 0
2121 self.directory = directory
2122 self.bytesReceived = 0
2123 self.overwrite = overwrite
2124
2125 # used for handling current received state
2126 self.state = 'CONNECTING'
2127 self.segmentLength = 0
2128 self.buffer = ''
2129
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')
2136 else:
2137 self.file = file
2138
2139 def connectionMade(self):
2140 self.connected = 1
2141 self.state = 'INHEADER'
2142 self.sendLine('VER MSNFTP')
2143
2144 def connectionLost(self, reason):
2145 self.connected = 0
2146 self.file.close()
2147
2148 def parseHeader(self, header):
2149 """ parse the header of each 'message' to obtain the segment length """
2150
2151 if ord(header[0]) != 0: # they requested that we close the connection
2152 self.transport.loseConnection()
2153 return
2154 try:
2155 extra, factor = header[1:]
2156 except ValueError:
2157 # munged header, ending transfer
2158 self.transport.loseConnection()
2159 raise
2160 extra = ord(extra)
2161 factor = ord(factor)
2162 return factor * 256 + extra
2163
2164 def lineReceived(self, line):
2165 temp = line.split()
2166 if len(temp) == 1: params = []
2167 else: params = temp[1:]
2168 cmd = temp[0]
2169 handler = getattr(self, "handle_%s" % cmd.upper(), None)
2170 if handler: handler(params) # try/except
2171 else: self.handle_UNKNOWN(cmd, params)
2172
2173 def rawDataReceived(self, data):
2174 bufferLen = len(self.buffer)
2175 if self.state == 'INHEADER':
2176 delim = 3-bufferLen
2177 self.buffer += data[:delim]
2178 if len(self.buffer) == 3:
2179 self.segmentLength = self.parseHeader(self.buffer)
2180 if not self.segmentLength: return # hrm
2181 self.buffer = ""
2182 self.state = 'INSEGMENT'
2183 extra = data[delim:]
2184 if len(extra) > 0: self.rawDataReceived(extra)
2185 return
2186
2187 elif self.state == 'INSEGMENT':
2188 dataSeg = data[:(self.segmentLength-bufferLen)]
2189 self.buffer += dataSeg
2190 self.bytesReceived += len(dataSeg)
2191 if len(self.buffer) == self.segmentLength:
2192 self.gotSegment(self.buffer)
2193 self.buffer = ""
2194 if self.bytesReceived == self.fileSize:
2195 self.completed = 1
2196 self.buffer = ""
2197 self.file.close()
2198 self.sendLine("BYE 16777989")
2199 return
2200 self.state = 'INHEADER'
2201 extra = data[(self.segmentLength-bufferLen):]
2202 if len(extra) > 0: self.rawDataReceived(extra)
2203 return
2204
2205 def handle_VER(self, params):
2206 checkParamLen(len(params), 1, 'VER')
2207 if params[0].upper() == "MSNFTP":
2208 self.sendLine("USR %s %s" % (self.myUserHandle, self.auth))
2209 else:
2210 log.msg('they sent the wrong version, time to quit this transfer')
2211 self.transport.loseConnection()
2212
2213 def handle_FIL(self, params):
2214 checkParamLen(len(params), 1, 'FIL')
2215 try:
2216 self.fileSize = int(params[0])
2217 except ValueError: # they sent the wrong file size - probably want to log this
2218 self.transport.loseConnection()
2219 return
2220 self.setRawMode()
2221 self.sendLine("TFR")
2222
2223 def handle_UNKNOWN(self, cmd, params):
2224 log.msg('received unknown command (%s), params: %s' % (cmd, params))
2225
2226 def gotSegment(self, data):
2227 """ called when a segment (block) of data arrives. """
2228 self.file.write(data)
2229
2230 class FileSend(LineReceiver):
2231 """
2232 This class provides support for sending files to other contacts.
2233
2234 @ivar bytesSent: the number of bytes that have currently been sent.
2235 @ivar completed: true if the send has completed.
2236 @ivar connected: true if a connection has been established.
2237 @ivar targetUser: the target user (contact).
2238 @ivar segmentSize: the segment (block) size.
2239 @ivar auth: the auth cookie (number) to use when sending the
2240 transfer invitation
2241 """
2242
2243 def __init__(self, file):
2244 """
2245 @param file: A string or file object represnting the file to send.
2246 """
2247
2248 if isinstance(file, types.StringType):
2249 self.file = open(file, 'rb')
2250 else:
2251 self.file = file
2252
2253 self.fileSize = 0
2254 self.bytesSent = 0
2255 self.completed = 0
2256 self.connected = 0
2257 self.targetUser = None
2258 self.segmentSize = 2045
2259 self.auth = randint(0, 2**30)
2260 self._pendingSend = None # :(
2261
2262 def connectionMade(self):
2263 self.connected = 1
2264
2265 def connectionLost(self, reason):
2266 if self._pendingSend:
2267 self._pendingSend.cancel()
2268 self._pendingSend = None
2269 self.connected = 0
2270 self.file.close()
2271
2272 def lineReceived(self, line):
2273 temp = line.split()
2274 if len(temp) == 1: params = []
2275 else: params = temp[1:]
2276 cmd = temp[0]
2277 handler = getattr(self, "handle_%s" % cmd.upper(), None)
2278 if handler: handler(params)
2279 else: self.handle_UNKNOWN(cmd, params)
2280
2281 def handle_VER(self, params):
2282 checkParamLen(len(params), 1, 'VER')
2283 if params[0].upper() == "MSNFTP":
2284 self.sendLine("VER MSNFTP")
2285 else: # they sent some weird version during negotiation, i'm quitting.
2286 self.transport.loseConnection()
2287
2288 def handle_USR(self, params):
2289 checkParamLen(len(params), 2, 'USR')
2290 self.targetUser = params[0]
2291 if self.auth == int(params[1]):
2292 self.sendLine("FIL %s" % (self.fileSize))
2293 else: # they failed the auth test, disconnecting.
2294 self.transport.loseConnection()
2295
2296 def handle_TFR(self, params):
2297 checkParamLen(len(params), 0, 'TFR')
2298 # they are ready for me to start sending
2299 self.sendPart()
2300
2301 def handle_BYE(self, params):
2302 self.completed = (self.bytesSent == self.fileSize)
2303 self.transport.loseConnection()
2304
2305 def handle_CCL(self, params):
2306 self.completed = (self.bytesSent == self.fileSize)
2307 self.transport.loseConnection()
2308
2309 def handle_UNKNOWN(self, cmd, params): log.msg('received unknown command (%s), params: %s' % (cmd, params))
2310
2311 def makeHeader(self, size):
2312 """ make the appropriate header given a specific segment size. """
2313 quotient, remainder = divmod(size, 256)
2314 return chr(0) + chr(remainder) + chr(quotient)
2315
2316 def sendPart(self):
2317 """ send a segment of data """
2318 if not self.connected:
2319 self._pendingSend = None
2320 return # may be buggy (if handle_CCL/BYE is called but self.connected is still 1)
2321 data = self.file.read(self.segmentSize)
2322 if data:
2323 dataSize = len(data)
2324 header = self.makeHeader(dataSize)
2325 self.transport.write(header + data)
2326 self.bytesSent += dataSize
2327 self._pendingSend = reactor.callLater(0, self.sendPart)
2328 else:
2329 self._pendingSend = None
2330 self.completed = 1
2331
2332 # mapping of error codes to error messages
2333 errorCodes = {
2334
2335 200 : "Syntax error",
2336 201 : "Invalid parameter",
2337 205 : "Invalid user",
2338 206 : "Domain name missing",
2339 207 : "Already logged in",
2340 208 : "Invalid username",
2341 209 : "Invalid screen name",
2342 210 : "User list full",
2343 215 : "User already there",
2344 216 : "User already on list",
2345 217 : "User not online",
2346 218 : "Already in mode",
2347 219 : "User is in the opposite list",
2348 223 : "Too many groups",
2349 224 : "Invalid group",
2350 225 : "User not in group",
2351 229 : "Group name too long",
2352 230 : "Cannot remove group 0",
2353 231 : "Invalid group",
2354 280 : "Switchboard failed",
2355 281 : "Transfer to switchboard failed",
2356
2357 300 : "Required field missing",
2358 301 : "Too many FND responses",
2359 302 : "Not logged in",
2360
2361 500 : "Internal server error",
2362 501 : "Database server error",
2363 502 : "Command disabled",
2364 510 : "File operation failed",
2365 520 : "Memory allocation failed",
2366 540 : "Wrong CHL value sent to server",
2367
2368 600 : "Server is busy",
2369 601 : "Server is unavaliable",
2370 602 : "Peer nameserver is down",
2371 603 : "Database connection failed",
2372 604 : "Server is going down",
2373 605 : "Server unavailable",
2374
2375 707 : "Could not create connection",
2376 710 : "Invalid CVR parameters",
2377 711 : "Write is blocking",
2378 712 : "Session is overloaded",
2379 713 : "Too many active users",
2380 714 : "Too many sessions",
2381 715 : "Not expected",
2382 717 : "Bad friend file",
2383 731 : "Not expected",
2384
2385 800 : "Requests too rapid",
2386
2387 910 : "Server too busy",
2388 911 : "Authentication failed",
2389 912 : "Server too busy",
2390 913 : "Not allowed when offline",
2391 914 : "Server too busy",
2392 915 : "Server too busy",
2393 916 : "Server too busy",
2394 917 : "Server too busy",
2395 918 : "Server too busy",
2396 919 : "Server too busy",
2397 920 : "Not accepting new users",
2398 921 : "Server too busy",
2399 922 : "Server too busy",
2400 923 : "No parent consent",
2401 924 : "Passport account not yet verified"
2402
2403 }
2404
2405 # mapping of status codes to readable status format
2406 statusCodes = {
2407
2408 STATUS_ONLINE : "Online",
2409 STATUS_OFFLINE : "Offline",
2410 STATUS_HIDDEN : "Appear Offline",
2411 STATUS_IDLE : "Idle",
2412 STATUS_AWAY : "Away",
2413 STATUS_BUSY : "Busy",
2414 STATUS_BRB : "Be Right Back",
2415 STATUS_PHONE : "On the Phone",
2416 STATUS_LUNCH : "Out to Lunch"
2417
2418 }
2419
2420 # mapping of list ids to list codes
2421 listIDToCode = {
2422
2423 FORWARD_LIST : 'fl',
2424 BLOCK_LIST : 'bl',
2425 ALLOW_LIST : 'al',
2426 REVERSE_LIST : 'rl'
2427
2428 }
2429
2430 # mapping of list codes to list ids
2431 listCodeToID = {}
2432 for id,code in listIDToCode.items():
2433 listCodeToID[code] = id
2434
2435 del id, code