commit c3bcb2b
Louis Brauer
·
2024-05-30 22:30:37 +0000 UTC
parent af8f1ef
Implement instance announcements
6 files changed,
+143,
-4
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@@ -2590,6 +2608,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@@ -1982,10 +1982,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);