AmendHub

Download

jcs

/

subtext

/

board.c

 

(View History)

jcs   board: Delete cached index if id_map fails allocation Latest amendment: 505 on 2023-05-01

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