Files
Joshua MoonandClaude Fable 5 6113a49d02 feat: populate the room switcher from the server
The rooms are published after all -- I had looked in the wrong place and
concluded they were not.

They are not on the websocket: dumping raw frames shows the server only
ever sends permissions, motd, history, messages, users and user. And
they are not on /chat, which contains just `const room_id = 1` and an
iframe. They are in that iframe, /test-chat, as

    <a class="chat-room" data-id="20">Lolcows</a>

chat::rooms fetches and parses that, and the list is now sent to the UI
as Event::Rooms. The switcher shows real names -- General, Lolcows,
Beauty Parlor, Fishtank, Gunt, Keno Kasino, SPORTS!! -- in the page's own
order, which is editorial rather than sorted by ID. The status bar shows
the current room's name too.

Config `rooms` entries still work, now as overrides for a fetched name or
as labels for rooms the page does not advertise. The fetch is
best-effort and runs alongside connecting: if it fails the switcher still
takes a typed number.

tests/frames_probe.rs and tests/rooms_probe.rs record how this was
established, so the next person does not repeat the same dead ends.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-28 13:11:46 -04:00

100 lines
3.5 KiB
Rust

//! Dumps raw websocket frames so we can see every key the server sends, not
//! just the ones the client currently deserializes.
//!
//! Uses the saved session; no credentials needed.
//!
//! cargo test --test frames_probe -- --ignored --nocapture
use std::collections::BTreeSet;
use std::time::Duration;
use futures_util::{SinkExt, StreamExt};
use sockchat::config::Config;
use sockchat::net::Transport;
use sockchat::net::http::{CookieJar, HttpClient};
use tokio_tungstenite::tungstenite::Message as WsMessage;
#[tokio::test]
#[ignore = "requires network and bootstraps Tor"]
async fn probe_socket_frames() {
sockchat::net::init_crypto();
let cfg = Config::load().expect("config");
let transport = Transport::bootstrap_tor().await.expect("Tor bootstrap");
// Clear the gate over HTTP first so the upgrade is not challenged.
let jar = CookieJar::new();
jar.seed(&cfg.cookies);
let _ = sockchat::auth::store::load(&jar);
let http = HttpClient::new(transport.clone(), jar.clone(), cfg.user_agent.clone());
match sockchat::net::kiwiflare::clear(&http, &cfg.http_url(), 8).await {
Ok(n) => eprintln!("cleared gate ({n})"),
Err(e) => eprintln!("gate: {e:#}"),
}
let ws = sockchat::chat::socket::connect(&cfg, &transport, jar.header())
.await
.expect("websocket");
let (mut sink, mut stream) = ws.split();
sink.send(WsMessage::text(format!("/join {}", cfg.room)))
.await
.expect("join");
let mut keys_seen: BTreeSet<String> = BTreeSet::new();
let deadline = tokio::time::Instant::now() + Duration::from_secs(45);
for i in 0..40 {
let frame = match tokio::time::timeout_at(deadline, stream.next()).await {
Ok(Some(Ok(f))) => f,
Ok(Some(Err(e))) => {
eprintln!("stream error: {e}");
break;
}
Ok(None) => break,
Err(_) => {
eprintln!("-- no more frames within the deadline --");
break;
}
};
let text = match frame {
WsMessage::Text(t) => t.to_string(),
WsMessage::Binary(b) => String::from_utf8_lossy(&b).into_owned(),
other => {
eprintln!("[{i}] non-text frame: {other:?}");
continue;
}
};
// Record the top-level keys of every JSON object we see.
if let Ok(serde_json::Value::Object(map)) = serde_json::from_str::<serde_json::Value>(&text)
{
for k in map.keys() {
keys_seen.insert(k.clone());
}
eprintln!("[{i}] object keys: {:?}", map.keys().collect::<Vec<_>>());
// Print anything that is not the bulk message/user payload in full.
for (k, v) in &map {
if k == "messages" || k == "users" {
eprintln!(
" {k}: <{} entries>",
v.as_array().map(|a| a.len()).unwrap_or(0)
);
continue;
}
let rendered = serde_json::to_string(v).unwrap_or_default();
eprintln!(
" {k}: {}",
rendered.chars().take(1200).collect::<String>()
);
}
} else {
eprintln!(
"[{i}] plaintext: {}",
text.chars().take(200).collect::<String>()
);
}
}
eprintln!("\n=== all top-level keys observed ===\n{keys_seen:?}");
}