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