karma-web: capture channel topics (332/TOPIC) and show them on each board
All checks were successful
ci / check (push) Successful in 46s

This commit is contained in:
Jean Chevronnet 2026-07-30 00:58:10 +00:00
parent 4ccca54963
commit 26496b5147
No known key found for this signature in database
GPG key ID: 439666D63A9477E4
5 changed files with 182 additions and 31 deletions

View file

@ -1,5 +1,5 @@
use rustbot::i18n::Locale;
use rustbot::karma_web::{parse_board, parse_events, parse_meta, render};
use rustbot::karma_web::{parse_board, parse_events, parse_meta, render, Channel};
fn board_with_events(net: &str, store: &str, log: &str) -> rustbot::karma_web::Board {
let mut b = parse_board(net, store);
@ -102,31 +102,66 @@ fn renders_spanish_chrome() {
}
#[test]
fn parses_meta_server_and_channels() {
let (server, channels) =
parse_meta(r##"{"server":"irc.libera.chat","channels":["#argentina","#ar"]}"##);
fn parses_meta_channels_with_and_without_topic() {
// new format: objects with an optional topic
let (server, channels) = parse_meta(
r##"{"server":"irc.libera.chat","channels":[{"name":"#argentina","topic":"Bienvenidos"},{"name":"#ar"}]}"##,
);
assert_eq!(server, "irc.libera.chat");
assert_eq!(channels, vec!["#argentina".to_string(), "#ar".to_string()]);
// missing/garbage meta degrades to empty
assert_eq!(parse_meta("not json"), (String::new(), Vec::new()));
assert_eq!(channels.len(), 2);
assert_eq!(channels[0].name, "#argentina");
assert_eq!(channels[0].topic.as_deref(), Some("Bienvenidos"));
assert!(channels[1].topic.is_none());
// old format (bare strings) still parses
let (_, old) = parse_meta(r##"{"server":"x","channels":["#c"]}"##);
assert_eq!(old[0].name, "#c");
assert!(old[0].topic.is_none());
// garbage degrades to empty
let (s, c) = parse_meta("not json");
assert!(s.is_empty() && c.is_empty());
}
#[test]
fn renders_source_channel_and_server() {
fn renders_source_channel_server_and_topic() {
let mut b = parse_board("libera", r#"{"romaka":2}"#);
b.server = "irc.libera.chat".to_string();
b.channels = vec!["#argentina".to_string()];
b.channels = vec![Channel {
name: "#argentina".to_string(),
topic: Some("Argentina en Libera - stats https://qadlo.example/".to_string()),
}];
let en = render(&[b], 0, Locale::En);
assert!(en.contains("#argentina"), "channel missing: {en}");
assert!(en.contains("#argentina"), "channel missing");
assert!(en.contains("irc.libera.chat"), "server missing");
assert!(en.contains(">from<"), "from label missing");
// localized label
// the topic text shows, and its URL becomes a safe link
assert!(en.contains("Argentina en Libera"), "topic text missing");
assert!(
en.contains("<a href=\"https://qadlo.example/\""),
"topic url not linkified: {en}"
);
// spanish label still works with no topic
let mut b2 = parse_board("libera", r#"{"romaka":2}"#);
b2.channels = vec!["#argentina".to_string()];
b2.channels = vec![Channel {
name: "#argentina".to_string(),
topic: None,
}];
let es = render(&[b2], 0, Locale::Es);
assert!(es.contains(">desde<") && es.contains("#argentina"), "{es}");
}
#[test]
fn topic_cannot_inject_markup() {
let mut b = parse_board("t", r#"{"x":1}"#);
b.channels = vec![Channel {
name: "#c".to_string(),
topic: Some("<script>evil</script> javascript:alert(1)".to_string()),
}];
let html = render(&[b], 0, Locale::En);
assert!(!html.contains("<script>evil"), "topic markup leaked");
assert!(!html.contains("href=\"javascript:"), "js scheme linkified");
assert!(html.contains("&lt;script&gt;evil"), "topic not escaped");
}
#[test]
fn escapes_untrusted_irc_input_everywhere() {
let log = "{\"t\":1,\"o\":\"<script>\",\"d\":1,\"by\":\"a&b\",\"why\":\"\\\"><img src=x>\"}\n";