Mercurial > hg > nnchat
view main.c @ 642:c8e5949a8961
Use th-libs functions.
author | Matti Hamalainen <ccr@tnsp.org> |
---|---|
date | Wed, 07 Jan 2015 16:56:54 +0200 |
parents | 51dd01786d25 |
children | d6792ccefe2f |
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 */ static const th_optarg_t optList[] = { { 0, '?', "help", "Show this help and be so very much verbose that it almost hurts you", 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 }, }; static 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, 0); 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 (th_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 (th_strcasecmp(proto, "socks4") == 0) optProxyType = TH_PROXY_SOCKS4; else if (th_strcasecmp(proto, "socks4a") == 0) optProxyType = TH_PROXY_SOCKS4A; else if (th_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 (!th_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, 0); 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; }