]> code.delx.au - pymsnt/blob - src/tlib/msn/msnw.py
Data received after the connection has been cleaned up is now ignored (instead of...
[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 loginFailure(self, message):
364 self.factory.msncon.loginFailed(message)
365
366 def loggedIn(self, userHandle, verified):
367 LogEvent(INFO, self.factory.msncon.ident)
368 msn.NotificationClient.loggedIn(self, userHandle, verified)
369 self.factory.msncon._notificationClientReady(self)
370 self.factory.msncon.loggedIn()
371 if not verified:
372 self.factory.msncon.accountNotVerified()
373
374 def logOut(self):
375 msn.NotificationClient.logOut(self)
376
377 def connectionLost(self, reason):
378 if not self.factory.msncon: return # If we called logOut
379 def wait():
380 LogEvent(INFO, self.factory.msncon.ident)
381 msn.NotificationClient.connectionLost(self, reason)
382 if self.factory.maxRetries >= self.factory.retries:
383 self.factory.msncon.connectionLost(reason)
384 # Make sure this event is handled after any others
385 reactor.callLater(0, wait)
386
387 def gotMSNAlert(self, body, action, subscr):
388 LogEvent(INFO, self.factory.msncon.ident)
389 self.factory.msncon.gotMSNAlert(body, action, subscr)
390
391 def gotInitialEmailNotification(self, inboxunread, foldersunread):
392 LogEvent(INFO, self.factory.msncon.ident)
393 self.factory.msncon.gotInitialEmailNotification(inboxunread, foldersunread)
394
395 def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
396 LogEvent(INFO, self.factory.msncon.ident)
397 self.factory.msncon.gotRealtimeEmailNotification(mailfrom, fromaddr, subject)
398
399 def userAddedMe(self, userGuid, userHandle, screenName):
400 LogEvent(INFO, self.factory.msncon.ident)
401 self.factory.msncon.contactAddedMe(userHandle)
402
403 def userRemovedMe(self, userHandle):
404 LogEvent(INFO, self.factory.msncon.ident)
405 self.factory.msncon.contactRemovedMe(userHandle)
406
407 def listSynchronized(self, *args):
408 LogEvent(INFO, self.factory.msncon.ident)
409 self.factory.msncon._sendSavedEvents()
410 self.factory.msncon.listSynchronized()
411
412 def contactAvatarChanged(self, userHandle, hash):
413 LogEvent(INFO, self.factory.msncon.ident)
414 self.factory.msncon.contactAvatarChanged(userHandle, hash)
415
416 def gotContactStatus(self, userHandle, statusCode, screenName):
417 LogEvent(INFO, self.factory.msncon.ident)
418 self.factory.msncon.contactStatusChanged(userHandle)
419
420 def contactStatusChanged(self, userHandle, statusCode, screenName):
421 LogEvent(INFO, self.factory.msncon.ident)
422 self.factory.msncon.contactStatusChanged(userHandle)
423
424 def contactPersonalChanged(self, userHandle, personal):
425 LogEvent(INFO, self.factory.msncon.ident)
426 self.factory.msncon.contactStatusChanged(userHandle)
427
428 def contactOffline(self, userHandle):
429 LogEvent(INFO, self.factory.msncon.ident)
430 self.factory.msncon.contactStatusChanged(userHandle)
431
432 def gotSwitchboardInvitation(self, sessionID, host, port, key, userHandle, screenName):
433 LogEvent(INFO, self.factory.msncon.ident)
434 sb = self.factory.msncon.switchboardSessions.get(userHandle)
435 if sb and sb.transport:
436 sb.transport.loseConnection()
437 else:
438 sb = OneSwitchboardSession(self.factory.msncon, userHandle)
439 self.factory.msncon.switchboardSessions[userHandle] = sb
440 sb.connectReply(host, port, key, sessionID)
441
442 def multipleLogin(self):
443 LogEvent(INFO, self.factory.msncon.ident)
444 self.factory.msncon.multipleLogin()
445
446 def serverGoingDown(self):
447 LogEvent(INFO, self.factory.msncon.ident)
448 self.factory.msncon.serverGoingDown()
449
450
451
452 class SwitchboardSessionBase(msn.SwitchboardClient):
453 def __init__(self, msncon):
454 msn.SwitchboardClient.__init__(self)
455 self.msncon = msncon
456 self.msnobj = msncon.notificationClient.msnobj
457 self.userHandle = msncon.username
458 self.ident = (msncon.ident, "INVALID!!")
459 self.messageBuffer = []
460 self.funcBuffer = []
461 self.ready = False
462
463 def __del__(self):
464 LogEvent(INFO, self.ident)
465 del self.msncon
466 self.transport.disconnect()
467
468 def connectionLost(self, reason):
469 self.msncon = None
470 self.msnobj = None
471 self.ident = (self.ident[0], self.ident[1] + " Disconnected!")
472
473 def loggedIn(self):
474 LogEvent(INFO, self.ident)
475 self.ready = True
476 self.flushBuffer()
477
478 def connect(self):
479 LogEvent(INFO, self.ident)
480 self.ready = False
481 def sbRequestAccepted((host, port, key)):
482 LogEvent(INFO, self.ident)
483 self.key = key
484 self.reply = 0
485 factory = ClientFactory()
486 factory.buildProtocol = lambda addr: self
487 reactor.connectTCP(host, port, factory)
488 def sbRequestFailed(ignored=None):
489 LogEvent(INFO, self.ident)
490 del self.msncon.switchboardSessions[self.remoteUser]
491 d = self.msncon.notificationClient.requestSwitchboardServer()
492 d.addCallbacks(sbRequestAccepted, sbRequestFailed)
493
494 def connectReply(self, host, port, key, sessionID):
495 LogEvent(INFO, self.ident)
496 self.ready = False
497 self.key = key
498 self.sessionID = sessionID
499 self.reply = 1
500 factory = ClientFactory()
501 factory.buildProtocol = lambda addr: self
502 reactor.connectTCP(host, port, factory)
503
504 def flushBuffer(self):
505 for message, noerror in self.messageBuffer[:]:
506 self.messageBuffer.remove((message, noerror))
507 self.sendMessage(message, noerror)
508 for f in self.funcBuffer[:]:
509 self.funcBuffer.remove(f)
510 f()
511
512 def failedMessage(self, *ignored):
513 raise NotImplementedError
514
515 def sendClientCaps(self):
516 message = msn.MSNMessage()
517 message.setHeader("Content-Type", "text/x-clientcaps")
518 message.setHeader("Client-Name", "PyMSNt")
519 if hasattr(self.msncon, "jabberID"):
520 message.setHeader("JabberID", str(self.msncon.jabberID))
521 self.sendMessage(message)
522
523 def sendMessage(self, message, noerror=False):
524 # Check to make sure that clientcaps only gets sent after
525 # the first text type message.
526 if isinstance(message, msn.MSNMessage) and message.getHeader("Content-Type").startswith("text"):
527 self.sendMessage = self.sendMessageReal
528 self.sendClientCaps()
529 return self.sendMessage(message, noerror)
530 else:
531 return self.sendMessageReal(message, noerror)
532
533 def sendMessageReal(self, text, noerror=False):
534 if not isinstance(text, basestring):
535 msn.SwitchboardClient.sendMessage(self, text)
536 return
537 if not self.ready:
538 self.messageBuffer.append((text, noerror))
539 else:
540 LogEvent(INFO, self.ident)
541 text = str(text.replace("\n", "\r\n").encode("utf-8"))
542 def failedMessage(ignored):
543 if not noerror:
544 self.failedMessage(text)
545
546 if len(text) < MSNConnection.MAXMESSAGESIZE:
547 message = msn.MSNMessage(message=text)
548 message.ack = msn.MSNMessage.MESSAGE_NACK
549
550 d = msn.SwitchboardClient.sendMessage(self, message)
551 if not noerror:
552 d.addCallback(failedMessage)
553
554 else:
555 chunks = int(math.ceil(len(text) / float(MSNConnection.MAXMESSAGESIZE)))
556 chunk = 0
557 guid = msn.random_guid()
558 while chunk < chunks:
559 offset = chunk * MSNConnection.MAXMESSAGESIZE
560 message = msn.MSNMessage(message=text[offset : offset + MSNConnection.MAXMESSAGESIZE])
561 message.ack = msn.MSNMessage.MESSAGE_NACK
562 message.setHeader("Message-ID", guid)
563 if chunk == 0:
564 message.setHeader("Chunks", str(chunks))
565 else:
566 message.delHeader("MIME-Version")
567 message.delHeader("Content-Type")
568 message.setHeader("Chunk", str(chunk))
569
570 d = msn.SwitchboardClient.sendMessage(self, message)
571 if not noerror:
572 d.addCallback(failedMessage)
573
574 chunk += 1
575
576
577 class MultiSwitchboardSession(SwitchboardSessionBase):
578 """ Create one of me to chat to multiple contacts """
579
580 def __init__(self, msncon):
581 """ Automatically creates a new switchboard connection to the server """
582 SwitchboardSessionBase.__init__(self, msncon)
583 self.ident = (self.msncon.ident, self)
584 self.contactCount = 0
585 self.groupchat = None
586 self.connect()
587
588 def failedMessage(self, text):
589 self.groupchat.gotMessage("BOUNCE", text)
590
591 def sendMessage(self, text, noerror=False):
592 """ Used to send a mesage to the groupchat. Can be called immediately
593 after instantiation. """
594 if self.contactCount > 0:
595 SwitchboardSessionBase.sendMessage(self, text, noerror)
596 else:
597 #self.messageBuffer.append((message, noerror))
598 pass # They're sending messages to an empty room. Ignore.
599
600 def inviteUser(self, userHandle):
601 """ Used to invite a contact to the groupchat. Can be called immediately
602 after instantiation. """
603 userHandle = str(userHandle)
604 if self.ready:
605 LogEvent(INFO, self.ident, "immediate")
606 msn.SwitchboardClient.inviteUser(self, userHandle)
607 else:
608 LogEvent(INFO, self.ident, "pending")
609 self.funcBuffer.append(lambda: msn.SwitchboardClient.inviteUser(self, userHandle))
610
611 def gotMessage(self, message):
612 self.groupchat.gotMessage(message.userHandle, message.getMessage())
613
614 def userJoined(self, userHandle, screenName=''):
615 LogEvent(INFO, self.ident)
616 self.contactCount += 1
617 self.groupchat.contactJoined(userHandle)
618
619 def userLeft(self, userHandle):
620 LogEvent(INFO, self.ident)
621 self.contactCount -= 1
622 self.groupchat.contactLeft(userHandle)
623
624
625
626 class OneSwitchboardSession(SwitchboardSessionBase):
627 def __init__(self, msncon, remoteUser):
628 SwitchboardSessionBase.__init__(self, msncon)
629 self.remoteUser = str(remoteUser)
630 self.ident = (self.msncon.ident, self.remoteUser)
631 self.chattingUsers = []
632 self.timeout = None
633
634 def __del__(self):
635 if self.timeout:
636 self.timeout.cancel()
637 self.timeout = None
638 for message, noerror in self.messageBuffer:
639 if not noerror:
640 self.failedMessage(message)
641
642 def _ready(self):
643 LogEvent(INFO, self.ident)
644 self.ready = True
645 for user in self.chattingUsers:
646 self.userJoined(user)
647 if self.timeout:
648 self.timeout.cancel()
649 self.timeout = None
650 self.flushBuffer()
651
652 def _switchToMulti(self, userHandle):
653 LogEvent(INFO, self.ident)
654 del self.msncon.switchboardSessions[self.remoteUser]
655 self.__class__ = MultiSwitchboardSession
656 del self.remoteUser
657 self.contactCount = 0
658 self.msncon.gotGroupchat(self, userHandle)
659 assert self.groupchat
660
661 def failedMessage(self, text):
662 self.msncon.failedMessage(self.remoteUser, text)
663
664 # Callbacks
665 def loggedIn(self):
666 LogEvent(INFO, self.ident)
667 if not self.reply:
668 def failCB(arg=None):
669 if not (self.msncon and self.msncon.switchboardSessions.has_key(self.remoteUser)):
670 return
671 LogEvent(INFO, self.ident, "User has not joined after 30 seconds.")
672 del self.msncon.switchboardSessions[self.remoteUser]
673 self.timeout = None
674 d = self.inviteUser(self.remoteUser)
675 d.addErrback(failCB)
676 self.timeout = reactor.callLater(30.0, failCB)
677 else:
678 self._ready()
679
680 def gotChattingUsers(self, users):
681 for userHandle in users.keys():
682 self.chattingUsers.append(userHandle)
683
684 def userJoined(self, userHandle, screenName=''):
685 LogEvent(INFO, self.ident)
686 if not self.reply:
687 self._ready()
688 if userHandle != self.remoteUser:
689 # Another user has joined, so we now have three participants.
690 remoteUser = self.remoteUser
691 self._switchToMulti(remoteUser)
692 self.userJoined(remoteUser)
693 self.userJoined(userHandle)
694 else:
695 def updateAvatarCB((imageData, )):
696 if self.msncon:
697 self.msncon.gotAvatarImageData(self.remoteUser, imageData)
698 d = self.sendAvatarRequest()
699 if d:
700 d.addCallback(updateAvatarCB)
701
702 def userLeft(self, userHandle):
703 def wait():
704 if userHandle == self.remoteUser:
705 if self.msncon and self.msncon.switchboardSessions.has_key(self.remoteUser):
706 del self.msncon.switchboardSessions[self.remoteUser]
707 reactor.callLater(0, wait) # Make sure this is handled after everything else
708
709 def gotMessage(self, message):
710 LogEvent(INFO, self.ident)
711 cTypes = [s.strip() for s in message.getHeader("Content-Type").split(';')]
712 if "text/plain" == cTypes[0]:
713 try:
714 if len(cTypes) > 1 and cTypes[1].lower().find("utf-8") >= 0:
715 text = message.getMessage().decode("utf-8")
716 else:
717 text = message.getMessage()
718 self.msncon.gotMessage(self.remoteUser, text)
719 except:
720 self.msncon.gotMessage(self.remoteUser, "A message was lost.")
721 raise
722 elif "text/x-clientcaps" == cTypes[0]:
723 if message.hasHeader("JabberID"):
724 jid = message.getHeader("JabberID")
725 self.msncon.userMapping(message.userHandle, jid)
726 else:
727 LogEvent(INFO, self.ident, "Discarding unknown message type.")
728
729 def gotFileReceive(self, fileReceive):
730 LogEvent(INFO, self.ident)
731 self.msncon.gotFileReceive(fileReceive)
732
733 def gotContactTyping(self, message):
734 LogEvent(INFO, self.ident)
735 self.msncon.gotContactTyping(message.userHandle)
736
737 def sendTypingNotification(self):
738 LogEvent(INFO, self.ident)
739 if self.ready:
740 msn.SwitchboardClient.sendTypingNotification(self)
741
742 CAPS = msn.MSNContact.MSNC1 | msn.MSNContact.MSNC2 | msn.MSNContact.MSNC3 | msn.MSNContact.MSNC4
743 def sendAvatarRequest(self):
744 if not self.ready: return
745 msnContacts = self.msncon.getContacts()
746 if not msnContacts: return
747 msnContact = msnContacts.getContact(self.remoteUser)
748 if not (msnContact and msnContact.caps & self.CAPS and msnContact.msnobj): return
749 if msnContact.msnobjGot: return
750 msnContact.msnobjGot = True # This is deliberately set before we get the avatar. So that we don't try to reget failed avatars over & over
751 return msn.SwitchboardClient.sendAvatarRequest(self, msnContact)
752
753 def sendFile(self, msnContact, filename, filesize):
754 def doSendFile(ignored=None):
755 d.callback(msn.SwitchboardClient.sendFile(self, msnContact, filename, filesize))
756 d = Deferred()
757 if self.ready:
758 reactor.callLater(0, doSendFile)
759 else:
760 self.funcBuffer.append(doSendFile)
761 return d
762