# chat.py -- Chat, ChatCallbacks classes # # Copyright (C) 2003 Manish Jethani (manish_jethani AT yahoo.com) # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import select import email import email.Message import email.Charset from string import split, join from session import _Session from command import Command, Msg from codec import url_codec import protocol class ChatCallbacks: """Callback interface for MSN chat The client must implement some or all of these methods to receive notifications on chat events. The value of Chat.callbacks must be set after receiving the Chat instance in SessionCallbacks.chat_started """ def friend_joined(self, passport_id, display_name): """Friend has joined the chat Keyword arguments: passport_id -- string representing friend's passport ID display_name -- friend's display name """ def friend_left(self, passport_id): """Friend has left the chat Keyword arguments: passport_id -- string representing friend's passport ID """ def message_received(self, passport_id, display_name, text, charset): """Message received Keyword arguments: passport_id -- string representing friend's passport ID display_name -- friend's display name text -- message text charset -- character set of text (usu. utf-8) """ def typing_received(self, passport_id, display_name): """Friend is typing Keyword arguments: passport_id -- string representing friend's passport ID display_name -- friend's display name """ class Chat(_Session): """MSN chat conversation When a conversation is started, an instance of Chat is created and passed on to the SessionCallbacks.chat_started method. """ def __init__(self, session, server, hash, passport_id, display_name, session_id = None, invitee = None): _Session.__init__(self, ChatCallbacks()) self.session = session self.hash = hash self.passport_id = passport_id self.display_name = display_name self.session_id = session_id self.transaction_id = 1 self.initial_members = [] self.http_proxy = session.http_proxy conn = self.conn = self._connect(server) if passport_id != session.passport_id: # invited to chat ans = Command('ANS', self.transaction_id, (session.passport_id, hash, session_id)) self._send_cmd(ans, conn) while 1: resp = self._receive_cmd(conn) if resp.cmd == 'ANS': break elif resp.cmd != 'IRO': raise Error(int(resp.cmd), protocol.errors[resp.cmd]) self.initial_members.append((resp.args[2], url_codec.decode(resp.args[3]))) else: # hosting chat usr = Command('USR', self.transaction_id, (passport_id, hash)) resp = self._sync_command(usr, conn) if resp.cmd != 'USR': raise Error(int(resp.cmd), protocol.errors[resp.cmd]) cal = Command('CAL', self.transaction_id, (invitee,)) resp = self._sync_command(cal, conn) if resp.cmd != 'CAL': raise Error(int(resp.cmd), protocol.errors[resp.cmd]) self.session_id = resp.args[1] def leave(self): """Leave a chat conversation This should be the last method called on a Chat instance. """ del self.session.active_chats[self.session_id] self.process() self.conn.break_() self.conn = None def __send_mime_message(self, mime_message, flag): msg = Msg() msg.trn = self.transaction_id msg.msg_buf = '' for hdr in mime_message.items(): msg.msg_buf = msg.msg_buf + join(hdr, ': ') + '\r\n' msg.msg_buf = msg.msg_buf + '\r\n' if mime_message.get_payload() != None: msg.msg_buf = msg.msg_buf + mime_message.get_payload() msg.args = (flag, str(len(msg.msg_buf))) self._async_command(msg) self.process() def send_message(self, text, charset = 'utf-8'): """Send message Keyword arguments: text -- message text charset -- character set of text (default utf-8) """ mime_message = email.Message.Message() mime_message['MIME-Version'] = '1.0' mime_message['Content-Type'] = 'text/plain; charset=' + charset mime_message.set_payload(text) self.__send_mime_message(mime_message, 'N') def send_typing(self): """Send typing notification""" mime_message = email.Message.Message() mime_message['MIME-Version'] = '1.0' mime_message['Content-Type'] = 'text/x-msmsgscontrol' mime_message['TypingUser'] = self.session.passport_id self.__send_mime_message(mime_message, 'U') def process(self): """Process events This method must be called periodically, preferably in the client application's main loop. """ while 1: if self.conn == None: break fd = self.conn.socket.fileno() r = select.select([fd], [], [], 0) if len(r[0]) > 0: buf = self.conn.receive_data_line() if buf == None: # connection closed?! break self.__process_command_buf(buf) elif len(self.send_queue) > 0: cmd = self.send_queue.pop(0) cmd.send(self.conn) else: break def __process_command_buf(self, buf): cmd = buf[:3] if cmd == 'MSG': self.__process_msg(buf) elif cmd == 'JOI': self.__process_joi(buf) elif cmd == 'BYE': self.__process_bye(buf) # TODO error handling def __process_msg(self, buf): msg = Msg() msg.parse(buf) msg.receive(self.conn) mime_message = email.message_from_string(msg.msg_buf) if mime_message.get_content_type() == 'text/plain': self.callbacks.message_received(msg.passport_id, msg.display_name, mime_message.get_payload(), mime_message.get_content_charset()) elif mime_message.get_content_type() == 'text/x-msmsgscontrol': self.callbacks.typing_received(msg.passport_id, msg.display_name) def __process_joi(self, buf): joi = split(buf) self.callbacks.friend_joined(joi[1], url_codec.decode(joi[2])) def __process_bye(self, buf): bye = split(buf) self.callbacks.friend_left(bye[1]) # vim: set ts=4 sw=4 et tw=79 :