#!/usr/bin/env python import sys import time import string import socket import select from Tkinter import * import tkMessageBox import tkSimpleDialog import msnlib import msncb """ MSN Tk Client This is a beta msn client based on msnlib. As you see, it's GUI based on the Tk bindings, which provide an abstraction to create graphical interfaces; it works both under linux, windows and probably others too. For further information refer to the documentation or the source (which is always preferred). Please direct any comments to albertito@blitiri.com.ar. You can find more information, and the package itself, at http://blitiri.com.ar/p/msnlib/ """ # main msnlib classes m = msnlib.msnd() m.cb = msncb.cb() # void debug output #def void(s): pass #msnlib.debug = msncb.debug = void # # useful functions # #sys.setdefaultencoding("iso-8859-15") encoding = 'iso-8859-1' def encode(s): try: return s.decode(encoding).encode('utf-8') except: return s def decode(s): try: return s.decode('utf-8').encode(encoding) except: return s def nick2email(nick): "Returns an email according to the given nick, or None if noone matches" for email in m.users.keys(): if str(m.users[email].nick) == str(nick): return email if nick in m.users.keys(): return nick return None def email2nick(email): "Returns a nick accoriding to the given email, or None if noone matches" if email in m.users.keys(): return m.users[email].nick else: return None def now(): "Returns the current time in format HH:MM:SSTT" return time.strftime('%I:%M:%S%p', time.localtime(time.time()) ) def quit(): "Cleans up and quits everything" try: m.disconnect() except: pass root.quit() sys.exit(0) # # GUI classes # class userlist(Frame): "The user list" def __init__(self, master): Frame.__init__(self, master) self.scrollbar = Scrollbar(self, orient = VERTICAL) self.list = Listbox(self, yscrollcommand = self.scrollbar.set) self.list.config(font = "Courier") self.scrollbar.config(command = self.list.yview) self.scrollbar.pack(side = RIGHT, fill = Y) self.list.pack(side = LEFT, fill = BOTH, expand = 1) self.list.bind("", self.create_chat) def create_chat(self, evt = None): "Creates a chat window" if m.status == 'HDN': tkMessageBox.showwarning("Warning", "You can't open chats when you're invisible") return nick = self.list.get(self.list.curselection())[4:] email = nick2email(nick) if email in emwin.keys(): emwin[email].lift() elif m.users[email].status == 'FLN': tkMessageBox.showwarning("Warning", "The user is offline") else: emwin[email] = chatwindow(root, email) class mainmenu(Menu): "Main menu used in the main window" def __init__(self, master): Menu.__init__(self, master) self.status_menu = Menu(self, tearoff = 0) self.add_cascade(label = "Status", menu = self.status_menu) self.status_menu.add_command(label = "Online", command = self.chst_online) self.status_menu.add_command(label = "Away", command = self.chst_away) self.status_menu.add_command(label = "Busy", command = self.chst_busy) self.status_menu.add_command(label = "Be Right Back", command = self.chst_brb) self.status_menu.add_command(label = "Lunch", command = self.chst_lunch) self.status_menu.add_command(label = "Phone", command = self.chst_phone) self.status_menu.add_command(label = "Invisible", command = self.chst_invisible) self.add_command(label = 'Info', command = self.show_info) def show_info(self, evt = None): csel = mainlist.list.curselection() if not csel: return nick = mainlist.list.get(csel)[4:] email = nick2email(nick) infowindow(root, email) # status change callbacks def clear_heads(self): for i in emwin.keys(): emwin[i].head.config(text = '') def chst_online(self): self.clear_heads() m.change_status('online') def chst_away(self): self.clear_heads() m.change_status('away') def chst_busy(self): self.clear_heads() m.change_status('busy') def chst_brb(self): self.clear_heads() m.change_status('brb') def chst_lunch(self): self.clear_heads() m.change_status('lunch') def chst_phone(self): self.clear_heads() m.change_status('phone') def chst_invisible(self): warn = "Warning: as you are invisible, it is possible that\n" warn += "the messages you type here never get to the user." for i in emwin.keys(): emwin[i].head.config(text = warn) m.change_status('invisible') class chatwindow(Toplevel): "Represents a chat window" def __init__(self, master, email): Toplevel.__init__(self, master) self.email = email self.protocol("WM_DELETE_WINDOW", self.destroy_window) nick = email2nick(email) # FIXME: update the title with status change status = msnlib.reverse_status[m.users[email].status] if nick: self.wm_title(nick + ' (' + status + ')') else: self.wm_title(email + ' (' + status + ')') # head label self.head = Label(self) self.head.pack(side = TOP, fill = X, expand = 0) self.head.config(justify = LEFT) self.head.config(text = "") # text box (with scrollbar), where the message goes self.frame = Frame(self) self.scrollbar = Scrollbar(self.frame, orient = VERTICAL) self.text = Text(self.frame, yscrollcommand = self.scrollbar.set) self.scrollbar.config(command = self.text.yview) self.scrollbar.pack(side = RIGHT, fill = Y) self.text.pack(side = TOP, fill = BOTH, expand = 1) self.frame.pack(side = TOP, fill = BOTH, expand = 1) self.text.config(state = DISABLED) self.text.tag_config('from', foreground = 'blue') self.text.tag_config('to', foreground = 'red') self.text.tag_config('typing', foreground = 'lightblue') # entry, where the user types self.entry = Entry(self) self.entry.pack(side = BOTTOM, fill = X, expand = 0) self.entry.bind('', self.send_line) def append(self, s, direction, scroll = 1): "Adds text to the window's text box" self.text.config(state = NORMAL) self.text.insert(END, s, direction) self.text.yview(SCROLL, scroll, UNITS) self.text.config(state = DISABLED) def send_line(self, evt = None): "Sends the current entry as a message" msg = self.entry.get() lines = msg.split('\n') if len(lines) == 1: s = now() + ' >>> ' + msg + '\n' else: s = now() + ' >>>\n\t' s += string.join(lines, '\n\t') s = s[:-1] self.append(s, 'to', scroll = len(lines)) # we need to encode it before sending because msg is already # an unicode string; so use utf-8 msg = msg.encode('utf-8') m.sendmsg(self.email, msg) self.entry.delete(0, END) def destroy_window(self, evt = None): "Clean up when the window is closed" del(emwin[self.email]) self.destroy() class infowindow(Toplevel): "Represents a window with user information" def __init__(self, master, email): Toplevel.__init__(self, master) self.email = email self.wm_title('Info on ' + email) u = m.users[email] out = '' out += 'Information for user ' + email + '\n\n' out += 'Nick: ' + u.nick + '\n' out += 'Status: ' + msnlib.reverse_status[u.status] + '\n' if 'B' in u.lists: out += 'Mode: ' + 'blocked' + '\n' if u.gid != None: out += 'Group: ' + m.groups[u.gid] + '\n' if u.realnick: out += 'Real Nick: ' + u.realnick + '\n' if u.homep: out += 'Home phone: ' + u.homep + '\n' if u.workp: out += 'Work phone: ' + u.workp + '\n' if u.mobilep: out += 'Mobile phone: ' + u.mobilep + '\n' self.label = Label(self) self.label.pack(side = TOP, fill = BOTH, expand = 1) self.label.config(justify = LEFT) self.label.config(text = out) def redraw_main(): "Redraws the main screen" # sync the user list - FIXME: instead of redrawing, use the callbacks # for status change notifications nicks = [] for i in m.users.keys(): if m.users[i].status == 'FLN': s = '[X] ' elif m.users[i].status in ('NLN', 'IDL'): s = '[ ] ' else: s = '[-] ' if 'B' in m.users[i].lists: s = '[!] ' s += m.users[i].nick nicks.append(s) nicks.sort() mainlist.list.delete(0, END) for i in nicks: mainlist.list.insert(END, i) # update status s = msnlib.reverse_status[m.status] status.config(text = s) # # callbacks # def cb_msg(md, type, tid, params, sbd): "Gets a message" t = tid.split(' ') email = t[0] # parse lines = params.split('\n') headers = {} eoh = 0 for i in lines: # end of headers if i == '\r': break tv = i.split(':', 1) type = tv[0] value = tv[1].strip() headers[type] = value eoh += 1 eoh +=1 # ignore hotmail messages if email == 'Hotmail': return if email not in emwin.keys(): emwin[email] = chatwindow(root, email) # typing notifications if (headers.has_key('Content-Type') and headers['Content-Type'] == 'text/x-msmsgscontrol'): if not m.users[email].priv.has_key('typing'): m.users[email].priv['typing'] = 1 msg = now() + ' --- is typing\n' emwin[email].append(msg, 'typing') # normal message else: if len(lines[eoh:]) > 1: msg = now() + ' <<<\n\t' msg += string.join(lines[eoh:], '\n\t') msg = msg.replace('\r', '') else: msg = now() + ' <<< ' + lines[eoh] + '\n' if m.users[email].priv.has_key('typing'): del(m.users[email].priv['typing']) emwin[email].append(msg, 'from') root.bell() msncb.cb_msg(md, type, tid, params, sbd) m.cb.msg = cb_msg # # main # # email - chatwindow dictionary emwin = {} # gui init root = Tk() root.wm_title('msnlib') mainlist = userlist(root) mainlist.pack(side = TOP, fill = BOTH, expand = 1) status = Label(root, text = "logging in...", bd=1, relief = SUNKEN, anchor = W) status.pack(side = BOTTOM, fill = X, expand = 0) menu = mainmenu(root) root.config(menu = menu) # initial update, to display at least something while we log in root.update() # ask for username and password if not given in the command line if len(sys.argv) < 3: m.email = tkSimpleDialog.askstring("Username", "Please insert your email") if not m.email: quit() m.pwd = tkSimpleDialog.askstring("Password", "Please insert your password") if not m.pwd: quit() else: m.email = sys.argv[1] m.pwd = sys.argv[2] m.email = m.email.strip() m.pwd = m.pwd.strip() # the encoding is utf-8 because the text class uses unicode directly m.encoding = 'utf-8' root.update() # login try: m.login() m.sync() except 'AuthError': tkMessageBox.showerror("Login", "Error logging in: wrong password") quit() # start as invisible m.change_status('invisible') # main loop while 1: fds = m.pollable() infd = fds[0] outfd = fds[1] try: # both network and gui checks fds = select.select(infd, outfd, [], 0) root.update() except KeyboardInterrupt: quit() except TclError: quit() for i in fds[0] + fds[1]: try: m.read(i) except ('SocketError', socket.error), err: if i != m: m.close(i) else: tkMessageBox.showwarning("Warning", "Server disconnected us - you " + "probably logged in somewhere else") quit() # always redraw after a network event redraw_main() # sleep a bit so we don't take over the cpu time.sleep(0.05)