AmendHub

Download

jcs

/

subtext

/

board.c

 

(View History)

jcs   board: free post_ids in board_index_sorted_post_ids Latest amendment: 592 on 2024-02-16

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