commit fe52b76

grunfink  ·  2024-05-31 09:09:54 +0000 UTC
parents a5e331c, ac3b5dc
Merge pull request 'Implement instance announcements' (#173) from louis77/snac2:announcements into master

Reviewed-on: https://codeberg.org/grunfink/snac2/pulls/173
6 files changed,  +143, -4
M data.c
M html.c
M snac.h
M data.c
+66, -0
 1@@ -3370,3 +3370,69 @@ void srv_archive_qitem(const char *prefix, xs_dict *q_item)
 2         fclose(f);
 3     }
 4 }
 5+
 6+
 7+t_announcement *announcement(const double after)
 8+/* returns announcement text or NULL if none exists or it is olde than "after" */
 9+{
10+    static const long int MAX_SIZE = 2048;
11+    static t_announcement a = {
12+        .text = NULL,
13+        .timestamp = 0.0,
14+    };
15+    static xs_str *fn = NULL;
16+    if (fn == NULL)
17+        fn = xs_fmt("%s/announcement.txt", srv_basedir);
18+
19+    const double ts = mtime(fn);
20+
21+    /* file does not exist or other than what was requested */
22+    if (ts == 0.0 || ts <= after)
23+        return NULL;
24+
25+    /* nothing changed, just return the current announcement */
26+    if (a.text != NULL && ts <= a.timestamp)
27+        return &a;
28+
29+    /* read and store new announcement */
30+    FILE *f;
31+
32+    if ((f = fopen(fn, "r")) != NULL) {
33+        fseek (f, 0, SEEK_END);
34+        const long int length = ftell(f);
35+
36+        if (length > MAX_SIZE) {
37+            /* this is probably unintentional */
38+            srv_log(xs_fmt("announcement.txt too big: %ld bytes, max is %ld, ignoring.", length, MAX_SIZE));
39+        }
40+        else
41+        if (length > 0) {
42+            fseek (f, 0, SEEK_SET);
43+            char *buffer = malloc(length + 1);
44+            if (buffer) {
45+                fread(buffer, 1, length, f);
46+                buffer[length] = '\0';
47+
48+                free(a.text);
49+                a.text = buffer;
50+                a.timestamp = ts;
51+            }
52+            else {
53+                srv_log("Error allocating memory for announcement");
54+            }
55+        }
56+        else {
57+            /* an empty file means no announcement */
58+            free(a.text);
59+            a.text = NULL;
60+            a.timestamp = 0.0;
61+        }
62+
63+        fclose (f);
64+    }
65+
66+    if (a.text != NULL)
67+        return &a;
68+
69+    return NULL;
70+}
+8, -0
 1@@ -121,6 +121,14 @@ rejected. This brings the flexibility and destruction power of regular expressio
 2 to your Fediverse experience. To be used wisely (see
 3 .Xr snac 8
 4 for more information).
 5+.It Pa announcement.txt
 6+If this file is present, an announcement will be shown to logged in users
 7+on every page with its contents. It is also available through the Mastodon API.
 8+Users can dismiss the announcement, which works by storing the modification time
 9+in the "last_announcement" field of the
10+.Pa user.json
11+file. When the file is modified, the announcement will then reappear. It can
12+contain only text and will be ignored if it has more than 2048 bytes.
13 .El
14 .Pp
15 Each user directory is a subdirectory of 
+1, -0
1@@ -6,6 +6,7 @@ pre { overflow-x: scroll; }
2 .snac-top-user { text-align: center; padding-bottom: 2em }
3 .snac-top-user-name { font-size: 200% }
4 .snac-top-user-id { font-size: 150% }
5+.snac-announcement { border: black 1px solid; padding: 0.5em }
6 .snac-avatar { float: left; height: 2.5em; padding: 0.25em }
7 .snac-author { font-size: 90%; text-decoration: none }
8 .snac-author-tag { font-size: 80% }
M html.c
+28, -0
 1@@ -786,6 +786,24 @@ static xs_html *html_user_body(snac *user, int read_only)
 2             xs_html_attr("class", "snac-top-user-id"),
 3             xs_html_text(handle)));
 4 
 5+    /** instance announcement **/
 6+
 7+    double la = 0.0;
 8+    xs *user_la = xs_dup(xs_dict_get(user->config, "last_announcement"));
 9+    if (user_la != NULL)
10+        la = xs_number_get(user_la);
11+
12+    const t_announcement *an = announcement(la);
13+    if (an != NULL && (an->text != NULL)) {
14+        xs_html_add(top_user,  xs_html_tag("div",
15+            xs_html_attr("class", "snac-announcement"),
16+                xs_html_text(an->text),
17+                xs_html_text(" "),
18+                xs_html_sctag("a",
19+                        xs_html_attr("href", xs_dup(xs_fmt("?da=%.0f", an->timestamp)))),
20+                        xs_html_text("Dismiss")));
21+    }
22+
23     if (read_only) {
24         xs *es1  = encode_html(xs_dict_get(user->config, "bio"));
25         xs *bio1 = not_really_markdown(es1, NULL, NULL);
26@@ -2606,6 +2624,16 @@ int html_get_handler(const xs_dict *req, const char *q_path,
27         skip = atoi(v), cache = 0, save = 0;
28     if ((v = xs_dict_get(q_vars, "show")) != NULL)
29         show = atoi(v), cache = 0, save = 0;
30+    if ((v = xs_dict_get(q_vars, "da")) != NULL) {
31+        /* user dismissed an announcement */
32+        if (login(&snac, req)) {
33+            double ts = atof(v);
34+            xs *timestamp = xs_number_new(ts);
35+            srv_log(xs_fmt("user dismissed announcements until %d", ts));
36+            snac.config = xs_dict_set(snac.config, "last_announcement", timestamp);
37+            user_persist(&snac);
38+        }
39+    }
40 
41     if (p_path == NULL) { /** public timeline **/
42         xs *h = xs_str_localtime(0, "%Y-%m.html");
+34, -4
 1@@ -1997,10 +1997,40 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 2     }
 3     else
 4     if (strcmp(cmd, "/v1/announcements") == 0) { /** **/
 5-        /* snac has no announcements (yet?) */
 6-        *body  = xs_dup("[]");
 7-        *ctype = "application/json";
 8-        status = HTTP_STATUS_OK;
 9+        if (logged_in) {
10+            xs *resp = xs_list_new();
11+            double la = 0.0;
12+            xs *user_la = xs_dup(xs_dict_get(snac1.config, "last_announcement"));
13+            if (user_la != NULL)
14+                la = xs_number_get(user_la);
15+            xs *val_date = xs_str_utctime(la, ISO_DATE_SPEC);
16+
17+            /* contrary to html, we always send the announcement and set the read flag instead */
18+
19+            const t_announcement *annce = announcement(la);
20+            if (annce != NULL && annce->text != NULL) {
21+                xs *an = xs_dict_new();
22+                an = xs_dict_set(an, "id",           xs_fmt("%d", annce->timestamp));
23+                an = xs_dict_set(an, "content",      xs_fmt("<p>%s</p>", annce->text));
24+                an = xs_dict_set(an, "starts_at",    xs_stock(XSTYPE_NULL));
25+                an = xs_dict_set(an, "ends_at",      xs_stock(XSTYPE_NULL));
26+                an = xs_dict_set(an, "all_day",      xs_stock(XSTYPE_TRUE));
27+                an = xs_dict_set(an, "published_at", val_date);
28+                an = xs_dict_set(an, "updated_at",   val_date);
29+                an = xs_dict_set(an, "read",         (annce->timestamp >= la)
30+                    ? xs_stock(XSTYPE_FALSE) : xs_stock(XSTYPE_TRUE));
31+                an = xs_dict_set(an, "mentions",     xs_stock(XSTYPE_LIST));
32+                an = xs_dict_set(an, "statuses",     xs_stock(XSTYPE_LIST));
33+                an = xs_dict_set(an, "tags",         xs_stock(XSTYPE_LIST));
34+                an = xs_dict_set(an, "emojis",       xs_stock(XSTYPE_LIST));
35+                an = xs_dict_set(an, "reactions",    xs_stock(XSTYPE_LIST));
36+                resp = xs_list_append(resp, an);
37+            }
38+
39+            *body  = xs_json_dumps(resp, 4);
40+            *ctype = "application/json";
41+            status = HTTP_STATUS_OK;
42+        }
43     }
44     else
45     if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/
M snac.h
+6, -0
 1@@ -375,3 +375,9 @@ typedef enum {
 2 } http_status;
 3 
 4 const char *http_status_text(int status);
 5+
 6+typedef struct {
 7+    double timestamp;
 8+    char   *text;
 9+} t_announcement;
10+t_announcement *announcement(double after);