]> code.delx.au - pymsnt/blob - src/legacy/msn/msnw.py
Updated rev and url
[pymsnt] / src / legacy / msn / msnw.py
1 # Copyright 2004-2006 James Bunton <james@delx.cjb.net>
2 # Licensed for distribution under the GPL version 2, check COPYING for details
3
4 # Twisted imports
5 from twisted.internet import reactor
6 from twisted.internet.defer import Deferred
7 from twisted.internet.protocol import ClientFactory
8
9 # System imports
10 import math, base64, binascii
11
12 # Local imports
13 from debug import LogEvent, INFO, WARN, ERROR
14 import msn
15
16
17
18 """
19 All interaction should be with the MSNConnection and MultiSwitchboardSession classes.
20 You should not directly instantiate any objects of other classes.
21 """
22
23 class MSNConnection:
24 """ Manages all the Twisted factories, etc """
25 MAXMESSAGESIZE = 1400
26 SWITCHBOARDTIMEOUT = 30.0*60.0
27 GETALLAVATARS = False
28 BINDADDRESS = "0.0.0.0"
29
30 def __init__(self, username, password, ident):
31 """ Connects to the MSN servers.
32 @param username: the MSN passport to connect with.
33 @param password: the password for this account.
34 @param ident: a unique identifier to use in logging.
35 """
36 self.username = username
37 self.password = password
38 self.ident = ident
39 self.timeout = None
40 self.notificationFactory = None
41 self.notificationClient = None
42 self.connect()
43 LogEvent(INFO, self.ident)
44
45 def connect(self):
46 """ Automatically called by the constructor """
47 self.connectors = []
48 self.switchboardSessions = {}
49 self.savedEvents = SavedEvents() # Save any events that occur before connect
50 self._getNotificationReferral()
51
52 def _getNotificationReferral(self):
53 def timeout():
54 self.timeout = None
55 dispatchFactory.d = None
56 if not d.called:
57 d.errback(Exception("Timeout"))
58 self.logOut() # Clean up everything
59 self.timeout = reactor.callLater(30, timeout)
60 dispatchFactory = msn.DispatchFactory()
61 dispatchFactory.userHandle = self.username
62 dispatchFactory.protocol = DispatchClient
63 d = Deferred()
64 dispatchFactory.d = d
65 d.addCallbacks(self._gotNotificationReferral, self.connectionFailed)
66 self.connectors.append(reactor.connectTCP("messenger.hotmail.com", 1863, dispatchFactory, bindAddress=(MSNConnection.BINDADDRESS, 0)))
67 LogEvent(INFO, self.ident)
68
69 def _gotNotificationReferral(self, (host, port)):
70 self.timeout.cancel()
71 self.timeout = None
72 # Create the NotificationClient
73 self.notificationFactory = msn.NotificationFactory()
74 self.notificationFactory.userHandle = self.username
75 self.notificationFactory.password = self.password
76 self.notificationFactory.msncon = self
77 self.notificationFactory.protocol = NotificationClient
78 self.connectors.append(reactor.connectTCP(host, port, self.notificationFactory, bindAddress=(MSNConnection.BINDADDRESS, 0)))
79 LogEvent(INFO, self.ident)
80
81 def _sendSavedEvents(self):
82 self.savedEvents.send(self)
83
84 def _notificationClientReady(self, notificationClient):
85 self.notificationClient = notificationClient
86
87 def _ensureSwitchboardSession(self, userHandle):
88 if not self.switchboardSessions.has_key(userHandle):
89 sb = OneSwitchboardSession(self, userHandle)
90 sb.connect()
91 self.switchboardSessions[userHandle] = sb
92
93
94
95 # API calls
96 def getContacts(self):
97 """ Gets the contact list.
98
99 @return an instance of MSNContactList (do not modify) if connected,
100 or None if not.
101 """
102 if self.notificationFactory:
103 return self.notificationFactory.contacts
104 else:
105 return None
106
107 def sendMessage(self, userHandle, text, noerror=False):
108 """
109 Sends a message to a contact. Can only be called after listSynchronized().
110
111 @param userHandle: the contact's MSN passport.
112 @param text: the text to send.
113 @param noerror: Set this to True if you don't want failed messages to bounce.
114 """
115 LogEvent(INFO, self.ident)
116 if self.notificationClient:
117 self._ensureSwitchboardSession(userHandle)
118 self.switchboardSessions[userHandle].sendMessage(text, noerror)
119 elif not noerror:
120 self.failedMessage(userHandle, text)
121
122 def sendAvatarRequest(self, userHandle):
123 """
124 Requests the avatar of a contact.
125
126 @param userHandle: the contact to request an avatar from.
127 @return: a Deferred() if the avatar can be fetched at this time.
128 This will fire with an argument of a tuple with the PNG
129 image data as the only element.
130 Otherwise returns None
131 """
132
133 LogEvent(INFO, self.ident)
134 if not self.notificationClient: return
135 if MSNConnection.GETALLAVATARS:
136 self._ensureSwitchboardSession(userHandle)
137 sb = self.switchboardSessions.get(userHandle)
138 if sb: return sb.sendAvatarRequest()
139
140 def sendFile(self, userHandle, filename, filesize):
141 """
142 Used to send a file to a contact.
143
144 @param username: the passport of the contact to send a file to.
145 @param filename: the name of the file to send.
146 @param filesize: the size of the file to send.
147
148 @return: A Deferred, which will fire with an argument of:
149 (fileSend, d) A FileSend object and a Deferred.
150 The new Deferred will pass one argument in a tuple,
151 whether or not the transfer is accepted. If you
152 receive a True, then you can call write() on the
153 fileSend object to send your file. Call close()
154 when the file is done.
155 NOTE: You MUST write() exactly as much as you
156 declare in filesize.
157 """
158 msnContact = self.getContacts().getContact(userHandle)
159 if not msnContact:
160 raise ValueError, "Contact not found"
161 self._ensureSwitchboardSession(userHandle)
162 return self.switchboardSessions[userHandle].sendFile(msnContact, filename, filesize)
163
164 def sendTypingToContact(self, userHandle):
165 """
166 Sends typing notification to a contact. Should send every 5secs.
167 @param userHandle: the contact to notify of our typing.
168 """
169
170 sb = self.switchboardSessions.get(userHandle)
171 if sb: return sb.sendTypingNotification()
172
173 def changeAvatar(self, imageDataFunc):
174 """
175 Changes the user's avatar.
176 @param imageDataFunc: a function which returns the new PNG avatar image data.
177 """
178 if self.notificationClient:
179 LogEvent(INFO, self.ident)
180 self.notificationClient.changeAvatar(imageDataFunc, push=True)
181 # Save the avatar for reuse on disconnection
182 self.savedEvents.avatarImageDataFunc = imageDataFunc
183
184 def changeStatus(self, statusCode, screenName, personal):
185 """
186 Changes your status details. All details must be given with
187 each call. This can be called before connection if you wish.
188
189 @param statusCode: the user's new status (look in msn.statusCodes).
190 @param screenName: the user's new screenName (up to 127 characters).
191 @param personal: the user's new personal message.
192 """
193
194 if not screenName: screenName = self.username
195 if not statusCode: statusCode = msn.STATUS_ONLINE
196 if not personal: personal = ""
197 if self.notificationClient:
198 changeCount = [0] # Hack for Python's limited scope :(
199 def cb(ignored=None):
200 changeCount[0] += 1
201 if changeCount[0] == 3:
202 self.ourStatusChanged(statusCode, screenName, personal)
203 def errcb(ignored=None):
204 pass # FIXME, should we do something here?
205 LogEvent(INFO, self.ident)
206 self.notificationClient.changeStatus(statusCode.encode("utf-8")).addCallbacks(cb, errcb)
207 self.notificationClient.changeScreenName(screenName.encode("utf-8")).addCallbacks(cb, errcb)
208 self.notificationClient.changePersonalMessage(personal.encode("utf-8")).addCallbacks(cb, errcb)
209 # Remember the saved status
210 self.savedEvents.statusCode = statusCode
211 self.savedEvents.screenName = screenName
212 self.savedEvents.personal = personal
213
214 def addContact(self, listType, userHandle):
215 """ See msn.NotificationClient.addContact """
216 if self.notificationClient:
217 return self.notificationClient.addContact(listType, str(userHandle))
218 else:
219 self.savedEvents.addContacts.append((listType, str(userHandle)))
220
221 def remContact(self, listType, userHandle):
222 """ See msn.NotificationClient.remContact """
223 if self.notificationClient:
224 return self.notificationClient.remContact(listType, str(userHandle))
225 else:
226 self.savedEvents.remContacts.append((listType, str(userHandle)))
227
228 def logOut(self):
229 """ Shuts down the whole connection. Don't try to call any
230 other methods after this one. Except maybe connect() """
231 if self.notificationClient:
232 self.notificationClient.logOut()
233 for c in self.connectors:
234 c.disconnect()
235 self.connectors = []
236 if self.notificationFactory:
237 self.notificationFactory.stopTrying()
238 self.notificationFactory.msncon = None
239 self.notificationFactory = None
240 for sbs in self.switchboardSessions.values():
241 if hasattr(sbs, "transport") and sbs.transport:
242 sbs.transport.loseConnection()
243 self.switchboardSessions = {}
244 if self.timeout:
245 self.timeout.cancel()
246 self.timeout = None
247 LogEvent(INFO, self.ident)
248
249
250 # Reimplement these!
251 def connectionFailed(self, reason=''):
252 """ Called when the connection to the server failed. """
253
254 def loginFailed(self, reason=''):
255 """ Called when the account could not be logged in. """
256
257 def connectionLost(self, reason=''):
258 """ Called when we are disconnected. """
259
260 def multipleLogin(self):
261 """ Called when the server says there has been another login
262 for this account. """
263
264 def serverGoingDown(self):
265 """ Called when the server says that it will be going down. """
266
267 def accountNotVerified(self):
268 """ Called if this passport has not been verified. Certain
269 functions are not available. """
270
271 def userMapping(self, passport, jid):
272 """ Called when it is brought to our attention that one of the
273 MSN contacts has a Jabber ID. You should communicate with Jabber. """
274
275 def loggedIn(self):
276 """ Called when we have authenticated, but before we receive
277 the contact list. """
278
279 def listSynchronized(self):
280 """ Called when we have received the contact list. All methods
281 in this class are now valid. """
282
283 def ourStatusChanged(self, statusCode, screenName, personal):
284 """ Called when the user's status has changed. """
285
286 def gotMessage(self, userHandle, text):
287 """ Called when a contact sends us a message """
288
289 def gotGroupchat(self, msnGroupchat, userHandle):
290 """ Called when a conversation with more than one contact begins.
291 userHandle is the person who invited us.
292 The overriding method is expected to set msnGroupchat.groupchat to an object
293 that implements the following methods:
294 contactJoined(userHandle)
295 contactLeft(userHandle)
296 gotMessage(userHandle, text)
297
298 The object received as 'msnGroupchat' is an instance of MultiSwitchboardSession.
299 """
300
301 def gotContactTyping(self, userHandle):
302 """ Called when a contact sends typing notification.
303 Will be called once every 5 seconds. """
304
305 def failedMessage(self, userHandle, text):
306 """ Called when a message we sent has been bounced back. """
307
308 def contactAvatarChanged(self, userHandle, hash):
309 """ Called when we receive a changed avatar hash for a contact.
310 You should call sendAvatarRequest(). """
311
312 def contactStatusChanged(self, userHandle):
313 """ Called when we receive status information for a contact. """
314
315 def gotFileReceive(self, fileReceive):
316 """ Called when a contact sends the user a file.
317 Call accept(fileHandle) or reject() on the object. """
318
319 def contactAddedMe(self, userHandle):
320 """ Called when a contact adds the user to their list. """
321
322 def contactRemovedMe(self, userHandle):
323 """ Called when a contact removes the user from their list. """
324
325 def gotInitialEmailNotification(self, inboxunread, foldersunread):
326 """ Received at login to tell about the user's Hotmail status """
327
328 def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
329 """ Received in realtime whenever an email comes into the hotmail account """
330
331 def gotMSNAlert(self, body, action, subscr):
332 """ An MSN Alert (http://alerts.msn.com) was received. Body is the
333 text of the alert. 'action' is a url for more information,
334 'subscr' is a url to modify your your alerts subscriptions. """
335
336 def gotAvatarImageData(self, userHandle, imageData):
337 """ An contact's avatar has been received because a switchboard
338 session with them was started. """
339
340
341 class SavedEvents:
342 def __init__(self):
343 self.screenName = ""
344 self.statusCode = ""
345 self.personal = ""
346 self.avatarImageDataFunc = None
347 self.addContacts = []
348 self.remContacts = []
349
350 def send(self, msncon):
351 if self.avatarImageDataFunc:
352 msncon.notificationClient.changeAvatar(self.avatarImageDataFunc, push=False)
353 if self.screenName or self.statusCode or self.personal:
354 msncon.changeStatus(self.statusCode, self.screenName, self.personal)
355 for listType, userHandle in self.addContacts:
356 msncon.addContact(listType, userHandle)
357 for listType, userHandle in self.remContacts:
358 msncon.remContact(listType, userHandle)
359
360
361
362 class DispatchClient(msn.DispatchClient):
363 def gotNotificationReferral(self, host, port):
364 d = self.factory.d
365 self.factory.d = None
366 if not d or d.called:
367 return # Too slow! We've already timed out
368 d.callback((host, port))
369
370
371 class NotificationClient(msn.NotificationClient):
372 def doDisconnect(self, *args):
373 if hasattr(self, "transport") and self.transport:
374 self.transport.loseConnection()
375
376 def loginFailure(self, message):
377 self.factory.msncon.loginFailed(message)
378
379 def loggedIn(self, userHandle, verified):
380 LogEvent(INFO, self.factory.msncon.ident)
381 msn.NotificationClient.loggedIn(self, userHandle, verified)
382 self.factory.msncon._notificationClientReady(self)
383 self.factory.msncon.loggedIn()
384 if not verified:
385 self.factory.msncon.accountNotVerified()
386
387 def logOut(self):
388 msn.NotificationClient.logOut(self)
389 # If we explicitly log out, then all of these events
390 # are now redundant
391 self.loginFailure = self.doDisconnect
392 self.loggedIn = self.doDisconnect
393 self.connectionLost = lambda reason: msn.NotificationClient.connectionLost(self, reason)
394
395 def connectionLost(self, reason):
396 if not self.factory.msncon:
397 # If MSNConnection.logOut is called before _notificationClientReady
398 return
399
400 def wait():
401 LogEvent(INFO, self.factory.msncon.ident)
402 msn.NotificationClient.connectionLost(self, reason)
403 if self.factory.maxRetries > self.factory.retries:
404 self.factory.stopTrying()
405 self.factory.msncon.connectionLost(reason)
406 # Make sure this event is handled after any others
407 reactor.callLater(0, wait)
408
409 def gotMSNAlert(self, body, action, subscr):
410 LogEvent(INFO, self.factory.msncon.ident)
411 self.factory.msncon.gotMSNAlert(body, action, subscr)
412
413 def gotInitialEmailNotification(self, inboxunread, foldersunread):
414 LogEvent(INFO, self.factory.msncon.ident)
415 self.factory.msncon.gotInitialEmailNotification(inboxunread, foldersunread)
416
417 def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
418 LogEvent(INFO, self.factory.msncon.ident)
419 self.factory.msncon.gotRealtimeEmailNotification(mailfrom, fromaddr, subject)
420
421 def userAddedMe(self, userGuid, userHandle, screenName):
422 LogEvent(INFO, self.factory.msncon.ident)
423 self.factory.msncon.contactAddedMe(userHandle)
424
425 def userRemovedMe(self, userHandle):
426 LogEvent(INFO, self.factory.msncon.ident)
427 self.factory.msncon.contactRemovedMe(userHandle)
428
429 def listSynchronized(self, *args):
430 LogEvent(INFO, self.factory.msncon.ident)
431 self.factory.msncon._sendSavedEvents()
432 self.factory.msncon.listSynchronized()
433
434 def contactAvatarChanged(self, userHandle, hash):
435 LogEvent(INFO, self.factory.msncon.ident)
436 self.factory.msncon.contactAvatarChanged(userHandle, hash)
437
438 def gotContactStatus(self, userHandle, statusCode, screenName):
439 LogEvent(INFO, self.factory.msncon.ident)
440 self.factory.msncon.contactStatusChanged(userHandle)
441
442 def contactStatusChanged(self, userHandle, statusCode, screenName):
443 LogEvent(INFO, self.factory.msncon.ident)
444 self.factory.msncon.contactStatusChanged(userHandle)
445
446 def contactPersonalChanged(self, userHandle, personal):
447 LogEvent(INFO, self.factory.msncon.ident)
448 self.factory.msncon.contactStatusChanged(userHandle)
449
450 def contactOffline(self, userHandle):
451 LogEvent(INFO, self.factory.msncon.ident)
452 self.factory.msncon.contactStatusChanged(userHandle)
453
454 def gotSwitchboardInvitation(self, sessionID, host, port, key, userHandle, screenName):
455 LogEvent(INFO, self.factory.msncon.ident)
456 sb = self.factory.msncon.switchboardSessions.get(userHandle)
457 if sb and sb.transport:
458 sb.transport.loseConnection()
459 sb = OneSwitchboardSession(self.factory.msncon, userHandle)
460 self.factory.msncon.switchboardSessions[userHandle] = sb
461 sb.connectReply(host, port, key, sessionID)
462
463 def multipleLogin(self):
464 LogEvent(INFO, self.factory.msncon.ident)
465 self.factory.msncon.multipleLogin()
466
467 def serverGoingDown(self):
468 LogEvent(INFO, self.factory.msncon.ident)
469 self.factory.msncon.serverGoingDown()
470
471
472
473 class SwitchboardSessionBase(msn.SwitchboardClient):
474 def __init__(self, msncon):
475 msn.SwitchboardClient.__init__(self)
476 self.msncon = msncon
477 self.msnobj = msncon.notificationClient.msnobj
478 self.userHandle = msncon.username
479 self.ident = (msncon.ident, "INVALID!!")
480 self.messageBuffer = []
481 self.funcBuffer = []
482 self.ready = False
483
484 def connectionLost(self, reason):
485 msn.SwitchboardClient.connectionLost(self, reason)
486 LogEvent(INFO, self.ident)
487 self.ready = False
488 self.msncon = None
489 self.msnobj = None
490 self.ident = (self.ident[0], self.ident[1], "Disconnected!")
491
492 def loggedIn(self):
493 LogEvent(INFO, self.ident)
494 self.ready = True
495 self.flushBuffer()
496
497 def connect(self):
498 LogEvent(INFO, self.ident)
499 self.ready = False
500 def sbRequestAccepted((host, port, key)):
501 LogEvent(INFO, self.ident)
502 self.key = key
503 self.reply = 0
504 factory = ClientFactory()
505 factory.buildProtocol = lambda addr: self
506 self.msncon.connectors.append(reactor.connectTCP(host, port, factory, bindAddress=(MSNConnection.BINDADDRESS, 0)))
507 def sbRequestFailed(ignored=None):
508 LogEvent(INFO, self.ident)
509 del self.msncon.switchboardSessions[self.remoteUser]
510 d = self.msncon.notificationClient.requestSwitchboardServer()
511 d.addCallbacks(sbRequestAccepted, sbRequestFailed)
512
513 def connectReply(self, host, port, key, sessionID):
514 LogEvent(INFO, self.ident)
515 self.ready = False
516 self.key = key
517 self.sessionID = sessionID
518 self.reply = 1
519 factory = ClientFactory()
520 factory.buildProtocol = lambda addr: self
521 self.msncon.connectors.append(reactor.connectTCP(host, port, factory, bindAddress=(MSNConnection.BINDADDRESS, 0)))
522
523 def flushBuffer(self):
524 for message, noerror in self.messageBuffer[:]:
525 self.messageBuffer.remove((message, noerror))
526 self.sendMessage(message, noerror)
527 for f in self.funcBuffer[:]:
528 self.funcBuffer.remove(f)
529 f()
530
531 def failedMessage(self, *ignored):
532 raise NotImplementedError
533
534 def sendClientCaps(self):
535 message = msn.MSNMessage()
536 message.setHeader("Content-Type", "text/x-clientcaps")
537 message.setHeader("Client-Name", "PyMSNt")
538 if hasattr(self.msncon, "jabberID"):
539 message.setHeader("JabberID", str(self.msncon.jabberID))
540 self.sendMessage(message)
541
542 def sendMessage(self, message, noerror=False):
543 # Check to make sure that clientcaps only gets sent after
544 # the first text type message.
545 if isinstance(message, msn.MSNMessage) and message.getHeader("Content-Type").startswith("text"):
546 self.sendMessage = self.sendMessageReal
547 self.sendClientCaps()
548 return self.sendMessage(message, noerror)
549 else:
550 return self.sendMessageReal(message, noerror)
551
552 def sendMessageReal(self, text, noerror=False):
553 if not isinstance(text, basestring):
554 msn.SwitchboardClient.sendMessage(self, text)
555 return
556 if not self.ready:
557 self.messageBuffer.append((text, noerror))
558 else:
559 LogEvent(INFO, self.ident)
560 text = str(text.replace("\n", "\r\n").encode("utf-8"))
561 def failedMessage(ignored):
562 if not noerror:
563 self.failedMessage(text)
564
565 if len(text) < MSNConnection.MAXMESSAGESIZE:
566 message = msn.MSNMessage(message=text)
567 message.ack = msn.MSNMessage.MESSAGE_NACK
568
569 d = msn.SwitchboardClient.sendMessage(self, message)
570 if not noerror:
571 d.addCallbacks(failedMessage, failedMessage)
572
573 else:
574 chunks = int(math.ceil(len(text) / float(MSNConnection.MAXMESSAGESIZE)))
575 chunk = 0
576 guid = msn.random_guid()
577 while chunk < chunks:
578 offset = chunk * MSNConnection.MAXMESSAGESIZE
579 message = msn.MSNMessage(message=text[offset : offset + MSNConnection.MAXMESSAGESIZE])
580 message.ack = msn.MSNMessage.MESSAGE_NACK
581 message.setHeader("Message-ID", guid)
582 if chunk == 0:
583 message.setHeader("Chunks", str(chunks))
584 else:
585 message.delHeader("MIME-Version")
586 message.delHeader("Content-Type")
587 message.setHeader("Chunk", str(chunk))
588
589 d = msn.SwitchboardClient.sendMessage(self, message)
590 if not noerror:
591 d.addCallbacks(failedMessage, failedMessage)
592
593 chunk += 1
594
595
596 class MultiSwitchboardSession(SwitchboardSessionBase):
597 """ Create one of me to chat to multiple contacts """
598
599 def __init__(self, msncon):
600 """ Automatically creates a new switchboard connection to the server """
601 SwitchboardSessionBase.__init__(self, msncon)
602 self.ident = (self.msncon.ident, repr(self))
603 self.contactCount = 0
604 self.groupchat = None
605 self.connect()
606
607 def failedMessage(self, text):
608 self.groupchat.gotMessage("BOUNCE", text)
609
610 def sendMessage(self, text, noerror=False):
611 """ Used to send a mesage to the groupchat. Can be called immediately
612 after instantiation. """
613 if self.contactCount > 0:
614 SwitchboardSessionBase.sendMessage(self, text, noerror)
615 else:
616 #self.messageBuffer.append((message, noerror))
617 pass # They're sending messages to an empty room. Ignore.
618
619 def inviteUser(self, userHandle):
620 """ Used to invite a contact to the groupchat. Can be called immediately
621 after instantiation. """
622 userHandle = str(userHandle)
623 if self.ready:
624 LogEvent(INFO, self.ident, "immediate")
625 msn.SwitchboardClient.inviteUser(self, userHandle)
626 else:
627 LogEvent(INFO, self.ident, "pending")
628 self.funcBuffer.append(lambda: msn.SwitchboardClient.inviteUser(self, userHandle))
629
630 def gotMessage(self, message):
631 self.groupchat.gotMessage(message.userHandle, message.getMessage())
632
633 def userJoined(self, userHandle, screenName=''):
634 LogEvent(INFO, self.ident)
635 self.contactCount += 1
636 self.groupchat.contactJoined(userHandle)
637
638 def userLeft(self, userHandle):
639 LogEvent(INFO, self.ident)
640 self.contactCount -= 1
641 self.groupchat.contactLeft(userHandle)
642
643
644
645 class OneSwitchboardSession(SwitchboardSessionBase):
646 def __init__(self, msncon, remoteUser):
647 SwitchboardSessionBase.__init__(self, msncon)
648 self.remoteUser = str(remoteUser)
649 self.ident = (self.msncon.ident, self.remoteUser)
650 self.chattingUsers = []
651 self.timeout = None
652
653 def connectionLost(self, reason):
654 if self.timeout:
655 self.timeout.cancel()
656 self.timeout = None
657 for message, noerror in self.messageBuffer:
658 if not noerror:
659 self.failedMessage(message)
660 self.messageBuffer = []
661
662 if self.msncon and self.msncon.switchboardSessions.has_key(self.remoteUser):
663 # Unexpected disconnection. Must remove us from msncon
664 self.msncon.switchboardSessions.pop(self.remoteUser)
665
666 SwitchboardSessionBase.connectionLost(self, reason)
667
668 def _ready(self):
669 LogEvent(INFO, self.ident)
670 self.ready = True
671 for user in self.chattingUsers:
672 self.userJoined(user)
673 if self.timeout:
674 self.timeout.cancel()
675 self.timeout = None
676 self.flushBuffer()
677
678 def _switchToMulti(self, userHandle):
679 LogEvent(INFO, self.ident)
680 del self.msncon.switchboardSessions[self.remoteUser]
681 self.__class__ = MultiSwitchboardSession
682 del self.remoteUser
683 self.contactCount = 0
684 self.msncon.gotGroupchat(self, userHandle)
685 assert self.groupchat
686
687 def failedMessage(self, text):
688 self.msncon.failedMessage(self.remoteUser, text)
689
690 # Callbacks
691 def loggedIn(self):
692 LogEvent(INFO, self.ident)
693 if not self.reply:
694 def failCB(arg=None):
695 self.timeout = None
696 self.transport.loseConnection()
697 if not (self.msncon and self.msncon.switchboardSessions.has_key(self.remoteUser)):
698 return
699 LogEvent(INFO, self.ident, "User has not joined after 30 seconds.")
700 del self.msncon.switchboardSessions[self.remoteUser]
701 d = self.inviteUser(self.remoteUser)
702 d.addErrback(failCB)
703 self.timeout = reactor.callLater(30.0, failCB)
704 else:
705 self._ready()
706
707 def gotChattingUsers(self, users):
708 for userHandle in users.keys():
709 self.chattingUsers.append(userHandle)
710
711 def userJoined(self, userHandle, screenName=''):
712 LogEvent(INFO, self.ident)
713 if not self.reply and not self.ready:
714 self._ready()
715 if userHandle != self.remoteUser:
716 # Another user has joined, so we now have three participants.
717 remoteUser = self.remoteUser
718 self._switchToMulti(remoteUser)
719 self.userJoined(remoteUser)
720 self.userJoined(userHandle)
721 else:
722 def updateAvatarCB((imageData, )):
723 if self.msncon:
724 self.msncon.gotAvatarImageData(self.remoteUser, imageData)
725 d = self.sendAvatarRequest()
726 if d:
727 d.addCallback(updateAvatarCB)
728
729 def userLeft(self, userHandle):
730 def wait():
731 if userHandle == self.remoteUser:
732 if self.msncon and self.msncon.switchboardSessions.has_key(self.remoteUser):
733 del self.msncon.switchboardSessions[self.remoteUser]
734 reactor.callLater(0, wait) # Make sure this is handled after everything else
735
736 def gotMessage(self, message):
737 LogEvent(INFO, self.ident)
738 cTypes = [s.strip() for s in message.getHeader("Content-Type").split(';')]
739 if "text/plain" == cTypes[0]:
740 try:
741 if len(cTypes) > 1 and cTypes[1].lower().find("utf-8") >= 0:
742 text = message.getMessage().decode("utf-8")
743 else:
744 text = message.getMessage()
745 self.msncon.gotMessage(self.remoteUser, text)
746 except UnicodeDecodeError:
747 LogEvent(WARN, self.ident, "Message lost!")
748 self.msncon.gotMessage(self.remoteUser, "A message was lost.")
749 raise
750 elif "text/x-clientcaps" == cTypes[0]:
751 if message.hasHeader("JabberID"):
752 jid = message.getHeader("JabberID")
753 self.msncon.userMapping(message.userHandle, jid)
754 else:
755 LogEvent(INFO, self.ident, "Discarding unknown message type.")
756
757 def gotFileReceive(self, fileReceive):
758 LogEvent(INFO, self.ident)
759 self.msncon.gotFileReceive(fileReceive)
760
761 def gotContactTyping(self, message):
762 LogEvent(INFO, self.ident)
763 self.msncon.gotContactTyping(message.userHandle)
764
765 def sendTypingNotification(self):
766 LogEvent(INFO, self.ident)
767 if self.ready:
768 msn.SwitchboardClient.sendTypingNotification(self)
769
770 CAPS = msn.MSNContact.MSNC1 | msn.MSNContact.MSNC2 | msn.MSNContact.MSNC3 | msn.MSNContact.MSNC4
771 def sendAvatarRequest(self):
772 if not self.ready: return
773 msnContacts = self.msncon.getContacts()
774 if not msnContacts: return
775 msnContact = msnContacts.getContact(self.remoteUser)
776 if not (msnContact and msnContact.caps & self.CAPS and msnContact.msnobj): return
777 if msnContact.msnobjGot: return
778 msnContact.msnobjGot = True # This is deliberately set before we get the avatar. So that we don't try to reget failed avatars over & over
779 return msn.SwitchboardClient.sendAvatarRequest(self, msnContact)
780
781 def sendFile(self, msnContact, filename, filesize):
782 def doSendFile(ignored=None):
783 d.callback(msn.SwitchboardClient.sendFile(self, msnContact, filename, filesize))
784 d = Deferred()
785 if self.ready:
786 reactor.callLater(0, doSendFile)
787 else:
788 self.funcBuffer.append(doSendFile)
789 return d
790