from twisted.internet.protocol import ClientFactory
# System imports
-import math, base64, binascii, math
+import math, base64, binascii
# Local imports
from debug import LogEvent, INFO, WARN, ERROR
from tlib.msn import msn
-# Imports from msn
-from msn import FORWARD_LIST, ALLOW_LIST, BLOCK_LIST, REVERSE_LIST, PENDING_LIST
-from msn import STATUS_ONLINE, STATUS_OFFLINE, STATUS_HIDDEN, STATUS_IDLE, STATUS_AWAY, STATUS_BUSY, STATUS_BRB, STATUS_PHONE, STATUS_LUNCH
-
-MAXMESSAGESIZE = 1400
-SWITCHBOARDTIMEOUT = 30.0*60.0
-GETALLAVATARS = True
"""
-All interaction should be with the MSNConnection class.
+All interaction should be with the MSNConnection and MultiSwitchboardSession classes.
You should not directly instantiate any objects of other classes.
"""
class MSNConnection:
""" Manages all the Twisted factories, etc """
+ MAXMESSAGESIZE = 1400
+ SWITCHBOARDTIMEOUT = 30.0*60.0
+ GETALLAVATARS = False
+
def __init__(self, username, password, ident):
""" Connects to the MSN servers.
@param username: the MSN passport to connect with.
self.password = password
self.ident = ident
self.timeout = None
+ self.notificationFactory = None
+ self.notificationClient = None
self.connect()
LogEvent(INFO, self.ident)
def _getNotificationReferral(self):
def timeout():
- if not d.called: d.errback()
+ if not d.called:
+ d.errback()
+ self.logOut() # Clean up everything
self.timeout = reactor.callLater(30, timeout)
dispatchFactory = msn.DispatchFactory()
dispatchFactory.userHandle = self.username
LogEvent(INFO, self.ident)
if not self.notificationClient: return
- if GETALLAVATARS:
+ if MSNConnection.GETALLAVATARS:
self._ensureSwitchboardSession(userHandle)
sb = self.switchboardSessions.get(userHandle)
if sb: return sb.sendAvatarRequest()
@return: A Deferred, which will fire with an argument of:
(fileSend, d) A FileSend object and a Deferred.
- The Deferred will pass one argument in a tuple,
+ The new Deferred will pass one argument in a tuple,
whether or not the transfer is accepted. If you
receive a True, then you can call write() on the
fileSend object to send your file. Call close()
def sendTypingToContact(self, userHandle):
"""
- Sends typing notification to a contact.
+ Sends typing notification to a contact. Should send every 5secs.
@param userHandle: the contact to notify of our typing.
"""
@param personal: the user's new personal message.
"""
+ if not screenName: screenName = self.username
if self.notificationClient:
- count = 0
- def cb():
- if count == 3:
+ changeCount = [0] # Hack
+ def cb(ignored=None):
+ changeCount[0] += 1
+ if changeCount[0] == 3:
self.ourStatusChanged(statusCode, screenName, personal)
LogEvent(INFO, self.ident)
self.notificationClient.changeStatus(statusCode.encode("utf-8")).addCallback(cb)
self.notificationClient.changeScreenName(screenName.encode("utf-8")).addCallback(cb)
if not personal: personal = ""
- self.notificationClient.changePersonalMessage(personal.encode("utf-8"))
+ self.notificationClient.changePersonalMessage(personal.encode("utf-8")).addCallback(cb)
else:
self.savedEvents.statusCode = statusCode
self.savedEvents.screenName = screenName
def logOut(self):
""" Shuts down the whole connection. Don't try to call any
- other methods after this one. """
+ other methods after this one. Except maybe connect() """
if self.notificationClient:
self.notificationClient.logOut()
for c in self.connectors:
if self.notificationFactory:
self.notificationFactory.msncon = None
self.connectors = []
+ for sbs in self.switchboardSessions.values():
+ if hasattr(sbs, "transport") and sbs.transport:
+ sbs.transport.loseConnection()
self.switchboardSessions = {}
LogEvent(INFO, self.ident)
def connectionFailed(self, reason=''):
""" Called when the connection to the server failed. """
+ def loginFailed(self, reason=''):
+ """ Called when the account could not be logged in. """
+
def connectionLost(self, reason=''):
""" Called when we are disconnected. """
def gotMessage(self, userHandle, text):
""" Called when a contact sends us a message """
+ def gotGroupchat(self, msnGroupchat, userHandle):
+ """ Called when a conversation with more than one contact begins.
+ userHandle is the person who invited us.
+ The overriding method is expected to set msnGroupchat.groupchat to an object
+ that implements the following methods:
+ contactJoined(userHandle)
+ contactLeft(userHandle)
+ gotMessage(userHandle, text)
+
+ The object received as 'msnGroupchat' is an instance of MultiSwitchboardSession.
+ """
+
def gotContactTyping(self, userHandle):
- """ Called when a contact sends typing notification """
+ """ Called when a contact sends typing notification.
+ Will be called once every 5 seconds. """
def failedMessage(self, userHandle, text):
""" Called when a message we sent has been bounced back. """
""" Called when we receive a changed avatar hash for a contact.
You should call sendAvatarRequest(). """
- def contactStatusChanged(self, userHandle, statusCode, screenName):
+ def contactStatusChanged(self, userHandle):
""" Called when we receive status information for a contact. """
- def contactPersonalChanged(self, personal):
- """ Called when we receive a new personal message for a contact. """
-
def gotFileReceive(self, fileReceive):
""" Called when a contact sends the user a file.
Call accept(fileHandle) or reject() on the object. """
def contactRemovedMe(self, userHandle):
""" Called when a contact removes the user from their list. """
+
+ def gotInitialEmailNotification(self, inboxunread, foldersunread):
+ """ Received at login to tell about the user's Hotmail status """
+
+ def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
+ """ Received in realtime whenever an email comes into the hotmail account """
+
+ def gotMSNAlert(self, body, action, subscr):
+ """ An MSN Alert (http://alerts.msn.com) was received. Body is the
+ text of the alert. 'action' is a url for more information,
+ 'subscr' is a url to modify your your alerts subscriptions. """
+
+ def gotAvatarImageData(self, userHandle, imageData):
+ """ An contact's avatar has been received because a switchboard
+ session with them was started. """
class SavedEvents:
def __init__(self):
- self.nickname = ""
+ self.screenName = ""
self.statusCode = ""
self.personal = ""
self.avatarImageData = ""
def send(self, msncon):
if self.avatarImageData:
msncon.notificationClient.changeAvatar(self.avatarImageData, push=False)
- if self.nickname or self.statusCode or self.personal:
- msncon.changeStatus(self.statusCode, self.nickname, self.personal)
+ if self.screenName or self.statusCode or self.personal:
+ msncon.changeStatus(self.statusCode, self.screenName, self.personal)
for listType, userHandle in self.addContacts:
msncon.addContact(listType, userHandle)
for listType, userHandle in self.remContacts:
class NotificationClient(msn.NotificationClient):
def loginFailure(self, message):
- self.factory.msncon.connectionFailed(message)
+ self.factory.msncon.loginFailed(message)
def loggedIn(self, userHandle, verified):
LogEvent(INFO, self.factory.msncon.ident)
msn.NotificationClient.loggedIn(self, userHandle, verified)
self.factory.msncon._notificationClientReady(self)
+ self.factory.msncon.loggedIn()
if not verified:
self.factory.msncon.accountNotVerified()
def gotContactStatus(self, userHandle, statusCode, screenName):
LogEvent(INFO, self.factory.msncon.ident)
- self.factory.msncon.contactStatusChanged(userHandle, statusCode, screenName)
+ self.factory.msncon.contactStatusChanged(userHandle)
def contactStatusChanged(self, userHandle, statusCode, screenName):
LogEvent(INFO, self.factory.msncon.ident)
- self.factory.msncon.contactStatusChanged(userHandle, statusCode, screenName)
+ self.factory.msncon.contactStatusChanged(userHandle)
- def contactOffline(self, userHandle):
+ def contactPersonalChanged(self, userHandle, personal):
LogEvent(INFO, self.factory.msncon.ident)
- self.factory.msncon.contactStatusChanged(userHandle, msn.STATUS_OFFLINE, "")
+ self.factory.msncon.contactStatusChanged(userHandle)
+ def contactOffline(self, userHandle):
+ LogEvent(INFO, self.factory.msncon.ident)
+ self.factory.msncon.contactStatusChanged(userHandle)
+
def gotSwitchboardInvitation(self, sessionID, host, port, key, userHandle, screenName):
LogEvent(INFO, self.factory.msncon.ident)
sb = self.factory.msncon.switchboardSessions.get(userHandle)
- if sb:
+ if sb and sb.transport:
sb.transport.loseConnection()
else:
sb = OneSwitchboardSession(self.factory.msncon, userHandle)
-def switchToGroupchat(*args):
- raise NotImplementedError
-
-
-class SwitchboardSessionBase:
+class SwitchboardSessionBase(msn.SwitchboardClient):
def __init__(self, msncon):
+ msn.SwitchboardClient.__init__(self)
self.msncon = msncon
+ self.msnobj = msncon.notificationClient.msnobj
self.userHandle = msncon.username
self.ident = (msncon.ident, "INVALID!!")
self.messageBuffer = []
del self.msncon
self.transport.disconnect()
- for message, noerror in self.messageBuffer:
- if not noerror:
- self.msncon.failedMessage(self.remoteUser, message)
+ def loggedIn(self):
+ LogEvent(INFO, self.ident)
+ self.ready = True
+ self.flushBuffer()
def connect(self):
LogEvent(INFO, self.ident)
self.funcBuffer.remove(f)
f()
- def failedMessage(self, ignored):
+ def failedMessage(self, *ignored):
raise NotImplementedError
- def sendMessage(self, text, noerror=False):
- if not isinstance(text, (str, unicode)):
+ def sendClientCaps(self):
+ message = msn.MSNMessage()
+ message.setHeader("Content-Type", "text/x-clientcaps")
+ message.setHeader("Client-Name", "PyMSNt")
+ if hasattr(self.msncon, "jabberID"):
+ message.setHeader("JabberID", str(self.msncon.jabberID))
+ self.sendMessage(message)
+
+ def sendMessage(self, message, noerror=False):
+ # Check to make sure that clientcaps only gets sent after
+ # the first text type message.
+ if isinstance(message, msn.MSNMessage) and message.getHeader("Content-Type").startswith("text"):
+ self.sendMessage = self.sendMessageReal
+ self.sendClientCaps()
+ return self.sendMessage(message, noerror)
+ else:
+ return self.sendMessageReal(message, noerror)
+
+ def sendMessageReal(self, text, noerror=False):
+ if not isinstance(text, basestring):
msn.SwitchboardClient.sendMessage(self, text)
return
if not self.ready:
if not noerror:
self.failedMessage(text)
- if len(text) < MAXMESSAGESIZE:
+ if len(text) < MSNConnection.MAXMESSAGESIZE:
message = msn.MSNMessage(message=str(text.replace("\n", "\r\n").encode("utf-8")))
message.setHeader("Content-Type", "text/plain; charset=UTF-8")
message.ack = msn.MSNMessage.MESSAGE_NACK
d.addCallback(failedMessage)
else:
- chunks = int(math.ceil(len(text) / float(MAXMESSAGESIZE)))
+ chunks = int(math.ceil(len(text) / float(MSNConnection.MAXMESSAGESIZE)))
chunk = 0
guid = msn.random_guid()
while chunk < chunks:
- offset = chunk * MAXMESSAGESIZE
- text = message[offset : offset + MAXMESSAGESIZE]
+ offset = chunk * MSNConnection.MAXMESSAGESIZE
+ text = message[offset : offset + MSNConnection.MAXMESSAGESIZE]
message = msn.MSNMessage(message=str(text.replace("\n", "\r\n").encode("utf-8")))
message.ack = msn.MSNMessage.MESSAGE_NACK
if chunk == 0:
chunk += 1
+class MultiSwitchboardSession(SwitchboardSessionBase):
+ """ Create one of me to chat to multiple contacts """
-class OneSwitchboardSession(SwitchboardSessionBase, msn.SwitchboardClient):
+ def __init__(self, msncon):
+ """ Automatically creates a new switchboard connection to the server """
+ SwitchboardSessionBase.__init__(self, msncon)
+ self.ident = (self.msncon.ident, self)
+ self.contactCount = 0
+ self.groupchat = None
+ self.connect()
+
+ def failedMessage(self, text):
+ self.groupchat.gotMessage("BOUNCE", text)
+
+ def sendMessage(self, text, noerror=False):
+ """ Used to send a mesage to the groupchat. Can be called immediately
+ after instantiation. """
+ if self.contactCount > 0:
+ SwitchboardSessionBase.sendMessage(self, text, noerror)
+ else:
+ #self.messageBuffer.append((message, noerror))
+ pass # They're sending messages to an empty room. Ignore.
+
+ def inviteUser(self, userHandle):
+ """ Used to invite a contact to the groupchat. Can be called immediately
+ after instantiation. """
+ userHandle = str(userHandle)
+ if self.ready:
+ LogEvent(INFO, self.ident, "immediate")
+ msn.SwitchboardClient.inviteUser(self, userHandle)
+ else:
+ LogEvent(INFO, self.ident, "pending")
+ self.funcBuffer.append(lambda: msn.SwitchboardClient.inviteUser(self, userHandle))
+
+ def gotMessage(self, message):
+ self.groupchat.gotMessage(message.userHandle, message.getMessage())
+
+ def userJoined(self, userHandle, screenName=''):
+ LogEvent(INFO, self.ident)
+ self.contactCount += 1
+ self.groupchat.contactJoined(userHandle)
+
+ def userLeft(self, userHandle):
+ LogEvent(INFO, self.ident)
+ self.contactCount -= 1
+ self.groupchat.contactLeft(userHandle)
+
+
+
+class OneSwitchboardSession(SwitchboardSessionBase):
def __init__(self, msncon, remoteUser):
SwitchboardSessionBase.__init__(self, msncon)
- msn.SwitchboardClient.__init__(self)
- self.remoteUser = remoteUser
- self.ident = (self.msncon, self.remoteUser)
+ self.remoteUser = str(remoteUser)
+ self.ident = (self.msncon.ident, self.remoteUser)
self.chattingUsers = []
self.timeout = None
+ def __del__(self):
+ if self.timeout:
+ self.timeout.cancel()
+ self.timeout = None
+ for message, noerror in self.messageBuffer:
+ if not noerror:
+ self.failedMessage(message)
+
def _ready(self):
LogEvent(INFO, self.ident)
self.ready = True
self.timeout.cancel()
self.timeout = None
self.flushBuffer()
+
+ def _switchToMulti(self, userHandle):
+ LogEvent(INFO, self.ident)
+ del self.msncon.switchboardSessions[self.remoteUser]
+ self.__class__ = MultiSwitchboardSession
+ del self.remoteUser
+ self.contactCount = 0
+ self.msncon.gotGroupchat(self, userHandle)
+ if not self.groupchat:
+ LogEvent(ERROR, self.ident)
+ raise Exception("YouNeedAGroupchat-WeHaveAProblemError") # FIXME
def failedMessage(self, text):
self.msncon.failedMessage(self.remoteUser, text)
LogEvent(INFO, self.ident)
if not self.reply:
def failCB(arg=None):
- LogEvent(INFO, ident, "User has not joined after 30 seconds.")
+ if not (self.msncon and self.msncon.switchboardSessions.has_key(self.remoteUser)):
+ return
+ LogEvent(INFO, self.ident, "User has not joined after 30 seconds.")
del self.msncon.switchboardSessions[self.remoteUser]
+ self.timeout = None
d = self.inviteUser(self.remoteUser)
d.addErrback(failCB)
self.timeout = reactor.callLater(30.0, failCB)
self._ready()
if userHandle != self.remoteUser:
# Another user has joined, so we now have three participants.
- switchToGroupchat(self, self.remoteUser, userHandle)
+ remoteUser = self.remoteUser
+ self._switchToMulti(remoteUser)
+ self.userJoined(remoteUser)
+ self.userJoined(userHandle)
+ else:
+ def updateAvatarCB((imageData, )):
+ if self.msncon:
+ self.msncon.gotAvatarImageData(self.remoteUser, imageData)
+ d = self.sendAvatarRequest()
+ if d:
+ d.addCallback(updateAvatarCB)
def userLeft(self, userHandle):
def wait():
if userHandle == self.remoteUser:
- del self.msncon.switchboardSessions[self.remoteUser]
+ if self.msncon and self.msncon.switchboardSessions.has_key(self.remoteUser):
+ del self.msncon.switchboardSessions[self.remoteUser]
reactor.callLater(0, wait) # Make sure this is handled after everything else
def gotMessage(self, message):
LogEvent(INFO, self.ident)
- self.msncon.gotMessage(self.remoteUser, message.getMessage())
+ cTypes = [s.strip() for s in message.getHeader("Content-Type").split(';')]
+ if "text/plain" == cTypes[0]:
+ try:
+ if len(cTypes) > 1 and cTypes[1].lower().find("utf-8") >= 0:
+ text = message.getMessage().decode("utf-8")
+ else:
+ text = message.getMessage()
+ self.msncon.gotMessage(self.remoteUser, text)
+ except:
+ self.msncon.gotMessage(self.remoteUser, "A message was lost.")
+ raise
+ elif "text/x-clientcaps" == cTypes[0]:
+ if message.hasHeader("JabberID"):
+ jid = message.getHeader("JabberID")
+ self.msncon.userMapping(message.userHandle, jid)
+ else:
+ LogEvent(INFO, self.ident, "Discarding unknown message type.")
def gotFileReceive(self, fileReceive):
LogEvent(INFO, self.ident)
def sendTypingNotification(self):
LogEvent(INFO, self.ident)
if self.ready:
- msn.SwitchboaldClient.sendTypingNotification(self)
+ msn.SwitchboardClient.sendTypingNotification(self)
CAPS = msn.MSNContact.MSNC1 | msn.MSNContact.MSNC2 | msn.MSNContact.MSNC3 | msn.MSNContact.MSNC4
def sendAvatarRequest(self):
if not (msnContact and msnContact.caps & self.CAPS and msnContact.msnobj): return
if msnContact.msnobjGot: return
msnContact.msnobjGot = True # This is deliberately set before we get the avatar. So that we don't try to reget failed avatars over & over
- msn.SwitchboardClient.sendAvatarRequest(self, msnContact)
+ return msn.SwitchboardClient.sendAvatarRequest(self, msnContact)
def sendFile(self, msnContact, filename, filesize):
def doSendFile(ignored=None):
self.funcBuffer.append(doSendFile)
return d
-