use std::time::SystemTime;
use crate::ban::parse_iso;
use crate::proto;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RegisteredUser {
pub user_id: u32,
pub name: Option<String>,
pub last_seen: Option<SystemTime>,
pub last_channel: Option<u32>,
}
impl RegisteredUser {
pub fn renamed(&self, name: &str) -> Self {
Self {
user_id: self.user_id,
name: Some(name.to_owned()),
last_seen: None,
last_channel: None,
}
}
pub fn unregistered(&self) -> Self {
Self {
user_id: self.user_id,
name: None,
last_seen: None,
last_channel: None,
}
}
}
pub fn from_message(message: &proto::UserList) -> Vec<RegisteredUser> {
message
.users
.iter()
.map(|user| RegisteredUser {
user_id: user.user_id,
name: user.name.clone(),
last_seen: user.last_seen.as_deref().and_then(parse_iso),
last_channel: user.last_channel,
})
.collect()
}
pub fn query_message() -> proto::UserList {
proto::UserList { users: Vec::new() }
}
pub fn change_message(user: &RegisteredUser) -> proto::UserList {
proto::UserList {
users: vec![proto::user_list::User {
user_id: user.user_id,
name: user.name.clone(),
last_seen: None,
last_channel: None,
}],
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(name: Option<&str>) -> proto::user_list::User {
proto::user_list::User {
user_id: 18,
name: name.map(str::to_owned),
last_seen: Some("2026-11-06T22:40:00.000000".into()),
last_channel: Some(2),
}
}
#[test]
fn a_server_answer_becomes_one_account_per_entry() {
let message = proto::UserList {
users: vec![entry(Some("Tove")), entry(None)],
};
let list = from_message(&message);
assert_eq!(list.len(), 2);
assert_eq!(list[0].name.as_deref(), Some("Tove"));
assert_eq!(list[0].last_channel, Some(2));
assert_eq!(
list[0]
.last_seen
.expect("read")
.duration_since(SystemTime::UNIX_EPOCH)
.expect("after the epoch")
.as_secs(),
1_794_004_800,
"the server writes UTC, so 22:40 is 22:40"
);
assert_eq!(list[1].name, None, "an entry without a name is a removal");
}
#[test]
fn the_last_seen_and_last_channel_stay_optional() {
let mut user = entry(Some("Tove"));
user.last_seen = None;
user.last_channel = None;
let list = from_message(&proto::UserList { users: vec![user] });
assert_eq!(list[0].last_seen, None, "an old server says nothing");
assert_eq!(list[0].last_channel, None);
}
#[test]
fn a_query_carries_no_entries() {
assert!(query_message().users.is_empty());
}
#[test]
fn a_rename_sends_the_id_and_the_new_name_only() {
let list = from_message(&proto::UserList {
users: vec![entry(Some("Tove"))],
});
let renamed = change_message(&list[0].renamed("Tove L."));
assert_eq!(renamed.users.len(), 1);
assert_eq!(renamed.users[0].user_id, 18);
assert_eq!(renamed.users[0].name.as_deref(), Some("Tove L."));
assert_eq!(renamed.users[0].last_channel, None);
let removed = change_message(&list[0].unregistered());
assert_eq!(removed.users[0].user_id, 18);
assert_eq!(removed.users[0].name, None, "no name removes the account");
}
#[test]
fn a_change_survives_the_wire() {
let message = change_message(&RegisteredUser {
user_id: 7,
name: Some("Freya".into()),
..Default::default()
});
let bytes = prost::Message::encode_to_vec(&message);
let back: proto::UserList = prost::Message::decode(bytes.as_slice()).expect("protobuf");
assert_eq!(
from_message(&back).first().expect("entry").name.as_deref(),
Some("Freya")
);
}
}