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