Implement instance announcements

This commit is contained in:
Louis Brauer 2024-05-31 00:30:37 +02:00
parent af8f1ef273
commit c3bcb2bd3b
6 changed files with 143 additions and 4 deletions

66
data.c
View file

@ -3370,3 +3370,69 @@ void srv_archive_qitem(const char *prefix, xs_dict *q_item)
fclose(f); 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;
}

View file

@ -121,6 +121,14 @@ rejected. This brings the flexibility and destruction power of regular expressio
to your Fediverse experience. To be used wisely (see to your Fediverse experience. To be used wisely (see
.Xr snac 8 .Xr snac 8
for more information). 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 .El
.Pp .Pp
Each user directory is a subdirectory of Each user directory is a subdirectory of

View file

@ -6,6 +6,7 @@ pre { overflow-x: scroll; }
.snac-top-user { text-align: center; padding-bottom: 2em } .snac-top-user { text-align: center; padding-bottom: 2em }
.snac-top-user-name { font-size: 200% } .snac-top-user-name { font-size: 200% }
.snac-top-user-id { font-size: 150% } .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-avatar { float: left; height: 2.5em; padding: 0.25em }
.snac-author { font-size: 90%; text-decoration: none } .snac-author { font-size: 90%; text-decoration: none }
.snac-author-tag { font-size: 80% } .snac-author-tag { font-size: 80% }

28
html.c
View file

@ -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_attr("class", "snac-top-user-id"),
xs_html_text(handle))); 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) { if (read_only) {
xs *es1 = encode_html(xs_dict_get(user->config, "bio")); xs *es1 = encode_html(xs_dict_get(user->config, "bio"));
xs *bio1 = not_really_markdown(es1, NULL, NULL); xs *bio1 = not_really_markdown(es1, NULL, NULL);
@ -2590,6 +2608,16 @@ int html_get_handler(const xs_dict *req, const char *q_path,
skip = atoi(v), cache = 0, save = 0; skip = atoi(v), cache = 0, save = 0;
if ((v = xs_dict_get(q_vars, "show")) != NULL) if ((v = xs_dict_get(q_vars, "show")) != NULL)
show = atoi(v), cache = 0, save = 0; 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 **/ if (p_path == NULL) { /** public timeline **/
xs *h = xs_str_localtime(0, "%Y-%m.html"); xs *h = xs_str_localtime(0, "%Y-%m.html");

View file

@ -1982,10 +1982,40 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
} }
else else
if (strcmp(cmd, "/v1/announcements") == 0) { /** **/ if (strcmp(cmd, "/v1/announcements") == 0) { /** **/
/* snac has no announcements (yet?) */ if (logged_in) {
*body = xs_dup("[]"); xs *resp = xs_list_new();
*ctype = "application/json"; double la = 0.0;
status = HTTP_STATUS_OK; 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("<p>%s</p>", 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 else
if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/ if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/

6
snac.h
View file

@ -375,3 +375,9 @@ typedef enum {
} http_status; } http_status;
const char *http_status_text(int status); const char *http_status_text(int status);
typedef struct {
double timestamp;
char *text;
} t_announcement;
t_announcement *announcement(double after);