]> code.delx.au - pymsnt/blob - src/tlib/msn/msnw.py
Partially working with new msnw
[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 MAXMESSAGESIZE = 1400
18 SWITCHBOARDTIMEOUT = 30.0*60.0
19 GETALLAVATARS = True
20
21
22 """
23 All interaction should be with the MSNConnection class.
24 You should not directly instantiate any objects of other classes.
25 """
26
27 class MSNConnection:
28 """ Manages all the Twisted factories, etc """
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.notificationClient = None
40 self.connect()
41 LogEvent(INFO, self.ident)
42
43 def connect(self):
44 """ Automatically called by the constructor """
45 self.connectors = []
46 self.switchboardSessions = {}
47 self.savedEvents = SavedEvents() # Save any events that occur before connect
48 self._getNotificationReferral()
49
50 def _getNotificationReferral(self):
51 def timeout():
52 if not d.called: d.errback()
53 self.timeout = reactor.callLater(30, timeout)
54 dispatchFactory = msn.DispatchFactory()
55 dispatchFactory.userHandle = self.username
56 dispatchFactory.protocol = DispatchClient
57 d = Deferred()
58 dispatchFactory.d = d
59 d.addCallbacks(self._gotNotificationReferral, self.connectionFailed)
60 self.connectors.append(reactor.connectTCP("messenger.hotmail.com", 1863, dispatchFactory))
61 LogEvent(INFO, self.ident)
62
63 def _gotNotificationReferral(self, (host, port)):
64 self.timeout.cancel()
65 # Create the NotificationClient
66 self.notificationFactory = msn.NotificationFactory()
67 self.notificationFactory.userHandle = self.username
68 self.notificationFactory.password = self.password
69 self.notificationFactory.msncon = self
70 self.notificationFactory.protocol = NotificationClient
71 self.connectors.append(reactor.connectTCP(host, port, self.notificationFactory))
72 LogEvent(INFO, self.ident)
73
74 def _sendSavedEvents(self):
75 self.savedEvents.send(self)
76 self.savedEvents = None
77
78 def _notificationClientReady(self, notificationClient):
79 self.notificationClient = notificationClient
80
81 def _ensureSwitchboardSession(self, userHandle):
82 if not self.switchboardSessions.has_key(userHandle):
83 sb = OneSwitchboardSession(self, userHandle)
84 sb.connect()
85 self.switchboardSessions[userHandle] = sb
86
87
88
89 # API calls
90 def getContacts(self):
91 """ Gets the contact list.
92
93 @return an instance of MSNContactList (do not modify) if connected,
94 or None if not.
95 """
96 if self.notificationFactory:
97 return self.notificationFactory.contacts
98 else:
99 return None
100
101 def sendMessage(self, userHandle, text, noerror=False):
102 """
103 Sends a message to a contact. Can only be called after listSynchronized().
104
105 @param userHandle: the contact's MSN passport.
106 @param text: the text to send.
107 @param noerror: Set this to True if you don't want failed messages to bounce.
108 """
109 LogEvent(INFO, self.ident)
110 if self.notificationClient:
111 self._ensureSwitchboardSession(userHandle)
112 self.switchboardSessions[userHandle].sendMessage(text, noerror)
113 elif not noerror:
114 self.failedMessage(userHandle, text)
115
116 def sendAvatarRequest(self, userHandle):
117 """
118 Requests the avatar of a contact.
119
120 @param userHandle: the contact to request an avatar from.
121 @return: a Deferred() if the avatar can be fetched at this time.
122 This will fire with an argument of a tuple with the PNG
123 image data as the only element.
124 Otherwise returns None
125 """
126
127 LogEvent(INFO, self.ident)
128 if not self.notificationClient: return
129 if GETALLAVATARS:
130 self._ensureSwitchboardSession(userHandle)
131 sb = self.switchboardSessions.get(userHandle)
132 if sb: return sb.sendAvatarRequest()
133
134 def sendFile(self, userHandle, filename, filesize):
135 """
136 Used to send a file to a contact.
137
138 @param username: the passport of the contact to send a file to.
139 @param filename: the name of the file to send.
140 @param filesize: the size of the file to send.
141
142 @return: A Deferred, which will fire with an argument of:
143 (fileSend, d) A FileSend object and a Deferred.
144 The Deferred will pass one argument in a tuple,
145 whether or not the transfer is accepted. If you
146 receive a True, then you can call write() on the
147 fileSend object to send your file. Call close()
148 when the file is done.
149 NOTE: You MUST write() exactly as much as you
150 declare in filesize.
151 """
152 msnContact = self.getContacts().getContact(userHandle)
153 if not msnContact:
154 raise ValueError, "Contact not found"
155 self._ensureSwitchboardSession(userHandle)
156 return self.switchboardSessions[userHandle].sendFile(msnContact, filename, filesize)
157
158 def sendTypingToContact(self, userHandle):
159 """
160 Sends typing notification to a contact.
161 @param userHandle: the contact to notify of our typing.
162 """
163
164 sb = self.switchboardSessions.get(userHandle)
165 if sb: return sb.sendTypingNotification()
166
167 def changeAvatar(self, imageData):
168 """
169 Changes the user's avatar.
170 @param imageData: the new PNG avatar image data.
171 """
172 if self.notificationClient:
173 LogEvent(INFO, self.ident)
174 self.notificationClient.changeAvatar(imageData, push=True)
175 else:
176 self.savedEvents.avatarImageData = imageData
177
178 def changeStatus(self, statusCode, screenName, personal):
179 """
180 Changes your status details. All details must be given with
181 each call. This can be called before connection if you wish.
182
183 @param statusCode: the user's new status (look in msn.statusCodes).
184 @param screenName: the user's new screenName (up to 127 characters).
185 @param personal: the user's new personal message.
186 """
187
188 if self.notificationClient:
189 count = 0
190 def cb(ignored=None):
191 if count == 3:
192 self.ourStatusChanged(statusCode, screenName, personal)
193 LogEvent(INFO, self.ident)
194 self.notificationClient.changeStatus(statusCode.encode("utf-8")).addCallback(cb)
195 self.notificationClient.changeScreenName(screenName.encode("utf-8")).addCallback(cb)
196 if not personal: personal = ""
197 self.notificationClient.changePersonalMessage(personal.encode("utf-8"))
198 else:
199 self.savedEvents.statusCode = statusCode
200 self.savedEvents.screenName = screenName
201 self.savedEvents.personal = personal
202
203 def addContact(self, listType, userHandle):
204 """ See msn.NotificationClient.addContact """
205 if self.notificationClient:
206 return self.notificationClient.addContact(listType, str(userHandle))
207 else:
208 self.savedEvents.addContacts.append((listType, str(userHandle)))
209
210 def remContact(self, listType, userHandle):
211 """ See msn.NotificationClient.remContact """
212 if self.notificationClient:
213 return self.notificationClient.remContact(listType, str(userHandle))
214 else:
215 self.savedEvents.remContacts.append((listType, str(userHandle)))
216
217 def logOut(self):
218 """ Shuts down the whole connection. Don't try to call any
219 other methods after this one. """
220 if self.notificationClient:
221 self.notificationClient.logOut()
222 for c in self.connectors:
223 c.disconnect()
224 if self.notificationFactory:
225 self.notificationFactory.msncon = None
226 self.connectors = []
227 self.switchboardSessions = {}
228 LogEvent(INFO, self.ident)
229
230
231 # Reimplement these!
232 def connectionFailed(self, reason=''):
233 """ Called when the connection to the server failed. """
234
235 def connectionLost(self, reason=''):
236 """ Called when we are disconnected. """
237
238 def multipleLogin(self):
239 """ Called when the server says there has been another login
240 for this account. """
241
242 def serverGoingDown(self):
243 """ Called when the server says that it will be going down. """
244
245 def accountNotVerified(self):
246 """ Called if this passport has not been verified. Certain
247 functions are not available. """
248
249 def userMapping(self, passport, jid):
250 """ Called when it is brought to our attention that one of the
251 MSN contacts has a Jabber ID. You should communicate with Jabber. """
252
253 def loggedIn(self):
254 """ Called when we have authenticated, but before we receive
255 the contact list. """
256
257 def listSynchronized(self):
258 """ Called when we have received the contact list. All methods
259 in this class are now valid. """
260
261 def ourStatusChanged(self, statusCode, screenName, personal):
262 """ Called when the user's status has changed. """
263
264 def gotMessage(self, userHandle, text):
265 """ Called when a contact sends us a message """
266
267 def gotContactTyping(self, userHandle):
268 """ Called when a contact sends typing notification """
269
270 def failedMessage(self, userHandle, text):
271 """ Called when a message we sent has been bounced back. """
272
273 def contactAvatarChanged(self, userHandle, hash):
274 """ Called when we receive a changed avatar hash for a contact.
275 You should call sendAvatarRequest(). """
276
277 def contactStatusChanged(self, userHandle, statusCode, screenName):
278 """ Called when we receive status information for a contact. """
279
280 def contactPersonalChanged(self, personal):
281 """ Called when we receive a new personal message for a contact. """
282
283 def gotFileReceive(self, fileReceive):
284 """ Called when a contact sends the user a file.
285 Call accept(fileHandle) or reject() on the object. """
286
287 def contactAddedMe(self, userHandle):
288 """ Called when a contact adds the user to their list. """
289
290 def contactRemovedMe(self, userHandle):
291 """ Called when a contact removes the user from their list. """
292
293
294 class SavedEvents:
295 def __init__(self):
296 self.nickname = ""
297 self.statusCode = ""
298 self.personal = ""
299 self.avatarImageData = ""
300 self.addContacts = []
301 self.remContacts = []
302
303 def send(self, msncon):
304 if self.avatarImageData:
305 msncon.notificationClient.changeAvatar(self.avatarImageData, push=False)
306 if self.nickname or self.statusCode or self.personal:
307 msncon.changeStatus(self.statusCode, self.nickname, self.personal)
308 for listType, userHandle in self.addContacts:
309 msncon.addContact(listType, userHandle)
310 for listType, userHandle in self.remContacts:
311 msncon.remContact(listType, userHandle)
312
313
314
315 class DispatchClient(msn.DispatchClient):
316 def gotNotificationReferral(self, host, port):
317 if self.factory.d.called: return # Too slow! We've already timed out
318 self.factory.d.callback((host, port))
319
320
321 class NotificationClient(msn.NotificationClient):
322 def loginFailure(self, message):
323 self.factory.msncon.connectionFailed(message)
324
325 def loggedIn(self, userHandle, verified):
326 LogEvent(INFO, self.factory.msncon.ident)
327 msn.NotificationClient.loggedIn(self, userHandle, verified)
328 self.factory.msncon._notificationClientReady(self)
329 self.factory.msncon.loggedIn()
330 if not verified:
331 self.factory.msncon.accountNotVerified()
332
333 def logOut(self):
334 msn.NotificationClient.logOut(self)
335
336 def connectionLost(self, reason):
337 if not self.factory.msncon: return # If we called logOut
338 def wait():
339 LogEvent(INFO, self.factory.msncon.ident)
340 msn.NotificationClient.connectionLost(self, reason)
341 self.factory.msncon.connectionLost(reason)
342 # Make sure this event is handled after any others
343 reactor.callLater(0, wait)
344
345 def gotMSNAlert(self, body, action, subscr):
346 LogEvent(INFO, self.factory.msncon.ident)
347 self.factory.msncon.gotMSNAlert(body, action, subscr)
348
349 def gotInitialEmailNotification(self, inboxunread, foldersunread):
350 LogEvent(INFO, self.factory.msncon.ident)
351 self.factory.msncon.gotInitialEmailNotification(inboxunread, foldersunread)
352
353 def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
354 LogEvent(INFO, self.factory.msncon.ident)
355 self.factory.msncon.gotRealtimeEmailNotification(mailfrom, fromaddr, subject)
356
357 def userAddedMe(self, userGuid, userHandle, screenName):
358 LogEvent(INFO, self.factory.msncon.ident)
359 self.factory.msncon.contactAddedMe(userHandle)
360
361 def userRemovedMe(self, userHandle):
362 LogEvent(INFO, self.factory.msncon.ident)
363 self.factory.msncon.contactRemovedMe(userHandle)
364
365 def listSynchronized(self, *args):
366 LogEvent(INFO, self.factory.msncon.ident)
367 self.factory.msncon._sendSavedEvents()
368 self.factory.msncon.listSynchronized()
369
370 def contactAvatarChanged(self, userHandle, hash):
371 LogEvent(INFO, self.factory.msncon.ident)
372 self.factory.msncon.contactAvatarChanged(userHandle, hash)
373
374 def gotContactStatus(self, userHandle, statusCode, screenName):
375 LogEvent(INFO, self.factory.msncon.ident)
376 self.factory.msncon.contactStatusChanged(userHandle, statusCode, screenName)
377
378 def contactStatusChanged(self, userHandle, statusCode, screenName):
379 LogEvent(INFO, self.factory.msncon.ident)
380 self.factory.msncon.contactStatusChanged(userHandle, statusCode, screenName)
381
382 def contactOffline(self, userHandle):
383 LogEvent(INFO, self.factory.msncon.ident)
384 self.factory.msncon.contactStatusChanged(userHandle, msn.STATUS_OFFLINE, "")
385
386 def gotSwitchboardInvitation(self, sessionID, host, port, key, userHandle, screenName):
387 LogEvent(INFO, self.factory.msncon.ident)
388 sb = self.factory.msncon.switchboardSessions.get(userHandle)
389 if sb:
390 sb.transport.loseConnection()
391 else:
392 sb = OneSwitchboardSession(self.factory.msncon, userHandle)
393 self.factory.msncon.switchboardSessions[userHandle] = sb
394 sb.connectReply(host, port, key, sessionID)
395
396 def multipleLogin(self):
397 LogEvent(INFO, self.factory.msncon.ident)
398 self.factory.msncon.multipleLogin()
399
400 def serverGoingDown(self):
401 LogEvent(INFO, self.factory.msncon.ident)
402 self.factory.msncon.serverGoingDown()
403
404
405
406 def switchToGroupchat(*args):
407 raise NotImplementedError
408
409
410 class SwitchboardSessionBase:
411 def __init__(self, msncon):
412 self.msncon = msncon
413 self.userHandle = msncon.username
414 self.ident = (msncon.ident, "INVALID!!")
415 self.messageBuffer = []
416 self.funcBuffer = []
417 self.ready = False
418
419 def __del__(self):
420 LogEvent(INFO, self.ident)
421 del self.msncon
422 self.transport.disconnect()
423
424 for message, noerror in self.messageBuffer:
425 if not noerror:
426 self.msncon.failedMessage(self.remoteUser, message)
427
428 def connect(self):
429 LogEvent(INFO, self.ident)
430 self.ready = False
431 def sbRequestAccepted((host, port, key)):
432 LogEvent(INFO, self.ident)
433 self.key = key
434 self.reply = 0
435 factory = ClientFactory()
436 factory.buildProtocol = lambda addr: self
437 reactor.connectTCP(host, port, factory)
438 def sbRequestFailed(ignored=None):
439 LogEvent(INFO, self.ident)
440 del self.msncon.switchboardSessions[self.remoteUser]
441 d = self.msncon.notificationClient.requestSwitchboardServer()
442 d.addCallbacks(sbRequestAccepted, sbRequestFailed)
443
444 def connectReply(self, host, port, key, sessionID):
445 LogEvent(INFO, self.ident)
446 self.ready = False
447 self.key = key
448 self.sessionID = sessionID
449 self.reply = 1
450 factory = ClientFactory()
451 factory.buildProtocol = lambda addr: self
452 reactor.connectTCP(host, port, factory)
453
454 def flushBuffer(self):
455 for message, noerror in self.messageBuffer[:]:
456 self.messageBuffer.remove((message, noerror))
457 self.sendMessage(message, noerror)
458 for f in self.funcBuffer[:]:
459 self.funcBuffer.remove(f)
460 f()
461
462 def failedMessage(self, ignored):
463 raise NotImplementedError
464
465 def sendMessage(self, text, noerror=False):
466 if not isinstance(text, (str, unicode)):
467 msn.SwitchboardClient.sendMessage(self, text)
468 return
469 if not self.ready:
470 self.messageBuffer.append((text, noerror))
471 else:
472 LogEvent(INFO, self.ident)
473 def failedMessage(ignored):
474 if not noerror:
475 self.failedMessage(text)
476
477 if len(text) < MAXMESSAGESIZE:
478 message = msn.MSNMessage(message=str(text.replace("\n", "\r\n").encode("utf-8")))
479 message.setHeader("Content-Type", "text/plain; charset=UTF-8")
480 message.ack = msn.MSNMessage.MESSAGE_NACK
481
482 d = msn.SwitchboardClient.sendMessage(self, message)
483 if not noerror:
484 d.addCallback(failedMessage)
485
486 else:
487 chunks = int(math.ceil(len(text) / float(MAXMESSAGESIZE)))
488 chunk = 0
489 guid = msn.random_guid()
490 while chunk < chunks:
491 offset = chunk * MAXMESSAGESIZE
492 text = message[offset : offset + MAXMESSAGESIZE]
493 message = msn.MSNMessage(message=str(text.replace("\n", "\r\n").encode("utf-8")))
494 message.ack = msn.MSNMessage.MESSAGE_NACK
495 if chunk == 0:
496 message.setHeader("Content-Type", "text/plain; charset=UTF-8")
497 message.setHeader("Chunks", str(chunks))
498 else:
499 message.setHeader("Chunk", str(chunk))
500
501 d = msn.SwitchboardClient.sendMessage(self, message)
502 if not noerror:
503 d.addCallback(failedMessage)
504
505 chunk += 1
506
507
508
509 class OneSwitchboardSession(SwitchboardSessionBase, msn.SwitchboardClient):
510 def __init__(self, msncon, remoteUser):
511 SwitchboardSessionBase.__init__(self, msncon)
512 msn.SwitchboardClient.__init__(self)
513 self.remoteUser = str(remoteUser)
514 self.ident = (self.msncon, self.remoteUser)
515 self.chattingUsers = []
516 self.timeout = None
517
518 def _ready(self):
519 LogEvent(INFO, self.ident)
520 self.ready = True
521 for user in self.chattingUsers:
522 self.userJoined(user)
523 if self.timeout:
524 self.timeout.cancel()
525 self.timeout = None
526 self.flushBuffer()
527
528 def failedMessage(self, text):
529 self.msncon.failedMessage(self.remoteUser, text)
530
531 # Callbacks
532 def loggedIn(self):
533 LogEvent(INFO, self.ident)
534 if not self.reply:
535 def failCB(arg=None):
536 LogEvent(INFO, ident, "User has not joined after 30 seconds.")
537 del self.msncon.switchboardSessions[self.remoteUser]
538 d = self.inviteUser(self.remoteUser)
539 d.addErrback(failCB)
540 self.timeout = reactor.callLater(30.0, failCB)
541 else:
542 self._ready()
543
544 def gotChattingUsers(self, users):
545 for userHandle in users.keys():
546 self.chattingUsers.append(userHandle)
547
548 def userJoined(self, userHandle, screenName=''):
549 LogEvent(INFO, self.ident)
550 if not self.reply:
551 self._ready()
552 if userHandle != self.remoteUser:
553 # Another user has joined, so we now have three participants.
554 switchToGroupchat(self, self.remoteUser, userHandle)
555
556 def userLeft(self, userHandle):
557 def wait():
558 if userHandle == self.remoteUser:
559 del self.msncon.switchboardSessions[self.remoteUser]
560 reactor.callLater(0, wait) # Make sure this is handled after everything else
561
562 def gotMessage(self, message):
563 LogEvent(INFO, self.ident)
564 self.msncon.gotMessage(self.remoteUser, message.getMessage())
565
566 def gotFileReceive(self, fileReceive):
567 LogEvent(INFO, self.ident)
568 self.msncon.gotFileReceive(fileReceive)
569
570 def gotContactTyping(self, message):
571 LogEvent(INFO, self.ident)
572 self.msncon.gotContactTyping(message.userHandle)
573
574 def sendTypingNotification(self):
575 LogEvent(INFO, self.ident)
576 if self.ready:
577 msn.SwitchboardClient.sendTypingNotification(self)
578
579 CAPS = msn.MSNContact.MSNC1 | msn.MSNContact.MSNC2 | msn.MSNContact.MSNC3 | msn.MSNContact.MSNC4
580 def sendAvatarRequest(self):
581 if not self.ready: return
582 msnContacts = self.msncon.getContacts()
583 if not msnContacts: return
584 msnContact = msnContacts.getContact(self.remoteUser)
585 if not (msnContact and msnContact.caps & self.CAPS and msnContact.msnobj): return
586 if msnContact.msnobjGot: return
587 msnContact.msnobjGot = True # This is deliberately set before we get the avatar. So that we don't try to reget failed avatars over & over
588 msn.SwitchboardClient.sendAvatarRequest(self, msnContact)
589
590 def sendFile(self, msnContact, filename, filesize):
591 def doSendFile(ignored=None):
592 d.callback(msn.SwitchboardClient.sendFile(self, msnContact, filename, filesize))
593 d = Deferred()
594 if self.ready:
595 reactor.callLater(0, doSendFile)
596 else:
597 self.funcBuffer.append(doSendFile)
598 return d
599
600