# ICQCorp interface for Python. # Adam Sampson, ats@offog.org # Protocol derived from ICQCorp import struct, socket, select, curses, string, sys # These are all shorts. ICQ_CMDxRCV_SETxOFFLINE = 0x0028 ICQ_CMDxRCV_LOGIN_ERR = 0x0370 ICQ_CMDxRCV_ACK = 0x000A ICQ_CMDxRCV_HELLO = 0x005A ICQ_CMDxRCV_WRONGxPASSWD = 0x0064 ICQ_CMDxRCV_SMxMSG = 0x0001 ICQ_CMDxRCV_SMxURL = 0x0004 ICQ_CMDxRCV_SMxUNKNOWN = 0x8001 ICQ_CMDxRCV_SMxUNKNOWN2 = 0x8004 ICQ_CMDxRCV_SMxREQxAUTH = 0x0006 ICQ_CMDxRCV_SMxAUTHxGRANTED = 0x0008 ICQ_CMDxRCV_SMxADDED = 0x000C ICQ_CMDxRCV_USERxONLINE = 0x006E ICQ_CMDxRCV_USERxOFFLINE = 0x0078 ICQ_CMDxRCV_SEARCHxFOUND = 0x008C ICQ_CMDxRCV_SEARCHxDONE = 0x00A0 ICQ_CMDxRCV_SYSxMSGxOFFLINE = 0x00DC ICQ_CMDxRCV_SYSxMSGxONLINE = 0x0104 ICQ_CMDxRCV_SYSxMSGxDONE = 0x00E6 ICQ_CMDxRCV_ERROR = 0x00F0 ICQ_CMDxRCV_BUSY = 0x00FA ICQ_CMDxRCV_USERxINFO = 0x02e4 ICQ_CMDxRCV_USERxSTATUS = 0x01A4 ICQ_CMDxRCV_USERxINVALIDxUIN = 0x02EE ICQ_CMDxRCV_USERxLISTxDONE = 0x021C ICQ_CMDxSND_ACK = 0x000A ICQ_CMDxSND_SEARCHxSTART = 0x05c8 ICQ_CMDxSND_THRUxSERVER = 0x010E ICQ_CMDxSND_PING = 0x042E ICQ_CMDxSND_LOGON = 0x03E8 ICQ_CMDxSND_LOGOFF = 0x0438 ICQ_CMDxSND_SYSxMSGxREQ = 0x044C ICQ_CMDxSND_USERxGETINFO = 0x05FA ICQ_CMDxSND_USERxADD = 0x053C ICQ_CMDxSND_SETxSTATUS = 0x04D8 ICQ_CMDxSND_USERxLIST = 0x0406 ICQ_CMDxSND_VISxLIST = 0x06AE ICQ_CMDxSND_INVISxLIST = 0x06A4 ICQ_CMDxSND_SYSxMSGxDONExACK = 0x0442 ICQ_CMDxSND_AUTHORIZE = 0x0456 ICQ_CMDxSND_PING2 = 0x051E # TCP commands ICQ_CMDxTCP_START = 0x07EE ICQ_CMDxTCP_CANCEL = 0x07D0 ICQ_CMDxTCP_ACK = 0x07DA ICQ_CMDxTCP_MSG = 0x0001 ICQ_CMDxTCP_FILE = 0x0003 ICQ_CMDxTCP_CHAT = 0x0002 ICQ_CMDxTCP_URL = 0x0004 ICQ_CMDxTCP_READxAWAYxMSG = 0x03E8 ICQ_CMDxTCP_READxBUSYxMSG = 0x03E9 ICQ_CMDxTCP_HANDSHAKE = 0x03FF ICQ_CMDxTCP_HANDSHAKE2 = 0x04FF ICQ_CMDxTCP_HANDSHAKE3 = 0x02FF # Status constants. ICQ_STATUS_OFFLINE = 0xFFFF ICQ_STATUS_ONLINE = 0x0000 ICQ_STATUS_AWAY = 0x0001 ICQ_STATUS_NA = 0x0005 ICQ_STATUS_OCCUPIED = 0x0011 ICQ_STATUS_DND = 0x0013 ICQ_STATUS_FREEFORCHAT = 0x0020 ICQ_STATUS_PRIVATE = 0x0100 # Miscellaneous constants. ICQ_VERSION = 0x0003 MAX_MESSAGE_SIZE = 450 PING_FREQUENCY = 45 DEFAULT_SERVER_PORT = 4000 MAX_SERVER_RETRIES = 3 MAX_WAIT_ACK = 10 LOCALHOST = 0x0100007F class Event: """Class representing an ICQ event we're waiting for.""" def do_periodic(self): """This will be run every 10 seconds. If it returns 1, the event will be destroyed.""" return 0 class UDPPacketEvent(Event): """An outgoing UDP packet.""" sequence = 0 packet = None sock = None callback = None tries = MAX_SERVER_RETRIES def __init__(self, packet, sock, callback=None): self.packet = packet self.sequence = packet.get_sequence() self.sock = sock self.callback = callback def __str__(self): return "UDP packet seq=" + str(self.sequence) def do_periodic(self): self.packet.send_udp(self.sock) self.tries -= 1 if self.tries == -1: return 1 return 0 class UserInfoEvent(Event): """A user information request.""" sequence = 0 user = None should_display = 0 def __init__(self, packet, user, should_display): self.sequence = packet.get_sequence() self.user = user self.should_display = should_display def __str__(self): return "UserInfo event seq=" + str(self.sequence) class SearchEvent(Event): """A user search request.""" id = 0 callback = None results = None def __init__(self, id, callback): self.id = id self.callback = callback self.results = [] def __str__(self): return "Search event seq=" + str(self.sequence) class Packet: """Class for dealing with ICQ packet data in little-endian format.""" MAXSIZE = 4096 data = "" def put(self, format, item): self.data += struct.pack(format, item) def c(self, char): self.put(" 0: try: t = socket.recv(size) except IOError: raise IOError("failed to get TCP data") size -= len(t) self.data += t def send_udp(self, socket): socket.send(self.data) def send_tcp(self, socket): socket.send(struct.pack("> Opened TCP connection from", uin) u = self.get_user(uin) u.sock = sock def handle_tcp_packet(self, p, sock): #self.p("handling tcp packet") p.getL() version = p.getS() command = p.getS() p.getS() useruin = p.getL() newcmd = p.getS() message = p.get_str() #self.p(">> TCP packet: version",version,"command",command,"uin",useruin,"newcmd",newcmd,"message",message) senderip = p.getL() localip = p.getL() senderport = p.getL() c = p.getC() userstatus = p.getL() #self.p(">> ... senderip",senderip,"localip",localip,"senderport",senderport,"userstatus",userstatus,"c",c) if command == ICQ_CMDxTCP_START: if newcmd == ICQ_CMDxTCP_MSG: tcpseq = p.getL() self.ack_tcp(newcmd, tcpseq, sock) self.handle_user_msg(useruin, message) elif newcmd == ICQ_CMDxTCP_READxBUSYxMSG or newcmd == ICQ_CMDxTCP_READxAWAYxMSG: tcpseq = p.getL() self.ack_tcp(newcmd, tcpseq, sock) self.p("User", useruin, "read away message.") elif newcmd == ICQ_CMDxTCP_URL: tcpseq = p.getL() self.ack_tcp(newcmd, tcpseq, sock) self.handle_user_url(useruin, message) else: self.p("Unknown packet from", useruin) # implement chat, file etc. elif command == ICQ_CMDxTCP_ACK: self.p(">> Got TCP ack") elif command == ICQ_CMDxTCP_CANCEL: self.p(">> Got TCP cancel") def handle_udp_packet(self, p): version = p.getS() command = p.getS() seq = p.getS() seq1 = p.getS() if command != ICQ_CMDxRCV_ACK: self.ack(seq) p.getL() p.getL() #self.p(">> Received packet, command", command, "seq", seq) if command == ICQ_CMDxRCV_ACK: #self.p(">> Received ack for", seq) for e in self.events: if e.__class__ == UDPPacketEvent and e.sequence == seq: #self.p(">> Removing matching event") if e.callback: e.callback() self.remove_event(e) elif command == ICQ_CMDxRCV_LOGIN_ERR: if self.state == STATE_WAITING_LOGON: raise LogonFailedException("login err") error = p.get_str() self.p("Login error: ", error) elif command == ICQ_CMDxRCV_HELLO: self.p("Hello!") if self.state == STATE_WAITING_LOGON: self.state == STATE_LOGGED_ON self.request_sys_msg() self.update_contact_list() self.send_visible_list() self.send_invisible_list() # Request the owner's information. self.request_search(0, 2, str(self.owner.uin)) elif command == ICQ_CMDxRCV_WRONGxPASSWD: self.p("Wrong password") if self.state == STATE_WAITING_LOGON: raise LogonFailedException("bad password") elif command == ICQ_CMDxRCV_BUSY: self.p("Server busy") if self.state == STATE_WAITING_LOGON: raise LogonFailedException("server busy") elif command == ICQ_CMDxRCV_USERxONLINE: useruin = p.getL() userip = p.getL() userport = p.getL() p.getL() p.getC() userstatus = p.getL() self.p("User ", useruin, " is online, ip ", userip, " port ", userport, " status ", userstatus) elif command == ICQ_CMDxRCV_USERxOFFLINE: useruin = p.getL() self.p("User ", useruin, " is offline") elif command == ICQ_CMDxRCV_USERxINFO: useralias = p.get_str() userfirstname = p.get_str() userlastname = p.get_str() useremail = p.get_str() for e in self.events: if e.__class__ == UserInfoEvent and e.sequence == seq1: u = e.user u.alias = useralias u.firstname = userfirstname u.lastname = userlastname u.email = useremail u.info_obtained = 1 if e.should_display: self.interface.user_info(u) self.remove_event(e) elif command == ICQ_CMDxRCV_USERxINVALIDxUIN: self.p("Invalid uin") # from enquiry with seq seq1 elif command == ICQ_CMDxRCV_USERxSTATUS: useruin = p.getL() userstatus = p.getS() self.p("User status: user ", useruin, " status ", userstatus) elif command == ICQ_CMDxRCV_USERxLISTxDONE: self.p("End of user list") elif command == ICQ_CMDxRCV_SEARCHxFOUND: useruin = p.getL() useralias = p.get_str() userfirstname = p.get_str() userlastname = p.get_str() useremail = p.get_str() userauth = p.getC() u = self.get_user(useruin) u.alias = useralias u.firstname = userfirstname u.lastname = userlastname u.email = useremail u.info_obtained = 1 for e in self.events: if e.__class__ == SearchEvent and e.id == seq1: e.results.append(useruin) #self.p("result",useruin,"seq",seq1) elif command == ICQ_CMDxRCV_SEARCHxDONE: # no need to read the contents... #self.p("search done") for e in self.events: if e.__class__ == SearchEvent and e.id == seq1: #self.p("search results found", len(e.results)) if e.callback: e.callback(e.results) self.remove_event(e) elif command == ICQ_CMDxRCV_SYSxMSGxDONE: self.p("End of system messages") elif command == ICQ_CMDxRCV_SYSxMSGxOFFLINE: sysuin = p.getL() systime = p.getL() syscmd = p.getS() self.handle_system_msg(p, sysuin, syscmd) self.system_ack(systime) elif command == ICQ_CMDxRCV_SYSxMSGxONLINE: sysuin = p.getL() syscmd = p.getS() self.handle_system_msg(p, sysuin, syscmd) elif command == ICQ_CMDxRCV_SETxOFFLINE: raise KickedOffException("got kicked off") elif command == ICQ_CMDxRCV_ERROR: self.p("Error!") else: self.p("Unknown ICQ packet received, command %04x" % command) def handle_system_msg(self, p, uin, command): if command == ICQ_CMDxRCV_SMxMSG: message = p.get_str() self.handle_user_msg(uin, message) elif command == ICQ_CMDxRCV_SMxURL: message = p.get_str() self.handle_user_url(uin, message) elif command == ICQ_CMDxRCV_SMxUNKNOWN: self.p("ICQ_CMDxRCV_SMxUNKNOWN") elif command == ICQ_CMDxRCV_SMxUNKNOWN2: self.p("ICQ_CMDxRCV_SMxUNKNOWN2") elif command == ICQ_CMDxRCV_SMxREQxAUTH: message = p.get_str() self.p("Auth request from user ", uin, ":") self.p(">>", message, "<<") elif command == ICQ_CMDxRCV_SMxAUTHxGRANTED: message = p.get_str() self.p("Auth granted from user ", uin, ":") self.p(">>", message, "<<") elif command == ICQ_CMDxRCV_SMxADDED: self.p("User ", uin, " added you to their contact list.") else: self.p("Unknown system message, command ", command) def add_event(self, event): self.events.append(event) def remove_event(self, event): self.events.remove(event) def send(self, packet, callback=None): self.add_event(UDPPacketEvent(packet, self.udpsock, callback)) packet.send_udp(self.udpsock) def logon(self): self.p("Logging on as", self.owner.uin) p = Packet() p.s(ICQ_VERSION) p.s(ICQ_CMDxSND_LOGON) p.s(self.owner.get_sequence()) p.s(self.owner.get_sequence1()) p.l(self.owner.uin) p.l(0) p.l(self.tcpport) p.s(len(self.owner.passwd) + 1) p.str0(self.owner.passwd) p.c(0x7a) p.c(0) p.c(2) p.c(0) p.l(LOCALHOST) p.c(4) p.l(0) p.c(2) p.l(0) p.c(0) p.c(0) p.c(0) p.c(0x13) p.c(0) p.c(0x7a) p.c(0) self.send(p) self.state = STATE_WAITING_LOGON def ack(self, seq): #self.p(">> Sending ack to seq ", seq) p = Packet() p.s(ICQ_VERSION) p.s(ICQ_CMDxSND_ACK) p.s(seq) p.s(0) p.l(self.owner.uin) p.l(0) p.send_udp(self.udpsock) # don't use the normal interface, as an ACK def system_ack(self, time): #self.p(">> Sending system ack to timestamp ", time) p = Packet() p.s(ICQ_VERSION) p.s(ICQ_CMDxSND_SYSxMSGxDONExACK) p.s(self.owner.get_sequence()) p.s(self.owner.get_sequence1()) p.l(self.owner.uin) p.l(0) p.l(time) p.send_udp(self.udpsock) # don't use the normal interface, as an ACK def ack_tcp(self, cmd, seq, sock): #self.p(">> Sending TCP ack to cmd", cmd, "seq", seq) p = Packet() p.l(self.owner.uin) p.s(0x0002) p.s(ICQ_CMDxTCP_ACK) p.s(0) p.l(self.owner.uin) p.s(cmd) p.str(self.owner.awaymsg) p.l(LOCALHOST) p.l(LOCALHOST) p.l(self.tcpport) p.c(4) if self.owner.status == ICQ_STATUS_FREEFORCHAT: status = 0x00000000 elif self.owner.status == ICQ_STATUS_AWAY: status = 0x01100000 elif self.owner.status == ICQ_STATUS_OCCUPIED: status = 0x02100000 elif self.owner.status == ICQ_STATUS_PRIVATE: status = 0x00900000 else: status = 0x00100000 p.l(status) p.l(seq) p.send_tcp(sock) def request_sys_msg(self): self.p("Requesting system messages") p = Packet() p.s(ICQ_VERSION) p.s(ICQ_CMDxSND_SYSxMSGxREQ) p.s(self.owner.get_sequence()) p.s(self.owner.get_sequence1()) p.l(self.owner.uin) p.l(0) self.send(p) def set_status(self, status): self.p("Setting status to", status) p = Packet() p.s(ICQ_VERSION) p.s(ICQ_CMDxSND_SETxSTATUS) p.s(self.owner.get_sequence()) p.s(self.owner.get_sequence1()) p.l(self.owner.uin) p.l(0) p.s(status) self.send(p) def ping(self): seq = self.owner.get_sequence() #self.p("Sending ping", seq) p = Packet() p.s(ICQ_VERSION) p.s(ICQ_CMDxSND_PING) p.s(seq) p.s(0) p.l(self.owner.uin) p.l(0) self.send(p) def send_offline_msg(self, uin, message, callback=None): p = Packet() p.s(ICQ_VERSION) p.s(ICQ_CMDxSND_THRUxSERVER) p.s(self.owner.get_sequence()) p.s(self.owner.get_sequence1()) p.l(self.owner.uin) p.l(0) p.l(uin) p.s(ICQ_CMDxTCP_MSG) p.str(message) self.send(p, callback) def quit(self): # Implement pass def update_contact_list(self): p = Packet() p.s(ICQ_VERSION) p.s(ICQ_CMDxSND_USERxLIST) p.s(self.owner.get_sequence()) p.s(self.owner.get_sequence1()) p.l(self.owner.uin) p.l(0) list = [] for x in self.users: if x.is_contact: list.append(x.uin) if len(list) > 255: list = list[:255] p.c(len(list)) for x in list: p.l(x) self.send(p) def send_visible_list(self): p = Packet() p.s(ICQ_VERSION) p.s(ICQ_CMDxSND_VISxLIST) p.s(self.owner.get_sequence()) p.s(self.owner.get_sequence1()) p.l(self.owner.uin) p.l(0) p.c(0) # no visible users self.send(p) def send_invisible_list(self): p = Packet() p.s(ICQ_VERSION) p.s(ICQ_CMDxSND_INVISxLIST) p.s(self.owner.get_sequence()) p.s(self.owner.get_sequence1()) p.l(self.owner.uin) p.l(0) p.c(0) # no invisible users self.send(p) def request_user_info(self, uin, should_display): p = Packet() p.s(ICQ_VERSION) p.s(ICQ_CMDxSND_USERxGETINFO) p.s(self.owner.get_sequence()) p.s(self.owner.get_sequence1()) p.l(self.owner.uin) p.l(0) p.l(uin) self.send(p) self.add_event(UserInfoEvent(p, self.get_user(uin), should_display)) def request_search(self, criteria, mode, string, callback=None): """Submit a search request. critera: 0=UIN 1=Nick 2=FirstName 3=LastName 4=Email 5=Age mode: 0=Contains 1=NoContains 2=Is 3=Isn't 4=Begins 5=Ends The callback, if specified, will be called with the list of UINs found as an argument; users found in a search always get added/updated in the user list.""" id = self.owner.get_sequence1() p = Packet() p.s(ICQ_VERSION) p.s(ICQ_CMDxSND_SEARCHxSTART) p.s(self.owner.get_sequence()) p.s(id) p.l(self.owner.uin) p.l(0) p.c(0xff) p.c(criteria) p.c(0) p.c(mode) p.str(string) self.send(p) self.add_event(SearchEvent(id, callback)) def handle_user_msg(self, uin, msg): """Handle an incoming message, making sure that the user is in the users list first before passing them on to the interface.""" u = self.get_user(uin) l = lambda self=self, msg=msg, u=u: \ self.interface.user_msg(u, self.owner, msg) if u.info_obtained: l() else: self.request_search(0, 2, str(uin), l) def handle_user_url(self, uin, msg): """Handle an incoming URL, making sure that the user is in the users list first before passing them on to the interface.""" (url, description) = (msg, "FIXME!") u = self.get_user(uin) l = lambda self=self, url=url, description=description: \ self.interface.user_url(u, self.owner, url, description) if u.info_obtained: l() else: self.request_search(0, 2, str(uin), l) def send_user_msg(self, uin, msg): """Send a message to a user, making sure that the user is in the users list first.""" u = self.get_user(uin) l = lambda self=self, msg=msg, u=u: \ self.interface.user_msg(self.owner, u, msg) ll = lambda self=self, msg=msg, u=u, l=l: \ self.send_offline_msg(u.uin, msg, l) if u.info_obtained: ll() else: self.request_search(0, 2, str(uin), ll) class ICQInterface: """UI interface for ICQ. Real interfaces should derive from this.""" icq = None addressing = None # Methods which ICQInterface implementations _must_ override. def append(self, s): pass def fileno(self): return sys.stdin.fileno() def handle_input(self): pass # Methods which ICQInterface implement might want to override. def status(self, *args): self.append("*** " + string.join(map(str, args))) def error(self, *args): self.append("!!! " + string.join(map(str, args))) def user_msg(self, ufrom, uto, msg): self.append(self.struser(ufrom) + "->" + self.struser(uto) + " " + msg) def user_url(self, ufrom, uto, url, desc): self.append(self.struser(ufrom) + "->" + self.struser(uto) + " " + desc) def user_info(self, u): self.append("--- User info for " + str(u.uin)) self.append(" First name: " + u.firstname) self.append(" Last name: " + u.lastname) self.append(" Alias: " + u.alias) self.append(" Email: " + u.email) def struser(self, u): """Turn a user object into a string suitable for printing message lines.""" if u.alias: return "<" + u.alias + ">" else: return "<[" + str(u.uin) + "]>" def split_command(self, input): """Split the first word off a command.""" try: (command, input) = string.split(input, " ", 1) return (command, input) except ValueError: return (input, "") def with_uin_do(self, input, func, args): """Call a function with the uin of the given user.""" # If it's a number already, assume that's the UIN. try: uin = string.atoi(input) if uin == 0: raise ValueError func(uin, args) return except ValueError: pass # Else look to see if it's an alias we already know about. for u in self.icq.users: if string.lower(u.alias) == string.lower(input): func(u.uin, args) return # Else search for the alias. self.with_search_do(1, 2, input, self.with_uin_cb, [func, args]) def with_search_do(self, criteria, mode, string, cb, args=[]): """Perform a search and then call a callback with the results.""" l = lambda r, self=self, cb=cb, args=args: apply(cb, [r] + args) self.icq.request_search(criteria, mode, string, l) def with_uin_cb(self, results, func, args): """A callback to check the result of a search for one person.""" l = len(results) if l == 0: self.error("No match for username") elif l > 1: self.error("Username ambiguous") else: func(results[0], args) def search_result_cb(self, results): """A callback to print the results of a search.""" for x in results: self.user_info(self.icq.get_user(x)) def send_msg(self, user, msg): """Send a message to a user, and set who we're talking to.""" self.with_uin_do(user, self.icq.send_user_msg, msg) if user != self.addressing: self.set_addressing(user) def set_addressing(self, user): self.addressing = user self.status("Addressing " + user) def do_command(self, input): """Parse a command.""" if input == "": return if input[0] != "/": if self.addressing: self.send_msg(self.addressing, input) return input = input[1:] if input == "": return (command, input) = self.split_command(input) if command[0] == "s": statuses = {"o":ICQ_STATUS_ONLINE, "f":ICQ_STATUS_FREEFORCHAT, "a":ICQ_STATUS_AWAY, "n":ICQ_STATUS_NA, "c":ICQ_STATUS_OCCUPIED, "d":ICQ_STATUS_DND, "p":ICQ_STATUS_PRIVATE, "l":ICQ_STATUS_OFFLINE} try: self.icq.set_status(statuses[input[0]]) except (ValueError, IndexError): self.error("Usage: status [ofancdpl]") elif command[0] == "m": try: (uin, input) = self.split_command(input) self.send_msg(uin, input) except ValueError: self.error("Usage: msg ") elif command[0] == "q": self.icq.quit() elif command[0] == "f": criteria = {"u":0, "n":1, "f":2, "l":3, "e":4, "a":5} mode = {"i":0, "n":1, "=":2, "!":3, "s":4, "e":5} try: (c, input) = self.split_command(input) (m, input) = self.split_command(input) self.with_search_do(criteria[c[0]], mode[m[0]], input, self.search_result_cb) except (ValueError, IndexError): self.error("Usage: find [unflea] [in=!se] ") elif command[0] == "l": self.status("User listing:") for x in self.icq.users: self.user_info(x) elif command[0] == "c": self.status("Updating contact list") self.icq.update_contact_list() else: self.error("Unknown command") def mainloop(self): """The generic main loop for interfaces. Hardcoded for me.""" self.status("Welcome to azzicq") owner = ICQOwner() # Insert your uin and password here owner.uin = 1527 owner.passwd = "ICQ" self.status("creating server") self.icq = ICQServer(owner, self) self.status("entering main loop") self.icq.mainloop() class ICQTtyInterface(ICQInterface): """Simple tty interface, suitable for use with ssfe.""" def handle_input(self): self.do_command(raw_input()) def append(self, s): print s sys.stdout.flush() class ICQCursesInterface(ICQInterface): """Curses interface.""" win = None history = [] max_history = 100 textpad = None inputwin = None def handle_input(self): ch = self.win.getch() if ch == curses.KEY_ENTER or ch == ord("\n"): self.do_command(self.textpad.gather()) (y, x) = self.inputwin.getyx() self.inputwin.move(y, 0) self.inputwin.clrtoeol() else: if ch == 127: ch = 263 self.textpad.do_command(ch) self.inputwin.refresh() def mainloop(self): curses.wrapper(self.internal_mainloop) def internal_mainloop(self, win): self.win = win (my, mx) = win.getmaxyx() curses.textpad.rectangle(win, my-3, 0, my-1, mx-2) self.inputwin = curses.newwin(1, mx-3, my-2, 1) self.textpad = curses.textpad.Textbox(self.inputwin) ICQInterface.mainloop() def update(self): (my, mx) = self.win.getmaxyx() n = len(self.history) - 1 y = my - 4 while y >= 0 and n >= 0: self.win.move(y, 0) self.win.clrtoeol() self.win.addnstr(str(self.history[n]), int(mx - 1)) n -= 1 y -= 1 self.win.refresh() def append(self, s): if len(self.history) >= self.max_history: self.history = self.history[:self.max_history] self.history.append(str(s)) self.update() if __name__ == "__main__": ICQTtyInterface().mainloop()