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