diff options
| -rw-r--r-- | data.c | 66 | ||||
| -rw-r--r-- | doc/snac.5 | 8 | ||||
| -rw-r--r-- | doc/style.css | 1 | ||||
| -rw-r--r-- | html.c | 28 | ||||
| -rw-r--r-- | mastoapi.c | 38 | ||||
| -rw-r--r-- | snac.h | 6 |
6 files changed, 143 insertions, 4 deletions
| @@ -3370,3 +3370,69 @@ void srv_archive_qitem(const char *prefix, xs_dict *q_item) | |||
| 3370 | fclose(f); | 3370 | fclose(f); |
| 3371 | } | 3371 | } |
| 3372 | } | 3372 | } |
| 3373 | |||
| 3374 | |||
| 3375 | t_announcement *announcement(const double after) | ||
| 3376 | /* returns announcement text or NULL if none exists or it is olde than "after" */ | ||
| 3377 | { | ||
| 3378 | static const long int MAX_SIZE = 2048; | ||
| 3379 | static t_announcement a = { | ||
| 3380 | .text = NULL, | ||
| 3381 | .timestamp = 0.0, | ||
| 3382 | }; | ||
| 3383 | static xs_str *fn = NULL; | ||
| 3384 | if (fn == NULL) | ||
| 3385 | fn = xs_fmt("%s/announcement.txt", srv_basedir); | ||
| 3386 | |||
| 3387 | const double ts = mtime(fn); | ||
| 3388 | |||
| 3389 | /* file does not exist or other than what was requested */ | ||
| 3390 | if (ts == 0.0 || ts <= after) | ||
| 3391 | return NULL; | ||
| 3392 | |||
| 3393 | /* nothing changed, just return the current announcement */ | ||
| 3394 | if (a.text != NULL && ts <= a.timestamp) | ||
| 3395 | return &a; | ||
| 3396 | |||
| 3397 | /* read and store new announcement */ | ||
| 3398 | FILE *f; | ||
| 3399 | |||
| 3400 | if ((f = fopen(fn, "r")) != NULL) { | ||
| 3401 | fseek (f, 0, SEEK_END); | ||
| 3402 | const long int length = ftell(f); | ||
| 3403 | |||
| 3404 | if (length > MAX_SIZE) { | ||
| 3405 | /* this is probably unintentional */ | ||
| 3406 | srv_log(xs_fmt("announcement.txt too big: %ld bytes, max is %ld, ignoring.", length, MAX_SIZE)); | ||
| 3407 | } | ||
| 3408 | else | ||
| 3409 | if (length > 0) { | ||
| 3410 | fseek (f, 0, SEEK_SET); | ||
| 3411 | char *buffer = malloc(length + 1); | ||
| 3412 | if (buffer) { | ||
| 3413 | fread(buffer, 1, length, f); | ||
| 3414 | buffer[length] = '\0'; | ||
| 3415 | |||
| 3416 | free(a.text); | ||
| 3417 | a.text = buffer; | ||
| 3418 | a.timestamp = ts; | ||
| 3419 | } | ||
| 3420 | else { | ||
| 3421 | srv_log("Error allocating memory for announcement"); | ||
| 3422 | } | ||
| 3423 | } | ||
| 3424 | else { | ||
| 3425 | /* an empty file means no announcement */ | ||
| 3426 | free(a.text); | ||
| 3427 | a.text = NULL; | ||
| 3428 | a.timestamp = 0.0; | ||
| 3429 | } | ||
| 3430 | |||
| 3431 | fclose (f); | ||
| 3432 | } | ||
| 3433 | |||
| 3434 | if (a.text != NULL) | ||
| 3435 | return &a; | ||
| 3436 | |||
| 3437 | return NULL; | ||
| 3438 | } | ||
| @@ -121,6 +121,14 @@ rejected. This brings the flexibility and destruction power of regular expressio | |||
| 121 | to your Fediverse experience. To be used wisely (see | 121 | to your Fediverse experience. To be used wisely (see |
| 122 | .Xr snac 8 | 122 | .Xr snac 8 |
| 123 | for more information). | 123 | for more information). |
| 124 | .It Pa announcement.txt | ||
| 125 | If this file is present, an announcement will be shown to logged in users | ||
| 126 | on every page with its contents. It is also available through the Mastodon API. | ||
| 127 | Users can dismiss the announcement, which works by storing the modification time | ||
| 128 | in the "last_announcement" field of the | ||
| 129 | .Pa user.json | ||
| 130 | file. When the file is modified, the announcement will then reappear. It can | ||
| 131 | contain only text and will be ignored if it has more than 2048 bytes. | ||
| 124 | .El | 132 | .El |
| 125 | .Pp | 133 | .Pp |
| 126 | Each user directory is a subdirectory of | 134 | Each user directory is a subdirectory of |
diff --git a/doc/style.css b/doc/style.css index a133db6..2273e03 100644 --- a/doc/style.css +++ b/doc/style.css | |||
| @@ -6,6 +6,7 @@ pre { overflow-x: scroll; } | |||
| 6 | .snac-top-user { text-align: center; padding-bottom: 2em } | 6 | .snac-top-user { text-align: center; padding-bottom: 2em } |
| 7 | .snac-top-user-name { font-size: 200% } | 7 | .snac-top-user-name { font-size: 200% } |
| 8 | .snac-top-user-id { font-size: 150% } | 8 | .snac-top-user-id { font-size: 150% } |
| 9 | .snac-announcement { border: black 1px solid; padding: 0.5em } | ||
| 9 | .snac-avatar { float: left; height: 2.5em; padding: 0.25em } | 10 | .snac-avatar { float: left; height: 2.5em; padding: 0.25em } |
| 10 | .snac-author { font-size: 90%; text-decoration: none } | 11 | .snac-author { font-size: 90%; text-decoration: none } |
| 11 | .snac-author-tag { font-size: 80% } | 12 | .snac-author-tag { font-size: 80% } |
| @@ -786,6 +786,24 @@ static xs_html *html_user_body(snac *user, int read_only) | |||
| 786 | xs_html_attr("class", "snac-top-user-id"), | 786 | xs_html_attr("class", "snac-top-user-id"), |
| 787 | xs_html_text(handle))); | 787 | xs_html_text(handle))); |
| 788 | 788 | ||
| 789 | /** instance announcement **/ | ||
| 790 | |||
| 791 | double la = 0.0; | ||
| 792 | xs *user_la = xs_dup(xs_dict_get(user->config, "last_announcement")); | ||
| 793 | if (user_la != NULL) | ||
| 794 | la = xs_number_get(user_la); | ||
| 795 | |||
| 796 | const t_announcement *an = announcement(la); | ||
| 797 | if (an != NULL && (an->text != NULL)) { | ||
| 798 | xs_html_add(top_user, xs_html_tag("div", | ||
| 799 | xs_html_attr("class", "snac-announcement"), | ||
| 800 | xs_html_text(an->text), | ||
| 801 | xs_html_text(" "), | ||
| 802 | xs_html_sctag("a", | ||
| 803 | xs_html_attr("href", xs_dup(xs_fmt("?da=%.0f", an->timestamp)))), | ||
| 804 | xs_html_text("Dismiss"))); | ||
| 805 | } | ||
| 806 | |||
| 789 | if (read_only) { | 807 | if (read_only) { |
| 790 | xs *es1 = encode_html(xs_dict_get(user->config, "bio")); | 808 | xs *es1 = encode_html(xs_dict_get(user->config, "bio")); |
| 791 | xs *bio1 = not_really_markdown(es1, NULL, NULL); | 809 | xs *bio1 = not_really_markdown(es1, NULL, NULL); |
| @@ -2590,6 +2608,16 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 2590 | skip = atoi(v), cache = 0, save = 0; | 2608 | skip = atoi(v), cache = 0, save = 0; |
| 2591 | if ((v = xs_dict_get(q_vars, "show")) != NULL) | 2609 | if ((v = xs_dict_get(q_vars, "show")) != NULL) |
| 2592 | show = atoi(v), cache = 0, save = 0; | 2610 | show = atoi(v), cache = 0, save = 0; |
| 2611 | if ((v = xs_dict_get(q_vars, "da")) != NULL) { | ||
| 2612 | /* user dismissed an announcement */ | ||
| 2613 | if (login(&snac, req)) { | ||
| 2614 | double ts = atof(v); | ||
| 2615 | xs *timestamp = xs_number_new(ts); | ||
| 2616 | srv_log(xs_fmt("user dismissed announcements until %d", ts)); | ||
| 2617 | snac.config = xs_dict_set(snac.config, "last_announcement", timestamp); | ||
| 2618 | user_persist(&snac); | ||
| 2619 | } | ||
| 2620 | } | ||
| 2593 | 2621 | ||
| 2594 | if (p_path == NULL) { /** public timeline **/ | 2622 | if (p_path == NULL) { /** public timeline **/ |
| 2595 | xs *h = xs_str_localtime(0, "%Y-%m.html"); | 2623 | xs *h = xs_str_localtime(0, "%Y-%m.html"); |
| @@ -1982,10 +1982,40 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 1982 | } | 1982 | } |
| 1983 | else | 1983 | else |
| 1984 | if (strcmp(cmd, "/v1/announcements") == 0) { /** **/ | 1984 | if (strcmp(cmd, "/v1/announcements") == 0) { /** **/ |
| 1985 | /* snac has no announcements (yet?) */ | 1985 | if (logged_in) { |
| 1986 | *body = xs_dup("[]"); | 1986 | xs *resp = xs_list_new(); |
| 1987 | *ctype = "application/json"; | 1987 | double la = 0.0; |
| 1988 | status = HTTP_STATUS_OK; | 1988 | xs *user_la = xs_dup(xs_dict_get(snac1.config, "last_announcement")); |
| 1989 | if (user_la != NULL) | ||
| 1990 | la = xs_number_get(user_la); | ||
| 1991 | xs *val_date = xs_str_utctime(la, ISO_DATE_SPEC); | ||
| 1992 | |||
| 1993 | /* contrary to html, we always send the announcement and set the read flag instead */ | ||
| 1994 | |||
| 1995 | const t_announcement *annce = announcement(la); | ||
| 1996 | if (annce != NULL && annce->text != NULL) { | ||
| 1997 | xs *an = xs_dict_new(); | ||
| 1998 | an = xs_dict_set(an, "id", xs_fmt("%d", annce->timestamp)); | ||
| 1999 | an = xs_dict_set(an, "content", xs_fmt("<p>%s</p>", annce->text)); | ||
| 2000 | an = xs_dict_set(an, "starts_at", xs_stock(XSTYPE_NULL)); | ||
| 2001 | an = xs_dict_set(an, "ends_at", xs_stock(XSTYPE_NULL)); | ||
| 2002 | an = xs_dict_set(an, "all_day", xs_stock(XSTYPE_TRUE)); | ||
| 2003 | an = xs_dict_set(an, "published_at", val_date); | ||
| 2004 | an = xs_dict_set(an, "updated_at", val_date); | ||
| 2005 | an = xs_dict_set(an, "read", (annce->timestamp >= la) | ||
| 2006 | ? xs_stock(XSTYPE_FALSE) : xs_stock(XSTYPE_TRUE)); | ||
| 2007 | an = xs_dict_set(an, "mentions", xs_stock(XSTYPE_LIST)); | ||
| 2008 | an = xs_dict_set(an, "statuses", xs_stock(XSTYPE_LIST)); | ||
| 2009 | an = xs_dict_set(an, "tags", xs_stock(XSTYPE_LIST)); | ||
| 2010 | an = xs_dict_set(an, "emojis", xs_stock(XSTYPE_LIST)); | ||
| 2011 | an = xs_dict_set(an, "reactions", xs_stock(XSTYPE_LIST)); | ||
| 2012 | resp = xs_list_append(resp, an); | ||
| 2013 | } | ||
| 2014 | |||
| 2015 | *body = xs_json_dumps(resp, 4); | ||
| 2016 | *ctype = "application/json"; | ||
| 2017 | status = HTTP_STATUS_OK; | ||
| 2018 | } | ||
| 1989 | } | 2019 | } |
| 1990 | else | 2020 | else |
| 1991 | if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/ | 2021 | if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/ |
| @@ -375,3 +375,9 @@ typedef enum { | |||
| 375 | } http_status; | 375 | } http_status; |
| 376 | 376 | ||
| 377 | const char *http_status_text(int status); | 377 | const char *http_status_text(int status); |
| 378 | |||
| 379 | typedef struct { | ||
| 380 | double timestamp; | ||
| 381 | char *text; | ||
| 382 | } t_announcement; | ||
| 383 | t_announcement *announcement(double after); | ||