aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuben Beltran del Rio <git@r.bdr.sh>2026-09-02 19:50:00 +0200
committerRuben Beltran del Rio <git@r.bdr.sh>2026-09-02 19:50:00 +0200
commita676bd58a875383b072293976e521ba7af8f34e8 (patch)
tree43920e9b611bb1a4ade8f5f783b88435440792ba
parent15d4dfc538dce7bfce4b0926b77f7f200cca99ff (diff)
parent32ce0553e373cb03c690a854e1b629aa10f3ade9 (diff)
Merge branch 'master' of https://codeberg.org/grunfink/snac2
-rw-r--r--RELEASE_NOTES.md38
-rw-r--r--activitypub.c51
-rw-r--r--data.c114
-rw-r--r--doc/snac.17
-rw-r--r--doc/snac.84
-rw-r--r--html.c202
-rw-r--r--httpd.c12
-rw-r--r--main.c27
-rw-r--r--mastoapi.c167
-rw-r--r--po/cs.po6
-rw-r--r--po/it.po110
-rw-r--r--po/ru.po100
-rw-r--r--po/uk.po98
-rw-r--r--rss.c2
-rw-r--r--snac.c4
-rw-r--r--snac.h37
-rw-r--r--utils.c21
-rw-r--r--xs.h9
-rw-r--r--xs_html.h17
-rw-r--r--xs_url.h2
-rw-r--r--xs_version.h2
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;
}
diff --git a/data.c b/data.c
index 269d8d7..a79f9e7 100644
--- a/data.c
+++ b/data.c
@@ -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;
diff --git a/doc/snac.1 b/doc/snac.1
index eddab8d..5d63c3c 100644
--- a/doc/snac.1
+++ b/doc/snac.1
@@ -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.
diff --git a/doc/snac.8 b/doc/snac.8
index 5698ce1..96bb678 100644
--- a/doc/snac.8
+++ b/doc/snac.8
@@ -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
diff --git a/html.c b/html.c
index 486853d..79eed5a 100644
--- a/html.c
+++ b/html.c
@@ -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("&#128148;")));
}
- 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;
diff --git a/httpd.c b/httpd.c
index acbe895..3504f3e 100644
--- a/httpd.c
+++ b/httpd.c
@@ -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;
}
diff --git a/main.c b/main.c
index e7d5b0d..433bfc2 100644
--- a/main.c
+++ b/main.c
@@ -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
diff --git a/mastoapi.c b/mastoapi.c
index cf2f53e..f3cc838 100644
--- a/mastoapi.c
+++ b/mastoapi.c
@@ -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");
diff --git a/po/cs.po b/po/cs.po
index 0361367..70ecb7f 100644
--- a/po/cs.po
+++ b/po/cs.po
@@ -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"
diff --git a/po/it.po b/po/it.po
index 9548796..69e71ec 100644
--- a/po/it.po
+++ b/po/it.po
@@ -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"
diff --git a/po/ru.po b/po/ru.po
index 0c6445e..05f3ff6 100644
--- a/po/ru.po
+++ b/po/ru.po
@@ -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 "симпатии"
diff --git a/po/uk.po b/po/uk.po
index 622c4ba..7386fe7 100644
--- a/po/uk.po
+++ b/po/uk.po
@@ -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 "захоплення"
diff --git a/rss.c b/rss.c
index 9b4ae65..5ec85cb 100644
--- a/rss.c
+++ b/rss.c
@@ -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));
diff --git a/snac.c b/snac.c
index f461280..d2dbc7d 100644
--- a/snac.c
+++ b/snac.c
@@ -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;
diff --git a/snac.h b/snac.h
index a39bfa4..4585bd9 100644
--- a/snac.h
+++ b/snac.h
@@ -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,
diff --git a/utils.c b/utils.c
index 9475bd7..8fab2d2 100644
--- a/utils.c
+++ b/utils.c
@@ -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);
diff --git a/xs.h b/xs.h
index 8d8dd9c..0727dd2 100644
--- a/xs.h
+++ b/xs.h
@@ -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;
}
diff --git a/xs_html.h b/xs_html.h
index ab77499..b3c7224 100644
--- a/xs_html.h
+++ b/xs_html.h
@@ -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 */
{
diff --git a/xs_url.h b/xs_url.h
index e25c73b..4986181 100644
--- a/xs_url.h
+++ b/xs_url.h
@@ -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 */