Mercurial > hg > nnchat
view nnchat.c @ 291:cc2a1d837e7b
Window / buffer functionality works now. Queries work (with few minor glitches).
author | Matti Hamalainen <ccr@tnsp.org> |
---|---|
date | Sat, 11 Jun 2011 03:18:22 +0300 |
parents | c53f880837f5 |
children | ff39ebf05b40 |
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 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 */ #define SET_MAX_WINDOWS (32) typedef struct { qringbuf_t *data; /* "Backbuffer" data for this window */ int pos; /* Current position in the window, 0 = real time */ BOOL dirty; char *id; /* Chatter ID, NULL = main window */ int num; /* Window number */ char *buf; size_t len, bufsize; } nn_window_t; /* Options */ int optPort = 8005; int optUserColor = 0x000000; char *optServer = "chat.newbienudes.com", *optUserName = NULL, *optUserNameCmd = NULL, *optUserNameEnc = NULL, *optPassword = NULL, *optPasswordCmd = NULL, *optLogFilename = NULL, *optSite = "NN"; char optNickSep = ':'; BOOL optDaemon = FALSE; FILE *optLogFile = NULL; BOOL setIgnoreMode = FALSE; BOOL optDebug = FALSE; BOOL optLogEnable = FALSE; nn_window_t *chatWindows[SET_MAX_WINDOWS], *currWin = NULL; WINDOW *mainWin = NULL, *statusWin = NULL, *editWin = NULL; qlist_t *setIgnoreList = NULL, *setIdleMessages = NULL; nn_userhash_t *nnUsers = NULL; char *setConfigFile = NULL, *setBrowser = NULL; cfgitem_t *cfg = NULL; /* Logging mode flags */ enum { LOG_FILE = 1, LOG_WINDOW = 2, LOG_STAMP = 4 }; /* Arguments */ optarg_t optList[] = { { 0, '?', "help", "Show this help", OPT_NONE }, { 1, 'v', "verbose", "Be more verbose", OPT_NONE }, { 2, 'p', "port", "Connect to port", OPT_ARGREQ }, { 3, 's', "server", "Server to connect to", OPT_ARGREQ }, { 4, 'C', "color", "Initial color in RGB hex 000000", OPT_ARGREQ }, { 5, 'l', "logfile", "Log filename", OPT_ARGREQ }, { 6, 'D', "daemon", "A pseudo-daemon mode for logging", OPT_NONE }, { 7, '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(void) { 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; } } nn_window_t *nn_window_new(const char *id) { nn_window_t *res = th_calloc(1, sizeof(nn_window_t)); if (res == NULL) return NULL; res->data = th_ringbuf_new(SET_BACKBUF_LEN, th_free); if (res->data == NULL) { th_free(res); return NULL; } res->id = th_strdup(id); res->num = 0; return res; } void nn_window_free(nn_window_t *win) { if (win != NULL) { th_ringbuf_free(win->data); th_free(win->id); th_free(win); } } nn_window_t *nn_find_window(const char *id) { int i; for (i = 0; i < SET_MAX_WINDOWS; i++) if (chatWindows[i] != NULL && chatWindows[i]->id != NULL && strcasecmp(id, chatWindows[i]->id) == 0) return chatWindows[i]; return NULL; } BOOL openWindow(const char *name, BOOL curwin) { int i; nn_window_t *res; if (name == NULL) return FALSE; if ((res = nn_window_new(name)) == NULL) return FALSE; for (i = 1; i < SET_MAX_WINDOWS; i++) if (chatWindows[i] == NULL) { res->num = i; chatWindows[i] = res; if (curwin) currWin = res; return TRUE; } return FALSE; } void closeWindow(nn_window_t *win) { int i; if (win == NULL) return; for (i = 1; i < SET_MAX_WINDOWS; i++) if (chatWindows[i] == win) { chatWindows[i] = NULL; nn_window_free(win); return; } } void updateStatus(void) { char tmpStr[128]; int i; if (statusWin == NULL) return; 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)); wattrset(statusWin, A_BOLD | COLOR_PAIR(13)); waddstr(statusWin, " | "); wattrset(statusWin, A_BOLD | COLOR_PAIR(11)); snprintf(tmpStr, sizeof(tmpStr), "#%06x", optUserColor); waddstr(statusWin, tmpStr); wattrset(statusWin, A_BOLD | COLOR_PAIR(13)); waddstr(statusWin, " | WIN: "); snprintf(tmpStr, sizeof(tmpStr), "%d: %s", currWin->num + 1, currWin->id ? currWin->id : "MAIN"); waddstr(statusWin, tmpStr); wattrset(statusWin, A_BOLD | COLOR_PAIR(13)); waddstr(statusWin, " | "); wattrset(statusWin, A_BOLD | COLOR_PAIR(11)); for (i = 0; i < SET_MAX_WINDOWS; i++) if (chatWindows[i] != NULL && chatWindows[i]->dirty) { snprintf(tmpStr, sizeof(tmpStr), "%d ", i + 1); waddstr(statusWin, tmpStr); } wrefresh(statusWin); } void printEditBuf(nn_editbuf_t *buf) { char *tmp; if (editWin == NULL || buf == NULL) return; buf->data[buf->len] = 0; tmp = nn_username_decode(th_strdup(buf->data)); werase(editWin); wattrset(editWin, A_NORMAL); if (buf->pos < buf->len) { waddnstr(editWin, tmp, buf->pos); wattrset(editWin, A_REVERSE); waddch(editWin, tmp[buf->pos]); wattrset(editWin, A_NORMAL); waddnstr(editWin, tmp + buf->pos + 1, buf->len - buf->pos - 1); } else { waddnstr(editWin, tmp, buf->len); wattrset(editWin, A_REVERSE); waddch(editWin, ' '); wattrset(editWin, A_NORMAL); } wrefresh(editWin); th_free(tmp); } int printWin(WINDOW *win, const char *fmt) { const char *s = fmt; int col = 0; while (*s) { if (*s == '½') { 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 { waddch(win, ((unsigned char) *s) | col); s++; } } return 0; } void nn_window_print(nn_window_t *win, const char *fmt) { const char *s = fmt; while (*s) { if (*s == '\n') { th_vputch(&(win->buf), &(win->bufsize), &(win->len), '\n'); th_vputch(&(win->buf), &(win->bufsize), &(win->len), 0); th_ringbuf_add(win->data, win->buf); win->buf = NULL; win->dirty = TRUE; } else if ((unsigned char) *s == 255) th_vputch(&(win->buf), &(win->bufsize), &(win->len), ' '); else if (*s != '\r') th_vputch(&(win->buf), &(win->bufsize), &(win->len), *s); s++; } } void updateMainWin(BOOL force) { int y, w, h, offs; qringbuf_t *buf; if (mainWin == NULL || currWin == NULL) return; if (!force && !currWin->dirty) return; buf = currWin->data; getmaxyx(mainWin, h, w); werase(mainWin); offs = buf->size - h - currWin->pos; if (offs < 0) offs = 0; for (y = 0; y < h && offs < buf->size; offs++) { if (buf->data[offs] != NULL) printWin(mainWin, (char *) buf->data[offs]); y = getcury(mainWin); } currWin->dirty = FALSE; wrefresh(mainWin); } int printFile(FILE *outFile, const char *fmt) { const char *s = fmt; while (*s) { if (*s == '½') { s++; if (*s == '½') { fputc((unsigned char) *s, outFile); s++; } else { while (*s && isdigit((int) *s)) s++; if (*s != '½') return -1; s++; } } else { if ((unsigned char) *s == 255) fputc(' ', outFile); else fputc((unsigned char) *s, outFile); s++; } } return 0; } void printMsgV(nn_window_t *win, int flags, const char *fmt, va_list ap) { char tmpStr[128], *buf; getTimeStamp(tmpStr, sizeof(tmpStr), "½17½[½11½%H:%M:%S½17½]½0½ "); buf = th_strdup_vprintf(fmt, ap); if (optLogFile && (flags & LOG_FILE)) { if (flags & LOG_STAMP) printFile(optLogFile, tmpStr); printFile(optLogFile, buf); fflush(optLogFile); } if (!optDaemon && (flags & LOG_WINDOW)) { nn_window_t *tmp = win != NULL ? win : chatWindows[0]; if (flags & LOG_STAMP) nn_window_print(tmp, tmpStr); nn_window_print(tmp, buf); updateMainWin(FALSE); } 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 printMsgC(nn_window_t *win, const char *fmt, ...) { va_list ap; va_start(ap, fmt); printMsgV(win, LOG_WINDOW | LOG_FILE, fmt, ap); va_end(ap); } void printMsgQ(nn_window_t *win, BOOL logOnly, const char *fmt, ...) { va_list ap; va_start(ap, fmt); printMsgV(win, logOnly ? (LOG_STAMP | LOG_FILE) : (LOG_STAMP | LOG_WINDOW | LOG_FILE), 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(chatWindows[0], LOG_STAMP | LOG_WINDOW | LOG_FILE, 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) { (void) conn; errorMsgV(fmt, ap); } void messageFunc(struct _nn_conn_t *conn, const char *fmt, va_list ap) { (void) conn; printMsgV(chatWindows[0], LOG_STAMP | LOG_WINDOW | LOG_FILE, fmt, ap); } BOOL checkIgnoreList(const char *name) { qlist_t *node = setIgnoreList; 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, *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; isIgnored = setIgnoreMode && !isMine && checkIgnoreList(userName); /* Is it a special control message? */ if (*s == '/') { /* Ignore room join/leave messages */ if (!optDebug && (strstr(s, "left the room") || strstr(s, "joined the room from"))) goto done; t = nn_strip_tags(s + 1); if (!strncmp(t, "BPRV ", 5)) { char *name, *tmp, *msg; if (!strncmp(t, "BPRV from ", 10)) { name = nn_decode_str2(t + 10); isMine = FALSE; } else { name = nn_decode_str2(t + 8); isMine = TRUE; } for (tmp = name; *tmp && *tmp != ':'; tmp++); if (tmp[0] != 0 && tmp[1] == ' ') msg = tmp + 2; else msg = ""; *tmp = 0; isIgnored = setIgnoreMode && checkIgnoreList(name); printMsgQ(nn_find_window(name), isIgnored, "½5½<½%d½%s½5½>½0½ %s\n", isMine ? 14 : 15, isMine ? optUserName : name, msg); } else { /* It's an action (/me) */ char *h = nn_decode_str2(t); printMsgQ(NULL, isIgnored, "½9½* %s½0½\n", h); th_free(h); } th_free(t); } else { /* It's a normal message */ char *h; t = nn_strip_tags(s); h = nn_decode_str2(t); printMsgQ(NULL, 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(NULL, "½1½Login failure½0½ - ½3½%s½0½\n", tmpStr); return -2; } else if (!strncmp(str, "SUCCESS", 7)) { printMsg(NULL, "½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, nn_username_encode(p)); printMsg(NULL, "! ½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, nn_username_encode(p)); printMsg(NULL, "! ½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(NULL, "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; /* Decode completed usernames */ nn_username_decode(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(currWin, "Invalid color value '%s'\n", buf+7); return 1; } optUserColor = tmpInt; printMsg(currWin, "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(setIgnoreList, name, compareUsername); if (user != NULL) { printMsg(currWin, "Removed user '%s' from ignore.\n", name); th_llist_delete_node(&setIgnoreList, user); } else { printMsg(currWin, "Now ignoring '%s'.\n", name); th_llist_append(&setIgnoreList, th_strdup(name)); } } else { /* Just list whomever is in ignore now */ qlist_t *user = setIgnoreList; ssize_t nuser = th_llist_length(setIgnoreList); char *result = th_strdup_printf("Users ignored (%d): ", nuser); while (user != NULL) { if (user->data != NULL) { th_pstr_printf(&result, "%s'%s'", result, (char *) user->data); if (--nuser > 0) th_pstr_printf(&result, "%s, ", result); } user = user->next; } printMsg(currWin, "%s\n", result); th_free(result); } return 0; } else if (!strncasecmp(buf, "/query", 6)) { char *name = trimLeft(buf + 6); if (strlen(name) > 0) { nn_user_t *user = nn_user_find(nnUsers, name); if (user != NULL) { printMsg(currWin, "Opening PRV query for '%s'.\n", user->name); if (openWindow(user->name, TRUE)) printMsg(currWin, "In PRV query with '%s'.\n", user->name); } } else { printMsg(currWin, "Usage: /query username\n"); printMsg(currWin, "To close a PRV query, use /close [username]\n"); printMsg(currWin, "/close without username will close the current PRV window (if any).\n"); } return 0; } else if (!strncasecmp(buf, "/close", 6)) { char *name = trimLeft(buf + 6); if (strlen(name) > 0) { nn_user_t *user = nn_user_find(nnUsers, name); if (user != NULL) { nn_window_t *win = nn_find_window(user->name); closeWindow(win); } else { printMsg(currWin, "No PRV query by name '%s'.\n", name); } } else { if (currWin != chatWindows[0]) { closeWindow(currWin); currWin = chatWindows[0]; } } return 0; } else if (!strncasecmp(buf, "/save", 5)) { /* Save configuration */ FILE *cfgfile = fopen(setConfigFile, "w"); if (cfgfile == NULL) { printMsg(currWin, "Could not create configuration to file '%s': %s\n", setConfigFile, strerror(errno)); return 0; } printMsg(currWin, "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(currWin, "Opening profile for: '%s'\n", name); tmpStr = nn_encode_str1(name); #ifdef __WIN32 { HINSTANCE status; snprintf(tmpBuf, sizeof(tmpBuf), "http://www.newbienudes.com/profile/%s/", tmpStr); th_free(tmpStr); status = ShellExecute(NULL, "open", tmpBuf, NULL, NULL, SW_SHOWNA); if (status <= 32) printMsg(currWin, "Could not launch default web browser: %d\n", status); } #else { int status; int fds[2]; pid_t pid; snprintf(tmpBuf, sizeof(tmpBuf), "openurl(http://www.newbienudes.com/profile/%s/,new-tab)", tmpStr); th_free(tmpStr); if (pipe(fds) == -1) { int ret = errno; printMsg(currWin, "Could not open process communication pipe! (%d, %s)\n", ret, strerror(ret)); return 0; } if ((pid = fork()) < 0) { printMsg(currWin, "Could not create sub-process!\n"); } else if (pid == 0) { dup2(fds[1], STDOUT_FILENO); dup2(fds[0], STDERR_FILENO); execlp(setBrowser, setBrowser, "-remote", tmpBuf, (void *)NULL); _exit(errno); } wait(&status); } #endif return 0; } else if (!strncasecmp(buf, "/who", 4)) { /* Alias /who to /listallusers */ snprintf(tmpBuf, sizeof(tmpBuf), "/listallusers"); buf = tmpBuf; } else if (currWin != chatWindows[0]) { if (currWin->id != NULL) { snprintf(tmpBuf, sizeof(tmpBuf), "/prv -to %s -msg %s", currWin->id, buf); buf = tmpBuf; } else { printMsg(NULL, "No target set, exiting prv mode.\n"); return 1; } } /* Send double-encoded */ tmpStr = nn_dblencode_str(nn_username_decode(buf)); if (tmpStr == 0) return -2; result = nn_conn_send_msg(conn, optUserNameEnc, "%s", tmpStr); th_free(tmpStr); return result ? 0 : -1; } void closeWindows(void) { if (mainWin) delwin(mainWin); if (statusWin) delwin(statusWin); if (editWin) delwin(editWin); } BOOL initializeWindows(void) { int w, h; getmaxyx(stdscr, h, w); closeWindows(); mainWin = subwin(stdscr, h - 4, w, 0, 0); statusWin = subwin(stdscr, 1, w, h - 4, 0); editWin = subwin(stdscr, 3, w, h - 3, 0); if (mainWin == NULL || statusWin == NULL || editWin == NULL) { THERR("Could not create curses chatWindows!\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(currWin, "no mode\n"); return FALSE; } if (str[endPos] == optNickSep) { endPos--; if (startPos > 0) { if (optDebug) printMsg(currWin, "str[endPos] == optNickSep && startPos > 0 (%d)\n", startPos); return FALSE; } hasSeparator = TRUE; } if (buf->pos > 0 && str[buf->pos - 1] == ' ') hasSpace = TRUE; if (buf->pos <= buf->len && str[buf->pos] == ' ') hasSpace = TRUE; if (newPattern) { /* Get pattern, check if it matches previous pattern and set 'again' flag */ char *npattern = nn_editbuf_get_string(buf, startPos, endPos); if (pattern && npattern && strcasecmp(npattern, pattern) == 0) again = TRUE; th_free(pattern); pattern = npattern; if (!again) { th_free(previous); previous = NULL; } } if (optDebug) { printMsg(currWin, "sPos=%d, ePos=%d <-> bPos=%d, bufLen=%d : pat='%s' (again=%s, hassep=%s, hasspc=%s, newpat=%s, mode=%d)\n", startPos, endPos, buf->pos, buf->len, pattern, again ? "yes" : "no", hasSeparator ? "yes" : "no", hasSpace ? "yes" : "no", newPattern ? "yes" : "no", mode); } if (pattern) { nn_user_t *user = nn_user_match(nnUsers, pattern, previous, again); if (user) { int i; char *c = user->name; if (optDebug) printMsg(currWin, "match='%s' / prev='%s'\n", user->name, previous); for (i = startPos; i <= endPos; i++) nn_editbuf_delete(buf, startPos); for (i = startPos; *c; i++, c++) nn_editbuf_insert(buf, i, *c); if (!hasSeparator && startPos == 0) { nn_editbuf_insert(buf, i++, optNickSep); startPos++; } if (hasSeparator) startPos++; if (!hasSpace) nn_editbuf_insert(buf, i++, ' '); nn_editbuf_setpos(buf, startPos + 1 + strlen(user->name)); th_free(previous); previous = th_strdup(user->name); return TRUE; } } return FALSE; } #define VPUTCH(CH) th_vputch(&bufData, &bufSize, &bufLen, CH) #define VPUTS(STR) th_vputs(&bufData, &bufSize, &bufLen, STR) char *logParseFilename(const char *fmt, int id) { size_t bufSize = strlen(fmt) + TH_BUFGROW, bufLen = 0; char *bufData = th_malloc(bufSize); char tmpBuf[32]; const char *s = fmt; while (*s) { if (*s == '%') { s++; switch (*s) { case 'i': snprintf(tmpBuf, sizeof(tmpBuf), "%05d", id); VPUTS(tmpBuf); break; case 'd': snprintf(tmpBuf, sizeof(tmpBuf), "%d", id); VPUTS(tmpBuf); break; case '%': VPUTCH('%'); break; } s++; } else { VPUTCH(*s); s++; } } VPUTCH(0); return bufData; } BOOL logFileOpen(void) { char *filename; if (optLogFilename == NULL || !optLogEnable) return FALSE; filename = logParseFilename(optLogFilename, optPort); if ((optLogFile = fopen(filename, "a")) == NULL) { errorMsg("Could not open logfile '%s' for appending!\n", filename); th_free(filename); return FALSE; } th_free(filename); return TRUE; } void logFileClose(void) { if (optLogFile) { fclose(optLogFile); optLogFile = NULL; } } char *promptRequester(WINDOW *win, const char *info, BOOL allowEmpty) { char tmpBuf[512], *ptr; ssize_t pos; int curVis = curs_set(1); echo(); waddstr(win, info); wgetnstr(win, tmpBuf, sizeof(tmpBuf) - 1); noecho(); if (curVis != ERR) curs_set(curVis); for (pos = strlen(tmpBuf) - 1; pos > 0 && (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; struct hostent *tmpHost; int curVis = ERR, updateCount = 0; BOOL argsOK, isError = FALSE, exitProg = FALSE, colorSet = FALSE, cursesInit = FALSE, networkInit = FALSE, insertMode = TRUE, firstUpdate = TRUE; time_t prevTime; char *tmpStr; nn_editbuf_t *editBuf = nn_editbuf_new(NN_TMPBUF_SIZE); nn_editbuf_t *histBuf[SET_MAX_HISTORY+2]; int histPos = 0, histMax = 0; cfgitem_t *tmpcfg; char *homeDir = NULL; /* 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 configuration file */ tmpcfg = NULL; th_cfg_add_comment(&tmpcfg, "General settings"); th_cfg_add_string(&tmpcfg, "username", &optUserName, NULL); th_cfg_add_string(&tmpcfg, "password", &optPassword, NULL); th_cfg_add_comment(&tmpcfg, "Default color as a hex-triplet"); th_cfg_add_hexvalue(&tmpcfg, "color", &optUserColor, optUserColor); th_cfg_add_comment(&tmpcfg, "Default setting of ignore mode"); th_cfg_add_bool(&tmpcfg, "ignore", &setIgnoreMode, setIgnoreMode); th_cfg_add_comment(&tmpcfg, "People to be ignored when ignore mode is enabled"); th_cfg_add_string_list(&tmpcfg, "ignore_list", &setIgnoreList); th_cfg_add_comment(&tmpcfg, "Random messages for idle timeout protection. If none are set, plain '.' is used."); th_cfg_add_string_list(&tmpcfg, "idle_messages", &setIdleMessages); th_cfg_add_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; } if (!argsOK) return -2; /* Allocate userhash */ if ((nnUsers = nn_userhash_new()) == NULL) { THERR("Could not allocate userhash. Fatal error.\n"); return -105; } /* If no idle messages are set, add default */ if (setIdleMessages == NULL) { th_llist_append(&setIdleMessages, th_strdup(".")); } /* Open logfile */ logFileOpen(); /* Initialize network */ if (!nn_network_init()) { THERR("Could not initialize network subsystem.\n"); goto err_exit; } else networkInit = TRUE; /* Initialize NCurses */ if (!optDaemon) { if (LINES < 0 || LINES > 1000) LINES = 24; if (COLS < 0 || COLS > 1000) COLS = 80; initscr(); raw(); keypad(stdscr, TRUE); noecho(); meta(stdscr, TRUE); timeout(SET_DELAY); curVis = curs_set(0); if (has_colors()) { start_color(); init_pair( 1, COLOR_RED, COLOR_BLACK); init_pair( 2, COLOR_GREEN, COLOR_BLACK); init_pair( 3, COLOR_YELLOW, COLOR_BLACK); init_pair( 4, COLOR_BLUE, COLOR_BLACK); init_pair( 5, COLOR_MAGENTA, COLOR_BLACK); init_pair( 6, COLOR_CYAN, COLOR_BLACK); init_pair( 7, COLOR_WHITE, COLOR_BLACK); init_pair( 8, COLOR_BLACK, COLOR_BLACK); init_pair(10, COLOR_BLACK, COLOR_RED); init_pair(11, COLOR_WHITE, COLOR_RED); init_pair(12, COLOR_GREEN, COLOR_RED); init_pair(13, COLOR_YELLOW, COLOR_RED); init_pair(14, COLOR_BLUE, COLOR_RED); init_pair(15, COLOR_MAGENTA, COLOR_RED); init_pair(16, COLOR_CYAN, COLOR_RED); } cursesInit = TRUE; if (!initializeWindows()) goto err_exit; memset(chatWindows, 0, sizeof(chatWindows)); chatWindows[0] = nn_window_new(NULL); currWin = chatWindows[0]; updateStatus(); } /* Check if we have username and password */ if (cursesInit && (optUserName == NULL || optPassword == NULL)) { printWin(editWin, "You can avoid this prompt by issuing '/save' after logging in.\n"); optUserName = promptRequester(editWin, "NN username: ", FALSE); optPassword = promptRequester(editWin, "NN password: ", TRUE); } if (optUserName == NULL || optPassword == NULL) { errorMsg("Username and/or password not specified.\n"); goto err_exit; } /* Okay ... */ printMsg(currWin, "Trying to resolve host '%s' ...\n", optServer); tmpHost = gethostbyname(optServer); if (tmpHost == NULL) { errorMsg("Could not resolve hostname: %s.\n", strerror(h_errno)); goto err_exit; } printMsg(currWin, "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)) { errorMsg("Policy file request connection setup failed!\n"); goto err_exit; } tmpStr = "<policy-file-request/>"; if (nn_conn_send_buf(conn, tmpStr, strlen(tmpStr) + 1) == FALSE) { errorMsg("Failed to send policy file request.\n"); goto err_exit; } else { int cres = nn_conn_pull(conn); if (cres == 0) { printMsg(currWin, "Probe got: %s\n", conn->buf); } else { printMsg(currWin, "Could not get policy probe.\n"); } } nn_conn_close(conn); /* Okay, now do the proper connection ... */ conn = nn_conn_open((struct in_addr *) tmpHost->h_addr, optPort); if (!nn_conn_check(conn)) { errorMsg("Main connection setup failed!\n"); goto err_exit; } conn->errfunc = errorFunc; conn->msgfunc = messageFunc; /* Send login command */ optUserNameEnc = nn_dblencode_str(optUserName); tmpStr = nn_dblencode_str(optSite); nn_conn_send_msg(conn, optUserNameEnc, "%%2Flogin%%20%%2Dsite%%20%s%%20%%2Dpassword%%20%s", tmpStr, optPassword); th_free(tmpStr); /* Initialize random numbers */ prevTime = time(NULL); srandom((int) prevTime); if (cursesInit) { /* Initialize rest of interactive UI code */ memset(histBuf, 0, sizeof(histBuf)); nn_editbuf_clear(editBuf); /* First update of screen */ printEditBuf(editBuf); updateStatus(); printMsg(NULL, "%s v%s - %s\n", th_prog_name, th_prog_version, th_prog_fullname); printMsg(NULL, "%s\n", th_prog_author); printMsg(NULL, "%s\n", th_prog_license); } /* Enter mainloop */ 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(currWin, "Could not handle: %s\n", conn->ptr); } else if (result < 0) { /* Fatal error, quit */ errorMsg("Fatal error with message: %s\n", conn->ptr); isError = TRUE; } conn->got -= bufLen; conn->ptr += bufLen; } while (conn->got > 0 && !isError); } if (!nn_conn_check(conn)) isError = TRUE; /* Handle user input */ if (cursesInit) { int c, cnt = 0; BOOL update = FALSE, updateMain = TRUE; /* 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(currWin, "Unhandled ESC-O key sequence 0x%02x\n", c); break; } } else if (c == '[') { c = wgetch(stdscr); switch (c) { case 0x31: c = wgetch(stdscr); if (c >= 0x31 && c <= 0x39) c = KEY_F(c - 0x30); else c = ERR; break; case 0x32: c = KEY_IC; break; case 0x33: c = KEY_DC; break; case 0x35: c = KEY_PPAGE; break; case 0x36: c = KEY_NPAGE; break; case 0x37: c = KEY_HOME; break; case 0x38: c = KEY_END; break; default: printMsg(currWin, "Unhandled ESC-[*~ key sequence 0x%02x\n", c); c = ERR; break; } /* Get the trailing ~ */ if (c != ERR) wgetch(stdscr); } if (c >= 0x31 && c <= 0x39) { /* Chat window switching via Meta/Esc-[1..9] */ int win = c - 0x31; if (win < SET_MAX_WINDOWS && chatWindows[win] != NULL) { currWin = chatWindows[win]; update = updateMain = TRUE; } c = ERR; } else { printMsg(currWin, "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 chatWindows\n"); isError = TRUE; } update = TRUE; updateMain = TRUE; break; #endif case KEY_ENTER: case '\n': case '\r': /* Call the user input handler */ if (editBuf->len > 0) { int result; if (histMax > 0) { nn_editbuf_free(histBuf[SET_MAX_HISTORY+1]); histBuf[SET_MAX_HISTORY+1] = NULL; memmove(&histBuf[2], &histBuf[1], histMax * sizeof(histBuf[0])); } histPos = 0; histBuf[1] = nn_editbuf_copy(editBuf); if (histMax < SET_MAX_HISTORY) histMax++; nn_editbuf_insert(editBuf, editBuf->len, 0); result = 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; } else { /* Update time value of last sent message for unidle timeouts */ prevTime = time(NULL); } update = TRUE; } break; case KEY_UP: /* Backwards in input history */ if (histPos == 0) { nn_editbuf_free(histBuf[0]); histBuf[0] = nn_editbuf_copy(editBuf); } if (histPos < histMax) { histPos++; nn_editbuf_free(editBuf); editBuf = nn_editbuf_copy(histBuf[histPos]); update = TRUE; } break; case KEY_DOWN: /* Forwards in input history */ if (histPos > 0) { histPos--; nn_editbuf_free(editBuf); editBuf = nn_editbuf_copy(histBuf[histPos]); update = TRUE; } break; case 0x204: /* ctrl+left arrow = Skip words left */ case 0x20b: while (editBuf->pos > 0 && isspace((int) editBuf->data[editBuf->pos - 1])) editBuf->pos--; while (editBuf->pos > 0 && !isspace((int) editBuf->data[editBuf->pos - 1])) editBuf->pos--; update = TRUE; break; case 0x206: /* ctrl+right arrow = Skip words right */ case 0x210: while (editBuf->pos < editBuf->len && isspace((int) editBuf->data[editBuf->pos])) editBuf->pos++; while (editBuf->pos < editBuf->len && !isspace((int) editBuf->data[editBuf->pos])) editBuf->pos++; update = TRUE; break; case KEY_HOME: nn_editbuf_setpos(editBuf, 0); update = TRUE; break; case KEY_END: nn_editbuf_setpos(editBuf, editBuf->len); update = TRUE; break; case KEY_LEFT: nn_editbuf_setpos(editBuf, editBuf->pos - 1); update = TRUE; break; case KEY_RIGHT: nn_editbuf_setpos(editBuf, editBuf->pos + 1); update = TRUE; break; case KEY_BACKSPACE: case 0x08: case 0x7f: nn_editbuf_delete(editBuf, editBuf->pos - 1); nn_editbuf_setpos(editBuf, editBuf->pos - 1); update = TRUE; break; case KEY_DC: /* Delete character */ nn_editbuf_delete(editBuf, editBuf->pos); update = TRUE; break; case KEY_IC: /* Ins = Toggle insert / overwrite mode */ insertMode = !insertMode; update = TRUE; break; case KEY_F(2): /* F2 = Clear editbuffer */ nn_editbuf_clear(editBuf); update = TRUE; break; case KEY_F(5): /* F5 = Ignore mode */ setIgnoreMode = !setIgnoreMode; printMsg(currWin, "Ignore mode = %s\n", setIgnoreMode ? "ON" : "OFF"); break; case 0x03: /* ^C = quit */ case KEY_F(9): /* F9 = Quit */ printMsg(currWin, "Quitting per user request.\n"); exitProg = TRUE; break; case 0x09: /* Tab = complete username */ performTabCompletion(editBuf); update = TRUE; break; case 0x0c: /* Ctrl + L */ updateWindows(); update = TRUE; updateMain = TRUE; break; case KEY_NPAGE: case KEY_PPAGE: if (currWin != NULL) { int numLines, numCols, oldPos = currWin->pos; getmaxyx(mainWin, numLines, numCols); numLines = (numLines / 2) + 1; if (c == KEY_PPAGE) currWin->pos = (currWin->pos > numLines) ? currWin->pos - numLines : 0; else currWin->pos = (currWin->pos < currWin->data->n - numLines) ? currWin->pos + numLines : currWin->data->n - numLines; if (oldPos != currWin->pos) updateMain = TRUE; } break; case ERR: /* Ignore */ break; default: if (isprint(c) || c == 0xe4 || c == 0xf6 || c == 0xc4 || c == 0xd6) { if (insertMode) nn_editbuf_insert(editBuf, editBuf->pos, c); else nn_editbuf_write(editBuf, editBuf->pos, c); nn_editbuf_setpos(editBuf, editBuf->pos + 1); update = TRUE; } else { printMsg(currWin, "Unhandled key: 0x%02x\n", c); } break; } } while (c != ERR && !exitProg && ++cnt < 10); if (update || firstUpdate) { /* Update edit line */ printEditBuf(editBuf); updateStatus(); firstUpdate = FALSE; /* a nasty hack ... */ } updateMainWin(updateMain); } /* cursesInit */ if (++updateCount > 10) { time_t tmpTime = time(NULL); if (tmpTime - prevTime > SET_KEEPALIVE) { int n = random() % th_llist_length(setIdleMessages); qlist_t *node = th_llist_get_nth(setIdleMessages, n); nn_conn_send_msg(conn, optUserNameEnc, node->data); prevTime = tmpTime; } if (!colorSet) { colorSet = TRUE; nn_conn_send_msg(conn, optUserNameEnc, "%%2FSetFontColor%%20%%2Dcolor%%20%06X", optUserColor); } updateStatus(); updateCount = 0; } } /* Shutdown */ err_exit: nn_userhash_free(nnUsers); nn_editbuf_free(editBuf); { int i; for (i = 0; i <= SET_MAX_HISTORY; i++) nn_editbuf_free(histBuf[i]); for (i = 0; i < SET_MAX_WINDOWS; i++) nn_window_free(chatWindows[i]); } #ifdef __WIN32 if (errorMessages) { char *tmp; wclear(editWin); tmp = promptRequester(editWin, "Press enter to quit.\n", FALSE); th_free(tmp); } #endif if (cursesInit) { if (curVis != ERR) curs_set(curVis); closeWindows(); endwin(); THMSG(1, "NCurses deinitialized.\n"); } #ifndef __WIN32 if (errorMessages) THERR("%s", errorMessages); #endif th_free(optUserNameEnc); nn_conn_close(conn); if (networkInit) nn_network_close(); THMSG(1, "Connection terminated.\n"); logFileClose(); return 0; }