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