# session.py -- Session, SessionCallbacks 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 md5
from string import split,join
from binascii import hexlify,unhexlify
from time import time
from protocol import States,Lists,PrivacyModes
from error import Error,HttpError
from friend import Group,Friend,FriendList
from net import Connection,HttpProxyConnection
from command import Command,Msg,Png,Qry
from codec import url_codec
import protocol
import chat
class _Session: # common base for Session and Chat
def __init__(self, callbacks):
self.callbacks = callbacks
self.transaction_id = 0
self.http_proxy = None
self.conn = None
self.send_queue = []
def _connect(self, server):
conn = None
if self.http_proxy:
conn = HttpProxyConnection(server, self.http_proxy)
else:
conn = Connection(server)
conn.establish()
return conn
def _increment_transaction_id(self):
self.transaction_id = self.transaction_id + 1
return self.transaction_id
def _send_cmd(self, cmd, conn):
conn.send_data_line(str(cmd))
self._increment_transaction_id()
def _receive_cmd(self, conn):
buf = conn.receive_data_line()
if buf == None: # connection closed
raise Error(1, 'Connection closed.')
cmd = Command()
cmd.parse(buf)
return cmd
def _sync_command(self, cmd, conn):
# synchronous command (receive response immediately)
self._send_cmd(cmd, conn)
return self._receive_cmd(conn)
def _async_command(self, cmd):
self.send_queue.append(cmd)
self._increment_transaction_id()
class SessionCallbacks: # callback interface
"""Callback interface for MSN instant messaging session
To receive notification on various protocol events, the client must
implement some or all of the methods in this callback interface.
"""
def ping(self):
"""Ping received from server"""
def state_changed(self, state):
"""User's presence state has changed
Keyword arguments:
state -- any of the msnp.States members
"""
def friend_online(self, state, passport_id, display_name):
"""Friend is online
Keyword arguments:
state -- any of the msnp.States members
passport_id -- string representing friend's passport ID
display_name -- friend's display name
"""
def friend_offline(self, passport_id):
"""Friend is offline
Keyword arguments:
passport_id -- string representing friend's passport ID
"""
def friend_list_updated(self, friend_list):
"""Friend list has been updated
Keyword arguments:
friend_list -- same as msnp.Session.friend_list
"""
def logged_out(self):
"""User has been logged out"""
def group_added(self, id, name):
"""Group has been added
Keyword arguments:
id -- group ID
name -- name of group
"""
def group_removed(self, id):
"""Group has been removed
Keyword arguments:
id -- group ID
"""
def group_renamed(self, id, name):
"""Group has been renamed
Keyword arguments:
id -- group ID
name -- new name of group
"""
def friend_added(self, list_, passport_id, display_name, group_id = -1):
"""Friend has been added
If list_ is msnp.Lists.REVERSE, it means that the user has been added
to someone's list. In that case, the passport_id and display_name
parameters contain information about that someone.
Keyword arguments:
list_ -- type of list (allow, block, etc.)
passport_id -- string representing friend's passport ID
display_name -- friend's display name
group_id -- group ID of group to which friend has been added
"""
def friend_removed(self, list_, passport_id, group_id = -1):
"""Friend has been removed
Keyword arguments:
list_ -- type of list (allow, block, etc.)
passport_id -- string representing friend's passport ID
group_id -- group ID of group from which friend has been removed
"""
def display_name_changed(self, display_name):
"""Display name changed
Keyword arguments:
display_name -- user's new display name
"""
def display_name_received(self, passport_id, display_name):
"""Display name received
Keyword arguments:
passport_id -- string representing friend's passport ID
display_name -- friend's display name
"""
def chat_started(self, chat):
"""Chat started
Keyword arguments:
chat -- Chat instance representing new chat started
"""
class Session(_Session):
"""MSN instant messaging session
To get into an instant messaging session, an instance of msnp.Session must
be created. The session can be started by calling the login method. After
logging in, the process method must be called periodically to process the
server's commands.
"""
class __ChatRequest:
def __init__(self, invitee):
self.invitee = invitee
def __init__(self, callbacks = None, dispatch_server = None):
"""Constructor for msnp.Session
Keyword arguments:
callbacks -- callback interface
dispatch_server -- dispatch server host, port
"""
if callbacks == None:
callbacks = SessionCallbacks()
_Session.__init__(self, callbacks)
if dispatch_server == None:
self.dispatch_server = ('messenger.hotmail.com', 1863)
self.logged_in = 0
self.passport_id = None
self.display_name = None
self.chat_requests = {}
self.friend_list = FriendList()
self.active_chats = {}
def __get_twn_ticket(self, twn_string, username, password):
from net import HTTPSConnection
from urllib import urlencode
debuglevel = 0
# step 1: get address of login server
con = HTTPSConnection('nexus.passport.com',
http_proxy = self.http_proxy)
con.set_debuglevel(debuglevel)
con.request('GET', '/rdr/pprdr.asp')
res = con.getresponse()
con.close()
if res.status != 200:
raise HttpError(0, 'Bad response from passport nexus server.',
res.status, res.reason)
hdr = res.getheader('PassportURLs')
url = {}
for u in hdr.split(','):
k, v = u.split('=')
url[k] = v
dalogin = url['DALogin'].split('/', 1)
# step 2: get "ticket" to notification server
while True:
con = HTTPSConnection(dalogin[0], http_proxy = self.http_proxy)
con.set_debuglevel(debuglevel)
auth = 'Passport1.4 OrgVerb=GET,%s,%s,%s,%s' \
% (urlencode({'OrgURL': 'http://messenger.msn.com'}),
urlencode({'sign-in': username}),
urlencode({'pwd': password}),
twn_string)
con.request('GET', '/%s' % (dalogin[1]), '',
{'Authorization': auth})
res = con.getresponse()
con.close()
if res.status != 200:
raise HttpError(0, 'Bad response from login server.',
res.status, res.reason) # XXX handle redirection?
else:
break
hdr = res.getheader('Authentication-Info') or \
res.getheader('WWW-Authenticate')
hdr = hdr[len('Passport1.4 '):]
auth = {}
for u in hdr.split(','):
k, v = u.split('=', 1)
if v[0] == '\'' and v[-1] == '\'':
v = v[1:-1]
auth[k] = v
ticket = auth['from-PP']
return ticket
# TODO code cleanup
def __handshake(self, server, username, password):
conn = self._connect(server)
try:
ver = Command('VER', self.transaction_id, ('MSNP8', 'CVR0'))
resp = self._sync_command(ver, conn)
if resp.cmd != 'VER' or resp.args[0] == '0':
raise Error(0, 'Bad response for VER command.')
cvr = Command('CVR', self.transaction_id,
('0x0409', 'win', '4.10', 'i386', 'MSNMSGR', '6.0.0602',
'MSMSGS ', username))
resp = self._sync_command(cvr, conn)
if resp.cmd != 'CVR':
raise Error(0, 'Bad response for CVR command.')
usr = Command('USR', self.transaction_id, ('TWN', 'I', username))
resp = self._sync_command(usr, conn)
if resp.cmd != 'USR' and resp.cmd != 'XFR':
raise Error(0, 'Bad response for USR command.')
# for dispatch server, response is ver, cvr, xfr; for notification
# server, it is ver, cvr, usr (or same as dispatch server, in some
# cases)
if resp.cmd == 'XFR':
return split(resp.args[1], ':', 1)
elif resp.cmd == 'USR':
twn_string = resp.args[2]
ticket = self.__get_twn_ticket(twn_string, username, password)
usr = Command('USR', self.transaction_id, ('TWN', 'S', ticket))
resp = self._sync_command(usr, conn)
if resp.cmd != 'USR':
raise Error(int(resp.cmd), protocol.errors[resp.cmd])
elif resp.args[0] != 'OK':
raise Error(0, 'Bad response for USR command.')
self.passport_id = resp.args[1]
self.display_name = url_codec.decode(resp.args[2])
self.logged_in = 1
finally:
if not self.logged_in:
conn.break_()
else:
self.conn = conn
def process(self, chats = False):
"""Process events
Keyword arguments:
chats -- whether or not to call msnp.Chat.process for all active
chat sessions
This method must be called periodically, preferably in the client
application's main loop.
"""
while self.logged_in:
fd = self.conn.socket.fileno()
r = select.select([fd], [], [], 0)
if len(r[0]) > 0:
buf = self.conn.receive_data_line()
self.__process_command_buf(buf)
elif len(self.send_queue) > 0:
cmd = self.send_queue.pop(0)
cmd.send(self.conn)
else:
break
if chats:
self.__process_active_chats()
def __process_active_chats(self):
[chat_.process() for chat_ in self.active_chats.values()]
def __process_command_buf(self, buf):
cmd = buf[:3]
if cmd == 'MSG':
self.__process_msg(buf)
elif cmd == 'QNG':
self.__process_qng(buf)
elif cmd == 'OUT':
self.__process_out(buf)
elif cmd == 'RNG':
self.__process_rng(buf)
else:
c = Command()
c.parse(buf)
if c.cmd == 'CHG':
self.__process_chg(c)
elif c.cmd == 'ILN':
self.__process_iln(c)
elif c.cmd == 'NLN':
self.__process_nln(c)
elif c.cmd == 'FLN':
self.__process_fln(c)
elif c.cmd == 'CHL':
self.__process_chl(c)
elif c.cmd == 'LSG':
self.__process_lsg(c)
elif c.cmd == 'LST':
self.__process_lst(c)
elif c.cmd == 'SYN':
self.__process_syn(c)
elif c.cmd == 'XFR':
self.__process_xfr(c)
elif c.cmd == 'BLP':
self.__process_blp(c)
elif c.cmd == 'GTC':
self.__process_gtc(c)
elif c.cmd == 'ADG':
self.__process_adg(c)
elif c.cmd == 'RMG':
self.__process_rmg(c)
elif c.cmd == 'REG':
self.__process_reg(c)
elif c.cmd == 'ADD':
self.__process_add(c)
elif c.cmd == 'REM':
self.__process_rem(c)
elif c.cmd == 'REA':
self.__process_rea(c)
elif c.cmd == '218':
pass
# TODO error handling
def __process_msg(self, buf):
msg = Msg()
msg.parse(buf)
msg.receive(self.conn)
# discard NS messages for now
def __process_qng(self, buf):
self.callbacks.ping()
def __process_out(self, buf):
self.conn.break_()
self.conn = None
self.logged_in = 0
self.callbacks.logged_out()
def __process_rng(self, buf):
cmdline = split(buf)
session_id = cmdline[1]
sb = split(cmdline[2], ':')
server = (sb[0], int(sb[1]))
hash = cmdline[4]
passport_id = cmdline[5]
display_name = url_codec.decode(cmdline[6])
try:
chat_ = chat.Chat(self, server, hash, passport_id,
display_name, session_id)
except Error, e:
if e.code == 1: # connection closed
return
raise e
self.active_chats[chat_.session_id] = chat_
self.callbacks.chat_started(chat_)
def __process_chg(self, command):
self.callbacks.state_changed(command.args[0])
def __process_iln(self, command):
state = command.args[0]
passport_id = command.args[1]
display_name = url_codec.decode(command.args[2])
friend = self.friend_list.get_friend(passport_id)
if friend != None:
friend.state = state
friend.display_name = display_name
self.__friend_list_updated()
else: # usu. immed. after login
self.friend_list.temp_iln[passport_id] = state
self.callbacks.friend_online(state, passport_id, display_name)
def __process_nln(self, command):
state = command.args[0]
passport_id = command.args[1]
display_name = url_codec.decode(command.args[2])
friend = self.friend_list.get_friend(passport_id)
if friend != None:
friend.display_name = display_name
friend.state = state
self.__friend_list_updated()
self.callbacks.friend_online(state, passport_id, display_name)
def __process_fln(self, command):
passport_id = command.args[0]
friend = self.friend_list.get_friend(passport_id)
if friend != None:
friend.state = States.OFFLINE
self.__friend_list_updated()
self.callbacks.friend_offline(passport_id)
def __process_chl(self, command):
qry = Qry(self.transaction_id, command.args[0])
self._async_command(qry)
def __process_lsg(self, command):
id = int(command.args[0])
name = url_codec.decode(command.args[1])
group = Group(id, name)
self.friend_list.groups[id] = group
self.__friend_list_updated()
def __process_lst(self, command):
from protocol import list_flags
passport_id = command.args[0]
display_name = url_codec.decode(command.args[1])
list_ = int(command.args[2])
group_id = []
if list_ & list_flags[Lists.FORWARD]:
group_id = [int(i) for i in split(command.args[3], ',')]
groups = None
if len(group_id):
groups = [self.friend_list.groups[g_id] for g_id in group_id]
friend = Friend(passport_id, display_name, groups = groups)
for f in list_flags.keys():
if list_ & list_flags[f]:
self.friend_list.lists[f][passport_id] = friend
if self.friend_list.temp_iln.has_key(passport_id):
friend.state = self.friend_list.temp_iln[passport_id]
self.__friend_list_updated()
def __process_syn(self, command):
ver = int(command.args[0])
self.friend_list.ver = ver
self.__friend_list_updated()
def __process_xfr(self, command):
sb = split(command.args[1], ':')
server = (sb[0], int(sb[1]))
cr = self.chat_requests[command.trn]
invitee = cr.invitee
chat_ = chat.Chat(self, server, command.args[3], self.passport_id,
self.display_name, None, invitee)
self.active_chats[chat_.session_id] = chat_
self.callbacks.chat_started(chat_)
def __process_blp(self, command):
privacy_mode = command.args[0]
self.friend_list.privacy_mode = privacy_mode
self.__friend_list_updated()
def __process_gtc(self, command):
notify_on_add = command.args[0] == 'A'
self.friend_list.notify_on_add_ = notify_on_add
self.__friend_list_updated()
def __process_adg(self, command):
ver = int(command.args[0])
name = url_codec.decode(command.args[1])
id = int(command.args[2])
self.friend_list.ver = ver
self.friend_list.groups[id] = Group(id, name)
self.__friend_list_updated()
self.callbacks.group_added(id, name)
def __process_rmg(self, command):
ver = int(command.args[0])
id = int(command.args[1])
self.friend_list.ver = ver
if self.friend_list.groups.has_key(id):
del self.friend_list.groups[id]
self.__friend_list_updated()
self.callbacks.group_removed(id)
def __process_reg(self, command):
ver = int(command.args[0])
id = int(command.args[1])
name = url_codec.decode(command.args[2])
self.friend_list.ver = ver
if self.friend_list.groups.has_key(id):
self.friend_list.groups[id].name = name
self.__friend_list_updated()
self.callbacks.group_renamed(id, name)
def __process_add(self, command):
list_ = command.args[0]
ver = int(command.args[1])
passport_id = command.args[2]
display_name = url_codec.decode(command.args[3])
group = None
if list_ == Lists.FORWARD:
group = self.friend_list.groups[int(command.args[4])]
self.friend_list.ver = ver
friend = self.friend_list.get_friend(passport_id, list_)
if friend != None:
friend.add_to_group(group)
else:
if group != None:
friend = Friend(passport_id, passport_id, (group))
else:
friend = Friend(passport_id, passport_id)
self.friend_list.lists[list_][passport_id] = friend
self.__friend_list_updated()
if group != None:
self.callbacks.friend_added(list_, passport_id, display_name,
group.get_id())
else:
self.callbacks.friend_added(list_, passport_id, display_name)
def __process_rem(self, command):
list_ = command.args[0]
ver = int(command.args[1])
passport_id = command.args[2]
group = None
if list_ == Lists.FORWARD:
group = self.friend_list.groups[int(command.args[3])]
self.friend_list.ver = ver
friend = self.friend_list.get_friend(passport_id, list_)
if friend != None: # this shouldn't be None, unless friend_list stale
if group != None:
friend.remove_from_group(group)
if len(friend.get_groups()) == 0:
del self.friend_list.lists[list_][passport_id]
self.__friend_list_updated()
if group != None:
self.callbacks.friend_removed(list_, passport_id, group.get_id())
else:
self.callbacks.friend_removed(list_, passport_id)
def __process_rea(self, command):
ver = int(command.args[0])
passport_id = command.args[1]
display_name = url_codec.decode(command.args[2])
if passport_id == self.passport_id:
self.display_name = display_name
self.callbacks.display_name_changed(display_name)
else:
self.callbacks.display_name_received(passport_id, display_name)
def __friend_list_updated(self):
self.friend_list.updated = time()
self.callbacks.friend_list_updated(self.friend_list)
def login(self, username, password, initial_state = States.ONLINE):
"""Login to MSN server
Keyword arguments:
username -- username
password -- password
initial_state -- initial state (default msnp.States.ONLINE)
"""
if self.logged_in:
return
server = self.dispatch_server
while not self.logged_in:
server = self.__handshake(server, username, password)
self.change_state(initial_state)
def ping(self):
"""Ping server"""
if not self.logged_in:
return
self._async_command(Png())
self.process()
def logout(self):
"""Logout from server"""
if not self.logged_in:
return
[chat_.leave() for chat_ in self.active_chats.values()]
self.process()
self.conn.break_()
self.conn = None
self.logged_in = 0
def change_state(self, state):
"""Change user's state
Keyword arguments:
state -- new state (see msnp.States)
"""
if not self.logged_in:
return
chg = Command('CHG', self.transaction_id, (state,))
self._async_command(chg)
self.process()
def sync_friend_list(self, ver = -1):
"""Synchronise friend list by getting new copy from server
The friend list is updated asynchronously.
msnp.SessionCallbacks.friend_list_updated will be called repeatedly
after a call to this method. The client may want to set a timer
instead, and check for updates to the friend list using the
msnp.FriendList.last_updated method.
Keyword arguments:
ver -- friend list version
"""
if not self.logged_in:
return
self.friend_list.dirty = False
if ver == -1:
ver = self.friend_list.ver
syn = Command('SYN', self.transaction_id, (str(ver),))
self._async_command(syn)
self.process()
def request_list(self, list_ = Lists.FORWARD):
"""Request a list from the server
Keyword arguments:
list_ -- type of list to request (see msnp.Lists)
"""
if not self.logged_in:
return
lst = Command('LST', self.transaction_id, (list_,))
self._async_command(lst)
self.process()
def request_groups(self):
"""Request groups from server"""
if not self.logged_in:
return
lsg = Command('LSG', self.transaction_id, ())
self._async_command(lsg)
self.process()
def change_privacy_mode(self, privacy_mode):
"""Change privacy mode
Keyword arguments:
privacy_mode -- new privacy mode (see msnp.PrivacyModes)
"""
if not self.logged_in:
return
blp = Command('BLP', self.transaction_id, (privacy_mode,))
self._async_command(blp)
self.process()
def notify_on_add(self, notify):
"""Change setting for being notified on being added"""
if not self.logged_in:
return
setting = 'N'
if notify:
setting = 'A'
gtc = Command('GTC', self.transaction_id, (setting,))
self._async_command(gtc)
self.process()
def add_group(self, name):
"""Add a group
Keyword arguments:
name -- name of new group
"""
if not self.logged_in:
return
adg = Command('ADG', self.transaction_id,
(url_codec.encode(name), '0'))
self._async_command(adg)
self.process()
def remove_group(self, id):
"""Remove a group
Keyword arguments:
id -- group ID
"""
if not self.logged_in:
return
rmg = Command('RMG', self.transaction_id, (str(id),))
self._async_command(rmg)
self.process()
def rename_group(self, id, name):
"""Rename a group
Keyword arguments:
id -- group ID
name -- new name of group
"""
if not self.logged_in:
return
reg = Command('REG', self.transaction_id,
(str(id), url_codec.encode(name), '0'))
self._async_command(reg)
self.process()
def add_friend(self, list_, passport_id, group_id = 0):
"""Add a friend
Keyword arguments:
list_ -- type of list (allow, block, etc.)
passport_id -- string representing friend's passport ID
group_id -- group ID of group to which friend is being added
"""
add = None
if list_ == Lists.FORWARD:
add = Command('ADD', self.transaction_id,
(list_, passport_id, passport_id, str(group_id)))
else:
add = Command('ADD', self.transaction_id,
(list_, passport_id, passport_id))
self._async_command(add)
self.process()
def remove_friend(self, list_, passport_id, group_id = 0):
"""Remove a friend
Keyword arguments:
list_ -- type of list (allow, block, etc.)
passport_id -- string representing friend's passport ID
group_id -- group ID of group from which friend is being removed
"""
rem = None
if list_ == Lists.FORWARD:
rem = Command('REM', self.transaction_id,
(list_, passport_id, str(group_id)))
else:
rem = Command('REM', self.transaction_id,
(list_, passport_id))
self._async_command(rem)
self.process()
def change_display_name(self, display_name):
"""Change user's display name
Keyword arguments:
display_name -- user's new display name
"""
if not self.logged_in:
return
rea = Command('REA', self.transaction_id,
(self.passport_id, url_codec.encode(display_name)))
self._async_command(rea)
self.process()
def request_display_name(self, passport_id):
"""Request display name of a friend
Keyword arguments:
passport_id -- string representing friend's passport ID
"""
if not self.logged_in:
return
rea = Command('REA', self.transaction_id,
(passport_id, url_codec.encode('MJ++')))
self._async_command(rea)
self.process()
def start_chat(self, invitee):
"""Start a chat
Keyword arguments:
invitee -- friend invited for chat
"""
if not self.logged_in:
return
xfr = Command('XFR', self.transaction_id, ('SB',))
self._async_command(xfr)
self.chat_requests[xfr.trn] = Session.__ChatRequest(invitee)
self.process()
# vim: set ts=4 sw=4 et tw=79 :
|