Browse Source

Implement instance announcements

Louis Brauer 10 months ago
parent
commit
c3bcb2bd3b
6 changed files with 143 additions and 4 deletions
  1. 66 0
      data.c
  2. 8 0
      doc/snac.5
  3. 1 0
      doc/style.css
  4. 28 0
      html.c
  5. 34 4
      mastoapi.c
  6. 6 0
      snac.h

+ 66 - 0
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;
+}

+ 8 - 0
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 

+ 1 - 0
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% }

+ 28 - 0
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);
@@ -2590,6 +2608,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");

+ 34 - 4
mastoapi.c

@@ -1982,10 +1982,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 - 0
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);