diff --git a/data.c b/data.c index e24bf16..b25ddf8 100644 --- a/data.c +++ b/data.c @@ -3370,3 +3370,69 @@ void srv_archive_qitem(const char *prefix, xs_dict *q_item) fclose(f); } } + + +t_announcement *announcement(const double after) +/* returns announcement text or NULL if none exists or it is olde than "after" */ +{ + static const long int MAX_SIZE = 2048; + static t_announcement a = { + .text = NULL, + .timestamp = 0.0, + }; + static xs_str *fn = NULL; + if (fn == NULL) + fn = xs_fmt("%s/announcement.txt", srv_basedir); + + const double ts = mtime(fn); + + /* file does not exist or other than what was requested */ + if (ts == 0.0 || ts <= after) + return NULL; + + /* nothing changed, just return the current announcement */ + if (a.text != NULL && ts <= a.timestamp) + return &a; + + /* read and store new announcement */ + FILE *f; + + if ((f = fopen(fn, "r")) != NULL) { + fseek (f, 0, SEEK_END); + const long int length = ftell(f); + + if (length > MAX_SIZE) { + /* this is probably unintentional */ + srv_log(xs_fmt("announcement.txt too big: %ld bytes, max is %ld, ignoring.", length, MAX_SIZE)); + } + else + if (length > 0) { + fseek (f, 0, SEEK_SET); + char *buffer = malloc(length + 1); + if (buffer) { + fread(buffer, 1, length, f); + buffer[length] = '\0'; + + free(a.text); + a.text = buffer; + a.timestamp = ts; + } + else { + srv_log("Error allocating memory for announcement"); + } + } + else { + /* an empty file means no announcement */ + free(a.text); + a.text = NULL; + a.timestamp = 0.0; + } + + fclose (f); + } + + if (a.text != NULL) + return &a; + + return NULL; +} diff --git a/doc/snac.5 b/doc/snac.5 index 42b257e..fec3af3 100644 --- a/doc/snac.5 +++ b/doc/snac.5 @@ -121,6 +121,14 @@ rejected. This brings the flexibility and destruction power of regular expressio to your Fediverse experience. To be used wisely (see .Xr snac 8 for more information). +.It Pa announcement.txt +If this file is present, an announcement will be shown to logged in users +on every page with its contents. It is also available through the Mastodon API. +Users can dismiss the announcement, which works by storing the modification time +in the "last_announcement" field of the +.Pa user.json +file. When the file is modified, the announcement will then reappear. It can +contain only text and will be ignored if it has more than 2048 bytes. .El .Pp 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; } .snac-top-user { text-align: center; padding-bottom: 2em } .snac-top-user-name { font-size: 200% } .snac-top-user-id { font-size: 150% } +.snac-announcement { border: black 1px solid; padding: 0.5em } .snac-avatar { float: left; height: 2.5em; padding: 0.25em } .snac-author { font-size: 90%; text-decoration: none } .snac-author-tag { font-size: 80% } diff --git a/html.c b/html.c index c3a2efe..2274f74 100644 --- a/html.c +++ b/html.c @@ -786,6 +786,24 @@ static xs_html *html_user_body(snac *user, int read_only) xs_html_attr("class", "snac-top-user-id"), xs_html_text(handle))); + /** instance announcement **/ + + double la = 0.0; + xs *user_la = xs_dup(xs_dict_get(user->config, "last_announcement")); + if (user_la != NULL) + la = xs_number_get(user_la); + + const t_announcement *an = announcement(la); + if (an != NULL && (an->text != NULL)) { + xs_html_add(top_user, xs_html_tag("div", + xs_html_attr("class", "snac-announcement"), + xs_html_text(an->text), + xs_html_text(" "), + xs_html_sctag("a", + xs_html_attr("href", xs_dup(xs_fmt("?da=%.0f", an->timestamp)))), + xs_html_text("Dismiss"))); + } + if (read_only) { xs *es1 = encode_html(xs_dict_get(user->config, "bio")); xs *bio1 = not_really_markdown(es1, NULL, NULL); @@ -2606,6 +2624,16 @@ int html_get_handler(const xs_dict *req, const char *q_path, skip = atoi(v), cache = 0, save = 0; if ((v = xs_dict_get(q_vars, "show")) != NULL) show = atoi(v), cache = 0, save = 0; + if ((v = xs_dict_get(q_vars, "da")) != NULL) { + /* user dismissed an announcement */ + if (login(&snac, req)) { + double ts = atof(v); + xs *timestamp = xs_number_new(ts); + srv_log(xs_fmt("user dismissed announcements until %d", ts)); + snac.config = xs_dict_set(snac.config, "last_announcement", timestamp); + user_persist(&snac); + } + } if (p_path == NULL) { /** public timeline **/ xs *h = xs_str_localtime(0, "%Y-%m.html"); diff --git a/mastoapi.c b/mastoapi.c index 058cc76..4a6c53e 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -1997,10 +1997,40 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, } else if (strcmp(cmd, "/v1/announcements") == 0) { /** **/ - /* snac has no announcements (yet?) */ - *body = xs_dup("[]"); - *ctype = "application/json"; - status = HTTP_STATUS_OK; + if (logged_in) { + xs *resp = xs_list_new(); + double la = 0.0; + xs *user_la = xs_dup(xs_dict_get(snac1.config, "last_announcement")); + if (user_la != NULL) + la = xs_number_get(user_la); + xs *val_date = xs_str_utctime(la, ISO_DATE_SPEC); + + /* contrary to html, we always send the announcement and set the read flag instead */ + + const t_announcement *annce = announcement(la); + if (annce != NULL && annce->text != NULL) { + xs *an = xs_dict_new(); + an = xs_dict_set(an, "id", xs_fmt("%d", annce->timestamp)); + an = xs_dict_set(an, "content", xs_fmt("
%s
", annce->text)); + an = xs_dict_set(an, "starts_at", xs_stock(XSTYPE_NULL)); + an = xs_dict_set(an, "ends_at", xs_stock(XSTYPE_NULL)); + an = xs_dict_set(an, "all_day", xs_stock(XSTYPE_TRUE)); + an = xs_dict_set(an, "published_at", val_date); + an = xs_dict_set(an, "updated_at", val_date); + an = xs_dict_set(an, "read", (annce->timestamp >= la) + ? xs_stock(XSTYPE_FALSE) : xs_stock(XSTYPE_TRUE)); + an = xs_dict_set(an, "mentions", xs_stock(XSTYPE_LIST)); + an = xs_dict_set(an, "statuses", xs_stock(XSTYPE_LIST)); + an = xs_dict_set(an, "tags", xs_stock(XSTYPE_LIST)); + an = xs_dict_set(an, "emojis", xs_stock(XSTYPE_LIST)); + an = xs_dict_set(an, "reactions", xs_stock(XSTYPE_LIST)); + resp = xs_list_append(resp, an); + } + + *body = xs_json_dumps(resp, 4); + *ctype = "application/json"; + status = HTTP_STATUS_OK; + } } else if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/ diff --git a/snac.h b/snac.h index 2561b6c..8193ca5 100644 --- a/snac.h +++ b/snac.h @@ -375,3 +375,9 @@ typedef enum { } http_status; const char *http_status_text(int status); + +typedef struct { + double timestamp; + char *text; +} t_announcement; +t_announcement *announcement(double after);