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