view main.c @ 516:08bbd850e2dc dev-1_2_0

Cosmetics.
author Matti Hamalainen <ccr@tnsp.org>
date Tue, 05 Jun 2012 22:26:53 +0300
parents 151edcb79ce4
children 8132548e1a07
line wrap: on
line source

/*
 * NNChat - Custom chat client for NewbieNudes.com chatrooms
 * Written by Matti 'ccr' Hämäläinen
 * (C) Copyright 2008-2012 Tecnic Software productions (TNSP)
 */
#include "util.h"
#include "network.h"
#include "ui.h"
#include "th_args.h"
#include "th_config.h"
#include <time.h>
#include <errno.h>
#ifdef __WIN32
#include <shlwapi.h>
#else
#include <sys/wait.h>
#endif

#ifdef __WIN32
#define SET_CONFIG_FILE    "nnchat.txt"
#define SET_DIR_SEPARATOR  "\\"
#else
#define SET_CONFIG_FILE    ".nnchat"
#define SET_DIR_SEPARATOR  "/"
#endif

#define SET_PROFILE_PREFIX "http://www.newbienudes.com/profile/%s/"
#define SET_NICK_SEPARATOR ':'

#define SET_MAX_HISTORY (16)        // Command history length
#define SET_KEEPALIVE   (15*60)     // Ping/keepalive period in seconds


/* 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;

qlist_t *setIgnoreList = NULL,
        *setIdleMessages = NULL;
nn_userhash_t *nnUsers = NULL;
char    *setConfigFile = NULL,
        *setBrowser = NULL;
cfgitem_t *cfg = NULL;

nn_editbuf_t *editHistBuf[SET_MAX_HISTORY+2];
int      editHistPos = 0,
         editHistMax = 0;

/* 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;
}


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 : nnwin_main_window();
        if (flags & LOG_STAMP) nnwin_print(tmp, tmpStr);
        nnwin_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 debugMsg(const char *fmt, ...)
{
    if (optDebug)
    {
        va_list ap;

        va_start(ap, fmt);
        printMsgV(NULL, LOG_FILE|LOG_WINDOW, 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_parse_user(nn_conn_t *conn)
{
    BOOL isMine, isIgnored = FALSE;
    char *name, *msg, *t;

    // Find start of the message
    name = conn->ptr;
    t = nn_conn_buf_strstr(conn, "</USER>");
    if (!t) return 1;
    *t = 0;

    // Find end of the message
    t = nn_conn_buf_strstr(conn, "<MESSAGE>");
    if (!t) return 2;
    msg = conn->ptr;
    
    t = nn_conn_buf_strstr(conn, "</MESSAGE>");
    if (!t) return 3;
    *t = 0;

    // Decode message string
    msg = nn_decode_str1(msg);
    if (!msg)
    {
        errorMsg("Decode/malloc failure @ nnproto_parse_user()\n");
        return -1;
    }

    // Decode username
    name = nn_decode_str1(name);
    if (!name)
    {
        errorMsg("Decode/malloc failure @ nnproto_parse_user()\n");
        th_free(msg);
        return -2;
    }

    /* Check if the username is on our ignore list and
     * that it is not our OWN username!
     */
    isMine = strcmp(name, optUserName) == 0;
    isIgnored = setIgnoreMode && !isMine && checkIgnoreList(name);

    // Is it a special control message?
    if (*msg == '/')
    {
        // Ignore room join/leave messages
        if (!optDebug && (strstr(msg, "left the room") || strstr(msg, "joined the room from")))
            goto done;

        t = nn_strip_tags(msg + 1);
        if (!strncmp(t, "BPRV ", 5))
        {
            char *in_name, *tmp, *in_msg, *h;
            nn_window_t *win;
            h = nn_decode_str2(t + 1);

            // Check type of 
            if ((isMine = strncmp(t, "BPRV from ", 10)) == 0)
                in_name = nn_decode_str2(t + 10);
            else
                in_name = nn_decode_str2(t + 8);

            for (tmp = in_name; *tmp && *tmp != ':'; tmp++);
            if (tmp[0] != 0 && tmp[1] == ' ')
                in_msg = tmp + 2;
            else
                in_msg = "";
            *tmp = 0;

            isIgnored = setIgnoreMode && checkIgnoreList(in_name);
            win = nnwin_find(in_name);

            if (win != NULL)
            {
                printMsgF(win, isIgnored ? 0 : LOG_WINDOW,
                    "½5½<½%d½%s½5½>½0½ %s\n",
                    isMine ? 14 : 15, isMine ? optUserName : in_name, in_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(in_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(msg);
        h = nn_decode_str2(t);
        printMsgF(NULL, isIgnored ? LOG_FILE : (LOG_WINDOW | LOG_FILE),
            "½5½<½%d½%s½5½>½0½ %s\n", isMine ? 14 : 15, name, h);
        th_free(h);
        th_free(t);
    }

done:
    th_free(msg);
    th_free(name);
    return 0;
}


int nnproto_parse_login(nn_conn_t *conn)
{
    char tmpStr[256];
    str_get_timestamp(tmpStr, sizeof(tmpStr), "%c");

    if (!nn_conn_buf_strcmp(conn, "FAILURE>"))
    {
        nn_conn_buf_strstr(conn, "</LOGIN_FAILURE>");
        nn_conn_buf_strstr(conn, "</USER>");
        printMsg(NULL, "½1½Login failure½0½ - ½3½%s½0½\n", tmpStr);
        return -2;
    }
    else if (!nn_conn_buf_strcmp(conn, "SUCCESS>"))
    {
        nn_conn_buf_strstr(conn, "</LOGIN_SUCCESS>");
        nn_conn_buf_strstr(conn, "</USER>");
        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_parse_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)
    {
        errorMsg("Decode/malloc failure @ nnproto_parse_add_user()\n");
        return -1;
    }

    win = nnwin_find(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_parse_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)
    {
        errorMsg("Decode/malloc failure @ nnproto_parse_delete_user()\n");
        return -1;
    }

    win = nnwin_find(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_parse_num_clients(nn_conn_t *conn)
{
    nn_conn_buf_strstr(conn, "</NUMCLIENTS>");
    return 0;
}


int nnproto_parse_boot(nn_conn_t *conn)
{
    (void) conn;
    errorMsg("Booted by server.\n");
    return -1;
}


typedef struct
{
    char *name;
    size_t len;
    int (*handler)(nn_conn_t *);
} nn_protocolcmd_t;


static nn_protocolcmd_t protoCmds[] =
{
    { "<USER>",         0, nnproto_parse_user },
    { "<LOGIN_",        0, nnproto_parse_login },
    { "<DELETE_USER>",  0, nnproto_parse_delete_user },
    { "<ADD_USER>",     0, nnproto_parse_add_user },
    { "<NUMCLIENTS>",   0, nnproto_parse_num_clients },
    { "<BOOT />",       0, nnproto_parse_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].name);

        protoCmdsInit = TRUE;
    }

    for (i = 0; i < nprotoCmds; i++)
    {
        if (!nn_conn_buf_strncmp(conn, protoCmds[i].name, 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_open_uri(const char *uri)
{
#ifdef __WIN32
    HINSTANCE status;

    status = ShellExecute(NULL, "open", uri, 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;

    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);
        char *url = th_strdup_printf("openurl(%s,new-tab)", uri);
        execlp(setBrowser, setBrowser, "-remote", url, (void *)NULL);
        th_free(url);
        _exit(errno);
    }

    wait(&status);
#endif

    return 0;
}


int nncmd_open_profile(nn_conn_t *conn, char *name)
{
    char *enc_name = nn_encode_str1(name);
    char *uri = th_strdup_printf(SET_PROFILE_PREFIX, name);
    (void) conn;

    printMsg(currWin, "Opening profile for: '%s'\n", name);
    
    nn_open_uri(uri);

    th_free(uri);
    th_free(enc_name);
    return 0;
}


int nncmd_ignore(nn_conn_t *conn, char *name)
{
    (void) conn;

    if (name[0])
    {
        // Add or remove someone to/from ignore
        qlist_t *user = th_llist_find_func(setIgnoreList, name, str_compare);
        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;
        size_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;
}


int nncmd_set_color(nn_conn_t *conn, char *arg)
{
    int val;
    (void) conn;

    if ((val = th_get_hex_triplet(arg)) < 0)
    {
        printMsgQ(currWin, "Invalid color value '%s'\n", arg);
        return 1;
    }

    optUserColor = val;
    printMsgQ(currWin, "Setting color to #%06x\n", optUserColor);
    nn_conn_send_msg_v(conn, optUserNameEnc, "%%2FSetFontColor%%20%%2Dcolor%%20%06X", optUserColor);
    return 0;
}


int nncmd_open_query(nn_conn_t *conn, char *name)
{
    (void) conn;

    if (name[0])
    {
        nn_user_t *user = nn_userhash_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 (nnwin_open(name, TRUE))
                printMsgQ(currWin, "In PRV query with '%s'.\n", name);
            th_free(name);
            return 0;
        }
        else
        {
            printMsgQ(currWin, "Could not find username '%s'.\n", name);
            return 1;
        }
    }
    else
    {
        printMsgQ(currWin,
        "Usage: /query username\n"
        "To close a PRV query, use /close [username]\n"
        "/close without username will close the current PRV window, if any.\n");
        return 1;
    }
}


int nncmd_close_query(nn_conn_t *conn, char *name)
{
    (void) conn;

    if (name[0])
    {
        nn_window_t *win = nnwin_find(name);
        if (win != NULL)
        {
            nnwin_close(win);
            printMsgQ(currWin, "Closed PRV query to '%s'.\n", name);
        }
        else
        {
            printMsgQ(currWin, "No PRV query by name '%s'.\n", name);
        }
    }
    else
    {
        if (currWin != nnwin_main_window())
        {
            nnwin_close(currWin);
            currWin = nnwin_main_window();
        }
        else
        {
            printMsgQ(currWin,
            "Usage: /close [username]\n"
            "/close without username will close the current PRV window. if any.\n");
        }
    }

    return 0;
}


int nncmd_window_info(nn_conn_t *conn, char *arg)
{
    (void) conn;

    if (arg[0])
    {
        int val = atoi(arg);
        nn_window_t *win = nnwin_get(val);
        if (win != NULL)
            currWin = win;
        else
        {
            printMsgQ(currWin, "Invalid window number '%s'\n", arg);
            return 1;
        }
    }
    else
    {
        printMsgQ(currWin, "Window   : #%d\n", currWin->num);
        printMsgQ(currWin, "ID       : %s\n", currWin->id);
    }
    return 0;
}


int nncmd_list_all_users(nn_conn_t *conn, char *buf)
{
    (void) buf;

    // Alias /listallusers
    return nn_conn_send_msg(conn, optUserNameEnc, "%2Flistallusers");
}


#define NAME_NUM_PER_LINE 3
#define NAME_ENTRY_SIZE   64
#define NAME_ENTRY_WIDTH  22

typedef struct
{
    char buf[(NAME_NUM_PER_LINE * NAME_ENTRY_SIZE) + 16];
    size_t offs;
    int i, total;
} nncmd_namedata_t;


static int nncmd_names_do(const nn_user_t *user, void *data)
{
    nncmd_namedata_t *d = data;
    char name[NAME_ENTRY_SIZE];
    size_t len;

    snprintf(name, sizeof(name), "[½3½%-20s½0½] ", user->name);
    
    d->total++;
    if (d->i >= NAME_NUM_PER_LINE)
    {
        printMsgQ(currWin, "%s\n", d->buf);
        d->i = 0;
        d->offs = 0;
    }
    
    len = strlen(name);
    memcpy(d->buf + d->offs, name, len + 1);
    d->offs += len;
    d->i++;

    return 0;
}


int nncmd_names(nn_conn_t *conn, char *arg)
{
    nncmd_namedata_t data;
    (void) conn;
    (void) arg;

    printMsgQ(currWin, "Users:\n");
    data.i = data.total = 0;
    data.offs = 0;

    nn_userhash_foreach(nnUsers, nncmd_names_do, &data);

    if (data.i > 0)
    {
        printMsgQ(currWin, "%s\n", data.buf);
    }

    printMsgQ(currWin, "%d users total.\n", data.total);
    return 0;
}


int nncmd_save_config(nn_conn_t *conn, char *buf)
{
    (void) conn;
    (void) buf;

    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;
}


enum
{
    CMDARG_NONE,
    CMDARG_STRING,
    CMDARG_OPTIONAL,
    CMDARG_NICK,
};

typedef struct
{
    char *name;
    int flags;
    size_t len;
    int (*handler)(nn_conn_t *, char *buf);
} nn_usercmd_t;


static nn_usercmd_t userCmdsTable[] =
{
    // Server side commands, we just implement completion
    { "/me",       CMDARG_STRING,   0, NULL },
    { "/status",   CMDARG_STRING,   0, NULL },
    { "/list",     CMDARG_NONE,     0, nncmd_list_all_users },

    // List internal username list
    { "/who",      CMDARG_NONE,     0, nncmd_names },
    { "/names",    CMDARG_NONE,     0, nncmd_names },

    { "/w",        CMDARG_NICK,     0, nncmd_open_profile },
    { "/profile",  CMDARG_NICK,     0, nncmd_open_profile },

    { "/query",    CMDARG_NICK,     0, nncmd_open_query },
    { "/close",    CMDARG_OPTIONAL, 0, nncmd_close_query },
    { "/win",      CMDARG_OPTIONAL, 0, nncmd_window_info },

    { "/ignore",   CMDARG_OPTIONAL, 0, nncmd_ignore },
    { "/color",    CMDARG_STRING,   0, nncmd_set_color },
    { "/save",     CMDARG_NONE,     0, nncmd_save_config },
};

static qlist_t *userCmds = NULL;


void nn_usercmd_init()
{
    size_t i;
    for (i = 0; i < sizeof(userCmdsTable) / sizeof(userCmdsTable[0]); i++)
    {
        th_llist_append(&userCmds, &userCmdsTable[i]);
        userCmdsTable[i].len = strlen(userCmdsTable[i].name);
    }
}


int nn_handle_command(nn_conn_t *conn, char *buf)
{
    qlist_t *curr;

    for (curr = userCmds; curr != NULL; curr = curr->next)
    {
        nn_usercmd_t *cmd = curr->data;

        if (!th_strncasecmp(buf, cmd->name, cmd->len))
        {
            char *nbuf;
            if (buf[cmd->len] != 0 && !th_isspace(buf[cmd->len]))
                continue;
            
            nbuf = str_trim_left(buf + cmd->len);

            switch (cmd->flags)
            {
                case CMDARG_NICK:
                case CMDARG_STRING:
                    if (!nbuf[0])
                    {
                        printMsgQ(currWin, "Command %s requires an argument.\n", cmd->name);
                        return 1;
                    }
                    break;
                
                case CMDARG_NONE:
                    if (nbuf[0])
                    {
                        printMsgQ(currWin, "Command %s does not take arguments.\n", cmd->name);
                        return 1;
                    }
                    break;

                case CMDARG_OPTIONAL:
                    break;
            }

            // Check if there is a handler function
            if (cmd->handler)
            {
                // Internal commands have a handler
                return cmd->handler(conn, nbuf);
            }
            else
            {
                // Server-side commands are just pass-through here
                char *tmp = nn_dblencode_str(buf);
                BOOL result;
                if (tmp == NULL) return -2;
                result = nn_conn_send_msg(conn, optUserNameEnc, tmp);
                th_free(tmp);
                return result ? 0 : -1;
            }
        }
    }
    
    printMsgQ(currWin, "Unknown command: %s\n", buf);
    return 1;
}


static nn_usercmd_t *nn_usercmd_match_do(qlist_t *list, const char *pattern, size_t len)
{
    qlist_t *node;
    for (node = list; node != NULL; node = node->next)
    {
        nn_usercmd_t *cmd = node->data;
        if (len <= strlen(cmd->name) && th_strncasecmp(cmd->name, pattern, len) == 0)
            return cmd;
    }
    return NULL;
}


nn_usercmd_t *nn_usercmd_match(qlist_t *list, const char *pattern, const char *current, BOOL again)
{
    nn_usercmd_t *curr;
    size_t len;

    if (list == NULL || pattern == NULL) return NULL;

    len = strlen(pattern);
    
    if (current != NULL)
    {
        qlist_t *node;
        for (node = list; node != NULL; node = node->next)
        {
            curr = node->data;
            if (th_strcasecmp(curr->name, current) == 0)
            {
                if (again)
                    return curr;

                if ((curr = nn_usercmd_match_do(node->next, pattern, len)) != NULL)
                    return curr;
            }
        }
    }

    if ((curr = nn_usercmd_match_do(list, pattern, len)) != NULL)
        return curr;

    return NULL;
}


int nn_handle_input(nn_conn_t *conn, char *buf, size_t bufLen)
{
    BOOL result;
    char *tmp;

    // Trim right side
    while (bufLen > 0 && th_isspace(buf[bufLen - 1]))
        buf[--bufLen] = 0;

    if (buf[0] == 0)
        return 1;

    // Decode completed usernames
    nn_username_decode(buf);

    // Check for commands
    if (buf[0] == '/')
        return nn_handle_command(conn, buf);

    // If current window is not the main room window, send private
    if (currWin != nnwin_main_window())
    {
        if (currWin->id != NULL)
        {
            char *msg = th_strdup_printf("/prv -to %s -msg %s", currWin->id, buf);
            if (msg == NULL) return -3;
            tmp = nn_dblencode_str(msg);
            if (tmp == NULL)
            {
                th_free(msg);
                return -2;
            }
            result = nn_conn_send_msg(conn, optUserNameEnc, tmp);
            th_free(tmp);
            th_free(msg);
            return result ? 0 : -1;
        }
        else
        {
            printMsgQ(NULL, "No target set, exiting prv mode.\n");
            return 1;
        }
    }

    // Send double-encoded message
    tmp = nn_dblencode_str(buf);
    if (tmp == NULL) return -2;
    result = nn_conn_send_msg(conn, optUserNameEnc, tmp);
    th_free(tmp);
    return result ? 0 : -1;
}


static void nn_tabcomplete_replace(nn_editbuf_t *buf, size_t *pi, const size_t startPos, const size_t endPos, char *c)
{
    size_t i;
    
    for (i = startPos; i <= endPos; i++)
        nn_editbuf_delete(buf, startPos);

    for (i = startPos; *c; i++, c++)
        nn_editbuf_insert(buf, i, *c);
    
    *pi = i;
}


static void nn_tabcomplete_finish(nn_editbuf_t *buf, char **previous, const size_t startPos, const char *name)
{
    nn_editbuf_setpos(buf, startPos + 1 + strlen(name));
    th_free(*previous);
    *previous = th_strdup(name);
}


BOOL nn_tabcomplete_buffer(nn_editbuf_t *buf)
{
    static char *previous = NULL, *pattern = NULL;
    char *str = buf->data;
    BOOL again = FALSE, hasSeparator = FALSE,
         hasSpace, newPattern = FALSE, isCommand;
    size_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--;
    }
    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;
    }
    else
    // previous word, new pattern
    if (startPos >= 1 && str[startPos - 1] != ' ')
    {
        startPos -= 1;
        endPos = startPos;
        while (startPos > 0 && str[startPos - 1] != ' ') startPos--;
        newPattern = TRUE;
    }
    else
        return FALSE;

    // Check if this is a command completion
    isCommand = (str[0] == '/' && startPos == 0);

    if (!isCommand && str[endPos] == optNickSep)
    {
        endPos--;
        if (startPos > 0)
            return FALSE;
        hasSeparator = TRUE;
    }

    hasSpace = (buf->pos > 0 && str[buf->pos - 1] == ' ') ||
               (buf->pos <= buf->len && str[buf->pos] == ' ');

    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 (!pattern)
        return FALSE;

    if (isCommand)
    {
        nn_usercmd_t *cmd = nn_usercmd_match(userCmds, pattern, previous, again);
        if (cmd)
        {
            size_t i;
            nn_tabcomplete_replace(buf, &i, startPos, endPos, cmd->name);

            if (!hasSpace)
                nn_editbuf_insert(buf, i++, ' ');

            nn_tabcomplete_finish(buf, &previous, startPos, cmd->name);
            return TRUE;
        }
    }
    else
    {
        nn_user_t *user = nn_userhash_match(nnUsers, pattern, previous, again);

        if (user)
        {
            size_t i;
            nn_tabcomplete_replace(buf, &i, startPos, endPos, user->name);

            if (!hasSeparator && startPos == 0)
            {
                nn_editbuf_insert(buf, i++, optNickSep);
                startPos++;
            }
            else
            if (hasSeparator)
                startPos++;

            if (!hasSpace)
                nn_editbuf_insert(buf, i++, ' ');

            nn_tabcomplete_finish(buf, &previous, startPos, user->name);
            return TRUE;
        }
    }

    return FALSE;
}


BOOL nn_log_file_open(void)
{
    char *filename;

    if (optLogFilename == NULL || !optLogEnable)
        return FALSE;

    filename = nn_log_parse_filename(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 nn_log_file_close(void)
{
    if (optLogFile)
    {
        fclose(optLogFile);
        optLogFile = NULL;
    }
}


BOOL processUserInput(int c, nn_editbuf_t *editBuf, nn_editstate_t *editState)
{
    // Chat window switching via Meta/Esc-[1..9]
    if (c >= 0x5001 && c <= 0x5009)
    {
        nn_window_t *win = nnwin_get(c - 0x5000);
        if (win != NULL)
        {
            currWin = win;
            editState->update = TRUE;
        }
    }
    else
    switch (c)
    {
    case KEY_ENTER:
        // Call the user input handler
        if (editBuf->len > 0)
        {
            int result;

            if (editHistMax > 0)
            {
                nn_editbuf_free(editHistBuf[SET_MAX_HISTORY+1]);
                editHistBuf[SET_MAX_HISTORY+1] = NULL;
                memmove(&editHistBuf[2], &editHistBuf[1],
                    editHistMax * sizeof(editHistBuf[0]));
            }

            editHistPos = 0;
            editHistBuf[1] = nn_editbuf_copy(editBuf);
            if (editHistMax < SET_MAX_HISTORY)
                editHistMax++;

            result = nn_handle_input(editState->conn, editBuf->data, editBuf->len);

            nn_editbuf_clear(editBuf);

            if (result < 0)
            {
                errorMsg("Fatal error handling user input: %s\n", editBuf->data);
                editState->isError = TRUE;
            }
            else
            {
                // Update time value of last sent message for unidle timeouts
                editState->prevKeepAlive = time(NULL);
            }
        }
        break;

    case KEY_NPAGE:
    case KEY_PPAGE:
        // Page Up / Page Down
        if (currWin != NULL)
        {
            int oldPos = currWin->pos, page = (scrHeight - 4) / 3;

            currWin->pos += (c == KEY_NPAGE) ? - page : page;

            if (currWin->pos >= currWin->data->n - page)
                currWin->pos = currWin->data->n - page;
            if (currWin->pos < 0)
                currWin->pos = 0;

            if (oldPos != currWin->pos)
                editState->update = TRUE;
        }
        break;

    case KEY_UP: // Backwards in input history
        if (editHistPos == 0)
        {
            nn_editbuf_free(editHistBuf[0]);
            editHistBuf[0] = nn_editbuf_copy(editBuf);
        }
        if (editHistPos < editHistMax)
        {
            editHistPos++;
            nn_editbuf_free(editBuf);
            editBuf = nn_editbuf_copy(editHistBuf[editHistPos]);
        }
        break;

    case KEY_DOWN: // Forwards in input history
        if (editHistPos > 0)
        {
            editHistPos--;
            nn_editbuf_free(editBuf);
            editBuf = nn_editbuf_copy(editHistBuf[editHistPos]);
        }
        break;

    case KEY_F(5): // F5 = Ignore mode
        setIgnoreMode = !setIgnoreMode;
        printMsgQ(currWin, "Ignore mode = %s\n", setIgnoreMode ? "ON" : "OFF");
        break;

    case 0x03: // ^C = quit
    case KEY_F(9): // F9 = Quit
        printMsg(currWin, "Quitting per user request (%d/0x%x).\n", c, c);
        editState->exitProg = TRUE;
        break;

    case 0x09: // Tab = complete username or command
        nn_tabcomplete_buffer(editBuf);
        break;
    
    default:
        return FALSE;
    }

    return TRUE;
}


BOOL processUserPrompt(int c, nn_editbuf_t *editBuf, nn_editstate_t *editState)
{
    (void) editBuf;
    
    switch (c)
    {
    case KEY_ENTER:
        editState->done = TRUE;
        break;
    
    default:
        return FALSE;
    }

    return TRUE;
}


void updateUserPrompt(nn_editbuf_t *editBuf, nn_editstate_t *editState)
{
    nnwin_update(editState->update, editState->mask, editBuf, optUserName, optUserColor);
}


int main(int argc, char *argv[])
{
    char *tmpStr;
    int index, updateCount = 0;
    BOOL argsOK, colorSet = FALSE;
    nn_conn_t *conn = NULL;
    nn_editbuf_t *editBuf = nn_editbuf_new(NN_TMPBUF_SIZE);
    nn_editstate_t editState;
    cfgitem_t *tmpcfg;
    char *setHomeDir = NULL;

    memset(editHistBuf, 0, sizeof(editHistBuf));
    memset(&editState, 0, sizeof(editState));
    editState.insertMode = TRUE;
    editState.debugMsg = debugMsg;

    // 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);

    // Get home directory path
#ifdef __WIN32
    {
        char tmpPath[MAX_PATH];
        if (SHGetFolderPath(NULL, CSIDL_APPDATA | CSIDL_FLAG_CREATE, NULL, 0, tmpPath) == S_OK)
            setHomeDir = th_strdup(tmpPath);

        CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
    }
#else
    {
        char *xdgConfigDir = getenv("XDG_CONFIG_HOME"),
             *userHomeDir = getenv("HOME");
#ifdef USE_XDG
        if (xdgConfigDir != NULL)
            setHomeDir = th_strdup(xdgConfigDir);
        else
            setHomeDir = th_strdup_printf("%s/.config", userHomeDir);
#else
        setHomeDir = th_strdup(userHomeDir);
#endif
    }
#endif

    if (setHomeDir != NULL)
    {
        FILE *cfgfile;
        setConfigFile = th_strdup_printf("%s" SET_DIR_SEPARATOR "%s", setHomeDir, 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)
    {
        THMSG(1, "Username set on commandline.\n");
        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
    nn_log_file_open();

    // Initialize network
    if (!nn_network_init())
    {
        THERR("Could not initialize network subsystem.\n");
        goto err_exit;
    }

    // Initialize curses windowing
    if (!optDaemon && !nnwin_init(SET_DELAY))
        goto err_exit;

    if (cursesInit)
    {
        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);

        nnwin_update(TRUE, FALSE, NULL, optUserName, optUserColor);

        // Check if we have username and password
        if (optUserName == NULL || optPassword == NULL)
        {
            printMsg(NULL, "Please enter your NN login credentials.\n");
            printMsg(NULL, "You can avoid doing this every time by issuing '/save' after logging in.\n");

            printMsg(NULL, "Enter your NN username ...\n");
            optUserName = nnwin_prompt_requester(FALSE, &editState, processUserPrompt, updateUserPrompt);

            editState.mask = TRUE;
            printMsg(NULL, "Enter your NN password ...\n");
            optPassword = nnwin_prompt_requester(TRUE, &editState, processUserPrompt, updateUserPrompt);
            editState.mask = FALSE;
        }
    }

    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;
    }
    
    editState.conn = conn;

    // 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, try to resolve the hostname
    conn->host = th_strdup(optServer);
    conn->hst = nn_resolve_host(conn, optServer);
    if (conn->hst == NULL)
        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_v(conn, optUserNameEnc, "%%2Flogin%%20%%2Dsite%%20%s%%20%%2Dpassword%%20%s", tmpStr, optPassword);
    th_free(tmpStr);

    // Initialize user commands
    nn_usercmd_init();
    
    // Initialize random numbers
    editState.prevKeepAlive = time(NULL);
    srandom((int) editState.prevKeepAlive);

    // Enter mainloop
    nn_conn_reset(conn);
    while (!editState.isError && !editState.exitProg)
    {
        int retries = 3, cres;

packet_retry:
        cres = nn_conn_pull(conn);
        if (cres == 0)
        {
            while (conn->ptr < conn->in_ptr &&
                   *(conn->in_ptr - 1) == 0 &&
                   retries > 0 && !editState.isError)
            {
//                nn_conn_dump_buffer(stderr, conn);
                int result = nn_parse_protocol(conn);
                if (result == 0)
                {
                    nn_conn_buf_skip(conn, 1);
                }
                else
                if (result > 0)
                {
                    // Retry if possible
                    if (--retries > 0)
                        goto packet_retry;

                    // Couldn't handle the message for some reason
                    printMsg(currWin, "Could not handle: %s\n", conn->ptr);
                    nn_conn_buf_skip(conn, strlen(conn->ptr) + 1);
                }
                else
                    editState.isError = TRUE;
            }
        }
        else
        if (cres < 0 || !nn_conn_check(conn))
            editState.isError = TRUE;

        // Handle user input
        if (cursesInit)
        {
            nnwin_input_process(editBuf, &editState, processUserInput);
            nnwin_update(editState.update, editState.mask, editBuf, optUserName, optUserColor);
        }

        if (++updateCount > 10)
        {
            time_t tmpTime = time(NULL);
            if (tmpTime - editState.prevKeepAlive > SET_KEEPALIVE)
            {
                size_t n = ((size_t) random()) % th_llist_length(setIdleMessages);
                qlist_t *node = th_llist_get_nth(setIdleMessages, n);
                nn_conn_send_msg(conn, optUserNameEnc, node->data);
                editState.prevKeepAlive = tmpTime;
            }

            if (!colorSet)
            {
                colorSet = TRUE;
                nn_conn_send_msg_v(conn, optUserNameEnc, "%%2FSetFontColor%%20%%2Dcolor%%20%06X", optUserColor);
            }
            
            if (cursesInit)
            {
                nnwin_update(FALSE, editState.mask, editBuf, optUserName, optUserColor);
            }

            updateCount = 0;
        }
    }

    // Shutdown
err_exit:
    th_cfg_free(cfg);
    th_free(setHomeDir);
    th_llist_free_func(setIdleMessages, th_free);
    nn_userhash_free(nnUsers);
    nn_editbuf_free(editBuf);

    for (index = 0; index <= SET_MAX_HISTORY; index++)
        nn_editbuf_free(editHistBuf[index]);

#ifdef __WIN32
    if (errorMessages)
    {
        char *tmp = nnwin_prompt_requester("Press enter to quit.\n", FALSE);
        th_free(tmp);
    }
#endif

    nnwin_shutdown();

#ifndef __WIN32
    if (errorMessages)
        THERR("%s", errorMessages);
#endif

    th_free(optUserNameEnc);
    nn_conn_close(conn);
    nn_network_close();

    THMSG(1, "Connection terminated.\n");

    nn_log_file_close();

    return 0;
}