jcs
/subtext
/amendments
/116
board: Add message boards!
jcs made amendment 116 over 2 years ago
--- board.c Fri Jun 3 14:01:37 2022
+++ board.c Fri Jun 3 14:01:37 2022
@@ -0,0 +1,771 @@
+/*
+ * Copyright (c) 2022 joshua stein <jcs@jcs.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "subtext.h"
+#include "ansi.h"
+#include "board.h"
+#include "user.h"
+
+#define POSTS_PER_PAGE 10
+
+#define POST_READ_RETURN_DONE -1
+#define POST_READ_RETURN_LIST -2
+#define POST_READ_RETURN_FIND -3
+
+struct struct_field board_fields[] = {
+ { "Board ID", CONFIG_TYPE_LONG,
+ offsetof(struct board, id),
+ 1, ULONG_MAX },
+ { "Name", CONFIG_TYPE_STRING,
+ offsetof(struct board, name),
+ 1, member_size(struct board, name) },
+ { "Description", CONFIG_TYPE_STRING,
+ offsetof(struct board, description),
+ 0, member_size(struct board, description) },
+ { "Restricted Posting", CONFIG_TYPE_BOOLEAN,
+ offsetof(struct board, restricted_posting),
+ 0, 0 },
+ { "Restricted Viewing", CONFIG_TYPE_BOOLEAN,
+ offsetof(struct board, restricted_viewing),
+ 0, 0 },
+ { "Days of Retention", CONFIG_TYPE_SHORT,
+ offsetof(struct board, retention_days),
+ 0, USHRT_MAX },
+};
+size_t nboard_fields = nitems(board_fields);
+
+struct bile_object_field board_object_fields[] = {
+ { offsetof(struct board, id),
+ member_size(struct board, id), -1 },
+ { offsetof(struct board, name),
+ member_size(struct board, name), -1 },
+ { offsetof(struct board, description),
+ member_size(struct board, description), -1 },
+ { offsetof(struct board, restricted_posting),
+ member_size(struct board, restricted_posting), -1 },
+ { offsetof(struct board, restricted_viewing),
+ member_size(struct board, restricted_viewing), -1 },
+ { offsetof(struct board, retention_days),
+ member_size(struct board, retention_days), -1 },
+ { offsetof(struct board, last_post_at),
+ member_size(struct board, last_post_at), -1 },
+ { offsetof(struct board, post_count),
+ member_size(struct board, post_count), -1 },
+};
+size_t nboard_object_fields = nitems(board_object_fields);
+
+struct bile_object_field board_post_object_fields[] = {
+ { offsetof(struct board_post, id),
+ member_size(struct board_post, id), -1 },
+ { offsetof(struct board_post, thread_id),
+ member_size(struct board_post, thread_id), -1 },
+ { offsetof(struct board_post, time),
+ member_size(struct board_post, time), -1 },
+ { offsetof(struct board_post, sender_user_id),
+ member_size(struct board_post, sender_user_id), -1 },
+ { offsetof(struct board_post, body_size),
+ member_size(struct board_post, body_size), -1 },
+ { offsetof(struct board_post, body),
+ -1, offsetof(struct board_post, body_size) },
+ { offsetof(struct board_post, parent_post_id),
+ member_size(struct board_post, parent_post_id), -1 },
+};
+size_t nboard_post_object_fields = nitems(board_post_object_fields);
+
+struct bile_object_field board_thread_object_fields[] = {
+ { offsetof(struct board_thread, thread_id),
+ member_size(struct board_thread, thread_id), -1 },
+ { offsetof(struct board_thread, last_post_at),
+ member_size(struct board_thread, last_post_at), -1 },
+ { offsetof(struct board_thread, subject_size),
+ member_size(struct board_thread, subject_size), -1 },
+ { offsetof(struct board_thread, subject),
+ -1, offsetof(struct board_thread, subject_size) },
+ { offsetof(struct board_thread, nposts),
+ member_size(struct board_thread, nposts), -1 },
+ { offsetof(struct board_thread, post_ids),
+ -(sizeof(long)), offsetof(struct board_thread, nposts) },
+ { offsetof(struct board_thread, parent_post_ids),
+ -(sizeof(long)), offsetof(struct board_thread, nposts) },
+};
+size_t nboard_thread_object_fields = nitems(board_thread_object_fields);
+
+unsigned long board_compose(struct session *s, struct board *board,
+ struct board_thread *thread, struct board_post *parent_post,
+ char *initial_subject, char *initial_body);
+void board_list_posts(struct session *s, struct board *board,
+ size_t npost_Ids, unsigned long *post_ids, size_t page, size_t pages);
+short board_post_read(struct session *s, struct board *board,
+ unsigned long id, short idx);
+size_t board_find_post_ids(struct board *board, size_t *npost_ids,
+ unsigned long **post_ids, size_t offset, size_t limit);
+short board_post_create(struct board *board, struct board_thread *thread,
+ struct board_post *post);
+
+void
+board_show(struct session *s, short id)
+{
+ static struct session_menu_option opts[] = {
+ { '#', "#0123456789", "Read post [#]" },
+ { '<', "<", "Previous page of posts" },
+ { 'l', "Ll", "List posts" },
+ { '>', ">", "Next page of posts" },
+ { 'p', "Pp", "Post new thread" },
+ { 'q', "QqXx", "Return to main menu" },
+ { '?', "?", "List menu options" },
+ };
+ struct board *board = NULL;
+ size_t n, nall_post_ids, page, pages, npost_ids;
+ unsigned long *post_ids = NULL;
+ short ret;
+ char c;
+ bool done, find_post_ids, show_list, show_help;
+
+ for (n = 0; n < db->nboards; n++) {
+ if (db->boards[n].id == id) {
+ board = &db->boards[n];
+ break;
+ }
+ }
+
+ if (!board) {
+ session_printf(s, "Invalid board\r\n");
+ session_flush(s);
+ return;
+ }
+
+ page = 0;
+ find_post_ids = true;
+ show_list = true;
+ show_help = false;
+ done = false;
+
+ while (!done) {
+ if (find_post_ids) {
+ nall_post_ids = board_find_post_ids(board, &npost_ids,
+ &post_ids, page * POSTS_PER_PAGE, POSTS_PER_PAGE);
+ /* ceil(npost_ids / POSTS_PER_PAGE) */
+ pages = (nall_post_ids + POSTS_PER_PAGE - 1) / POSTS_PER_PAGE;
+
+ if (page >= pages)
+ page = pages - 1;
+
+ find_post_ids = false;
+ }
+
+ if (show_list) {
+ board_list_posts(s, board, npost_ids, post_ids, page + 1,
+ pages);
+ show_list = false;
+ }
+
+ c = session_menu(s, board->description, board->name, opts,
+ nitems(opts), show_help);
+ show_help = false;
+
+handle_opt:
+ switch (c) {
+ case 'l':
+ show_list = true;
+ break;
+ case 'p':
+ board_compose(s, board, NULL, NULL, NULL, NULL);
+ break;
+ case '>':
+ case '<':
+ if (c == '>' && page == pages - 1) {
+ session_printf(s, "You are at the last page of posts\r\n");
+ session_flush(s);
+ break;
+ }
+ if (c == '<' && page == 0) {
+ session_printf(s, "You are already at the first page\r\n");
+ session_flush(s);
+ break;
+ }
+ if (c == '>')
+ page++;
+ else
+ page--;
+ find_post_ids = true;
+ show_list = true;
+ break;
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ case 7:
+ case 8:
+ case 9:
+ if (c >= npost_ids) {
+ session_printf(s, "Invalid post\r\n");
+ session_flush(s);
+ break;
+ }
+ ret = board_post_read(s, board, post_ids[c], c);
+ switch (ret) {
+ case POST_READ_RETURN_DONE:
+ break;
+ case POST_READ_RETURN_LIST:
+ show_list = true;
+ break;
+ case POST_READ_RETURN_FIND:
+ find_post_ids = true;
+ show_list = true;
+ break;
+ default:
+ c = ret;
+ goto handle_opt;
+ }
+ break;
+ case '?':
+ show_help = true;
+ break;
+ default:
+ done = true;
+ break;
+ }
+ }
+}
+
+void
+board_list_posts(struct session *s, struct board *board, size_t npost_ids,
+ unsigned long *post_ids, unsigned long page, unsigned long pages)
+{
+ char time[24];
+ unsigned long indent_parent_ids[10] = { 0 };
+ char indent_s[22];
+ size_t n, size, idx;
+ struct username_cache *user;
+ struct bile_object *obj;
+ struct board_thread thread = { 0 };
+ struct board_post post;
+ short indent, j, k;
+ char *data;
+
+ session_printf(s, "{{B}}%s: %s (Page %ld of %ld){{/B}}\r\n",
+ board->name, board->description, page, pages);
+ session_printf(s, "%s# N Date From Subject%s\r\n",
+ ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END));
+ session_flush(s);
+
+ if (npost_ids == 0) {
+ session_printf(s, "No posts here yet.\r\n");
+ session_flush(s);
+ return;
+ }
+
+ for (n = 0; n < npost_ids; n++) {
+ size = bile_read_alloc(board->bile, BOARD_POST_RTYPE, post_ids[n],
+ &data);
+ bile_unmarshall_object(board->bile, board_post_object_fields,
+ nboard_post_object_fields, data, size, &post, false);
+ free(data);
+
+ if (post.thread_id != thread.thread_id) {
+ if (thread.thread_id) {
+ free(thread.subject);
+ free(thread.post_ids);
+ free(thread.parent_post_ids);
+ }
+ size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE,
+ post.thread_id, &data);
+ bile_unmarshall_object(board->bile, board_thread_object_fields,
+ nboard_thread_object_fields, data, size, &thread, true);
+ free(data);
+
+ for (j = 0; j < nitems(indent_parent_ids); j++)
+ indent_parent_ids[j] = 0;
+ }
+
+ user = user_find_username(post.sender_user_id);
+ strftime(time, sizeof(time), "%b %d", localtime(&post.time));
+
+ if (post.parent_post_id == 0) {
+ indent_s[0] = '\0';
+ } else {
+ indent = -1;
+ for (j = 0; j < nitems(indent_parent_ids); j++) {
+ if (indent_parent_ids[j] == post.parent_post_id ||
+ indent_parent_ids[j] == 0) {
+ indent = j;
+ indent_parent_ids[j] = post.parent_post_id;
+ for (k = j + 1; k < nitems(indent_parent_ids); k++)
+ indent_parent_ids[k] = 0;
+ break;
+ }
+ }
+ if (indent == -1)
+ indent = nitems(indent_parent_ids) - 1;
+
+ for (j = 0; j < indent - 1; j++) {
+ indent_s[j] = ' ';
+ }
+ indent_s[j] = '`';
+ indent_s[j + 1] = '-';
+ indent_s[j + 2] = '>';
+ indent_s[j + 3] = '\0';
+ }
+ session_printf(s, "%s%ld %c %s %- 10s %s{{#}}%- 40s%s\r\n",
+ true ? "" : ansi(s, ANSI_BOLD, ANSI_END),
+ n,
+ true ? ' ' : 'N',
+ time,
+ user ? user->username : "(unknown)",
+ post.parent_post_id != 0 && n == 0 ? "Re: " : "",
+ post.parent_post_id == 0 || n == 0 ? thread.subject : indent_s,
+ true ? "" : ansi(s, ANSI_RESET, ANSI_END));
+ session_flush(s);
+ }
+
+ if (thread.subject != NULL)
+ free(thread.subject);
+ if (thread.post_ids != NULL)
+ free(thread.post_ids);
+ if (thread.parent_post_ids != NULL)
+ free(thread.parent_post_ids);
+}
+
+unsigned long
+board_compose(struct session *s, struct board *board,
+ struct board_thread *thread, struct board_post *parent_post,
+ char *initial_subject, char *initial_body)
+{
+ struct board_post post = { 0 };
+ char *data = NULL, *tmp = NULL;
+ size_t size;
+ short c, ret;
+
+ if (!s->user) {
+ session_output_string(s, "Posting is not available to guests.\r\n"
+ "Please create an account first.\r\n");
+ session_flush(s);
+ return 0;
+ }
+
+ if (initial_body)
+ post.body = xstrdup(initial_body);
+ if (thread) {
+ post.thread_id = thread->thread_id;
+ post.parent_post_id = parent_post->id;
+ } else
+ thread = xmalloczero(sizeof(struct board_thread));
+
+ post.sender_user_id = s->user->id;
+
+ session_printf(s, "{{B}}Compose %s{{/B}}\r\n",
+ parent_post ? "Reply" : "New Post");
+ session_printf(s, "{{B}}From:{{/B}} %s\r\n", s->user->username);
+ session_printf(s, "{{B}}To:{{/B}} %s\r\n", board->name);
+
+ if (parent_post) {
+ session_printf(s, "{{B}}Subject:{{/B}}{{#}} Re: %s\r\n",
+ thread->subject);
+ session_flush(s);
+ } else {
+ if (initial_subject)
+ thread->subject = xstrdup(initial_subject);
+
+post_compose_start:
+ for (;;) {
+ session_output_template(s, "{{B}}Subject:{{/B}} ");
+ session_flush(s);
+
+ tmp = session_field_input(s, 100, 50, thread->subject,
+ false, 0);
+ if (thread->subject != NULL)
+ free(thread->subject);
+ thread->subject = tmp;
+ session_output(s, "\r\n", 2);
+ session_flush(s);
+ if (thread->subject == NULL)
+ goto post_compose_done;
+ if (thread->subject[0] == '\0') {
+ session_output_template(s, "{{B}}Error:{{/B}} Subject "
+ "cannot be blank (^C to cancel)\r\n");
+ session_flush(s);
+ free(thread->subject);
+ continue;
+ }
+ thread->subject_size = strlen(thread->subject) + 1;
+ break;
+ }
+ }
+
+ for (;;) {
+ session_output_template(s,
+ "{{B}}Message (^D when finished):{{/B}}\r\n");
+ session_flush(s);
+
+ tmp = session_field_input(s, 2048, s->terminal_columns - 1,
+ post.body, true, 0);
+ if (post.body != NULL)
+ free(post.body);
+ post.body = tmp;
+ session_output(s, "\r\n", 2);
+ session_flush(s);
+ if (post.body == NULL)
+ goto post_compose_done;
+ if (post.body[0] == '\0') {
+ free(post.body);
+ goto post_compose_done;
+ }
+ post.body_size = strlen(post.body) + 1;
+ break;
+ }
+
+ for (;;) {
+ session_output_template(s, "\r\n{{B}}(P){{/B}}ost message, "
+ "{{B}}(E){{/B}}dit again, or {{B}}(C){{/B}}ancel? ");
+ session_flush(s);
+
+ c = session_input_char(s);
+ if (c == 0 || s->ending)
+ goto post_compose_done;
+
+ switch (c) {
+ case 'p':
+ case 'P':
+ case 'y':
+ session_printf(s, "%c\r\n", c);
+ session_flush(s);
+ /* FALLTHROUGH */
+ case '\n':
+ case '\r':
+ /* send */
+ session_output_string(s, "Posting message... ");
+ session_flush(s);
+
+ if (board_post_create(board, thread, &post) == 0) {
+ session_log(s, "Posted message %ld to %s", post.id,
+ board->name);
+ session_output_string(s, "done\r\n");
+ } else
+ session_output_string(s, "failed!\r\n");
+
+ session_flush(s);
+
+ goto post_compose_done;
+ case 'e':
+ case 'E':
+ session_printf(s, "%c\r\n", c);
+ session_flush(s);
+ goto post_compose_start;
+ case 'c':
+ case 'C':
+ session_printf(s, "%c\r\n", c);
+ session_flush(s);
+ /* FALLTHROUGH */
+ case CONTROL_C:
+ goto post_compose_done;
+ }
+ }
+
+post_compose_error:
+ session_output_string(s, "Failed saving message!\r\n");
+ session_flush(s);
+
+post_compose_done:
+ if (parent_post == NULL) {
+ if (thread->subject != NULL)
+ free(thread->subject);
+ free(thread);
+ }
+ if (post.body)
+ free(post.body);
+
+ return post.id;
+}
+
+short
+board_post_read(struct session *s, struct board *board, unsigned long id,
+ short idx)
+{
+ static struct session_menu_option opts[] = {
+ { '#', "#0123456789", "Read post [#]" },
+ { 'r', "Rr", "Post reply to this message" },
+ { 'q', "QqXx", "Return to message list" },
+ { '?', "?", "List these options" },
+ };
+ char time[32];
+ struct board_thread thread;
+ struct board_post post;
+ struct username_cache *sender, *recipient;
+ unsigned long new_id;
+ char prompt[DB_BOARD_NAME_LENGTH + 8];
+ size_t n, size;
+ short ret = POST_READ_RETURN_DONE;
+ char c;
+ char *data;
+ bool done = false, show_help = false;
+
+ size = bile_read_alloc(board->bile, BOARD_POST_RTYPE, id, &data);
+ if (size == 0)
+ panic("failed fetching message %ld: %d", id,
+ bile_error(board->bile));
+ bile_unmarshall_object(board->bile, board_post_object_fields,
+ nboard_post_object_fields, data, size, &post, true);
+ free(data);
+
+ size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE,
+ post.thread_id, &data);
+ if (size == 0)
+ panic("failed fetching thread %ld: %d", post.thread_id,
+ bile_error(board->bile));
+
+ bile_unmarshall_object(board->bile, board_thread_object_fields,
+ nboard_thread_object_fields, data, size, &thread, true);
+ free(data);
+
+ sender = user_find_username(post.sender_user_id);
+
+ strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S",
+ localtime(&post.time));
+
+ session_printf(s, "{{B}}From:{{/B}} %s\r\n",
+ sender ? sender->username : "(unknown)");
+ session_printf(s, "{{B}}To:{{/B}} %s\r\n", board->name);
+ session_printf(s, "{{B}}Date:{{/B}} %s %s\r\n", time,
+ db->config.timezone);
+ session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s%s\r\n",
+ (post.parent_post_id ? "Re: " : ""), thread.subject);
+ session_flush(s);
+ session_output_string(s, "\r\n");
+ session_output(s, post.body, post.body_size);
+ session_output_string(s, "\r\n");
+
+ snprintf(prompt, sizeof(prompt), "%s:%ld", board->name, post.id);
+
+ while (!done) {
+ c = session_menu(s, thread.subject, prompt, opts,
+ nitems(opts), show_help);
+ show_help = false;
+
+ switch (c) {
+ case 'r':
+ new_id = board_compose(s, board, &thread, &post, NULL, NULL);
+ if (new_id) {
+ ; //ret = POST_READ_RETURN_FIND;
+ done = true;
+ }
+ break;
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ case 7:
+ case 8:
+ case 9:
+ ret = c;
+ done = true;
+ break;
+ case 'q':
+ done = true;
+ break;
+ case '?':
+ show_help = true;
+ break;
+ }
+ }
+
+ return ret;
+}
+
+size_t
+board_find_post_ids(struct board *board, size_t *npost_ids,
+ unsigned long **post_ids, size_t offset, size_t limit)
+{
+ struct board_thread_map {
+ unsigned long id;
+ time_t time;
+ size_t nposts;
+ };
+ struct bile_object *o;
+ struct board_thread_map *thread_map = NULL, tmp_map;
+ struct board_thread thread;
+ size_t nthreads, nall_post_ids, n, post_ids_size, size, seen_posts;
+ short i, j;
+ char *data;
+
+ post_ids_size = sizeof(long) * 16;
+ *post_ids = xmalloc(post_ids_size);
+ *npost_ids = 0;
+ nall_post_ids = 0;
+
+ nthreads = bile_count_by_type(board->bile, BOARD_THREAD_RTYPE);
+ if (nthreads == 0)
+ return 0;
+
+ thread_map = xcalloc(sizeof(struct board_thread_map), nthreads);
+
+ for (n = 0; o = bile_get_nth_of_type(board->bile, n,
+ BOARD_THREAD_RTYPE); n++) {
+ bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, o->id, &data);
+ bile_unmarshall_object(board->bile, board_thread_object_fields,
+ nboard_thread_object_fields, data, o->size, &thread, false);
+ free(data);
+ free(o);
+
+ thread_map[n].id = thread.thread_id;
+ thread_map[n].time = thread.last_post_at;
+ thread_map[n].nposts = thread.nposts;
+ nall_post_ids += thread.nposts;
+ }
+
+ if (offset >= nall_post_ids)
+ goto done;
+
+ /* sort by last post date descending */
+ for (i = 0; i < nthreads; i++) {
+ for (j = 0; j < nthreads - i - 1; j++) {
+ if (thread_map[j].time < thread_map[j + 1].time) {
+ tmp_map = thread_map[j];
+ thread_map[j] = thread_map[j + 1];
+ thread_map[j + 1] = tmp_map;
+ }
+ }
+ }
+
+ /* gather threads until we run out of space for posts */
+ seen_posts = 0;
+ for (j = 0; j < nthreads; j++) {
+ size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE,
+ thread_map[j].id, &data);
+ bile_unmarshall_object(board->bile, board_thread_object_fields,
+ nboard_thread_object_fields, data, size, &thread, true);
+ free(data);
+
+ for (i = 0; i < thread.nposts; i++) {
+ if (offset > 0 && seen_posts++ < offset)
+ continue;
+
+ EXPAND_TO_FIT(*post_ids, post_ids_size,
+ ((*npost_ids) + 1) * sizeof(long), sizeof(long),
+ sizeof(long) * 16);
+ (*post_ids)[*npost_ids] = thread.post_ids[i];
+ (*npost_ids)++;
+
+ if (*npost_ids >= limit)
+ break;
+ }
+
+ free(thread.post_ids);
+ free(thread.subject);
+
+ if (*npost_ids >= limit)
+ break;
+ }
+
+done:
+ if (thread_map != NULL)
+ free(thread_map);
+
+ return nall_post_ids;
+}
+
+short
+board_post_create(struct board *board, struct board_thread *thread,
+ struct board_post *post)
+{
+ short ret;
+ char *data;
+ size_t size;
+ ssize_t n, j;
+ size_t insert;
+
+ if (post->parent_post_id == 0) {
+ thread->thread_id = bile_next_id(board->bile, BOARD_THREAD_RTYPE);
+ post->thread_id = thread->thread_id;
+ }
+
+ post->id = bile_next_id(board->bile, BOARD_POST_RTYPE);
+ post->time = Time;
+
+ ret = bile_marshall_object(board->bile, board_post_object_fields,
+ nboard_post_object_fields, post, &data, &size);
+ if (ret != 0 || size == 0) {
+ warn("failed to marshall new post object");
+ post->id = 0;
+ goto done;
+ }
+ if (bile_write(board->bile, BOARD_POST_RTYPE, post->id, data,
+ size) != size) {
+ warn("bile_write of new post failed! %d", bile_error(board->bile));
+ post->id = 0;
+ free(data);
+ goto done;
+ }
+
+ thread->last_post_at = post->time;
+ thread->nposts++;
+ thread->post_ids = xreallocarray(thread->post_ids, thread->nposts,
+ sizeof(long));
+ thread->parent_post_ids = xreallocarray(thread->parent_post_ids,
+ thread->nposts, sizeof(long));
+
+ /*
+ * Add new post id to thread post_ids, but put it in the right place
+ * so that reading post_ids will present the tree in order. Walk
+ * parent_post_ids and find the first match of our parent, then insert
+ * our new post there. This puts newest replies at the top.
+ */
+
+ insert = thread->nposts - 1;
+ for (n = 0; n < thread->nposts - 1; n++) {
+ if (thread->parent_post_ids[n] != post->parent_post_id)
+ continue;
+
+ for (j = thread->nposts - 2; j >= n; j--) {
+ thread->post_ids[j + 1] = thread->post_ids[j];
+ thread->parent_post_ids[j + 1] = thread->parent_post_ids[j];
+ }
+ insert = n;
+ break;
+ }
+ thread->post_ids[insert] = post->id;
+ thread->parent_post_ids[insert] = post->parent_post_id;
+
+ ret = bile_marshall_object(board->bile, board_thread_object_fields,
+ nboard_thread_object_fields, thread, &data, &size);
+ if (ret != 0 || size == 0) {
+ warn("failed to marshall thread object");
+ post->id = 0;
+ goto done;
+ }
+ if (bile_write(board->bile, BOARD_THREAD_RTYPE, thread->thread_id,
+ data, size) != size) {
+ warn("bile_write of thread failed! %d", bile_error(board->bile));
+ post->id = 0;
+ goto done;
+ }
+ free(data);
+
+ bile_flush(board->bile, true);
+
+done:
+ return (post->id == 0 ? -1 : 0);
+}
--- board.h Thu May 26 15:23:00 2022
+++ board.h Thu May 26 15:23:00 2022
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2022 joshua stein <jcs@jcs.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef __BOARD_H__
+#define __BOARD_H__
+
+#include <stddef.h>
+#include "session.h"
+
+struct board {
+ unsigned long id;
+ char name[DB_BOARD_NAME_LENGTH];
+ char description[DB_BOARD_DESCR_LENGTH];
+ bool restricted_posting;
+ bool restricted_viewing;
+ unsigned short retention_days;
+ unsigned long last_post_at;
+ unsigned long post_count;
+
+ struct bile *bile;
+};
+
+extern struct struct_field board_fields[];
+extern size_t nboard_fields;
+extern struct bile_object_field board_object_fields[];
+extern size_t nboard_object_fields;
+
+struct board_post {
+ unsigned long id;
+ unsigned long thread_id;
+ time_t time;
+ unsigned long sender_user_id;
+ size_t body_size;
+ char *body;
+ unsigned long parent_post_id;
+};
+extern struct bile_object_field board_post_object_fields[];
+extern size_t nboard_post_object_fields;
+
+struct board_thread {
+ unsigned long thread_id;
+ time_t last_post_at;
+ size_t subject_size;
+ char *subject;
+ unsigned long nposts;
+ unsigned long *post_ids;
+ unsigned long *parent_post_ids;
+};
+extern struct bile_object_field board_thread_object_fields[];
+extern size_t nboard_thread_object_fields;
+
+void board_show(struct session *s, short id);
+
+#endif