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