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