Download
jcs
/subtext
/board.c
(View History)
jcs board: constify fields | Latest amendment: 275 on 2022-11-11 |
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 "board.h" |
25 | #include "user.h" |
26 | |
27 | #define POSTS_PER_PAGE 10 |
28 | |
29 | #define POST_READ_RETURN_DONE -1 |
30 | #define POST_READ_RETURN_LIST -2 |
31 | #define POST_READ_RETURN_FIND -3 |
32 | |
33 | const struct struct_field board_fields[] = { |
34 | { "Board ID", CONFIG_TYPE_LONG, |
35 | offsetof(struct board, id), |
36 | 1, ULONG_MAX }, |
37 | { "Name", CONFIG_TYPE_STRING, |
38 | offsetof(struct board, name), |
39 | 1, member_size(struct board, name) }, |
40 | { "Description", CONFIG_TYPE_STRING, |
41 | offsetof(struct board, description), |
42 | 0, member_size(struct board, description) }, |
43 | { "Restricted Posting", CONFIG_TYPE_BOOLEAN, |
44 | offsetof(struct board, restricted_posting), |
45 | 0, 0 }, |
46 | { "Restricted Viewing", CONFIG_TYPE_BOOLEAN, |
47 | offsetof(struct board, restricted_viewing), |
48 | 0, 0 }, |
49 | { "Days of Retention", CONFIG_TYPE_SHORT, |
50 | offsetof(struct board, retention_days), |
51 | 0, USHRT_MAX }, |
52 | }; |
53 | const size_t nboard_fields = nitems(board_fields); |
54 | |
55 | const struct bile_object_field board_object_fields[] = { |
56 | { offsetof(struct board, id), |
57 | member_size(struct board, id), -1 }, |
58 | { offsetof(struct board, name), |
59 | member_size(struct board, name), -1 }, |
60 | { offsetof(struct board, description), |
61 | member_size(struct board, description), -1 }, |
62 | { offsetof(struct board, restricted_posting), |
63 | member_size(struct board, restricted_posting), -1 }, |
64 | { offsetof(struct board, restricted_viewing), |
65 | member_size(struct board, restricted_viewing), -1 }, |
66 | { offsetof(struct board, retention_days), |
67 | member_size(struct board, retention_days), -1 }, |
68 | { offsetof(struct board, last_post_at), |
69 | member_size(struct board, last_post_at), -1 }, |
70 | { offsetof(struct board, post_count), |
71 | member_size(struct board, post_count), -1 }, |
72 | }; |
73 | const size_t nboard_object_fields = nitems(board_object_fields); |
74 | |
75 | const struct bile_object_field board_post_object_fields[] = { |
76 | { offsetof(struct board_post, id), |
77 | member_size(struct board_post, id), -1 }, |
78 | { offsetof(struct board_post, thread_id), |
79 | member_size(struct board_post, thread_id), -1 }, |
80 | { offsetof(struct board_post, time), |
81 | member_size(struct board_post, time), -1 }, |
82 | { offsetof(struct board_post, sender_user_id), |
83 | member_size(struct board_post, sender_user_id), -1 }, |
84 | { offsetof(struct board_post, body_size), |
85 | member_size(struct board_post, body_size), -1 }, |
86 | { offsetof(struct board_post, body), |
87 | -1, offsetof(struct board_post, body_size) }, |
88 | { offsetof(struct board_post, parent_post_id), |
89 | member_size(struct board_post, parent_post_id), -1 }, |
90 | { offsetof(struct board_post, via), |
91 | member_size(struct board_post, via), -1 }, |
92 | }; |
93 | const size_t nboard_post_object_fields = nitems(board_post_object_fields); |
94 | |
95 | const struct bile_object_field board_thread_object_fields[] = { |
96 | { offsetof(struct board_thread, thread_id), |
97 | member_size(struct board_thread, thread_id), -1 }, |
98 | { offsetof(struct board_thread, last_post_at), |
99 | member_size(struct board_thread, last_post_at), -1 }, |
100 | { offsetof(struct board_thread, subject_size), |
101 | member_size(struct board_thread, subject_size), -1 }, |
102 | { offsetof(struct board_thread, subject), |
103 | -1, offsetof(struct board_thread, subject_size) }, |
104 | { offsetof(struct board_thread, nposts), |
105 | member_size(struct board_thread, nposts), -1 }, |
106 | { offsetof(struct board_thread, post_ids), |
107 | -(sizeof(long)), offsetof(struct board_thread, nposts) }, |
108 | { offsetof(struct board_thread, parent_post_ids), |
109 | -(sizeof(long)), offsetof(struct board_thread, nposts) }, |
110 | }; |
111 | const size_t nboard_thread_object_fields = nitems(board_thread_object_fields); |
112 | |
113 | unsigned long board_compose(struct session *s, struct board *board, |
114 | struct board_thread *thread, struct board_post *parent_post, |
115 | char *initial_subject, char *initial_body); |
116 | void board_list_posts(struct session *s, struct board *board, |
117 | size_t npost_Ids, unsigned long *post_ids, size_t page, size_t pages); |
118 | short board_post_read(struct session *s, struct board *board, |
119 | unsigned long id, short idx); |
120 | size_t board_find_post_ids(struct board *board, size_t *npost_ids, |
121 | unsigned long **post_ids, size_t offset, size_t limit); |
122 | short board_post_create(struct board *board, struct board_thread *thread, |
123 | struct board_post *post); |
124 | void board_delete_post(struct session *s, struct board *board, |
125 | struct board_post *post, struct board_thread *thread); |
126 | |
127 | void |
128 | board_show(struct session *s, short id) |
129 | { |
130 | static const struct session_menu_option opts[] = { |
131 | { '#', "#0123456789", "Read post [#]" }, |
132 | { '<', "<", "Previous page of posts" }, |
133 | { 'l', "Ll", "List posts" }, |
134 | { '>', ">", "Next page of posts" }, |
135 | { 'p', "Pp", "Post new thread" }, |
136 | { 'q', "QqXx", "Return to main menu" }, |
137 | { '?', "?", "List menu options" }, |
138 | }; |
139 | char prompt[7 + BOARD_NAME_LENGTH]; |
140 | struct board *board = NULL; |
141 | size_t n, nall_post_ids, page, pages, npost_ids; |
142 | unsigned long *post_ids = NULL; |
143 | short ret; |
144 | char c; |
145 | bool done, find_post_ids, show_list, show_help; |
146 | |
147 | for (n = 0; n < db->nboards; n++) { |
148 | if (db->boards[n].id == id) { |
149 | board = &db->boards[n]; |
150 | break; |
151 | } |
152 | } |
153 | |
154 | if (!board) { |
155 | session_printf(s, "Invalid board\r\n"); |
156 | session_flush(s); |
157 | return; |
158 | } |
159 | |
160 | page = 0; |
161 | find_post_ids = true; |
162 | show_list = true; |
163 | show_help = false; |
164 | done = false; |
165 | |
166 | snprintf(prompt, sizeof(prompt), "Boards:%s", board->name); |
167 | |
168 | while (!done && !s->ending) { |
169 | if (find_post_ids) { |
170 | if (post_ids != NULL) { |
171 | xfree(&post_ids); |
172 | post_ids = NULL; |
173 | } |
174 | nall_post_ids = board_find_post_ids(board, &npost_ids, |
175 | &post_ids, page * POSTS_PER_PAGE, POSTS_PER_PAGE); |
176 | /* ceil(nall_post_ids / POSTS_PER_PAGE) */ |
177 | pages = (nall_post_ids + POSTS_PER_PAGE - 1) / POSTS_PER_PAGE; |
178 | |
179 | if (page >= pages) |
180 | page = pages - 1; |
181 | |
182 | find_post_ids = false; |
183 | } |
184 | |
185 | if (show_list) { |
186 | board_list_posts(s, board, npost_ids, post_ids, page + 1, |
187 | pages); |
188 | show_list = false; |
189 | } |
190 | |
191 | c = session_menu(s, board->description, prompt, opts, |
192 | nitems(opts), show_help); |
193 | show_help = false; |
194 | |
195 | handle_opt: |
196 | switch (c) { |
197 | case 'l': |
198 | show_list = true; |
199 | break; |
200 | case 'p': |
201 | if (board_compose(s, board, NULL, NULL, NULL, NULL)) { |
202 | find_post_ids = true; |
203 | show_list = true; |
204 | } |
205 | break; |
206 | case '>': |
207 | case '<': |
208 | if (c == '>' && page == pages - 1) { |
209 | session_printf(s, "You are at the last page of posts\r\n"); |
210 | session_flush(s); |
211 | break; |
212 | } |
213 | if (c == '<' && page == 0) { |
214 | session_printf(s, "You are already at the first page\r\n"); |
215 | session_flush(s); |
216 | break; |
217 | } |
218 | if (c == '>') |
219 | page++; |
220 | else |
221 | page--; |
222 | find_post_ids = true; |
223 | show_list = true; |
224 | break; |
225 | case 0: |
226 | case 1: |
227 | case 2: |
228 | case 3: |
229 | case 4: |
230 | case 5: |
231 | case 6: |
232 | case 7: |
233 | case 8: |
234 | case 9: |
235 | if (c >= npost_ids) { |
236 | session_printf(s, "Invalid post\r\n"); |
237 | session_flush(s); |
238 | break; |
239 | } |
240 | ret = board_post_read(s, board, post_ids[c], c); |
241 | switch (ret) { |
242 | case POST_READ_RETURN_DONE: |
243 | break; |
244 | case POST_READ_RETURN_LIST: |
245 | show_list = true; |
246 | break; |
247 | case POST_READ_RETURN_FIND: |
248 | find_post_ids = true; |
249 | show_list = true; |
250 | break; |
251 | default: |
252 | c = ret; |
253 | goto handle_opt; |
254 | } |
255 | break; |
256 | case '?': |
257 | show_help = true; |
258 | break; |
259 | default: |
260 | done = true; |
261 | break; |
262 | } |
263 | } |
264 | |
265 | if (post_ids != NULL) |
266 | xfree(&post_ids); |
267 | } |
268 | |
269 | void |
270 | board_list_posts(struct session *s, struct board *board, size_t npost_ids, |
271 | unsigned long *post_ids, unsigned long page, unsigned long pages) |
272 | { |
273 | char time[24]; |
274 | unsigned long indent_parent_ids[10] = { 0 }; |
275 | char indent_s[22]; |
276 | size_t n, size, idx; |
277 | struct username_cache *user; |
278 | struct bile_object *obj; |
279 | struct board_thread thread = { 0 }; |
280 | struct board_post post; |
281 | short indent, j, k; |
282 | char *data; |
283 | |
284 | session_printf(s, "{{B}}%s: %s (Page %ld of %ld){{/B}}\r\n", |
285 | board->name, board->description, page, pages); |
286 | session_printf(s, "%s# N Date From Subject%s\r\n", |
287 | ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); |
288 | session_flush(s); |
289 | |
290 | if (npost_ids == 0) { |
291 | session_printf(s, "No posts here yet.\r\n"); |
292 | session_flush(s); |
293 | return; |
294 | } |
295 | |
296 | for (n = 0; n < npost_ids; n++) { |
297 | size = bile_read_alloc(board->bile, BOARD_POST_RTYPE, post_ids[n], |
298 | &data); |
299 | bile_unmarshall_object(board->bile, board_post_object_fields, |
300 | nboard_post_object_fields, data, size, &post, sizeof(post), |
301 | false, "board_list_posts"); |
302 | xfree(&data); |
303 | |
304 | if (post.thread_id != thread.thread_id) { |
305 | if (thread.thread_id) { |
306 | if (thread.subject != NULL) |
307 | xfree(&thread.subject); |
308 | if (thread.post_ids != NULL) |
309 | xfree(&thread.post_ids); |
310 | if (thread.parent_post_ids != NULL) |
311 | xfree(&thread.parent_post_ids); |
312 | } |
313 | size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, |
314 | post.thread_id, &data); |
315 | bile_unmarshall_object(board->bile, board_thread_object_fields, |
316 | nboard_thread_object_fields, data, size, &thread, |
317 | sizeof(thread), true, "board_list_posts"); |
318 | xfree(&data); |
319 | |
320 | for (j = 0; j < nitems(indent_parent_ids); j++) |
321 | indent_parent_ids[j] = 0; |
322 | } |
323 | |
324 | user = user_username(post.sender_user_id); |
325 | strftime(time, sizeof(time), "%b %d", localtime(&post.time)); |
326 | |
327 | if (post.parent_post_id == 0) { |
328 | indent_s[0] = '\0'; |
329 | } else { |
330 | indent = -1; |
331 | for (j = 0; j < nitems(indent_parent_ids); j++) { |
332 | if (indent_parent_ids[j] == post.parent_post_id || |
333 | indent_parent_ids[j] == 0) { |
334 | indent = j; |
335 | indent_parent_ids[j] = post.parent_post_id; |
336 | for (k = j + 1; k < nitems(indent_parent_ids); k++) { |
337 | if (indent_parent_ids[k] == 0) |
338 | break; |
339 | indent_parent_ids[k] = 0; |
340 | } |
341 | break; |
342 | } |
343 | } |
344 | if (indent == -1) |
345 | indent = nitems(indent_parent_ids) - 1; |
346 | |
347 | for (j = 0; j < indent; j++) { |
348 | indent_s[j] = ' '; |
349 | } |
350 | indent_s[j] = '`'; |
351 | indent_s[j + 1] = '-'; |
352 | indent_s[j + 2] = '>'; |
353 | indent_s[j + 3] = '\0'; |
354 | } |
355 | session_printf(s, "%s%ld %c %s %- 10s %s{{#}}%- 40s%s\r\n", |
356 | true ? "" : ansi(s, ANSI_BOLD, ANSI_END), |
357 | n, |
358 | true ? ' ' : 'N', |
359 | time, |
360 | user ? user->username : "(unknown)", |
361 | post.parent_post_id != 0 && n == 0 ? "Re: " : "", |
362 | post.parent_post_id == 0 || n == 0 ? thread.subject : indent_s, |
363 | true ? "" : ansi(s, ANSI_RESET, ANSI_END)); |
364 | } |
365 | session_flush(s); |
366 | |
367 | if (thread.subject != NULL) |
368 | xfree(&thread.subject); |
369 | if (thread.post_ids != NULL) |
370 | xfree(&thread.post_ids); |
371 | if (thread.parent_post_ids != NULL) |
372 | xfree(&thread.parent_post_ids); |
373 | } |
374 | |
375 | unsigned long |
376 | board_compose(struct session *s, struct board *board, |
377 | struct board_thread *thread, struct board_post *parent_post, |
378 | char *initial_subject, char *initial_body) |
379 | { |
380 | struct board_post post = { 0 }; |
381 | char *data = NULL, *tmp = NULL; |
382 | size_t size; |
383 | short c, ret; |
384 | |
385 | if (!s->user) { |
386 | session_printf(s, "Posting is not available to guests.\r\n" |
387 | "Please create an account first.\r\n"); |
388 | session_flush(s); |
389 | return 0; |
390 | } |
391 | |
392 | if (initial_body) |
393 | post.body = xstrdup(initial_body, "board_compose body"); |
394 | if (thread) { |
395 | post.thread_id = thread->thread_id; |
396 | post.parent_post_id = parent_post->id; |
397 | } else |
398 | thread = xmalloczero(sizeof(struct board_thread), |
399 | "board_compose thread"); |
400 | |
401 | post.sender_user_id = s->user->id; |
402 | strlcpy(post.via, s->via, sizeof(post.via)); |
403 | |
404 | session_printf(s, "{{B}}Compose %s{{/B}}\r\n", |
405 | parent_post ? "Reply" : "New Post"); |
406 | session_printf(s, "{{B}}From:{{/B}} %s (via %s)\r\n", |
407 | s->user->username, s->via); |
408 | session_printf(s, "{{B}}To:{{/B}} %s\r\n", board->name); |
409 | |
410 | post_compose_start: |
411 | if (parent_post) { |
412 | session_printf(s, "{{B}}Subject:{{/B}}{{#}} Re: %s\r\n", |
413 | thread->subject); |
414 | session_flush(s); |
415 | } else { |
416 | if (initial_subject && !thread->subject) |
417 | thread->subject = xstrdup(initial_subject, |
418 | "board_compose subject"); |
419 | |
420 | for (;;) { |
421 | session_printf(s, "{{B}}Subject:{{/B}} "); |
422 | session_flush(s); |
423 | |
424 | tmp = session_field_input(s, 100, 50, thread->subject, |
425 | false, 0); |
426 | if (thread->subject != NULL) |
427 | xfree(&thread->subject); |
428 | thread->subject = tmp; |
429 | session_output(s, "\r\n", 2); |
430 | session_flush(s); |
431 | |
432 | if (thread->subject == NULL) |
433 | goto post_compose_done; |
434 | |
435 | rtrim(thread->subject, "\r\n\t "); |
436 | |
437 | if (thread->subject[0] == '\0') { |
438 | session_printf(s, "{{B}}Error:{{/B}} Subject " |
439 | "cannot be blank (^C to cancel)\r\n"); |
440 | session_flush(s); |
441 | xfree(&thread->subject); |
442 | continue; |
443 | } |
444 | thread->subject_size = strlen(thread->subject) + 1; |
445 | break; |
446 | } |
447 | } |
448 | |
449 | for (;;) { |
450 | session_printf(s, "{{B}}Message (^D when finished):{{/B}}\r\n"); |
451 | session_flush(s); |
452 | |
453 | tmp = session_field_input(s, 2048, s->terminal_columns - 1, |
454 | post.body, true, 0); |
455 | if (post.body != NULL) |
456 | xfree(&post.body); |
457 | post.body = tmp; |
458 | session_output(s, "\r\n", 2); |
459 | session_flush(s); |
460 | |
461 | if (post.body == NULL) |
462 | goto post_compose_done; |
463 | |
464 | rtrim(post.body, "\r\n\t "); |
465 | |
466 | if (post.body[0] == '\0') { |
467 | xfree(&post.body); |
468 | goto post_compose_done; |
469 | } |
470 | post.body_size = strlen(post.body) + 1; |
471 | break; |
472 | } |
473 | |
474 | for (;;) { |
475 | session_printf(s, "\r\n{{B}}(P){{/B}}ost message, " |
476 | "{{B}}(E){{/B}}dit again, or {{B}}(C){{/B}}ancel? "); |
477 | session_flush(s); |
478 | |
479 | c = session_input_char(s); |
480 | if (c == 0 || s->ending) |
481 | goto post_compose_done; |
482 | |
483 | switch (c) { |
484 | case 'p': |
485 | case 'P': |
486 | case 'y': |
487 | session_printf(s, "%c\r\n", c); |
488 | session_flush(s); |
489 | /* FALLTHROUGH */ |
490 | case '\n': |
491 | case '\r': |
492 | /* send */ |
493 | session_printf(s, "Posting message... "); |
494 | session_flush(s); |
495 | |
496 | if (board_post_create(board, thread, &post) == 0) { |
497 | session_logf(s, "Posted message %ld to %s", post.id, |
498 | board->name); |
499 | session_printf(s, "done\r\n"); |
500 | } else |
501 | session_printf(s, "failed!\r\n"); |
502 | |
503 | session_flush(s); |
504 | |
505 | goto post_compose_done; |
506 | case 'e': |
507 | case 'E': |
508 | session_printf(s, "%c\r\n", c); |
509 | session_flush(s); |
510 | goto post_compose_start; |
511 | case 'c': |
512 | case 'C': |
513 | session_printf(s, "%c\r\n", c); |
514 | session_flush(s); |
515 | /* FALLTHROUGH */ |
516 | case CONTROL_C: |
517 | goto post_compose_done; |
518 | } |
519 | } |
520 | |
521 | post_compose_error: |
522 | session_printf(s, "Failed saving message!\r\n"); |
523 | session_flush(s); |
524 | |
525 | post_compose_done: |
526 | if (parent_post == NULL) { |
527 | if (thread->subject != NULL) |
528 | xfree(&thread->subject); |
529 | xfree(&thread); |
530 | } |
531 | if (post.body) |
532 | xfree(&post.body); |
533 | |
534 | return post.id; |
535 | } |
536 | |
537 | short |
538 | board_post_read(struct session *s, struct board *board, unsigned long id, |
539 | short idx) |
540 | { |
541 | static const struct session_menu_option opts[] = { |
542 | { '#', "#0123456789", "Read post [#]" }, |
543 | { 'd', "Dd", "Delete this post" }, |
544 | { 'r', "Rr", "Reply to this post" }, |
545 | { 'q', "QqXx", "Return to threads" }, |
546 | { '?', "?", "List these options" }, |
547 | }; |
548 | char time[32]; |
549 | struct board_thread thread; |
550 | struct board_post post; |
551 | struct username_cache *sender, *recipient; |
552 | struct session_menu_option *dopts = NULL; |
553 | unsigned long new_id; |
554 | char prompt[7 + BOARD_NAME_LENGTH + 8]; |
555 | size_t n, size; |
556 | short ret = POST_READ_RETURN_DONE; |
557 | char c; |
558 | char *data; |
559 | short cc; |
560 | bool done = false, show_help = false; |
561 | |
562 | size = bile_read_alloc(board->bile, BOARD_POST_RTYPE, id, &data); |
563 | if (size == 0) |
564 | panic("failed fetching message %ld: %d", id, |
565 | bile_error(board->bile)); |
566 | bile_unmarshall_object(board->bile, board_post_object_fields, |
567 | nboard_post_object_fields, data, size, &post, sizeof(post), true, |
568 | "board_post_read post"); |
569 | xfree(&data); |
570 | |
571 | size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, |
572 | post.thread_id, &data); |
573 | if (size == 0) |
574 | panic("failed fetching thread %ld: %d", post.thread_id, |
575 | bile_error(board->bile)); |
576 | bile_unmarshall_object(board->bile, board_thread_object_fields, |
577 | nboard_thread_object_fields, data, size, &thread, sizeof(thread), |
578 | true, "board_post_read thread"); |
579 | xfree(&data); |
580 | |
581 | dopts = xmalloc(sizeof(opts), "board_post_read opts"); |
582 | memcpy(dopts, opts, sizeof(opts)); |
583 | if (!(s->user && (s->user->is_sysop || |
584 | s->user->id == post.sender_user_id))) { |
585 | /* disable deleting */ |
586 | dopts[1].key[0] = '\0'; |
587 | } |
588 | |
589 | sender = user_username(post.sender_user_id); |
590 | |
591 | strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S", |
592 | localtime(&post.time)); |
593 | |
594 | session_printf(s, "{{B}}From:{{/B}} %s", |
595 | sender ? sender->username : "(unknown)"); |
596 | if (post.via[0]) |
597 | session_printf(s, " (via %s)", post.via); |
598 | session_printf(s, "\r\n"); |
599 | session_printf(s, "{{B}}To:{{/B}} %s\r\n", board->name); |
600 | session_printf(s, "{{B}}Date:{{/B}} %s %s\r\n", time, |
601 | db->config.timezone); |
602 | session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s%s\r\n", |
603 | (post.parent_post_id ? "Re: " : ""), thread.subject); |
604 | session_flush(s); |
605 | session_printf(s, "\r\n"); |
606 | session_output(s, post.body, post.body_size); |
607 | session_printf(s, "\r\n"); |
608 | |
609 | snprintf(prompt, sizeof(prompt), "Boards:%s:%d", board->name, idx); |
610 | |
611 | while (!done && !s->ending) { |
612 | c = session_menu(s, thread.subject, prompt, dopts, |
613 | nitems(opts), show_help); |
614 | show_help = false; |
615 | |
616 | switch (c) { |
617 | case 'd': |
618 | if (!(s->user && (s->user->is_sysop || |
619 | s->user->id == post.sender_user_id))) { |
620 | session_printf(s, "Invalid option\r\n"); |
621 | session_flush(s); |
622 | break; |
623 | } |
624 | |
625 | session_printf(s, "Are you sure you want to permanently " |
626 | "delete this post? [y/N] "); |
627 | session_flush(s); |
628 | |
629 | cc = session_input_char(s); |
630 | if (cc == 'y' || c == 'Y') { |
631 | session_printf(s, "%c\r\n", cc); |
632 | session_flush(s); |
633 | |
634 | board_delete_post(s, board, &post, &thread); |
635 | |
636 | session_logf(s, "Deleted post %ld (thread %ld)", post.id, |
637 | thread.thread_id); |
638 | |
639 | session_printf(s, "\r\n{{B}}Post deleted!{{/B}}\r\n"); |
640 | session_flush(s); |
641 | ret = POST_READ_RETURN_FIND; |
642 | done = true; |
643 | } else { |
644 | session_printf(s, "\r\Post not deleted.\r\n"); |
645 | session_flush(s); |
646 | } |
647 | break; |
648 | case 'r': |
649 | if (board_compose(s, board, &thread, &post, NULL, NULL)) { |
650 | ret = POST_READ_RETURN_FIND; |
651 | done = true; |
652 | } |
653 | break; |
654 | case 0: |
655 | case 1: |
656 | case 2: |
657 | case 3: |
658 | case 4: |
659 | case 5: |
660 | case 6: |
661 | case 7: |
662 | case 8: |
663 | case 9: |
664 | ret = c; |
665 | done = true; |
666 | break; |
667 | case 'q': |
668 | done = true; |
669 | break; |
670 | case '?': |
671 | show_help = true; |
672 | break; |
673 | } |
674 | } |
675 | |
676 | xfree(&dopts); |
677 | |
678 | if (post.body != NULL) |
679 | xfree(&post.body); |
680 | if (thread.subject != NULL) |
681 | xfree(&thread.subject); |
682 | if (thread.post_ids != NULL) |
683 | xfree(&thread.post_ids); |
684 | if (thread.parent_post_ids != NULL) |
685 | xfree(&thread.parent_post_ids); |
686 | |
687 | return ret; |
688 | } |
689 | |
690 | size_t |
691 | board_find_post_ids(struct board *board, size_t *npost_ids, |
692 | unsigned long **post_ids, size_t offset, size_t limit) |
693 | { |
694 | struct board_thread_map { |
695 | unsigned long id; |
696 | time_t time; |
697 | size_t nposts; |
698 | }; |
699 | struct bile_object *o; |
700 | struct board_thread_map *thread_map = NULL, tmp_map; |
701 | struct board_thread thread; |
702 | size_t nthreads, nall_post_ids, n, post_ids_size, size, seen_posts; |
703 | short i, j; |
704 | char *data; |
705 | |
706 | post_ids_size = 0; |
707 | *post_ids = NULL; |
708 | *npost_ids = 0; |
709 | nall_post_ids = 0; |
710 | |
711 | nthreads = bile_count_by_type(board->bile, BOARD_THREAD_RTYPE); |
712 | if (nthreads == 0) |
713 | return 0; |
714 | |
715 | thread_map = xcalloc(sizeof(struct board_thread_map), nthreads, |
716 | "board_find_post_ids thread_map"); |
717 | |
718 | for (n = 0; o = bile_get_nth_of_type(board->bile, n, |
719 | BOARD_THREAD_RTYPE); n++) { |
720 | if (n >= nthreads) { |
721 | xfree(&o); |
722 | break; |
723 | } |
724 | bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, o->id, &data); |
725 | bile_unmarshall_object(board->bile, board_thread_object_fields, |
726 | nboard_thread_object_fields, data, o->size, &thread, |
727 | sizeof(thread), false, "board_find_post_ids 1"); |
728 | xfree(&data); |
729 | xfree(&o); |
730 | |
731 | thread_map[n].id = thread.thread_id; |
732 | thread_map[n].time = thread.last_post_at; |
733 | thread_map[n].nposts = thread.nposts; |
734 | nall_post_ids += thread.nposts; |
735 | } |
736 | |
737 | if (offset >= nall_post_ids) |
738 | goto done; |
739 | |
740 | /* sort by last post date descending */ |
741 | for (i = 0; i < nthreads; i++) { |
742 | for (j = 0; j < nthreads - i - 1; j++) { |
743 | if (thread_map[j].time < thread_map[j + 1].time) { |
744 | tmp_map = thread_map[j]; |
745 | thread_map[j] = thread_map[j + 1]; |
746 | thread_map[j + 1] = tmp_map; |
747 | } |
748 | } |
749 | } |
750 | |
751 | /* gather threads until we run out of space for posts */ |
752 | seen_posts = 0; |
753 | for (j = 0; j < nthreads; j++) { |
754 | size = bile_read_alloc(board->bile, BOARD_THREAD_RTYPE, |
755 | thread_map[j].id, &data); |
756 | bile_unmarshall_object(board->bile, board_thread_object_fields, |
757 | nboard_thread_object_fields, data, size, &thread, sizeof(thread), |
758 | true, "board_find_post_ids 2"); |
759 | xfree(&data); |
760 | |
761 | for (i = 0; i < thread.nposts; i++) { |
762 | if (offset > 0 && seen_posts++ < offset) |
763 | continue; |
764 | |
765 | EXPAND_TO_FIT(*post_ids, post_ids_size, |
766 | ((*npost_ids) + 1) * sizeof(long), sizeof(long), |
767 | sizeof(long) * 16); |
768 | (*post_ids)[*npost_ids] = thread.post_ids[i]; |
769 | (*npost_ids)++; |
770 | |
771 | if (*npost_ids >= limit) |
772 | break; |
773 | } |
774 | |
775 | if (thread.subject != NULL) |
776 | xfree(&thread.subject); |
777 | if (thread.post_ids != NULL) |
778 | xfree(&thread.post_ids); |
779 | if (thread.parent_post_ids != NULL) |
780 | xfree(&thread.parent_post_ids); |
781 | |
782 | if (*npost_ids >= limit) |
783 | break; |
784 | } |
785 | |
786 | done: |
787 | if (thread_map != NULL) |
788 | xfree(&thread_map); |
789 | |
790 | return nall_post_ids; |
791 | } |
792 | |
793 | short |
794 | board_post_create(struct board *board, struct board_thread *thread, |
795 | struct board_post *post) |
796 | { |
797 | short ret; |
798 | char *data; |
799 | size_t size; |
800 | ssize_t n, j; |
801 | size_t insert; |
802 | |
803 | if (post->parent_post_id == 0) { |
804 | thread->thread_id = bile_next_id(board->bile, BOARD_THREAD_RTYPE); |
805 | post->thread_id = thread->thread_id; |
806 | } |
807 | |
808 | post->id = bile_next_id(board->bile, BOARD_POST_RTYPE); |
809 | post->time = Time; |
810 | |
811 | ret = bile_marshall_object(board->bile, board_post_object_fields, |
812 | nboard_post_object_fields, post, &data, &size, |
813 | "board_post_create post"); |
814 | if (ret != 0 || size == 0) { |
815 | warn("failed to marshall new post object"); |
816 | post->id = 0; |
817 | goto done; |
818 | } |
819 | if (bile_write(board->bile, BOARD_POST_RTYPE, post->id, data, |
820 | size) != size) { |
821 | warn("bile_write of new post failed! %d", bile_error(board->bile)); |
822 | post->id = 0; |
823 | xfree(&data); |
824 | goto done; |
825 | } |
826 | xfree(&data); |
827 | |
828 | thread->last_post_at = post->time; |
829 | thread->nposts++; |
830 | thread->post_ids = xreallocarray(thread->post_ids, thread->nposts, |
831 | sizeof(long)); |
832 | thread->parent_post_ids = xreallocarray(thread->parent_post_ids, |
833 | thread->nposts, sizeof(long)); |
834 | |
835 | /* |
836 | * Add new post id to thread post_ids, but put it in the right place |
837 | * so that reading post_ids will present the tree in order. Walk |
838 | * parent_post_ids and find the first match of our parent, then insert |
839 | * our new post there. This puts newest replies at the top. |
840 | */ |
841 | |
842 | insert = thread->nposts - 1; |
843 | for (n = 0; n < thread->nposts - 1; n++) { |
844 | if (thread->post_ids[n] != post->parent_post_id) |
845 | continue; |
846 | |
847 | for (j = thread->nposts - 2; j > n; j--) { |
848 | thread->post_ids[j + 1] = thread->post_ids[j]; |
849 | thread->parent_post_ids[j + 1] = thread->parent_post_ids[j]; |
850 | } |
851 | insert = n + 1; |
852 | break; |
853 | } |
854 | thread->post_ids[insert] = post->id; |
855 | thread->parent_post_ids[insert] = post->parent_post_id; |
856 | |
857 | ret = bile_marshall_object(board->bile, board_thread_object_fields, |
858 | nboard_thread_object_fields, thread, &data, &size, |
859 | "board_post_create thread"); |
860 | if (ret != 0 || size == 0) { |
861 | warn("failed to marshall thread object"); |
862 | post->id = 0; |
863 | goto done; |
864 | } |
865 | if (bile_write(board->bile, BOARD_THREAD_RTYPE, thread->thread_id, |
866 | data, size) != size) { |
867 | warn("bile_write of thread failed! %d", bile_error(board->bile)); |
868 | post->id = 0; |
869 | xfree(&data); |
870 | goto done; |
871 | } |
872 | xfree(&data); |
873 | |
874 | bile_flush(board->bile, true); |
875 | |
876 | done: |
877 | return (post->id == 0 ? -1 : 0); |
878 | } |
879 | |
880 | void |
881 | board_delete_post(struct session *s, struct board *board, |
882 | struct board_post *post, struct board_thread *thread) |
883 | { |
884 | size_t size, n, nn; |
885 | char *data; |
886 | char del[50]; |
887 | short ret; |
888 | bool childs = false; |
889 | unsigned long *new_post_ids; |
890 | unsigned long *new_parent_post_ids; |
891 | |
892 | if (thread->nposts == 1) { |
893 | bile_delete(board->bile, BOARD_THREAD_RTYPE, thread->thread_id); |
894 | bile_delete(board->bile, BOARD_POST_RTYPE, post->id); |
895 | return; |
896 | } |
897 | |
898 | for (n = 0; n < thread->nposts; n++) { |
899 | if (thread->parent_post_ids[n] == post->id) { |
900 | childs = true; |
901 | break; |
902 | } |
903 | } |
904 | |
905 | if (!childs) { |
906 | /* just zap this off the end of the tree */ |
907 | new_post_ids = xcalloc(thread->nposts - 1, sizeof(unsigned long), |
908 | "board_delete_post new_post_ids"); |
909 | new_parent_post_ids = xcalloc(thread->nposts - 1, |
910 | sizeof(unsigned long), "board_delete_post new_parent_post_ids"); |
911 | |
912 | for (n = 0, nn = 0; n < thread->nposts; n++) { |
913 | if (thread->post_ids[n] == post->id) |
914 | continue; |
915 | |
916 | new_post_ids[nn] = thread->post_ids[n]; |
917 | nn++; |
918 | } |
919 | |
920 | for (n = 0, nn = 0; n < thread->nposts; n++) { |
921 | if (thread->post_ids[n] == post->id) |
922 | continue; |
923 | |
924 | new_parent_post_ids[nn] = thread->parent_post_ids[n]; |
925 | nn++; |
926 | } |
927 | |
928 | thread->nposts--; |
929 | |
930 | xfree(&thread->post_ids); |
931 | thread->post_ids = new_post_ids; |
932 | xfree(&thread->parent_post_ids); |
933 | thread->parent_post_ids = new_parent_post_ids; |
934 | |
935 | ret = bile_marshall_object(board->bile, board_thread_object_fields, |
936 | nboard_thread_object_fields, thread, &data, &size, |
937 | "board_delete_post"); |
938 | if (ret != 0 || size == 0) { |
939 | warn("failed to marshall thread object during post delete"); |
940 | return; |
941 | } |
942 | if (bile_write(board->bile, BOARD_THREAD_RTYPE, thread->thread_id, |
943 | data, size) != size) { |
944 | warn("bile_write of updated thread after post delete failed! " |
945 | "%d", bile_error(board->bile)); |
946 | xfree(&data); |
947 | return; |
948 | } |
949 | xfree(&data); |
950 | |
951 | bile_delete(board->bile, BOARD_POST_RTYPE, post->id); |
952 | |
953 | bile_flush(board->bile, true); |
954 | return; |
955 | } |
956 | |
957 | /* all we can do is change the post to say deleted */ |
958 | if (post->body != NULL) |
959 | xfree(&post->body); |
960 | snprintf(del, sizeof(del), "[ Post deleted by %s ]", |
961 | s->user ? s->user->username : "unknown"); |
962 | post->body = xstrdup(del, "board_delete_post deleted body"); |
963 | post->body_size = strlen(post->body) + 1; |
964 | |
965 | ret = bile_marshall_object(board->bile, board_post_object_fields, |
966 | nboard_post_object_fields, post, &data, &size, "board_delete_post"); |
967 | if (ret != 0 || size == 0) { |
968 | warn("failed to marshall updated post object"); |
969 | return; |
970 | } |
971 | if (bile_write(board->bile, BOARD_POST_RTYPE, post->id, data, |
972 | size) != size) { |
973 | warn("bile_write of updated post failed! %d", |
974 | bile_error(board->bile)); |
975 | xfree(&data); |
976 | return; |
977 | } |
978 | |
979 | xfree(&data); |
980 | } |