AmendHub

Download

jcs

/

subtext

/

board.c

 

(View History)

jcs   board: For new FTN messages posted locally, just toss them Latest amendment: 560 on 2023-11-21

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 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 { 's', "Ss", "Show this post" },
884 { 'd', "Dd", "Delete this post" },
885 { 'l', "Ll", "List posts" },
886 { 'q', "QqXx", "Return to threads" },
887 { '?', "?", "List these options" },
888 };
889 static const char prompt_help[] =
890 "<:Newer >:Older R:Reply S:Show D:Delete L:List Q:Return ?:Help";
891 char time[32], prompt[7 + member_size(struct board, name) + 8];
892 struct board_thread thread;
893 struct board_post post;
894 struct board_ftn_post fpost;
895 struct username_cache *sender;
896 struct session_menu_option *dopts = NULL;
897 size_t size, plain_post_size, j;
898 short ret = POST_READ_RETURN_DONE;
899 char c;
900 char *data, *subject, *plain_post, *tplain_post;
901 short cc, bcret, n;
902 bool done = false, show_help = false;
903
904 dopts = xmalloc(sizeof(opts));
905 if (dopts == NULL)
906 return 0;
907 memcpy(dopts, opts, sizeof(opts));
908
909 show_post:
910 if (board->ftn_area[0]) {
911 size = bile_read_alloc(board->bile, BOARD_FTN_POST_RTYPE, id,
912 &data);
913 if (size == 0)
914 panic("failed fetching message %ld: %d", id,
915 bile_error(board->bile));
916
917 ret = bile_unmarshall_object(board->bile,
918 board_ftn_post_object_fields, nboard_ftn_post_object_fields,
919 data, size, &fpost, sizeof(fpost), true);
920 xfree(&data);
921 if (ret == BILE_ERR_NO_MEMORY)
922 goto done_reading;
923
924 if (!(s->user && s->user->is_sysop)) {
925 /* disable deleting */
926 for (n = 0; n < nitems(opts); n++) {
927 if (dopts[n].ret == 'd') {
928 dopts[n].key[0] = '\0';
929 break;
930 }
931 }
932 }
933
934 strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S",
935 localtime(&fpost.time));
936
937 session_printf(s, "{{B}}From:{{/B}}{{#}} %s\r\n", fpost.from);
938 session_printf(s, "{{B}}Origin:{{/B}}{{#}} %s\r\n", fpost.origin);
939 session_printf(s, "{{B}}To:{{/B}}{{#}} %s@%s\r\n", fpost.to,
940 board->name);
941 session_printf(s, "{{B}}Date:{{/B}}{{#}} %s %s\r\n", time,
942 db->config.timezone);
943 session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s%s\r\n",
944 (fpost.reply[0] && strncasecmp(fpost.subject, "Re:", 3) != 0 ?
945 "Re: " : ""), fpost.subject);
946 session_printf(s, "\r\n");
947 session_flush(s);
948
949 plain_post_size = 0;
950 plain_post = xmalloc(fpost.body_size);
951 if (plain_post == NULL)
952 session_paginate(s, fpost.body, fpost.body_size, 6);
953 else {
954 /* strip out renegade-style pipe color codes ("abc|01def") */
955 for (j = 0; j < fpost.body_size; j++) {
956 if (fpost.body[j] == '|' &&
957 fpost.body[j + 1] >= '0' && fpost.body[j + 1] <= '9' &&
958 fpost.body[j + 2] >= '0' && fpost.body[j + 2] <= '9') {
959 j += 2;
960 continue;
961 }
962 plain_post[plain_post_size++] = fpost.body[j];
963 }
964 tplain_post = xrealloc(plain_post, plain_post_size);
965 if (tplain_post != NULL)
966 plain_post = tplain_post;
967 session_paginate(s, plain_post, plain_post_size, 6);
968 xfree(&plain_post);
969 }
970 } else {
971 size = bile_read_alloc(board->bile, BOARD_POST_RTYPE, id, &data);
972 if (size == 0)
973 panic("failed fetching message %ld: %d", id,
974 bile_error(board->bile));
975 ret = bile_unmarshall_object(board->bile, board_post_object_fields,
976 nboard_post_object_fields, data, size, &post, sizeof(post), true);
977 xfree(&data);
978 if (ret == BILE_ERR_NO_MEMORY)
979 goto done_reading;
980
981 size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE,
982 post.thread_id, &data);
983 if (size == 0)
984 panic("failed fetching thread %ld: %d", post.thread_id,
985 bile_error(board->bile));
986 ret = bile_unmarshall_object(board->bile, board_thread_object_fields,
987 nboard_thread_object_fields, data, size, &thread,
988 sizeof(thread), true);
989 xfree(&data);
990 if (ret == BILE_ERR_NO_MEMORY)
991 goto done_reading;
992
993 if (!(s->user && (s->user->is_sysop ||
994 s->user->id == post.sender_user_id))) {
995 /* disable deleting */
996 for (n = 0; n < nitems(opts); n++) {
997 if (dopts[n].ret == 'd') {
998 dopts[n].key[0] = '\0';
999 break;
1000 }
1001 }
1002 }
1003
1004 sender = user_username(post.sender_user_id);
1005
1006 strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S",
1007 localtime(&post.time));
1008
1009 session_printf(s, "{{B}}From:{{/B}}{{#}} %s",
1010 sender ? sender->username : "(unknown)");
1011 if (post.via[0])
1012 session_printf(s, " (via %s)", post.via);
1013 session_printf(s, "\r\n");
1014 session_printf(s, "{{B}}To:{{/B}}{{#}} %s\r\n", board->name);
1015 session_printf(s, "{{B}}Date:{{/B}}{{#}} %s %s\r\n", time,
1016 db->config.timezone);
1017 session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s%s\r\n",
1018 (post.parent_post_id ? "Re: " : ""), thread.subject);
1019 session_printf(s, "\r\n");
1020 session_flush(s);
1021 session_paginate(s, post.body, post.body_size, 5);
1022 }
1023
1024 snprintf(prompt, sizeof(prompt), "%s:%d", prompt_prefix, idx);
1025
1026 if (board->ftn_area[0])
1027 subject = fpost.subject;
1028 else
1029 subject = thread.subject;
1030
1031 while (!done && !s->ending) {
1032 c = session_menu(s, subject, prompt, (char *)prompt_help, dopts,
1033 nitems(opts), show_help, NULL, NULL);
1034 show_help = false;
1035
1036 switch (c) {
1037 case 'd':
1038 if (!(s->user && (s->user->is_sysop ||
1039 s->user->id == post.sender_user_id))) {
1040 session_printf(s, "Invalid option\r\n");
1041 session_flush(s);
1042 break;
1043 }
1044
1045 session_printf(s, "Are you sure you want to permanently "
1046 "delete this post? [y/N] ");
1047 session_flush(s);
1048
1049 cc = session_input_char(s);
1050 if (cc == 'y' || c == 'Y') {
1051 session_printf(s, "%c\r\n", cc);
1052 session_flush(s);
1053
1054 if (board->ftn_area[0]) {
1055 board_delete_ftn_post(board, &fpost);
1056 session_logf(s, "Deleted %s post %ld",
1057 db->config.ftn_network, fpost.id);
1058 } else {
1059 board_delete_post(board, &post, &thread);
1060 session_logf(s, "Deleted post %ld (thread %ld)",
1061 post.id, thread.thread_id);
1062 }
1063
1064 session_printf(s, "\r\n{{B}}Post deleted!{{/B}}\r\n");
1065 session_flush(s);
1066 ret = POST_READ_RETURN_FIND;
1067 done = true;
1068 } else {
1069 session_printf(s, "\r\nPost not deleted.\r\n");
1070 session_flush(s);
1071 }
1072 break;
1073 case 'r':
1074 if (board->ftn_area[0])
1075 bcret = board_compose(s, board, NULL, NULL, &fpost, NULL,
1076 NULL);
1077 else
1078 bcret = board_compose(s, board, &thread, &post, NULL, NULL,
1079 NULL);
1080
1081 if (bcret) {
1082 ret = POST_READ_RETURN_FIND;
1083 done = true;
1084 }
1085 break;
1086 case 's':
1087 goto show_post;
1088 break;
1089 case '<':
1090 ret = POST_READ_RETURN_NEWER;
1091 done = true;
1092 break;
1093 case '>':
1094 ret = POST_READ_RETURN_OLDER;
1095 done = true;
1096 break;
1097 case 'l':
1098 ret = POST_READ_RETURN_LIST;
1099 done = true;
1100 break;
1101 case 'q':
1102 ret = POST_READ_RETURN_DONE;
1103 done = true;
1104 break;
1105 case '?':
1106 show_help = true;
1107 break;
1108 }
1109 }
1110
1111 done_reading:
1112 xfree(&dopts);
1113
1114 if (board->ftn_area[0]) {
1115 if (fpost.body != NULL)
1116 xfree(&fpost.body);
1117 } else {
1118 if (post.body != NULL)
1119 xfree(&post.body);
1120 if (thread.subject != NULL)
1121 xfree(&thread.subject);
1122 if (thread.post_ids != NULL)
1123 xfree(&thread.post_ids);
1124 if (thread.parent_post_ids != NULL)
1125 xfree(&thread.parent_post_ids);
1126 }
1127
1128 return ret;
1129 }
1130
1131 size_t
1132 board_find_post_ids(struct session *s, struct board *board,
1133 size_t *npost_ids, unsigned long **post_ids, size_t offset, size_t limit)
1134 {
1135 struct board_id_time_map *all_post_id_map;
1136 size_t n, size, nall_post_ids;
1137
1138 *post_ids = NULL;
1139 *npost_ids = 0;
1140
1141 size = bile_read_alloc(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1,
1142 &all_post_id_map);
1143 if (all_post_id_map == NULL) {
1144 session_printf(s, "%sPlease wait, re-indexing board posts...%s",
1145 ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END));
1146 session_flush(s);
1147 nall_post_ids = board_index_sorted_post_ids(board,
1148 &all_post_id_map);
1149 session_output(s, "\r\n", 2);
1150 session_flush(s);
1151 if (nall_post_ids == 0)
1152 return 0;
1153 } else
1154 nall_post_ids = size / sizeof(struct board_id_time_map);
1155
1156 *post_ids = xcalloc(sizeof(long), MIN(limit, nall_post_ids));
1157 if (*post_ids == NULL)
1158 return 0;
1159
1160 for (n = 0; n < nall_post_ids; n++) {
1161 if (n < offset)
1162 continue;
1163
1164 (*post_ids)[*npost_ids] = all_post_id_map[n].id;
1165 (*npost_ids)++;
1166
1167 if (*npost_ids >= limit)
1168 break;
1169 }
1170
1171 if (all_post_id_map != NULL)
1172 xfree(&all_post_id_map);
1173
1174 return nall_post_ids;
1175 }
1176
1177 short
1178 board_post_create(struct board *board, struct board_thread *thread,
1179 struct board_ftn_post *ftn_parent_post, struct board_post *post)
1180 {
1181 struct board_ftn_post ftn_post = { 0 };
1182 struct fidopkt_message fidomsg = { 0 };
1183 struct username_cache *user;
1184 struct fidopkt_address our_address, hub_address;
1185 struct bile_object *o;
1186 short ret;
1187 char *data;
1188 size_t size, insert, npost_ids;
1189 ssize_t n, j;
1190 unsigned long *post_ids, *parent_post_ids;
1191 struct board_id_time_map *id_map;
1192
1193 if (board->ftn_area[0]) {
1194 if (!post->id)
1195 post->id = bile_next_id(board->bile, BOARD_FTN_POST_RTYPE);
1196 if (!post->time)
1197 post->time = Time;
1198
1199 if (!fidopkt_parse_address(db->config.ftn_node_addr,
1200 &our_address)) {
1201 logger_printf("[board] Invalid FTN local node address, can't "
1202 "create board post");
1203 post->id = 0;
1204 goto done;
1205 }
1206 if (!fidopkt_parse_address(db->config.ftn_hub_addr,
1207 &hub_address)) {
1208 logger_printf("[board] Invalid FTN hub address, can't "
1209 "create board post");
1210 post->id = 0;
1211 goto done;
1212 }
1213
1214 ftn_post.id = post->id;
1215 ftn_post.time = post->time;
1216
1217 if (ftn_parent_post) {
1218 snprintf(ftn_post.subject, sizeof(ftn_post.subject),
1219 "%s%s",
1220 strncmp("Re:", ftn_parent_post->subject, 3) == 1 ?
1221 "" : "Re: ",
1222 ftn_parent_post->subject);
1223 strlcpy(ftn_post.reply, ftn_parent_post->msgid_orig,
1224 sizeof(ftn_post.reply));
1225 strlcpy(ftn_post.to, ftn_parent_post->from,
1226 sizeof(ftn_post.to));
1227 } else {
1228 strlcpy(ftn_post.subject, thread->subject,
1229 sizeof(ftn_post.subject));
1230 strlcpy(ftn_post.to, "All", sizeof(ftn_post.to));
1231 }
1232
1233 user = user_username(post->sender_user_id);
1234 if (user == NULL) {
1235 logger_printf("[board] Can't find username of user posting "
1236 "new message");
1237 post->id = 0;
1238 goto done;
1239 }
1240 strlcpy(ftn_post.from, user->username, sizeof(ftn_post.from));
1241
1242 ftn_post.body = post->body;
1243 ftn_post.body_size = post->body_size;
1244
1245 /* make each board's posts have ids unique to our zone/net/node */
1246 ftn_post.msgid.id = 0x10000000 |
1247 ((unsigned long)our_address.node << 24) |
1248 ((unsigned long)(board->id) << 16) | post->id;
1249 ftn_post.msgid.zone = our_address.zone;
1250 ftn_post.msgid.net = our_address.net;
1251 ftn_post.msgid.node = our_address.node;
1252 ftn_post.msgid.point = our_address.point;
1253
1254 snprintf(ftn_post.origin, sizeof(ftn_post.origin),
1255 "%s | %s (%s)",
1256 db->config.name, db->config.hostname,
1257 db->config.ftn_node_addr);
1258
1259 fidomsg.time = ftn_post.time;
1260 memcpy(&fidomsg.header.orig, &our_address,
1261 sizeof(fidomsg.header.orig));
1262 memcpy(&fidomsg.header.dest, &hub_address,
1263 sizeof(fidomsg.header.dest));
1264 strlcpy(fidomsg.area, board->ftn_area, sizeof(fidomsg.area));
1265 strlcpy(fidomsg.to, ftn_post.to, sizeof(fidomsg.to));
1266 strlcpy(fidomsg.from, ftn_post.from, sizeof(fidomsg.from));
1267 strlcpy(fidomsg.subject, ftn_post.subject, sizeof(fidomsg.subject));
1268 fidomsg.body = ftn_post.body;
1269 fidomsg.body_len = ftn_post.body_size - 1;
1270 strlcpy(fidomsg.reply, ftn_post.reply, sizeof(fidomsg.reply));
1271 memcpy(&fidomsg.msgid, &ftn_post.msgid, sizeof(fidomsg.msgid));
1272 strlcpy(fidomsg.origin, ftn_post.origin, sizeof(fidomsg.origin));
1273
1274 if (!binkp_scan_message(&fidomsg)) {
1275 logger_printf("[board] Failed scanning new FTN message being "
1276 "posted");
1277 post->id = 0;
1278 goto done;
1279 }
1280
1281 if (board_toss_ftn_message(board, &fidomsg, true) != 1) {
1282 logger_printf("[board] Failed tossing new FTN message being "
1283 "posted");
1284 post->id = 0;
1285 goto done;
1286 }
1287 } else {
1288 if (!post->id)
1289 post->id = bile_next_id(board->bile, BOARD_POST_RTYPE);
1290 if (!post->time)
1291 post->time = Time;
1292
1293 if (post->parent_post_id == 0) {
1294 thread->thread_id = bile_next_id(board->bile,
1295 BOARD_THREAD_RTYPE);
1296 post->thread_id = thread->thread_id;
1297 }
1298
1299 ret = bile_marshall_object(board->bile, board_post_object_fields,
1300 nboard_post_object_fields, post, &data, &size);
1301 if (ret != 0 || size == 0) {
1302 logger_printf("[board] Failed to marshall new post object");
1303 post->id = 0;
1304 goto done;
1305 }
1306 if (bile_write(board->bile, BOARD_POST_RTYPE, post->id, data,
1307 size) != size) {
1308 warn("bile_write of new post failed! %d",
1309 bile_error(board->bile));
1310 post->id = 0;
1311 xfree(&data);
1312 goto done;
1313 }
1314 xfree(&data);
1315
1316 if (post->time > thread->last_post_at)
1317 thread->last_post_at = post->time;
1318 thread->nposts++;
1319 post_ids = xreallocarray(thread->post_ids, thread->nposts,
1320 sizeof(long));
1321 if (post_ids == NULL)
1322 return 0;
1323 thread->post_ids = post_ids;
1324 parent_post_ids = xreallocarray(thread->parent_post_ids,
1325 thread->nposts, sizeof(long));
1326 if (parent_post_ids == NULL)
1327 return 0;
1328 thread->parent_post_ids = parent_post_ids;
1329
1330 /*
1331 * Add new post id to thread post_ids, but put it in the right
1332 * place so that reading post_ids will present the tree in order.
1333 * Walk parent_post_ids and find the first match of our parent,
1334 * then insert our new post there. This puts newest replies at
1335 * the top.
1336 */
1337
1338 insert = thread->nposts - 1;
1339 for (n = 0; n < thread->nposts - 1; n++) {
1340 if (thread->post_ids[n] != post->parent_post_id)
1341 continue;
1342
1343 for (j = thread->nposts - 2; j > n; j--) {
1344 thread->post_ids[j + 1] = thread->post_ids[j];
1345 thread->parent_post_ids[j + 1] =
1346 thread->parent_post_ids[j];
1347 }
1348 insert = n + 1;
1349 break;
1350 }
1351 thread->post_ids[insert] = post->id;
1352 thread->parent_post_ids[insert] = post->parent_post_id;
1353
1354 ret = bile_marshall_object(board->bile, board_thread_object_fields,
1355 nboard_thread_object_fields, thread, &data, &size);
1356 if (ret != 0 || size == 0) {
1357 logger_printf("[board] Failed to marshall new thread object");
1358 post->id = 0;
1359 goto done;
1360 }
1361 if (bile_write(board->bile, BOARD_THREAD_RTYPE, thread->thread_id,
1362 data, size) != size) {
1363 warn("bile_write of thread failed! %d",
1364 bile_error(board->bile));
1365 post->id = 0;
1366 xfree(&data);
1367 goto done;
1368 }
1369 xfree(&data);
1370
1371 /* it would be nice not to have to rebuild this every time... */
1372 board_delete_cached_index(board);
1373 bile_flush(board->bile, true);
1374
1375 if (post->time > board->last_post_at)
1376 board->last_post_at = post->time;
1377 }
1378
1379 done:
1380 return (post->id == 0 ? -1 : 0);
1381 }
1382
1383 void
1384 board_delete_post(struct board *board, struct board_post *post,
1385 struct board_thread *thread)
1386 {
1387 size_t size, n, nn;
1388 char *data, *body;
1389 char del[50];
1390 short ret;
1391 bool childs = false;
1392 unsigned long *new_post_ids;
1393 unsigned long *new_parent_post_ids;
1394
1395 if (thread->nposts == 1) {
1396 bile_delete(board->bile, BOARD_THREAD_RTYPE, thread->thread_id,
1397 0);
1398 bile_delete(board->bile, BOARD_POST_RTYPE, post->id,
1399 BILE_DELETE_FLAG_PURGE);
1400 board_delete_cached_index(board);
1401 return;
1402 }
1403
1404 for (n = 0; n < thread->nposts; n++) {
1405 if (thread->parent_post_ids[n] == post->id) {
1406 childs = true;
1407 break;
1408 }
1409 }
1410
1411 if (!childs) {
1412 /* just zap this off the end of the tree */
1413 new_post_ids = xcalloc(thread->nposts - 1, sizeof(unsigned long));
1414 if (new_post_ids == NULL)
1415 return;
1416 new_parent_post_ids = xcalloc(thread->nposts - 1,
1417 sizeof(unsigned long));
1418 if (new_parent_post_ids == NULL)
1419 return;
1420
1421 for (n = 0, nn = 0; n < thread->nposts; n++) {
1422 if (thread->post_ids[n] == post->id)
1423 continue;
1424
1425 new_post_ids[nn] = thread->post_ids[n];
1426 nn++;
1427 }
1428
1429 for (n = 0, nn = 0; n < thread->nposts; n++) {
1430 if (thread->post_ids[n] == post->id)
1431 continue;
1432
1433 new_parent_post_ids[nn] = thread->parent_post_ids[n];
1434 nn++;
1435 }
1436
1437 thread->nposts--;
1438
1439 xfree(&thread->post_ids);
1440 thread->post_ids = new_post_ids;
1441 xfree(&thread->parent_post_ids);
1442 thread->parent_post_ids = new_parent_post_ids;
1443
1444 ret = bile_marshall_object(board->bile, board_thread_object_fields,
1445 nboard_thread_object_fields, thread, &data, &size);
1446 if (ret != 0 || size == 0) {
1447 logger_printf("[board] Failed to marshall thread object "
1448 "during post deletion");
1449 return;
1450 }
1451 if (bile_write(board->bile, BOARD_THREAD_RTYPE, thread->thread_id,
1452 data, size) != size) {
1453 warn("bile_write of updated thread after post delete failed! "
1454 "%d", bile_error(board->bile));
1455 xfree(&data);
1456 return;
1457 }
1458 xfree(&data);
1459
1460 bile_delete(board->bile, BOARD_POST_RTYPE, post->id,
1461 BILE_DELETE_FLAG_ZERO | BILE_DELETE_FLAG_PURGE);
1462 board_delete_cached_index(board);
1463
1464 bile_flush(board->bile, true);
1465 return;
1466 }
1467
1468 /* all we can do is change the post to say deleted */
1469 if (post->body != NULL)
1470 xfree(&post->body);
1471 snprintf(del, sizeof(del), "[ Post deleted ]");
1472 body = xstrdup(del);
1473 if (body == NULL)
1474 return;
1475 post->body = body;
1476 post->body_size = strlen(post->body) + 1;
1477
1478 ret = bile_marshall_object(board->bile, board_post_object_fields,
1479 nboard_post_object_fields, post, &data, &size);
1480 if (ret != 0 || size == 0) {
1481 logger_printf("[board] Failed to marshall updated post object");
1482 return;
1483 }
1484 if (bile_write(board->bile, BOARD_POST_RTYPE, post->id, data,
1485 size) != size) {
1486 warn("bile_write of updated post failed! %d",
1487 bile_error(board->bile));
1488 xfree(&data);
1489 return;
1490 }
1491
1492 xfree(&data);
1493 }
1494
1495 void
1496 board_delete_ftn_post(struct board *board, struct board_ftn_post *post)
1497 {
1498 size_t size, npost_ids, n;
1499 struct board_id_time_map *id_map;
1500
1501 bile_delete(board->bile, BOARD_FTN_POST_RTYPE, post->id,
1502 BILE_DELETE_FLAG_PURGE);
1503
1504 size = bile_read_alloc(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1,
1505 &id_map);
1506 if (size == 0 || id_map == NULL)
1507 return;
1508
1509 npost_ids = size / sizeof(struct board_id_time_map);
1510
1511 for (n = 0; n < npost_ids; n++) {
1512 if (id_map[n].id == post->id) {
1513 for (; n < npost_ids - 1; n++)
1514 id_map[n] = id_map[n + 1];
1515 npost_ids--;
1516 break;
1517 }
1518 }
1519
1520 if (npost_ids == 0)
1521 board_delete_cached_index(board);
1522 else
1523 bile_write(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, id_map,
1524 sizeof(struct board_id_time_map) * npost_ids);
1525
1526 /* TODO: delete from msgid cache too */
1527 }
1528
1529 size_t
1530 board_index_sorted_post_ids(struct board *board,
1531 struct board_id_time_map **sorted_id_map)
1532 {
1533 struct thread_time_map {
1534 unsigned long id;
1535 time_t time;
1536 size_t nposts;
1537 };
1538 struct board_ftn_post fpost;
1539 struct board_post post;
1540 struct board_thread thread;
1541 size_t ret, size, i, j, n, npost_ids, nthread_ids;
1542 unsigned long *post_ids, *thread_ids;
1543 struct board_id_time_map *id_map, tmp_id_map;
1544 struct thread_time_map *thread_map, tmp_thread_map;
1545 char *data;
1546
1547 if (board->ftn_area[0]) {
1548 npost_ids = bile_ids_by_type(board->bile, BOARD_FTN_POST_RTYPE,
1549 &post_ids);
1550 if (npost_ids == 0)
1551 goto write_out;
1552
1553 id_map = xcalloc(sizeof(struct board_id_time_map), npost_ids);
1554 if (id_map == NULL)
1555 goto write_out;
1556
1557 for (n = 0; n < npost_ids; n++) {
1558 /* only read as far as the time */
1559 ret = bile_read(board->bile, BOARD_FTN_POST_RTYPE,
1560 post_ids[n], &fpost,
1561 offsetof(struct board_ftn_post, time) +
1562 member_size(struct board_ftn_post, time));
1563 if (ret == 0)
1564 goto write_out;
1565
1566 id_map[n].id = fpost.id;
1567 id_map[n].time = fpost.time;
1568 }
1569
1570 /* sort by date descending */
1571 for (i = 1; i < npost_ids; i++) {
1572 for (j = i; j > 0; j--) {
1573 if (id_map[j].time < id_map[j - 1].time)
1574 break;
1575 tmp_id_map = id_map[j];
1576 id_map[j] = id_map[j - 1];
1577 id_map[j - 1] = tmp_id_map;
1578 }
1579 }
1580 } else {
1581 npost_ids = 0;
1582 nthread_ids = bile_ids_by_type(board->bile, BOARD_THREAD_RTYPE,
1583 &thread_ids);
1584 if (nthread_ids == 0)
1585 goto write_out;
1586
1587 thread_map = xcalloc(sizeof(struct thread_time_map), nthread_ids);
1588 if (thread_map == NULL)
1589 goto write_out;
1590
1591 for (n = 0; n < nthread_ids; n++) {
1592 size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE,
1593 thread_ids[n], &data);
1594 ret = bile_unmarshall_object(board->bile,
1595 board_thread_object_fields, nboard_thread_object_fields, data,
1596 size, &thread, sizeof(thread), false);
1597 xfree(&data);
1598 if (ret != 0)
1599 goto write_out;
1600
1601 thread_map[n].id = thread.thread_id;
1602 thread_map[n].time = thread.last_post_at;
1603 thread_map[n].nposts = thread.nposts;
1604 npost_ids += thread.nposts;
1605 }
1606
1607 xfree(&thread_ids);
1608
1609 /* sort by last post date descending */
1610 for (i = 1; i < nthread_ids; i++) {
1611 for (j = i; j > 0; j--) {
1612 if (thread_map[j].time < thread_map[j - 1].time)
1613 break;
1614 tmp_thread_map = thread_map[j];
1615 thread_map[j] = thread_map[j - 1];
1616 thread_map[j - 1] = tmp_thread_map;
1617 }
1618 }
1619
1620 id_map = xcalloc(sizeof(struct board_id_time_map), npost_ids);
1621 if (id_map == NULL)
1622 goto write_out;
1623
1624 npost_ids = 0;
1625 for (j = 0; j < nthread_ids; j++) {
1626 size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE,
1627 thread_map[j].id, &data);
1628 if (data == NULL)
1629 goto write_out;
1630 ret = bile_unmarshall_object(board->bile,
1631 board_thread_object_fields, nboard_thread_object_fields, data,
1632 size, &thread, sizeof(thread), true);
1633 xfree(&data);
1634 if (ret != 0)
1635 goto write_out;
1636
1637 /* these are already sorted, and we want to keep thread sort */
1638 for (i = 0; i < thread.nposts; i++) {
1639 /* only read as far as the time */
1640 size = bile_read(board->bile, BOARD_POST_RTYPE,
1641 thread.post_ids[i], &post,
1642 offsetof(struct board_post, time) +
1643 member_size(struct board_post, time));
1644 if (size == 0) {
1645 logger_printf("[board] Board %s thread %ld post %ld "
1646 "is missing", board->name, thread.thread_id,
1647 thread.post_ids[i]);
1648 continue;
1649 }
1650
1651 id_map[npost_ids].id = post.id;
1652 id_map[npost_ids].time = post.time;
1653 npost_ids++;
1654 }
1655
1656 if (thread.subject != NULL)
1657 xfree(&thread.subject);
1658 if (thread.post_ids != NULL)
1659 xfree(&thread.post_ids);
1660 if (thread.parent_post_ids != NULL)
1661 xfree(&thread.parent_post_ids);
1662 }
1663
1664 xfree(&thread_map);
1665 }
1666
1667 write_out:
1668 if (npost_ids == 0 || id_map == NULL) {
1669 board_delete_cached_index(board);
1670 if (sorted_id_map != NULL)
1671 *sorted_id_map = NULL;
1672 return 0;
1673 }
1674
1675 bile_write(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, id_map,
1676 sizeof(struct board_id_time_map) * npost_ids);
1677 if (sorted_id_map == NULL)
1678 xfree(&id_map);
1679 else
1680 *sorted_id_map = id_map;
1681 return npost_ids;
1682 }
1683
1684 short
1685 board_toss_ftn_message(struct board *board,
1686 struct fidopkt_message *fidomsg, bool local)
1687 {
1688 struct board_ftn_msgid_cache {
1689 unsigned long id;
1690 struct fidopkt_msgid msgid;
1691 } *msgid_cache = NULL;
1692 struct bile_object *o;
1693 struct board_ftn_post post;
1694 struct fidopkt_msgid msgid;
1695 unsigned long *post_ids;
1696 char *pdata;
1697 size_t asize, cache_size, psize, npost_ids;
1698 short n, ret, bret;
1699 bool dirty_cache = false;
1700
1701 msgid = fidomsg->msgid;
1702
1703 o = bile_find(board->bile, BOARD_FTN_MSGID_CACHE_RTYPE, 1);
1704 if (o) {
1705 /* allocate its size plus one entry that we may add */
1706 asize = o->size + sizeof(struct board_ftn_msgid_cache);
1707 msgid_cache = xmalloc(asize);
1708 if (msgid_cache == NULL) {
1709 logger_printf("[board] toss: malloc(%ld) failed",
1710 asize);
1711 return -1;
1712 }
1713 bile_read(board->bile, o->type, o->id, msgid_cache, o->size);
1714 npost_ids = o->size / sizeof(struct board_ftn_msgid_cache);
1715 xfree(&o);
1716 } else {
1717 npost_ids = bile_ids_by_type(board->bile, BOARD_FTN_POST_RTYPE,
1718 &post_ids);
1719 msgid_cache = xcalloc(sizeof(struct board_ftn_msgid_cache),
1720 npost_ids + 1);
1721 if (msgid_cache == NULL) {
1722 logger_printf("[board] toss: calloc(%ld, %ld) failed",
1723 sizeof(struct board_ftn_msgid_cache), npost_ids + 1);
1724 return -1;
1725 }
1726 for (n = 0; n < npost_ids; n++) {
1727 /* only read as far as we have to */
1728 bile_read(board->bile, BOARD_FTN_POST_RTYPE, post_ids[n],
1729 &post, offsetof(struct board_ftn_post, msgid) +
1730 member_size(struct board_ftn_post, msgid));
1731
1732 msgid_cache[n].id = post.id;
1733 memcpy(&msgid_cache[n].msgid, &post.msgid,
1734 sizeof(post.msgid));
1735 }
1736 dirty_cache = true;
1737 }
1738
1739 uthread_yield();
1740
1741 if (!local) {
1742 for (n = 0; n < npost_ids; n++) {
1743 if (memcmp(&msgid_cache[n].msgid, &msgid,
1744 sizeof(msgid)) == 0) {
1745 logger_printf("[board] Already have %s EchoMail %s in %s "
1746 "(%ld), skipping", db->config.ftn_network,
1747 fidomsg->msgid_orig, board->name, msgid_cache[n].id);
1748 ret = 0;
1749 goto done;
1750 }
1751 }
1752 }
1753
1754 memset(&post, 0, sizeof(post));
1755 post.time = fidomsg->time;
1756 post.body_size = fidomsg->body_len + 1;
1757 post.body = fidomsg->body;
1758 strlcpy(post.reply, fidomsg->reply, sizeof(post.reply));
1759 strlcpy(post.from, fidomsg->from, sizeof(post.from));
1760 strlcpy(post.subject, fidomsg->subject, sizeof(post.subject));
1761 strlcpy(post.to, fidomsg->to, sizeof(post.to));
1762 strlcpy(post.origin, fidomsg->origin, sizeof(post.origin));
1763 strlcpy(post.msgid_orig, fidomsg->msgid_orig, sizeof(post.msgid_orig));
1764 post.msgid = msgid;
1765
1766 if (!local || !post.id)
1767 post.id = bile_next_id(board->bile, BOARD_FTN_POST_RTYPE);
1768 if (!post.id) {
1769 logger_printf("[board] Failed get next id for %s", board->name);
1770 ret = -1;
1771 goto done;
1772 }
1773
1774 bret = bile_marshall_object(board->bile,
1775 board_ftn_post_object_fields, nboard_ftn_post_object_fields,
1776 &post, &pdata, &psize);
1777 if (bret != 0 || psize == 0) {
1778 logger_printf("[board] Failed to marshall new post %s %s: %d",
1779 fidomsg->area, fidomsg->msgid_orig, bile_error(board->bile));
1780 ret = -1;
1781 goto done;
1782 }
1783 if (bile_write(board->bile, BOARD_FTN_POST_RTYPE, post.id, pdata,
1784 psize) != psize) {
1785 logger_printf("[fidopkt] Failed to save new post %s %s: %d",
1786 fidomsg->area, fidomsg->msgid_orig, bile_error(board->bile));
1787 ret = -1;
1788 xfree(&pdata);
1789 goto done;
1790 }
1791 xfree(&pdata);
1792
1793 ret = 1;
1794
1795 /* we already allocated one empty space at the end of the cache */
1796 msgid_cache[npost_ids].id = post.id;
1797 memcpy(&msgid_cache[npost_ids].msgid, &post.msgid, sizeof(post.msgid));
1798 npost_ids++;
1799 dirty_cache = true;
1800
1801 board_delete_cached_index(board);
1802
1803 if (post.time > board->last_post_at)
1804 board->last_post_at = post.time;
1805
1806 if (!local)
1807 logger_printf("[board] Tossed %s EchoMail %s as %ld",
1808 fidomsg->area, fidomsg->msgid_orig, post.id);
1809 uthread_yield();
1810
1811 done:
1812 if (dirty_cache) {
1813 cache_size = npost_ids * sizeof(struct board_ftn_msgid_cache);
1814 if (bile_write(board->bile, BOARD_FTN_MSGID_CACHE_RTYPE, 1,
1815 msgid_cache, cache_size) != cache_size) {
1816 logger_printf("[board] Failed to save msgid cache for %s: %d",
1817 fidomsg->area, bile_error(board->bile));
1818 ret = -1;
1819 }
1820 }
1821 if (msgid_cache != NULL)
1822 xfree(&msgid_cache);
1823
1824 return ret;
1825 }
1826
1827 void
1828 board_delete_cached_index(struct board *board)
1829 {
1830 bile_delete(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1,
1831 BILE_DELETE_FLAG_PURGE);
1832 }
1833
1834 void
1835 board_prune_old_posts(struct board *board)
1836 {
1837 size_t nposts, n;
1838 struct board_id_time_map *sorted_id_map;
1839 struct board_ftn_post fpost;
1840 struct board_post post;
1841 struct board_thread thread;
1842 time_t oldest;
1843 size_t size, deleted;
1844 char *data;
1845 short ret;
1846
1847 if (board->retention_days == 0)
1848 return;
1849
1850 deleted = 0;
1851
1852 nposts = board_index_sorted_post_ids(board, &sorted_id_map);
1853 if (nposts == 0)
1854 goto done;
1855
1856 oldest = Time - ((unsigned long)(board->retention_days) *
1857 (60UL * 60UL * 24UL));
1858
1859 for (n = 0; n < nposts; n++) {
1860 if (sorted_id_map[n].time >= oldest)
1861 continue;
1862
1863 if (board->ftn_area[0]) {
1864 size = bile_read_alloc(board->bile, BOARD_FTN_POST_RTYPE,
1865 sorted_id_map[n].id, &data);
1866 if (!size)
1867 continue;
1868 ret = bile_unmarshall_object(board->bile,
1869 board_ftn_post_object_fields, nboard_ftn_post_object_fields,
1870 data, size, &fpost, sizeof(fpost), false);
1871 xfree(&data);
1872 if (ret == BILE_ERR_NO_MEMORY)
1873 goto done;
1874
1875 board_delete_ftn_post(board, &fpost);
1876 } else {
1877 size = bile_read_alloc(board->bile, BOARD_POST_RTYPE,
1878 sorted_id_map[n].id, &data);
1879 if (!size)
1880 continue;
1881 ret = bile_unmarshall_object(board->bile,
1882 board_post_object_fields, nboard_post_object_fields,
1883 data, size, &post, sizeof(post), false);
1884 xfree(&data);
1885 if (ret == BILE_ERR_NO_MEMORY)
1886 goto done;
1887
1888 size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE,
1889 post.thread_id, &data);
1890 if (!size)
1891 continue;
1892 ret = bile_unmarshall_object(board->bile,
1893 board_thread_object_fields, nboard_thread_object_fields,
1894 data, size, &thread, sizeof(thread), false);
1895 xfree(&data);
1896 if (ret == BILE_ERR_NO_MEMORY)
1897 goto done;
1898
1899 board_delete_post(board, &post, &thread);
1900 }
1901
1902 deleted++;
1903 uthread_yield();
1904 }
1905
1906 done:
1907 if (sorted_id_map != NULL)
1908 xfree(&sorted_id_map);
1909
1910 if (deleted) {
1911 board_index_sorted_post_ids(board, NULL);
1912
1913 logger_printf("[board] Pruned %lu post%s in %s older than %d day%s",
1914 deleted, deleted == 1 ? "" : "s", board->name,
1915 board->retention_days, board->retention_days == 1 ? "" : "s");
1916 }
1917 }