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