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