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