view nnchat.c @ 247:fce4e2e31d69

Add the config section ...
author Matti Hamalainen <ccr@tnsp.org>
date Wed, 20 Apr 2011 18:59:42 +0300
parents cb86d7543be2
children dcf78e2c458c
line wrap: on
line source

/*
 * NNChat - Custom chat client for NewbieNudes.com chatrooms
 * Written by Matti 'ccr' Hämäläinen
 * (C) Copyright 2008-2011 Tecnic Software productions (TNSP)
 */
#include "libnnchat.h"
#include <stdlib.h>
#include "th_args.h"
#include "th_config.h"
#include <string.h>
#include <errno.h>
#ifdef __WIN32
/* Undefine because both windows.h and curses.h #define it */
#undef MOUSE_MOVED
#include <shlwapi.h>
#else
#include <sys/wait.h>
#endif
#ifdef HAVE_NCURSES_H
#include <ncurses.h>
#else
#include <curses.h>
#endif

#ifdef __WIN32
#define SET_CONFIG_FILE    "nnchat.txt"
#define SET_DIR_SEPARATOR  "\\"
#define SET_DELAY          (0)
#else
#define SET_CONFIG_FILE    ".nnchat"
#define SET_DIR_SEPARATOR  "/"
#define SET_DELAY          (5)
#endif

/* Define the weak "encryption" key used for locally stored passwords.
 * This has no other purpose than to obfuscate keys in configuration
 * file(s) from casual observers. In NO WAY this can be seen as a
 * true security measure!
 */
#define SET_ENC_KEY     "1a#!sCbZxcGj0a04hBz&S"

#define SET_BACKBUF_LEN (512)       /* Backbuffer size (in lines) */
#define SET_MAX_HISTORY (16)        /* Command history length */
#define SET_KEEPALIVE   (15*60)     /* Ping/keepalive period in seconds */


/* Options
 */
int     optPort = 8003;
int     optUserColor = 0x000000;
char    *optServer = "chat.newbienudes.com",
        *optUserName = NULL,
        *optUserNameCmd = NULL,
        *optUserNameEnc = NULL,
        *optPassword = NULL,
        *optPasswordCmd = NULL,
        *optLogFilename = NULL,
        *setTarget = NULL,
        *optSite = "NN";
char    optNickSep = ':';
BOOL    optDaemon = FALSE;
FILE    *optLogFile = NULL;
WINDOW  *mainWin = NULL,
        *statusWin = NULL,
        *editWin = NULL;
BOOL    setPrvMode = FALSE;
BOOL	setIgnoreMode = FALSE;
BOOL    optDebug = FALSE;
BOOL    optLogEnable = FALSE;

qlist_t *nnIgnoreList = NULL;
nn_userhash_t *nnUsers = NULL;
char    *setConfigFile = NULL,
        *setBrowser = NULL;
cfgitem_t *cfg = NULL;
nn_ringbuf_t *backBuf = NULL;

/* 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, 'S', "site",       "Site (default: NN)", OPT_ARGREQ },
    { 8, 'd', "debug",      "Enable various debug features", OPT_NONE },
};

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


void argShowHelp()
{
    th_args_help(stdout, optList, optListN, th_prog_name,
        "[options] <username> <password>");
}


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;

    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 getTimeStamp(char *str, size_t len, const char *fmt)
{
    time_t stamp = time(NULL);
    struct tm *stamp_tm;
    if ((stamp_tm = localtime(&stamp)) != NULL) {
        strftime(str, len, fmt, stamp_tm);
        return TRUE;
    } else {
        str[0] = 0;
        return FALSE;
    }
}


char *encodeUsername(char *str)
{
    unsigned char *c = (unsigned char *) str;
    if (str == NULL) return NULL;
    for (; *c ; c++) 
        if (*c == ' ') *c = 255;
    return str;
}

char *decodeUsername(char *str)
{
    unsigned char *c = (unsigned char *) str;
    if (str == NULL) return NULL;
    for (; *c ; c++) 
        if (*c == 255) *c = ' ';
    return str;
}

void updateStatus(BOOL insertMode)
{
    char tmpStr[128];
    
    if (statusWin == NULL) return;
    
    getTimeStamp(tmpStr, sizeof(tmpStr), "%H:%M:%S");

    wbkgdset(statusWin, COLOR_PAIR(10));
    werase(statusWin);
    
    wattrset(statusWin, A_BOLD | COLOR_PAIR(11));
    mvwaddstr(statusWin, 0, 1, tmpStr);
    
    wattrset(statusWin, A_BOLD | COLOR_PAIR(13));
    waddstr(statusWin, " | ");
    wattrset(statusWin, A_BOLD | COLOR_PAIR(16));
    waddstr(statusWin, optUserName);
    wattrset(statusWin, A_BOLD | COLOR_PAIR(13));

    waddstr(statusWin, " | ");
    wattrset(statusWin, A_BOLD | COLOR_PAIR(11));
    waddstr(statusWin, insertMode ? "INS" : "DEL");
    
    wattrset(statusWin, A_BOLD | COLOR_PAIR(13));
    waddstr(statusWin, " | Prv: ");

    wattrset(statusWin, A_BOLD | COLOR_PAIR(11));
    waddstr(statusWin, setTarget != NULL ? setTarget : "-");

    wattrset(statusWin, A_BOLD | COLOR_PAIR(13));
    waddstr(statusWin, " | P/C: ");
    wattrset(statusWin, A_BOLD | COLOR_PAIR(11));
    snprintf(tmpStr, sizeof(tmpStr), "%d / #%06x", optPort, optUserColor);
    waddstr(statusWin, tmpStr);

    wrefresh(statusWin);
}

void printEditBuf(const char *str, nn_editbuf_t *buf)
{
    char *tmp;
    if (statusWin == NULL || buf == NULL) return;

    buf->data[buf->len] = 0;
    tmp = decodeUsername(th_strdup(buf->data));
    
    werase(editWin);
    
    wattrset(editWin, A_BOLD);
    mvwaddstr(editWin, 0, 0, str);
    waddstr(editWin, "> ");
    wattrset(editWin, A_NORMAL);
    
    if (buf->pos < buf->len) {
        waddnstr(editWin, tmp, buf->pos);
        wattrset(editWin, A_REVERSE);
        waddch(editWin, tmp[buf->pos]);
        wattrset(editWin, A_NORMAL);
        waddnstr(editWin, tmp + buf->pos + 1, buf->len - buf->pos - 1);
    } else {
        waddnstr(editWin, tmp, buf->len);
        wattrset(editWin, A_REVERSE);
        waddch(editWin, ' ');
        wattrset(editWin, A_NORMAL);
    }
    wrefresh(editWin);
    th_free(tmp);
}


int printWin(WINDOW *win, const char *fmt)
{
    const char *s = fmt;
    int col = 0;

    while (*s) {
        if (*s == '½') {
            int val = 0;
            s++;
            if (*s == '½') {
                waddch(win, ((unsigned char) *s) | col);
                s++;
            } else {
                while (*s >= '0' && *s <= '9') {
                    val *= 10;
                    val += (*s - '0');
                    s++;
                }
                if (*s != '½') return -1;
                s++;

                if (val < 9) {
                    col = A_DIM | COLOR_PAIR(val);
                } else if (val < 30) {
                    col = A_BOLD | COLOR_PAIR(val - 9);
                }
            }
        } else {
            if ((unsigned char) *s == 255)
                waddch(win, ((unsigned char) ' ') | col);
            else
            if (*s != '\r')
                waddch(win, ((unsigned char) *s) | col);
            s++;
        }
    }
    return 0;
}


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(BOOL addStamp, BOOL logOnly, const char *fmt, va_list ap)
{
    char tmpStr[128], buf[8192];
    
    getTimeStamp(tmpStr, sizeof(tmpStr), "½17½[½11½%H:%M:%S½17½]½0½ ");
    
    vsnprintf(buf, sizeof(buf), fmt, ap);
    
    if (optLogFile) {
        if (addStamp) printFile(optLogFile, tmpStr);
        printFile(optLogFile, buf);
        fflush(optLogFile);
    }
    
    if (!optDaemon && !logOnly) {
        if (addStamp) printWin(mainWin, tmpStr);
        printWin(mainWin, buf);
        wrefresh(mainWin);
    }
}

void printMsg(const char *fmt, ...)
{
    va_list ap;
    
    va_start(ap, fmt);
    printMsgV(TRUE, FALSE, fmt, ap);
    va_end(ap);
}

void printMsgC(const char *fmt, ...)
{
    va_list ap;
    
    va_start(ap, fmt);
    printMsgV(FALSE, FALSE, fmt, ap);
    va_end(ap);
}

void printMsgQ(BOOL logOnly, const char *fmt, ...)
{
    va_list ap;
    
    va_start(ap, fmt);
    printMsgV(TRUE, logOnly, fmt, ap);
    va_end(ap);
}


char *errorMessages = NULL;

void errorMsgV(const char *fmt, va_list ap)
{
    char *tmp;
    va_list ap2;

    va_copy(ap2, ap);
    printMsgV(TRUE, FALSE, fmt, ap);

    tmp = th_strdup_vprintf(fmt, ap2);
    
    if (errorMessages != NULL) {
        char *tmp2 = th_strdup_printf("%s%s", errorMessages, tmp);
        th_free(errorMessages);
        th_free(tmp);
        errorMessages = tmp2;
    } else
        errorMessages = tmp;
}

void errorMsg(const char *fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);
    errorMsgV(fmt, ap);
    va_end(ap);
}

void errorFunc(struct _nn_conn_t *conn, const char *fmt, va_list ap)
{
    errorMsgV(fmt, ap);
}

void messageFunc(struct _nn_conn_t *conn, const char *fmt, va_list ap)
{
    printMsgV(TRUE, FALSE, fmt, ap);
}


BOOL checkIgnoreList(const char *name)
{
    qlist_t *node = nnIgnoreList;
    while (node != NULL) {
        if (strcasecmp(name, (char *) node->data) == 0)
            return TRUE;
        node = node->next;
    }
    return FALSE;
}


int handleUser(nn_conn_t *conn, const char *str)
{
    const char *msg = "</USER><MESSAGE>", *p = str;
    BOOL isMine, isIgnored = FALSE;
    char *s, *t, *h, *userName;
    
    (void) conn;
    
    /* Find start of the message */
    s = strstr(str, msg);
    if (!s) return 1;
    *s = 0;
    s += strlen(msg);
    
    /* Find end of the message */
    t = strstr(s, "</MESSAGE>");
    if (!t) return 3;
    *t = 0;
    
    /* Decode message string */
    s = nn_decode_str1(s);
    if (!s) return -1;
    
    /* Decode username */
    userName = nn_decode_str1(p);
    if (!userName) {
        th_free(s);
        return -2;
    }

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

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

        t = nn_strip_tags(s + 1);
        if (!strncmp(t, "BPRV", 4)) {
            h = nn_decode_str2(t + 1);
            if (!isIgnored && setTarget == NULL && !strncmp(h, "PRV from ", 9)) {
                char *q;
                setTarget = th_strdup(h + 9);
                for (q = setTarget; *q && *q != ':'; q++);
                *q = 0;
                printMsg("PRV target autoset to '%s'\n", setTarget);
            }
            printMsgQ(isIgnored, "½11½%s½0½\n", h);
        } else {
            /* It's an action (/me) */
            h = nn_decode_str2(t);
            printMsgQ(isIgnored, "½9½* %s½0½\n", h);
        }
        th_free(h);
        th_free(t);
    } else {
        /* It's a normal message */
        t = nn_strip_tags(s);
        h = nn_decode_str2(t);
        printMsgQ(isIgnored, "½5½<½%d½%s½5½>½0½ %s\n", isMine ? 14 : 15, userName, h);
        th_free(h);
        th_free(t);
    }

done:
    th_free(s);
    th_free(userName);
    return 0;
}


int handleLogin(nn_conn_t *conn, const char *str)
{
    char tmpStr[256];
    
    getTimeStamp(tmpStr, sizeof(tmpStr), "%c");
    
    if (!strncmp(str, "FAILURE", 7)) {
        printMsg("½1½Login failure½0½ - ½3½%s½0½\n", tmpStr);
        return -2;
    } else if (!strncmp(str, "SUCCESS", 7)) {
        printMsg("½2½Login success½0½ - ½3½%s½0½\n", tmpStr);
        nn_conn_send_msg(conn, optUserNameEnc, "%%2FRequestUserList");
        return 0;
    } else
        return 1;
}


int handleAddUser(nn_conn_t *conn, const char *str)
{
    char *p, *s = strstr(str, "</ADD_USER>");

    (void) conn;

    if (!s) return 1;
    *s = 0;
    
    p = nn_dbldecode_str(str);
    if (!p) return -1;

    nn_userhash_insert(nnUsers, encodeUsername(p));

    printMsg("! ½3½%s½0½ ½2½ADDED.½0½\n", p);
    th_free(p);
    return 0;
}


int handleDeleteUser(nn_conn_t *conn, const char *str)
{
    char *p, *s = strstr(str, "</DELETE_USER>");

    (void) conn;

    if (!s) return 1;
    *s = 0;
    
    p = nn_dbldecode_str(str);
    if (!p) return -1;

    nn_userhash_delete(nnUsers, encodeUsername(p));

    printMsg("! ½3½%s½0½ ½1½DELETED.½0½\n", p);
    th_free(p);
    return 0;
}


int handleFoo(nn_conn_t *conn, const char *str)
{
    (void) conn; (void) str;
    
    return 0;
}


int handleBoot(nn_conn_t *conn, const char *str)
{
    (void) conn; (void) str;
    errorMsg("Booted by server.\n");
    return -1;
}


typedef struct {
    char *cmd;
    ssize_t len;
    int (*handler)(nn_conn_t *, const char *);
} protocmd_t;


static protocmd_t protoCmds[] = {
    { "<USER>",         -1, handleUser },
    { "<LOGIN_",        -1, handleLogin },
    { "<DELETE_USER>",  -1, handleDeleteUser },
    { "<ADD_USER>",     -1, handleAddUser },
    { "<NUMCLIENTS>",   -1, handleFoo },
    { "<BOOT />",       -1, handleBoot },
};

static const int nprotoCmds = sizeof(protoCmds) / sizeof(protoCmds[0]);


int handleProtocol(nn_conn_t *conn, const char *buf, const ssize_t bufLen)
{
    static BOOL protoCmdsInit = FALSE;
    int i;

    if (!protoCmdsInit) {
        for (i = 0; i < nprotoCmds; i++)
            protoCmds[i].len = strlen(protoCmds[i].cmd);
        protoCmdsInit = TRUE;
    }
    
    for (i = 0; i < nprotoCmds; i++) {
        ssize_t cmdLen = protoCmds[i].len;
        if (cmdLen < bufLen && !strncmp(buf, protoCmds[i].cmd, cmdLen))
            return protoCmds[i].handler(conn, buf + cmdLen);
    }

    if (optDebug) {
        printMsg("Unknown protocmd: \"%s\"\n", buf);
        return 0;
    } else
        return 1;
}

char * trimLeft(char *buf)
{
    while (*buf != 0 && th_isspace(*buf)) buf++;
    return buf;
}

int compareUsername(const void *s1, const void *s2)
{
    return strcasecmp((char *) s1, (char *) s2);
}

int handleUserInput(nn_conn_t *conn, char *buf, size_t bufLen)
{
    char *tmpStr, tmpBuf[4096];
    BOOL result;
    
    /* Trim right */
    bufLen--;
    buf[bufLen--] = 0;
    while (bufLen > 0 && (buf[bufLen] == '\n' || buf[bufLen] == '\r' || th_isspace(buf[bufLen])))
        buf[bufLen--] = 0;
    decodeUsername(buf);
    
    /* Check for special user commands */
    if (*buf == 0) {
        return 1;
    } else if (!strncasecmp(buf, "/color ", 7)) {
        /* Change color */
        int tmpInt;
        if ((tmpInt = th_get_hex_triplet(trimLeft(buf + 7))) < 0) {
            printMsg("Invalid color value '%s'\n", buf+7);
            return 1;
        }
        optUserColor = tmpInt;
        printMsg("Setting color to #%06x\n", optUserColor);
        nn_conn_send_msg(conn, optUserNameEnc, "%%2FSetFontColor%%20%%2Dcolor%%20%06X", optUserColor);
        return 0;
    } else if (!strncasecmp(buf, "/ignore", 7)) {
        char *name = trimLeft(buf + 7);
        if (strlen(name) > 0) {
            /* Add or remove someone to/from ignore */
            qlist_t *user = th_llist_find_func(nnIgnoreList, name, compareUsername);
            if (user != NULL) {
                printMsg("Removed user '%s' from ignore.\n", name);
                th_llist_delete_node(&nnIgnoreList, user);
            } else {
                printMsg("Now ignoring '%s'.\n", name);
                th_llist_append(&nnIgnoreList, th_strdup(name));
            }
        } else {
            /* Just list whomever is in ignore now */
            qlist_t *user = nnIgnoreList;
            ssize_t nuser = th_llist_length(nnIgnoreList);
            printMsg("Users ignored (%d): ", nuser);
            while (user != NULL) {
                if (user->data != NULL) {
                    printMsgC("'%s'", (char *) user->data);
                    if (--nuser > 0)
                        printMsgC(", ");
                }
                user = user->next;
            }
            printMsgC("\n");
        }
        return 0;
    } else if (!strncasecmp(buf, "/save", 5)) {
        /* Save configuration */
        FILE *cfgfile = fopen(setConfigFile, "w");
        if (cfgfile == NULL) {
            printMsg("Could not create configuration to file '%s': %s\n", setConfigFile,
                strerror(errno));
            return 0;
        }
        printMsg("Configuration saved in file '%s', res=%d\n",
            setConfigFile,
            th_cfg_write(cfgfile, setConfigFile, cfg));

        fclose(cfgfile);
        return 0;
    } else if (!strncasecmp(buf, "/w ", 3)) {
        /* Open given username's profile via firefox in a new tab */
        char *name = trimLeft(buf + 3);

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

        tmpStr = nn_encode_str1(name);
#ifdef __WIN32
        {
        HINSTANCE status;
        snprintf(tmpBuf, sizeof(tmpBuf), "http://www.newbienudes.com/profile/%s/", tmpStr);
        th_free(tmpStr);
        status = ShellExecute(NULL, "open", tmpBuf, NULL, NULL, SW_SHOWNA);
        if (status <= 32)
            printMsg("Could not launch default web browser: %d\n", status);
        }
#else
        {
        int status;
        int fds[2];
        pid_t pid;
        snprintf(tmpBuf, sizeof(tmpBuf), "openurl(http://www.newbienudes.com/profile/%s/,new-tab)", tmpStr);
        th_free(tmpStr);

        if (pipe(fds) == -1) {
            int ret = errno;
            printMsg("Could not open process communication pipe! (%d, %s)\n", ret, strerror(ret));
            return 0;
        }

        if ((pid = fork()) < 0) {
            printMsg("Could not create sub-process!\n");
        } else if (pid == 0) {
            dup2(fds[1], STDOUT_FILENO);
            dup2(fds[0], STDERR_FILENO);
            execlp(setBrowser, setBrowser, "-remote", tmpBuf, (void *)NULL);
            _exit(errno);
        }
        
        wait(&status);
        }
#endif
        return 0;
    } else if (!strncasecmp(buf, "/to", 3)) {
        char *name = trimLeft(buf + 3);
        /* Set private messaging target */
        th_free(setTarget);
        if (strlen(name) > 0) {
            setTarget = th_strdup(trimLeft(buf + 3));
            printMsg("Set prv target to '%s'\n", setTarget);
        } else {
            setTarget = NULL;
            printMsg("Cleared prv target.\n");
        }
        return 0;
    } else if (!strncasecmp(buf, "/who", 4)) {
        /* Alias /who to /listallusers */
        snprintf(tmpBuf, sizeof(tmpBuf), "/listallusers");
        buf = tmpBuf;
    } else if (setPrvMode) {
        /* Private chat mode, send as PRV */
        if (setTarget != NULL) {
            snprintf(tmpBuf, sizeof(tmpBuf), "/prv -to %s -msg %s", setTarget, buf);
            buf = tmpBuf;
        } else {
            printMsg("No target set, exiting prv mode.\n");
            setPrvMode = FALSE;
            return 1;
        }
    }
    
    /* Send double-encoded */
    tmpStr = nn_dblencode_str(decodeUsername(buf));
    if (tmpStr == 0) return -2;
    result = nn_conn_send_msg(conn, optUserNameEnc, "%s", tmpStr);
    th_free(tmpStr);
    
    return result ? 0 : -1;
}

void closeWindows(void)
{
    if (mainWin) delwin(mainWin);
    if (statusWin) delwin(statusWin);
    if (editWin) delwin(editWin);
}

BOOL initializeWindows(void)
{
    int w, h;

    getmaxyx(stdscr, h, w);
    
    closeWindows();

    mainWin = subwin(stdscr, h - 4, w, 0, 0);
    statusWin = subwin(stdscr, 1, w, h - 4, 0);
    editWin = subwin(stdscr, 3, w, h - 3, 0);
        
    if (mainWin == NULL || statusWin == NULL || editWin == NULL) {
        THERR("Could not create ncurses windows!\n");
        return FALSE;
    }
    scrollok(mainWin, 1);
        
    return TRUE;
}

void updateWindows(void)
{
    if (mainWin) redrawwin(mainWin);
    if (statusWin) redrawwin(statusWin);
    if (editWin) redrawwin(editWin);
}

BOOL performTabCompletion(nn_editbuf_t *buf)
{
    static char *previous = NULL, *pattern = NULL;
    BOOL again = FALSE, hasSeparator = FALSE, newPattern = FALSE, hasSpace = FALSE;
    char *str = buf->data;
    int mode = 0;
    ssize_t endPos, startPos = buf->pos;

    /* previous word */
    if (startPos >= 2 && str[startPos - 1] == ' ' && str[startPos - 2] != ' ') {
        startPos -= 2;
        endPos = startPos;
        while (startPos > 0 && str[startPos - 1] != ' ') startPos--;
        mode = 1;
    } else
    /* middle of a word, new pattern */
    if (startPos < buf->len && str[startPos] != ' ') {
        endPos = startPos;
        while (startPos > 0 && str[startPos - 1] != ' ') startPos--;
        while (endPos < buf->len - 1 && str[endPos + 1] != ' ') endPos++;
        newPattern = TRUE;
        mode = 2;
    } else
    /* previous word, new pattern */
    if (startPos >= 1 && str[startPos - 1] != ' ') {
        startPos -= 1;
        endPos = startPos;
        while (startPos > 0 && str[startPos - 1] != ' ') startPos--;
        newPattern = TRUE;
        mode = 3;
    } else {
        if (optDebug)
            printMsg("no mode\n");
        return FALSE;
    }

    if (str[endPos] == optNickSep) {
        endPos--;
        if (startPos > 0) {
            if (optDebug)
                printMsg("str[endPos] == optNickSep && startPos > 0 (%d)\n", startPos);
            return FALSE;
        }
        hasSeparator = TRUE;
    }

    if (buf->pos > 0 && str[buf->pos - 1] == ' ')
        hasSpace = TRUE;
    if (buf->pos <= buf->len && str[buf->pos] == ' ')
        hasSpace = TRUE;
    
    if (newPattern) {
        /* Get pattern, check if it matches previous pattern and set 'again' flag */
        char *npattern = nn_editbuf_get_string(buf, startPos, endPos);
        if (pattern && npattern && strcasecmp(npattern, pattern) == 0)
            again = TRUE;
        
        th_free(pattern);
        pattern = npattern;
        
        if (!again) {
            th_free(previous);
            previous = NULL;
        }
    }

    if (optDebug) {
        printMsg("sPos=%d, ePos=%d <-> bPos=%d, bufLen=%d : pat='%s' (again=%s, hassep=%s, hasspc=%s, newpat=%s, mode=%d)\n",
                  startPos, endPos, buf->pos, buf->len, pattern,
                  again ? "yes" : "no",
                  hasSeparator ? "yes" : "no",
                  hasSpace ? "yes" : "no",
                  newPattern ? "yes" : "no", mode);
    }

    if (pattern) {
        nn_user_t *user = nn_user_match(nnUsers, pattern, previous, again);

        if (user) {
            int i;
            char *c = user->name;
            if (optDebug)
                printMsg("match='%s' / prev='%s'\n", user->name, previous);

            for (i = startPos; i <= endPos; i++)
                nn_editbuf_delete(buf, startPos);
                
            for (i = startPos; *c; i++, c++)
                nn_editbuf_insert(buf, i, *c);

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

            nn_editbuf_setpos(buf, startPos + 1 + strlen(user->name));

            th_free(previous);
            previous = th_strdup(user->name);
            
            return TRUE;
        }
    }
    
    return FALSE;
}

#define VPUTCH(CH)    th_vputch(&bufData, &bufSize, &bufLen, CH)
#define VPUTS(STR)  th_vputs(&bufData, &bufSize, &bufLen, STR)

char *logParseFilename(const char *fmt, int id)
{
    size_t bufSize = strlen(fmt) + 32, bufLen = 0;
    char *bufData = NULL, tmpBuf[32];
    const char *s = fmt;
    
    while (*s) {
        if (*s == '%') {
            s++;
            switch (*s) {
                case 'i':
                    snprintf(tmpBuf, sizeof(tmpBuf), "%05d", id);
                    VPUTS(tmpBuf);
                    break;
                
                case '%':
                    VPUTCH('%');
                    break;
                
                default:
                    VPUTCH('%');
                    VPUTCH(*s);
                    break;
            }
            s++;
        } else {
            VPUTCH(*s);
            s++;
        }
    }
    
    VPUTCH(0);
    return bufData;
}


BOOL logFileOpen(void)
{
    char *filename;
    
    if (optLogFilename == NULL || !optLogEnable)
        return FALSE;

    filename = logParseFilename(optLogFilename, optPort);

    if ((optLogFile = fopen(filename, "a")) == NULL) {
        errorMsg("Could not open logfile '%s' for appending!\n", filename);
        th_free(filename);
        return FALSE;
    }
    
    th_free(filename);

    return TRUE;
}

void logFileClose(void)
{
    if (optLogFile) {
        fclose(optLogFile);
        optLogFile = NULL;
    }
}

char *promptRequester(const char *info, BOOL allowEmpty)
{
    char tmpBuf[512], *ptr;
    ssize_t pos;

    fputs(info, stdout);
    fgets(tmpBuf, sizeof(tmpBuf), stdin);
    
    for (pos = strlen(tmpBuf) - 1; pos > 0 && (tmpBuf[pos] == '\n' || tmpBuf[pos] == '\r' || th_isspace(tmpBuf[pos])); pos--)
        tmpBuf[pos] = 0;

    ptr = trimLeft(tmpBuf);

    if (allowEmpty || strlen(ptr) > 0)
        return th_strdup(ptr);
    else
        return NULL;
}

int main(int argc, char *argv[])
{
    nn_conn_t *conn = NULL;
    int curVis = ERR, updateCount = 0;
    struct hostent *tmpHost;
    BOOL argsOK, isError = FALSE,
        exitProg = FALSE,
        colorSet = FALSE,
        cursesInit = FALSE,
        networkInit = FALSE,
        insertMode = TRUE;
    time_t prevTime;
    char *tmpStr;
    nn_editbuf_t *editBuf = nn_editbuf_new(NN_TMPBUF_SIZE);
    nn_editbuf_t *histBuf[SET_MAX_HISTORY+2];
    int histPos = 0, histMax = 0;
    BOOL firstUpdate = TRUE;

    cfgitem_t *tmpcfg;
    char *homeDir = NULL;
    
    memset(histBuf, 0, sizeof(histBuf));
    backBuf = nn_ringbuf_new(SET_BACKBUF_LEN);
    
    /* Initialize */
    th_init("NNChat", "Newbie Nudes chat client", NN_VERSION,
        "Written and designed by Anonymous Finnish Guy (C) 2008-2011",
        "This software is freeware, use and distribute as you wish.");
    th_verbosityLevel = 0;

    /* Read config */
    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", &nnIgnoreList);
    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 (8002 = public room, 8003 = passion pit, 8005 = members only)");
    th_cfg_add_int(&tmpcfg, "port", &optPort, optPort);
    th_cfg_add_section(&cfg, "server", tmpcfg);

    tmpcfg = NULL;
    th_cfg_add_comment(&tmpcfg, "Enable logging");
    th_cfg_add_bool(&tmpcfg, "enable", &optLogEnable, optLogEnable);
    th_cfg_add_comment(&tmpcfg, "Log filename format");
    th_cfg_add_string(&tmpcfg, "filename", &optLogFilename, optLogFilename);
    th_cfg_add_section(&cfg, "logging", tmpcfg);

#ifdef __WIN32
    {
    char tmpPath[MAX_PATH];
    if (SHGetFolderPath(NULL, CSIDL_APPDATA | CSIDL_FLAG_CREATE, NULL, 0, tmpPath) == S_OK)
        homeDir = th_strdup(tmpPath);

    CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
    }
#else
    homeDir = th_strdup(getenv("HOME"));
#endif
    
    if (homeDir != NULL) {
        FILE *cfgfile;
        setConfigFile = th_strdup_printf("%s" SET_DIR_SEPARATOR "%s", homeDir, SET_CONFIG_FILE);

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

        if ((cfgfile = fopen(setConfigFile, "r")) != NULL)
            th_cfg_read(cfgfile, setConfigFile, cfg);
    }

    setBrowser = getenv("BROWSER");
    if (setBrowser == NULL)
        setBrowser = "firefox";
    
    /* Parse command line arguments */
    argsOK = th_args_process(argc, argv, optList, optListN,
        argHandleOpt, argHandleFile, FALSE);

    if (optUserNameCmd != NULL) {
        optUserName = optUserNameCmd;
        optPassword = optPasswordCmd;
    }

    /* Check if we have username and password */
    if (optUserName == NULL || optPassword == NULL) {
        printf("\nYou can avoid this prompt by issuing '/save' after logging in.\n\n");
        optUserName = promptRequester("NN username: ", FALSE);
        optPassword = promptRequester("NN password: ", TRUE);
        if (optUserName == NULL || optPassword == NULL) {
            THERR("User/pass not specified, get some --help\n");
            return -1;
        }
    }
    
    if (!argsOK)
        return -2;

    /* Allocate userhash */
    if ((nnUsers = nn_userhash_new()) == NULL) {
        THERR("Could not allocate userhash. Fatal error.\n");
        return -105;
    }

    /* Open logfile */
    logFileOpen();
        
    
    if (!nn_network_init()) {
        THERR("Could not initialize network subsystem.\n");
        goto err_exit;
    } else
        networkInit = TRUE;

    /* Okay ... */
    THMSG(1, "Trying to resolve host '%s' ...\n", optServer);
    tmpHost = gethostbyname(optServer);
    if (tmpHost == NULL) {
        THERR("Could not resolve hostname: %s.\n", strerror(h_errno));
        goto err_exit;
    }
    THMSG(2, "True hostname: %s\n", tmpHost->h_name);

    /* To emulate the official client, we first make a request for
     * policy file, even though we don't use it for anything...
     */
    conn = nn_conn_open((struct in_addr *) tmpHost->h_addr, 843);
    if (!nn_conn_check(conn)) {
        THERR("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) {
        THERR("Failed to send policy file request.\n");
        goto err_exit;
    } else {
        int cres = nn_conn_pull(conn);
        if (cres == 0) {
            THMSG(2, "Probe got: %s\n", conn->buf);
        } else {
            THMSG(2, "Could not get policy probe.\n");
        }
    }
    nn_conn_close(conn);

    /* Okay, now do the proper connection ... */
    conn = nn_conn_open((struct in_addr *) tmpHost->h_addr, optPort);
    if (!nn_conn_check(conn)) {
        THERR("Main connection setup failed!\n");
        goto err_exit;
    }
    
    conn->errfunc = errorFunc;
    conn->msgfunc = messageFunc;

    THMSG(1, "Connected, logging in as '%s', site '%s'.\n", optUserName, optSite);
    optUserNameEnc = nn_dblencode_str(optUserName);
    tmpStr = nn_dblencode_str(optSite);
    nn_conn_send_msg(conn, optUserNameEnc, "%%2Flogin%%20%%2Dsite%%20%s%%20%%2Dpassword%%20%s", tmpStr, optPassword);
    th_free(tmpStr);
    
    /* Initialize NCurses */
    if (!optDaemon) {
        if (LINES < 0 || LINES > 1000) LINES = 24;
        if (COLS < 0 || COLS > 1000) COLS = 80;
        initscr();
        raw();
        keypad(stdscr, TRUE);
        noecho();
        meta(stdscr, TRUE);
        timeout(SET_DELAY);
        curVis = curs_set(0);

        if (has_colors()) {
            start_color();
            
            init_pair( 1, COLOR_RED,     COLOR_BLACK);
            init_pair( 2, COLOR_GREEN,   COLOR_BLACK);
            init_pair( 3, COLOR_YELLOW,  COLOR_BLACK);
            init_pair( 4, COLOR_BLUE,    COLOR_BLACK);
            init_pair( 5, COLOR_MAGENTA, COLOR_BLACK);
            init_pair( 6, COLOR_CYAN,    COLOR_BLACK);
            init_pair( 7, COLOR_WHITE,   COLOR_BLACK);
            init_pair( 8, COLOR_BLACK,   COLOR_BLACK);

            init_pair(10, COLOR_BLACK,   COLOR_RED);
            init_pair(11, COLOR_WHITE,   COLOR_RED);
            init_pair(12, COLOR_GREEN,   COLOR_RED);
            init_pair(13, COLOR_YELLOW,  COLOR_RED);
            init_pair(14, COLOR_BLUE,    COLOR_RED);
            init_pair(15, COLOR_MAGENTA, COLOR_RED);
            init_pair(16, COLOR_CYAN,    COLOR_RED);
        }
        
        cursesInit = TRUE;
        
        if (!initializeWindows())
            goto err_exit;
        
        nn_editbuf_clear(editBuf);
        printEditBuf("", editBuf);
        updateStatus(insertMode);
    }

    /* Enter mainloop */
    prevTime = time(NULL);

    while (!isError && !exitProg) {
        int cres = nn_conn_pull(conn);
        if (cres == 0) {
            do {
                size_t bufLen = strlen(conn->ptr) + 1;
                int result = handleProtocol(conn, conn->ptr, bufLen);

                if (result > 0) {
                    /* Couldn't handle the message for some reason */
                    printMsg("Could not handle: %s\n", conn->ptr);
                } else if (result < 0) {
                    /* Fatal error, quit */
                    errorMsg("Fatal error with message: %s\n", conn->ptr);
                    isError = TRUE;
                }

                conn->got -= bufLen;
                conn->ptr += bufLen;
            } while (conn->got > 0 && !isError);
        }
        if (!nn_conn_check(conn))
            isError = TRUE;

        /* Handle user input */
        if (!optDaemon) {
            int c, cnt = 0;
            BOOL update = FALSE;
            
            /* Handle several buffered keypresses at once */
            do {
            c = wgetch(stdscr);
            if (c == 0x1b) {
                c = wgetch(stdscr);
                if (c == 'O') {
                    c = wgetch(stdscr);
                    switch (c) {
                        case 'd': c = 0x204; break;
                        case 'c': c = 0x206; break;
                        default:
                            printMsg("Unhandled ESC-O key sequence 0x%02x\n", c);
                            break;
                    }
                } else
                if (c == '[') {
                    c = wgetch(stdscr);
                    switch (c) {
                        case 0x32: c = KEY_IC; break;
                        case 0x33: c = KEY_DC; break;

                        case 0x35: c = KEY_PPAGE; break;
                        case 0x36: c = KEY_NPAGE; break;

                        case 0x37: c = KEY_HOME; break;
                        case 0x38: c = KEY_END; break;
                        
                        default:
                            printMsg("Unhandled ESC-[*~ key sequence 0x%02x\n", c);
                            c = ERR;
                            break;
                    }
                    /* Get the trailing ~ */
                    if (c != ERR)
                        wgetch(stdscr);
                } else {
                    printMsg("Unhandled ESC key sequence 0x%02x\n", c);
                    continue;
                }
            }
            

            switch (c) {
#ifdef KEY_RESIZE
            case KEY_RESIZE:
                resize_term(0, 0);
                erase();
#ifdef PDCURSES
                timeout(SET_DELAY);
#endif
                                
                if (!initializeWindows()) {
                    errorMsg("Error resizing curses windows\n");
                    isError = TRUE;
                }
                update = TRUE;
                break;
#endif
            
            case KEY_ENTER:
            case '\n':
            case '\r':
                /* Call the user input handler */
                if (editBuf->len > 0) {
                    int result;
                    
                    if (histMax > 0) {
                        nn_editbuf_free(histBuf[SET_MAX_HISTORY+1]);
                        histBuf[SET_MAX_HISTORY+1] = NULL;
                        memmove(&histBuf[2], &histBuf[1], histMax * sizeof(histBuf[0]));
                    }
                    
                    histPos = 0;
                    histBuf[1] = nn_editbuf_copy(editBuf);
                    if (histMax < SET_MAX_HISTORY) histMax++;
                    
                    nn_editbuf_insert(editBuf, editBuf->len, 0);
                    result = handleUserInput(conn, editBuf->data, editBuf->len);
                    
                    nn_editbuf_clear(editBuf);
                    
                    if (result < 0) {
                        errorMsg("Fatal error handling user input: %s\n", editBuf->data);
                        isError = TRUE;
                    }
                    
                    update = TRUE;
                }
                break;
            
            case KEY_UP: /* Backwards in input history */
                if (histPos == 0) {
                    nn_editbuf_free(histBuf[0]);
                    histBuf[0] = nn_editbuf_copy(editBuf);
                }
                if (histPos < histMax) {
                    histPos++;
                    nn_editbuf_free(editBuf);
                    editBuf = nn_editbuf_copy(histBuf[histPos]);
                    update = TRUE;
                }
                break;
                
            case KEY_DOWN: /* Forwards in input history */
                if (histPos > 0) {
                    histPos--;
                    nn_editbuf_free(editBuf);
                    editBuf = nn_editbuf_copy(histBuf[histPos]);
                    update = TRUE;
                }
                break;
                
            case 0x204: /* ctrl+left arrow = Skip words left */
            case 0x20b:
                while (editBuf->pos > 0 && isspace((int) editBuf->data[editBuf->pos - 1]))
                    editBuf->pos--;
                while (editBuf->pos > 0 && !isspace((int) editBuf->data[editBuf->pos - 1]))
                    editBuf->pos--;
                update = TRUE;
                break;
            
            case 0x206: /* ctrl+right arrow = Skip words right */
            case 0x210:
                while (editBuf->pos < editBuf->len && isspace((int) editBuf->data[editBuf->pos]))
                    editBuf->pos++;
                while (editBuf->pos < editBuf->len && !isspace((int) editBuf->data[editBuf->pos]))
                    editBuf->pos++;
                if (editBuf->pos > editBuf->len)
                    editBuf->pos = editBuf->len;
                update = TRUE;
                break;

            case KEY_HOME: nn_editbuf_setpos(editBuf, 0); update = TRUE; break;
            case KEY_END: nn_editbuf_setpos(editBuf, editBuf->len); update = TRUE; break;
            case KEY_LEFT: nn_editbuf_setpos(editBuf, editBuf->pos - 1); update = TRUE; break;
            case KEY_RIGHT: nn_editbuf_setpos(editBuf, editBuf->pos + 1); update = TRUE; break;
            
            case KEY_BACKSPACE:
            case 0x08:
            case 0x7f:
                nn_editbuf_delete(editBuf, editBuf->pos - 1);
                nn_editbuf_setpos(editBuf, editBuf->pos - 1);
                update = TRUE;
                break;
            
            case KEY_DC: /* Delete character */
                nn_editbuf_delete(editBuf, editBuf->pos);
                update = TRUE;
                break;
                

            case KEY_IC: /* Ins = Toggle insert / overwrite mode */
                insertMode = !insertMode;
                update = TRUE;
                break;
            
            case KEY_F(2): /* F2 = Clear editbuffer */
                nn_editbuf_clear(editBuf);
                update = TRUE;
                break;

            case KEY_F(5): /* F5 = Ignore mode */
                setIgnoreMode = !setIgnoreMode;
                printMsg("Ignore mode = %s\n", setIgnoreMode ? "ON" : "OFF");
                break;
            
            case KEY_F(7): /* F7 = Clear PRV target */
                if (setTarget) {
                    printMsg("Cleared PRV target.\n");
                    setPrvMode = FALSE;
                    th_free(setTarget);
                    setTarget = NULL;
                    update = TRUE;
                }
                break;

            case KEY_F(8): /* F8 = switch between PRV */
                if (setPrvMode)
                    setPrvMode = FALSE;
                else {
                    if (setTarget != NULL)
                        setPrvMode = TRUE;
                }
                update = TRUE;
                break;
            
            case 0x03: /* ^C = quit */
            case KEY_F(9): /* F9 = Quit */
                printMsg("Quitting per user request.\n");
                exitProg = TRUE;
                break;
            
            case 0x09: /* Tab = complete username */
                performTabCompletion(editBuf);
                update = TRUE;
                break;
            
            case 0x0c: /* Ctrl + L */
                updateWindows();
                break;

            case KEY_NPAGE:
            case KEY_PPAGE:
#if 0
                {
                int nlines, ncol, old;
                getmaxyx(mainWin, nlines, ncol);
                nlines /= 2;
                old = backBufPos;

                if (c == KEY_NPAGE)
                    backBufPos = (backBufPos > nlines) ? backBufPos - nlines : 0;
                else
                    backBufPos = (backBufPos < );

                if (old != backBufPos)
                    updateMain();
                }
#endif
                break;

            case ERR:
                /* Ignore */
                break;
                
            default:
                if (isprint(c) || c == 0xe4 || c == 0xf6 || c == 0xc4 || c == 0xd6) {
                    if (insertMode)
                        nn_editbuf_insert(editBuf, editBuf->pos, c);
                    else
                        nn_editbuf_write(editBuf, editBuf->pos, c);
                    nn_editbuf_setpos(editBuf, editBuf->pos + 1);
                    update = TRUE; 
                } else {
                    printMsg("Unhandled key: 0x%02x\n", c);
                }
                break;
            }
            } while (c != ERR && !exitProg && ++cnt < 10);
            
            if (update || firstUpdate) {
                /* Update edit line */
                printEditBuf(setPrvMode ? setTarget : "", editBuf);
                updateStatus(insertMode);
                firstUpdate = FALSE; /* a nasty hack ... */
            }
        } /* !optDaemon */
        
        if (++updateCount > 10) {
            time_t tmpTime = time(NULL);
            if (tmpTime - prevTime > SET_KEEPALIVE) {
                nn_conn_send_msg(conn, optUserNameEnc, "/listallusers"); 
                prevTime = tmpTime;
            }
            
            if (!colorSet) {
                colorSet = TRUE;
                printMsg("%s v%s - %s\n", th_prog_name, th_prog_version, th_prog_fullname);
                printMsg("%s\n", th_prog_author);
                printMsg("%s\n", th_prog_license);
                nn_conn_send_msg(conn, optUserNameEnc, "%%2FSetFontColor%%20%%2Dcolor%%20%06X", optUserColor);
            }
            
            updateStatus(insertMode);
            updateCount = 0;
        }
        
    }
    
    /* Shutdown */
err_exit:
    nn_userhash_free(nnUsers);
    nn_ringbuf_free(backBuf);
    nn_editbuf_free(editBuf);
    for (histPos = 0; histPos <= SET_MAX_HISTORY; histPos++)
        nn_editbuf_free(histBuf[histPos]);
    
    if (cursesInit) {
        if (curVis != ERR)
            curs_set(curVis);
        closeWindows();
        endwin();
        THMSG(1, "NCurses deinitialized.\n");
    }
    
    if (errorMessages)
        THERR("%s", errorMessages);

    if (isError) {
#ifdef __WIN32
        char *tmp = promptRequester("Press enter to quit.\n", FALSE);
        th_free(tmp);
#else
        THMSG(1, "Error exit.\n");
#endif
    }
    
    th_free(optUserNameEnc);

    nn_conn_close(conn);
    
    if (networkInit)
        nn_network_close();

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

    return 0;
}