Signals: an admin chooses whether the server's TypeSafe key pays for everyone (#271)

With signals_on_server set and TYPESAFE_KEY present, the judge asks Jev
about everyone's Signals on the server's key; otherwise each person turns
Signals on in Settings with a key of their own, which is now checked with
TypeSafe before it is saved. The Signals folder shows only when they are on.

The browser suite's daemon inherited the shell's TYPESAFE_KEY, so fixture
feeds were sent to the real TypeSafe to be categorised; it is blanked now,
and an empty key counts as none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 13:18:03 +00:00
parent 33eb1f4db2
commit 9a41641eaa
15 changed files with 242 additions and 71 deletions

View File

@@ -2579,7 +2579,8 @@ pub struct Ask {
#[derive(Debug)]
pub struct Unjudged {
pub user_id: i64,
pub key: String,
/// Their own key; None when the server's is used for everyone.
pub key: Option<String>,
pub feed_id: String,
pub guid: String,
pub feed_title: Option<String>,
@@ -2704,8 +2705,9 @@ impl Db {
/// Up to `limit` items, newest first, that someone with a key has a Signal not yet asked
/// about: from the feeds they subscribe to, the last `SIGNAL_DAYS`, and not hidden from them.
/// `skip` are people whose key TypeSafe has refused.
pub async fn unjudged(&self, skip: &[i64], limit: i64) -> Result<Vec<Unjudged>> {
/// `skip` are people whose key TypeSafe has refused. With `everyone`, the server's key is
/// used, and people without a key of their own are asked for too (#271).
pub async fn unjudged(&self, skip: &[i64], limit: i64, everyone: bool) -> Result<Vec<Unjudged>> {
let mut a = Args::default();
let since = a.p(now() - SIGNAL_DAYS * 86_400);
let skip = if skip.is_empty() {
@@ -2714,6 +2716,7 @@ impl Db {
format!("AND g.user_id NOT IN ({})", skip.iter().map(|u| a.p(*u)).collect::<Vec<_>>().join(", "))
};
let limit = a.p(limit);
let keyed = if everyone { "true" } else { "u.typesafe_key IS NOT NULL" };
let sql = format!(
"SELECT g.id AS signal_id, g.name, g.keywords, g.description AS signal_description, g.user_id, u.typesafe_key, e.feed_id, e.guid, e.title,
e.description, f.title AS feed_title
@@ -2722,7 +2725,7 @@ impl Db {
JOIN subscriptions s ON s.user_id = g.user_id
JOIN entries e ON e.feed_id = s.feed_id
LEFT JOIN feeds f ON f.id = e.feed_id
WHERE u.typesafe_key IS NOT NULL {skip}
WHERE {keyed} {skip}
AND coalesce(e.published, e.first_seen) >= {since}
AND NOT EXISTS (SELECT 1 FROM signal_verdicts v
WHERE v.signal_id = g.id AND v.feed_id = e.feed_id AND v.guid = e.guid)
@@ -3394,19 +3397,23 @@ mod tests {
let food = db.add_signal(1, "Food", &words(&["baking"]), Some("Bread and cakes")).await.unwrap();
db.add_signal(2, "Theirs", &words(&["x"]), None).await.unwrap();
assert!(db.unjudged(&[], 50).await.unwrap().is_empty(), "nobody has a key yet");
assert!(db.unjudged(&[], 50, false).await.unwrap().is_empty(), "nobody has a key yet");
let everyone = db.unjudged(&[], 50, true).await.unwrap();
let mut seen: Vec<_> = everyone.iter().map(|u| (u.user_id, u.guid.as_str(), u.key.is_none())).collect();
seen.sort();
assert_eq!(seen, [(1, "x", true), (1, "y", true), (2, "x", true)], "on the server's key, everyone's Signals, no key of their own needed");
db.set_typesafe_key(1, Some("k")).await.unwrap();
let queue = db.unjudged(&[], 50).await.unwrap();
let queue = db.unjudged(&[], 50, false).await.unwrap();
let mut seen: Vec<_> = queue.iter().map(|u| (u.guid.as_str(), u.signals.len())).collect();
seen.sort();
assert_eq!(seen, [("x", 2), ("y", 2)], "each recent subscribed item once, with both Signals; old and unsubscribed left out");
let ask = queue[0].signals.iter().find(|a| a.id == food).unwrap();
assert_eq!((ask.name.as_str(), ask.description.as_deref()), ("Food", Some("Bread and cakes")), "Jev is asked about the name and the sentence");
assert!(db.unjudged(&[1], 50).await.unwrap().is_empty(), "a refused key is passed over");
assert!(db.unjudged(&[1], 50, false).await.unwrap().is_empty(), "a refused key is passed over");
db.record_verdicts("f", "x", &[(f1, true), (food, false)]).await.unwrap();
db.record_verdicts("g", "y", &[(f1, false), (food, true)]).await.unwrap();
assert!(db.unjudged(&[], 50).await.unwrap().is_empty(), "nothing is asked twice");
assert!(db.unjudged(&[], 50, false).await.unwrap().is_empty(), "nothing is asked twice");
let order = order_sql("published", "desc");
let page = db.signal_entries(1, f1, Filter::All, None, 0, 50, &order).await.unwrap();