commit 3c427fa
Merge pull request 'master' (#1) from grunfink/snac2:master into master Reviewed-on: https://codeberg.org/pmjv/snac2/pulls/1
+12,
-2
1@@ -1,6 +1,16 @@
2 # Release Notes
3
4-## 2.74
5+## UNRELEASED
6+
7+Added support for scheduled posts.
8+
9+Fixed incorrect poll vote format, which was causing problems in platforms like GotoSocial.
10+
11+Mastodon API: added support for `/api/v1/instance/peers`.
12+
13+Some Czech and Russian translation fixes.
14+
15+## 2.74 "The Days of Nicole, the Fediverse Chick"
16
17 Added Spanish (default, Argentina and Uruguay) translation (contributed by gnemmi).
18
19@@ -22,7 +32,7 @@ Added Greek translation (contributed by uhuru).
20
21 Added Italian translation (contributed by anzu).
22
23-Mastodon API: added support for /api/v1/custom_emojis (contributed by violette).
24+Mastodon API: added support for `/api/v1/custom_emojis` (contributed by violette).
25
26 Improved Undo+Follow logic (contributed by rozenglass).
27
M
TODO.md
+8,
-8
1@@ -6,16 +6,12 @@ Investigate the problem with boosts inside the same instance (see https://codebe
2
3 Editing / Updating a post does not index newly added hashtags.
4
5-Wrong level of message visibility when using the Mastodon API: https://codeberg.org/grunfink/snac2/issues/200#issuecomment-2351042
6-
7 Unfollowing guppe groups seems to work (http status of 200), but messages continue to arrive as if it didn't.
8
9 Important: deleting a follower should do more that just delete the object, see https://codeberg.org/grunfink/snac2/issues/43#issuecomment-956721
10
11 ## Wishlist
12
13-Each notification should show a link to the full thread, to see it in context.
14-
15 The instance timeline should also show boosts from users.
16
17 Mastoapi: implement /v1/conversations.
18@@ -30,14 +26,10 @@ Integrate "Added handling for International Domain Names" PR https://codeberg.or
19
20 Do something about Akkoma and Misskey's quoted replies (they use the `quoteUrl` field instead of `inReplyTo`).
21
22-Add a list of hashtags to drop.
23-
24 Take 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).
25
26 The '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.
27
28-The actual storage system wastes too much disk space (lots of small files that really consume 4k of storage). Consider alternatives.
29-
30 ## Closed
31
32 Start a TODO file (2022-08-25T10:07:44+0200).
33@@ -367,3 +359,11 @@ Add support for /authorize_interaction (whatever it is) (2025-01-16T14:45:28+010
34 Implement following of hashtags (this is not trivial) (2025-01-30T16:12:16+0100).
35
36 Add support for subscribing and posting to relays (see https://codeberg.org/grunfink/snac2/issues/216 for more information) (2025-01-30T16:12:34+0100).
37+
38+Wrong level of message visibility when using the Mastodon API: https://codeberg.org/grunfink/snac2/issues/200#issuecomment-2351042 (2025-03-23T15:44:35+0100).
39+
40+Each notification should show a link to the full thread, to see it in context (2025-03-23T15:44:50+0100).
41+
42+Add a list of hashtags to drop (2025-03-23T15:45:30+0100).
43+
44+The 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).
+2,
-0
1@@ -2759,6 +2759,8 @@ int process_user_queue(snac *snac)
2 cnt++;
3 }
4
5+ scheduled_process(snac);
6+
7 return cnt;
8 }
9
M
data.c
+69,
-4
1@@ -1929,6 +1929,70 @@ xs_list *draft_list(snac *user)
2 }
3
4
5+/** scheduled posts **/
6+
7+int is_scheduled(snac *user, const char *id)
8+/* returns true if this note is scheduled for future sending */
9+{
10+ return object_user_cache_in(user, id, "sched");
11+}
12+
13+
14+void schedule_del(snac *user, const char *id)
15+/* deletes an scheduled post */
16+{
17+ object_user_cache_del(user, id, "sched");
18+}
19+
20+
21+void schedule_add(snac *user, const char *id, const xs_dict *msg)
22+/* schedules this post for later */
23+{
24+ /* delete from the index, in case it was already there */
25+ schedule_del(user, id);
26+
27+ /* overwrite object */
28+ object_add_ow(id, msg);
29+
30+ /* [re]add to the index */
31+ object_user_cache_add(user, id, "sched");
32+}
33+
34+
35+xs_list *scheduled_list(snac *user)
36+/* return the list of scheduled posts */
37+{
38+ return object_user_cache_list(user, "sched", XS_ALL, 1);
39+}
40+
41+
42+void scheduled_process(snac *user)
43+/* processes the scheduled list, sending those ready to be sent */
44+{
45+ xs *posts = scheduled_list(user);
46+ const char *md5;
47+ xs *right_now = xs_str_utctime(0, ISO_DATE_SPEC);
48+
49+ xs_list_foreach(posts, md5) {
50+ xs *msg = NULL;
51+
52+ if (valid_status(object_get_by_md5(md5, &msg))) {
53+ if (strcmp(xs_dict_get(msg, "published"), right_now) < 0) {
54+ /* due date! */
55+ const char *id = xs_dict_get(msg, "id");
56+
57+ timeline_add(user, id, msg);
58+
59+ xs *c_msg = msg_create(user, msg);
60+ enqueue_message(user, c_msg);
61+
62+ schedule_del(user, id);
63+ }
64+ }
65+ }
66+}
67+
68+
69 /** hiding **/
70
71 xs_str *_hidden_fn(snac *snac, const char *id)
72@@ -2619,10 +2683,9 @@ xs_list *inbox_list(void)
73 xs_list *ibl = xs_list_new();
74 xs *spec = xs_fmt("%s/inbox/" "*", srv_basedir);
75 xs *files = xs_glob(spec, 0, 0);
76- xs_list *p = files;
77 const xs_val *v;
78
79- while (xs_list_iter(&p, &v)) {
80+ xs_list_foreach(files, v) {
81 FILE *f;
82
83 if ((f = fopen(v, "r")) != NULL) {
84@@ -2630,7 +2693,9 @@ xs_list *inbox_list(void)
85
86 if (line && *line) {
87 line = xs_strip_i(line);
88- ibl = xs_list_append(ibl, line);
89+
90+ if (!is_instance_blocked(line))
91+ ibl = xs_list_append(ibl, line);
92 }
93
94 fclose(f);
95@@ -3696,7 +3761,7 @@ void purge_user(snac *snac)
96 _purge_user_subdir(snac, "public", pub_days);
97
98 const char *idxs[] = { "followers.idx", "private.idx", "public.idx",
99- "pinned.idx", "bookmark.idx", "draft.idx", NULL };
100+ "pinned.idx", "bookmark.idx", "draft.idx", "sched.idx", NULL };
101
102 for (n = 0; idxs[n]; n++) {
103 xs *idx = xs_fmt("%s/%s", snac->basedir, idxs[n]);
+51,
-0
1@@ -0,0 +1,51 @@
2+#!/usr/bin/env bash
3+##
4+## SNAC-ADMIN
5+## a simple script that is supposed to improve
6+## a snac admin's life, especially when snac
7+## is being run as a systemd.unit with
8+## DynamicUser=yes enabled.
9+## Please make sure to adjust SNAC_DIR
10+## down below according to your setup.
11+##
12+## USAGE
13+## snac-admin state
14+## snac-admin adduser rikkert
15+## snac-admin block example.org
16+## snac-admin verify_links lisa
17+## ...
18+##
19+## Author: @chris@social.shtrophic.net
20+##
21+## Released into the public domain
22+##
23+
24+set -e
25+
26+SNAC_PID=$(pidof snac)
27+SNAC_DIR=/var/lib/snac
28+
29+SNAC_VERB=$1
30+shift
31+
32+if [ -z $SNAC_PID ]; then
33+ echo "no such process" >&2
34+ exit 1
35+fi
36+
37+if [ $(id -u) -ne 0 ]; then
38+ echo "not root" >&2
39+ exit 1
40+fi
41+
42+if [ ! -d $SNAC_DIR ]; then
43+ echo "$SNAC_DIR is not a directory" >&2
44+ exit 1
45+fi
46+
47+if [ -z $SNAC_VERB ]; then
48+ echo "no arguments" >&2
49+ exit 1
50+fi
51+
52+nsenter -ae -S follow -G follow -t $SNAC_PID -- snac $SNAC_VERB $SNAC_DIR $@
M
html.c
+152,
-21
1@@ -14,6 +14,7 @@
2 #include "xs_curl.h"
3 #include "xs_unicode.h"
4 #include "xs_url.h"
5+#include "xs_random.h"
6
7 #include "snac.h"
8
9@@ -72,6 +73,9 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
10 const xs_dict *v;
11 int c = 0;
12
13+ xs_set rep_emoji;
14+ xs_set_init(&rep_emoji);
15+
16 while (xs_list_next(tag_list, &v, &c)) {
17 const char *t = xs_dict_get(v, "type");
18
19@@ -79,6 +83,10 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
20 const char *n = xs_dict_get(v, "name");
21 const xs_dict *i = xs_dict_get(v, "icon");
22
23+ /* avoid repeated emojis (Misskey seems to return this) */
24+ if (xs_set_add(&rep_emoji, n) == 0)
25+ continue;
26+
27 if (xs_is_string(n) && xs_is_dict(i)) {
28 const char *u = xs_dict_get(i, "url");
29 const char *mt = xs_dict_get(i, "mediaType");
30@@ -93,6 +101,8 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
31 xs_html_attr("loading", "lazy"),
32 xs_html_attr("src", url),
33 xs_html_attr("alt", n),
34+ xs_html_attr("title", n),
35+ xs_html_attr("class", "snac-emoji"),
36 xs_html_attr("style", style));
37
38 xs *s1 = xs_html_render(img);
39@@ -104,6 +114,8 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
40 }
41 }
42 }
43+
44+ xs_set_free(&rep_emoji);
45 }
46
47 return s;
48@@ -339,7 +351,7 @@ xs_html *html_note(snac *user, const char *summary,
49 const xs_val *mnt_only, const char *redir,
50 const char *in_reply_to, int poll,
51 const xs_list *att_files, const xs_list *att_alt_texts,
52- int is_draft)
53+ int is_draft, const char *published)
54 /* Yes, this is a FUCKTON of arguments and I'm a bit embarrased */
55 {
56 xs *action = xs_fmt("%s/admin/note", user->actor);
57@@ -429,6 +441,36 @@ xs_html *html_note(snac *user, const char *summary,
58 xs_html_attr("name", "is_draft"),
59 xs_html_attr(is_draft ? "checked" : "", NULL))));
60
61+ /* post date and time */
62+ xs *post_date = NULL;
63+ xs *post_time = NULL;
64+
65+ if (xs_is_string(published)) {
66+ time_t t = xs_parse_iso_date(published, 0);
67+
68+ if (t > 0) {
69+ post_date = xs_str_time(t, "%Y-%m-%d", 1);
70+ post_time = xs_str_time(t, "%H:%M:%S", 1);
71+ }
72+ }
73+
74+ if (edit_id == NULL || is_draft || is_scheduled(user, edit_id)) {
75+ xs_html_add(form,
76+ xs_html_tag("p",
77+ xs_html_text(L("Post date and time (empty, right now; in the future, schedule for later):")),
78+ xs_html_sctag("br", NULL),
79+ xs_html_sctag("input",
80+ xs_html_attr("type", "date"),
81+ xs_html_attr("value", post_date ? post_date : ""),
82+ xs_html_attr("name", "post_date")),
83+ xs_html_text(" "),
84+ xs_html_sctag("input",
85+ xs_html_attr("type", "time"),
86+ xs_html_attr("value", post_time ? post_time : ""),
87+ xs_html_attr("step", "1"),
88+ xs_html_attr("name", "post_time"))));
89+ }
90+
91 if (edit_id)
92 xs_html_add(form,
93 xs_html_sctag("input",
94@@ -1116,7 +1158,7 @@ xs_html *html_top_controls(snac *user)
95 NULL, NULL,
96 xs_stock(XSTYPE_FALSE), "",
97 xs_stock(XSTYPE_FALSE), NULL,
98- NULL, 1, NULL, NULL, 0),
99+ NULL, 1, NULL, NULL, 0, NULL),
100
101 /** operations **/
102 xs_html_tag("details",
103@@ -1774,7 +1816,8 @@ xs_html *html_entry_controls(snac *user, const char *actor,
104 id, NULL,
105 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
106 xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
107- NULL, 0, att_files, att_alt_texts, is_draft(user, id))),
108+ NULL, 0, att_files, att_alt_texts, is_draft(user, id),
109+ xs_dict_get(msg, "published"))),
110 xs_html_tag("p", NULL));
111 }
112
113@@ -1793,7 +1836,7 @@ xs_html *html_entry_controls(snac *user, const char *actor,
114 NULL, NULL,
115 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
116 xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
117- id, 0, NULL, NULL, 0)),
118+ id, 0, NULL, NULL, 0, NULL)),
119 xs_html_tag("p", NULL));
120 }
121
122@@ -2689,6 +2732,11 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
123 }
124 }
125
126+ /* add an invisible hr, to help differentiate between posts in text browsers */
127+ xs_html_add(entry_top,
128+ xs_html_sctag("hr",
129+ xs_html_attr("hidden", NULL)));
130+
131 return entry_top;
132 }
133
134@@ -2830,6 +2878,18 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
135 xs_html_text(L("drafts")))));
136 }
137
138+ {
139+ /* show the list of scheduled posts */
140+ xs *url = xs_fmt("%s/sched", user->actor);
141+ xs_html_add(lol,
142+ xs_html_tag("li",
143+ xs_html_tag("a",
144+ xs_html_attr("href", url),
145+ xs_html_attr("class", "snac-list-link"),
146+ xs_html_attr("title", L("Scheduled posts")),
147+ xs_html_text(L("scheduled posts")))));
148+ }
149+
150 /* the list of followed hashtags */
151 const char *followed_hashtags = xs_dict_get(user->config, "followed_hashtags");
152
153@@ -3130,7 +3190,7 @@ xs_html *html_people_list(snac *user, xs_list *list, const char *header, const c
154 NULL, actor_id,
155 xs_stock(XSTYPE_FALSE), "",
156 xs_stock(XSTYPE_FALSE), NULL,
157- NULL, 0, NULL, NULL, 0),
158+ NULL, 0, NULL, NULL, 0, NULL),
159 xs_html_tag("p", NULL));
160
161 xs_html_add(snac_post, snac_controls);
162@@ -3879,6 +3939,21 @@ int html_get_handler(const xs_dict *req, const char *q_path,
163 }
164 }
165 else
166+ if (strcmp(p_path, "sched") == 0) { /** list of scheduled posts **/
167+ if (!login(&snac, req)) {
168+ *body = xs_dup(uid);
169+ status = HTTP_STATUS_UNAUTHORIZED;
170+ }
171+ else {
172+ xs *list = scheduled_list(&snac);
173+
174+ *body = html_timeline(&snac, list, 0, skip, show,
175+ 0, L("Scheduled posts"), "", 0, error);
176+ *b_size = strlen(*body);
177+ status = HTTP_STATUS_OK;
178+ }
179+ }
180+ else
181 if (xs_startswith(p_path, "list/")) { /** list timelines **/
182 if (!login(&snac, req)) {
183 *body = xs_dup(uid);
184@@ -4175,12 +4250,14 @@ int html_post_handler(const xs_dict *req, const char *q_path,
185 snac_debug(&snac, 1, xs_fmt("web action '%s' received", p_path));
186
187 /* post note */
188- const xs_str *content = xs_dict_get(p_vars, "content");
189- const xs_str *in_reply_to = xs_dict_get(p_vars, "in_reply_to");
190- const xs_str *to = xs_dict_get(p_vars, "to");
191- const xs_str *sensitive = xs_dict_get(p_vars, "sensitive");
192- const xs_str *summary = xs_dict_get(p_vars, "summary");
193- const xs_str *edit_id = xs_dict_get(p_vars, "edit_id");
194+ const char *content = xs_dict_get(p_vars, "content");
195+ const char *in_reply_to = xs_dict_get(p_vars, "in_reply_to");
196+ const char *to = xs_dict_get(p_vars, "to");
197+ const char *sensitive = xs_dict_get(p_vars, "sensitive");
198+ const char *summary = xs_dict_get(p_vars, "summary");
199+ const char *edit_id = xs_dict_get(p_vars, "edit_id");
200+ const char *post_date = xs_dict_get_def(p_vars, "post_date", "");
201+ const char *post_time = xs_dict_get_def(p_vars, "post_time", "");
202 int priv = !xs_is_null(xs_dict_get(p_vars, "mentioned_only"));
203 int store_as_draft = !xs_is_null(xs_dict_get(p_vars, "is_draft"));
204 xs *attach_list = xs_list_new();
205@@ -4210,9 +4287,12 @@ int html_post_handler(const xs_dict *req, const char *q_path,
206 const char *fn = xs_list_get(attach_file, 0);
207
208 if (xs_is_string(fn) && *fn != '\0') {
209- char *ext = strrchr(fn, '.');
210- xs *hash = xs_md5_hex(fn, strlen(fn));
211- xs *id = xs_fmt("%s%s", hash, ext);
212+ char rnd[32];
213+ xs_rnd_buf(rnd, sizeof(rnd));
214+
215+ const char *ext = strrchr(fn, '.');
216+ xs *hash = xs_md5_hex(rnd, strlen(rnd));
217+ xs *id = xs_fmt("post-%s%s", hash, ext ? ext : "");
218 xs *url = xs_fmt("%s/s/%s", snac.actor, id);
219 int fo = xs_number_get(xs_list_get(attach_file, 1));
220 int fs = xs_number_get(xs_list_get(attach_file, 2));
221@@ -4268,6 +4348,29 @@ int html_post_handler(const xs_dict *req, const char *q_path,
222 msg = xs_dict_set(msg, "summary", xs_is_null(summary) ? "..." : summary);
223 }
224
225+ if (xs_is_string(post_date) && *post_date) {
226+ xs *local_pubdate = xs_fmt("%sT%s", post_date,
227+ xs_is_string(post_time) && *post_time ? post_time : "00:00:00");
228+
229+ time_t t = xs_parse_iso_date(local_pubdate, 1);
230+
231+ if (t != 0) {
232+ xs *iso_date = xs_str_iso_date(t);
233+ msg = xs_dict_set(msg, "published", iso_date);
234+
235+ snac_debug(&snac, 1, xs_fmt("Published date: [%s]", iso_date));
236+ }
237+ else
238+ snac_log(&snac, xs_fmt("Invalid post date: [%s]", local_pubdate));
239+ }
240+
241+ /* is the published date from the future? */
242+ int future_post = 0;
243+ xs *right_now = xs_str_utctime(0, ISO_DATE_SPEC);
244+
245+ if (strcmp(xs_dict_get(msg, "published"), right_now) > 0)
246+ future_post = 1;
247+
248 if (xs_is_null(edit_id)) {
249 /* new message */
250 const char *id = xs_dict_get(msg, "id");
251@@ -4275,6 +4378,10 @@ int html_post_handler(const xs_dict *req, const char *q_path,
252 if (store_as_draft) {
253 draft_add(&snac, id, msg);
254 }
255+ else
256+ if (future_post) {
257+ schedule_add(&snac, id, msg);
258+ }
259 else {
260 c_msg = msg_create(&snac, msg);
261 timeline_add(&snac, id, msg);
262@@ -4286,7 +4393,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
263
264 if (valid_status(object_get(edit_id, &p_msg))) {
265 /* copy relevant fields from previous version */
266- char *fields[] = { "id", "context", "url", "published",
267+ char *fields[] = { "id", "context", "url",
268 "to", "inReplyTo", NULL };
269 int n;
270
271@@ -4302,18 +4409,34 @@ int html_post_handler(const xs_dict *req, const char *q_path,
272 if (is_draft(&snac, edit_id)) {
273 /* message was previously a draft; it's a create activity */
274
275- /* set the published field to now */
276- xs *published = xs_str_utctime(0, ISO_DATE_SPEC);
277- msg = xs_dict_set(msg, "published", published);
278+ /* if the date is from the past, overwrite it with right_now */
279+ if (strcmp(xs_dict_get(msg, "published"), right_now) < 0) {
280+ snac_debug(&snac, 1, xs_fmt("setting draft ancient date to %s", right_now));
281+ msg = xs_dict_set(msg, "published", right_now);
282+ }
283
284 /* overwrite object */
285 object_add_ow(edit_id, msg);
286
287- c_msg = msg_create(&snac, msg);
288- timeline_add(&snac, edit_id, msg);
289+ if (future_post) {
290+ schedule_add(&snac, edit_id, msg);
291+ }
292+ else {
293+ c_msg = msg_create(&snac, msg);
294+ timeline_add(&snac, edit_id, msg);
295+ }
296+
297 draft_del(&snac, edit_id);
298 }
299+ else
300+ if (is_scheduled(&snac, edit_id)) {
301+ /* editing an scheduled post; just update it */
302+ schedule_add(&snac, edit_id, msg);
303+ }
304 else {
305+ /* ignore the (possibly changed) published date */
306+ msg = xs_dict_set(msg, "published", xs_dict_get(p_msg, "published"));
307+
308 /* set the updated field */
309 xs *updated = xs_str_utctime(0, ISO_DATE_SPEC);
310 msg = xs_dict_set(msg, "updated", updated);
311@@ -4397,6 +4520,9 @@ int html_post_handler(const xs_dict *req, const char *q_path,
312 if (strcmp(action, L("Hide")) == 0) { /** **/
313 if (is_draft(&snac, id))
314 draft_del(&snac, id);
315+ else
316+ if (is_scheduled(&snac, id))
317+ schedule_del(&snac, id);
318 else
319 hide(&snac, id);
320 }
321@@ -4493,6 +4619,8 @@ int html_post_handler(const xs_dict *req, const char *q_path,
322
323 draft_del(&snac, id);
324
325+ schedule_del(&snac, id);
326+
327 snac_log(&snac, xs_fmt("deleted entry %s", id));
328 }
329 }
330@@ -4636,7 +4764,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
331 if (xs_startswith(mimetype, "image/")) {
332 const char *ext = strrchr(fn, '.');
333 xs *hash = xs_md5_hex(fn, strlen(fn));
334- xs *id = xs_fmt("%s%s", hash, ext);
335+ xs *id = xs_fmt("%s-%s%s", uploads[n], hash, ext ? ext : "");
336 xs *url = xs_fmt("%s/s/%s", snac.actor, id);
337 int fo = xs_number_get(xs_list_get(uploaded_file, 1));
338 int fs = xs_number_get(xs_list_get(uploaded_file, 2));
339@@ -4705,6 +4833,9 @@ int html_post_handler(const xs_dict *req, const char *q_path,
340 /* set the option */
341 msg = xs_dict_append(msg, "name", v);
342
343+ /* delete the content */
344+ msg = xs_dict_del(msg, "content");
345+
346 xs *c_msg = msg_create(&snac, msg);
347
348 enqueue_message(&snac, c_msg);
+35,
-6
1@@ -2256,6 +2256,25 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
2 status = HTTP_STATUS_OK;
3 }
4 else
5+ if (strcmp(cmd, "/v1/instance/peers") == 0) { /** **/
6+ /* get the collected inbox list as the instances "this domain is aware of" */
7+ xs *list = inbox_list();
8+ xs *peers = xs_list_new();
9+ const char *inbox;
10+
11+ xs_list_foreach(list, inbox) {
12+ xs *l = xs_split(inbox, "/");
13+ const char *domain = xs_list_get(l, 2);
14+
15+ if (xs_is_string(domain))
16+ peers = xs_list_append(peers, domain);
17+ }
18+
19+ *body = xs_json_dumps(peers, 4);
20+ *ctype = "application/json";
21+ status = HTTP_STATUS_OK;
22+ }
23+ else
24 if (xs_startswith(cmd, "/v1/statuses/")) { /** **/
25 /* information about a status */
26 if (logged_in) {
27@@ -2707,14 +2726,24 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
28 msg = xs_dict_set(msg, "summary", summary);
29 }
30
31- /* store */
32- timeline_add(&snac, xs_dict_get(msg, "id"), msg);
33+ /* scheduled? */
34+ const char *scheduled_at = xs_dict_get(args, "scheduled_at");
35
36- /* 'Create' message */
37- xs *c_msg = msg_create(&snac, msg);
38- enqueue_message(&snac, c_msg);
39+ if (xs_is_string(scheduled_at) && *scheduled_at) {
40+ msg = xs_dict_set(msg, "published", scheduled_at);
41
42- timeline_touch(&snac);
43+ schedule_add(&snac, xs_dict_get(msg, "id"), msg);
44+ }
45+ else {
46+ /* store */
47+ timeline_add(&snac, xs_dict_get(msg, "id"), msg);
48+
49+ /* 'Create' message */
50+ xs *c_msg = msg_create(&snac, msg);
51+ enqueue_message(&snac, c_msg);
52+
53+ timeline_touch(&snac);
54+ }
55
56 /* convert to a mastodon status as a response code */
57 xs *st = mastoapi_status(&snac, msg);
M
po/ru.po
+1,
-1
1@@ -446,7 +446,7 @@ msgstr "Событие"
2
3 #: html.c:1977 html.c:2006
4 msgid "boosted"
5-msgstr "продвинуто"
6+msgstr "поделился"
7
8 #: html.c:2022
9 msgid "in reply to"
M
snac.h
+7,
-1
1@@ -1,7 +1,7 @@
2 /* snac - A simple, minimalistic ActivityPub instance */
3 /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
4
5-#define VERSION "2.74"
6+#define VERSION "2.75-dev"
7
8 #define USER_AGENT "snac/" VERSION
9
10@@ -205,6 +205,12 @@ void draft_del(snac *user, const char *id);
11 void draft_add(snac *user, const char *id, const xs_dict *msg);
12 xs_list *draft_list(snac *user);
13
14+int is_scheduled(snac *user, const char *id);
15+void schedule_del(snac *user, const char *id);
16+void schedule_add(snac *user, const char *id, const xs_dict *msg);
17+xs_list *scheduled_list(snac *user);
18+void scheduled_process(snac *user);
19+
20 int limited(snac *user, const char *id, int cmd);
21 #define is_limited(user, id) limited((user), (id), 0)
22 #define limit(user, id) limited((user), (id), 1)