diff options
| author | Ruben Beltran del Rio <git@r.bdr.sh> | 2026-09-02 19:50:00 +0200 |
|---|---|---|
| committer | Ruben Beltran del Rio <git@r.bdr.sh> | 2026-09-02 19:50:00 +0200 |
| commit | a676bd58a875383b072293976e521ba7af8f34e8 (patch) | |
| tree | 43920e9b611bb1a4ade8f5f783b88435440792ba | |
| parent | 15d4dfc538dce7bfce4b0926b77f7f200cca99ff (diff) | |
| parent | 32ce0553e373cb03c690a854e1b629aa10f3ade9 (diff) | |
Merge branch 'master' of https://codeberg.org/grunfink/snac2
| -rw-r--r-- | RELEASE_NOTES.md | 38 | ||||
| -rw-r--r-- | activitypub.c | 51 | ||||
| -rw-r--r-- | data.c | 114 | ||||
| -rw-r--r-- | doc/snac.1 | 7 | ||||
| -rw-r--r-- | doc/snac.8 | 4 | ||||
| -rw-r--r-- | html.c | 202 | ||||
| -rw-r--r-- | httpd.c | 12 | ||||
| -rw-r--r-- | main.c | 27 | ||||
| -rw-r--r-- | mastoapi.c | 167 | ||||
| -rw-r--r-- | po/cs.po | 6 | ||||
| -rw-r--r-- | po/it.po | 110 | ||||
| -rw-r--r-- | po/ru.po | 100 | ||||
| -rw-r--r-- | po/uk.po | 98 | ||||
| -rw-r--r-- | rss.c | 2 | ||||
| -rw-r--r-- | snac.c | 4 | ||||
| -rw-r--r-- | snac.h | 37 | ||||
| -rw-r--r-- | utils.c | 21 | ||||
| -rw-r--r-- | xs.h | 9 | ||||
| -rw-r--r-- | xs_html.h | 17 | ||||
| -rw-r--r-- | xs_url.h | 2 | ||||
| -rw-r--r-- | xs_version.h | 2 |
21 files changed, 689 insertions, 341 deletions
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e62be07..4b5b52c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,43 @@ # Release Notes +## 2.95 + +Fixed a bug in the notification page that made snac hang forever while trying to read abnormally big files. + +Improved support for text-only web browsers: it's now possible to configure a set of web browser user-agent strings that will receive simpler HTML in the private timeline web UI. Basically, it consists in avoiding `details` / `summary` HTML tags as much as possible. + +Added some fixes to media proxy code. + +Fixed EmojiReact code to allow any emoticon defined in `emojis.json`, not only those with colon-wrapped identifiers. + +Fixed a bug in notification filtering (paging was sometimes incorrect). + +Added a notification filter for Webmentions. + +Mastodon API: Don't return reactions count as string (contributed by mkljczk), implemented GET /v1/media (contributed by clairemont). + +Fixed bug in documentation examples (contributed by Sprite_tm). + +Updated Ukrainian and Russian translations (contributed by wincentbalin and koru). + +## 2.94 + +The author of a replied post is no longer added to the CC: line, as it does not have much sense. + +Fixed mentions for users with hyphens in their identifiers. + +Unlisted replies to unknown posts were being skipped from the timeline, so don't do that. + +If an actor marked as broken gets a post boosted (proving that is [back] functioning), it's unmarked as such. + +Don't try (and fail) to parse non-UTC dates in posts (I'll provide a real fix eventually). + +Fixed a redirection loop in the `/users` alias. + +Mastodon API: fixed a bug that made changing the sensitive content string impossible, avatars and headers can be deleted (contributed by bm90). + +Updated Czech and Italian translations (contributed by pmjv and fidiben). + ## 2.93 Added a new *admirations* entry point to the web UI, containing a timeline of liked, boosted and reacted posts. Please take note that, after installing the new version, it will start empty, and be filled with subsequent reactions (contributed by violette). diff --git a/activitypub.c b/activitypub.c index 514e764..af009ae 100644 --- a/activitypub.c +++ b/activitypub.c @@ -96,6 +96,9 @@ int activitypub_request(snac *user, const char *url, xs_dict **data) NULL, 0, &status, &payload, &p_size, 0); } + if (p_size >= 100000) + return HTTP_STATUS_BAD_REQUEST; + if (valid_status(status)) { /* ensure it's ActivityPub data */ ctype = xs_dict_get(response, "content-type"); @@ -940,7 +943,7 @@ xs_str *process_tags(snac *snac, const char *content, xs_list **tag) /* use this same server */ def_srv = xs_dup(xs_dict_get(srv_config, "host")); - split = xs_regex_split(content, "(@[A-Za-z0-9_]+(@[A-Za-z0-9\\.-]+)?|&#[0-9]+;|#(_|[^[:punct:][:space:]])+)"); + split = xs_regex_split(content, "(@[A-Za-z0-9_-]+(@[A-Za-z0-9\\.-]+)?|&#[0-9]+;|#(_|[^[:punct:][:space:]])+)"); p = split; while (xs_list_iter(&p, &v)) { @@ -1607,7 +1610,7 @@ xs_dict *msg_emoji_init(snac *snac, const char *mid, const char *eid_o) { xs_dict *n_msg = msg_admiration(snac, mid, "EmojiReact"); - xs *eid = xs_strip_chars_i(xs_dup(eid_o), ":"); + xs *eid = xs_dup(eid_o); xs *content = NULL; xs *tag = xs_list_new(); xs *dict = xs_dict_new(); @@ -1623,14 +1626,14 @@ xs_dict *msg_emoji_init(snac *snac, const char *mid, const char *eid_o) content = xs_dup(eid); else if (*eid == '%') { - content = xs_url_dec_emoji(xs_dup(eid)); + content = xs_url_dec_emoji(eid); if (content == NULL) { xs_free(n_msg); return NULL; } } else { - content = xs_fmt(":%s:", eid); + content = xs_dup(eid); const char *emo = xs_dict_get(emjs, content); if (emo == NULL) { @@ -2594,7 +2597,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) } /* this instance is alive */ - instance_failure(actor, 2); + instance_failure(actor, OP_DEL); /* question votes may not have a type */ if (xs_is_null(type)) @@ -2813,6 +2816,10 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) utype = "Follow"; } + if (strcmp(actor, key_id)) { + snac_log(snac, xs_fmt("Undo: mismatched actor '%s' and key '%s'", actor, key_id)); + } + else if (strcmp(utype, "Follow") == 0) { /** **/ if (!id) { snac_log(snac, xs_fmt("no id (msg.object.object) when " @@ -2836,10 +2843,9 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) } /* *key emojis are like w/ Emoji tag */ else - if ((isEmoji || strcmp(utype, "EmojiReact") == 0) && - (content && strcmp(content, "♥") != 0)) { + if (isEmoji || strcmp(utype, "EmojiReact") == 0) { const xs_val *mid = xs_dict_get(object, "id"); - int status = object_rm_emoji_react((char *)id, mid); + int status = object_rm_emoji_react(id, mid); /* ensure *key notifications type */ utype = "EmojiReact"; @@ -3029,7 +3035,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) const char *who = get_atto(a_msg); /* got the admired object: instance is [back] online */ - instance_failure(object, 2); + instance_failure(object, OP_DEL); if (who && !is_muted(snac, who)) { /* bring the actor */ @@ -3053,6 +3059,9 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) /* distribute the post to users following these hashtags */ followed_hashtag_distribute(a_msg); + /* actor is [back] alive */ + actor_failure(who, OP_DEL); + do_notify = 1; } else @@ -3458,7 +3467,7 @@ void process_user_queue_item(snac *user, xs_dict *q_item) const char *actor = xs_dict_get(q_item, "actor"); double mtime = object_mtime(actor); - if (actor_failure(actor, 0) == -1) { + if (actor_failure(actor, OP_CHECK) == -1) { /* actor is broken beyond repair */ snac_debug(user, 1, xs_fmt("actor_refresh skipped broken actor %s", actor)); } @@ -3475,16 +3484,16 @@ void process_user_queue_item(snac *user, xs_dict *q_item) actor_add(actor, actor_o); /* mark actor and instance as working */ - actor_failure(actor, 2); - instance_failure(actor, 2); + actor_failure(actor, OP_DEL); + instance_failure(actor, OP_DEL); } else { if (status == HTTP_STATUS_GONE || status == HTTP_STATUS_NOT_FOUND) { - actor_failure(actor, 1); + actor_failure(actor, OP_ADD); snac_log(user, xs_fmt("actor_refresh marking actor %s as broken %d", actor, status)); } else { - actor_failure(actor, 2); + actor_failure(actor, OP_DEL); object_touch(actor); } } @@ -3601,7 +3610,7 @@ void process_queue_item(xs_dict *q_item) return; } - if (instance_failure(inbox, 0)) { + if (instance_failure(inbox, OP_CHECK)) { srv_debug(1, xs_fmt("output message error: too many failures for instance %s", inbox)); return; } @@ -3618,7 +3627,7 @@ void process_queue_item(xs_dict *q_item) status = send_to_inbox_raw(keyid, seckey, inbox, msg, &payload, &p_size, timeout); /* register or clear a value for this instance */ - instance_failure(inbox, valid_status(status) ? 2 : 1); + instance_failure(inbox, valid_status(status) ? OP_DEL : OP_ADD); if (payload) { if (p_size > 1024) { @@ -3948,7 +3957,7 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path, uid = xs_list_get(l, 1); if (!user_open(&snac, uid)) { /* invalid user */ - status = grave(uid, 0) ? HTTP_STATUS_GONE : HTTP_STATUS_NOT_FOUND; + status = grave(uid, OP_CHECK) ? HTTP_STATUS_GONE : HTTP_STATUS_NOT_FOUND; srv_debug(1, xs_fmt("activitypub_get_handler bad user %s %d", uid, status)); return status; } @@ -4104,6 +4113,12 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path, xs_str_in(i_ctype, "application/ld+json") == -1) return 0; + if (p_size >= 100000) { + *body = xs_str_new("too big"); + *ctype = "text/plain"; + return HTTP_STATUS_BAD_REQUEST; + } + /* decode the message */ xs *msg = xs_json_loads(payload); const char *id = xs_dict_get(msg, "id"); @@ -4148,7 +4163,7 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path, const char *uid = xs_list_get(l, 1); if (!user_open(&snac, uid)) { /* invalid user */ - status = grave(uid, 0) ? HTTP_STATUS_GONE : HTTP_STATUS_NOT_FOUND; + status = grave(uid, OP_CHECK) ? HTTP_STATUS_GONE : HTTP_STATUS_NOT_FOUND; srv_debug(1, xs_fmt("activitypub_post_handler bad user %s %d", uid, status)); return status; } @@ -1051,6 +1051,14 @@ int object_announces_len(const char *id) } +int object_emojireacts_len(const char *id) +/* returns the number of emojireacts (without reading the index) */ +{ + xs *fn = _object_index_fn(id, "_e.idx"); + return index_len(fn); +} + + xs_list *object_children(const char *id) /* returns the list of an object's children */ { @@ -2355,7 +2363,7 @@ int actor_get_refresh(snac *user, const char *actor, xs_dict **data) /** user limiting (announce blocks) **/ -int limited(snac *user, const char *id, int cmd) +int limited(snac *user, const char *id, snac_op op) /* announce messages from a followed (0: check, 1: limit; 2: unlimit) */ { int ret = 0; @@ -2363,12 +2371,12 @@ int limited(snac *user, const char *id, int cmd) xs *md5 = xs_md5_hex(id, strlen(id)); xs *fn = xs_fmt("%s/%s", dir, md5); - switch (cmd) { - case 0: /** check **/ + switch (op) { + case OP_CHECK: /** check **/ ret = !!(mtime(fn) > 0.0); break; - case 1: /** limit **/ + case OP_ADD: /** limit **/ mkdirx(dir); if (mtime(fn) > 0.0) @@ -2385,12 +2393,15 @@ int limited(snac *user, const char *id, int cmd) } break; - case 2: /** unlimit **/ + case OP_DEL: /** unlimit **/ if (mtime(fn) > 0.0) ret = unlink(fn); else ret = -1; break; + + default: + break; } return ret; @@ -2474,13 +2485,13 @@ xs_list *tag_search(const char *tag, int skip, int show) /** lists **/ -xs_val *list_maint(snac *user, const char *list, int op) +xs_val *list_maint(snac *user, const char *list, snac_op op) /* list maintenance */ { xs_val *l = NULL; switch (op) { - case 0: /** list of lists **/ + case OP_LIST: /** list of lists **/ { FILE *f; xs *spec = xs_fmt("%s/list/" "*.id", user->basedir); @@ -2509,9 +2520,9 @@ xs_val *list_maint(snac *user, const char *list, int op) break; - case 1: /** create new list (list is the name) **/ + case OP_ADD: /** create new list (list is the name) **/ { - xs *lol = list_maint(user, NULL, 0); + xs *lol = list_maint(user, NULL, OP_LIST); int c = 0; const xs_list *v; int add = 1; @@ -2546,7 +2557,7 @@ xs_val *list_maint(snac *user, const char *list, int op) break; - case 2: /** delete list (list is the id) **/ + case OP_DEL: /** delete list (list is the id) **/ { if (xs_is_hex(list)) { xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list); @@ -2565,7 +2576,7 @@ xs_val *list_maint(snac *user, const char *list, int op) break; - case 3: /** get list name **/ + case OP_ID: /** get list name **/ if (xs_is_hex(list)) { FILE *f; xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list); @@ -2578,9 +2589,9 @@ xs_val *list_maint(snac *user, const char *list, int op) break; - case 4: /** find list id by name **/ + case OP_FIND: /** find list id by name **/ if (xs_is_string(list)) { - xs *lol = list_maint(user, NULL, 0); + xs *lol = list_maint(user, NULL, OP_LIST); const xs_list *li; xs_list_foreach(lol, li) { @@ -2590,6 +2601,9 @@ xs_val *list_maint(snac *user, const char *list, int op) } } } + + default: + break; } return l; @@ -2621,7 +2635,7 @@ xs_list *list_timeline(snac *user, const char *list, int skip, int show) } -xs_val *list_members(snac *user, const char *list, const char *actor_md5, int op) +xs_val *list_members(snac *user, const char *list, const char *actor_md5, snac_op op) /* list member management */ { xs_val *l = NULL; @@ -2635,12 +2649,12 @@ xs_val *list_members(snac *user, const char *list, const char *actor_md5, int op xs *fn = xs_fmt("%s/list/%s.lst", user->basedir, list); switch (op) { - case 0: /** list members **/ + case OP_LIST: /** list members **/ l = index_list(fn, XS_ALL); break; - case 1: /** append actor to list **/ + case OP_ADD: /** append actor to list **/ if (xs_is_string(actor_md5) && xs_is_hex(actor_md5)) { if (!index_in_md5(fn, actor_md5)) index_add_md5(fn, actor_md5); @@ -2648,7 +2662,7 @@ xs_val *list_members(snac *user, const char *list, const char *actor_md5, int op break; - case 2: /** delete actor from list **/ + case OP_DEL: /** delete actor from list **/ if (xs_is_string(actor_md5) && xs_is_hex(actor_md5)) index_del_md5(fn, actor_md5); @@ -2734,7 +2748,7 @@ static int _load_raw_file(const char *fn, xs_val **data, int *size, if (etag != NULL) *etag = xs_dup(e); - srv_debug(1, xs_fmt("_load_raw_file(): %s %d", fn, status)); + srv_debug(2, xs_fmt("_load_raw_file(): %s %d", fn, status)); } } @@ -3233,7 +3247,7 @@ xs_list *content_search(snac *user, const char *regex, } -int actor_failure(const char *actor, int op) +int actor_failure(const char *actor, snac_op op) /* actor failure maintenance */ { int ret = 0; @@ -3242,13 +3256,13 @@ int actor_failure(const char *actor, int op) xs *fn = xs_fmt("%s/failure/%s", srv_basedir, md5); switch (op) { - case 0: /** check **/ + case OP_CHECK: /** check **/ if (mtime(fn)) ret = -1; break; - case 1: /** register a failure **/ + case OP_ADD: /** register a failure **/ if (mtime(fn) == 0.0) { FILE *f; @@ -3261,18 +3275,21 @@ int actor_failure(const char *actor, int op) break; - case 2: /** clear a failure **/ + case OP_DEL: /** clear a failure **/ /* called whenever a message comes from this instance */ unlink(fn); break; + + default: + break; } return ret; } -int instance_failure(const char *url, int op) +int instance_failure(const char *url, snac_op op) /* do some checks and accounting on instance failures */ { int ret = 0; @@ -3287,7 +3304,7 @@ int instance_failure(const char *url, int op) xs *fn = xs_fmt("%s/failure/%s", srv_basedir, md5); switch (op) { - case 0: /** check **/ + case OP_CHECK: /** check **/ if ((mt = mtime(fn)) != 0.0) { /* grace time */ double seconds_failing = xs_number_get(xs_dict_get_def(srv_config, "max_failing_days", "15")) @@ -3299,7 +3316,7 @@ int instance_failure(const char *url, int op) break; - case 1: /** register a failure **/ + case OP_ADD: /** register a failure **/ if (mtime(fn) == 0.0) { FILE *f; @@ -3312,18 +3329,21 @@ int instance_failure(const char *url, int op) break; - case 2: /** clear a failure **/ + case OP_DEL: /** clear a failure **/ /* called whenever a message comes from this instance */ unlink(fn); break; + + default: + break; } return ret; } -int grave(const char *objid, int op) +int grave(const char *objid, snac_op op) /* the graveyeard of deleted objects */ { int ret = 0; @@ -3333,11 +3353,11 @@ int grave(const char *objid, int op) FILE *f; switch (op) { - case 0: /** check **/ + case OP_CHECK: /** check **/ ret = mtime(fn) > 0.0 ? 1 : 0; break; - case 1: /** add **/ + case OP_ADD: /** add **/ mkdirx(dir); if ((f = fopen(fn, "w")) != NULL) { @@ -3347,9 +3367,12 @@ int grave(const char *objid, int op) break; - case 2: /** del **/ + case OP_DEL: /** del **/ unlink(fn); break; + + default: + break; } return ret; @@ -3514,12 +3537,18 @@ xs_dict *notify_get(snac *snac, const char *id) fn = xs_strip_i(fn); fn = xs_str_cat(fn, ".json"); - FILE *f; xs_dict *out = NULL; - if ((f = fopen(fn, "r")) != NULL) { - out = xs_json_load(f); - fclose(f); + struct stat st; + + if (stat(fn, &st) != -1) { + if (st.st_size < 100000) { + FILE *f; + if ((f = fopen(fn, "r")) != NULL) { + out = xs_json_load(f); + fclose(f); + } + } } return out; @@ -3561,7 +3590,7 @@ xs_list *notify_list(snac *snac, int skip, int show) } -xs_list *notify_filter_list(snac *snac, xs_list *notifs) +xs_list *notify_filter_list(snac *snac, xs_list *notifs, int skip, int show) /* apply user-defined notification filter to IDs */ { const xs_dict *n_filter = xs_dict_get(snac->config, "notify_filter"); @@ -3578,6 +3607,7 @@ xs_list *notify_filter_list(snac *snac, xs_list *notifs) int n_folreq_on = xs_is_true(xs_dict_get_def(n_filter, "folreqs", n_def)); int n_blocks_on = xs_is_true(xs_dict_get_def(n_filter, "blocks", n_def)); int n_polls_on = xs_is_true(xs_dict_get_def(n_filter, "polls", n_def)); + int n_webmen_on = xs_is_true(xs_dict_get_def(n_filter, "webmentions", n_def)); const xs_str *v; xs_list *flt = xs_list_new(); @@ -3618,7 +3648,19 @@ xs_list *notify_filter_list(snac *snac, xs_list *notifs) continue; if (strcmp(type, "Announce") == 0 && !n_ann_on) continue; + if (strcmp(type, "Webmention") == 0 && !n_webmen_on) + continue; + + if (skip) { + skip--; + continue; + } + flt = xs_list_append(flt, v); + show--; + + if (show == 0) + break; } return flt; } @@ -3629,7 +3671,7 @@ int notify_new_num(snac *snac) { xs *t = notify_check_time(snac, 0); xs *lst_unfilt = notify_list(snac, 0, XS_ALL); - xs *lst = notify_filter_list(snac, lst_unfilt); + xs *lst = notify_filter_list(snac, lst_unfilt, 0, XS_ALL); int cnt = 0; xs_list *p = lst; @@ -207,6 +207,13 @@ write your posts in, space-separated (example: en fr pt_BR). .It Don't show posts written in these languages A space-separated string of ISO 639 Language Codes (example: en fr de). +.It Show a simpler web UI for browsers with these user agents +A list of partial strings that will be matched against the current +web browser user agent string. If it contains any of these strings, +the private timeline is shown using simpler HTML (basically, avoiding +the details / summary tags as much as possible, which are usually not +rendered correctly if at all in basic or text-based browsers). If any +of these lines is an asterisk, the simpler web UI is always used. .It Password Write the same string in these two fields to change your password. Don't write anything if you don't want to do this. @@ -869,7 +869,7 @@ ProxyPreserveHost On # Main web access point <Location /fedi> - ProxyPass http://127.0.0.1:8001/social + ProxyPass http://127.0.0.1:8001/fedi </Location> # WebFinger @@ -908,7 +908,7 @@ ProxyPreserveHost On # optional (Mastodon-like "authorize interaction" entrypoint) <Location /authorize_interaction> - ProxyPass http://127.0.0.1:8001/share + ProxyPass http://127.0.0.1:8001/authorize_interaction </Location> .Ed .Pp @@ -216,7 +216,7 @@ xs_str *html_date_label(snac *user, const char *date) /* check if a user has actually set a timezone */ if (user != NULL && xs_dict_get(user->config, "tz") != NULL && - (t = xs_parse_iso_date(date, 0)) != 0) { + xs_endswith(date, "Z") && (t = xs_parse_iso_date(date, 0)) != 0) { t += xs_tz_offset(user->tz); time_t today = time(NULL); @@ -362,7 +362,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, xs_html_raw("💔"))); } - if (actor_failure(actor_id, 0) == -1) { + if (actor_failure(actor_id, OP_CHECK) == -1) { xs_html_add(actor_icon, xs_html_text(" "), xs_html_tag("span", @@ -1478,9 +1478,18 @@ xs_html *html_checkbox(const char *form_name, const char *label, int flag) } -xs_html *html_top_controls(snac *user) +xs_html *html_top_controls(snac *user, int tb_friendly) /* generates the top controls */ { + if (tb_friendly) { + /* generate only a link to the /operations entrypoint */ + xs *url = xs_fmt("%s/operations", user->actor); + + return xs_html_tag("a", + xs_html_attr("href", url), + xs_html_text(L("Operations..."))); + } + xs *ops_action = xs_fmt("%s/admin/action", user->actor); xs_html *top_controls = xs_html_tag("div", @@ -1550,6 +1559,7 @@ xs_html *html_top_controls(snac *user) xs_html_attr("value", L("Like"))), xs_html_text(" "), xs_html_text(L("(by URL)"))), + xs_html_tag("p", NULL), xs_html_tag("form", xs_html_attr("autocomplete", "off"), xs_html_attr("method", "post"), @@ -1627,6 +1637,7 @@ xs_html *html_top_controls(snac *user) const char *webhook = xs_dict_get_def(user->config, "notify_webhook", ""); const char *post_langs = xs_dict_get_def(user->config, "post_langs", ""); const char *excluded_langs = xs_dict_get_def(user->config, "excluded_langs", ""); + const char *text_browser_uas = xs_dict_get_def(user->config, "text_browser_uas", ""); xs *metadata = NULL; const xs_dict *md = xs_dict_get(user->config, "metadata"); @@ -1874,6 +1885,16 @@ xs_html *html_top_controls(snac *user) xs_html_attr("placeholder", L("en fr es de_AT")))), xs_html_tag("p", + xs_html_text(L("Show a simpler web UI for browsers with these user agents (one per line):")), + xs_html_sctag("br", NULL), + xs_html_tag("textarea", + xs_html_attr("name", "text_browser_uas"), + xs_html_attr("cols", "40"), + xs_html_attr("rows", "4"), + xs_html_attr("placeholder", "Links\nLynx\nNetSurf"), + xs_html_text(text_browser_uas))), + + xs_html_tag("p", xs_html_text(L("New password:")), xs_html_sctag("br", NULL), xs_html_sctag("input", @@ -2058,6 +2079,9 @@ xs_str *build_mentions(snac *user, const xs_dict *msg) } } +#if 0 + /* disabled by now */ + /* also add the author of this post as a mention */ const char *atto = get_atto(msg); @@ -2078,6 +2102,7 @@ xs_str *build_mentions(snac *user, const xs_dict *msg) } } } +#endif if (*s) { xs *s1 = s; @@ -2089,7 +2114,7 @@ xs_str *build_mentions(snac *user, const xs_dict *msg) xs_html *html_entry_controls(snac *user, const char *actor, - const xs_dict *msg, const char *md5) + const xs_dict *msg, const char *md5, int tb_friendly) { const char *id = xs_dict_get(msg, "id"); const char *group = xs_dict_get(msg, "audience"); @@ -2201,6 +2226,21 @@ xs_html *html_entry_controls(snac *user, const char *actor, xs_html_add(form, html_button("hide", L("Hide"), L("Hide this post and its children"))); + if (tb_friendly) { + /* generate a link a More... text pointing to the one-post-only timeline */ + xs *url = xs_fmt("%s/admin/p/%s#%s_entry", user->actor, md5, md5); + + xs_html_add(controls, + xs_html_tag("p", NULL), + xs_html_tag("a", + xs_html_attr("href", url), + xs_html_text(L("More...")))); + + /* and DONE */ + + return controls; + } + const char *prev_src = xs_dict_get(msg, "sourceContent"); if (!xs_is_null(prev_src) && strcmp(actor, user->actor) == 0) { /** edit **/ @@ -2317,7 +2357,7 @@ static const xs_str* words_in_content(const xs_list *words, const xs_val *conten xs_html *html_entry(snac *user, xs_dict *msg, int read_only, - int level, const char *md5, int hide_children) + int level, const char *md5, int hide_children, int tb_friendly) { const char *id = xs_dict_get(msg, "id"); const char *type = xs_dict_get(msg, "type"); @@ -2880,7 +2920,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs_html_add(snac_content, xs_html_tag("blockquote", xs_html_attr("class", "snac-quoted-post"), - html_entry(user, quoted_post, 1, level + 1, md5, 1))); + html_entry(user, quoted_post, 1, level + 1, md5, 1, tb_friendly))); } else if (user) @@ -3144,7 +3184,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs_html_tag("p", xs_html_tag("a", xs_html_attr("href", o_href), - xs_html_text(href)))); + xs_html_text(o_href)))); /* do not generate an Alt... */ name = NULL; @@ -3328,7 +3368,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, if (!read_only && user) { xs_html_add(entry, - html_entry_controls(user, actor, msg, md5)); + html_entry_controls(user, actor, msg, md5, tb_friendly)); } /** children **/ @@ -3390,7 +3430,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, so that it appears unindented just before the parent like a fucking Twitter-like thread */ xs_html_add(fch_container, - html_entry(user, f_chd, read_only, level + 1, cmd5, hide_children)); + html_entry(user, f_chd, read_only, level + 1, cmd5, hide_children, tb_friendly)); cnt++; f_cnt++; @@ -3411,7 +3451,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, 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); + level + 1, cmd5, hide_children, tb_friendly); if (che != NULL) { if (left > 3) { @@ -3478,7 +3518,7 @@ xs_html *html_footer(const snac *user) xs_str *html_timeline(snac *user, const xs_list *list, int read_only, int skip, int show, int show_more, const char *title, const char *page, - int utl, const char *error, int terse) + int utl, const char *error, int terse, int tb_friendly) /* returns the HTML for the timeline */ { const char *v; @@ -3522,7 +3562,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, if (user && !read_only) xs_html_add(body, - html_top_controls(user)); + html_top_controls(user, tb_friendly)); if (error != NULL) { xs_html_add(body, @@ -3543,12 +3583,11 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, xs_html_attr("class", "snac-list-of-lists")); xs_html_add(body, lol); - xs *lists = list_maint(user, NULL, 0); /* get list of lists */ + xs *lists = list_maint(user, NULL, OP_LIST); /* get list of lists */ - int ct = 0; const char *v; - while (xs_list_next(lists, &v, &ct)) { + xs_list_foreach(lists, v) { const char *lname = xs_list_get(v, 1); xs *url = xs_fmt("%s/list/%s", user->actor, xs_list_get(v, 0)); xs *ttl = xs_fmt(L("Timeline for list '%s'"), lname); @@ -3672,6 +3711,11 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, int show_unlisted = user ? xs_is_true(xs_dict_get(user->config, "show_unlisted")) : 0; + /* disable text-browser friendliness if it's just one post, + to force the details/summary controls to appear */ + if (xs_list_len(list) == 1) + tb_friendly = 0; + xs_list_foreach(list, v) { xs *msg = NULL; int status; @@ -3709,7 +3753,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, continue; const int scope = get_msg_visibility(msg); - if (user != NULL && scope != SCOPE_PUBLIC && !is_msg_mine(user, xs_dict_get(msg, "id"))) { + if (user != NULL && scope != SCOPE_PUBLIC && scope != SCOPE_UNLISTED && !is_msg_mine(user, xs_dict_get(msg, "id"))) { /* is this message a non-public reply? */ const char *irt = get_in_reply_to(msg); @@ -3738,7 +3782,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, continue; } - xs_html *entry = html_entry(user, msg, read_only, 0, v, (user && !hide_children) ? 0 : 1); + xs_html *entry = html_entry(user, msg, read_only, 0, v, (user && !hide_children) ? 0 : 1, tb_friendly); if (entry != NULL) xs_html_add(posts, @@ -3908,7 +3952,10 @@ xs_html *html_people_list(snac *user, xs_list *list, const char *header, const c const char *lo = xs_is_string(longitude) ? longitude : xs_number_str(longitude); if (xs_is_string(la) && xs_is_string(lo)) { - xs *label = xs_fmt("%s,%s", la, lo); + xs *sla = xs_utf8_crop_i(xs_dup(la), 0, 7); + xs *slo = xs_utf8_crop_i(xs_dup(lo), 0, 7); + + xs *label = xs_fmt("%s,%s", sla, slo); xs *url = xs_fmt("https://openstreetmap.org/search?query=%s,%s", la, lo); xs_html_add(snac_post, @@ -4187,7 +4234,7 @@ xs_str *html_people_one(snac *user, const char *actor, const xs_list *list, xs_list_in((reacts = object_get_emoji_reacts(id)), actor_md5) == -1))) continue; - xs_html *entry = html_entry(user, msg, 0, 0, v, 1); + xs_html *entry = html_entry(user, msg, 0, 0, v, 1, 0); if (entry != NULL) xs_html_add(lists, @@ -4252,6 +4299,7 @@ void notify_filter(snac *user, const xs_dict *p_vars) int folreq_on = (v = xs_dict_get(p_vars, "folreqs_on")) ? strcmp(v, "on") == 0 : 0; int blocks_on = (v = xs_dict_get(p_vars, "blocks_on")) ? strcmp(v, "on") == 0 : 0; int polls_on = (v = xs_dict_get(p_vars, "polls_on")) ? strcmp(v, "on") == 0 : 0; + int webmen_on = (v = xs_dict_get(p_vars, "webmentions_on")) ? strcmp(v, "on") == 0 : 0; xs *filter = xs_dict_new(); filter = xs_dict_set(filter, "likes", xs_stock(likes_on ? XSTYPE_TRUE : XSTYPE_FALSE)); filter = xs_dict_set(filter, "reacts", xs_stock(reacts_on ? XSTYPE_TRUE : XSTYPE_FALSE)); @@ -4262,6 +4310,7 @@ void notify_filter(snac *user, const xs_dict *p_vars) filter = xs_dict_set(filter, "folreqs", xs_stock(folreq_on ? XSTYPE_TRUE : XSTYPE_FALSE)); filter = xs_dict_set(filter, "blocks", xs_stock(blocks_on ? XSTYPE_TRUE : XSTYPE_FALSE)); filter = xs_dict_set(filter, "polls", xs_stock(polls_on ? XSTYPE_TRUE : XSTYPE_FALSE)); + filter = xs_dict_set(filter, "webmentions", xs_stock(webmen_on ? XSTYPE_TRUE : XSTYPE_FALSE)); user->config = xs_dict_set(user->config, "notify_filter", filter); user->tz = xs_dict_get_def(user->config, "tz", "UTC"); // previous line invalidates user->tz } @@ -4273,7 +4322,7 @@ xs_str *html_notifications(snac *user, int skip, int show) if (xs_is_true(xs_dict_get(srv_config, "proxy_media"))) proxy = user->actor; - xs *n_list_unfilt = notify_list(user, skip, show); + xs *n_list_unfilt = notify_list(user, 0, XS_ALL); xs *n_time = notify_check_time(user, 0); xs_html *body = html_user_body(user, 0); @@ -4284,7 +4333,7 @@ xs_str *html_notifications(snac *user, int skip, int show) user->tz = xs_dict_get_def(user->config, "tz", "UTC"); // previous line invalidates user->tz n_filter = xs_dict_get(user->config, "notify_filter"); } - xs *n_list = notify_filter_list(user, n_list_unfilt); + xs *n_list = notify_filter_list(user, n_list_unfilt, skip, show); /* all filters are true by default */ const xs_val *n_def = xs_stock( XSTYPE_TRUE ); int n_likes_on = xs_is_true(xs_dict_get_def(n_filter, "likes", n_def)); @@ -4296,6 +4345,7 @@ xs_str *html_notifications(snac *user, int skip, int show) int n_folreq_on = xs_is_true(xs_dict_get_def(n_filter, "folreqs", n_def)); int n_blocks_on = xs_is_true(xs_dict_get_def(n_filter, "blocks", n_def)); int n_polls_on = xs_is_true(xs_dict_get_def(n_filter, "polls", n_def)); + int n_webmen_on = xs_is_true(xs_dict_get_def(n_filter, "webmentions", n_def)); xs_html *html = xs_html_tag("html", html_user_head(user, NULL, NULL), @@ -4322,6 +4372,7 @@ xs_str *html_notifications(snac *user, int skip, int show) html_checkbox("folreqs_on", L("Follow requests"), n_folreq_on), html_checkbox("blocks_on", L("Blocks"), n_blocks_on), html_checkbox("polls_on", L("Polls"), n_polls_on), + html_checkbox("webmentions_on", L("Webmentions"), n_webmen_on), xs_html_sctag("input", xs_html_attr("type", "submit"), xs_html_attr("class", "button"), @@ -4346,7 +4397,7 @@ xs_str *html_notifications(snac *user, int skip, int show) xs_html_attr("id", "clear"), xs_html_sctag("input", xs_html_attr("type", "submit"), - xs_html_attr("class", "snac-btn-like"), + xs_html_attr("class", "snac-btn-clear-all"), xs_html_attr("value", L("Clear all"))))); xs_html *noti_new = NULL; @@ -4555,7 +4606,7 @@ xs_str *html_notifications(snac *user, int skip, int show) xs *md5 = xs_md5_hex(id, strlen(id)); xs *ctxt = xs_fmt("%s/admin/p/%s#%s_entry", user->actor, md5, md5); - xs_html *h = html_entry(user, obj, 0, 0, md5, 1); + xs_html *h = html_entry(user, obj, 0, 0, md5, 1, 0); if (h != NULL) { xs_html_add(entry, @@ -4613,7 +4664,8 @@ xs_str *html_notifications(snac *user, int skip, int show) xs_html_text(L("None")))); /* add the navigation footer */ - xs *next_p = notify_list(user, skip + show, 1); + xs *next_p = notify_filter_list(user, n_list_unfilt, skip + show, 1); + if (xs_list_len(next_p)) { xs *url = xs_fmt("%s/notifications?skip=%d&show=%d", user->actor, skip + show, show); @@ -4651,6 +4703,40 @@ void set_user_lang(snac *user) } +int text_browser_friendly(const snac *user, const char *user_agent) +/* returns true if the web UI must be adapted to text browsers */ +/* basically, this means to avoid details/summary as much as possible */ +{ + if (user != NULL && xs_is_string(user_agent)) { + snac_debug(user, 2, xs_fmt("user-agent: %s", user_agent)); + + const char *text_browser_uas = xs_dict_get(user->config, "text_browser_uas"); + + if (xs_is_string(text_browser_uas)) { + xs *l = xs_split(text_browser_uas, "\n"); + const char *v; + + xs_list_foreach(l, v) { + if (*v == '\0') + continue; + + xs *v2 = xs_strip_i(xs_dup(v)); + + /* an asterisk alone means "all browsers", so yes */ + if (strcmp(v2, "*") == 0) + return 1; + + /* if this string is inside user_agent, then yes */ + if (xs_str_in(user_agent, v2) != -1) + return 1; + } + } + } + + return 0; +} + + int html_get_handler(const xs_dict *req, const char *q_path, char **body, int *b_size, char **ctype, xs_str **etag, xs_str **last_modified) @@ -4665,6 +4751,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, int save = 1; int proxy = 0; int terse = 0; + int tb_friendly = 0; const char *v; const xs_dict *q_vars = xs_dict_get(req, "q_vars"); @@ -4723,7 +4810,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, if (!uid || !user_open(&snac, uid)) { /* invalid user */ - status = grave(uid, 0) ? HTTP_STATUS_GONE : HTTP_STATUS_NOT_FOUND; + status = grave(uid, OP_CHECK) ? HTTP_STATUS_GONE : HTTP_STATUS_NOT_FOUND; srv_debug(2, xs_fmt("html_get_handler bad user '%s' %d", xs_or(uid, "(null)"), status)); return status; } @@ -4734,6 +4821,8 @@ int html_get_handler(const xs_dict *req, const char *q_path, if (xs_is_true(xs_dict_get(srv_config, "proxy_media"))) proxy = 1; + tb_friendly = text_browser_friendly(user, xs_dict_get(req, "user-agent")); + /* return the RSS if requested by Accept header */ if (accept != NULL) { if (xs_str_in(accept, "text/xml") != -1 || @@ -4785,7 +4874,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE) { /** empty public timeline for private users **/ - *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1, error, terse); + *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1, error, terse, tb_friendly); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4808,7 +4897,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *pins = pinned_list(&snac); pins = xs_list_cat(pins, list); - *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error, terse); + *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error, terse, tb_friendly); *b_size = strlen(*body); status = HTTP_STATUS_OK; @@ -4818,6 +4907,28 @@ int html_get_handler(const xs_dict *req, const char *q_path, } } else + if (strcmp(p_path, "operations") == 0) { /** operations and user settings **/ + if (!login(&snac, req)) { + *body = xs_dup(uid); + status = HTTP_STATUS_UNAUTHORIZED; + } + else { + xs_html *h_body = html_user_body(&snac, 0); + + xs_html *html = xs_html_tag("html", + html_user_head(&snac, NULL, NULL), + h_body); + + xs_html_add(h_body, + html_top_controls(&snac, 0), + html_footer(&snac)); + + *body = xs_html_render_s(html, "<!DOCTYPE html>\n"); + *b_size = strlen(*body); + status = HTTP_STATUS_OK; + } + } + else if (strcmp(p_path, "admin") == 0) { /** private timeline **/ if (!login(&snac, req)) { *body = xs_dup(uid); @@ -4936,7 +5047,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *title = xs_fmt(xs_list_len(tl) ? L("Search results for tag %s") : L("Nothing found for tag %s"), q); - *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0, error, terse); + *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0, error, terse, tb_friendly); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4961,7 +5072,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, title = xs_fmt(L("Nothing found for '%s'"), q); *body = html_timeline(&snac, tl, 0, skip, tl_len, to || tl_len == show, - title, page, 0, error, terse); + title, page, 0, error, terse, tb_friendly); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4988,7 +5099,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list = timeline_list(&snac, "private", skip, show, &more); *body = html_timeline(&snac, list, 0, skip, show, - more, NULL, "/admin", 1, error, terse); + more, NULL, "/admin", 1, error, terse, tb_friendly); *b_size = strlen(*body); status = HTTP_STATUS_OK; @@ -5015,7 +5126,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list0 = xs_list_append(xs_list_new(), md5); xs *list = timeline_top_level(&snac, list0); - *body = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error, terse); + *body = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error, terse, tb_friendly); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -5087,7 +5198,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *next = timeline_instance_list(skip + show, 1); *body = html_timeline(&snac, list, 0, skip, show, - xs_list_len(next), L("Showing instance timeline"), "/instance", 0, error, terse); + xs_list_len(next), L("Showing instance timeline"), "/instance", 0, error, terse, tb_friendly); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -5125,7 +5236,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list = pinned_list(&snac); *body = html_timeline(&snac, list, 0, skip, show, - 0, L("Pinned posts"), "", 0, error, terse); + 0, L("Pinned posts"), "", 0, error, terse, tb_friendly); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -5141,7 +5252,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list = timeline_list(&snac, "admire", skip, show, &more); *body = html_timeline(&snac, list, 0, skip, show, - more, L("Liked, boosted or reacted posts"), "/admirations", 0, error, terse); + more, L("Liked, boosted or reacted posts"), "/admirations", 0, error, terse, tb_friendly); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -5156,7 +5267,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list = bookmark_list(&snac); *body = html_timeline(&snac, list, 0, skip, show, - 0, L("Bookmarked posts"), "", 0, error, terse); + 0, L("Bookmarked posts"), "", 0, error, terse, tb_friendly); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -5171,7 +5282,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list = draft_list(&snac); *body = html_timeline(&snac, list, 0, skip, show, - 0, L("Post drafts"), "", 0, error, terse); + 0, L("Post drafts"), "", 0, error, terse, tb_friendly); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -5186,7 +5297,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list = scheduled_list(&snac); *body = html_timeline(&snac, list, 0, skip, show, - 0, L("Scheduled posts"), "", 0, error, terse); + 0, L("Scheduled posts"), "", 0, error, terse, tb_friendly); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -5208,11 +5319,11 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *ttl = timeline_top_level(&snac, list); xs *base = xs_fmt("/list/%s", lid); - xs *name = list_maint(&snac, lid, 3); + xs *name = list_maint(&snac, lid, OP_ID); xs *title = xs_fmt(L("Showing timeline for list '%s'"), name); *body = html_timeline(&snac, ttl, 0, skip, show, - xs_list_len(next), title, base, 1, error, terse); + xs_list_len(next), title, base, 1, error, terse, tb_friendly); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -5232,7 +5343,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, list = xs_list_append(list, md5); - *body = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1, error, terse); + *body = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1, error, terse, tb_friendly); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -5325,6 +5436,10 @@ int html_get_handler(const xs_dict *req, const char *q_path, /* pick the raw path (including optional ? arguments) */ const char *raw_path = xs_dict_get(req, "raw_path"); + /* skip the prefix and user part */ + xs *s = xs_fmt("%s/%s/", xs_dict_get(srv_config, "prefix"), snac.uid); + raw_path += strlen(s); + /* skip to where the proxy/ string starts */ raw_path += xs_str_in(raw_path, proxy_prefix); @@ -5362,6 +5477,9 @@ int html_get_handler(const xs_dict *req, const char *q_path, } snac_debug(&snac, 1, xs_fmt("Proxy for %s %d", url, status)); + + if (status >= 400 && status <= 499) + status = HTTP_STATUS_NOT_FOUND; } } else @@ -5749,8 +5867,6 @@ int html_post_handler(const xs_dict *req, const char *q_path, if (strcmp(action, L("EmojiReact")) == 0) { /** **/ xs *eid = xs_dup(xs_dict_get(p_vars, "eid")); - eid = xs_strip_chars_i(eid, ":"); - xs *ret = msg_emoji_init(&snac, id, eid); /* fails if either invalid or already reacted */ if (!ret) { @@ -6050,6 +6166,8 @@ int html_post_handler(const xs_dict *req, const char *q_path, if ((v = xs_dict_get(p_vars, "metadata")) != NULL) snac.config = xs_dict_set(snac.config, "metadata", v); + snac.config = xs_dict_set(snac.config, "text_browser_uas", xs_dict_get_def(p_vars, "text_browser_uas", "")); + /* uploads */ const char *uploads[] = { "avatar", "header", NULL }; int n; @@ -258,7 +258,7 @@ int server_get_handler(xs_dict *req, const char *q_path, else { xs *page = xs_fmt("?t=%s", t); xs *title = xs_fmt(L("Search results for tag #%s"), t); - *body = html_timeline(NULL, tl, 0, skip, show, more, title, page, 0, NULL, 0); + *body = html_timeline(NULL, tl, 0, skip, show, more, title, page, 0, NULL, 0, 0); } } else @@ -266,7 +266,7 @@ int server_get_handler(xs_dict *req, const char *q_path, /** instance timeline **/ xs *tl = timeline_instance_list(0, 30); *body = html_timeline(NULL, tl, 0, 0, 0, 0, - L("Recent posts by users in this instance"), NULL, 0, NULL, 0); + L("Recent posts by users in this instance"), NULL, 0, NULL, 0, 0); } else *body = greeting_html(); @@ -285,8 +285,8 @@ int server_get_handler(xs_dict *req, const char *q_path, status = HTTP_STATUS_OK; *ctype = "application/json; charset=utf-8"; *body = xs_fmt("{\"links\":[" - "{\"rel\":\"http:/" "/nodeinfo.diaspora.software/ns/schema/2.0\",\"href\":\"%s/nodeinfo_2_0\"}," - "{\"rel\":\"http:/" "/nodeinfo.diaspora.software/ns/schema/2.1\",\"href\":\"%s/nodeinfo_2_1\"}" + "{\"rel\":\"http:/" "/nodeinfo.diaspora.software/ns/schema/2.1\",\"href\":\"%s/nodeinfo_2_1\"}," + "{\"rel\":\"http:/" "/nodeinfo.diaspora.software/ns/schema/2.0\",\"href\":\"%s/nodeinfo_2_0\"}" "]}", srv_baseurl, srv_baseurl); } @@ -504,9 +504,9 @@ void httpd_connection(FILE *f) q_path = xs_crop_i(q_path, strlen(p), 0); /* add users endpoint redirection mimic Mastodon behaviour */ - const char *users_endpoint = "/users"; + const char *users_endpoint = "/users/"; if (xs_startswith(q_path, users_endpoint)) { - q_path = xs_crop_i(q_path, strlen(users_endpoint), 0); + q_path = xs_crop_i(q_path, strlen(users_endpoint) - 1, 0); status = HTTP_STATUS_FOUND; } @@ -332,7 +332,7 @@ int main(int argc, char *argv[]) } if (strcmp(cmd, "lists") == 0) { /** **/ - xs *lol = list_maint(&snac, NULL, 0); + xs *lol = list_maint(&snac, NULL, OP_LIST); const xs_list *l; xs_list_foreach(lol, l) { @@ -362,9 +362,10 @@ int main(int argc, char *argv[]) const xs_list *i; xs_list_foreach(l, i) { - printf("%s %ld★ %ld↺\n", xs_list_get(i, 0), + printf("%s %ld★ %ld↺ %ld☺\n", xs_list_get(i, 0), xs_number_get_l(xs_list_get(i, 1)), - xs_number_get_l(xs_list_get(i, 2))); + xs_number_get_l(xs_list_get(i, 2)), + xs_number_get_l(xs_list_get(i, 3))); } return 0; @@ -388,10 +389,10 @@ int main(int argc, char *argv[]) return usage(cmd); if (strcmp(cmd, "list_members") == 0) { /** **/ - xs *lid = list_maint(&snac, url, 4); + xs *lid = list_maint(&snac, url, OP_FIND); if (lid != NULL) { - xs *lcont = list_members(&snac, lid, NULL, 0); + xs *lcont = list_members(&snac, lid, NULL, OP_LIST); const char *md5; xs_list_foreach(lcont, md5) { @@ -409,10 +410,10 @@ int main(int argc, char *argv[]) } if (strcmp(cmd, "list_create") == 0) { /** **/ - xs *lid = list_maint(&snac, url, 4); + xs *lid = list_maint(&snac, url, OP_FIND); if (lid == NULL) { - xs *n_lid = list_maint(&snac, url, 1); + xs *n_lid = list_maint(&snac, url, OP_ADD); printf("New list named '%s' created (%s)\n", url, n_lid); } else @@ -422,10 +423,10 @@ int main(int argc, char *argv[]) } if (strcmp(cmd, "list_remove") == 0) { /** **/ - xs *lid = list_maint(&snac, url, 4); + xs *lid = list_maint(&snac, url, OP_FIND); if (lid != NULL) { - list_maint(&snac, lid, 2); + list_maint(&snac, lid, OP_DEL); printf("List '%s' (%s) deleted\n", url, lid); } else @@ -438,7 +439,7 @@ int main(int argc, char *argv[]) const char *account = GET_ARGV(); if (account != NULL) { - xs *lid = list_maint(&snac, url, 4); + xs *lid = list_maint(&snac, url, OP_FIND); if (lid != NULL) { xs *actor = NULL; @@ -447,7 +448,7 @@ int main(int argc, char *argv[]) if (valid_status(webfinger_request(account, &actor, &uid))) { xs *md5 = xs_md5_hex(actor, strlen(actor)); - list_members(&snac, lid, md5, 1); + list_members(&snac, lid, md5, OP_ADD); printf("Actor %s (%s) added to list '%s' (%s)\n", actor, uid, url, lid); } else @@ -465,12 +466,12 @@ int main(int argc, char *argv[]) const char *account = GET_ARGV(); if (account != NULL) { - xs *lid = list_maint(&snac, url, 4); + xs *lid = list_maint(&snac, url, OP_FIND); if (lid != NULL) { xs *md5 = xs_md5_hex(account, strlen(account)); - list_members(&snac, lid, md5, 2); + list_members(&snac, lid, md5, OP_DEL); printf("Actor %s deleted from list '%s' (%s)\n", account, url, lid); } else @@ -1186,13 +1186,13 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) int count = 1; if (contentl) { - count = atoi(xs_list_get(contentl, 0)) + 1; + count = xs_number_get(xs_list_get(contentl, 0)) + 1; if (strncmp(xs_list_get(contentl, 1), xs_stock(XSTYPE_TRUE), 1) == 0) me = xs_stock(XSTYPE_TRUE); } xs *fl = xs_list_new(); - xs *c1 = xs_fmt("%d", count); + xs *c1 = xs_number_new(count); fl = xs_list_append(fl, c1, me); sfrl = xs_dict_append(sfrl, content, fl); } @@ -1781,7 +1781,7 @@ xs_list *mastoapi_account_lists(snac *user, const char *uid) { xs_list *out = xs_list_new(); xs *actor_md5 = NULL; - xs *lol = list_maint(user, NULL, 0); + xs *lol = list_maint(user, NULL, OP_LIST); if (uid) { if (!xs_is_hex(uid)) @@ -1795,7 +1795,7 @@ xs_list *mastoapi_account_lists(snac *user, const char *uid) const char *list_id = xs_list_get(li, 0); const char *list_title = xs_list_get(li, 1); if (uid) { - xs *users = list_members(user, list_id, NULL, 0); + xs *users = list_members(user, list_id, NULL, OP_LIST); if (xs_list_in(users, actor_md5) == -1) continue; } @@ -2307,10 +2307,13 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, } else if (strcmp(cmd, "/v1/conversations") == 0) { /** **/ - /* TBD */ - *body = xs_dup("[]"); - *ctype = "application/json"; - status = HTTP_STATUS_OK; + if (logged_in) { + *body = xs_dup("[]"); + *ctype = "application/json"; + status = HTTP_STATUS_OK; + } + else + status = HTTP_STATUS_UNAUTHORIZED; } else if (strcmp(cmd, "/v1/notifications") == 0) { /** **/ @@ -2471,25 +2474,37 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, else if (strcmp(cmd, "/v1/filters") == 0) { /** **/ /* snac will never have filters */ - *body = xs_dup("[]"); - *ctype = "application/json"; - status = HTTP_STATUS_OK; + if (logged_in) { + *body = xs_dup("[]"); + *ctype = "application/json"; + status = HTTP_STATUS_OK; + } + else + status = HTTP_STATUS_UNAUTHORIZED; } 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 */ - *body = xs_dup("[]"); - *ctype = "application/json"; - status = HTTP_STATUS_OK; + if (logged_in) { + *body = xs_dup("[]"); + *ctype = "application/json"; + status = HTTP_STATUS_OK; + } + else + status = HTTP_STATUS_UNAUTHORIZED; } else if (strcmp(cmd, "/v1/favourites") == 0) { /** **/ /* snac will never support a list of favourites */ - *body = xs_dup("[]"); - *ctype = "application/json"; - status = HTTP_STATUS_OK; + if (logged_in) { + *body = xs_dup("[]"); + *ctype = "application/json"; + status = HTTP_STATUS_OK; + } + else + status = HTTP_STATUS_UNAUTHORIZED; } else if (strcmp(cmd, "/v1/bookmarks") == 0) { /** **/ @@ -2527,7 +2542,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, p = xs_list_get(l, -2); if (p && xs_is_hex(p)) { - xs *actors = list_members(&snac1, p, NULL, 0); + xs *actors = list_members(&snac1, p, NULL, OP_LIST); xs *out = xs_list_new(); int c = 0; const char *v; @@ -2549,7 +2564,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, else if (xs_is_hex(p)) { xs *out = xs_list_new(); - xs *lol = list_maint(&snac1, NULL, 0); + xs *lol = list_maint(&snac1, NULL, OP_LIST); int c = 0; const xs_list *v; @@ -2580,10 +2595,13 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, } else if (strcmp(cmd, "/v1/scheduled_statuses") == 0) { /** **/ - /* TBD */ - *body = xs_dup("[]"); - *ctype = "application/json"; - status = HTTP_STATUS_OK; + if (logged_in) { + *body = xs_dup("[]"); + *ctype = "application/json"; + status = HTTP_STATUS_OK; + } + else + status = HTTP_STATUS_UNAUTHORIZED; } else if (strcmp(cmd, "/v1/follow_requests") == 0) { /** **/ @@ -2951,9 +2969,13 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, else if (strcmp(cmd, "/v1/preferences") == 0) { /** **/ /* TBD */ - *body = xs_dup("{}"); - *ctype = "application/json"; - status = HTTP_STATUS_OK; + if (logged_in) { + *body = xs_dup("{}"); + *ctype = "application/json"; + status = HTTP_STATUS_OK; + } + else + status = HTTP_STATUS_UNAUTHORIZED; } else if (strcmp(cmd, "/v1/markers") == 0) { /** **/ @@ -3004,15 +3026,23 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, } else if (strcmp(cmd, "/v1/blocks") == 0) { /** **/ - *body = xs_dup("[]"); - *ctype = "application/json"; - status = HTTP_STATUS_OK; + if (logged_in) { + *body = xs_dup("[]"); + *ctype = "application/json"; + status = HTTP_STATUS_OK; + } + else + status = HTTP_STATUS_UNAUTHORIZED; } else if (strcmp(cmd, "/v1/mutes") == 0) { /** **/ - *body = xs_dup("[]"); - *ctype = "application/json"; - status = HTTP_STATUS_OK; + if (logged_in) { + *body = xs_dup("[]"); + *ctype = "application/json"; + status = HTTP_STATUS_OK; + } + else + status = HTTP_STATUS_UNAUTHORIZED; } else if (strcmp(cmd, "/v1/trends/tags") == 0) { /** **/ @@ -3140,6 +3170,43 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, else status = HTTP_STATUS_UNAUTHORIZED; } + else + if (xs_startswith(cmd, "/v1/media/") || xs_startswith(cmd, "/v2/media/")) { /** **/ + if (logged_in) { + xs *p = xs_split(cmd, "/"); + const char *id = xs_list_get(p, -1); + + if (xs_is_string(id)) { + xs *filename = xs_fmt("%s/static/%s", snac1.basedir, id); + + if (mtime(filename)) { + /* file exists */ + xs *alt = static_get_meta(&snac1, id); + xs *url = xs_fmt("%s/s/%s", snac1.actor, id); + xs *mime = xs_split(xs_mime_by_ext(filename), "/"); + const char *type = xs_list_get(mime, 0); + + if (!xs_match(type, "image|video|audio")) + type = "unknown"; + + xs *d = xs_dict_new(); + + d = xs_dict_set(d, "id", id); + d = xs_dict_set(d, "url", url); + d = xs_dict_set(d, "preview_url", url); + d = xs_dict_set(d, "remote_url", url); + d = xs_dict_set(d, "type", type); + d = xs_dict_set(d, "description", xs_or(alt, "")); + + *body = xs_json_dumps(d, 4); + *ctype = "application/json"; + status = HTTP_STATUS_OK; + } + } + } + else + status = HTTP_STATUS_UNAUTHORIZED; + } /* user cleanup */ if (logged_in) @@ -3803,7 +3870,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, if (xs_type(title) == XSTYPE_STRING) { /* add the list */ xs *out = xs_dict_new(); - xs *lid = list_maint(&snac, title, 1); + xs *lid = list_maint(&snac, title, OP_ADD); if (!xs_is_null(lid)) { out = xs_dict_append(out, "id", lid); @@ -3843,7 +3910,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, const char *v; while (xs_list_next(accts, &v, &c)) { - list_members(&snac, id, v, 1); + list_members(&snac, id, v, OP_ADD); } xs *out = xs_dict_new(); @@ -4052,13 +4119,13 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path, const char *v; while (xs_list_next(accts, &v, &c)) { - list_members(&snac, p, v, 2); + list_members(&snac, p, v, OP_DEL); } } else { /* delete list */ if (xs_is_hex(p)) { - list_maint(&snac, p, 2); + list_maint(&snac, p, OP_DEL); } } } @@ -4104,6 +4171,24 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path, else status = HTTP_STATUS_UNAUTHORIZED; } + else + if (xs_startswith(cmd, "/v1/profile/")) { + if (logged_in) { + xs *l = xs_split(cmd, "/"); + const char *p = xs_list_get(l, -1); + + if (strcmp(p, "avatar") == 0 || strcmp(p, "header") == 0) { + snac.config = xs_dict_set(snac.config, p, ""); + + if (user_persist(&snac, 1) == 0) + credentials_get(body, ctype, &status, snac); + else + status = HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + } + else + status = HTTP_STATUS_UNAUTHORIZED; + } /* user cleanup */ if (logged_in) @@ -4233,6 +4318,16 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path, msg = xs_dict_set(msg, "tag", tag); + if (xs_is_true(xs_dict_get(args, "sensitive"))) { + const char *spoiler_text = xs_dict_get(args, "spoiler_text"); + msg = xs_dict_set(msg, "summary", spoiler_text); + msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE)); + } + else { + msg = xs_dict_set(msg, "summary", ""); + msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_FALSE)); + } + /* overwrite object, not updating the indexes */ const char *id = xs_dict_get(msg, "id"); @@ -941,12 +941,12 @@ msgstr "Zobrazit neveřejné příspěvky na profilu a v RSS" #: html.c:1855 msgid "Don't show posts written in these languages:" -msgstr "" +msgstr "Nezobrazovat příspěvky v těchto jazycích:" #: html.c:3572 html.c:5108 msgid "Liked, boosted or reacted posts" -msgstr "" +msgstr "Oblíbené a boostované příspěvky" #: html.c:3573 msgid "admirations" -msgstr "" +msgstr "interakce" @@ -162,7 +162,7 @@ msgstr "(per URL o user@host)" #: html.c:1520 html.c:2139 html.c:5737 msgid "Boost" -msgstr "Annuncia" +msgstr "Rilancia" #: html.c:1522 html.c:1539 html.c:1561 msgid "(by URL)" @@ -238,7 +238,7 @@ msgstr "Questo account è un bot" #: html.c:1797 msgid "Auto-boost all mentions to this account" -msgstr "Annuncio automatico delle citazioni a quest'account" +msgstr "Rilancio automatico delle citazioni a quest'account" #: html.c:1799 msgid "This account is private (posts are not shown through the web)" @@ -326,7 +326,7 @@ msgstr "Annuncia questo post ai tuoi seguenti" #: html.c:2144 html.c:5755 msgid "Unboost" -msgstr "Rimuovi annuncio" +msgstr "Ritira il boost" #: html.c:2144 msgid "I regret I boosted this" @@ -370,7 +370,7 @@ msgstr "Smetti di seguire il gruppo o canale" #: html.c:2174 html.c:5820 msgid "Follow Group" -msgstr "Segui grupp" +msgstr "Segui il gruppo" #: html.c:2175 msgid "Start following this group or channel" @@ -438,7 +438,7 @@ msgstr "Evento" #: html.c:2510 html.c:2539 msgid "boosted" -msgstr "Annunciato" +msgstr "rilanciato" #: html.c:2553 msgid "in reply to" @@ -539,7 +539,7 @@ msgstr "Senza limite" #: html.c:4033 msgid "Allow announces (boosts) from this user" -msgstr "Permetti annunci dall'utente" +msgstr "Permetti annunci (boost) dall'utente" #: html.c:4036 html.c:5782 msgid "Limit" @@ -547,7 +547,7 @@ msgstr "Limite" #: html.c:4037 msgid "Block announces (boosts) from this user" -msgstr "Blocca annunci dall'utente" +msgstr "Blocca annunci (boost) dall'utente" #: html.c:4046 msgid "Delete this user" @@ -737,23 +737,23 @@ msgstr "bozze" #: html.c:733 msgid "Scheduled post..." -msgstr "" +msgstr "Post programmati..." msgid "Post date and time:" -msgstr "" +msgstr "Data e ora del post:" #: html.c:3608 html.c:5153 msgid "Scheduled posts" -msgstr "" +msgstr "Post programmati" #: html.c:3609 msgid "scheduled posts" -msgstr "" +msgstr "post programmati" #: html.c:727 #, c-format msgid "Post date and time (timezone: %s):" -msgstr "" +msgstr "Data e ora del post (timezone: %s):" #: html.c:1841 msgid "Time zone:" @@ -761,7 +761,7 @@ msgstr "" #: html.c:708 msgid "Language:" -msgstr "" +msgstr "Lingua:" #: html.c:1779 msgid "Notify webhook:" @@ -773,7 +773,7 @@ msgstr "" #: html.c:1846 msgid "Languages you usually post in:" -msgstr "" +msgstr "Lingua nella quale posti di solito" #: html.c:1852 html.c:1861 msgid "en fr es de_AT" @@ -781,19 +781,19 @@ msgstr "" #: html.c:521 msgid "Visibility: " -msgstr "" +msgstr "Visibilità: " msgid "Public" -msgstr "" +msgstr "Pubblico" msgid "Unlisted" -msgstr "" +msgstr "Non elencato" msgid "Followers-only" -msgstr "" +msgstr "Solo follower" msgid "Direct Message" -msgstr "" +msgstr "Messaggio diretto" #: html.c:581 html.c:2728 html.c:2753 html.c:5701 msgid "EmojiUnreact" @@ -809,144 +809,144 @@ msgstr "" #: html.c:2803 msgid "Emoji reactions: " -msgstr "" +msgstr "Reazioni Emoji: " #: html.c:1957 msgid "Muted words..." -msgstr "" +msgstr "Parole silenziate..." #: html.c:1959 msgid "One word per line, partial matches count" -msgstr "" +msgstr "Una parola per linea, vale la corrispondenza parziale" #: html.c:1980 msgid "Update muted words" -msgstr "" +msgstr "Aggiorna le parole silenziate" #: html.c:2628 msgid "Muted: " -msgstr "" +msgstr "Silenziati: " #: html.c:4146 msgid "Contact's posts" -msgstr "" +msgstr "Post dei contatti" #: html.c:4215 msgid "More (x 10)..." -msgstr "" +msgstr "Ancora (x 10)..." #: html.c:873 msgid "End in 3 days" -msgstr "" +msgstr "Termina in 3 giorni" #: html.c:876 msgid "End in 1 year" -msgstr "" +msgstr "Termina in 1 anno" #: html.c:4303 msgid "Likes" -msgstr "" +msgstr "Like" #: html.c:4304 msgid "Emoji reacts" -msgstr "" +msgstr "Reazioni Emoji" #: html.c:4305 msgid "Mentions" -msgstr "" +msgstr "Citazioni" #: html.c:4306 msgid "Announces" -msgstr "" +msgstr "Boost" #: html.c:4307 msgid "Follows" -msgstr "" +msgstr "Ti segue" #: html.c:4308 msgid "Unfollows" -msgstr "" +msgstr "Ha smesso di seguirti" #: html.c:4309 msgid "Follow requests" -msgstr "" +msgstr "Ha chiesto di seguirti" #: html.c:4310 msgid "Blocks" -msgstr "" +msgstr "Ti ha bloccato" #: html.c:4311 msgid "Polls" -msgstr "" +msgstr "Sondaggi" #: html.c:4315 msgid "Save" -msgstr "" +msgstr "Salva" #: html.c:4323 msgid "Notifications filter..." -msgstr "" +msgstr "Filtro delle notifiche..." #: data.c:45 msgid "Jan" -msgstr "" +msgstr "Gen" #: data.c:46 msgid "Feb" -msgstr "" +msgstr "Feb" #: data.c:47 msgid "Mar" -msgstr "" +msgstr "Mar" #: data.c:48 msgid "Apr" -msgstr "" +msgstr "Apr" #: data.c:49 msgid "May" -msgstr "" +msgstr "Mag" #: data.c:50 msgid "Jun" -msgstr "" +msgstr "Giu" #: data.c:51 msgid "Jul" -msgstr "" +msgstr "Lug" #: data.c:52 msgid "Aug" -msgstr "" +msgstr "Ago" #: data.c:53 msgid "Sep" -msgstr "" +msgstr "Set" #: data.c:54 msgid "Oct" -msgstr "" +msgstr "Ott" #: data.c:55 msgid "Nov" -msgstr "" +msgstr "Mov" #: data.c:56 msgid "Dec" -msgstr "" +msgstr "Dic" #: html.c:1808 msgid "Show unlisted posts in the public page and RSS feed" -msgstr "" +msgstr "Mostra i post non elencati nella pagina pubblica e nei feed RSS" #: html.c:1855 msgid "Don't show posts written in these languages:" -msgstr "" +msgstr "Non mostrare i post scritti in queste lingue:" #: html.c:3572 html.c:5108 msgid "Liked, boosted or reacted posts" -msgstr "" +msgstr "Post piaciuti, rilanciati o con reazioni" #: html.c:3573 msgid "admirations" -msgstr "" +msgstr "apprezzati" @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: snac\n" -"Last-Translator: grunfink\n" +"Last-Translator: koru\n" "Language: ru\n" "Content-Type: text/plain; charset=UTF-8\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " @@ -768,192 +768,192 @@ msgstr "Часовой пояс:" #: html.c:708 msgid "Language:" -msgstr "" +msgstr "Язык:" #: html.c:1779 msgid "Notify webhook:" -msgstr "" +msgstr "Вебхук для уведомлений:" #: html.c:1785 msgid "http://example.com/webhook" -msgstr "" +msgstr "http://example.com/webhook" #: html.c:1846 msgid "Languages you usually post in:" -msgstr "" +msgstr "Языки, на которых вы обычно пишете:" #: html.c:1852 html.c:1861 msgid "en fr es de_AT" -msgstr "" +msgstr "en fr es de_AT" #: html.c:521 msgid "Visibility: " -msgstr "" +msgstr "Видимость: " msgid "Public" -msgstr "" +msgstr "Публичное" msgid "Unlisted" -msgstr "" +msgstr "Скрытое" msgid "Followers-only" -msgstr "" +msgstr "Только для подписчиков" msgid "Direct Message" -msgstr "" +msgstr "Личное сообщение" #: html.c:581 html.c:2728 html.c:2753 html.c:5701 msgid "EmojiUnreact" -msgstr "" +msgstr "Снять эмодзи-реакцию" #: html.c:581 html.c:1559 html.c:2728 html.c:2753 html.c:5713 msgid "EmojiReact" -msgstr "" +msgstr "Эмодзи-реакция" #: html.c:2251 msgid "Emoji react..." -msgstr "" +msgstr "Эмодзи-реакция..." #: html.c:2803 msgid "Emoji reactions: " -msgstr "" +msgstr "Эмодзи-реакции: " #: html.c:1957 msgid "Muted words..." -msgstr "" +msgstr "Скрытые слова..." #: html.c:1959 msgid "One word per line, partial matches count" -msgstr "" +msgstr "По одному слову на строку, учитываются частичные совпадения" #: html.c:1980 msgid "Update muted words" -msgstr "" +msgstr "Обновить скрытые слова" #: html.c:2628 msgid "Muted: " -msgstr "" +msgstr "Скрыто: " #: html.c:4146 msgid "Contact's posts" -msgstr "" +msgstr "Сообщения контакта" #: html.c:4215 msgid "More (x 10)..." -msgstr "" +msgstr "Ещё (x 10)..." #: html.c:873 msgid "End in 3 days" -msgstr "" +msgstr "Заканчивается через 3 дня" #: html.c:876 msgid "End in 1 year" -msgstr "" +msgstr "Заканчивается через 1 год" #: html.c:4303 msgid "Likes" -msgstr "" +msgstr "Лайки" #: html.c:4304 msgid "Emoji reacts" -msgstr "" +msgstr "Эмодзи-реакции" #: html.c:4305 msgid "Mentions" -msgstr "" +msgstr "Упоминания" #: html.c:4306 msgid "Announces" -msgstr "" +msgstr "Продвижения" #: html.c:4307 msgid "Follows" -msgstr "" +msgstr "Подписки" #: html.c:4308 msgid "Unfollows" -msgstr "" +msgstr "Отписки" #: html.c:4309 msgid "Follow requests" -msgstr "" +msgstr "Запросы на подписку" #: html.c:4310 msgid "Blocks" -msgstr "" +msgstr "Блокировки" #: html.c:4311 msgid "Polls" -msgstr "" +msgstr "Опросы" #: html.c:4315 msgid "Save" -msgstr "" +msgstr "Сохранить" #: html.c:4323 msgid "Notifications filter..." -msgstr "" +msgstr "Фильтр уведомлений..." #: data.c:45 msgid "Jan" -msgstr "" +msgstr "Янв" #: data.c:46 msgid "Feb" -msgstr "" +msgstr "Фев" #: data.c:47 msgid "Mar" -msgstr "" +msgstr "Мар" #: data.c:48 msgid "Apr" -msgstr "" +msgstr "Апр" #: data.c:49 msgid "May" -msgstr "" +msgstr "Май" #: data.c:50 msgid "Jun" -msgstr "" +msgstr "Июн" #: data.c:51 msgid "Jul" -msgstr "" +msgstr "Июл" #: data.c:52 msgid "Aug" -msgstr "" +msgstr "Авг" #: data.c:53 msgid "Sep" -msgstr "" +msgstr "Сен" #: data.c:54 msgid "Oct" -msgstr "" +msgstr "Окт" #: data.c:55 msgid "Nov" -msgstr "" +msgstr "Ноя" #: data.c:56 msgid "Dec" -msgstr "" +msgstr "Дек" #: html.c:1808 msgid "Show unlisted posts in the public page and RSS feed" -msgstr "" +msgstr "Показывать скрытые сообщения на публичной странице и в RSS-ленте" #: html.c:1855 msgid "Don't show posts written in these languages:" -msgstr "" +msgstr "Не показывать сообщения на этих языках:" #: html.c:3572 html.c:5108 msgid "Liked, boosted or reacted posts" -msgstr "" +msgstr "Понравившиеся, продвинутые сообщения или сообщения с реакцией" #: html.c:3573 msgid "admirations" -msgstr "" +msgstr "симпатии" @@ -768,192 +768,192 @@ msgstr "Часовий пояс:" #: html.c:708 msgid "Language:" -msgstr "" +msgstr "Мова:" #: html.c:1779 msgid "Notify webhook:" -msgstr "" +msgstr "Вебхук для сповіщення:" #: html.c:1785 msgid "http://example.com/webhook" -msgstr "" +msgstr "http://example.com/webhook" #: html.c:1846 msgid "Languages you usually post in:" -msgstr "" +msgstr "Типові мови для дописів:" #: html.c:1852 html.c:1861 msgid "en fr es de_AT" -msgstr "" +msgstr "en fr es de_AT" #: html.c:521 msgid "Visibility: " -msgstr "" +msgstr "Видимість: " msgid "Public" -msgstr "" +msgstr "Публічний допис" msgid "Unlisted" -msgstr "" +msgstr "Прихований допис" msgid "Followers-only" -msgstr "" +msgstr "Тільки для підписників" msgid "Direct Message" -msgstr "" +msgstr "Приватний допис" #: html.c:581 html.c:2728 html.c:2753 html.c:5701 msgid "EmojiUnreact" -msgstr "" +msgstr "Прибрати реакцію" #: html.c:581 html.c:1559 html.c:2728 html.c:2753 html.c:5713 msgid "EmojiReact" -msgstr "" +msgstr "Відреагувати" #: html.c:2251 msgid "Emoji react..." -msgstr "" +msgstr "Реакції..." #: html.c:2803 msgid "Emoji reactions: " -msgstr "" +msgstr "Реакції: " #: html.c:1957 msgid "Muted words..." -msgstr "" +msgstr "Приглушені слова..." #: html.c:1959 msgid "One word per line, partial matches count" -msgstr "" +msgstr "Одне слово в кожному рядку, враховуються часткові збіги" #: html.c:1980 msgid "Update muted words" -msgstr "" +msgstr "Оновити приглушені слова" #: html.c:2628 msgid "Muted: " -msgstr "" +msgstr "Приглушено: " #: html.c:4146 msgid "Contact's posts" -msgstr "" +msgstr "Дописи контакту" #: html.c:4215 msgid "More (x 10)..." -msgstr "" +msgstr "Більше (x 10)..." #: html.c:873 msgid "End in 3 days" -msgstr "" +msgstr "Закінчується через 3 дні" #: html.c:876 msgid "End in 1 year" -msgstr "" +msgstr "Закінчується через 1 рік" #: html.c:4303 msgid "Likes" -msgstr "" +msgstr "Вподобайки" #: html.c:4304 msgid "Emoji reacts" -msgstr "" +msgstr "Реакції" #: html.c:4305 msgid "Mentions" -msgstr "" +msgstr "Згадки" #: html.c:4306 msgid "Announces" -msgstr "" +msgstr "Оголошення" #: html.c:4307 msgid "Follows" -msgstr "" +msgstr "Підписки" #: html.c:4308 msgid "Unfollows" -msgstr "" +msgstr "Відписки" #: html.c:4309 msgid "Follow requests" -msgstr "" +msgstr "Запити на підписку" #: html.c:4310 msgid "Blocks" -msgstr "" +msgstr "Блокування" #: html.c:4311 msgid "Polls" -msgstr "" +msgstr "Опитування" #: html.c:4315 msgid "Save" -msgstr "" +msgstr "Зберегти" #: html.c:4323 msgid "Notifications filter..." -msgstr "" +msgstr "Фільтр сповіщень..." #: data.c:45 msgid "Jan" -msgstr "" +msgstr "січ" #: data.c:46 msgid "Feb" -msgstr "" +msgstr "лют" #: data.c:47 msgid "Mar" -msgstr "" +msgstr "бер" #: data.c:48 msgid "Apr" -msgstr "" +msgstr "кві" #: data.c:49 msgid "May" -msgstr "" +msgstr "тра" #: data.c:50 msgid "Jun" -msgstr "" +msgstr "чер" #: data.c:51 msgid "Jul" -msgstr "" +msgstr "лип" #: data.c:52 msgid "Aug" -msgstr "" +msgstr "сер" #: data.c:53 msgid "Sep" -msgstr "" +msgstr "вер" #: data.c:54 msgid "Oct" -msgstr "" +msgstr "жов" #: data.c:55 msgid "Nov" -msgstr "" +msgstr "лис" #: data.c:56 msgid "Dec" -msgstr "" +msgstr "гру" #: html.c:1808 msgid "Show unlisted posts in the public page and RSS feed" -msgstr "" +msgstr "Показати приховані дописи на публічній сторінці та в RSS-каналі" #: html.c:1855 msgid "Don't show posts written in these languages:" -msgstr "" +msgstr "Не показувати дописи, написані цими мовами:" #: html.c:3572 html.c:5108 msgid "Liked, boosted or reacted posts" -msgstr "" +msgstr "Дописи з вподобайками, поширеннями та реакціями" #: html.c:3573 msgid "admirations" -msgstr "" +msgstr "захоплення" @@ -282,7 +282,7 @@ void rss_to_timeline(snac *user, const char *url) continue; /* the instance is [back] online */ - instance_failure(id, 2); + instance_failure(id, OP_DEL); if (timeline_here(user, id)) { snac_debug(user, 1, xs_fmt("RSS entry already in timeline (id) %s", id)); @@ -93,7 +93,7 @@ int validate_uid(const char *uid) return 0; while (*uid) { - if (!(isalnum(*uid) || *uid == '_')) + if (!(isalnum((int)*uid) || *uid == '_')) return 0; uid++; @@ -127,7 +127,7 @@ void srv_log(xs_str *str) } -void snac_log(snac *snac, xs_str *str) +void snac_log(const snac *snac, xs_str *str) /* prints a user debugging information */ { xs *o_str = str; @@ -1,7 +1,7 @@ /* snac - A simple, minimalistic ActivityPub instance */ /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ -#define VERSION "2.93" +#define VERSION "2.95" #define USER_AGENT "snac/" VERSION @@ -79,6 +79,15 @@ typedef struct { enum { THST_STOP, THST_WAIT, THST_IN, THST_QUEUE } th_state[MAX_THREADS]; } srv_state; +typedef enum { + OP_CHECK = 0, + OP_ADD = 1, + OP_DEL = 2, + OP_LIST = 3, + OP_FIND = 4, + OP_ID = 5 +} snac_op; + extern srv_state *p_state; enum { @@ -88,7 +97,7 @@ enum { SCOPE_FOLLOWERS = 3, }; -void snac_log(snac *user, xs_str *str); +void snac_log(const snac *user, xs_str *str); #define snac_debug(user, level, str) do { if (dbglevel >= (level)) \ { snac_log((user), (str)); } } while (0) @@ -157,6 +166,7 @@ int object_emoji_react(const char *mid, const char *eid); int object_rm_emoji_react(const char *mid, const char *eid); int object_likes_len(const char *id); int object_announces_len(const char *id); +int object_emojireacts_len(const char *id); xs_list *object_children(const char *id); xs_list *object_likes(const char *id); @@ -247,10 +257,10 @@ void schedule_add(snac *user, const char *id, const xs_dict *msg); xs_list *scheduled_list(snac *user); void scheduled_process(snac *user); -int limited(snac *user, const char *id, int cmd); -#define is_limited(user, id) limited((user), (id), 0) -#define limit(user, id) limited((user), (id), 1) -#define unlimit(user, id) limited((user), (id), 2) +int limited(snac *user, const char *id, snac_op); +#define is_limited(user, id) limited((user), (id), OP_CHECK) +#define limit(user, id) limited((user), (id), OP_ADD) +#define unlimit(user, id) limited((user), (id), OP_DEL) void hide(snac *snac, const char *id); int is_hidden(snac *snac, const char *id); @@ -260,10 +270,10 @@ void tag_index(const char *id, const xs_dict *obj); xs_str *tag_fn(const char *tag); xs_list *tag_search(const char *tag, int skip, int show); -xs_val *list_maint(snac *user, const char *list, int op); +xs_val *list_maint(snac *user, const char *list, snac_op op); xs_str *list_timeline_fn(snac *user, const char *list); xs_list *list_timeline(snac *user, const char *list, int skip, int show); -xs_val *list_members(snac *user, const char *list_id, const char *actor_md5, int op); +xs_val *list_members(snac *user, const char *list_id, const char *actor_md5, snac_op op); void list_distribute(snac *user, const char *who, const xs_dict *post); int actor_add(const char *actor, const xs_dict *msg); @@ -291,7 +301,7 @@ void notify_add(snac *snac, const char *type, const char *utype, xs_dict *notify_get(snac *snac, const char *id); int notify_new_num(snac *snac); xs_list *notify_list(snac *snac, int skip, int show); -xs_list *notify_filter_list(snac *snac, xs_list *ids); +xs_list *notify_filter_list(snac *snac, xs_list *ids, int skip, int show); void notify_clear(snac *snac); xs_dict *markers_get(snac *snac, const xs_list *markers); @@ -309,10 +319,10 @@ int content_match(const char *file, const xs_dict *msg); xs_list *content_search(snac *user, const char *regex, int priv, int skip, int show, int max_secs, int *timeout); -int actor_failure(const char *actor, int op); -int instance_failure(const char *url, int op); +int actor_failure(const char *actor, snac_op op); +int instance_failure(const char *url, snac_op op); -int grave(const char *objid, int op); +int grave(const char *objid, snac_op op); 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); @@ -438,7 +448,8 @@ xs_str *encode_html(const char *str); xs_str *html_timeline(snac *user, const xs_list *list, int read_only, int skip, int show, int show_more, - const char *title, const char *page, int utl, const char *error, int terse); + const char *title, const char *page, + int utl, const char *error, int terse, int tb_friendly); int html_get_handler(const xs_dict *req, const char *q_path, char **body, int *b_size, char **ctype, @@ -372,7 +372,7 @@ int adduser(const char *uid) } /* remove the grave */ - grave(uid, 2); + grave(uid, OP_DEL); printf("\nUser password is %s\n", pwd); @@ -467,7 +467,7 @@ int deluser(snac *user) } } - grave(user->uid, 1); + grave(user->uid, OP_ADD); rm_rf(user->basedir); @@ -698,14 +698,14 @@ void export_csv(snac *user) if ((f = fopen(fn, "w")) != NULL) { snac_log(user, xs_fmt("Creating %s...", fn)); - xs *lol = list_maint(user, NULL, 0); + xs *lol = list_maint(user, NULL, OP_LIST); const xs_list *li; xs_list_foreach(lol, li) { const char *lid = xs_list_get(li, 0); const char *ltitle = xs_list_get(li, 1); - xs *actors = list_members(user, lid, NULL, 0); + xs *actors = list_members(user, lid, NULL, OP_LIST); const char *md5; xs_list_foreach(actors, md5) { @@ -744,7 +744,7 @@ void export_csv(snac *user) webfinger_request_fake(actor, NULL, &uid); if (xs_is_string(uid)) - fprintf(f, "%s,%s,false,\n", uid, limited(user, actor, 0) ? "false" : "true"); + fprintf(f, "%s,%s,false,\n", uid, limited(user, actor, OP_CHECK) ? "false" : "true"); } fclose(f); @@ -927,7 +927,7 @@ void import_list_csv(snac *user, const char *ifn) if (lname && acct) { /* create the list */ - xs *list_id = list_maint(user, lname, 1); + xs *list_id = list_maint(user, lname, OP_ADD); xs *url = NULL; xs *uid = NULL; @@ -935,7 +935,7 @@ void import_list_csv(snac *user, const char *ifn) if (valid_status(webfinger_request(acct, &url, &uid))) { xs *actor_md5 = xs_md5_hex(url, strlen(url)); - list_members(user, list_id, actor_md5, 1); + list_members(user, list_id, actor_md5, OP_ADD); snac_log(user, xs_fmt("Added %s to list %s", url, lname)); if (!following_check(user, url)) { @@ -1024,8 +1024,8 @@ static int top_ten_sort(const void *v1, const void *v2) const xs_list *l1 = *(const xs_list **)v1; const xs_list *l2 = *(const xs_list **)v2; - const char *c1 = xs_list_get(l1, 3); - const char *c2 = xs_list_get(l2, 3); + const char *c1 = xs_list_get(l1, 4); + const char *c2 = xs_list_get(l2, 4); return xs_cmp(c2, c1); } @@ -1060,9 +1060,10 @@ xs_list *user_top_ten(snac *user, int count) /* get metrics */ int ls = object_likes_len(id); int as = object_announces_len(id); + int es = object_emojireacts_len(id); /* build the entry and convert to list */ - xs *s = xs_fmt("%s\t%d\t%d\t%010d", id, ls, as, ls + as); + xs *s = xs_fmt("%s\t%d\t%d\t%d\t%010d", id, ls, as, es, ls + as + es); xs *l = xs_split(s, "\t"); u_list = xs_list_append(u_list, l); @@ -135,6 +135,7 @@ xs_dict *xs_dict_set_path_sep(xs_dict *dict, const char *path, const xs_val *val #define xs_dict_set_path(dict, path, value) xs_dict_set_path_sep(dict, path, value, ".") xs_val *xs_val_new(xstype t); +xs_number *_xs_number_new(const char *str); xs_number *xs_number_new(double f); xs_number *xs_number_new_l(long l); double xs_number_get(const xs_number *v); @@ -164,6 +165,7 @@ uint64_t xs_hash64_func(const char *data, int size); #define xs_is_true(v) (xs_type((v)) == XSTYPE_TRUE) #define xs_is_false(v) (xs_type((v)) == XSTYPE_FALSE) +#define xs_bool(v) xs_stock((v) ? XSTYPE_TRUE : XSTYPE_FALSE) #define xs_not(v) xs_stock(xs_is_true((v)) ? XSTYPE_FALSE : XSTYPE_TRUE) #define xs_is_string(v) (xs_type((v)) == XSTYPE_STRING) #define xs_is_list(v) (xs_type((v)) == XSTYPE_LIST) @@ -706,7 +708,7 @@ xs_str *xs_tolower_i(xs_str *str) int n; for (n = 0; str[n]; n++) - str[n] = tolower(str[n]); + str[n] = tolower((int)str[n]); return str; } @@ -720,7 +722,7 @@ xs_str *xs_toupper_i(xs_str *str) int n; for (n = 0; str[n]; n++) - str[n] = toupper(str[n]); + str[n] = toupper((int)str[n]); return str; } @@ -1559,7 +1561,8 @@ xs_data *xs_data_new(const void *data, int size) _xs_put_size(v, total_size); - memcpy(&v[1 + _XS_TYPE_SIZE], data, size); + if (data) + memcpy(&v[1 + _XS_TYPE_SIZE], data, size); return v; } @@ -24,6 +24,9 @@ xs_html *_xs_html_sctag(const char *tag, xs_html *var[]); xs_html *_xs_html_container(xs_html *var[]); #define xs_html_container(...) _xs_html_container((xs_html *[]) { __VA_ARGS__, NULL }) +xs_html *_xs_html_xtag(const char *tag, xs_html *var[]); +#define xs_html_xtag(tag, ...) _xs_html_xtag(tag, (xs_html *[]) { __VA_ARGS__, NULL }) + void xs_html_render_f(xs_html *h, FILE *f); xs_str *xs_html_render_s(xs_html *tag, const char *prefix); #define xs_html_render(tag) xs_html_render_s(tag, NULL) @@ -187,6 +190,20 @@ xs_html *_xs_html_container(xs_html *var[]) } +xs_html *_xs_html_xtag(const char *tag, xs_html *var[]) +{ + if (xs_is_null(tag)) + return _xs_html_container(var); + else + if (xs_endswith(tag, "/")) { + xs *tag2 = xs_crop_i(xs_dup(tag), 0, -1); + return _xs_html_sctag(tag2, var); + } + + return _xs_html_tag(tag, var); +} + + void xs_html_render_f(xs_html *h, FILE *f) /* renders the tag and its subtags into a file */ { @@ -118,7 +118,7 @@ xs_str *xs_url_enc(const char *str) xs_str *s = xs_str_new(NULL); while (*str) { - if (isalnum(*str) || strchr("-._~", *str)) { + if (isalnum((int)*str) || strchr("-._~", *str)) { s = xs_append_m(s, str, 1); } else { diff --git a/xs_version.h b/xs_version.h index 500b156..afc995f 100644 --- a/xs_version.h +++ b/xs_version.h @@ -1 +1 @@ -/* 4110cac45d3d8a6c86b6552c59ec9c73d7a3a4c0 2026-06-17T11:46:41+02:00 */ +/* 243a85157ca228412b681c8c4a57aab164d50b93 2026-08-08T07:08:26+02:00 */ |