diff --git a/Makefile b/Makefile index c0e4170..7dbd9e7 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ PREFIX=/usr/local PREFIX_MAN=$(PREFIX)/man -CFLAGS?=-g -Wall +CFLAGS?=-g -Wall -Wextra all: snac @@ -40,7 +40,7 @@ httpd.o: httpd.c xs.h xs_io.h xs_encdec.h xs_json.h xs_socket.h \ xs_httpd.h xs_mime.h snac.h main.o: main.c xs.h xs_io.h xs_encdec.h xs_json.h snac.h mastoapi.o: mastoapi.c xs.h xs_encdec.h xs_openssl.h xs_json.h xs_io.h \ - xs_time.h snac.h + xs_time.h xs_glob.h snac.h snac.o: snac.c xs.h xs_io.h xs_encdec.h xs_json.h xs_curl.h xs_openssl.h \ xs_socket.h xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h xs_glob.h \ snac.h diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 8a153e9..1af4b73 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,15 @@ # Release Notes +## 2.30 + +Fixed a bug that made some notifications to be missed. + +New Mastodon API features: the instance public timeline is now a real one, unfavourite / unreblog is supported (somewhat). Some regression bugs regarding image posting were also fixed. + +The non-standard `Ping` and `Pong` #ActivityPub activities have been implemented as proposed by @tedu@honk.tedunangst.com in the https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt document (with a minor diversion: retries are managed in the same way as the rest of #snac messages). + +The build process now includes the `-Wextra` flag. + ## 2.29 New Mastodon API features: account search, relationships (so the Follow/Unfollow buttons now appear for each account), follow and unfollow accounts, an instance-level timeline (very kludgy), custom emojis for accounts and statuses, many bug fixes (sadly, the Mastodon official app still does not work). diff --git a/activitypub.c b/activitypub.c index c8c166d..9069d03 100644 --- a/activitypub.c +++ b/activitypub.c @@ -109,7 +109,7 @@ int actor_request(snac *snac, const char *actor, xs_dict **data) if (valid_status(status2)) { /* renew data */ - status = actor_add(snac, actor, payload); + status = actor_add(actor, payload); if (data != NULL) { *data = payload; @@ -437,7 +437,8 @@ void process_tags(snac *snac, const char *content, d_char **n_content, d_char ** /** messages **/ -d_char *msg_base(snac *snac, char *type, char *id, char *actor, char *date, char *object) +xs_dict *msg_base(snac *snac, const char *type, const char *id, + const char *actor, const char *date, const char *object) /* creates a base ActivityPub message */ { xs *did = NULL; @@ -467,7 +468,7 @@ d_char *msg_base(snac *snac, char *type, char *id, char *actor, char *date, char } } - d_char *msg = xs_dict_new(); + xs_dict *msg = xs_dict_new(); msg = xs_dict_append(msg, "@context", "https:/" "/www.w3.org/ns/activitystreams"); msg = xs_dict_append(msg, "type", type); @@ -845,6 +846,28 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts, } +xs_dict *msg_ping(snac *user, const char *rcpt) +/* creates a Ping message (https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt) */ +{ + xs_dict *msg = msg_base(user, "Ping", "@dummy", user->actor, NULL, NULL); + + msg = xs_dict_append(msg, "to", rcpt); + + return msg; +} + + +xs_dict *msg_pong(snac *user, const char *rcpt, const char *object) +/* creates a Pong message (https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt) */ +{ + xs_dict *msg = msg_base(user, "Pong", "@dummy", user->actor, NULL, object); + + msg = xs_dict_append(msg, "to", rcpt); + + return msg; +} + + void notify(snac *snac, xs_str *type, xs_str *utype, xs_str *actor, xs_dict *msg) /* notifies the user of relevant events */ { @@ -1121,7 +1144,7 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req) else if (strcmp(type, "Update") == 0) { if (strcmp(utype, "Person") == 0) { - actor_add(snac, actor, xs_dict_get(msg, "object")); + actor_add(actor, xs_dict_get(msg, "object")); snac_log(snac, xs_fmt("updated actor %s", actor)); } @@ -1147,7 +1170,19 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req) snac_debug(snac, 1, xs_fmt("ignored 'Delete' for unknown object %s", object)); } else - snac_debug(snac, 1, xs_fmt("process_message type '%s' ignored", type)); + if (strcmp(type, "Pong") == 0) { + snac_log(snac, xs_fmt("'Pong' received from %s", actor)); + } + else + if (strcmp(type, "Ping") == 0) { + snac_log(snac, xs_fmt("'Ping' requested from %s", actor)); + + xs *rsp = msg_pong(snac, actor, xs_dict_get(msg, "id")); + + enqueue_output_by_actor(snac, rsp, actor, 0); + } + else + snac_debug(snac, 1, xs_fmt("process_input_message type '%s' ignored", type)); if (do_notify) { notify(snac, type, utype, actor, msg); @@ -1384,8 +1419,9 @@ void process_queue_item(xs_dict *q_item) xs *headers = xs_dict_new(); headers = xs_dict_append(headers, "content-type", "application/json"); - xs *rsp = xs_http_request("POST", url, headers, + xs *rsp = xs_http_request("POST", url, headers, body, strlen(body), &status, NULL, NULL, 0); + rsp = xs_free(rsp); srv_debug(0, xs_fmt("telegram post %d", status)); } @@ -1426,7 +1462,7 @@ int process_queue(void) /** HTTP handlers */ -int activitypub_get_handler(d_char *req, char *q_path, +int activitypub_get_handler(const xs_dict *req, const char *q_path, char **body, int *b_size, char **ctype) { int status = 200; @@ -1519,11 +1555,13 @@ int activitypub_get_handler(d_char *req, char *q_path, } -int activitypub_post_handler(d_char *req, char *q_path, - d_char *payload, int p_size, +int activitypub_post_handler(const xs_dict *req, const char *q_path, + char *payload, int p_size, char **body, int *b_size, char **ctype) /* processes an input message */ { + (void)b_size; + int status = 202; /* accepted */ char *i_ctype = xs_dict_get(req, "content-type"); snac snac; diff --git a/data.c b/data.c index c2d29c1..72b63f8 100644 --- a/data.c +++ b/data.c @@ -328,6 +328,51 @@ int index_add(const char *fn, const char *id) } +int index_del_md5(const char *fn, const char *md5) +/* deletes an md5 from an index */ +{ + int status = 404; + FILE *f; + + pthread_mutex_lock(&data_mutex); + + if ((f = fopen(fn, "r+")) != NULL) { + char line[256]; + + while (fgets(line, sizeof(line), f) != NULL) { + line[32] = '\0'; + + if (strcmp(line, md5) == 0) { + /* found! just rewind, overwrite it with garbage + and an eventual call to index_gc() will clean it + [yes: this breaks index_len()] */ + fseek(f, -33, SEEK_CUR); + fwrite("-", 1, 1, f); + status = 200; + + break; + } + } + + fclose(f); + } + else + status = 500; + + pthread_mutex_unlock(&data_mutex); + + return status; +} + + +int index_del(const char *fn, const char *id) +/* deletes an id from an index */ +{ + xs *md5 = xs_md5_hex(id, strlen(id)); + return index_del_md5(fn, md5); +} + + int index_gc(const char *fn) /* garbage-collects an index, deleting objects that are not here */ { @@ -772,6 +817,23 @@ int object_admire(const char *id, const char *actor, int like) } +int object_unadmire(const char *id, const char *actor, int like) +/* actor no longer likes or announces this object */ +{ + int status; + xs *fn = _object_fn(id); + + fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx"); + + status = index_del(fn, actor); + + srv_debug(0, + xs_fmt("object_unadmire (%s) %s %s %d", like ? "Like" : "Announce", actor, fn, status)); + + return status; +} + + int _object_user_cache(snac *snac, const char *id, const char *cachedir, int del) /* adds or deletes from a user cache */ { @@ -969,8 +1031,13 @@ void timeline_update_indexes(snac *snac, const char *id) if (valid_status(object_get(id, &msg))) { /* if its ours and is public, also store in public */ - if (is_msg_public(snac, msg)) + if (is_msg_public(snac, msg)) { object_user_cache_add(snac, id, "public"); + + /* also add it to the instance public timeline */ + xs *ipt = xs_fmt("%s/public.idx", srv_basedir); + index_add(ipt, id); + } } } } @@ -1041,7 +1108,7 @@ xs_list *timeline_top_level(snac *snac, xs_list *list) } -d_char *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show) +xs_list *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show) /* returns a timeline (with all entries) */ { int c_max; @@ -1059,7 +1126,7 @@ d_char *timeline_simple_list(snac *snac, const char *idx_name, int skip, int sho } -d_char *timeline_list(snac *snac, const char *idx_name, int skip, int show) +xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show) /* returns a timeline (only top level entries) */ { xs *list = timeline_simple_list(snac, idx_name, skip, show); @@ -1068,6 +1135,15 @@ d_char *timeline_list(snac *snac, const char *idx_name, int skip, int show) } +xs_list *timeline_instance_list(int skip, int show) +/* returns the timeline for the full instance */ +{ + xs *idx = xs_fmt("%s/public.idx", srv_basedir); + + return index_list_desc(idx, skip, show); +} + + /** following **/ /* this needs special treatment and cannot use the object db as is, @@ -1273,30 +1349,49 @@ int is_hidden(snac *snac, const char *id) } -int actor_add(snac *snac, const char *actor, d_char *msg) +int actor_add(const char *actor, xs_dict *msg) /* adds an actor */ { return object_add_ow(actor, msg); } -int actor_get(snac *snac, const char *actor, d_char **data) +int actor_get(snac *snac1, const char *actor, xs_dict **data) /* returns an already downloaded actor */ { int status = 200; - d_char *d; + xs_dict *d = NULL; - if (strcmp(actor, snac->actor) == 0) { + if (strcmp(actor, snac1->actor) == 0) { /* this actor */ if (data) - *data = msg_actor(snac); + *data = msg_actor(snac1); return status; } + if (xs_startswith(actor, srv_baseurl)) { + /* it's a (possible) local user */ + xs *l = xs_split(actor, "/"); + const char *uid = xs_list_get(l, -1); + snac user; + + if (!xs_is_null(uid) && user_open(&user, uid)) { + if (data) + *data = msg_actor(&user); + + user_free(&user); + return 200; + } + else + return 404; + } + /* read the object */ - if (!valid_status(status = object_get(actor, &d))) + if (!valid_status(status = object_get(actor, &d))) { + d = xs_free(d); return status; + } if (data) *data = d; @@ -1719,7 +1814,7 @@ static xs_dict *_new_qmsg(const char *type, const xs_val *msg, int retries) } -void enqueue_input(snac *snac, xs_dict *msg, xs_dict *req, int retries) +void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries) /* enqueues an input message */ { xs *qmsg = _new_qmsg("input", msg, retries); @@ -2020,7 +2115,11 @@ void purge_server(void) xs *ib_dir = xs_fmt("%s/inbox", srv_basedir); _purge_dir(ib_dir, 7); - srv_debug(1, xs_fmt("purge: global (obj: %d, idx: %d)", cnt, icnt)); + /* purge the instance timeline */ + xs *itl_fn = xs_fmt("%s/public.idx", srv_basedir); + int itl_gc = index_gc(itl_fn); + + srv_debug(1, xs_fmt("purge: global (obj: %d, idx: %d, itl: %d)", cnt, icnt, itl_gc)); } diff --git a/doc/snac.1 b/doc/snac.1 index ec8df64..ef84f3d 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -220,6 +220,36 @@ Please take note that they will show your timeline in a 'Mastodon fashion' post display with the most active threads at the top that the web interface of .Nm provides. +.Ss Implementing post bots +.Nm +makes very easy to post messages in a non-interactive manner. This example +posts a string: +.Bd -literal -offset indent +uptime | snac note $SNAC_BASEDIR $SNAC_USER - +.Ed +.Pp +You can setup a line like this from a +.Xr crontab 5 +or similar. Take note that you need a) command-line access to the same machine +that hosts the +.Nm +instance, and b) write permissions to the storage directories and files. +.Pp +You can also post non-interactively using the Mastodon API and a command-line +http tool like +.Xr curl 1 +or similar. This has the advantage that you can do it remotely from any host, +anywhere; the only thing you need is an API Token. This is an example: +.Bd -literal -offset indent +curl -X POST https://$SNAC_HOST/api/v1/statuses \\ +--header "Authorization: Bearer ${TOKEN}" -d "status=$(uptime)" +.Ed +.Pp +You can obtain an API Token by connecting to the following URL: +.Bd -literal -offset indent +https://$SNAC_HOST/oauth/x-snac-get-token +.Ed +.Pp .Sh ENVIRONMENT .Bl -tag -width Ds .It Ev DEBUG diff --git a/examples/docker-entrypoint.sh b/examples/docker-entrypoint.sh index 639b692..a6216b2 100755 --- a/examples/docker-entrypoint.sh +++ b/examples/docker-entrypoint.sh @@ -1,7 +1,7 @@ #! /bin/sh if [ ! -e /data/data/server.json ] then - echo -ne "0.0.0.0\r\n8001\r\nlocalhost\r\n\r\n" | snac init /data/data + echo -ne "0.0.0.0\r\n8001\r\nlocalhost\r\n\r\n\r\n" | snac init /data/data snac adduser /data/data testuser fi SSLKEYLOGFILE=/data/key snac httpd /data/data diff --git a/html.c b/html.c index 5f57262..8d4203d 100644 --- a/html.c +++ b/html.c @@ -79,7 +79,7 @@ xs_str *actor_name(xs_dict *actor) } -d_char *html_actor_icon(snac *snac, d_char *os, char *actor, +xs_str *html_actor_icon(xs_str *os, char *actor, const char *date, const char *udate, const char *url, int priv) { xs *s = xs_str_new(NULL); @@ -168,7 +168,7 @@ d_char *html_msg_icon(snac *snac, d_char *os, char *msg) date = xs_dict_get(msg, "published"); udate = xs_dict_get(msg, "updated"); - os = html_actor_icon(snac, os, actor, date, udate, url, priv); + os = html_actor_icon(os, actor, date, udate, url, priv); } return os; @@ -983,7 +983,7 @@ d_char *html_entry(snac *snac, d_char *os, char *msg, int local, } -d_char *html_user_footer(snac *snac, d_char *s) +xs_str *html_user_footer(xs_str *s) { xs *s1 = xs_fmt( "