Download
jcs
/subtext
/chat.c
(View History)
jcs session+chat: Finally implement sysop paging/answering | Latest amendment: 477 on 2023-04-10 |
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 | |
82 | pbuf_line = chat_pbuf; |
83 | max_llen = s->terminal_columns; |
84 | |
85 | while (plen > 0) { |
86 | if (plen > max_llen) { |
87 | /* chop at last word boundary */ |
88 | llen = max_llen; |
89 | while (llen >= 0 && pbuf_line[llen] != ' ') |
90 | llen--; |
91 | if (llen == -1) |
92 | /* no words to break on, hard break */ |
93 | llen = max_llen; |
94 | } else { |
95 | llen = plen; |
96 | } |
97 | |
98 | if (around_bar) |
99 | final_len = strlcat(chat_final_pbuf, |
100 | ansi(s, ANSI_CURSOR_LINE_COL, 1, 1, |
101 | ANSI_DELETE_LINES_N, 1, |
102 | ANSI_CURSOR_LINE_COL, s->terminal_lines - 2, 1, |
103 | ANSI_INSERT_LINES_N, 1, |
104 | ANSI_END), |
105 | sizeof(chat_final_pbuf)); |
106 | |
107 | if (pbuf_line != chat_pbuf) { |
108 | if (!around_bar) |
109 | strlcat(chat_final_pbuf, "\r\n", sizeof(chat_final_pbuf)); |
110 | final_len = strlcat(chat_final_pbuf, " ", |
111 | sizeof(chat_final_pbuf)); |
112 | } |
113 | |
114 | /* manually copy since pbuf_line+llen is not null terminated */ |
115 | if (final_len + llen + 1 > sizeof(chat_final_pbuf)) |
116 | break; |
117 | memcpy(chat_final_pbuf + final_len, pbuf_line, llen); |
118 | chat_final_pbuf[final_len + llen] = '\0'; |
119 | |
120 | pbuf_line += llen; |
121 | plen -= llen; |
122 | } |
123 | |
124 | if (around_bar) |
125 | final_len = strlcat(chat_final_pbuf, |
126 | ansi(s, ANSI_RESTORE_SAVED_CURSOR, ANSI_END), |
127 | sizeof(chat_final_pbuf)); |
128 | else |
129 | final_len = strlcat(chat_final_pbuf, "\r\n", |
130 | sizeof(chat_final_pbuf)); |
131 | |
132 | if (final_len > sizeof(chat_final_pbuf)) |
133 | panic("chat_final_pbuf overflow! %ld", final_len); |
134 | |
135 | /* |
136 | * If this session's output buffer is too full to take this, kick them |
137 | * out of chat |
138 | */ |
139 | if (s->obuflen + final_len > sizeof(s->obuf)) { |
140 | s->chatting = 0; |
141 | s->abort_input = 1; |
142 | return 0; |
143 | } |
144 | |
145 | memcpy(s->obuf + s->obuflen, chat_final_pbuf, final_len); |
146 | s->obuflen += final_len; |
147 | |
148 | return final_len; |
149 | } |
150 | |
151 | void |
152 | chat_start(struct session *s, char *with_node) |
153 | { |
154 | static char chat_bar[255]; |
155 | char chatting_with[DB_USERNAME_LENGTH + 1]; |
156 | size_t len, n; |
157 | char *input; |
158 | bool lagged = false; |
159 | |
160 | chatting_with[0] = '\0'; |
161 | s->chatting_with_node[0] = '\0'; |
162 | |
163 | if (with_node && strcmp(with_node, CHAT_WITH_SYSOP) == 0) { |
164 | strlcpy(s->chatting_with_node, CHAT_WITH_SYSOP, |
165 | sizeof(s->chatting_with_node)); |
166 | strlcpy(chatting_with, "sysop", sizeof(chatting_with)); |
167 | } else if (with_node) { |
168 | for (n = 0; n < MAX_SESSIONS; n++) { |
169 | if (sessions[n] == NULL || !sessions[n]->logged_in) |
170 | continue; |
171 | if (strcmp(sessions[n]->node, with_node) == 0) { |
172 | strlcpy(chatting_with, sessions[n]->chat_username, |
173 | sizeof(chatting_with)); |
174 | break; |
175 | } |
176 | } |
177 | |
178 | if (chatting_with[0] == '\0') { |
179 | session_logf(s, "Tried to enter chat with node %s but no " |
180 | "user logged in there", with_node); |
181 | session_printf(s, "Error: Couldn't find user on node %s", |
182 | with_node); |
183 | return; |
184 | } |
185 | |
186 | strlcpy(s->chatting_with_node, with_node, |
187 | sizeof(s->chatting_with_node)); |
188 | } else { |
189 | strlcpy(s->chatting_with_node, CHAT_WITH_ALL, |
190 | sizeof(s->chatting_with_node)); |
191 | } |
192 | |
193 | s->chatting = true; |
194 | |
195 | session_logf(s, "Entered chat with %s", chatting_with[0] ? |
196 | chatting_with : "everyone"); |
197 | |
198 | session_printf(s, |
199 | ansi(s, ANSI_CURSOR_LINE_COL, s->terminal_lines, 1, ANSI_END)); |
200 | |
201 | chat_printf_line(s, 0, "%sWelcome to Multi-User Chat%s", |
202 | ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); |
203 | chat_printf_line(s, 0, "Type %s/help%s for available commands", |
204 | ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); |
205 | session_flush(s); |
206 | |
207 | len = snprintf(chat_bar, sizeof(chat_bar), |
208 | " [ %s ] [ Chatting with %s ] ", |
209 | s->chat_username, chatting_with[0] ? chatting_with : "everyone"); |
210 | |
211 | while (len < sizeof(chat_bar) && len < s->terminal_columns) { |
212 | chat_bar[len - 1] = ' '; |
213 | chat_bar[len] = '\0'; |
214 | len++; |
215 | } |
216 | |
217 | session_printf(s, "\r%s", |
218 | ansi(s, ANSI_REVERSE, ANSI_ERASE_LINE, ANSI_END)); |
219 | session_output(s, chat_bar, len); |
220 | session_printf(s, "%s\r\n", |
221 | ansi(s, ANSI_RESET, ANSI_END)); |
222 | session_flush(s); |
223 | |
224 | snprintf(chat_tbuf, sizeof(chat_tbuf), |
225 | "*** %s%s has joined chat", |
226 | s->chat_username, s->user && s->user->is_sysop ? " (sysop)" : ""); |
227 | chat_broadcast(s, chat_tbuf); |
228 | session_flush(s); |
229 | |
230 | chat_who(s); |
231 | |
232 | if (strcmp(s->chatting_with_node, CHAT_WITH_SYSOP) == 0) { |
233 | snprintf(chat_tbuf, sizeof(chat_tbuf), |
234 | "*** Waiting for sysop to answer page..."); |
235 | chat_broadcast(s, chat_tbuf); |
236 | } |
237 | |
238 | for (;;) { |
239 | input = session_field_input(s, CHAT_MAX_INPUT - 1, |
240 | s->terminal_columns - 1, NULL, false, 0); |
241 | if (!input) |
242 | break; |
243 | session_printf(s, |
244 | ansi(s, ANSI_COL_N, 1, ANSI_ERASE_LINE, ANSI_END)); |
245 | session_flush(s); |
246 | |
247 | if (input[0] == '/') { |
248 | if (strcmp(input, "/quit") == 0 || |
249 | strcmp(input, "/exit") == 0 || |
250 | strcmp(input, "/leave") == 0) { |
251 | xfree(&input); |
252 | break; |
253 | } else if (strcmp(input, "/help") == 0) { |
254 | chat_help(s); |
255 | } else if (strncmp(input, "/msg ", 5) == 0 || |
256 | strcmp(input, "/msg") == 0) { |
257 | chat_privmsg(s, input + 5); |
258 | session_flush(s); |
259 | } else if (strcmp(input, "/who") == 0) { |
260 | chat_who(s); |
261 | } else { |
262 | chat_printf_line(s, 1, "*** Unknown command: %s", |
263 | input + 1); |
264 | session_flush(s); |
265 | } |
266 | } else { |
267 | snprintf(chat_tbuf, sizeof(chat_tbuf), "<%s> %s", |
268 | s->chat_username, input); |
269 | chat_broadcast(s, chat_tbuf); |
270 | session_flush(s); |
271 | } |
272 | |
273 | xfree(&input); |
274 | } |
275 | |
276 | session_logf(s, "Left chat with %s", chatting_with[0] ? |
277 | chatting_with : "everyone"); |
278 | |
279 | snprintf(chat_tbuf, sizeof(chat_tbuf), |
280 | "*** %s%s has left chat", |
281 | s->chat_username, s->user && s->user->is_sysop ? " (sysop)" : ""); |
282 | |
283 | if (s->ending) |
284 | strlcat(chat_tbuf, " (disconnected)", sizeof(chat_tbuf)); |
285 | else if (s->abort_input) |
286 | strlcat(chat_tbuf, " (too lagged)", sizeof(chat_tbuf)); |
287 | chat_broadcast(s, chat_tbuf); |
288 | session_flush(s); |
289 | |
290 | if (s->abort_input && !s->chatting) |
291 | lagged = true; |
292 | |
293 | s->chatting = false; |
294 | memset(s->chatting_with_node, 0, sizeof(s->chatting_with_node)); |
295 | |
296 | /* clear chat bar */ |
297 | session_printf(s, ansi(s, ANSI_CURSOR_LINE_COL, |
298 | s->terminal_lines - 1, 1, ANSI_ERASE_LINE, ANSI_END)); |
299 | |
300 | if (lagged) { |
301 | /* |
302 | * We were kicked out for being too lagged, so we didn't see |
303 | * that last broadcast; show it once we've flushed our output on |
304 | * our own time. |
305 | */ |
306 | session_printf(s, "%s\r\n", chat_tbuf); |
307 | session_flush(s); |
308 | } |
309 | |
310 | session_output(s, "\r\n", 2); |
311 | session_flush(s); |
312 | } |
313 | |
314 | void |
315 | chat_help(struct session *s) |
316 | { |
317 | chat_printf_line(s, 1, "*** Available chat commands:"); |
318 | chat_printf_line(s, 1, "*** %s/quit%s : Leave chat", |
319 | ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); |
320 | chat_printf_line(s, 1, "*** %s/msg user message%s : " |
321 | "Send \"message\" only to user \"user\"", |
322 | ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); |
323 | chat_printf_line(s, 1, "*** %s/who%s : " |
324 | "List users in this chat", |
325 | ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); |
326 | session_flush(s); |
327 | } |
328 | |
329 | void |
330 | chat_who(struct session *s) |
331 | { |
332 | short n, printed = 0; |
333 | size_t len = 0; |
334 | |
335 | for (n = 0; n < MAX_SESSIONS; n++) { |
336 | if (sessions[n] == NULL || !sessions[n]->logged_in || |
337 | !sessions[n]->chatting) |
338 | continue; |
339 | if (strcmp(s->chatting_with_node, |
340 | sessions[n]->chatting_with_node) != 0 && |
341 | strcmp(sessions[n]->chatting_with_node, s->node) != 0) |
342 | continue; |
343 | |
344 | len += snprintf(chat_tbuf + len, sizeof(chat_tbuf), "[ %c%-16s ]", |
345 | sessions[n]->user && sessions[n]->user->is_sysop ? '@' : ' ', |
346 | sessions[n]->chat_username); |
347 | |
348 | if (++printed == 3) { |
349 | chat_printf_line(s, 1, chat_tbuf); |
350 | printed = 0; |
351 | len = 0; |
352 | } |
353 | } |
354 | |
355 | if (printed) |
356 | chat_printf_line(s, 1, chat_tbuf); |
357 | } |
358 | |
359 | void |
360 | chat_privmsg(struct session *s, char *user_and_msg) |
361 | { |
362 | char *username = NULL; |
363 | size_t n; |
364 | bool found = false; |
365 | |
366 | for (n = 0; user_and_msg[n] != '\0'; n++) { |
367 | if (user_and_msg[n] == ' ') { |
368 | user_and_msg[n] = '\0'; |
369 | |
370 | username = user_and_msg; |
371 | user_and_msg = user_and_msg + n + 1; |
372 | break; |
373 | } |
374 | } |
375 | |
376 | if (username == NULL || user_and_msg[0] == '\0') { |
377 | chat_printf_line(s, 1, "*** Usage: /msg user message"); |
378 | session_flush(s); |
379 | return; |
380 | } |
381 | |
382 | for (n = 0; n < MAX_SESSIONS; n++) { |
383 | if (sessions[n] == NULL || !sessions[n]->logged_in || |
384 | !sessions[n]->chatting) |
385 | continue; |
386 | if (strcmp(sessions[n]->chat_username, username) != 0) |
387 | continue; |
388 | |
389 | chat_printf_line(sessions[n], 1, "*[priv] %s* %s", |
390 | s->chat_username, user_and_msg); |
391 | found = true; |
392 | } |
393 | |
394 | if (!found) { |
395 | chat_printf_line(s, 1, "*** User %s does not exist or " |
396 | "is not chatting", username); |
397 | session_flush(s); |
398 | return; |
399 | } |
400 | |
401 | chat_printf_line(s, 1, "*-> %s* %s", username, user_and_msg); |
402 | session_flush(s); |
403 | } |