commit 2a65e13

grunfink  ·  2026-01-27 09:46:57 +0000 UTC
parents 4781337, c87c4dc
Merge pull request 'notifications filter' (#524) from byte/snac2:notif_filter into master

Reviewed-on: https://codeberg.org/grunfink/snac2/pulls/524
3 files changed,  +187, -75
M data.c
M html.c
M snac.h
M data.c
+65, -1
 1@@ -3503,11 +3503,75 @@ xs_list *notify_list(snac *snac, int skip, int show)
 2 }
 3 
 4 
 5+xs_list *notify_filter_list(snac *snac, xs_list *notifs)
 6+/* apply user-defined notification filter to IDs */
 7+{
 8+    const xs_dict *n_filter = xs_dict_get(snac->config, "notify_filter");
 9+    if (!n_filter) {
10+        return xs_dup(notifs);
11+    }
12+    const xs_val *n_def = xs_stock( XSTYPE_TRUE );
13+    int n_likes_on  = xs_is_true(xs_dict_get_def(n_filter, "likes", n_def));
14+    int n_reacts_on = xs_is_true(xs_dict_get_def(n_filter, "reacts", n_def));
15+    int n_ments_on  = xs_is_true(xs_dict_get_def(n_filter, "mentions", n_def));
16+    int n_ann_on    = xs_is_true(xs_dict_get_def(n_filter, "announces", n_def));
17+    int n_fol_on    = xs_is_true(xs_dict_get_def(n_filter, "follows", n_def));
18+    int n_unfol_on  = xs_is_true(xs_dict_get_def(n_filter, "unfollows", n_def));
19+    int n_folreq_on = xs_is_true(xs_dict_get_def(n_filter, "folreqs", n_def));
20+    int n_blocks_on = xs_is_true(xs_dict_get_def(n_filter, "blocks", n_def));
21+    int n_polls_on  = xs_is_true(xs_dict_get_def(n_filter, "polls", n_def));
22+
23+    const xs_str *v;
24+    xs_list *flt = xs_list_new();
25+
26+    xs_list_foreach(notifs, v) {
27+        xs *noti = notify_get(snac, v);
28+
29+        if (noti == NULL)
30+            continue;
31+
32+        const char *type  = xs_dict_get(noti, "type");
33+        const char *utype = xs_dict_get(noti, "utype");
34+        const char *actor_id = xs_dict_get(noti, "actor");
35+        if (strcmp(type, "EmojiReact") == 0 && xs_is_true(xs_dict_get(srv_config, "disable_emojireact")))
36+            continue;
37+        if (strcmp(type, "Create") == 0 && !n_ments_on)
38+            continue;
39+        if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0 && !n_polls_on)
40+            continue;
41+        if (strcmp(type, "Undo") == 0 && strcmp(utype, "Follow") == 0 && !n_unfol_on)
42+            continue;
43+        if (strcmp(type, "EmojiReact") == 0 || strcmp(type, "Like") == 0) {
44+            if (strcmp(type, "Like") == 0 && !n_likes_on)
45+                continue;
46+            if (strcmp(type, "EmojiReact") == 0 && !n_reacts_on)
47+                continue;
48+        }
49+        if (strcmp(type, "Follow") == 0) {
50+            if (pending_check(snac, actor_id)) {
51+                if (!n_folreq_on)
52+                    continue;
53+            }
54+            else
55+            if (!n_fol_on)
56+                continue;
57+        }
58+        if (strcmp(type, "Block") == 0 && !n_blocks_on)
59+            continue;
60+        if (strcmp(type, "Announce") == 0 && !n_ann_on)
61+            continue;
62+        flt = xs_list_append(flt, v);
63+    }
64+    return flt;
65+}
66+
67+
68 int notify_new_num(snac *snac)
69 /* counts the number of new notifications */
70 {
71     xs *t = notify_check_time(snac, 0);
72-    xs *lst = notify_list(snac, 0, XS_ALL);
73+    xs *lst_unfilt = notify_list(snac, 0, XS_ALL);
74+    xs *lst = notify_filter_list(snac, lst_unfilt);
75     int cnt = 0;
76 
77     xs_list *p = lst;
M html.c
+121, -74
  1@@ -492,7 +492,7 @@ void html_note_render_visibility(snac* user, xs_html *form, const int scope)
  2 
  3     xs_html *paragraph = xs_html_tag("p", xs_html_text(L("Visibility: ")));
  4     const int* to_render = scopes[scope];
  5-    for( int i = 0; to_render[i] != -1; i++ ){
  6+    for( int i = 0; to_render[i] != -1; i++ ) {
  7         const int scope_i = to_render[i];
  8         const char* value = scopes_tags[scope_i];
  9         const char* name = scopes_names[scope_i];
 10@@ -1422,6 +1422,20 @@ static xs_html *html_user_body(snac *user, int read_only)
 11 }
 12 
 13 
 14+xs_html *html_checkbox(const char *form_name, const char *label, int flag)
 15+/* helper for checkbox rendering */
 16+{
 17+    return xs_html_tag("p",
 18+        xs_html_sctag("input",
 19+            xs_html_attr("type",     "checkbox"),
 20+            xs_html_attr("name",     form_name),
 21+            xs_html_attr(flag ? "checked" : "", NULL)),
 22+        xs_html_tag("label",
 23+            xs_html_attr("for", form_name),
 24+            xs_html_text(label)));
 25+}
 26+
 27+
 28 xs_html *html_top_controls(snac *user)
 29 /* generates the top controls */
 30 {
 31@@ -1745,70 +1759,21 @@ xs_html *html_top_controls(snac *user)
 32                         xs_html_attr("type", "number"),
 33                         xs_html_attr("name", "purge_days"),
 34                         xs_html_attr("value", purge_days))),
 35-                xs_html_tag("p",
 36-                    xs_html_sctag("input",
 37-                        xs_html_attr("type", "checkbox"),
 38-                        xs_html_attr("name", "drop_dm_from_unknown"),
 39-                        xs_html_attr("id", "drop_dm_from_unknown"),
 40-                        xs_html_attr(xs_type(d_dm_f_u) == XSTYPE_TRUE ? "checked" : "", NULL)),
 41-                    xs_html_tag("label",
 42-                        xs_html_attr("for", "drop_dm_from_unknown"),
 43-                        xs_html_text(L("Drop direct messages from people you don't follow")))),
 44-                xs_html_tag("p",
 45-                    xs_html_sctag("input",
 46-                        xs_html_attr("type", "checkbox"),
 47-                        xs_html_attr("name", "bot"),
 48-                        xs_html_attr("id", "bot"),
 49-                        xs_html_attr(xs_type(bot) == XSTYPE_TRUE ? "checked" : "", NULL)),
 50-                    xs_html_tag("label",
 51-                        xs_html_attr("for", "bot"),
 52-                        xs_html_text(L("This account is a bot")))),
 53-                xs_html_tag("p",
 54-                    xs_html_sctag("input",
 55-                        xs_html_attr("type", "checkbox"),
 56-                        xs_html_attr("name", "auto_boost"),
 57-                        xs_html_attr("id", "auto_boost"),
 58-                        xs_html_attr(xs_is_true(auto_boost) ? "checked" : "", NULL)),
 59-                    xs_html_tag("label",
 60-                        xs_html_attr("for", "auto_boost"),
 61-                        xs_html_text(L("Auto-boost all mentions to this account")))),
 62-                xs_html_tag("p",
 63-                    xs_html_sctag("input",
 64-                        xs_html_attr("type", "checkbox"),
 65-                        xs_html_attr("name", "private"),
 66-                        xs_html_attr("id", "private"),
 67-                        xs_html_attr(xs_type(a_private) == XSTYPE_TRUE ? "checked" : "", NULL)),
 68-                    xs_html_tag("label",
 69-                        xs_html_attr("for", "private"),
 70-                        xs_html_text(L("This account is private "
 71-                            "(posts are not shown through the web)")))),
 72-                xs_html_tag("p",
 73-                    xs_html_sctag("input",
 74-                        xs_html_attr("type", "checkbox"),
 75-                        xs_html_attr("name", "collapse_threads"),
 76-                        xs_html_attr("id",   "collapse_threads"),
 77-                        xs_html_attr(xs_is_true(coll_thrds) ? "checked" : "", NULL)),
 78-                    xs_html_tag("label",
 79-                        xs_html_attr("for", "collapse_threads"),
 80-                        xs_html_text(L("Collapse top threads by default")))),
 81-                xs_html_tag("p",
 82-                    xs_html_sctag("input",
 83-                        xs_html_attr("type", "checkbox"),
 84-                        xs_html_attr("name", "approve_followers"),
 85-                        xs_html_attr("id",   "approve_followers"),
 86-                        xs_html_attr(xs_is_true(pending) ? "checked" : "", NULL)),
 87-                    xs_html_tag("label",
 88-                        xs_html_attr("for", "approve_followers"),
 89-                        xs_html_text(L("Follow requests must be approved")))),
 90-                xs_html_tag("p",
 91-                    xs_html_sctag("input",
 92-                        xs_html_attr("type", "checkbox"),
 93-                        xs_html_attr("name", "show_contact_metrics"),
 94-                        xs_html_attr("id",   "show_contact_metrics"),
 95-                        xs_html_attr(xs_is_true(show_foll) ? "checked" : "", NULL)),
 96-                    xs_html_tag("label",
 97-                        xs_html_attr("for", "show_contact_metrics"),
 98-                        xs_html_text(L("Publish follower and following metrics")))),
 99+                html_checkbox("drop_dm_from_unknown", L("Drop direct messages from people you don't follow"),
100+                        xs_is_true(d_dm_f_u)),
101+                html_checkbox("bot", L("This account is a bot"),
102+                        xs_is_true(bot)),
103+                html_checkbox("auto_boost", L("Auto-boost all mentions to this account"),
104+                        xs_is_true(auto_boost)),
105+                html_checkbox("private", L("This account is private "
106+                        "(posts are not shown through the web)"),
107+                        xs_is_true(a_private)),
108+                html_checkbox("collapse_threads", L("Collapse top threads by default"),
109+                        xs_is_true(coll_thrds)),
110+                html_checkbox("approve_followers", L("Follow requests must be approved"),
111+                        xs_is_true(pending)),
112+                html_checkbox("show_contact_metrics", L("Publish follower and following metrics"),
113+                        xs_is_true(show_foll)),
114                 xs_html_tag("p",
115                     xs_html_text(L("Current location:")),
116                     xs_html_sctag("br", NULL),
117@@ -2256,7 +2221,7 @@ static const xs_str* words_in_content(const xs_list *words, const xs_val *conten
118 /* returns a word that matches any of the words in content */
119 {
120     if (!xs_is_list(words) || !xs_is_string(content)) {
121-	    return NULL;
122+        return NULL;
123     }
124     xs *c = xs_split(content, " ");
125     xs *sc = xs_list_sort(c, NULL);
126@@ -2266,9 +2231,8 @@ static const xs_str* words_in_content(const xs_list *words, const xs_val *conten
127     xs_list_foreach(words, wv) {
128         xs_list_foreach(sc, cv) {
129             xs_tolower_i((xs_str*)cv);
130-            if(xs_str_in(cv, wv) != -1){
131+            if(xs_str_in(cv, wv) != -1)
132                 return wv;
133-            }
134         }
135     }
136 
137@@ -3634,7 +3598,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
138             continue;
139 
140         const int scope = get_msg_visibility(msg);
141-        if (user != NULL && scope != SCOPE_PUBLIC){
142+        if (user != NULL && scope != SCOPE_PUBLIC) {
143             /* is this message a non-public reply? */
144             const char *irt = get_in_reply_to(msg);
145 
146@@ -3651,13 +3615,11 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
147             }
148         }
149         /* hide non-public posts from /instance view */
150-        if (page != NULL && strcmp(page, "/instance") == 0 && scope != SCOPE_PUBLIC){
151+        if (page != NULL && strcmp(page, "/instance") == 0 && scope != SCOPE_PUBLIC)
152             continue;
153-        }
154         /* hide non-public posts viewed from outside */
155-        if (read_only && (scope != SCOPE_PUBLIC && scope != SCOPE_UNLISTED)) {
156+        if (read_only && (scope != SCOPE_PUBLIC && scope != SCOPE_UNLISTED))
157             continue;
158-        }
159 
160         xs_html *entry = html_entry(user, msg, read_only, 0, v, (user && !hide_children) ? 0 : 1);
161 
162@@ -4112,6 +4074,32 @@ xs_str *html_people_one(snac *user, const char *actor, const xs_list *list,
163     return xs_html_render_s(html, "<!DOCTYPE html>\n");
164 }
165 
166+void notify_filter(snac *user, const xs_dict *p_vars)
167+/* sets filter for notifications */
168+{
169+    const char *v;
170+    int likes_on  = (v = xs_dict_get(p_vars, "likes_on")) ? strcmp(v, "on") == 0 : 0;
171+    int reacts_on = (v = xs_dict_get(p_vars, "reacts_on")) ? strcmp(v, "on") == 0 : 0;
172+    int ments_on  = (v = xs_dict_get(p_vars, "mentions_on")) ? strcmp(v, "on") == 0 : 0;
173+    int ann_on    = (v = xs_dict_get(p_vars, "announces_on")) ? strcmp(v, "on") == 0 : 0;
174+    int foll_on   = (v = xs_dict_get(p_vars, "follows_on")) ? strcmp(v, "on") == 0 : 0;
175+    int unfoll_on = (v = xs_dict_get(p_vars, "unfollows_on")) ? strcmp(v, "on") == 0 : 0;
176+    int folreq_on = (v = xs_dict_get(p_vars, "folreqs_on")) ? strcmp(v, "on") == 0 : 0;
177+    int blocks_on = (v = xs_dict_get(p_vars, "blocks_on")) ? strcmp(v, "on") == 0 : 0;
178+    int polls_on  = (v = xs_dict_get(p_vars, "polls_on")) ? strcmp(v, "on") == 0 : 0;
179+    xs_dict *filter = xs_dict_new();
180+    filter = xs_dict_set(filter, "likes", xs_stock(likes_on ? XSTYPE_TRUE : XSTYPE_FALSE));
181+    filter = xs_dict_set(filter, "reacts", xs_stock(reacts_on ? XSTYPE_TRUE : XSTYPE_FALSE));
182+    filter = xs_dict_set(filter, "mentions", xs_stock(ments_on ? XSTYPE_TRUE : XSTYPE_FALSE));
183+    filter = xs_dict_set(filter, "announces", xs_stock(ann_on ? XSTYPE_TRUE : XSTYPE_FALSE));
184+    filter = xs_dict_set(filter, "follows", xs_stock(foll_on ? XSTYPE_TRUE : XSTYPE_FALSE));
185+    filter = xs_dict_set(filter, "unfollows", xs_stock(unfoll_on ? XSTYPE_TRUE : XSTYPE_FALSE));
186+    filter = xs_dict_set(filter, "folreqs", xs_stock(folreq_on ? XSTYPE_TRUE : XSTYPE_FALSE));
187+    filter = xs_dict_set(filter, "blocks", xs_stock(blocks_on ? XSTYPE_TRUE : XSTYPE_FALSE));
188+    filter = xs_dict_set(filter, "polls", xs_stock(polls_on ? XSTYPE_TRUE : XSTYPE_FALSE));
189+    user->config = xs_dict_set(user->config, "notify_filter", filter);
190+}
191+
192 xs_str *html_notifications(snac *user, int skip, int show)
193 {
194     const char *proxy = NULL;
195@@ -4119,15 +4107,66 @@ xs_str *html_notifications(snac *user, int skip, int show)
196     if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
197         proxy = user->actor;
198 
199-    xs *n_list = notify_list(user, skip, show);
200+    xs *n_list_unfilt = notify_list(user, skip, show);
201     xs *n_time = notify_check_time(user, 0);
202 
203     xs_html *body = html_user_body(user, 0);
204+    const xs_dict *n_filter = xs_dict_get(user->config, "notify_filter");
205+    if (!n_filter) {
206+        user->config = xs_dict_set(user->config, "notify_filter", xs_dict_new());
207+        n_filter = xs_dict_get(user->config, "notify_filter");
208+    }
209+    xs *n_list = notify_filter_list(user, n_list_unfilt);
210+    /* all filters are true by default */
211+    const xs_val *n_def = xs_stock( XSTYPE_TRUE );
212+    int n_likes_on  = xs_is_true(xs_dict_get_def(n_filter, "likes", n_def));
213+    int n_reacts_on = xs_is_true(xs_dict_get_def(n_filter, "reacts", n_def));
214+    int n_ments_on  = xs_is_true(xs_dict_get_def(n_filter, "mentions", n_def));
215+    int n_ann_on    = xs_is_true(xs_dict_get_def(n_filter, "announces", n_def));
216+    int n_fol_on    = xs_is_true(xs_dict_get_def(n_filter, "follows", n_def));
217+    int n_unfol_on  = xs_is_true(xs_dict_get_def(n_filter, "unfollows", n_def));
218+    int n_folreq_on = xs_is_true(xs_dict_get_def(n_filter, "folreqs", n_def));
219+    int n_blocks_on = xs_is_true(xs_dict_get_def(n_filter, "blocks", n_def));
220+    int n_polls_on  = xs_is_true(xs_dict_get_def(n_filter, "polls", n_def));
221 
222     xs_html *html = xs_html_tag("html",
223         html_user_head(user, NULL, NULL),
224         body);
225 
226+    xs *filter_notifs_action = xs_fmt("%s/admin/filter-notifications", user->actor);
227+    xs_html *notifs_form = xs_html_tag("form",
228+        xs_html_attr("autocomplete", "off"),
229+        xs_html_attr("method",       "post"),
230+        xs_html_attr("action",       filter_notifs_action),
231+        xs_html_attr("enctype",      "multipart/form-data"),
232+        xs_html_attr("id",           "filter"),
233+        xs_html_sctag("input",
234+            xs_html_attr("type",    "hidden"),
235+            xs_html_attr("name",    "hard-redir"),
236+            xs_html_attr("value",   xs_fmt("%s/notifications", user->actor))),
237+        html_checkbox("likes_on", L("Likes"), n_likes_on),
238+        html_checkbox("reacts_on", L("Emoji reacts"), n_reacts_on),
239+        html_checkbox("mentions_on", L("Mentions"), n_ments_on),
240+        html_checkbox("announces_on", L("Announces"), n_ann_on),
241+        html_checkbox("follows_on", L("Follows"), n_fol_on),
242+        html_checkbox("unfollows_on", L("Unollows"), n_unfol_on),
243+        html_checkbox("folreqs_on", L("Follow requests"), n_folreq_on),
244+        html_checkbox("blocks_on", L("Blocks"), n_blocks_on),
245+        html_checkbox("polls_on", L("Polls"), n_polls_on),
246+        xs_html_sctag("input",
247+            xs_html_attr("type",     "submit"),
248+            xs_html_attr("class",    "button"),
249+            xs_html_attr("value",    L("Save"))));
250+
251+    xs_html_add(body,
252+        xs_html_tag("p",
253+            xs_html_tag("div",
254+                xs_html_attr("class", "snac-notify-filter"),
255+                xs_html_tag("details",
256+                    xs_html_tag("summary",
257+                        xs_html_text(L("Notifications filter"))),
258+                    notifs_form))));
259+
260     xs *clear_all_action = xs_fmt("%s/admin/clear-notifications", user->actor);
261 
262     xs_html_add(body,
263@@ -4227,6 +4266,8 @@ xs_str *html_notifications(snac *user, int skip, int show)
264                 isEmoji = 1;
265 
266             if (xs_type(content) == XSTYPE_STRING) {
267+                if (!isEmoji && !n_likes_on)
268+                    continue;
269                 xs *emoji = replace_shortnames(xs_dup(content), xs_dict_get_path(noti, "msg.tag"), 1, proxy);
270                 wrk = xs_fmt("%s (%s&#xFE0F;)", isEmoji ? "EmojiReact" : "Like", emoji);
271                 label = wrk;
272@@ -5835,6 +5876,12 @@ int html_post_handler(const xs_dict *req, const char *q_path,
273         status = HTTP_STATUS_SEE_OTHER;
274     }
275     else
276+    if (p_path && strcmp(p_path, "admin/filter-notifications") == 0) { /** **/
277+        notify_filter(&snac, p_vars);
278+        user_persist(&snac, 0);
279+        status = HTTP_STATUS_SEE_OTHER;
280+    }
281+    else
282     if (p_path && strcmp(p_path, "admin/clear-notifications") == 0) { /** **/
283         notify_clear(&snac);
284         timeline_touch(&snac);
M snac.h
+1, -0
1@@ -277,6 +277,7 @@ void notify_add(snac *snac, const char *type, const char *utype,
2 xs_dict *notify_get(snac *snac, const char *id);
3 int notify_new_num(snac *snac);
4 xs_list *notify_list(snac *snac, int skip, int show);
5+xs_list *notify_filter_list(snac *snac, xs_list *ids);
6 void notify_clear(snac *snac);
7 
8 xs_dict *markers_get(snac *snac, const xs_list *markers);