Download
jcs
/subtext
/board.c
(View History)
jcs board: For new FTN messages posted locally, just toss them | Latest amendment: 560 on 2023-11-21 |
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 | snprintf(prompt, sizeof(prompt), "%s:%s", prompt_prefix, board->name); |
422 | |
423 | while (!done && !s->ending) { |
424 | if (find_post_ids) { |
425 | if (post_ids != NULL) { |
426 | xfree(&post_ids); |
427 | post_ids = NULL; |
428 | } |
429 | ppp = POSTS_PER_PAGE; |
430 | if (s->terminal_lines < ppp + 3) |
431 | ppp = BOUND(ppp, 5, s->terminal_lines - 3); |
432 | nall_post_ids = board_find_post_ids(s, board, &npost_ids, |
433 | &post_ids, page * ppp, ppp); |
434 | /* ceil(nall_post_ids / ppp) */ |
435 | pages = (nall_post_ids + ppp - 1) / ppp; |
436 | |
437 | if (page >= pages) |
438 | page = pages - 1; |
439 | |
440 | find_post_ids = false; |
441 | } |
442 | |
443 | if (show_list) { |
444 | board_list_posts(s, board, npost_ids, post_ids, page + 1, |
445 | pages); |
446 | show_list = false; |
447 | } |
448 | |
449 | if (next_c) { |
450 | c = next_c; |
451 | next_c = 0; |
452 | } else { |
453 | c = session_menu(s, board->description, prompt, |
454 | (char *)prompt_help, opts, nitems(opts), show_help, "Post #", |
455 | &pn); |
456 | show_help = false; |
457 | } |
458 | |
459 | handle_opt: |
460 | switch (c) { |
461 | case 'l': |
462 | show_list = true; |
463 | break; |
464 | case 'p': |
465 | if (board_compose(s, board, NULL, NULL, NULL, NULL, NULL)) { |
466 | find_post_ids = true; |
467 | show_list = true; |
468 | } |
469 | break; |
470 | case '>': |
471 | case '<': |
472 | if (c == '>' && page == pages - 1) { |
473 | session_printf(s, "You are at the last page of posts\r\n"); |
474 | session_flush(s); |
475 | break; |
476 | } |
477 | if (c == '<' && page == 0) { |
478 | session_printf(s, "You are already at the first page\r\n"); |
479 | session_flush(s); |
480 | break; |
481 | } |
482 | if (c == '>') |
483 | page++; |
484 | else |
485 | page--; |
486 | find_post_ids = true; |
487 | show_list = true; |
488 | break; |
489 | case '#': |
490 | check_pn: |
491 | if (pn < 1 || pn > npost_ids) { |
492 | session_printf(s, "Invalid post\r\n"); |
493 | session_flush(s); |
494 | break; |
495 | } |
496 | ret = board_post_read(s, board, prompt, post_ids[pn - 1], pn); |
497 | switch (ret) { |
498 | case POST_READ_RETURN_DONE: |
499 | break; |
500 | case POST_READ_RETURN_LIST: |
501 | show_list = true; |
502 | break; |
503 | case POST_READ_RETURN_FIND: |
504 | find_post_ids = true; |
505 | show_list = true; |
506 | break; |
507 | case POST_READ_RETURN_NEWER: |
508 | if (pn == 1) { |
509 | if (page == 0) { |
510 | session_printf(s, "No newer posts.\r\n"); |
511 | session_flush(s); |
512 | break; |
513 | } else { |
514 | page--; |
515 | find_post_ids = true; |
516 | pn = npost_ids; |
517 | next_c = '#'; |
518 | } |
519 | } else { |
520 | pn--; |
521 | goto check_pn; |
522 | } |
523 | break; |
524 | case POST_READ_RETURN_OLDER: |
525 | if (pn == npost_ids) { |
526 | if (page == pages - 1) { |
527 | session_printf(s, "No more posts.\r\n"); |
528 | session_flush(s); |
529 | break; |
530 | } else { |
531 | page++; |
532 | find_post_ids = true; |
533 | pn = 1; |
534 | next_c = '#'; |
535 | } |
536 | } else { |
537 | pn++; |
538 | goto check_pn; |
539 | } |
540 | break; |
541 | default: |
542 | c = ret; |
543 | goto handle_opt; |
544 | } |
545 | break; |
546 | case '?': |
547 | show_help = true; |
548 | break; |
549 | default: |
550 | done = true; |
551 | break; |
552 | } |
553 | } |
554 | |
555 | if (post_ids != NULL) |
556 | xfree(&post_ids); |
557 | } |
558 | |
559 | void |
560 | board_list_posts(struct session *s, struct board *board, size_t npost_ids, |
561 | unsigned long *post_ids, unsigned long page, unsigned long pages) |
562 | { |
563 | char time[24]; |
564 | unsigned long indent_parent_ids[10] = { 0 }; |
565 | char indent_s[22]; |
566 | size_t n, size; |
567 | struct username_cache *user; |
568 | struct board_thread thread = { 0 }; |
569 | struct board_post post; |
570 | struct board_ftn_post fpost; |
571 | short indent, j, k, ret; |
572 | char *data; |
573 | |
574 | session_printf(s, "{{B}}%s: %s (Page %ld of %ld){{/B}}\r\n", |
575 | board->name, board->description, page, pages); |
576 | session_printf(s, "%s # N Date From Subject%s\r\n", |
577 | ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); |
578 | session_flush(s); |
579 | |
580 | if (npost_ids == 0) { |
581 | session_printf(s, "No posts here yet.\r\n"); |
582 | session_flush(s); |
583 | return; |
584 | } |
585 | |
586 | for (n = 0; n < npost_ids; n++) { |
587 | if (board->ftn_area[0]) { |
588 | size = bile_read_alloc(board->bile, BOARD_FTN_POST_RTYPE, |
589 | post_ids[n], &data); |
590 | ret = bile_unmarshall_object(board->bile, |
591 | board_ftn_post_object_fields, nboard_ftn_post_object_fields, |
592 | data, size, &fpost, sizeof(fpost), false); |
593 | xfree(&data); |
594 | if (ret == BILE_ERR_NO_MEMORY) |
595 | break; |
596 | |
597 | strftime(time, sizeof(time), "%b %d", localtime(&fpost.time)); |
598 | |
599 | session_printf(s, "%s%2ld %c %s {{#}}%-10.10s %s%.50s%s\r\n", |
600 | true ? "" : ansi(s, ANSI_BOLD, ANSI_END), |
601 | n + 1, |
602 | true ? ' ' : 'N', |
603 | time, |
604 | fpost.from, |
605 | (fpost.reply[0] && strncasecmp(fpost.subject, "Re:", 3) != 0 ? |
606 | "Re: " : ""), |
607 | fpost.subject, |
608 | true ? "" : ansi(s, ANSI_RESET, ANSI_END)); |
609 | } else { |
610 | size = bile_read_alloc(board->bile, BOARD_POST_RTYPE, |
611 | post_ids[n], &data); |
612 | ret = bile_unmarshall_object(board->bile, |
613 | board_post_object_fields, nboard_post_object_fields, data, |
614 | size, &post, sizeof(post), false); |
615 | xfree(&data); |
616 | if (ret != 0) |
617 | break; |
618 | |
619 | if (post.thread_id != thread.thread_id) { |
620 | if (thread.thread_id) { |
621 | if (thread.subject != NULL) |
622 | xfree(&thread.subject); |
623 | if (thread.post_ids != NULL) |
624 | xfree(&thread.post_ids); |
625 | if (thread.parent_post_ids != NULL) |
626 | xfree(&thread.parent_post_ids); |
627 | } |
628 | size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, |
629 | post.thread_id, &data); |
630 | ret = bile_unmarshall_object(board->bile, |
631 | board_thread_object_fields, nboard_thread_object_fields, |
632 | data, size, &thread, sizeof(thread), true); |
633 | xfree(&data); |
634 | if (ret == BILE_ERR_NO_MEMORY) |
635 | break; |
636 | |
637 | for (j = 0; j < nitems(indent_parent_ids); j++) |
638 | indent_parent_ids[j] = 0; |
639 | } |
640 | |
641 | user = user_username(post.sender_user_id); |
642 | strftime(time, sizeof(time), "%b %d", localtime(&post.time)); |
643 | |
644 | if (post.parent_post_id == 0) { |
645 | indent_s[0] = '\0'; |
646 | } else { |
647 | indent = -1; |
648 | for (j = 0; j < nitems(indent_parent_ids); j++) { |
649 | if (indent_parent_ids[j] == post.parent_post_id || |
650 | indent_parent_ids[j] == 0) { |
651 | indent = j; |
652 | indent_parent_ids[j] = post.parent_post_id; |
653 | for (k = j + 1; k < nitems(indent_parent_ids); |
654 | k++) { |
655 | if (indent_parent_ids[k] == 0) |
656 | break; |
657 | indent_parent_ids[k] = 0; |
658 | } |
659 | break; |
660 | } |
661 | } |
662 | if (indent == -1) |
663 | indent = nitems(indent_parent_ids) - 1; |
664 | |
665 | for (j = 0; j < indent; j++) { |
666 | indent_s[j] = ' '; |
667 | } |
668 | indent_s[j] = '`'; |
669 | indent_s[j + 1] = '-'; |
670 | indent_s[j + 2] = '>'; |
671 | indent_s[j + 3] = '\0'; |
672 | } |
673 | session_printf(s, "%s%2ld %c %s %-10.10s %s{{#}}%.40s%s\r\n", |
674 | true ? "" : ansi(s, ANSI_BOLD, ANSI_END), |
675 | n + 1, |
676 | true ? ' ' : 'N', |
677 | time, |
678 | user ? user->username : "(unknown)", |
679 | post.parent_post_id != 0 && n == 0 ? "Re: " : "", |
680 | post.parent_post_id == 0 || n == 0 ? thread.subject : indent_s, |
681 | true ? "" : ansi(s, ANSI_RESET, ANSI_END)); |
682 | } |
683 | } |
684 | session_flush(s); |
685 | |
686 | if (thread.subject != NULL) |
687 | xfree(&thread.subject); |
688 | if (thread.post_ids != NULL) |
689 | xfree(&thread.post_ids); |
690 | if (thread.parent_post_ids != NULL) |
691 | xfree(&thread.parent_post_ids); |
692 | } |
693 | |
694 | unsigned long |
695 | board_compose(struct session *s, struct board *board, |
696 | struct board_thread *thread, struct board_post *parent_post, |
697 | struct board_ftn_post *ftn_parent_post, char *initial_subject, |
698 | char *initial_body) |
699 | { |
700 | struct board_post post = { 0 }; |
701 | char *tmp = NULL; |
702 | short c; |
703 | |
704 | if (!s->user) { |
705 | session_printf(s, "Posting is not available to guests.\r\n" |
706 | "Please create an account first.\r\n"); |
707 | session_flush(s); |
708 | return 0; |
709 | } |
710 | |
711 | if (board->restricted_posting && !s->user->is_sysop) { |
712 | session_printf(s, "Posting to this board is not permitted.\r\n"); |
713 | session_flush(s); |
714 | return 0; |
715 | } |
716 | |
717 | if (initial_body) { |
718 | post.body = xstrdup(initial_body); |
719 | if (post.body == NULL) |
720 | return 0; |
721 | } |
722 | |
723 | if (thread) { |
724 | post.thread_id = thread->thread_id; |
725 | post.parent_post_id = parent_post->id; |
726 | } else { |
727 | thread = xmalloczero(sizeof(struct board_thread)); |
728 | if (thread == NULL) |
729 | return 0; |
730 | } |
731 | post.sender_user_id = s->user->id; |
732 | strlcpy(post.via, s->via, sizeof(post.via)); |
733 | |
734 | session_printf(s, "{{B}}Compose %s{{/B}}\r\n", |
735 | parent_post ? "Reply" : "New Post"); |
736 | session_printf(s, "{{B}}From:{{/B}} %s (via %s)\r\n", |
737 | s->user->username, s->via); |
738 | session_printf(s, "{{B}}To:{{/B}} %s\r\n", board->name); |
739 | |
740 | post_compose_start: |
741 | if (parent_post) { |
742 | session_printf(s, "{{B}}Subject:{{/B}}{{#}} Re: %s\r\n", |
743 | thread->subject); |
744 | session_flush(s); |
745 | } else if (ftn_parent_post) { |
746 | session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s%s\r\n", |
747 | strncmp("Re:", ftn_parent_post->subject, 3) == 1 ? |
748 | "" : "Re: ", ftn_parent_post->subject); |
749 | session_flush(s); |
750 | } else { |
751 | if (initial_subject && !thread->subject) { |
752 | thread->subject = xstrdup(initial_subject); |
753 | if (thread->subject == NULL) |
754 | return 0; |
755 | } |
756 | |
757 | for (;;) { |
758 | session_printf(s, "{{B}}Subject:{{/B}} "); |
759 | session_flush(s); |
760 | |
761 | tmp = session_field_input(s, 100, 50, thread->subject, |
762 | false, 0); |
763 | if (thread->subject != NULL) |
764 | xfree(&thread->subject); |
765 | thread->subject = tmp; |
766 | session_output(s, "\r\n", 2); |
767 | session_flush(s); |
768 | |
769 | if (thread->subject == NULL) |
770 | goto post_compose_done; |
771 | |
772 | rtrim(thread->subject, "\r\n\t "); |
773 | |
774 | if (thread->subject[0] == '\0') { |
775 | session_printf(s, "{{B}}Error:{{/B}} Subject " |
776 | "cannot be blank (^C to cancel)\r\n"); |
777 | session_flush(s); |
778 | xfree(&thread->subject); |
779 | continue; |
780 | } |
781 | thread->subject_size = strlen(thread->subject) + 1; |
782 | break; |
783 | } |
784 | } |
785 | |
786 | for (;;) { |
787 | session_printf(s, "{{B}}Message (^D when finished):{{/B}}\r\n"); |
788 | session_flush(s); |
789 | |
790 | tmp = session_field_input(s, 2048, s->terminal_columns - 1, |
791 | post.body, true, 0); |
792 | if (post.body != NULL) |
793 | xfree(&post.body); |
794 | post.body = tmp; |
795 | session_output(s, "\r\n", 2); |
796 | session_flush(s); |
797 | |
798 | if (post.body == NULL) |
799 | goto post_compose_done; |
800 | |
801 | rtrim(post.body, "\r\n\t "); |
802 | |
803 | if (post.body[0] == '\0') { |
804 | xfree(&post.body); |
805 | goto post_compose_done; |
806 | } |
807 | post.body_size = strlen(post.body) + 1; |
808 | break; |
809 | } |
810 | |
811 | for (;;) { |
812 | session_printf(s, "\r\n{{B}}(P){{/B}}ost message, " |
813 | "{{B}}(E){{/B}}dit again, or {{B}}(C){{/B}}ancel? "); |
814 | session_flush(s); |
815 | |
816 | c = session_input_char(s); |
817 | if (c == 0 || s->ending) |
818 | goto post_compose_done; |
819 | |
820 | switch (c) { |
821 | case 'p': |
822 | case 'P': |
823 | case 'y': |
824 | session_printf(s, "%c\r\n", c); |
825 | session_flush(s); |
826 | /* FALLTHROUGH */ |
827 | case '\n': |
828 | case '\r': |
829 | /* send */ |
830 | session_printf(s, "Posting message... "); |
831 | session_flush(s); |
832 | |
833 | if (board_post_create(board, thread, ftn_parent_post, |
834 | &post) == 0) { |
835 | session_logf(s, "Posted message %ld to %s", post.id, |
836 | board->name); |
837 | session_printf(s, "done\r\n"); |
838 | } else |
839 | session_printf(s, "failed!\r\n"); |
840 | |
841 | session_flush(s); |
842 | |
843 | goto post_compose_done; |
844 | case 'e': |
845 | case 'E': |
846 | session_printf(s, "%c\r\n", c); |
847 | session_flush(s); |
848 | goto post_compose_start; |
849 | case 'c': |
850 | case 'C': |
851 | session_printf(s, "%c\r\n", c); |
852 | session_flush(s); |
853 | /* FALLTHROUGH */ |
854 | case CONTROL_C: |
855 | goto post_compose_done; |
856 | } |
857 | } |
858 | |
859 | post_compose_error: |
860 | session_printf(s, "Failed saving message!\r\n"); |
861 | session_flush(s); |
862 | |
863 | post_compose_done: |
864 | if (parent_post == NULL) { |
865 | if (thread->subject != NULL) |
866 | xfree(&thread->subject); |
867 | xfree(&thread); |
868 | } |
869 | if (post.body) |
870 | xfree(&post.body); |
871 | |
872 | return post.id; |
873 | } |
874 | |
875 | short |
876 | board_post_read(struct session *s, struct board *board, char *prompt_prefix, |
877 | unsigned long id, short idx) |
878 | { |
879 | static const struct session_menu_option opts[] = { |
880 | { '<', "<Nn", "Read newer post" }, |
881 | { '>', ">Pp", "Read older post" }, |
882 | { 'r', "Rr", "Reply to this post" }, |
883 | { 's', "Ss", "Show this post" }, |
884 | { 'd', "Dd", "Delete this post" }, |
885 | { 'l', "Ll", "List posts" }, |
886 | { 'q', "QqXx", "Return to threads" }, |
887 | { '?', "?", "List these options" }, |
888 | }; |
889 | static const char prompt_help[] = |
890 | "<:Newer >:Older R:Reply S:Show D:Delete L:List Q:Return ?:Help"; |
891 | char time[32], prompt[7 + member_size(struct board, name) + 8]; |
892 | struct board_thread thread; |
893 | struct board_post post; |
894 | struct board_ftn_post fpost; |
895 | struct username_cache *sender; |
896 | struct session_menu_option *dopts = NULL; |
897 | size_t size, plain_post_size, j; |
898 | short ret = POST_READ_RETURN_DONE; |
899 | char c; |
900 | char *data, *subject, *plain_post, *tplain_post; |
901 | short cc, bcret, n; |
902 | bool done = false, show_help = false; |
903 | |
904 | dopts = xmalloc(sizeof(opts)); |
905 | if (dopts == NULL) |
906 | return 0; |
907 | memcpy(dopts, opts, sizeof(opts)); |
908 | |
909 | show_post: |
910 | if (board->ftn_area[0]) { |
911 | size = bile_read_alloc(board->bile, BOARD_FTN_POST_RTYPE, id, |
912 | &data); |
913 | if (size == 0) |
914 | panic("failed fetching message %ld: %d", id, |
915 | bile_error(board->bile)); |
916 | |
917 | ret = bile_unmarshall_object(board->bile, |
918 | board_ftn_post_object_fields, nboard_ftn_post_object_fields, |
919 | data, size, &fpost, sizeof(fpost), true); |
920 | xfree(&data); |
921 | if (ret == BILE_ERR_NO_MEMORY) |
922 | goto done_reading; |
923 | |
924 | if (!(s->user && s->user->is_sysop)) { |
925 | /* disable deleting */ |
926 | for (n = 0; n < nitems(opts); n++) { |
927 | if (dopts[n].ret == 'd') { |
928 | dopts[n].key[0] = '\0'; |
929 | break; |
930 | } |
931 | } |
932 | } |
933 | |
934 | strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S", |
935 | localtime(&fpost.time)); |
936 | |
937 | session_printf(s, "{{B}}From:{{/B}}{{#}} %s\r\n", fpost.from); |
938 | session_printf(s, "{{B}}Origin:{{/B}}{{#}} %s\r\n", fpost.origin); |
939 | session_printf(s, "{{B}}To:{{/B}}{{#}} %s@%s\r\n", fpost.to, |
940 | board->name); |
941 | session_printf(s, "{{B}}Date:{{/B}}{{#}} %s %s\r\n", time, |
942 | db->config.timezone); |
943 | session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s%s\r\n", |
944 | (fpost.reply[0] && strncasecmp(fpost.subject, "Re:", 3) != 0 ? |
945 | "Re: " : ""), fpost.subject); |
946 | session_printf(s, "\r\n"); |
947 | session_flush(s); |
948 | |
949 | plain_post_size = 0; |
950 | plain_post = xmalloc(fpost.body_size); |
951 | if (plain_post == NULL) |
952 | session_paginate(s, fpost.body, fpost.body_size, 6); |
953 | else { |
954 | /* strip out renegade-style pipe color codes ("abc|01def") */ |
955 | for (j = 0; j < fpost.body_size; j++) { |
956 | if (fpost.body[j] == '|' && |
957 | fpost.body[j + 1] >= '0' && fpost.body[j + 1] <= '9' && |
958 | fpost.body[j + 2] >= '0' && fpost.body[j + 2] <= '9') { |
959 | j += 2; |
960 | continue; |
961 | } |
962 | plain_post[plain_post_size++] = fpost.body[j]; |
963 | } |
964 | tplain_post = xrealloc(plain_post, plain_post_size); |
965 | if (tplain_post != NULL) |
966 | plain_post = tplain_post; |
967 | session_paginate(s, plain_post, plain_post_size, 6); |
968 | xfree(&plain_post); |
969 | } |
970 | } else { |
971 | size = bile_read_alloc(board->bile, BOARD_POST_RTYPE, id, &data); |
972 | if (size == 0) |
973 | panic("failed fetching message %ld: %d", id, |
974 | bile_error(board->bile)); |
975 | ret = bile_unmarshall_object(board->bile, board_post_object_fields, |
976 | nboard_post_object_fields, data, size, &post, sizeof(post), true); |
977 | xfree(&data); |
978 | if (ret == BILE_ERR_NO_MEMORY) |
979 | goto done_reading; |
980 | |
981 | size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, |
982 | post.thread_id, &data); |
983 | if (size == 0) |
984 | panic("failed fetching thread %ld: %d", post.thread_id, |
985 | bile_error(board->bile)); |
986 | ret = bile_unmarshall_object(board->bile, board_thread_object_fields, |
987 | nboard_thread_object_fields, data, size, &thread, |
988 | sizeof(thread), true); |
989 | xfree(&data); |
990 | if (ret == BILE_ERR_NO_MEMORY) |
991 | goto done_reading; |
992 | |
993 | if (!(s->user && (s->user->is_sysop || |
994 | s->user->id == post.sender_user_id))) { |
995 | /* disable deleting */ |
996 | for (n = 0; n < nitems(opts); n++) { |
997 | if (dopts[n].ret == 'd') { |
998 | dopts[n].key[0] = '\0'; |
999 | break; |
1000 | } |
1001 | } |
1002 | } |
1003 | |
1004 | sender = user_username(post.sender_user_id); |
1005 | |
1006 | strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S", |
1007 | localtime(&post.time)); |
1008 | |
1009 | session_printf(s, "{{B}}From:{{/B}}{{#}} %s", |
1010 | sender ? sender->username : "(unknown)"); |
1011 | if (post.via[0]) |
1012 | session_printf(s, " (via %s)", post.via); |
1013 | session_printf(s, "\r\n"); |
1014 | session_printf(s, "{{B}}To:{{/B}}{{#}} %s\r\n", board->name); |
1015 | session_printf(s, "{{B}}Date:{{/B}}{{#}} %s %s\r\n", time, |
1016 | db->config.timezone); |
1017 | session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s%s\r\n", |
1018 | (post.parent_post_id ? "Re: " : ""), thread.subject); |
1019 | session_printf(s, "\r\n"); |
1020 | session_flush(s); |
1021 | session_paginate(s, post.body, post.body_size, 5); |
1022 | } |
1023 | |
1024 | snprintf(prompt, sizeof(prompt), "%s:%d", prompt_prefix, idx); |
1025 | |
1026 | if (board->ftn_area[0]) |
1027 | subject = fpost.subject; |
1028 | else |
1029 | subject = thread.subject; |
1030 | |
1031 | while (!done && !s->ending) { |
1032 | c = session_menu(s, subject, prompt, (char *)prompt_help, dopts, |
1033 | nitems(opts), show_help, NULL, NULL); |
1034 | show_help = false; |
1035 | |
1036 | switch (c) { |
1037 | case 'd': |
1038 | if (!(s->user && (s->user->is_sysop || |
1039 | s->user->id == post.sender_user_id))) { |
1040 | session_printf(s, "Invalid option\r\n"); |
1041 | session_flush(s); |
1042 | break; |
1043 | } |
1044 | |
1045 | session_printf(s, "Are you sure you want to permanently " |
1046 | "delete this post? [y/N] "); |
1047 | session_flush(s); |
1048 | |
1049 | cc = session_input_char(s); |
1050 | if (cc == 'y' || c == 'Y') { |
1051 | session_printf(s, "%c\r\n", cc); |
1052 | session_flush(s); |
1053 | |
1054 | if (board->ftn_area[0]) { |
1055 | board_delete_ftn_post(board, &fpost); |
1056 | session_logf(s, "Deleted %s post %ld", |
1057 | db->config.ftn_network, fpost.id); |
1058 | } else { |
1059 | board_delete_post(board, &post, &thread); |
1060 | session_logf(s, "Deleted post %ld (thread %ld)", |
1061 | post.id, thread.thread_id); |
1062 | } |
1063 | |
1064 | session_printf(s, "\r\n{{B}}Post deleted!{{/B}}\r\n"); |
1065 | session_flush(s); |
1066 | ret = POST_READ_RETURN_FIND; |
1067 | done = true; |
1068 | } else { |
1069 | session_printf(s, "\r\nPost not deleted.\r\n"); |
1070 | session_flush(s); |
1071 | } |
1072 | break; |
1073 | case 'r': |
1074 | if (board->ftn_area[0]) |
1075 | bcret = board_compose(s, board, NULL, NULL, &fpost, NULL, |
1076 | NULL); |
1077 | else |
1078 | bcret = board_compose(s, board, &thread, &post, NULL, NULL, |
1079 | NULL); |
1080 | |
1081 | if (bcret) { |
1082 | ret = POST_READ_RETURN_FIND; |
1083 | done = true; |
1084 | } |
1085 | break; |
1086 | case 's': |
1087 | goto show_post; |
1088 | break; |
1089 | case '<': |
1090 | ret = POST_READ_RETURN_NEWER; |
1091 | done = true; |
1092 | break; |
1093 | case '>': |
1094 | ret = POST_READ_RETURN_OLDER; |
1095 | done = true; |
1096 | break; |
1097 | case 'l': |
1098 | ret = POST_READ_RETURN_LIST; |
1099 | done = true; |
1100 | break; |
1101 | case 'q': |
1102 | ret = POST_READ_RETURN_DONE; |
1103 | done = true; |
1104 | break; |
1105 | case '?': |
1106 | show_help = true; |
1107 | break; |
1108 | } |
1109 | } |
1110 | |
1111 | done_reading: |
1112 | xfree(&dopts); |
1113 | |
1114 | if (board->ftn_area[0]) { |
1115 | if (fpost.body != NULL) |
1116 | xfree(&fpost.body); |
1117 | } else { |
1118 | if (post.body != NULL) |
1119 | xfree(&post.body); |
1120 | if (thread.subject != NULL) |
1121 | xfree(&thread.subject); |
1122 | if (thread.post_ids != NULL) |
1123 | xfree(&thread.post_ids); |
1124 | if (thread.parent_post_ids != NULL) |
1125 | xfree(&thread.parent_post_ids); |
1126 | } |
1127 | |
1128 | return ret; |
1129 | } |
1130 | |
1131 | size_t |
1132 | board_find_post_ids(struct session *s, struct board *board, |
1133 | size_t *npost_ids, unsigned long **post_ids, size_t offset, size_t limit) |
1134 | { |
1135 | struct board_id_time_map *all_post_id_map; |
1136 | size_t n, size, nall_post_ids; |
1137 | |
1138 | *post_ids = NULL; |
1139 | *npost_ids = 0; |
1140 | |
1141 | size = bile_read_alloc(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, |
1142 | &all_post_id_map); |
1143 | if (all_post_id_map == NULL) { |
1144 | session_printf(s, "%sPlease wait, re-indexing board posts...%s", |
1145 | ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); |
1146 | session_flush(s); |
1147 | nall_post_ids = board_index_sorted_post_ids(board, |
1148 | &all_post_id_map); |
1149 | session_output(s, "\r\n", 2); |
1150 | session_flush(s); |
1151 | if (nall_post_ids == 0) |
1152 | return 0; |
1153 | } else |
1154 | nall_post_ids = size / sizeof(struct board_id_time_map); |
1155 | |
1156 | *post_ids = xcalloc(sizeof(long), MIN(limit, nall_post_ids)); |
1157 | if (*post_ids == NULL) |
1158 | return 0; |
1159 | |
1160 | for (n = 0; n < nall_post_ids; n++) { |
1161 | if (n < offset) |
1162 | continue; |
1163 | |
1164 | (*post_ids)[*npost_ids] = all_post_id_map[n].id; |
1165 | (*npost_ids)++; |
1166 | |
1167 | if (*npost_ids >= limit) |
1168 | break; |
1169 | } |
1170 | |
1171 | if (all_post_id_map != NULL) |
1172 | xfree(&all_post_id_map); |
1173 | |
1174 | return nall_post_ids; |
1175 | } |
1176 | |
1177 | short |
1178 | board_post_create(struct board *board, struct board_thread *thread, |
1179 | struct board_ftn_post *ftn_parent_post, struct board_post *post) |
1180 | { |
1181 | struct board_ftn_post ftn_post = { 0 }; |
1182 | struct fidopkt_message fidomsg = { 0 }; |
1183 | struct username_cache *user; |
1184 | struct fidopkt_address our_address, hub_address; |
1185 | struct bile_object *o; |
1186 | short ret; |
1187 | char *data; |
1188 | size_t size, insert, npost_ids; |
1189 | ssize_t n, j; |
1190 | unsigned long *post_ids, *parent_post_ids; |
1191 | struct board_id_time_map *id_map; |
1192 | |
1193 | if (board->ftn_area[0]) { |
1194 | if (!post->id) |
1195 | post->id = bile_next_id(board->bile, BOARD_FTN_POST_RTYPE); |
1196 | if (!post->time) |
1197 | post->time = Time; |
1198 | |
1199 | if (!fidopkt_parse_address(db->config.ftn_node_addr, |
1200 | &our_address)) { |
1201 | logger_printf("[board] Invalid FTN local node address, can't " |
1202 | "create board post"); |
1203 | post->id = 0; |
1204 | goto done; |
1205 | } |
1206 | if (!fidopkt_parse_address(db->config.ftn_hub_addr, |
1207 | &hub_address)) { |
1208 | logger_printf("[board] Invalid FTN hub address, can't " |
1209 | "create board post"); |
1210 | post->id = 0; |
1211 | goto done; |
1212 | } |
1213 | |
1214 | ftn_post.id = post->id; |
1215 | ftn_post.time = post->time; |
1216 | |
1217 | if (ftn_parent_post) { |
1218 | snprintf(ftn_post.subject, sizeof(ftn_post.subject), |
1219 | "%s%s", |
1220 | strncmp("Re:", ftn_parent_post->subject, 3) == 1 ? |
1221 | "" : "Re: ", |
1222 | ftn_parent_post->subject); |
1223 | strlcpy(ftn_post.reply, ftn_parent_post->msgid_orig, |
1224 | sizeof(ftn_post.reply)); |
1225 | strlcpy(ftn_post.to, ftn_parent_post->from, |
1226 | sizeof(ftn_post.to)); |
1227 | } else { |
1228 | strlcpy(ftn_post.subject, thread->subject, |
1229 | sizeof(ftn_post.subject)); |
1230 | strlcpy(ftn_post.to, "All", sizeof(ftn_post.to)); |
1231 | } |
1232 | |
1233 | user = user_username(post->sender_user_id); |
1234 | if (user == NULL) { |
1235 | logger_printf("[board] Can't find username of user posting " |
1236 | "new message"); |
1237 | post->id = 0; |
1238 | goto done; |
1239 | } |
1240 | strlcpy(ftn_post.from, user->username, sizeof(ftn_post.from)); |
1241 | |
1242 | ftn_post.body = post->body; |
1243 | ftn_post.body_size = post->body_size; |
1244 | |
1245 | /* make each board's posts have ids unique to our zone/net/node */ |
1246 | ftn_post.msgid.id = 0x10000000 | |
1247 | ((unsigned long)our_address.node << 24) | |
1248 | ((unsigned long)(board->id) << 16) | post->id; |
1249 | ftn_post.msgid.zone = our_address.zone; |
1250 | ftn_post.msgid.net = our_address.net; |
1251 | ftn_post.msgid.node = our_address.node; |
1252 | ftn_post.msgid.point = our_address.point; |
1253 | |
1254 | snprintf(ftn_post.origin, sizeof(ftn_post.origin), |
1255 | "%s | %s (%s)", |
1256 | db->config.name, db->config.hostname, |
1257 | db->config.ftn_node_addr); |
1258 | |
1259 | fidomsg.time = ftn_post.time; |
1260 | memcpy(&fidomsg.header.orig, &our_address, |
1261 | sizeof(fidomsg.header.orig)); |
1262 | memcpy(&fidomsg.header.dest, &hub_address, |
1263 | sizeof(fidomsg.header.dest)); |
1264 | strlcpy(fidomsg.area, board->ftn_area, sizeof(fidomsg.area)); |
1265 | strlcpy(fidomsg.to, ftn_post.to, sizeof(fidomsg.to)); |
1266 | strlcpy(fidomsg.from, ftn_post.from, sizeof(fidomsg.from)); |
1267 | strlcpy(fidomsg.subject, ftn_post.subject, sizeof(fidomsg.subject)); |
1268 | fidomsg.body = ftn_post.body; |
1269 | fidomsg.body_len = ftn_post.body_size - 1; |
1270 | strlcpy(fidomsg.reply, ftn_post.reply, sizeof(fidomsg.reply)); |
1271 | memcpy(&fidomsg.msgid, &ftn_post.msgid, sizeof(fidomsg.msgid)); |
1272 | strlcpy(fidomsg.origin, ftn_post.origin, sizeof(fidomsg.origin)); |
1273 | |
1274 | if (!binkp_scan_message(&fidomsg)) { |
1275 | logger_printf("[board] Failed scanning new FTN message being " |
1276 | "posted"); |
1277 | post->id = 0; |
1278 | goto done; |
1279 | } |
1280 | |
1281 | if (board_toss_ftn_message(board, &fidomsg, true) != 1) { |
1282 | logger_printf("[board] Failed tossing new FTN message being " |
1283 | "posted"); |
1284 | post->id = 0; |
1285 | goto done; |
1286 | } |
1287 | } else { |
1288 | if (!post->id) |
1289 | post->id = bile_next_id(board->bile, BOARD_POST_RTYPE); |
1290 | if (!post->time) |
1291 | post->time = Time; |
1292 | |
1293 | if (post->parent_post_id == 0) { |
1294 | thread->thread_id = bile_next_id(board->bile, |
1295 | BOARD_THREAD_RTYPE); |
1296 | post->thread_id = thread->thread_id; |
1297 | } |
1298 | |
1299 | ret = bile_marshall_object(board->bile, board_post_object_fields, |
1300 | nboard_post_object_fields, post, &data, &size); |
1301 | if (ret != 0 || size == 0) { |
1302 | logger_printf("[board] Failed to marshall new post object"); |
1303 | post->id = 0; |
1304 | goto done; |
1305 | } |
1306 | if (bile_write(board->bile, BOARD_POST_RTYPE, post->id, data, |
1307 | size) != size) { |
1308 | warn("bile_write of new post failed! %d", |
1309 | bile_error(board->bile)); |
1310 | post->id = 0; |
1311 | xfree(&data); |
1312 | goto done; |
1313 | } |
1314 | xfree(&data); |
1315 | |
1316 | if (post->time > thread->last_post_at) |
1317 | thread->last_post_at = post->time; |
1318 | thread->nposts++; |
1319 | post_ids = xreallocarray(thread->post_ids, thread->nposts, |
1320 | sizeof(long)); |
1321 | if (post_ids == NULL) |
1322 | return 0; |
1323 | thread->post_ids = post_ids; |
1324 | parent_post_ids = xreallocarray(thread->parent_post_ids, |
1325 | thread->nposts, sizeof(long)); |
1326 | if (parent_post_ids == NULL) |
1327 | return 0; |
1328 | thread->parent_post_ids = parent_post_ids; |
1329 | |
1330 | /* |
1331 | * Add new post id to thread post_ids, but put it in the right |
1332 | * place so that reading post_ids will present the tree in order. |
1333 | * Walk parent_post_ids and find the first match of our parent, |
1334 | * then insert our new post there. This puts newest replies at |
1335 | * the top. |
1336 | */ |
1337 | |
1338 | insert = thread->nposts - 1; |
1339 | for (n = 0; n < thread->nposts - 1; n++) { |
1340 | if (thread->post_ids[n] != post->parent_post_id) |
1341 | continue; |
1342 | |
1343 | for (j = thread->nposts - 2; j > n; j--) { |
1344 | thread->post_ids[j + 1] = thread->post_ids[j]; |
1345 | thread->parent_post_ids[j + 1] = |
1346 | thread->parent_post_ids[j]; |
1347 | } |
1348 | insert = n + 1; |
1349 | break; |
1350 | } |
1351 | thread->post_ids[insert] = post->id; |
1352 | thread->parent_post_ids[insert] = post->parent_post_id; |
1353 | |
1354 | ret = bile_marshall_object(board->bile, board_thread_object_fields, |
1355 | nboard_thread_object_fields, thread, &data, &size); |
1356 | if (ret != 0 || size == 0) { |
1357 | logger_printf("[board] Failed to marshall new thread object"); |
1358 | post->id = 0; |
1359 | goto done; |
1360 | } |
1361 | if (bile_write(board->bile, BOARD_THREAD_RTYPE, thread->thread_id, |
1362 | data, size) != size) { |
1363 | warn("bile_write of thread failed! %d", |
1364 | bile_error(board->bile)); |
1365 | post->id = 0; |
1366 | xfree(&data); |
1367 | goto done; |
1368 | } |
1369 | xfree(&data); |
1370 | |
1371 | /* it would be nice not to have to rebuild this every time... */ |
1372 | board_delete_cached_index(board); |
1373 | bile_flush(board->bile, true); |
1374 | |
1375 | if (post->time > board->last_post_at) |
1376 | board->last_post_at = post->time; |
1377 | } |
1378 | |
1379 | done: |
1380 | return (post->id == 0 ? -1 : 0); |
1381 | } |
1382 | |
1383 | void |
1384 | board_delete_post(struct board *board, struct board_post *post, |
1385 | struct board_thread *thread) |
1386 | { |
1387 | size_t size, n, nn; |
1388 | char *data, *body; |
1389 | char del[50]; |
1390 | short ret; |
1391 | bool childs = false; |
1392 | unsigned long *new_post_ids; |
1393 | unsigned long *new_parent_post_ids; |
1394 | |
1395 | if (thread->nposts == 1) { |
1396 | bile_delete(board->bile, BOARD_THREAD_RTYPE, thread->thread_id, |
1397 | 0); |
1398 | bile_delete(board->bile, BOARD_POST_RTYPE, post->id, |
1399 | BILE_DELETE_FLAG_PURGE); |
1400 | board_delete_cached_index(board); |
1401 | return; |
1402 | } |
1403 | |
1404 | for (n = 0; n < thread->nposts; n++) { |
1405 | if (thread->parent_post_ids[n] == post->id) { |
1406 | childs = true; |
1407 | break; |
1408 | } |
1409 | } |
1410 | |
1411 | if (!childs) { |
1412 | /* just zap this off the end of the tree */ |
1413 | new_post_ids = xcalloc(thread->nposts - 1, sizeof(unsigned long)); |
1414 | if (new_post_ids == NULL) |
1415 | return; |
1416 | new_parent_post_ids = xcalloc(thread->nposts - 1, |
1417 | sizeof(unsigned long)); |
1418 | if (new_parent_post_ids == NULL) |
1419 | return; |
1420 | |
1421 | for (n = 0, nn = 0; n < thread->nposts; n++) { |
1422 | if (thread->post_ids[n] == post->id) |
1423 | continue; |
1424 | |
1425 | new_post_ids[nn] = thread->post_ids[n]; |
1426 | nn++; |
1427 | } |
1428 | |
1429 | for (n = 0, nn = 0; n < thread->nposts; n++) { |
1430 | if (thread->post_ids[n] == post->id) |
1431 | continue; |
1432 | |
1433 | new_parent_post_ids[nn] = thread->parent_post_ids[n]; |
1434 | nn++; |
1435 | } |
1436 | |
1437 | thread->nposts--; |
1438 | |
1439 | xfree(&thread->post_ids); |
1440 | thread->post_ids = new_post_ids; |
1441 | xfree(&thread->parent_post_ids); |
1442 | thread->parent_post_ids = new_parent_post_ids; |
1443 | |
1444 | ret = bile_marshall_object(board->bile, board_thread_object_fields, |
1445 | nboard_thread_object_fields, thread, &data, &size); |
1446 | if (ret != 0 || size == 0) { |
1447 | logger_printf("[board] Failed to marshall thread object " |
1448 | "during post deletion"); |
1449 | return; |
1450 | } |
1451 | if (bile_write(board->bile, BOARD_THREAD_RTYPE, thread->thread_id, |
1452 | data, size) != size) { |
1453 | warn("bile_write of updated thread after post delete failed! " |
1454 | "%d", bile_error(board->bile)); |
1455 | xfree(&data); |
1456 | return; |
1457 | } |
1458 | xfree(&data); |
1459 | |
1460 | bile_delete(board->bile, BOARD_POST_RTYPE, post->id, |
1461 | BILE_DELETE_FLAG_ZERO | BILE_DELETE_FLAG_PURGE); |
1462 | board_delete_cached_index(board); |
1463 | |
1464 | bile_flush(board->bile, true); |
1465 | return; |
1466 | } |
1467 | |
1468 | /* all we can do is change the post to say deleted */ |
1469 | if (post->body != NULL) |
1470 | xfree(&post->body); |
1471 | snprintf(del, sizeof(del), "[ Post deleted ]"); |
1472 | body = xstrdup(del); |
1473 | if (body == NULL) |
1474 | return; |
1475 | post->body = body; |
1476 | post->body_size = strlen(post->body) + 1; |
1477 | |
1478 | ret = bile_marshall_object(board->bile, board_post_object_fields, |
1479 | nboard_post_object_fields, post, &data, &size); |
1480 | if (ret != 0 || size == 0) { |
1481 | logger_printf("[board] Failed to marshall updated post object"); |
1482 | return; |
1483 | } |
1484 | if (bile_write(board->bile, BOARD_POST_RTYPE, post->id, data, |
1485 | size) != size) { |
1486 | warn("bile_write of updated post failed! %d", |
1487 | bile_error(board->bile)); |
1488 | xfree(&data); |
1489 | return; |
1490 | } |
1491 | |
1492 | xfree(&data); |
1493 | } |
1494 | |
1495 | void |
1496 | board_delete_ftn_post(struct board *board, struct board_ftn_post *post) |
1497 | { |
1498 | size_t size, npost_ids, n; |
1499 | struct board_id_time_map *id_map; |
1500 | |
1501 | bile_delete(board->bile, BOARD_FTN_POST_RTYPE, post->id, |
1502 | BILE_DELETE_FLAG_PURGE); |
1503 | |
1504 | size = bile_read_alloc(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, |
1505 | &id_map); |
1506 | if (size == 0 || id_map == NULL) |
1507 | return; |
1508 | |
1509 | npost_ids = size / sizeof(struct board_id_time_map); |
1510 | |
1511 | for (n = 0; n < npost_ids; n++) { |
1512 | if (id_map[n].id == post->id) { |
1513 | for (; n < npost_ids - 1; n++) |
1514 | id_map[n] = id_map[n + 1]; |
1515 | npost_ids--; |
1516 | break; |
1517 | } |
1518 | } |
1519 | |
1520 | if (npost_ids == 0) |
1521 | board_delete_cached_index(board); |
1522 | else |
1523 | bile_write(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, id_map, |
1524 | sizeof(struct board_id_time_map) * npost_ids); |
1525 | |
1526 | /* TODO: delete from msgid cache too */ |
1527 | } |
1528 | |
1529 | size_t |
1530 | board_index_sorted_post_ids(struct board *board, |
1531 | struct board_id_time_map **sorted_id_map) |
1532 | { |
1533 | struct thread_time_map { |
1534 | unsigned long id; |
1535 | time_t time; |
1536 | size_t nposts; |
1537 | }; |
1538 | struct board_ftn_post fpost; |
1539 | struct board_post post; |
1540 | struct board_thread thread; |
1541 | size_t ret, size, i, j, n, npost_ids, nthread_ids; |
1542 | unsigned long *post_ids, *thread_ids; |
1543 | struct board_id_time_map *id_map, tmp_id_map; |
1544 | struct thread_time_map *thread_map, tmp_thread_map; |
1545 | char *data; |
1546 | |
1547 | if (board->ftn_area[0]) { |
1548 | npost_ids = bile_ids_by_type(board->bile, BOARD_FTN_POST_RTYPE, |
1549 | &post_ids); |
1550 | if (npost_ids == 0) |
1551 | goto write_out; |
1552 | |
1553 | id_map = xcalloc(sizeof(struct board_id_time_map), npost_ids); |
1554 | if (id_map == NULL) |
1555 | goto write_out; |
1556 | |
1557 | for (n = 0; n < npost_ids; n++) { |
1558 | /* only read as far as the time */ |
1559 | ret = bile_read(board->bile, BOARD_FTN_POST_RTYPE, |
1560 | post_ids[n], &fpost, |
1561 | offsetof(struct board_ftn_post, time) + |
1562 | member_size(struct board_ftn_post, time)); |
1563 | if (ret == 0) |
1564 | goto write_out; |
1565 | |
1566 | id_map[n].id = fpost.id; |
1567 | id_map[n].time = fpost.time; |
1568 | } |
1569 | |
1570 | /* sort by date descending */ |
1571 | for (i = 1; i < npost_ids; i++) { |
1572 | for (j = i; j > 0; j--) { |
1573 | if (id_map[j].time < id_map[j - 1].time) |
1574 | break; |
1575 | tmp_id_map = id_map[j]; |
1576 | id_map[j] = id_map[j - 1]; |
1577 | id_map[j - 1] = tmp_id_map; |
1578 | } |
1579 | } |
1580 | } else { |
1581 | npost_ids = 0; |
1582 | nthread_ids = bile_ids_by_type(board->bile, BOARD_THREAD_RTYPE, |
1583 | &thread_ids); |
1584 | if (nthread_ids == 0) |
1585 | goto write_out; |
1586 | |
1587 | thread_map = xcalloc(sizeof(struct thread_time_map), nthread_ids); |
1588 | if (thread_map == NULL) |
1589 | goto write_out; |
1590 | |
1591 | for (n = 0; n < nthread_ids; n++) { |
1592 | size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, |
1593 | thread_ids[n], &data); |
1594 | ret = bile_unmarshall_object(board->bile, |
1595 | board_thread_object_fields, nboard_thread_object_fields, data, |
1596 | size, &thread, sizeof(thread), false); |
1597 | xfree(&data); |
1598 | if (ret != 0) |
1599 | goto write_out; |
1600 | |
1601 | thread_map[n].id = thread.thread_id; |
1602 | thread_map[n].time = thread.last_post_at; |
1603 | thread_map[n].nposts = thread.nposts; |
1604 | npost_ids += thread.nposts; |
1605 | } |
1606 | |
1607 | xfree(&thread_ids); |
1608 | |
1609 | /* sort by last post date descending */ |
1610 | for (i = 1; i < nthread_ids; i++) { |
1611 | for (j = i; j > 0; j--) { |
1612 | if (thread_map[j].time < thread_map[j - 1].time) |
1613 | break; |
1614 | tmp_thread_map = thread_map[j]; |
1615 | thread_map[j] = thread_map[j - 1]; |
1616 | thread_map[j - 1] = tmp_thread_map; |
1617 | } |
1618 | } |
1619 | |
1620 | id_map = xcalloc(sizeof(struct board_id_time_map), npost_ids); |
1621 | if (id_map == NULL) |
1622 | goto write_out; |
1623 | |
1624 | npost_ids = 0; |
1625 | for (j = 0; j < nthread_ids; j++) { |
1626 | size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, |
1627 | thread_map[j].id, &data); |
1628 | if (data == NULL) |
1629 | goto write_out; |
1630 | ret = bile_unmarshall_object(board->bile, |
1631 | board_thread_object_fields, nboard_thread_object_fields, data, |
1632 | size, &thread, sizeof(thread), true); |
1633 | xfree(&data); |
1634 | if (ret != 0) |
1635 | goto write_out; |
1636 | |
1637 | /* these are already sorted, and we want to keep thread sort */ |
1638 | for (i = 0; i < thread.nposts; i++) { |
1639 | /* only read as far as the time */ |
1640 | size = bile_read(board->bile, BOARD_POST_RTYPE, |
1641 | thread.post_ids[i], &post, |
1642 | offsetof(struct board_post, time) + |
1643 | member_size(struct board_post, time)); |
1644 | if (size == 0) { |
1645 | logger_printf("[board] Board %s thread %ld post %ld " |
1646 | "is missing", board->name, thread.thread_id, |
1647 | thread.post_ids[i]); |
1648 | continue; |
1649 | } |
1650 | |
1651 | id_map[npost_ids].id = post.id; |
1652 | id_map[npost_ids].time = post.time; |
1653 | npost_ids++; |
1654 | } |
1655 | |
1656 | if (thread.subject != NULL) |
1657 | xfree(&thread.subject); |
1658 | if (thread.post_ids != NULL) |
1659 | xfree(&thread.post_ids); |
1660 | if (thread.parent_post_ids != NULL) |
1661 | xfree(&thread.parent_post_ids); |
1662 | } |
1663 | |
1664 | xfree(&thread_map); |
1665 | } |
1666 | |
1667 | write_out: |
1668 | if (npost_ids == 0 || id_map == NULL) { |
1669 | board_delete_cached_index(board); |
1670 | if (sorted_id_map != NULL) |
1671 | *sorted_id_map = NULL; |
1672 | return 0; |
1673 | } |
1674 | |
1675 | bile_write(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, id_map, |
1676 | sizeof(struct board_id_time_map) * npost_ids); |
1677 | if (sorted_id_map == NULL) |
1678 | xfree(&id_map); |
1679 | else |
1680 | *sorted_id_map = id_map; |
1681 | return npost_ids; |
1682 | } |
1683 | |
1684 | short |
1685 | board_toss_ftn_message(struct board *board, |
1686 | struct fidopkt_message *fidomsg, bool local) |
1687 | { |
1688 | struct board_ftn_msgid_cache { |
1689 | unsigned long id; |
1690 | struct fidopkt_msgid msgid; |
1691 | } *msgid_cache = NULL; |
1692 | struct bile_object *o; |
1693 | struct board_ftn_post post; |
1694 | struct fidopkt_msgid msgid; |
1695 | unsigned long *post_ids; |
1696 | char *pdata; |
1697 | size_t asize, cache_size, psize, npost_ids; |
1698 | short n, ret, bret; |
1699 | bool dirty_cache = false; |
1700 | |
1701 | msgid = fidomsg->msgid; |
1702 | |
1703 | o = bile_find(board->bile, BOARD_FTN_MSGID_CACHE_RTYPE, 1); |
1704 | if (o) { |
1705 | /* allocate its size plus one entry that we may add */ |
1706 | asize = o->size + sizeof(struct board_ftn_msgid_cache); |
1707 | msgid_cache = xmalloc(asize); |
1708 | if (msgid_cache == NULL) { |
1709 | logger_printf("[board] toss: malloc(%ld) failed", |
1710 | asize); |
1711 | return -1; |
1712 | } |
1713 | bile_read(board->bile, o->type, o->id, msgid_cache, o->size); |
1714 | npost_ids = o->size / sizeof(struct board_ftn_msgid_cache); |
1715 | xfree(&o); |
1716 | } else { |
1717 | npost_ids = bile_ids_by_type(board->bile, BOARD_FTN_POST_RTYPE, |
1718 | &post_ids); |
1719 | msgid_cache = xcalloc(sizeof(struct board_ftn_msgid_cache), |
1720 | npost_ids + 1); |
1721 | if (msgid_cache == NULL) { |
1722 | logger_printf("[board] toss: calloc(%ld, %ld) failed", |
1723 | sizeof(struct board_ftn_msgid_cache), npost_ids + 1); |
1724 | return -1; |
1725 | } |
1726 | for (n = 0; n < npost_ids; n++) { |
1727 | /* only read as far as we have to */ |
1728 | bile_read(board->bile, BOARD_FTN_POST_RTYPE, post_ids[n], |
1729 | &post, offsetof(struct board_ftn_post, msgid) + |
1730 | member_size(struct board_ftn_post, msgid)); |
1731 | |
1732 | msgid_cache[n].id = post.id; |
1733 | memcpy(&msgid_cache[n].msgid, &post.msgid, |
1734 | sizeof(post.msgid)); |
1735 | } |
1736 | dirty_cache = true; |
1737 | } |
1738 | |
1739 | uthread_yield(); |
1740 | |
1741 | if (!local) { |
1742 | for (n = 0; n < npost_ids; n++) { |
1743 | if (memcmp(&msgid_cache[n].msgid, &msgid, |
1744 | sizeof(msgid)) == 0) { |
1745 | logger_printf("[board] Already have %s EchoMail %s in %s " |
1746 | "(%ld), skipping", db->config.ftn_network, |
1747 | fidomsg->msgid_orig, board->name, msgid_cache[n].id); |
1748 | ret = 0; |
1749 | goto done; |
1750 | } |
1751 | } |
1752 | } |
1753 | |
1754 | memset(&post, 0, sizeof(post)); |
1755 | post.time = fidomsg->time; |
1756 | post.body_size = fidomsg->body_len + 1; |
1757 | post.body = fidomsg->body; |
1758 | strlcpy(post.reply, fidomsg->reply, sizeof(post.reply)); |
1759 | strlcpy(post.from, fidomsg->from, sizeof(post.from)); |
1760 | strlcpy(post.subject, fidomsg->subject, sizeof(post.subject)); |
1761 | strlcpy(post.to, fidomsg->to, sizeof(post.to)); |
1762 | strlcpy(post.origin, fidomsg->origin, sizeof(post.origin)); |
1763 | strlcpy(post.msgid_orig, fidomsg->msgid_orig, sizeof(post.msgid_orig)); |
1764 | post.msgid = msgid; |
1765 | |
1766 | if (!local || !post.id) |
1767 | post.id = bile_next_id(board->bile, BOARD_FTN_POST_RTYPE); |
1768 | if (!post.id) { |
1769 | logger_printf("[board] Failed get next id for %s", board->name); |
1770 | ret = -1; |
1771 | goto done; |
1772 | } |
1773 | |
1774 | bret = bile_marshall_object(board->bile, |
1775 | board_ftn_post_object_fields, nboard_ftn_post_object_fields, |
1776 | &post, &pdata, &psize); |
1777 | if (bret != 0 || psize == 0) { |
1778 | logger_printf("[board] Failed to marshall new post %s %s: %d", |
1779 | fidomsg->area, fidomsg->msgid_orig, bile_error(board->bile)); |
1780 | ret = -1; |
1781 | goto done; |
1782 | } |
1783 | if (bile_write(board->bile, BOARD_FTN_POST_RTYPE, post.id, pdata, |
1784 | psize) != psize) { |
1785 | logger_printf("[fidopkt] Failed to save new post %s %s: %d", |
1786 | fidomsg->area, fidomsg->msgid_orig, bile_error(board->bile)); |
1787 | ret = -1; |
1788 | xfree(&pdata); |
1789 | goto done; |
1790 | } |
1791 | xfree(&pdata); |
1792 | |
1793 | ret = 1; |
1794 | |
1795 | /* we already allocated one empty space at the end of the cache */ |
1796 | msgid_cache[npost_ids].id = post.id; |
1797 | memcpy(&msgid_cache[npost_ids].msgid, &post.msgid, sizeof(post.msgid)); |
1798 | npost_ids++; |
1799 | dirty_cache = true; |
1800 | |
1801 | board_delete_cached_index(board); |
1802 | |
1803 | if (post.time > board->last_post_at) |
1804 | board->last_post_at = post.time; |
1805 | |
1806 | if (!local) |
1807 | logger_printf("[board] Tossed %s EchoMail %s as %ld", |
1808 | fidomsg->area, fidomsg->msgid_orig, post.id); |
1809 | uthread_yield(); |
1810 | |
1811 | done: |
1812 | if (dirty_cache) { |
1813 | cache_size = npost_ids * sizeof(struct board_ftn_msgid_cache); |
1814 | if (bile_write(board->bile, BOARD_FTN_MSGID_CACHE_RTYPE, 1, |
1815 | msgid_cache, cache_size) != cache_size) { |
1816 | logger_printf("[board] Failed to save msgid cache for %s: %d", |
1817 | fidomsg->area, bile_error(board->bile)); |
1818 | ret = -1; |
1819 | } |
1820 | } |
1821 | if (msgid_cache != NULL) |
1822 | xfree(&msgid_cache); |
1823 | |
1824 | return ret; |
1825 | } |
1826 | |
1827 | void |
1828 | board_delete_cached_index(struct board *board) |
1829 | { |
1830 | bile_delete(board->bile, BOARD_SORTED_ID_MAP_RTYPE, 1, |
1831 | BILE_DELETE_FLAG_PURGE); |
1832 | } |
1833 | |
1834 | void |
1835 | board_prune_old_posts(struct board *board) |
1836 | { |
1837 | size_t nposts, n; |
1838 | struct board_id_time_map *sorted_id_map; |
1839 | struct board_ftn_post fpost; |
1840 | struct board_post post; |
1841 | struct board_thread thread; |
1842 | time_t oldest; |
1843 | size_t size, deleted; |
1844 | char *data; |
1845 | short ret; |
1846 | |
1847 | if (board->retention_days == 0) |
1848 | return; |
1849 | |
1850 | deleted = 0; |
1851 | |
1852 | nposts = board_index_sorted_post_ids(board, &sorted_id_map); |
1853 | if (nposts == 0) |
1854 | goto done; |
1855 | |
1856 | oldest = Time - ((unsigned long)(board->retention_days) * |
1857 | (60UL * 60UL * 24UL)); |
1858 | |
1859 | for (n = 0; n < nposts; n++) { |
1860 | if (sorted_id_map[n].time >= oldest) |
1861 | continue; |
1862 | |
1863 | if (board->ftn_area[0]) { |
1864 | size = bile_read_alloc(board->bile, BOARD_FTN_POST_RTYPE, |
1865 | sorted_id_map[n].id, &data); |
1866 | if (!size) |
1867 | continue; |
1868 | ret = bile_unmarshall_object(board->bile, |
1869 | board_ftn_post_object_fields, nboard_ftn_post_object_fields, |
1870 | data, size, &fpost, sizeof(fpost), false); |
1871 | xfree(&data); |
1872 | if (ret == BILE_ERR_NO_MEMORY) |
1873 | goto done; |
1874 | |
1875 | board_delete_ftn_post(board, &fpost); |
1876 | } else { |
1877 | size = bile_read_alloc(board->bile, BOARD_POST_RTYPE, |
1878 | sorted_id_map[n].id, &data); |
1879 | if (!size) |
1880 | continue; |
1881 | ret = bile_unmarshall_object(board->bile, |
1882 | board_post_object_fields, nboard_post_object_fields, |
1883 | data, size, &post, sizeof(post), false); |
1884 | xfree(&data); |
1885 | if (ret == BILE_ERR_NO_MEMORY) |
1886 | goto done; |
1887 | |
1888 | size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, |
1889 | post.thread_id, &data); |
1890 | if (!size) |
1891 | continue; |
1892 | ret = bile_unmarshall_object(board->bile, |
1893 | board_thread_object_fields, nboard_thread_object_fields, |
1894 | data, size, &thread, sizeof(thread), false); |
1895 | xfree(&data); |
1896 | if (ret == BILE_ERR_NO_MEMORY) |
1897 | goto done; |
1898 | |
1899 | board_delete_post(board, &post, &thread); |
1900 | } |
1901 | |
1902 | deleted++; |
1903 | uthread_yield(); |
1904 | } |
1905 | |
1906 | done: |
1907 | if (sorted_id_map != NULL) |
1908 | xfree(&sorted_id_map); |
1909 | |
1910 | if (deleted) { |
1911 | board_index_sorted_post_ids(board, NULL); |
1912 | |
1913 | logger_printf("[board] Pruned %lu post%s in %s older than %d day%s", |
1914 | deleted, deleted == 1 ? "" : "s", board->name, |
1915 | board->retention_days, board->retention_days == 1 ? "" : "s"); |
1916 | } |
1917 | } |