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