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