view main.c @ 639:02e1307e2a62

API changes in th_network module.
author Matti Hamalainen <ccr@tnsp.org>
date Fri, 04 Jul 2014 02:18:52 +0300
parents bda973fa2b3b
children 51dd01786d25
line wrap: on
line source

/*
 * NNChat - Custom chat client for NewbieNudes.com chatrooms
 * Written by Matti 'ccr' Hämäläinen
 * (C) Copyright 2008-2014 Tecnic Software productions (TNSP)
 */
#include "th_args.h"
#include "th_config.h"
#include "th_network.h"
#include "util.h"
#include "ui.h"
#include <unistd.h>
#include <fcntl.h>
#ifdef __WIN32
#include <shlwapi.h>
#include <shfolder.h>
#define srandom srand
#define random rand
#else
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/types.h>
#endif

#ifdef __WIN32
#define SET_CONFIG_FILE    "nnchat.txt"
#define SET_LOG_DIR        "NNChat Log Files"
#define SET_DIR_SEPARATOR  '\\'
#else
#define SET_CONFIG_FILE    ".nnchat"
#define SET_LOG_DIR        "nnlogs"
#define SET_DIR_SEPARATOR  '/'
#endif

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

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


typedef struct
{
    char *name;
    int port;
    char *desc;
} nn_room_data_t;

static const nn_room_data_t nn_room_data[] =
{
    { "main" , 8005, "Main room" },
    { "pit"  , 8003, "Passion Pit" },
};

static const int nn_room_data_n = sizeof(nn_room_data) / sizeof(nn_room_data[0]);


/* Options
 */
int     optPort = 8005,
        optProxyPort = SET_PROXY_PORT,
        optProxyType = TH_PROXY_NONE,
        optProxyAuthType = TH_PROXY_AUTH_NONE,
        optProxyAddrType = TH_PROXY_ADDR_DOMAIN;
int     optUserColor = 0x000000;
char    *optServer = "chat.newbienudes.com",
        *optProxyServer = NULL,
        *optProxyUserID = NULL,
        *optProxyPassword = NULL,
        *optUserName = NULL,
        *optUserNameCmd = NULL,
        *optUserNameEnc = NULL,
        *optPassword = NULL,
        *optPasswordCmd = NULL,
        *optLogPath = NULL,
        *optLogExtension = ".log",
        *optSite = "NN",
        *optNickSepStr = NULL;
char    optNickSep;
BOOL    optDaemon = FALSE,
        optProxyEnable = FALSE,
        setIgnoreMode = FALSE,
        optDebug = FALSE,
        optLogEnable = FALSE,
        optLogDaily = FALSE,
        optOnlyFriendPrv = FALSE;

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

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

/* Logging mode flags
 */
enum
{
    LOG_FILE       = 0x0001,
    LOG_WINDOW     = 0x0002,
    LOG_STAMP      = 0x0004,
    LOG_FILE2      = 0x0008,
    LOG_RECURSIVE  = 0x0010,
};


/* 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 },
    { 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, 'P', "proxy",      "Set proxy data, see below for syntax", OPT_ARGREQ },

    {13, 'r', "room",       "Connect to room (main, pit)", OPT_ARGREQ },
};

const int optListN = (sizeof(optList) / sizeof(optList[0]));


void argShowHelp(void)
{
    int i;
    th_print_banner(stdout, th_prog_name, "[options] <username> <password>");

    th_args_help(stdout, optList, optListN);

    printf(
    "\n"
    "Supported proxy types are SOCKS 4/4A and SOCKS 5.\n"
    "(Only user/pass auth and no auth supported, no GSSAPI!)\n"
    "These can be set with the -P option as follows:\n"
    "\n"
    " -P disable  (to disable proxy use)\n"
    " -P <type>://[<userid>[:passwd]@]<host>[:<port>]\n"
    " -P socks4://localhost:9000\n"
    " -P socks5://foobar:pass@localhost\n"
    "\n"
    "Type can be socks4, socks4a or socks5. Only socks5\n"
    "supports user/pass authentication. Default port is %d.\n"
    "\n"
    "Supported rooms (for option '-r'):\n",
    SET_PROXY_PORT);

    for (i = 0; i < nn_room_data_n; i++)
    {
        printf("  %s  - %s (port %d)\n",
            nn_room_data[i].name,
            nn_room_data[i].desc,
            nn_room_data[i].port);
    }
    
    printf("\n");
}


BOOL argSplitStr(const char *src, const char *at, char **res1, char **res2)
{
    char *pos, *tmp = th_strdup(src);

    if (tmp != NULL && (pos = strstr(tmp, at)) != NULL)
    {
        *pos = 0;
        pos += strlen(at);
        *res1 = th_strdup_trim(tmp, TH_TRIM_BOTH);
        *res2 = th_strdup_trim(pos, TH_TRIM_BOTH);
        th_free(tmp);
        return TRUE;
    }
    else
    {
        th_free(tmp);
        return FALSE;
    }
}


BOOL argHandleProxyURI(const char *uri)
{
    // Attempt to parse the proxy URI
    BOOL ret = FALSE;
    char *proto = NULL, *rest = NULL, *host = NULL,
         *auth = NULL, *port = NULL;
    size_t len;

    optProxyEnable = FALSE;

    // Handle disable case
    if (strncasecmp(uri, "disab", 5) == 0)
        return TRUE;

    // Split the URI
    if (!argSplitStr(uri, "://", &proto, &rest))
    {
        THERR("Malformed proxy URI, should be <type>://[<userid>[:passwd]@]<host>[:<port>]\n");
        goto out;
    }

    // Validate proxy type
    if (strcasecmp(proto, "socks4") == 0)
        optProxyType = TH_PROXY_SOCKS4;
    else
    if (strcasecmp(proto, "socks4a") == 0)
        optProxyType = TH_PROXY_SOCKS4A;
    else
    if (strcasecmp(proto, "socks5") == 0)
        optProxyType = TH_PROXY_SOCKS5;
    else
    {
        THERR("Invalid proxy type specified: '%s'\n", proto);
        goto out;
    }

    // Does the URI contain anything else?
    if (strlen(rest) == 0)
    {
        THERR("Malformed proxy URI, no host specified.\n");
        goto out;
    }

    // Remove trailing slash
    len = strlen(rest) - 1;
    if (rest[len] == '/')
        rest[len] = 0;

    // Check for auth credentials
    if (argSplitStr(rest, "@", &auth, &host))
    {
        if (strlen(auth) == 0)
        {
            THERR("Malformed proxy URI, zero length authentication credentials.\n");
            goto out;
        }

        // Should have authentication credentials
        if (!argSplitStr(auth, ":", &optProxyUserID, &optProxyPassword))
            optProxyUserID = th_strdup(auth);
    }
    else
        host = th_strdup(rest);

    // Check if proxy port was specified
    if (argSplitStr(host, ":", &optProxyServer, &port))
        optProxyPort = atoi(port);
    else
        optProxyServer = th_strdup(host);

    // Check what authentication type to use
    if (optProxyType == TH_PROXY_SOCKS5 &&
        optProxyUserID != NULL && optProxyPassword != NULL)
        optProxyAuthType = TH_PROXY_AUTH_USER;
    else
        optProxyAuthType = TH_PROXY_AUTH_NONE;

    optProxyEnable = TRUE;
    ret = TRUE;

out:
    th_free(proto);
    th_free(rest);
    th_free(host);
    th_free(auth);
    th_free(port);

    return ret;
}


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 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:
        if (!argHandleProxyURI(optArg))
            return FALSE;
        break;

    case 13:
        {
            int i;
            for (i = 0; i < nn_room_data_n; i++)
            if (!strcasecmp(nn_room_data[i].name, optArg))
            {
                optPort = nn_room_data[i].port;
                return TRUE;
            }

            THERR("Unsupported room '%s'.\n", optArg);
            return FALSE;
        }
        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 nn_conn_send_msg(th_conn_t *conn, const char *user, const char *str)
{
    char *msg;

    if (str == NULL)
        return FALSE;
    
    msg = th_strdup_printf("<USER>%s</USER><MESSAGE>%s</MESSAGE>", user, str);

    if (msg != NULL)
    {
        int ret = th_conn_send_buf(conn, msg, strlen(msg) + 1);
        th_free(msg);
        return ret == THERR_OK;
    }
    else
        return FALSE;
}


BOOL nn_conn_send_msg_v(th_conn_t *conn, const char *user, const char *fmt, ...)
{
    BOOL res;
    char *tmp;
    va_list ap;

    va_start(ap, fmt);
    tmp = th_strdup_vprintf(fmt, ap);
    va_end(ap);

    res = nn_conn_send_msg(conn, user, tmp);
    th_free(tmp);
    return res;
}


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 printMsgFile(nn_window_t *win, int flags, const char *stamp, const char *msg)
{
    if (win != NULL && win->logFile != NULL)
    {
        if (flags & LOG_STAMP) printFile(win->logFile, stamp);
        printFile(win->logFile, msg);
        fflush(win->logFile);
    }
}


void printMsgF(nn_window_t *win, int flags, const char *fmt, ...);
BOOL nn_log_reopen(nn_window_t *win);


void printMsgConst(nn_window_t *win, int flags, const char *msg)
{
    char tmpStr[128];
    nn_window_t *tmpwin = (win != NULL) ? win : nnwin_main_window();

    // Only the main window 
    if (win == NULL && tmpwin != NULL && (flags & LOG_RECURSIVE) == 0)
    {
        time_t currTime = time(NULL);
        struct tm *currTm, *prevTm;

        if ((currTm = localtime(&currTime)) != NULL &&
            currTm->tm_hour == 0 &&
            (prevTm = localtime(&tmpwin->logPrevMsgTime)) != NULL &&
            prevTm->tm_hour == 23)
        {
            str_get_timestamp(tmpStr, sizeof(tmpStr), "%d %b %Y");
            printMsgF(win, LOG_RECURSIVE, "Day changed to %s.\n", tmpStr);
            nn_log_reopen(tmpwin);
        }

        tmpwin->logPrevMsgTime = currTime;
    }

    if (flags & LOG_STAMP)
    {
        str_get_timestamp(tmpStr, sizeof(tmpStr), "½17½[½11½%H:%M:%S½17½]½0½ ");
    }

    if (flags & LOG_FILE)
    {
        printMsgFile(win != NULL ? win : nnwin_main_window(), flags, tmpStr, msg);
    }

    if (flags & LOG_FILE2)
    {
        printMsgFile(nnwin_main_window(), flags, tmpStr, msg);
    }

    if (!optDaemon && (flags & LOG_WINDOW))
    {
        if (flags & LOG_STAMP) nnwin_print(tmpwin, tmpStr);
        nnwin_print(tmpwin, msg);
    }
}

void printMsgV(nn_window_t *win, int flags, const char *fmt, va_list ap)
{
    char *buf = th_strdup_vprintf(fmt, ap);
    printMsgConst(win, flags, 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 errorMsgConst(const char *msg)
{
    printMsgConst(NULL, LOG_STAMP | LOG_WINDOW | LOG_FILE, msg);

    if (errorMessages != NULL)
    {
        // XXX Yes, this is lazy.
        char *tmp = th_strdup_printf("%s%s", errorMessages, msg);
        th_free(errorMessages);
        errorMessages = tmp;
    }
    else
        errorMessages = th_strdup(msg);
}


void errorMsgV(const char *fmt, va_list ap)
{
    char *msg = th_strdup_vprintf(fmt, ap);
    errorMsgConst(msg);
    th_free(msg);
}


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 nn_network_errfunc(struct _th_conn_t *conn, int err, const char *msg)
{
    (void) conn;
    (void) err;
    errorMsg("%s", msg);
}


void nn_network_msgfunc(struct _th_conn_t *conn, int loglevel, const char *msg)
{
    (void) conn;
    (void) loglevel;
    printMsgConst(NULL, LOG_STAMP | LOG_WINDOW | LOG_FILE, msg);
}


void nn_ioctx_errfunc(th_ioctx_t *ctx, int err, const char *msg)
{
    (void) err;
    errorMsg("[%s:%d] %s",
        ctx->filename, ctx->line, msg);
}


void nn_ioctx_msgfunc(th_ioctx_t *ctx, const char *msg)
{
    (void) ctx;
    printMsgConst(NULL, LOG_STAMP | LOG_WINDOW | LOG_FILE, msg);
}


BOOL nn_check_name_list(qlist_t *list, const char *name)
{
    qlist_t *node;
    
    for (node = list; node != NULL; node = node->next)
    {
        if (th_strcasecmp(name, (char *) node->data) == 0)
            return TRUE;
    }

    return FALSE;
}


int nnproto_parse_user(th_conn_t *conn)
{
    BOOL isMine, isIgnored = FALSE;
    char *name, *msg, *t;

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

    // Find end of the message
    t = th_conn_buf_strstr(conn, "<MESSAGE>");
    if (!t) return 2;
    msg = conn->base.ptr;
    
    t = th_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 && nn_check_name_list(setIgnoreList, 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;

            if (!optOnlyFriendPrv || !nn_check_name_list(setFriendList, in_name))
            {
                isIgnored = setIgnoreMode && nn_check_name_list(setIgnoreList, in_name);
                win = nnwin_find(in_name);

                if (win != NULL)
                {
                    printMsgF(win, isIgnored ? LOG_FILE : (LOG_FILE | LOG_WINDOW),
                        "½5½<½%d½%s½5½>½0½ %s\n",
                        isMine ? 14 : 15, isMine ? optUserName : in_name, in_msg);

                    printMsgF(NULL, LOG_FILE2, "½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;
        int colorNick, colorText;
        t = nn_strip_tags(msg);
        h = nn_decode_str2(t);
        if (isMine)
        {
            colorNick = 14;
            colorText = 0;
        }
        else
        {
           if (nn_check_name_list(setFriendList, name))
           {
               colorNick = 11;
               colorText = 0;
           }
           else
           {
               colorNick = 15;
               colorText = 0;
           }
        }
            
        printMsgF(NULL, isIgnored ? LOG_FILE : (LOG_WINDOW | LOG_FILE),
            "½5½<½%d½%s½5½>½%d½ %s½0½\n", colorNick, name, colorText, h);
        th_free(h);
        th_free(t);
    }

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


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

    if (!th_conn_buf_strcmp(conn, "FAILURE>"))
    {
        th_conn_buf_strstr(conn, "</LOGIN_FAILURE>");
        th_conn_buf_strstr(conn, "</USER>");
        printMsg(NULL, "½1½Login failure½0½ - ½3½%s½0½\n", tmpStr);
        return -2;
    }
    else if (!th_conn_buf_strcmp(conn, "SUCCESS>"))
    {
        th_conn_buf_strstr(conn, "</LOGIN_SUCCESS>");
        th_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(th_conn_t *conn)
{
    char *p, *s, *str = conn->base.ptr;
    nn_window_t *win;

    s = th_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(th_conn_t *conn)
{
    char *p, *s, *str = conn->base.ptr;
    nn_window_t *win;

    s = th_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(th_conn_t *conn)
{
    th_conn_buf_strstr(conn, "</NUMCLIENTS>");
    return 0;
}


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


typedef struct
{
    char *name;
    size_t len;
    int (*handler)(th_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(th_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 (!th_conn_buf_strncmp(conn, protoCmds[i].name, protoCmds[i].len))
            return protoCmds[i].handler(conn);
    }

    if (optDebug)
    {
        printMsg(NULL, "Unknown protocmd: \"%s\"\n", conn->base.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 = th_get_error();
        printMsgQ(currWin, "Could not open process communication pipe! (%d, %s)\n",
            ret, th_error_str(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(th_get_error());
    }

    wait(&status);
#endif

    return 0;
}


int nncmd_send_raw(th_conn_t *conn, char *str)
{
#if 1
    char *tmp = nn_encode_str1(str);
    if (tmp == NULL) return -2;
    nn_conn_send_msg(conn, optUserNameEnc, tmp);
    th_free(tmp);
#else
    nn_conn_send_msg(conn, optUserNameEnc, str);
#endif

    return 0;
}


int nncmd_open_profile(th_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_change_list(th_conn_t *conn, const char *listname, qlist_t **list, const char *name)
{
    (void) conn;

    if (name[0])
    {
        // Add or remove someone to/from ignore
        qlist_t *user = th_llist_find_func(*list, name, str_compare);
        if (user != NULL)
        {
            printMsgQ(currWin, "Removed user '%s' from %s list.\n", name, listname);
            th_llist_delete_node(list, user);
        }
        else
        {
            printMsgQ(currWin, "Added '%s' to %s list.\n", name, listname);
            th_llist_append(list, th_strdup(name));
        }
    }
    else
    {
        // Just list whomever is in ignore now
        qlist_t *user = *list;
        size_t nuser = th_llist_length(*list);
        char *result = th_strdup_printf("Users on %s list (%d): ", listname, 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_ignore(th_conn_t *conn, char *name)
{
    return nncmd_change_list(conn, "ignore", &setIgnoreList, name);
}


int nncmd_friend(th_conn_t *conn, char *name)
{
    return nncmd_change_list(conn, "friend", &setFriendList, name);
}


int nncmd_set_color(th_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(th_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(th_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(th_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(th_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;
    int color;

    if (nn_check_name_list(setFriendList, user->name))
        color = 11;
    else
    if (nn_check_name_list(setIgnoreList, user->name))
        color = 1;
    else
        color = 3;

    snprintf(name, sizeof(name), "[½%d½%-20s½0½] ", color, 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(th_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(th_conn_t *conn, char *buf)
{
    (void) conn;
    (void) buf;
    th_ioctx_t ctx;
#ifndef __WIN32
    int cfgfd = -1;
#endif

    if (!th_ioctx_init(&ctx, setConfigFile, nn_ioctx_errfunc, nn_ioctx_msgfunc))
    {
        printMsgQ(currWin, "Could not initialize I/O context for configuration file writing!\n");
        goto error;
    }

#ifdef __WIN32
    if ((ctx.fp = fopen(setConfigFile, "w")) == NULL)
#else
    if ((cfgfd = open(setConfigFile, O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR)) == -1 ||
        (ctx.fp = fdopen(cfgfd, "w")) == NULL)
#endif
    {
        int err = th_get_error();
        printMsgQ(currWin, "Could not create configuration to file '%s', %d: %s\n",
            setConfigFile, err, th_error_str(err));
        goto error;
    }

    printMsgQ(currWin, "Configuration saved in file '%s', res=%d\n",
        setConfigFile, th_cfg_write(&ctx, cfg));

error:
    th_ioctx_close(&ctx);
    return 0;
}


int nncmd_quit(th_conn_t *conn, char *buf)
{
    (void) conn;
    (void) buf;

    appQuitFlag = TRUE;
    return 0;
}


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

typedef struct
{
    char *name;
    int flags;
    size_t len;
    int (*handler)(th_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 },
    { "/prvon",    CMDARG_NONE,     0, NULL },
    { "/prvoff",   CMDARG_NONE,     0, NULL },
    { "/mute",     CMDARG_STRING,   0, NULL },

    // 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 },
    { "/friend",   CMDARG_OPTIONAL, 0, nncmd_friend },
    { "/color",    CMDARG_STRING,   0, nncmd_set_color },
    { "/save",     CMDARG_NONE,     0, nncmd_save_config },

    { "/raw",      CMDARG_STRING,   0, nncmd_send_raw },

    { "/quit",     CMDARG_NONE,     0, nncmd_quit },
};

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(th_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(th_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 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 KEY_F(6): // F6 = Ignore mode
        optOnlyFriendPrv = !optOnlyFriendPrv;
        printMsgQ(currWin, "Only friends allowed to PRV you = %s\n", optOnlyFriendPrv ? "ON" : "OFF");
        break;

    case KEY_F(9): // F9 = Quit
        printMsg(currWin, "Quitting per user request (%d/0x%x).\n", c, c);
        appQuitFlag = 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;

    case KEY_F(9): // F9 = Quit
        printMsg(currWin, "Quitting per user request (%d/0x%x).\n", c, c);
        appQuitFlag = 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);
}


void clearEditState(nn_editstate_t *st)
{
    memset(st, 0, sizeof(nn_editstate_t));
    st->insertMode = TRUE;
    st->debugMsg = debugMsg;
}


BOOL nn_log_open(nn_window_t *win)
{
    char *path = NULL;
#ifndef __WIN32
    int logFd = -1;
#endif

    if (!optLogEnable)
        return FALSE;

    if (optLogPath != NULL)
    {
        char *lt = strrchr(optLogPath, SET_DIR_SEPARATOR);
        if (lt == NULL || lt[1] != 0)
            path = th_strdup_printf("%s%c", optLogPath, SET_DIR_SEPARATOR);
        else
            path = th_strdup(optLogPath);
    }

    if (win->id == NULL)
    {
        // Main window log (aka room log)
        if (optLogDaily)
        {
            char stamp[64];
            str_get_timestamp(stamp, sizeof(stamp), "%Y-%m-%d");
            win->logFilename = th_strdup_printf("%sroom_%d-%s%s",
                path != NULL ? path : "", optPort,
                stamp, optLogExtension);
        }
        else
        {
            win->logFilename = th_strdup_printf("%sroom_%d%s",
                path != NULL ? path : "", optPort, optLogExtension);
        }
    }
    else
    {
        // PRV chat log
        size_t pos;
        char *cleaned;
        
        if ((cleaned = th_strdup(win->id)) == NULL)
            return FALSE;

        for (pos = 0; cleaned[pos] != 0; pos++)
        {
            if (!isalnum(cleaned[pos]))
                cleaned[pos] = '_';
        }

        win->logFilename = th_strdup_printf("%s%s%s",
            path != NULL ? path : "", cleaned, optLogExtension);

        th_free(cleaned);
    }
    
    // Try to open the file for appending
    if (win->logFilename == NULL)
        goto error;

#ifdef __WIN32
    if ((win->logFile = fopen(win->logFilename, "a")) == NULL)
#else
    if ((logFd = open(win->logFilename, O_CREAT | O_APPEND | O_WRONLY, S_IRUSR | S_IWUSR)) == -1 ||
        (win->logFile = fdopen(logFd, "a")) == NULL)
#endif
    {
        int err = th_get_error();
        errorMsg("Could not open logfile '%s' for appending, %d: %s\n",
            win->logFilename, err, th_error_str(err));
        goto error;
    }

    printMsg(win, "Logging to '%s'.\n", win->logFilename);

    th_free(path);
    return TRUE;

error:
    th_free(path);
    th_free(win->logFilename);
    win->logFilename = NULL;
    if (win->logFile != NULL)
        fclose(win->logFile);
#ifndef __WIN32
    else
    if (logFd >= 0)
        close(logFd);
#endif
    return FALSE;
}


void nn_log_close(nn_window_t *win)
{
    if (win->logFile != NULL)
        fclose(win->logFile);
    win->logFile = NULL;

    th_free(win->logFilename);
    win->logFilename = NULL;
}


BOOL nn_log_reopen(nn_window_t *win)
{
    nn_log_close(win);
    return nn_log_open(win);
}


BOOL nn_stat_path(const char *path, BOOL *isDirectory, BOOL *isWritable, BOOL *isReadable)
{
#ifdef __WIN32
    DWORD attr = GetFileAttributes(path);

    *isDirectory = (attr & FILE_ATTRIBUTE_DIRECTORY) != 0;
    *isWritable = (attr & FILE_ATTRIBUTE_READONLY) == 0;
    *isReadable = TRUE;
#else
    uid_t id = geteuid();
    struct stat sb;
    if (stat(path, &sb) < 0)
        return FALSE;

    *isDirectory = (S_ISDIR(sb.st_mode));
    *isWritable = (id == sb.st_uid && (sb.st_mode & S_IWUSR));
    *isReadable = (id == sb.st_uid && (sb.st_mode & S_IRUSR));
#endif

//    THERR("'%s': dir=%d, wr=%d, rd=%d\n", path, *isDirectory, *isWritable, *isReadable);
    return TRUE;
}


BOOL nn_mkdir_rec(const char *cpath)
{
    char save, *path = th_strdup(cpath);
    size_t start = 0, end;
    BOOL res = FALSE, exists, isDir, isWritable, isReadable;

    THMSG(0, "Creating directory %s\n", cpath);
    do
    {
        for (save = 0, end = start; path[end] != 0; end++)
        if (path[end] == SET_DIR_SEPARATOR)
        {
            save = path[end];
            path[end] = 0;
            break;
        }
        

        if (path[start] != 0)
        {
            exists = nn_stat_path(path, &isDir, &isWritable, &isReadable);
            if (exists && !isDir)
                goto error;

            if (!exists)
            {
#ifdef __WIN32
            if (!CreateDirectory(path, NULL))
                goto error;
#else
            if (mkdir(path, 0x1c9) < 0)
                goto error;
#endif
            }
        }

        path[end] = save;
        start = end + 1;
    } while (save != 0);

    res = TRUE;

error:
    th_free(path);
    return res;
}




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

    memset(editHistBuf, 0, sizeof(editHistBuf));
    clearEditState(&editState);

    // Initialize
    th_init("NNChat", "Newbie Nudes chat client", NN_VERSION,
        "Written and designed by Anonymous Finnish Guy (C) 2008-2014",
        "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, "Allow only defined friends to private to you");
    th_cfg_add_bool(&tmpcfg, "prv_friends", &optOnlyFriendPrv, optOnlyFriendPrv);
    th_cfg_add_comment(&tmpcfg, "List of your friends");
    th_cfg_add_string_list(&tmpcfg, "friend_list", &setFriendList);

    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");
    th_cfg_add_int(&tmpcfg, "port", &optPort, optPort);
    th_cfg_add_section(&cfg, "server", tmpcfg);

    tmpcfg = NULL;
    th_cfg_add_comment(&tmpcfg, "Enable proxy");
    th_cfg_add_bool(&tmpcfg, "enable", &optProxyEnable, optProxyEnable);
    th_cfg_add_comment(&tmpcfg, "Proxy URI (see comandline help for more information)");
    th_cfg_add_string(&tmpcfg, "uri", &setProxyURI, NULL);
    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, "Use daily logfiles for room logs");
    th_cfg_add_bool(&tmpcfg, "daily", &optLogDaily, optLogDaily);

    th_cfg_add_comment(&tmpcfg, "Log files path");
    th_cfg_add_string(&tmpcfg, "path", &optLogPath, optLogPath);

    th_cfg_add_comment(&tmpcfg, "Log filename extension");
    th_cfg_add_string(&tmpcfg, "extension", &optLogExtension, optLogExtension);

    th_cfg_add_section(&cfg, "logging", tmpcfg);

    // Get home directory path
    {
#if defined(__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);
#elif defined(USE_XDG)
        char *xdgConfigDir = getenv("XDG_CONFIG_HOME");

        // If XDG is enabled, try the environment variable first
        if (xdgConfigDir != NULL)
            setHomeDir = th_strdup(xdgConfigDir);
        else
            // Nope, try the obvious alternative
            setHomeDir = th_strdup_printf("%s/.config", getenv("HOME"));
#else
        setHomeDir = th_strdup(getenv("HOME"));
#endif
    }

    if (setHomeDir != NULL)
    {
        th_ioctx_t ctx;
        setConfigFile = th_strdup_printf("%s%c%s", setHomeDir, SET_DIR_SEPARATOR, SET_CONFIG_FILE);

        THMSG(0, "Reading configuration from '%s'.\n", setConfigFile);

        if (th_ioctx_open(&ctx, setConfigFile, "r", nn_ioctx_errfunc, nn_ioctx_msgfunc))
        {
            th_cfg_read(&ctx, cfg);
            th_ioctx_close(&ctx);
        }
    }

    if (setProxyURI && !argHandleProxyURI(setProxyURI))
        goto err_exit;

    optNickSep = optNickSepStr ? optNickSepStr[0] : SET_NICK_SEPARATOR;

    setBrowser = getenv("BROWSER");
    if (setBrowser == NULL)
        setBrowser = "firefox";

    if (optLogPath == NULL)
    {
        optLogPath = th_strdup_printf("%s%c%s%c",
            setHomeDir, SET_DIR_SEPARATOR, SET_LOG_DIR, SET_DIR_SEPARATOR);
    }

    if (optLogEnable)
    {
        BOOL isDir, isWritable, isReadable;
        if (nn_stat_path(optLogPath, &isDir, &isWritable, &isReadable))
        {
            if (!isDir)
            {
                THERR("The log file path '%s' is not a directory.\n",
                    optLogPath);
                goto err_exit;
            }
            else
            if (!isWritable)
            {
#ifdef __WIN32
                if (!nn_mkdir_rec(optLogPath))
                {
                    THERR("Could not create log file directory '%s'.\n",
                        optLogPath);
                    goto err_exit;
                }
#else
                THERR("The log file path '%s' is not writable.\n",
                    optLogPath);
                goto err_exit;
#endif
            }
        }
        else
        if (!nn_mkdir_rec(optLogPath))
        {
            THERR("Could not create log file directory '%s'.\n",
                optLogPath);
            goto err_exit;
        }
    }

    // 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("."));
    }

    // Initialize network
    if ((ret = th_network_init()) != THERR_OK)
    {
        THERR("Could not initialize network subsystem: %s\n", th_error_str(ret));
        goto err_exit;
    }

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


    if (appCursesInit)
    {
        printMsg(NULL, "%s v%s - %s\n", th_prog_name, th_prog_version, th_prog_desc);
        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);
            if (appQuitFlag)
                goto err_exit;

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

    if (optUserName == NULL || optPassword == NULL)
    {
        errorMsg("Username and/or password not specified.\n");
        goto err_exit;
    }

    // Create a connection
    conn = th_conn_new(nn_network_errfunc, nn_network_msgfunc, -1);
    if (conn == NULL)
    {
        errorMsg("Could not create connection structure.\n");
        goto err_exit;
    }
    
    editState.conn = conn;

    // Are we using a proxy?
    if (optProxyEnable && optProxyType != TH_PROXY_NONE && optProxyServer != NULL)
    {
        if (optProxyUserID == NULL)
            optProxyUserID = "James Bond";

        if (th_conn_set_proxy(conn, optProxyType, optProxyPort, optProxyServer, optProxyAuthType) != THERR_OK ||
            th_conn_set_proxy_auth_user(conn, optProxyUserID, optProxyPassword) != THERR_OK ||
            th_conn_set_proxy_mode(conn, TH_PROXY_CMD_CONNECT) != THERR_OK ||
            th_conn_set_proxy_addr_type(conn, optProxyAddrType) != THERR_OK)
        {
            errorMsg("Error setting proxy information.\n");
            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 (th_conn_open(conn, 843, optServer) != 0)
    {
        errorMsg("Policy file request connection setup failed!\n");
        goto err_exit;
    }

    tmpStr = "<policy-file-request/>";
    if (th_conn_send_buf(conn, tmpStr, strlen(tmpStr) + 1) != THERR_OK)
    {
        errorMsg("Failed to send policy file request.\n");
        goto err_exit;
    }
    else
    {
        int cres = th_conn_pull(conn);
        if (cres == TH_CONN_DATA_AVAIL)
        {
            printMsg(currWin, "Probe got: %s\n", conn->buf);
        }
        else
        {
            printMsg(currWin, "Could not get policy probe.\n");
        }
    }
    th_conn_free(conn);
#endif

    // Okay, now do the proper connection ...
    if (th_conn_open(conn, optPort, optServer) != 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
    th_conn_reset(conn);
    while (!editState.isError && !appQuitFlag)
    {
        int retries = 3, cres;
        editState.update = FALSE;

packet_retry:
        cres = th_conn_pull(conn);
        if (cres == TH_CONN_DATA_AVAIL)
        {
            while (conn->base.ptr < conn->base.in_ptr &&
                   *(conn->base.in_ptr - 1) == 0 &&
                   retries > 0 && !editState.isError)
            {
//                th_conn_dump_buffer(stderr, conn);
                int result = nn_parse_protocol(conn);
                if (result == 0)
                {
                    th_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->base.ptr);
                    th_conn_buf_skip(conn, strlen(conn->base.ptr) + 1);
                }
                else
                    editState.isError = TRUE;
            }
        }
        else
        if (cres < TH_CONN_ERROR || !th_conn_check(conn))
            editState.isError = TRUE;

        // Handle user input
        BOOL flushed = FALSE;
        if (appCursesInit)
        {
            nnwin_input_process(editBuf, &editState, processUserInput);
            nnwin_update(editState.update, editState.mask, editBuf, optUserName, optUserColor);
            flushed = TRUE;
        }

        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 (appCursesInit && !flushed)
            {
                nnwin_update(FALSE, editState.mask, editBuf, optUserName, optUserColor);
            }

            updateCount = 0;
        }
    }

    // Shutdown
err_exit:
    if (errorMessages || editState.isError)
    {
        char *tmp;
        printMsg(NULL, "Press enter to exit.\n");
        clearEditState(&editState);
        tmp = nnwin_prompt_requester(FALSE, &editState, processUserPrompt, updateUserPrompt);
        th_free(tmp);
    }

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

    nnwin_shutdown();

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

    th_free(optUserNameEnc);
    th_conn_free(conn);
    th_network_close();

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

    return 0;
}