diff options
-rw-r--r-- | Makefile | 7 | ||||
-rw-r--r-- | Makefile.NetBSD | 8 | ||||
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | RELEASE_NOTES.md | 32 | ||||
-rw-r--r-- | TODO.md | 12 | ||||
-rw-r--r-- | activitypub.c | 146 | ||||
-rw-r--r-- | data.c | 130 | ||||
-rw-r--r-- | doc/snac.1 | 8 | ||||
-rw-r--r-- | doc/snac.5 | 10 | ||||
-rw-r--r-- | doc/snac.8 | 57 | ||||
-rw-r--r-- | format.c | 79 | ||||
-rw-r--r-- | html.c | 212 | ||||
-rw-r--r-- | httpd.c | 3 | ||||
-rw-r--r-- | main.c | 115 | ||||
-rw-r--r-- | mastoapi.c | 177 | ||||
-rw-r--r-- | snac.h | 14 | ||||
-rw-r--r-- | xs.h | 103 | ||||
-rw-r--r-- | xs_json.h | 107 | ||||
-rw-r--r-- | xs_mime.h | 22 | ||||
-rw-r--r-- | xs_unicode.h | 16 | ||||
-rw-r--r-- | xs_url.h | 2 | ||||
-rw-r--r-- | xs_version.h | 2 |
22 files changed, 954 insertions, 312 deletions
@@ -36,15 +36,16 @@ uninstall: activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \ xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h snac.h data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \ - xs_set.h xs_time.h snac.h -format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h snac.h + xs_set.h xs_time.h xs_regex.h snac.h +format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \ + xs_time.h snac.h html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \ xs_time.h xs_mime.h xs_match.h xs_html.h snac.h http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \ snac.h httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_httpd.h xs_mime.h \ xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h -main.o: main.c xs.h xs_io.h xs_json.h xs_time.h snac.h xs_html.h +main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h snac.h mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ snac.h diff --git a/Makefile.NetBSD b/Makefile.NetBSD index 5ab361f..67c77a5 100644 --- a/Makefile.NetBSD +++ b/Makefile.NetBSD @@ -1,6 +1,7 @@ PREFIX=/usr/pkg PREFIX_MAN=$(PREFIX)/man CFLAGS?=-g -Wall -Wextra +LDFLAGS=-lrt all: snac @@ -37,15 +38,16 @@ uninstall: activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \ xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h snac.h data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \ - xs_set.h xs_time.h snac.h -format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h snac.h + xs_set.h xs_time.h xs_regex.h snac.h +format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \ + xs_time.h snac.h html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \ xs_time.h xs_mime.h xs_match.h xs_html.h snac.h http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \ snac.h httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_httpd.h xs_mime.h \ xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h -main.o: main.c xs.h xs_io.h xs_json.h xs_time.h snac.h xs_html.h +main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h snac.h mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ snac.h @@ -93,11 +93,13 @@ This will: - [Online snac manuals (user, administrator and data formats)](https://comam.es/snac-doc/). - [How to run your own ActivityPub server on OpenBSD via snac (by Jordan Reger)](https://man.sr.ht/~jordanreger/activitypub-server-on-openbsd/). - [How to install & run your own ActivityPub server on FreeBSD using snac, nginx, lets'encrypt (by gyptazy)](https://gyptazy.ch/blog/install-snac2-on-freebsd-an-activitypub-instance-for-the-fediverse/). +- [How to install snac on OpenBSD without relayd (by @antics@mastodon.nu)](https://chai.guru/pub/openbsd/snac.html). +- [Setting up Snac in OpenBSD (by Yonle)](https://wiki.ircnow.org/index.php?n=Openbsd.Snac). ## Incredibly awesome CSS themes for snac +- [A compilation of themes for snac (by Ворон)](https://codeberg.org/voron/snac-style). - [A cool, elegant theme (by Haijo7)](https://codeberg.org/Haijo7/snac-custom-css). -- [A light, lean theme (by Ворон)](https://codeberg.org/voron/snac-style). - [A terminal-like theme (by Tetra)](https://codeberg.org/ERROR404NULLNOTFOUND/snac-terminal-theme). ## License diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 6b52712..f272f5c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,37 @@ # Release Notes +## 2.52 + +Posts that were liked or boosted can now be unliked and unboosted. + +Added a header to avoid over-zealous caching in some browsers (contributed by louis77). + +## 2.51 + +Support for custom Emojis has been added; they are no longer hardcoded, but read from the `emojis.json` file at the server base directory. Also, they are no longer limited to string substitutions, but images as external URLs are also supported (see `snac(8)` for more information). + +Fixed a bug that caused some notifications to be lost when coming from a user in the same instance. + +Added an additional check for blocked instances (sometimes, posts from blocked sites that were ancestors of legit posts were 'leaking' into the timeline). + +On OpenBSD, if the `disable_email_notifications` server flag is set to `true`, `unveil()` is not called for the execution of the `/usr/sbin/sendmail` binary and `pledge()` doesn't set the `exec` promise. + +## 2.50 + +Incoming posts can now be filtered out by content using regular expressions on a server level (these regexes are written in the `filter_reject.txt` file at the server base directory; see `snac(5)` and `snac(8)`). + +Improved page position after hitting the `Hide` or `MUTE` buttons (for most cases). + +Use a shorter maximum conversation thread level (also, this maximum value is now configurable at compilation level with the `MAX_CONVERSATION_LEVELS` define). + +Fixed a bug where editing a post made the attached media or video to be lost. + +The way of refreshing remote actor data has been improved. + +Posting from the command-line now allows attachments. + +Added defines for time to enable MacOS builds (contributed by andypiper). + ## 2.49 Mastodon API: Fixed a bug in how validated links are reported. @@ -6,19 +6,19 @@ Unfollowing lemmy groups gets rejected with an http status of 400. Unfollowing guppe groups seems to work (http status of 200), but messages continue to arrive as if it didn't. -Post edits should preserve the image and the image description somewhat. - Mastodon API: fix whatever the fuck is making the official app and Megalodon to crash. Important: deleting a follower should do more that just delete the object, see https://codeberg.org/grunfink/snac2/issues/43#issuecomment-956721 ## Wishlist +Implement `Group`-like accounts (i.e. an actor that boosts to their followers all posts that mention it). + Integrate "Ability to federate with hidden networks" see https://codeberg.org/grunfink/snac2/issues/93 Integrate "Added handling for International Domain Names" PR https://codeberg.org/grunfink/snac2/pulls/104 -Consider discarding posts by content using string or regex to mitigate spam. +Consider adding Mastodon import functionality (for following_accounts.csv and outbox.json). Consider adding milter-like support to reject posts to mitigate spam. @@ -26,7 +26,7 @@ Do something about Akkoma and Misskey's quoted replies (they use the `quoteUrl` Add more CSS classes according to https://comam.es/snac/grunfink/p/1705598619.090050 -Add support for /share?text=tt&website=url (whatever it is). +Add support for /share?text=tt&website=url (whatever it is, see https://mastodonshare.com/ for details). Add support for /authorize_interaction (whatever it is). @@ -307,3 +307,7 @@ Add support for rel="me" links, see https://codeberg.org/grunfink/snac2/issues/1 Hide followers-only replies to unknown accounts, see https://codeberg.org/grunfink/snac2/issues/123 (2024-02-22T12:40:58+0100). Consider implementing the rejection of activities from recently-created accounts to mitigate spam, see https://akkoma.dev/AkkomaGang/akkoma/src/branch/develop/lib/pleroma/web/activity_pub/mrf/reject_newly_created_account_note_policy.ex (2024-02-24T07:46:10+0100). + +Consider discarding posts by content using string or regex to mitigate spam (2024-03-14T10:40:14+0100). + +Post edits should preserve the image and the image description somewhat (2024-03-22T09:57:18+0100). diff --git a/activitypub.c b/activitypub.c index 73fbbc6..53f102e 100644 --- a/activitypub.c +++ b/activitypub.c @@ -125,10 +125,10 @@ int actor_request(snac *user, const char *actor, xs_dict **data) *data = NULL; /* get from disk first */ - status = actor_get(actor, data); + status = actor_get_refresh(user, actor, data); - if (status != 200) { - /* actor data non-existent or stale: get from the net */ + if (!valid_status(status)) { + /* actor data non-existent: get from the net */ status = activitypub_request(user, actor, &payload); if (valid_status(status)) { @@ -149,8 +149,6 @@ int actor_request(snac *user, const char *actor, xs_dict **data) if (valid_status(status) && data && *data) inbox_add_by_actor(*data); } - else - srv_debug(2, xs_fmt("NOT collected")); return status; } @@ -313,6 +311,12 @@ int timeline_request(snac *snac, char **id, xs_str **wrk, int level) if (level < MAX_CONVERSATION_LEVELS && !xs_is_null(*id)) { xs *msg = NULL; + /* from a blocked instance? discard and break */ + if (is_instance_blocked(*id)) { + snac_debug(snac, 1, xs_fmt("timeline_request blocked instance %s", *id)); + return status; + } + /* is the object already there? */ if (!valid_status(object_get(*id, &msg))) { /* no; download it */ @@ -354,18 +358,22 @@ int timeline_request(snac *snac, char **id, xs_str **wrk, int level) if (xs_match(type, "Note|Page|Article|Video")) { const char *actor = get_atto(object); - /* request (and drop) the actor for this entry */ - if (!xs_is_null(actor)) - actor_request(snac, actor, NULL); + if (content_check("filter_reject.txt", object)) + snac_log(snac, xs_fmt("timeline_request rejected by content %s", nid)); + else { + /* request (and drop) the actor for this entry */ + if (!xs_is_null(actor)) + actor_request(snac, actor, NULL); - /* does it have an ancestor? */ - char *in_reply_to = xs_dict_get(object, "inReplyTo"); + /* does it have an ancestor? */ + char *in_reply_to = xs_dict_get(object, "inReplyTo"); - /* store */ - timeline_add(snac, nid, object); + /* store */ + timeline_add(snac, nid, object); - /* recurse! */ - timeline_request(snac, &in_reply_to, NULL, level + 1); + /* recurse! */ + timeline_request(snac, &in_reply_to, NULL, level + 1); + } } } } @@ -623,6 +631,12 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg) const char *type = xs_dict_get(c_msg, "type"); const char *actor = xs_dict_get(c_msg, "actor"); + if (strcmp(actor, snac->actor) == 0) { + /* message by myself? (most probably via the shared-inbox) reject */ + snac_debug(snac, 1, xs_fmt("ignoring message by myself")); + return 0; + } + if (xs_match(type, "Like|Announce")) { const char *object = xs_dict_get(c_msg, "object"); @@ -657,6 +671,12 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg) return !xs_is_null(object) && strcmp(snac->actor, object) == 0; } + /* only accept Ping directed to us */ + if (xs_match(type, "Ping")) { + char *dest = xs_dict_get(c_msg, "to"); + return !xs_is_null(dest) && strcmp(snac->actor, dest) == 0; + } + /* if it's not a Create or Update, allow as is */ if (!xs_match(type, "Create|Update")) { return 1; @@ -1072,7 +1092,7 @@ xs_dict *msg_collection(snac *snac, char *id) msg = xs_dict_append(msg, "attributedTo", snac->actor); msg = xs_dict_append(msg, "orderedItems", ol); - msg = xs_dict_append(msg, "totalItems", xs_stock_0); + msg = xs_dict_append(msg, "totalItems", xs_stock(0)); return msg; } @@ -1129,8 +1149,10 @@ xs_dict *msg_admiration(snac *snac, char *object, char *type) if (valid_status(object_get(object, &a_msg))) { xs *rcpts = xs_list_new(); + xs *o_md5 = xs_md5_hex(object, strlen(object)); + xs *id = xs_fmt("%s/%s/%s", snac->actor, *type == 'L' ? "l" : "a", o_md5); - msg = msg_base(snac, type, "@dummy", snac->actor, "@now", object); + msg = msg_base(snac, type, id, snac->actor, "@now", object); if (is_msg_public(a_msg)) rcpts = xs_list_append(rcpts, public_address); @@ -1146,6 +1168,33 @@ xs_dict *msg_admiration(snac *snac, char *object, char *type) } +xs_dict *msg_repulsion(snac *user, char *id, char *type) +/* creates an Undo + admiration message */ +{ + xs *a_msg = NULL; + xs_dict *msg = NULL; + + if (valid_status(object_get(id, &a_msg))) { + /* create a clone of the original admiration message */ + xs *object = msg_admiration(user, id, type); + + /* delete the published date */ + object = xs_dict_del(object, "published"); + + /* create an undo message for this object */ + msg = msg_undo(user, object); + + /* copy the 'to' field */ + msg = xs_dict_set(msg, "to", xs_dict_get(object, "to")); + } + + /* now we despise this */ + object_unadmire(id, user->actor, *type == 'L' ? 1 : 0); + + return msg; +} + + xs_dict *msg_actor(snac *snac) /* create a Person message for this actor */ { @@ -1170,7 +1219,7 @@ xs_dict *msg_actor(snac *snac) msg = xs_dict_set(msg, "preferredUsername", snac->uid); msg = xs_dict_set(msg, "published", xs_dict_get(snac->config, "published")); - xs *f_bio_2 = not_really_markdown(xs_dict_get(snac->config, "bio"), NULL); + xs *f_bio_2 = not_really_markdown(xs_dict_get(snac->config, "bio"), NULL, NULL); f_bio = process_tags(snac, f_bio_2, &tags); msg = xs_dict_set(msg, "summary", f_bio); msg = xs_dict_set(msg, "tag", tags); @@ -1378,7 +1427,7 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts, } /* format the content */ - fc2 = not_really_markdown(content, &atls); + fc2 = not_really_markdown(content, &atls, &tag); if (in_reply_to != NULL && *in_reply_to) { xs *p_msg = NULL; @@ -1556,6 +1605,7 @@ xs_dict *msg_question(snac *user, const char *content, xs_list *attach, } if (xs_set_add(&seen, v2) == 1) { + d = xs_dict_append(d, "type", "Note"); d = xs_dict_append(d, "name", v2); d = xs_dict_append(d, "replies", replies); o = xs_list_append(o, d); @@ -1608,7 +1658,7 @@ int update_question(snac *user, const char *id) const char *name = xs_dict_get(v, "name"); if (name) { lopts = xs_list_append(lopts, name); - rcnt = xs_dict_set(rcnt, name, xs_stock_0); + rcnt = xs_dict_set(rcnt, name, xs_stock(0)); } } @@ -1891,6 +1941,8 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req) } else if (strcmp(type, "Undo") == 0) { /** **/ + char *id = xs_dict_get(object, "object"); + if (xs_type(object) != XSTYPE_DICT) utype = "Follow"; @@ -1903,6 +1955,23 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req) snac_log(snac, xs_fmt("error deleting follower %s", actor)); } else + if (strcmp(utype, "Like") == 0) { /** **/ + int status = object_unadmire(id, actor, 1); + + snac_log(snac, xs_fmt("Unlike for %s %d", id, status)); + } + else + if (strcmp(utype, "Announce") == 0) { /** **/ + int status = 200; + + /* commented out: if a followed user boosts something that + is requested and then unboosts, the post remains here, + but with no apparent reason, and that is confusing */ + //status = object_unadmire(id, actor, 0); + + snac_log(snac, xs_fmt("Unboost for %s %d", id, status)); + } + else snac_debug(snac, 1, xs_fmt("ignored 'Undo' for object type '%s'", utype)); } else @@ -1912,7 +1981,7 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req) return 1; } - if (strcmp(utype, "Note") == 0) { /** **/ + if (xs_match(utype, "Note|Article")) { /** **/ char *id = xs_dict_get(object, "id"); char *in_reply_to = xs_dict_get(object, "inReplyTo"); xs *wrk = NULL; @@ -1921,10 +1990,15 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req) snac_debug(snac, 0, xs_fmt("dropped reply %s to hidden post %s", id, in_reply_to)); } else { + if (content_check("filter_reject.txt", object)) { + snac_log(snac, xs_fmt("rejected by content %s", id)); + return 1; + } + timeline_request(snac, &in_reply_to, &wrk, 0); if (timeline_add(snac, id, object)) { - snac_log(snac, xs_fmt("new 'Note' %s %s", actor, id)); + snac_log(snac, xs_fmt("new '%s' %s %s", utype, actor, id)); do_notify = 1; } @@ -1988,12 +2062,12 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req) if (xs_type(object) == XSTYPE_DICT) object = xs_dict_get(object, "id"); - if (timeline_admire(snac, object, actor, 1) == 201) { + if (timeline_admire(snac, object, actor, 1) == 201) snac_log(snac, xs_fmt("new 'Like' %s %s", actor, object)); - do_notify = 1; - } else snac_log(snac, xs_fmt("repeated 'Like' from %s to %s", actor, object)); + + do_notify = 1; } else if (strcmp(type, "Announce") == 0) { /** **/ @@ -2019,13 +2093,13 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req) xs *who_o = NULL; if (valid_status(actor_request(snac, who, &who_o))) { - if (timeline_admire(snac, object, actor, 0) == 201) { + if (timeline_admire(snac, object, actor, 0) == 201) snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object)); - do_notify = 1; - } else snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s", actor, object)); + + do_notify = 1; } else snac_debug(snac, 1, xs_fmt("dropped 'Announce' on actor request error %s", who)); @@ -2236,6 +2310,24 @@ void process_user_queue_item(snac *snac, xs_dict *q_item) verify_links(snac); } else + if (strcmp(type, "actor_refresh") == 0) { + const char *actor = xs_dict_get(q_item, "actor"); + double mtime = object_mtime(actor); + + /* only refresh if it was refreshed more than an hour ago */ + if (mtime + 3600.0 < (double) time(NULL)) { + xs *actor_o = NULL; + int status; + + if (valid_status((status = activitypub_request(snac, actor, &actor_o)))) + actor_add(actor, actor_o); + else + object_touch(actor); + + snac_log(snac, xs_fmt("actor_refresh %s %d", actor, status)); + } + } + else snac_log(snac, xs_fmt("unexpected user q_item type '%s'", type)); } @@ -9,6 +9,7 @@ #include "xs_glob.h" #include "xs_set.h" #include "xs_time.h" +#include "xs_regex.h" #include "snac.h" @@ -116,21 +117,33 @@ int srv_open(char *basedir, int auto_upgrade) srv_debug(1, xs_dup("OpenBSD security disabled by admin")); } else { + int smail = xs_type(xs_dict_get(srv_config, "disable_email_notifications")) != XSTYPE_TRUE; + srv_debug(1, xs_fmt("Calling unveil()")); unveil(basedir, "rwc"); unveil("/tmp", "rwc"); - unveil("/usr/sbin/sendmail", "x"); unveil("/etc/resolv.conf", "r"); unveil("/etc/hosts", "r"); unveil("/etc/ssl/openssl.cnf", "r"); unveil("/etc/ssl/cert.pem", "r"); unveil("/usr/share/zoneinfo", "r"); + + if (smail) + unveil("/usr/sbin/sendmail", "x"); + unveil(NULL, NULL); srv_debug(1, xs_fmt("Calling pledge()")); - pledge("stdio rpath wpath cpath flock inet proc exec dns fattr", NULL); + + if (smail) + pledge("stdio rpath wpath cpath flock inet proc exec dns fattr", NULL); + else + pledge("stdio rpath wpath cpath flock inet proc dns fattr", NULL); } #endif /* __OpenBSD__ */ + /* read (and drop) emojis.json, possibly creating it */ + xs_free(emojis()); + return ret; } @@ -393,7 +406,7 @@ int index_del_md5(const char *fn, const char *md5) fclose(f); } else - status = 500; + status = 410; pthread_mutex_unlock(&data_mutex); @@ -796,6 +809,30 @@ double object_ctime(const char *id) } +double object_mtime_by_md5(const char *md5) +{ + xs *fn = _object_fn_by_md5(md5, "object_mtime_by_md5"); + return mtime(fn); +} + + +double object_mtime(const char *id) +{ + xs *md5 = xs_md5_hex(id, strlen(id)); + return object_mtime_by_md5(md5); +} + + +void object_touch(const char *id) +{ + xs *md5 = xs_md5_hex(id, strlen(id)); + xs *fn = _object_fn_by_md5(md5, "object_touch"); + + if (mtime(fn)) + utimes(fn, NULL); +} + + xs_str *_object_index_fn(const char *id, const char *idxsfx) /* returns the filename of an object's index */ { @@ -880,6 +917,9 @@ int object_unadmire(const char *id, const char *actor, int like) status = index_del(fn, actor); + if (valid_status(status)) + index_gc(fn); + srv_debug(0, xs_fmt("object_unadmire (%s) %s %s %d", like ? "Like" : "Announce", actor, fn, status)); @@ -1551,7 +1591,6 @@ int actor_get(const char *actor, xs_dict **data) else d = xs_free(d); -#ifdef STALE_ACTORS xs *fn = _object_fn(actor); double max_time; @@ -1560,13 +1599,20 @@ int actor_get(const char *actor, xs_dict **data) if (mtime(fn) + max_time < (double) time(NULL)) { /* actor data exists but also stinks */ - - /* touch the file */ - utimes(fn, NULL); - status = 205; /* "205: Reset Content" "110: Response Is Stale" */ } -#endif /* STALE_ACTORS */ + + return status; +} + + +int actor_get_refresh(snac *user, const char *actor, xs_dict **data) +/* gets an actor and requests a refresh if it's stale */ +{ + int status = actor_get(actor, data); + + if (status == 205 && user && !xs_startswith(actor, srv_baseurl)) + enqueue_actor_refresh(user, actor); return status; } @@ -2007,6 +2053,47 @@ int instance_unblock(const char *instance) } +/** content filtering **/ + +int content_check(const char *file, const xs_dict *msg) +/* checks if a message's content matches any of the regexes in file */ +/* file format: one regex per line */ +{ + xs *fn = xs_fmt("%s/%s", srv_basedir, file); + FILE *f; + int r = 0; + char *v = xs_dict_get(msg, "content"); + + if (xs_type(v) == XSTYPE_STRING && *v) { + if ((f = fopen(fn, "r")) != NULL) { + srv_debug(1, xs_fmt("content_check: loading regexes from %s", fn)); + + /* massage content (strip HTML tags, etc.) */ + xs *c = xs_regex_replace(v, "<[^>]+>", " "); + c = xs_regex_replace_i(c, " {2,}", " "); + c = xs_tolower_i(c); + + while (!r && !feof(f)) { + xs *rx = xs_strip_i(xs_readline(f)); + + if (*rx) { + xs *l = xs_regex_select_n(c, rx, 1); + + if (xs_list_len(l)) { + srv_debug(1, xs_fmt("content_check: match for '%s'", rx)); + r = 1; + } + } + } + + fclose(f); + } + } + + return r; +} + + /** notifications **/ xs_str *notify_check_time(snac *snac, int reset) @@ -2388,6 +2475,21 @@ void enqueue_verify_links(snac *user) } +void enqueue_actor_refresh(snac *user, const char *actor) +/* enqueues an actor refresh */ +{ + xs *qmsg = _new_qmsg("actor_refresh", "", 0); + char *ntid = xs_dict_get(qmsg, "ntid"); + xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid); + + qmsg = xs_dict_append(qmsg, "actor", actor); + + qmsg = _enqueue_put(fn, qmsg); + + snac_debug(user, 1, xs_fmt("enqueue_actor_refresh %s", actor)); +} + + void enqueue_request_replies(snac *user, const char *id) /* enqueues a request for the replies of a message */ { @@ -2645,6 +2747,16 @@ void purge_server(void) } } } + + /* delete index backups */ + xs *specb = xs_fmt("%s/" "*.bak", v); + xs *bakfs = xs_glob(specb, 0, 0); + + p2 = bakfs; + while (xs_list_iter(&p2, &v2)) { + unlink(v2); + srv_debug(1, xs_fmt("purged %s", v2)); + } } } @@ -221,12 +221,16 @@ Sends a Follow message for the specified actor URL. .It Cm request Ar basedir Ar uid Ar url Requests an object and dumps it to stdout. This is a very low level command that is not very useful to you. -.It Cm note Ar basedir Ar uid Ar text +.It Cm announce Ar basedir Ar uid Ar url +Announces (boosts) a post via its URL. +.It Cm note Ar basedir Ar uid Ar text Op file file ... Enqueues a Create + Note message to all followers. If the .Ar text -argument is -e, the external editor defined by the EDITOR +argument is -e, the external editor defined by the EDITOR environment variable will be invoked to prepare a message; if it's - (a lonely hyphen), the post content will be read from stdin. +The rest of command line arguments are treated as media files to be +attached to the post. .It Cm block Ar basedir Ar instance_url Blocks a full instance, given its URL or domain name. All subsequent incoming activities with identifiers from that instance will be immediately @@ -46,6 +46,9 @@ Strings in the format @user@host are requested using the Webfinger protocol and converted to links and mentions if something reasonable is found. .It Emoticons / Smileys / Silly Symbols +(Note: from version 2.51, these symbols are configurable by the +instance administrator, so the available ones may differ). +.Pp The following traditional ASCII emoticons or special strings are converted to related emojis: .Bd -literal @@ -106,6 +109,13 @@ This file is served when the server base URL is requested from a web browser. Se for more information about the customization options. .It Pa public.idx This file contains the list of public posts from all users in the server. +.It Pa filter_reject.txt +This (optional) file contains a list of regular expressions, one per line, to be +applied to the content of all incoming posts; if any of them match, the post is +rejected. This brings the flexibility and destruction power of regular expressions +to your Fediverse experience. To be used wisely (see +.Xr snac 8 +for more information). .El .Pp Each user directory is a subdirectory of @@ -230,9 +230,56 @@ for details. Further, every user can have a private CSS file in their that will be served instead of the server-wide one. It's not modifiable from the web interface to avoid users shooting themselves in the foot by destroying everything. -.Ss Old Data Purging -From version 2.06, there is no longer a need to add a special -cron job for purging old data, as this is managed internally. +.Ss Custom Emojis +From version 2.51, support for customized Emojis in posts is available +(previously, they were hardcoded). Emojis are read from the +.Pa emojis.json +file in the instance base directory, as a JSON object of key / value +pairs (if this file does not exist, it will be created with +the predefined set). Each key in the object contains the text to be found (e.g., +the :-) for a smiling face), and its associated value, the text string that +will replace it (in this example case, the HTML entity for the Unicode codepoint +for the smiley or the Emoji itself as text). +.Pp +Emoji values can also be URLs to image files; in this case, they will not be +substituted in the post content, but added to the 'tag' array as an ActivityPub +standard 'Emoji' object (it's recommendable that the Emoji key be enclosed in +colons for maximum compatilibity with other ActivityPub implementations, like +e.g. :happydoggo:). These images can be served from an external source or from the +.Pa static +directory of the instance admin. +.Pp +If you want to disable any Emoji substitution, change the file to contain +just an empty JSON object ({}). +.Ss SPAM Mitigation +There have been some SPAM attacks on the Fediverse and, as too many +instances and server implementations out there still allow automatic +account creation, it will only get worse. +.Nm +includes some (not very strong) tools for trying to survive the SPAM +flood that will eventually happen. +.Pp +The +.Ic min_account_age +field in the main configuration file allows setting a minimum age (in +seconds) to consider too recently created accounts suspicious of being +a potential source of SPAM. This is a naïve assumption, because spammers +can create accounts, let them dormant for a while and then start to use +them. Also, some ActivityPub implementations don't even bother to return +a creation date for their accounts, so this is not very useful. +.Pp +From version 2.50, post content can be filtered out by regular expressions. +These weapons of mass destruction can be written into the +.Ic filter_reject.txt +file in the server base directory, one per line; if this file exists, +all posts' content will be matched (after being stripped of HTML tags) +against these regexes, one by one, and any match will make the post to +be rejected. If you don't know about regular expressions, don't use this +option (or learn about them in some tutorial, there are gazillions of +them out there), as you and your users may start missing posts. Also, +given that every regular expression implementation supports a different +set of features, consider reading the documentation about the one +implemented in your system. .Ss ActivityPub Support These are the following activities and objects that .Nm @@ -421,7 +468,7 @@ This is an example of a similar configuration for the Apache2 web server: ProxyPreserveHost On # Main web access point -<Location /social> +<Location /fedi> ProxyPass http://127.0.0.1:8001/social </Location> @@ -481,7 +528,7 @@ an example: # other server configuration [...] -location "/fedi*" { +location "/fedi/*" { fastcgi socket tcp "127.0.0.1" 8001 } @@ -5,6 +5,8 @@ #include "xs_regex.h" #include "xs_mime.h" #include "xs_html.h" +#include "xs_json.h" +#include "xs_time.h" #include "snac.h" @@ -36,6 +38,46 @@ const char *smileys[] = { }; +xs_dict *emojis(void) +/* returns a dict with the emojis */ +{ + xs *fn = xs_fmt("%s/emojis.json", srv_basedir); + FILE *f; + + if (mtime(fn) == 0) { + /* file does not exist; create it with the defaults */ + xs *d = xs_dict_new(); + const char **emo = smileys; + + while (*emo) { + d = xs_dict_append(d, emo[0], emo[1]); + emo += 2; + } + + if ((f = fopen(fn, "w")) != NULL) { + xs_json_dump(d, 4, f); + fclose(f); + } + else + srv_log(xs_fmt("Error creating '%s'", fn)); + } + + xs_dict *d = NULL; + + if ((f = fopen(fn, "r")) != NULL) { + d = xs_json_load(f); + fclose(f); + + if (d == NULL) + srv_log(xs_fmt("JSON parse error in '%s'", fn)); + } + else + srv_log(xs_fmt("Error opening '%s'", fn)); + + return d; +} + + static xs_str *format_line(const char *line, xs_list **attach) /* formats a line */ { @@ -106,7 +148,7 @@ static xs_str *format_line(const char *line, xs_list **attach) } -xs_str *not_really_markdown(const char *content, xs_list **attach) +xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag) /* formats a content using some Markdown rules */ { xs_str *s = xs_str_new(NULL); @@ -190,11 +232,36 @@ xs_str *not_really_markdown(const char *content, xs_list **attach) { /* traditional emoticons */ - const char **emo = smileys; - - while (*emo) { - s = xs_replace_i(s, emo[0], emo[1]); - emo += 2; + xs *d = emojis(); + int c = 0; + char *k, *v; + + while (xs_dict_next(d, &k, &v, &c)) { + const char *t = NULL; + + /* is it an URL to an image? */ + if (xs_startswith(v, "https:/" "/") && xs_startswith((t = xs_mime_by_ext(v)), "image/")) { + if (tag) { + /* add the emoji to the tag list */ + xs *e = xs_dict_new(); + xs *i = xs_dict_new(); + xs *u = xs_str_utctime(0, ISO_DATE_SPEC); + + e = xs_dict_append(e, "id", v); + e = xs_dict_append(e, "type", "Emoji"); + e = xs_dict_append(e, "name", k); + e = xs_dict_append(e, "updated", u); + + i = xs_dict_append(i, "type", "Image"); + i = xs_dict_append(i, "mediaType", t); + i = xs_dict_append(i, "url", v); + e = xs_dict_append(e, "icon", i); + + *tag = xs_list_append(*tag, e); + } + } + else + s = xs_replace_i(s, k, v); } } @@ -113,9 +113,13 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, xs *name = actor_name(actor); /* get the avatar */ - if ((v = xs_dict_get(actor, "icon")) != NULL && - (v = xs_dict_get(v, "url")) != NULL) { - avatar = xs_dup(v); + if ((v = xs_dict_get(actor, "icon")) != NULL) { + /* if it's a list (Peertube), get the first one */ + if (xs_type(v) == XSTYPE_LIST) + v = xs_list_get(v, 0); + + if ((v = xs_dict_get(v, "url")) != NULL) + avatar = xs_dup(v); } if (avatar == NULL) @@ -245,7 +249,7 @@ xs_html *html_msg_icon(snac *user, char *actor_id, const xs_dict *msg) xs *actor = NULL; xs_html *actor_icon = NULL; - if (actor_id && valid_status(actor_get(actor_id, &actor))) { + if (actor_id && valid_status(actor_get_refresh(user, actor_id, &actor))) { char *date = NULL; char *udate = NULL; char *url = NULL; @@ -273,7 +277,8 @@ xs_html *html_note(snac *user, char *summary, char *edit_id, char *actor_id, xs_val *cw_yn, char *cw_text, xs_val *mnt_only, char *redir, - char *in_reply_to, int poll) + char *in_reply_to, int poll, + char *att_file, char *att_alt_text) { xs *action = xs_fmt("%s/admin/note", user->actor); @@ -359,19 +364,37 @@ xs_html *html_note(snac *user, char *summary, xs_html_attr("name", "edit_id"), xs_html_attr("value", edit_id))); + /* attachment controls */ + xs_html *att; + xs_html_add(form, xs_html_tag("p", NULL), - xs_html_tag("details", + att = xs_html_tag("details", xs_html_tag("summary", xs_html_text(L("Attachment..."))), - xs_html_tag("p", NULL), - xs_html_sctag("input", - xs_html_attr("type", "file"), - xs_html_attr("name", "attach")), - xs_html_sctag("input", - xs_html_attr("type", "text"), - xs_html_attr("name", "alt_text"), - xs_html_attr("placeholder", L("Attachment description"))))); + xs_html_tag("p", NULL))); + + if (att_file && *att_file) + xs_html_add(att, + xs_html_text(L("File:")), + xs_html_sctag("input", + xs_html_attr("type", "text"), + xs_html_attr("name", "attach_url"), + xs_html_attr("title", L("Clear this field to delete the attachment")), + xs_html_attr("value", att_file))); + else + xs_html_add(att, + xs_html_sctag("input", + xs_html_attr("type", "file"), + xs_html_attr("name", "attach"))); + + xs_html_add(att, + xs_html_text(" "), + xs_html_sctag("input", + xs_html_attr("type", "text"), + xs_html_attr("name", "alt_text"), + xs_html_attr("value", att_alt_text), + xs_html_attr("placeholder", L("Attachment description")))); /* add poll controls */ if (poll) { @@ -551,7 +574,7 @@ static xs_html *html_instance_body(char *tag) } -xs_html *html_user_head(snac *user, char *desc) +xs_html *html_user_head(snac *user, char *desc, char *url) { xs_html *head = html_base_head(); @@ -641,6 +664,13 @@ xs_html *html_user_head(snac *user, char *desc) xs_html_attr("title", "RSS"), xs_html_attr("href", rss_url))); + /* ActivityPub alternate link (actor id) */ + xs_html_add(head, + xs_html_sctag("link", + xs_html_attr("rel", "alternate"), + xs_html_attr("type", "application/activity+json"), + xs_html_attr("href", url ? url : user->actor))); + return head; } @@ -756,7 +786,7 @@ static xs_html *html_user_body(snac *user, int read_only) if (read_only) { xs *es1 = encode_html(xs_dict_get(user->config, "bio")); - xs *bio1 = not_really_markdown(es1, NULL); + xs *bio1 = not_really_markdown(es1, NULL, NULL); xs *tags = xs_list_new(); xs *bio2 = process_tags(user, bio1, &tags); @@ -774,7 +804,7 @@ static xs_html *html_user_body(snac *user, int read_only) xs_dict *val_links = user->links; if (xs_is_null(val_links)) - val_links = xs_stock_dict; + val_links = xs_stock(XSTYPE_DICT); xs_html *snac_metadata = xs_html_tag("div", xs_html_attr("class", "snac-metadata")); @@ -852,9 +882,9 @@ xs_html *html_top_controls(snac *snac) "new_post_div", "new_post_form", L("What's on your mind?"), "", NULL, NULL, - xs_stock_false, "", - xs_stock_false, NULL, - NULL, 1), + xs_stock(XSTYPE_FALSE), "", + xs_stock(XSTYPE_FALSE), NULL, + NULL, 1, "", ""), /** operations **/ xs_html_tag("details", @@ -1219,6 +1249,11 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const xs_html_add(form, html_button("like", L("Like"), L("Say you like this post"))); } + else { + /* not like it anymore */ + xs_html_add(form, + html_button("unlike", L("Unlike"), L("Nah don't like it that much"))); + } } else { if (is_pinned(snac, id)) @@ -1235,6 +1270,11 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const xs_html_add(form, html_button("boost", L("Boost"), L("Announce this post to your followers"))); } + else { + /* already boosted; add button to regret */ + xs_html_add(form, + html_button("unboost", L("Unboost"), L("I regret I boosted this"))); + } } if (strcmp(actor, snac->actor) != 0) { @@ -1278,6 +1318,20 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const xs *form_id = xs_fmt("%s_edit_form", md5); xs *redir = xs_fmt("%s_entry", md5); + char *att_file = ""; + char *att_alt_text = ""; + xs_list *att_list = xs_dict_get(msg, "attachment"); + + /* does it have an attachment? */ + if (xs_type(att_list) == XSTYPE_LIST && xs_list_len(att_list)) { + xs_dict *d = xs_list_get(att_list, 0); + + if (xs_type(d) == XSTYPE_DICT) { + att_file = xs_dict_get_def(d, "url", ""); + att_alt_text = xs_dict_get_def(d, "name", ""); + } + } + xs_html_add(controls, xs_html_tag("div", xs_html_tag("p", NULL), html_note(snac, L("Edit..."), @@ -1285,8 +1339,8 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const "", prev_src, id, NULL, xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"), - xs_stock_false, redir, - NULL, 0)), + xs_stock(XSTYPE_FALSE), redir, + NULL, 0, att_file, att_alt_text)), xs_html_tag("p", NULL)); } @@ -1304,8 +1358,8 @@ xs_html *html_entry_controls(snac *snac, char *actor, const xs_dict *msg, const "", ct, NULL, NULL, xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"), - xs_stock_false, redir, - id, 0)), + xs_stock(XSTYPE_FALSE), redir, + id, 0, "", "")), xs_html_tag("p", NULL)); } @@ -1858,6 +1912,9 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs_list *p = children; char *cmd5; + int cnt = 0; + int o_cnt = 0; + while (xs_list_iter(&p, &cmd5)) { xs *chd = NULL; @@ -1866,23 +1923,40 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, else object_get_by_md5(cmd5, &chd); - if (chd != NULL && xs_is_null(xs_dict_get(chd, "name"))) { - xs_html *che = html_entry(user, chd, read_only, level + 1, cmd5, hide_children); + if (chd != NULL) { + if (xs_is_null(xs_dict_get(chd, "name"))) { + xs_html *che = html_entry(user, chd, read_only, + level + 1, cmd5, hide_children); + + if (che != NULL) { + if (left > 3) { + xs_html_add(ch_older, + che); + + o_cnt++; + } + else + xs_html_add(ch_container, + che); - if (che != NULL) { - if (left > 3) - xs_html_add(ch_older, - che); - else - xs_html_add(ch_container, - che); + cnt++; + } } + + left--; } else srv_debug(2, xs_fmt("cannot read child %s", cmd5)); - - left--; } + + /* if no children were finally added, hide the details */ + if (cnt == 0) + xs_html_add(ch_details, + xs_html_attr("style", "display: none")); + + if (o_cnt == 0 && ch_older) + xs_html_add(ch_older, + xs_html_attr("style", "display: none")); } } @@ -1917,6 +1991,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, double t = ftime(); xs *desc = NULL; + xs *alternate = NULL; if (xs_list_len(list) == 1) { /* only one element? pick the description from the source */ @@ -1925,13 +2000,15 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, object_get_by_md5(id, &d); if (d && (v = xs_dict_get(d, "sourceContent")) != NULL) desc = xs_dup(v); + + alternate = xs_dup(xs_dict_get(d, "id")); } xs_html *head; xs_html *body; if (user) { - head = html_user_head(user, desc); + head = html_user_head(user, desc, alternate); body = html_user_body(user, read_only); } else { @@ -1977,9 +2054,16 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, if (user != NULL && !is_msg_public(msg)) { char *irt = xs_dict_get(msg, "inReplyTo"); + /* is it a reply to something not in the storage? */ if (!xs_is_null(irt) && !object_here(irt)) { - snac_debug(user, 1, xs_fmt("skipping non-public reply to an unknown post %s", v)); - continue; + /* is it for me? */ + xs_list *to = xs_dict_get_def(msg, "to", xs_stock(XSTYPE_LIST)); + xs_list *cc = xs_dict_get_def(msg, "cc", xs_stock(XSTYPE_LIST)); + + if (xs_list_in(to, user->actor) == -1 && xs_list_in(cc, user->actor) == -1) { + snac_debug(user, 1, xs_fmt("skipping non-public reply to an unknown post %s", v)); + continue; + } } } @@ -2077,9 +2161,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t) snac_posts = xs_html_tag("details", xs_html_attr("open", NULL), xs_html_tag("summary", - xs_html_text("...")), - xs_html_tag("div", - xs_html_attr("class", "snac-posts")))); + xs_html_text("...")))); xs_list *p = list; char *actor_id; @@ -2181,9 +2263,9 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t) dm_div_id, dm_form_id, "", "", NULL, actor_id, - xs_stock_false, "", - xs_stock_false, NULL, - NULL, 0), + xs_stock(XSTYPE_FALSE), "", + xs_stock(XSTYPE_FALSE), NULL, + NULL, 0, "", ""), xs_html_tag("p", NULL)); xs_html_add(snac_post, snac_controls); @@ -2202,10 +2284,12 @@ xs_str *html_people(snac *user) xs *wers = follower_list(user); xs_html *html = xs_html_tag("html", - html_user_head(user, NULL), + html_user_head(user, NULL, NULL), xs_html_add(html_user_body(user, 0), - html_people_list(user, wing, L("People you follow"), "i"), - html_people_list(user, wers, L("People that follow you"), "e"), + xs_html_tag("div", + xs_html_attr("class", "snac-posts"), + html_people_list(user, wing, L("People you follow"), "i"), + html_people_list(user, wers, L("People that follow you"), "e")), html_footer())); return xs_html_render_s(html, "<!DOCTYPE html>\n"); @@ -2220,7 +2304,7 @@ xs_str *html_notifications(snac *user, int skip, int show) xs_html *body = html_user_body(user, 0); xs_html *html = xs_html_tag("html", - html_user_head(user, NULL), + html_user_head(user, NULL, NULL), body); xs *clear_all_action = xs_fmt("%s/admin/clear-notifications", user->actor); @@ -2604,7 +2688,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, return 403; xs *elems = timeline_simple_list(&snac, "public", 0, 20); - xs *bio = not_really_markdown(xs_dict_get(snac.config, "bio"), NULL); + xs *bio = not_really_markdown(xs_dict_get(snac.config, "bio"), NULL, NULL); xs *rss_title = xs_fmt("%s (@%s@%s)", xs_dict_get(snac.config, "name"), @@ -2802,7 +2886,7 @@ int html_post_handler(const xs_dict *req, const char *q_path, msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, priv); if (sensitive != NULL) { - msg = xs_dict_set(msg, "sensitive", xs_stock_true); + msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE)); msg = xs_dict_set(msg, "summary", xs_is_null(summary) ? "..." : summary); } @@ -2881,6 +2965,22 @@ int html_post_handler(const xs_dict *req, const char *q_path, } } else + if (strcmp(action, L("Unlike")) == 0) { /** **/ + xs *msg = msg_repulsion(&snac, id, "Like"); + + if (msg != NULL) { + enqueue_message(&snac, msg); + } + } + else + if (strcmp(action, L("Unboost")) == 0) { /** **/ + xs *msg = msg_repulsion(&snac, id, "Announce"); + + if (msg != NULL) { + enqueue_message(&snac, msg); + } + } + else if (strcmp(action, L("MUTE")) == 0) { /** **/ mute(&snac, actor); } @@ -3036,17 +3136,17 @@ int html_post_handler(const xs_dict *req, const char *q_path, snac.config = xs_dict_set(snac.config, "purge_days", days); } if ((v = xs_dict_get(p_vars, "drop_dm_from_unknown")) != NULL && strcmp(v, "on") == 0) - snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock_true); + snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_TRUE)); else - snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock_false); + snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_FALSE)); if ((v = xs_dict_get(p_vars, "bot")) != NULL && strcmp(v, "on") == 0) - snac.config = xs_dict_set(snac.config, "bot", xs_stock_true); + snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_TRUE)); else - snac.config = xs_dict_set(snac.config, "bot", xs_stock_false); + snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_FALSE)); if ((v = xs_dict_get(p_vars, "private")) != NULL && strcmp(v, "on") == 0) - snac.config = xs_dict_set(snac.config, "private", xs_stock_true); + snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_TRUE)); else - snac.config = xs_dict_set(snac.config, "private", xs_stock_false); + snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_FALSE)); if ((v = xs_dict_get(p_vars, "metadata")) != NULL) { /* split the metadata and store it as a dict */ xs_dict *md = xs_dict_new(); @@ -388,6 +388,7 @@ void httpd_connection(FILE *f) body, xs_dict_get(srv_config, "host")); headers = xs_dict_append(headers, "WWW-Authenticate", www_auth); + headers = xs_dict_append(headers, "Cache-Control", "no-cache, must-revalidate, max-age=0"); } if (ctype == NULL) @@ -814,7 +815,7 @@ void httpd(void) /* send as many exit jobs as working threads */ for (n = 1; n < p_state->n_threads; n++) - job_post(xs_stock_false, 0); + job_post(xs_stock(XSTYPE_FALSE), 0); /* wait for all the threads to exit */ for (n = 0; n < p_state->n_threads; n++) @@ -5,6 +5,7 @@ #include "xs_io.h" #include "xs_json.h" #include "xs_time.h" +#include "xs_openssl.h" #include "snac.h" @@ -17,30 +18,32 @@ int usage(void) printf("\n"); printf("Commands:\n"); printf("\n"); - printf("init [{basedir}] Initializes the data storage\n"); - printf("upgrade {basedir} Upgrade to a new version\n"); - printf("adduser {basedir} [{uid}] Adds a new user\n"); - printf("deluser {basedir} {uid} Deletes a user\n"); - printf("httpd {basedir} Starts the HTTPD daemon\n"); - printf("purge {basedir} Purges old data\n"); - printf("state {basedir} Prints server state\n"); - printf("webfinger {basedir} {actor} Queries about an actor (@user@host or actor url)\n"); - printf("queue {basedir} {uid} Processes a user queue\n"); - printf("follow {basedir} {uid} {actor} Follows an actor\n"); - printf("unfollow {basedir} {uid} {actor} Unfollows an actor\n"); - printf("request {basedir} {uid} {url} Requests an object\n"); - printf("actor {basedir} [{uid}] {url} Requests an actor\n"); - printf("note {basedir} {uid} {'text'} Sends a note to followers\n"); - printf("resetpwd {basedir} {uid} Resets the password of a user\n"); - printf("ping {basedir} {uid} {actor} Pings an actor\n"); - printf("webfinger_s {basedir} {uid} {actor} Queries about an actor (@user@host or actor url)\n"); - printf("pin {basedir} {uid} {msg_url} Pins a message\n"); - printf("unpin {basedir} {uid} {msg_url} Unpins a message\n"); - printf("block {basedir} {instance_url} Blocks a full instance\n"); - printf("unblock {basedir} {instance_url} Unblocks a full instance\n"); - printf("limit {basedir} {uid} {actor} Limits an actor (drops their announces)\n"); - printf("unlimit {basedir} {uid} {actor} Unlimits an actor\n"); - printf("verify_links {basedir} {uid} Verifies a user's links (in the metadata)\n"); + printf("init [{basedir}] Initializes the data storage\n"); + printf("upgrade {basedir} Upgrade to a new version\n"); + printf("adduser {basedir} [{uid}] Adds a new user\n"); + printf("deluser {basedir} {uid} Deletes a user\n"); + printf("httpd {basedir} Starts the HTTPD daemon\n"); + printf("purge {basedir} Purges old data\n"); + printf("state {basedir} Prints server state\n"); + printf("webfinger {basedir} {actor} Queries about an actor (@user@host or actor url)\n"); + printf("queue {basedir} {uid} Processes a user queue\n"); + printf("follow {basedir} {uid} {actor} Follows an actor\n"); + printf("unfollow {basedir} {uid} {actor} Unfollows an actor\n"); + printf("request {basedir} {uid} {url} Requests an object\n"); + printf("actor {basedir} [{uid}] {url} Requests an actor\n"); + printf("note {basedir} {uid} {text} [files...] Sends a note with optional attachments\n"); + printf("boost|announce {basedir} {uid} {url} Boosts (announces) a post\n"); + printf("unboost {basedir} {uid} {url} Unboosts a post\n"); + printf("resetpwd {basedir} {uid} Resets the password of a user\n"); + printf("ping {basedir} {uid} {actor} Pings an actor\n"); + printf("webfinger_s {basedir} {uid} {actor} Queries about an actor (@user@host or actor url)\n"); + printf("pin {basedir} {uid} {msg_url} Pins a message\n"); + printf("unpin {basedir} {uid} {msg_url} Unpins a message\n"); + printf("block {basedir} {instance_url} Blocks a full instance\n"); + printf("unblock {basedir} {instance_url} Unblocks a full instance\n"); + printf("limit {basedir} {uid} {actor} Limits an actor (drops their announces)\n"); + printf("unlimit {basedir} {uid} {actor} Unlimits an actor\n"); + printf("verify_links {basedir} {uid} Verifies a user's links (in the metadata)\n"); return 1; } @@ -96,7 +99,7 @@ int main(int argc, char *argv[]) if (strcmp(cmd, "markdown") == 0) { /** **/ /* undocumented, for testing only */ xs *c = xs_readall(stdin); - xs *fc = not_really_markdown(c, NULL); + xs *fc = not_really_markdown(c, NULL, NULL); printf("<html>\n%s\n</html>\n", fc); return 0; @@ -279,7 +282,7 @@ int main(int argc, char *argv[]) return 0; } - if (strcmp(cmd, "announce") == 0) { /** **/ + if (strcmp(cmd, "boost") == 0 || strcmp(cmd, "announce") == 0) { /** **/ xs *msg = msg_admiration(&snac, url, "Announce"); if (msg != NULL) { @@ -293,6 +296,20 @@ int main(int argc, char *argv[]) return 0; } + if (strcmp(cmd, "unboost") == 0) { /** **/ + xs *msg = msg_repulsion(&snac, url, "Announce"); + + if (msg != NULL) { + enqueue_message(&snac, msg); + + if (dbglevel) { + xs_json_dump(msg, 4, stdout); + } + } + + return 0; + } + if (strcmp(cmd, "follow") == 0) { /** **/ xs *msg = msg_follow(&snac, url); @@ -360,6 +377,14 @@ int main(int argc, char *argv[]) if (strcmp(cmd, "ping") == 0) { /** **/ xs *actor_o = NULL; + if (!xs_startswith(url, "https:/")) { + /* try to resolve via webfinger */ + if (!valid_status(webfinger_request(url, &url, NULL))) { + srv_log(xs_fmt("cannot resolve %s via webfinger", url)); + return 1; + } + } + if (valid_status(actor_request(&snac, url, &actor_o))) { xs *msg = msg_ping(&snac, url); @@ -368,6 +393,8 @@ int main(int argc, char *argv[]) if (dbglevel) { xs_json_dump(msg, 4, stdout); } + + srv_log(xs_fmt("Ping sent to %s -- see log for Pong reply", url)); } else { srv_log(xs_fmt("Error getting actor %s", url)); @@ -450,7 +477,39 @@ int main(int argc, char *argv[]) xs *content = NULL; xs *msg = NULL; xs *c_msg = NULL; - char *in_reply_to = GET_ARGV(); + xs *attl = xs_list_new(); + char *fn = NULL; + + /* iterate possible attachments */ + while ((fn = GET_ARGV())) { + FILE *f; + + if ((f = fopen(fn, "rb")) != NULL) { + /* get the file size and content */ + fseek(f, 0, SEEK_END); + int sz = ftell(f); + fseek(f, 0, SEEK_SET); + xs *atc = xs_readall(f); + fclose(f); + + char *ext = strrchr(fn, '.'); + xs *hash = xs_md5_hex(fn, strlen(fn)); + xs *id = xs_fmt("%s%s", hash, ext); + xs *url = xs_fmt("%s/s/%s", snac.actor, id); + + /* store */ + static_put(&snac, id, atc, sz); + + xs *l = xs_list_new(); + + l = xs_list_append(l, url); + l = xs_list_append(l, ""); /* alt text */ + + attl = xs_list_append(attl, l); + } + else + fprintf(stderr, "Error opening '%s' as attachment\n", fn); + } if (strcmp(url, "-e") == 0) { /* get the content from an editor */ @@ -478,7 +537,7 @@ int main(int argc, char *argv[]) else content = xs_dup(url); - msg = msg_note(&snac, content, NULL, in_reply_to, NULL, 0); + msg = msg_note(&snac, content, NULL, NULL, attl, 0); c_msg = msg_create(&snac, msg); @@ -289,7 +289,11 @@ int oauth_post_handler(const xs_dict *req, const char *q_path, *body = xs_dup(code); } else { - *body = xs_fmt("%s?code=%s", redir, code); + if (xs_str_in(redir, "?")) + *body = xs_fmt("%s&code=%s", redir, code); + else + *body = xs_fmt("%s?code=%s", redir, code); + status = 303; } @@ -335,8 +339,8 @@ int oauth_post_handler(const xs_dict *req, const char *q_path, /* FIXME: this 'scope' parameter is mandatory for the official Mastodon API, but if it's enabled, it makes it crash after some more steps, which is FAR WORSE */ -// const char *scope = xs_dict_get(args, "scope"); const char *scope = NULL; +// scope = xs_dict_get(args, "scope"); /* no client_secret? check if it's inside an authorization header (AndStatus does it this way) */ @@ -359,9 +363,9 @@ int oauth_post_handler(const xs_dict *req, const char *q_path, } } - /* no code? - I'm not sure of the impacts of this right now, but Subway Tooter does not - provide a code so one must be generated */ + /* no code? + I'm not sure of the impacts of this right now, but Subway Tooter does not + provide a code so one must be generated */ if (xs_is_null(code)){ code = random_str(); } @@ -522,6 +526,12 @@ xs_dict *mastoapi_account(const xs_dict *actor) acct = xs_dict_append(acct, "id", acct_md5); acct = xs_dict_append(acct, "username", prefu); acct = xs_dict_append(acct, "display_name", display_name); + acct = xs_dict_append(acct, "discoverable", xs_stock(XSTYPE_TRUE)); + acct = xs_dict_append(acct, "group", xs_stock(XSTYPE_FALSE)); + acct = xs_dict_append(acct, "hide_collections", xs_stock(XSTYPE_FALSE)); + acct = xs_dict_append(acct, "indexable", xs_stock(XSTYPE_TRUE)); + acct = xs_dict_append(acct, "noindex", xs_stock(XSTYPE_FALSE)); + acct = xs_dict_append(acct, "roles", xs_stock(XSTYPE_LIST)); { /* create the acct field as user@host */ @@ -543,13 +553,14 @@ xs_dict *mastoapi_account(const xs_dict *actor) note = ""; if (strcmp(xs_dict_get(actor, "type"), "Service") == 0) - acct = xs_dict_append(acct, "bot", xs_stock_true); + acct = xs_dict_append(acct, "bot", xs_stock(XSTYPE_TRUE)); else - acct = xs_dict_append(acct, "bot", xs_stock_false); + acct = xs_dict_append(acct, "bot", xs_stock(XSTYPE_FALSE)); acct = xs_dict_append(acct, "note", note); acct = xs_dict_append(acct, "url", id); + acct = xs_dict_append(acct, "uri", id); xs *avatar = NULL; xs_dict *av = xs_dict_get(actor, "icon"); @@ -574,7 +585,7 @@ xs_dict *mastoapi_account(const xs_dict *actor) header = xs_dup(xs_dict_get(hd, "url")); if (xs_is_null(header)) - header = xs_dup(""); + header = xs_fmt("%s/header.png", srv_baseurl); acct = xs_dict_append(acct, "header", header); acct = xs_dict_append(acct, "header_static", header); @@ -602,7 +613,7 @@ xs_dict *mastoapi_account(const xs_dict *actor) d1 = xs_dict_append(d1, "shortcode", nm); d1 = xs_dict_append(d1, "url", url); d1 = xs_dict_append(d1, "static_url", url); - d1 = xs_dict_append(d1, "visible_in_picker", xs_stock_true); + d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE)); eml = xs_list_append(eml, d1); } @@ -613,10 +624,10 @@ xs_dict *mastoapi_account(const xs_dict *actor) acct = xs_dict_append(acct, "emojis", eml); } - acct = xs_dict_append(acct, "locked", xs_stock_false); - acct = xs_dict_append(acct, "followers_count", xs_stock_0); - acct = xs_dict_append(acct, "following_count", xs_stock_0); - acct = xs_dict_append(acct, "statuses_count", xs_stock_0); + acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE)); + acct = xs_dict_append(acct, "followers_count", xs_stock(0)); + acct = xs_dict_append(acct, "following_count", xs_stock(0)); + acct = xs_dict_append(acct, "statuses_count", xs_stock(0)); xs *fields = xs_list_new(); p = xs_dict_get(actor, "attachment"); @@ -624,19 +635,19 @@ xs_dict *mastoapi_account(const xs_dict *actor) /* dict of validated links */ xs_dict *val_links = NULL; - xs_dict *metadata = xs_stock_dict; + xs_dict *metadata = xs_stock(XSTYPE_DICT); snac user = {0}; if (xs_startswith(id, srv_baseurl)) { /* if it's a local user, open it and pick its validated links */ if (user_open(&user, prefu)) { val_links = user.links; - metadata = xs_dict_get_def(user.config, "metadata", xs_stock_dict); + metadata = xs_dict_get_def(user.config, "metadata", xs_stock(XSTYPE_DICT)); } } if (xs_is_null(val_links)) - val_links = xs_stock_dict; + val_links = xs_stock(XSTYPE_DICT); while (xs_list_iter(&p, &v)) { char *type = xs_dict_get(v, "type"); @@ -665,7 +676,7 @@ xs_dict *mastoapi_account(const xs_dict *actor) d = xs_dict_append(d, "value", value); d = xs_dict_append(d, "verified_at", xs_type(val_date) == XSTYPE_STRING && *val_date ? - val_date : xs_stock_null); + val_date : xs_stock(XSTYPE_NULL)); fields = xs_list_append(fields, d); } @@ -703,13 +714,13 @@ xs_dict *mastoapi_poll(snac *snac, const xs_dict *msg) xs *fd = mastoapi_date(xs_dict_get(msg, "endTime")); poll = xs_dict_append(poll, "expires_at", fd); poll = xs_dict_append(poll, "expired", - xs_dict_get(msg, "closed") != NULL ? xs_stock_true : xs_stock_false); + xs_dict_get(msg, "closed") != NULL ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE)); if ((opts = xs_dict_get(msg, "oneOf")) != NULL) - poll = xs_dict_append(poll, "multiple", xs_stock_false); + poll = xs_dict_append(poll, "multiple", xs_stock(XSTYPE_FALSE)); else { opts = xs_dict_get(msg, "anyOf"); - poll = xs_dict_append(poll, "multiple", xs_stock_true); + poll = xs_dict_append(poll, "multiple", xs_stock(XSTYPE_TRUE)); } while (xs_list_iter(&opts, &v)) { @@ -736,7 +747,7 @@ xs_dict *mastoapi_poll(snac *snac, const xs_dict *msg) poll = xs_dict_append(poll, "voted", (snac && was_question_voted(snac, xs_dict_get(msg, "id"))) ? - xs_stock_true : xs_stock_false); + xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE)); return poll; } @@ -746,7 +757,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) /* converts an ActivityPub note to a Mastodon status */ { xs *actor = NULL; - actor_get(get_atto(msg), &actor); + actor_get_refresh(snac, get_atto(msg), &actor); /* if the author is not here, discard */ if (actor == NULL) @@ -802,7 +813,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) tmp = xs_dict_get(msg, "sensitive"); if (xs_is_null(tmp)) - tmp = xs_stock_false; + tmp = xs_stock(XSTYPE_FALSE); st = xs_dict_append(st, "sensitive", tmp); @@ -921,7 +932,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) d1 = xs_dict_append(d1, "shortcode", nm); d1 = xs_dict_append(d1, "url", url); d1 = xs_dict_append(d1, "static_url", url); - d1 = xs_dict_append(d1, "visible_in_picker", xs_stock_true); + d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE)); d1 = xs_dict_append(d1, "category", "Emojis"); eml = xs_list_append(eml, d1); @@ -942,7 +953,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) st = xs_dict_append(st, "favourites_count", ixc); st = xs_dict_append(st, "favourited", - (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock_true : xs_stock_false); + (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE)); xs_free(idx); xs_free(ixc); @@ -951,7 +962,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) st = xs_dict_append(st, "reblogs_count", ixc); st = xs_dict_append(st, "reblogged", - (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock_true : xs_stock_false); + (snac && xs_list_in(idx, snac->md5) != -1) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE)); /* get the last person who boosted this */ xs *boosted_by_md5 = NULL; @@ -966,8 +977,8 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) st = xs_dict_append(st, "replies_count", ixc); /* default in_reply_to values */ - st = xs_dict_append(st, "in_reply_to_id", xs_stock_null); - st = xs_dict_append(st, "in_reply_to_account_id", xs_stock_null); + st = xs_dict_append(st, "in_reply_to_id", xs_stock(XSTYPE_NULL)); + st = xs_dict_append(st, "in_reply_to_account_id", xs_stock(XSTYPE_NULL)); tmp = xs_dict_get(msg, "inReplyTo"); if (!xs_is_null(tmp)) { @@ -985,9 +996,9 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) } } - st = xs_dict_append(st, "reblog", xs_stock_null); - st = xs_dict_append(st, "card", xs_stock_null); - st = xs_dict_append(st, "language", xs_stock_null); + st = xs_dict_append(st, "reblog", xs_stock(XSTYPE_NULL)); + st = xs_dict_append(st, "card", xs_stock(XSTYPE_NULL)); + st = xs_dict_append(st, "language", xs_stock(XSTYPE_NULL)); tmp = xs_dict_get(msg, "sourceContent"); if (xs_is_null(tmp)) @@ -998,7 +1009,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) tmp = xs_dict_get(msg, "updated"); xs *fd2 = NULL; if (xs_is_null(tmp)) - tmp = xs_stock_null; + tmp = xs_stock(XSTYPE_NULL); else { fd2 = mastoapi_date(tmp); tmp = fd2; @@ -1011,12 +1022,12 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) st = xs_dict_append(st, "poll", poll); } else - st = xs_dict_append(st, "poll", xs_stock_null); + st = xs_dict_append(st, "poll", xs_stock(XSTYPE_NULL)); - st = xs_dict_append(st, "bookmarked", xs_stock_false); + st = xs_dict_append(st, "bookmarked", xs_stock(XSTYPE_FALSE)); st = xs_dict_append(st, "pinned", - (snac && is_pinned(snac, id)) ? xs_stock_true : xs_stock_false); + (snac && is_pinned(snac, id)) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE)); /* is it a boost? */ if (!xs_is_null(boosted_by_md5)) { @@ -1060,21 +1071,21 @@ xs_dict *mastoapi_relationship(snac *snac, const char *md5) rel = xs_dict_append(rel, "id", md5); rel = xs_dict_append(rel, "following", - following_check(snac, actor) ? xs_stock_true : xs_stock_false); + following_check(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE)); - rel = xs_dict_append(rel, "showing_reblogs", xs_stock_true); - rel = xs_dict_append(rel, "notifying", xs_stock_false); + rel = xs_dict_append(rel, "showing_reblogs", xs_stock(XSTYPE_TRUE)); + rel = xs_dict_append(rel, "notifying", xs_stock(XSTYPE_FALSE)); rel = xs_dict_append(rel, "followed_by", - follower_check(snac, actor) ? xs_stock_true : xs_stock_false); + follower_check(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE)); rel = xs_dict_append(rel, "blocking", - is_muted(snac, actor) ? xs_stock_true : xs_stock_false); + is_muted(snac, actor) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE)); - rel = xs_dict_append(rel, "muting", xs_stock_false); - rel = xs_dict_append(rel, "muting_notifications", xs_stock_false); - rel = xs_dict_append(rel, "requested", xs_stock_false); - rel = xs_dict_append(rel, "domain_blocking", xs_stock_false); - rel = xs_dict_append(rel, "endorsed", xs_stock_false); + rel = xs_dict_append(rel, "muting", xs_stock(XSTYPE_FALSE)); + rel = xs_dict_append(rel, "muting_notifications", xs_stock(XSTYPE_FALSE)); + rel = xs_dict_append(rel, "requested", xs_stock(XSTYPE_FALSE)); + rel = xs_dict_append(rel, "domain_blocking", xs_stock(XSTYPE_FALSE)); + rel = xs_dict_append(rel, "endorsed", xs_stock(XSTYPE_FALSE)); rel = xs_dict_append(rel, "note", ""); } @@ -1142,9 +1153,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, acct = xs_dict_append(acct, "last_status_at", xs_dict_get(snac1.config, "published")); acct = xs_dict_append(acct, "note", xs_dict_get(snac1.config, "bio")); acct = xs_dict_append(acct, "url", snac1.actor); - acct = xs_dict_append(acct, "header", ""); - acct = xs_dict_append(acct, "header_static", ""); - acct = xs_dict_append(acct, "locked", xs_stock_false); + acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE)); acct = xs_dict_append(acct, "bot", xs_dict_get(snac1.config, "bot")); xs *src = xs_json_loads("{\"privacy\":\"public\"," @@ -1162,6 +1171,17 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, acct = xs_dict_append(acct, "avatar", avatar); acct = xs_dict_append(acct, "avatar_static", avatar); + xs *header = NULL; + char *hd = xs_dict_get(snac1.config, "header"); + + if (!xs_is_null(hd)) + header = xs_dup(hd); + else + header = xs_fmt("%s/header.png", srv_baseurl); + + acct = xs_dict_append(acct, "header", header); + acct = xs_dict_append(acct, "header_static", header); + xs_dict *metadata = xs_dict_get(snac1.config, "metadata"); if (xs_type(metadata) == XSTYPE_DICT) { xs *fields = xs_list_new(); @@ -1170,7 +1190,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, xs_dict *val_links = snac1.links; if (xs_is_null(val_links)) - val_links = xs_stock_dict; + val_links = xs_stock(XSTYPE_DICT); int c = 0; while (xs_dict_next(metadata, &k, &v, &c)) { @@ -1190,7 +1210,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, d = xs_dict_append(d, "value", v); d = xs_dict_append(d, "verified_at", xs_type(val_date) == XSTYPE_STRING && *val_date ? - val_date : xs_stock_null); + val_date : xs_stock(XSTYPE_NULL)); fields = xs_list_append(fields, d); } @@ -1198,9 +1218,9 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, acct = xs_dict_set(acct, "fields", fields); } - acct = xs_dict_append(acct, "followers_count", xs_stock_0); - acct = xs_dict_append(acct, "following_count", xs_stock_0); - acct = xs_dict_append(acct, "statuses_count", xs_stock_0); + acct = xs_dict_append(acct, "followers_count", xs_stock(0)); + acct = xs_dict_append(acct, "following_count", xs_stock(0)); + acct = xs_dict_append(acct, "statuses_count", xs_stock(0)); *body = xs_json_dumps(acct, 4); *ctype = "application/json"; @@ -1716,8 +1736,8 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, else if (strcmp(cmd, "/v2/filters") == 0) { /** **/ /* snac will never have filters - * but still, without a v2 endpoint a short delay is introduced - * in some apps */ + * but still, without a v2 endpoint a short delay is introduced + * in some apps */ *body = xs_dup("[]"); *ctype = "application/json"; status = 200; @@ -1797,7 +1817,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, ins = xs_dict_append(ins, "email", v); - ins = xs_dict_append(ins, "rules", xs_stock_list); + ins = xs_dict_append(ins, "rules", xs_stock(XSTYPE_LIST)); xs *l1 = xs_list_append(xs_list_new(), "en"); ins = xs_dict_append(ins, "languages", l1); @@ -1808,14 +1828,14 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, ins = xs_dict_append(ins, "urls", urls); - xs *d2 = xs_dict_append(xs_dict_new(), "user_count", xs_stock_0); - d2 = xs_dict_append(d2, "status_count", xs_stock_0); - d2 = xs_dict_append(d2, "domain_count", xs_stock_0); + xs *d2 = xs_dict_append(xs_dict_new(), "user_count", xs_stock(0)); + d2 = xs_dict_append(d2, "status_count", xs_stock(0)); + d2 = xs_dict_append(d2, "domain_count", xs_stock(0)); ins = xs_dict_append(ins, "stats", d2); - ins = xs_dict_append(ins, "registrations", xs_stock_false); - ins = xs_dict_append(ins, "approval_required", xs_stock_false); - ins = xs_dict_append(ins, "invites_enabled", xs_stock_false); + ins = xs_dict_append(ins, "registrations", xs_stock(XSTYPE_FALSE)); + ins = xs_dict_append(ins, "approval_required", xs_stock(XSTYPE_FALSE)); + ins = xs_dict_append(ins, "invites_enabled", xs_stock(XSTYPE_FALSE)); xs *cfg = xs_dict_new(); @@ -2063,7 +2083,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, d = xs_dict_append(d, "name", q); xs *url = xs_fmt("%s?t=%s", srv_baseurl, q); d = xs_dict_append(d, "url", url); - d = xs_dict_append(d, "history", xs_stock_list); + d = xs_dict_append(d, "history", xs_stock(XSTYPE_LIST)); htl = xs_list_append(htl, d); } @@ -2103,8 +2123,6 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/")) return 0; - srv_debug(1, xs_fmt("mastoapi_post_handler %s", q_path)); - int status = 404; xs *args = NULL; char *i_ctype = xs_dict_get(req, "content-type"); @@ -2115,7 +2133,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, } else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded")) { - // Some apps send form data instead of json so we should cater for those + // Some apps send form data instead of json so we should cater for those if (!xs_is_null(payload)) { xs *upl = xs_url_dec(payload); args = xs_url_vars(upl); @@ -2241,7 +2259,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, strcmp(visibility, "public") == 0 ? 0 : 1); if (!xs_is_null(summary) && *summary) { - msg = xs_dict_set(msg, "sensitive", xs_stock_true); + msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE)); msg = xs_dict_set(msg, "summary", summary); } @@ -2298,11 +2316,13 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, } else if (strcmp(op, "unfavourite") == 0) { /** **/ - /* partial support: as the original Like message - is not stored anywhere here, it's not possible - to send an Undo + Like; the only thing done here - is to delete the actor from the list of likes */ - object_unadmire(id, snac.actor, 1); + xs *n_msg = msg_repulsion(&snac, id, "Like"); + + if (n_msg != NULL) { + enqueue_message(&snac, n_msg); + + out = mastoapi_status(&snac, msg); + } } else if (strcmp(op, "reblog") == 0) { /** **/ @@ -2317,8 +2337,13 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, } else if (strcmp(op, "unreblog") == 0) { /** **/ - /* partial support: see comment in 'unfavourite' */ - object_unadmire(id, snac.actor, 0); + xs *n_msg = msg_repulsion(&snac, id, "Announce"); + + if (n_msg != NULL) { + enqueue_message(&snac, n_msg); + + out = mastoapi_status(&snac, msg); + } } else if (strcmp(op, "bookmark") == 0) { /** **/ @@ -2603,6 +2628,8 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, if (logged_in) user_free(&snac); + srv_debug(1, xs_fmt("mastoapi_post_handler %s %d", q_path, status)); + return status; } @@ -2701,7 +2728,7 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path, if (valid_status(timeline_get_by_md5(&snac, md5, &msg))) { const char *content = xs_dict_get(args, "status"); xs *atls = xs_list_new(); - xs *f_content = not_really_markdown(content, &atls); + xs *f_content = not_really_markdown(content, &atls, NULL); /* replace fields with new content */ msg = xs_dict_set(msg, "sourceContent", content); @@ -1,7 +1,7 @@ /* snac - A simple, minimalistic ActivityPub instance */ /* copyright (c) 2022 - 2024 grunfink et al. / MIT license */ -#define VERSION "2.50-dev" +#define VERSION "2.52-dev" #define USER_AGENT "snac/" VERSION @@ -110,6 +110,10 @@ int object_del(const char *id); int object_del_if_unref(const char *id); double object_ctime_by_md5(const char *md5); double object_ctime(const char *id); +double object_mtime_by_md5(const char *md5); +double object_mtime(const char *id); +void object_touch(const char *id); + int object_admire(const char *id, const char *actor, int like); int object_unadmire(const char *id, const char *actor, int like); @@ -172,6 +176,7 @@ xs_list *tag_search(char *tag, int skip, int show); int actor_add(const char *actor, xs_dict *msg); int actor_get(const char *actor, xs_dict **data); +int actor_get_refresh(snac *user, const char *actor, xs_dict **data); int static_get(snac *snac, const char *id, xs_val **data, int *size, const char *inm, xs_str **etag); void static_put(snac *snac, const char *id, const char *data, int size); @@ -204,6 +209,8 @@ int is_instance_blocked(const char *instance); int instance_block(const char *instance); int instance_unblock(const char *instance); +int content_check(const char *file, const xs_dict *msg); + void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries); void enqueue_shared_input(const xs_dict *msg, const xs_dict *req, int retries); void enqueue_output_raw(const char *keyid, const char *seckey, @@ -216,6 +223,7 @@ void enqueue_ntfy(const xs_str *msg, const char *ntfy_server, const char *ntfy_t void enqueue_message(snac *snac, const xs_dict *msg); void enqueue_close_question(snac *user, const char *id, int end_secs); void enqueue_verify_links(snac *user); +void enqueue_actor_refresh(snac *user, const char *actor); void enqueue_request_replies(snac *user, const char *id); int was_question_voted(snac *user, const char *id); @@ -256,6 +264,7 @@ char *get_atto(const xs_dict *msg); xs_list *get_attachments(const xs_dict *msg); xs_dict *msg_admiration(snac *snac, char *object, char *type); +xs_dict *msg_repulsion(snac *user, char *id, char *type); xs_dict *msg_create(snac *snac, const xs_dict *object); xs_dict *msg_follow(snac *snac, const char *actor); @@ -296,7 +305,8 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path, char *payload, int p_size, char **body, int *b_size, char **ctype); -xs_str *not_really_markdown(const char *content, xs_list **attach); +xs_dict *emojis(void); +xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag); xs_str *sanitize(const char *content); xs_str *encode_html(const char *str); @@ -45,6 +45,10 @@ typedef char xs_data; /* not really all, just very much */ #define XS_ALL 0xfffffff +#ifndef xs_countof +#define xs_countof(a) (sizeof((a)) / sizeof((*a))) +#endif + void *xs_free(void *ptr); void *_xs_realloc(void *ptr, size_t size, const char *file, int line, const char *func); #define xs_realloc(ptr, size) _xs_realloc(ptr, size, __FILE__, __LINE__, __FUNCTION__) @@ -61,6 +65,7 @@ xs_val *xs_collapse(xs_val *data, int offset, int size); xs_val *xs_insert_m(xs_val *data, int offset, const char *mem, int size); #define xs_insert(data, offset, data2) xs_insert_m(data, offset, data2, xs_size(data2)) #define xs_append_m(data, mem, size) xs_insert_m(data, xs_size(data) - 1, mem, size) +xs_val *xs_stock(int type); xs_str *xs_str_new(const char *str); xs_str *xs_str_new_sz(const char *mem, int sz); @@ -137,34 +142,11 @@ unsigned int xs_hash_func(const char *data, int size); #define XS_ASSERT_TYPE_NULL(v, t) (void)(0) #endif -extern xs_val xs_stock_null[]; -extern xs_val xs_stock_true[]; -extern xs_val xs_stock_false[]; -extern xs_val xs_stock_0[]; -extern xs_val xs_stock_1[]; -extern xs_val xs_stock_list[]; -extern xs_val xs_stock_dict[]; - #define xs_return(v) xs_val *__r = v; v = NULL; return __r #ifdef XS_IMPLEMENTATION -xs_val xs_stock_null[] = { XSTYPE_NULL }; -xs_val xs_stock_true[] = { XSTYPE_TRUE }; -xs_val xs_stock_false[] = { XSTYPE_FALSE }; -xs_val xs_stock_0[] = { XSTYPE_NUMBER, '0', '\0' }; -xs_val xs_stock_1[] = { XSTYPE_NUMBER, '1', '\0' }; - -#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ -xs_val xs_stock_list[] = { XSTYPE_LIST, 0, 0, 0, 1 + _XS_TYPE_SIZE + 1, XSTYPE_EOM }; -xs_val xs_stock_dict[] = { XSTYPE_DICT, 0, 0, 0, 1 + _XS_TYPE_SIZE + 1, XSTYPE_EOM }; -#else -xs_val xs_stock_list[] = { XSTYPE_LIST, 1 + _XS_TYPE_SIZE + 1, 0, 0, 0, XSTYPE_EOM }; -xs_val xs_stock_dict[] = { XSTYPE_DICT, 1 + _XS_TYPE_SIZE + 1, 0, 0, 0, XSTYPE_EOM }; -#endif - - void *_xs_realloc(void *ptr, size_t size, const char *file, int line, const char *func) { xs_val *ndata = realloc(ptr, size); @@ -369,10 +351,14 @@ int xs_cmp(const xs_val *v1, const xs_val *v2) xs_val *xs_dup(const xs_val *data) /* creates a duplicate of data */ { - int sz = xs_size(data); - xs_val *s = xs_realloc(NULL, _xs_blk_size(sz)); + xs_val *s = NULL; + + if (data) { + int sz = xs_size(data); + s = xs_realloc(NULL, _xs_blk_size(sz)); - memcpy(s, data, sz); + memcpy(s, data, sz); + } return s; } @@ -437,6 +423,39 @@ xs_val *xs_insert_m(xs_val *data, int offset, const char *mem, int size) } +xs_val *xs_stock(int type) +/* returns stock values */ +{ + static xs_val stock_null[] = { XSTYPE_NULL }; + static xs_val stock_true[] = { XSTYPE_TRUE }; + static xs_val stock_false[] = { XSTYPE_FALSE }; + static xs_val stock_0[] = { XSTYPE_NUMBER, '0', '\0' }; + static xs_val stock_1[] = { XSTYPE_NUMBER, '1', '\0' }; + static xs_list *stock_list = NULL; + static xs_dict *stock_dict = NULL; + + switch (type) { + case 0: return stock_0; + case 1: return stock_1; + case XSTYPE_NULL: return stock_null; + case XSTYPE_TRUE: return stock_true; + case XSTYPE_FALSE: return stock_false; + + case XSTYPE_LIST: + if (stock_list == NULL) + stock_list = xs_list_new(); + return stock_list; + + case XSTYPE_DICT: + if (stock_dict == NULL) + stock_dict = xs_dict_new(); + return stock_dict; + } + + return NULL; +} + + /** strings **/ xs_str *xs_str_new(const char *str) @@ -647,10 +666,14 @@ xs_str *xs_tolower_i(xs_str *str) xs_list *xs_list_new(void) /* creates a new list */ { - return memcpy( - xs_realloc(NULL, _xs_blk_size(sizeof(xs_stock_list))), - xs_stock_list, sizeof(xs_stock_list) - ); + int sz = 1 + _XS_TYPE_SIZE + 1; + xs_list *l = xs_realloc(NULL, sz); + memset(l, '\0', sz); + + l[0] = XSTYPE_LIST; + _xs_put_size(&l[1], sz); + + return l; } @@ -660,8 +683,8 @@ xs_list *_xs_list_write_litem(xs_list *list, int offset, const char *mem, int ds XS_ASSERT_TYPE(list, XSTYPE_LIST); if (mem == NULL) { - mem = xs_stock_null; - dsz = sizeof(xs_stock_null); + mem = xs_stock(XSTYPE_NULL); + dsz = xs_size(mem); } list = xs_expand(list, offset, dsz + 1); @@ -947,10 +970,14 @@ xs_list *xs_list_cat(xs_list *l1, const xs_list *l2) xs_dict *xs_dict_new(void) /* creates a new dict */ { - return memcpy( - xs_realloc(NULL, _xs_blk_size(sizeof(xs_stock_dict))), - xs_stock_dict, sizeof(xs_stock_dict) - ); + int sz = 1 + _XS_TYPE_SIZE + 1; + xs_dict *d = xs_realloc(NULL, sz); + memset(d, '\0', sz); + + d[0] = XSTYPE_DICT; + _xs_put_size(&d[1], sz); + + return d; } @@ -962,8 +989,8 @@ xs_dict *_xs_dict_write_ditem(xs_dict *dict, int offset, const xs_str *key, XS_ASSERT_TYPE(key, XSTYPE_STRING); if (data == NULL) { - data = xs_stock_null; - dsz = sizeof(xs_stock_null); + data = xs_stock(XSTYPE_NULL); + dsz = xs_size(data); } int ksz = xs_size(key); @@ -11,8 +11,10 @@ xs_val *xs_json_load(FILE *f); xs_val *xs_json_loads(const xs_str *json); xstype xs_json_load_type(FILE *f); -int xs_json_load_array_iter(FILE *f, xs_val **value, int *c); -int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, int *c); +int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c); +int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, int *c); +xs_list *xs_json_load_array(FILE *f); +xs_dict *xs_json_load_object(FILE *f); #ifdef XS_IMPLEMENTATION @@ -324,10 +326,9 @@ static xs_val *_xs_json_load_lexer(FILE *f, js_type *t) } -static xs_list *_xs_json_load_array(FILE *f); -static xs_dict *_xs_json_load_object(FILE *f); - -int xs_json_load_array_iter(FILE *f, xs_val **value, int *c) +int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c) +/* loads the next scalar value from the JSON stream */ +/* if the value ahead is compound, value is NULL and pt is set */ { js_type t; @@ -346,14 +347,16 @@ int xs_json_load_array_iter(FILE *f, xs_val **value, int *c) return -1; } - if (t == JS_OBRACK) - *value = _xs_json_load_array(f); - else - if (t == JS_OCURLY) - *value = _xs_json_load_object(f); - - if (*value == NULL) - return -1; + if (*value == NULL) { + /* possible compound type ahead */ + if (t == JS_OBRACK) + *pt = XSTYPE_LIST; + else + if (t == JS_OCURLY) + *pt = XSTYPE_DICT; + else + return -1; + } *c = *c + 1; @@ -361,21 +364,38 @@ int xs_json_load_array_iter(FILE *f, xs_val **value, int *c) } -static xs_list *_xs_json_load_array(FILE *f) -/* parses a JSON array */ +xs_list *xs_json_load_array(FILE *f) +/* loads a full JSON array (after the initial OBRACK) */ { + xstype t; xs_list *l = xs_list_new(); int c = 0; for (;;) { xs *v = NULL; - int r = xs_json_load_array_iter(f, &v, &c); + int r = xs_json_load_array_iter(f, &v, &t, &c); if (r == -1) l = xs_free(l); - if (r == 1) + if (r == 1) { + /* partial load? */ + if (v == NULL) { + if (t == XSTYPE_LIST) + v = xs_json_load_array(f); + else + if (t == XSTYPE_DICT) + v = xs_json_load_object(f); + } + + /* still null? fail */ + if (v == NULL) { + l = xs_free(l); + break; + } + l = xs_list_append(l, v); + } else break; } @@ -384,7 +404,9 @@ static xs_list *_xs_json_load_array(FILE *f) } -int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, int *c) +int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, int *c) +/* loads the next key and scalar value from the JSON stream */ +/* if the value ahead is compound, value is NULL and pt is set */ { js_type t; @@ -413,14 +435,16 @@ int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, int *c) *value = _xs_json_load_lexer(f, &t); - if (t == JS_OBRACK) - *value = _xs_json_load_array(f); - else - if (t == JS_OCURLY) - *value = _xs_json_load_object(f); - - if (*value == NULL) - return -1; + if (*value == NULL) { + /* possible complex type ahead */ + if (t == JS_OBRACK) + *pt = XSTYPE_LIST; + else + if (t == JS_OCURLY) + *pt = XSTYPE_DICT; + else + return -1; + } *c = *c + 1; @@ -428,22 +452,39 @@ int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, int *c) } -static xs_dict *_xs_json_load_object(FILE *f) -/* parses a JSON object */ +xs_dict *xs_json_load_object(FILE *f) +/* loads a full JSON object (after the initial OCURLY) */ { + xstype t; xs_dict *d = xs_dict_new(); int c = 0; for (;;) { xs *k = NULL; xs *v = NULL; - int r = xs_json_load_object_iter(f, &k, &v, &c); + int r = xs_json_load_object_iter(f, &k, &v, &t, &c); if (r == -1) d = xs_free(d); - if (r == 1) + if (r == 1) { + /* partial load? */ + if (v == NULL) { + if (t == XSTYPE_LIST) + v = xs_json_load_array(f); + else + if (t == XSTYPE_DICT) + v = xs_json_load_object(f); + } + + /* still null? fail */ + if (v == NULL) { + d = xs_free(d); + break; + } + d = xs_dict_append(d, k, v); + } else break; } @@ -492,10 +533,10 @@ xs_val *xs_json_load(FILE *f) xstype t = xs_json_load_type(f); if (t == XSTYPE_LIST) - v = _xs_json_load_array(f); + v = xs_json_load_array(f); else if (t == XSTYPE_DICT) - v = _xs_json_load_object(f); + v = xs_json_load_object(f); return v; } @@ -55,19 +55,23 @@ const char *xs_mime_by_ext(const char *file) const char *ext = strrchr(file, '.'); if (ext) { - const char **p = xs_mime_types; - xs *uext = xs_tolower_i(xs_dup(ext + 1)); + xs *uext = xs_tolower_i(xs_dup(ext + 1)); + int b = 0; + int t = xs_countof(xs_mime_types) / 2 - 2; - while (*p) { - int c; + while (t >= b) { + int n = (b + t) / 2; + const char *p = xs_mime_types[n * 2]; - if ((c = strcmp(*p, uext)) == 0) - return p[1]; + int c = strcmp(uext, p); + + if (c < 0) + t = n - 1; else if (c > 0) - break; - - p += 2; + b = n + 1; + else + return xs_mime_types[(n * 2) + 1]; } } diff --git a/xs_unicode.h b/xs_unicode.h index 47e1101..6654da4 100644 --- a/xs_unicode.h +++ b/xs_unicode.h @@ -27,8 +27,8 @@ #ifdef XS_IMPLEMENTATION -#ifndef countof -#define countof(a) (sizeof((a)) / sizeof((*a))) +#ifndef xs_countof +#define xs_countof(a) (sizeof((a)) / sizeof((*a))) #endif int _xs_utf8_enc(char buf[4], unsigned int cpoint) @@ -125,7 +125,7 @@ int xs_unicode_width(unsigned int cpoint) /* returns the width in columns of a Unicode codepoint (somewhat simplified) */ { int b = 0; - int t = countof(xs_unicode_width_table) / 3 - 1; + int t = xs_countof(xs_unicode_width_table) / 3 - 1; while (t >= b) { int n = (b + t) / 2; @@ -193,7 +193,7 @@ unsigned int *_xs_unicode_upper_search(unsigned int cpoint) /* searches for an uppercase codepoint in the case fold table */ { int b = 0; - int t = countof(xs_unicode_case_fold_table) / 2 + 1; + int t = xs_countof(xs_unicode_case_fold_table) / 2 + 1; while (t >= b) { int n = (b + t) / 2; @@ -216,7 +216,7 @@ unsigned int *_xs_unicode_lower_search(unsigned int cpoint) /* searches for a lowercase codepoint in the case fold table */ { unsigned int *p = xs_unicode_case_fold_table; - unsigned int *e = p + countof(xs_unicode_case_fold_table); + unsigned int *e = p + xs_countof(xs_unicode_case_fold_table); while (p < e) { if (cpoint == p[1]) @@ -251,7 +251,7 @@ int xs_unicode_nfd(unsigned int cpoint, unsigned int *base, unsigned int *diac) /* applies unicode Normalization Form D */ { int b = 0; - int t = countof(xs_unicode_nfd_table) / 3 - 1; + int t = xs_countof(xs_unicode_nfd_table) / 3 - 1; while (t >= b) { int n = (b + t) / 2; @@ -279,7 +279,7 @@ int xs_unicode_nfc(unsigned int base, unsigned int diac, unsigned int *cpoint) /* applies unicode Normalization Form C */ { unsigned int *p = xs_unicode_nfd_table; - unsigned int *e = p + countof(xs_unicode_nfd_table); + unsigned int *e = p + xs_countof(xs_unicode_nfd_table); while (p < e) { if (p[1] == base && p[2] == diac) { @@ -298,7 +298,7 @@ int xs_unicode_is_alpha(unsigned int cpoint) /* checks if a codepoint is an alpha (i.e. a letter) */ { int b = 0; - int t = countof(xs_unicode_alpha_table) / 2 - 1; + int t = xs_countof(xs_unicode_alpha_table) / 2 - 1; while (t >= b) { int n = (b + t) / 2; @@ -56,7 +56,7 @@ xs_dict *xs_url_vars(const char *str) l = args; while (xs_list_iter(&l, &v)) { - xs *kv = xs_split_n(v, "=", 2); + xs *kv = xs_split_n(v, "=", 1); if (xs_list_len(kv) == 2) { const char *key = xs_list_get(kv, 0); diff --git a/xs_version.h b/xs_version.h index 50dcb5e..f655735 100644 --- a/xs_version.h +++ b/xs_version.h @@ -1 +1 @@ -/* f46d5b29627b20a6e9ec4ef60c01df1d2d778520 2024-03-09T08:26:31+01:00 */ +/* f712d1336ef427c3b56305364b2687578537543f 2024-04-14T19:11:53+02:00 */ |