AmendHub

Download

jcs

/

subtext

/

chat.c

 

(View History)

jcs   chat: Fix corner cases, pass buf as string instead of format Latest amendment: 635 on 2026-09-18

1 /*
2 * Copyright (c) 2021 joshua stein <jcs@jcs.org>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17 #include <time.h>
18 #include <stdarg.h>
19 #include <stdio.h>
20 #include <string.h>
21
22 #include "ansi.h"
23 #include "chat.h"
24 #include "db.h"
25 #include "session.h"
26 #include "user.h"
27 #include "util.h"
28
29 #define CHAT_MAX_INPUT 80
30
31 static char chat_tbuf[256];
32
33 void chat_output_bar(struct session *s, char *left_str, char *right_str);
34 void chat_help(struct session *s);
35 void chat_who(struct session *s);
36 void chat_privmsg(struct session *s, char *user_and_msg);
37
38 void
39 chat_broadcast(struct session *s, char *str)
40 {
41 short n;
42
43 for (n = 0; n < MAX_SESSIONS; n++) {
44 if (sessions[n] == NULL || !sessions[n]->logged_in ||
45 !sessions[n]->chatting)
46 continue;
47 if (strcmp(s->chatting_with_node,
48 sessions[n]->chatting_with_node) != 0 &&
49 strcmp(sessions[n]->chatting_with_node, s->node) != 0)
50 continue;
51
52 chat_printf_line(sessions[n], 1, "%s", str);
53 }
54 }
55
56 size_t
57 chat_printf_line(struct session *s, short around_bar, char *format, ...)
58 {
59 static char chat_pbuf[256];
60 static char chat_final_pbuf[512];
61 char *pbuf_line;
62 va_list ap;
63 struct tm *tm;
64 size_t final_len = 0, plen, max_llen;
65 ssize_t llen;
66
67 if (around_bar)
68 final_len = strlcpy(chat_final_pbuf,
69 ansi(s, ANSI_SAVE_CURSOR, ANSI_END),
70 sizeof(chat_final_pbuf));
71
72 /* prepend time */
73 tm = localtime((time_t *)&Time);
74 plen = strftime(chat_pbuf, sizeof(chat_pbuf), "[%H:%M] ", tm);
75
76 /* append orignal line */
77 va_start(ap, format);
78 plen += vsnprintf(chat_pbuf + plen, sizeof(chat_pbuf) - plen, format,
79 ap);
80 va_end(ap);
81 if (plen >= sizeof(chat_pbuf))
82 plen = sizeof(chat_pbuf) - 1;
83
84 pbuf_line = chat_pbuf;
85 max_llen = s->terminal_columns;
86 if (max_llen < MIN_TERMINAL_COLUMNS)
87 max_llen = MIN_TERMINAL_COLUMNS;
88
89 while (plen > 0) {
90 if (plen > max_llen) {
91 /* chop at last word boundary */
92 llen = max_llen;
93 while (llen > 0 && pbuf_line[llen] != ' ')
94 llen--;
95 if (llen == 0)
96 /* no words to break on, hard break */
97 llen = max_llen;
98 } else {
99 llen = plen;
100 }
101
102 if (around_bar)
103 final_len = strlcat(chat_final_pbuf,
104 ansi(s, ANSI_CURSOR_LINE_COL, 1, 1,
105 ANSI_DELETE_LINES_N, 1,
106 ANSI_CURSOR_LINE_COL, s->terminal_lines - 2, 1,
107 ANSI_INSERT_LINES_N, 1,
108 ANSI_END),
109 sizeof(chat_final_pbuf));
110
111 if (pbuf_line != chat_pbuf) {
112 if (!around_bar)
113 strlcat(chat_final_pbuf, "\r\n", sizeof(chat_final_pbuf));
114 final_len = strlcat(chat_final_pbuf, " ",
115 sizeof(chat_final_pbuf));
116 }
117
118 /* manually copy since pbuf_line+llen is not null terminated */
119 if (final_len + llen + 1 < sizeof(chat_final_pbuf))
120 memcpy(chat_final_pbuf + final_len, pbuf_line, llen);
121 chat_final_pbuf[final_len + llen] = '\0';
122
123 pbuf_line += llen;
124 plen -= llen;
125 }
126
127 if (around_bar)
128 final_len = strlcat(chat_final_pbuf,
129 ansi(s, ANSI_RESTORE_SAVED_CURSOR, ANSI_END),
130 sizeof(chat_final_pbuf));
131 else
132 final_len = strlcat(chat_final_pbuf, "\r\n",
133 sizeof(chat_final_pbuf));
134
135 if (final_len > sizeof(chat_final_pbuf))
136 panic("chat_final_pbuf overflow! %ld", final_len);
137
138 /*
139 * If this session's output buffer is too full to take this, kick them
140 * out of chat
141 */
142 if (s->obuflen + final_len > sizeof(s->obuf)) {
143 s->chatting = 0;
144 s->abort_input = 1;
145 return 0;
146 }
147
148 memcpy(s->obuf + s->obuflen, chat_final_pbuf, final_len);
149 s->obuflen += final_len;
150
151 return final_len;
152 }
153
154 void
155 chat_start(struct session *s, char *with_node)
156 {
157 static char chat_bar[255];
158 char chatting_with[DB_USERNAME_LENGTH + 1];
159 size_t len, n;
160 char *input;
161 bool lagged = false;
162
163 chatting_with[0] = '\0';
164 s->chatting_with_node[0] = '\0';
165
166 if (with_node && strcmp(with_node, CHAT_WITH_SYSOP) == 0) {
167 strlcpy(s->chatting_with_node, CHAT_WITH_SYSOP,
168 sizeof(s->chatting_with_node));
169 strlcpy(chatting_with, "sysop", sizeof(chatting_with));
170 } else if (with_node) {
171 for (n = 0; n < MAX_SESSIONS; n++) {
172 if (sessions[n] == NULL || !sessions[n]->logged_in)
173 continue;
174 if (strcmp(sessions[n]->node, with_node) == 0) {
175 strlcpy(chatting_with, sessions[n]->chat_username,
176 sizeof(chatting_with));
177 break;
178 }
179 }
180
181 if (chatting_with[0] == '\0') {
182 session_logf(s, "Tried to enter chat with node %s but no "
183 "user logged in there", with_node);
184 session_printf(s, "Error: Couldn't find user on node %s",
185 with_node);
186 return;
187 }
188
189 strlcpy(s->chatting_with_node, with_node,
190 sizeof(s->chatting_with_node));
191 } else {
192 strlcpy(s->chatting_with_node, CHAT_WITH_ALL,
193 sizeof(s->chatting_with_node));
194 }
195
196 s->chatting = true;
197
198 session_logf(s, "Entered chat with %s", chatting_with[0] ?
199 chatting_with : "everyone");
200
201 session_printf(s,
202 ansi(s, ANSI_CURSOR_LINE_COL, s->terminal_lines, 1, ANSI_END));
203
204 chat_printf_line(s, 0, "%sWelcome to Multi-User Chat%s",
205 ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END));
206 chat_printf_line(s, 0, "Type %s/help%s for available commands",
207 ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END));
208 session_flush(s);
209
210 len = snprintf(chat_bar, sizeof(chat_bar),
211 " [ %s ] [ Chatting with %s ] ",
212 s->chat_username, chatting_with[0] ? chatting_with : "everyone");
213
214 while (len < sizeof(chat_bar) && len < s->terminal_columns) {
215 chat_bar[len - 1] = ' ';
216 chat_bar[len] = '\0';
217 len++;
218 }
219
220 session_printf(s, "\r%s",
221 ansi(s, ANSI_REVERSE, ANSI_ERASE_LINE, ANSI_END));
222 session_output(s, chat_bar, len);
223 session_printf(s, "%s\r\n",
224 ansi(s, ANSI_RESET, ANSI_END));
225 session_flush(s);
226
227 snprintf(chat_tbuf, sizeof(chat_tbuf),
228 "*** %s%s has joined chat",
229 s->chat_username, s->user && s->user->is_sysop ? " (sysop)" : "");
230 chat_broadcast(s, chat_tbuf);
231 session_flush(s);
232
233 chat_who(s);
234
235 if (strcmp(s->chatting_with_node, CHAT_WITH_SYSOP) == 0) {
236 snprintf(chat_tbuf, sizeof(chat_tbuf),
237 "*** Waiting for sysop to answer page...");
238 chat_broadcast(s, chat_tbuf);
239 }
240
241 for (;;) {
242 input = session_field_input(s, CHAT_MAX_INPUT - 1,
243 s->terminal_columns - 1, NULL, false, 0);
244 if (!input)
245 break;
246 session_printf(s,
247 ansi(s, ANSI_COL_N, 1, ANSI_ERASE_LINE, ANSI_END));
248 session_flush(s);
249
250 if (input[0] == '/') {
251 if (strcmp(input, "/quit") == 0 ||
252 strcmp(input, "/exit") == 0 ||
253 strcmp(input, "/leave") == 0) {
254 xfree(&input);
255 break;
256 } else if (strcmp(input, "/help") == 0) {
257 chat_help(s);
258 } else if (strncmp(input, "/msg ", 5) == 0 ||
259 strcmp(input, "/msg") == 0) {
260 chat_privmsg(s, input + 5);
261 session_flush(s);
262 } else if (strcmp(input, "/who") == 0) {
263 chat_who(s);
264 } else {
265 chat_printf_line(s, 1, "*** Unknown command: %s",
266 input + 1);
267 session_flush(s);
268 }
269 } else {
270 snprintf(chat_tbuf, sizeof(chat_tbuf), "<%s> %s",
271 s->chat_username, input);
272 chat_broadcast(s, chat_tbuf);
273 session_flush(s);
274 }
275
276 xfree(&input);
277 }
278
279 session_logf(s, "Left chat with %s", chatting_with[0] ?
280 chatting_with : "everyone");
281
282 snprintf(chat_tbuf, sizeof(chat_tbuf),
283 "*** %s%s has left chat",
284 s->chat_username, s->user && s->user->is_sysop ? " (sysop)" : "");
285
286 if (s->ending)
287 strlcat(chat_tbuf, " (disconnected)", sizeof(chat_tbuf));
288 else if (s->abort_input)
289 strlcat(chat_tbuf, " (too lagged)", sizeof(chat_tbuf));
290 chat_broadcast(s, chat_tbuf);
291 session_flush(s);
292
293 if (s->abort_input && !s->chatting)
294 lagged = true;
295
296 s->chatting = false;
297 memset(s->chatting_with_node, 0, sizeof(s->chatting_with_node));
298
299 /* clear chat bar */
300 session_printf(s, ansi(s, ANSI_CURSOR_LINE_COL,
301 s->terminal_lines - 1, 1, ANSI_ERASE_LINE, ANSI_END));
302
303 if (lagged) {
304 /*
305 * We were kicked out for being too lagged, so we didn't see
306 * that last broadcast; show it once we've flushed our output on
307 * our own time.
308 */
309 session_printf(s, "%s\r\n", chat_tbuf);
310 session_flush(s);
311 }
312
313 session_output(s, "\r\n", 2);
314 session_flush(s);
315 }
316
317 void
318 chat_help(struct session *s)
319 {
320 chat_printf_line(s, 1, "*** Available chat commands:");
321 chat_printf_line(s, 1, "*** %s/quit%s : Leave chat",
322 ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END));
323 chat_printf_line(s, 1, "*** %s/msg user message%s : "
324 "Send \"message\" only to user \"user\"",
325 ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END));
326 chat_printf_line(s, 1, "*** %s/who%s : "
327 "List users in this chat",
328 ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END));
329 session_flush(s);
330 }
331
332 void
333 chat_who(struct session *s)
334 {
335 short n, printed = 0;
336 size_t len = 0;
337
338 for (n = 0; n < MAX_SESSIONS; n++) {
339 if (sessions[n] == NULL || !sessions[n]->logged_in ||
340 !sessions[n]->chatting)
341 continue;
342 if (strcmp(s->chatting_with_node,
343 sessions[n]->chatting_with_node) != 0 &&
344 strcmp(sessions[n]->chatting_with_node, s->node) != 0)
345 continue;
346
347 len += snprintf(chat_tbuf + len, sizeof(chat_tbuf) - len,
348 "[ %c%-16s ]",
349 sessions[n]->user && sessions[n]->user->is_sysop ? '@' : ' ',
350 sessions[n]->chat_username);
351
352 if (len >= sizeof(chat_tbuf))
353 len = sizeof(chat_tbuf) - 1;
354
355 if (++printed == 3) {
356 chat_printf_line(s, 1, "%s", chat_tbuf);
357 printed = 0;
358 len = 0;
359 }
360 }
361
362 if (printed)
363 chat_printf_line(s, 1, "%s", chat_tbuf);
364 }
365
366 void
367 chat_privmsg(struct session *s, char *user_and_msg)
368 {
369 char *username = NULL;
370 size_t n;
371 bool found = false;
372
373 for (n = 0; user_and_msg[n] != '\0'; n++) {
374 if (user_and_msg[n] == ' ') {
375 user_and_msg[n] = '\0';
376
377 username = user_and_msg;
378 user_and_msg = user_and_msg + n + 1;
379 break;
380 }
381 }
382
383 if (username == NULL || user_and_msg[0] == '\0') {
384 chat_printf_line(s, 1, "*** Usage: /msg user message");
385 session_flush(s);
386 return;
387 }
388
389 for (n = 0; n < MAX_SESSIONS; n++) {
390 if (sessions[n] == NULL || !sessions[n]->logged_in ||
391 !sessions[n]->chatting)
392 continue;
393 if (strcmp(sessions[n]->chat_username, username) != 0)
394 continue;
395
396 chat_printf_line(sessions[n], 1, "*[priv] %s* %s",
397 s->chat_username, user_and_msg);
398 found = true;
399 }
400
401 if (!found) {
402 chat_printf_line(s, 1, "*** User %s does not exist or "
403 "is not chatting", username);
404 session_flush(s);
405 return;
406 }
407
408 chat_printf_line(s, 1, "*-> %s* %s", username, user_and_msg);
409 session_flush(s);
410 }