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