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