# 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 :
|