Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(200)

Side by Side Diff: third_party/twisted_8_1/twisted/words/protocols/msn.py

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

Powered by Google App Engine
This is Rietveld 408576698