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