AmendHub

Download

jcs

/

subtext

/

session.c

 

(View History)

jcs   session: Add support for {{center_XX}} template variable Latest amendment: 570 on 2023-11-28

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 <stdarg.h>
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <string.h>
21
22 #include "ansi.h"
23 #include "board.h"
24 #include "chat.h"
25 #include "folder.h"
26 #include "logger.h"
27 #include "mail.h"
28 #include "main_menu.h"
29 #include "subtext.h"
30 #include "session.h"
31 #include "settings.h"
32 #include "signup.h"
33 #include "sysop.h"
34 #include "user.h"
35 #include "user_settings.h"
36 #include "uthread.h"
37 #include "util.h"
38
39 #include "console.h"
40
41 #define OBUF_CANARY 0xaaaaaaaa
42 #define IBUF_CANARY 0xe38e38e3
43 #define OBUFLEN_CANARY 0x1c71c71c
44 #define IBUFLEN_CANARY 0x55555555
45
46 struct session *sessions[MAX_SESSIONS] = { 0 };
47 short nsessions = 0;
48
49 struct session_tally session_today_tally = { 0 };
50 static char session_log_tbuf[256];
51 static const char invalid_option_help[] =
52 "Invalid option (press {{B}}?{{/B}} for help)\r\n";
53
54 /* must match session_area order */
55 static const char area_labels[][12] = {
56 "Main Menu",
57 "MOTD",
58 "Boards",
59 "FTN Boards",
60 "Chat",
61 "Mail",
62 "Files",
63 "Who",
64 "Recents",
65 "Settings",
66 "Signup"
67 };
68
69 void session_run(struct uthread *uthread, void *arg);
70 void session_print_menu(struct session *session, bool short_menu);
71 short session_login(struct session *s);
72 size_t session_log(struct session *session, const char *str);
73 size_t session_vprintf(struct session *session, const char *format,
74 va_list ap);
75 size_t session_expand_var(struct session *session, char *ivar, char **ret,
76 bool *end_expansion);
77 struct session * session_first_waiting_for_sysop(void);
78 void session_page_sysop(struct session *s);
79 void session_answer_page(struct session *s);
80 void sysop_edit_settings(struct session *s);
81 bool session_idled_out(struct session *session);
82
83 struct session *
84 session_create(char *node, char *via, struct node_funcs *node_funcs)
85 {
86 struct session *session;
87 size_t n;
88
89 /* reserve 1 session for the console */
90 if (nsessions >= MAX_SESSIONS - 1) {
91 if (strcmp(node, "console") != 0)
92 return NULL;
93
94 /* but only 1 */
95 if (nsessions == MAX_SESSIONS)
96 return NULL;
97 }
98
99 session = xmalloczero(sizeof(struct session));
100 if (session == NULL)
101 return NULL;
102
103 session->node_funcs = node_funcs;
104 strlcpy(session->node, node, sizeof(session->node));
105 strlcpy(session->log.node, node, sizeof(session->log.node));
106 strlcpy(session->via, via, sizeof(session->via));
107 strlcpy(session->location, "Unknown", sizeof(session->location));
108 strlcpy(session->log.location, session->location,
109 sizeof(session->log.location));
110 session->last_input_at = session->established_at = Time;
111
112 session->obuf_canary = OBUF_CANARY;
113 session->ibuf_canary = IBUF_CANARY;
114 session->obuflen_canary = OBUFLEN_CANARY;
115 session->ibuflen_canary = IBUFLEN_CANARY;
116
117 for (n = 0; n < MAX_SESSIONS; n++) {
118 if (sessions[n] != NULL)
119 continue;
120
121 session->uthread = uthread_add(session_run, session);
122 if (!session->uthread) {
123 xfree(&session);
124 return NULL;
125 }
126
127 sessions[n] = session;
128 nsessions++;
129 return session;
130 }
131
132 /* no room, but how did we get here? */
133 xfree(&session);
134 return NULL;
135 }
136
137 void
138 session_run(struct uthread *uthread, void *arg)
139 {
140 struct session *s = (struct session *)arg;
141 struct session *waiting;
142 char date[9];
143 struct tm *date_tm;
144 unsigned short c, last_c = 0;
145 bool done = false;
146 short auth, i, action, sc;
147
148 /* until we negotiate otherwise */
149 s->terminal_columns = DEFAULT_TERMINAL_COLUMNS;
150 s->terminal_lines = DEFAULT_TERMINAL_LINES;
151 s->area = SESSION_AREA_MAIN_MENU;
152
153 if (main_menu_options == NULL)
154 panic("No main menu!");
155
156 if (s->node_funcs->setup) {
157 s->node_funcs->setup(s);
158
159 if (s->ending) {
160 session_close(s);
161 return;
162 }
163 }
164
165 session_output_view_or_printf(s, DB_VIEW_ISSUE,
166 "\r\nWelcome to %s (%s)\r\n\r\n", db->config.name, s->node);
167 session_flush(s);
168 uthread_yield();
169
170 auth = session_login(s);
171 if (auth == AUTH_USER_FAILED) {
172 session_close(s);
173 return;
174 }
175
176 s->logged_in = true;
177 logger_update_title();
178
179 /* update session log */
180 if (s->user) {
181 s->user->last_seen_at = Time;
182 user_save(s->user);
183
184 strlcpy(s->log.username, s->user->username,
185 sizeof(s->log.username));
186 strlcpy(s->chat_username, s->user->username,
187 sizeof(s->chat_username));
188 } else {
189 strlcpy(s->log.username, GUEST_USERNAME, sizeof(s->log.username));
190 /* guest + ttyt0 -> guest[t0] */
191 snprintf(s->chat_username, sizeof(s->chat_username), "%s[%s]",
192 GUEST_USERNAME, s->node + 3);
193 }
194
195 strlcpy(s->log.via, s->via, sizeof(s->log.via));
196 s->log.tspeed = s->tspeed;
197 s->log.logged_on_at = Time;
198 s->log.id = bile_next_id(db->sessions_bile, SL_LOG_RTYPE);
199
200 if (bile_write(db->sessions_bile, SL_LOG_RTYPE, s->log.id, &s->log,
201 sizeof(s->log)) != sizeof(s->log))
202 panic("bile_write of session log failed: %d",
203 bile_error(db->sessions_bile));
204
205 /* update call tally for today */
206 date_tm = localtime((time_t *)&Time);
207 strftime(date, sizeof(date), "%Y%m%d", date_tm);
208 if (strcmp(date, session_today_tally.date) != 0) {
209 strlcpy(session_today_tally.date, date,
210 sizeof(session_today_tally.date));
211 session_today_tally.calls = 0;
212 }
213 session_today_tally.calls++;
214
215 if (s->terminal_type[0] == '\0') {
216 /* per-node setup didn't fill this in, ask the user */
217 session_output(s, "\r\n", 2);
218 user_settings_renegotiate(s);
219 session_output(s, "\r\n", 2);
220 session_flush(s);
221 }
222
223 session_printf(s, "\r\n"
224 "Welcome, {{B}}%s{{/B}}, you are the %ld%s caller today.\r\n",
225 s->user ? s->user->username : GUEST_USERNAME,
226 session_today_tally.calls, ordinal(session_today_tally.calls));
227 session_flush(s);
228 uthread_yield();
229
230 if (s->user && s->user->is_sysop &&
231 (waiting = session_first_waiting_for_sysop())) {
232 session_printf(s, "\r\n{{B}}%s{{/B}} paged sysop, answer? [Y/n] ",
233 waiting->user ? waiting->user->username : GUEST_USERNAME);
234 session_flush(s);
235
236 sc = session_input_char(s);
237 if (sc == 'N' || sc == 'n') {
238 session_printf(s, "%c\r\n", sc == 'N' ? 'N' : 'n');
239 session_flush(s);
240 } else {
241 session_output(s, "\r\n", 2);
242 session_flush(s);
243 session_answer_page(s);
244 }
245 }
246
247 s->area = SESSION_AREA_MOTD;
248 session_print_motd(s, false);
249
250 main_menu:
251 s->area = SESSION_AREA_MAIN_MENU;
252 session_output(s, "\r\n", 2);
253 session_print_menu(s, false);
254 session_flush(s);
255
256 while (!done && !s->ending) {
257 s->area = SESSION_AREA_MAIN_MENU;
258 session_printf(s, "{{B}}Main Menu>{{/B}} ");
259 session_flush(s);
260
261 get_another_char:
262 action = ACTION_NONE;
263
264 if (auth == AUTH_USER_SIGNUP) {
265 action = ACTION_SIGNUP;
266 auth = AUTH_USER_GUEST;
267 } else
268 c = session_input_char(s);
269
270 if (s->ending)
271 break;
272 if (c == '\r' || c == 0 || c > 255)
273 goto get_another_char;
274
275 session_printf(s, "%c\r\n", c);
276 session_flush(s);
277
278 if (action == ACTION_NONE) {
279 /* check each menu option's all_keys array for matching key */
280 for (i = 0; ; i++) {
281 struct main_menu_option *option = &main_menu_options[i];
282 short j;
283
284 if (option->action == ACTION_NONE)
285 break;
286
287 for (j = 0; option->all_keys[j] != '\0'; j++) {
288 if (option->all_keys[j] == c) {
289 action = option->action;
290 break;
291 }
292 }
293
294 if (action != ACTION_NONE)
295 break;
296 }
297 }
298
299 /* change actions for conditionals */
300 if (action == ACTION_SETTINGS_OR_SIGNUP) {
301 if (s->user)
302 action = ACTION_SETTINGS;
303 else
304 action = ACTION_SIGNUP;
305 } else if (action == ACTION_PAGE_SEND_OR_ANSWER) {
306 if (s->user && s->user->is_sysop)
307 action = ACTION_PAGE_ANSWER;
308 else
309 action = ACTION_PAGE_SEND;
310 }
311
312 switch (action) {
313 case ACTION_BOARD_SHOW_FIRST:
314 s->area = SESSION_AREA_BOARDS;
315 board_show(s, 1, NULL);
316 break;
317 case ACTION_CHAT:
318 s->area = SESSION_AREA_CHAT;
319 chat_start(s, NULL);
320 break;
321 case ACTION_FILES_MENU:
322 s->area = SESSION_AREA_FILES;
323 folder_list(s);
324 break;
325 case ACTION_GOODBYE:
326 session_output_view_or_printf(s, DB_VIEW_SIGNOFF,
327 "Goodbye!\r\n");
328 session_flush(s);
329 done = true;
330 break;
331 case ACTION_RECENT_LOGINS:
332 s->area = SESSION_AREA_RECENT_LOGINS;
333 session_recents(s);
334 break;
335 case ACTION_MAIL_MENU:
336 s->area = SESSION_AREA_MAIL;
337 mail_menu(s);
338 break;
339 case ACTION_MAIL_COMPOSE:
340 s->area = SESSION_AREA_MAIL;
341 mail_compose(s, NULL, NULL, NULL, NULL);
342 break;
343 case ACTION_MOTD:
344 s->area = SESSION_AREA_MOTD;
345 session_print_motd(s, true);
346 break;
347 case ACTION_PAGE_ANSWER:
348 if (!s->user || !s->user->is_sysop) {
349 session_printf(s, invalid_option_help);
350 session_flush(s);
351 break;
352 }
353 s->area = SESSION_AREA_CHAT;
354 session_answer_page(s);
355 break;
356 case ACTION_PAGE_SEND:
357 session_page_sysop(s);
358 break;
359 case ACTION_SETTINGS:
360 if (s->user) {
361 s->area = SESSION_AREA_SETTINGS;
362 user_settings_menu(s);
363 }
364 break;
365 case ACTION_SIGNUP:
366 s->area = SESSION_AREA_SIGNUP;
367 if ((s->user = signup(s))) {
368 session_printf(s, "\r\n"
369 "Welcome, {{B}}%s{{/B}}!\r\n", s->user->username);
370 goto main_menu;
371 }
372 break;
373 case ACTION_WHOS_ONLINE:
374 s->area = SESSION_AREA_WHO;
375 session_who(s);
376 break;
377 case ACTION_BOARD_LIST_BOARDS:
378 s->area = SESSION_AREA_BOARDS;
379 board_list_boards(s);
380 break;
381 case ACTION_BOARD_LIST_FTN_AREAS:
382 s->area = SESSION_AREA_FTN;
383 board_list_ftn_areas(s);
384 break;
385 case ACTION_BOARD_SHOW_1:
386 s->area = SESSION_AREA_BOARDS;
387 board_show(s, 1, NULL);
388 break;
389 case ACTION_BOARD_SHOW_2:
390 s->area = SESSION_AREA_BOARDS;
391 board_show(s, 2, NULL);
392 break;
393 case ACTION_BOARD_SHOW_3:
394 s->area = SESSION_AREA_BOARDS;
395 board_show(s, 3, NULL);
396 break;
397 case ACTION_BOARD_SHOW_4:
398 s->area = SESSION_AREA_BOARDS;
399 board_show(s, 4, NULL);
400 break;
401 case ACTION_BOARD_SHOW_5:
402 s->area = SESSION_AREA_BOARDS;
403 board_show(s, 5, NULL);
404 break;
405 case ACTION_BOARD_SHOW_6:
406 s->area = SESSION_AREA_BOARDS;
407 board_show(s, 6, NULL);
408 break;
409 case ACTION_BOARD_SHOW_7:
410 s->area = SESSION_AREA_BOARDS;
411 board_show(s, 7, NULL);
412 break;
413 case ACTION_BOARD_SHOW_8:
414 s->area = SESSION_AREA_BOARDS;
415 board_show(s, 8, NULL);
416 break;
417 case ACTION_BOARD_SHOW_9:
418 s->area = SESSION_AREA_BOARDS;
419 board_show(s, 9, NULL);
420 break;
421 case ACTION_BOARD_SHOW_10:
422 s->area = SESSION_AREA_BOARDS;
423 board_show(s, 10, NULL);
424 break;
425 case ACTION_SYSOP_MENU:
426 if (!s->user || !s->user->is_sysop) {
427 session_printf(s, invalid_option_help);
428 session_flush(s);
429 break;
430 }
431 sysop_menu(s);
432 break;
433 case ACTION_SHOW_MENU:
434 if (last_c == c)
435 /* asking twice in a row will print the full menu */
436 session_print_menu(s, false);
437 else
438 session_print_menu(s, true);
439 session_flush(s);
440 break;
441 default:
442 session_printf(s, invalid_option_help);
443 session_flush(s);
444 break;
445 }
446
447 last_c = c;
448 }
449
450 session_close(s);
451 }
452
453 void
454 session_print_menu(struct session *session, bool short_menu)
455 {
456 struct main_menu_option *option = NULL;
457 size_t size;
458 short i;
459 char *output = NULL;
460
461 if (short_menu && session_output_view_or_printf(session,
462 DB_VIEW_SHORTMENU, NULL) != 0)
463 return;
464
465 if (session_output_view_or_printf(session, DB_VIEW_MENU, NULL) != 0)
466 return;
467
468 for (i = 0; ; i++) {
469 option = &main_menu_options[i];
470
471 if (option->action == ACTION_NONE)
472 break;
473
474 if (option->menu_key == 0 || option->label[0] == '\0')
475 continue;
476
477 size = session_expand_template(session, option->label, &output);
478 if (!size)
479 continue;
480
481 session_printf(session, "{{B}}%c{{/B}}: %s\r\n", option->menu_key,
482 output);
483 xfree(&output);
484 }
485 }
486
487 void
488 session_close(struct session *session)
489 {
490 unsigned long now;
491 short n;
492 bool found;
493
494 if (!session->ending) {
495 if (!session->ban_node_source) {
496 /* give 1 second to flush output */
497 now = Ticks;
498 while (session->obuflen && (Ticks - now) < 60) {
499 session->node_funcs->output(session);
500 uthread_yield();
501 }
502 }
503 session->ending = true;
504 }
505
506 if (session->log.logged_on_at) {
507 /* finalize session log */
508 session->log.logged_off_at = Time;
509
510 if (bile_write(db->sessions_bile, SL_LOG_RTYPE, session->log.id,
511 &session->log, sizeof(session->log)) != sizeof(session->log))
512 panic("bile_write of session log failed: %d",
513 bile_error(db->sessions_bile));
514 }
515
516 /* close the node */
517 session->node_funcs->close(session);
518
519 /* remove session from sessions */
520 found = false;
521 for (n = 0; n < MAX_SESSIONS; n++) {
522 if (sessions[n] == session) {
523 sessions[n] = NULL;
524 found = true;
525 break;
526 }
527 }
528
529 if (!found)
530 panic("session_close failed to find session to remove");
531
532 nsessions--;
533 if (session->user)
534 xfree(&session->user);
535 xfree(&session);
536
537 logger_update_title();
538 }
539
540 void
541 session_check_buf_canaries(struct session *session)
542 {
543 if (session->obuf_canary != OBUF_CANARY)
544 warn("obuf canary dead");
545 if (session->ibuf_canary != IBUF_CANARY)
546 warn("ibuf canary dead");
547 if (session->obuflen_canary != OBUFLEN_CANARY)
548 warn("obuflen canary dead");
549 if (session->ibuflen_canary != IBUFLEN_CANARY)
550 warn("ibuflen canary dead");
551 }
552
553 void
554 session_flush(struct session *session)
555 {
556 if (session->ending || session->obuflen == 0)
557 return;
558
559 while (session->obuflen != 0) {
560 session->node_funcs->output(session);
561 session_check_buf_canaries(session);
562 if (session->obuflen != 0)
563 uthread_yield();
564 if (session->ending)
565 return;
566
567 /*
568 * The console will leave \e in the output buffer if it's the only
569 * thing in there, so don't loop forever.
570 */
571 if (session->obuflen == 1 && session->obuf[0] == '\33')
572 return;
573 }
574 }
575
576 size_t
577 session_log(struct session *session, const char *str)
578 {
579 return logger_printf("[%s] [%s] %s", session->node,
580 session->logged_in ?
581 (session->user ? session->user->username : GUEST_USERNAME) : "-",
582 str);
583 }
584
585 size_t
586 session_logf(struct session *session, const char *format, ...)
587 {
588 va_list ap;
589
590 va_start(ap, format);
591 vsnprintf(session_log_tbuf, sizeof(session_log_tbuf), format, ap);
592 va_end(ap);
593
594 return session_log(session, session_log_tbuf);
595 }
596
597 size_t
598 session_logf_buffered(struct session *session, const char *format, ...)
599 {
600 va_list ap;
601 size_t ret;
602
603 va_start(ap, format);
604 vsnprintf(session_log_tbuf, sizeof(session_log_tbuf), format, ap);
605 va_end(ap);
606
607 logger->autoflush = false;
608 ret = session_log(session, session_log_tbuf);
609 logger->autoflush = true;
610
611 return ret;
612 }
613
614 size_t
615 session_output(struct session *session, const char *str, size_t len)
616 {
617 size_t chunk, olen = len, stroff = 0;
618
619 while (len && !session->ending) {
620 chunk = (sizeof(session->obuf) - session->obuflen);
621 if (chunk == 0) {
622 session_flush(session);
623 if (session->ending)
624 return 0;
625 continue;
626 }
627 if (chunk > len)
628 chunk = len;
629
630 memcpy(session->obuf + session->obuflen, str + stroff, chunk);
631 session->obuflen += chunk;
632 if (session->obuflen > sizeof(session->obuf)) {
633 warn("[%s] Bogus obuflen %d > %ld", session->node,
634 session->obuflen, sizeof(session->obuf));
635 session->ending = true;
636 session_close(session);
637 return 0;
638 }
639 stroff += chunk;
640 len -= chunk;
641 }
642
643 session_check_buf_canaries(session);
644
645 return olen;
646 }
647
648 size_t
649 session_paginate(struct session *session, const char *str, size_t len,
650 size_t first_page_printed)
651 {
652 size_t olen = 0, n, adv;
653 size_t olines = first_page_printed;
654 size_t line_len = session->terminal_columns;
655 ssize_t last_space = -1;
656 short last_input = 0;
657
658 while (len && !session->ending) {
659 for (n = 0, adv = 0; n < line_len && n < len; n++) {
660 if (str[n] == '\r') {
661 last_space = n;
662 adv = n + 1;
663 if (str[n + 1] == '\n')
664 adv++;
665 break;
666 } else if (str[n] == '\n') {
667 last_space = n;
668 adv = n + 1;
669 break;
670 } else if (str[n] == ' ') {
671 last_space = n;
672 adv = n + 1;
673 }
674 }
675 if (last_space == -1) {
676 /* no space, break hard */
677 last_space = MIN(line_len, len);
678 adv = last_space;
679 }
680 if (len == n)
681 adv = last_space = len;
682
683 if (last_space > 0) {
684 session_output(session, str, last_space);
685 olen += last_space;
686 }
687 str += adv;
688 len -= adv;
689
690 session_output(session, "\r\n", 2);
691 olen += 2;
692 olines++;
693
694 if (len && (olines == session->terminal_lines - 1 ||
695 last_input == '\r' || last_input == '\n')) {
696 olines = 0;
697
698 session_printf(session, "-- More --");
699 session_flush(session);
700
701 last_input = session_input_char(session);
702
703 if (session->vt100)
704 session_printf(session, "\r%s",
705 ansi(session, ANSI_ERASE_LINE, ANSI_END));
706 else
707 session_output(session, "\r", 1);
708
709 if (last_input == 'q' || last_input == 'Q')
710 break;
711 }
712 }
713
714 return olen;
715 }
716
717 size_t
718 session_printf(struct session *session, const char *format, ...)
719 {
720 va_list ap;
721 size_t len;
722
723 va_start(ap, format);
724 len = session_vprintf(session, format, ap);
725 va_end(ap);
726
727 return len;
728 }
729
730 size_t
731 session_vprintf(struct session *session, const char *format, va_list ap)
732 {
733 static char session_printf_ebuf[160], session_printf_tbuf[160];
734 size_t len, n, en;
735 bool stop = false;
736
737 /* avoid a full session_expand_template for {{B}}, {{/B}}, and {{#}} */
738 session_printf_ebuf[0] = '\0';
739 for (n = 0, en = 0; format[n] != '\0'; n++) {
740 if (!stop) {
741 if (format[n] == '{' && format[n + 1] == '{' &&
742 format[n + 2] == 'B' && format[n + 3] == '}' &&
743 format[n + 4] == '}') {
744 en = strlcat(session_printf_ebuf, ansi_bold(session),
745 sizeof(session_printf_ebuf));
746 n += 4;
747 continue;
748 }
749 if (format[n] == '{' &&
750 format[n + 1] == '{' && format[n + 2] == '/' &&
751 format[n + 3] == 'B' && format[n + 4] == '}' &&
752 format[n + 5] == '}') {
753 en = strlcat(session_printf_ebuf, ansi_reset(session),
754 sizeof(session_printf_ebuf));
755 n += 5;
756 continue;
757 }
758 if (format[n] == '{' &&
759 format[n + 1] == '{' && format[n + 2] == '#' &&
760 format[n + 3] == '}' && format[n + 4] == '}') {
761 stop = true;
762 n += 4;
763 continue;
764 }
765 }
766
767 session_printf_ebuf[en++] = format[n];
768
769 if (en >= sizeof(session_printf_ebuf) - 1)
770 panic("session_printf_ebuf overflow!");
771 session_printf_ebuf[en] = '\0';
772 }
773
774 len = vsnprintf(session_printf_tbuf, sizeof(session_printf_tbuf),
775 session_printf_ebuf, ap);
776
777 if (len > sizeof(session_printf_tbuf))
778 panic("session_printf overflow! (%ld > %ld)", len,
779 sizeof(session_printf_tbuf));
780
781 return session_output(session, session_printf_tbuf, len);
782 }
783
784 size_t
785 session_output_view_or_printf(struct session *session, short id,
786 const char *format, ...)
787 {
788 size_t size;
789 char *output;
790 va_list ap;
791
792 /* can't use bile_read_alloc because we need to null terminate */
793 if (db->views[id] == NULL || strlen(db->views[id]) == 0) {
794 if (format == NULL)
795 return 0;
796
797 va_start(ap, format);
798 size = session_vprintf(session, format, ap);
799 va_end(ap);
800
801 return size;
802 }
803
804 size = session_expand_template(session, db->views[id], &output);
805 if (!size)
806 return 0;
807
808 size = session_output(session, output, size);
809 xfree(&output);
810
811 return size;
812 }
813
814 bool
815 session_idled_out(struct session *session)
816 {
817 if (session->logged_in) {
818 if (session->user && session->user->is_sysop) {
819 if (db->config.max_sysop_idle_minutes != 0 &&
820 Time - session->last_input_at >
821 (db->config.max_sysop_idle_minutes * 60))
822 return true;
823 } else {
824 if (db->config.max_idle_minutes != 0 &&
825 Time - session->last_input_at >
826 (db->config.max_idle_minutes * 60))
827 return true;
828 }
829 } else {
830 if (Time - session->established_at >
831 db->config.max_login_seconds)
832 return true;
833 }
834
835 return false;
836 }
837
838 short
839 session_get_char(struct session *session, unsigned char *b)
840 {
841 return session_get_chars(session, b, 1);
842 }
843
844 short
845 session_get_chars(struct session *session, unsigned char *b, size_t count)
846 {
847 short ret = 0;
848
849 while (ret < count) {
850 if (session->ibuflen == 0) {
851 if (session_idled_out(session))
852 return 0;
853
854 uthread_yield();
855 session->node_funcs->input(session);
856
857 if (session->ending || session->abort_input)
858 return 0;
859 if (session->ibuflen == 0)
860 return 0;
861 }
862
863 session->last_input_at = Time;
864
865 *b = session->ibuf[session->ibufoff];
866 b++;
867
868 if (session->ibuflen == 1) {
869 session->ibuflen = 0;
870 session->ibufoff = 0;
871 } else {
872 session->ibuflen--;
873 session->ibufoff++;
874 }
875
876 ret++;
877 }
878
879 session_check_buf_canaries(session);
880
881 return ret;
882 }
883
884 bool
885 session_wait_for_chars(struct session *session, unsigned short timeout_ms,
886 unsigned short num_chars)
887 {
888 unsigned long expire = 0;
889
890 if (timeout_ms)
891 expire = Ticks + (timeout_ms / ((double)1000 / (double)60));
892
893 while (session->ibuflen < num_chars) {
894 session->node_funcs->input(session);
895 if (session->ending || session->abort_input)
896 return false;
897 if (session->obuflen != 0 && session->chatting)
898 return false;
899 if (expire && Ticks > expire)
900 return false;
901 if (session_idled_out(session))
902 return false;
903 if (session->ibuflen >= num_chars)
904 break;
905 uthread_yield();
906 }
907
908 session->last_input_at = Time;
909 session_check_buf_canaries(session);
910 return true;
911 }
912
913 unsigned short
914 session_input_char(struct session *session)
915 {
916 unsigned short ret;
917 short waiting_for = 1;
918 short consumed = 0;
919
920 wait_for_char:
921 if (session->ibuflen < waiting_for &&
922 !session_wait_for_chars(session, 0, waiting_for)) {
923 if (session_idled_out(session))
924 goto idled_out;
925 return 0;
926 }
927
928 if (session->ibuf[0] == ESC) {
929 /* look for a VT100 escape sequence */
930 if (session->ibuflen == 1) {
931 waiting_for = 2;
932 goto wait_for_char;
933 } else if (session->ibuf[1] == '[') {
934 if (session->ibuflen == 2) {
935 waiting_for = 3;
936 goto wait_for_char;
937 } else {
938 consumed = 3;
939 switch (session->ibuf[2]) {
940 case 'A':
941 ret = KEY_UP;
942 break;
943 case 'B':
944 ret = KEY_DOWN;
945 break;
946 case 'C':
947 ret = KEY_RIGHT;
948 break;
949 case 'D':
950 ret = KEY_LEFT;
951 break;
952 default:
953 /* probably not good to pass it through as-is */
954 ret = KEY_OTHER;
955 }
956
957 goto done_consuming;
958 }
959 }
960 }
961
962 ret = session->ibuf[0];
963 consumed = 1;
964
965 done_consuming:
966 if (session->ibuflen == consumed)
967 session->ibuflen = 0;
968 else {
969 memmove(session->ibuf, session->ibuf + consumed,
970 session->ibuflen - consumed);
971 session->ibuflen--;
972 }
973
974 if (session->last_input == '\r' && (ret == '\n' || ret == 0)) {
975 /* we already responded to the \r, just ignore this */
976 session->last_input = ret;
977 goto wait_for_char;
978 }
979
980 session->last_input = ret;
981 return ret;
982
983 idled_out:
984 if (session->logged_in)
985 session_logf(session, "Idle too long, logging out");
986 else
987 session_logf(session, "Took too long to login, disconnecting");
988 session_printf(session,
989 "\r\n\r\nYou have been idle too long, goodbye.\r\n\r\n");
990 session_flush(session);
991 session->ending = true;
992 return 0;
993 }
994
995 void
996 session_clear_input(struct session *session)
997 {
998 uthread_yield();
999 session->node_funcs->input(session);
1000 uthread_yield();
1001 session->node_funcs->input(session);
1002
1003 session->ibuflen = 0;
1004 }
1005
1006 /*
1007 * Collect up to size-1 bytes of input with trailing null, in a field
1008 * width columns wide, optionally masking echoed output with mask char.
1009 * For multiline-capable input,
1010 */
1011 char *
1012 session_field_input(struct session *session, unsigned short size,
1013 unsigned short width, char *initial_input, bool multiline, char mask)
1014 {
1015 short ilen = 0, ipos = 0, over;
1016 long n;
1017 char *field;
1018 unsigned short c;
1019 unsigned char chc;
1020 bool redraw = false;
1021
1022 session->abort_input = false;
1023
1024 field = xmalloczero(size);
1025 if (field == NULL) {
1026 session->ending = true;
1027 return NULL;
1028 }
1029
1030 if (initial_input) {
1031 ipos = ilen = strlcpy(field, initial_input, size);
1032 /* TODO: handle initial value being longer than width */
1033 if (mask) {
1034 for (n = 0; n < ilen; n++)
1035 session_output(session, &mask, 1);
1036 } else
1037 session_output(session, field, ilen);
1038 session_flush(session);
1039 }
1040
1041 for (;;) {
1042 c = session_input_char(session);
1043 if (session->ending || session->abort_input)
1044 goto field_input_bail;
1045 switch (c) {
1046 case 0: /* session_input_char bailed */
1047 if (session->obuflen > 0) {
1048 session->node_funcs->output(session);
1049 uthread_yield();
1050 if (session->ending)
1051 goto field_input_bail;
1052 }
1053 break;
1054 case CONTROL_D: /* ^D */
1055 return field;
1056 case CONTROL_C: /* ^C */
1057 if (multiline)
1058 return field;
1059 goto field_input_bail;
1060 case BACKSPACE: /* ^H */
1061 case DELETE: /* backspace through telnet */
1062 case KEY_LEFT:
1063 if (ipos == 0)
1064 continue;
1065
1066 if (ipos < ilen && c != KEY_LEFT) {
1067 redraw = true;
1068 memmove(field + ipos - 1, field + ipos, ilen - 1);
1069 }
1070
1071 if (field[ipos - 1] == '\n') {
1072 /* need to jump up a line and go over */
1073 session_printf(session,
1074 ansi(session, ANSI_UP_N, 1, ANSI_END));
1075 over = 1;
1076 for (n = ipos - 2; n >= 0; n--) {
1077 if (field[n] == '\n')
1078 break;
1079 over++;
1080 }
1081 session_printf(session,
1082 ansi(session, ANSI_COL_N, over, ANSI_END));
1083 session_flush(session);
1084 }
1085
1086 ipos--;
1087
1088 if (c != KEY_LEFT) {
1089 ilen--;
1090 field[ilen] = '\0';
1091
1092 session_printf(session,
1093 ansi(session, ANSI_BACKSPACE, ANSI_END));
1094 }
1095
1096 if (redraw) {
1097 session_printf(session,
1098 ansi(session, ANSI_SAVE_CURSOR, ANSI_END));
1099 session_output(session, field + ipos, ilen - ipos);
1100 session_output(session, " ", 1);
1101 session_printf(session,
1102 ansi(session, ANSI_RESTORE_SAVED_CURSOR, ANSI_END));
1103 }
1104
1105 session_flush(session);
1106 break;
1107 case '\r':
1108 case '\n':
1109 if (multiline)
1110 goto append_char;
1111 return field;
1112 case KEY_RIGHT:
1113 if (ipos == ilen)
1114 continue;
1115 ipos++;
1116 session_printf(session,
1117 ansi(session, ANSI_FORWARD_N, 1, ANSI_END));
1118 session_flush(session);
1119 break;
1120 default:
1121 append_char:
1122 if ((c < 32 || c > 127) && (c != '\r' && c != '\n'))
1123 break;
1124 if (ilen >= size - 1)
1125 break;
1126 chc = c;
1127 if (ipos < ilen)
1128 memmove(field + ipos + 1, field + ipos, ilen - ipos);
1129
1130 field[ipos] = chc;
1131 ipos++;
1132 ilen++;
1133 field[ilen] = '\0';
1134 session_output(session, mask ? &mask : (char *)&chc, 1);
1135
1136 if (ipos < ilen) {
1137 if (mask) {
1138 /* TODO: repeat mask */
1139 } else {
1140 session_printf(session,
1141 ansi(session, ANSI_SAVE_CURSOR, ANSI_END));
1142 session_output(session, field + ipos,
1143 ilen - ipos);
1144 session_printf(session,
1145 ansi(session, ANSI_RESTORE_SAVED_CURSOR, ANSI_END));
1146 }
1147 }
1148
1149 if (chc == '\r') {
1150 c = '\n';
1151 goto append_char;
1152 }
1153
1154 session_flush(session);
1155 }
1156 }
1157
1158 field_input_bail:
1159 xfree(&field);
1160 return NULL;
1161 }
1162
1163 short
1164 session_login(struct session *s)
1165 {
1166 struct user *user = NULL;
1167 char junk[SHA256_DIGEST_STRING_LENGTH];
1168 char *username = NULL, *password = NULL;
1169 size_t len;
1170 short n;
1171
1172 for (n = 1; n <= 3; n++) {
1173 session_printf(s, "Username: ");
1174
1175 if (s->autologin_username[0]) {
1176 session_printf(s, "{{#}}%s\r\n", s->autologin_username);
1177 session_flush(s);
1178 username = xstrdup(s->autologin_username);
1179 if (username == NULL)
1180 goto login_bail;
1181 } else {
1182 session_flush(s);
1183 username = session_field_input(s, DB_USERNAME_LENGTH + 1,
1184 DB_USERNAME_LENGTH, NULL, false, 0);
1185 session_output(s, "\r\n", 2);
1186 session_flush(s);
1187
1188 if (username == NULL || s->ending)
1189 goto login_bail;
1190
1191 if (username[0] == '\0') {
1192 n--;
1193 xfree(&username);
1194 username = NULL;
1195 continue;
1196 }
1197 }
1198
1199 if (strcasecmp(username, GUEST_USERNAME) == 0) {
1200 session_logf(s, "Successful guest login in as %s", username);
1201 xfree(&username);
1202 return AUTH_USER_GUEST;
1203 }
1204
1205 if ((strcasecmp(username, "signup") == 0 ||
1206 strcasecmp(username, "new") == 0) && db->config.open_signup) {
1207 session_logf(s, "Successful guest signup login in as %s",
1208 username);
1209 xfree(&username);
1210 return AUTH_USER_SIGNUP;
1211 }
1212
1213 user = user_find_by_username(username);
1214
1215 if (!user && user_username_is_banned(username)) {
1216 session_logf(s, "Attempted login as banned username %s",
1217 username);
1218 s->ban_node_source = 1;
1219 goto login_bail;
1220 }
1221
1222 if (s->autologin_username[0]) {
1223 if (user) {
1224 xfree(&username);
1225 s->user = user;
1226 session_logf(s, "Automatically logged in as %s",
1227 s->autologin_username);
1228 return AUTH_USER_OK;
1229 }
1230
1231 memset(s->autologin_username, 0,
1232 sizeof(s->autologin_username));
1233 }
1234
1235 session_printf(s, "Password: ");
1236 session_flush(s);
1237 password = session_field_input(s, 64, 64, NULL, false, '*');
1238 session_output(s, "\r\n", 2);
1239 session_flush(s);
1240
1241 if (password == NULL || s->ending)
1242 goto login_bail;
1243
1244 if (user) {
1245 if (user_authenticate(user, password) == AUTH_USER_OK) {
1246 if (user->is_enabled)
1247 s->user = user;
1248 else
1249 session_logf(s, "Successful password login for %s but "
1250 "account is disabled", user->username);
1251 } else
1252 session_logf(s, "Failed password login for %s",
1253 user->username);
1254 } else {
1255 /* kill some time */
1256 SHA256Data((const u_int8_t *)password, strlen(password), junk);
1257 session_logf(s, "Failed password login for invalid user %s",
1258 username);
1259 }
1260
1261 len = strlen(username);
1262 memset(username, 0, len);
1263 xfree(&username);
1264 username = NULL;
1265
1266 len = strlen(password);
1267 memset(password, 0, len);
1268 xfree(&password);
1269 password = NULL;
1270
1271 if (s->user) {
1272 session_logf(s, "Successful password login for user %s",
1273 s->user->username);
1274 return AUTH_USER_OK;
1275 }
1276
1277 uthread_msleep(60);
1278 session_printf(s, "Login incorrect\r\n");
1279 session_flush(s);
1280
1281 if (user)
1282 xfree(&user);
1283 }
1284
1285 login_bail:
1286 if (username != NULL)
1287 xfree(&username);
1288 if (password != NULL)
1289 xfree(&password);
1290 if (!s->ban_node_source) {
1291 if (session_idled_out(s)) {
1292 session_logf(s, "Login timed out after %d seconds",
1293 db->config.max_login_seconds);
1294 session_printf(s, "\r\nLogin timed out after %d seconds\r\n",
1295 db->config.max_login_seconds);
1296 /* session_flush won't do anything since s->ending is set */
1297 s->node_funcs->output(s);
1298 } else {
1299 session_printf(s, "Thanks for playing\r\n");
1300 session_flush(s);
1301 }
1302 }
1303
1304 return AUTH_USER_FAILED;
1305 }
1306
1307 size_t
1308 session_expand_template(struct session *session, const char *tmpl,
1309 char **ret)
1310 {
1311 static char curvar[128], matchvar[128];
1312 size_t tmpllen, retsize, retpos;
1313 size_t vallen;
1314 short n, invar = 0, varlen = 0, doif, sep;
1315 char *varseek, *curvarpos, *val, *data;
1316 bool end_expansion = false;
1317
1318 retsize = 0;
1319 retpos = 0;
1320 data = NULL;
1321 tmpllen = strlen(tmpl);
1322
1323 for (n = 0; n < tmpllen; n++) {
1324 if (invar) {
1325 if (!varlen && (tmpl[n] == ' ' || tmpl[n] == '\t'))
1326 continue;
1327 if (tmpl[n] == '}' && tmpl[n + 1] == '}') {
1328 n++;
1329 goto expand_var;
1330 } else if (varlen < sizeof(curvar) - 2)
1331 curvar[varlen++] = tmpl[n];
1332 } else if (!end_expansion && tmpl[n] == '{' && tmpl[n + 1] == '{') {
1333 invar = 1;
1334 varlen = 0;
1335 n++;
1336 } else if (tmpl[n] == '\r' && tmpl[n + 1] != '\n') {
1337 if (!grow_to_fit(&data, &retsize, retpos, 2, 64)) {
1338 xfree(&data);
1339 break;
1340 }
1341 data[retpos++] = '\r';
1342 data[retpos++] = '\n';
1343 } else {
1344 if (!grow_to_fit(&data, &retsize, retpos, 1, 64)) {
1345 xfree(&data);
1346 break;
1347 }
1348 data[retpos++] = tmpl[n];
1349 }
1350
1351 continue;
1352
1353 expand_var:
1354 invar = 0;
1355 curvar[varlen] = '\0';
1356 varlen = 0;
1357
1358 if (strpos_quoted(curvar, '?') != -1 &&
1359 strpos_quoted(curvar, ':') != -1) {
1360 /* ternary: user ? "a string" : time */
1361 varseek = curvar;
1362 while (varseek[0] == ' ')
1363 varseek++;
1364 sep = strpos_quoted(varseek, '?');
1365 if (sep == -1)
1366 continue;
1367 memcpy(matchvar, varseek, sep);
1368 matchvar[sep] = '\0';
1369 curvarpos = varseek + sep + 1;
1370 while (matchvar[sep - 1] == ' ')
1371 matchvar[--sep] = '\0';
1372
1373 /* matchvar is now the condition */
1374 if (strcmp(matchvar, "user") == 0)
1375 doif = (session->user != NULL);
1376 else if (strcmp(matchvar, "sysop") == 0)
1377 doif = (session->user && session->user->is_sysop);
1378 else
1379 doif = 0;
1380
1381 while (curvarpos[0] == ' ')
1382 curvarpos++;
1383
1384 sep = strpos_quoted(curvarpos, ':');
1385 if (sep == -1)
1386 continue;
1387
1388 while (curvarpos[sep] == ' ') {
1389 curvarpos++;
1390 sep--;
1391 }
1392
1393 if (doif) {
1394 /* copy the "then" */
1395 memcpy(matchvar, curvarpos, sep);
1396 matchvar[sep] = '\0';
1397 } else {
1398 /* copy from after the "then" and : */
1399 strlcpy(matchvar, curvarpos + sep + 1, sizeof(matchvar));
1400 }
1401
1402 sep = strlen(matchvar);
1403 while (matchvar[sep - 1] == ' ')
1404 matchvar[--sep] = '\0';
1405
1406 vallen = session_expand_var(session, matchvar, &val,
1407 &end_expansion);
1408 } else {
1409 vallen = session_expand_var(session, curvar, &val,
1410 &end_expansion);
1411 }
1412
1413 if (vallen) {
1414 if (!grow_to_fit(&data, &retsize, retpos, vallen, 128)) {
1415 xfree(&data);
1416 break;
1417 }
1418 memcpy(data + retpos, val, vallen);
1419 retpos += vallen;
1420 }
1421 }
1422
1423 if (data == NULL) {
1424 *ret = NULL;
1425 return 0;
1426 }
1427
1428 data[retpos] = '\0';
1429 *ret = data;
1430 return retpos;
1431 }
1432
1433 size_t
1434 session_expand_var(struct session *session, char *ivar, char **ret,
1435 bool *end_expansion)
1436 {
1437 static char var[128], retval[128];
1438 size_t retsize, retlen, varlen, unread_count;
1439 long elapsed, telapsed;
1440 struct tm *now;
1441 short count, n;
1442 bool pad = false;
1443
1444 *ret = (char *)&retval;
1445 retlen = 0;
1446
1447 if (sscanf(ivar, "%127[^|]|%lu%n", &var, &retsize, &count) == 2 &&
1448 count > 0) {
1449 /* field of fixed length, either truncated or padded */
1450 if (retsize > sizeof(retval))
1451 retsize = sizeof(retval);
1452 pad = true;
1453 varlen = strlen(var);
1454 } else {
1455 while (ivar[0] == ' ')
1456 ivar++;
1457
1458 varlen = strlcpy(var, ivar, sizeof(var));
1459 retsize = sizeof(retval);
1460 }
1461
1462 while (varlen && var[varlen - 1] == ' ') {
1463 var[varlen - 1] = '\0';
1464 varlen--;
1465 }
1466
1467 if (strcmp(var, "B") == 0) {
1468 retlen = strlcpy(retval, ansi_bold(session), retsize);
1469 } else if (strcmp(var, "/B") == 0) {
1470 retlen = strlcpy(retval, ansi_reset(session), retsize);
1471 } else if (strcmp(var, "#") == 0) {
1472 *end_expansion = true;
1473 retlen = 0;
1474 } else if (strncmp(var, "center_", 7) == 0 &&
1475 sscanf(var, "center_%d", &count) == 1) {
1476 if (session->terminal_columns <= count)
1477 retlen = 0;
1478 else {
1479 retlen = MIN((session->terminal_columns - count) / 2, retsize);
1480 for (n = 0; n < retlen; n++)
1481 retval[n] = ' ';
1482 retval[retlen] = '\0';
1483 }
1484 } else if (strcmp(var, "logged_in_time") == 0) {
1485 memset(retval, 0, retsize);
1486 retlen = 0;
1487 elapsed = Time - session->log.logged_on_at;
1488 while (elapsed > 0) {
1489 if (elapsed < 60) {
1490 telapsed = elapsed;
1491 retlen += snprintf(retval + retlen,
1492 retsize - retlen, "%lus", telapsed);
1493 } else if (elapsed < (60 * 60)) {
1494 telapsed = elapsed / 60;
1495 retlen += snprintf(retval + retlen,
1496 retsize - retlen, "%lum", telapsed);
1497 telapsed *= 60;
1498 } else if (elapsed < (60 * 60 * 24)) {
1499 telapsed = elapsed / (60 * 60);
1500 retlen += snprintf(retval + retlen,
1501 retsize - retlen, "%luh", telapsed);
1502 telapsed *= (60 * 60);
1503 } else {
1504 telapsed = elapsed / (60 * 60 * 24);
1505 retlen += snprintf(retval + retlen,
1506 retsize - retlen, "%luh", telapsed);
1507 telapsed *= (60 * 60 * 24);
1508 }
1509 elapsed -= telapsed;
1510 }
1511 } else if (strcmp(var, "node") == 0) {
1512 retlen = strlcpy(retval, session->node, retsize);
1513 } else if (strcmp(var, "phone_number") == 0) {
1514 retlen = strlcpy(retval, db->config.phone_number, retsize);
1515 } else if (strcmp(var, "time") == 0) {
1516 now = localtime((time_t *)&Time);
1517 retlen = strftime(retval, retsize, "%H:%M", now);
1518 } else if (strcmp(var, "timezone") == 0) {
1519 retlen = strlcpy(retval, db->config.timezone, retsize);
1520 } else if (strcmp(var, "username") == 0) {
1521 retlen = strlcpy(retval, session->user ?
1522 session->user->username : GUEST_USERNAME, retsize);
1523 } else if (strcmp(var, "new_mail") == 0) {
1524 if (session->user) {
1525 mail_find_ids_for_user(session->user, &unread_count, NULL, 0,
1526 0, true);
1527 if (unread_count)
1528 retlen = sprintf(retval, "(%lu New)", unread_count);
1529 }
1530 } else if (var[0] == '"') {
1531 /* a literal string, remove leading and trailing quotes */
1532 var[varlen - 1] = '\0';
1533 varlen--;
1534 if (varlen > retsize)
1535 varlen = retsize;
1536 strlcpy(retval, var + 1, varlen);
1537 retlen = varlen - 1;
1538 } else {
1539 /* shrug */
1540 strlcpy(retval, var, varlen);
1541 retlen = varlen;
1542 }
1543
1544 if (pad && retlen < retsize) {
1545 while (retlen < retsize)
1546 retval[retlen++] = ' ';
1547 retval[retlen] = '\0';
1548 }
1549
1550 return retlen;
1551 }
1552
1553 void
1554 session_pause_return(struct session *s, char enforce, char *msg)
1555 {
1556 unsigned char c;
1557
1558 session_printf(s, "{{/B}}Press ");
1559 if (enforce == 0 || enforce == '\r')
1560 session_printf(s, "{{B}}<Enter>{{/B}} ");
1561 else if (enforce == CONTROL_C)
1562 session_printf(s, "{{B}}^C{{/B}} ");
1563 else
1564 session_printf(s, "{{B}}%c{{/B}} ", enforce);
1565
1566 if (msg)
1567 session_printf(s, msg);
1568 else
1569 session_printf(s, "to return to the main menu...");
1570 session_flush(s);
1571
1572 for (;;) {
1573 c = session_input_char(s);
1574 if (s->ending)
1575 return;
1576 if (c == 0)
1577 continue;
1578 if (enforce == 0 || enforce == c)
1579 break;
1580 }
1581 session_output(s, "\r\n", 2);
1582 session_flush(s);
1583 }
1584
1585 void
1586 session_page_sysop(struct session *s)
1587 {
1588 char *message = NULL;
1589
1590 session_printf(s, "{{B}}Page Sysop{{/B}} "
1591 "(^C to cancel)\r\n"
1592 "{{B}}-------------------------{{/B}}\r\n");
1593 session_output_view_or_printf(s, DB_VIEW_PAGE_SYSOP,
1594 "(Instructions missing)\r\n");
1595 session_flush(s);
1596
1597 session_printf(s, "{{B}}Message:{{/B}} ");
1598 session_flush(s);
1599 message = session_field_input(s, 64, 64, NULL, false, 0);
1600 session_output(s, "\r\n", 2);
1601 session_flush(s);
1602
1603 if (message == NULL || s->ending)
1604 goto page_done;
1605
1606 session_logf(s, "Paging sysop: %s", message);
1607
1608 progress("Page from %s: %s",
1609 s->user ? s->user->username : GUEST_USERNAME, message);
1610 SysBeep(30);
1611 uthread_yield();
1612
1613 chat_start(s, CHAT_WITH_SYSOP);
1614
1615 page_done:
1616 progress(NULL);
1617
1618 if (message != NULL)
1619 xfree(&message);
1620 }
1621
1622 struct session *
1623 session_first_waiting_for_sysop(void)
1624 {
1625 short n;
1626
1627 for (n = 0; n < MAX_SESSIONS; n++) {
1628 if (sessions[n] == NULL || !sessions[n]->logged_in ||
1629 !sessions[n]->chatting)
1630 continue;
1631 if (strcmp(sessions[n]->chatting_with_node, CHAT_WITH_SYSOP) != 0)
1632 continue;
1633
1634 return sessions[n];
1635 }
1636
1637 return NULL;
1638 }
1639
1640 void
1641 session_answer_page(struct session *s)
1642 {
1643 struct session *waiting;
1644
1645 waiting = session_first_waiting_for_sysop();
1646 if (waiting == NULL) {
1647 session_printf(s, "No users waiting to chat.\r\n");
1648 session_flush(s);
1649 return;
1650 }
1651
1652 progress(NULL);
1653 session_logf(s, "Answering page from %s on %s",
1654 waiting->user ? waiting->user->username : GUEST_USERNAME,
1655 waiting->node);
1656
1657 /* point paging user's chatting_with_node to ours */
1658 strlcpy(waiting->chatting_with_node, s->node,
1659 sizeof(waiting->chatting_with_node));
1660
1661 /* and join the party */
1662 chat_start(s, waiting->node);
1663 }
1664
1665 void
1666 session_print_motd(struct session *s, bool force)
1667 {
1668 struct bile_object *o;
1669 unsigned long last_seen_motd = 0, motd_id = 0;
1670 size_t size;
1671 char *view = NULL, *output = NULL;
1672
1673 if (s->user)
1674 last_seen_motd = s->user->last_motd_seen;
1675
1676 o = bile_get_nth_of_type(db->bile, 0, DB_MOTD_RTYPE);
1677 if (o == NULL || (!force && o->id <= last_seen_motd)) {
1678 if (o)
1679 xfree(&o);
1680 return;
1681 }
1682 motd_id = o->id;
1683
1684 view = xmalloc(o->size + 1);
1685 if (view == NULL) {
1686 s->ending = true;
1687 xfree(&o);
1688 return;
1689 }
1690 size = bile_read_object(db->bile, o, view, o->size);
1691 view[size] = '\0';
1692 xfree(&o);
1693
1694 size = session_expand_template(s, view, &output);
1695 xfree(&view);
1696 if (!size)
1697 return;
1698
1699 session_output(s, "\r\n", 2);
1700 session_output(s, output, size);
1701 session_output(s, "\r\n\r\n", 4);
1702 xfree(&output);
1703
1704 session_pause_return(s, 0, "to continue...");
1705
1706 if (s->user && !force) {
1707 s->user->last_motd_seen = motd_id;
1708 user_save(s->user);
1709 }
1710 }
1711
1712 void
1713 session_recents(struct session *s)
1714 {
1715 struct session_log slog;
1716 struct tm *date_tm;
1717 size_t scount, rsize, *ids;
1718 char sdate[12];
1719 short printed;
1720
1721 session_printf(s, "{{B}}Recent Logins{{/B}}\r\n");
1722 session_printf(s,
1723 "{{B}}Date User Via Speed Location{{/B}}\r\n");
1724 session_flush(s);
1725
1726 scount = bile_sorted_ids_by_type(db->sessions_bile, SL_LOG_RTYPE,
1727 &ids);
1728 for (printed = 0; printed < 10 && printed < scount; printed++) {
1729 rsize = bile_read(db->sessions_bile, SL_LOG_RTYPE,
1730 ids[scount - 1 - printed], (char *)&slog, sizeof(slog));
1731 if (rsize != sizeof(slog))
1732 panic("unexpected bile_read response %d (read %ld, "
1733 "expected %ld)", bile_error(db->sessions_bile), rsize,
1734 sizeof(slog));
1735
1736 date_tm = localtime((time_t *)&slog.logged_on_at);
1737 strftime(sdate, sizeof(sdate), "%m/%d", date_tm);
1738
1739 session_printf(s, "%-5s %-20s %-7s %-6u %-32s\r\n",
1740 sdate,
1741 slog.username,
1742 slog.via,
1743 slog.tspeed,
1744 slog.location);
1745 }
1746
1747 session_output(s, "\r\n", 2);
1748 session_flush(s);
1749 session_pause_return(s, 0, NULL);
1750
1751 xfree(&ids);
1752 }
1753
1754 void
1755 session_who(struct session *s)
1756 {
1757 char idle_s[20];
1758 char username[20];
1759 unsigned long idle;
1760 short n;
1761
1762 session_printf(s, "{{B}}Who's Online{{/B}}\r\n");
1763 session_printf(s,
1764 "{{B}}Node User Via Speed Cur Area Idle Location{{/B}}\r\n");
1765 session_flush(s);
1766
1767 for (n = 0; n < MAX_SESSIONS; n++) {
1768 if (sessions[n] == NULL || !sessions[n]->logged_in)
1769 continue;
1770
1771 idle = Time - sessions[n]->last_input_at;
1772 if (idle < 60)
1773 snprintf(idle_s, sizeof(idle_s), "%lus", idle);
1774 else if (idle < (60 * 60))
1775 snprintf(idle_s, sizeof(idle_s), "%lum", idle / 60);
1776 else if (idle < (60 * 60 * 24))
1777 snprintf(idle_s, sizeof(idle_s), "%luh", idle / (60 * 60));
1778 else
1779 snprintf(idle_s, sizeof(idle_s), "%lud", idle / (60 * 60 * 24));
1780
1781 snprintf(username, sizeof(username), "%s%s",
1782 sessions[n]->user ? sessions[n]->user->username : GUEST_USERNAME,
1783 sessions[n]->user && sessions[n]->user->is_sysop ? " (sysop)" : "");
1784
1785 session_printf(s, "%-7s %-17s %-7s %-6u %-10s %-5s %-22s\r\n",
1786 sessions[n]->node,
1787 username,
1788 sessions[n]->via,
1789 sessions[n]->tspeed,
1790 area_labels[sessions[n]->area],
1791 idle_s,
1792 sessions[n]->location);
1793 }
1794
1795 session_output(s, "\r\n", 2);
1796 session_flush(s);
1797 session_pause_return(s, 0, NULL);
1798 }
1799
1800 char
1801 session_menu(struct session *s, char *title, char *prompt,
1802 char *prompt_help, const struct session_menu_option *opts,
1803 size_t nopts, bool show_opts, char *number_prompt, short *ret_number)
1804 {
1805 size_t n;
1806 short j, num;
1807 unsigned short c;
1808 char cn[2], *num_input = NULL;
1809 short invalids = 0;
1810
1811 if (show_opts && title != NULL) {
1812 session_printf(s, "{{B}}%s{{/B}}\r\n", title);
1813 session_flush(s);
1814 }
1815
1816 print_options:
1817 if (show_opts) {
1818 for (n = 0; n < nopts; n++) {
1819 if (opts[n].key[0] == '\0')
1820 continue;
1821 session_printf(s, "{{B}}%c{{/B}}: %s\r\n", opts[n].key[0],
1822 opts[n].title);
1823 }
1824 session_flush(s);
1825 }
1826
1827 #if 0
1828 if (prompt_help) {
1829 session_printf(s, "{{B}}%s{{/B}}\r\n", prompt_help);
1830 session_flush(s);
1831 }
1832 #endif
1833
1834 for (;;) {
1835 show_prompt:
1836 session_printf(s, "{{B}}%s>{{/B}} ", prompt);
1837 session_flush(s);
1838
1839 get_menu_option:
1840 uthread_yield();
1841 c = session_input_char(s);
1842 if (s->ending)
1843 return 0;
1844 if (c == '\r' || c == 0 || c > 255)
1845 goto get_menu_option;
1846
1847 for (n = 0; n < nopts; n++) {
1848 if (opts[n].ret == '#' && c >= '0' && c <= '9') {
1849 session_printf(s, "%s:%s>%s ",
1850 ansi(s, ANSI_BACKSPACE, ANSI_BACKSPACE,
1851 ANSI_BOLD, ANSI_END), number_prompt,
1852 ansi(s, ANSI_RESET, ANSI_END));
1853 session_flush(s);
1854
1855 cn[0] = c;
1856 cn[1] = '\0';
1857 num_input = session_field_input(s, 4, 3, (char *)&cn,
1858 false, 0);
1859 session_printf(s, "\r\n");
1860 session_flush(s);
1861 if (num_input == NULL)
1862 goto show_prompt;
1863 if (num_input[0] == '\0') {
1864 xfree(&num_input);
1865 goto show_prompt;
1866 }
1867 num = atoi(num_input);
1868 xfree(&num_input);
1869 *ret_number = num;
1870 return opts[n].ret;
1871 }
1872
1873 for (j = 0; ; j++) {
1874 if (opts[n].key[j] == '\0')
1875 break;
1876 if (opts[n].key[j] == c) {
1877 session_printf(s, "%c\r\n", c);
1878 session_flush(s);
1879 return opts[n].ret;
1880 }
1881 }
1882 }
1883
1884 /*
1885 * Avoid a constant back-and-forth of the user's session spewing
1886 * bogus characters and us having to print 'invalid option' over
1887 * and over.
1888 */
1889 if (invalids > 3)
1890 goto get_menu_option;
1891
1892 session_printf(s, "%c\r\n", c);
1893 session_printf(s, invalid_option_help);
1894 session_flush(s);
1895 invalids++;
1896 }
1897 }
1898
1899 void
1900 session_prune_logs(short days)
1901 {
1902 struct bile_object *obj;
1903 struct session_log log;
1904 size_t n, size = 0, count = 0, delta;
1905 unsigned long secs, *ids = NULL;
1906
1907 if (days < 1)
1908 return;
1909
1910 secs = (60UL * 60UL * 24UL * (unsigned long)days);
1911
1912 logger_printf("[db] Pruning session logs older than %d days", days);
1913
1914 for (n = 0;
1915 (obj = bile_get_nth_of_type(db->sessions_bile, n, SL_LOG_RTYPE));
1916 n++) {
1917 bile_read_object(db->sessions_bile, obj, &log, sizeof(log));
1918
1919 delta = Time - log.logged_on_at;
1920 if (delta > secs) {
1921 if (!grow_to_fit(&ids, &size, count * sizeof(unsigned long),
1922 sizeof(unsigned long), 10 * sizeof(unsigned long)))
1923 break;
1924 ids[count++] = obj->id;
1925 }
1926
1927 xfree(&obj);
1928 }
1929
1930 uthread_yield();
1931
1932 if (count) {
1933 logger_printf("[db] Deleting %ld of %ld log entries", count, n);
1934
1935 for (n = 0; n < count; n++)
1936 bile_delete(db->sessions_bile, SL_LOG_RTYPE, ids[n], 0);
1937 bile_write_map(db->sessions_bile);
1938 }
1939
1940 if (ids)
1941 xfree(&ids);
1942 }