AmendHub

Download

jcs

/

subtext

/

board.c

 

(View History)

jcs   board: Minor bugfixes, memory leaks Latest amendment: 643 on 2026-09-22

1 /*
2 * Copyright (c) 2022 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 "subtext.h"
23 #include "ansi.h"
24 #include "binkp.h"
25 #include "board.h"
26 #include "logger.h"
27 #include "user.h"
28
29 #define POSTS_PER_PAGE 20
30
31 #define POST_READ_RETURN_DONE -1
32 #define POST_READ_RETURN_LIST -2
33 #define POST_READ_RETURN_FIND -3
34 #define POST_READ_RETURN_NEWER -4
35 #define POST_READ_RETURN_OLDER -5
36
37 const struct struct_field board_fields[] = {
38 { "Board ID", CONFIG_TYPE_LONG,
39 offsetof(struct board, id),
40 1, LONG_MAX },
41 { "Name", CONFIG_TYPE_STRING,
42 offsetof(struct board, name),
43 1, member_size(struct board, name) },
44 { "Description", CONFIG_TYPE_STRING,
45 offsetof(struct board, description),
46 0, member_size(struct board, description) },
47 { "Restricted Posting", CONFIG_TYPE_BOOLEAN,
48 offsetof(struct board, restricted_posting),
49 0, 0 },
50 { "Restricted Viewing", CONFIG_TYPE_BOOLEAN,
51 offsetof(struct board, restricted_viewing),
52 0, 0 },
53 { "Days of Retention", CONFIG_TYPE_SHORT,
54 offsetof(struct board, retention_days),
55 0, USHRT_MAX },
56 { "FTN Area Name", CONFIG_TYPE_STRING,
57 offsetof(struct board, ftn_area),
58 0, member_size(struct board, ftn_area) },
59 };
60 const size_t nboard_fields = nitems(board_fields);
61
62 const struct bile_object_field board_object_fields[] = {
63 { offsetof(struct board, id),
64 member_size(struct board, id), -1 },
65 { offsetof(struct board, name),
66 member_size(struct board, name), -1 },
67 { offsetof(struct board, description),
68 member_size(struct board, description), -1 },
69 { offsetof(struct board, restricted_posting),
70 member_size(struct board, restricted_posting), -1 },
71 { offsetof(struct board, restricted_viewing),
72 member_size(struct board, restricted_viewing), -1 },
73 { offsetof(struct board, retention_days),
74 member_size(struct board, retention_days), -1 },
75 { offsetof(struct board, last_post_at),
76 member_size(struct board, last_post_at), -1 },
77 { offsetof(struct board, post_count),
78 member_size(struct board, post_count), -1 },
79 { offsetof(struct board, ftn_area),
80 member_size(struct board, ftn_area), -1 },
81 };
82 const size_t nboard_object_fields = nitems(board_object_fields);
83
84 const struct bile_object_field board_post_object_fields[] = {
85 { offsetof(struct board_post, id),
86 member_size(struct board_post, id), -1 },
87 { offsetof(struct board_post, thread_id),
88 member_size(struct board_post, thread_id), -1 },
89 { offsetof(struct board_post, time),
90 member_size(struct board_post, time), -1 },
91 { offsetof(struct board_post, sender_user_id),
92 member_size(struct board_post, sender_user_id), -1 },
93 { offsetof(struct board_post, body_size),
94 member_size(struct board_post, body_size), -1 },
95 { offsetof(struct board_post, body),
96 -1, offsetof(struct board_post, body_size) },
97 { offsetof(struct board_post, parent_post_id),
98 member_size(struct board_post, parent_post_id), -1 },
99 { offsetof(struct board_post, via),
100 member_size(struct board_post, via), -1 },
101 };
102 const size_t nboard_post_object_fields = nitems(board_post_object_fields);
103
104 const struct bile_object_field board_ftn_post_object_fields[] = {
105 { offsetof(struct board_ftn_post, id),
106 member_size(struct board_ftn_post, id), -1 },
107 { offsetof(struct board_ftn_post, msgid),
108 member_size(struct board_ftn_post, msgid), -1 },
109 { offsetof(struct board_ftn_post, time),
110 member_size(struct board_ftn_post, time), -1 },
111 { offsetof(struct board_ftn_post, from),
112 member_size(struct board_ftn_post, from), -1 },
113 { offsetof(struct board_ftn_post, subject),
114 member_size(struct board_ftn_post, subject), -1 },
115 { offsetof(struct board_ftn_post, msgid_orig),
116 member_size(struct board_ftn_post, msgid_orig), -1 },
117 { offsetof(struct board_ftn_post, origin),
118 member_size(struct board_ftn_post, origin), -1 },
119 { offsetof(struct board_ftn_post, reply),
120 member_size(struct board_ftn_post, reply), -1 },
121 { offsetof(struct board_ftn_post, to),
122 member_size(struct board_ftn_post, to), -1 },
123 { offsetof(struct board_ftn_post, body_size),
124 member_size(struct board_ftn_post, body_size), -1 },
125 { offsetof(struct board_ftn_post, body),
126 -1, offsetof(struct board_ftn_post, body_size) },
127 };
128 const size_t nboard_ftn_post_object_fields =
129 nitems(board_ftn_post_object_fields);
130
131 const struct bile_object_field board_thread_object_fields[] = {
132 { offsetof(struct board_thread, thread_id),
133 member_size(struct board_thread, thread_id), -1 },
134 { offsetof(struct board_thread, last_post_at),
135 member_size(struct board_thread, last_post_at), -1 },
136 { offsetof(struct board_thread, subject_size),
137 member_size(struct board_thread, subject_size), -1 },
138 { offsetof(struct board_thread, subject),
139 -1, offsetof(struct board_thread, subject_size) },
140 { offsetof(struct board_thread, nposts),
141 member_size(struct board_thread, nposts), -1 },
142 { offsetof(struct board_thread, post_ids),
143 -(sizeof(long)), offsetof(struct board_thread, nposts) },
144 { offsetof(struct board_thread, parent_post_ids),
145 -(sizeof(long)), offsetof(struct board_thread, nposts) },
146 };
147 const size_t nboard_thread_object_fields = nitems(board_thread_object_fields);
148
149 unsigned long board_compose(struct session *s, struct board *board,
150 struct board_thread *thread, struct board_post *parent_post,
151 struct board_ftn_post *ftn_parent_post, char *initial_subject,
152 char *initial_body);
153 short board_post_create(struct board *board, struct board_thread *thread,
154 struct board_ftn_post *ftn_parent_post, struct board_post *post);
155 void board_list_posts(struct session *s, struct board *board,
156 size_t npost_Ids, unsigned long *post_ids, size_t page, size_t pages);
157 short board_post_read(struct session *s, struct board *board,
158 char *prompt_prefix, unsigned long id, short idx);
159 size_t board_find_post_ids(struct session *s, struct board *board,
160 size_t *npost_ids, unsigned long **post_ids, size_t offset,
161 size_t limit);
162 void board_delete_cached_index(struct board *board);
163
164 void
165 board_list_boards(struct session *s)
166 {
167 static struct session_menu_option opts[] = {
168 { '#', "#", "Enter board number to read" },
169 { 'l', "Ll", "List boards" },
170 { 'q', "QqXx", "Return to main menu" },
171 { '?', "?", "List menu options" },
172 };
173 static const char prompt_help[] =
174 "#:View Board L:List Boards Q:Return ?:Help";
175 struct board *lboards = NULL, tboard;
176 size_t nlboards;
177 char title[] = "Message Boards";
178 char c;
179 short an, n, i, j;
180 bool done, show_list, show_help;
181
182 if (db->nboards == 0) {
183 session_printf(s, "No boards created.\n");
184 session_flush(s);
185 return;
186 }
187
188 lboards = xcalloc(sizeof(struct board), db->nboards);
189 if (lboards == NULL)
190 return;
191
192 nlboards = 0;
193 for (n = 0; n < db->nboards; n++) {
194 if (db->boards[n].restricted_viewing &&
195 !(s->user && s->user->is_sysop))
196 continue;
197
198 if (db->boards[n].ftn_area[0] == '\0') {
199 memcpy(&lboards[nlboards], &db->boards[n],
200 sizeof(struct board));
201 nlboards++;
202 }
203 }
204
205 /* sort by board name */
206 for (i = 1; i < nlboards; i++) {
207 for (j = i; j > 0; j--) {
208 tboard = lboards[j];
209 if (strcmp(lboards[j].name, lboards[j - 1].name) > 0)
210 break;
211 tboard = lboards[j];
212 lboards[j] = lboards[j - 1];
213 lboards[j - 1] = tboard;
214 }
215 }
216
217 show_list = true;
218 show_help = false;
219 done = false;
220
221 while (!done && !s->ending) {
222 if (show_list) {
223 session_printf(s, "{{B}}%s{{/B}}\r\n", title);
224 session_printf(s, "%s # Board Description%s\r\n",
225 ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END));
226 session_flush(s);
227
228 for (n = 0; n < nlboards; n++) {
229 session_printf(s, "%2d %- 13.13s %s\r\n",
230 n + 1,
231 lboards[n].name,
232 lboards[n].description);
233 }
234 session_flush(s);
235
236 show_list = false;
237 }
238
239 c = session_menu(s, title, "Boards",
240 (char *)prompt_help, opts, nitems(opts), show_help, "Board #",
241 &an);
242 show_help = false;
243
244 switch (c) {
245 case 'l':
246 show_list = true;
247 break;
248 case '#':
249 if (an < 1 || an > nlboards) {
250 session_printf(s, "Invalid board\r\n");
251 session_flush(s);
252 break;
253 }
254 board_show(s, lboards[an - 1].id, "Boards");
255 break;
256 case '?':
257 show_help = true;
258 break;
259 default:
260 done = true;
261 break;
262 }
263 }
264
265 if (lboards != NULL)
266 xfree(&lboards);
267 }
268
269 void
270 board_list_ftn_areas(struct session *s)
271 {
272 static struct session_menu_option opts[] = {
273 { '#', "#", "Enter area number to read" },
274 { 'l', "Ll", "..." },
275 { 'q', "QqXx", "Return to main menu" },
276 { '?', "?", "List menu options" },
277 };
278 static const char prompt_help[] =
279 "#:View Area L:List Areas Q:Return ?:Help";
280 struct board *fboards = NULL, tboard;
281 struct fidopkt_address our_address;
282 size_t nfboards;
283 char title[50], latest[10];
284 char c;
285 short an, n, i, j;
286 bool done, show_list, show_help;
287
288 if (!fidopkt_parse_address(db->config.ftn_node_addr, &our_address)) {
289 session_printf(s, "{{B}}Error:{{/B}} FTN Areas are not supported "
290 "on this system.\r\n");
291 session_flush(s);
292 return;
293 }
294
295 if (db->nboards == 0) {
296 session_printf(s, "No %s areas defined.\n", db->config.ftn_network);
297 session_flush(s);
298 return;
299 }
300
301 snprintf(opts[1].title, sizeof(opts[1].title), "List %s Areas",
302 db->config.ftn_network);
303
304 fboards = xcalloc(sizeof(struct board), db->nboards);
305 if (fboards == NULL)
306 return;
307
308 nfboards = 0;
309 for (n = 0; n < db->nboards; n++) {
310 if (db->boards[n].restricted_viewing &&
311 !(s->user && s->user->is_sysop))
312 continue;
313
314 if (db->boards[n].ftn_area[0]) {
315 memcpy(&fboards[nfboards], &db->boards[n],
316 sizeof(struct board));
317 nfboards++;
318 }
319 }
320
321 /* sort by area name */
322 for (i = 1; i < nfboards; i++) {
323 for (j = i; j > 0; j--) {
324 if (strcmp(fboards[j].ftn_area, fboards[j - 1].ftn_area) > 0)
325 break;
326 tboard = fboards[j];
327 fboards[j] = fboards[j - 1];
328 fboards[j - 1] = tboard;
329 }
330 }
331
332 show_list = true;
333 show_help = false;
334 done = false;
335
336 snprintf(title, sizeof(title), "%s Areas (Local Node %s)",
337 db->config.ftn_network, db->config.ftn_node_addr);
338
339 while (!done && !s->ending) {
340 if (show_list) {
341 session_printf(s, "{{B}}%s{{/B}}\r\n", title);
342 session_printf(s, "%s # Latest Area Description%s\r\n",
343 ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END));
344 session_flush(s);
345
346 for (n = 0; n < nfboards; n++) {
347 if (fboards[n].last_post_at)
348 strftime(latest, sizeof(latest), "%b %d",
349 localtime(&fboards[n].last_post_at));
350 else
351 latest[0] = '\0';
352
353 session_printf(s, "%2d % 6s %- 13.13s %s\r\n",
354 n + 1,
355 latest,
356 fboards[n].ftn_area,
357 fboards[n].description);
358 }
359 session_flush(s);
360
361 show_list = false;
362 }
363
364 c = session_menu(s, title, db->config.ftn_network,
365 (char *)prompt_help, opts, nitems(opts), show_help, "Area #",
366 &an);
367 show_help = false;
368
369 switch (c) {
370 case 'l':
371 show_list = true;
372 break;
373 case '#':
374 if (an < 1 || an > nfboards) {
375 session_printf(s, "Invalid area\r\n");
376 session_flush(s);
377 break;
378 }
379 board_show(s, fboards[an - 1].id, db->config.ftn_network);
380 break;
381 case '?':
382 show_help = true;
383 break;
384 default:
385 done = true;
386 break;
387 }
388 }
389
390 if (fboards != NULL)
391 xfree(&fboards);
392 }
393
394 void
395 board_show(struct session *s, short id, char *prompt_prefix)
396 {
397 static const struct session_menu_option opts[] = {
398 { '#', "#", "Enter post to read" },
399 { '<', "<", "View newer posts" },
400 { 'l', "Ll", "List posts" },
401 { '>', ">", "View older posts" },
402 { 'p', "Pp", "Post new thread" },
403 { 'q', "QqXx", "Return to main menu" },
404 { '?', "?", "List menu options" },
405 };
406 static const char prompt_help[] =
407 "#:Read <:Newer >:Older L:List P:New Q:Return ?:Help";
408 char prompt[7 + member_size(struct board, name)];
409 struct board *board = NULL;
410 size_t n, nall_post_ids, page, pages, npost_ids;
411 unsigned long *post_ids = NULL;
412 short ppp, ret, pn;
413 char c, next_c;
414 bool done, find_post_ids, show_list, show_help;
415
416 for (n = 0; n < db->nboards; n++) {
417 if (db->boards[n].id == id) {
418 board = &db->boards[n];
419 break;
420 }
421 }
422
423 if (!board ||
424 (board->restricted_viewing && !(s->user && s->user->is_sysop))) {
425 session_printf(s, "Invalid board\r\n");
426 session_flush(s);
427 return;
428 }
429
430 page = 0;
431 find_post_ids = true;
432 show_list = true;
433 show_help = false;
434 done = false;
435 next_c = 0;
436
437 if (prompt_prefix == NULL)
438 prompt_prefix = "Boards";
439
440 while (!done && !s->ending) {
441 if (find_post_ids) {
442 if (post_ids != NULL) {
443 xfree(&post_ids);
444 post_ids = NULL;
445 }
446 ppp = POSTS_PER_PAGE;
447 if (s->terminal_lines < ppp + 3)
448 ppp = BOUND(ppp, 5, s->terminal_lines - 3);
449 nall_post_ids = board_find_post_ids(s, board, &npost_ids,
450 &post_ids, page * ppp, ppp);
451 /* ceil(nall_post_ids / ppp) */
452 pages = (nall_post_ids + ppp - 1) / ppp;
453
454 if (page >= pages)
455 page = pages - 1;
456
457 find_post_ids = false;
458 }
459
460 if (show_list) {
461 board_list_posts(s, board, npost_ids, post_ids, page + 1,
462 pages);
463 show_list = false;
464 }
465
466 snprintf(prompt, sizeof(prompt), "%s:%s:%ld", prompt_prefix,
467 board->name, page + 1);
468
469 if (next_c) {
470 c = next_c;
471 next_c = 0;
472 } else {
473 c = session_menu(s, board->description, prompt,
474 (char *)prompt_help, opts, nitems(opts), show_help, "Post #",
475 &pn);
476 show_help = false;
477 }
478
479 handle_opt:
480 switch (c) {
481 case 'l':
482 show_list = true;
483 break;
484 case 'p':
485 if (board_compose(s, board, NULL, NULL, NULL, NULL, NULL)) {
486 find_post_ids = true;
487 show_list = true;
488 }
489 break;
490 case '>':
491 case '<':
492 if (c == '>' && page == pages - 1) {
493 session_printf(s, "You are at the last page of posts\r\n");
494 session_flush(s);
495 break;
496 }
497 if (c == '<' && page == 0) {
498 session_printf(s, "You are already at the first page\r\n");
499 session_flush(s);
500 break;
501 }
502 if (c == '>')
503 page++;
504 else
505 page--;
506 find_post_ids = true;
507 show_list = true;
508 break;
509 case '#':
510 check_pn:
511 if (pn < 1 || pn > npost_ids) {
512 session_printf(s, "Invalid post\r\n");
513 session_flush(s);
514 break;
515 }
516 ret = board_post_read(s, board, prompt, post_ids[pn - 1], pn);
517 switch (ret) {
518 case POST_READ_RETURN_DONE:
519 break;
520 case POST_READ_RETURN_LIST:
521 show_list = true;
522 break;
523 case POST_READ_RETURN_FIND:
524 find_post_ids = true;
525 show_list = true;
526 break;
527 case POST_READ_RETURN_NEWER:
528 if (pn == 1) {
529 if (page == 0) {
530 session_printf(s, "No newer posts.\r\n");
531 session_flush(s);
532 break;
533 } else {
534 page--;
535 find_post_ids = true;
536 pn = npost_ids;
537 next_c = '#';
538 }
539 } else {
540 pn--;
541 goto check_pn;
542 }
543 break;
544 case POST_READ_RETURN_OLDER:
545 if (pn == npost_ids) {
546 if (page == pages - 1) {
547 session_printf(s, "No more posts.\r\n");
548 session_flush(s);
549 break;
550 } else {
551 page++;
552 find_post_ids = true;
553 pn = 1;
554 next_c = '#';
555 }
556 } else {
557 pn++;
558 goto check_pn;
559 }
560 break;
561 default:
562 c = ret;
563 goto handle_opt;
564 }
565 break;
566 case '?':
567 show_help = true;
568 break;
569 default:
570 done = true;
571 break;
572 }
573 }
574
575 if (post_ids != NULL)
576 xfree(&post_ids);
577 }
578
579 void
580 board_list_posts(struct session *s, struct board *board, size_t npost_ids,
581 unsigned long *post_ids, unsigned long page, unsigned long pages)
582 {
583 char time[24];
584 unsigned long indent_parent_ids[10] = { 0 };
585 char indent_s[22];
586 size_t n, size;
587 struct username_cache *user;
588 struct board_thread thread = { 0 };
589 struct board_post post;
590 struct board_ftn_post fpost;
591 short indent, j, k, ret;
592 char *data;
593
594 session_printf(s, "{{B}}%s: %s (Page %ld of %ld){{/B}}\r\n",
595 board->name, board->description, page, pages);
596 session_printf(s, "%s # N Date From Subject%s\r\n",
597 ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END));
598 session_flush(s);
599
600 if (npost_ids == 0) {
601 session_printf(s, "No posts here yet.\r\n");
602 session_flush(s);
603 return;
604 }
605
606 for (n = 0; n < npost_ids; n++) {
607 if (board->ftn_area[0]) {
608 size = bile_read_alloc(board->bile, BOARD_FTN_POST_RTYPE,
609 post_ids[n], &data);
610 ret = bile_unmarshall_object(board->bile,
611 board_ftn_post_object_fields, nboard_ftn_post_object_fields,
612 data, size, &fpost, sizeof(fpost), false);
613 xfree(&data);
614 if (ret == BILE_ERR_NO_MEMORY)
615 break;
616
617 strftime(time, sizeof(time), "%b %d", localtime(&fpost.time));
618
619 session_printf(s, "%s%2ld %c %s {{#}}%-10.10s %s%.50s%s\r\n",
620 true ? "" : ansi(s, ANSI_BOLD, ANSI_END),
621 n + 1,
622 true ? ' ' : 'N',
623 time,
624 fpost.from,
625 (fpost.reply[0] && strncasecmp(fpost.subject, "Re:", 3) != 0 ?
626 "Re: " : ""),
627 fpost.subject,
628 true ? "" : ansi(s, ANSI_RESET, ANSI_END));
629 } else {
630 size = bile_read_alloc(board->bile, BOARD_POST_RTYPE,
631 post_ids[n], &data);
632 ret = bile_unmarshall_object(board->bile,
633 board_post_object_fields, nboard_post_object_fields, data,
634 size, &post, sizeof(post), false);
635 xfree(&data);
636 if (ret != 0)
637 break;
638
639 if (post.thread_id != thread.thread_id) {
640 if (thread.thread_id) {
641 if (thread.subject != NULL)
642 xfree(&thread.subject);
643 if (thread.post_ids != NULL)
644 xfree(&thread.post_ids);
645 if (thread.parent_post_ids != NULL)
646 xfree(&thread.parent_post_ids);
647 }
648 size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE,
649 post.thread_id, &data);
650 ret = bile_unmarshall_object(board->bile,
651 board_thread_object_fields, nboard_thread_object_fields,
652 data, size, &thread, sizeof(thread), true);
653 xfree(&data);
654 if (ret == BILE_ERR_NO_MEMORY)
655 break;
656
657 for (j = 0; j < nitems(indent_parent_ids); j++)
658 indent_parent_ids[j] = 0;
659 }
660
661 user = user_username(post.sender_user_id);
662 strftime(time, sizeof(time), "%b %d", localtime(&post.time));
663
664 if (post.parent_post_id == 0) {
665 indent_s[0] = '\0';
666 } else {
667 indent = -1;
668 for (j = 0; j < nitems(indent_parent_ids); j++) {
669 if (indent_parent_ids[j] == post.parent_post_id ||
670 indent_parent_ids[j] == 0) {
671 indent = j;
672 indent_parent_ids[j] = post.parent_post_id;
673 for (k = j + 1; k < nitems(indent_parent_ids);
674 k++) {
675 if (indent_parent_ids[k] == 0)
676 break;
677 indent_parent_ids[k] = 0;
678 }
679 break;
680 }
681 }
682 if (indent == -1)
683 indent = nitems(indent_parent_ids) - 1;
684
685 for (j = 0; j < indent; j++) {
686 indent_s[j] = ' ';
687 }
688 indent_s[j] = '`';
689 indent_s[j + 1] = '-';
690 indent_s[j + 2] = '>';
691 indent_s[j + 3] = '\0';
692 }
693 session_printf(s, "%s%2ld %c %s %-10.10s %s{{#}}%.40s%s\r\n",
694 true ? "" : ansi(s, ANSI_BOLD, ANSI_END),
695 n + 1,
696 true ? ' ' : 'N',
697 time,
698 user ? user->username : "(unknown)",
699 post.parent_post_id != 0 && n == 0 ? "Re: " : "",
700 post.parent_post_id == 0 || n == 0 ? thread.subject : indent_s,
701 true ? "" : ansi(s, ANSI_RESET, ANSI_END));
702 }
703 }
704 session_flush(s);
705
706 if (thread.subject != NULL)
707 xfree(&thread.subject);
708 if (thread.post_ids != NULL)
709 xfree(&thread.post_ids);
710 if (thread.parent_post_ids != NULL)
711 xfree(&thread.parent_post_ids);
712 }
713
714 unsigned long
715 board_compose(struct session *s, struct board *board,
716 struct board_thread *thread, struct board_post *parent_post,
717 struct board_ftn_post *ftn_parent_post, char *initial_subject,
718 char *initial_body)
719 {
720 struct board_post post = { 0 };
721 char *tmp = NULL;
722 short c;
723
724 if (!s->user) {
725 session_printf(s, "Posting is not available to guests.\r\n"
726 "Please create an account first.\r\n");
727 session_flush(s);
728 return 0;
729 }
730
731 if (board->restricted_posting && !s->user->is_sysop) {
732 session_printf(s, "Posting to this board is not permitted.\r\n");
733 session_flush(s);
734 return 0;
735 }
736
737 if (initial_body) {
738 post.body = xstrdup(initial_body);
739 if (post.body == NULL)
740 return 0;
741 }
742
743 if (thread) {
744 post.thread_id = thread->thread_id;
745 post.parent_post_id = parent_post->id;
746 } else {
747 thread = xmalloczero(sizeof(struct board_thread));
748 if (thread == NULL)
749 return 0;
750 }
751 post.sender_user_id = s->user->id;
752 strlcpy(post.via, s->via, sizeof(post.via));
753
754 session_printf(s, "{{B}}Compose %s{{/B}}\r\n",
755 parent_post ? "Reply" : "New Post");
756 session_printf(s, "{{B}}From:{{/B}} %s (via %s)\r\n",
757 s->user->username, s->via);
758 session_printf(s, "{{B}}To:{{/B}} %s\r\n", board->name);
759
760 post_compose_start:
761 if (parent_post) {
762 session_printf(s, "{{B}}Subject:{{/B}}{{#}} Re: %s\r\n",
763 thread->subject);
764 session_flush(s);
765 } else if (ftn_parent_post) {
766 session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s%s\r\n",
767 strncasecmp(ftn_parent_post->subject, "Re:", 3) == 0 ?
768 "" : "Re: ", ftn_parent_post->subject);
769 session_flush(s);
770 } else {
771 if (initial_subject && !thread->subject) {
772 thread->subject = xstrdup(initial_subject);
773 if (thread->subject == NULL)
774 return 0;
775 }
776
777 for (;;) {
778 session_printf(s, "{{B}}Subject:{{/B}} ");
779 session_flush(s);
780
781 tmp = session_field_input(s, 100, 50, thread->subject,
782 false, 0);
783 if (thread->subject != NULL)
784 xfree(&thread->subject);
785 thread->subject = tmp;
786 session_output(s, "\r\n", 2);
787 session_flush(s);
788
789 if (thread->subject == NULL)
790 goto post_compose_done;
791
792 rtrim(thread->subject, "\r\n\t ");
793
794 if (thread->subject[0] == '\0') {
795 session_printf(s, "{{B}}Error:{{/B}} Subject "
796 "cannot be blank (^C to cancel)\r\n");
797 session_flush(s);
798 xfree(&thread->subject);
799 continue;
800 }
801 thread->subject_size = strlen(thread->subject) + 1;
802 break;
803 }
804 }
805
806 for (;;) {
807 session_printf(s, "{{B}}Message (^D when finished):{{/B}}\r\n");
808 session_flush(s);
809
810 tmp = session_field_input(s, 2048, s->terminal_columns - 1,
811 post.body, true, 0);
812 if (post.body != NULL)
813 xfree(&post.body);
814 post.body = tmp;
815 session_output(s, "\r\n", 2);
816 session_flush(s);
817
818 if (post.body == NULL)
819 goto post_compose_done;
820
821 rtrim(post.body, "\r\n\t ");
822
823 if (post.body[0] == '\0') {
824 xfree(&post.body);
825 goto post_compose_done;
826 }
827 post.body_size = strlen(post.body) + 1;
828 break;
829 }
830
831 for (;;) {
832 session_printf(s, "\r\n{{B}}(P){{/B}}ost message, "
833 "{{B}}(E){{/B}}dit again, or {{B}}(C){{/B}}ancel? ");
834 session_flush(s);
835
836 c = session_input_char(s);
837 if (c == 0 || s->ending)
838 goto post_compose_done;
839
840 switch (c) {
841 case 'p':
842 case 'P':
843 case 'y':
844 session_printf(s, "%c\r\n", c);
845 session_flush(s);
846 /* FALLTHROUGH */
847 case '\n':
848 case '\r':
849 /* send */
850 session_printf(s, "Posting message... ");
851 session_flush(s);
852
853 if (board_post_create(board, thread, ftn_parent_post,
854 &post) == 0) {
855 session_logf(s, "Posted message %ld to %s", post.id,
856 board->name);
857 session_printf(s, "done\r\n");
858 } else
859 session_printf(s, "failed!\r\n");
860
861 session_flush(s);
862
863 goto post_compose_done;
864 case 'e':
865 case 'E':
866 session_printf(s, "%c\r\n", c);
867 session_flush(s);
868 goto post_compose_start;
869 case 'c':
870 case 'C':
871 session_printf(s, "%c\r\n", c);
872 session_flush(s);
873 /* FALLTHROUGH */
874 case CONTROL_C:
875 goto post_compose_done;
876 }
877 }
878
879 post_compose_error:
880 session_printf(s, "Failed saving message!\r\n");
881 session_flush(s);
882
883 post_compose_done:
884 if (parent_post == NULL) {
885 if (thread->subject != NULL)
886 xfree(&thread->subject);
887 xfree(&thread);
888 }
889 if (post.body)
890 xfree(&post.body);
891
892 return post.id;
893 }
894
895 short
896 board_post_read(struct session *s, struct board *board, char *prompt_prefix,
897 unsigned long id, short idx)
898 {
899 static const struct session_menu_option opts[] = {
900 { '<', "<Nn", "Read newer post" },
901 { '>', ">Pp", "Read older post" },
902 { 'r', "Rr", "Reply to this post" },
903 { 's', "Ss", "Show this post" },
904 { 'd', "Dd", "Delete this post" },
905 { 'l', "Ll", "List posts" },
906 { 'q', "QqXx", "Return to threads" },
907 { '?', "?", "List these options" },
908 };
909 static const char prompt_help[] =
910 "<:Newer >:Older R:Reply S:Show D:Delete L:List Q:Return ?:Help";
911 char time[32], prompt[7 + member_size(struct board, name) + 8];
912 struct board_thread thread = { 0 };
913 struct board_post post = { 0 };
914 struct board_ftn_post fpost = { 0 };
915 struct username_cache *sender;
916 struct session_menu_option *dopts = NULL;
917 size_t size, plain_post_size, j;
918 short ret = POST_READ_RETURN_DONE;
919 char c;
920 char *data, *subject, *plain_post, *tplain_post;
921 short cc, bcret, n;
922 bool done = false, show_help = false;
923
924 dopts = xmalloc(sizeof(opts));
925 if (dopts == NULL)
926 return 0;
927 memcpy(dopts, opts, sizeof(opts));
928
929 show_post:
930 if (fpost.body != NULL)
931 xfree(&fpost.body);
932 if (post.body != NULL)
933 xfree(&post.body);
934 if (thread.subject != NULL)
935 xfree(&thread.subject);
936 if (thread.post_ids != NULL)
937 xfree(&thread.post_ids);
938 if (thread.parent_post_ids != NULL)
939 xfree(&thread.parent_post_ids);
940 if (board->ftn_area[0]) {
941 size = bile_read_alloc(board->bile, BOARD_FTN_POST_RTYPE, id,
942 &data);
943 if (size == 0)
944 panic("failed fetching message %ld: %d", id,
945 bile_error(board->bile));
946
947 ret = bile_unmarshall_object(board->bile,
948 board_ftn_post_object_fields, nboard_ftn_post_object_fields,
949 data, size, &fpost, sizeof(fpost), true);
950 xfree(&data);
951 if (ret == BILE_ERR_NO_MEMORY)
952 goto done_reading;
953
954 if (!(s->user && s->user->is_sysop)) {
955 /* disable deleting */
956 for (n = 0; n < nitems(opts); n++) {
957 if (dopts[n].ret == 'd') {
958 dopts[n].key[0] = '\0';
959 break;
960 }
961 }
962 }
963
964 strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S",
965 localtime(&fpost.time));
966
967 session_printf(s, "{{B}}From:{{/B}}{{#}} %s\r\n", fpost.from);
968 session_printf(s, "{{B}}Origin:{{/B}}{{#}} %s\r\n", fpost.origin);
969 session_printf(s, "{{B}}To:{{/B}}{{#}} %s@%s\r\n", fpost.to,
970 board->name);
971 session_printf(s, "{{B}}Date:{{/B}}{{#}} %s %s\r\n", time,
972 db->config.timezone);
973 session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s%s\r\n",
974 (fpost.reply[0] && strncasecmp(fpost.subject, "Re:", 3) != 0 ?
975 "Re: " : ""), fpost.subject);
976 session_printf(s, "\r\n");
977 session_flush(s);
978
979 plain_post_size = 0;
980 plain_post = xmalloc(fpost.body_size);
981 if (plain_post == NULL)
982 session_paginate(s, fpost.body, fpost.body_size, 6);
983 else {
984 /* strip out renegade-style pipe color codes ("abc|01def") */
985 for (j = 0; j < fpost.body_size; j++) {
986 if (fpost.body[j] == '|' && j + 2 < fpost.body_size &&
987 fpost.body[j + 1] >= '0' && fpost.body[j + 1] <= '9' &&
988 fpost.body[j + 2] >= '0' && fpost.body[j + 2] <= '9') {
989 j += 2;
990 continue;
991 }
992 plain_post[plain_post_size++] = fpost.body[j];
993 }
994 tplain_post = xrealloc(plain_post, plain_post_size);
995 if (tplain_post != NULL)
996 plain_post = tplain_post;
997 session_paginate(s, plain_post, plain_post_size, 6);
998 xfree(&plain_post);
999 }
1000 } else {
1001 size = bile_read_alloc(board->bile, BOARD_POST_RTYPE, id, &data);
1002 if (size == 0)
1003 panic("failed fetching message %ld: %d", id,
1004 bile_error(board->bile));
1005 ret = bile_unmarshall_object(board->bile, board_post_object_fields,
1006 nboard_post_object_fields, data, size, &post, sizeof(post), true);
1007 xfree(&data);
1008 if (ret == BILE_ERR_NO_MEMORY)
1009 goto done_reading;
1010
1011 size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE,
1012 post.thread_id, &data);
1013 if (size == 0)
1014 panic("failed fetching thread %ld: %d", post.thread_id,
1015 bile_error(board->bile));
1016 ret = bile_unmarshall_object(board->bile, board_thread_object_fields,
1017 nboard_thread_object_fields, data, size, &thread,
1018 sizeof(thread), true);
1019 xfree(&data);
1020 if (ret == BILE_ERR_NO_MEMORY)
1021 goto done_reading;
1022
1023 if (!(s->user && (s->user->is_sysop ||
1024 s->user->id == post.sender_user_id))) {
1025 /* disable deleting */
1026 for (n = 0; n < nitems(opts); n++) {
1027 if (dopts[n].ret == 'd') {
1028 dopts[n].key[0] = '\0';
1029 break;
1030 }
1031 }
1032 }
1033
1034 sender = user_username(post.sender_user_id);
1035
1036 strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S",
1037 localtime(&post.time));
1038
1039 session_printf(s, "{{B}}From:{{/B}}{{#}} %s",
1040 sender ? sender->username : "(unknown)");
1041 if (post.via[0])
1042 session_printf(s, " (via %s)", post.via);
1043 session_printf(s, "\r\n");
1044 session_printf(s, "{{B}}To:{{/B}}{{#}} %s\r\n", board->name);
1045 session_printf(s, "{{B}}Date:{{/B}}{{#}} %s %s\r\n", time,
1046 db->config.timezone);
1047 session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s%s\r\n",
1048 (post.parent_post_id ? "Re: " : ""), thread.subject);
1049 session_printf(s, "\r\n");
1050 session_flush(s);
1051 session_paginate(s, post.body, post.body_size, 5);
1052 }
1053
1054 snprintf(prompt, sizeof(prompt), "%s:%d", prompt_prefix, idx);
1055
1056 if (board->ftn_area[0])
1057 subject = fpost.subject;
1058 else
1059 subject = thread.subject;
1060
1061 while (!done && !s->ending) {
1062 c = session_menu(s, subject, prompt, (char *)prompt_help, dopts,
1063 nitems(opts), show_help, NULL, NULL);
1064 show_help = false;
1065
1066 switch (c) {
1067 case 'd':
1068 if (!(s->user && (s->user->is_sysop ||
1069 s->user->id == post.sender_user_id))) {
1070 session_printf(s, "Invalid option\r\n");
1071 session_flush(s);
1072 break;
1073 }
1074
1075 session_printf(s, "Are you sure you want to permanently "
1076 "delete this post? [y/N] ");
1077 session_flush(s);
1078
1079 cc = session_input_char(s);
1080 if (cc == 'y' || cc == 'Y') {
1081 session_printf(s, "%c\r\n", cc);
1082 session_flush(s);
1083
1084 if (board->ftn_area[0]) {
1085 board_delete_ftn_post(board, &fpost);
1086 session_logf(s, "Deleted %s post %ld",
1087 db->config.ftn_network, fpost.id);
1088 } else {
1089 board_delete_post(board, &post, &thread);
1090 session_logf(s, "Deleted post %ld (thread %ld)",
1091 post.id, thread.thread_id);
1092 }
1093
1094 session_printf(s, "\r\n{{B}}Post deleted!{{/B}}\r\n");
1095 session_flush(s);
1096 ret = POST_READ_RETURN_FIND;
1097 done = true;
1098 } else {
1099 session_printf(s, "\r\nPost not deleted.\r\n");
1100 session_flush(s);
1101 }
1102 break;
1103 case 'r':
1104 if (board->ftn_area[0])
1105 bcret = board_compose(s, board, NULL, NULL, &fpost, NULL,
1106 NULL);
1107 else
1108 bcret = board_compose(s, board, &thread, &post, NULL, NULL,
1109 NULL);
1110
1111 if (bcret) {
1112 ret = POST_READ_RETURN_FIND;
1113 done = true;
1114 }
1115 break;
1116 case 's':
1117 goto show_post;
1118 break;
1119 case '<':
1120 ret = POST_READ_RETURN_NEWER;
1121 done = true;
1122 break;
1123 case '>':
1124 ret = POST_READ_RETURN_OLDER;
1125 done = true;
1126 break;
1127 case 'l':
1128 ret = POST_READ_RETURN_LIST;
1129 done = true;
1130 break;
1131 case 'q':
1132 ret = POST_READ_RETURN_DONE;
1133 done = true;
1134 break;
1135 case '?':
1136 show_help = true;
1137 break;
1138 }
1139 }
1140
1141 done_reading:
1142 xfree(&dopts);
1143
1144 if (board->ftn_area[0]) {
1145 if (fpost.body != NULL)
1146 xfree(&fpost.body);
1147 } else {
1148 if (post.body != NULL)
1149 xfree(&post.body);
1150 if (thread.subject != NULL)
1151 xfree(&thread.subject);
1152 if (thread.post_ids != NULL)
1153 xfree(&thread.post_ids);
1154 if (thread.parent_post_ids != NULL)
1155 xfree(&thread.parent_post_ids);
1156 }
1157
1158 return ret;
1159 }
1160
1161 size_t
1162 board_find_post_ids(struct session *s, struct board *board,
1163 size_t *npost_ids, unsigned long **post_ids, size_t offset, size_t limit)
1164 {
1165 struct board_id_time_map *all_post_id_map = NULL;
1166 size_t n, size, nall_post_ids;
1167
1168 *post_ids = NULL;
1169 *npost_ids = 0;
1170
1171 if (board->need_reindex) {
1172 session_printf(s, "%sPlease wait, re-indexing board posts...%s",
1173 ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END));
1174 session_flush(s);
1175 /* hopefully periodic_jobs is working on it */
1176 while (board->need_reindex)
1177 uthread_yield();
1178 session_output(s, "\r\n", 2);
1179 session_flush(s);
1180 }
1181
1182 size = bile_read_alloc(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1,
1183 &all_post_id_map);
1184 if (all_post_id_map == NULL)
1185 return 0;
1186
1187 nall_post_ids = size / sizeof(struct board_id_time_map);
1188
1189 *post_ids = xcalloc(sizeof(long), MIN(limit, nall_post_ids));
1190 if (*post_ids == NULL) {
1191 if (all_post_id_map != NULL)
1192 xfree(&all_post_id_map);
1193 return 0;
1194 }
1195
1196 for (n = 0; n < nall_post_ids; n++) {
1197 if (n < offset)
1198 continue;
1199
1200 (*post_ids)[*npost_ids] = all_post_id_map[n].id;
1201 (*npost_ids)++;
1202
1203 if (*npost_ids >= limit)
1204 break;
1205 }
1206
1207 if (all_post_id_map != NULL)
1208 xfree(&all_post_id_map);
1209
1210 return nall_post_ids;
1211 }
1212
1213 short
1214 board_post_create(struct board *board, struct board_thread *thread,
1215 struct board_ftn_post *ftn_parent_post, struct board_post *post)
1216 {
1217 struct board_ftn_post ftn_post = { 0 };
1218 struct fidopkt_message fidomsg = { 0 };
1219 struct username_cache *user;
1220 struct fidopkt_address our_address, hub_address;
1221 short ret;
1222 char *data;
1223 size_t size, insert;
1224 ssize_t n, j;
1225 unsigned long *post_ids, *parent_post_ids;
1226
1227 if (board->ftn_area[0]) {
1228 if (!post->id)
1229 post->id = bile_next_id(board->bile, BOARD_FTN_POST_RTYPE);
1230 if (!post->time)
1231 post->time = Time;
1232
1233 if (!fidopkt_parse_address(db->config.ftn_node_addr,
1234 &our_address)) {
1235 logger_printf("[board] Invalid FTN local node address, can't "
1236 "create board post");
1237 post->id = 0;
1238 goto done;
1239 }
1240 if (!fidopkt_parse_address(db->config.ftn_hub_addr,
1241 &hub_address)) {
1242 logger_printf("[board] Invalid FTN hub address, can't "
1243 "create board post");
1244 post->id = 0;
1245 goto done;
1246 }
1247
1248 ftn_post.id = post->id;
1249 ftn_post.time = post->time;
1250
1251 if (ftn_parent_post) {
1252 snprintf(ftn_post.subject, sizeof(ftn_post.subject),
1253 "%s%s",
1254 strncasecmp(ftn_parent_post->subject, "Re:", 3) == 0 ?
1255 "" : "Re: ", ftn_parent_post->subject);
1256 strlcpy(ftn_post.reply, ftn_parent_post->msgid_orig,
1257 sizeof(ftn_post.reply));
1258 strlcpy(ftn_post.to, ftn_parent_post->from,
1259 sizeof(ftn_post.to));
1260 } else {
1261 strlcpy(ftn_post.subject, thread->subject,
1262 sizeof(ftn_post.subject));
1263 strlcpy(ftn_post.to, "All", sizeof(ftn_post.to));
1264 }
1265
1266 user = user_username(post->sender_user_id);
1267 if (user == NULL) {
1268 logger_printf("[board] Can't find username of user posting "
1269 "new message");
1270 post->id = 0;
1271 goto done;
1272 }
1273 strlcpy(ftn_post.from, user->username, sizeof(ftn_post.from));
1274
1275 ftn_post.body = post->body;
1276 ftn_post.body_size = post->body_size;
1277
1278 /* make each board's posts have ids unique to our zone/net/node */
1279 ftn_post.msgid.id = 0x10000000 |
1280 ((unsigned long)our_address.node << 24) |
1281 ((unsigned long)(board->id) << 16) | post->id;
1282 ftn_post.msgid.zone = our_address.zone;
1283 ftn_post.msgid.net = our_address.net;
1284 ftn_post.msgid.node = our_address.node;
1285 ftn_post.msgid.point = our_address.point;
1286
1287 snprintf(ftn_post.origin, sizeof(ftn_post.origin),
1288 "%s | %s (%s)",
1289 db->config.name, db->config.hostname,
1290 db->config.ftn_node_addr);
1291
1292 fidomsg.time = ftn_post.time;
1293 memcpy(&fidomsg.header.orig, &our_address,
1294 sizeof(fidomsg.header.orig));
1295 memcpy(&fidomsg.header.dest, &hub_address,
1296 sizeof(fidomsg.header.dest));
1297 strlcpy(fidomsg.area, board->ftn_area, sizeof(fidomsg.area));
1298 strlcpy(fidomsg.to, ftn_post.to, sizeof(fidomsg.to));
1299 strlcpy(fidomsg.from, ftn_post.from, sizeof(fidomsg.from));
1300 strlcpy(fidomsg.subject, ftn_post.subject, sizeof(fidomsg.subject));
1301 fidomsg.body = ftn_post.body;
1302 fidomsg.body_len = ftn_post.body_size - 1;
1303 strlcpy(fidomsg.reply, ftn_post.reply, sizeof(fidomsg.reply));
1304 memcpy(&fidomsg.msgid, &ftn_post.msgid, sizeof(fidomsg.msgid));
1305 strlcpy(fidomsg.origin, ftn_post.origin, sizeof(fidomsg.origin));
1306
1307 if (!binkp_scan_message(&fidomsg)) {
1308 logger_printf("[board] Failed scanning new FTN message being "
1309 "posted");
1310 post->id = 0;
1311 goto done;
1312 }
1313
1314 if (board_toss_ftn_message(board, &fidomsg, true) != 1) {
1315 logger_printf("[board] Failed tossing new FTN message being "
1316 "posted");
1317 post->id = 0;
1318 goto done;
1319 }
1320 } else {
1321 if (!post->id)
1322 post->id = bile_next_id(board->bile, BOARD_POST_RTYPE);
1323 if (!post->time)
1324 post->time = Time;
1325
1326 if (post->parent_post_id == 0) {
1327 thread->thread_id = bile_next_id(board->bile,
1328 BOARD_THREAD_RTYPE);
1329 post->thread_id = thread->thread_id;
1330 }
1331
1332 ret = bile_marshall_object(board->bile, board_post_object_fields,
1333 nboard_post_object_fields, post, &data, &size);
1334 if (ret != 0 || size == 0) {
1335 logger_printf("[board] Failed to marshall new post object");
1336 post->id = 0;
1337 goto done;
1338 }
1339 if (bile_write(board->bile, BOARD_POST_RTYPE, post->id, data,
1340 size) != size) {
1341 warn("bile_write of new post failed! %d",
1342 bile_error(board->bile));
1343 post->id = 0;
1344 xfree(&data);
1345 goto done;
1346 }
1347 xfree(&data);
1348
1349 if (post->time > thread->last_post_at)
1350 thread->last_post_at = post->time;
1351 thread->nposts++;
1352 post_ids = xreallocarray(thread->post_ids, thread->nposts,
1353 sizeof(long));
1354 if (post_ids == NULL)
1355 return 0;
1356 parent_post_ids = xreallocarray(thread->parent_post_ids,
1357 thread->nposts, sizeof(long));
1358 if (parent_post_ids == NULL)
1359 return 0;
1360 thread->post_ids = post_ids;
1361 thread->parent_post_ids = parent_post_ids;
1362
1363 /*
1364 * Add new post id to thread post_ids, but put it in the right
1365 * place so that reading post_ids will present the tree in order.
1366 * Walk parent_post_ids and find the first match of our parent,
1367 * then insert our new post there. This puts newest replies at
1368 * the top.
1369 */
1370
1371 insert = thread->nposts - 1;
1372 for (n = 0; n < thread->nposts - 1; n++) {
1373 if (thread->post_ids[n] != post->parent_post_id)
1374 continue;
1375
1376 for (j = thread->nposts - 2; j > n; j--) {
1377 thread->post_ids[j + 1] = thread->post_ids[j];
1378 thread->parent_post_ids[j + 1] =
1379 thread->parent_post_ids[j];
1380 }
1381 insert = n + 1;
1382 break;
1383 }
1384 thread->post_ids[insert] = post->id;
1385 thread->parent_post_ids[insert] = post->parent_post_id;
1386
1387 ret = bile_marshall_object(board->bile, board_thread_object_fields,
1388 nboard_thread_object_fields, thread, &data, &size);
1389 if (ret != 0 || size == 0) {
1390 logger_printf("[board] Failed to marshall new thread object");
1391 post->id = 0;
1392 goto done;
1393 }
1394 if (bile_write(board->bile, BOARD_THREAD_RTYPE, thread->thread_id,
1395 data, size) != size) {
1396 warn("bile_write of thread failed! %d",
1397 bile_error(board->bile));
1398 post->id = 0;
1399 xfree(&data);
1400 goto done;
1401 }
1402 xfree(&data);
1403
1404 /* it would be nice not to have to rebuild this every time... */
1405 board_delete_cached_index(board);
1406 bile_flush(board->bile, true);
1407
1408 if (post->time > board->last_post_at)
1409 board->last_post_at = post->time;
1410 }
1411
1412 done:
1413 return (post->id == 0 ? -1 : 0);
1414 }
1415
1416 void
1417 board_delete_post(struct board *board, struct board_post *post,
1418 struct board_thread *thread)
1419 {
1420 size_t size, n, nn;
1421 char *data, *body;
1422 char del[50];
1423 short ret;
1424 bool childs = false;
1425 unsigned long *new_post_ids;
1426 unsigned long *new_parent_post_ids;
1427
1428 if (thread->nposts == 1) {
1429 bile_delete(board->bile, BOARD_THREAD_RTYPE, thread->thread_id,
1430 0);
1431 bile_delete(board->bile, BOARD_POST_RTYPE, post->id,
1432 BILE_DELETE_FLAG_PURGE);
1433 board_delete_cached_index(board);
1434 return;
1435 }
1436
1437 for (n = 0; n < thread->nposts; n++) {
1438 if (thread->parent_post_ids[n] == post->id) {
1439 childs = true;
1440 break;
1441 }
1442 }
1443
1444 if (!childs) {
1445 /* just zap this off the end of the tree */
1446 new_post_ids = xcalloc(thread->nposts - 1, sizeof(unsigned long));
1447 if (new_post_ids == NULL)
1448 return;
1449 new_parent_post_ids = xcalloc(thread->nposts - 1,
1450 sizeof(unsigned long));
1451 if (new_parent_post_ids == NULL) {
1452 xfree(&new_post_ids);
1453 return;
1454 }
1455
1456 for (n = 0, nn = 0; n < thread->nposts; n++) {
1457 if (thread->post_ids[n] == post->id)
1458 continue;
1459
1460 new_post_ids[nn] = thread->post_ids[n];
1461 nn++;
1462 }
1463
1464 for (n = 0, nn = 0; n < thread->nposts; n++) {
1465 if (thread->post_ids[n] == post->id)
1466 continue;
1467
1468 new_parent_post_ids[nn] = thread->parent_post_ids[n];
1469 nn++;
1470 }
1471
1472 thread->nposts--;
1473
1474 xfree(&thread->post_ids);
1475 thread->post_ids = new_post_ids;
1476 xfree(&thread->parent_post_ids);
1477 thread->parent_post_ids = new_parent_post_ids;
1478
1479 ret = bile_marshall_object(board->bile, board_thread_object_fields,
1480 nboard_thread_object_fields, thread, &data, &size);
1481 if (ret != 0 || size == 0) {
1482 logger_printf("[board] Failed to marshall thread object "
1483 "during post deletion");
1484 return;
1485 }
1486 if (bile_write(board->bile, BOARD_THREAD_RTYPE, thread->thread_id,
1487 data, size) != size) {
1488 warn("bile_write of updated thread after post delete failed! "
1489 "%d", bile_error(board->bile));
1490 xfree(&data);
1491 return;
1492 }
1493 xfree(&data);
1494
1495 bile_delete(board->bile, BOARD_POST_RTYPE, post->id,
1496 BILE_DELETE_FLAG_ZERO | BILE_DELETE_FLAG_PURGE);
1497 board_delete_cached_index(board);
1498
1499 bile_flush(board->bile, true);
1500 return;
1501 }
1502
1503 /* all we can do is change the post to say deleted */
1504 if (post->body != NULL)
1505 xfree(&post->body);
1506 snprintf(del, sizeof(del), "[ Post deleted ]");
1507 body = xstrdup(del);
1508 if (body == NULL)
1509 return;
1510 post->body = body;
1511 post->body_size = strlen(post->body) + 1;
1512
1513 ret = bile_marshall_object(board->bile, board_post_object_fields,
1514 nboard_post_object_fields, post, &data, &size);
1515 if (ret != 0 || size == 0) {
1516 logger_printf("[board] Failed to marshall updated post object");
1517 return;
1518 }
1519 if (bile_write(board->bile, BOARD_POST_RTYPE, post->id, data,
1520 size) != size) {
1521 warn("bile_write of updated post failed! %d",
1522 bile_error(board->bile));
1523 xfree(&data);
1524 return;
1525 }
1526
1527 xfree(&data);
1528 }
1529
1530 void
1531 board_delete_ftn_post(struct board *board, struct board_ftn_post *post)
1532 {
1533 size_t size, npost_ids, n;
1534 struct board_id_time_map *id_map;
1535
1536 bile_delete(board->bile, BOARD_FTN_POST_RTYPE, post->id,
1537 BILE_DELETE_FLAG_PURGE);
1538
1539 size = bile_read_alloc(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1,
1540 &id_map);
1541 if (size == 0 || id_map == NULL)
1542 return;
1543
1544 npost_ids = size / sizeof(struct board_id_time_map);
1545
1546 for (n = 0; n < npost_ids; n++) {
1547 if (id_map[n].id == post->id) {
1548 for (; n < npost_ids - 1; n++)
1549 id_map[n] = id_map[n + 1];
1550 npost_ids--;
1551 break;
1552 }
1553 }
1554
1555 if (npost_ids == 0)
1556 board_delete_cached_index(board);
1557 else
1558 bile_write(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, id_map,
1559 sizeof(struct board_id_time_map) * npost_ids);
1560
1561 board->need_reindex = false;
1562
1563 /* TODO: delete from msgid cache too */
1564
1565 xfree(&id_map);
1566 }
1567
1568 size_t
1569 board_index_sorted_post_ids(struct board *board,
1570 struct board_id_time_map **sorted_id_map)
1571 {
1572 struct thread_time_map {
1573 unsigned long id;
1574 time_t time;
1575 size_t nposts;
1576 };
1577 struct board_ftn_post fpost;
1578 struct board_post post;
1579 struct board_thread thread;
1580 size_t ret, size, i, j, n, npost_ids, nthread_ids;
1581 unsigned long *post_ids = NULL, *thread_ids = NULL;
1582 struct board_id_time_map *id_map = NULL, tmp_id_map;
1583 struct thread_time_map *thread_map = NULL, tmp_thread_map;
1584 char *data;
1585 bool failed = false;
1586
1587 if (board->ftn_area[0]) {
1588 npost_ids = bile_ids_by_type(board->bile, BOARD_FTN_POST_RTYPE,
1589 &post_ids);
1590 if (npost_ids == 0)
1591 goto write_out;
1592
1593 id_map = xcalloc(sizeof(struct board_id_time_map), npost_ids);
1594 if (id_map == NULL) {
1595 failed = true;
1596 goto write_out;
1597 }
1598
1599 uthread_yield();
1600
1601 for (n = 0; n < npost_ids; n++) {
1602 /* only read as far as the time */
1603 ret = bile_read(board->bile, BOARD_FTN_POST_RTYPE,
1604 post_ids[n], &fpost,
1605 offsetof(struct board_ftn_post, time) +
1606 member_size(struct board_ftn_post, time));
1607 if (ret == 0) {
1608 failed = true;
1609 goto write_out;
1610 }
1611
1612 id_map[n].id = fpost.id;
1613 id_map[n].time = fpost.time;
1614 }
1615
1616 xfree(&post_ids);
1617
1618 uthread_yield();
1619
1620 /* sort by date descending */
1621 for (i = 1; i < npost_ids; i++) {
1622 for (j = i; j > 0; j--) {
1623 if (id_map[j].time < id_map[j - 1].time)
1624 break;
1625 tmp_id_map = id_map[j];
1626 id_map[j] = id_map[j - 1];
1627 id_map[j - 1] = tmp_id_map;
1628 }
1629 }
1630 } else {
1631 npost_ids = 0;
1632 nthread_ids = bile_ids_by_type(board->bile, BOARD_THREAD_RTYPE,
1633 &thread_ids);
1634 if (nthread_ids == 0)
1635 goto write_out;
1636
1637 thread_map = xcalloc(sizeof(struct thread_time_map), nthread_ids);
1638 if (thread_map == NULL) {
1639 failed = true;
1640 goto write_out;
1641 }
1642
1643 uthread_yield();
1644
1645 for (n = 0; n < nthread_ids; n++) {
1646 size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE,
1647 thread_ids[n], &data);
1648 ret = bile_unmarshall_object(board->bile,
1649 board_thread_object_fields, nboard_thread_object_fields, data,
1650 size, &thread, sizeof(thread), false);
1651 xfree(&data);
1652 if (ret != 0) {
1653 failed = true;
1654 goto write_out;
1655 }
1656
1657 thread_map[n].id = thread.thread_id;
1658 thread_map[n].time = thread.last_post_at;
1659 thread_map[n].nposts = thread.nposts;
1660 npost_ids += thread.nposts;
1661 }
1662
1663 xfree(&thread_ids);
1664
1665 uthread_yield();
1666
1667 /* sort by last post date descending */
1668 for (i = 1; i < nthread_ids; i++) {
1669 for (j = i; j > 0; j--) {
1670 if (thread_map[j].time < thread_map[j - 1].time)
1671 break;
1672 tmp_thread_map = thread_map[j];
1673 thread_map[j] = thread_map[j - 1];
1674 thread_map[j - 1] = tmp_thread_map;
1675 }
1676 }
1677
1678 uthread_yield();
1679
1680 id_map = xcalloc(sizeof(struct board_id_time_map), npost_ids);
1681 if (id_map == NULL) {
1682 failed = true;
1683 goto write_out;
1684 }
1685
1686 npost_ids = 0;
1687 for (j = 0; j < nthread_ids; j++) {
1688 size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE,
1689 thread_map[j].id, &data);
1690 if (data == NULL) {
1691 failed = true;
1692 goto write_out;
1693 }
1694 ret = bile_unmarshall_object(board->bile,
1695 board_thread_object_fields, nboard_thread_object_fields, data,
1696 size, &thread, sizeof(thread), true);
1697 xfree(&data);
1698 if (ret != 0) {
1699 failed = true;
1700 goto write_out;
1701 }
1702
1703 /* these are already sorted, and we want to keep thread sort */
1704 for (i = 0; i < thread.nposts; i++) {
1705 /* only read as far as the time */
1706 size = bile_read(board->bile, BOARD_POST_RTYPE,
1707 thread.post_ids[i], &post,
1708 offsetof(struct board_post, time) +
1709 member_size(struct board_post, time));
1710 if (size == 0) {
1711 logger_printf("[board] Board %s thread %ld post %ld "
1712 "is missing", board->name, thread.thread_id,
1713 thread.post_ids[i]);
1714 continue;
1715 }
1716
1717 id_map[npost_ids].id = post.id;
1718 id_map[npost_ids].time = post.time;
1719 npost_ids++;
1720 }
1721
1722 if (thread.subject != NULL)
1723 xfree(&thread.subject);
1724 if (thread.post_ids != NULL)
1725 xfree(&thread.post_ids);
1726 if (thread.parent_post_ids != NULL)
1727 xfree(&thread.parent_post_ids);
1728 }
1729
1730 xfree(&thread_map);
1731 }
1732
1733 logger_printf("[board] Reindexed %ld post%s on board %s", npost_ids,
1734 npost_ids == 1 ? "" : "s", board->name);
1735
1736 write_out:
1737 if (thread_map)
1738 xfree(&thread_map);
1739 if (thread_ids)
1740 xfree(&thread_ids);
1741 if (post_ids)
1742 xfree(&post_ids);
1743
1744 if (npost_ids == 0 || id_map == NULL || failed) {
1745 if (failed)
1746 logger_printf("[board] Failed reindexing board %s",
1747 board->name);
1748 board_delete_cached_index(board);
1749 if (!failed)
1750 board->need_reindex = false;
1751 if (sorted_id_map != NULL)
1752 *sorted_id_map = NULL;
1753 return 0;
1754 }
1755
1756 bile_write(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, id_map,
1757 sizeof(struct board_id_time_map) * npost_ids);
1758 if (sorted_id_map == NULL)
1759 xfree(&id_map);
1760 else
1761 *sorted_id_map = id_map;
1762 board->need_reindex = false;
1763 return npost_ids;
1764 }
1765
1766 short
1767 board_toss_ftn_message(struct board *board,
1768 struct fidopkt_message *fidomsg, bool local)
1769 {
1770 struct board_ftn_msgid_cache {
1771 unsigned long id;
1772 struct fidopkt_msgid msgid;
1773 } *msgid_cache = NULL;
1774 struct bile_object *o;
1775 struct board_ftn_post post;
1776 struct fidopkt_msgid msgid;
1777 unsigned long *post_ids = NULL;
1778 char *pdata;
1779 size_t asize, cache_size, psize, npost_ids;
1780 short n, ret, bret;
1781 bool dirty_cache = false;
1782
1783 msgid = fidomsg->msgid;
1784
1785 o = bile_find(board->bile, BOARD_FTN_MSGID_CACHE_RTYPE, 1);
1786 if (o) {
1787 /* allocate its size plus one entry that we may add */
1788 asize = o->size + sizeof(struct board_ftn_msgid_cache);
1789 msgid_cache = xmalloc(asize);
1790 if (msgid_cache == NULL) {
1791 logger_printf("[board] Toss to %s: malloc(%ld) failed, "
1792 "building new cache", board->name, asize);
1793 } else {
1794 bile_read(board->bile, o->type, o->id, msgid_cache, o->size);
1795 npost_ids = o->size / sizeof(struct board_ftn_msgid_cache);
1796 }
1797 xfree(&o);
1798 }
1799
1800 if (msgid_cache == NULL) {
1801 npost_ids = bile_ids_by_type(board->bile, BOARD_FTN_POST_RTYPE,
1802 &post_ids);
1803 msgid_cache = xcalloc(sizeof(struct board_ftn_msgid_cache),
1804 npost_ids + 1);
1805 if (msgid_cache == NULL) {
1806 logger_printf("[board] toss: calloc(%ld, %ld) failed",
1807 sizeof(struct board_ftn_msgid_cache), npost_ids + 1);
1808 xfree(&post_ids);
1809 return -1;
1810 }
1811 if (npost_ids == 0)
1812 goto add;
1813 for (n = 0; n < npost_ids; n++) {
1814 /* only read as far as we have to */
1815 bile_read(board->bile, BOARD_FTN_POST_RTYPE, post_ids[n],
1816 &post, offsetof(struct board_ftn_post, msgid) +
1817 member_size(struct board_ftn_post, msgid));
1818
1819 msgid_cache[n].id = post.id;
1820 memcpy(&msgid_cache[n].msgid, &post.msgid,
1821 sizeof(post.msgid));
1822 }
1823 dirty_cache = true;
1824 xfree(&post_ids);
1825 }
1826
1827 add:
1828 uthread_yield();
1829
1830 if (!local) {
1831 for (n = 0; n < npost_ids; n++) {
1832 if (memcmp(&msgid_cache[n].msgid, &msgid, sizeof(msgid)) == 0) {
1833 logger_printf("[board] Already have %s EchoMail %s in %s "
1834 "(%ld), skipping", db->config.ftn_network,
1835 fidomsg->msgid_orig, board->name, msgid_cache[n].id);
1836 ret = 0;
1837 goto done;
1838 }
1839 }
1840 }
1841
1842 memset(&post, 0, sizeof(post));
1843 post.time = fidomsg->time;
1844 post.body_size = fidomsg->body_len + 1;
1845 post.body = fidomsg->body;
1846 strlcpy(post.reply, fidomsg->reply, sizeof(post.reply));
1847 strlcpy(post.from, fidomsg->from, sizeof(post.from));
1848 strlcpy(post.subject, fidomsg->subject, sizeof(post.subject));
1849 strlcpy(post.to, fidomsg->to, sizeof(post.to));
1850 strlcpy(post.origin, fidomsg->origin, sizeof(post.origin));
1851 strlcpy(post.msgid_orig, fidomsg->msgid_orig, sizeof(post.msgid_orig));
1852 post.msgid = msgid;
1853
1854 if (!local || !post.id)
1855 post.id = bile_next_id(board->bile, BOARD_FTN_POST_RTYPE);
1856 if (!post.id) {
1857 logger_printf("[board] Failed get next id for %s", board->name);
1858 ret = -1;
1859 goto done;
1860 }
1861
1862 bret = bile_marshall_object(board->bile,
1863 board_ftn_post_object_fields, nboard_ftn_post_object_fields,
1864 &post, &pdata, &psize);
1865 if (bret != 0 || psize == 0) {
1866 logger_printf("[board] Failed to marshall new post %s %s: %d",
1867 fidomsg->area, fidomsg->msgid_orig, bile_error(board->bile));
1868 ret = -1;
1869 goto done;
1870 }
1871 if (bile_write(board->bile, BOARD_FTN_POST_RTYPE, post.id, pdata,
1872 psize) != psize) {
1873 logger_printf("[fidopkt] Failed to save new post %s %s: %d",
1874 fidomsg->area, fidomsg->msgid_orig, bile_error(board->bile));
1875 ret = -1;
1876 xfree(&pdata);
1877 goto done;
1878 }
1879 xfree(&pdata);
1880
1881 ret = 1;
1882
1883 /* we already allocated one empty space at the end of the cache */
1884 msgid_cache[npost_ids].id = post.id;
1885 memcpy(&msgid_cache[npost_ids].msgid, &post.msgid, sizeof(post.msgid));
1886 npost_ids++;
1887 dirty_cache = true;
1888
1889 board_delete_cached_index(board);
1890
1891 if (post.time > board->last_post_at)
1892 board->last_post_at = post.time;
1893
1894 if (!local)
1895 logger_printf("[board] Tossed %s EchoMail %s as %ld",
1896 fidomsg->area, fidomsg->msgid_orig, post.id);
1897 uthread_yield();
1898
1899 done:
1900 if (dirty_cache) {
1901 cache_size = npost_ids * sizeof(struct board_ftn_msgid_cache);
1902 if (bile_write(board->bile, BOARD_FTN_MSGID_CACHE_RTYPE, 1,
1903 msgid_cache, cache_size) != cache_size) {
1904 logger_printf("[board] Failed to save msgid cache for %s: %d",
1905 fidomsg->area, bile_error(board->bile));
1906 ret = -1;
1907 }
1908 }
1909 if (msgid_cache != NULL)
1910 xfree(&msgid_cache);
1911 if (post_ids != NULL)
1912 xfree(&post_ids);
1913
1914 return ret;
1915 }
1916
1917 void
1918 board_delete_cached_index(struct board *board)
1919 {
1920 bile_delete(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1,
1921 BILE_DELETE_FLAG_PURGE);
1922 board->need_reindex = true;
1923 }
1924
1925 void
1926 board_prune_old_posts(struct board *board)
1927 {
1928 size_t nposts, n;
1929 struct board_id_time_map *sorted_id_map;
1930 struct board_ftn_post fpost;
1931 struct board_post post;
1932 struct board_thread thread;
1933 time_t oldest;
1934 size_t size, deleted;
1935 char *data;
1936 short ret;
1937
1938 if (board->retention_days == 0)
1939 return;
1940
1941 deleted = 0;
1942
1943 nposts = board_index_sorted_post_ids(board, &sorted_id_map);
1944 if (nposts == 0)
1945 goto done;
1946
1947 oldest = Time - ((unsigned long)(board->retention_days) *
1948 (60UL * 60UL * 24UL));
1949
1950 for (n = 0; n < nposts; n++) {
1951 if (sorted_id_map[n].time >= oldest)
1952 continue;
1953
1954 if (board->ftn_area[0]) {
1955 size = bile_read_alloc(board->bile, BOARD_FTN_POST_RTYPE,
1956 sorted_id_map[n].id, &data);
1957 if (!size)
1958 continue;
1959 ret = bile_unmarshall_object(board->bile,
1960 board_ftn_post_object_fields, nboard_ftn_post_object_fields,
1961 data, size, &fpost, sizeof(fpost), false);
1962 xfree(&data);
1963 if (ret == BILE_ERR_NO_MEMORY)
1964 goto done;
1965
1966 board_delete_ftn_post(board, &fpost);
1967 } else {
1968 size = bile_read_alloc(board->bile, BOARD_POST_RTYPE,
1969 sorted_id_map[n].id, &data);
1970 if (!size)
1971 continue;
1972 ret = bile_unmarshall_object(board->bile,
1973 board_post_object_fields, nboard_post_object_fields,
1974 data, size, &post, sizeof(post), true);
1975 xfree(&data);
1976 if (ret == BILE_ERR_NO_MEMORY)
1977 goto done;
1978
1979 size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE,
1980 post.thread_id, &data);
1981 if (!size) {
1982 if (post.body != NULL)
1983 xfree(&post.body);
1984 continue;
1985 }
1986 ret = bile_unmarshall_object(board->bile,
1987 board_thread_object_fields, nboard_thread_object_fields,
1988 data, size, &thread, sizeof(thread), true);
1989 xfree(&data);
1990 if (ret == BILE_ERR_NO_MEMORY)
1991 goto done;
1992
1993 board_delete_post(board, &post, &thread);
1994
1995 if (post.body != NULL)
1996 xfree(&post.body);
1997 if (thread.subject != NULL)
1998 xfree(&thread.subject);
1999 if (thread.post_ids != NULL)
2000 xfree(&thread.post_ids);
2001 if (thread.parent_post_ids != NULL)
2002 xfree(&thread.parent_post_ids);
2003 }
2004
2005 deleted++;
2006 uthread_yield();
2007 }
2008
2009 done:
2010 if (sorted_id_map != NULL)
2011 xfree(&sorted_id_map);
2012
2013 if (deleted) {
2014 board_index_sorted_post_ids(board, NULL);
2015
2016 logger_printf("[board] Pruned %lu post%s in %s older than %d day%s",
2017 deleted, deleted == 1 ? "" : "s", board->name,
2018 board->retention_days, board->retention_days == 1 ? "" : "s");
2019 }
2020 }