| 1 |
/* |
| 2 |
* Copyright (c) 2022 joshua stein <jcs@jcs.org> |
| 3 |
* |
| 4 |
* Permission to use, copy, modify, and distribute this software for any |
| 5 |
* purpose with or without fee is hereby granted, provided that the above |
| 6 |
* copyright notice and this permission notice appear in all copies. |
| 7 |
* |
| 8 |
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
| 9 |
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
| 10 |
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
| 11 |
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
| 12 |
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
| 13 |
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
| 14 |
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| 15 |
*/ |
| 16 |
|
| 17 |
#include <stdarg.h> |
| 18 |
#include <stdio.h> |
| 19 |
#include <stdlib.h> |
| 20 |
#include <string.h> |
| 21 |
|
| 22 |
#include "subtext.h" |
| 23 |
#include "ansi.h" |
| 24 |
#include "binkp.h" |
| 25 |
#include "board.h" |
| 26 |
#include "logger.h" |
| 27 |
#include "user.h" |
| 28 |
|
| 29 |
#define POSTS_PER_PAGE 20 |
| 30 |
|
| 31 |
#define POST_READ_RETURN_DONE -1 |
| 32 |
#define POST_READ_RETURN_LIST -2 |
| 33 |
#define POST_READ_RETURN_FIND -3 |
| 34 |
#define POST_READ_RETURN_NEWER -4 |
| 35 |
#define POST_READ_RETURN_OLDER -5 |
| 36 |
|
| 37 |
const struct struct_field board_fields[] = { |
| 38 |
{ "Board ID", CONFIG_TYPE_LONG, |
| 39 |
offsetof(struct board, id), |
| 40 |
1, LONG_MAX }, |
| 41 |
{ "Name", CONFIG_TYPE_STRING, |
| 42 |
offsetof(struct board, name), |
| 43 |
1, member_size(struct board, name) }, |
| 44 |
{ "Description", CONFIG_TYPE_STRING, |
| 45 |
offsetof(struct board, description), |
| 46 |
0, member_size(struct board, description) }, |
| 47 |
{ "Restricted Posting", CONFIG_TYPE_BOOLEAN, |
| 48 |
offsetof(struct board, restricted_posting), |
| 49 |
0, 0 }, |
| 50 |
{ "Restricted Viewing", CONFIG_TYPE_BOOLEAN, |
| 51 |
offsetof(struct board, restricted_viewing), |
| 52 |
0, 0 }, |
| 53 |
{ "Days of Retention", CONFIG_TYPE_SHORT, |
| 54 |
offsetof(struct board, retention_days), |
| 55 |
0, USHRT_MAX }, |
| 56 |
{ "FTN Area Name", CONFIG_TYPE_STRING, |
| 57 |
offsetof(struct board, ftn_area), |
| 58 |
0, member_size(struct board, ftn_area) }, |
| 59 |
}; |
| 60 |
const size_t nboard_fields = nitems(board_fields); |
| 61 |
|
| 62 |
const struct bile_object_field board_object_fields[] = { |
| 63 |
{ offsetof(struct board, id), |
| 64 |
member_size(struct board, id), -1 }, |
| 65 |
{ offsetof(struct board, name), |
| 66 |
member_size(struct board, name), -1 }, |
| 67 |
{ offsetof(struct board, description), |
| 68 |
member_size(struct board, description), -1 }, |
| 69 |
{ offsetof(struct board, restricted_posting), |
| 70 |
member_size(struct board, restricted_posting), -1 }, |
| 71 |
{ offsetof(struct board, restricted_viewing), |
| 72 |
member_size(struct board, restricted_viewing), -1 }, |
| 73 |
{ offsetof(struct board, retention_days), |
| 74 |
member_size(struct board, retention_days), -1 }, |
| 75 |
{ offsetof(struct board, last_post_at), |
| 76 |
member_size(struct board, last_post_at), -1 }, |
| 77 |
{ offsetof(struct board, post_count), |
| 78 |
member_size(struct board, post_count), -1 }, |
| 79 |
{ offsetof(struct board, ftn_area), |
| 80 |
member_size(struct board, ftn_area), -1 }, |
| 81 |
}; |
| 82 |
const size_t nboard_object_fields = nitems(board_object_fields); |
| 83 |
|
| 84 |
const struct bile_object_field board_post_object_fields[] = { |
| 85 |
{ offsetof(struct board_post, id), |
| 86 |
member_size(struct board_post, id), -1 }, |
| 87 |
{ offsetof(struct board_post, thread_id), |
| 88 |
member_size(struct board_post, thread_id), -1 }, |
| 89 |
{ offsetof(struct board_post, time), |
| 90 |
member_size(struct board_post, time), -1 }, |
| 91 |
{ offsetof(struct board_post, sender_user_id), |
| 92 |
member_size(struct board_post, sender_user_id), -1 }, |
| 93 |
{ offsetof(struct board_post, body_size), |
| 94 |
member_size(struct board_post, body_size), -1 }, |
| 95 |
{ offsetof(struct board_post, body), |
| 96 |
-1, offsetof(struct board_post, body_size) }, |
| 97 |
{ offsetof(struct board_post, parent_post_id), |
| 98 |
member_size(struct board_post, parent_post_id), -1 }, |
| 99 |
{ offsetof(struct board_post, via), |
| 100 |
member_size(struct board_post, via), -1 }, |
| 101 |
}; |
| 102 |
const size_t nboard_post_object_fields = nitems(board_post_object_fields); |
| 103 |
|
| 104 |
const struct bile_object_field board_ftn_post_object_fields[] = { |
| 105 |
{ offsetof(struct board_ftn_post, id), |
| 106 |
member_size(struct board_ftn_post, id), -1 }, |
| 107 |
{ offsetof(struct board_ftn_post, msgid), |
| 108 |
member_size(struct board_ftn_post, msgid), -1 }, |
| 109 |
{ offsetof(struct board_ftn_post, time), |
| 110 |
member_size(struct board_ftn_post, time), -1 }, |
| 111 |
{ offsetof(struct board_ftn_post, from), |
| 112 |
member_size(struct board_ftn_post, from), -1 }, |
| 113 |
{ offsetof(struct board_ftn_post, subject), |
| 114 |
member_size(struct board_ftn_post, subject), -1 }, |
| 115 |
{ offsetof(struct board_ftn_post, msgid_orig), |
| 116 |
member_size(struct board_ftn_post, msgid_orig), -1 }, |
| 117 |
{ offsetof(struct board_ftn_post, origin), |
| 118 |
member_size(struct board_ftn_post, origin), -1 }, |
| 119 |
{ offsetof(struct board_ftn_post, reply), |
| 120 |
member_size(struct board_ftn_post, reply), -1 }, |
| 121 |
{ offsetof(struct board_ftn_post, to), |
| 122 |
member_size(struct board_ftn_post, to), -1 }, |
| 123 |
{ offsetof(struct board_ftn_post, body_size), |
| 124 |
member_size(struct board_ftn_post, body_size), -1 }, |
| 125 |
{ offsetof(struct board_ftn_post, body), |
| 126 |
-1, offsetof(struct board_ftn_post, body_size) }, |
| 127 |
}; |
| 128 |
const size_t nboard_ftn_post_object_fields = |
| 129 |
nitems(board_ftn_post_object_fields); |
| 130 |
|
| 131 |
const struct bile_object_field board_thread_object_fields[] = { |
| 132 |
{ offsetof(struct board_thread, thread_id), |
| 133 |
member_size(struct board_thread, thread_id), -1 }, |
| 134 |
{ offsetof(struct board_thread, last_post_at), |
| 135 |
member_size(struct board_thread, last_post_at), -1 }, |
| 136 |
{ offsetof(struct board_thread, subject_size), |
| 137 |
member_size(struct board_thread, subject_size), -1 }, |
| 138 |
{ offsetof(struct board_thread, subject), |
| 139 |
-1, offsetof(struct board_thread, subject_size) }, |
| 140 |
{ offsetof(struct board_thread, nposts), |
| 141 |
member_size(struct board_thread, nposts), -1 }, |
| 142 |
{ offsetof(struct board_thread, post_ids), |
| 143 |
-(sizeof(long)), offsetof(struct board_thread, nposts) }, |
| 144 |
{ offsetof(struct board_thread, parent_post_ids), |
| 145 |
-(sizeof(long)), offsetof(struct board_thread, nposts) }, |
| 146 |
}; |
| 147 |
const size_t nboard_thread_object_fields = nitems(board_thread_object_fields); |
| 148 |
|
| 149 |
unsigned long board_compose(struct session *s, struct board *board, |
| 150 |
struct board_thread *thread, struct board_post *parent_post, |
| 151 |
struct board_ftn_post *ftn_parent_post, char *initial_subject, |
| 152 |
char *initial_body); |
| 153 |
short board_post_create(struct board *board, struct board_thread *thread, |
| 154 |
struct board_ftn_post *ftn_parent_post, struct board_post *post); |
| 155 |
void board_list_posts(struct session *s, struct board *board, |
| 156 |
size_t npost_Ids, unsigned long *post_ids, size_t page, size_t pages); |
| 157 |
short board_post_read(struct session *s, struct board *board, |
| 158 |
char *prompt_prefix, unsigned long id, short idx); |
| 159 |
size_t board_find_post_ids(struct session *s, struct board *board, |
| 160 |
size_t *npost_ids, unsigned long **post_ids, size_t offset, |
| 161 |
size_t limit); |
| 162 |
void board_delete_cached_index(struct board *board); |
| 163 |
|
| 164 |
void |
| 165 |
board_list_boards(struct session *s) |
| 166 |
{ |
| 167 |
static struct session_menu_option opts[] = { |
| 168 |
{ '#', "#", "Enter board number to read" }, |
| 169 |
{ 'l', "Ll", "List boards" }, |
| 170 |
{ 'q', "QqXx", "Return to main menu" }, |
| 171 |
{ '?', "?", "List menu options" }, |
| 172 |
}; |
| 173 |
static const char prompt_help[] = |
| 174 |
"#:View Board L:List Boards Q:Return ?:Help"; |
| 175 |
struct board *lboards = NULL, tboard; |
| 176 |
size_t nlboards; |
| 177 |
char title[] = "List Boards"; |
| 178 |
char c; |
| 179 |
short an, n, i, j; |
| 180 |
bool done, show_list, show_help; |
| 181 |
|
| 182 |
lboards = xcalloc(sizeof(struct board), db->nboards); |
| 183 |
if (lboards == NULL) |
| 184 |
return; |
| 185 |
|
| 186 |
nlboards = 0; |
| 187 |
for (n = 0; n < db->nboards; n++) { |
| 188 |
if (db->boards[n].ftn_area[0] == '\0') { |
| 189 |
memcpy(&lboards[nlboards], &db->boards[n], |
| 190 |
sizeof(struct board)); |
| 191 |
nlboards++; |
| 192 |
} |
| 193 |
} |
| 194 |
|
| 195 |
/* sort by board name */ |
| 196 |
for (i = 1; i < nlboards; i++) { |
| 197 |
for (j = i; j > 0; j--) { |
| 198 |
tboard = lboards[j]; |
| 199 |
if (strcmp(lboards[j].name, lboards[j - 1].name) > 0) |
| 200 |
break; |
| 201 |
tboard = lboards[j]; |
| 202 |
lboards[j] = lboards[j - 1]; |
| 203 |
lboards[j - 1] = tboard; |
| 204 |
} |
| 205 |
} |
| 206 |
|
| 207 |
show_list = true; |
| 208 |
show_help = false; |
| 209 |
done = false; |
| 210 |
|
| 211 |
snprintf(title, sizeof(title), "Message Boards"); |
| 212 |
|
| 213 |
while (!done && !s->ending) { |
| 214 |
if (show_list) { |
| 215 |
session_printf(s, "{{B}}%s{{/B}}\r\n", title); |
| 216 |
session_printf(s, "%s # Board Description%s\r\n", |
| 217 |
ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); |
| 218 |
session_flush(s); |
| 219 |
|
| 220 |
for (n = 0; n < nlboards; n++) { |
| 221 |
session_printf(s, "%2d %- 13.13s %s\r\n", |
| 222 |
n + 1, |
| 223 |
lboards[n].name, |
| 224 |
lboards[n].description); |
| 225 |
} |
| 226 |
session_flush(s); |
| 227 |
|
| 228 |
show_list = false; |
| 229 |
} |
| 230 |
|
| 231 |
c = session_menu(s, title, "Boards", |
| 232 |
(char *)prompt_help, opts, nitems(opts), show_help, "Board #", |
| 233 |
&an); |
| 234 |
show_help = false; |
| 235 |
|
| 236 |
switch (c) { |
| 237 |
case 'l': |
| 238 |
show_list = true; |
| 239 |
break; |
| 240 |
case '#': |
| 241 |
if (an < 1 || an > nlboards) { |
| 242 |
session_printf(s, "Invalid board\r\n"); |
| 243 |
session_flush(s); |
| 244 |
break; |
| 245 |
} |
| 246 |
board_show(s, lboards[an - 1].id, "Boards"); |
| 247 |
break; |
| 248 |
case '?': |
| 249 |
show_help = true; |
| 250 |
break; |
| 251 |
default: |
| 252 |
done = true; |
| 253 |
break; |
| 254 |
} |
| 255 |
} |
| 256 |
|
| 257 |
if (lboards != NULL) |
| 258 |
xfree(&lboards); |
| 259 |
} |
| 260 |
|
| 261 |
void |
| 262 |
board_list_ftn_areas(struct session *s) |
| 263 |
{ |
| 264 |
static struct session_menu_option opts[] = { |
| 265 |
{ '#', "#", "Enter area number to read" }, |
| 266 |
{ 'l', "Ll", "..." }, |
| 267 |
{ 'q', "QqXx", "Return to main menu" }, |
| 268 |
{ '?', "?", "List menu options" }, |
| 269 |
}; |
| 270 |
static const char prompt_help[] = |
| 271 |
"#:View Area L:List Areas Q:Return ?:Help"; |
| 272 |
struct board *fboards = NULL, tboard; |
| 273 |
struct fidopkt_address our_address; |
| 274 |
size_t nfboards; |
| 275 |
char title[50], latest[10]; |
| 276 |
char c; |
| 277 |
short an, n, i, j; |
| 278 |
bool done, show_list, show_help; |
| 279 |
|
| 280 |
if (!fidopkt_parse_address(db->config.ftn_node_addr, &our_address)) { |
| 281 |
session_printf(s, "{{B}}Error:{{/B}} FTN Areas are not supported " |
| 282 |
"on this system.\r\n"); |
| 283 |
session_flush(s); |
| 284 |
return; |
| 285 |
} |
| 286 |
|
| 287 |
snprintf(opts[1].title, sizeof(opts[1].title), "List %s Areas", |
| 288 |
db->config.ftn_network); |
| 289 |
|
| 290 |
fboards = xcalloc(sizeof(struct board), db->nboards); |
| 291 |
if (fboards == NULL) |
| 292 |
return; |
| 293 |
|
| 294 |
nfboards = 0; |
| 295 |
for (n = 0; n < db->nboards; n++) { |
| 296 |
if (db->boards[n].ftn_area[0]) { |
| 297 |
memcpy(&fboards[nfboards], &db->boards[n], |
| 298 |
sizeof(struct board)); |
| 299 |
nfboards++; |
| 300 |
} |
| 301 |
} |
| 302 |
|
| 303 |
/* sort by area name */ |
| 304 |
for (i = 1; i < nfboards; i++) { |
| 305 |
for (j = i; j > 0; j--) { |
| 306 |
if (strcmp(fboards[j].ftn_area, fboards[j - 1].ftn_area) > 0) |
| 307 |
break; |
| 308 |
tboard = fboards[j]; |
| 309 |
fboards[j] = fboards[j - 1]; |
| 310 |
fboards[j - 1] = tboard; |
| 311 |
} |
| 312 |
} |
| 313 |
|
| 314 |
show_list = true; |
| 315 |
show_help = false; |
| 316 |
done = false; |
| 317 |
|
| 318 |
snprintf(title, sizeof(title), "%s Areas (Local Node %s)", |
| 319 |
db->config.ftn_network, db->config.ftn_node_addr); |
| 320 |
|
| 321 |
while (!done && !s->ending) { |
| 322 |
if (show_list) { |
| 323 |
session_printf(s, "{{B}}%s{{/B}}\r\n", title); |
| 324 |
session_printf(s, "%s # Latest Area Description%s\r\n", |
| 325 |
ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); |
| 326 |
session_flush(s); |
| 327 |
|
| 328 |
for (n = 0; n < nfboards; n++) { |
| 329 |
if (fboards[n].last_post_at) |
| 330 |
strftime(latest, sizeof(latest), "%b %d", |
| 331 |
localtime(&fboards[n].last_post_at)); |
| 332 |
else |
| 333 |
latest[0] = '\0'; |
| 334 |
|
| 335 |
session_printf(s, "%2d % 6s %- 13.13s %s\r\n", |
| 336 |
n + 1, |
| 337 |
latest, |
| 338 |
fboards[n].ftn_area, |
| 339 |
fboards[n].description); |
| 340 |
} |
| 341 |
session_flush(s); |
| 342 |
|
| 343 |
show_list = false; |
| 344 |
} |
| 345 |
|
| 346 |
c = session_menu(s, title, db->config.ftn_network, |
| 347 |
(char *)prompt_help, opts, nitems(opts), show_help, "Area #", |
| 348 |
&an); |
| 349 |
show_help = false; |
| 350 |
|
| 351 |
switch (c) { |
| 352 |
case 'l': |
| 353 |
show_list = true; |
| 354 |
break; |
| 355 |
case '#': |
| 356 |
if (an < 1 || an > nfboards) { |
| 357 |
session_printf(s, "Invalid area\r\n"); |
| 358 |
session_flush(s); |
| 359 |
break; |
| 360 |
} |
| 361 |
board_show(s, fboards[an - 1].id, db->config.ftn_network); |
| 362 |
break; |
| 363 |
case '?': |
| 364 |
show_help = true; |
| 365 |
break; |
| 366 |
default: |
| 367 |
done = true; |
| 368 |
break; |
| 369 |
} |
| 370 |
} |
| 371 |
|
| 372 |
if (fboards != NULL) |
| 373 |
xfree(&fboards); |
| 374 |
} |
| 375 |
|
| 376 |
void |
| 377 |
board_show(struct session *s, short id, char *prompt_prefix) |
| 378 |
{ |
| 379 |
static const struct session_menu_option opts[] = { |
| 380 |
{ '#', "#", "Enter post to read" }, |
| 381 |
{ '<', "<", "View newer posts" }, |
| 382 |
{ 'l', "Ll", "List posts" }, |
| 383 |
{ '>', ">", "View older posts" }, |
| 384 |
{ 'p', "Pp", "Post new thread" }, |
| 385 |
{ 'q', "QqXx", "Return to main menu" }, |
| 386 |
{ '?', "?", "List menu options" }, |
| 387 |
}; |
| 388 |
static const char prompt_help[] = |
| 389 |
"#:Read <:Newer >:Older L:List P:New Q:Return ?:Help"; |
| 390 |
char prompt[7 + member_size(struct board, name)]; |
| 391 |
struct board *board = NULL; |
| 392 |
size_t n, nall_post_ids, page, pages, npost_ids; |
| 393 |
unsigned long *post_ids = NULL; |
| 394 |
short ppp, ret, pn; |
| 395 |
char c, next_c; |
| 396 |
bool done, find_post_ids, show_list, show_help; |
| 397 |
|
| 398 |
for (n = 0; n < db->nboards; n++) { |
| 399 |
if (db->boards[n].id == id) { |
| 400 |
board = &db->boards[n]; |
| 401 |
break; |
| 402 |
} |
| 403 |
} |
| 404 |
|
| 405 |
if (!board) { |
| 406 |
session_printf(s, "Invalid board\r\n"); |
| 407 |
session_flush(s); |
| 408 |
return; |
| 409 |
} |
| 410 |
|
| 411 |
page = 0; |
| 412 |
find_post_ids = true; |
| 413 |
show_list = true; |
| 414 |
show_help = false; |
| 415 |
done = false; |
| 416 |
next_c = 0; |
| 417 |
|
| 418 |
if (prompt_prefix == NULL) |
| 419 |
prompt_prefix = "Boards"; |
| 420 |
|
| 421 |
while (!done && !s->ending) { |
| 422 |
if (find_post_ids) { |
| 423 |
if (post_ids != NULL) { |
| 424 |
xfree(&post_ids); |
| 425 |
post_ids = NULL; |
| 426 |
} |
| 427 |
ppp = POSTS_PER_PAGE; |
| 428 |
if (s->terminal_lines < ppp + 3) |
| 429 |
ppp = BOUND(ppp, 5, s->terminal_lines - 3); |
| 430 |
nall_post_ids = board_find_post_ids(s, board, &npost_ids, |
| 431 |
&post_ids, page * ppp, ppp); |
| 432 |
/* ceil(nall_post_ids / ppp) */ |
| 433 |
pages = (nall_post_ids + ppp - 1) / ppp; |
| 434 |
|
| 435 |
if (page >= pages) |
| 436 |
page = pages - 1; |
| 437 |
|
| 438 |
find_post_ids = false; |
| 439 |
} |
| 440 |
|
| 441 |
if (show_list) { |
| 442 |
board_list_posts(s, board, npost_ids, post_ids, page + 1, |
| 443 |
pages); |
| 444 |
show_list = false; |
| 445 |
} |
| 446 |
|
| 447 |
snprintf(prompt, sizeof(prompt), "%s:%s:%ld", prompt_prefix, |
| 448 |
board->name, page + 1); |
| 449 |
|
| 450 |
if (next_c) { |
| 451 |
c = next_c; |
| 452 |
next_c = 0; |
| 453 |
} else { |
| 454 |
c = session_menu(s, board->description, prompt, |
| 455 |
(char *)prompt_help, opts, nitems(opts), show_help, "Post #", |
| 456 |
&pn); |
| 457 |
show_help = false; |
| 458 |
} |
| 459 |
|
| 460 |
handle_opt: |
| 461 |
switch (c) { |
| 462 |
case 'l': |
| 463 |
show_list = true; |
| 464 |
break; |
| 465 |
case 'p': |
| 466 |
if (board_compose(s, board, NULL, NULL, NULL, NULL, NULL)) { |
| 467 |
find_post_ids = true; |
| 468 |
show_list = true; |
| 469 |
} |
| 470 |
break; |
| 471 |
case '>': |
| 472 |
case '<': |
| 473 |
if (c == '>' && page == pages - 1) { |
| 474 |
session_printf(s, "You are at the last page of posts\r\n"); |
| 475 |
session_flush(s); |
| 476 |
break; |
| 477 |
} |
| 478 |
if (c == '<' && page == 0) { |
| 479 |
session_printf(s, "You are already at the first page\r\n"); |
| 480 |
session_flush(s); |
| 481 |
break; |
| 482 |
} |
| 483 |
if (c == '>') |
| 484 |
page++; |
| 485 |
else |
| 486 |
page--; |
| 487 |
find_post_ids = true; |
| 488 |
show_list = true; |
| 489 |
break; |
| 490 |
case '#': |
| 491 |
check_pn: |
| 492 |
if (pn < 1 || pn > npost_ids) { |
| 493 |
session_printf(s, "Invalid post\r\n"); |
| 494 |
session_flush(s); |
| 495 |
break; |
| 496 |
} |
| 497 |
ret = board_post_read(s, board, prompt, post_ids[pn - 1], pn); |
| 498 |
switch (ret) { |
| 499 |
case POST_READ_RETURN_DONE: |
| 500 |
break; |
| 501 |
case POST_READ_RETURN_LIST: |
| 502 |
show_list = true; |
| 503 |
break; |
| 504 |
case POST_READ_RETURN_FIND: |
| 505 |
find_post_ids = true; |
| 506 |
show_list = true; |
| 507 |
break; |
| 508 |
case POST_READ_RETURN_NEWER: |
| 509 |
if (pn == 1) { |
| 510 |
if (page == 0) { |
| 511 |
session_printf(s, "No newer posts.\r\n"); |
| 512 |
session_flush(s); |
| 513 |
break; |
| 514 |
} else { |
| 515 |
page--; |
| 516 |
find_post_ids = true; |
| 517 |
pn = npost_ids; |
| 518 |
next_c = '#'; |
| 519 |
} |
| 520 |
} else { |
| 521 |
pn--; |
| 522 |
goto check_pn; |
| 523 |
} |
| 524 |
break; |
| 525 |
case POST_READ_RETURN_OLDER: |
| 526 |
if (pn == npost_ids) { |
| 527 |
if (page == pages - 1) { |
| 528 |
session_printf(s, "No more posts.\r\n"); |
| 529 |
session_flush(s); |
| 530 |
break; |
| 531 |
} else { |
| 532 |
page++; |
| 533 |
find_post_ids = true; |
| 534 |
pn = 1; |
| 535 |
next_c = '#'; |
| 536 |
} |
| 537 |
} else { |
| 538 |
pn++; |
| 539 |
goto check_pn; |
| 540 |
} |
| 541 |
break; |
| 542 |
default: |
| 543 |
c = ret; |
| 544 |
goto handle_opt; |
| 545 |
} |
| 546 |
break; |
| 547 |
case '?': |
| 548 |
show_help = true; |
| 549 |
break; |
| 550 |
default: |
| 551 |
done = true; |
| 552 |
break; |
| 553 |
} |
| 554 |
} |
| 555 |
|
| 556 |
if (post_ids != NULL) |
| 557 |
xfree(&post_ids); |
| 558 |
} |
| 559 |
|
| 560 |
void |
| 561 |
board_list_posts(struct session *s, struct board *board, size_t npost_ids, |
| 562 |
unsigned long *post_ids, unsigned long page, unsigned long pages) |
| 563 |
{ |
| 564 |
char time[24]; |
| 565 |
unsigned long indent_parent_ids[10] = { 0 }; |
| 566 |
char indent_s[22]; |
| 567 |
size_t n, size; |
| 568 |
struct username_cache *user; |
| 569 |
struct board_thread thread = { 0 }; |
| 570 |
struct board_post post; |
| 571 |
struct board_ftn_post fpost; |
| 572 |
short indent, j, k, ret; |
| 573 |
char *data; |
| 574 |
|
| 575 |
session_printf(s, "{{B}}%s: %s (Page %ld of %ld){{/B}}\r\n", |
| 576 |
board->name, board->description, page, pages); |
| 577 |
session_printf(s, "%s # N Date From Subject%s\r\n", |
| 578 |
ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); |
| 579 |
session_flush(s); |
| 580 |
|
| 581 |
if (npost_ids == 0) { |
| 582 |
session_printf(s, "No posts here yet.\r\n"); |
| 583 |
session_flush(s); |
| 584 |
return; |
| 585 |
} |
| 586 |
|
| 587 |
for (n = 0; n < npost_ids; n++) { |
| 588 |
if (board->ftn_area[0]) { |
| 589 |
size = bile_read_alloc(board->bile, BOARD_FTN_POST_RTYPE, |
| 590 |
post_ids[n], &data); |
| 591 |
ret = bile_unmarshall_object(board->bile, |
| 592 |
board_ftn_post_object_fields, nboard_ftn_post_object_fields, |
| 593 |
data, size, &fpost, sizeof(fpost), false); |
| 594 |
xfree(&data); |
| 595 |
if (ret == BILE_ERR_NO_MEMORY) |
| 596 |
break; |
| 597 |
|
| 598 |
strftime(time, sizeof(time), "%b %d", localtime(&fpost.time)); |
| 599 |
|
| 600 |
session_printf(s, "%s%2ld %c %s {{#}}%-10.10s %s%.50s%s\r\n", |
| 601 |
true ? "" : ansi(s, ANSI_BOLD, ANSI_END), |
| 602 |
n + 1, |
| 603 |
true ? ' ' : 'N', |
| 604 |
time, |
| 605 |
fpost.from, |
| 606 |
(fpost.reply[0] && strncasecmp(fpost.subject, "Re:", 3) != 0 ? |
| 607 |
"Re: " : ""), |
| 608 |
fpost.subject, |
| 609 |
true ? "" : ansi(s, ANSI_RESET, ANSI_END)); |
| 610 |
} else { |
| 611 |
size = bile_read_alloc(board->bile, BOARD_POST_RTYPE, |
| 612 |
post_ids[n], &data); |
| 613 |
ret = bile_unmarshall_object(board->bile, |
| 614 |
board_post_object_fields, nboard_post_object_fields, data, |
| 615 |
size, &post, sizeof(post), false); |
| 616 |
xfree(&data); |
| 617 |
if (ret != 0) |
| 618 |
break; |
| 619 |
|
| 620 |
if (post.thread_id != thread.thread_id) { |
| 621 |
if (thread.thread_id) { |
| 622 |
if (thread.subject != NULL) |
| 623 |
xfree(&thread.subject); |
| 624 |
if (thread.post_ids != NULL) |
| 625 |
xfree(&thread.post_ids); |
| 626 |
if (thread.parent_post_ids != NULL) |
| 627 |
xfree(&thread.parent_post_ids); |
| 628 |
} |
| 629 |
size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, |
| 630 |
post.thread_id, &data); |
| 631 |
ret = bile_unmarshall_object(board->bile, |
| 632 |
board_thread_object_fields, nboard_thread_object_fields, |
| 633 |
data, size, &thread, sizeof(thread), true); |
| 634 |
xfree(&data); |
| 635 |
if (ret == BILE_ERR_NO_MEMORY) |
| 636 |
break; |
| 637 |
|
| 638 |
for (j = 0; j < nitems(indent_parent_ids); j++) |
| 639 |
indent_parent_ids[j] = 0; |
| 640 |
} |
| 641 |
|
| 642 |
user = user_username(post.sender_user_id); |
| 643 |
strftime(time, sizeof(time), "%b %d", localtime(&post.time)); |
| 644 |
|
| 645 |
if (post.parent_post_id == 0) { |
| 646 |
indent_s[0] = '\0'; |
| 647 |
} else { |
| 648 |
indent = -1; |
| 649 |
for (j = 0; j < nitems(indent_parent_ids); j++) { |
| 650 |
if (indent_parent_ids[j] == post.parent_post_id || |
| 651 |
indent_parent_ids[j] == 0) { |
| 652 |
indent = j; |
| 653 |
indent_parent_ids[j] = post.parent_post_id; |
| 654 |
for (k = j + 1; k < nitems(indent_parent_ids); |
| 655 |
k++) { |
| 656 |
if (indent_parent_ids[k] == 0) |
| 657 |
break; |
| 658 |
indent_parent_ids[k] = 0; |
| 659 |
} |
| 660 |
break; |
| 661 |
} |
| 662 |
} |
| 663 |
if (indent == -1) |
| 664 |
indent = nitems(indent_parent_ids) - 1; |
| 665 |
|
| 666 |
for (j = 0; j < indent; j++) { |
| 667 |
indent_s[j] = ' '; |
| 668 |
} |
| 669 |
indent_s[j] = '`'; |
| 670 |
indent_s[j + 1] = '-'; |
| 671 |
indent_s[j + 2] = '>'; |
| 672 |
indent_s[j + 3] = '\0'; |
| 673 |
} |
| 674 |
session_printf(s, "%s%2ld %c %s %-10.10s %s{{#}}%.40s%s\r\n", |
| 675 |
true ? "" : ansi(s, ANSI_BOLD, ANSI_END), |
| 676 |
n + 1, |
| 677 |
true ? ' ' : 'N', |
| 678 |
time, |
| 679 |
user ? user->username : "(unknown)", |
| 680 |
post.parent_post_id != 0 && n == 0 ? "Re: " : "", |
| 681 |
post.parent_post_id == 0 || n == 0 ? thread.subject : indent_s, |
| 682 |
true ? "" : ansi(s, ANSI_RESET, ANSI_END)); |
| 683 |
} |
| 684 |
} |
| 685 |
session_flush(s); |
| 686 |
|
| 687 |
if (thread.subject != NULL) |
| 688 |
xfree(&thread.subject); |
| 689 |
if (thread.post_ids != NULL) |
| 690 |
xfree(&thread.post_ids); |
| 691 |
if (thread.parent_post_ids != NULL) |
| 692 |
xfree(&thread.parent_post_ids); |
| 693 |
} |
| 694 |
|
| 695 |
unsigned long |
| 696 |
board_compose(struct session *s, struct board *board, |
| 697 |
struct board_thread *thread, struct board_post *parent_post, |
| 698 |
struct board_ftn_post *ftn_parent_post, char *initial_subject, |
| 699 |
char *initial_body) |
| 700 |
{ |
| 701 |
struct board_post post = { 0 }; |
| 702 |
char *tmp = NULL; |
| 703 |
short c; |
| 704 |
|
| 705 |
if (!s->user) { |
| 706 |
session_printf(s, "Posting is not available to guests.\r\n" |
| 707 |
"Please create an account first.\r\n"); |
| 708 |
session_flush(s); |
| 709 |
return 0; |
| 710 |
} |
| 711 |
|
| 712 |
if (board->restricted_posting && !s->user->is_sysop) { |
| 713 |
session_printf(s, "Posting to this board is not permitted.\r\n"); |
| 714 |
session_flush(s); |
| 715 |
return 0; |
| 716 |
} |
| 717 |
|
| 718 |
if (initial_body) { |
| 719 |
post.body = xstrdup(initial_body); |
| 720 |
if (post.body == NULL) |
| 721 |
return 0; |
| 722 |
} |
| 723 |
|
| 724 |
if (thread) { |
| 725 |
post.thread_id = thread->thread_id; |
| 726 |
post.parent_post_id = parent_post->id; |
| 727 |
} else { |
| 728 |
thread = xmalloczero(sizeof(struct board_thread)); |
| 729 |
if (thread == NULL) |
| 730 |
return 0; |
| 731 |
} |
| 732 |
post.sender_user_id = s->user->id; |
| 733 |
strlcpy(post.via, s->via, sizeof(post.via)); |
| 734 |
|
| 735 |
session_printf(s, "{{B}}Compose %s{{/B}}\r\n", |
| 736 |
parent_post ? "Reply" : "New Post"); |
| 737 |
session_printf(s, "{{B}}From:{{/B}} %s (via %s)\r\n", |
| 738 |
s->user->username, s->via); |
| 739 |
session_printf(s, "{{B}}To:{{/B}} %s\r\n", board->name); |
| 740 |
|
| 741 |
post_compose_start: |
| 742 |
if (parent_post) { |
| 743 |
session_printf(s, "{{B}}Subject:{{/B}}{{#}} Re: %s\r\n", |
| 744 |
thread->subject); |
| 745 |
session_flush(s); |
| 746 |
} else if (ftn_parent_post) { |
| 747 |
session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s%s\r\n", |
| 748 |
strncmp("Re:", ftn_parent_post->subject, 3) == 1 ? |
| 749 |
"" : "Re: ", ftn_parent_post->subject); |
| 750 |
session_flush(s); |
| 751 |
} else { |
| 752 |
if (initial_subject && !thread->subject) { |
| 753 |
thread->subject = xstrdup(initial_subject); |
| 754 |
if (thread->subject == NULL) |
| 755 |
return 0; |
| 756 |
} |
| 757 |
|
| 758 |
for (;;) { |
| 759 |
session_printf(s, "{{B}}Subject:{{/B}} "); |
| 760 |
session_flush(s); |
| 761 |
|
| 762 |
tmp = session_field_input(s, 100, 50, thread->subject, |
| 763 |
false, 0); |
| 764 |
if (thread->subject != NULL) |
| 765 |
xfree(&thread->subject); |
| 766 |
thread->subject = tmp; |
| 767 |
session_output(s, "\r\n", 2); |
| 768 |
session_flush(s); |
| 769 |
|
| 770 |
if (thread->subject == NULL) |
| 771 |
goto post_compose_done; |
| 772 |
|
| 773 |
rtrim(thread->subject, "\r\n\t "); |
| 774 |
|
| 775 |
if (thread->subject[0] == '\0') { |
| 776 |
session_printf(s, "{{B}}Error:{{/B}} Subject " |
| 777 |
"cannot be blank (^C to cancel)\r\n"); |
| 778 |
session_flush(s); |
| 779 |
xfree(&thread->subject); |
| 780 |
continue; |
| 781 |
} |
| 782 |
thread->subject_size = strlen(thread->subject) + 1; |
| 783 |
break; |
| 784 |
} |
| 785 |
} |
| 786 |
|
| 787 |
for (;;) { |
| 788 |
session_printf(s, "{{B}}Message (^D when finished):{{/B}}\r\n"); |
| 789 |
session_flush(s); |
| 790 |
|
| 791 |
tmp = session_field_input(s, 2048, s->terminal_columns - 1, |
| 792 |
post.body, true, 0); |
| 793 |
if (post.body != NULL) |
| 794 |
xfree(&post.body); |
| 795 |
post.body = tmp; |
| 796 |
session_output(s, "\r\n", 2); |
| 797 |
session_flush(s); |
| 798 |
|
| 799 |
if (post.body == NULL) |
| 800 |
goto post_compose_done; |
| 801 |
|
| 802 |
rtrim(post.body, "\r\n\t "); |
| 803 |
|
| 804 |
if (post.body[0] == '\0') { |
| 805 |
xfree(&post.body); |
| 806 |
goto post_compose_done; |
| 807 |
} |
| 808 |
post.body_size = strlen(post.body) + 1; |
| 809 |
break; |
| 810 |
} |
| 811 |
|
| 812 |
for (;;) { |
| 813 |
session_printf(s, "\r\n{{B}}(P){{/B}}ost message, " |
| 814 |
"{{B}}(E){{/B}}dit again, or {{B}}(C){{/B}}ancel? "); |
| 815 |
session_flush(s); |
| 816 |
|
| 817 |
c = session_input_char(s); |
| 818 |
if (c == 0 || s->ending) |
| 819 |
goto post_compose_done; |
| 820 |
|
| 821 |
switch (c) { |
| 822 |
case 'p': |
| 823 |
case 'P': |
| 824 |
case 'y': |
| 825 |
session_printf(s, "%c\r\n", c); |
| 826 |
session_flush(s); |
| 827 |
/* FALLTHROUGH */ |
| 828 |
case '\n': |
| 829 |
case '\r': |
| 830 |
/* send */ |
| 831 |
session_printf(s, "Posting message... "); |
| 832 |
session_flush(s); |
| 833 |
|
| 834 |
if (board_post_create(board, thread, ftn_parent_post, |
| 835 |
&post) == 0) { |
| 836 |
session_logf(s, "Posted message %ld to %s", post.id, |
| 837 |
board->name); |
| 838 |
session_printf(s, "done\r\n"); |
| 839 |
} else |
| 840 |
session_printf(s, "failed!\r\n"); |
| 841 |
|
| 842 |
session_flush(s); |
| 843 |
|
| 844 |
goto post_compose_done; |
| 845 |
case 'e': |
| 846 |
case 'E': |
| 847 |
session_printf(s, "%c\r\n", c); |
| 848 |
session_flush(s); |
| 849 |
goto post_compose_start; |
| 850 |
case 'c': |
| 851 |
case 'C': |
| 852 |
session_printf(s, "%c\r\n", c); |
| 853 |
session_flush(s); |
| 854 |
/* FALLTHROUGH */ |
| 855 |
case CONTROL_C: |
| 856 |
goto post_compose_done; |
| 857 |
} |
| 858 |
} |
| 859 |
|
| 860 |
post_compose_error: |
| 861 |
session_printf(s, "Failed saving message!\r\n"); |
| 862 |
session_flush(s); |
| 863 |
|
| 864 |
post_compose_done: |
| 865 |
if (parent_post == NULL) { |
| 866 |
if (thread->subject != NULL) |
| 867 |
xfree(&thread->subject); |
| 868 |
xfree(&thread); |
| 869 |
} |
| 870 |
if (post.body) |
| 871 |
xfree(&post.body); |
| 872 |
|
| 873 |
return post.id; |
| 874 |
} |
| 875 |
|
| 876 |
short |
| 877 |
board_post_read(struct session *s, struct board *board, char *prompt_prefix, |
| 878 |
unsigned long id, short idx) |
| 879 |
{ |
| 880 |
static const struct session_menu_option opts[] = { |
| 881 |
{ '<', "<Nn", "Read newer post" }, |
| 882 |
{ '>', ">Pp", "Read older post" }, |
| 883 |
{ 'r', "Rr", "Reply to this post" }, |
| 884 |
{ 's', "Ss", "Show this post" }, |
| 885 |
{ 'd', "Dd", "Delete this post" }, |
| 886 |
{ 'l', "Ll", "List posts" }, |
| 887 |
{ 'q', "QqXx", "Return to threads" }, |
| 888 |
{ '?', "?", "List these options" }, |
| 889 |
}; |
| 890 |
static const char prompt_help[] = |
| 891 |
"<:Newer >:Older R:Reply S:Show D:Delete L:List Q:Return ?:Help"; |
| 892 |
char time[32], prompt[7 + member_size(struct board, name) + 8]; |
| 893 |
struct board_thread thread; |
| 894 |
struct board_post post; |
| 895 |
struct board_ftn_post fpost; |
| 896 |
struct username_cache *sender; |
| 897 |
struct session_menu_option *dopts = NULL; |
| 898 |
size_t size, plain_post_size, j; |
| 899 |
short ret = POST_READ_RETURN_DONE; |
| 900 |
char c; |
| 901 |
char *data, *subject, *plain_post, *tplain_post; |
| 902 |
short cc, bcret, n; |
| 903 |
bool done = false, show_help = false; |
| 904 |
|
| 905 |
dopts = xmalloc(sizeof(opts)); |
| 906 |
if (dopts == NULL) |
| 907 |
return 0; |
| 908 |
memcpy(dopts, opts, sizeof(opts)); |
| 909 |
|
| 910 |
show_post: |
| 911 |
if (board->ftn_area[0]) { |
| 912 |
size = bile_read_alloc(board->bile, BOARD_FTN_POST_RTYPE, id, |
| 913 |
&data); |
| 914 |
if (size == 0) |
| 915 |
panic("failed fetching message %ld: %d", id, |
| 916 |
bile_error(board->bile)); |
| 917 |
|
| 918 |
ret = bile_unmarshall_object(board->bile, |
| 919 |
board_ftn_post_object_fields, nboard_ftn_post_object_fields, |
| 920 |
data, size, &fpost, sizeof(fpost), true); |
| 921 |
xfree(&data); |
| 922 |
if (ret == BILE_ERR_NO_MEMORY) |
| 923 |
goto done_reading; |
| 924 |
|
| 925 |
if (!(s->user && s->user->is_sysop)) { |
| 926 |
/* disable deleting */ |
| 927 |
for (n = 0; n < nitems(opts); n++) { |
| 928 |
if (dopts[n].ret == 'd') { |
| 929 |
dopts[n].key[0] = '\0'; |
| 930 |
break; |
| 931 |
} |
| 932 |
} |
| 933 |
} |
| 934 |
|
| 935 |
strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S", |
| 936 |
localtime(&fpost.time)); |
| 937 |
|
| 938 |
session_printf(s, "{{B}}From:{{/B}}{{#}} %s\r\n", fpost.from); |
| 939 |
session_printf(s, "{{B}}Origin:{{/B}}{{#}} %s\r\n", fpost.origin); |
| 940 |
session_printf(s, "{{B}}To:{{/B}}{{#}} %s@%s\r\n", fpost.to, |
| 941 |
board->name); |
| 942 |
session_printf(s, "{{B}}Date:{{/B}}{{#}} %s %s\r\n", time, |
| 943 |
db->config.timezone); |
| 944 |
session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s%s\r\n", |
| 945 |
(fpost.reply[0] && strncasecmp(fpost.subject, "Re:", 3) != 0 ? |
| 946 |
"Re: " : ""), fpost.subject); |
| 947 |
session_printf(s, "\r\n"); |
| 948 |
session_flush(s); |
| 949 |
|
| 950 |
plain_post_size = 0; |
| 951 |
plain_post = xmalloc(fpost.body_size); |
| 952 |
if (plain_post == NULL) |
| 953 |
session_paginate(s, fpost.body, fpost.body_size, 6); |
| 954 |
else { |
| 955 |
/* strip out renegade-style pipe color codes ("abc|01def") */ |
| 956 |
for (j = 0; j < fpost.body_size; j++) { |
| 957 |
if (fpost.body[j] == '|' && |
| 958 |
fpost.body[j + 1] >= '0' && fpost.body[j + 1] <= '9' && |
| 959 |
fpost.body[j + 2] >= '0' && fpost.body[j + 2] <= '9') { |
| 960 |
j += 2; |
| 961 |
continue; |
| 962 |
} |
| 963 |
plain_post[plain_post_size++] = fpost.body[j]; |
| 964 |
} |
| 965 |
tplain_post = xrealloc(plain_post, plain_post_size); |
| 966 |
if (tplain_post != NULL) |
| 967 |
plain_post = tplain_post; |
| 968 |
session_paginate(s, plain_post, plain_post_size, 6); |
| 969 |
xfree(&plain_post); |
| 970 |
} |
| 971 |
} else { |
| 972 |
size = bile_read_alloc(board->bile, BOARD_POST_RTYPE, id, &data); |
| 973 |
if (size == 0) |
| 974 |
panic("failed fetching message %ld: %d", id, |
| 975 |
bile_error(board->bile)); |
| 976 |
ret = bile_unmarshall_object(board->bile, board_post_object_fields, |
| 977 |
nboard_post_object_fields, data, size, &post, sizeof(post), true); |
| 978 |
xfree(&data); |
| 979 |
if (ret == BILE_ERR_NO_MEMORY) |
| 980 |
goto done_reading; |
| 981 |
|
| 982 |
size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, |
| 983 |
post.thread_id, &data); |
| 984 |
if (size == 0) |
| 985 |
panic("failed fetching thread %ld: %d", post.thread_id, |
| 986 |
bile_error(board->bile)); |
| 987 |
ret = bile_unmarshall_object(board->bile, board_thread_object_fields, |
| 988 |
nboard_thread_object_fields, data, size, &thread, |
| 989 |
sizeof(thread), true); |
| 990 |
xfree(&data); |
| 991 |
if (ret == BILE_ERR_NO_MEMORY) |
| 992 |
goto done_reading; |
| 993 |
|
| 994 |
if (!(s->user && (s->user->is_sysop || |
| 995 |
s->user->id == post.sender_user_id))) { |
| 996 |
/* disable deleting */ |
| 997 |
for (n = 0; n < nitems(opts); n++) { |
| 998 |
if (dopts[n].ret == 'd') { |
| 999 |
dopts[n].key[0] = '\0'; |
| 1000 |
break; |
| 1001 |
} |
| 1002 |
} |
| 1003 |
} |
| 1004 |
|
| 1005 |
sender = user_username(post.sender_user_id); |
| 1006 |
|
| 1007 |
strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S", |
| 1008 |
localtime(&post.time)); |
| 1009 |
|
| 1010 |
session_printf(s, "{{B}}From:{{/B}}{{#}} %s", |
| 1011 |
sender ? sender->username : "(unknown)"); |
| 1012 |
if (post.via[0]) |
| 1013 |
session_printf(s, " (via %s)", post.via); |
| 1014 |
session_printf(s, "\r\n"); |
| 1015 |
session_printf(s, "{{B}}To:{{/B}}{{#}} %s\r\n", board->name); |
| 1016 |
session_printf(s, "{{B}}Date:{{/B}}{{#}} %s %s\r\n", time, |
| 1017 |
db->config.timezone); |
| 1018 |
session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s%s\r\n", |
| 1019 |
(post.parent_post_id ? "Re: " : ""), thread.subject); |
| 1020 |
session_printf(s, "\r\n"); |
| 1021 |
session_flush(s); |
| 1022 |
session_paginate(s, post.body, post.body_size, 5); |
| 1023 |
} |
| 1024 |
|
| 1025 |
snprintf(prompt, sizeof(prompt), "%s:%d", prompt_prefix, idx); |
| 1026 |
|
| 1027 |
if (board->ftn_area[0]) |
| 1028 |
subject = fpost.subject; |
| 1029 |
else |
| 1030 |
subject = thread.subject; |
| 1031 |
|
| 1032 |
while (!done && !s->ending) { |
| 1033 |
c = session_menu(s, subject, prompt, (char *)prompt_help, dopts, |
| 1034 |
nitems(opts), show_help, NULL, NULL); |
| 1035 |
show_help = false; |
| 1036 |
|
| 1037 |
switch (c) { |
| 1038 |
case 'd': |
| 1039 |
if (!(s->user && (s->user->is_sysop || |
| 1040 |
s->user->id == post.sender_user_id))) { |
| 1041 |
session_printf(s, "Invalid option\r\n"); |
| 1042 |
session_flush(s); |
| 1043 |
break; |
| 1044 |
} |
| 1045 |
|
| 1046 |
session_printf(s, "Are you sure you want to permanently " |
| 1047 |
"delete this post? [y/N] "); |
| 1048 |
session_flush(s); |
| 1049 |
|
| 1050 |
cc = session_input_char(s); |
| 1051 |
if (cc == 'y' || c == 'Y') { |
| 1052 |
session_printf(s, "%c\r\n", cc); |
| 1053 |
session_flush(s); |
| 1054 |
|
| 1055 |
if (board->ftn_area[0]) { |
| 1056 |
board_delete_ftn_post(board, &fpost); |
| 1057 |
session_logf(s, "Deleted %s post %ld", |
| 1058 |
db->config.ftn_network, fpost.id); |
| 1059 |
} else { |
| 1060 |
board_delete_post(board, &post, &thread); |
| 1061 |
session_logf(s, "Deleted post %ld (thread %ld)", |
| 1062 |
post.id, thread.thread_id); |
| 1063 |
} |
| 1064 |
|
| 1065 |
session_printf(s, "\r\n{{B}}Post deleted!{{/B}}\r\n"); |
| 1066 |
session_flush(s); |
| 1067 |
ret = POST_READ_RETURN_FIND; |
| 1068 |
done = true; |
| 1069 |
} else { |
| 1070 |
session_printf(s, "\r\nPost not deleted.\r\n"); |
| 1071 |
session_flush(s); |
| 1072 |
} |
| 1073 |
break; |
| 1074 |
case 'r': |
| 1075 |
if (board->ftn_area[0]) |
| 1076 |
bcret = board_compose(s, board, NULL, NULL, &fpost, NULL, |
| 1077 |
NULL); |
| 1078 |
else |
| 1079 |
bcret = board_compose(s, board, &thread, &post, NULL, NULL, |
| 1080 |
NULL); |
| 1081 |
|
| 1082 |
if (bcret) { |
| 1083 |
ret = POST_READ_RETURN_FIND; |
| 1084 |
done = true; |
| 1085 |
} |
| 1086 |
break; |
| 1087 |
case 's': |
| 1088 |
goto show_post; |
| 1089 |
break; |
| 1090 |
case '<': |
| 1091 |
ret = POST_READ_RETURN_NEWER; |
| 1092 |
done = true; |
| 1093 |
break; |
| 1094 |
case '>': |
| 1095 |
ret = POST_READ_RETURN_OLDER; |
| 1096 |
done = true; |
| 1097 |
break; |
| 1098 |
case 'l': |
| 1099 |
ret = POST_READ_RETURN_LIST; |
| 1100 |
done = true; |
| 1101 |
break; |
| 1102 |
case 'q': |
| 1103 |
ret = POST_READ_RETURN_DONE; |
| 1104 |
done = true; |
| 1105 |
break; |
| 1106 |
case '?': |
| 1107 |
show_help = true; |
| 1108 |
break; |
| 1109 |
} |
| 1110 |
} |
| 1111 |
|
| 1112 |
done_reading: |
| 1113 |
xfree(&dopts); |
| 1114 |
|
| 1115 |
if (board->ftn_area[0]) { |
| 1116 |
if (fpost.body != NULL) |
| 1117 |
xfree(&fpost.body); |
| 1118 |
} else { |
| 1119 |
if (post.body != NULL) |
| 1120 |
xfree(&post.body); |
| 1121 |
if (thread.subject != NULL) |
| 1122 |
xfree(&thread.subject); |
| 1123 |
if (thread.post_ids != NULL) |
| 1124 |
xfree(&thread.post_ids); |
| 1125 |
if (thread.parent_post_ids != NULL) |
| 1126 |
xfree(&thread.parent_post_ids); |
| 1127 |
} |
| 1128 |
|
| 1129 |
return ret; |
| 1130 |
} |
| 1131 |
|
| 1132 |
size_t |
| 1133 |
board_find_post_ids(struct session *s, struct board *board, |
| 1134 |
size_t *npost_ids, unsigned long **post_ids, size_t offset, size_t limit) |
| 1135 |
{ |
| 1136 |
struct board_id_time_map *all_post_id_map = NULL; |
| 1137 |
size_t n, size, nall_post_ids; |
| 1138 |
|
| 1139 |
*post_ids = NULL; |
| 1140 |
*npost_ids = 0; |
| 1141 |
|
| 1142 |
size = bile_read_alloc(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, |
| 1143 |
&all_post_id_map); |
| 1144 |
if (all_post_id_map == NULL) { |
| 1145 |
session_printf(s, "%sPlease wait, re-indexing board posts...%s", |
| 1146 |
ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); |
| 1147 |
session_flush(s); |
| 1148 |
nall_post_ids = board_index_sorted_post_ids(board, |
| 1149 |
&all_post_id_map); |
| 1150 |
session_output(s, "\r\n", 2); |
| 1151 |
session_flush(s); |
| 1152 |
if (nall_post_ids == 0) |
| 1153 |
return 0; |
| 1154 |
} else |
| 1155 |
nall_post_ids = size / sizeof(struct board_id_time_map); |
| 1156 |
|
| 1157 |
*post_ids = xcalloc(sizeof(long), MIN(limit, nall_post_ids)); |
| 1158 |
if (*post_ids == NULL) { |
| 1159 |
if (all_post_id_map != NULL) |
| 1160 |
xfree(&all_post_id_map); |
| 1161 |
return 0; |
| 1162 |
} |
| 1163 |
|
| 1164 |
for (n = 0; n < nall_post_ids; n++) { |
| 1165 |
if (n < offset) |
| 1166 |
continue; |
| 1167 |
|
| 1168 |
(*post_ids)[*npost_ids] = all_post_id_map[n].id; |
| 1169 |
(*npost_ids)++; |
| 1170 |
|
| 1171 |
if (*npost_ids >= limit) |
| 1172 |
break; |
| 1173 |
} |
| 1174 |
|
| 1175 |
if (all_post_id_map != NULL) |
| 1176 |
xfree(&all_post_id_map); |
| 1177 |
|
| 1178 |
return nall_post_ids; |
| 1179 |
} |
| 1180 |
|
| 1181 |
short |
| 1182 |
board_post_create(struct board *board, struct board_thread *thread, |
| 1183 |
struct board_ftn_post *ftn_parent_post, struct board_post *post) |
| 1184 |
{ |
| 1185 |
struct board_ftn_post ftn_post = { 0 }; |
| 1186 |
struct fidopkt_message fidomsg = { 0 }; |
| 1187 |
struct username_cache *user; |
| 1188 |
struct fidopkt_address our_address, hub_address; |
| 1189 |
short ret; |
| 1190 |
char *data; |
| 1191 |
size_t size, insert; |
| 1192 |
ssize_t n, j; |
| 1193 |
unsigned long *post_ids, *parent_post_ids; |
| 1194 |
|
| 1195 |
if (board->ftn_area[0]) { |
| 1196 |
if (!post->id) |
| 1197 |
post->id = bile_next_id(board->bile, BOARD_FTN_POST_RTYPE); |
| 1198 |
if (!post->time) |
| 1199 |
post->time = Time; |
| 1200 |
|
| 1201 |
if (!fidopkt_parse_address(db->config.ftn_node_addr, |
| 1202 |
&our_address)) { |
| 1203 |
logger_printf("[board] Invalid FTN local node address, can't " |
| 1204 |
"create board post"); |
| 1205 |
post->id = 0; |
| 1206 |
goto done; |
| 1207 |
} |
| 1208 |
if (!fidopkt_parse_address(db->config.ftn_hub_addr, |
| 1209 |
&hub_address)) { |
| 1210 |
logger_printf("[board] Invalid FTN hub address, can't " |
| 1211 |
"create board post"); |
| 1212 |
post->id = 0; |
| 1213 |
goto done; |
| 1214 |
} |
| 1215 |
|
| 1216 |
ftn_post.id = post->id; |
| 1217 |
ftn_post.time = post->time; |
| 1218 |
|
| 1219 |
if (ftn_parent_post) { |
| 1220 |
snprintf(ftn_post.subject, sizeof(ftn_post.subject), |
| 1221 |
"%s%s", |
| 1222 |
strncmp("Re:", ftn_parent_post->subject, 3) == 1 ? |
| 1223 |
"" : "Re: ", |
| 1224 |
ftn_parent_post->subject); |
| 1225 |
strlcpy(ftn_post.reply, ftn_parent_post->msgid_orig, |
| 1226 |
sizeof(ftn_post.reply)); |
| 1227 |
strlcpy(ftn_post.to, ftn_parent_post->from, |
| 1228 |
sizeof(ftn_post.to)); |
| 1229 |
} else { |
| 1230 |
strlcpy(ftn_post.subject, thread->subject, |
| 1231 |
sizeof(ftn_post.subject)); |
| 1232 |
strlcpy(ftn_post.to, "All", sizeof(ftn_post.to)); |
| 1233 |
} |
| 1234 |
|
| 1235 |
user = user_username(post->sender_user_id); |
| 1236 |
if (user == NULL) { |
| 1237 |
logger_printf("[board] Can't find username of user posting " |
| 1238 |
"new message"); |
| 1239 |
post->id = 0; |
| 1240 |
goto done; |
| 1241 |
} |
| 1242 |
strlcpy(ftn_post.from, user->username, sizeof(ftn_post.from)); |
| 1243 |
|
| 1244 |
ftn_post.body = post->body; |
| 1245 |
ftn_post.body_size = post->body_size; |
| 1246 |
|
| 1247 |
/* make each board's posts have ids unique to our zone/net/node */ |
| 1248 |
ftn_post.msgid.id = 0x10000000 | |
| 1249 |
((unsigned long)our_address.node << 24) | |
| 1250 |
((unsigned long)(board->id) << 16) | post->id; |
| 1251 |
ftn_post.msgid.zone = our_address.zone; |
| 1252 |
ftn_post.msgid.net = our_address.net; |
| 1253 |
ftn_post.msgid.node = our_address.node; |
| 1254 |
ftn_post.msgid.point = our_address.point; |
| 1255 |
|
| 1256 |
snprintf(ftn_post.origin, sizeof(ftn_post.origin), |
| 1257 |
"%s | %s (%s)", |
| 1258 |
db->config.name, db->config.hostname, |
| 1259 |
db->config.ftn_node_addr); |
| 1260 |
|
| 1261 |
fidomsg.time = ftn_post.time; |
| 1262 |
memcpy(&fidomsg.header.orig, &our_address, |
| 1263 |
sizeof(fidomsg.header.orig)); |
| 1264 |
memcpy(&fidomsg.header.dest, &hub_address, |
| 1265 |
sizeof(fidomsg.header.dest)); |
| 1266 |
strlcpy(fidomsg.area, board->ftn_area, sizeof(fidomsg.area)); |
| 1267 |
strlcpy(fidomsg.to, ftn_post.to, sizeof(fidomsg.to)); |
| 1268 |
strlcpy(fidomsg.from, ftn_post.from, sizeof(fidomsg.from)); |
| 1269 |
strlcpy(fidomsg.subject, ftn_post.subject, sizeof(fidomsg.subject)); |
| 1270 |
fidomsg.body = ftn_post.body; |
| 1271 |
fidomsg.body_len = ftn_post.body_size - 1; |
| 1272 |
strlcpy(fidomsg.reply, ftn_post.reply, sizeof(fidomsg.reply)); |
| 1273 |
memcpy(&fidomsg.msgid, &ftn_post.msgid, sizeof(fidomsg.msgid)); |
| 1274 |
strlcpy(fidomsg.origin, ftn_post.origin, sizeof(fidomsg.origin)); |
| 1275 |
|
| 1276 |
if (!binkp_scan_message(&fidomsg)) { |
| 1277 |
logger_printf("[board] Failed scanning new FTN message being " |
| 1278 |
"posted"); |
| 1279 |
post->id = 0; |
| 1280 |
goto done; |
| 1281 |
} |
| 1282 |
|
| 1283 |
if (board_toss_ftn_message(board, &fidomsg, true) != 1) { |
| 1284 |
logger_printf("[board] Failed tossing new FTN message being " |
| 1285 |
"posted"); |
| 1286 |
post->id = 0; |
| 1287 |
goto done; |
| 1288 |
} |
| 1289 |
} else { |
| 1290 |
if (!post->id) |
| 1291 |
post->id = bile_next_id(board->bile, BOARD_POST_RTYPE); |
| 1292 |
if (!post->time) |
| 1293 |
post->time = Time; |
| 1294 |
|
| 1295 |
if (post->parent_post_id == 0) { |
| 1296 |
thread->thread_id = bile_next_id(board->bile, |
| 1297 |
BOARD_THREAD_RTYPE); |
| 1298 |
post->thread_id = thread->thread_id; |
| 1299 |
} |
| 1300 |
|
| 1301 |
ret = bile_marshall_object(board->bile, board_post_object_fields, |
| 1302 |
nboard_post_object_fields, post, &data, &size); |
| 1303 |
if (ret != 0 || size == 0) { |
| 1304 |
logger_printf("[board] Failed to marshall new post object"); |
| 1305 |
post->id = 0; |
| 1306 |
goto done; |
| 1307 |
} |
| 1308 |
if (bile_write(board->bile, BOARD_POST_RTYPE, post->id, data, |
| 1309 |
size) != size) { |
| 1310 |
warn("bile_write of new post failed! %d", |
| 1311 |
bile_error(board->bile)); |
| 1312 |
post->id = 0; |
| 1313 |
xfree(&data); |
| 1314 |
goto done; |
| 1315 |
} |
| 1316 |
xfree(&data); |
| 1317 |
|
| 1318 |
if (post->time > thread->last_post_at) |
| 1319 |
thread->last_post_at = post->time; |
| 1320 |
thread->nposts++; |
| 1321 |
post_ids = xreallocarray(thread->post_ids, thread->nposts, |
| 1322 |
sizeof(long)); |
| 1323 |
if (post_ids == NULL) |
| 1324 |
return 0; |
| 1325 |
thread->post_ids = post_ids; |
| 1326 |
parent_post_ids = xreallocarray(thread->parent_post_ids, |
| 1327 |
thread->nposts, sizeof(long)); |
| 1328 |
if (parent_post_ids == NULL) |
| 1329 |
return 0; |
| 1330 |
thread->parent_post_ids = parent_post_ids; |
| 1331 |
|
| 1332 |
/* |
| 1333 |
* Add new post id to thread post_ids, but put it in the right |
| 1334 |
* place so that reading post_ids will present the tree in order. |
| 1335 |
* Walk parent_post_ids and find the first match of our parent, |
| 1336 |
* then insert our new post there. This puts newest replies at |
| 1337 |
* the top. |
| 1338 |
*/ |
| 1339 |
|
| 1340 |
insert = thread->nposts - 1; |
| 1341 |
for (n = 0; n < thread->nposts - 1; n++) { |
| 1342 |
if (thread->post_ids[n] != post->parent_post_id) |
| 1343 |
continue; |
| 1344 |
|
| 1345 |
for (j = thread->nposts - 2; j > n; j--) { |
| 1346 |
thread->post_ids[j + 1] = thread->post_ids[j]; |
| 1347 |
thread->parent_post_ids[j + 1] = |
| 1348 |
thread->parent_post_ids[j]; |
| 1349 |
} |
| 1350 |
insert = n + 1; |
| 1351 |
break; |
| 1352 |
} |
| 1353 |
thread->post_ids[insert] = post->id; |
| 1354 |
thread->parent_post_ids[insert] = post->parent_post_id; |
| 1355 |
|
| 1356 |
ret = bile_marshall_object(board->bile, board_thread_object_fields, |
| 1357 |
nboard_thread_object_fields, thread, &data, &size); |
| 1358 |
if (ret != 0 || size == 0) { |
| 1359 |
logger_printf("[board] Failed to marshall new thread object"); |
| 1360 |
post->id = 0; |
| 1361 |
goto done; |
| 1362 |
} |
| 1363 |
if (bile_write(board->bile, BOARD_THREAD_RTYPE, thread->thread_id, |
| 1364 |
data, size) != size) { |
| 1365 |
warn("bile_write of thread failed! %d", |
| 1366 |
bile_error(board->bile)); |
| 1367 |
post->id = 0; |
| 1368 |
xfree(&data); |
| 1369 |
goto done; |
| 1370 |
} |
| 1371 |
xfree(&data); |
| 1372 |
|
| 1373 |
/* it would be nice not to have to rebuild this every time... */ |
| 1374 |
board_delete_cached_index(board); |
| 1375 |
bile_flush(board->bile, true); |
| 1376 |
|
| 1377 |
if (post->time > board->last_post_at) |
| 1378 |
board->last_post_at = post->time; |
| 1379 |
} |
| 1380 |
|
| 1381 |
done: |
| 1382 |
return (post->id == 0 ? -1 : 0); |
| 1383 |
} |
| 1384 |
|
| 1385 |
void |
| 1386 |
board_delete_post(struct board *board, struct board_post *post, |
| 1387 |
struct board_thread *thread) |
| 1388 |
{ |
| 1389 |
size_t size, n, nn; |
| 1390 |
char *data, *body; |
| 1391 |
char del[50]; |
| 1392 |
short ret; |
| 1393 |
bool childs = false; |
| 1394 |
unsigned long *new_post_ids; |
| 1395 |
unsigned long *new_parent_post_ids; |
| 1396 |
|
| 1397 |
if (thread->nposts == 1) { |
| 1398 |
bile_delete(board->bile, BOARD_THREAD_RTYPE, thread->thread_id, |
| 1399 |
0); |
| 1400 |
bile_delete(board->bile, BOARD_POST_RTYPE, post->id, |
| 1401 |
BILE_DELETE_FLAG_PURGE); |
| 1402 |
board_delete_cached_index(board); |
| 1403 |
return; |
| 1404 |
} |
| 1405 |
|
| 1406 |
for (n = 0; n < thread->nposts; n++) { |
| 1407 |
if (thread->parent_post_ids[n] == post->id) { |
| 1408 |
childs = true; |
| 1409 |
break; |
| 1410 |
} |
| 1411 |
} |
| 1412 |
|
| 1413 |
if (!childs) { |
| 1414 |
/* just zap this off the end of the tree */ |
| 1415 |
new_post_ids = xcalloc(thread->nposts - 1, sizeof(unsigned long)); |
| 1416 |
if (new_post_ids == NULL) |
| 1417 |
return; |
| 1418 |
new_parent_post_ids = xcalloc(thread->nposts - 1, |
| 1419 |
sizeof(unsigned long)); |
| 1420 |
if (new_parent_post_ids == NULL) |
| 1421 |
return; |
| 1422 |
|
| 1423 |
for (n = 0, nn = 0; n < thread->nposts; n++) { |
| 1424 |
if (thread->post_ids[n] == post->id) |
| 1425 |
continue; |
| 1426 |
|
| 1427 |
new_post_ids[nn] = thread->post_ids[n]; |
| 1428 |
nn++; |
| 1429 |
} |
| 1430 |
|
| 1431 |
for (n = 0, nn = 0; n < thread->nposts; n++) { |
| 1432 |
if (thread->post_ids[n] == post->id) |
| 1433 |
continue; |
| 1434 |
|
| 1435 |
new_parent_post_ids[nn] = thread->parent_post_ids[n]; |
| 1436 |
nn++; |
| 1437 |
} |
| 1438 |
|
| 1439 |
thread->nposts--; |
| 1440 |
|
| 1441 |
xfree(&thread->post_ids); |
| 1442 |
thread->post_ids = new_post_ids; |
| 1443 |
xfree(&thread->parent_post_ids); |
| 1444 |
thread->parent_post_ids = new_parent_post_ids; |
| 1445 |
|
| 1446 |
ret = bile_marshall_object(board->bile, board_thread_object_fields, |
| 1447 |
nboard_thread_object_fields, thread, &data, &size); |
| 1448 |
if (ret != 0 || size == 0) { |
| 1449 |
logger_printf("[board] Failed to marshall thread object " |
| 1450 |
"during post deletion"); |
| 1451 |
return; |
| 1452 |
} |
| 1453 |
if (bile_write(board->bile, BOARD_THREAD_RTYPE, thread->thread_id, |
| 1454 |
data, size) != size) { |
| 1455 |
warn("bile_write of updated thread after post delete failed! " |
| 1456 |
"%d", bile_error(board->bile)); |
| 1457 |
xfree(&data); |
| 1458 |
return; |
| 1459 |
} |
| 1460 |
xfree(&data); |
| 1461 |
|
| 1462 |
bile_delete(board->bile, BOARD_POST_RTYPE, post->id, |
| 1463 |
BILE_DELETE_FLAG_ZERO | BILE_DELETE_FLAG_PURGE); |
| 1464 |
board_delete_cached_index(board); |
| 1465 |
|
| 1466 |
bile_flush(board->bile, true); |
| 1467 |
return; |
| 1468 |
} |
| 1469 |
|
| 1470 |
/* all we can do is change the post to say deleted */ |
| 1471 |
if (post->body != NULL) |
| 1472 |
xfree(&post->body); |
| 1473 |
snprintf(del, sizeof(del), "[ Post deleted ]"); |
| 1474 |
body = xstrdup(del); |
| 1475 |
if (body == NULL) |
| 1476 |
return; |
| 1477 |
post->body = body; |
| 1478 |
post->body_size = strlen(post->body) + 1; |
| 1479 |
|
| 1480 |
ret = bile_marshall_object(board->bile, board_post_object_fields, |
| 1481 |
nboard_post_object_fields, post, &data, &size); |
| 1482 |
if (ret != 0 || size == 0) { |
| 1483 |
logger_printf("[board] Failed to marshall updated post object"); |
| 1484 |
return; |
| 1485 |
} |
| 1486 |
if (bile_write(board->bile, BOARD_POST_RTYPE, post->id, data, |
| 1487 |
size) != size) { |
| 1488 |
warn("bile_write of updated post failed! %d", |
| 1489 |
bile_error(board->bile)); |
| 1490 |
xfree(&data); |
| 1491 |
return; |
| 1492 |
} |
| 1493 |
|
| 1494 |
xfree(&data); |
| 1495 |
} |
| 1496 |
|
| 1497 |
void |
| 1498 |
board_delete_ftn_post(struct board *board, struct board_ftn_post *post) |
| 1499 |
{ |
| 1500 |
size_t size, npost_ids, n; |
| 1501 |
struct board_id_time_map *id_map; |
| 1502 |
|
| 1503 |
bile_delete(board->bile, BOARD_FTN_POST_RTYPE, post->id, |
| 1504 |
BILE_DELETE_FLAG_PURGE); |
| 1505 |
|
| 1506 |
size = bile_read_alloc(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, |
| 1507 |
&id_map); |
| 1508 |
if (size == 0 || id_map == NULL) |
| 1509 |
return; |
| 1510 |
|
| 1511 |
npost_ids = size / sizeof(struct board_id_time_map); |
| 1512 |
|
| 1513 |
for (n = 0; n < npost_ids; n++) { |
| 1514 |
if (id_map[n].id == post->id) { |
| 1515 |
for (; n < npost_ids - 1; n++) |
| 1516 |
id_map[n] = id_map[n + 1]; |
| 1517 |
npost_ids--; |
| 1518 |
break; |
| 1519 |
} |
| 1520 |
} |
| 1521 |
|
| 1522 |
if (npost_ids == 0) |
| 1523 |
board_delete_cached_index(board); |
| 1524 |
else |
| 1525 |
bile_write(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, id_map, |
| 1526 |
sizeof(struct board_id_time_map) * npost_ids); |
| 1527 |
|
| 1528 |
/* TODO: delete from msgid cache too */ |
| 1529 |
|
| 1530 |
xfree(&id_map); |
| 1531 |
} |
| 1532 |
|
| 1533 |
size_t |
| 1534 |
board_index_sorted_post_ids(struct board *board, |
| 1535 |
struct board_id_time_map **sorted_id_map) |
| 1536 |
{ |
| 1537 |
struct thread_time_map { |
| 1538 |
unsigned long id; |
| 1539 |
time_t time; |
| 1540 |
size_t nposts; |
| 1541 |
}; |
| 1542 |
struct board_ftn_post fpost; |
| 1543 |
struct board_post post; |
| 1544 |
struct board_thread thread; |
| 1545 |
size_t ret, size, i, j, n, npost_ids, nthread_ids; |
| 1546 |
unsigned long *post_ids = NULL, *thread_ids = NULL; |
| 1547 |
struct board_id_time_map *id_map, tmp_id_map; |
| 1548 |
struct thread_time_map *thread_map = NULL, tmp_thread_map; |
| 1549 |
char *data; |
| 1550 |
|
| 1551 |
if (board->ftn_area[0]) { |
| 1552 |
npost_ids = bile_ids_by_type(board->bile, BOARD_FTN_POST_RTYPE, |
| 1553 |
&post_ids); |
| 1554 |
if (npost_ids == 0) |
| 1555 |
goto write_out; |
| 1556 |
|
| 1557 |
id_map = xcalloc(sizeof(struct board_id_time_map), npost_ids); |
| 1558 |
if (id_map == NULL) |
| 1559 |
goto write_out; |
| 1560 |
|
| 1561 |
for (n = 0; n < npost_ids; n++) { |
| 1562 |
/* only read as far as the time */ |
| 1563 |
ret = bile_read(board->bile, BOARD_FTN_POST_RTYPE, |
| 1564 |
post_ids[n], &fpost, |
| 1565 |
offsetof(struct board_ftn_post, time) + |
| 1566 |
member_size(struct board_ftn_post, time)); |
| 1567 |
if (ret == 0) |
| 1568 |
goto write_out; |
| 1569 |
|
| 1570 |
id_map[n].id = fpost.id; |
| 1571 |
id_map[n].time = fpost.time; |
| 1572 |
} |
| 1573 |
|
| 1574 |
xfree(&post_ids); |
| 1575 |
|
| 1576 |
/* sort by date descending */ |
| 1577 |
for (i = 1; i < npost_ids; i++) { |
| 1578 |
for (j = i; j > 0; j--) { |
| 1579 |
if (id_map[j].time < id_map[j - 1].time) |
| 1580 |
break; |
| 1581 |
tmp_id_map = id_map[j]; |
| 1582 |
id_map[j] = id_map[j - 1]; |
| 1583 |
id_map[j - 1] = tmp_id_map; |
| 1584 |
} |
| 1585 |
} |
| 1586 |
} else { |
| 1587 |
npost_ids = 0; |
| 1588 |
nthread_ids = bile_ids_by_type(board->bile, BOARD_THREAD_RTYPE, |
| 1589 |
&thread_ids); |
| 1590 |
if (nthread_ids == 0) |
| 1591 |
goto write_out; |
| 1592 |
|
| 1593 |
thread_map = xcalloc(sizeof(struct thread_time_map), nthread_ids); |
| 1594 |
if (thread_map == NULL) |
| 1595 |
goto write_out; |
| 1596 |
|
| 1597 |
for (n = 0; n < nthread_ids; n++) { |
| 1598 |
size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, |
| 1599 |
thread_ids[n], &data); |
| 1600 |
ret = bile_unmarshall_object(board->bile, |
| 1601 |
board_thread_object_fields, nboard_thread_object_fields, data, |
| 1602 |
size, &thread, sizeof(thread), false); |
| 1603 |
xfree(&data); |
| 1604 |
if (ret != 0) |
| 1605 |
goto write_out; |
| 1606 |
|
| 1607 |
thread_map[n].id = thread.thread_id; |
| 1608 |
thread_map[n].time = thread.last_post_at; |
| 1609 |
thread_map[n].nposts = thread.nposts; |
| 1610 |
npost_ids += thread.nposts; |
| 1611 |
} |
| 1612 |
|
| 1613 |
xfree(&thread_ids); |
| 1614 |
|
| 1615 |
/* sort by last post date descending */ |
| 1616 |
for (i = 1; i < nthread_ids; i++) { |
| 1617 |
for (j = i; j > 0; j--) { |
| 1618 |
if (thread_map[j].time < thread_map[j - 1].time) |
| 1619 |
break; |
| 1620 |
tmp_thread_map = thread_map[j]; |
| 1621 |
thread_map[j] = thread_map[j - 1]; |
| 1622 |
thread_map[j - 1] = tmp_thread_map; |
| 1623 |
} |
| 1624 |
} |
| 1625 |
|
| 1626 |
id_map = xcalloc(sizeof(struct board_id_time_map), npost_ids); |
| 1627 |
if (id_map == NULL) |
| 1628 |
goto write_out; |
| 1629 |
|
| 1630 |
npost_ids = 0; |
| 1631 |
for (j = 0; j < nthread_ids; j++) { |
| 1632 |
size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, |
| 1633 |
thread_map[j].id, &data); |
| 1634 |
if (data == NULL) |
| 1635 |
goto write_out; |
| 1636 |
ret = bile_unmarshall_object(board->bile, |
| 1637 |
board_thread_object_fields, nboard_thread_object_fields, data, |
| 1638 |
size, &thread, sizeof(thread), true); |
| 1639 |
xfree(&data); |
| 1640 |
if (ret != 0) |
| 1641 |
goto write_out; |
| 1642 |
|
| 1643 |
/* these are already sorted, and we want to keep thread sort */ |
| 1644 |
for (i = 0; i < thread.nposts; i++) { |
| 1645 |
/* only read as far as the time */ |
| 1646 |
size = bile_read(board->bile, BOARD_POST_RTYPE, |
| 1647 |
thread.post_ids[i], &post, |
| 1648 |
offsetof(struct board_post, time) + |
| 1649 |
member_size(struct board_post, time)); |
| 1650 |
if (size == 0) { |
| 1651 |
logger_printf("[board] Board %s thread %ld post %ld " |
| 1652 |
"is missing", board->name, thread.thread_id, |
| 1653 |
thread.post_ids[i]); |
| 1654 |
continue; |
| 1655 |
} |
| 1656 |
|
| 1657 |
id_map[npost_ids].id = post.id; |
| 1658 |
id_map[npost_ids].time = post.time; |
| 1659 |
npost_ids++; |
| 1660 |
} |
| 1661 |
|
| 1662 |
if (thread.subject != NULL) |
| 1663 |
xfree(&thread.subject); |
| 1664 |
if (thread.post_ids != NULL) |
| 1665 |
xfree(&thread.post_ids); |
| 1666 |
if (thread.parent_post_ids != NULL) |
| 1667 |
xfree(&thread.parent_post_ids); |
| 1668 |
} |
| 1669 |
|
| 1670 |
xfree(&thread_map); |
| 1671 |
} |
| 1672 |
|
| 1673 |
write_out: |
| 1674 |
if (thread_map) |
| 1675 |
xfree(&thread_map); |
| 1676 |
if (thread_ids) |
| 1677 |
xfree(&thread_ids); |
| 1678 |
if (post_ids) |
| 1679 |
xfree(&post_ids); |
| 1680 |
|
| 1681 |
if (npost_ids == 0 || id_map == NULL) { |
| 1682 |
board_delete_cached_index(board); |
| 1683 |
if (sorted_id_map != NULL) |
| 1684 |
*sorted_id_map = NULL; |
| 1685 |
return 0; |
| 1686 |
} |
| 1687 |
|
| 1688 |
bile_write(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, id_map, |
| 1689 |
sizeof(struct board_id_time_map) * npost_ids); |
| 1690 |
if (sorted_id_map == NULL) |
| 1691 |
xfree(&id_map); |
| 1692 |
else |
| 1693 |
*sorted_id_map = id_map; |
| 1694 |
return npost_ids; |
| 1695 |
} |
| 1696 |
|
| 1697 |
short |
| 1698 |
board_toss_ftn_message(struct board *board, |
| 1699 |
struct fidopkt_message *fidomsg, bool local) |
| 1700 |
{ |
| 1701 |
struct board_ftn_msgid_cache { |
| 1702 |
unsigned long id; |
| 1703 |
struct fidopkt_msgid msgid; |
| 1704 |
} *msgid_cache = NULL; |
| 1705 |
struct bile_object *o; |
| 1706 |
struct board_ftn_post post; |
| 1707 |
struct fidopkt_msgid msgid; |
| 1708 |
unsigned long *post_ids; |
| 1709 |
char *pdata; |
| 1710 |
size_t asize, cache_size, psize, npost_ids; |
| 1711 |
short n, ret, bret; |
| 1712 |
bool dirty_cache = false; |
| 1713 |
|
| 1714 |
msgid = fidomsg->msgid; |
| 1715 |
|
| 1716 |
o = bile_find(board->bile, BOARD_FTN_MSGID_CACHE_RTYPE, 1); |
| 1717 |
if (o) { |
| 1718 |
/* allocate its size plus one entry that we may add */ |
| 1719 |
asize = o->size + sizeof(struct board_ftn_msgid_cache); |
| 1720 |
msgid_cache = xmalloc(asize); |
| 1721 |
if (msgid_cache == NULL) { |
| 1722 |
logger_printf("[board] toss: malloc(%ld) failed", |
| 1723 |
asize); |
| 1724 |
return -1; |
| 1725 |
} |
| 1726 |
bile_read(board->bile, o->type, o->id, msgid_cache, o->size); |
| 1727 |
npost_ids = o->size / sizeof(struct board_ftn_msgid_cache); |
| 1728 |
xfree(&o); |
| 1729 |
} else { |
| 1730 |
npost_ids = bile_ids_by_type(board->bile, BOARD_FTN_POST_RTYPE, |
| 1731 |
&post_ids); |
| 1732 |
msgid_cache = xcalloc(sizeof(struct board_ftn_msgid_cache), |
| 1733 |
npost_ids + 1); |
| 1734 |
if (msgid_cache == NULL) { |
| 1735 |
logger_printf("[board] toss: calloc(%ld, %ld) failed", |
| 1736 |
sizeof(struct board_ftn_msgid_cache), npost_ids + 1); |
| 1737 |
return -1; |
| 1738 |
} |
| 1739 |
for (n = 0; n < npost_ids; n++) { |
| 1740 |
/* only read as far as we have to */ |
| 1741 |
bile_read(board->bile, BOARD_FTN_POST_RTYPE, post_ids[n], |
| 1742 |
&post, offsetof(struct board_ftn_post, msgid) + |
| 1743 |
member_size(struct board_ftn_post, msgid)); |
| 1744 |
|
| 1745 |
msgid_cache[n].id = post.id; |
| 1746 |
memcpy(&msgid_cache[n].msgid, &post.msgid, |
| 1747 |
sizeof(post.msgid)); |
| 1748 |
} |
| 1749 |
dirty_cache = true; |
| 1750 |
} |
| 1751 |
|
| 1752 |
uthread_yield(); |
| 1753 |
|
| 1754 |
if (!local) { |
| 1755 |
for (n = 0; n < npost_ids; n++) { |
| 1756 |
if (memcmp(&msgid_cache[n].msgid, &msgid, |
| 1757 |
sizeof(msgid)) == 0) { |
| 1758 |
logger_printf("[board] Already have %s EchoMail %s in %s " |
| 1759 |
"(%ld), skipping", db->config.ftn_network, |
| 1760 |
fidomsg->msgid_orig, board->name, msgid_cache[n].id); |
| 1761 |
ret = 0; |
| 1762 |
goto done; |
| 1763 |
} |
| 1764 |
} |
| 1765 |
} |
| 1766 |
|
| 1767 |
memset(&post, 0, sizeof(post)); |
| 1768 |
post.time = fidomsg->time; |
| 1769 |
post.body_size = fidomsg->body_len + 1; |
| 1770 |
post.body = fidomsg->body; |
| 1771 |
strlcpy(post.reply, fidomsg->reply, sizeof(post.reply)); |
| 1772 |
strlcpy(post.from, fidomsg->from, sizeof(post.from)); |
| 1773 |
strlcpy(post.subject, fidomsg->subject, sizeof(post.subject)); |
| 1774 |
strlcpy(post.to, fidomsg->to, sizeof(post.to)); |
| 1775 |
strlcpy(post.origin, fidomsg->origin, sizeof(post.origin)); |
| 1776 |
strlcpy(post.msgid_orig, fidomsg->msgid_orig, sizeof(post.msgid_orig)); |
| 1777 |
post.msgid = msgid; |
| 1778 |
|
| 1779 |
if (!local || !post.id) |
| 1780 |
post.id = bile_next_id(board->bile, BOARD_FTN_POST_RTYPE); |
| 1781 |
if (!post.id) { |
| 1782 |
logger_printf("[board] Failed get next id for %s", board->name); |
| 1783 |
ret = -1; |
| 1784 |
goto done; |
| 1785 |
} |
| 1786 |
|
| 1787 |
bret = bile_marshall_object(board->bile, |
| 1788 |
board_ftn_post_object_fields, nboard_ftn_post_object_fields, |
| 1789 |
&post, &pdata, &psize); |
| 1790 |
if (bret != 0 || psize == 0) { |
| 1791 |
logger_printf("[board] Failed to marshall new post %s %s: %d", |
| 1792 |
fidomsg->area, fidomsg->msgid_orig, bile_error(board->bile)); |
| 1793 |
ret = -1; |
| 1794 |
goto done; |
| 1795 |
} |
| 1796 |
if (bile_write(board->bile, BOARD_FTN_POST_RTYPE, post.id, pdata, |
| 1797 |
psize) != psize) { |
| 1798 |
logger_printf("[fidopkt] Failed to save new post %s %s: %d", |
| 1799 |
fidomsg->area, fidomsg->msgid_orig, bile_error(board->bile)); |
| 1800 |
ret = -1; |
| 1801 |
xfree(&pdata); |
| 1802 |
goto done; |
| 1803 |
} |
| 1804 |
xfree(&pdata); |
| 1805 |
|
| 1806 |
ret = 1; |
| 1807 |
|
| 1808 |
/* we already allocated one empty space at the end of the cache */ |
| 1809 |
msgid_cache[npost_ids].id = post.id; |
| 1810 |
memcpy(&msgid_cache[npost_ids].msgid, &post.msgid, sizeof(post.msgid)); |
| 1811 |
npost_ids++; |
| 1812 |
dirty_cache = true; |
| 1813 |
|
| 1814 |
board_delete_cached_index(board); |
| 1815 |
|
| 1816 |
if (post.time > board->last_post_at) |
| 1817 |
board->last_post_at = post.time; |
| 1818 |
|
| 1819 |
if (!local) |
| 1820 |
logger_printf("[board] Tossed %s EchoMail %s as %ld", |
| 1821 |
fidomsg->area, fidomsg->msgid_orig, post.id); |
| 1822 |
uthread_yield(); |
| 1823 |
|
| 1824 |
done: |
| 1825 |
if (dirty_cache) { |
| 1826 |
cache_size = npost_ids * sizeof(struct board_ftn_msgid_cache); |
| 1827 |
if (bile_write(board->bile, BOARD_FTN_MSGID_CACHE_RTYPE, 1, |
| 1828 |
msgid_cache, cache_size) != cache_size) { |
| 1829 |
logger_printf("[board] Failed to save msgid cache for %s: %d", |
| 1830 |
fidomsg->area, bile_error(board->bile)); |
| 1831 |
ret = -1; |
| 1832 |
} |
| 1833 |
} |
| 1834 |
if (msgid_cache != NULL) |
| 1835 |
xfree(&msgid_cache); |
| 1836 |
|
| 1837 |
return ret; |
| 1838 |
} |
| 1839 |
|
| 1840 |
void |
| 1841 |
board_delete_cached_index(struct board *board) |
| 1842 |
{ |
| 1843 |
bile_delete(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, |
| 1844 |
BILE_DELETE_FLAG_PURGE); |
| 1845 |
} |
| 1846 |
|
| 1847 |
void |
| 1848 |
board_prune_old_posts(struct board *board) |
| 1849 |
{ |
| 1850 |
size_t nposts, n; |
| 1851 |
struct board_id_time_map *sorted_id_map; |
| 1852 |
struct board_ftn_post fpost; |
| 1853 |
struct board_post post; |
| 1854 |
struct board_thread thread; |
| 1855 |
time_t oldest; |
| 1856 |
size_t size, deleted; |
| 1857 |
char *data; |
| 1858 |
short ret; |
| 1859 |
|
| 1860 |
if (board->retention_days == 0) |
| 1861 |
return; |
| 1862 |
|
| 1863 |
deleted = 0; |
| 1864 |
|
| 1865 |
nposts = board_index_sorted_post_ids(board, &sorted_id_map); |
| 1866 |
if (nposts == 0) |
| 1867 |
goto done; |
| 1868 |
|
| 1869 |
oldest = Time - ((unsigned long)(board->retention_days) * |
| 1870 |
(60UL * 60UL * 24UL)); |
| 1871 |
|
| 1872 |
for (n = 0; n < nposts; n++) { |
| 1873 |
if (sorted_id_map[n].time >= oldest) |
| 1874 |
continue; |
| 1875 |
|
| 1876 |
if (board->ftn_area[0]) { |
| 1877 |
size = bile_read_alloc(board->bile, BOARD_FTN_POST_RTYPE, |
| 1878 |
sorted_id_map[n].id, &data); |
| 1879 |
if (!size) |
| 1880 |
continue; |
| 1881 |
ret = bile_unmarshall_object(board->bile, |
| 1882 |
board_ftn_post_object_fields, nboard_ftn_post_object_fields, |
| 1883 |
data, size, &fpost, sizeof(fpost), false); |
| 1884 |
xfree(&data); |
| 1885 |
if (ret == BILE_ERR_NO_MEMORY) |
| 1886 |
goto done; |
| 1887 |
|
| 1888 |
board_delete_ftn_post(board, &fpost); |
| 1889 |
} else { |
| 1890 |
size = bile_read_alloc(board->bile, BOARD_POST_RTYPE, |
| 1891 |
sorted_id_map[n].id, &data); |
| 1892 |
if (!size) |
| 1893 |
continue; |
| 1894 |
ret = bile_unmarshall_object(board->bile, |
| 1895 |
board_post_object_fields, nboard_post_object_fields, |
| 1896 |
data, size, &post, sizeof(post), false); |
| 1897 |
xfree(&data); |
| 1898 |
if (ret == BILE_ERR_NO_MEMORY) |
| 1899 |
goto done; |
| 1900 |
|
| 1901 |
size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, |
| 1902 |
post.thread_id, &data); |
| 1903 |
if (!size) |
| 1904 |
continue; |
| 1905 |
ret = bile_unmarshall_object(board->bile, |
| 1906 |
board_thread_object_fields, nboard_thread_object_fields, |
| 1907 |
data, size, &thread, sizeof(thread), false); |
| 1908 |
xfree(&data); |
| 1909 |
if (ret == BILE_ERR_NO_MEMORY) |
| 1910 |
goto done; |
| 1911 |
|
| 1912 |
board_delete_post(board, &post, &thread); |
| 1913 |
} |
| 1914 |
|
| 1915 |
deleted++; |
| 1916 |
uthread_yield(); |
| 1917 |
} |
| 1918 |
|
| 1919 |
done: |
| 1920 |
if (sorted_id_map != NULL) |
| 1921 |
xfree(&sorted_id_map); |
| 1922 |
|
| 1923 |
if (deleted) { |
| 1924 |
board_index_sorted_post_ids(board, NULL); |
| 1925 |
|
| 1926 |
logger_printf("[board] Pruned %lu post%s in %s older than %d day%s", |
| 1927 |
deleted, deleted == 1 ? "" : "s", board->name, |
| 1928 |
board->retention_days, board->retention_days == 1 ? "" : "s"); |
| 1929 |
} |
| 1930 |
} |