/* * Copyright (c) 2022 joshua stein * * 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 #include #include #include #include "subtext.h" #include "ansi.h" #include "binkp.h" #include "board.h" #include "logger.h" #include "user.h" #define POSTS_PER_PAGE 20 #define POST_READ_RETURN_DONE -1 #define POST_READ_RETURN_LIST -2 #define POST_READ_RETURN_FIND -3 #define POST_READ_RETURN_NEWER -4 #define POST_READ_RETURN_OLDER -5 const struct struct_field board_fields[] = { { "Board ID", CONFIG_TYPE_LONG, offsetof(struct board, id), 1, LONG_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 }, { "FTN Area Name", CONFIG_TYPE_STRING, offsetof(struct board, ftn_area), 0, member_size(struct board, ftn_area) }, }; const size_t nboard_fields = nitems(board_fields); const 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 }, { offsetof(struct board, ftn_area), member_size(struct board, ftn_area), -1 }, }; const size_t nboard_object_fields = nitems(board_object_fields); const 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 }, { offsetof(struct board_post, via), member_size(struct board_post, via), -1 }, }; const size_t nboard_post_object_fields = nitems(board_post_object_fields); const struct bile_object_field board_ftn_post_object_fields[] = { { offsetof(struct board_ftn_post, id), member_size(struct board_ftn_post, id), -1 }, { offsetof(struct board_ftn_post, msgid), member_size(struct board_ftn_post, msgid), -1 }, { offsetof(struct board_ftn_post, time), member_size(struct board_ftn_post, time), -1 }, { offsetof(struct board_ftn_post, from), member_size(struct board_ftn_post, from), -1 }, { offsetof(struct board_ftn_post, subject), member_size(struct board_ftn_post, subject), -1 }, { offsetof(struct board_ftn_post, msgid_orig), member_size(struct board_ftn_post, msgid_orig), -1 }, { offsetof(struct board_ftn_post, origin), member_size(struct board_ftn_post, origin), -1 }, { offsetof(struct board_ftn_post, reply), member_size(struct board_ftn_post, reply), -1 }, { offsetof(struct board_ftn_post, to), member_size(struct board_ftn_post, to), -1 }, { offsetof(struct board_ftn_post, body_size), member_size(struct board_ftn_post, body_size), -1 }, { offsetof(struct board_ftn_post, body), -1, offsetof(struct board_ftn_post, body_size) }, }; const size_t nboard_ftn_post_object_fields = nitems(board_ftn_post_object_fields); const 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) }, }; const 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, struct board_ftn_post *ftn_parent_post, char *initial_subject, char *initial_body); short board_post_create(struct board *board, struct board_thread *thread, struct board_ftn_post *ftn_parent_post, struct board_post *post); 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, char *prompt_prefix, unsigned long id, short idx); size_t board_find_post_ids(struct session *s, struct board *board, size_t *npost_ids, unsigned long **post_ids, size_t offset, size_t limit); void board_delete_cached_index(struct board *board); void board_list_boards(struct session *s) { static struct session_menu_option opts[] = { { '#', "#", "Enter board number to read" }, { 'l', "Ll", "List boards" }, { 'q', "QqXx", "Return to main menu" }, { '?', "?", "List menu options" }, }; static const char prompt_help[] = "#:View Board L:List Boards Q:Return ?:Help"; struct board *lboards = NULL, tboard; size_t nlboards; char title[] = "List Boards"; char c; short an, n, i, j; bool done, show_list, show_help; lboards = xcalloc(sizeof(struct board), db->nboards); if (lboards == NULL) return; nlboards = 0; for (n = 0; n < db->nboards; n++) { if (db->boards[n].ftn_area[0] == '\0') { memcpy(&lboards[nlboards], &db->boards[n], sizeof(struct board)); nlboards++; } } /* sort by board name */ for (i = 1; i < nlboards; i++) { for (j = i; j > 0; j--) { tboard = lboards[j]; if (strcmp(lboards[j].name, lboards[j - 1].name) > 0) break; tboard = lboards[j]; lboards[j] = lboards[j - 1]; lboards[j - 1] = tboard; } } show_list = true; show_help = false; done = false; snprintf(title, sizeof(title), "Message Boards"); while (!done && !s->ending) { if (show_list) { session_printf(s, "{{B}}%s{{/B}}\r\n", title); session_printf(s, "%s # Board Description%s\r\n", ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); session_flush(s); for (n = 0; n < nlboards; n++) { session_printf(s, "%2d %- 13.13s %s\r\n", n + 1, lboards[n].name, lboards[n].description); } session_flush(s); show_list = false; } c = session_menu(s, title, "Boards", (char *)prompt_help, opts, nitems(opts), show_help, "Board #", &an); show_help = false; switch (c) { case 'l': show_list = true; break; case '#': if (an < 1 || an > nlboards) { session_printf(s, "Invalid board\r\n"); session_flush(s); break; } board_show(s, lboards[an - 1].id, "Boards"); break; case '?': show_help = true; break; default: done = true; break; } } if (lboards != NULL) xfree(&lboards); } void board_list_ftn_areas(struct session *s) { static struct session_menu_option opts[] = { { '#', "#", "Enter area number to read" }, { 'l', "Ll", "..." }, { 'q', "QqXx", "Return to main menu" }, { '?', "?", "List menu options" }, }; static const char prompt_help[] = "#:View Area L:List Areas Q:Return ?:Help"; struct board *fboards = NULL, tboard; struct fidopkt_address our_address; size_t nfboards; char title[50], latest[10]; char c; short an, n, i, j; bool done, show_list, show_help; if (!fidopkt_parse_address(db->config.ftn_node_addr, &our_address)) { session_printf(s, "{{B}}Error:{{/B}} FTN Areas are not supported " "on this system.\r\n"); session_flush(s); return; } snprintf(opts[1].title, sizeof(opts[1].title), "List %s Areas", db->config.ftn_network); fboards = xcalloc(sizeof(struct board), db->nboards); if (fboards == NULL) return; nfboards = 0; for (n = 0; n < db->nboards; n++) { if (db->boards[n].ftn_area[0]) { memcpy(&fboards[nfboards], &db->boards[n], sizeof(struct board)); nfboards++; } } /* sort by area name */ for (i = 1; i < nfboards; i++) { for (j = i; j > 0; j--) { if (strcmp(fboards[j].ftn_area, fboards[j - 1].ftn_area) > 0) break; tboard = fboards[j]; fboards[j] = fboards[j - 1]; fboards[j - 1] = tboard; } } show_list = true; show_help = false; done = false; snprintf(title, sizeof(title), "%s Areas (Local Node %s)", db->config.ftn_network, db->config.ftn_node_addr); while (!done && !s->ending) { if (show_list) { session_printf(s, "{{B}}%s{{/B}}\r\n", title); session_printf(s, "%s # Latest Area Description%s\r\n", ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); session_flush(s); for (n = 0; n < nfboards; n++) { if (fboards[n].last_post_at) strftime(latest, sizeof(latest), "%b %d", localtime(&fboards[n].last_post_at)); else latest[0] = '\0'; session_printf(s, "%2d % 6s %- 13.13s %s\r\n", n + 1, latest, fboards[n].ftn_area, fboards[n].description); } session_flush(s); show_list = false; } c = session_menu(s, title, db->config.ftn_network, (char *)prompt_help, opts, nitems(opts), show_help, "Area #", &an); show_help = false; switch (c) { case 'l': show_list = true; break; case '#': if (an < 1 || an > nfboards) { session_printf(s, "Invalid area\r\n"); session_flush(s); break; } board_show(s, fboards[an - 1].id, db->config.ftn_network); break; case '?': show_help = true; break; default: done = true; break; } } if (fboards != NULL) xfree(&fboards); } void board_show(struct session *s, short id, char *prompt_prefix) { static const struct session_menu_option opts[] = { { '#', "#", "Enter post to read" }, { '<', "<", "View newer posts" }, { 'l', "Ll", "List posts" }, { '>', ">", "View older posts" }, { 'p', "Pp", "Post new thread" }, { 'q', "QqXx", "Return to main menu" }, { '?', "?", "List menu options" }, }; static const char prompt_help[] = "#:Read <:Newer >:Older L:List P:New Q:Return ?:Help"; char prompt[7 + member_size(struct board, name)]; struct board *board = NULL; size_t n, nall_post_ids, page, pages, npost_ids; unsigned long *post_ids = NULL; short ppp, ret, pn; char c, next_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; next_c = 0; if (prompt_prefix == NULL) prompt_prefix = "Boards"; while (!done && !s->ending) { if (find_post_ids) { if (post_ids != NULL) { xfree(&post_ids); post_ids = NULL; } ppp = POSTS_PER_PAGE; if (s->terminal_lines < ppp + 3) ppp = BOUND(ppp, 5, s->terminal_lines - 3); nall_post_ids = board_find_post_ids(s, board, &npost_ids, &post_ids, page * ppp, ppp); /* ceil(nall_post_ids / ppp) */ pages = (nall_post_ids + ppp - 1) / ppp; 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; } snprintf(prompt, sizeof(prompt), "%s:%s:%ld", prompt_prefix, board->name, page + 1); if (next_c) { c = next_c; next_c = 0; } else { c = session_menu(s, board->description, prompt, (char *)prompt_help, opts, nitems(opts), show_help, "Post #", &pn); show_help = false; } handle_opt: switch (c) { case 'l': show_list = true; break; case 'p': if (board_compose(s, board, NULL, NULL, NULL, NULL, NULL)) { find_post_ids = true; show_list = true; } 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 '#': check_pn: if (pn < 1 || pn > npost_ids) { session_printf(s, "Invalid post\r\n"); session_flush(s); break; } ret = board_post_read(s, board, prompt, post_ids[pn - 1], pn); 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; case POST_READ_RETURN_NEWER: if (pn == 1) { if (page == 0) { session_printf(s, "No newer posts.\r\n"); session_flush(s); break; } else { page--; find_post_ids = true; pn = npost_ids; next_c = '#'; } } else { pn--; goto check_pn; } break; case POST_READ_RETURN_OLDER: if (pn == npost_ids) { if (page == pages - 1) { session_printf(s, "No more posts.\r\n"); session_flush(s); break; } else { page++; find_post_ids = true; pn = 1; next_c = '#'; } } else { pn++; goto check_pn; } break; default: c = ret; goto handle_opt; } break; case '?': show_help = true; break; default: done = true; break; } } if (post_ids != NULL) xfree(&post_ids); } 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; struct username_cache *user; struct board_thread thread = { 0 }; struct board_post post; struct board_ftn_post fpost; short indent, j, k, ret; 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++) { if (board->ftn_area[0]) { size = bile_read_alloc(board->bile, BOARD_FTN_POST_RTYPE, post_ids[n], &data); ret = bile_unmarshall_object(board->bile, board_ftn_post_object_fields, nboard_ftn_post_object_fields, data, size, &fpost, sizeof(fpost), false); xfree(&data); if (ret == BILE_ERR_NO_MEMORY) break; strftime(time, sizeof(time), "%b %d", localtime(&fpost.time)); session_printf(s, "%s%2ld %c %s {{#}}%-10.10s %s%.50s%s\r\n", true ? "" : ansi(s, ANSI_BOLD, ANSI_END), n + 1, true ? ' ' : 'N', time, fpost.from, (fpost.reply[0] && strncasecmp(fpost.subject, "Re:", 3) != 0 ? "Re: " : ""), fpost.subject, true ? "" : ansi(s, ANSI_RESET, ANSI_END)); } else { size = bile_read_alloc(board->bile, BOARD_POST_RTYPE, post_ids[n], &data); ret = bile_unmarshall_object(board->bile, board_post_object_fields, nboard_post_object_fields, data, size, &post, sizeof(post), false); xfree(&data); if (ret != 0) break; if (post.thread_id != thread.thread_id) { if (thread.thread_id) { if (thread.subject != NULL) xfree(&thread.subject); if (thread.post_ids != NULL) xfree(&thread.post_ids); if (thread.parent_post_ids != NULL) xfree(&thread.parent_post_ids); } size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, post.thread_id, &data); ret = bile_unmarshall_object(board->bile, board_thread_object_fields, nboard_thread_object_fields, data, size, &thread, sizeof(thread), true); xfree(&data); if (ret == BILE_ERR_NO_MEMORY) break; for (j = 0; j < nitems(indent_parent_ids); j++) indent_parent_ids[j] = 0; } user = user_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++) { if (indent_parent_ids[k] == 0) break; indent_parent_ids[k] = 0; } break; } } if (indent == -1) indent = nitems(indent_parent_ids) - 1; for (j = 0; j < indent; j++) { indent_s[j] = ' '; } indent_s[j] = '`'; indent_s[j + 1] = '-'; indent_s[j + 2] = '>'; indent_s[j + 3] = '\0'; } session_printf(s, "%s%2ld %c %s %-10.10s %s{{#}}%.40s%s\r\n", true ? "" : ansi(s, ANSI_BOLD, ANSI_END), n + 1, 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) xfree(&thread.subject); if (thread.post_ids != NULL) xfree(&thread.post_ids); if (thread.parent_post_ids != NULL) xfree(&thread.parent_post_ids); } unsigned long board_compose(struct session *s, struct board *board, struct board_thread *thread, struct board_post *parent_post, struct board_ftn_post *ftn_parent_post, char *initial_subject, char *initial_body) { struct board_post post = { 0 }; char *tmp = NULL; short c; if (!s->user) { session_printf(s, "Posting is not available to guests.\r\n" "Please create an account first.\r\n"); session_flush(s); return 0; } if (board->restricted_posting && !s->user->is_sysop) { session_printf(s, "Posting to this board is not permitted.\r\n"); session_flush(s); return 0; } if (initial_body) { post.body = xstrdup(initial_body); if (post.body == NULL) return 0; } if (thread) { post.thread_id = thread->thread_id; post.parent_post_id = parent_post->id; } else { thread = xmalloczero(sizeof(struct board_thread)); if (thread == NULL) return 0; } post.sender_user_id = s->user->id; strlcpy(post.via, s->via, sizeof(post.via)); session_printf(s, "{{B}}Compose %s{{/B}}\r\n", parent_post ? "Reply" : "New Post"); session_printf(s, "{{B}}From:{{/B}} %s (via %s)\r\n", s->user->username, s->via); session_printf(s, "{{B}}To:{{/B}} %s\r\n", board->name); post_compose_start: if (parent_post) { session_printf(s, "{{B}}Subject:{{/B}}{{#}} Re: %s\r\n", thread->subject); session_flush(s); } else if (ftn_parent_post) { session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s%s\r\n", strncmp("Re:", ftn_parent_post->subject, 3) == 1 ? "" : "Re: ", ftn_parent_post->subject); session_flush(s); } else { if (initial_subject && !thread->subject) { thread->subject = xstrdup(initial_subject); if (thread->subject == NULL) return 0; } for (;;) { session_printf(s, "{{B}}Subject:{{/B}} "); session_flush(s); tmp = session_field_input(s, 100, 50, thread->subject, false, 0); if (thread->subject != NULL) xfree(&thread->subject); thread->subject = tmp; session_output(s, "\r\n", 2); session_flush(s); if (thread->subject == NULL) goto post_compose_done; rtrim(thread->subject, "\r\n\t "); if (thread->subject[0] == '\0') { session_printf(s, "{{B}}Error:{{/B}} Subject " "cannot be blank (^C to cancel)\r\n"); session_flush(s); xfree(&thread->subject); continue; } thread->subject_size = strlen(thread->subject) + 1; break; } } for (;;) { session_printf(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) xfree(&post.body); post.body = tmp; session_output(s, "\r\n", 2); session_flush(s); if (post.body == NULL) goto post_compose_done; rtrim(post.body, "\r\n\t "); if (post.body[0] == '\0') { xfree(&post.body); goto post_compose_done; } post.body_size = strlen(post.body) + 1; break; } for (;;) { session_printf(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_printf(s, "Posting message... "); session_flush(s); if (board_post_create(board, thread, ftn_parent_post, &post) == 0) { session_logf(s, "Posted message %ld to %s", post.id, board->name); session_printf(s, "done\r\n"); } else session_printf(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_printf(s, "Failed saving message!\r\n"); session_flush(s); post_compose_done: if (parent_post == NULL) { if (thread->subject != NULL) xfree(&thread->subject); xfree(&thread); } if (post.body) xfree(&post.body); return post.id; } short board_post_read(struct session *s, struct board *board, char *prompt_prefix, unsigned long id, short idx) { static const struct session_menu_option opts[] = { { '<', "', ">Pp", "Read older post" }, { 'r', "Rr", "Reply to this post" }, { 's', "Ss", "Show this post" }, { 'd', "Dd", "Delete this post" }, { 'l', "Ll", "List posts" }, { 'q', "QqXx", "Return to threads" }, { '?', "?", "List these options" }, }; static const char prompt_help[] = "<:Newer >:Older R:Reply S:Show D:Delete L:List Q:Return ?:Help"; char time[32], prompt[7 + member_size(struct board, name) + 8]; struct board_thread thread; struct board_post post; struct board_ftn_post fpost; struct username_cache *sender; struct session_menu_option *dopts = NULL; size_t size, plain_post_size, j; short ret = POST_READ_RETURN_DONE; char c; char *data, *subject, *plain_post, *tplain_post; short cc, bcret, n; bool done = false, show_help = false; dopts = xmalloc(sizeof(opts)); if (dopts == NULL) return 0; memcpy(dopts, opts, sizeof(opts)); show_post: if (board->ftn_area[0]) { size = bile_read_alloc(board->bile, BOARD_FTN_POST_RTYPE, id, &data); if (size == 0) panic("failed fetching message %ld: %d", id, bile_error(board->bile)); ret = bile_unmarshall_object(board->bile, board_ftn_post_object_fields, nboard_ftn_post_object_fields, data, size, &fpost, sizeof(fpost), true); xfree(&data); if (ret == BILE_ERR_NO_MEMORY) goto done_reading; if (!(s->user && s->user->is_sysop)) { /* disable deleting */ for (n = 0; n < nitems(opts); n++) { if (dopts[n].ret == 'd') { dopts[n].key[0] = '\0'; break; } } } strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S", localtime(&fpost.time)); session_printf(s, "{{B}}From:{{/B}}{{#}} %s\r\n", fpost.from); session_printf(s, "{{B}}Origin:{{/B}}{{#}} %s\r\n", fpost.origin); session_printf(s, "{{B}}To:{{/B}}{{#}} %s@%s\r\n", fpost.to, 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", (fpost.reply[0] && strncasecmp(fpost.subject, "Re:", 3) != 0 ? "Re: " : ""), fpost.subject); session_printf(s, "\r\n"); session_flush(s); plain_post_size = 0; plain_post = xmalloc(fpost.body_size); if (plain_post == NULL) session_paginate(s, fpost.body, fpost.body_size, 6); else { /* strip out renegade-style pipe color codes ("abc|01def") */ for (j = 0; j < fpost.body_size; j++) { if (fpost.body[j] == '|' && fpost.body[j + 1] >= '0' && fpost.body[j + 1] <= '9' && fpost.body[j + 2] >= '0' && fpost.body[j + 2] <= '9') { j += 2; continue; } plain_post[plain_post_size++] = fpost.body[j]; } tplain_post = xrealloc(plain_post, plain_post_size); if (tplain_post != NULL) plain_post = tplain_post; session_paginate(s, plain_post, plain_post_size, 6); xfree(&plain_post); } } else { 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)); ret = bile_unmarshall_object(board->bile, board_post_object_fields, nboard_post_object_fields, data, size, &post, sizeof(post), true); xfree(&data); if (ret == BILE_ERR_NO_MEMORY) goto done_reading; 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)); ret = bile_unmarshall_object(board->bile, board_thread_object_fields, nboard_thread_object_fields, data, size, &thread, sizeof(thread), true); xfree(&data); if (ret == BILE_ERR_NO_MEMORY) goto done_reading; if (!(s->user && (s->user->is_sysop || s->user->id == post.sender_user_id))) { /* disable deleting */ for (n = 0; n < nitems(opts); n++) { if (dopts[n].ret == 'd') { dopts[n].key[0] = '\0'; break; } } } sender = user_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", sender ? sender->username : "(unknown)"); if (post.via[0]) session_printf(s, " (via %s)", post.via); session_printf(s, "\r\n"); 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_printf(s, "\r\n"); session_flush(s); session_paginate(s, post.body, post.body_size, 5); } snprintf(prompt, sizeof(prompt), "%s:%d", prompt_prefix, idx); if (board->ftn_area[0]) subject = fpost.subject; else subject = thread.subject; while (!done && !s->ending) { c = session_menu(s, subject, prompt, (char *)prompt_help, dopts, nitems(opts), show_help, NULL, NULL); show_help = false; switch (c) { case 'd': if (!(s->user && (s->user->is_sysop || s->user->id == post.sender_user_id))) { session_printf(s, "Invalid option\r\n"); session_flush(s); break; } session_printf(s, "Are you sure you want to permanently " "delete this post? [y/N] "); session_flush(s); cc = session_input_char(s); if (cc == 'y' || c == 'Y') { session_printf(s, "%c\r\n", cc); session_flush(s); if (board->ftn_area[0]) { board_delete_ftn_post(board, &fpost); session_logf(s, "Deleted %s post %ld", db->config.ftn_network, fpost.id); } else { board_delete_post(board, &post, &thread); session_logf(s, "Deleted post %ld (thread %ld)", post.id, thread.thread_id); } session_printf(s, "\r\n{{B}}Post deleted!{{/B}}\r\n"); session_flush(s); ret = POST_READ_RETURN_FIND; done = true; } else { session_printf(s, "\r\nPost not deleted.\r\n"); session_flush(s); } break; case 'r': if (board->ftn_area[0]) bcret = board_compose(s, board, NULL, NULL, &fpost, NULL, NULL); else bcret = board_compose(s, board, &thread, &post, NULL, NULL, NULL); if (bcret) { ret = POST_READ_RETURN_FIND; done = true; } break; case 's': goto show_post; break; case '<': ret = POST_READ_RETURN_NEWER; done = true; break; case '>': ret = POST_READ_RETURN_OLDER; done = true; break; case 'l': ret = POST_READ_RETURN_LIST; done = true; break; case 'q': ret = POST_READ_RETURN_DONE; done = true; break; case '?': show_help = true; break; } } done_reading: xfree(&dopts); if (board->ftn_area[0]) { if (fpost.body != NULL) xfree(&fpost.body); } else { if (post.body != NULL) xfree(&post.body); if (thread.subject != NULL) xfree(&thread.subject); if (thread.post_ids != NULL) xfree(&thread.post_ids); if (thread.parent_post_ids != NULL) xfree(&thread.parent_post_ids); } return ret; } size_t board_find_post_ids(struct session *s, struct board *board, size_t *npost_ids, unsigned long **post_ids, size_t offset, size_t limit) { struct board_id_time_map *all_post_id_map = NULL; size_t n, size, nall_post_ids; *post_ids = NULL; *npost_ids = 0; size = bile_read_alloc(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, &all_post_id_map); if (all_post_id_map == NULL) { session_printf(s, "%sPlease wait, re-indexing board posts...%s", ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); session_flush(s); nall_post_ids = board_index_sorted_post_ids(board, &all_post_id_map); session_output(s, "\r\n", 2); session_flush(s); if (nall_post_ids == 0) return 0; } else nall_post_ids = size / sizeof(struct board_id_time_map); *post_ids = xcalloc(sizeof(long), MIN(limit, nall_post_ids)); if (*post_ids == NULL) { if (all_post_id_map != NULL) xfree(&all_post_id_map); return 0; } for (n = 0; n < nall_post_ids; n++) { if (n < offset) continue; (*post_ids)[*npost_ids] = all_post_id_map[n].id; (*npost_ids)++; if (*npost_ids >= limit) break; } if (all_post_id_map != NULL) xfree(&all_post_id_map); return nall_post_ids; } short board_post_create(struct board *board, struct board_thread *thread, struct board_ftn_post *ftn_parent_post, struct board_post *post) { struct board_ftn_post ftn_post = { 0 }; struct fidopkt_message fidomsg = { 0 }; struct username_cache *user; struct fidopkt_address our_address, hub_address; short ret; char *data; size_t size, insert; ssize_t n, j; unsigned long *post_ids, *parent_post_ids; if (board->ftn_area[0]) { if (!post->id) post->id = bile_next_id(board->bile, BOARD_FTN_POST_RTYPE); if (!post->time) post->time = Time; if (!fidopkt_parse_address(db->config.ftn_node_addr, &our_address)) { logger_printf("[board] Invalid FTN local node address, can't " "create board post"); post->id = 0; goto done; } if (!fidopkt_parse_address(db->config.ftn_hub_addr, &hub_address)) { logger_printf("[board] Invalid FTN hub address, can't " "create board post"); post->id = 0; goto done; } ftn_post.id = post->id; ftn_post.time = post->time; if (ftn_parent_post) { snprintf(ftn_post.subject, sizeof(ftn_post.subject), "%s%s", strncmp("Re:", ftn_parent_post->subject, 3) == 1 ? "" : "Re: ", ftn_parent_post->subject); strlcpy(ftn_post.reply, ftn_parent_post->msgid_orig, sizeof(ftn_post.reply)); strlcpy(ftn_post.to, ftn_parent_post->from, sizeof(ftn_post.to)); } else { strlcpy(ftn_post.subject, thread->subject, sizeof(ftn_post.subject)); strlcpy(ftn_post.to, "All", sizeof(ftn_post.to)); } user = user_username(post->sender_user_id); if (user == NULL) { logger_printf("[board] Can't find username of user posting " "new message"); post->id = 0; goto done; } strlcpy(ftn_post.from, user->username, sizeof(ftn_post.from)); ftn_post.body = post->body; ftn_post.body_size = post->body_size; /* make each board's posts have ids unique to our zone/net/node */ ftn_post.msgid.id = 0x10000000 | ((unsigned long)our_address.node << 24) | ((unsigned long)(board->id) << 16) | post->id; ftn_post.msgid.zone = our_address.zone; ftn_post.msgid.net = our_address.net; ftn_post.msgid.node = our_address.node; ftn_post.msgid.point = our_address.point; snprintf(ftn_post.origin, sizeof(ftn_post.origin), "%s | %s (%s)", db->config.name, db->config.hostname, db->config.ftn_node_addr); fidomsg.time = ftn_post.time; memcpy(&fidomsg.header.orig, &our_address, sizeof(fidomsg.header.orig)); memcpy(&fidomsg.header.dest, &hub_address, sizeof(fidomsg.header.dest)); strlcpy(fidomsg.area, board->ftn_area, sizeof(fidomsg.area)); strlcpy(fidomsg.to, ftn_post.to, sizeof(fidomsg.to)); strlcpy(fidomsg.from, ftn_post.from, sizeof(fidomsg.from)); strlcpy(fidomsg.subject, ftn_post.subject, sizeof(fidomsg.subject)); fidomsg.body = ftn_post.body; fidomsg.body_len = ftn_post.body_size - 1; strlcpy(fidomsg.reply, ftn_post.reply, sizeof(fidomsg.reply)); memcpy(&fidomsg.msgid, &ftn_post.msgid, sizeof(fidomsg.msgid)); strlcpy(fidomsg.origin, ftn_post.origin, sizeof(fidomsg.origin)); if (!binkp_scan_message(&fidomsg)) { logger_printf("[board] Failed scanning new FTN message being " "posted"); post->id = 0; goto done; } if (board_toss_ftn_message(board, &fidomsg, true) != 1) { logger_printf("[board] Failed tossing new FTN message being " "posted"); post->id = 0; goto done; } } else { if (!post->id) post->id = bile_next_id(board->bile, BOARD_POST_RTYPE); if (!post->time) post->time = Time; if (post->parent_post_id == 0) { thread->thread_id = bile_next_id(board->bile, BOARD_THREAD_RTYPE); post->thread_id = thread->thread_id; } ret = bile_marshall_object(board->bile, board_post_object_fields, nboard_post_object_fields, post, &data, &size); if (ret != 0 || size == 0) { logger_printf("[board] 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; xfree(&data); goto done; } xfree(&data); if (post->time > thread->last_post_at) thread->last_post_at = post->time; thread->nposts++; post_ids = xreallocarray(thread->post_ids, thread->nposts, sizeof(long)); if (post_ids == NULL) return 0; thread->post_ids = post_ids; parent_post_ids = xreallocarray(thread->parent_post_ids, thread->nposts, sizeof(long)); if (parent_post_ids == NULL) return 0; thread->parent_post_ids = parent_post_ids; /* * 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->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 + 1; 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) { logger_printf("[board] Failed to marshall new 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; xfree(&data); goto done; } xfree(&data); /* it would be nice not to have to rebuild this every time... */ board_delete_cached_index(board); bile_flush(board->bile, true); if (post->time > board->last_post_at) board->last_post_at = post->time; } done: return (post->id == 0 ? -1 : 0); } void board_delete_post(struct board *board, struct board_post *post, struct board_thread *thread) { size_t size, n, nn; char *data, *body; char del[50]; short ret; bool childs = false; unsigned long *new_post_ids; unsigned long *new_parent_post_ids; if (thread->nposts == 1) { bile_delete(board->bile, BOARD_THREAD_RTYPE, thread->thread_id, 0); bile_delete(board->bile, BOARD_POST_RTYPE, post->id, BILE_DELETE_FLAG_PURGE); board_delete_cached_index(board); return; } for (n = 0; n < thread->nposts; n++) { if (thread->parent_post_ids[n] == post->id) { childs = true; break; } } if (!childs) { /* just zap this off the end of the tree */ new_post_ids = xcalloc(thread->nposts - 1, sizeof(unsigned long)); if (new_post_ids == NULL) return; new_parent_post_ids = xcalloc(thread->nposts - 1, sizeof(unsigned long)); if (new_parent_post_ids == NULL) return; for (n = 0, nn = 0; n < thread->nposts; n++) { if (thread->post_ids[n] == post->id) continue; new_post_ids[nn] = thread->post_ids[n]; nn++; } for (n = 0, nn = 0; n < thread->nposts; n++) { if (thread->post_ids[n] == post->id) continue; new_parent_post_ids[nn] = thread->parent_post_ids[n]; nn++; } thread->nposts--; xfree(&thread->post_ids); thread->post_ids = new_post_ids; xfree(&thread->parent_post_ids); thread->parent_post_ids = new_parent_post_ids; ret = bile_marshall_object(board->bile, board_thread_object_fields, nboard_thread_object_fields, thread, &data, &size); if (ret != 0 || size == 0) { logger_printf("[board] Failed to marshall thread object " "during post deletion"); return; } if (bile_write(board->bile, BOARD_THREAD_RTYPE, thread->thread_id, data, size) != size) { warn("bile_write of updated thread after post delete failed! " "%d", bile_error(board->bile)); xfree(&data); return; } xfree(&data); bile_delete(board->bile, BOARD_POST_RTYPE, post->id, BILE_DELETE_FLAG_ZERO | BILE_DELETE_FLAG_PURGE); board_delete_cached_index(board); bile_flush(board->bile, true); return; } /* all we can do is change the post to say deleted */ if (post->body != NULL) xfree(&post->body); snprintf(del, sizeof(del), "[ Post deleted ]"); body = xstrdup(del); if (body == NULL) return; post->body = body; post->body_size = strlen(post->body) + 1; ret = bile_marshall_object(board->bile, board_post_object_fields, nboard_post_object_fields, post, &data, &size); if (ret != 0 || size == 0) { logger_printf("[board] Failed to marshall updated post object"); return; } if (bile_write(board->bile, BOARD_POST_RTYPE, post->id, data, size) != size) { warn("bile_write of updated post failed! %d", bile_error(board->bile)); xfree(&data); return; } xfree(&data); } void board_delete_ftn_post(struct board *board, struct board_ftn_post *post) { size_t size, npost_ids, n; struct board_id_time_map *id_map; bile_delete(board->bile, BOARD_FTN_POST_RTYPE, post->id, BILE_DELETE_FLAG_PURGE); size = bile_read_alloc(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, &id_map); if (size == 0 || id_map == NULL) return; npost_ids = size / sizeof(struct board_id_time_map); for (n = 0; n < npost_ids; n++) { if (id_map[n].id == post->id) { for (; n < npost_ids - 1; n++) id_map[n] = id_map[n + 1]; npost_ids--; break; } } if (npost_ids == 0) board_delete_cached_index(board); else bile_write(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, id_map, sizeof(struct board_id_time_map) * npost_ids); /* TODO: delete from msgid cache too */ xfree(&id_map); } size_t board_index_sorted_post_ids(struct board *board, struct board_id_time_map **sorted_id_map) { struct thread_time_map { unsigned long id; time_t time; size_t nposts; }; struct board_ftn_post fpost; struct board_post post; struct board_thread thread; size_t ret, size, i, j, n, npost_ids, nthread_ids; unsigned long *post_ids = NULL, *thread_ids = NULL; struct board_id_time_map *id_map, tmp_id_map; struct thread_time_map *thread_map = NULL, tmp_thread_map; char *data; if (board->ftn_area[0]) { npost_ids = bile_ids_by_type(board->bile, BOARD_FTN_POST_RTYPE, &post_ids); if (npost_ids == 0) goto write_out; id_map = xcalloc(sizeof(struct board_id_time_map), npost_ids); if (id_map == NULL) goto write_out; for (n = 0; n < npost_ids; n++) { /* only read as far as the time */ ret = bile_read(board->bile, BOARD_FTN_POST_RTYPE, post_ids[n], &fpost, offsetof(struct board_ftn_post, time) + member_size(struct board_ftn_post, time)); if (ret == 0) goto write_out; id_map[n].id = fpost.id; id_map[n].time = fpost.time; } xfree(&post_ids); /* sort by date descending */ for (i = 1; i < npost_ids; i++) { for (j = i; j > 0; j--) { if (id_map[j].time < id_map[j - 1].time) break; tmp_id_map = id_map[j]; id_map[j] = id_map[j - 1]; id_map[j - 1] = tmp_id_map; } } } else { npost_ids = 0; nthread_ids = bile_ids_by_type(board->bile, BOARD_THREAD_RTYPE, &thread_ids); if (nthread_ids == 0) goto write_out; thread_map = xcalloc(sizeof(struct thread_time_map), nthread_ids); if (thread_map == NULL) goto write_out; for (n = 0; n < nthread_ids; n++) { size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, thread_ids[n], &data); ret = bile_unmarshall_object(board->bile, board_thread_object_fields, nboard_thread_object_fields, data, size, &thread, sizeof(thread), false); xfree(&data); if (ret != 0) goto write_out; thread_map[n].id = thread.thread_id; thread_map[n].time = thread.last_post_at; thread_map[n].nposts = thread.nposts; npost_ids += thread.nposts; } xfree(&thread_ids); /* sort by last post date descending */ for (i = 1; i < nthread_ids; i++) { for (j = i; j > 0; j--) { if (thread_map[j].time < thread_map[j - 1].time) break; tmp_thread_map = thread_map[j]; thread_map[j] = thread_map[j - 1]; thread_map[j - 1] = tmp_thread_map; } } id_map = xcalloc(sizeof(struct board_id_time_map), npost_ids); if (id_map == NULL) goto write_out; npost_ids = 0; for (j = 0; j < nthread_ids; j++) { size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, thread_map[j].id, &data); if (data == NULL) goto write_out; ret = bile_unmarshall_object(board->bile, board_thread_object_fields, nboard_thread_object_fields, data, size, &thread, sizeof(thread), true); xfree(&data); if (ret != 0) goto write_out; /* these are already sorted, and we want to keep thread sort */ for (i = 0; i < thread.nposts; i++) { /* only read as far as the time */ size = bile_read(board->bile, BOARD_POST_RTYPE, thread.post_ids[i], &post, offsetof(struct board_post, time) + member_size(struct board_post, time)); if (size == 0) { logger_printf("[board] Board %s thread %ld post %ld " "is missing", board->name, thread.thread_id, thread.post_ids[i]); continue; } id_map[npost_ids].id = post.id; id_map[npost_ids].time = post.time; npost_ids++; } if (thread.subject != NULL) xfree(&thread.subject); if (thread.post_ids != NULL) xfree(&thread.post_ids); if (thread.parent_post_ids != NULL) xfree(&thread.parent_post_ids); } xfree(&thread_map); } write_out: if (thread_map) xfree(&thread_map); if (thread_ids) xfree(&thread_ids); if (post_ids) xfree(&post_ids); if (npost_ids == 0 || id_map == NULL) { board_delete_cached_index(board); if (sorted_id_map != NULL) *sorted_id_map = NULL; return 0; } bile_write(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, id_map, sizeof(struct board_id_time_map) * npost_ids); if (sorted_id_map == NULL) xfree(&id_map); else *sorted_id_map = id_map; return npost_ids; } short board_toss_ftn_message(struct board *board, struct fidopkt_message *fidomsg, bool local) { struct board_ftn_msgid_cache { unsigned long id; struct fidopkt_msgid msgid; } *msgid_cache = NULL; struct bile_object *o; struct board_ftn_post post; struct fidopkt_msgid msgid; unsigned long *post_ids; char *pdata; size_t asize, cache_size, psize, npost_ids; short n, ret, bret; bool dirty_cache = false; msgid = fidomsg->msgid; o = bile_find(board->bile, BOARD_FTN_MSGID_CACHE_RTYPE, 1); if (o) { /* allocate its size plus one entry that we may add */ asize = o->size + sizeof(struct board_ftn_msgid_cache); msgid_cache = xmalloc(asize); if (msgid_cache == NULL) { logger_printf("[board] toss: malloc(%ld) failed", asize); return -1; } bile_read(board->bile, o->type, o->id, msgid_cache, o->size); npost_ids = o->size / sizeof(struct board_ftn_msgid_cache); xfree(&o); } else { npost_ids = bile_ids_by_type(board->bile, BOARD_FTN_POST_RTYPE, &post_ids); msgid_cache = xcalloc(sizeof(struct board_ftn_msgid_cache), npost_ids + 1); if (msgid_cache == NULL) { logger_printf("[board] toss: calloc(%ld, %ld) failed", sizeof(struct board_ftn_msgid_cache), npost_ids + 1); return -1; } for (n = 0; n < npost_ids; n++) { /* only read as far as we have to */ bile_read(board->bile, BOARD_FTN_POST_RTYPE, post_ids[n], &post, offsetof(struct board_ftn_post, msgid) + member_size(struct board_ftn_post, msgid)); msgid_cache[n].id = post.id; memcpy(&msgid_cache[n].msgid, &post.msgid, sizeof(post.msgid)); } dirty_cache = true; } uthread_yield(); if (!local) { for (n = 0; n < npost_ids; n++) { if (memcmp(&msgid_cache[n].msgid, &msgid, sizeof(msgid)) == 0) { logger_printf("[board] Already have %s EchoMail %s in %s " "(%ld), skipping", db->config.ftn_network, fidomsg->msgid_orig, board->name, msgid_cache[n].id); ret = 0; goto done; } } } memset(&post, 0, sizeof(post)); post.time = fidomsg->time; post.body_size = fidomsg->body_len + 1; post.body = fidomsg->body; strlcpy(post.reply, fidomsg->reply, sizeof(post.reply)); strlcpy(post.from, fidomsg->from, sizeof(post.from)); strlcpy(post.subject, fidomsg->subject, sizeof(post.subject)); strlcpy(post.to, fidomsg->to, sizeof(post.to)); strlcpy(post.origin, fidomsg->origin, sizeof(post.origin)); strlcpy(post.msgid_orig, fidomsg->msgid_orig, sizeof(post.msgid_orig)); post.msgid = msgid; if (!local || !post.id) post.id = bile_next_id(board->bile, BOARD_FTN_POST_RTYPE); if (!post.id) { logger_printf("[board] Failed get next id for %s", board->name); ret = -1; goto done; } bret = bile_marshall_object(board->bile, board_ftn_post_object_fields, nboard_ftn_post_object_fields, &post, &pdata, &psize); if (bret != 0 || psize == 0) { logger_printf("[board] Failed to marshall new post %s %s: %d", fidomsg->area, fidomsg->msgid_orig, bile_error(board->bile)); ret = -1; goto done; } if (bile_write(board->bile, BOARD_FTN_POST_RTYPE, post.id, pdata, psize) != psize) { logger_printf("[fidopkt] Failed to save new post %s %s: %d", fidomsg->area, fidomsg->msgid_orig, bile_error(board->bile)); ret = -1; xfree(&pdata); goto done; } xfree(&pdata); ret = 1; /* we already allocated one empty space at the end of the cache */ msgid_cache[npost_ids].id = post.id; memcpy(&msgid_cache[npost_ids].msgid, &post.msgid, sizeof(post.msgid)); npost_ids++; dirty_cache = true; board_delete_cached_index(board); if (post.time > board->last_post_at) board->last_post_at = post.time; if (!local) logger_printf("[board] Tossed %s EchoMail %s as %ld", fidomsg->area, fidomsg->msgid_orig, post.id); uthread_yield(); done: if (dirty_cache) { cache_size = npost_ids * sizeof(struct board_ftn_msgid_cache); if (bile_write(board->bile, BOARD_FTN_MSGID_CACHE_RTYPE, 1, msgid_cache, cache_size) != cache_size) { logger_printf("[board] Failed to save msgid cache for %s: %d", fidomsg->area, bile_error(board->bile)); ret = -1; } } if (msgid_cache != NULL) xfree(&msgid_cache); return ret; } void board_delete_cached_index(struct board *board) { bile_delete(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, BILE_DELETE_FLAG_PURGE); } void board_prune_old_posts(struct board *board) { size_t nposts, n; struct board_id_time_map *sorted_id_map; struct board_ftn_post fpost; struct board_post post; struct board_thread thread; time_t oldest; size_t size, deleted; char *data; short ret; if (board->retention_days == 0) return; deleted = 0; nposts = board_index_sorted_post_ids(board, &sorted_id_map); if (nposts == 0) goto done; oldest = Time - ((unsigned long)(board->retention_days) * (60UL * 60UL * 24UL)); for (n = 0; n < nposts; n++) { if (sorted_id_map[n].time >= oldest) continue; if (board->ftn_area[0]) { size = bile_read_alloc(board->bile, BOARD_FTN_POST_RTYPE, sorted_id_map[n].id, &data); if (!size) continue; ret = bile_unmarshall_object(board->bile, board_ftn_post_object_fields, nboard_ftn_post_object_fields, data, size, &fpost, sizeof(fpost), false); xfree(&data); if (ret == BILE_ERR_NO_MEMORY) goto done; board_delete_ftn_post(board, &fpost); } else { size = bile_read_alloc(board->bile, BOARD_POST_RTYPE, sorted_id_map[n].id, &data); if (!size) continue; ret = bile_unmarshall_object(board->bile, board_post_object_fields, nboard_post_object_fields, data, size, &post, sizeof(post), false); xfree(&data); if (ret == BILE_ERR_NO_MEMORY) goto done; size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, post.thread_id, &data); if (!size) continue; ret = bile_unmarshall_object(board->bile, board_thread_object_fields, nboard_thread_object_fields, data, size, &thread, sizeof(thread), false); xfree(&data); if (ret == BILE_ERR_NO_MEMORY) goto done; board_delete_post(board, &post, &thread); } deleted++; uthread_yield(); } done: if (sorted_id_map != NULL) xfree(&sorted_id_map); if (deleted) { board_index_sorted_post_ids(board, NULL); logger_printf("[board] Pruned %lu post%s in %s older than %d day%s", deleted, deleted == 1 ? "" : "s", board->name, board->retention_days, board->retention_days == 1 ? "" : "s"); } }