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