summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--RELEASE_NOTES.md14
-rw-r--r--TODO.md16
-rw-r--r--activitypub.c2
-rw-r--r--data.c73
-rw-r--r--examples/snac-admin51
-rw-r--r--html.c173
-rw-r--r--mastoapi.c41
-rw-r--r--po/ru.po2
-rw-r--r--snac.h8
9 files changed, 337 insertions, 43 deletions
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index a8f04e8..4ca15e4 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,6 +1,16 @@
1# Release Notes 1# Release Notes
2 2
3## 2.74 3## UNRELEASED
4
5Added support for scheduled posts.
6
7Fixed incorrect poll vote format, which was causing problems in platforms like GotoSocial.
8
9Mastodon API: added support for `/api/v1/instance/peers`.
10
11Some Czech and Russian translation fixes.
12
13## 2.74 "The Days of Nicole, the Fediverse Chick"
4 14
5Added Spanish (default, Argentina and Uruguay) translation (contributed by gnemmi). 15Added Spanish (default, Argentina and Uruguay) translation (contributed by gnemmi).
6 16
@@ -22,7 +32,7 @@ Added Greek translation (contributed by uhuru).
22 32
23Added Italian translation (contributed by anzu). 33Added Italian translation (contributed by anzu).
24 34
25Mastodon API: added support for /api/v1/custom_emojis (contributed by violette). 35Mastodon API: added support for `/api/v1/custom_emojis` (contributed by violette).
26 36
27Improved Undo+Follow logic (contributed by rozenglass). 37Improved Undo+Follow logic (contributed by rozenglass).
28 38
diff --git a/TODO.md b/TODO.md
index bdb860c..baa910a 100644
--- a/TODO.md
+++ b/TODO.md
@@ -6,16 +6,12 @@ Investigate the problem with boosts inside the same instance (see https://codebe
6 6
7Editing / Updating a post does not index newly added hashtags. 7Editing / Updating a post does not index newly added hashtags.
8 8
9Wrong level of message visibility when using the Mastodon API: https://codeberg.org/grunfink/snac2/issues/200#issuecomment-2351042
10
11Unfollowing guppe groups seems to work (http status of 200), but messages continue to arrive as if it didn't. 9Unfollowing guppe groups seems to work (http status of 200), but messages continue to arrive as if it didn't.
12 10
13Important: deleting a follower should do more that just delete the object, see https://codeberg.org/grunfink/snac2/issues/43#issuecomment-956721 11Important: deleting a follower should do more that just delete the object, see https://codeberg.org/grunfink/snac2/issues/43#issuecomment-956721
14 12
15## Wishlist 13## Wishlist
16 14
17Each notification should show a link to the full thread, to see it in context.
18
19The instance timeline should also show boosts from users. 15The instance timeline should also show boosts from users.
20 16
21Mastoapi: implement /v1/conversations. 17Mastoapi: implement /v1/conversations.
@@ -30,14 +26,10 @@ Integrate "Added handling for International Domain Names" PR https://codeberg.or
30 26
31Do something about Akkoma and Misskey's quoted replies (they use the `quoteUrl` field instead of `inReplyTo`). 27Do something about Akkoma and Misskey's quoted replies (they use the `quoteUrl` field instead of `inReplyTo`).
32 28
33Add a list of hashtags to drop.
34
35Take a look at crashes in the brittle Mastodon official app (crashes when hitting the reply button, crashes or 'ownVotes is null' errors when trying to show polls). 29Take a look at crashes in the brittle Mastodon official app (crashes when hitting the reply button, crashes or 'ownVotes is null' errors when trying to show polls).
36 30
37The 'history' pages are just monthly HTML snapshots of the local timeline. This is ok and cheap and easy, but is problematic if you e.g. intentionally delete a post because it will remain there in the history forever. If you activate local timeline purging, purged entries will remain in the history as 'ghosts', which may or may not be what the user wants. 31The 'history' pages are just monthly HTML snapshots of the local timeline. This is ok and cheap and easy, but is problematic if you e.g. intentionally delete a post because it will remain there in the history forever. If you activate local timeline purging, purged entries will remain in the history as 'ghosts', which may or may not be what the user wants.
38 32
39The actual storage system wastes too much disk space (lots of small files that really consume 4k of storage). Consider alternatives.
40
41## Closed 33## Closed
42 34
43Start a TODO file (2022-08-25T10:07:44+0200). 35Start a TODO file (2022-08-25T10:07:44+0200).
@@ -367,3 +359,11 @@ Add support for /authorize_interaction (whatever it is) (2025-01-16T14:45:28+010
367Implement following of hashtags (this is not trivial) (2025-01-30T16:12:16+0100). 359Implement following of hashtags (this is not trivial) (2025-01-30T16:12:16+0100).
368 360
369Add support for subscribing and posting to relays (see https://codeberg.org/grunfink/snac2/issues/216 for more information) (2025-01-30T16:12:34+0100). 361Add support for subscribing and posting to relays (see https://codeberg.org/grunfink/snac2/issues/216 for more information) (2025-01-30T16:12:34+0100).
362
363Wrong level of message visibility when using the Mastodon API: https://codeberg.org/grunfink/snac2/issues/200#issuecomment-2351042 (2025-03-23T15:44:35+0100).
364
365Each notification should show a link to the full thread, to see it in context (2025-03-23T15:44:50+0100).
366
367Add a list of hashtags to drop (2025-03-23T15:45:30+0100).
368
369The actual storage system wastes too much disk space (lots of small files that really consume 4k of storage). Consider alternatives (2025-03-23T15:46:02+0100).
diff --git a/activitypub.c b/activitypub.c
index c00c371..4c22c25 100644
--- a/activitypub.c
+++ b/activitypub.c
@@ -2759,6 +2759,8 @@ int process_user_queue(snac *snac)
2759 cnt++; 2759 cnt++;
2760 } 2760 }
2761 2761
2762 scheduled_process(snac);
2763
2762 return cnt; 2764 return cnt;
2763} 2765}
2764 2766
diff --git a/data.c b/data.c
index ce040dd..6661472 100644
--- a/data.c
+++ b/data.c
@@ -1929,6 +1929,70 @@ xs_list *draft_list(snac *user)
1929} 1929}
1930 1930
1931 1931
1932/** scheduled posts **/
1933
1934int is_scheduled(snac *user, const char *id)
1935/* returns true if this note is scheduled for future sending */
1936{
1937 return object_user_cache_in(user, id, "sched");
1938}
1939
1940
1941void schedule_del(snac *user, const char *id)
1942/* deletes an scheduled post */
1943{
1944 object_user_cache_del(user, id, "sched");
1945}
1946
1947
1948void schedule_add(snac *user, const char *id, const xs_dict *msg)
1949/* schedules this post for later */
1950{
1951 /* delete from the index, in case it was already there */
1952 schedule_del(user, id);
1953
1954 /* overwrite object */
1955 object_add_ow(id, msg);
1956
1957 /* [re]add to the index */
1958 object_user_cache_add(user, id, "sched");
1959}
1960
1961
1962xs_list *scheduled_list(snac *user)
1963/* return the list of scheduled posts */
1964{
1965 return object_user_cache_list(user, "sched", XS_ALL, 1);
1966}
1967
1968
1969void scheduled_process(snac *user)
1970/* processes the scheduled list, sending those ready to be sent */
1971{
1972 xs *posts = scheduled_list(user);
1973 const char *md5;
1974 xs *right_now = xs_str_utctime(0, ISO_DATE_SPEC);
1975
1976 xs_list_foreach(posts, md5) {
1977 xs *msg = NULL;
1978
1979 if (valid_status(object_get_by_md5(md5, &msg))) {
1980 if (strcmp(xs_dict_get(msg, "published"), right_now) < 0) {
1981 /* due date! */
1982 const char *id = xs_dict_get(msg, "id");
1983
1984 timeline_add(user, id, msg);
1985
1986 xs *c_msg = msg_create(user, msg);
1987 enqueue_message(user, c_msg);
1988
1989 schedule_del(user, id);
1990 }
1991 }
1992 }
1993}
1994
1995
1932/** hiding **/ 1996/** hiding **/
1933 1997
1934xs_str *_hidden_fn(snac *snac, const char *id) 1998xs_str *_hidden_fn(snac *snac, const char *id)
@@ -2619,10 +2683,9 @@ xs_list *inbox_list(void)
2619 xs_list *ibl = xs_list_new(); 2683 xs_list *ibl = xs_list_new();
2620 xs *spec = xs_fmt("%s/inbox/" "*", srv_basedir); 2684 xs *spec = xs_fmt("%s/inbox/" "*", srv_basedir);
2621 xs *files = xs_glob(spec, 0, 0); 2685 xs *files = xs_glob(spec, 0, 0);
2622 xs_list *p = files;
2623 const xs_val *v; 2686 const xs_val *v;
2624 2687
2625 while (xs_list_iter(&p, &v)) { 2688 xs_list_foreach(files, v) {
2626 FILE *f; 2689 FILE *f;
2627 2690
2628 if ((f = fopen(v, "r")) != NULL) { 2691 if ((f = fopen(v, "r")) != NULL) {
@@ -2630,7 +2693,9 @@ xs_list *inbox_list(void)
2630 2693
2631 if (line && *line) { 2694 if (line && *line) {
2632 line = xs_strip_i(line); 2695 line = xs_strip_i(line);
2633 ibl = xs_list_append(ibl, line); 2696
2697 if (!is_instance_blocked(line))
2698 ibl = xs_list_append(ibl, line);
2634 } 2699 }
2635 2700
2636 fclose(f); 2701 fclose(f);
@@ -3696,7 +3761,7 @@ void purge_user(snac *snac)
3696 _purge_user_subdir(snac, "public", pub_days); 3761 _purge_user_subdir(snac, "public", pub_days);
3697 3762
3698 const char *idxs[] = { "followers.idx", "private.idx", "public.idx", 3763 const char *idxs[] = { "followers.idx", "private.idx", "public.idx",
3699 "pinned.idx", "bookmark.idx", "draft.idx", NULL }; 3764 "pinned.idx", "bookmark.idx", "draft.idx", "sched.idx", NULL };
3700 3765
3701 for (n = 0; idxs[n]; n++) { 3766 for (n = 0; idxs[n]; n++) {
3702 xs *idx = xs_fmt("%s/%s", snac->basedir, idxs[n]); 3767 xs *idx = xs_fmt("%s/%s", snac->basedir, idxs[n]);
diff --git a/examples/snac-admin b/examples/snac-admin
new file mode 100644
index 0000000..e51e28b
--- /dev/null
+++ b/examples/snac-admin
@@ -0,0 +1,51 @@
1#!/usr/bin/env bash
2##
3## SNAC-ADMIN
4## a simple script that is supposed to improve
5## a snac admin's life, especially when snac
6## is being run as a systemd.unit with
7## DynamicUser=yes enabled.
8## Please make sure to adjust SNAC_DIR
9## down below according to your setup.
10##
11## USAGE
12## snac-admin state
13## snac-admin adduser rikkert
14## snac-admin block example.org
15## snac-admin verify_links lisa
16## ...
17##
18## Author: @chris@social.shtrophic.net
19##
20## Released into the public domain
21##
22
23set -e
24
25SNAC_PID=$(pidof snac)
26SNAC_DIR=/var/lib/snac
27
28SNAC_VERB=$1
29shift
30
31if [ -z $SNAC_PID ]; then
32 echo "no such process" >&2
33 exit 1
34fi
35
36if [ $(id -u) -ne 0 ]; then
37 echo "not root" >&2
38 exit 1
39fi
40
41if [ ! -d $SNAC_DIR ]; then
42 echo "$SNAC_DIR is not a directory" >&2
43 exit 1
44fi
45
46if [ -z $SNAC_VERB ]; then
47 echo "no arguments" >&2
48 exit 1
49fi
50
51nsenter -ae -S follow -G follow -t $SNAC_PID -- snac $SNAC_VERB $SNAC_DIR $@
diff --git a/html.c b/html.c
index 78a9854..a598038 100644
--- a/html.c
+++ b/html.c
@@ -14,6 +14,7 @@
14#include "xs_curl.h" 14#include "xs_curl.h"
15#include "xs_unicode.h" 15#include "xs_unicode.h"
16#include "xs_url.h" 16#include "xs_url.h"
17#include "xs_random.h"
17 18
18#include "snac.h" 19#include "snac.h"
19 20
@@ -72,6 +73,9 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
72 const xs_dict *v; 73 const xs_dict *v;
73 int c = 0; 74 int c = 0;
74 75
76 xs_set rep_emoji;
77 xs_set_init(&rep_emoji);
78
75 while (xs_list_next(tag_list, &v, &c)) { 79 while (xs_list_next(tag_list, &v, &c)) {
76 const char *t = xs_dict_get(v, "type"); 80 const char *t = xs_dict_get(v, "type");
77 81
@@ -79,6 +83,10 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
79 const char *n = xs_dict_get(v, "name"); 83 const char *n = xs_dict_get(v, "name");
80 const xs_dict *i = xs_dict_get(v, "icon"); 84 const xs_dict *i = xs_dict_get(v, "icon");
81 85
86 /* avoid repeated emojis (Misskey seems to return this) */
87 if (xs_set_add(&rep_emoji, n) == 0)
88 continue;
89
82 if (xs_is_string(n) && xs_is_dict(i)) { 90 if (xs_is_string(n) && xs_is_dict(i)) {
83 const char *u = xs_dict_get(i, "url"); 91 const char *u = xs_dict_get(i, "url");
84 const char *mt = xs_dict_get(i, "mediaType"); 92 const char *mt = xs_dict_get(i, "mediaType");
@@ -93,6 +101,8 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
93 xs_html_attr("loading", "lazy"), 101 xs_html_attr("loading", "lazy"),
94 xs_html_attr("src", url), 102 xs_html_attr("src", url),
95 xs_html_attr("alt", n), 103 xs_html_attr("alt", n),
104 xs_html_attr("title", n),
105 xs_html_attr("class", "snac-emoji"),
96 xs_html_attr("style", style)); 106 xs_html_attr("style", style));
97 107
98 xs *s1 = xs_html_render(img); 108 xs *s1 = xs_html_render(img);
@@ -104,6 +114,8 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
104 } 114 }
105 } 115 }
106 } 116 }
117
118 xs_set_free(&rep_emoji);
107 } 119 }
108 120
109 return s; 121 return s;
@@ -339,7 +351,7 @@ xs_html *html_note(snac *user, const char *summary,
339 const xs_val *mnt_only, const char *redir, 351 const xs_val *mnt_only, const char *redir,
340 const char *in_reply_to, int poll, 352 const char *in_reply_to, int poll,
341 const xs_list *att_files, const xs_list *att_alt_texts, 353 const xs_list *att_files, const xs_list *att_alt_texts,
342 int is_draft) 354 int is_draft, const char *published)
343/* Yes, this is a FUCKTON of arguments and I'm a bit embarrased */ 355/* Yes, this is a FUCKTON of arguments and I'm a bit embarrased */
344{ 356{
345 xs *action = xs_fmt("%s/admin/note", user->actor); 357 xs *action = xs_fmt("%s/admin/note", user->actor);
@@ -429,6 +441,36 @@ xs_html *html_note(snac *user, const char *summary,
429 xs_html_attr("name", "is_draft"), 441 xs_html_attr("name", "is_draft"),
430 xs_html_attr(is_draft ? "checked" : "", NULL)))); 442 xs_html_attr(is_draft ? "checked" : "", NULL))));
431 443
444 /* post date and time */
445 xs *post_date = NULL;
446 xs *post_time = NULL;
447
448 if (xs_is_string(published)) {
449 time_t t = xs_parse_iso_date(published, 0);
450
451 if (t > 0) {
452 post_date = xs_str_time(t, "%Y-%m-%d", 1);
453 post_time = xs_str_time(t, "%H:%M:%S", 1);
454 }
455 }
456
457 if (edit_id == NULL || is_draft || is_scheduled(user, edit_id)) {
458 xs_html_add(form,
459 xs_html_tag("p",
460 xs_html_text(L("Post date and time (empty, right now; in the future, schedule for later):")),
461 xs_html_sctag("br", NULL),
462 xs_html_sctag("input",
463 xs_html_attr("type", "date"),
464 xs_html_attr("value", post_date ? post_date : ""),
465 xs_html_attr("name", "post_date")),
466 xs_html_text(" "),
467 xs_html_sctag("input",
468 xs_html_attr("type", "time"),
469 xs_html_attr("value", post_time ? post_time : ""),
470 xs_html_attr("step", "1"),
471 xs_html_attr("name", "post_time"))));
472 }
473
432 if (edit_id) 474 if (edit_id)
433 xs_html_add(form, 475 xs_html_add(form,
434 xs_html_sctag("input", 476 xs_html_sctag("input",
@@ -1116,7 +1158,7 @@ xs_html *html_top_controls(snac *user)
1116 NULL, NULL, 1158 NULL, NULL,
1117 xs_stock(XSTYPE_FALSE), "", 1159 xs_stock(XSTYPE_FALSE), "",
1118 xs_stock(XSTYPE_FALSE), NULL, 1160 xs_stock(XSTYPE_FALSE), NULL,
1119 NULL, 1, NULL, NULL, 0), 1161 NULL, 1, NULL, NULL, 0, NULL),
1120 1162
1121 /** operations **/ 1163 /** operations **/
1122 xs_html_tag("details", 1164 xs_html_tag("details",
@@ -1774,7 +1816,8 @@ xs_html *html_entry_controls(snac *user, const char *actor,
1774 id, NULL, 1816 id, NULL,
1775 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"), 1817 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
1776 xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir, 1818 xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
1777 NULL, 0, att_files, att_alt_texts, is_draft(user, id))), 1819 NULL, 0, att_files, att_alt_texts, is_draft(user, id),
1820 xs_dict_get(msg, "published"))),
1778 xs_html_tag("p", NULL)); 1821 xs_html_tag("p", NULL));
1779 } 1822 }
1780 1823
@@ -1793,7 +1836,7 @@ xs_html *html_entry_controls(snac *user, const char *actor,
1793 NULL, NULL, 1836 NULL, NULL,
1794 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"), 1837 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
1795 xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir, 1838 xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
1796 id, 0, NULL, NULL, 0)), 1839 id, 0, NULL, NULL, 0, NULL)),
1797 xs_html_tag("p", NULL)); 1840 xs_html_tag("p", NULL));
1798 } 1841 }
1799 1842
@@ -2689,6 +2732,11 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2689 } 2732 }
2690 } 2733 }
2691 2734
2735 /* add an invisible hr, to help differentiate between posts in text browsers */
2736 xs_html_add(entry_top,
2737 xs_html_sctag("hr",
2738 xs_html_attr("hidden", NULL)));
2739
2692 return entry_top; 2740 return entry_top;
2693} 2741}
2694 2742
@@ -2830,6 +2878,18 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
2830 xs_html_text(L("drafts"))))); 2878 xs_html_text(L("drafts")))));
2831 } 2879 }
2832 2880
2881 {
2882 /* show the list of scheduled posts */
2883 xs *url = xs_fmt("%s/sched", user->actor);
2884 xs_html_add(lol,
2885 xs_html_tag("li",
2886 xs_html_tag("a",
2887 xs_html_attr("href", url),
2888 xs_html_attr("class", "snac-list-link"),
2889 xs_html_attr("title", L("Scheduled posts")),
2890 xs_html_text(L("scheduled posts")))));
2891 }
2892
2833 /* the list of followed hashtags */ 2893 /* the list of followed hashtags */
2834 const char *followed_hashtags = xs_dict_get(user->config, "followed_hashtags"); 2894 const char *followed_hashtags = xs_dict_get(user->config, "followed_hashtags");
2835 2895
@@ -3130,7 +3190,7 @@ xs_html *html_people_list(snac *user, xs_list *list, const char *header, const c
3130 NULL, actor_id, 3190 NULL, actor_id,
3131 xs_stock(XSTYPE_FALSE), "", 3191 xs_stock(XSTYPE_FALSE), "",
3132 xs_stock(XSTYPE_FALSE), NULL, 3192 xs_stock(XSTYPE_FALSE), NULL,
3133 NULL, 0, NULL, NULL, 0), 3193 NULL, 0, NULL, NULL, 0, NULL),
3134 xs_html_tag("p", NULL)); 3194 xs_html_tag("p", NULL));
3135 3195
3136 xs_html_add(snac_post, snac_controls); 3196 xs_html_add(snac_post, snac_controls);
@@ -3879,6 +3939,21 @@ int html_get_handler(const xs_dict *req, const char *q_path,
3879 } 3939 }
3880 } 3940 }
3881 else 3941 else
3942 if (strcmp(p_path, "sched") == 0) { /** list of scheduled posts **/
3943 if (!login(&snac, req)) {
3944 *body = xs_dup(uid);
3945 status = HTTP_STATUS_UNAUTHORIZED;
3946 }
3947 else {
3948 xs *list = scheduled_list(&snac);
3949
3950 *body = html_timeline(&snac, list, 0, skip, show,
3951 0, L("Scheduled posts"), "", 0, error);
3952 *b_size = strlen(*body);
3953 status = HTTP_STATUS_OK;
3954 }
3955 }
3956 else
3882 if (xs_startswith(p_path, "list/")) { /** list timelines **/ 3957 if (xs_startswith(p_path, "list/")) { /** list timelines **/
3883 if (!login(&snac, req)) { 3958 if (!login(&snac, req)) {
3884 *body = xs_dup(uid); 3959 *body = xs_dup(uid);
@@ -4175,12 +4250,14 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4175 snac_debug(&snac, 1, xs_fmt("web action '%s' received", p_path)); 4250 snac_debug(&snac, 1, xs_fmt("web action '%s' received", p_path));
4176 4251
4177 /* post note */ 4252 /* post note */
4178 const xs_str *content = xs_dict_get(p_vars, "content"); 4253 const char *content = xs_dict_get(p_vars, "content");
4179 const xs_str *in_reply_to = xs_dict_get(p_vars, "in_reply_to"); 4254 const char *in_reply_to = xs_dict_get(p_vars, "in_reply_to");
4180 const xs_str *to = xs_dict_get(p_vars, "to"); 4255 const char *to = xs_dict_get(p_vars, "to");
4181 const xs_str *sensitive = xs_dict_get(p_vars, "sensitive"); 4256 const char *sensitive = xs_dict_get(p_vars, "sensitive");
4182 const xs_str *summary = xs_dict_get(p_vars, "summary"); 4257 const char *summary = xs_dict_get(p_vars, "summary");
4183 const xs_str *edit_id = xs_dict_get(p_vars, "edit_id"); 4258 const char *edit_id = xs_dict_get(p_vars, "edit_id");
4259 const char *post_date = xs_dict_get_def(p_vars, "post_date", "");
4260 const char *post_time = xs_dict_get_def(p_vars, "post_time", "");
4184 int priv = !xs_is_null(xs_dict_get(p_vars, "mentioned_only")); 4261 int priv = !xs_is_null(xs_dict_get(p_vars, "mentioned_only"));
4185 int store_as_draft = !xs_is_null(xs_dict_get(p_vars, "is_draft")); 4262 int store_as_draft = !xs_is_null(xs_dict_get(p_vars, "is_draft"));
4186 xs *attach_list = xs_list_new(); 4263 xs *attach_list = xs_list_new();
@@ -4210,9 +4287,12 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4210 const char *fn = xs_list_get(attach_file, 0); 4287 const char *fn = xs_list_get(attach_file, 0);
4211 4288
4212 if (xs_is_string(fn) && *fn != '\0') { 4289 if (xs_is_string(fn) && *fn != '\0') {
4213 char *ext = strrchr(fn, '.'); 4290 char rnd[32];
4214 xs *hash = xs_md5_hex(fn, strlen(fn)); 4291 xs_rnd_buf(rnd, sizeof(rnd));
4215 xs *id = xs_fmt("%s%s", hash, ext); 4292
4293 const char *ext = strrchr(fn, '.');
4294 xs *hash = xs_md5_hex(rnd, strlen(rnd));
4295 xs *id = xs_fmt("post-%s%s", hash, ext ? ext : "");
4216 xs *url = xs_fmt("%s/s/%s", snac.actor, id); 4296 xs *url = xs_fmt("%s/s/%s", snac.actor, id);
4217 int fo = xs_number_get(xs_list_get(attach_file, 1)); 4297 int fo = xs_number_get(xs_list_get(attach_file, 1));
4218 int fs = xs_number_get(xs_list_get(attach_file, 2)); 4298 int fs = xs_number_get(xs_list_get(attach_file, 2));
@@ -4268,6 +4348,29 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4268 msg = xs_dict_set(msg, "summary", xs_is_null(summary) ? "..." : summary); 4348 msg = xs_dict_set(msg, "summary", xs_is_null(summary) ? "..." : summary);
4269 } 4349 }
4270 4350
4351 if (xs_is_string(post_date) && *post_date) {
4352 xs *local_pubdate = xs_fmt("%sT%s", post_date,
4353 xs_is_string(post_time) && *post_time ? post_time : "00:00:00");
4354
4355 time_t t = xs_parse_iso_date(local_pubdate, 1);
4356
4357 if (t != 0) {
4358 xs *iso_date = xs_str_iso_date(t);
4359 msg = xs_dict_set(msg, "published", iso_date);
4360
4361 snac_debug(&snac, 1, xs_fmt("Published date: [%s]", iso_date));
4362 }
4363 else
4364 snac_log(&snac, xs_fmt("Invalid post date: [%s]", local_pubdate));
4365 }
4366
4367 /* is the published date from the future? */
4368 int future_post = 0;
4369 xs *right_now = xs_str_utctime(0, ISO_DATE_SPEC);
4370
4371 if (strcmp(xs_dict_get(msg, "published"), right_now) > 0)
4372 future_post = 1;
4373
4271 if (xs_is_null(edit_id)) { 4374 if (xs_is_null(edit_id)) {
4272 /* new message */ 4375 /* new message */
4273 const char *id = xs_dict_get(msg, "id"); 4376 const char *id = xs_dict_get(msg, "id");
@@ -4275,6 +4378,10 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4275 if (store_as_draft) { 4378 if (store_as_draft) {
4276 draft_add(&snac, id, msg); 4379 draft_add(&snac, id, msg);
4277 } 4380 }
4381 else
4382 if (future_post) {
4383 schedule_add(&snac, id, msg);
4384 }
4278 else { 4385 else {
4279 c_msg = msg_create(&snac, msg); 4386 c_msg = msg_create(&snac, msg);
4280 timeline_add(&snac, id, msg); 4387 timeline_add(&snac, id, msg);
@@ -4286,7 +4393,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4286 4393
4287 if (valid_status(object_get(edit_id, &p_msg))) { 4394 if (valid_status(object_get(edit_id, &p_msg))) {
4288 /* copy relevant fields from previous version */ 4395 /* copy relevant fields from previous version */
4289 char *fields[] = { "id", "context", "url", "published", 4396 char *fields[] = { "id", "context", "url",
4290 "to", "inReplyTo", NULL }; 4397 "to", "inReplyTo", NULL };
4291 int n; 4398 int n;
4292 4399
@@ -4302,18 +4409,34 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4302 if (is_draft(&snac, edit_id)) { 4409 if (is_draft(&snac, edit_id)) {
4303 /* message was previously a draft; it's a create activity */ 4410 /* message was previously a draft; it's a create activity */
4304 4411
4305 /* set the published field to now */ 4412 /* if the date is from the past, overwrite it with right_now */
4306 xs *published = xs_str_utctime(0, ISO_DATE_SPEC); 4413 if (strcmp(xs_dict_get(msg, "published"), right_now) < 0) {
4307 msg = xs_dict_set(msg, "published", published); 4414 snac_debug(&snac, 1, xs_fmt("setting draft ancient date to %s", right_now));
4415 msg = xs_dict_set(msg, "published", right_now);
4416 }
4308 4417
4309 /* overwrite object */ 4418 /* overwrite object */
4310 object_add_ow(edit_id, msg); 4419 object_add_ow(edit_id, msg);
4311 4420
4312 c_msg = msg_create(&snac, msg); 4421 if (future_post) {
4313 timeline_add(&snac, edit_id, msg); 4422 schedule_add(&snac, edit_id, msg);
4423 }
4424 else {
4425 c_msg = msg_create(&snac, msg);
4426 timeline_add(&snac, edit_id, msg);
4427 }
4428
4314 draft_del(&snac, edit_id); 4429 draft_del(&snac, edit_id);
4315 } 4430 }
4431 else
4432 if (is_scheduled(&snac, edit_id)) {
4433 /* editing an scheduled post; just update it */
4434 schedule_add(&snac, edit_id, msg);
4435 }
4316 else { 4436 else {
4437 /* ignore the (possibly changed) published date */
4438 msg = xs_dict_set(msg, "published", xs_dict_get(p_msg, "published"));
4439
4317 /* set the updated field */ 4440 /* set the updated field */
4318 xs *updated = xs_str_utctime(0, ISO_DATE_SPEC); 4441 xs *updated = xs_str_utctime(0, ISO_DATE_SPEC);
4319 msg = xs_dict_set(msg, "updated", updated); 4442 msg = xs_dict_set(msg, "updated", updated);
@@ -4398,6 +4521,9 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4398 if (is_draft(&snac, id)) 4521 if (is_draft(&snac, id))
4399 draft_del(&snac, id); 4522 draft_del(&snac, id);
4400 else 4523 else
4524 if (is_scheduled(&snac, id))
4525 schedule_del(&snac, id);
4526 else
4401 hide(&snac, id); 4527 hide(&snac, id);
4402 } 4528 }
4403 else 4529 else
@@ -4493,6 +4619,8 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4493 4619
4494 draft_del(&snac, id); 4620 draft_del(&snac, id);
4495 4621
4622 schedule_del(&snac, id);
4623
4496 snac_log(&snac, xs_fmt("deleted entry %s", id)); 4624 snac_log(&snac, xs_fmt("deleted entry %s", id));
4497 } 4625 }
4498 } 4626 }
@@ -4636,7 +4764,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4636 if (xs_startswith(mimetype, "image/")) { 4764 if (xs_startswith(mimetype, "image/")) {
4637 const char *ext = strrchr(fn, '.'); 4765 const char *ext = strrchr(fn, '.');
4638 xs *hash = xs_md5_hex(fn, strlen(fn)); 4766 xs *hash = xs_md5_hex(fn, strlen(fn));
4639 xs *id = xs_fmt("%s%s", hash, ext); 4767 xs *id = xs_fmt("%s-%s%s", uploads[n], hash, ext ? ext : "");
4640 xs *url = xs_fmt("%s/s/%s", snac.actor, id); 4768 xs *url = xs_fmt("%s/s/%s", snac.actor, id);
4641 int fo = xs_number_get(xs_list_get(uploaded_file, 1)); 4769 int fo = xs_number_get(xs_list_get(uploaded_file, 1));
4642 int fs = xs_number_get(xs_list_get(uploaded_file, 2)); 4770 int fs = xs_number_get(xs_list_get(uploaded_file, 2));
@@ -4705,6 +4833,9 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4705 /* set the option */ 4833 /* set the option */
4706 msg = xs_dict_append(msg, "name", v); 4834 msg = xs_dict_append(msg, "name", v);
4707 4835
4836 /* delete the content */
4837 msg = xs_dict_del(msg, "content");
4838
4708 xs *c_msg = msg_create(&snac, msg); 4839 xs *c_msg = msg_create(&snac, msg);
4709 4840
4710 enqueue_message(&snac, c_msg); 4841 enqueue_message(&snac, c_msg);
diff --git a/mastoapi.c b/mastoapi.c
index 8d61681..d93afc5 100644
--- a/mastoapi.c
+++ b/mastoapi.c
@@ -2256,6 +2256,25 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
2256 status = HTTP_STATUS_OK; 2256 status = HTTP_STATUS_OK;
2257 } 2257 }
2258 else 2258 else
2259 if (strcmp(cmd, "/v1/instance/peers") == 0) { /** **/
2260 /* get the collected inbox list as the instances "this domain is aware of" */
2261 xs *list = inbox_list();
2262 xs *peers = xs_list_new();
2263 const char *inbox;
2264
2265 xs_list_foreach(list, inbox) {
2266 xs *l = xs_split(inbox, "/");
2267 const char *domain = xs_list_get(l, 2);
2268
2269 if (xs_is_string(domain))
2270 peers = xs_list_append(peers, domain);
2271 }
2272
2273 *body = xs_json_dumps(peers, 4);
2274 *ctype = "application/json";
2275 status = HTTP_STATUS_OK;
2276 }
2277 else
2259 if (xs_startswith(cmd, "/v1/statuses/")) { /** **/ 2278 if (xs_startswith(cmd, "/v1/statuses/")) { /** **/
2260 /* information about a status */ 2279 /* information about a status */
2261 if (logged_in) { 2280 if (logged_in) {
@@ -2707,14 +2726,24 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
2707 msg = xs_dict_set(msg, "summary", summary); 2726 msg = xs_dict_set(msg, "summary", summary);
2708 } 2727 }
2709 2728
2710 /* store */ 2729 /* scheduled? */
2711 timeline_add(&snac, xs_dict_get(msg, "id"), msg); 2730 const char *scheduled_at = xs_dict_get(args, "scheduled_at");
2712 2731
2713 /* 'Create' message */ 2732 if (xs_is_string(scheduled_at) && *scheduled_at) {
2714 xs *c_msg = msg_create(&snac, msg); 2733 msg = xs_dict_set(msg, "published", scheduled_at);
2715 enqueue_message(&snac, c_msg);
2716 2734
2717 timeline_touch(&snac); 2735 schedule_add(&snac, xs_dict_get(msg, "id"), msg);
2736 }
2737 else {
2738 /* store */
2739 timeline_add(&snac, xs_dict_get(msg, "id"), msg);
2740
2741 /* 'Create' message */
2742 xs *c_msg = msg_create(&snac, msg);
2743 enqueue_message(&snac, c_msg);
2744
2745 timeline_touch(&snac);
2746 }
2718 2747
2719 /* convert to a mastodon status as a response code */ 2748 /* convert to a mastodon status as a response code */
2720 xs *st = mastoapi_status(&snac, msg); 2749 xs *st = mastoapi_status(&snac, msg);
diff --git a/po/ru.po b/po/ru.po
index 90195d0..e3bdd2c 100644
--- a/po/ru.po
+++ b/po/ru.po
@@ -446,7 +446,7 @@ msgstr "Событие"
446 446
447#: html.c:1977 html.c:2006 447#: html.c:1977 html.c:2006
448msgid "boosted" 448msgid "boosted"
449msgstr "продио" 449msgstr "под"
450 450
451#: html.c:2022 451#: html.c:2022
452msgid "in reply to" 452msgid "in reply to"
diff --git a/snac.h b/snac.h
index 142ebc1..0d2aafe 100644
--- a/snac.h
+++ b/snac.h
@@ -1,7 +1,7 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
3 3
4#define VERSION "2.74" 4#define VERSION "2.75-dev"
5 5
6#define USER_AGENT "snac/" VERSION 6#define USER_AGENT "snac/" VERSION
7 7
@@ -205,6 +205,12 @@ void draft_del(snac *user, const char *id);
205void draft_add(snac *user, const char *id, const xs_dict *msg); 205void draft_add(snac *user, const char *id, const xs_dict *msg);
206xs_list *draft_list(snac *user); 206xs_list *draft_list(snac *user);
207 207
208int is_scheduled(snac *user, const char *id);
209void schedule_del(snac *user, const char *id);
210void schedule_add(snac *user, const char *id, const xs_dict *msg);
211xs_list *scheduled_list(snac *user);
212void scheduled_process(snac *user);
213
208int limited(snac *user, const char *id, int cmd); 214int limited(snac *user, const char *id, int cmd);
209#define is_limited(user, id) limited((user), (id), 0) 215#define is_limited(user, id) limited((user), (id), 0)
210#define limit(user, id) limited((user), (id), 1) 216#define limit(user, id) limited((user), (id), 1)