]> code.delx.au - pymsnt/blob - src/tlib/msn/msn.py
Declines now work in backend
[pymsnt] / src / tlib / msn / msn.py
1 # Twisted, the Framework of Your Internet
2 # Copyright (C) 2001-2002 Matthew W. Lefkowitz
3 # Copyright (C) 2004-2005 James C. Bunton
4 #
5 # This library is free software; you can redistribute it and/or
6 # modify it under the terms of version 2.1 of the GNU Lesser General Public
7 # License as published by the Free Software Foundation.
8 #
9 # This library is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # Lesser General Public License for more details.
13 #
14 # You should have received a copy of the GNU Lesser General Public
15 # License along with this library; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 #
18
19 """
20 MSNP11 Protocol (client only) - semi-experimental
21
22 Stability: unstable.
23
24 This module provides support for clients using the MSN Protocol (MSNP11).
25 There are basically 3 servers involved in any MSN session:
26
27 I{Dispatch server}
28
29 The DispatchClient class handles connections to the
30 dispatch server, which basically delegates users to a
31 suitable notification server.
32
33 You will want to subclass this and handle the gotNotificationReferral
34 method appropriately.
35
36 I{Notification Server}
37
38 The NotificationClient class handles connections to the
39 notification server, which acts as a session server
40 (state updates, message negotiation etc...)
41
42 I{Switcboard Server}
43
44 The SwitchboardClient handles connections to switchboard
45 servers which are used to conduct conversations with other users.
46
47 There are also two classes (FileSend and FileReceive) used
48 for file transfers.
49
50 Clients handle events in two ways.
51
52 - each client request requiring a response will return a Deferred,
53 the callback for same will be fired when the server sends the
54 required response
55 - Events which are not in response to any client request have
56 respective methods which should be overridden and handled in
57 an adequate manner
58
59 Most client request callbacks require more than one argument,
60 and since Deferreds can only pass the callback one result,
61 most of the time the callback argument will be a tuple of
62 values (documented in the respective request method).
63 To make reading/writing code easier, callbacks can be defined in
64 a number of ways to handle this 'cleanly'. One way would be to
65 define methods like: def callBack(self, (arg1, arg2, arg)): ...
66 another way would be to do something like:
67 d.addCallback(lambda result: myCallback(*result)).
68
69 If the server sends an error response to a client request,
70 the errback of the corresponding Deferred will be called,
71 the argument being the corresponding error code.
72
73 B{NOTE}:
74 Due to the lack of an official spec for MSNP11, extra checking
75 than may be deemed necessary often takes place considering the
76 server is never 'wrong'. Thus, if gotBadLine (in any of the 3
77 main clients) is called, or an MSNProtocolError is raised, it's
78 probably a good idea to submit a bug report. ;)
79 Use of this module requires that PyOpenSSL is installed.
80
81 @author: U{Sam Jordan<mailto:sam@twistedmatrix.com>}
82 @author: U{James Bunton<mailto:james@delx.cjb.net>}
83 """
84
85 from __future__ import nested_scopes
86
87 # Sibling imports
88 from twisted.protocols.basic import LineReceiver
89 try:
90 from twisted.web.http import HTTPClient
91 except ImportError:
92 try:
93 from twisted.protocols.http import HTTPClient
94 except ImportError:
95 print "Couldn't find a HTTPClient. If you're using Twisted 2.0 make sure you've installed twisted.web"
96 raise
97 import msnp11chl
98
99 # Twisted imports
100 from twisted.internet import reactor, task
101 from twisted.internet.defer import Deferred
102 from twisted.internet.protocol import ClientFactory
103 from twisted.internet.ssl import ClientContextFactory
104 from twisted.python import failure, log
105 from twisted.xish.domish import unescapeFromXml
106
107 # Compat stuff
108 from tlib import xmlw
109
110 # System imports
111 import types, operator, os, sys, base64, random, struct, random, sha, base64, StringIO
112 from urllib import quote, unquote
113
114
115 MSN_PROTOCOL_VERSION = "MSNP11 CVR0" # protocol version
116 MSN_PORT = 1863 # default dispatch server port
117 MSN_MAX_MESSAGE = 1664 # max message length
118 MSN_CVR_STR = "0x040c winnt 5.1 i386 MSNMSGR 7.0.0777 msmsgs"
119 MSN_AVATAR_GUID = "{A4268EEC-FEC5-49E5-95C3-F126696BDBF6}"
120 MSN_MSNFTP_GUID = "{5D3E02AB-6190-11D3-BBBB-00C04F795683}"
121
122 # auth constants
123 LOGIN_SUCCESS = 1
124 LOGIN_FAILURE = 2
125 LOGIN_REDIRECT = 3
126
127 # list constants
128 FORWARD_LIST = 1
129 ALLOW_LIST = 2
130 BLOCK_LIST = 4
131 REVERSE_LIST = 8
132 PENDING_LIST = 16
133
134 # phone constants
135 HOME_PHONE = "PHH"
136 WORK_PHONE = "PHW"
137 MOBILE_PHONE = "PHM"
138 HAS_PAGER = "MOB"
139 HAS_BLOG = "HSB"
140
141 # status constants
142 STATUS_ONLINE = 'NLN'
143 STATUS_OFFLINE = 'FLN'
144 STATUS_HIDDEN = 'HDN'
145 STATUS_IDLE = 'IDL'
146 STATUS_AWAY = 'AWY'
147 STATUS_BUSY = 'BSY'
148 STATUS_BRB = 'BRB'
149 STATUS_PHONE = 'PHN'
150 STATUS_LUNCH = 'LUN'
151
152 CR = "\r"
153 LF = "\n"
154 PINGSPEED = 50.0
155
156 DEBUGALL = False
157 LINEDEBUG = False
158 MESSAGEDEBUG = False
159 MSNP2PDEBUG = False
160
161 if DEBUGALL:
162 LINEDEBUG = True
163 MESSAGEDEBUG = True
164 MSNP2PDEBUG = True
165
166 def getVal(inp):
167 return inp.split('=')[1]
168
169 def getVals(params):
170 userHandle = ""
171 screenName = ""
172 userGuid = ""
173 lists = -1
174 groups = []
175 for p in params:
176 if not p:
177 continue
178 elif p[0] == 'N':
179 userHandle = getVal(p)
180 elif p[0] == 'F':
181 screenName = unquote(getVal(p))
182 elif p[0] == 'C':
183 userGuid = getVal(p)
184 elif p.isdigit():
185 lists = int(p)
186 else: # Must be the groups
187 try:
188 groups = p.split(',')
189 except:
190 raise MSNProtocolError, "Unknown LST/ADC response" + str(params) # debug
191
192 return userHandle, screenName, userGuid, lists, groups
193
194 def b64enc(s):
195 return base64.encodestring(s).replace("\n", "")
196
197 def random_guid():
198 format = "{%4X%4X-%4X-%4X-%4X-%4X%4X%4X}"
199 data = []
200 for x in xrange(8):
201 data.append(random.random() * 0xAAFF + 0x1111)
202 data = tuple(data)
203
204 return format % data
205
206 def checkParamLen(num, expected, cmd, error=None):
207 if error == None: error = "Invalid Number of Parameters for %s" % cmd
208 if num != expected: raise MSNProtocolError, error
209
210 def _parseHeader(h, v):
211 """
212 Split a certin number of known
213 header values with the format:
214 field1=val,field2=val,field3=val into
215 a dict mapping fields to values.
216 @param h: the header's key
217 @param v: the header's value as a string
218 """
219
220 if h in ('passporturls','authentication-info','www-authenticate'):
221 v = v.replace('Passport1.4','').lstrip()
222 fields = {}
223 for fieldPair in v.split(','):
224 try:
225 field,value = fieldPair.split('=',1)
226 fields[field.lower()] = value
227 except ValueError:
228 fields[field.lower()] = ''
229 return fields
230 else: return v
231
232 def _parsePrimitiveHost(host):
233 # Ho Ho Ho
234 h,p = host.replace('https://','').split('/',1)
235 p = '/' + p
236 return h,p
237
238 def _login(userHandle, passwd, nexusServer, cached=0, authData=''):
239 """
240 This function is used internally and should not ever be called
241 directly.
242 """
243 cb = Deferred()
244 def _cb(server, auth):
245 loginFac = ClientFactory()
246 loginFac.protocol = lambda : PassportLogin(cb, userHandle, passwd, server, auth)
247 reactor.connectSSL(_parsePrimitiveHost(server)[0], 443, loginFac, ClientContextFactory())
248
249 if cached:
250 _cb(nexusServer, authData)
251 else:
252 fac = ClientFactory()
253 d = Deferred()
254 d.addCallbacks(_cb, callbackArgs=(authData,))
255 d.addErrback(lambda f: cb.errback(f))
256 fac.protocol = lambda : PassportNexus(d, nexusServer)
257 reactor.connectSSL(_parsePrimitiveHost(nexusServer)[0], 443, fac, ClientContextFactory())
258 return cb
259
260
261 class PassportNexus(HTTPClient):
262
263 """
264 Used to obtain the URL of a valid passport
265 login HTTPS server.
266
267 This class is used internally and should
268 not be instantiated directly -- that is,
269 The passport logging in process is handled
270 transparantly by NotificationClient.
271 """
272
273 def __init__(self, deferred, host):
274 self.deferred = deferred
275 self.host, self.path = _parsePrimitiveHost(host)
276
277 def connectionMade(self):
278 HTTPClient.connectionMade(self)
279 self.sendCommand('GET', self.path)
280 self.sendHeader('Host', self.host)
281 self.endHeaders()
282 self.headers = {}
283
284 def handleHeader(self, header, value):
285 h = header.lower()
286 self.headers[h] = _parseHeader(h, value)
287
288 def handleEndHeaders(self):
289 if self.connected: self.transport.loseConnection()
290 if not self.headers.has_key('passporturls') or not self.headers['passporturls'].has_key('dalogin'):
291 self.deferred.errback(failure.Failure(failure.DefaultException("Invalid Nexus Reply")))
292 else:
293 self.deferred.callback('https://' + self.headers['passporturls']['dalogin'])
294
295 def handleResponse(self, r): pass
296
297 class PassportLogin(HTTPClient):
298 """
299 This class is used internally to obtain
300 a login ticket from a passport HTTPS
301 server -- it should not be used directly.
302 """
303
304 _finished = 0
305
306 def __init__(self, deferred, userHandle, passwd, host, authData):
307 self.deferred = deferred
308 self.userHandle = userHandle
309 self.passwd = passwd
310 self.authData = authData
311 self.host, self.path = _parsePrimitiveHost(host)
312
313 def connectionMade(self):
314 self.sendCommand('GET', self.path)
315 self.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
316 'sign-in=%s,pwd=%s,%s' % (quote(self.userHandle), self.passwd,self.authData))
317 self.sendHeader('Host', self.host)
318 self.endHeaders()
319 self.headers = {}
320
321 def handleHeader(self, header, value):
322 h = header.lower()
323 self.headers[h] = _parseHeader(h, value)
324
325 def handleEndHeaders(self):
326 if self._finished: return
327 self._finished = 1 # I think we need this because of HTTPClient
328 if self.connected: self.transport.loseConnection()
329 authHeader = 'authentication-info'
330 _interHeader = 'www-authenticate'
331 if self.headers.has_key(_interHeader): authHeader = _interHeader
332 try:
333 info = self.headers[authHeader]
334 status = info['da-status']
335 handler = getattr(self, 'login_%s' % (status,), None)
336 if handler:
337 handler(info)
338 else: raise Exception()
339 except Exception, e:
340 self.deferred.errback(failure.Failure(e))
341
342 def handleResponse(self, r): pass
343
344 def login_success(self, info):
345 ticket = info['from-pp']
346 ticket = ticket[1:len(ticket)-1]
347 self.deferred.callback((LOGIN_SUCCESS, ticket))
348
349 def login_failed(self, info):
350 self.deferred.callback((LOGIN_FAILURE, unquote(info['cbtxt'])))
351
352 def login_redir(self, info):
353 self.deferred.callback((LOGIN_REDIRECT, self.headers['location'], self.authData))
354
355 class MSNProtocolError(Exception):
356 """
357 This Exception is basically used for debugging
358 purposes, as the official MSN server should never
359 send anything _wrong_ and nobody in their right
360 mind would run their B{own} MSN server.
361 If it is raised by default command handlers
362 (handle_BLAH) the error will be logged.
363 """
364 pass
365
366 class MSNMessage:
367
368 """
369 I am the class used to represent an 'instant' message.
370
371 @ivar userHandle: The user handle (passport) of the sender
372 (this is only used when receiving a message)
373 @ivar screenName: The screen name of the sender (this is only used
374 when receiving a message)
375 @ivar message: The message
376 @ivar headers: The message headers
377 @type headers: dict
378 @ivar length: The message length (including headers and line endings)
379 @ivar ack: This variable is used to tell the server how to respond
380 once the message has been sent. If set to MESSAGE_ACK
381 (default) the server will respond with an ACK upon receiving
382 the message, if set to MESSAGE_NACK the server will respond
383 with a NACK upon failure to receive the message.
384 If set to MESSAGE_ACK_NONE the server will do nothing.
385 This is relevant for the return value of
386 SwitchboardClient.sendMessage (which will return
387 a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
388 and will fire when the respective ACK or NACK is received).
389 If set to MESSAGE_ACK_NONE sendMessage will return None.
390 """
391 MESSAGE_ACK = 'A'
392 MESSAGE_ACK_FAT = 'D'
393 MESSAGE_NACK = 'N'
394 MESSAGE_ACK_NONE = 'U'
395
396 ack = MESSAGE_ACK
397
398 def __init__(self, length=0, userHandle="", screenName="", message="", specialMessage=False):
399 self.userHandle = userHandle
400 self.screenName = screenName
401 self.specialMessage = specialMessage
402 self.message = message
403 self.headers = {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
404 self.length = length
405 self.readPos = 0
406
407 def _calcMessageLen(self):
408 """
409 used to calculte the number to send
410 as the message length when sending a message.
411 """
412 return reduce(operator.add, [len(x[0]) + len(x[1]) + 4 for x in self.headers.items()]) + len(self.message) + 2
413
414 def setHeader(self, header, value):
415 """ set the desired header """
416 self.headers[header] = value
417
418 def getHeader(self, header):
419 """
420 get the desired header value
421 @raise KeyError: if no such header exists.
422 """
423 return self.headers[header]
424
425 def hasHeader(self, header):
426 """ check to see if the desired header exists """
427 return self.headers.has_key(header)
428
429 def getMessage(self):
430 """ return the message - not including headers """
431 return self.message
432
433 def setMessage(self, message):
434 """ set the message text """
435 self.message = message
436
437
438 class MSNObject:
439 """
440 Used to represent a MSNObject. This can be currently only be an avatar.
441
442 @ivar creator: The userHandle of the creator of this picture.
443 @ivar imageData: The PNG image data (only for our own avatar)
444 @ivar type: Always set to 3, for avatar.
445 @ivar size: The size of the image.
446 @ivar location: The filename of the image.
447 @ivar friendly: Unknown.
448 @ivar text: The textual representation of this MSNObject.
449 """
450 def __init__(self, s=""):
451 """ Pass a XML MSNObject string to parse it, or pass no arguments for a null MSNObject to be created. """
452 if s:
453 self.parse(s)
454 else:
455 self.setNull()
456
457 def setData(self, creator, imageData):
458 """ Set the creator and imageData for this object """
459 self.creator = creator
460 self.imageData = imageData
461 self.size = len(imageData)
462 self.type = 3
463 self.location = "TMP" + str(random.randint(1000,9999))
464 self.friendly = "AAA="
465 self.sha1d = b64enc(sha.sha(imageData).digest())
466 self.makeText()
467
468 def setNull(self):
469 self.creator = ""
470 self.imageData = ""
471 self.size = 0
472 self.type = 0
473 self.location = ""
474 self.friendly = ""
475 self.sha1d = ""
476 self.text = ""
477
478 def makeText(self):
479 """ Makes a textual representation of this MSNObject. Stores it in self.text """
480 h = []
481 h.append("Creator")
482 h.append(self.creator)
483 h.append("Size")
484 h.append(str(self.size))
485 h.append("Type")
486 h.append(str(self.type))
487 h.append("Location")
488 h.append(self.location)
489 h.append("Friendly")
490 h.append(self.friendly)
491 h.append("SHA1D")
492 h.append(self.sha1d)
493 sha1c = b64enc(sha.sha("".join(h)).digest())
494 self.text = '<msnobj Creator="%s" Size="%s" Type="%s" Location="%s" Friendly="%s" SHA1D="%s" SHA1C="%s"/>' % (self.creator, str(self.size), str(self.type), self.location, self.friendly, self.sha1d, sha1c)
495
496 def parse(self, s):
497 e = xmlw.parseText(s, True)
498 self.creator = e.getAttribute("Creator")
499 self.size = int(e.getAttribute("Size"))
500 self.type = int(e.getAttribute("Type"))
501 self.location = e.getAttribute("Location")
502 self.friendly = e.getAttribute("Friendly")
503 self.sha1d = e.getAttribute("SHA1D")
504 self.text = s
505
506
507 class MSNContact:
508
509 """
510 This class represents a contact (user).
511
512 @ivar userGuid: The contact's user guid (unique string)
513 @ivar userHandle: The contact's user handle (passport).
514 @ivar screenName: The contact's screen name.
515 @ivar groups: A list of all the group IDs which this
516 contact belongs to.
517 @ivar lists: An integer representing the sum of all lists
518 that this contact belongs to.
519 @ivar caps: int, The capabilities of this client
520 @ivar msnobj: The MSNObject representing the contact's avatar
521 @ivar status: The contact's status code.
522 @type status: str if contact's status is known, None otherwise.
523 @ivar personal: The contact's personal message .
524 @type personal: str if contact's personal message is known, None otherwise.
525
526 @ivar homePhone: The contact's home phone number.
527 @type homePhone: str if known, otherwise None.
528 @ivar workPhone: The contact's work phone number.
529 @type workPhone: str if known, otherwise None.
530 @ivar mobilePhone: The contact's mobile phone number.
531 @type mobilePhone: str if known, otherwise None.
532 @ivar hasPager: Whether or not this user has a mobile pager
533 @ivar hasBlog: Whether or not this user has a MSN Spaces blog
534 (true=yes, false=no)
535 """
536 MSNC1 = 0x10000000
537 MSNC2 = 0x20000000
538 MSNC3 = 0x30000000
539 MSNC4 = 0x40000000
540
541 def __init__(self, userGuid="", userHandle="", screenName="", lists=0, caps=0, msnobj=None, groups={}, status=None, personal=""):
542 self.userGuid = userGuid
543 self.userHandle = userHandle
544 self.screenName = screenName
545 self.lists = lists
546 self.caps = caps
547 self.msnobj = msnobj
548 self.msnobjGot = True
549 self.groups = [] # if applicable
550 self.status = status # current status
551 self.personal = personal
552
553 # phone details
554 self.homePhone = None
555 self.workPhone = None
556 self.mobilePhone = None
557 self.hasPager = None
558 self.hasBlog = None
559
560 def setPhone(self, phoneType, value):
561 """
562 set phone numbers/values for this specific user.
563 for phoneType check the *_PHONE constants and HAS_PAGER
564 """
565
566 t = phoneType.upper()
567 if t == HOME_PHONE: self.homePhone = value
568 elif t == WORK_PHONE: self.workPhone = value
569 elif t == MOBILE_PHONE: self.mobilePhone = value
570 elif t == HAS_PAGER: self.hasPager = value
571 elif t == HAS_BLOG: self.hasBlog = value
572 #else: raise ValueError, "Invalid Phone Type: " + t
573
574 def addToList(self, listType):
575 """
576 Update the lists attribute to
577 reflect being part of the
578 given list.
579 """
580 self.lists |= listType
581
582 def removeFromList(self, listType):
583 """
584 Update the lists attribute to
585 reflect being removed from the
586 given list.
587 """
588 self.lists ^= listType
589
590 class MSNContactList:
591 """
592 This class represents a basic MSN contact list.
593
594 @ivar contacts: All contacts on my various lists
595 @type contacts: dict (mapping user handles to MSNContact objects)
596 @ivar groups: a mapping of group ids to group names
597 (groups can only exist on the forward list)
598 @type groups: dict
599
600 B{Note}:
601 This is used only for storage and doesn't effect the
602 server's contact list.
603 """
604
605 def __init__(self):
606 self.contacts = {}
607 self.groups = {}
608 self.autoAdd = 0
609 self.privacy = 0
610 self.version = 0
611
612 def _getContactsFromList(self, listType):
613 """
614 Obtain all contacts which belong
615 to the given list type.
616 """
617 return dict([(uH,obj) for uH,obj in self.contacts.items() if obj.lists & listType])
618
619 def addContact(self, contact):
620 """
621 Add a contact
622 """
623 self.contacts[contact.userHandle] = contact
624
625 def remContact(self, userHandle):
626 """
627 Remove a contact
628 """
629 try:
630 del self.contacts[userHandle]
631 except KeyError: pass
632
633 def getContact(self, userHandle):
634 """
635 Obtain the MSNContact object
636 associated with the given
637 userHandle.
638 @return: the MSNContact object if
639 the user exists, or None.
640 """
641 try:
642 return self.contacts[userHandle]
643 except KeyError:
644 return None
645
646 def getBlockedContacts(self):
647 """
648 Obtain all the contacts on my block list
649 """
650 return self._getContactsFromList(BLOCK_LIST)
651
652 def getAuthorizedContacts(self):
653 """
654 Obtain all the contacts on my auth list.
655 (These are contacts which I have verified
656 can view my state changes).
657 """
658 return self._getContactsFromList(ALLOW_LIST)
659
660 def getReverseContacts(self):
661 """
662 Get all contacts on my reverse list.
663 (These are contacts which have added me
664 to their forward list).
665 """
666 return self._getContactsFromList(REVERSE_LIST)
667
668 def getContacts(self):
669 """
670 Get all contacts on my forward list.
671 (These are the contacts which I have added
672 to my list).
673 """
674 return self._getContactsFromList(FORWARD_LIST)
675
676 def setGroup(self, id, name):
677 """
678 Keep a mapping from the given id
679 to the given name.
680 """
681 self.groups[id] = name
682
683 def remGroup(self, id):
684 """
685 Removed the stored group
686 mapping for the given id.
687 """
688 try:
689 del self.groups[id]
690 except KeyError: pass
691 for c in self.contacts:
692 if id in c.groups: c.groups.remove(id)
693
694
695 class MSNEventBase(LineReceiver):
696 """
697 This class provides support for handling / dispatching events and is the
698 base class of the three main client protocols (DispatchClient,
699 NotificationClient, SwitchboardClient)
700 """
701
702 def __init__(self):
703 self.ids = {} # mapping of ids to Deferreds
704 self.currentID = 0
705 self.connected = 0
706 self.setLineMode()
707 self.currentMessage = None
708
709 def connectionLost(self, reason):
710 self.ids = {}
711 self.connected = 0
712
713 def connectionMade(self):
714 self.connected = 1
715
716 def _fireCallback(self, id, *args):
717 """
718 Fire the callback for the given id
719 if one exists and return 1, else return false
720 """
721 if self.ids.has_key(id):
722 self.ids[id][0].callback(args)
723 del self.ids[id]
724 return 1
725 return 0
726
727 def _nextTransactionID(self):
728 """ return a usable transaction ID """
729 self.currentID += 1
730 if self.currentID > 1000: self.currentID = 1
731 return self.currentID
732
733 def _createIDMapping(self, data=None):
734 """
735 return a unique transaction ID that is mapped internally to a
736 deferred .. also store arbitrary data if it is needed
737 """
738 id = self._nextTransactionID()
739 d = Deferred()
740 self.ids[id] = (d, data)
741 return (id, d)
742
743 def checkMessage(self, message):
744 """
745 process received messages to check for file invitations and
746 typing notifications and other control type messages
747 """
748 raise NotImplementedError
749
750 def sendLine(self, line):
751 if LINEDEBUG: log.msg(">> " + line)
752 LineReceiver.sendLine(self, line)
753
754 def lineReceived(self, line):
755 if LINEDEBUG: log.msg("<< " + line)
756 if self.currentMessage:
757 self.currentMessage.readPos += len(line+CR+LF)
758 try:
759 header, value = line.split(':')
760 self.currentMessage.setHeader(header, unquote(value).lstrip())
761 return
762 except ValueError:
763 #raise MSNProtocolError, "Invalid Message Header"
764 line = ""
765 if line == "" or self.currentMessage.specialMessage:
766 self.setRawMode()
767 if self.currentMessage.readPos == self.currentMessage.length: self.rawDataReceived("") # :(
768 return
769 try:
770 cmd, params = line.split(' ', 1)
771 except ValueError:
772 raise MSNProtocolError, "Invalid Message, %s" % repr(line)
773
774 if len(cmd) != 3: raise MSNProtocolError, "Invalid Command, %s" % repr(cmd)
775 if cmd.isdigit():
776 if self.ids.has_key(params.split(' ')[0]):
777 self.ids[id].errback(int(cmd))
778 del self.ids[id]
779 return
780 else: # we received an error which doesn't map to a sent command
781 self.gotError(int(cmd))
782 return
783
784 handler = getattr(self, "handle_%s" % cmd.upper(), None)
785 if handler:
786 try: handler(params.split(' '))
787 except MSNProtocolError, why: self.gotBadLine(line, why)
788 else:
789 self.handle_UNKNOWN(cmd, params.split(' '))
790
791 def rawDataReceived(self, data):
792 extra = ""
793 self.currentMessage.readPos += len(data)
794 diff = self.currentMessage.readPos - self.currentMessage.length
795 if diff > 0:
796 self.currentMessage.message += data[:-diff]
797 extra = data[-diff:]
798 elif diff == 0:
799 self.currentMessage.message += data
800 else:
801 self.currentMessage.message += data
802 return
803 del self.currentMessage.readPos
804 m = self.currentMessage
805 self.currentMessage = None
806 if MESSAGEDEBUG: log.msg(m.message)
807 if not self.checkMessage(m):
808 self.setLineMode(extra)
809 return
810 self.gotMessage(m)
811 self.setLineMode(extra)
812
813 ### protocol command handlers - no need to override these.
814
815 def handle_MSG(self, params):
816 checkParamLen(len(params), 3, 'MSG')
817 try:
818 messageLen = int(params[2])
819 except ValueError: raise MSNProtocolError, "Invalid Parameter for MSG length argument"
820 self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName=unquote(params[1]))
821
822 def handle_UNKNOWN(self, cmd, params):
823 """ implement me in subclasses if you want to handle unknown events """
824 log.msg("Received unknown command (%s), params: %s" % (cmd, params))
825
826 ### callbacks
827
828 def gotBadLine(self, line, why):
829 """ called when a handler notifies me that this line is broken """
830 log.msg('Error in line: %s (%s)' % (line, why))
831
832 def gotError(self, errorCode):
833 """
834 called when the server sends an error which is not in
835 response to a sent command (ie. it has no matching transaction ID)
836 """
837 log.msg('Error %s' % (errorCodes[errorCode]))
838
839 class DispatchClient(MSNEventBase):
840 """
841 This class provides support for clients connecting to the dispatch server
842 @ivar userHandle: your user handle (passport) needed before connecting.
843 """
844
845 # eventually this may become an attribute of the
846 # factory.
847 userHandle = ""
848
849 def connectionMade(self):
850 MSNEventBase.connectionMade(self)
851 self.sendLine('VER %s %s' % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
852
853 ### protocol command handlers ( there is no need to override these )
854
855 def handle_VER(self, params):
856 versions = params[1:]
857 if versions is None or ' '.join(versions) != MSN_PROTOCOL_VERSION:
858 self.transport.loseConnection()
859 raise MSNProtocolError, "Invalid version response"
860 id = self._nextTransactionID()
861 self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.userHandle))
862
863 def handle_CVR(self, params):
864 self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.userHandle))
865
866 def handle_XFR(self, params):
867 if len(params) < 4: raise MSNProtocolError, "Invalid number of parameters for XFR"
868 id, refType, addr = params[:3]
869 # was addr a host:port pair?
870 try:
871 host, port = addr.split(':')
872 except ValueError:
873 host = addr
874 port = MSN_PORT
875 if refType == "NS":
876 self.gotNotificationReferral(host, int(port))
877
878 ### callbacks
879
880 def gotNotificationReferral(self, host, port):
881 """
882 called when we get a referral to the notification server.
883
884 @param host: the notification server's hostname
885 @param port: the port to connect to
886 """
887 pass
888
889
890 class NotificationClient(MSNEventBase):
891 """
892 This class provides support for clients connecting
893 to the notification server.
894 """
895
896 factory = None # sssh pychecker
897
898 def __init__(self, currentID=0):
899 MSNEventBase.__init__(self)
900 self.currentID = currentID
901 self._state = ['DISCONNECTED', {}]
902 self.pingCounter = 0
903 self.pingCheckTask = None
904 self.msnobj = MSNObject()
905
906 def _setState(self, state):
907 self._state[0] = state
908
909 def _getState(self):
910 return self._state[0]
911
912 def _getStateData(self, key):
913 return self._state[1][key]
914
915 def _setStateData(self, key, value):
916 self._state[1][key] = value
917
918 def _remStateData(self, *args):
919 for key in args: del self._state[1][key]
920
921 def connectionMade(self):
922 MSNEventBase.connectionMade(self)
923 self._setState('CONNECTED')
924 self.sendLine("VER %s %s" % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
925
926 def connectionLost(self, reason):
927 self._setState('DISCONNECTED')
928 self._state[1] = {}
929 if self.pingCheckTask:
930 self.pingCheckTask.stop()
931 self.pingCheckTask = None
932 MSNEventBase.connectionLost(self, reason)
933
934 def _getEmailFields(self, message):
935 fields = message.getMessage().strip().split('\n')
936 values = {}
937 for i in fields:
938 a = i.split(':')
939 if len(a) != 2: continue
940 f, v = a
941 f = f.strip()
942 v = v.strip()
943 values[f] = v
944 return values
945
946 def _gotInitialEmailNotification(self, message):
947 values = self._getEmailFields(message)
948 try:
949 inboxunread = int(values["Inbox-Unread"])
950 foldersunread = int(values["Folders-Unread"])
951 except KeyError:
952 return
953 if foldersunread + inboxunread > 0: # For some reason MSN sends notifications about empty inboxes sometimes?
954 self.gotInitialEmailNotification(inboxunread, foldersunread)
955
956 def _gotEmailNotification(self, message):
957 values = self._getEmailFields(message)
958 try:
959 mailfrom = values["From"]
960 fromaddr = values["From-Addr"]
961 subject = values["Subject"]
962 junkbeginning = "=?\"us-ascii\"?Q?"
963 junkend = "?="
964 subject = subject.replace(junkbeginning, "").replace(junkend, "").replace("_", " ")
965 except KeyError:
966 # If any of the fields weren't found then it's not a big problem. We just ignore the message
967 return
968 self.gotRealtimeEmailNotification(mailfrom, fromaddr, subject)
969
970 def _gotMSNAlert(self, message):
971 notification = xmlw.parseText(message.message, beExtremelyLenient=True)
972 siteurl = notification.getAttribute("siteurl")
973 notid = notification.getAttribute("id")
974
975 msg = None
976 for e in notification.elements():
977 if e.name == "MSG":
978 msg = e
979 break
980 else: return
981
982 msgid = msg.getAttribute("id")
983
984 action = None
985 subscr = None
986 bodytext = None
987 for e in msg.elements():
988 if e.name == "ACTION":
989 action = e.getAttribute("url")
990 if e.name == "SUBSCR":
991 subscr = e.getAttribute("url")
992 if e.name == "BODY":
993 for e2 in e.elements():
994 if e2.name == "TEXT":
995 bodytext = e2.__str__()
996 if not (action and subscr and bodytext): return
997
998 actionurl = "%s&notification_id=%s&message_id=%s&agent=messenger" % (action, notid, msgid) # Used to have $siteurl// at the beginning, but it seems to not work with that now. Weird
999 subscrurl = "%s&notification_id=%s&message_id=%s&agent=messenger" % (subscr, notid, msgid)
1000
1001 self.gotMSNAlert(bodytext, actionurl, subscrurl)
1002
1003 def _gotUBX(self, message):
1004 lm = message.message.lower()
1005 p1 = lm.find("<psm>") + 5
1006 p2 = lm.find("</psm>")
1007 if p1 >= 0 and p2 >= 0:
1008 personal = unescapeFromXml(message.message[p1:p2])
1009 msnContact = self.factory.contacts.getContact(message.userHandle)
1010 if not msnContact: return
1011 msnContact.personal = personal
1012 self.contactPersonalChanged(message.userHandle, personal)
1013 else:
1014 self.contactPersonalChanged(message.userHandle, '')
1015
1016 def checkMessage(self, message):
1017 """ hook used for detecting specific notification messages """
1018 cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
1019 if 'text/x-msmsgsprofile' in cTypes:
1020 self.gotProfile(message)
1021 return 0
1022 elif "text/x-msmsgsinitialemailnotification" in cTypes:
1023 self._gotInitialEmailNotification(message)
1024 return 0
1025 elif "text/x-msmsgsemailnotification" in cTypes:
1026 self._gotEmailNotification(message)
1027 return 0
1028 elif "NOTIFICATION" == message.userHandle and message.specialMessage == True:
1029 self._gotMSNAlert(message)
1030 return 0
1031 elif "UBX" == message.screenName and message.specialMessage == True:
1032 self._gotUBX(message)
1033 return 0
1034 return 1
1035
1036 ### protocol command handlers - no need to override these
1037
1038 def handle_VER(self, params):
1039 versions = params[1:]
1040 if versions is None or ' '.join(versions) != MSN_PROTOCOL_VERSION:
1041 self.transport.loseConnection()
1042 raise MSNProtocolError, "Invalid version response"
1043 self.sendLine("CVR %s %s %s" % (self._nextTransactionID(), MSN_CVR_STR, self.factory.userHandle))
1044
1045 def handle_CVR(self, params):
1046 self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.factory.userHandle))
1047
1048 def handle_USR(self, params):
1049 if not (4 <= len(params) <= 6):
1050 raise MSNProtocolError, "Invalid Number of Parameters for USR"
1051
1052 mechanism = params[1]
1053 if mechanism == "OK":
1054 self.loggedIn(params[2], int(params[3]))
1055 elif params[2].upper() == "S":
1056 # we need to obtain auth from a passport server
1057 f = self.factory
1058 d = _login(f.userHandle, f.password, f.passportServer, authData=params[3])
1059 d.addCallback(self._passportLogin)
1060 d.addErrback(self._passportError)
1061
1062 def _passportLogin(self, result):
1063 if result[0] == LOGIN_REDIRECT:
1064 d = _login(self.factory.userHandle, self.factory.password,
1065 result[1], cached=1, authData=result[2])
1066 d.addCallback(self._passportLogin)
1067 d.addErrback(self._passportError)
1068 elif result[0] == LOGIN_SUCCESS:
1069 self.sendLine("USR %s TWN S %s" % (self._nextTransactionID(), result[1]))
1070 elif result[0] == LOGIN_FAILURE:
1071 self.loginFailure(result[1])
1072
1073 def _passportError(self, failure):
1074 self.loginFailure("Exception while authenticating: %s" % failure)
1075
1076 def handle_CHG(self, params):
1077 id = int(params[0])
1078 if not self._fireCallback(id, params[1]):
1079 if self.factory: self.factory.status = params[1]
1080 self.statusChanged(params[1])
1081
1082 def handle_ILN(self, params):
1083 #checkParamLen(len(params), 6, 'ILN')
1084 msnContact = self.factory.contacts.getContact(params[2])
1085 if not msnContact: return
1086 msnContact.status = params[1]
1087 msnContact.screenName = unquote(params[3])
1088 if len(params) > 4: msnContact.caps = int(params[4])
1089 if len(params) > 5:
1090 self.handleAvatarHelper(msnContact, params[5])
1091 else:
1092 self.handleAvatarGoneHelper(msnContact)
1093 self.gotContactStatus(params[1], params[2], unquote(params[3]))
1094
1095 def handleAvatarGoneHelper(self, msnContact):
1096 if msnContact.msnobj:
1097 msnContact.msnobj = None
1098 msnContact.msnobjGot = True
1099 self.contactAvatarChanged(msnContact.userHandle, "")
1100
1101 def handleAvatarHelper(self, msnContact, msnobjStr):
1102 msnobj = MSNObject(unquote(msnobjStr))
1103 if not msnContact.msnobj or msnobj.sha1d != msnContact.msnobj.sha1d:
1104 if MSNP2PDEBUG: log.msg("Updated MSNObject received!" + msnobjStr)
1105 msnContact.msnobj = msnobj
1106 msnContact.msnobjGot = False
1107 self.contactAvatarChanged(msnContact.userHandle, msnContact.msnobj.sha1d)
1108
1109 def handle_CHL(self, params):
1110 checkParamLen(len(params), 2, 'CHL')
1111 response = msnp11chl.doChallenge(params[1])
1112 self.sendLine("QRY %s %s %s" % (self._nextTransactionID(), msnp11chl.MSNP11_PRODUCT_ID, len(response)))
1113 self.transport.write(response)
1114
1115 def handle_QRY(self, params):
1116 pass
1117
1118 def handle_NLN(self, params):
1119 if not self.factory: return
1120 msnContact = self.factory.contacts.getContact(params[1])
1121 if not msnContact: return
1122 msnContact.status = params[0]
1123 msnContact.screenName = unquote(params[2])
1124 if len(params) > 3: msnContact.caps = int(params[3])
1125 if len(params) > 4:
1126 self.handleAvatarHelper(msnContact, params[4])
1127 else:
1128 self.handleAvatarGoneHelper(msnContact)
1129 self.contactStatusChanged(params[0], params[1], unquote(params[2]))
1130
1131 def handle_FLN(self, params):
1132 checkParamLen(len(params), 1, 'FLN')
1133 msnContact = self.factory.contacts.getContact(params[0])
1134 if msnContact:
1135 msnContact.status = STATUS_OFFLINE
1136 self.contactOffline(params[0])
1137
1138 def handle_LST(self, params):
1139 if self._getState() != 'SYNC': return
1140
1141 userHandle, screenName, userGuid, lists, groups = getVals(params)
1142
1143 if not userHandle or lists < 1:
1144 raise MSNProtocolError, "Unknown LST " + str(params) # debug
1145 contact = MSNContact(userGuid, userHandle, screenName, lists)
1146 if contact.lists & FORWARD_LIST:
1147 contact.groups.extend(map(str, groups))
1148 self._getStateData('list').addContact(contact)
1149 self._setStateData('last_contact', contact)
1150 sofar = self._getStateData('lst_sofar') + 1
1151 if sofar == self._getStateData('lst_reply'):
1152 # this is the best place to determine that
1153 # a syn realy has finished - msn _may_ send
1154 # BPR information for the last contact
1155 # which is unfortunate because it means
1156 # that the real end of a syn is non-deterministic.
1157 # to handle this we'll keep 'last_contact' hanging
1158 # around in the state data and update it if we need
1159 # to later.
1160 self._setState('SESSION')
1161 contacts = self._getStateData('list')
1162 phone = self._getStateData('phone')
1163 id = self._getStateData('synid')
1164 self._remStateData('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
1165 self._fireCallback(id, contacts, phone)
1166 else:
1167 self._setStateData('lst_sofar',sofar)
1168
1169 def handle_BLP(self, params):
1170 # check to see if this is in response to a SYN
1171 if self._getState() == 'SYNC':
1172 self._getStateData('list').privacy = listCodeToID[params[0].lower()]
1173 else:
1174 id = int(params[0])
1175 self.factory.contacts.privacy = listCodeToID[params[1].lower()]
1176 self._fireCallback(id, params[1])
1177
1178 def handle_GTC(self, params):
1179 # check to see if this is in response to a SYN
1180 if self._getState() == 'SYNC':
1181 if params[0].lower() == "a": self._getStateData('list').autoAdd = 0
1182 elif params[0].lower() == "n": self._getStateData('list').autoAdd = 1
1183 else: raise MSNProtocolError, "Invalid Paramater for GTC" # debug
1184 else:
1185 id = int(params[0])
1186 if params[1].lower() == "a": self._fireCallback(id, 0)
1187 elif params[1].lower() == "n": self._fireCallback(id, 1)
1188 else: raise MSNProtocolError, "Invalid Paramater for GTC" # debug
1189
1190 def handle_SYN(self, params):
1191 id = int(params[0])
1192 self._setStateData('phone', []) # Always needs to be set
1193 if params[3] == 0: # No LST will be received. New account?
1194 self._setState('SESSION')
1195 self._fireCallback(id, None, None)
1196 else:
1197 contacts = MSNContactList()
1198 self._setStateData('list', contacts)
1199 self._setStateData('lst_reply', int(params[3]))
1200 self._setStateData('lsg_reply', int(params[4]))
1201 self._setStateData('lst_sofar', 0)
1202
1203 def handle_LSG(self, params):
1204 if self._getState() == 'SYNC':
1205 self._getStateData('list').groups[params[1]] = unquote(params[0])
1206
1207 def handle_PRP(self, params):
1208 if params[1] == "MFN":
1209 self._fireCallback(int(params[0]), unquote(params[2]))
1210 elif self._getState() == 'SYNC':
1211 self._getStateData('phone').append((params[0], unquote(params[1])))
1212 else:
1213 self._fireCallback(int(params[0]), int(params[1]), unquote(params[3]))
1214
1215 def handle_BPR(self, params):
1216 if not self.factory.contacts: raise MSNProtocolError, "handle_BPR called with no contact list" # debug
1217 numParams = len(params)
1218 if numParams == 2: # part of a syn
1219 self._getStateData('last_contact').setPhone(params[0], unquote(params[1]))
1220 elif numParams == 4:
1221 self.factory.contacts.version = int(params[0])
1222 userHandle, phoneType, number = params[1], params[2], unquote(params[3])
1223 self.factory.contacts.getContact(userHandle).setPhone(phoneType, number)
1224 self.gotPhoneNumber(userHandle, phoneType, number)
1225
1226
1227 def handle_ADG(self, params):
1228 checkParamLen(len(params), 5, 'ADG')
1229 id = int(params[0])
1230 if not self._fireCallback(id, int(params[1]), unquote(params[2]), int(params[3])):
1231 raise MSNProtocolError, "ADG response does not match up to a request" # debug
1232
1233 def handle_RMG(self, params):
1234 checkParamLen(len(params), 3, 'RMG')
1235 id = int(params[0])
1236 if not self._fireCallback(id, int(params[1]), int(params[2])):
1237 raise MSNProtocolError, "RMG response does not match up to a request" # debug
1238
1239 def handle_REG(self, params):
1240 checkParamLen(len(params), 5, 'REG')
1241 id = int(params[0])
1242 if not self._fireCallback(id, int(params[1]), int(params[2]), unquote(params[3])):
1243 raise MSNProtocolError, "REG response does not match up to a request" # debug
1244
1245 def handle_ADC(self, params):
1246 if not self.factory.contacts: raise MSNProtocolError, "handle_ADC called with no contact list"
1247 numParams = len(params)
1248 if numParams < 3 or params[1].upper() not in ('AL','BL','RL','FL', 'PL'):
1249 raise MSNProtocolError, "Invalid Paramaters for ADC" # debug
1250 id = int(params[0])
1251 listType = params[1].lower()
1252 userHandle, screenName, userGuid, ignored1, groups = getVals(params[2:])
1253
1254 if groups and listType.upper() != FORWARD_LIST:
1255 raise MSNProtocolError, "Only forward list can contain groups" # debug
1256
1257 if not self._fireCallback(id, listCodeToID[listType], userGuid, userHandle, screenName):
1258 c = self.factory.contacts.getContact(userHandle)
1259 if not c:
1260 c = MSNContact(userGuid=userGuid, userHandle=userHandle, screenName=screenName)
1261 self.factory.contacts.addContact(c)
1262 c.addToList(PENDING_LIST)
1263 self.userAddedMe(userGuid, userHandle, screenName)
1264
1265 def handle_REM(self, params):
1266 if not self.factory.contacts: raise MSNProtocolError, "handle_REM called with no contact list available!"
1267 numParams = len(params)
1268 if numParams < 3 or params[1].upper() not in ('AL','BL','FL','RL'):
1269 raise MSNProtocolError, "Invalid Paramaters for REM" # debug
1270 id = int(params[0])
1271 listType = params[1].lower()
1272 userHandle = params[2]
1273 groupID = None
1274 if numParams == 4:
1275 if params[1] != "FL": raise MSNProtocolError, "Only forward list can contain groups" # debug
1276 groupID = int(params[3])
1277 if not self._fireCallback(id, listCodeToID[listType], userHandle, groupID):
1278 if listType.upper() != "RL": return
1279 c = self.factory.contacts.getContact(userHandle)
1280 if not c: return
1281 c.removeFromList(REVERSE_LIST)
1282 if c.lists == 0: self.factory.contacts.remContact(c.userHandle)
1283 self.userRemovedMe(userHandle)
1284
1285 def handle_XFR(self, params):
1286 checkParamLen(len(params), 5, 'XFR')
1287 id = int(params[0])
1288 # check to see if they sent a host/port pair
1289 try:
1290 host, port = params[2].split(':')
1291 except ValueError:
1292 host = params[2]
1293 port = MSN_PORT
1294
1295 if not self._fireCallback(id, host, int(port), params[4]):
1296 raise MSNProtocolError, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
1297
1298 def handle_RNG(self, params):
1299 checkParamLen(len(params), 6, 'RNG')
1300 # check for host:port pair
1301 try:
1302 host, port = params[1].split(":")
1303 port = int(port)
1304 except ValueError:
1305 host = params[1]
1306 port = MSN_PORT
1307 self.gotSwitchboardInvitation(int(params[0]), host, port, params[3], params[4],
1308 unquote(params[5]))
1309
1310 def handle_NOT(self, params):
1311 checkParamLen(len(params), 1, 'NOT')
1312 try:
1313 messageLen = int(params[0])
1314 except ValueError: raise MSNProtocolError, "Invalid Parameter for NOT length argument"
1315 self.currentMessage = MSNMessage(length=messageLen, userHandle="NOTIFICATION", specialMessage=True)
1316 self.setRawMode()
1317
1318 def handle_UBX(self, params):
1319 checkParamLen(len(params), 2, 'UBX')
1320 try:
1321 messageLen = int(params[1])
1322 except ValueError: raise MSNProtocolError, "Invalid Parameter for UBX length argument"
1323 if messageLen > 0:
1324 self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName="UBX", specialMessage=True)
1325 self.setRawMode()
1326 else:
1327 self.contactPersonalChanged(params[0], '')
1328
1329 def handle_UUX(self, params):
1330 checkParamLen(len(params), 2, 'UUX')
1331 if params[1] != '0': return
1332 id = int(params[0])
1333 self._fireCallback(id)
1334
1335 def handle_OUT(self, params):
1336 checkParamLen(len(params), 1, 'OUT')
1337 if params[0] == "OTH": self.multipleLogin()
1338 elif params[0] == "SSD": self.serverGoingDown()
1339 else: raise MSNProtocolError, "Invalid Parameters received for OUT" # debug
1340
1341 def handle_QNG(self, params):
1342 self.pingCounter = 0 # They replied to a ping. We'll forgive them for any they may have missed, because they're alive again now
1343
1344 # callbacks
1345
1346 def pingChecker(self):
1347 if self.pingCounter > 5:
1348 # The server has ignored 5 pings, lets kill the connection
1349 self.transport.loseConnection()
1350 else:
1351 self.sendLine("PNG")
1352 self.pingCounter += 1
1353
1354 def pingCheckerStart(self, *args):
1355 self.pingCheckTask = task.LoopingCall(self.pingChecker)
1356 self.pingCheckTask.start(PINGSPEED)
1357
1358 def loggedIn(self, userHandle, verified):
1359 """
1360 Called when the client has logged in.
1361 The default behaviour of this method is to
1362 update the factory with our screenName and
1363 to sync the contact list (factory.contacts).
1364 When this is complete self.listSynchronized
1365 will be called.
1366
1367 @param userHandle: our userHandle
1368 @param verified: 1 if our passport has been (verified), 0 if not.
1369 (i'm not sure of the significace of this)
1370 @type verified: int
1371 """
1372 d = self.syncList()
1373 d.addCallback(self.listSynchronized)
1374 d.addCallback(self.pingCheckerStart)
1375
1376 def loginFailure(self, message):
1377 """
1378 Called when the client fails to login.
1379
1380 @param message: a message indicating the problem that was encountered
1381 """
1382 pass
1383
1384 def gotProfile(self, message):
1385 """
1386 Called after logging in when the server sends an initial
1387 message with MSN/passport specific profile information
1388 such as country, number of kids, etc.
1389 Check the message headers for the specific values.
1390
1391 @param message: The profile message
1392 """
1393 pass
1394
1395 def listSynchronized(self, *args):
1396 """
1397 Lists are now synchronized by default upon logging in, this
1398 method is called after the synchronization has finished
1399 and the factory now has the up-to-date contacts.
1400 """
1401 pass
1402
1403 def contactAvatarChanged(self, userHandle, hash):
1404 """
1405 Called when we receive the first, or a new <msnobj/> from a
1406 contact.
1407
1408 @param userHandle: contact who's msnobj has been changed
1409 @param hash: sha1 hash of their avatar
1410 """
1411
1412 def statusChanged(self, statusCode):
1413 """
1414 Called when our status changes and its not in response to a
1415 client command.
1416
1417 @param statusCode: 3-letter status code
1418 """
1419 pass
1420
1421 def gotContactStatus(self, statusCode, userHandle, screenName):
1422 """
1423 Called when we receive a list of statuses upon login.
1424
1425 @param statusCode: 3-letter status code
1426 @param userHandle: the contact's user handle (passport)
1427 @param screenName: the contact's screen name
1428 """
1429 pass
1430
1431 def contactStatusChanged(self, statusCode, userHandle, screenName):
1432 """
1433 Called when we're notified that a contact's status has changed.
1434
1435 @param statusCode: 3-letter status code
1436 @param userHandle: the contact's user handle (passport)
1437 @param screenName: the contact's screen name
1438 """
1439 pass
1440
1441 def contactPersonalChanged(self, userHandle, personal):
1442 """
1443 Called when a contact's personal message changes.
1444
1445 @param userHandle: the contact who changed their personal message
1446 @param personal : the new personal message
1447 """
1448 pass
1449
1450 def contactOffline(self, userHandle):
1451 """
1452 Called when a contact goes offline.
1453
1454 @param userHandle: the contact's user handle
1455 """
1456 pass
1457
1458 def gotMessage(self, message):
1459 """
1460 Called when there is a message from the notification server
1461 that is not understood by default.
1462
1463 @param message: the MSNMessage.
1464 """
1465 pass
1466
1467 def gotMSNAlert(self, body, action, subscr):
1468 """
1469 Called when the server sends an MSN Alert (http://alerts.msn.com)
1470
1471 @param body : the alert text
1472 @param action: a URL with more information for the user to view
1473 @param subscr: a URL the user can use to modify their alert subscription
1474 """
1475 pass
1476
1477 def gotInitialEmailNotification(self, inboxunread, foldersunread):
1478 """
1479 Called when the server sends you details about your hotmail
1480 inbox. This is only ever called once, on login.
1481
1482 @param inboxunread : the number of unread items in your inbox
1483 @param foldersunread: the number of unread items in other folders
1484 """
1485 pass
1486
1487 def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
1488 """
1489 Called when the server sends us realtime email
1490 notification. This means that you have received
1491 a new email in your hotmail inbox.
1492
1493 @param mailfrom: the sender of the email
1494 @param fromaddr: the sender of the email (I don't know :P)
1495 @param subject : the email subject
1496 """
1497 pass
1498
1499 def gotPhoneNumber(self, userHandle, phoneType, number):
1500 """
1501 Called when the server sends us phone details about
1502 a specific user (for example after a user is added
1503 the server will send their status, phone details etc.
1504
1505 @param userHandle: the contact's user handle (passport)
1506 @param phoneType: the specific phoneType
1507 (*_PHONE constants or HAS_PAGER)
1508 @param number: the value/phone number.
1509 """
1510 pass
1511
1512 def userAddedMe(self, userGuid, userHandle, screenName):
1513 """
1514 Called when a user adds me to their list. (ie. they have been added to
1515 the reverse list.
1516
1517 @param userHandle: the userHandle of the user
1518 @param screenName: the screen name of the user
1519 """
1520 pass
1521
1522 def userRemovedMe(self, userHandle):
1523 """
1524 Called when a user removes us from their contact list
1525 (they are no longer on our reverseContacts list.
1526
1527 @param userHandle: the contact's user handle (passport)
1528 """
1529 pass
1530
1531 def gotSwitchboardInvitation(self, sessionID, host, port,
1532 key, userHandle, screenName):
1533 """
1534 Called when we get an invitation to a switchboard server.
1535 This happens when a user requests a chat session with us.
1536
1537 @param sessionID: session ID number, must be remembered for logging in
1538 @param host: the hostname of the switchboard server
1539 @param port: the port to connect to
1540 @param key: used for authorization when connecting
1541 @param userHandle: the user handle of the person who invited us
1542 @param screenName: the screen name of the person who invited us
1543 """
1544 pass
1545
1546 def multipleLogin(self):
1547 """
1548 Called when the server says there has been another login
1549 under our account, the server should disconnect us right away.
1550 """
1551 pass
1552
1553 def serverGoingDown(self):
1554 """
1555 Called when the server has notified us that it is going down for
1556 maintenance.
1557 """
1558 pass
1559
1560 # api calls
1561
1562 def changeStatus(self, status):
1563 """
1564 Change my current status. This method will add
1565 a default callback to the returned Deferred
1566 which will update the status attribute of the
1567 factory.
1568
1569 @param status: 3-letter status code (as defined by
1570 the STATUS_* constants)
1571 @return: A Deferred, the callback of which will be
1572 fired when the server confirms the change
1573 of status. The callback argument will be
1574 a tuple with the new status code as the
1575 only element.
1576 """
1577
1578 id, d = self._createIDMapping()
1579 self.sendLine("CHG %s %s %s %s" % (id, status, str(MSNContact.MSNC1 | MSNContact.MSNC2 | MSNContact.MSNC3 | MSNContact.MSNC4), quote(self.msnobj.text)))
1580 def _cb(r):
1581 self.factory.status = r[0]
1582 return r
1583 return d.addCallback(_cb)
1584
1585 def setPrivacyMode(self, privLevel):
1586 """
1587 Set my privacy mode on the server.
1588
1589 B{Note}:
1590 This only keeps the current privacy setting on
1591 the server for later retrieval, it does not
1592 effect the way the server works at all.
1593
1594 @param privLevel: This parameter can be true, in which
1595 case the server will keep the state as
1596 'al' which the official client interprets
1597 as -> allow messages from only users on
1598 the allow list. Alternatively it can be
1599 false, in which case the server will keep
1600 the state as 'bl' which the official client
1601 interprets as -> allow messages from all
1602 users except those on the block list.
1603
1604 @return: A Deferred, the callback of which will be fired when
1605 the server replies with the new privacy setting.
1606 The callback argument will be a tuple, the only element
1607 of which being either 'al' or 'bl' (the new privacy setting).
1608 """
1609
1610 id, d = self._createIDMapping()
1611 if privLevel: self.sendLine("BLP %s AL" % id)
1612 else: self.sendLine("BLP %s BL" % id)
1613 return d
1614
1615 def syncList(self):
1616 """
1617 Used for keeping an up-to-date contact list.
1618 A callback is added to the returned Deferred
1619 that updates the contact list on the factory
1620 and also sets my state to STATUS_ONLINE.
1621
1622 B{Note}:
1623 This is called automatically upon signing
1624 in using the version attribute of
1625 factory.contacts, so you may want to persist
1626 this object accordingly. Because of this there
1627 is no real need to ever call this method
1628 directly.
1629
1630 @return: A Deferred, the callback of which will be
1631 fired when the server sends an adequate reply.
1632 The callback argument will be a tuple with two
1633 elements, the new list (MSNContactList) and
1634 your current state (a dictionary). If the version
1635 you sent _was_ the latest list version, both elements
1636 will be None. To just request the list send a version of 0.
1637 """
1638
1639 self._setState('SYNC')
1640 id, d = self._createIDMapping(data=None)
1641 self._setStateData('synid',id)
1642 self.sendLine("SYN %s %s %s" % (id, 0, 0))
1643 def _cb(r):
1644 self.changeStatus(STATUS_ONLINE)
1645 if r[0] is not None:
1646 self.factory.contacts = r[0]
1647 return r
1648 return d.addCallback(_cb)
1649
1650 def setPhoneDetails(self, phoneType, value):
1651 """
1652 Set/change my phone numbers stored on the server.
1653
1654 @param phoneType: phoneType can be one of the following
1655 constants - HOME_PHONE, WORK_PHONE,
1656 MOBILE_PHONE, HAS_PAGER.
1657 These are pretty self-explanatory, except
1658 maybe HAS_PAGER which refers to whether or
1659 not you have a pager.
1660 @param value: for all of the *_PHONE constants the value is a
1661 phone number (str), for HAS_PAGER accepted values
1662 are 'Y' (for yes) and 'N' (for no).
1663
1664 @return: A Deferred, the callback for which will be fired when
1665 the server confirms the change has been made. The
1666 callback argument will be a tuple with 2 elements, the
1667 first being the new list version (int) and the second
1668 being the new phone number value (str).
1669 """
1670 raise "ProbablyDoesntWork"
1671 # XXX: Add a default callback which updates
1672 # factory.contacts.version and the relevant phone
1673 # number
1674 id, d = self._createIDMapping()
1675 self.sendLine("PRP %s %s %s" % (id, phoneType, quote(value)))
1676 return d
1677
1678 def addListGroup(self, name):
1679 """
1680 Used to create a new list group.
1681 A default callback is added to the
1682 returned Deferred which updates the
1683 contacts attribute of the factory.
1684
1685 @param name: The desired name of the new group.
1686
1687 @return: A Deferred, the callbacck for which will be called
1688 when the server clarifies that the new group has been
1689 created. The callback argument will be a tuple with 3
1690 elements: the new list version (int), the new group name
1691 (str) and the new group ID (int).
1692 """
1693
1694 raise "ProbablyDoesntWork"
1695 id, d = self._createIDMapping()
1696 self.sendLine("ADG %s %s 0" % (id, quote(name)))
1697 def _cb(r):
1698 if self.factory.contacts:
1699 self.factory.contacts.version = r[0]
1700 self.factory.contacts.setGroup(r[1], r[2])
1701 return r
1702 return d.addCallback(_cb)
1703
1704 def remListGroup(self, groupID):
1705 """
1706 Used to remove a list group.
1707 A default callback is added to the
1708 returned Deferred which updates the
1709 contacts attribute of the factory.
1710
1711 @param groupID: the ID of the desired group to be removed.
1712
1713 @return: A Deferred, the callback for which will be called when
1714 the server clarifies the deletion of the group.
1715 The callback argument will be a tuple with 2 elements:
1716 the new list version (int) and the group ID (int) of
1717 the removed group.
1718 """
1719
1720 raise "ProbablyDoesntWork"
1721 id, d = self._createIDMapping()
1722 self.sendLine("RMG %s %s" % (id, groupID))
1723 def _cb(r):
1724 self.factory.contacts.version = r[0]
1725 self.factory.contacts.remGroup(r[1])
1726 return r
1727 return d.addCallback(_cb)
1728
1729 def renameListGroup(self, groupID, newName):
1730 """
1731 Used to rename an existing list group.
1732 A default callback is added to the returned
1733 Deferred which updates the contacts attribute
1734 of the factory.
1735
1736 @param groupID: the ID of the desired group to rename.
1737 @param newName: the desired new name for the group.
1738
1739 @return: A Deferred, the callback for which will be called
1740 when the server clarifies the renaming.
1741 The callback argument will be a tuple of 3 elements,
1742 the new list version (int), the group id (int) and
1743 the new group name (str).
1744 """
1745
1746 raise "ProbablyDoesntWork"
1747 id, d = self._createIDMapping()
1748 self.sendLine("REG %s %s %s 0" % (id, groupID, quote(newName)))
1749 def _cb(r):
1750 self.factory.contacts.version = r[0]
1751 self.factory.contacts.setGroup(r[1], r[2])
1752 return r
1753 return d.addCallback(_cb)
1754
1755 def addContact(self, listType, userHandle):
1756 """
1757 Used to add a contact to the desired list.
1758 A default callback is added to the returned
1759 Deferred which updates the contacts attribute of
1760 the factory with the new contact information.
1761 If you are adding a contact to the forward list
1762 and you want to associate this contact with multiple
1763 groups then you will need to call this method for each
1764 group you would like to add them to, changing the groupID
1765 parameter. The default callback will take care of updating
1766 the group information on the factory's contact list.
1767
1768 @param listType: (as defined by the *_LIST constants)
1769 @param userHandle: the user handle (passport) of the contact
1770 that is being added
1771
1772 @return: A Deferred, the callback for which will be called when
1773 the server has clarified that the user has been added.
1774 The callback argument will be a tuple with 4 elements:
1775 the list type, the contact's user handle, the new list
1776 version, and the group id (if relevant, otherwise it
1777 will be None)
1778 """
1779
1780 id, d = self._createIDMapping()
1781 try: # Make sure the contact isn't actually on the list
1782 if self.factory.contacts.getContact(userHandle).lists & listType: return
1783 except AttributeError: pass
1784 listType = listIDToCode[listType].upper()
1785 if listType == "FL":
1786 self.sendLine("ADC %s %s N=%s F=%s" % (id, listType, userHandle, userHandle))
1787 else:
1788 self.sendLine("ADC %s %s N=%s" % (id, listType, userHandle))
1789
1790 def _cb(r):
1791 if not self.factory: return
1792 c = self.factory.contacts.getContact(r[2])
1793 if not c:
1794 c = MSNContact(userGuid=r[1], userHandle=r[2], screenName=r[3])
1795 #if r[3]: c.groups.append(r[3])
1796 c.addToList(r[0])
1797 return r
1798 return d.addCallback(_cb)
1799
1800 def remContact(self, listType, userHandle):
1801 """
1802 Used to remove a contact from the desired list.
1803 A default callback is added to the returned deferred
1804 which updates the contacts attribute of the factory
1805 to reflect the new contact information.
1806
1807 @param listType: (as defined by the *_LIST constants)
1808 @param userHandle: the user handle (passport) of the
1809 contact being removed
1810
1811 @return: A Deferred, the callback for which will be called when
1812 the server has clarified that the user has been removed.
1813 The callback argument will be a tuple of 3 elements:
1814 the list type, the contact's user handle and the group ID
1815 (if relevant, otherwise it will be None)
1816 """
1817
1818 id, d = self._createIDMapping()
1819 try: # Make sure the contact is actually on this list
1820 if not (self.factory.contacts.getContact(userHandle).lists & listType): return
1821 except AttributeError: return
1822 listType = listIDToCode[listType].upper()
1823 if listType == "FL":
1824 try:
1825 c = self.factory.contacts.getContact(userHandle)
1826 userGuid = c.userGuid
1827 except AttributeError: return
1828 self.sendLine("REM %s FL %s" % (id, userGuid))
1829 else:
1830 self.sendLine("REM %s %s %s" % (id, listType, userHandle))
1831
1832 def _cb(r):
1833 if listType == "FL":
1834 r = (r[0], userHandle, r[2]) # make sure we always get a userHandle
1835 l = self.factory.contacts
1836 c = l.getContact(r[1])
1837 if not c: return
1838 group = r[2]
1839 shouldRemove = 1
1840 if group: # they may not have been removed from the list
1841 c.groups.remove(group)
1842 if c.groups: shouldRemove = 0
1843 if shouldRemove:
1844 c.removeFromList(r[0])
1845 if c.lists == 0: l.remContact(c.userHandle)
1846 return r
1847 return d.addCallback(_cb)
1848
1849 def changeScreenName(self, newName):
1850 """
1851 Used to change your current screen name.
1852 A default callback is added to the returned
1853 Deferred which updates the screenName attribute
1854 of the factory and also updates the contact list
1855 version.
1856
1857 @param newName: the new screen name
1858
1859 @return: A Deferred, the callback for which will be called
1860 when the server acknowledges the change.
1861 The callback argument will be a tuple of 1 element,
1862 the new screen name.
1863 """
1864
1865 id, d = self._createIDMapping()
1866 self.sendLine("PRP %s MFN %s" % (id, quote(newName)))
1867 def _cb(r):
1868 self.factory.screenName = r[0]
1869 return r
1870 return d.addCallback(_cb)
1871
1872 def changePersonalMessage(self, personal):
1873 """
1874 Used to change your personal message.
1875
1876 @param personal: the new screen name
1877
1878 @return: A Deferred, the callback for which will be called
1879 when the server acknowledges the change.
1880 The callback argument will be a tuple of 1 element,
1881 the personal message.
1882 """
1883
1884 id, d = self._createIDMapping()
1885 data = ""
1886 if personal:
1887 data = "<Data><PSM>" + personal + "</PSM><CurrentMedia></CurrentMedia></Data>"
1888 self.sendLine("UUX %s %s" % (id, len(data)))
1889 self.transport.write(data)
1890 def _cb(r):
1891 self.factory.personal = personal
1892 return (personal,)
1893 return d.addCallback(_cb)
1894
1895 def changeAvatar(self, imageData, push):
1896 """
1897 Used to change the avatar that other users see.
1898
1899 @param imageData: the PNG image data to set as the avatar
1900 @param push : whether to push the update to the server
1901 (it will otherwise be sent with the next
1902 changeStatus())
1903
1904 @return: If push==True, a Deferred, the callback for which
1905 will be called when the server acknowledges the change.
1906 The callback argument will be the same as for changeStatus.
1907 """
1908
1909 if self.msnobj and imageData == self.msnobj.imageData: return
1910 if imageData:
1911 self.msnobj.setData(self.factory.userHandle, imageData)
1912 else:
1913 self.msnobj.setNull()
1914 if push: return self.changeStatus(self.factory.status) # Push to server
1915
1916
1917 def requestSwitchboardServer(self):
1918 """
1919 Used to request a switchboard server to use for conversations.
1920
1921 @return: A Deferred, the callback for which will be called when
1922 the server responds with the switchboard information.
1923 The callback argument will be a tuple with 3 elements:
1924 the host of the switchboard server, the port and a key
1925 used for logging in.
1926 """
1927
1928 id, d = self._createIDMapping()
1929 self.sendLine("XFR %s SB" % id)
1930 return d
1931
1932 def logOut(self):
1933 """
1934 Used to log out of the notification server.
1935 After running the method the server is expected
1936 to close the connection.
1937 """
1938
1939 if self.pingCheckTask:
1940 self.pingCheckTask.stop()
1941 self.pingCheckTask = None
1942 self.sendLine("OUT")
1943
1944 class NotificationFactory(ClientFactory):
1945 """
1946 Factory for the NotificationClient protocol.
1947 This is basically responsible for keeping
1948 the state of the client and thus should be used
1949 in a 1:1 situation with clients.
1950
1951 @ivar contacts: An MSNContactList instance reflecting
1952 the current contact list -- this is
1953 generally kept up to date by the default
1954 command handlers.
1955 @ivar userHandle: The client's userHandle, this is expected
1956 to be set by the client and is used by the
1957 protocol (for logging in etc).
1958 @ivar screenName: The client's current screen-name -- this is
1959 generally kept up to date by the default
1960 command handlers.
1961 @ivar password: The client's password -- this is (obviously)
1962 expected to be set by the client.
1963 @ivar passportServer: This must point to an msn passport server
1964 (the whole URL is required)
1965 @ivar status: The status of the client -- this is generally kept
1966 up to date by the default command handlers
1967 """
1968
1969 contacts = None
1970 userHandle = ''
1971 screenName = ''
1972 password = ''
1973 passportServer = 'https://nexus.passport.com/rdr/pprdr.asp'
1974 status = 'FLN'
1975 protocol = NotificationClient
1976
1977
1978 class SwitchboardClient(MSNEventBase):
1979 """
1980 This class provides support for clients connecting to a switchboard server.
1981
1982 Switchboard servers are used for conversations with other people
1983 on the MSN network. This means that the number of conversations at
1984 any given time will be directly proportional to the number of
1985 connections to varioius switchboard servers.
1986
1987 MSN makes no distinction between single and group conversations,
1988 so any number of users may be invited to join a specific conversation
1989 taking place on a switchboard server.
1990
1991 @ivar key: authorization key, obtained when receiving
1992 invitation / requesting switchboard server.
1993 @ivar userHandle: your user handle (passport)
1994 @ivar sessionID: unique session ID, used if you are replying
1995 to a switchboard invitation
1996 @ivar reply: set this to 1 in connectionMade or before to signifiy
1997 that you are replying to a switchboard invitation.
1998 @ivar msnobj: the MSNObject for the user. So that the switchboard can distribute it.
1999 """
2000
2001 key = 0
2002 userHandle = ""
2003 sessionID = ""
2004 reply = 0
2005 msnobj = None
2006
2007 _iCookie = 0
2008
2009 def __init__(self):
2010 MSNEventBase.__init__(self)
2011 self.pendingUsers = {}
2012 self.cookies = {'iCookies' : {}} # will maybe be moved to a factory in the future
2013 self.slpLinks = {}
2014
2015 def connectionMade(self):
2016 MSNEventBase.connectionMade(self)
2017 self._sendInit()
2018
2019 def connectionLost(self, reason):
2020 self.cookies['iCookies'] = {}
2021 MSNEventBase.connectionLost(self, reason)
2022
2023 def _sendInit(self):
2024 """
2025 send initial data based on whether we are replying to an invitation
2026 or starting one.
2027 """
2028 id = self._nextTransactionID()
2029 if not self.reply:
2030 self.sendLine("USR %s %s %s" % (id, self.userHandle, self.key))
2031 else:
2032 self.sendLine("ANS %s %s %s %s" % (id, self.userHandle, self.key, self.sessionID))
2033
2034 def _newInvitationCookie(self):
2035 self._iCookie += 1
2036 if self._iCookie > 1000: self._iCookie = 1
2037 return self._iCookie
2038
2039 def _checkTyping(self, message, cTypes):
2040 """ helper method for checkMessage """
2041 if 'text/x-msmsgscontrol' in cTypes and message.hasHeader('TypingUser'):
2042 self.userTyping(message)
2043 return 1
2044
2045 def _checkFileInvitation(self, message, info):
2046 """ helper method for checkMessage """
2047 if not info.get('Application-GUID', '').upper() == MSN_MSNFTP_GUID: return 0
2048 try:
2049 cookie = info['Invitation-Cookie']
2050 filename = info['Application-File']
2051 filesize = int(info['Application-FileSize'])
2052 connectivity = (info.get('Connectivity').lower() == 'y')
2053 except KeyError:
2054 log.msg('Received munged file transfer request ... ignoring.')
2055 return 0
2056 raise NotImplementedError
2057 self.gotSendRequest(msnft.MSNFTP_Receive(filename, filesize, message.userHandle, cookie, connectivity, self))
2058 return 1
2059
2060 def _handleP2PMessage(self, message):
2061 """ helper method for msnslp messages (file transfer & avatars) """
2062 if not message.getHeader("P2P-Dest") == self.userHandle: return
2063 packet = message.message
2064 binaryFields = BinaryFields(packet=packet)
2065 if binaryFields[0] != 0:
2066 slpLink = self.slpLinks[binaryFields[0]]
2067 if slpLink.remoteUser == message.userHandle:
2068 slpLink.handlePacket(packet)
2069 elif binaryFields[5] == BinaryFields.ACK or binaryFields[5] == BinaryFields.BYEGOT:
2070 pass # Ignore the ACKs
2071 else:
2072 slpMessage = MSNSLPMessage(packet)
2073 slpLink = None
2074 if slpMessage.method == "INVITE":
2075 if slpMessage.euf_guid == MSN_MSNFTP_GUID:
2076 #FIXME!
2077 context = FileContext(slpMessage.context)
2078 slpLink = SLPLink_FileReceive(remoteUser=slpMessage.fro, switchboard=self, filename=context.filename, filesize=context.filesize, sessionID=slpMessage.sessionID, sessionGuid=slpMessage.sessionGuid)
2079 self.slpLinks[slpMessage.sessionID] = slpLink
2080 self.gotFileReceive(slpLink)
2081 elif slpMessage.euf_guid == MSN_AVATAR_GUID:
2082 # Check that we have an avatar to send
2083 if self.msnobj:
2084 slpLink = SLPLink_AvatarSend(remoteUser=slpMessage.fro, switchboard=self, filesize=self.msnobj.size, sessionID=slpMessage.sessionID, sessionGuid=slpMessage.sessionGuid)
2085 slpLink.write(self.msnobj.imageData)
2086 slpLink.close()
2087 else:
2088 # They shouldn't have sent a request if we have
2089 # no avatar. So we'll just ignore them.
2090 pass
2091 if slpLink:
2092 self.slpLinks[slpMessage.sessionID] = slpLink
2093 else:
2094 for slpLink in self.slpLinks.values():
2095 if slpLink.sessionGuid == slpMessage.sessionGuid:
2096 slpLink.handleSLPMessage(slpMessage)
2097 if slpLink:
2098 # Always need to ACK these packets if we can
2099 slpLink.sendP2PACK(binaryFields)
2100
2101
2102 def checkMessage(self, message):
2103 """
2104 hook for detecting any notification type messages
2105 (e.g. file transfer)
2106 """
2107 cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
2108 if self._checkTyping(message, cTypes): return 0
2109 if 'text/x-msmsgsinvite' in cTypes:
2110 # header like info is sent as part of the message body.
2111 info = {}
2112 for line in message.message.split('\r\n'):
2113 try:
2114 key, val = line.split(':')
2115 info[key] = val.lstrip()
2116 except ValueError: continue
2117 if self._checkFileInvitation(message, info): return 0
2118 elif 'application/x-msnmsgrp2p' in cTypes:
2119 self._handleP2PMessage(message)
2120 return 0
2121 return 1
2122
2123 # negotiation
2124 def handle_USR(self, params):
2125 checkParamLen(len(params), 4, 'USR')
2126 if params[1] == "OK":
2127 self.loggedIn()
2128
2129 # invite a user
2130 def handle_CAL(self, params):
2131 checkParamLen(len(params), 3, 'CAL')
2132 id = int(params[0])
2133 if params[1].upper() == "RINGING":
2134 self._fireCallback(id, int(params[2])) # session ID as parameter
2135
2136 # user joined
2137 def handle_JOI(self, params):
2138 checkParamLen(len(params), 2, 'JOI')
2139 self.userJoined(params[0], unquote(params[1]))
2140
2141 # users participating in the current chat
2142 def handle_IRO(self, params):
2143 checkParamLen(len(params), 5, 'IRO')
2144 self.pendingUsers[params[3]] = unquote(params[4])
2145 if params[1] == params[2]:
2146 self.gotChattingUsers(self.pendingUsers)
2147 self.pendingUsers = {}
2148
2149 # finished listing users
2150 def handle_ANS(self, params):
2151 checkParamLen(len(params), 2, 'ANS')
2152 if params[1] == "OK":
2153 self.loggedIn()
2154
2155 def handle_ACK(self, params):
2156 checkParamLen(len(params), 1, 'ACK')
2157 self._fireCallback(int(params[0]), None)
2158
2159 def handle_NAK(self, params):
2160 checkParamLen(len(params), 1, 'NAK')
2161 self._fireCallback(int(params[0]), None)
2162
2163 def handle_BYE(self, params):
2164 #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
2165 self.userLeft(params[0])
2166
2167 # callbacks
2168
2169 def loggedIn(self):
2170 """
2171 called when all login details have been negotiated.
2172 Messages can now be sent, or new users invited.
2173 """
2174 pass
2175
2176 def gotChattingUsers(self, users):
2177 """
2178 called after connecting to an existing chat session.
2179
2180 @param users: A dict mapping user handles to screen names
2181 (current users taking part in the conversation)
2182 """
2183 pass
2184
2185 def userJoined(self, userHandle, screenName):
2186 """
2187 called when a user has joined the conversation.
2188
2189 @param userHandle: the user handle (passport) of the user
2190 @param screenName: the screen name of the user
2191 """
2192 pass
2193
2194 def userLeft(self, userHandle):
2195 """
2196 called when a user has left the conversation.
2197
2198 @param userHandle: the user handle (passport) of the user.
2199 """
2200 pass
2201
2202 def gotMessage(self, message):
2203 """
2204 called when we receive a message.
2205
2206 @param message: the associated MSNMessage object
2207 """
2208 pass
2209
2210 def gotFileReceive(self, fileReceive):
2211 """
2212 called when we receive a file send request from a contact
2213
2214 @param fileReceive: msnft.MSNFTReceive_Base instance
2215 """
2216 pass
2217
2218
2219 def gotSendRequest(self, fileReceive):
2220 """
2221 called when we receive a file send request from a contact
2222
2223 @param fileReceive: msnft.MSNFTReceive_Base instance
2224 """
2225 pass
2226
2227 def userTyping(self, message):
2228 """
2229 called when we receive the special type of message notifying
2230 us that a user is typing a message.
2231
2232 @param message: the associated MSNMessage object
2233 """
2234 pass
2235
2236 # api calls
2237
2238 def inviteUser(self, userHandle):
2239 """
2240 used to invite a user to the current switchboard server.
2241
2242 @param userHandle: the user handle (passport) of the desired user.
2243
2244 @return: A Deferred, the callback for which will be called
2245 when the server notifies us that the user has indeed
2246 been invited. The callback argument will be a tuple
2247 with 1 element, the sessionID given to the invited user.
2248 I'm not sure if this is useful or not.
2249 """
2250
2251 id, d = self._createIDMapping()
2252 self.sendLine("CAL %s %s" % (id, userHandle))
2253 return d
2254
2255 def sendMessage(self, message):
2256 """
2257 used to send a message.
2258
2259 @param message: the corresponding MSNMessage object.
2260
2261 @return: Depending on the value of message.ack.
2262 If set to MSNMessage.MESSAGE_ACK or
2263 MSNMessage.MESSAGE_NACK a Deferred will be returned,
2264 the callback for which will be fired when an ACK or
2265 NACK is received - the callback argument will be
2266 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
2267 the return value is None.
2268 """
2269
2270 if message.ack not in ('A','N','D'): id, d = self._nextTransactionID(), None
2271 else: id, d = self._createIDMapping()
2272 if message.length == 0: message.length = message._calcMessageLen()
2273 self.sendLine("MSG %s %s %s" % (id, message.ack, message.length))
2274 # apparently order matters with at least MIME-Version and Content-Type
2275 self.sendLine('MIME-Version: %s' % message.getHeader('MIME-Version'))
2276 self.sendLine('Content-Type: %s' % message.getHeader('Content-Type'))
2277 # send the rest of the headers
2278 for header in [h for h in message.headers.items() if h[0].lower() not in ('mime-version','content-type')]:
2279 self.sendLine("%s: %s" % (header[0], header[1]))
2280 self.transport.write(CR+LF)
2281 self.transport.write(message.message)
2282 if MESSAGEDEBUG: log.msg(message.message)
2283 return d
2284
2285 def sendAvatarRequest(self, msnContact):
2286 """
2287 used to request an avatar from a user in this switchboard
2288 session.
2289
2290 @param msnContact: the msnContact object to request an avatar for
2291
2292 @return: A Deferred, the callback for which will be called
2293 when the avatar transfer succeeds.
2294 The callback argument will be a tuple with one element,
2295 the PNG avatar data.
2296 """
2297 if not msnContact.msnobj: return
2298 d = Deferred()
2299 def bufferClosed(data):
2300 d.callback((data,))
2301 buffer = StringBuffer(bufferClosed)
2302 slpLink = SLPLink_AvatarReceive(remoteUser=msnContact.userHandle, switchboard=self, consumer=buffer, context=msnContact.msnobj.text)
2303 slpLink.avatarDataBuffer = buffer
2304 self.slpLinks[slpLink.sessionID] = slpLink
2305 return d
2306
2307 def sendFile(self, msnContact, filename, filesize):
2308 """
2309 used to send a file to a contact.
2310
2311 @param msnContact: the MSNContact object to send a file to.
2312 @param filename: the name of the file to send.
2313 @param filesize: the size of the file to send.
2314
2315 @return: (fileSend, d) A FileSend object and a Deferred.
2316 The Deferred will pass one argument in a tuple,
2317 whether or not the transfer is accepted. If you
2318 receive a True, then you can call write() on the
2319 fileSend object to send your file. Call close()
2320 when the file is done.
2321 NOTE: You MUST write() exactly as much as you
2322 declare in filesize.
2323 """
2324 if not msnContact.userHandle: return
2325 # FIXME, check msnContact.caps to see if we should use old-style
2326 fileSend = SLPLink_FileSend(remoteUser=msnContact.userHandle, switchboard=self, filename=filename, filesize=filesize)
2327 self.slpLinks[fileSend.sessionID] = fileSend
2328 return fileSend, fileSend.acceptDeferred
2329
2330 def sendTypingNotification(self):
2331 """
2332 Used to send a typing notification. Upon receiving this
2333 message the official client will display a 'user is typing'
2334 message to all other users in the chat session for 10 seconds.
2335 You should send one of these every 5 seconds as long as the
2336 user is typing.
2337 """
2338 m = MSNMessage()
2339 m.ack = m.MESSAGE_ACK_NONE
2340 m.setHeader('Content-Type', 'text/x-msmsgscontrol')
2341 m.setHeader('TypingUser', self.userHandle)
2342 m.message = "\r\n"
2343 self.sendMessage(m)
2344
2345 def sendFileInvitation(self, fileName, fileSize):
2346 """
2347 send an notification that we want to send a file.
2348
2349 @param fileName: the file name
2350 @param fileSize: the file size
2351
2352 @return: A Deferred, the callback of which will be fired
2353 when the user responds to this invitation with an
2354 appropriate message. The callback argument will be
2355 a tuple with 3 elements, the first being 1 or 0
2356 depending on whether they accepted the transfer
2357 (1=yes, 0=no), the second being an invitation cookie
2358 to identify your follow-up responses and the third being
2359 the message 'info' which is a dict of information they
2360 sent in their reply (this doesn't really need to be used).
2361 If you wish to proceed with the transfer see the
2362 sendTransferInfo method.
2363 """
2364 cookie = self._newInvitationCookie()
2365 d = Deferred()
2366 m = MSNMessage()
2367 m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2368 m.message += 'Application-Name: File Transfer\r\n'
2369 m.message += 'Application-GUID: %s\r\n' % MSN_MSNFTP_GUID
2370 m.message += 'Invitation-Command: INVITE\r\n'
2371 m.message += 'Invitation-Cookie: %s\r\n' % str(cookie)
2372 m.message += 'Application-File: %s\r\n' % fileName
2373 m.message += 'Application-FileSize: %s\r\n\r\n' % str(fileSize)
2374 m.ack = m.MESSAGE_ACK_NONE
2375 self.sendMessage(m)
2376 self.cookies['iCookies'][cookie] = (d, m)
2377 return d
2378
2379 def sendTransferInfo(self, accept, iCookie, authCookie, ip, port):
2380 """
2381 send information relating to a file transfer session.
2382
2383 @param accept: whether or not to go ahead with the transfer
2384 (1=yes, 0=no)
2385 @param iCookie: the invitation cookie of previous replies
2386 relating to this transfer
2387 @param authCookie: the authentication cookie obtained from
2388 an FileSend instance
2389 @param ip: your ip
2390 @param port: the port on which an FileSend protocol is listening.
2391 """
2392 m = MSNMessage()
2393 m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2394 m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
2395 m.message += 'Invitation-Cookie: %s\r\n' % iCookie
2396 m.message += 'IP-Address: %s\r\n' % ip
2397 m.message += 'Port: %s\r\n' % port
2398 m.message += 'AuthCookie: %s\r\n' % authCookie
2399 m.message += '\r\n'
2400 m.ack = m.MESSAGE_NACK
2401 self.sendMessage(m)
2402
2403
2404 class FileReceive:
2405 def __init__(self, filename, filesize, userHandle):
2406 self.consumer = None
2407 self.finished = False
2408 self.error = False
2409 self.buffer = []
2410 self.filename, self.filesize, self.userHandle = filename, filesize, userHandle
2411
2412 def reject(self):
2413 raise NotImplementedError
2414
2415 def accept(self, consumer):
2416 if self.consumer: raise "AlreadyAccepted"
2417 self.consumer = consumer
2418 for data in self.buffer:
2419 self.consumer.write(data)
2420 self.buffer = None
2421 if self.finished:
2422 self.consumer.close()
2423 if self.error:
2424 self.consumer.error()
2425
2426 def write(self, data):
2427 if self.error or self.finished:
2428 raise IOError, "Attempt to write in an invalid state"
2429 if self.consumer:
2430 self.consumer.write(data)
2431 else:
2432 self.buffer.append(data)
2433
2434 def close(self):
2435 self.finished = True
2436 if self.consumer:
2437 self.consumer.close()
2438
2439 class FileContext:
2440 """ Represents the Context field for P2P file transfers """
2441 def __init__(self, data=""):
2442 if data:
2443 self.parse(data)
2444 else:
2445 self.filename = ""
2446 self.filesize = 0
2447
2448 def pack(self):
2449 if MSNP2PDEBUG: print "FileContext packing:", self.filename, self.filesize
2450 data = struct.pack("<LLQL", 574, 0x03, self.filesize, 0x01)
2451 data += self.filename.encode("utf-16")
2452 data = data.ljust(570, '\0')
2453 data += struct.pack("<L", 0xFFFFFFFF)
2454 return data
2455
2456 def parse(self, packet):
2457 self.filesize = struct.unpack("<Q", packet[8:16])[0]
2458 chunk = packet[20:540]
2459 chunk = chunk[:chunk.find('\x00\x00')]
2460 self.filename = unicode(chunk.decode("utf-16"))
2461 if MSNP2PDEBUG: print "FileContext parsed:", self.filesize, self.filename
2462
2463
2464 class BinaryFields:
2465 """ Utility class for the binary header & footer in p2p messages """
2466 ACK = 0x02
2467 WAIT = 0x04
2468 ERR = 0x08
2469 DATA = 0x20
2470 BYEGOT = 0x40
2471 BYESENT = 0x80
2472 DATAFT = 0x1000030
2473
2474 def __init__(self, fields=None, packet=None):
2475 if fields:
2476 self.fields = fields
2477 else:
2478 self.fields = [0] * 10
2479 if packet:
2480 self.unpackFields(packet)
2481
2482 def __getitem__(self, key):
2483 return self.fields[key]
2484
2485 def __setitem__(self, key, value):
2486 self.fields[key] = value
2487
2488 def unpackFields(self, packet):
2489 self.fields = struct.unpack("<LLQQLLLLQ", packet[0:48])
2490 self.fields += struct.unpack(">L", packet[len(packet)-4:])
2491 if MSNP2PDEBUG:
2492 print "Unpacked fields:",
2493 for i in self.fields:
2494 print hex(i),
2495 print
2496
2497 def packHeaders(self):
2498 f = tuple(self.fields)
2499 if MSNP2PDEBUG:
2500 print "Packed fields:",
2501 for i in self.fields:
2502 print hex(i),
2503 print
2504 return struct.pack("<LLQQLLLLQ", f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8])
2505
2506 def packFooter(self):
2507 return struct.pack(">L", self.fields[9])
2508
2509
2510 class MSNSLPMessage:
2511 """ Representation of a single MSNSLP message """
2512 def __init__(self, packet=None):
2513 self.method = ""
2514 self.status = ""
2515 self.to = ""
2516 self.fro = ""
2517 self.cseq = 0
2518 self.sessionGuid = ""
2519 self.sessionID = None
2520 self.euf_guid = ""
2521 self.data = "\r\n" + chr(0)
2522 if packet:
2523 self.parse(packet)
2524
2525 def create(self, method=None, status=None, to=None, fro=None, cseq=0, sessionGuid=None, data=None):
2526 self.method = method
2527 self.status = status
2528 self.to = to
2529 self.fro = fro
2530 self.cseq = cseq
2531 self.sessionGuid = sessionGuid
2532 if data: self.data = data
2533
2534 def setData(self, sessionID=None, appID=None, guid=None, context=None):
2535 s = []
2536 if guid: s.append("EUF-GUID: %s\r\n" % guid)
2537 if sessionID: s.append("SessionID: %s\r\n" % sessionID)
2538 if appID: s.append("AppID: %s\r\n" % appID)
2539 if context: s.append("Context: %s\r\n\r\n" % context)
2540 s.append(chr(0))
2541
2542 self.data = "".join(s)
2543
2544 def parse(self, s):
2545 s = s[48:len(s)-4:]
2546 if s.find("MSNSLP/1.0") < 0: return
2547
2548 lines = s.split("\r\n")
2549
2550 # Get the MSNSLP method or status
2551 msnslp = lines[0].split(" ")
2552 if MSNP2PDEBUG: print "Parsing MSNSLPMessage", s, len(s)
2553 if msnslp[0] in ("INVITE", "BYE"):
2554 self.method = msnslp[0].strip()
2555 else:
2556 self.status = msnslp[1].strip()
2557
2558 lines.remove(lines[0])
2559
2560 for line in lines:
2561 line = line.split(":")
2562 if len(line) > 1:
2563 if len(line) > 2 and line[0] == "To":
2564 self.to = line[2][:line[2].find('>')]
2565 elif len(line) > 2 and line[0] == "From":
2566 self.fro = line[2][:line[2].find('>')]
2567 elif line[0] == "Call-ID":
2568 self.sessionGuid = line[1].strip()
2569 elif line[0] == "CSeq":
2570 self.cseq = int(line[1].strip())
2571 elif line[0] == "SessionID":
2572 self.sessionID = int(line[1].strip())
2573 elif line[0] == "EUF-GUID":
2574 self.euf_guid = line[1].strip()
2575 elif line[0] == "Context":
2576 self.context = base64.decodestring(line[1])
2577
2578 def __str__(self):
2579 s = []
2580 if self.method:
2581 s.append("%s MSNMSGR:%s MSNSLP/1.0\r\n" % (self.method, self.to))
2582 else:
2583 s.append("MSNSLP/1.0 %s\r\n" % self.status)
2584 s.append("To: <msnmsgr:%s>\r\n" % self.to)
2585 s.append("From: <msnmsgr:%s>\r\n" % self.fro)
2586 s.append("Via: MSNSLP/1.0/TLP ;branch=%s\r\n" % random_guid())
2587 s.append("CSeq: %s \r\n" % str(self.cseq))
2588 s.append("Call-ID: %s\r\n" % self.sessionGuid)
2589 s.append("Max-Forwards: 0\r\n")
2590 if self.method == "BYE":
2591 s.append("Content-Type: application/x-msnmsgr-sessionclosebody\r\n")
2592 else:
2593 s.append("Content-Type: application/x-msnmsgr-sessionreqbody\r\n")
2594 s.append("Content-Length: %s\r\n\r\n" % len(self.data))
2595 s.append(self.data)
2596 return "".join(s)
2597
2598
2599 class SeqID:
2600 """ Utility for handling the weird sequence IDs in p2p messages """
2601 def __init__(self, baseID=None):
2602 if baseID:
2603 self.seqID = baseID
2604 else:
2605 self.seqID = random.randint(1000, sys.maxint)
2606 self.pos = -1
2607
2608 def get(self):
2609 if self.pos == 0:
2610 return self.seqID
2611 else:
2612 return self.seqID + self.pos - 3
2613
2614 def next(self):
2615 self.pos += 1
2616 if self.pos == 3:
2617 self.pos += 1
2618 return self.get()
2619
2620 class StringBuffer(StringIO.StringIO):
2621 def __init__(self, notifyFunc=None):
2622 self.notifyFunc = notifyFunc
2623 StringIO.StringIO.__init__(self)
2624
2625 def close(self):
2626 if self.notifyFunc:
2627 self.notifyFunc(self.getvalue())
2628 self.notifyFunc = None
2629 StringIO.StringIO.close(self)
2630
2631
2632 class SLPLink:
2633 def __init__(self, remoteUser, switchboard, sessionID, sessionGuid):
2634 if not sessionID:
2635 sessionID = random.randint(1000, sys.maxint)
2636 if not sessionGuid:
2637 sessionGuid = random_guid()
2638 self.remoteUser = remoteUser
2639 self.switchboard = switchboard
2640 self.sessionID = sessionID
2641 self.sessionGuid = sessionGuid
2642 self.seqID = SeqID()
2643
2644 def killLink(self):
2645 def kill():
2646 del self.switchboard.slpLinks[self.sessionID]
2647 self.switchboard = None
2648 # This is so that handleP2PMessage can still use the SLPLink
2649 # one last time, for ACKing BYEs and 601s.
2650 reactor.callLater(0, kill)
2651
2652 def setError(self, text):
2653 if MSNP2PDEBUG:
2654 print "ERROR in avatar transfer: ", self, text, "in state:", self.state
2655
2656 def warn(self, text):
2657 if MSNP2PDEBUG:
2658 print "Warning in avatar transfer: ", self, text, "in state:", self.state
2659
2660 def sendP2PACK(self, ackHeaders):
2661 binaryFields = BinaryFields()
2662 binaryFields[1] = self.seqID.next()
2663 binaryFields[3] = ackHeaders[3]
2664 binaryFields[5] = BinaryFields.ACK
2665 binaryFields[6] = ackHeaders[1]
2666 binaryFields[7] = ackHeaders[6]
2667 binaryFields[8] = ackHeaders[3]
2668 self.sendP2PMessage(binaryFields, "")
2669
2670 def sendMSNSLPCommand(self, command, guid, context):
2671 msg = MSNSLPMessage()
2672 msg.create(method=command, to=self.remoteUser, fro=self.switchboard.userHandle, cseq=0, sessionGuid=self.sessionGuid)
2673 msg.setData(sessionID=self.sessionID, appID="1", guid=guid, context=b64enc(context + chr(0)))
2674 self.sendMSNSLPMessage(msg)
2675
2676 def sendMSNSLPResponse(self, response):
2677 msg = MSNSLPMessage()
2678 msg.create(status=response, to=self.remoteUser, fro=self.switchboard.userHandle, cseq=1, sessionGuid=self.sessionGuid)
2679 msg.setData(sessionID=self.sessionID)
2680 self.sendMSNSLPMessage(msg)
2681
2682 def sendMSNSLPMessage(self, msnSLPMessage):
2683 msgStr = str(msnSLPMessage)
2684 binaryFields = BinaryFields()
2685 binaryFields[1] = self.seqID.next()
2686 binaryFields[3] = len(msgStr)
2687 binaryFields[4] = binaryFields[3]
2688 binaryFields[6] = random.randint(1000, sys.maxint)
2689 self.sendP2PMessage(binaryFields, msgStr)
2690
2691 def sendP2PMessage(self, binaryFields, msgStr):
2692 packet = binaryFields.packHeaders() + msgStr + binaryFields.packFooter()
2693
2694 message = MSNMessage(message=packet)
2695 message.setHeader("Content-Type", "application/x-msnmsgrp2p")
2696 message.setHeader("P2P-Dest", self.remoteUser)
2697 message.ack = MSNMessage.MESSAGE_ACK_FAT
2698 self.switchboard.sendMessage(message)
2699
2700 def handleSLPMessage(self):
2701 raise NotImplementedError
2702
2703
2704
2705
2706
2707 class SLPLink_Send(SLPLink):
2708 def __init__(self, remoteUser, switchboard, filesize, sessionID=None, sessionGuid=None):
2709 SLPLink.__init__(self, remoteUser, switchboard, sessionID, sessionGuid)
2710 self.handlePacket = None
2711 self.offset = 0
2712 self.filesize = filesize
2713 self.data = ""
2714
2715 def send_dataprep(self):
2716 binaryFields = BinaryFields()
2717 binaryFields[0] = self.sessionID
2718 binaryFields[1] = self.seqID.next()
2719 binaryFields[3] = 4
2720 binaryFields[4] = 4
2721 binaryFields[6] = random.randint(1000, sys.maxint)
2722 binaryFields[9] = 1
2723 self.sendP2PMessage(binaryFields, chr(0) * 4)
2724
2725 def write(self, data):
2726 i = 0
2727 length = len(data)
2728 while i < length:
2729 if i + 1202 < length:
2730 self._writeChunk(data[i:i+1202])
2731 i += 1202
2732 else:
2733 self.data += data[i:]
2734 if len(self.data) >= 1202:
2735 data = self.data
2736 self.data = ""
2737 self.write(data)
2738 return
2739
2740 def _writeChunk(self, chunk):
2741 binaryFields = BinaryFields()
2742 binaryFields[0] = self.sessionID
2743 binaryFields[1] = self.seqID.get()
2744 binaryFields[2] = self.offset
2745 binaryFields[3] = self.filesize
2746 binaryFields[4] = len(chunk)
2747 binaryFields[5] = BinaryFields.DATA
2748 binaryFields[6] = random.randint(1000, sys.maxint)
2749 binaryFields[9] = 1
2750 self.offset += len(chunk)
2751 self.sendP2PMessage(binaryFields, chunk)
2752
2753 def close(self):
2754 if self.data:
2755 self._writeChunk(self.data)
2756 self.killLink()
2757
2758 def error(self):
2759 pass
2760 # FIXME, should send 601 or something
2761
2762 class SLPLink_FileSend(SLPLink_Send):
2763 def __init__(self, remoteUser, switchboard, filename, filesize):
2764 SLPLink_Send.__init__(self, remoteUser=remoteUser, switchboard=switchboard, filesize=filesize)
2765 # Send invite & wait for 200OK before sending dataprep
2766 context = FileContext()
2767 context.filename = filename
2768 context.filesize = filesize
2769 self.sendMSNSLPCommand("INVITE", MSN_MSNFTP_GUID, context.pack())
2770 self.acceptDeferred = Deferred()
2771
2772 def handleSLPMessage(self, slpMessage):
2773 if slpMessage.status == "200":
2774 self.send_dataprep()
2775 self.acceptDeferred.callback((True,))
2776 else:
2777 if slpMessage.status == "603":
2778 self.acceptDeferred.callback((False,))
2779 # SLPLink is over due to decline, error or BYE
2780 self.killLink()
2781
2782 def close(self):
2783 SLPLink_Send.close(self)
2784 # FIXME, check whether we should wait for a BYE or send one
2785 self.sendMSNSLPCommand("BYE", MSN_AVATAR_GUID, "\0")
2786
2787
2788 class SLPLink_AvatarSend(SLPLink_Send):
2789 def __init__(self, remoteUser, switchboard, filesize, sessionID=None, sessionGuid=None):
2790 SLPLink_Send.__init__(self, remoteUser=remoteUser, switchboard=switchboard, filesize=filesize, sessionID=sessionID, sessionGuid=sessionGuid)
2791 self.sendMSNSLPResponse("200 OK")
2792 self.send_dataprep()
2793
2794 def close(self):
2795 SLPLink_Send.close(self)
2796 # Keep the link open to wait for a BYE
2797
2798 class SLPLink_Receive(SLPLink):
2799 def __init__(self, remoteUser, switchboard, consumer, context=None, sessionID=None, sessionGuid=None):
2800 SLPLink.__init__(self, remoteUser, switchboard, sessionID, sessionGuid)
2801 self.handlePacket = None
2802 self.consumer = consumer
2803 self.pos = 0
2804
2805 def wait_dataprep(self, packet):
2806 binaryFields = BinaryFields()
2807 binaryFields.unpackFields(packet)
2808
2809 if binaryFields[0] != self.sessionID:
2810 self.warn("field0," + str(binaryFields[0]) + "," + str(self.sessionID))
2811 return
2812 if binaryFields[3] != 4:
2813 self.setError("field3," + str(binaryFields[3]))
2814 return
2815 if binaryFields[4] != 4:
2816 self.setError("field4," + str(binaryFields[4]))
2817 return
2818 if binaryFields[9] != 1:
2819 self.warn("field9," + str(binaryFields[9]))
2820 # return
2821
2822 self.sendP2PACK(binaryFields)
2823 self.handlePacket = self.wait_data
2824
2825 def wait_data(self, packet):
2826 binaryFields = BinaryFields()
2827 binaryFields.unpackFields(packet)
2828 if binaryFields[0] != self.sessionID:
2829 self.warn("field0," + str(binaryFields[0]) + "," + str(self.sessionID))
2830 return
2831 if binaryFields[5] != BinaryFields.DATA:
2832 self.setError("field5," + str(binaryFields[5]))
2833 return
2834 if binaryFields[9] != 1:
2835 self.warn("field9," + str(binaryFields[9]))
2836 # return
2837 offset = binaryFields[2]
2838 total = binaryFields[3]
2839 length = binaryFields[4]
2840
2841 data = packet[48:-4]
2842 if offset != self.pos:
2843 self.setError("Received packet out of order")
2844 self.consumer.error()
2845 return
2846 if len(data) != length:
2847 self.setError("Received bad length of slp")
2848 self.consumer.error()
2849 return
2850
2851 self.pos += length
2852
2853 self.consumer.write(data)
2854
2855 if self.pos == total:
2856 self.sendP2PACK(binaryFields)
2857 self.consumer.close()
2858 self.handlePacket = None
2859 self.doFinished()
2860
2861 def doFinished(self):
2862 raise NotImplementedError
2863
2864
2865 class SLPLink_FileReceive(SLPLink_Receive, FileReceive):
2866 def __init__(self, remoteUser, switchboard, filename, filesize, sessionID, sessionGuid):
2867 SLPLink_Receive.__init__(self, remoteUser=remoteUser, switchboard=switchboard, consumer=self, sessionID=sessionID, sessionGuid=sessionGuid)
2868 FileReceive.__init__(self, filename, filesize, remoteUser)
2869
2870 def reject(self):
2871 # Send a 603 decline
2872 self.sendMSNSLPResponse("603 Decline")
2873 self.killLink()
2874
2875 def accept(self, consumer):
2876 FileReceive.accept(self, consumer)
2877 self.sendMSNSLPResponse("200 OK")
2878 self.handlePacket = self.wait_dataprep
2879
2880 def handleSLPMessage(self, slpMessage):
2881 # FIXME, do some error handling if it was an error
2882 self.killLink() # It's either a BYE or an error
2883
2884 def doFinished(self):
2885 pass # Link is kept around waiting for a BYE
2886
2887 class SLPLink_AvatarReceive(SLPLink_Receive):
2888 def __init__(self, remoteUser, switchboard, consumer, context):
2889 SLPLink_Receive.__init__(self, remoteUser=remoteUser, switchboard=switchboard, consumer=consumer, context=context)
2890 self.sendMSNSLPCommand("INVITE", MSN_AVATAR_GUID, context)
2891
2892 def handleSLPMessage(self, slpMessage):
2893 if slpMessage.status == "200":
2894 self.handlePacket = self.wait_dataprep
2895 else:
2896 # SLPLink is over due to error or BYE
2897 self.killLink()
2898
2899 def doFinished(self):
2900 self.sendMSNSLPCommand("BYE", MSN_AVATAR_GUID, "\0")
2901
2902 # mapping of error codes to error messages
2903 errorCodes = {
2904
2905 200 : "Syntax error",
2906 201 : "Invalid parameter",
2907 205 : "Invalid user",
2908 206 : "Domain name missing",
2909 207 : "Already logged in",
2910 208 : "Invalid username",
2911 209 : "Invalid screen name",
2912 210 : "User list full",
2913 215 : "User already there",
2914 216 : "User already on list",
2915 217 : "User not online",
2916 218 : "Already in mode",
2917 219 : "User is in the opposite list",
2918 223 : "Too many groups",
2919 224 : "Invalid group",
2920 225 : "User not in group",
2921 229 : "Group name too long",
2922 230 : "Cannot remove group 0",
2923 231 : "Invalid group",
2924 280 : "Switchboard failed",
2925 281 : "Transfer to switchboard failed",
2926
2927 300 : "Required field missing",
2928 301 : "Too many FND responses",
2929 302 : "Not logged in",
2930
2931 402 : "Error accessing contact list",
2932 403 : "Error accessing contact list",
2933
2934 500 : "Internal server error",
2935 501 : "Database server error",
2936 502 : "Command disabled",
2937 510 : "File operation failed",
2938 520 : "Memory allocation failed",
2939 540 : "Wrong CHL value sent to server",
2940
2941 600 : "Server is busy",
2942 601 : "Server is unavaliable",
2943 602 : "Peer nameserver is down",
2944 603 : "Database connection failed",
2945 604 : "Server is going down",
2946 605 : "Server unavailable",
2947
2948 707 : "Could not create connection",
2949 710 : "Invalid CVR parameters",
2950 711 : "Write is blocking",
2951 712 : "Session is overloaded",
2952 713 : "Too many active users",
2953 714 : "Too many sessions",
2954 715 : "Not expected",
2955 717 : "Bad friend file",
2956 731 : "Not expected",
2957
2958 800 : "Requests too rapid",
2959
2960 910 : "Server too busy",
2961 911 : "Authentication failed",
2962 912 : "Server too busy",
2963 913 : "Not allowed when offline",
2964 914 : "Server too busy",
2965 915 : "Server too busy",
2966 916 : "Server too busy",
2967 917 : "Server too busy",
2968 918 : "Server too busy",
2969 919 : "Server too busy",
2970 920 : "Not accepting new users",
2971 921 : "Server too busy",
2972 922 : "Server too busy",
2973 923 : "No parent consent",
2974 924 : "Passport account not yet verified"
2975
2976 }
2977
2978 # mapping of status codes to readable status format
2979 statusCodes = {
2980
2981 STATUS_ONLINE : "Online",
2982 STATUS_OFFLINE : "Offline",
2983 STATUS_HIDDEN : "Appear Offline",
2984 STATUS_IDLE : "Idle",
2985 STATUS_AWAY : "Away",
2986 STATUS_BUSY : "Busy",
2987 STATUS_BRB : "Be Right Back",
2988 STATUS_PHONE : "On the Phone",
2989 STATUS_LUNCH : "Out to Lunch"
2990
2991 }
2992
2993 # mapping of list ids to list codes
2994 listIDToCode = {
2995
2996 FORWARD_LIST : 'fl',
2997 BLOCK_LIST : 'bl',
2998 ALLOW_LIST : 'al',
2999 REVERSE_LIST : 'rl',
3000 PENDING_LIST : 'pl'
3001
3002 }
3003
3004 # mapping of list codes to list ids
3005 listCodeToID = {}
3006 for id,code in listIDToCode.items():
3007 listCodeToID[code] = id
3008
3009 del id, code