summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgrunfink <grunfink@noreply.codeberg.org>2024-05-31 09:09:54 +0000
committergrunfink <grunfink@noreply.codeberg.org>2024-05-31 09:09:54 +0000
commitfe52b7612e4a96d491f925dfcbbbaa6251654ca4 (patch)
tree40fb16a0f78b0988dd8167c101554d8425d2a875
parenta5e331c05e79a6cd0904b23ab9c71724cd628d16 (diff)
parentac3b5dcbd472b533ec543331692921b804fd02e4 (diff)
Merge pull request 'Implement instance announcements' (#173) from louis77/snac2:announcements into master
Reviewed-on: https://codeberg.org/grunfink/snac2/pulls/173
-rw-r--r--data.c66
-rw-r--r--doc/snac.58
-rw-r--r--doc/style.css1
-rw-r--r--html.c28
-rw-r--r--mastoapi.c38
-rw-r--r--snac.h6
6 files changed, 143 insertions, 4 deletions
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 9860872..2337007 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("<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) { /** **/
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);