/* * Copyright (c) 2021 joshua stein * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include "ansi.h" #include "chat.h" #include "db.h" #include "session.h" #include "user.h" #include "util.h" #define CHAT_MAX_INPUT 80 static char chat_tbuf[256]; void chat_output_bar(struct session *s, char *left_str, char *right_str); void chat_help(struct session *s); void chat_who(struct session *s); void chat_privmsg(struct session *s, char *user_and_msg); void chat_broadcast(struct session *s, char *str) { short n; for (n = 0; n < MAX_SESSIONS; n++) { if (sessions[n] == NULL || !sessions[n]->logged_in || !sessions[n]->chatting) continue; if (strcmp(s->chatting_with_node, sessions[n]->chatting_with_node) != 0 && strcmp(sessions[n]->chatting_with_node, s->node) != 0) continue; chat_printf_line(sessions[n], 1, "%s", str); } } size_t chat_printf_line(struct session *s, short around_bar, char *format, ...) { static char chat_pbuf[256]; static char chat_final_pbuf[512]; char *pbuf_line; va_list ap; struct tm *tm; size_t final_len = 0, plen, max_llen; ssize_t llen; if (around_bar) final_len = strlcpy(chat_final_pbuf, ansi(s, ANSI_SAVE_CURSOR, ANSI_END), sizeof(chat_final_pbuf)); /* prepend time */ tm = localtime((time_t *)&Time); plen = strftime(chat_pbuf, sizeof(chat_pbuf), "[%H:%M] ", tm); /* append orignal line */ va_start(ap, format); plen += vsnprintf(chat_pbuf + plen, sizeof(chat_pbuf) - plen, format, ap); va_end(ap); pbuf_line = chat_pbuf; max_llen = s->terminal_columns; while (plen > 0) { if (plen > max_llen) { /* chop at last word boundary */ llen = max_llen; while (llen >= 0 && pbuf_line[llen] != ' ') llen--; if (llen == -1) /* no words to break on, hard break */ llen = max_llen; } else { llen = plen; } if (around_bar) final_len = strlcat(chat_final_pbuf, ansi(s, ANSI_CURSOR_LINE_COL, 1, 1, ANSI_DELETE_LINES_N, 1, ANSI_CURSOR_LINE_COL, s->terminal_lines - 2, 1, ANSI_INSERT_LINES_N, 1, ANSI_END), sizeof(chat_final_pbuf)); if (pbuf_line != chat_pbuf) { if (!around_bar) strlcat(chat_final_pbuf, "\r\n", sizeof(chat_final_pbuf)); final_len = strlcat(chat_final_pbuf, " ", sizeof(chat_final_pbuf)); } /* manually copy since pbuf_line+llen is not null terminated */ if (final_len + llen + 1 > sizeof(chat_final_pbuf)) break; memcpy(chat_final_pbuf + final_len, pbuf_line, llen); chat_final_pbuf[final_len + llen] = '\0'; pbuf_line += llen; plen -= llen; } if (around_bar) final_len = strlcat(chat_final_pbuf, ansi(s, ANSI_RESTORE_SAVED_CURSOR, ANSI_END), sizeof(chat_final_pbuf)); else final_len = strlcat(chat_final_pbuf, "\r\n", sizeof(chat_final_pbuf)); if (final_len > sizeof(chat_final_pbuf)) panic("chat_final_pbuf overflow! %ld", final_len); /* * If this session's output buffer is too full to take this, kick them * out of chat */ if (s->obuflen + final_len > sizeof(s->obuf)) { s->chatting = 0; s->abort_input = 1; return 0; } memcpy(s->obuf + s->obuflen, chat_final_pbuf, final_len); s->obuflen += final_len; return final_len; } void chat_start(struct session *s, char *with_node) { static char chat_bar[255]; char chatting_with[DB_USERNAME_LENGTH + 1]; size_t len, n; char *input; bool lagged = false; chatting_with[0] = '\0'; s->chatting_with_node[0] = '\0'; if (with_node && strcmp(with_node, CHAT_WITH_SYSOP) == 0) { strlcpy(s->chatting_with_node, CHAT_WITH_SYSOP, sizeof(s->chatting_with_node)); strlcpy(chatting_with, "sysop", sizeof(chatting_with)); } else if (with_node) { for (n = 0; n < MAX_SESSIONS; n++) { if (sessions[n] == NULL || !sessions[n]->logged_in) continue; if (strcmp(sessions[n]->node, with_node) == 0) { strlcpy(chatting_with, sessions[n]->chat_username, sizeof(chatting_with)); break; } } if (chatting_with[0] == '\0') { session_logf(s, "Tried to enter chat with node %s but no " "user logged in there", with_node); session_printf(s, "Error: Couldn't find user on node %s", with_node); return; } strlcpy(s->chatting_with_node, with_node, sizeof(s->chatting_with_node)); } else { strlcpy(s->chatting_with_node, CHAT_WITH_ALL, sizeof(s->chatting_with_node)); } s->chatting = true; session_logf(s, "Entered chat with %s", chatting_with[0] ? chatting_with : "everyone"); session_printf(s, ansi(s, ANSI_CURSOR_LINE_COL, s->terminal_lines, 1, ANSI_END)); chat_printf_line(s, 0, "%sWelcome to Multi-User Chat%s", ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); chat_printf_line(s, 0, "Type %s/help%s for available commands", ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); session_flush(s); len = snprintf(chat_bar, sizeof(chat_bar), " [ %s ] [ Chatting with %s ] ", s->chat_username, chatting_with[0] ? chatting_with : "everyone"); while (len < sizeof(chat_bar) && len < s->terminal_columns) { chat_bar[len - 1] = ' '; chat_bar[len] = '\0'; len++; } session_printf(s, "\r%s", ansi(s, ANSI_REVERSE, ANSI_ERASE_LINE, ANSI_END)); session_output(s, chat_bar, len); session_printf(s, "%s\r\n", ansi(s, ANSI_RESET, ANSI_END)); session_flush(s); snprintf(chat_tbuf, sizeof(chat_tbuf), "*** %s%s has joined chat", s->chat_username, s->user && s->user->is_sysop ? " (sysop)" : ""); chat_broadcast(s, chat_tbuf); session_flush(s); chat_who(s); if (strcmp(s->chatting_with_node, CHAT_WITH_SYSOP) == 0) { snprintf(chat_tbuf, sizeof(chat_tbuf), "*** Waiting for sysop to answer page..."); chat_broadcast(s, chat_tbuf); } for (;;) { input = session_field_input(s, CHAT_MAX_INPUT - 1, s->terminal_columns - 1, NULL, false, 0); if (!input) break; session_printf(s, ansi(s, ANSI_COL_N, 1, ANSI_ERASE_LINE, ANSI_END)); session_flush(s); if (input[0] == '/') { if (strcmp(input, "/quit") == 0 || strcmp(input, "/exit") == 0 || strcmp(input, "/leave") == 0) { xfree(&input); break; } else if (strcmp(input, "/help") == 0) { chat_help(s); } else if (strncmp(input, "/msg ", 5) == 0 || strcmp(input, "/msg") == 0) { chat_privmsg(s, input + 5); session_flush(s); } else if (strcmp(input, "/who") == 0) { chat_who(s); } else { chat_printf_line(s, 1, "*** Unknown command: %s", input + 1); session_flush(s); } } else { snprintf(chat_tbuf, sizeof(chat_tbuf), "<%s> %s", s->chat_username, input); chat_broadcast(s, chat_tbuf); session_flush(s); } xfree(&input); } session_logf(s, "Left chat with %s", chatting_with[0] ? chatting_with : "everyone"); snprintf(chat_tbuf, sizeof(chat_tbuf), "*** %s%s has left chat", s->chat_username, s->user && s->user->is_sysop ? " (sysop)" : ""); if (s->ending) strlcat(chat_tbuf, " (disconnected)", sizeof(chat_tbuf)); else if (s->abort_input) strlcat(chat_tbuf, " (too lagged)", sizeof(chat_tbuf)); chat_broadcast(s, chat_tbuf); session_flush(s); if (s->abort_input && !s->chatting) lagged = true; s->chatting = false; memset(s->chatting_with_node, 0, sizeof(s->chatting_with_node)); /* clear chat bar */ session_printf(s, ansi(s, ANSI_CURSOR_LINE_COL, s->terminal_lines - 1, 1, ANSI_ERASE_LINE, ANSI_END)); if (lagged) { /* * We were kicked out for being too lagged, so we didn't see * that last broadcast; show it once we've flushed our output on * our own time. */ session_printf(s, "%s\r\n", chat_tbuf); session_flush(s); } session_output(s, "\r\n", 2); session_flush(s); } void chat_help(struct session *s) { chat_printf_line(s, 1, "*** Available chat commands:"); chat_printf_line(s, 1, "*** %s/quit%s : Leave chat", ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); chat_printf_line(s, 1, "*** %s/msg user message%s : " "Send \"message\" only to user \"user\"", ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); chat_printf_line(s, 1, "*** %s/who%s : " "List users in this chat", ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); session_flush(s); } void chat_who(struct session *s) { short n, printed = 0; size_t len = 0; for (n = 0; n < MAX_SESSIONS; n++) { if (sessions[n] == NULL || !sessions[n]->logged_in || !sessions[n]->chatting) continue; if (strcmp(s->chatting_with_node, sessions[n]->chatting_with_node) != 0 && strcmp(sessions[n]->chatting_with_node, s->node) != 0) continue; len += snprintf(chat_tbuf + len, sizeof(chat_tbuf), "[ %c%-16s ]", sessions[n]->user && sessions[n]->user->is_sysop ? '@' : ' ', sessions[n]->chat_username); if (++printed == 3) { chat_printf_line(s, 1, chat_tbuf); printed = 0; len = 0; } } if (printed) chat_printf_line(s, 1, chat_tbuf); } void chat_privmsg(struct session *s, char *user_and_msg) { char *username = NULL; size_t n; bool found = false; for (n = 0; user_and_msg[n] != '\0'; n++) { if (user_and_msg[n] == ' ') { user_and_msg[n] = '\0'; username = user_and_msg; user_and_msg = user_and_msg + n + 1; break; } } if (username == NULL || user_and_msg[0] == '\0') { chat_printf_line(s, 1, "*** Usage: /msg user message"); session_flush(s); return; } for (n = 0; n < MAX_SESSIONS; n++) { if (sessions[n] == NULL || !sessions[n]->logged_in || !sessions[n]->chatting) continue; if (strcmp(sessions[n]->chat_username, username) != 0) continue; chat_printf_line(sessions[n], 1, "*[priv] %s* %s", s->chat_username, user_and_msg); found = true; } if (!found) { chat_printf_line(s, 1, "*** User %s does not exist or " "is not chatting", username); session_flush(s); return; } chat_printf_line(s, 1, "*-> %s* %s", username, user_and_msg); session_flush(s); }