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