AmendHub

Download

jcs

/

subtext

/

board.c

 

(View History)

jcs   board: constify fields Latest amendment: 275 on 2022-11-11

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 "board.h"
25 #include "user.h"
26
27 #define POSTS_PER_PAGE 10
28
29 #define POST_READ_RETURN_DONE -1
30 #define POST_READ_RETURN_LIST -2
31 #define POST_READ_RETURN_FIND -3
32
33 const struct struct_field board_fields[] = {
34 { "Board ID", CONFIG_TYPE_LONG,
35 offsetof(struct board, id),
36 1, ULONG_MAX },
37 { "Name", CONFIG_TYPE_STRING,
38 offsetof(struct board, name),
39 1, member_size(struct board, name) },
40 { "Description", CONFIG_TYPE_STRING,
41 offsetof(struct board, description),
42 0, member_size(struct board, description) },
43 { "Restricted Posting", CONFIG_TYPE_BOOLEAN,
44 offsetof(struct board, restricted_posting),
45 0, 0 },
46 { "Restricted Viewing", CONFIG_TYPE_BOOLEAN,
47 offsetof(struct board, restricted_viewing),
48 0, 0 },
49 { "Days of Retention", CONFIG_TYPE_SHORT,
50 offsetof(struct board, retention_days),
51 0, USHRT_MAX },
52 };
53 const size_t nboard_fields = nitems(board_fields);
54
55 const struct bile_object_field board_object_fields[] = {
56 { offsetof(struct board, id),
57 member_size(struct board, id), -1 },
58 { offsetof(struct board, name),
59 member_size(struct board, name), -1 },
60 { offsetof(struct board, description),
61 member_size(struct board, description), -1 },
62 { offsetof(struct board, restricted_posting),
63 member_size(struct board, restricted_posting), -1 },
64 { offsetof(struct board, restricted_viewing),
65 member_size(struct board, restricted_viewing), -1 },
66 { offsetof(struct board, retention_days),
67 member_size(struct board, retention_days), -1 },
68 { offsetof(struct board, last_post_at),
69 member_size(struct board, last_post_at), -1 },
70 { offsetof(struct board, post_count),
71 member_size(struct board, post_count), -1 },
72 };
73 const size_t nboard_object_fields = nitems(board_object_fields);
74
75 const struct bile_object_field board_post_object_fields[] = {
76 { offsetof(struct board_post, id),
77 member_size(struct board_post, id), -1 },
78 { offsetof(struct board_post, thread_id),
79 member_size(struct board_post, thread_id), -1 },
80 { offsetof(struct board_post, time),
81 member_size(struct board_post, time), -1 },
82 { offsetof(struct board_post, sender_user_id),
83 member_size(struct board_post, sender_user_id), -1 },
84 { offsetof(struct board_post, body_size),
85 member_size(struct board_post, body_size), -1 },
86 { offsetof(struct board_post, body),
87 -1, offsetof(struct board_post, body_size) },
88 { offsetof(struct board_post, parent_post_id),
89 member_size(struct board_post, parent_post_id), -1 },
90 { offsetof(struct board_post, via),
91 member_size(struct board_post, via), -1 },
92 };
93 const size_t nboard_post_object_fields = nitems(board_post_object_fields);
94
95 const struct bile_object_field board_thread_object_fields[] = {
96 { offsetof(struct board_thread, thread_id),
97 member_size(struct board_thread, thread_id), -1 },
98 { offsetof(struct board_thread, last_post_at),
99 member_size(struct board_thread, last_post_at), -1 },
100 { offsetof(struct board_thread, subject_size),
101 member_size(struct board_thread, subject_size), -1 },
102 { offsetof(struct board_thread, subject),
103 -1, offsetof(struct board_thread, subject_size) },
104 { offsetof(struct board_thread, nposts),
105 member_size(struct board_thread, nposts), -1 },
106 { offsetof(struct board_thread, post_ids),
107 -(sizeof(long)), offsetof(struct board_thread, nposts) },
108 { offsetof(struct board_thread, parent_post_ids),
109 -(sizeof(long)), offsetof(struct board_thread, nposts) },
110 };
111 const size_t nboard_thread_object_fields = nitems(board_thread_object_fields);
112
113 unsigned long board_compose(struct session *s, struct board *board,
114 struct board_thread *thread, struct board_post *parent_post,
115 char *initial_subject, char *initial_body);
116 void board_list_posts(struct session *s, struct board *board,
117 size_t npost_Ids, unsigned long *post_ids, size_t page, size_t pages);
118 short board_post_read(struct session *s, struct board *board,
119 unsigned long id, short idx);
120 size_t board_find_post_ids(struct board *board, size_t *npost_ids,
121 unsigned long **post_ids, size_t offset, size_t limit);
122 short board_post_create(struct board *board, struct board_thread *thread,
123 struct board_post *post);
124 void board_delete_post(struct session *s, struct board *board,
125 struct board_post *post, struct board_thread *thread);
126
127 void
128 board_show(struct session *s, short id)
129 {
130 static const struct session_menu_option opts[] = {
131 { '#', "#0123456789", "Read post [#]" },
132 { '<', "<", "Previous page of posts" },
133 { 'l', "Ll", "List posts" },
134 { '>', ">", "Next page of posts" },
135 { 'p', "Pp", "Post new thread" },
136 { 'q', "QqXx", "Return to main menu" },
137 { '?', "?", "List menu options" },
138 };
139 char prompt[7 + BOARD_NAME_LENGTH];
140 struct board *board = NULL;
141 size_t n, nall_post_ids, page, pages, npost_ids;
142 unsigned long *post_ids = NULL;
143 short ret;
144 char c;
145 bool done, find_post_ids, show_list, show_help;
146
147 for (n = 0; n < db->nboards; n++) {
148 if (db->boards[n].id == id) {
149 board = &db->boards[n];
150 break;
151 }
152 }
153
154 if (!board) {
155 session_printf(s, "Invalid board\r\n");
156 session_flush(s);
157 return;
158 }
159
160 page = 0;
161 find_post_ids = true;
162 show_list = true;
163 show_help = false;
164 done = false;
165
166 snprintf(prompt, sizeof(prompt), "Boards:%s", board->name);
167
168 while (!done && !s->ending) {
169 if (find_post_ids) {
170 if (post_ids != NULL) {
171 xfree(&post_ids);
172 post_ids = NULL;
173 }
174 nall_post_ids = board_find_post_ids(board, &npost_ids,
175 &post_ids, page * POSTS_PER_PAGE, POSTS_PER_PAGE);
176 /* ceil(nall_post_ids / POSTS_PER_PAGE) */
177 pages = (nall_post_ids + POSTS_PER_PAGE - 1) / POSTS_PER_PAGE;
178
179 if (page >= pages)
180 page = pages - 1;
181
182 find_post_ids = false;
183 }
184
185 if (show_list) {
186 board_list_posts(s, board, npost_ids, post_ids, page + 1,
187 pages);
188 show_list = false;
189 }
190
191 c = session_menu(s, board->description, prompt, opts,
192 nitems(opts), show_help);
193 show_help = false;
194
195 handle_opt:
196 switch (c) {
197 case 'l':
198 show_list = true;
199 break;
200 case 'p':
201 if (board_compose(s, board, NULL, NULL, NULL, NULL)) {
202 find_post_ids = true;
203 show_list = true;
204 }
205 break;
206 case '>':
207 case '<':
208 if (c == '>' && page == pages - 1) {
209 session_printf(s, "You are at the last page of posts\r\n");
210 session_flush(s);
211 break;
212 }
213 if (c == '<' && page == 0) {
214 session_printf(s, "You are already at the first page\r\n");
215 session_flush(s);
216 break;
217 }
218 if (c == '>')
219 page++;
220 else
221 page--;
222 find_post_ids = true;
223 show_list = true;
224 break;
225 case 0:
226 case 1:
227 case 2:
228 case 3:
229 case 4:
230 case 5:
231 case 6:
232 case 7:
233 case 8:
234 case 9:
235 if (c >= npost_ids) {
236 session_printf(s, "Invalid post\r\n");
237 session_flush(s);
238 break;
239 }
240 ret = board_post_read(s, board, post_ids[c], c);
241 switch (ret) {
242 case POST_READ_RETURN_DONE:
243 break;
244 case POST_READ_RETURN_LIST:
245 show_list = true;
246 break;
247 case POST_READ_RETURN_FIND:
248 find_post_ids = true;
249 show_list = true;
250 break;
251 default:
252 c = ret;
253 goto handle_opt;
254 }
255 break;
256 case '?':
257 show_help = true;
258 break;
259 default:
260 done = true;
261 break;
262 }
263 }
264
265 if (post_ids != NULL)
266 xfree(&post_ids);
267 }
268
269 void
270 board_list_posts(struct session *s, struct board *board, size_t npost_ids,
271 unsigned long *post_ids, unsigned long page, unsigned long pages)
272 {
273 char time[24];
274 unsigned long indent_parent_ids[10] = { 0 };
275 char indent_s[22];
276 size_t n, size, idx;
277 struct username_cache *user;
278 struct bile_object *obj;
279 struct board_thread thread = { 0 };
280 struct board_post post;
281 short indent, j, k;
282 char *data;
283
284 session_printf(s, "{{B}}%s: %s (Page %ld of %ld){{/B}}\r\n",
285 board->name, board->description, page, pages);
286 session_printf(s, "%s# N Date From Subject%s\r\n",
287 ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END));
288 session_flush(s);
289
290 if (npost_ids == 0) {
291 session_printf(s, "No posts here yet.\r\n");
292 session_flush(s);
293 return;
294 }
295
296 for (n = 0; n < npost_ids; n++) {
297 size = bile_read_alloc(board->bile, BOARD_POST_RTYPE, post_ids[n],
298 &data);
299 bile_unmarshall_object(board->bile, board_post_object_fields,
300 nboard_post_object_fields, data, size, &post, sizeof(post),
301 false, "board_list_posts");
302 xfree(&data);
303
304 if (post.thread_id != thread.thread_id) {
305 if (thread.thread_id) {
306 if (thread.subject != NULL)
307 xfree(&thread.subject);
308 if (thread.post_ids != NULL)
309 xfree(&thread.post_ids);
310 if (thread.parent_post_ids != NULL)
311 xfree(&thread.parent_post_ids);
312 }
313 size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE,
314 post.thread_id, &data);
315 bile_unmarshall_object(board->bile, board_thread_object_fields,
316 nboard_thread_object_fields, data, size, &thread,
317 sizeof(thread), true, "board_list_posts");
318 xfree(&data);
319
320 for (j = 0; j < nitems(indent_parent_ids); j++)
321 indent_parent_ids[j] = 0;
322 }
323
324 user = user_username(post.sender_user_id);
325 strftime(time, sizeof(time), "%b %d", localtime(&post.time));
326
327 if (post.parent_post_id == 0) {
328 indent_s[0] = '\0';
329 } else {
330 indent = -1;
331 for (j = 0; j < nitems(indent_parent_ids); j++) {
332 if (indent_parent_ids[j] == post.parent_post_id ||
333 indent_parent_ids[j] == 0) {
334 indent = j;
335 indent_parent_ids[j] = post.parent_post_id;
336 for (k = j + 1; k < nitems(indent_parent_ids); k++) {
337 if (indent_parent_ids[k] == 0)
338 break;
339 indent_parent_ids[k] = 0;
340 }
341 break;
342 }
343 }
344 if (indent == -1)
345 indent = nitems(indent_parent_ids) - 1;
346
347 for (j = 0; j < indent; j++) {
348 indent_s[j] = ' ';
349 }
350 indent_s[j] = '`';
351 indent_s[j + 1] = '-';
352 indent_s[j + 2] = '>';
353 indent_s[j + 3] = '\0';
354 }
355 session_printf(s, "%s%ld %c %s %- 10s %s{{#}}%- 40s%s\r\n",
356 true ? "" : ansi(s, ANSI_BOLD, ANSI_END),
357 n,
358 true ? ' ' : 'N',
359 time,
360 user ? user->username : "(unknown)",
361 post.parent_post_id != 0 && n == 0 ? "Re: " : "",
362 post.parent_post_id == 0 || n == 0 ? thread.subject : indent_s,
363 true ? "" : ansi(s, ANSI_RESET, ANSI_END));
364 }
365 session_flush(s);
366
367 if (thread.subject != NULL)
368 xfree(&thread.subject);
369 if (thread.post_ids != NULL)
370 xfree(&thread.post_ids);
371 if (thread.parent_post_ids != NULL)
372 xfree(&thread.parent_post_ids);
373 }
374
375 unsigned long
376 board_compose(struct session *s, struct board *board,
377 struct board_thread *thread, struct board_post *parent_post,
378 char *initial_subject, char *initial_body)
379 {
380 struct board_post post = { 0 };
381 char *data = NULL, *tmp = NULL;
382 size_t size;
383 short c, ret;
384
385 if (!s->user) {
386 session_printf(s, "Posting is not available to guests.\r\n"
387 "Please create an account first.\r\n");
388 session_flush(s);
389 return 0;
390 }
391
392 if (initial_body)
393 post.body = xstrdup(initial_body, "board_compose body");
394 if (thread) {
395 post.thread_id = thread->thread_id;
396 post.parent_post_id = parent_post->id;
397 } else
398 thread = xmalloczero(sizeof(struct board_thread),
399 "board_compose thread");
400
401 post.sender_user_id = s->user->id;
402 strlcpy(post.via, s->via, sizeof(post.via));
403
404 session_printf(s, "{{B}}Compose %s{{/B}}\r\n",
405 parent_post ? "Reply" : "New Post");
406 session_printf(s, "{{B}}From:{{/B}} %s (via %s)\r\n",
407 s->user->username, s->via);
408 session_printf(s, "{{B}}To:{{/B}} %s\r\n", board->name);
409
410 post_compose_start:
411 if (parent_post) {
412 session_printf(s, "{{B}}Subject:{{/B}}{{#}} Re: %s\r\n",
413 thread->subject);
414 session_flush(s);
415 } else {
416 if (initial_subject && !thread->subject)
417 thread->subject = xstrdup(initial_subject,
418 "board_compose subject");
419
420 for (;;) {
421 session_printf(s, "{{B}}Subject:{{/B}} ");
422 session_flush(s);
423
424 tmp = session_field_input(s, 100, 50, thread->subject,
425 false, 0);
426 if (thread->subject != NULL)
427 xfree(&thread->subject);
428 thread->subject = tmp;
429 session_output(s, "\r\n", 2);
430 session_flush(s);
431
432 if (thread->subject == NULL)
433 goto post_compose_done;
434
435 rtrim(thread->subject, "\r\n\t ");
436
437 if (thread->subject[0] == '\0') {
438 session_printf(s, "{{B}}Error:{{/B}} Subject "
439 "cannot be blank (^C to cancel)\r\n");
440 session_flush(s);
441 xfree(&thread->subject);
442 continue;
443 }
444 thread->subject_size = strlen(thread->subject) + 1;
445 break;
446 }
447 }
448
449 for (;;) {
450 session_printf(s, "{{B}}Message (^D when finished):{{/B}}\r\n");
451 session_flush(s);
452
453 tmp = session_field_input(s, 2048, s->terminal_columns - 1,
454 post.body, true, 0);
455 if (post.body != NULL)
456 xfree(&post.body);
457 post.body = tmp;
458 session_output(s, "\r\n", 2);
459 session_flush(s);
460
461 if (post.body == NULL)
462 goto post_compose_done;
463
464 rtrim(post.body, "\r\n\t ");
465
466 if (post.body[0] == '\0') {
467 xfree(&post.body);
468 goto post_compose_done;
469 }
470 post.body_size = strlen(post.body) + 1;
471 break;
472 }
473
474 for (;;) {
475 session_printf(s, "\r\n{{B}}(P){{/B}}ost message, "
476 "{{B}}(E){{/B}}dit again, or {{B}}(C){{/B}}ancel? ");
477 session_flush(s);
478
479 c = session_input_char(s);
480 if (c == 0 || s->ending)
481 goto post_compose_done;
482
483 switch (c) {
484 case 'p':
485 case 'P':
486 case 'y':
487 session_printf(s, "%c\r\n", c);
488 session_flush(s);
489 /* FALLTHROUGH */
490 case '\n':
491 case '\r':
492 /* send */
493 session_printf(s, "Posting message... ");
494 session_flush(s);
495
496 if (board_post_create(board, thread, &post) == 0) {
497 session_logf(s, "Posted message %ld to %s", post.id,
498 board->name);
499 session_printf(s, "done\r\n");
500 } else
501 session_printf(s, "failed!\r\n");
502
503 session_flush(s);
504
505 goto post_compose_done;
506 case 'e':
507 case 'E':
508 session_printf(s, "%c\r\n", c);
509 session_flush(s);
510 goto post_compose_start;
511 case 'c':
512 case 'C':
513 session_printf(s, "%c\r\n", c);
514 session_flush(s);
515 /* FALLTHROUGH */
516 case CONTROL_C:
517 goto post_compose_done;
518 }
519 }
520
521 post_compose_error:
522 session_printf(s, "Failed saving message!\r\n");
523 session_flush(s);
524
525 post_compose_done:
526 if (parent_post == NULL) {
527 if (thread->subject != NULL)
528 xfree(&thread->subject);
529 xfree(&thread);
530 }
531 if (post.body)
532 xfree(&post.body);
533
534 return post.id;
535 }
536
537 short
538 board_post_read(struct session *s, struct board *board, unsigned long id,
539 short idx)
540 {
541 static const struct session_menu_option opts[] = {
542 { '#', "#0123456789", "Read post [#]" },
543 { 'd', "Dd", "Delete this post" },
544 { 'r', "Rr", "Reply to this post" },
545 { 'q', "QqXx", "Return to threads" },
546 { '?', "?", "List these options" },
547 };
548 char time[32];
549 struct board_thread thread;
550 struct board_post post;
551 struct username_cache *sender, *recipient;
552 struct session_menu_option *dopts = NULL;
553 unsigned long new_id;
554 char prompt[7 + BOARD_NAME_LENGTH + 8];
555 size_t n, size;
556 short ret = POST_READ_RETURN_DONE;
557 char c;
558 char *data;
559 short cc;
560 bool done = false, show_help = false;
561
562 size = bile_read_alloc(board->bile, BOARD_POST_RTYPE, id, &data);
563 if (size == 0)
564 panic("failed fetching message %ld: %d", id,
565 bile_error(board->bile));
566 bile_unmarshall_object(board->bile, board_post_object_fields,
567 nboard_post_object_fields, data, size, &post, sizeof(post), true,
568 "board_post_read post");
569 xfree(&data);
570
571 size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE,
572 post.thread_id, &data);
573 if (size == 0)
574 panic("failed fetching thread %ld: %d", post.thread_id,
575 bile_error(board->bile));
576 bile_unmarshall_object(board->bile, board_thread_object_fields,
577 nboard_thread_object_fields, data, size, &thread, sizeof(thread),
578 true, "board_post_read thread");
579 xfree(&data);
580
581 dopts = xmalloc(sizeof(opts), "board_post_read opts");
582 memcpy(dopts, opts, sizeof(opts));
583 if (!(s->user && (s->user->is_sysop ||
584 s->user->id == post.sender_user_id))) {
585 /* disable deleting */
586 dopts[1].key[0] = '\0';
587 }
588
589 sender = user_username(post.sender_user_id);
590
591 strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S",
592 localtime(&post.time));
593
594 session_printf(s, "{{B}}From:{{/B}} %s",
595 sender ? sender->username : "(unknown)");
596 if (post.via[0])
597 session_printf(s, " (via %s)", post.via);
598 session_printf(s, "\r\n");
599 session_printf(s, "{{B}}To:{{/B}} %s\r\n", board->name);
600 session_printf(s, "{{B}}Date:{{/B}} %s %s\r\n", time,
601 db->config.timezone);
602 session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s%s\r\n",
603 (post.parent_post_id ? "Re: " : ""), thread.subject);
604 session_flush(s);
605 session_printf(s, "\r\n");
606 session_output(s, post.body, post.body_size);
607 session_printf(s, "\r\n");
608
609 snprintf(prompt, sizeof(prompt), "Boards:%s:%d", board->name, idx);
610
611 while (!done && !s->ending) {
612 c = session_menu(s, thread.subject, prompt, dopts,
613 nitems(opts), show_help);
614 show_help = false;
615
616 switch (c) {
617 case 'd':
618 if (!(s->user && (s->user->is_sysop ||
619 s->user->id == post.sender_user_id))) {
620 session_printf(s, "Invalid option\r\n");
621 session_flush(s);
622 break;
623 }
624
625 session_printf(s, "Are you sure you want to permanently "
626 "delete this post? [y/N] ");
627 session_flush(s);
628
629 cc = session_input_char(s);
630 if (cc == 'y' || c == 'Y') {
631 session_printf(s, "%c\r\n", cc);
632 session_flush(s);
633
634 board_delete_post(s, board, &post, &thread);
635
636 session_logf(s, "Deleted post %ld (thread %ld)", post.id,
637 thread.thread_id);
638
639 session_printf(s, "\r\n{{B}}Post deleted!{{/B}}\r\n");
640 session_flush(s);
641 ret = POST_READ_RETURN_FIND;
642 done = true;
643 } else {
644 session_printf(s, "\r\Post not deleted.\r\n");
645 session_flush(s);
646 }
647 break;
648 case 'r':
649 if (board_compose(s, board, &thread, &post, NULL, NULL)) {
650 ret = POST_READ_RETURN_FIND;
651 done = true;
652 }
653 break;
654 case 0:
655 case 1:
656 case 2:
657 case 3:
658 case 4:
659 case 5:
660 case 6:
661 case 7:
662 case 8:
663 case 9:
664 ret = c;
665 done = true;
666 break;
667 case 'q':
668 done = true;
669 break;
670 case '?':
671 show_help = true;
672 break;
673 }
674 }
675
676 xfree(&dopts);
677
678 if (post.body != NULL)
679 xfree(&post.body);
680 if (thread.subject != NULL)
681 xfree(&thread.subject);
682 if (thread.post_ids != NULL)
683 xfree(&thread.post_ids);
684 if (thread.parent_post_ids != NULL)
685 xfree(&thread.parent_post_ids);
686
687 return ret;
688 }
689
690 size_t
691 board_find_post_ids(struct board *board, size_t *npost_ids,
692 unsigned long **post_ids, size_t offset, size_t limit)
693 {
694 struct board_thread_map {
695 unsigned long id;
696 time_t time;
697 size_t nposts;
698 };
699 struct bile_object *o;
700 struct board_thread_map *thread_map = NULL, tmp_map;
701 struct board_thread thread;
702 size_t nthreads, nall_post_ids, n, post_ids_size, size, seen_posts;
703 short i, j;
704 char *data;
705
706 post_ids_size = 0;
707 *post_ids = NULL;
708 *npost_ids = 0;
709 nall_post_ids = 0;
710
711 nthreads = bile_count_by_type(board->bile, BOARD_THREAD_RTYPE);
712 if (nthreads == 0)
713 return 0;
714
715 thread_map = xcalloc(sizeof(struct board_thread_map), nthreads,
716 "board_find_post_ids thread_map");
717
718 for (n = 0; o = bile_get_nth_of_type(board->bile, n,
719 BOARD_THREAD_RTYPE); n++) {
720 if (n >= nthreads) {
721 xfree(&o);
722 break;
723 }
724 bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, o->id, &data);
725 bile_unmarshall_object(board->bile, board_thread_object_fields,
726 nboard_thread_object_fields, data, o->size, &thread,
727 sizeof(thread), false, "board_find_post_ids 1");
728 xfree(&data);
729 xfree(&o);
730
731 thread_map[n].id = thread.thread_id;
732 thread_map[n].time = thread.last_post_at;
733 thread_map[n].nposts = thread.nposts;
734 nall_post_ids += thread.nposts;
735 }
736
737 if (offset >= nall_post_ids)
738 goto done;
739
740 /* sort by last post date descending */
741 for (i = 0; i < nthreads; i++) {
742 for (j = 0; j < nthreads - i - 1; j++) {
743 if (thread_map[j].time < thread_map[j + 1].time) {
744 tmp_map = thread_map[j];
745 thread_map[j] = thread_map[j + 1];
746 thread_map[j + 1] = tmp_map;
747 }
748 }
749 }
750
751 /* gather threads until we run out of space for posts */
752 seen_posts = 0;
753 for (j = 0; j < nthreads; j++) {
754 size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE,
755 thread_map[j].id, &data);
756 bile_unmarshall_object(board->bile, board_thread_object_fields,
757 nboard_thread_object_fields, data, size, &thread, sizeof(thread),
758 true, "board_find_post_ids 2");
759 xfree(&data);
760
761 for (i = 0; i < thread.nposts; i++) {
762 if (offset > 0 && seen_posts++ < offset)
763 continue;
764
765 EXPAND_TO_FIT(*post_ids, post_ids_size,
766 ((*npost_ids) + 1) * sizeof(long), sizeof(long),
767 sizeof(long) * 16);
768 (*post_ids)[*npost_ids] = thread.post_ids[i];
769 (*npost_ids)++;
770
771 if (*npost_ids >= limit)
772 break;
773 }
774
775 if (thread.subject != NULL)
776 xfree(&thread.subject);
777 if (thread.post_ids != NULL)
778 xfree(&thread.post_ids);
779 if (thread.parent_post_ids != NULL)
780 xfree(&thread.parent_post_ids);
781
782 if (*npost_ids >= limit)
783 break;
784 }
785
786 done:
787 if (thread_map != NULL)
788 xfree(&thread_map);
789
790 return nall_post_ids;
791 }
792
793 short
794 board_post_create(struct board *board, struct board_thread *thread,
795 struct board_post *post)
796 {
797 short ret;
798 char *data;
799 size_t size;
800 ssize_t n, j;
801 size_t insert;
802
803 if (post->parent_post_id == 0) {
804 thread->thread_id = bile_next_id(board->bile, BOARD_THREAD_RTYPE);
805 post->thread_id = thread->thread_id;
806 }
807
808 post->id = bile_next_id(board->bile, BOARD_POST_RTYPE);
809 post->time = Time;
810
811 ret = bile_marshall_object(board->bile, board_post_object_fields,
812 nboard_post_object_fields, post, &data, &size,
813 "board_post_create post");
814 if (ret != 0 || size == 0) {
815 warn("failed to marshall new post object");
816 post->id = 0;
817 goto done;
818 }
819 if (bile_write(board->bile, BOARD_POST_RTYPE, post->id, data,
820 size) != size) {
821 warn("bile_write of new post failed! %d", bile_error(board->bile));
822 post->id = 0;
823 xfree(&data);
824 goto done;
825 }
826 xfree(&data);
827
828 thread->last_post_at = post->time;
829 thread->nposts++;
830 thread->post_ids = xreallocarray(thread->post_ids, thread->nposts,
831 sizeof(long));
832 thread->parent_post_ids = xreallocarray(thread->parent_post_ids,
833 thread->nposts, sizeof(long));
834
835 /*
836 * Add new post id to thread post_ids, but put it in the right place
837 * so that reading post_ids will present the tree in order. Walk
838 * parent_post_ids and find the first match of our parent, then insert
839 * our new post there. This puts newest replies at the top.
840 */
841
842 insert = thread->nposts - 1;
843 for (n = 0; n < thread->nposts - 1; n++) {
844 if (thread->post_ids[n] != post->parent_post_id)
845 continue;
846
847 for (j = thread->nposts - 2; j > n; j--) {
848 thread->post_ids[j + 1] = thread->post_ids[j];
849 thread->parent_post_ids[j + 1] = thread->parent_post_ids[j];
850 }
851 insert = n + 1;
852 break;
853 }
854 thread->post_ids[insert] = post->id;
855 thread->parent_post_ids[insert] = post->parent_post_id;
856
857 ret = bile_marshall_object(board->bile, board_thread_object_fields,
858 nboard_thread_object_fields, thread, &data, &size,
859 "board_post_create thread");
860 if (ret != 0 || size == 0) {
861 warn("failed to marshall thread object");
862 post->id = 0;
863 goto done;
864 }
865 if (bile_write(board->bile, BOARD_THREAD_RTYPE, thread->thread_id,
866 data, size) != size) {
867 warn("bile_write of thread failed! %d", bile_error(board->bile));
868 post->id = 0;
869 xfree(&data);
870 goto done;
871 }
872 xfree(&data);
873
874 bile_flush(board->bile, true);
875
876 done:
877 return (post->id == 0 ? -1 : 0);
878 }
879
880 void
881 board_delete_post(struct session *s, struct board *board,
882 struct board_post *post, struct board_thread *thread)
883 {
884 size_t size, n, nn;
885 char *data;
886 char del[50];
887 short ret;
888 bool childs = false;
889 unsigned long *new_post_ids;
890 unsigned long *new_parent_post_ids;
891
892 if (thread->nposts == 1) {
893 bile_delete(board->bile, BOARD_THREAD_RTYPE, thread->thread_id);
894 bile_delete(board->bile, BOARD_POST_RTYPE, post->id);
895 return;
896 }
897
898 for (n = 0; n < thread->nposts; n++) {
899 if (thread->parent_post_ids[n] == post->id) {
900 childs = true;
901 break;
902 }
903 }
904
905 if (!childs) {
906 /* just zap this off the end of the tree */
907 new_post_ids = xcalloc(thread->nposts - 1, sizeof(unsigned long),
908 "board_delete_post new_post_ids");
909 new_parent_post_ids = xcalloc(thread->nposts - 1,
910 sizeof(unsigned long), "board_delete_post new_parent_post_ids");
911
912 for (n = 0, nn = 0; n < thread->nposts; n++) {
913 if (thread->post_ids[n] == post->id)
914 continue;
915
916 new_post_ids[nn] = thread->post_ids[n];
917 nn++;
918 }
919
920 for (n = 0, nn = 0; n < thread->nposts; n++) {
921 if (thread->post_ids[n] == post->id)
922 continue;
923
924 new_parent_post_ids[nn] = thread->parent_post_ids[n];
925 nn++;
926 }
927
928 thread->nposts--;
929
930 xfree(&thread->post_ids);
931 thread->post_ids = new_post_ids;
932 xfree(&thread->parent_post_ids);
933 thread->parent_post_ids = new_parent_post_ids;
934
935 ret = bile_marshall_object(board->bile, board_thread_object_fields,
936 nboard_thread_object_fields, thread, &data, &size,
937 "board_delete_post");
938 if (ret != 0 || size == 0) {
939 warn("failed to marshall thread object during post delete");
940 return;
941 }
942 if (bile_write(board->bile, BOARD_THREAD_RTYPE, thread->thread_id,
943 data, size) != size) {
944 warn("bile_write of updated thread after post delete failed! "
945 "%d", bile_error(board->bile));
946 xfree(&data);
947 return;
948 }
949 xfree(&data);
950
951 bile_delete(board->bile, BOARD_POST_RTYPE, post->id);
952
953 bile_flush(board->bile, true);
954 return;
955 }
956
957 /* all we can do is change the post to say deleted */
958 if (post->body != NULL)
959 xfree(&post->body);
960 snprintf(del, sizeof(del), "[ Post deleted by %s ]",
961 s->user ? s->user->username : "unknown");
962 post->body = xstrdup(del, "board_delete_post deleted body");
963 post->body_size = strlen(post->body) + 1;
964
965 ret = bile_marshall_object(board->bile, board_post_object_fields,
966 nboard_post_object_fields, post, &data, &size, "board_delete_post");
967 if (ret != 0 || size == 0) {
968 warn("failed to marshall updated post object");
969 return;
970 }
971 if (bile_write(board->bile, BOARD_POST_RTYPE, post->id, data,
972 size) != size) {
973 warn("bile_write of updated post failed! %d",
974 bile_error(board->bile));
975 xfree(&data);
976 return;
977 }
978
979 xfree(&data);
980 }