AmendHub

Download:

jcs

/

subtext

/

amendments

/

116

board: Add message boards!


jcs made amendment 116 about 1 year 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