diff main.c @ 413:14b685cdbd2c

Rename files.
author Matti Hamalainen <ccr@tnsp.org>
date Thu, 24 May 2012 06:41:07 +0300
parents nnchat.c@3e64acb433e8
children 8263cb88556a
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main.c	Thu May 24 06:41:07 2012 +0300
@@ -0,0 +1,2158 @@
+/*
+ * NNChat - Custom chat client for NewbieNudes.com chatrooms
+ * Written by Matti 'ccr' Hämäläinen
+ * (C) Copyright 2008-2011 Tecnic Software productions (TNSP)
+ */
+#include "util.h"
+#include "network.h"
+#include "th_args.h"
+#include "th_config.h"
+#ifdef __WIN32
+/* Undefine because both windows.h and curses.h #define it */
+#undef MOUSE_MOVED
+#include <shlwapi.h>
+#else
+#include <sys/wait.h>
+#endif
+#ifdef HAVE_NCURSES_H
+#include <ncurses.h>
+#else
+#include <curses.h>
+#endif
+
+#ifdef __WIN32
+#define SET_CONFIG_FILE    "nnchat.txt"
+#define SET_DIR_SEPARATOR  "\\"
+#define SET_DELAY          (0)
+#else
+#define SET_CONFIG_FILE    ".nnchat"
+#define SET_DIR_SEPARATOR  "/"
+#define SET_DELAY          (5)
+#endif
+
+#define SET_NICK_SEPARATOR ':'
+
+#define SET_MAX_HISTORY (16)        /* Command history length */
+#define SET_KEEPALIVE   (15*60)     /* Ping/keepalive period in seconds */
+#define SET_MAX_WINDOWS (32)
+
+
+/* Options
+ */
+int     optPort = 8005,
+        optProxyPort = 1080,
+        optProxyType = NN_PROXY_NONE;
+int     optUserColor = 0x000000;
+char    *optServer = "chat.newbienudes.com",
+        *optProxyServer = NULL,
+        *optUserName = NULL,
+        *optUserNameCmd = NULL,
+        *optUserNameEnc = NULL,
+        *optPassword = NULL,
+        *optPasswordCmd = NULL,
+        *optLogFilename = NULL,
+        *optSite = "NN",
+        *optNickSepStr = NULL;
+char    optNickSep;
+BOOL    optDaemon = FALSE;
+FILE    *optLogFile = NULL;
+BOOL    setIgnoreMode = FALSE;
+BOOL    optDebug = FALSE;
+BOOL    optLogEnable = FALSE;
+
+nn_window_t *chatWindows[SET_MAX_WINDOWS],
+        *currWin = NULL;
+WINDOW  *mainWin = NULL,
+        *statusWin = NULL,
+        *editWin = NULL;
+
+qlist_t *setIgnoreList = NULL,
+        *setIdleMessages = NULL;
+nn_userhash_t *nnUsers = NULL;
+char    *setConfigFile = NULL,
+        *setBrowser = NULL;
+cfgitem_t *cfg = NULL;
+
+
+/* Logging mode flags
+ */
+enum
+{
+    LOG_FILE   = 1,
+    LOG_WINDOW = 2,
+    LOG_STAMP  = 4
+};
+
+
+/* Arguments
+ */
+optarg_t optList[] =
+{
+    { 0, '?', "help",       "Show this help", OPT_NONE },
+    { 1, 'v', "verbose",    "Be more verbose", OPT_NONE },
+    { 2, 'p', "port",       "Connect to port", OPT_ARGREQ },
+    { 3, 's', "server",     "Server to connect to", OPT_ARGREQ },
+    { 4, 'C', "color",      "Initial color in RGB hex 000000", OPT_ARGREQ },
+    { 5, 'l', "logfile",    "Log filename", OPT_ARGREQ },
+    { 6, 'D', "daemon",     "A pseudo-daemon mode for logging", OPT_NONE },
+    { 7, 'f', "force-site", "Force site (default: NN)", OPT_ARGREQ },
+    { 8, 'd', "debug",      "Enable various debug features", OPT_NONE },
+
+    {10, '4', "socks4",     "SOCKS4 proxy server", OPT_ARGREQ },
+    {11, 'A', "socks4a",    "SOCKS4A proxy server", OPT_ARGREQ },
+    {12, 'P', "proxy-port", "Proxy port (default: 1080)", OPT_ARGREQ },
+};
+
+const int optListN = (sizeof(optList) / sizeof(optList[0]));
+
+
+void argShowHelp(void)
+{
+    th_print_banner(stdout, th_prog_name,
+                    "[options] <username> <password>");
+
+    th_args_help(stdout, optList, optListN);
+}
+
+
+BOOL argHandleOpt(const int optN, char *optArg, char *currArg)
+{
+    switch (optN)
+    {
+    case 0:
+        argShowHelp();
+        exit(0);
+        break;
+
+    case 1:
+        th_verbosityLevel++;
+        break;
+
+    case 2:
+        optPort = atoi(optArg);
+        break;
+
+    case 3:
+        optServer = optArg;
+        break;
+
+    case 4:
+        if ((optUserColor = th_get_hex_triplet(optArg)) < 0)
+        {
+            THERR("Invalid color argument '%s', should be a RGB hex triplet '000000'.\n",
+                optArg);
+            return FALSE;
+        }
+        THMSG(1, "Using color #%06x\n", optUserColor);
+        break;
+
+    case 5:
+        optLogFilename = optArg;
+        optLogEnable = TRUE;
+        break;
+
+    case 7:
+        optSite = optArg;
+        break;
+
+    case 6:
+        optDaemon = TRUE;
+        THMSG(1, "Running in pseudo-daemon mode.\n");
+        break;
+
+    case 8:
+        optDebug = TRUE;
+        THMSG(1, "Debug mode enabled.\n");
+        break;
+
+
+    case 10:
+        optProxyServer = optArg;
+        optProxyType = NN_PROXY_SOCKS4;
+        break;
+
+    case 11:
+        optProxyServer = optArg;
+        optProxyType = NN_PROXY_SOCKS4A;
+        break;
+
+    case 12:
+        optPort = atoi(optArg);
+        break;
+
+
+    default:
+        THERR("Unknown option '%s'.\n", currArg);
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+
+BOOL argHandleFile(char *currArg)
+{
+    if (!optUserNameCmd)
+        optUserNameCmd = currArg;
+    else if (!optPasswordCmd)
+        optPasswordCmd = currArg;
+    else
+    {
+        THERR("Username '%s' already specified on commandline!\n", optUserNameCmd);
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+BOOL str_get_timestamp(char *str, size_t len, const char *fmt)
+{
+    time_t stamp = time(NULL);
+    struct tm *stamp_tm;
+    if ((stamp_tm = localtime(&stamp)) != NULL)
+    {
+        strftime(str, len, fmt, stamp_tm);
+        return TRUE;
+    }
+    else
+    {
+        str[0] = 0;
+        return FALSE;
+    }
+}
+
+
+char * str_trim_left(char *buf)
+{
+    while (*buf != 0 && th_isspace(*buf)) buf++;
+    return buf;
+}
+
+int compareUsername(const void *s1, const void *s2)
+{
+    return th_strcasecmp((char *) s1, (char *) s2);
+}
+
+nn_window_t *findWindow(const char *id)
+{
+    int i;
+
+    for (i = 0; i < SET_MAX_WINDOWS; i++)
+        if (chatWindows[i] != NULL &&
+            chatWindows[i]->id != NULL &&
+            th_strcasecmp(id, chatWindows[i]->id) == 0)
+            return chatWindows[i];
+
+    return NULL;
+}
+
+
+BOOL openWindow(const char *name, BOOL curwin)
+{
+    int i;
+    nn_window_t *res;
+    if (name == NULL)
+        return FALSE;
+
+    if ((res = nn_window_new(name)) == NULL)
+        return FALSE;
+
+    for (i = 1; i < SET_MAX_WINDOWS; i++)
+        if (chatWindows[i] == NULL)
+        {
+            res->num = i;
+            chatWindows[i] = res;
+            if (curwin)
+                currWin = res;
+            return TRUE;
+        }
+
+    return FALSE;
+}
+
+
+void closeWindow(nn_window_t *win)
+{
+    int i;
+    if (win == NULL) return;
+
+    for (i = 1; i < SET_MAX_WINDOWS; i++)
+        if (chatWindows[i] == win)
+        {
+            chatWindows[i] = NULL;
+            nn_window_free(win);
+            return;
+        }
+}
+
+
+void updateStatus(void)
+{
+    char tmpStr[128];
+    int i;
+
+    if (statusWin == NULL) return;
+
+    str_get_timestamp(tmpStr, sizeof(tmpStr), "%H:%M:%S");
+
+    wbkgdset(statusWin, COLOR_PAIR(10));
+    werase(statusWin);
+
+    wattrset(statusWin, A_BOLD | COLOR_PAIR(11));
+    mvwaddstr(statusWin, 0, 1, tmpStr);
+
+    wattrset(statusWin, A_BOLD | COLOR_PAIR(13));
+    waddstr(statusWin, " | ");
+    wattrset(statusWin, A_BOLD | COLOR_PAIR(16));
+    waddstr(statusWin, optUserName);
+    wattrset(statusWin, A_BOLD | COLOR_PAIR(13));
+
+    wattrset(statusWin, A_BOLD | COLOR_PAIR(13));
+    waddstr(statusWin, " | ");
+    wattrset(statusWin, A_BOLD | COLOR_PAIR(11));
+    snprintf(tmpStr, sizeof(tmpStr), "#%06x", optUserColor);
+    waddstr(statusWin, tmpStr);
+
+    wattrset(statusWin, A_BOLD | COLOR_PAIR(13));
+    waddstr(statusWin, " | WIN: ");
+    snprintf(tmpStr, sizeof(tmpStr), "%d: %s / %d",
+        currWin->num + 1,
+        currWin->id != NULL ? currWin->id : "MAIN",
+        currWin->pos);
+    waddstr(statusWin, tmpStr);
+
+    wattrset(statusWin, A_BOLD | COLOR_PAIR(13));
+    waddstr(statusWin, " | ");
+    wattrset(statusWin, A_BOLD | COLOR_PAIR(11));
+
+    for (i = 0; i < SET_MAX_WINDOWS; i++)
+        if (chatWindows[i] != NULL && chatWindows[i]->dirty)
+        {
+            snprintf(tmpStr, sizeof(tmpStr), "%d ", i + 1);
+            waddstr(statusWin, tmpStr);
+        }
+
+    wrefresh(statusWin);
+}
+
+
+void printEditBuf(nn_editbuf_t *buf)
+{
+    char *tmp;
+    if (editWin == NULL || buf == NULL) return;
+
+    buf->data[buf->len] = 0;
+    tmp = nn_username_decode(th_strdup(buf->data));
+
+    werase(editWin);
+
+    wattrset(editWin, A_NORMAL);
+
+    if (buf->pos < buf->len)
+    {
+        waddnstr(editWin, tmp, buf->pos);
+        wattrset(editWin, A_REVERSE);
+        waddch(editWin, tmp[buf->pos]);
+        wattrset(editWin, A_NORMAL);
+        waddnstr(editWin, tmp + buf->pos + 1, buf->len - buf->pos - 1);
+    }
+    else
+    {
+        waddnstr(editWin, tmp, buf->len);
+        wattrset(editWin, A_REVERSE);
+        waddch(editWin, ' ');
+        wattrset(editWin, A_NORMAL);
+    }
+    wrefresh(editWin);
+    th_free(tmp);
+}
+
+
+int printWin(WINDOW *win, const char *fmt)
+{
+    const char *s = fmt;
+    int col = 0;
+
+    while (*s)
+    {
+        if (*s == '½')
+        {
+            s++;
+            if (*s == '½')
+            {
+                waddch(win, ((unsigned char) *s) | col);
+                s++;
+            }
+            else
+            {
+                memcpy(&col, s, sizeof(int));
+                s += sizeof(int);
+            }
+        }
+        else
+        {
+            waddch(win, ((unsigned char) *s) | col);
+            s++;
+        }
+    }
+    return 0;
+}
+
+
+#define QPUTCH(ch) th_vputch(&(win->buf), &(win->bufsize), &(win->len), ch)
+
+int nn_window_print(nn_window_t *win, const char *fmt)
+{
+    const char *s = fmt;
+    int col = 0;
+    while (*s)
+    {
+        if (*s == '½')
+        {
+            s++;
+            if (*s == '½')
+            {
+                QPUTCH(*s);
+                QPUTCH(*s);
+                win->chlen++;
+            }
+            else
+            {
+                int val = 0;
+                while (*s >= '0' && *s <= '9')
+                {
+                    val *= 10;
+                    val += (*s - '0');
+                    s++;
+                }
+                if (*s != '½') return -1;
+
+                if (val < 9)
+                    col = A_DIM | COLOR_PAIR(val);
+                else if (val < 30)
+                    col = A_BOLD | COLOR_PAIR(val - 9);
+
+                QPUTCH('½');
+
+                if (!th_growbuf(&(win->buf), &(win->bufsize), &(win->len), sizeof(int)))
+                    return -2;
+
+                memcpy(win->buf + win->len, &col, sizeof(int));
+                win->len += sizeof(int);
+            }
+        }
+        else if (*s == '\n')
+        {
+            QPUTCH('\n');
+            QPUTCH(0);
+            th_ringbuf_add(win->data, win->buf);
+            win->buf = NULL;
+            win->chlen = 0;
+            win->dirty = TRUE;
+        }
+        else if (*s != '\r')
+        {
+            QPUTCH((unsigned char) *s == 255 ? ' ' : *s);
+            win->chlen++;
+        }
+
+        s++;
+    }
+
+    return 0;
+}
+
+
+BOOL updateMainWin(BOOL force)
+{
+    int h, offs;
+    qringbuf_t *buf;
+
+    /* Check pointers */
+    if (mainWin == NULL || currWin == NULL)
+        return FALSE;
+
+    /* Check if update is forced or if the window is dirty */
+    if (!force && !currWin->dirty)
+        return FALSE;
+
+    /* Compute how many lines from backbuffer fit on the screen */
+    buf = currWin->data;
+    h = getmaxy(mainWin);
+
+    /* Clear and redraw window */
+    werase(mainWin);
+    scrollok(mainWin, 1);
+    for (offs = buf->size - h - currWin->pos; offs >= 0 && offs < buf->size - currWin->pos && offs < buf->size; offs++)
+    {
+        if (buf->data[offs] != NULL)
+            printWin(mainWin, buf->data[offs]);
+    }
+
+    currWin->dirty = FALSE;
+    wrefresh(mainWin);
+    return TRUE;
+}
+
+
+int printFile(FILE *outFile, const char *fmt)
+{
+    const char *s = fmt;
+
+    while (*s)
+    {
+        if (*s == '½')
+        {
+            s++;
+            if (*s == '½')
+            {
+                fputc((unsigned char) *s, outFile);
+                s++;
+            }
+            else
+            {
+                while (*s && isdigit((int) *s)) s++;
+                if (*s != '½') return -1;
+                s++;
+            }
+        }
+        else
+        {
+            if ((unsigned char) *s == 255)
+                fputc(' ', outFile);
+            else
+                fputc((unsigned char) *s, outFile);
+            s++;
+        }
+    }
+
+    return 0;
+}
+
+void printMsgV(nn_window_t *win, int flags, const char *fmt, va_list ap)
+{
+    char tmpStr[128], *buf;
+
+    str_get_timestamp(tmpStr, sizeof(tmpStr), "½17½[½11½%H:%M:%S½17½]½0½ ");
+
+    buf = th_strdup_vprintf(fmt, ap);
+
+    if (optLogFile && (flags & LOG_FILE))
+    {
+        if (flags & LOG_STAMP) printFile(optLogFile, tmpStr);
+        printFile(optLogFile, buf);
+        fflush(optLogFile);
+    }
+
+    if (!optDaemon && (flags & LOG_WINDOW))
+    {
+        nn_window_t *tmp = win != NULL ? win : chatWindows[0];
+        if (flags & LOG_STAMP) nn_window_print(tmp, tmpStr);
+        nn_window_print(tmp, buf);
+    }
+
+    th_free(buf);
+}
+
+void printMsg(nn_window_t *win, const char *fmt, ...)
+{
+    va_list ap;
+
+    va_start(ap, fmt);
+    printMsgV(win, LOG_STAMP | LOG_WINDOW | LOG_FILE, fmt, ap);
+    va_end(ap);
+}
+
+void printMsgF(nn_window_t *win, int flags, const char *fmt, ...)
+{
+    va_list ap;
+
+    va_start(ap, fmt);
+    printMsgV(win, flags | LOG_STAMP, fmt, ap);
+    va_end(ap);
+}
+
+void printMsgQ(nn_window_t *win, const char *fmt, ...)
+{
+    va_list ap;
+
+    va_start(ap, fmt);
+    printMsgV(win, LOG_STAMP | LOG_WINDOW, fmt, ap);
+    va_end(ap);
+}
+
+
+char *errorMessages = NULL;
+
+void errorMsgV(const char *fmt, va_list ap)
+{
+    char *tmp = th_strdup_vprintf(fmt, ap);
+
+    printMsg(NULL, "%s", tmp);
+
+    if (errorMessages != NULL)
+    {
+        char *tmp2 = th_strdup_printf("%s%s", errorMessages, tmp);
+        th_free(errorMessages);
+        th_free(tmp);
+        errorMessages = tmp2;
+    }
+    else
+        errorMessages = tmp;
+}
+
+void errorMsg(const char *fmt, ...)
+{
+    va_list ap;
+
+    va_start(ap, fmt);
+    errorMsgV(fmt, ap);
+    va_end(ap);
+}
+
+void errorFunc(struct _nn_conn_t *conn, const char *fmt, va_list ap)
+{
+    (void) conn;
+    errorMsgV(fmt, ap);
+}
+
+void messageFunc(struct _nn_conn_t *conn, const char *fmt, va_list ap)
+{
+    (void) conn;
+    printMsgV(NULL, LOG_STAMP | LOG_WINDOW | LOG_FILE, fmt, ap);
+}
+
+
+BOOL checkIgnoreList(const char *name)
+{
+    qlist_t *node = setIgnoreList;
+    while (node != NULL)
+    {
+        if (th_strcasecmp(name, (char *) node->data) == 0)
+            return TRUE;
+        node = node->next;
+    }
+    return FALSE;
+}
+
+
+int nnproto_handle_user(nn_conn_t *conn)
+{
+    static const char *msg = "</USER><MESSAGE>";
+    char *p = conn->ptr;
+    BOOL isMine, isIgnored = FALSE;
+    char *s, *t, *userName;
+
+    /* Find start of the message */
+    s = strstr(p, msg);
+    if (!s) return 1;
+    *s = 0;
+    s += strlen(msg);
+
+    /* Find end of the message */
+    t = strstr(s, "</MESSAGE>");
+    if (!t) return 3;
+    *t = 0;
+
+    /* Decode message string */
+    s = nn_decode_str1(s);
+    if (!s) return -1;
+
+    /* Decode username */
+    userName = nn_decode_str1(p);
+    if (!userName)
+    {
+        th_free(s);
+        return -2;
+    }
+
+    /* Check if the username is on our ignore list and
+     * that it is not our OWN username!
+     */
+    isMine = strcmp(userName, optUserName) == 0;
+    isIgnored = setIgnoreMode && !isMine && checkIgnoreList(userName);
+
+    /* Is it a special control message? */
+    if (*s == '/')
+    {
+        /* Ignore room join/leave messages */
+        if (!optDebug && (strstr(s, "left the room") || strstr(s, "joined the room from")))
+            goto done;
+
+        t = nn_strip_tags(s + 1);
+        if (!strncmp(t, "BPRV ", 5))
+        {
+            char *name, *tmp, *msg, *h;
+            nn_window_t *win;
+            h = nn_decode_str2(t + 1);
+
+            if (!strncmp(t, "BPRV from ", 10))
+            {
+                name = nn_decode_str2(t + 10);
+                isMine = FALSE;
+            }
+            else
+            {
+                name = nn_decode_str2(t + 8);
+                isMine = TRUE;
+            }
+
+            for (tmp = name; *tmp && *tmp != ':'; tmp++);
+            if (tmp[0] != 0 && tmp[1] == ' ')
+                msg = tmp + 2;
+            else
+                msg = "";
+            *tmp = 0;
+
+            isIgnored = setIgnoreMode && checkIgnoreList(name);
+            win = findWindow(name);
+
+            if (win != NULL)
+            {
+                printMsgF(win, isIgnored ? 0 : LOG_WINDOW,
+                    "½5½<½%d½%s½5½>½0½ %s\n",
+                    isMine ? 14 : 15, isMine ? optUserName : name, msg);
+
+                printMsgF(NULL, LOG_FILE, "½11½%s½0½\n", h);
+            }
+            else
+            {
+                printMsgF(NULL, isIgnored ? LOG_FILE : (LOG_WINDOW | LOG_FILE),
+                    "½11½%s½0½\n", h);
+            }
+            th_free(name);
+            th_free(h);
+        }
+        else
+        {
+            /* It's an action (/me) */
+            char *h = nn_decode_str2(t);
+            printMsgF(NULL, isIgnored ? LOG_FILE : (LOG_WINDOW | LOG_FILE),
+                "½9½* %s½0½\n", h);
+            th_free(h);
+        }
+        th_free(t);
+    }
+    else
+    {
+        /* It's a normal message */
+        char *h;
+        t = nn_strip_tags(s);
+        h = nn_decode_str2(t);
+        printMsgF(NULL, isIgnored ? LOG_FILE : (LOG_WINDOW | LOG_FILE),
+            "½5½<½%d½%s½5½>½0½ %s\n", isMine ? 14 : 15, userName, h);
+        th_free(h);
+        th_free(t);
+    }
+
+done:
+    th_free(s);
+    th_free(userName);
+    return 0;
+}
+
+
+int nnproto_handle_login(nn_conn_t *conn)
+{
+    char tmpStr[256];
+    str_get_timestamp(tmpStr, sizeof(tmpStr), "%c");
+
+    if (!nn_conn_buf_strcmp(conn, "FAILURE>"))
+    {
+        printMsg(NULL, "½1½Login failure½0½ - ½3½%s½0½\n", tmpStr);
+        return -2;
+    }
+    else if (!nn_conn_buf_strcmp(conn, "SUCCESS>"))
+    {
+        printMsg(NULL, "½2½Login success½0½ - ½3½%s½0½\n", tmpStr);
+        nn_conn_send_msg(conn, optUserNameEnc, "%%2FRequestUserList");
+        return 0;
+    }
+    else
+        return 1;
+}
+
+
+int nnproto_handle_add_user(nn_conn_t *conn)
+{
+    char *p, *s, *str = conn->ptr;
+    nn_window_t *win;
+
+    s = nn_conn_buf_strstr(conn, "</ADD_USER>");
+    if (!s) return 1;
+    *s = 0;
+
+    p = nn_dbldecode_str(str);
+    if (!p) return -1;
+
+    win = findWindow(p);
+    nn_userhash_insert(nnUsers, nn_username_encode(p));
+
+    printMsg(NULL, "! ½3½%s½0½ ½2½ADDED.½0½\n", p);
+    if (win != NULL)
+        printMsg(win, "! ½3½%s½0½ ½2½joined the chat.½0½\n", p);
+
+    th_free(p);
+    return 0;
+}
+
+
+int nnproto_handle_delete_user(nn_conn_t *conn)
+{
+    char *p, *s, *str = conn->ptr;
+    nn_window_t *win;
+
+    s = nn_conn_buf_strstr(conn, "</DELETE_USER>");
+    if (!s) return 1;
+    *s = 0;
+
+    p = nn_dbldecode_str(str);
+    if (!p) return -1;
+
+    win = findWindow(p);
+    nn_userhash_delete(nnUsers, nn_username_encode(p));
+
+    printMsg(NULL, "! ½3½%s½0½ ½1½DELETED.½0½\n", p);
+    if (win != NULL)
+        printMsg(win, "! ½3½%s½0½ ½1½left the chat.½0½\n", p);
+
+    th_free(p);
+    return 0;
+}
+
+
+int nnproto_handle_num_clients(nn_conn_t *conn)
+{
+    nn_conn_buf_strstr(conn, "</NUMCLIENTS>");
+    return 0;
+}
+
+
+int nnproto_handle_boot(nn_conn_t *conn)
+{
+    (void) conn;
+    errorMsg("Booted by server.\n");
+    return -1;
+}
+
+
+typedef struct
+{
+    char *cmd;
+    ssize_t len;
+    int (*handler)(nn_conn_t *);
+} nn_protocolcmd_t;
+
+
+static nn_protocolcmd_t protoCmds[] =
+{
+    { "<USER>",         -1, nnproto_handle_user },
+    { "<LOGIN_",        -1, nnproto_handle_login },
+    { "<DELETE_USER>",  -1, nnproto_handle_delete_user },
+    { "<ADD_USER>",     -1, nnproto_handle_add_user },
+    { "<NUMCLIENTS>",   -1, nnproto_handle_num_clients },
+    { "<BOOT />",       -1, nnproto_handle_boot },
+};
+
+static const int nprotoCmds = sizeof(protoCmds) / sizeof(protoCmds[0]);
+
+
+int nn_parse_protocol(nn_conn_t *conn)
+{
+    static BOOL protoCmdsInit = FALSE;
+    int i;
+
+    if (!protoCmdsInit)
+    {
+        for (i = 0; i < nprotoCmds; i++)
+            protoCmds[i].len = strlen(protoCmds[i].cmd);
+
+        protoCmdsInit = TRUE;
+    }
+
+    for (i = 0; i < nprotoCmds; i++)
+    {
+        if (!nn_conn_buf_strncmp(conn, protoCmds[i].cmd, protoCmds[i].len))
+            return protoCmds[i].handler(conn);
+    }
+
+    if (optDebug)
+    {
+        printMsg(NULL, "Unknown protocmd: \"%s\"\n", conn->ptr);
+        return 0;
+    }
+    else
+        return 1;
+}
+
+
+int nn_handle_input(nn_conn_t *conn, char *buf, size_t bufLen)
+{
+    char *tmpStr, tmpBuf[4096];
+    BOOL result;
+
+    /* Trim right */
+    bufLen--;
+    buf[bufLen--] = 0;
+    while (bufLen > 0 && th_isspace(buf[bufLen]))
+        buf[bufLen--] = 0;
+
+    /* Decode completed usernames */
+    nn_username_decode(buf);
+
+    /* Check for special user commands */
+    if (*buf == 0)
+    {
+        return 1;
+    }
+    else if (!th_strncasecmp(buf, "/color ", 7))
+    {
+        /* Change color */
+        int tmpInt;
+        if ((tmpInt = th_get_hex_triplet(str_trim_left(buf + 7))) < 0)
+        {
+            printMsgQ(currWin, "Invalid color value '%s'\n", buf+7);
+            return 1;
+        }
+        optUserColor = tmpInt;
+        printMsgQ(currWin, "Setting color to #%06x\n", optUserColor);
+        nn_conn_send_msg(conn, optUserNameEnc, "%%2FSetFontColor%%20%%2Dcolor%%20%06X", optUserColor);
+        return 0;
+    }
+    else if (!th_strncasecmp(buf, "/ignore", 7))
+    {
+        char *name = str_trim_left(buf + 7);
+        if (strlen(name) > 0)
+        {
+            /* Add or remove someone to/from ignore */
+            qlist_t *user = th_llist_find_func(setIgnoreList, name, compareUsername);
+            if (user != NULL)
+            {
+                printMsgQ(currWin, "Removed user '%s' from ignore.\n", name);
+                th_llist_delete_node(&setIgnoreList, user);
+            }
+            else
+            {
+                printMsgQ(currWin, "Now ignoring '%s'.\n", name);
+                th_llist_append(&setIgnoreList, th_strdup(name));
+            }
+        }
+        else
+        {
+            /* Just list whomever is in ignore now */
+            qlist_t *user = setIgnoreList;
+            ssize_t nuser = th_llist_length(setIgnoreList);
+            char *result = th_strdup_printf("Users ignored (%d): ", nuser);
+            while (user != NULL)
+            {
+                if (user->data != NULL)
+                {
+                    th_pstr_printf(&result, "%s'%s'", result, (char *) user->data);
+                    if (--nuser > 0)
+                        th_pstr_printf(&result, "%s, ", result);
+                }
+                user = user->next;
+            }
+            printMsgQ(currWin, "%s\n", result);
+            th_free(result);
+        }
+        return 0;
+    }
+    else if (!th_strncasecmp(buf, "/query", 6))
+    {
+        char *name = str_trim_left(buf + 6);
+        if (strlen(name) > 0)
+        {
+            nn_user_t *user = nn_user_find(nnUsers, nn_username_encode(name));
+            if (user != NULL)
+            {
+                name = nn_username_decode(th_strdup(user->name));
+                printMsgQ(currWin, "Opening PRV query for '%s'.\n", name);
+                if (openWindow(name, TRUE))
+                    printMsgQ(currWin, "In PRV query with '%s'.\n", name);
+                th_free(name);
+            }
+        }
+        else
+        {
+            printMsgQ(currWin, "Usage: /query username\n");
+            printMsgQ(currWin, "To close a PRV query, use /close [username]\n");
+            printMsgQ(currWin, "/close without username will close the current PRV window.\n");
+        }
+        return 0;
+    }
+    else if (!th_strncasecmp(buf, "/win", 4))
+    {
+        /* Change color */
+        char *tmp = str_trim_left(buf + 4);
+        if (strlen(tmp) > 0)
+        {
+            int val = atoi(tmp);
+            if (val >= 1 && val < SET_MAX_WINDOWS)
+            {
+                if (chatWindows[val - 1] != NULL)
+                    currWin = chatWindows[val - 1];
+            }
+            else
+            {
+                printMsgQ(currWin, "Invalid window number '%s'\n", tmp);
+                return 1;
+            }
+        }
+        else
+        {
+            printMsgQ(currWin, "Window   : #%d\n", currWin->num);
+            printMsgQ(currWin, "ID       : %s\n", currWin->id);
+        }
+        return 0;
+    }
+    else if (!th_strncasecmp(buf, "/close", 6))
+    {
+        char *name = str_trim_left(buf + 6);
+        if (strlen(name) > 0)
+        {
+            nn_window_t *win = findWindow(name);
+            if (win != NULL)
+            {
+                closeWindow(win);
+                printMsgQ(currWin, "Closed PRV query to '%s'.\n", name);
+            }
+            else
+            {
+                printMsgQ(currWin, "No PRV query by name '%s'.\n", name);
+            }
+        }
+        else
+        {
+            if (currWin != chatWindows[0])
+            {
+                closeWindow(currWin);
+                currWin = chatWindows[0];
+            }
+        }
+        return 0;
+    }
+    else if (!th_strncasecmp(buf, "/save", 5))
+    {
+        /* Save configuration */
+        FILE *cfgfile = fopen(setConfigFile, "w");
+        if (cfgfile == NULL)
+        {
+            printMsgQ(currWin, "Could not create configuration to file '%s': %s\n",
+                setConfigFile, strerror(errno));
+            return 0;
+        }
+        printMsgQ(currWin, "Configuration saved in file '%s', res=%d\n",
+            setConfigFile,
+            th_cfg_write(cfgfile, setConfigFile, cfg));
+
+        fclose(cfgfile);
+        return 0;
+    }
+    else if (!th_strncasecmp(buf, "/w ", 3))
+    {
+        /* Open given username's profile via firefox in a new tab */
+        char *name = str_trim_left(buf + 3);
+
+        printMsg(currWin, "Opening profile for: '%s'\n", name);
+
+        tmpStr = nn_encode_str1(name);
+#ifdef __WIN32
+        {
+            HINSTANCE status;
+            snprintf(tmpBuf, sizeof(tmpBuf), "http://www.newbienudes.com/profile/%s/", tmpStr);
+            th_free(tmpStr);
+            status = ShellExecute(NULL, "open", tmpBuf, NULL, NULL, SW_SHOWNA);
+            if (status <= (HINSTANCE) 32)
+                printMsgQ(currWin, "Could not launch default web browser: %d\n", status);
+        }
+#else
+        {
+            int status;
+            int fds[2];
+            pid_t pid;
+            snprintf(tmpBuf, sizeof(tmpBuf), "openurl(http://www.newbienudes.com/profile/%s/,new-tab)", tmpStr);
+            th_free(tmpStr);
+
+            if (pipe(fds) == -1)
+            {
+                int ret = errno;
+                printMsgQ(currWin, "Could not open process communication pipe! (%d, %s)\n", ret, strerror(ret));
+                return 0;
+            }
+
+            if ((pid = fork()) < 0)
+            {
+                printMsgQ(currWin, "Could not create sub-process!\n");
+            }
+            else if (pid == 0)
+            {
+                dup2(fds[1], STDOUT_FILENO);
+                dup2(fds[0], STDERR_FILENO);
+                execlp(setBrowser, setBrowser, "-remote", tmpBuf, (void *)NULL);
+                _exit(errno);
+            }
+
+            wait(&status);
+        }
+#endif
+        return 0;
+    }
+    else if (!th_strncasecmp(buf, "/who", 4))
+    {
+        /* Alias /who to /listallusers */
+        snprintf(tmpBuf, sizeof(tmpBuf), "/listallusers");
+        buf = tmpBuf;
+    }
+
+    if (currWin != chatWindows[0])
+    {
+        if (currWin->id != NULL)
+        {
+            snprintf(tmpBuf, sizeof(tmpBuf), "/prv -to %s -msg %s", currWin->id, buf);
+            buf = tmpBuf;
+        }
+        else
+        {
+            printMsgQ(NULL, "No target set, exiting prv mode.\n");
+            return 1;
+        }
+    }
+
+    /* Send double-encoded */
+    tmpStr = nn_dblencode_str(nn_username_decode(buf));
+    if (tmpStr == 0) return -2;
+    result = nn_conn_send_msg(conn, optUserNameEnc, "%s", tmpStr);
+    th_free(tmpStr);
+
+    return result ? 0 : -1;
+}
+
+
+void closeWindows(void)
+{
+    if (mainWin) delwin(mainWin);
+    if (statusWin) delwin(statusWin);
+    if (editWin) delwin(editWin);
+}
+
+
+BOOL initializeWindows(void)
+{
+    int w, h;
+
+    getmaxyx(stdscr, h, w);
+
+    closeWindows();
+
+    mainWin = subwin(stdscr, h - 4, w, 0, 0);
+    statusWin = subwin(stdscr, 1, w, h - 4, 0);
+    editWin = subwin(stdscr, 3, w, h - 3, 0);
+
+    if (mainWin == NULL || statusWin == NULL || editWin == NULL)
+        return FALSE;
+
+    return TRUE;
+}
+
+
+void updateWindows(void)
+{
+    if (mainWin) redrawwin(mainWin);
+    if (statusWin) redrawwin(statusWin);
+    if (editWin) redrawwin(editWin);
+}
+
+
+BOOL performTabCompletion(nn_editbuf_t *buf)
+{
+    static char *previous = NULL, *pattern = NULL;
+    BOOL again = FALSE, hasSeparator = FALSE, newPattern = FALSE, hasSpace = FALSE;
+    char *str = buf->data;
+    int mode = 0;
+    ssize_t endPos, startPos = buf->pos;
+
+    /* previous word */
+    if (startPos >= 2 && str[startPos - 1] == ' ' && str[startPos - 2] != ' ')
+    {
+        startPos -= 2;
+        endPos = startPos;
+        while (startPos > 0 && str[startPos - 1] != ' ') startPos--;
+        mode = 1;
+    }
+    else
+    /* middle of a word, new pattern */
+    if (startPos < buf->len && str[startPos] != ' ')
+    {
+        endPos = startPos;
+        while (startPos > 0 && str[startPos - 1] != ' ') startPos--;
+        while (endPos < buf->len - 1 && str[endPos + 1] != ' ') endPos++;
+        newPattern = TRUE;
+        mode = 2;
+    }
+    else
+    /* previous word, new pattern */
+    if (startPos >= 1 && str[startPos - 1] != ' ')
+    {
+        startPos -= 1;
+        endPos = startPos;
+        while (startPos > 0 && str[startPos - 1] != ' ') startPos--;
+        newPattern = TRUE;
+        mode = 3;
+    }
+    else
+    {
+        if (optDebug)
+            printMsg(currWin, "no mode\n");
+        return FALSE;
+    }
+
+    if (str[endPos] == optNickSep)
+    {
+        endPos--;
+        if (startPos > 0)
+        {
+            if (optDebug)
+                printMsg(currWin, "str[endPos] == optNickSep && startPos > 0 (%d)\n", startPos);
+            return FALSE;
+        }
+        hasSeparator = TRUE;
+    }
+
+    if (buf->pos > 0 && str[buf->pos - 1] == ' ')
+        hasSpace = TRUE;
+    if (buf->pos <= buf->len && str[buf->pos] == ' ')
+        hasSpace = TRUE;
+
+    if (newPattern)
+    {
+        /* Get pattern, check if it matches previous pattern and set 'again' flag */
+        char *npattern = nn_editbuf_get_string(buf, startPos, endPos);
+        if (pattern && npattern && th_strcasecmp(npattern, pattern) == 0)
+            again = TRUE;
+
+        th_free(pattern);
+        pattern = npattern;
+
+        if (!again)
+        {
+            th_free(previous);
+            previous = NULL;
+        }
+    }
+
+    if (optDebug)
+    {
+        printMsg(currWin, "sPos=%d, ePos=%d <-> bPos=%d, bufLen=%d : pat='%s' (again=%s, hassep=%s, hasspc=%s, newpat=%s, mode=%d)\n",
+                 startPos, endPos, buf->pos, buf->len, pattern,
+                 again ? "yes" : "no",
+                 hasSeparator ? "yes" : "no",
+                 hasSpace ? "yes" : "no",
+                 newPattern ? "yes" : "no", mode);
+    }
+
+    if (pattern)
+    {
+        nn_user_t *user = nn_user_match(nnUsers, pattern, previous, again);
+
+        if (user)
+        {
+            int i;
+            char *c = user->name;
+            if (optDebug)
+                printMsg(currWin, "match='%s' / prev='%s'\n", user->name, previous);
+
+            for (i = startPos; i <= endPos; i++)
+                nn_editbuf_delete(buf, startPos);
+
+            for (i = startPos; *c; i++, c++)
+                nn_editbuf_insert(buf, i, *c);
+
+            if (!hasSeparator && startPos == 0)
+            {
+                nn_editbuf_insert(buf, i++, optNickSep);
+                startPos++;
+            }
+            if (hasSeparator)
+                startPos++;
+            if (!hasSpace)
+                nn_editbuf_insert(buf, i++, ' ');
+
+            nn_editbuf_setpos(buf, startPos + 1 + strlen(user->name));
+
+            th_free(previous);
+            previous = th_strdup(user->name);
+
+            return TRUE;
+        }
+    }
+
+    return FALSE;
+}
+
+
+#define VPUTCH(CH)  th_vputch(&bufData, &bufSize, &bufLen, CH)
+#define VPUTS(STR)  th_vputs(&bufData, &bufSize, &bufLen, STR)
+
+char *logParseFilename(const char *fmt, int id)
+{
+    size_t bufSize = strlen(fmt) + TH_BUFGROW, bufLen = 0;
+    char *bufData = th_malloc(bufSize);
+    char tmpBuf[32];
+    const char *s = fmt;
+
+    while (*s)
+    {
+        if (*s == '%')
+        {
+            s++;
+            switch (*s)
+            {
+            case 'i':
+                snprintf(tmpBuf, sizeof(tmpBuf), "%05d", id);
+                VPUTS(tmpBuf);
+                break;
+
+            case 'd':
+                snprintf(tmpBuf, sizeof(tmpBuf), "%d", id);
+                VPUTS(tmpBuf);
+                break;
+
+            case '%':
+                VPUTCH('%');
+                break;
+            }
+            s++;
+        }
+        else
+        {
+            VPUTCH(*s);
+            s++;
+        }
+    }
+
+    VPUTCH(0);
+    return bufData;
+}
+
+
+BOOL logFileOpen(void)
+{
+    char *filename;
+
+    if (optLogFilename == NULL || !optLogEnable)
+        return FALSE;
+
+    filename = logParseFilename(optLogFilename, optPort);
+
+    if ((optLogFile = fopen(filename, "a")) == NULL)
+    {
+        errorMsg("Could not open logfile '%s' for appending!\n", filename);
+        th_free(filename);
+        return FALSE;
+    }
+
+    th_free(filename);
+
+    return TRUE;
+}
+
+
+void logFileClose(void)
+{
+    if (optLogFile)
+    {
+        fclose(optLogFile);
+        optLogFile = NULL;
+    }
+}
+
+
+char *promptRequester(WINDOW *win, const char *info, BOOL allowEmpty)
+{
+    char tmpBuf[512], *ptr;
+    ssize_t pos;
+    int curVis = curs_set(1);
+
+    echo();
+    waddstr(win, info);
+    wgetnstr(win, tmpBuf, sizeof(tmpBuf) - 1);
+    noecho();
+    if (curVis != ERR)
+        curs_set(curVis);
+
+    for (pos = strlen(tmpBuf) - 1; pos > 0 && th_isspace(tmpBuf[pos]); pos--)
+        tmpBuf[pos] = 0;
+
+    ptr = str_trim_left(tmpBuf);
+
+    if (allowEmpty || strlen(ptr) > 0)
+        return th_strdup(ptr);
+    else
+        return NULL;
+}
+
+
+void printHelp(void)
+{
+    printMsgQ(currWin, "\n"
+    "NNChat Help\n"
+    "===========\n"
+    "\n"
+    "F1             This help.\n"
+    "F2             \n"
+    );
+}
+
+
+int main(int argc, char *argv[])
+{
+    nn_conn_t *conn = NULL;
+    int curVis = ERR, updateCount = 0;
+    BOOL argsOK, isError = FALSE,
+        exitProg = FALSE,
+        colorSet = FALSE,
+        cursesInit = FALSE,
+        networkInit = FALSE,
+        insertMode = TRUE,
+        firstUpdate = TRUE;
+    time_t prevTime;
+    char *tmpStr;
+    nn_editbuf_t *editBuf = nn_editbuf_new(NN_TMPBUF_SIZE);
+    nn_editbuf_t *histBuf[SET_MAX_HISTORY+2];
+    int histPos = 0, histMax = 0;
+
+    cfgitem_t *tmpcfg;
+    char *homeDir = NULL;
+
+    memset(histBuf, 0, sizeof(histBuf));
+
+    /* Initialize */
+    th_init("NNChat", "Newbie Nudes chat client", NN_VERSION,
+        "Written and designed by Anonymous Finnish Guy (C) 2008-2012",
+        "This software is freeware, use and distribute as you wish.");
+    th_verbosityLevel = 0;
+
+    /* Read configuration file */
+    tmpcfg = NULL;
+    th_cfg_add_comment(&tmpcfg, "General settings");
+    th_cfg_add_string(&tmpcfg, "username", &optUserName, NULL);
+    th_cfg_add_string(&tmpcfg, "password", &optPassword, NULL);
+
+    th_cfg_add_comment(&tmpcfg, "Default color as a hex-triplet");
+    th_cfg_add_hexvalue(&tmpcfg, "color", &optUserColor, optUserColor);
+
+    th_cfg_add_comment(&tmpcfg, "Default setting of ignore mode");
+    th_cfg_add_bool(&tmpcfg, "ignore", &setIgnoreMode, setIgnoreMode);
+    th_cfg_add_comment(&tmpcfg, "People to be ignored when ignore mode is enabled");
+    th_cfg_add_string_list(&tmpcfg, "ignore_list", &setIgnoreList);
+
+    th_cfg_add_comment(&tmpcfg, "Random messages for idle timeout protection. If none are set, plain '.' is used.");
+    th_cfg_add_string_list(&tmpcfg, "idle_messages", &setIdleMessages);
+
+    th_cfg_add_comment(&tmpcfg, "Character used as nickname auto-completion separator (default is ':')");
+    th_cfg_add_string(&tmpcfg, "nick_separator", &optNickSepStr, NULL);
+
+    th_cfg_add_section(&cfg, "general", tmpcfg);
+
+
+    tmpcfg = NULL;
+    th_cfg_add_comment(&tmpcfg, "Chat server hostname or IP address");
+    th_cfg_add_string(&tmpcfg, "host", &optServer, optServer);
+    th_cfg_add_comment(&tmpcfg, "Default port to connect to (8005 = main room, 8003 = passion pit)");
+    th_cfg_add_int(&tmpcfg, "port", &optPort, optPort);
+    th_cfg_add_section(&cfg, "server", tmpcfg);
+
+    tmpcfg = NULL;
+    th_cfg_add_comment(&tmpcfg, "Proxy server type (0 = none, 1 = SOCKS 4, 2 = SOCKS 4a)");
+    th_cfg_add_int(&tmpcfg, "type", &optProxyType, optProxyType);
+    th_cfg_add_comment(&tmpcfg, "Proxy server host name");
+    th_cfg_add_string(&tmpcfg, "host", &optProxyServer, optProxyServer);
+    th_cfg_add_comment(&tmpcfg, "Proxy port, 1080 is the standard SOCKS port");
+    th_cfg_add_int(&tmpcfg, "port", &optProxyPort, optProxyPort);
+    th_cfg_add_section(&cfg, "proxy", tmpcfg);
+
+    tmpcfg = NULL;
+    th_cfg_add_comment(&tmpcfg, "Enable logging");
+    th_cfg_add_bool(&tmpcfg, "enable", &optLogEnable, optLogEnable);
+    th_cfg_add_comment(&tmpcfg, "Log filename format");
+    th_cfg_add_string(&tmpcfg, "filename", &optLogFilename, optLogFilename);
+    th_cfg_add_section(&cfg, "logging", tmpcfg);
+
+#ifdef __WIN32
+    {
+        char tmpPath[MAX_PATH];
+        if (SHGetFolderPath(NULL, CSIDL_APPDATA | CSIDL_FLAG_CREATE, NULL, 0, tmpPath) == S_OK)
+            homeDir = th_strdup(tmpPath);
+
+        CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
+    }
+#else
+    homeDir = th_strdup(getenv("HOME"));
+#endif
+
+    if (homeDir != NULL)
+    {
+        FILE *cfgfile;
+        setConfigFile = th_strdup_printf("%s" SET_DIR_SEPARATOR "%s", homeDir, SET_CONFIG_FILE);
+
+        THMSG(0, "Reading configuration from '%s'.\n", setConfigFile);
+
+        if ((cfgfile = fopen(setConfigFile, "r")) != NULL)
+        {
+            th_cfg_read(cfgfile, setConfigFile, cfg);
+            fclose(cfgfile);
+        }
+    }
+
+    if (optNickSepStr)
+        optNickSep = optNickSepStr[0];
+    else
+        optNickSep = SET_NICK_SEPARATOR;
+
+
+    setBrowser = getenv("BROWSER");
+    if (setBrowser == NULL)
+        setBrowser = "firefox";
+
+    /* Parse command line arguments */
+    argsOK = th_args_process(argc, argv, optList, optListN,
+                             argHandleOpt, argHandleFile, FALSE);
+
+    if (optUserNameCmd != NULL)
+    {
+        optUserName = optUserNameCmd;
+        optPassword = optPasswordCmd;
+    }
+
+    if (!argsOK)
+        return -2;
+
+    /* Allocate userhash */
+    if ((nnUsers = nn_userhash_new()) == NULL)
+    {
+        THERR("Could not allocate userhash. Fatal error.\n");
+        return -105;
+    }
+
+    /* If no idle messages are set, add default */
+    if (setIdleMessages == NULL)
+    {
+        th_llist_append(&setIdleMessages, th_strdup("."));
+    }
+
+    /* Open logfile */
+    logFileOpen();
+
+    /* Initialize network */
+    if (!nn_network_init())
+    {
+        THERR("Could not initialize network subsystem.\n");
+        goto err_exit;
+    }
+    else
+        networkInit = TRUE;
+
+    /* Initialize NCurses */
+    if (!optDaemon)
+    {
+        if (LINES < 0 || LINES > 1000) LINES = 24;
+        if (COLS < 0 || COLS > 1000) COLS = 80;
+        initscr();
+        raw();
+        keypad(stdscr, TRUE);
+        noecho();
+        meta(stdscr, TRUE);
+        timeout(SET_DELAY);
+        curVis = curs_set(0);
+
+        if (has_colors())
+        {
+            start_color();
+
+            init_pair( 1, COLOR_RED,     COLOR_BLACK);
+            init_pair( 2, COLOR_GREEN,   COLOR_BLACK);
+            init_pair( 3, COLOR_YELLOW,  COLOR_BLACK);
+            init_pair( 4, COLOR_BLUE,    COLOR_BLACK);
+            init_pair( 5, COLOR_MAGENTA, COLOR_BLACK);
+            init_pair( 6, COLOR_CYAN,    COLOR_BLACK);
+            init_pair( 7, COLOR_WHITE,   COLOR_BLACK);
+            init_pair( 8, COLOR_BLACK,   COLOR_BLACK);
+
+            init_pair(10, COLOR_BLACK,   COLOR_RED);
+            init_pair(11, COLOR_WHITE,   COLOR_RED);
+            init_pair(12, COLOR_GREEN,   COLOR_RED);
+            init_pair(13, COLOR_YELLOW,  COLOR_RED);
+            init_pair(14, COLOR_BLUE,    COLOR_RED);
+            init_pair(15, COLOR_MAGENTA, COLOR_RED);
+            init_pair(16, COLOR_CYAN,    COLOR_RED);
+        }
+
+        cursesInit = TRUE;
+
+        if (!initializeWindows())
+            goto err_exit;
+
+#ifdef PDCURSES
+        PDC_set_title("NNChat v" NN_VERSION);
+#endif
+
+        memset(chatWindows, 0, sizeof(chatWindows));
+        chatWindows[0] = nn_window_new(NULL);
+        currWin = chatWindows[0];
+        updateStatus();
+    }
+
+    /* Check if we have username and password */
+    if (cursesInit && (optUserName == NULL || optPassword == NULL))
+    {
+        printWin(editWin, "You can avoid this prompt by issuing '/save' after logging in.\n");
+        optUserName = promptRequester(editWin, "NN username: ", FALSE);
+        optPassword = promptRequester(editWin, "NN password: ", TRUE);
+    }
+
+    if (optUserName == NULL || optPassword == NULL)
+    {
+        errorMsg("Username and/or password not specified.\n");
+        goto err_exit;
+    }
+
+    /* Create a connection */
+    conn = nn_conn_new(errorFunc, messageFunc);
+    if (conn == NULL)
+    {
+        errorMsg("Could not create connection structure.\n");
+        goto err_exit;
+    }
+
+    /* Are we using a proxy? */
+    if (optProxyType != NN_PROXY_NONE && optProxyServer != NULL)
+    {
+        if (nn_conn_set_proxy(conn, optProxyType, optProxyPort, optProxyServer) != 0)
+        {
+            errorMsg("Error setting proxy information.\n");
+            goto err_exit;
+        }
+    }
+
+    /* Okay ... */
+    printMsg(currWin, "Trying to resolve host '%s' ...\n", optServer);
+    conn->host = th_strdup(optServer);
+    conn->hst = nn_resolve_host(conn, optServer);
+    if (conn->hst == NULL)
+    {
+        errorMsg("Could not resolve hostname: %s.\n", strerror(h_errno));
+        goto err_exit;
+    }
+
+#ifdef FINAL_BUILD
+    /* To emulate the official client, we first make a request for
+     * policy file, even though we don't use it for anything...
+     */
+    if (nn_conn_open(conn, 843, NULL) != 0)
+    {
+        errorMsg("Policy file request connection setup failed!\n");
+        goto err_exit;
+    }
+
+    tmpStr = "<policy-file-request/>";
+    if (nn_conn_send_buf(conn, tmpStr, strlen(tmpStr) + 1) == FALSE)
+    {
+        errorMsg("Failed to send policy file request.\n");
+        goto err_exit;
+    }
+    else
+    {
+        int cres = nn_conn_pull(conn);
+        if (cres == 0)
+        {
+            printMsg(currWin, "Probe got: %s\n", conn->buf);
+        }
+        else
+        {
+            printMsg(currWin, "Could not get policy probe.\n");
+        }
+    }
+    nn_conn_close(conn);
+#endif
+
+    /* Okay, now do the proper connection ... */
+    if (nn_conn_open(conn, optPort, NULL) != 0)
+    {
+        errorMsg("Main connection setup failed!\n");
+        goto err_exit;
+    }
+
+    /* Send login command */
+    optUserNameEnc = nn_dblencode_str(optUserName);
+    tmpStr = nn_dblencode_str(optSite);
+    nn_conn_send_msg(conn, optUserNameEnc, "%%2Flogin%%20%%2Dsite%%20%s%%20%%2Dpassword%%20%s", tmpStr, optPassword);
+    th_free(tmpStr);
+
+    /* Initialize random numbers */
+    prevTime = time(NULL);
+    srandom((int) prevTime);
+
+    if (cursesInit)
+    {
+        /* Initialize rest of interactive UI code */
+        nn_editbuf_clear(editBuf);
+
+        /* First update of screen */
+        printEditBuf(editBuf);
+        updateStatus();
+
+        printMsg(NULL, "%s v%s - %s\n", th_prog_name, th_prog_version, th_prog_fullname);
+        printMsg(NULL, "%s\n", th_prog_author);
+        printMsg(NULL, "%s\n", th_prog_license);
+    }
+
+    /* Enter mainloop */
+    nn_conn_reset(conn);
+    while (!isError && !exitProg)
+    {
+        nn_conn_reset(conn);
+        do {
+            int cres = nn_conn_pull(conn);
+            if (cres == 0 && *(conn->in_ptr - 1) == 0)
+            {
+                int result = nn_parse_protocol(conn);
+                if (result > 0)
+                {
+                    /* Couldn't handle the message for some reason */
+                    printMsg(currWin, "Could not handle: %s\n", conn->ptr);
+                }
+                else if (result < 0)
+                {
+                    /* Fatal error, quit */
+                    errorMsg("Fatal error with message: %s\n", conn->ptr);
+                    isError = TRUE;
+                }
+            }
+            else if (cres < 0)
+                isError = TRUE;
+            else
+                break;
+        }
+        while (conn->total_bytes > 0 && !isError);
+
+        if (!nn_conn_check(conn))
+            isError = TRUE;
+
+        /* Handle user input */
+        if (cursesInit)
+        {
+            int c, cnt = 0;
+            BOOL update = FALSE, updateMain = FALSE;
+
+            /* Handle several buffered keypresses at once */
+            do
+            {
+                c = wgetch(stdscr);
+                
+                /* Handle various problematic cases where terminal 
+                 * keycodes do not get properly translated by curses
+                 */
+                if (c == 0x1b)
+                {
+                    /* ^[O */
+                    c = wgetch(stdscr);
+                    if (c == 'O')
+                    {
+                        c = wgetch(stdscr);
+                        switch (c)
+                        {
+                        case 'd':
+                            c = 0x204;
+                            break;
+                        case 'c':
+                            c = 0x206;
+                            break;
+                        default:
+                            if (optDebug)
+                                printMsg(currWin, "Unhandled ESC-O key sequence 0x%02x\n", c);
+                            break;
+                        }
+                    }
+                    /* ^[[ */
+                    else if (c == '[')
+                    {
+                        c = wgetch(stdscr);
+                        switch (c)
+                        {
+                        case 0x31:
+                            c = wgetch(stdscr);
+                            if (c >= 0x31 && c <= 0x39)
+                                c = KEY_F(c - 0x30);
+                            else
+                                c = ERR;
+                            break;
+
+                        case 0x32:
+                            c = KEY_IC;
+                            break;
+                        case 0x33:
+                            c = KEY_DC;
+                            break;
+
+                        case 0x35:
+                            c = KEY_PPAGE;
+                            break;
+                        case 0x36:
+                            c = KEY_NPAGE;
+                            break;
+
+                        case 0x37:
+                            c = KEY_HOME;
+                            break;
+                        case 0x38:
+                            c = KEY_END;
+                            break;
+
+                        default:
+                            if (optDebug)
+                                printMsg(currWin, "Unhandled ESC-[*~ key sequence 0x%02x\n", c);
+                            c = ERR;
+                            break;
+                        }
+                        /* Get the trailing ~ */
+                        if (c != ERR)
+                            wgetch(stdscr);
+                    }
+                    if (c >= 0x31 && c <= 0x39)
+                    {
+                        /* Chat window switching via Meta/Esc-[1..9] */
+                        int win = c - 0x31;
+                        if (win < SET_MAX_WINDOWS && chatWindows[win] != NULL)
+                        {
+                            currWin = chatWindows[win];
+                            update = updateMain = TRUE;
+                        }
+                        c = ERR;
+                    }
+                    else
+                    {
+                        if (optDebug)
+                            printMsg(currWin, "Unhandled ESC key sequence 0x%02x\n", c);
+                    }
+                }
+#if defined(__WIN32) && defined(PDCURSES)
+                else if (c >= 0x198 && c <= 0x1a0)
+                {
+                    /* Chat window switching via Meta/Esc-[1..9] */
+                    int win = c - 0x198;
+                    if (win < SET_MAX_WINDOWS && chatWindows[win] != NULL)
+                    {
+                        currWin = chatWindows[win];
+                        update = updateMain = TRUE;
+                    }
+                    c = ERR;
+                }
+#endif
+
+                switch (c)
+                {
+#ifdef KEY_RESIZE
+                case KEY_RESIZE:
+                    resize_term(0, 0);
+                    erase();
+                    timeout(SET_DELAY);
+
+                    if (!initializeWindows())
+                    {
+                        errorMsg("Error resizing curses chatWindows\n");
+                        isError = TRUE;
+                    }
+                    update = updateMain = TRUE;
+                    break;
+#endif
+
+                case KEY_ENTER:
+                case '\n':
+                case '\r':
+                    /* Call the user input handler */
+                    if (editBuf->len > 0)
+                    {
+                        int result;
+
+                        if (histMax > 0)
+                        {
+                            nn_editbuf_free(histBuf[SET_MAX_HISTORY+1]);
+                            histBuf[SET_MAX_HISTORY+1] = NULL;
+                            memmove(&histBuf[2], &histBuf[1], histMax * sizeof(histBuf[0]));
+                        }
+
+                        histPos = 0;
+                        histBuf[1] = nn_editbuf_copy(editBuf);
+                        if (histMax < SET_MAX_HISTORY) histMax++;
+
+                        nn_editbuf_insert(editBuf, editBuf->len, 0);
+                        result = nn_handle_input(conn, editBuf->data, editBuf->len);
+
+                        nn_editbuf_clear(editBuf);
+
+                        if (result < 0)
+                        {
+                            errorMsg("Fatal error handling user input: %s\n", editBuf->data);
+                            isError = TRUE;
+                        }
+                        else
+                        {
+                            /* Update time value of last sent message for unidle timeouts */
+                            prevTime = time(NULL);
+                        }
+
+                        updateMain = update = TRUE;
+                    }
+                    break;
+
+                case KEY_UP: /* Backwards in input history */
+                    if (histPos == 0)
+                    {
+                        nn_editbuf_free(histBuf[0]);
+                        histBuf[0] = nn_editbuf_copy(editBuf);
+                    }
+                    if (histPos < histMax)
+                    {
+                        histPos++;
+                        nn_editbuf_free(editBuf);
+                        editBuf = nn_editbuf_copy(histBuf[histPos]);
+                        update = TRUE;
+                    }
+                    break;
+
+                case KEY_DOWN: /* Forwards in input history */
+                    if (histPos > 0)
+                    {
+                        histPos--;
+                        nn_editbuf_free(editBuf);
+                        editBuf = nn_editbuf_copy(histBuf[histPos]);
+                        update = TRUE;
+                    }
+                    break;
+
+                case 0x204: /* ctrl+left arrow = Skip words left */
+                case 0x20b:
+                    while (editBuf->pos > 0 && isspace((int) editBuf->data[editBuf->pos - 1]))
+                        editBuf->pos--;
+                    while (editBuf->pos > 0 && !isspace((int) editBuf->data[editBuf->pos - 1]))
+                        editBuf->pos--;
+                    update = TRUE;
+                    break;
+
+                case 0x206: /* ctrl+right arrow = Skip words right */
+                case 0x210:
+                    while (editBuf->pos < editBuf->len && isspace((int) editBuf->data[editBuf->pos]))
+                        editBuf->pos++;
+                    while (editBuf->pos < editBuf->len && !isspace((int) editBuf->data[editBuf->pos]))
+                        editBuf->pos++;
+                    update = TRUE;
+                    break;
+
+                case KEY_HOME:
+                    nn_editbuf_setpos(editBuf, 0);
+                    update = TRUE;
+                    break;
+                case KEY_END:
+                    nn_editbuf_setpos(editBuf, editBuf->len);
+                    update = TRUE;
+                    break;
+                case KEY_LEFT:
+                    nn_editbuf_setpos(editBuf, editBuf->pos - 1);
+                    update = TRUE;
+                    break;
+                case KEY_RIGHT:
+                    nn_editbuf_setpos(editBuf, editBuf->pos + 1);
+                    update = TRUE;
+                    break;
+
+                case KEY_BACKSPACE:
+                case 0x08:
+                case 0x7f:
+                    nn_editbuf_delete(editBuf, editBuf->pos - 1);
+                    nn_editbuf_setpos(editBuf, editBuf->pos - 1);
+                    update = TRUE;
+                    break;
+
+                case KEY_DC: /* Delete character */
+                    nn_editbuf_delete(editBuf, editBuf->pos);
+                    update = TRUE;
+                    break;
+
+
+                case KEY_IC: /* Ins = Toggle insert / overwrite mode */
+                    insertMode = !insertMode;
+                    update = TRUE;
+                    break;
+
+                case KEY_F(1): /* F1 = Print help */
+                    printHelp();
+                    updateMain = TRUE;
+                    break;
+
+                case KEY_F(2): /* F2 = Clear editbuffer */
+                    nn_editbuf_clear(editBuf);
+                    update = TRUE;
+                    break;
+
+                case KEY_F(5): /* F5 = Ignore mode */
+                    setIgnoreMode = !setIgnoreMode;
+                    printMsgQ(currWin, "Ignore mode = %s\n", setIgnoreMode ? "ON" : "OFF");
+                    break;
+
+#if 0
+                case KEY_F(8): /* F8 = Debug */
+                    optDebug = !optDebug;
+                    update = TRUE;
+                    break;
+#endif
+
+                case 0x03: /* ^C = quit */
+                case KEY_F(9): /* F9 = Quit */
+                    printMsg(currWin, "Quitting per user request (%d/0x%x).\n", c, c);
+                    exitProg = TRUE;
+                    break;
+
+                case 0x09: /* Tab = complete username */
+                    performTabCompletion(editBuf);
+                    update = TRUE;
+                    break;
+
+                case 0x0c: /* Ctrl + L */
+                    updateWindows();
+                    update = updateMain = TRUE;
+                    break;
+
+                case KEY_NPAGE:
+                case KEY_PPAGE:
+                    /* Page Up / Page Down */
+                    if (currWin != NULL)
+                    {
+                        int oldPos = currWin->pos;
+
+                        currWin->pos += (c == KEY_NPAGE) ? -10 : +10;
+
+                        if (currWin->pos < 0)
+                            currWin->pos = 0;
+                        else if (currWin->pos >= currWin->data->n - 10)
+                            currWin->pos = currWin->data->n - 10;
+
+                        if (oldPos != currWin->pos)
+                            updateMain = TRUE;
+                    }
+                    break;
+
+                case ERR:
+                    /* Ignore */
+                    break;
+
+                default:
+                    if (isprint(c) || c == 0xe4 || c == 0xf6 || c == 0xc4 || c == 0xd6)
+                    {
+                        if (insertMode)
+                            nn_editbuf_insert(editBuf, editBuf->pos, c);
+                        else
+                            nn_editbuf_write(editBuf, editBuf->pos, c);
+                        nn_editbuf_setpos(editBuf, editBuf->pos + 1);
+                        update = TRUE;
+                    }
+                    else
+                    {
+                        if (optDebug)
+                            printMsg(currWin, "Unhandled key: 0x%02x\n", c);
+                    }
+                    break;
+                }
+            }
+            while (c != ERR && !exitProg && ++cnt < 10);
+
+            updateMainWin(updateMain);
+
+            if (update || firstUpdate)
+            {
+                /* Update edit line */
+                updateStatus();
+                printEditBuf(editBuf);
+                firstUpdate = FALSE; /* a nasty hack ... */
+            }
+
+        } /* cursesInit */
+
+        if (++updateCount > 10)
+        {
+            time_t tmpTime = time(NULL);
+            if (tmpTime - prevTime > SET_KEEPALIVE)
+            {
+                int n = random() % th_llist_length(setIdleMessages);
+                qlist_t *node = th_llist_get_nth(setIdleMessages, n);
+                nn_conn_send_msg(conn, optUserNameEnc, node->data);
+                prevTime = tmpTime;
+            }
+
+            if (!colorSet)
+            {
+                colorSet = TRUE;
+                nn_conn_send_msg(conn, optUserNameEnc, "%%2FSetFontColor%%20%%2Dcolor%%20%06X", optUserColor);
+            }
+
+            updateStatus();
+            printEditBuf(editBuf);
+            updateCount = 0;
+        }
+
+    }
+
+    /* Shutdown */
+err_exit:
+    th_cfg_free(cfg);
+    th_free(homeDir);
+    th_llist_free_func(setIdleMessages, th_free);
+    nn_userhash_free(nnUsers);
+    nn_editbuf_free(editBuf);
+
+    {
+        int i;
+        for (i = 0; i <= SET_MAX_HISTORY; i++)
+            nn_editbuf_free(histBuf[i]);
+
+        for (i = 0; i < SET_MAX_WINDOWS; i++)
+            nn_window_free(chatWindows[i]);
+    }
+
+#ifdef __WIN32
+    if (errorMessages)
+    {
+        char *tmp;
+        wclear(editWin);
+        tmp = promptRequester(editWin, "Press enter to quit.\n", FALSE);
+        th_free(tmp);
+    }
+#endif
+
+    if (cursesInit)
+    {
+        if (curVis != ERR)
+            curs_set(curVis);
+        closeWindows();
+        endwin();
+        THMSG(1, "NCurses deinitialized.\n");
+    }
+
+#ifndef __WIN32
+    if (errorMessages)
+        THERR("%s", errorMessages);
+#endif
+
+    th_free(optUserNameEnc);
+
+    nn_conn_close(conn);
+
+    if (networkInit)
+        nn_network_close();
+
+    THMSG(1, "Connection terminated.\n");
+
+    logFileClose();
+
+    return 0;
+}