##
## user.py -- IRC server interface.
##
## Copyleft 1997,1998 Teemu Kalvas <chery@s2.org>
##

## +class User              ()
##  -method read            (p)     persistent storage object
##  -method write           (p)     persistent storage object
##  -method handle_XXX      (p)     persistent storage object
##  -method handle_cmd_XXX  (p,     persistent storage object
##                           tail)  arguments to the command
##  -method sock
##  -method out
##  -method connected

import curses
import fcntl
import TERMIOS
import regex
import string
import struct
import sys
import time
import resizeterm

import config
import crypt
import idea
import server

class User:
    def __init__(self):
	self.sock = sys.stdin
	self.out = []
	self.connected = 1

    def read(self, p):
	ch = p.screen.window.getch()
	if (ch >= 32 and ch < 127) or (ch >= 160 and ch < 256):
	    p.screen.edit_field.insert(ch)
	else:
	    if ch >= curses.KEY_MIN and ch <= curses.KEY_MAX:
		handler = 'handle_' + string.lower(curses.keyname(ch))
            elif ch > 0 and ch < 27:
                handler = 'handle_ctrl_' + chr(ch + 96)
            elif ch >= 27 and ch < 32:
                handler = 'handle_ctrl_' + \
                          ['leftbracket', 'backslash', 'rightbracket', 'caret',
                           'underscore'][ch - 27]
	    elif ch == 127:
		handler = 'handle_key_backspace'
	    else:
		handler = 'handle_keynum_' + repr(ch)
            if hasattr(self, handler):
                getattr(self, handler)(p)
	    elif config.verbose:
		p.screen.current.log('*** unknown: ' + handler)

    def write(self, p):
	scr = p.screen
	if scr.touched & 64:
	    scr.log_area = 0
	    scr.stat_area = 0
	    scr.edit_area = 0
	    height, width = struct.unpack('4h',
					  fcntl.ioctl(sys.stdout.fileno(),
						      TERMIOS.TIOCGWINSZ,
						      struct.pack('4h', 0, 0,
								  0, 0)))[:2]
	    resizeterm.resizeterm(height, width)
	    scr.log_area = scr.window.subwin(height - 2, width, 0, 0)
	    scr.log_area.scrollok(1)
	    scr.stat_area = scr.window.subwin(1, width, height - 2, 0)
	    scr.edit_area = scr.window.subwin(1, width, height - 1, 0)
	    scr.touched = 63
	if scr.touched & 8:
	    scr.current.redraw()
	if scr.touched & 16:
	    scr.stat_line.redraw()
	if scr.touched & 32:
	    scr.edit_field.redraw()
	scr.edit_area.move(0, scr.edit_field.position -	scr.edit_field.offset)
	if scr.touched & 1:
	    scr.log_area.touchwin()
	    scr.log_area.refresh()
	if scr.touched & 2:
	    scr.stat_area.touchwin()
	    scr.stat_area.refresh()
	scr.edit_area.touchwin()
	scr.edit_area.refresh()
	p.screen.touched = 0
	self.out = []

    def handle_keycmd_del_char_left(self, p):
	p.screen.edit_field.del_char_left()

    def handle_keycmd_del_char_right(self, p):
	p.screen.edit_field.del_char_right()

    def handle_keycmd_line_beg(self, p):
	p.screen.edit_field.line_beg()

    def handle_keycmd_line_end(self, p):
	p.screen.edit_field.line_end()

    def handle_keycmd_char_left(self, p):
	p.screen.edit_field.char_left()

    def handle_keycmd_char_right(self, p):
	p.screen.edit_field.char_right()

    def handle_keycmd_log_down(self, p):
	p.screen.current.page_down()

    def handle_keycmd_log_up(self, p):
	p.screen.current.page_up()

    def handle_keycmd_send_encrypted(self, p):
	screen = p.screen
	line = screen.edit_field.get_string()
	current = screen.current
	serv = current.server
	if len(line) > 0:
	    cm = crypt.encrypt_message(serv.nick, line, current.name, 1)
	    if cm:
		serv.out.append('PRIVMSG ' + current.name + ' :' + cm)
                self.display_privmsg_self(current, current.name, line, 1, 0)
	    else:
		current.log('*** encryption failed')
	screen.edit_field.reset()

    def handle_ctrl_i(self, p):
        ef = p.screen.edit_field
        str = ef.get_string()
        i = 8 - len(str) % 8
        while i > 0:
            ef.insert(ord(' '))
            i = i - 1

    def handle_keycmd_del_line_end(self, p):
	p.screen.edit_field.del_line_end()

    def handle_keycmd_refresh(self, p):
	p.screen.touched = p.screen.touched | 63

    def handle_keycmd_accept(self, p):
	screen = p.screen
	line = screen.edit_field.get_string()
        screen.edit_field.accept()
	current = screen.current
	if len(line) > 0:
	    if line[0] not in config.cmd_chars:
		current.server.out.append('PRIVMSG ' + current.name + ' :' +
					  line)
                self.display_privmsg_self(current, current.name, line, 0, 0)
	    else:
		f = string.find(line, ' ')
		if f <> -1:
		    cmd, tail = line[1:f], line[f + 1:]
		else:
		    cmd, tail = line[1:], ''
		handler = 'handle_cmd_' + cmd
                if hasattr(self, handler):
                    getattr(self, handler)(p, tail)
	screen.edit_field.reset()

    def display_privmsg_self(self, win, target, message, crypt, private):
        n = crypt + 1
        if private:
            win.log('-> ' + n*'*' + target + n*'*' + ' ' + message)
        else:
            win.log(n*'<' + win.server.nick + n*'>' + ' ' + message)

    def handle_keycmd_del_line(self, p):
	p.screen.edit_field.reset()

    def handle_keycmd_prev_window(self, p):
	p.screen.prev()

    def handle_keycmd_next_window(self, p):
	p.screen.next()

    def handle_keycmd_history_prev(self, p):
        p.screen.edit_field.history_up()

    def handle_keycmd_history_next(self, p):
        p.screen.edit_field.history_down()

    def handle_keycmd_mark_window(self, p):
        p.screen.mark()

    def handle_keycmd_to_marked_window(self, p):
        p.screen.to_marked()

    def handle_keycmd_to_active(self, p):
        p.screen.to_active()

    def handle_keycmd_walk_nicklist(self, p):
        nlist = p.screen.current.server.msg_nicklist
        if len(nlist):
            p.screen.edit_field.set_string('/msg ' + nlist[0])
            p.screen.current.server.msg_nicklist = nlist[1:] + [nlist[0]]

    handle_key_backspace = handle_keycmd_del_char_left
    handle_key_dc = handle_keycmd_del_char_right
    handle_ctrl_d = handle_keycmd_del_char_right
    handle_key_home = handle_keycmd_line_beg
    handle_ctrl_a = handle_keycmd_line_beg
    handle_key_end = handle_keycmd_line_end
    handle_ctrl_e = handle_keycmd_line_end
    handle_key_enter = handle_keycmd_accept
    handle_ctrl_m = handle_keycmd_accept
    handle_key_left = handle_keycmd_char_left
    handle_ctrl_b = handle_keycmd_char_left
    handle_key_right = handle_keycmd_char_right
    handle_ctrl_f = handle_keycmd_char_right
    handle_key_npage = handle_keycmd_log_down
    handle_ctrl_n = handle_keycmd_log_down
    handle_key_ppage = handle_keycmd_log_up
    handle_ctrl_p = handle_keycmd_log_up
    handle_ctrl_c = handle_keycmd_send_encrypted
    handle_ctrl_k = handle_keycmd_del_line_end
    handle_ctrl_u = handle_keycmd_del_line
    handle_ctrl_l = handle_keycmd_refresh
    handle_ctrl_q = handle_keycmd_prev_window
    handle_ctrl_w = handle_keycmd_next_window
    handle_key_up = handle_keycmd_history_prev
    handle_key_down = handle_keycmd_history_next

    def handle_cmd_addkey(self, p, tail):
        """addkey <address> <key>
Associates <key> to be used with <address> when using IDEA encryption of
IRC messages."""
	f = string.find(tail, ' ')
	if f <> -1:
	    addr, key = tail[:f], tail[f + 1:]
	    fingerprint = idea.key_fingerprint(key)
	    if not config.known_key.has_key(fingerprint):
		config.default_key.append((regex.compile(addr), key))
		config.known_key[fingerprint] = key

    def handle_cmd_ban(self, p, tail):
        """ban <address>
Adds a ban on <address> on the current channel.  Requires channel
operator status."""
	current = p.screen.current
	current.server.out.append('MODE ' + current.name + ' +b *!*@' + tail)

    def handle_cmd_bans(self, p, tail):
        """bans
Lists all bans on the current channel."""
	current = p.screen.current
	current.server.out.append('MODE ' + current.name + ' b')

    def handle_cmd_ctcp(self, p, tail):
        """ctcp <channel/nick> <ctcp-data>
Sends an arbitrary CTCP message to <channel> or <nick>."""
	f = string.find(tail, ' ')
	if f <> -1:
	    p.screen.current.server.out.append('PRIVMSG ' + tail[:f] +
					       ' :\001' + tail[f+1:] + '\001')

    def handle_cmd_dv(self, p, tail):
        """dv <nick>
Removes the +v (voice) mode of <nick> on the current channel.  Requires
channel operator status."""
	current = p.screen.current
	current.server.out.append('MODE ' + current.name + ' -v ' + tail)

    def handle_cmd_eval(self, p, tail):
        """eval [:<module>] <expression>
Evaluates a python <expression> in <module> or at top level if none is
given.  Prints the result in the current buffer."""
        glob = sys.modules['__main__']
        if tail[0] == ':':
            f = string.find(tail, ' ')
            if f <> -1:
                glob = sys.modules[tail[1:f]]
                tail = tail[f + 1:]
        p.screen.current.log('*** ' + tail + ' = ' + repr(eval(tail, glob)))

    def handle_cmd_exit(self, p, tail):
        """exit <reason>
Quits the connection to the current server."""
	p.screen.current.server.out.append('QUIT :' + tail)
	p.screen.current.server.quitting = 1

    def handle_cmd_help(self, p, tail):
        """help [<command>]
Prints out the help text associated with <command>."""
        if tail:
            key = 'handle_cmd_' + tail
            if User.__dict__.has_key(key):
                help = User.__dict__[key].__doc__
                if help:
                    p.screen.current.log_lines('(help) ', help)
                    return
            p.screen.current.log('*** No help for ' + tail)
        else:
            p.screen.current.log(
                "(help) help <command> for help on any command.  The commands defined are:")
            cmds = ''
            for k, v in User.__dict__.items():
                if k[:11] == "handle_cmd_":
                    cmds = cmds + ' ' + k[11:]
            p.screen.current.log('(help)  ' + cmds)

    def handle_cmd_join(self, p, tail):
        """join [<channel> [<key>]]
Joins <channel> and opens a new window for it, or rejoins the channel
associated with the current window if no <channel> is given.  If <key>
is given, uses it as the channel key."""
	if tail <> '':
            f = string.find(tail, ' ')
            if f <> -1:
                p.screen.current.server.out.append('JOIN ' + tail[:f] + ' :'
                                                   + tail[f + 1:])
            else:
                p.screen.current.server.out.append('JOIN ' + tail)
	else:
	    p.screen.current.server.out.append('JOIN ' + p.screen.current.name)

    def handle_cmd_kick(self, p, tail):
        """kick <nick> <reason>
Removes <nick> from the current channel.  Requires channel operator
status."""
	current = p.screen.current
	f = string.find(tail, ' ')
	if f <> -1:
	    current.server.out.append('KICK ' + current.name + ' ' + tail[:f] +
				      ' :' + tail[f + 1:])
	else:
	    current.server.out.append('KICK ' + current.name + ' ' + tail +
				      ' :')

    def handle_cmd_leave(self, p, tail):
        """leave <reason>
Leaves the current channel and deletes the associated window."""
	win = p.screen.current
	name = win.name
        lname = config.low(name)
	serv = win.server
        if name <> serv.name:
            if name[0] in '#&' and serv.channels[lname][1] <> {}:
                serv.out.append('PART ' + name + ' :' + tail)
            else:
                p.screen.current = win.next
                p.screen.touched = p.screen.touched | 63
                win.next.prev = win.prev
                win.prev.next = win.next
                del serv.channels[lname]

    def handle_cmd_names(self, p, tail):
        """names [<channel>]
Lists the members of <channel>, or of the current channel if none is
given."""
        current = p.screen.current
        serv = current.server
        if tail:
            serv.out.append('NAMES ' + tail)
        else:
            serv.out.append('NAMES ' + current.name)

    def handle_cmd_me(self, p, tail):
        """me <message>
Sends a CTCP action to the current channel."""
	current = p.screen.current
	serv = current.server
	serv.out.append('PRIVMSG ' + current.name + ' :\001ACTION ' + tail +
			'\001')
	serv.channels[config.low(current.name)][0].log('* ' + serv.nick + ' ' +
						       tail)

    def handle_cmd_mode(self, p, tail):
        """mode <mode-string>
Sets mode bits on the current channel.  Requires channel operator
status."""
	current = p.screen.current
	current.server.out.append('MODE ' + current.name + ' ' + tail)

    def handle_cmd_msg(self, p, tail):
        """msg <channel/nick> <message>
Sends a (private) message to <channel> or <nick>."""
	server = p.screen.current.server
	f = string.find(tail, ' ')
	if f <> -1:
	    chan = config.low(tail[:f])
	    tail = tail[f + 1:]
	    server.out.append('PRIVMSG ' + chan + ' :' + tail)
	    if server.channels.has_key(chan):
                self.display_privmsg_self(server.channels[chan][0], tail[:f],
                                          tail, 0, 0)
	    else:
                try:
                    server.msg_nicklist.remove(chan)
                except ValueError:
                    pass
                server.msg_nicklist.insert(0, chan)
                self.display_privmsg_self(server.window, chan, tail, 0, 1)

    def handle_cmd_nicklist(self, p, tail):
        """nicklist
Shows the list of nicks you have exchanged msgs with."""
        p.screen.current.log('*** '
                             + repr(p.screen.current.server.msg_nicklist))
 
    def handle_cmd_ping(self, p, tail):
        """ping
Pings the current server."""
	serv = p.screen.current.server
	serv.out.append('PING ' + serv.window.name)
	serv.ping_time = time.time()

    def handle_cmd_query(self, p, tail):
        """query <nick>
Opens a new window for private conversation with <nick>."""
	serv = p.screen.current.server
	win = p.screen.new_window(tail, serv)
	serv.channels[config.low(tail)] = win, {}

    def handle_cmd_quit(self, p, tail):
        """quit <reason>
Quits the connections to all servers."""
        def timer_fn(p, c):
            p.quit = 1
	for serv in p.servers[1:]:
            if serv.connected:
                serv.out.append('QUIT :' + tail)
	    serv.quitting = 1
        p.wait_add(time.time() + 2.0, timer_fn, ())

    def handle_cmd_quote(self, p, tail):
        """quote <data>
Sends arbitrary data to the current server."""
	p.screen.current.server.out.append(tail)

    def handle_cmd_reconnect(self, p, tail):
        """reconnect
Reconnects to the IRC server if the connection has been broken."""
        current = p.screen.current
	if not current.server.connected:
	    current.server.reconnect(p)

    def handle_cmd_reload(self, p, tail):
        """reload
Reloads the client code and the user configuration."""
	p.reload = 1

    def handle_cmd_terminate(self, p, tail):
        """terminate
Exits the program immediately."""
	p.quit = 1

    def handle_cmd_topic(self, p, tail):
        """topic [<topic>]
Sets a new topic on the current channel, or displays the current topic
if none is given.  If the channel has mode +t set, setting a new topic
requires channel operator status."""
        current = p.screen.current
        out = current.server.out
        if tail:
            out.append('TOPIC ' + current.name + ' :' + tail)
        else:
            out.append('TOPIC ' + current.name)

    def handle_cmd_unban(self, p, tail):
        """unban <address>
Removes a ban on <address> on the current channel.  Requires channel
operator status."""
	current = p.screen.current
	current.server.out.append('MODE ' + current.name + ' -b *!*@' + tail)

    def handle_cmd_whois(self, p, tail):
        """whois <nick>
Retrieves user information on <nick>."""
	p.screen.current.server.out.append('WHOIS ' + tail + ' ' + tail)

    def handle_cmd_winkill(self, p, tail):
        """winkill
Removes the current window."""
        p.screen.current = win.next
        p.screen.touched = p.screen.touched | 63
        win.next.prev = win.prev
        win.prev.next = win.next

    def handle_cmd_server(self, p, tail):
        """server <server-name>
Opens a connection to <server-name>.  Does not terminate any existing
connections."""
        srv = server.Server(p, (tail, 6667, p.servers[1].nicks[:], []))
        if srv.connected:
            p.servers.append(srv)

## End. ##
