Merge pull request 'Implement instance announcements' (#173) from louis77/snac2:announcements into master

Reviewed-on: https://codeberg.org/grunfink/snac2/pulls/173
This commit is contained in:
grunfink 2024-05-31 09:09:54 +00:00
commit fe52b7612e
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);
}
}
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
.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

View File

@ -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% }

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_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");

View File

@ -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("<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
if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/

6
snac.h
View File

@ -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);