]> code.delx.au - pymsnt/blob - src/tlib/msn/msn.py
Basic tests for switchboard functionality
[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 from msnp2p import random_guid
99 import msnp2p
100
101 # Twisted imports
102 from twisted.internet import reactor, task
103 from twisted.internet.defer import Deferred
104 from twisted.internet.protocol import ClientFactory
105 from twisted.internet.ssl import ClientContextFactory
106 from twisted.python import failure, log
107 from twisted.xish.domish import unescapeFromXml
108
109 # Compat stuff
110 from tlib import xmlw
111
112 # System imports
113 import types, operator, os, sys, base64, random
114 from urllib import quote, unquote
115
116 MSN_PROTOCOL_VERSION = "MSNP11 CVR0" # protocol version
117 MSN_PORT = 1863 # default dispatch server port
118 MSN_MAX_MESSAGE = 1664 # max message length
119 MSN_CVR_STR = "0x040c winnt 5.1 i386 MSNMSGR 7.0.0777 msmsgs"
120 MSN_AVATAR_GUID = "{A4268EEC-FEC5-49E5-95C3-F126696BDBF6}"
121 MSN_MSNFTP_GUID = "{5D3E02AB-6190-11D3-BBBB-00C04F795683}"
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 CR = "\r"
154 LF = "\n"
155 PINGSPEED = 50.0
156
157 LINEDEBUG = True
158 MESSAGEDEBUG = True
159
160 def getVal(inp):
161 return inp.split('=')[1]
162
163 def getVals(params):
164 userHandle = ""
165 screenName = ""
166 userGuid = ""
167 lists = -1
168 groups = []
169 for p in params:
170 if not p:
171 continue
172 elif p[0] == 'N':
173 userHandle = getVal(p)
174 elif p[0] == 'F':
175 screenName = unquote(getVal(p))
176 elif p[0] == 'C':
177 userGuid = getVal(p)
178 elif p.isdigit():
179 lists = int(p)
180 else: # Must be the groups
181 try:
182 groups = p.split(',')
183 except:
184 raise MSNProtocolError, "Unknown LST/ADC response" + str(params) # debug
185
186 return userHandle, screenName, userGuid, lists, groups
187
188 def checkParamLen(num, expected, cmd, error=None):
189 if error == None: error = "Invalid Number of Parameters for %s" % cmd
190 if num != expected: raise MSNProtocolError, error
191
192 def _parseHeader(h, v):
193 """
194 Split a certin number of known
195 header values with the format:
196 field1=val,field2=val,field3=val into
197 a dict mapping fields to values.
198 @param h: the header's key
199 @param v: the header's value as a string
200 """
201
202 if h in ('passporturls','authentication-info','www-authenticate'):
203 v = v.replace('Passport1.4','').lstrip()
204 fields = {}
205 for fieldPair in v.split(','):
206 try:
207 field,value = fieldPair.split('=',1)
208 fields[field.lower()] = value
209 except ValueError:
210 fields[field.lower()] = ''
211 return fields
212 else: return v
213
214 def _parsePrimitiveHost(host):
215 # Ho Ho Ho
216 h,p = host.replace('https://','').split('/',1)
217 p = '/' + p
218 return h,p
219
220 def _login(userHandle, passwd, nexusServer, cached=0, authData=''):
221 """
222 This function is used internally and should not ever be called
223 directly.
224 """
225 cb = Deferred()
226 def _cb(server, auth):
227 loginFac = ClientFactory()
228 loginFac.protocol = lambda : PassportLogin(cb, userHandle, passwd, server, auth)
229 reactor.connectSSL(_parsePrimitiveHost(server)[0], 443, loginFac, ClientContextFactory())
230
231 if cached:
232 _cb(nexusServer, authData)
233 else:
234 fac = ClientFactory()
235 d = Deferred()
236 d.addCallbacks(_cb, callbackArgs=(authData,))
237 d.addErrback(lambda f: cb.errback(f))
238 fac.protocol = lambda : PassportNexus(d, nexusServer)
239 reactor.connectSSL(_parsePrimitiveHost(nexusServer)[0], 443, fac, ClientContextFactory())
240 return cb
241
242
243 class PassportNexus(HTTPClient):
244
245 """
246 Used to obtain the URL of a valid passport
247 login HTTPS server.
248
249 This class is used internally and should
250 not be instantiated directly -- that is,
251 The passport logging in process is handled
252 transparantly by NotificationClient.
253 """
254
255 def __init__(self, deferred, host):
256 self.deferred = deferred
257 self.host, self.path = _parsePrimitiveHost(host)
258
259 def connectionMade(self):
260 HTTPClient.connectionMade(self)
261 self.sendCommand('GET', self.path)
262 self.sendHeader('Host', self.host)
263 self.endHeaders()
264 self.headers = {}
265
266 def handleHeader(self, header, value):
267 h = header.lower()
268 self.headers[h] = _parseHeader(h, value)
269
270 def handleEndHeaders(self):
271 if self.connected: self.transport.loseConnection()
272 if not self.headers.has_key('passporturls') or not self.headers['passporturls'].has_key('dalogin'):
273 self.deferred.errback(failure.Failure(failure.DefaultException("Invalid Nexus Reply")))
274 else:
275 self.deferred.callback('https://' + self.headers['passporturls']['dalogin'])
276
277 def handleResponse(self, r): pass
278
279 class PassportLogin(HTTPClient):
280 """
281 This class is used internally to obtain
282 a login ticket from a passport HTTPS
283 server -- it should not be used directly.
284 """
285
286 _finished = 0
287
288 def __init__(self, deferred, userHandle, passwd, host, authData):
289 self.deferred = deferred
290 self.userHandle = userHandle
291 self.passwd = passwd
292 self.authData = authData
293 self.host, self.path = _parsePrimitiveHost(host)
294
295 def connectionMade(self):
296 self.sendCommand('GET', self.path)
297 self.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
298 'sign-in=%s,pwd=%s,%s' % (quote(self.userHandle), self.passwd,self.authData))
299 self.sendHeader('Host', self.host)
300 self.endHeaders()
301 self.headers = {}
302
303 def handleHeader(self, header, value):
304 h = header.lower()
305 self.headers[h] = _parseHeader(h, value)
306
307 def handleEndHeaders(self):
308 if self._finished: return
309 self._finished = 1 # I think we need this because of HTTPClient
310 if self.connected: self.transport.loseConnection()
311 authHeader = 'authentication-info'
312 _interHeader = 'www-authenticate'
313 if self.headers.has_key(_interHeader): authHeader = _interHeader
314 try:
315 info = self.headers[authHeader]
316 status = info['da-status']
317 handler = getattr(self, 'login_%s' % (status,), None)
318 if handler:
319 handler(info)
320 else: raise Exception()
321 except Exception, e:
322 self.deferred.errback(failure.Failure(e))
323
324 def handleResponse(self, r): pass
325
326 def login_success(self, info):
327 ticket = info['from-pp']
328 ticket = ticket[1:len(ticket)-1]
329 self.deferred.callback((LOGIN_SUCCESS, ticket))
330
331 def login_failed(self, info):
332 self.deferred.callback((LOGIN_FAILURE, unquote(info['cbtxt'])))
333
334 def login_redir(self, info):
335 self.deferred.callback((LOGIN_REDIRECT, self.headers['location'], self.authData))
336
337 class MSNProtocolError(Exception):
338 """
339 This Exception is basically used for debugging
340 purposes, as the official MSN server should never
341 send anything _wrong_ and nobody in their right
342 mind would run their B{own} MSN server.
343 If it is raised by default command handlers
344 (handle_BLAH) the error will be logged.
345 """
346 pass
347
348 class MSNMessage:
349
350 """
351 I am the class used to represent an 'instant' message.
352
353 @ivar userHandle: The user handle (passport) of the sender
354 (this is only used when receiving a message)
355 @ivar screenName: The screen name of the sender (this is only used
356 when receiving a message)
357 @ivar message: The message
358 @ivar headers: The message headers
359 @type headers: dict
360 @ivar length: The message length (including headers and line endings)
361 @ivar ack: This variable is used to tell the server how to respond
362 once the message has been sent. If set to MESSAGE_ACK
363 (default) the server will respond with an ACK upon receiving
364 the message, if set to MESSAGE_NACK the server will respond
365 with a NACK upon failure to receive the message.
366 If set to MESSAGE_ACK_NONE the server will do nothing.
367 This is relevant for the return value of
368 SwitchboardClient.sendMessage (which will return
369 a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
370 and will fire when the respective ACK or NACK is received).
371 If set to MESSAGE_ACK_NONE sendMessage will return None.
372 """
373 MESSAGE_ACK = 'A'
374 MESSAGE_ACK_FAT = 'D'
375 MESSAGE_NACK = 'N'
376 MESSAGE_ACK_NONE = 'U'
377
378 ack = MESSAGE_ACK
379
380 def __init__(self, length=0, userHandle="", screenName="", message="", specialMessage=False):
381 self.userHandle = userHandle
382 self.screenName = screenName
383 self.specialMessage = specialMessage
384 self.message = message
385 self.headers = {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
386 self.length = length
387 self.readPos = 0
388
389 def _calcMessageLen(self):
390 """
391 used to calculte the number to send
392 as the message length when sending a message.
393 """
394 return reduce(operator.add, [len(x[0]) + len(x[1]) + 4 for x in self.headers.items()]) + len(self.message) + 2
395
396 def setHeader(self, header, value):
397 """ set the desired header """
398 self.headers[header] = value
399
400 def getHeader(self, header):
401 """
402 get the desired header value
403 @raise KeyError: if no such header exists.
404 """
405 return self.headers[header]
406
407 def hasHeader(self, header):
408 """ check to see if the desired header exists """
409 return self.headers.has_key(header)
410
411 def getMessage(self):
412 """ return the message - not including headers """
413 return self.message
414
415 def setMessage(self, message):
416 """ set the message text """
417 self.message = message
418
419 class MSNContact:
420
421 """
422 This class represents a contact (user).
423
424 @ivar userGuid: The contact's user guid (unique string)
425 @ivar userHandle: The contact's user handle (passport).
426 @ivar screenName: The contact's screen name.
427 @ivar groups: A list of all the group IDs which this
428 contact belongs to.
429 @ivar lists: An integer representing the sum of all lists
430 that this contact belongs to.
431 @ivar caps: int, The capabilities of this client
432 @ivar msnobj: msnp2p.MSNOBJ, The MSNObject representing the contact's avatar
433 @ivar status: The contact's status code.
434 @type status: str if contact's status is known, None otherwise.
435 @ivar personal: The contact's personal message .
436 @type personal: str if contact's personal message is known, None otherwise.
437
438 @ivar homePhone: The contact's home phone number.
439 @type homePhone: str if known, otherwise None.
440 @ivar workPhone: The contact's work phone number.
441 @type workPhone: str if known, otherwise None.
442 @ivar mobilePhone: The contact's mobile phone number.
443 @type mobilePhone: str if known, otherwise None.
444 @ivar hasPager: Whether or not this user has a mobile pager
445 @ivar hasBlog: Whether or not this user has a MSN Spaces blog
446 (true=yes, false=no)
447 """
448 MSNC1 = 0x10000000
449 MSNC2 = 0x20000000
450 MSNC3 = 0x30000000
451 MSNC4 = 0x40000000
452
453 def __init__(self, userGuid="", userHandle="", screenName="", lists=0, caps=0, msnobj=None, groups={}, status=None, personal=""):
454 self.userGuid = userGuid
455 self.userHandle = userHandle
456 self.screenName = screenName
457 self.lists = lists
458 self.caps = caps
459 self.msnobj = msnobj
460 self.msnobjGot = True
461 self.groups = [] # if applicable
462 self.status = status # current status
463 self.personal = personal
464
465 # phone details
466 self.homePhone = None
467 self.workPhone = None
468 self.mobilePhone = None
469 self.hasPager = None
470 self.hasBlog = None
471
472 def setPhone(self, phoneType, value):
473 """
474 set phone numbers/values for this specific user.
475 for phoneType check the *_PHONE constants and HAS_PAGER
476 """
477
478 t = phoneType.upper()
479 if t == HOME_PHONE: self.homePhone = value
480 elif t == WORK_PHONE: self.workPhone = value
481 elif t == MOBILE_PHONE: self.mobilePhone = value
482 elif t == HAS_PAGER: self.hasPager = value
483 elif t == HAS_BLOG: self.hasBlog = value
484 #else: raise ValueError, "Invalid Phone Type: " + t
485
486 def addToList(self, listType):
487 """
488 Update the lists attribute to
489 reflect being part of the
490 given list.
491 """
492 self.lists |= listType
493
494 def removeFromList(self, listType):
495 """
496 Update the lists attribute to
497 reflect being removed from the
498 given list.
499 """
500 self.lists ^= listType
501
502 class MSNContactList:
503 """
504 This class represents a basic MSN contact list.
505
506 @ivar contacts: All contacts on my various lists
507 @type contacts: dict (mapping user handles to MSNContact objects)
508 @ivar groups: a mapping of group ids to group names
509 (groups can only exist on the forward list)
510 @type groups: dict
511
512 B{Note}:
513 This is used only for storage and doesn't effect the
514 server's contact list.
515 """
516
517 def __init__(self):
518 self.contacts = {}
519 self.groups = {}
520 self.autoAdd = 0
521 self.privacy = 0
522 self.version = 0
523
524 def _getContactsFromList(self, listType):
525 """
526 Obtain all contacts which belong
527 to the given list type.
528 """
529 return dict([(uH,obj) for uH,obj in self.contacts.items() if obj.lists & listType])
530
531 def addContact(self, contact):
532 """
533 Add a contact
534 """
535 self.contacts[contact.userHandle] = contact
536
537 def remContact(self, userHandle):
538 """
539 Remove a contact
540 """
541 try:
542 del self.contacts[userHandle]
543 except KeyError: pass
544
545 def getContact(self, userHandle):
546 """
547 Obtain the MSNContact object
548 associated with the given
549 userHandle.
550 @return: the MSNContact object if
551 the user exists, or None.
552 """
553 try:
554 return self.contacts[userHandle]
555 except KeyError:
556 return None
557
558 def getBlockedContacts(self):
559 """
560 Obtain all the contacts on my block list
561 """
562 return self._getContactsFromList(BLOCK_LIST)
563
564 def getAuthorizedContacts(self):
565 """
566 Obtain all the contacts on my auth list.
567 (These are contacts which I have verified
568 can view my state changes).
569 """
570 return self._getContactsFromList(ALLOW_LIST)
571
572 def getReverseContacts(self):
573 """
574 Get all contacts on my reverse list.
575 (These are contacts which have added me
576 to their forward list).
577 """
578 return self._getContactsFromList(REVERSE_LIST)
579
580 def getContacts(self):
581 """
582 Get all contacts on my forward list.
583 (These are the contacts which I have added
584 to my list).
585 """
586 return self._getContactsFromList(FORWARD_LIST)
587
588 def setGroup(self, id, name):
589 """
590 Keep a mapping from the given id
591 to the given name.
592 """
593 self.groups[id] = name
594
595 def remGroup(self, id):
596 """
597 Removed the stored group
598 mapping for the given id.
599 """
600 try:
601 del self.groups[id]
602 except KeyError: pass
603 for c in self.contacts:
604 if id in c.groups: c.groups.remove(id)
605
606
607 class MSNEventBase(LineReceiver):
608 """
609 This class provides support for handling / dispatching events and is the
610 base class of the three main client protocols (DispatchClient,
611 NotificationClient, SwitchboardClient)
612 """
613
614 def __init__(self):
615 self.ids = {} # mapping of ids to Deferreds
616 self.currentID = 0
617 self.connected = 0
618 self.setLineMode()
619 self.currentMessage = None
620
621 def connectionLost(self, reason):
622 self.ids = {}
623 self.connected = 0
624
625 def connectionMade(self):
626 self.connected = 1
627
628 def _fireCallback(self, id, *args):
629 """
630 Fire the callback for the given id
631 if one exists and return 1, else return false
632 """
633 if self.ids.has_key(id):
634 self.ids[id][0].callback(args)
635 del self.ids[id]
636 return 1
637 return 0
638
639 def _nextTransactionID(self):
640 """ return a usable transaction ID """
641 self.currentID += 1
642 if self.currentID > 1000: self.currentID = 1
643 return self.currentID
644
645 def _createIDMapping(self, data=None):
646 """
647 return a unique transaction ID that is mapped internally to a
648 deferred .. also store arbitrary data if it is needed
649 """
650 id = self._nextTransactionID()
651 d = Deferred()
652 self.ids[id] = (d, data)
653 return (id, d)
654
655 def checkMessage(self, message):
656 """
657 process received messages to check for file invitations and
658 typing notifications and other control type messages
659 """
660 raise NotImplementedError
661
662 def sendLine(self, line):
663 if LINEDEBUG: log.msg(">> " + line)
664 LineReceiver.sendLine(self, line)
665
666 def lineReceived(self, line):
667 if LINEDEBUG: log.msg("<< " + line)
668 if self.currentMessage:
669 self.currentMessage.readPos += len(line+CR+LF)
670 try:
671 header, value = line.split(':')
672 self.currentMessage.setHeader(header, unquote(value).lstrip())
673 return
674 except ValueError:
675 #raise MSNProtocolError, "Invalid Message Header"
676 line = ""
677 if line == "" or self.currentMessage.specialMessage:
678 self.setRawMode()
679 if self.currentMessage.readPos == self.currentMessage.length: self.rawDataReceived("") # :(
680 return
681 try:
682 cmd, params = line.split(' ', 1)
683 except ValueError:
684 raise MSNProtocolError, "Invalid Message, %s" % repr(line)
685
686 if len(cmd) != 3: raise MSNProtocolError, "Invalid Command, %s" % repr(cmd)
687 if cmd.isdigit():
688 if self.ids.has_key(params.split(' ')[0]):
689 self.ids[id].errback(int(cmd))
690 del self.ids[id]
691 return
692 else: # we received an error which doesn't map to a sent command
693 self.gotError(int(cmd))
694 return
695
696 handler = getattr(self, "handle_%s" % cmd.upper(), None)
697 if handler:
698 try: handler(params.split(' '))
699 except MSNProtocolError, why: self.gotBadLine(line, why)
700 else:
701 self.handle_UNKNOWN(cmd, params.split(' '))
702
703 def rawDataReceived(self, data):
704 extra = ""
705 self.currentMessage.readPos += len(data)
706 diff = self.currentMessage.readPos - self.currentMessage.length
707 if diff > 0:
708 self.currentMessage.message += data[:-diff]
709 extra = data[-diff:]
710 elif diff == 0:
711 self.currentMessage.message += data
712 else:
713 self.currentMessage.message += data
714 return
715 del self.currentMessage.readPos
716 m = self.currentMessage
717 self.currentMessage = None
718 if MESSAGEDEBUG: log.msg(m.message)
719 if not self.checkMessage(m):
720 self.setLineMode(extra)
721 return
722 self.gotMessage(m)
723 self.setLineMode(extra)
724
725 ### protocol command handlers - no need to override these.
726
727 def handle_MSG(self, params):
728 checkParamLen(len(params), 3, 'MSG')
729 try:
730 messageLen = int(params[2])
731 except ValueError: raise MSNProtocolError, "Invalid Parameter for MSG length argument"
732 self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName=unquote(params[1]))
733
734 def handle_UNKNOWN(self, cmd, params):
735 """ implement me in subclasses if you want to handle unknown events """
736 log.msg("Received unknown command (%s), params: %s" % (cmd, params))
737
738 ### callbacks
739
740 def gotBadLine(self, line, why):
741 """ called when a handler notifies me that this line is broken """
742 log.msg('Error in line: %s (%s)' % (line, why))
743
744 def gotError(self, errorCode):
745 """
746 called when the server sends an error which is not in
747 response to a sent command (ie. it has no matching transaction ID)
748 """
749 log.msg('Error %s' % (errorCodes[errorCode]))
750
751 class DispatchClient(MSNEventBase):
752 """
753 This class provides support for clients connecting to the dispatch server
754 @ivar userHandle: your user handle (passport) needed before connecting.
755 """
756
757 # eventually this may become an attribute of the
758 # factory.
759 userHandle = ""
760
761 def connectionMade(self):
762 MSNEventBase.connectionMade(self)
763 self.sendLine('VER %s %s' % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
764
765 ### protocol command handlers ( there is no need to override these )
766
767 def handle_VER(self, params):
768 versions = params[1:]
769 if versions is None or ' '.join(versions) != MSN_PROTOCOL_VERSION:
770 self.transport.loseConnection()
771 raise MSNProtocolError, "Invalid version response"
772 id = self._nextTransactionID()
773 self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.userHandle))
774
775 def handle_CVR(self, params):
776 self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.userHandle))
777
778 def handle_XFR(self, params):
779 if len(params) < 4: raise MSNProtocolError, "Invalid number of parameters for XFR"
780 id, refType, addr = params[:3]
781 # was addr a host:port pair?
782 try:
783 host, port = addr.split(':')
784 except ValueError:
785 host = addr
786 port = MSN_PORT
787 if refType == "NS":
788 self.gotNotificationReferral(host, int(port))
789
790 ### callbacks
791
792 def gotNotificationReferral(self, host, port):
793 """
794 called when we get a referral to the notification server.
795
796 @param host: the notification server's hostname
797 @param port: the port to connect to
798 """
799 pass
800
801
802 class NotificationClient(MSNEventBase):
803 """
804 This class provides support for clients connecting
805 to the notification server.
806 """
807
808 factory = None # sssh pychecker
809
810 def __init__(self, currentID=0):
811 MSNEventBase.__init__(self)
812 self.currentID = currentID
813 self._state = ['DISCONNECTED', {}]
814 self.pingCounter = 0
815 self.pingCheckTask = None
816 self.msnobj = msnp2p.MSNOBJ()
817
818 def _setState(self, state):
819 self._state[0] = state
820
821 def _getState(self):
822 return self._state[0]
823
824 def _getStateData(self, key):
825 return self._state[1][key]
826
827 def _setStateData(self, key, value):
828 self._state[1][key] = value
829
830 def _remStateData(self, *args):
831 for key in args: del self._state[1][key]
832
833 def connectionMade(self):
834 MSNEventBase.connectionMade(self)
835 self._setState('CONNECTED')
836 self.sendLine("VER %s %s" % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
837
838 def connectionLost(self, reason):
839 self._setState('DISCONNECTED')
840 self._state[1] = {}
841 if self.pingCheckTask:
842 self.pingCheckTask.stop()
843 self.pingCheckTask = None
844 MSNEventBase.connectionLost(self, reason)
845
846 def _getEmailFields(self, message):
847 fields = message.getMessage().strip().split('\n')
848 values = {}
849 for i in fields:
850 a = i.split(':')
851 if len(a) != 2: continue
852 f, v = a
853 f = f.strip()
854 v = v.strip()
855 values[f] = v
856 return values
857
858 def _gotInitialEmailNotification(self, message):
859 values = self._getEmailFields(message)
860 try:
861 inboxunread = int(values["Inbox-Unread"])
862 foldersunread = int(values["Folders-Unread"])
863 except KeyError:
864 return
865 if foldersunread + inboxunread > 0: # For some reason MSN sends notifications about empty inboxes sometimes?
866 self.gotInitialEmailNotification(inboxunread, foldersunread)
867
868 def _gotEmailNotification(self, message):
869 values = self._getEmailFields(message)
870 try:
871 mailfrom = values["From"]
872 fromaddr = values["From-Addr"]
873 subject = values["Subject"]
874 junkbeginning = "=?\"us-ascii\"?Q?"
875 junkend = "?="
876 subject = subject.replace(junkbeginning, "").replace(junkend, "").replace("_", " ")
877 except KeyError:
878 # If any of the fields weren't found then it's not a big problem. We just ignore the message
879 return
880 self.gotRealtimeEmailNotification(mailfrom, fromaddr, subject)
881
882 def _gotMSNAlert(self, message):
883 notification = xmlw.parseText(message.message, beExtremelyLenient=True)
884 siteurl = notification.getAttribute("siteurl")
885 notid = notification.getAttribute("id")
886
887 msg = None
888 for e in notification.elements():
889 if e.name == "MSG":
890 msg = e
891 break
892 else: return
893
894 msgid = msg.getAttribute("id")
895
896 action = None
897 subscr = None
898 bodytext = None
899 for e in msg.elements():
900 if e.name == "ACTION":
901 action = e.getAttribute("url")
902 if e.name == "SUBSCR":
903 subscr = e.getAttribute("url")
904 if e.name == "BODY":
905 for e2 in e.elements():
906 if e2.name == "TEXT":
907 bodytext = e2.__str__()
908 if not (action and subscr and bodytext): return
909
910 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
911 subscrurl = "%s&notification_id=%s&message_id=%s&agent=messenger" % (subscr, notid, msgid)
912
913 self.gotMSNAlert(bodytext, actionurl, subscrurl)
914
915 def _gotUBX(self, message):
916 lm = message.message.lower()
917 p1 = lm.find("<psm>") + 5
918 p2 = lm.find("</psm>")
919 if p1 >= 0 and p2 >= 0:
920 personal = unescapeFromXml(message.message[p1:p2])
921 msnContact = self.factory.contacts.getContact(message.userHandle)
922 if not msnContact: return
923 msnContact.personal = personal
924 self.contactPersonalChanged(message.userHandle, personal)
925 else:
926 self.contactPersonalChanged(message.userHandle, '')
927
928 def checkMessage(self, message):
929 """ hook used for detecting specific notification messages """
930 cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
931 if 'text/x-msmsgsprofile' in cTypes:
932 self.gotProfile(message)
933 return 0
934 elif "text/x-msmsgsinitialemailnotification" in cTypes:
935 self._gotInitialEmailNotification(message)
936 return 0
937 elif "text/x-msmsgsemailnotification" in cTypes:
938 self._gotEmailNotification(message)
939 return 0
940 elif "NOTIFICATION" == message.userHandle and message.specialMessage == True:
941 self._gotMSNAlert(message)
942 return 0
943 elif "UBX" == message.screenName and message.specialMessage == True:
944 self._gotUBX(message)
945 return 0
946 return 1
947
948 ### protocol command handlers - no need to override these
949
950 def handle_VER(self, params):
951 versions = params[1:]
952 if versions is None or ' '.join(versions) != MSN_PROTOCOL_VERSION:
953 self.transport.loseConnection()
954 raise MSNProtocolError, "Invalid version response"
955 self.sendLine("CVR %s %s %s" % (self._nextTransactionID(), MSN_CVR_STR, self.factory.userHandle))
956
957 def handle_CVR(self, params):
958 self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.factory.userHandle))
959
960 def handle_USR(self, params):
961 if not (4 <= len(params) <= 6):
962 raise MSNProtocolError, "Invalid Number of Parameters for USR"
963
964 mechanism = params[1]
965 if mechanism == "OK":
966 self.loggedIn(params[2], int(params[3]))
967 elif params[2].upper() == "S":
968 # we need to obtain auth from a passport server
969 f = self.factory
970 d = _login(f.userHandle, f.password, f.passportServer, authData=params[3])
971 d.addCallback(self._passportLogin)
972 d.addErrback(self._passportError)
973
974 def _passportLogin(self, result):
975 if result[0] == LOGIN_REDIRECT:
976 d = _login(self.factory.userHandle, self.factory.password,
977 result[1], cached=1, authData=result[2])
978 d.addCallback(self._passportLogin)
979 d.addErrback(self._passportError)
980 elif result[0] == LOGIN_SUCCESS:
981 self.sendLine("USR %s TWN S %s" % (self._nextTransactionID(), result[1]))
982 elif result[0] == LOGIN_FAILURE:
983 self.loginFailure(result[1])
984
985 def _passportError(self, failure):
986 self.loginFailure("Exception while authenticating: %s" % failure)
987
988 def handle_CHG(self, params):
989 id = int(params[0])
990 if not self._fireCallback(id, params[1]):
991 if self.factory: self.factory.status = params[1]
992 self.statusChanged(params[1])
993
994 def handle_ILN(self, params):
995 #checkParamLen(len(params), 6, 'ILN')
996 msnContact = self.factory.contacts.getContact(params[2])
997 if not msnContact: return
998 msnContact.status = params[1]
999 msnContact.screenName = unquote(params[3])
1000 if len(params) > 4: msnContact.caps = int(params[4])
1001 if len(params) > 5:
1002 self.handleAvatarHelper(msnContact, params[5])
1003 else:
1004 self.handleAvatarGoneHelper(msnContact)
1005 self.gotContactStatus(params[1], params[2], unquote(params[3]))
1006
1007 def handleAvatarGoneHelper(self, msnContact):
1008 if msnContact.msnobj:
1009 msnContact.msnobj = None
1010 msnContact.msnobjGot = True
1011 self.contactAvatarChanged(msnContact.userHandle, "")
1012
1013 def handleAvatarHelper(self, msnContact, msnobjStr):
1014 msnobj = msnp2p.MSNOBJ(unquote(msnobjStr))
1015 if not msnContact.msnobj or msnobj.sha1d != msnContact.msnobj.sha1d:
1016 if msnp2p.MSNP2P_DEBUG: log.msg("Updated MSNOBJ received!" + msnobjStr)
1017 msnContact.msnobj = msnobj
1018 msnContact.msnobjGot = False
1019 self.contactAvatarChanged(msnContact.userHandle, msnContact.msnobj.sha1d)
1020
1021 def handle_CHL(self, params):
1022 checkParamLen(len(params), 2, 'CHL')
1023 response = msnp11chl.doChallenge(params[1])
1024 self.sendLine("QRY %s %s %s" % (self._nextTransactionID(), msnp11chl.MSNP11_PRODUCT_ID, len(response)))
1025 self.transport.write(response)
1026
1027 def handle_QRY(self, params):
1028 pass
1029
1030 def handle_NLN(self, params):
1031 if not self.factory: return
1032 msnContact = self.factory.contacts.getContact(params[1])
1033 if not msnContact: return
1034 msnContact.status = params[0]
1035 msnContact.screenName = unquote(params[2])
1036 if len(params) > 3: msnContact.caps = int(params[3])
1037 if len(params) > 4:
1038 self.handleAvatarHelper(msnContact, params[4])
1039 else:
1040 self.handleAvatarGoneHelper(msnContact)
1041 self.contactStatusChanged(params[0], params[1], unquote(params[2]))
1042
1043 def handle_FLN(self, params):
1044 checkParamLen(len(params), 1, 'FLN')
1045 msnContact = self.factory.contacts.getContact(params[0])
1046 if msnContact:
1047 msnContact.status = STATUS_OFFLINE
1048 self.contactOffline(params[0])
1049
1050 def handle_LST(self, params):
1051 if self._getState() != 'SYNC': return
1052
1053 userHandle, screenName, userGuid, lists, groups = getVals(params)
1054
1055 if not userHandle or lists < 1:
1056 raise MSNProtocolError, "Unknown LST " + str(params) # debug
1057 contact = MSNContact(userGuid, userHandle, screenName, lists)
1058 if contact.lists & FORWARD_LIST:
1059 contact.groups.extend(map(str, groups))
1060 self._getStateData('list').addContact(contact)
1061 self._setStateData('last_contact', contact)
1062 sofar = self._getStateData('lst_sofar') + 1
1063 if sofar == self._getStateData('lst_reply'):
1064 # this is the best place to determine that
1065 # a syn realy has finished - msn _may_ send
1066 # BPR information for the last contact
1067 # which is unfortunate because it means
1068 # that the real end of a syn is non-deterministic.
1069 # to handle this we'll keep 'last_contact' hanging
1070 # around in the state data and update it if we need
1071 # to later.
1072 self._setState('SESSION')
1073 contacts = self._getStateData('list')
1074 phone = self._getStateData('phone')
1075 id = self._getStateData('synid')
1076 self._remStateData('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
1077 self._fireCallback(id, contacts, phone)
1078 else:
1079 self._setStateData('lst_sofar',sofar)
1080
1081 def handle_BLP(self, params):
1082 # check to see if this is in response to a SYN
1083 if self._getState() == 'SYNC':
1084 self._getStateData('list').privacy = listCodeToID[params[0].lower()]
1085 else:
1086 id = int(params[0])
1087 self.factory.contacts.privacy = listCodeToID[params[1].lower()]
1088 self._fireCallback(id, params[1])
1089
1090 def handle_GTC(self, params):
1091 # check to see if this is in response to a SYN
1092 if self._getState() == 'SYNC':
1093 if params[0].lower() == "a": self._getStateData('list').autoAdd = 0
1094 elif params[0].lower() == "n": self._getStateData('list').autoAdd = 1
1095 else: raise MSNProtocolError, "Invalid Paramater for GTC" # debug
1096 else:
1097 id = int(params[0])
1098 if params[1].lower() == "a": self._fireCallback(id, 0)
1099 elif params[1].lower() == "n": self._fireCallback(id, 1)
1100 else: raise MSNProtocolError, "Invalid Paramater for GTC" # debug
1101
1102 def handle_SYN(self, params):
1103 id = int(params[0])
1104 self._setStateData('phone', []) # Always needs to be set
1105 if params[3] == 0: # No LST will be received. New account?
1106 self._setState('SESSION')
1107 self._fireCallback(id, None, None)
1108 else:
1109 contacts = MSNContactList()
1110 self._setStateData('list', contacts)
1111 self._setStateData('lst_reply', int(params[3]))
1112 self._setStateData('lsg_reply', int(params[4]))
1113 self._setStateData('lst_sofar', 0)
1114
1115 def handle_LSG(self, params):
1116 if self._getState() == 'SYNC':
1117 self._getStateData('list').groups[params[1]] = unquote(params[0])
1118
1119 def handle_PRP(self, params):
1120 if params[1] == "MFN":
1121 self._fireCallback(int(params[0]), unquote(params[2]))
1122 elif self._getState() == 'SYNC':
1123 self._getStateData('phone').append((params[0], unquote(params[1])))
1124 else:
1125 self._fireCallback(int(params[0]), int(params[1]), unquote(params[3]))
1126
1127 def handle_BPR(self, params):
1128 if not self.factory.contacts: raise MSNProtocolError, "handle_BPR called with no contact list" # debug
1129 numParams = len(params)
1130 if numParams == 2: # part of a syn
1131 self._getStateData('last_contact').setPhone(params[0], unquote(params[1]))
1132 elif numParams == 4:
1133 self.factory.contacts.version = int(params[0])
1134 userHandle, phoneType, number = params[1], params[2], unquote(params[3])
1135 self.factory.contacts.getContact(userHandle).setPhone(phoneType, number)
1136 self.gotPhoneNumber(userHandle, phoneType, number)
1137
1138
1139 def handle_ADG(self, params):
1140 checkParamLen(len(params), 5, 'ADG')
1141 id = int(params[0])
1142 if not self._fireCallback(id, int(params[1]), unquote(params[2]), int(params[3])):
1143 raise MSNProtocolError, "ADG response does not match up to a request" # debug
1144
1145 def handle_RMG(self, params):
1146 checkParamLen(len(params), 3, 'RMG')
1147 id = int(params[0])
1148 if not self._fireCallback(id, int(params[1]), int(params[2])):
1149 raise MSNProtocolError, "RMG response does not match up to a request" # debug
1150
1151 def handle_REG(self, params):
1152 checkParamLen(len(params), 5, 'REG')
1153 id = int(params[0])
1154 if not self._fireCallback(id, int(params[1]), int(params[2]), unquote(params[3])):
1155 raise MSNProtocolError, "REG response does not match up to a request" # debug
1156
1157 def handle_ADC(self, params):
1158 if not self.factory.contacts: raise MSNProtocolError, "handle_ADC called with no contact list"
1159 numParams = len(params)
1160 if numParams < 3 or params[1].upper() not in ('AL','BL','RL','FL', 'PL'):
1161 raise MSNProtocolError, "Invalid Paramaters for ADC" # debug
1162 id = int(params[0])
1163 listType = params[1].lower()
1164 userHandle, screenName, userGuid, ignored1, groups = getVals(params[2:])
1165
1166 if groups and listType.upper() != FORWARD_LIST:
1167 raise MSNProtocolError, "Only forward list can contain groups" # debug
1168
1169 if not self._fireCallback(id, listCodeToID[listType], userGuid, userHandle, screenName):
1170 c = self.factory.contacts.getContact(userHandle)
1171 if not c:
1172 c = MSNContact(userGuid=userGuid, userHandle=userHandle, screenName=screenName)
1173 self.factory.contacts.addContact(c)
1174 c.addToList(PENDING_LIST)
1175 self.userAddedMe(userGuid, userHandle, screenName)
1176
1177 def handle_REM(self, params):
1178 if not self.factory.contacts: raise MSNProtocolError, "handle_REM called with no contact list available!"
1179 numParams = len(params)
1180 if numParams < 3 or params[1].upper() not in ('AL','BL','FL','RL'):
1181 raise MSNProtocolError, "Invalid Paramaters for REM" # debug
1182 id = int(params[0])
1183 listType = params[1].lower()
1184 userHandle = params[2]
1185 groupID = None
1186 if numParams == 4:
1187 if params[1] != "FL": raise MSNProtocolError, "Only forward list can contain groups" # debug
1188 groupID = int(params[3])
1189 if not self._fireCallback(id, listCodeToID[listType], userHandle, groupID):
1190 if listType.upper() != "RL": return
1191 c = self.factory.contacts.getContact(userHandle)
1192 if not c: return
1193 c.removeFromList(REVERSE_LIST)
1194 if c.lists == 0: self.factory.contacts.remContact(c.userHandle)
1195 self.userRemovedMe(userHandle)
1196
1197 def handle_XFR(self, params):
1198 checkParamLen(len(params), 5, 'XFR')
1199 id = int(params[0])
1200 # check to see if they sent a host/port pair
1201 try:
1202 host, port = params[2].split(':')
1203 except ValueError:
1204 host = params[2]
1205 port = MSN_PORT
1206
1207 if not self._fireCallback(id, host, int(port), params[4]):
1208 raise MSNProtocolError, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
1209
1210 def handle_RNG(self, params):
1211 checkParamLen(len(params), 6, 'RNG')
1212 # check for host:port pair
1213 try:
1214 host, port = params[1].split(":")
1215 port = int(port)
1216 except ValueError:
1217 host = params[1]
1218 port = MSN_PORT
1219 self.gotSwitchboardInvitation(int(params[0]), host, port, params[3], params[4],
1220 unquote(params[5]))
1221
1222 def handle_NOT(self, params):
1223 checkParamLen(len(params), 1, 'NOT')
1224 try:
1225 messageLen = int(params[0])
1226 except ValueError: raise MSNProtocolError, "Invalid Parameter for NOT length argument"
1227 self.currentMessage = MSNMessage(length=messageLen, userHandle="NOTIFICATION", specialMessage=True)
1228 self.setRawMode()
1229
1230 def handle_UBX(self, params):
1231 checkParamLen(len(params), 2, 'UBX')
1232 try:
1233 messageLen = int(params[1])
1234 except ValueError: raise MSNProtocolError, "Invalid Parameter for UBX length argument"
1235 if messageLen > 0:
1236 self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName="UBX", specialMessage=True)
1237 self.setRawMode()
1238 else:
1239 self.contactPersonalChanged(params[0], '')
1240
1241 def handle_UUX(self, params):
1242 checkParamLen(len(params), 2, 'UUX')
1243 if params[1] != '0': return
1244 id = int(params[0])
1245 self._fireCallback(id)
1246
1247 def handle_OUT(self, params):
1248 checkParamLen(len(params), 1, 'OUT')
1249 if params[0] == "OTH": self.multipleLogin()
1250 elif params[0] == "SSD": self.serverGoingDown()
1251 else: raise MSNProtocolError, "Invalid Parameters received for OUT" # debug
1252
1253 def handle_QNG(self, params):
1254 self.pingCounter = 0 # They replied to a ping. We'll forgive them for any they may have missed, because they're alive again now
1255
1256 # callbacks
1257
1258 def pingChecker(self):
1259 if self.pingCounter > 5:
1260 # The server has ignored 5 pings, lets kill the connection
1261 self.transport.loseConnection()
1262 else:
1263 self.sendLine("PNG")
1264 self.pingCounter += 1
1265
1266 def pingCheckerStart(self, *args):
1267 self.pingCheckTask = task.LoopingCall(self.pingChecker)
1268 self.pingCheckTask.start(PINGSPEED)
1269
1270 def loggedIn(self, userHandle, verified):
1271 """
1272 Called when the client has logged in.
1273 The default behaviour of this method is to
1274 update the factory with our screenName and
1275 to sync the contact list (factory.contacts).
1276 When this is complete self.listSynchronized
1277 will be called.
1278
1279 @param userHandle: our userHandle
1280 @param verified: 1 if our passport has been (verified), 0 if not.
1281 (i'm not sure of the significace of this)
1282 @type verified: int
1283 """
1284 d = self.syncList()
1285 d.addCallback(self.listSynchronized)
1286 d.addCallback(self.pingCheckerStart)
1287
1288 def loginFailure(self, message):
1289 """
1290 Called when the client fails to login.
1291
1292 @param message: a message indicating the problem that was encountered
1293 """
1294 pass
1295
1296 def gotProfile(self, message):
1297 """
1298 Called after logging in when the server sends an initial
1299 message with MSN/passport specific profile information
1300 such as country, number of kids, etc.
1301 Check the message headers for the specific values.
1302
1303 @param message: The profile message
1304 """
1305 pass
1306
1307 def listSynchronized(self, *args):
1308 """
1309 Lists are now synchronized by default upon logging in, this
1310 method is called after the synchronization has finished
1311 and the factory now has the up-to-date contacts.
1312 """
1313 pass
1314
1315 def contactAvatarChanged(self, userHandle, hash):
1316 """
1317 Called when we receive the first, or a new <msnobj/> from a
1318 contact.
1319
1320 @param userHandle: contact who's msnobj has been changed
1321 @param hash: sha1 hash of their avatar
1322 """
1323
1324 def statusChanged(self, statusCode):
1325 """
1326 Called when our status changes and its not in response to a
1327 client command.
1328
1329 @param statusCode: 3-letter status code
1330 """
1331 pass
1332
1333 def gotContactStatus(self, statusCode, userHandle, screenName):
1334 """
1335 Called when we receive a list of statuses upon login.
1336
1337 @param statusCode: 3-letter status code
1338 @param userHandle: the contact's user handle (passport)
1339 @param screenName: the contact's screen name
1340 """
1341 pass
1342
1343 def contactStatusChanged(self, statusCode, userHandle, screenName):
1344 """
1345 Called when we're notified that a contact's status has changed.
1346
1347 @param statusCode: 3-letter status code
1348 @param userHandle: the contact's user handle (passport)
1349 @param screenName: the contact's screen name
1350 """
1351 pass
1352
1353 def contactPersonalChanged(self, userHandle, personal):
1354 """
1355 Called when a contact's personal message changes.
1356
1357 @param userHandle: the contact who changed their personal message
1358 @param personal : the new personal message
1359 """
1360 pass
1361
1362 def contactOffline(self, userHandle):
1363 """
1364 Called when a contact goes offline.
1365
1366 @param userHandle: the contact's user handle
1367 """
1368 pass
1369
1370 def gotMessage(self, message):
1371 """
1372 Called when there is a message from the notification server
1373 that is not understood by default.
1374
1375 @param message: the MSNMessage.
1376 """
1377 pass
1378
1379 def gotMSNAlert(self, body, action, subscr):
1380 """
1381 Called when the server sends an MSN Alert (http://alerts.msn.com)
1382
1383 @param body : the alert text
1384 @param action: a URL with more information for the user to view
1385 @param subscr: a URL the user can use to modify their alert subscription
1386 """
1387 pass
1388
1389 def gotInitialEmailNotification(self, inboxunread, foldersunread):
1390 """
1391 Called when the server sends you details about your hotmail
1392 inbox. This is only ever called once, on login.
1393
1394 @param inboxunread : the number of unread items in your inbox
1395 @param foldersunread: the number of unread items in other folders
1396 """
1397 pass
1398
1399 def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
1400 """
1401 Called when the server sends us realtime email
1402 notification. This means that you have received
1403 a new email in your hotmail inbox.
1404
1405 @param mailfrom: the sender of the email
1406 @param fromaddr: the sender of the email (I don't know :P)
1407 @param subject : the email subject
1408 """
1409 pass
1410
1411 def gotPhoneNumber(self, userHandle, phoneType, number):
1412 """
1413 Called when the server sends us phone details about
1414 a specific user (for example after a user is added
1415 the server will send their status, phone details etc.
1416
1417 @param userHandle: the contact's user handle (passport)
1418 @param phoneType: the specific phoneType
1419 (*_PHONE constants or HAS_PAGER)
1420 @param number: the value/phone number.
1421 """
1422 pass
1423
1424 def userAddedMe(self, userGuid, userHandle, screenName):
1425 """
1426 Called when a user adds me to their list. (ie. they have been added to
1427 the reverse list.
1428
1429 @param userHandle: the userHandle of the user
1430 @param screenName: the screen name of the user
1431 """
1432 pass
1433
1434 def userRemovedMe(self, userHandle):
1435 """
1436 Called when a user removes us from their contact list
1437 (they are no longer on our reverseContacts list.
1438
1439 @param userHandle: the contact's user handle (passport)
1440 """
1441 pass
1442
1443 def gotSwitchboardInvitation(self, sessionID, host, port,
1444 key, userHandle, screenName):
1445 """
1446 Called when we get an invitation to a switchboard server.
1447 This happens when a user requests a chat session with us.
1448
1449 @param sessionID: session ID number, must be remembered for logging in
1450 @param host: the hostname of the switchboard server
1451 @param port: the port to connect to
1452 @param key: used for authorization when connecting
1453 @param userHandle: the user handle of the person who invited us
1454 @param screenName: the screen name of the person who invited us
1455 """
1456 pass
1457
1458 def multipleLogin(self):
1459 """
1460 Called when the server says there has been another login
1461 under our account, the server should disconnect us right away.
1462 """
1463 pass
1464
1465 def serverGoingDown(self):
1466 """
1467 Called when the server has notified us that it is going down for
1468 maintenance.
1469 """
1470 pass
1471
1472 # api calls
1473
1474 def changeStatus(self, status):
1475 """
1476 Change my current status. This method will add
1477 a default callback to the returned Deferred
1478 which will update the status attribute of the
1479 factory.
1480
1481 @param status: 3-letter status code (as defined by
1482 the STATUS_* constants)
1483 @return: A Deferred, the callback of which will be
1484 fired when the server confirms the change
1485 of status. The callback argument will be
1486 a tuple with the new status code as the
1487 only element.
1488 """
1489
1490 id, d = self._createIDMapping()
1491 self.sendLine("CHG %s %s %s %s" % (id, status, str(MSNContact.MSNC1 | MSNContact.MSNC2 | MSNContact.MSNC3 | MSNContact.MSNC4), quote(self.msnobj.text)))
1492 def _cb(r):
1493 self.factory.status = r[0]
1494 return r
1495 return d.addCallback(_cb)
1496
1497 def setPrivacyMode(self, privLevel):
1498 """
1499 Set my privacy mode on the server.
1500
1501 B{Note}:
1502 This only keeps the current privacy setting on
1503 the server for later retrieval, it does not
1504 effect the way the server works at all.
1505
1506 @param privLevel: This parameter can be true, in which
1507 case the server will keep the state as
1508 'al' which the official client interprets
1509 as -> allow messages from only users on
1510 the allow list. Alternatively it can be
1511 false, in which case the server will keep
1512 the state as 'bl' which the official client
1513 interprets as -> allow messages from all
1514 users except those on the block list.
1515
1516 @return: A Deferred, the callback of which will be fired when
1517 the server replies with the new privacy setting.
1518 The callback argument will be a tuple, the only element
1519 of which being either 'al' or 'bl' (the new privacy setting).
1520 """
1521
1522 id, d = self._createIDMapping()
1523 if privLevel: self.sendLine("BLP %s AL" % id)
1524 else: self.sendLine("BLP %s BL" % id)
1525 return d
1526
1527 def syncList(self):
1528 """
1529 Used for keeping an up-to-date contact list.
1530 A callback is added to the returned Deferred
1531 that updates the contact list on the factory
1532 and also sets my state to STATUS_ONLINE.
1533
1534 B{Note}:
1535 This is called automatically upon signing
1536 in using the version attribute of
1537 factory.contacts, so you may want to persist
1538 this object accordingly. Because of this there
1539 is no real need to ever call this method
1540 directly.
1541
1542 @return: A Deferred, the callback of which will be
1543 fired when the server sends an adequate reply.
1544 The callback argument will be a tuple with two
1545 elements, the new list (MSNContactList) and
1546 your current state (a dictionary). If the version
1547 you sent _was_ the latest list version, both elements
1548 will be None. To just request the list send a version of 0.
1549 """
1550
1551 self._setState('SYNC')
1552 id, d = self._createIDMapping(data=None)
1553 self._setStateData('synid',id)
1554 self.sendLine("SYN %s %s %s" % (id, 0, 0))
1555 def _cb(r):
1556 self.changeStatus(STATUS_ONLINE)
1557 if r[0] is not None:
1558 self.factory.contacts = r[0]
1559 return r
1560 return d.addCallback(_cb)
1561
1562 def setPhoneDetails(self, phoneType, value):
1563 """
1564 Set/change my phone numbers stored on the server.
1565
1566 @param phoneType: phoneType can be one of the following
1567 constants - HOME_PHONE, WORK_PHONE,
1568 MOBILE_PHONE, HAS_PAGER.
1569 These are pretty self-explanatory, except
1570 maybe HAS_PAGER which refers to whether or
1571 not you have a pager.
1572 @param value: for all of the *_PHONE constants the value is a
1573 phone number (str), for HAS_PAGER accepted values
1574 are 'Y' (for yes) and 'N' (for no).
1575
1576 @return: A Deferred, the callback for which will be fired when
1577 the server confirms the change has been made. The
1578 callback argument will be a tuple with 2 elements, the
1579 first being the new list version (int) and the second
1580 being the new phone number value (str).
1581 """
1582 raise "ProbablyDoesntWork"
1583 # XXX: Add a default callback which updates
1584 # factory.contacts.version and the relevant phone
1585 # number
1586 id, d = self._createIDMapping()
1587 self.sendLine("PRP %s %s %s" % (id, phoneType, quote(value)))
1588 return d
1589
1590 def addListGroup(self, name):
1591 """
1592 Used to create a new list group.
1593 A default callback is added to the
1594 returned Deferred which updates the
1595 contacts attribute of the factory.
1596
1597 @param name: The desired name of the new group.
1598
1599 @return: A Deferred, the callbacck for which will be called
1600 when the server clarifies that the new group has been
1601 created. The callback argument will be a tuple with 3
1602 elements: the new list version (int), the new group name
1603 (str) and the new group ID (int).
1604 """
1605
1606 raise "ProbablyDoesntWork"
1607 id, d = self._createIDMapping()
1608 self.sendLine("ADG %s %s 0" % (id, quote(name)))
1609 def _cb(r):
1610 if self.factory.contacts:
1611 self.factory.contacts.version = r[0]
1612 self.factory.contacts.setGroup(r[1], r[2])
1613 return r
1614 return d.addCallback(_cb)
1615
1616 def remListGroup(self, groupID):
1617 """
1618 Used to remove a list group.
1619 A default callback is added to the
1620 returned Deferred which updates the
1621 contacts attribute of the factory.
1622
1623 @param groupID: the ID of the desired group to be removed.
1624
1625 @return: A Deferred, the callback for which will be called when
1626 the server clarifies the deletion of the group.
1627 The callback argument will be a tuple with 2 elements:
1628 the new list version (int) and the group ID (int) of
1629 the removed group.
1630 """
1631
1632 raise "ProbablyDoesntWork"
1633 id, d = self._createIDMapping()
1634 self.sendLine("RMG %s %s" % (id, groupID))
1635 def _cb(r):
1636 self.factory.contacts.version = r[0]
1637 self.factory.contacts.remGroup(r[1])
1638 return r
1639 return d.addCallback(_cb)
1640
1641 def renameListGroup(self, groupID, newName):
1642 """
1643 Used to rename an existing list group.
1644 A default callback is added to the returned
1645 Deferred which updates the contacts attribute
1646 of the factory.
1647
1648 @param groupID: the ID of the desired group to rename.
1649 @param newName: the desired new name for the group.
1650
1651 @return: A Deferred, the callback for which will be called
1652 when the server clarifies the renaming.
1653 The callback argument will be a tuple of 3 elements,
1654 the new list version (int), the group id (int) and
1655 the new group name (str).
1656 """
1657
1658 raise "ProbablyDoesntWork"
1659 id, d = self._createIDMapping()
1660 self.sendLine("REG %s %s %s 0" % (id, groupID, quote(newName)))
1661 def _cb(r):
1662 self.factory.contacts.version = r[0]
1663 self.factory.contacts.setGroup(r[1], r[2])
1664 return r
1665 return d.addCallback(_cb)
1666
1667 def addContact(self, listType, userHandle):
1668 """
1669 Used to add a contact to the desired list.
1670 A default callback is added to the returned
1671 Deferred which updates the contacts attribute of
1672 the factory with the new contact information.
1673 If you are adding a contact to the forward list
1674 and you want to associate this contact with multiple
1675 groups then you will need to call this method for each
1676 group you would like to add them to, changing the groupID
1677 parameter. The default callback will take care of updating
1678 the group information on the factory's contact list.
1679
1680 @param listType: (as defined by the *_LIST constants)
1681 @param userHandle: the user handle (passport) of the contact
1682 that is being added
1683
1684 @return: A Deferred, the callback for which will be called when
1685 the server has clarified that the user has been added.
1686 The callback argument will be a tuple with 4 elements:
1687 the list type, the contact's user handle, the new list
1688 version, and the group id (if relevant, otherwise it
1689 will be None)
1690 """
1691
1692 id, d = self._createIDMapping()
1693 try: # Make sure the contact isn't actually on the list
1694 if self.factory.contacts.getContact(userHandle).lists & listType: return
1695 except AttributeError: pass
1696 listType = listIDToCode[listType].upper()
1697 if listType == "FL":
1698 self.sendLine("ADC %s %s N=%s F=%s" % (id, listType, userHandle, userHandle))
1699 else:
1700 self.sendLine("ADC %s %s N=%s" % (id, listType, userHandle))
1701
1702 def _cb(r):
1703 if not self.factory: return
1704 c = self.factory.contacts.getContact(r[2])
1705 if not c:
1706 c = MSNContact(userGuid=r[1], userHandle=r[2], screenName=r[3])
1707 #if r[3]: c.groups.append(r[3])
1708 c.addToList(r[0])
1709 return r
1710 return d.addCallback(_cb)
1711
1712 def remContact(self, listType, userHandle):
1713 """
1714 Used to remove a contact from the desired list.
1715 A default callback is added to the returned deferred
1716 which updates the contacts attribute of the factory
1717 to reflect the new contact information.
1718
1719 @param listType: (as defined by the *_LIST constants)
1720 @param userHandle: the user handle (passport) of the
1721 contact being removed
1722
1723 @return: A Deferred, the callback for which will be called when
1724 the server has clarified that the user has been removed.
1725 The callback argument will be a tuple of 3 elements:
1726 the list type, the contact's user handle and the group ID
1727 (if relevant, otherwise it will be None)
1728 """
1729
1730 id, d = self._createIDMapping()
1731 try: # Make sure the contact is actually on this list
1732 if not (self.factory.contacts.getContact(userHandle).lists & listType): return
1733 except AttributeError: return
1734 listType = listIDToCode[listType].upper()
1735 if listType == "FL":
1736 try:
1737 c = self.factory.contacts.getContact(userHandle)
1738 userGuid = c.userGuid
1739 except AttributeError: return
1740 self.sendLine("REM %s FL %s" % (id, userGuid))
1741 else:
1742 self.sendLine("REM %s %s %s" % (id, listType, userHandle))
1743
1744 def _cb(r):
1745 if listType == "FL":
1746 r = (r[0], userHandle, r[2]) # make sure we always get a userHandle
1747 l = self.factory.contacts
1748 c = l.getContact(r[1])
1749 if not c: return
1750 group = r[2]
1751 shouldRemove = 1
1752 if group: # they may not have been removed from the list
1753 c.groups.remove(group)
1754 if c.groups: shouldRemove = 0
1755 if shouldRemove:
1756 c.removeFromList(r[0])
1757 if c.lists == 0: l.remContact(c.userHandle)
1758 return r
1759 return d.addCallback(_cb)
1760
1761 def changeScreenName(self, newName):
1762 """
1763 Used to change your current screen name.
1764 A default callback is added to the returned
1765 Deferred which updates the screenName attribute
1766 of the factory and also updates the contact list
1767 version.
1768
1769 @param newName: the new screen name
1770
1771 @return: A Deferred, the callback for which will be called
1772 when the server acknowledges the change.
1773 The callback argument will be a tuple of 1 element,
1774 the new screen name.
1775 """
1776
1777 id, d = self._createIDMapping()
1778 self.sendLine("PRP %s MFN %s" % (id, quote(newName)))
1779 def _cb(r):
1780 self.factory.screenName = r[0]
1781 return r
1782 return d.addCallback(_cb)
1783
1784 def changePersonalMessage(self, personal):
1785 """
1786 Used to change your personal message.
1787
1788 @param personal: the new screen name
1789
1790 @return: A Deferred, the callback for which will be called
1791 when the server acknowledges the change.
1792 The callback argument will be a tuple of 1 element,
1793 the personal message.
1794 """
1795
1796 id, d = self._createIDMapping()
1797 data = ""
1798 if personal:
1799 data = "<Data><PSM>" + personal + "</PSM><CurrentMedia></CurrentMedia></Data>"
1800 self.sendLine("UUX %s %s" % (id, len(data)))
1801 self.transport.write(data)
1802 def _cb(r):
1803 self.factory.personal = personal
1804 return (personal,)
1805 return d.addCallback(_cb)
1806
1807 def changeAvatar(self, imageData, push):
1808 """
1809 Used to change the avatar that other users see.
1810
1811 @param imageData: the PNG image data to set as the avatar
1812 @param push : whether to push the update to the server
1813 (it will otherwise be sent with the next
1814 changeStatus())
1815
1816 @return: If push==True, a Deferred, the callback for which
1817 will be called when the server acknowledges the change.
1818 The callback argument will be the same as for changeStatus.
1819 """
1820
1821 if self.msnobj and imageData == self.msnobj.imageData: return
1822 if imageData:
1823 self.msnobj.setData(self.factory.userHandle, imageData)
1824 else:
1825 self.msnobj.setNull()
1826 if push: return self.changeStatus(self.factory.status) # Push to server
1827
1828
1829 def requestSwitchboardServer(self):
1830 """
1831 Used to request a switchboard server to use for conversations.
1832
1833 @return: A Deferred, the callback for which will be called when
1834 the server responds with the switchboard information.
1835 The callback argument will be a tuple with 3 elements:
1836 the host of the switchboard server, the port and a key
1837 used for logging in.
1838 """
1839
1840 id, d = self._createIDMapping()
1841 self.sendLine("XFR %s SB" % id)
1842 return d
1843
1844 def logOut(self):
1845 """
1846 Used to log out of the notification server.
1847 After running the method the server is expected
1848 to close the connection.
1849 """
1850
1851 if self.pingCheckTask:
1852 self.pingCheckTask.stop()
1853 self.pingCheckTask = None
1854 self.sendLine("OUT")
1855
1856 class NotificationFactory(ClientFactory):
1857 """
1858 Factory for the NotificationClient protocol.
1859 This is basically responsible for keeping
1860 the state of the client and thus should be used
1861 in a 1:1 situation with clients.
1862
1863 @ivar contacts: An MSNContactList instance reflecting
1864 the current contact list -- this is
1865 generally kept up to date by the default
1866 command handlers.
1867 @ivar userHandle: The client's userHandle, this is expected
1868 to be set by the client and is used by the
1869 protocol (for logging in etc).
1870 @ivar screenName: The client's current screen-name -- this is
1871 generally kept up to date by the default
1872 command handlers.
1873 @ivar password: The client's password -- this is (obviously)
1874 expected to be set by the client.
1875 @ivar passportServer: This must point to an msn passport server
1876 (the whole URL is required)
1877 @ivar status: The status of the client -- this is generally kept
1878 up to date by the default command handlers
1879 """
1880
1881 contacts = None
1882 userHandle = ''
1883 screenName = ''
1884 password = ''
1885 passportServer = 'https://nexus.passport.com/rdr/pprdr.asp'
1886 status = 'FLN'
1887 protocol = NotificationClient
1888
1889
1890 class SwitchboardClient(MSNEventBase):
1891 """
1892 This class provides support for clients connecting to a switchboard server.
1893
1894 Switchboard servers are used for conversations with other people
1895 on the MSN network. This means that the number of conversations at
1896 any given time will be directly proportional to the number of
1897 connections to varioius switchboard servers.
1898
1899 MSN makes no distinction between single and group conversations,
1900 so any number of users may be invited to join a specific conversation
1901 taking place on a switchboard server.
1902
1903 @ivar key: authorization key, obtained when receiving
1904 invitation / requesting switchboard server.
1905 @ivar userHandle: your user handle (passport)
1906 @ivar sessionID: unique session ID, used if you are replying
1907 to a switchboard invitation
1908 @ivar reply: set this to 1 in connectionMade or before to signifiy
1909 that you are replying to a switchboard invitation.
1910 """
1911
1912 key = 0
1913 userHandle = ""
1914 sessionID = ""
1915 reply = 0
1916
1917 _iCookie = 0
1918
1919 def __init__(self, msnobj=None):
1920 MSNEventBase.__init__(self)
1921 self.pendingUsers = {}
1922 self.cookies = {'iCookies' : {}} # will maybe be moved to a factory in the future
1923 self.slpLinks = {}
1924 self.msnobj = msnobj
1925
1926 def connectionMade(self):
1927 MSNEventBase.connectionMade(self)
1928 self._sendInit()
1929
1930 def connectionLost(self, reason):
1931 self.cookies['iCookies'] = {}
1932 MSNEventBase.connectionLost(self, reason)
1933
1934 def _sendInit(self):
1935 """
1936 send initial data based on whether we are replying to an invitation
1937 or starting one.
1938 """
1939 id = self._nextTransactionID()
1940 if not self.reply:
1941 self.sendLine("USR %s %s %s" % (id, self.userHandle, self.key))
1942 else:
1943 self.sendLine("ANS %s %s %s %s" % (id, self.userHandle, self.key, self.sessionID))
1944
1945 def _newInvitationCookie(self):
1946 self._iCookie += 1
1947 if self._iCookie > 1000: self._iCookie = 1
1948 return self._iCookie
1949
1950 def _checkTyping(self, message, cTypes):
1951 """ helper method for checkMessage """
1952 if 'text/x-msmsgscontrol' in cTypes and message.hasHeader('TypingUser'):
1953 self.userTyping(message)
1954 return 1
1955
1956 def _checkFileInvitation(self, message, info):
1957 """ helper method for checkMessage """
1958 if not info.get('Application-GUID', '').upper() == MSN_MSNFTP_GUID: return 0
1959 try:
1960 cookie = info['Invitation-Cookie']
1961 filename = info['Application-File']
1962 filesize = int(info['Application-FileSize'])
1963 connectivity = (info.get('Connectivity').lower() == 'y')
1964 except KeyError:
1965 log.msg('Received munged file transfer request ... ignoring.')
1966 return 0
1967 import msnft
1968 self.gotSendRequest(msnft.MSNFTP_Receive(filename, filesize, message.userHandle, cookie, connectivity, self))
1969 return 1
1970
1971 def _checkP2PMessage(self, message, cTypes):
1972 """ helper method for msnslp messages (file transfer & avatars) """
1973 packet = message.message
1974 binaryFields = msnp2p.BinaryFields(packet=packet)
1975 if binaryFields[0] != 0:
1976 slpLink = self.slpLinks[binaryFields[0]]
1977 if slpLink.remoteUser == message.userHandle:
1978 slpLink.handlePacket(packet)
1979 elif binaryFields[5] == BinaryFields.ACK or binaryFields[5] == BinaryFields.BYEGOT:
1980 pass # Ignore the ACKs
1981 else:
1982 slpMessage = msnp2p.MSNSLPMessage(packet)
1983 slpLink = None
1984 if slpMessage.method == "INVITE":
1985 if slpMessage.euf_guid == MSN_MSNFTP_GUID:
1986 slpLink = msnp2p.SLPLink_Receive(slpMessage.fro, slpMessage.sessionID, slpMessage.sessionGuid)
1987 context = msnp2p.FileContext(slpMessage.context)
1988 fileReceive = msnft.MSNP2P_Receive(context.filename, context.filesize, slpMessage.fro, self)
1989 elif slpMessage.euf_guid == MSN_AVATAR_GUID:
1990 slpLink = msnp2p.SLPLink_Send(slpMessage.fro, slpMessage.sessionID, slpMessage.sessionGuid)
1991 self._sendMSNSLPResponse(slpLink, "200 OK")
1992 if slpLink:
1993 self.slpLinks[slpMessage.sessionID] = slpLink
1994 else:
1995 if slpMessage.status != "200":
1996 for slpLink in self.slpLinks:
1997 if slpLink.sessionGuid == slpMessage.sessionGuid:
1998 del self.slpLinks[slpLink.sessionID]
1999 if slpMessage.method != "BYE":
2000 # Must be an error. If its a file transfer we need to signal that it failed
2001 slpLink.transferError()
2002 else:
2003 slpLink = self.slpLinks[slpMessage.sessionID]
2004 slpLink.transferReady()
2005 if slpLink:
2006 # Always need to ACK these packets if we can
2007 self._sendP2PACK(self, slpLink, binaryHeaders)
2008
2009 return 1
2010
2011 def _sendP2PACK(self, slpLink, ackHeaders):
2012 binaryFields = msnp2p.BinaryFields()
2013 binaryFields[1] = slpLink.nextBaseID()
2014 binaryFields[3] = ackHeaders[3]
2015 binaryFields[5] = BinaryFields.ACK
2016 binaryFields[6] = ackHeaders[1]
2017 binaryFields[7] = ackHeaders[6]
2018 binaryFields[8] = ackHeaders[3]
2019 self._sendP2PMessage(binaryFields, "")
2020
2021 def _sendMSNSLPInvite(self, slpLink, guid, context):
2022 msg = msnp2p.MSNSLP_Message()
2023 msg.create(method="INVITE", to=slpLink.remoteUser, fro=self.userHandle, cseq=0, sessionGuid=slpLink.sessionGuid)
2024 msg.setData(sessionID=slpLink.sessionID, appID="1", guid=guid, context=msnp2p.b64enc(context))
2025 self._sendMSNSLPMessage(slpLink, msg)
2026
2027 def _sendMSNSLPResponse(self, slpLink, response):
2028 msg = msnp2p.MSNSLPMessage()
2029 msg.create(status=response, to=slpLink.remoteUser, fro=self.userHandle, cseq=1, sessionGuid=slpLink.sessionGuid)
2030 msg.setData(sessionID=slpLink.sessionID)
2031 self._sendMSNSLPMessage(slpLink, msg)
2032
2033 def _sendMSNSLPMessage(self, slpLink, msnSlpMessage):
2034 msgStr = str(msg)
2035 binaryFields = msnp2p.BinaryFields()
2036 binaryFields[1] = slpLink.nextBaseID()
2037 binaryFields[3] = len(msgStr)
2038 binaryFields[4] = binaryFields[3]
2039 binaryFields[6] = random.randint(0, sys.maxint)
2040 self._sendP2PMessage(binaryFields, msgStr)
2041
2042 def _sendP2PMessage(self, binaryFields, msgStr):
2043 packet = binaryFields.packHeaders() + msgStr + binaryFields.packFooter()
2044
2045 message = MSNMessage(message=packet)
2046 message.setHeader("Content-Type", "application/x-msnmsgrp2p")
2047 message.setHeader("P2P-Dest", handler.to)
2048 message.ack = MSNMessage.MESSAGE_ACK_FAT
2049 self.sendMessage(message)
2050
2051 def checkMessage(self, message):
2052 """
2053 hook for detecting any notification type messages
2054 (e.g. file transfer)
2055 """
2056 cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
2057 if self._checkTyping(message, cTypes): return 0
2058 if 'text/x-msmsgsinvite' in cTypes:
2059 # header like info is sent as part of the message body.
2060 info = {}
2061 for line in message.message.split('\r\n'):
2062 try:
2063 key, val = line.split(':')
2064 info[key] = val.lstrip()
2065 except ValueError: continue
2066 if self._checkFileInvitation(message, info): return 0
2067 if 'application/x-msnmsgrp2p' in cTypes:
2068 if self._checkP2PMessage(message, cTypes): return 0
2069 return 1
2070
2071 # negotiation
2072 def handle_USR(self, params):
2073 checkParamLen(len(params), 4, 'USR')
2074 if params[1] == "OK":
2075 self.loggedIn()
2076
2077 # invite a user
2078 def handle_CAL(self, params):
2079 checkParamLen(len(params), 3, 'CAL')
2080 id = int(params[0])
2081 if params[1].upper() == "RINGING":
2082 self._fireCallback(id, int(params[2])) # session ID as parameter
2083
2084 # user joined
2085 def handle_JOI(self, params):
2086 checkParamLen(len(params), 2, 'JOI')
2087 self.userJoined(params[0], unquote(params[1]))
2088
2089 # users participating in the current chat
2090 def handle_IRO(self, params):
2091 checkParamLen(len(params), 5, 'IRO')
2092 self.pendingUsers[params[3]] = unquote(params[4])
2093 if params[1] == params[2]:
2094 self.gotChattingUsers(self.pendingUsers)
2095 self.pendingUsers = {}
2096
2097 # finished listing users
2098 def handle_ANS(self, params):
2099 checkParamLen(len(params), 2, 'ANS')
2100 if params[1] == "OK":
2101 self.loggedIn()
2102
2103 def handle_ACK(self, params):
2104 checkParamLen(len(params), 1, 'ACK')
2105 self._fireCallback(int(params[0]), None)
2106
2107 def handle_NAK(self, params):
2108 checkParamLen(len(params), 1, 'NAK')
2109 self._fireCallback(int(params[0]), None)
2110
2111 def handle_BYE(self, params):
2112 #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
2113 self.userLeft(params[0])
2114
2115 # callbacks
2116
2117 def loggedIn(self):
2118 """
2119 called when all login details have been negotiated.
2120 Messages can now be sent, or new users invited.
2121 """
2122 pass
2123
2124 def gotChattingUsers(self, users):
2125 """
2126 called after connecting to an existing chat session.
2127
2128 @param users: A dict mapping user handles to screen names
2129 (current users taking part in the conversation)
2130 """
2131 pass
2132
2133 def userJoined(self, userHandle, screenName):
2134 """
2135 called when a user has joined the conversation.
2136
2137 @param userHandle: the user handle (passport) of the user
2138 @param screenName: the screen name of the user
2139 """
2140 pass
2141
2142 def userLeft(self, userHandle):
2143 """
2144 called when a user has left the conversation.
2145
2146 @param userHandle: the user handle (passport) of the user.
2147 """
2148 pass
2149
2150 def gotMessage(self, message):
2151 """
2152 called when we receive a message.
2153
2154 @param message: the associated MSNMessage object
2155 """
2156 pass
2157
2158 def gotAvatarImage(self, userHandle, image):
2159 """
2160 called when we receive an avatar from a contact
2161
2162 @param userHandle: the person who's avatar we have got
2163 @param image: the avatar image
2164 """
2165 pass
2166
2167 def gotSendRequest(self, fileReceive):
2168 """
2169 called when we receive a file send request from a contact
2170
2171 @param fileReceive: msnft.MSNFTReceive_Base instance
2172 """
2173 pass
2174
2175 def userTyping(self, message):
2176 """
2177 called when we receive the special type of message notifying
2178 us that a user is typing a message.
2179
2180 @param message: the associated MSNMessage object
2181 """
2182 pass
2183
2184 # api calls
2185
2186 def inviteUser(self, userHandle):
2187 """
2188 used to invite a user to the current switchboard server.
2189
2190 @param userHandle: the user handle (passport) of the desired user.
2191
2192 @return: A Deferred, the callback for which will be called
2193 when the server notifies us that the user has indeed
2194 been invited. The callback argument will be a tuple
2195 with 1 element, the sessionID given to the invited user.
2196 I'm not sure if this is useful or not.
2197 """
2198
2199 id, d = self._createIDMapping()
2200 self.sendLine("CAL %s %s" % (id, userHandle))
2201 return d
2202
2203 def sendMessage(self, message):
2204 """
2205 used to send a message.
2206
2207 @param message: the corresponding MSNMessage object.
2208
2209 @return: Depending on the value of message.ack.
2210 If set to MSNMessage.MESSAGE_ACK or
2211 MSNMessage.MESSAGE_NACK a Deferred will be returned,
2212 the callback for which will be fired when an ACK or
2213 NACK is received - the callback argument will be
2214 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
2215 the return value is None.
2216 """
2217
2218 if message.ack not in ('A','N','D'): id, d = self._nextTransactionID(), None
2219 else: id, d = self._createIDMapping()
2220 if message.length == 0: message.length = message._calcMessageLen()
2221 self.sendLine("MSG %s %s %s" % (id, message.ack, message.length))
2222 # apparently order matters with at least MIME-Version and Content-Type
2223 self.sendLine('MIME-Version: %s' % message.getHeader('MIME-Version'))
2224 self.sendLine('Content-Type: %s' % message.getHeader('Content-Type'))
2225 # send the rest of the headers
2226 for header in [h for h in message.headers.items() if h[0].lower() not in ('mime-version','content-type')]:
2227 self.sendLine("%s: %s" % (header[0], header[1]))
2228 self.transport.write(CR+LF)
2229 self.transport.write(message.message)
2230 if MESSAGEDEBUG: log.msg(message.message)
2231 return d
2232
2233 def sendAvatarRequest(self, userHandle, msnobj):
2234 pass
2235
2236 def sendTypingNotification(self):
2237 """
2238 used to send a typing notification. Upon receiving this
2239 message the official client will display a 'user is typing'
2240 message to all other users in the chat session for 10 seconds.
2241 The official client sends one of these every 5 seconds (I think)
2242 as long as you continue to type.
2243 """
2244 m = MSNMessage()
2245 m.ack = m.MESSAGE_ACK_NONE
2246 m.setHeader('Content-Type', 'text/x-msmsgscontrol')
2247 m.setHeader('TypingUser', self.userHandle)
2248 m.message = "\r\n"
2249 self.sendMessage(m)
2250
2251 def sendFileInvitation(self, fileName, fileSize):
2252 """
2253 send an notification that we want to send a file.
2254
2255 @param fileName: the file name
2256 @param fileSize: the file size
2257
2258 @return: A Deferred, the callback of which will be fired
2259 when the user responds to this invitation with an
2260 appropriate message. The callback argument will be
2261 a tuple with 3 elements, the first being 1 or 0
2262 depending on whether they accepted the transfer
2263 (1=yes, 0=no), the second being an invitation cookie
2264 to identify your follow-up responses and the third being
2265 the message 'info' which is a dict of information they
2266 sent in their reply (this doesn't really need to be used).
2267 If you wish to proceed with the transfer see the
2268 sendTransferInfo method.
2269 """
2270 cookie = self._newInvitationCookie()
2271 d = Deferred()
2272 m = MSNMessage()
2273 m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2274 m.message += 'Application-Name: File Transfer\r\n'
2275 m.message += 'Application-GUID: %s\r\n' % MSN_MSNFTP_GUID
2276 m.message += 'Invitation-Command: INVITE\r\n'
2277 m.message += 'Invitation-Cookie: %s\r\n' % str(cookie)
2278 m.message += 'Application-File: %s\r\n' % fileName
2279 m.message += 'Application-FileSize: %s\r\n\r\n' % str(fileSize)
2280 m.ack = m.MESSAGE_ACK_NONE
2281 self.sendMessage(m)
2282 self.cookies['iCookies'][cookie] = (d, m)
2283 return d
2284
2285 def sendTransferInfo(self, accept, iCookie, authCookie, ip, port):
2286 """
2287 send information relating to a file transfer session.
2288
2289 @param accept: whether or not to go ahead with the transfer
2290 (1=yes, 0=no)
2291 @param iCookie: the invitation cookie of previous replies
2292 relating to this transfer
2293 @param authCookie: the authentication cookie obtained from
2294 an FileSend instance
2295 @param ip: your ip
2296 @param port: the port on which an FileSend protocol is listening.
2297 """
2298 m = MSNMessage()
2299 m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2300 m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
2301 m.message += 'Invitation-Cookie: %s\r\n' % iCookie
2302 m.message += 'IP-Address: %s\r\n' % ip
2303 m.message += 'Port: %s\r\n' % port
2304 m.message += 'AuthCookie: %s\r\n' % authCookie
2305 m.message += '\r\n'
2306 m.ack = m.MESSAGE_NACK
2307 self.sendMessage(m)
2308
2309
2310 # mapping of error codes to error messages
2311 errorCodes = {
2312
2313 200 : "Syntax error",
2314 201 : "Invalid parameter",
2315 205 : "Invalid user",
2316 206 : "Domain name missing",
2317 207 : "Already logged in",
2318 208 : "Invalid username",
2319 209 : "Invalid screen name",
2320 210 : "User list full",
2321 215 : "User already there",
2322 216 : "User already on list",
2323 217 : "User not online",
2324 218 : "Already in mode",
2325 219 : "User is in the opposite list",
2326 223 : "Too many groups",
2327 224 : "Invalid group",
2328 225 : "User not in group",
2329 229 : "Group name too long",
2330 230 : "Cannot remove group 0",
2331 231 : "Invalid group",
2332 280 : "Switchboard failed",
2333 281 : "Transfer to switchboard failed",
2334
2335 300 : "Required field missing",
2336 301 : "Too many FND responses",
2337 302 : "Not logged in",
2338
2339 402 : "Error accessing contact list",
2340 403 : "Error accessing contact list",
2341
2342 500 : "Internal server error",
2343 501 : "Database server error",
2344 502 : "Command disabled",
2345 510 : "File operation failed",
2346 520 : "Memory allocation failed",
2347 540 : "Wrong CHL value sent to server",
2348
2349 600 : "Server is busy",
2350 601 : "Server is unavaliable",
2351 602 : "Peer nameserver is down",
2352 603 : "Database connection failed",
2353 604 : "Server is going down",
2354 605 : "Server unavailable",
2355
2356 707 : "Could not create connection",
2357 710 : "Invalid CVR parameters",
2358 711 : "Write is blocking",
2359 712 : "Session is overloaded",
2360 713 : "Too many active users",
2361 714 : "Too many sessions",
2362 715 : "Not expected",
2363 717 : "Bad friend file",
2364 731 : "Not expected",
2365
2366 800 : "Requests too rapid",
2367
2368 910 : "Server too busy",
2369 911 : "Authentication failed",
2370 912 : "Server too busy",
2371 913 : "Not allowed when offline",
2372 914 : "Server too busy",
2373 915 : "Server too busy",
2374 916 : "Server too busy",
2375 917 : "Server too busy",
2376 918 : "Server too busy",
2377 919 : "Server too busy",
2378 920 : "Not accepting new users",
2379 921 : "Server too busy",
2380 922 : "Server too busy",
2381 923 : "No parent consent",
2382 924 : "Passport account not yet verified"
2383
2384 }
2385
2386 # mapping of status codes to readable status format
2387 statusCodes = {
2388
2389 STATUS_ONLINE : "Online",
2390 STATUS_OFFLINE : "Offline",
2391 STATUS_HIDDEN : "Appear Offline",
2392 STATUS_IDLE : "Idle",
2393 STATUS_AWAY : "Away",
2394 STATUS_BUSY : "Busy",
2395 STATUS_BRB : "Be Right Back",
2396 STATUS_PHONE : "On the Phone",
2397 STATUS_LUNCH : "Out to Lunch"
2398
2399 }
2400
2401 # mapping of list ids to list codes
2402 listIDToCode = {
2403
2404 FORWARD_LIST : 'fl',
2405 BLOCK_LIST : 'bl',
2406 ALLOW_LIST : 'al',
2407 REVERSE_LIST : 'rl',
2408 PENDING_LIST : 'pl'
2409
2410 }
2411
2412 # mapping of list codes to list ids
2413 listCodeToID = {}
2414 for id,code in listIDToCode.items():
2415 listCodeToID[code] = id
2416
2417 del id, code