feed: viewer mute/block — mute actors/tags/objects + apply-prefs bag + 9 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 42s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 42s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ if [ ! -x "$SX_SERVER" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SUITES=(basic fanout rank integration content notify home dedupe trending)
|
SUITES=(basic fanout rank integration content notify home dedupe trending mute)
|
||||||
|
|
||||||
OUT_JSON="lib/feed/scoreboard.json"
|
OUT_JSON="lib/feed/scoreboard.json"
|
||||||
OUT_MD="lib/feed/scoreboard.md"
|
OUT_MD="lib/feed/scoreboard.md"
|
||||||
@@ -41,6 +41,7 @@ run_suite() {
|
|||||||
(load "lib/feed/notify.sx")
|
(load "lib/feed/notify.sx")
|
||||||
(load "lib/feed/home.sx")
|
(load "lib/feed/home.sx")
|
||||||
(load "lib/feed/trending.sx")
|
(load "lib/feed/trending.sx")
|
||||||
|
(load "lib/feed/mute.sx")
|
||||||
(epoch 2)
|
(epoch 2)
|
||||||
(eval "(define feed-test-pass 0)")
|
(eval "(define feed-test-pass 0)")
|
||||||
(eval "(define feed-test-fail 0)")
|
(eval "(define feed-test-fail 0)")
|
||||||
|
|||||||
44
lib/feed/mute.sx
Normal file
44
lib/feed/mute.sx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
; feed/mute — viewer-controlled filtering. ACL (acl.sx) is author-controlled
|
||||||
|
; visibility; mute is the reader's own preference: hide muted actors or tags.
|
||||||
|
; Like ACL it is per-viewer and applied per request, never cached.
|
||||||
|
;
|
||||||
|
; Requires: lib/feed/normalize.sx, lib/feed/stream.sx, lib/feed/fanout.sx
|
||||||
|
; (feed/-elem?).
|
||||||
|
|
||||||
|
; drop activities authored by a muted actor
|
||||||
|
(define
|
||||||
|
feed/mute-actors
|
||||||
|
(fn
|
||||||
|
(stream actors)
|
||||||
|
(feed/filter
|
||||||
|
stream
|
||||||
|
(fn (a) (not (feed/-elem? (get a :actor) actors))))))
|
||||||
|
|
||||||
|
; drop activities carrying any muted tag
|
||||||
|
(define
|
||||||
|
feed/mute-tags
|
||||||
|
(fn
|
||||||
|
(stream tags)
|
||||||
|
(feed/filter
|
||||||
|
stream
|
||||||
|
(fn (a) (not (some (fn (t) (feed/-elem? t tags)) (get a :tags)))))))
|
||||||
|
|
||||||
|
; drop activities about a muted object (thread mute)
|
||||||
|
(define
|
||||||
|
feed/mute-objects
|
||||||
|
(fn
|
||||||
|
(stream objects)
|
||||||
|
(feed/filter
|
||||||
|
stream
|
||||||
|
(fn (a) (not (feed/-elem? (get a :object) objects))))))
|
||||||
|
|
||||||
|
; apply a viewer preference bag: {:mute-actors (...) :mute-tags (...) :mute-objects (...)}
|
||||||
|
(define
|
||||||
|
feed/apply-prefs
|
||||||
|
(fn
|
||||||
|
(stream prefs)
|
||||||
|
(feed/mute-objects
|
||||||
|
(feed/mute-tags
|
||||||
|
(feed/mute-actors stream (get prefs :mute-actors (list)))
|
||||||
|
(get prefs :mute-tags (list)))
|
||||||
|
(get prefs :mute-objects (list)))))
|
||||||
@@ -8,9 +8,10 @@
|
|||||||
"notify": {"pass": 8, "fail": 0},
|
"notify": {"pass": 8, "fail": 0},
|
||||||
"home": {"pass": 6, "fail": 0},
|
"home": {"pass": 6, "fail": 0},
|
||||||
"dedupe": {"pass": 9, "fail": 0},
|
"dedupe": {"pass": 9, "fail": 0},
|
||||||
"trending": {"pass": 11, "fail": 0}
|
"trending": {"pass": 11, "fail": 0},
|
||||||
|
"mute": {"pass": 9, "fail": 0}
|
||||||
},
|
},
|
||||||
"total_pass": 154,
|
"total_pass": 163,
|
||||||
"total_fail": 0,
|
"total_fail": 0,
|
||||||
"total": 154
|
"total": 163
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,5 @@ _Generated by `lib/feed/conformance.sh`_
|
|||||||
| home | 6 | 0 | 6 |
|
| home | 6 | 0 | 6 |
|
||||||
| dedupe | 9 | 0 | 9 |
|
| dedupe | 9 | 0 | 9 |
|
||||||
| trending | 11 | 0 | 11 |
|
| trending | 11 | 0 | 11 |
|
||||||
| **Total** | **154** | **0** | **154** |
|
| mute | 9 | 0 | 9 |
|
||||||
|
| **Total** | **163** | **0** | **163** |
|
||||||
|
|||||||
68
lib/feed/tests/mute.sx
Normal file
68
lib/feed/tests/mute.sx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
; Follow-up — viewer mute/block filtering. (feed-test name got expected)
|
||||||
|
|
||||||
|
(define
|
||||||
|
S
|
||||||
|
(feed/stream
|
||||||
|
(list
|
||||||
|
(feed/normalize {:actor "alice" :object "P1" :at 1 :tags (list "news")})
|
||||||
|
(feed/normalize {:actor "bob" :object "P2" :at 2 :tags (list "spam")})
|
||||||
|
(feed/normalize {:actor "alice" :object "P3" :at 3 :tags (list "cats")})
|
||||||
|
(feed/normalize {:actor "carol" :object "P4" :at 4 :tags (list "news" "spam")}))))
|
||||||
|
|
||||||
|
; ---------- mute actors ----------
|
||||||
|
|
||||||
|
(feed-test
|
||||||
|
"mute bob drops his post"
|
||||||
|
(map
|
||||||
|
(fn (a) (get a :object))
|
||||||
|
(feed/items (feed/mute-actors S (list "bob"))))
|
||||||
|
(list "P1" "P3" "P4"))
|
||||||
|
(feed-test
|
||||||
|
"mute alice drops two"
|
||||||
|
(feed/count (feed/mute-actors S (list "alice")))
|
||||||
|
2)
|
||||||
|
(feed-test
|
||||||
|
"mute nobody keeps all"
|
||||||
|
(feed/count (feed/mute-actors S (list)))
|
||||||
|
4)
|
||||||
|
|
||||||
|
; ---------- mute tags ----------
|
||||||
|
|
||||||
|
(feed-test
|
||||||
|
"mute spam tag drops two"
|
||||||
|
(map
|
||||||
|
(fn (a) (get a :object))
|
||||||
|
(feed/items (feed/mute-tags S (list "spam"))))
|
||||||
|
(list "P1" "P3"))
|
||||||
|
(feed-test
|
||||||
|
"mute news+cats leaves spam-only"
|
||||||
|
(map
|
||||||
|
(fn (a) (get a :object))
|
||||||
|
(feed/items (feed/mute-tags S (list "news" "cats"))))
|
||||||
|
(list "P2"))
|
||||||
|
|
||||||
|
; ---------- mute objects ----------
|
||||||
|
|
||||||
|
(feed-test
|
||||||
|
"mute object P3 (thread mute)"
|
||||||
|
(feed/count (feed/mute-objects S (list "P3")))
|
||||||
|
3)
|
||||||
|
|
||||||
|
; ---------- combined prefs ----------
|
||||||
|
|
||||||
|
(feed-test
|
||||||
|
"apply-prefs actors + tags"
|
||||||
|
(map
|
||||||
|
(fn (a) (get a :object))
|
||||||
|
(feed/items (feed/apply-prefs S {:mute-actors (list "bob") :mute-tags (list "cats")})))
|
||||||
|
(list "P1" "P4"))
|
||||||
|
(feed-test
|
||||||
|
"apply-prefs empty keeps all"
|
||||||
|
(feed/count (feed/apply-prefs S {}))
|
||||||
|
4)
|
||||||
|
(feed-test
|
||||||
|
"apply-prefs all three filters"
|
||||||
|
(map
|
||||||
|
(fn (a) (get a :object))
|
||||||
|
(feed/items (feed/apply-prefs S {:mute-objects (list "P3") :mute-actors (list "carol") :mute-tags (list "spam")})))
|
||||||
|
(list "P1"))
|
||||||
@@ -14,7 +14,7 @@ APL, ACL visibility filtering via `lib/acl/`, federation via fed-sx.
|
|||||||
|
|
||||||
## Status (rolling)
|
## Status (rolling)
|
||||||
|
|
||||||
`bash lib/feed/conformance.sh` → **154/154** (Phases 1–4 + TF-IDF + notifications + home + smart-dedupe + trending)
|
`bash lib/feed/conformance.sh` → **163/163** (Phases 1–4 + TF-IDF, notifications, home, smart-dedupe, trending, mute)
|
||||||
|
|
||||||
## Ground rules
|
## Ground rules
|
||||||
|
|
||||||
@@ -154,6 +154,9 @@ are function parameters. Real acl-sx / fed-sx wire in at the call site unchanged
|
|||||||
- [x] Trending — `feed/trending` / `feed/trending-actors`: objects/actors ranked
|
- [x] Trending — `feed/trending` / `feed/trending-actors`: objects/actors ranked
|
||||||
by activity count in a recency window, count-desc with key-asc tiebreak
|
by activity count in a recency window, count-desc with key-asc tiebreak
|
||||||
(`trending.sx`); 11 tests. (154/154 total.)
|
(`trending.sx`); 11 tests. (154/154 total.)
|
||||||
|
- [x] Mute/block — `feed/mute-actors` / `feed/mute-tags` / `feed/mute-objects` /
|
||||||
|
`feed/apply-prefs`: viewer-controlled per-request filtering (complements ACL's
|
||||||
|
author-controlled visibility) (`mute.sx`); 9 tests. (163/163 total.)
|
||||||
|
|
||||||
(none)
|
(none)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user