use std::
{
fs,
collections::{ HashMap, HashSet },
sync::{ LazyLock, Mutex },
};
use wincode::{ SchemaWrite, SchemaRead };
use why2::consts as why2_consts;
use crate::
{
misc,
crypto,
consts::{ self, SharedKeys },
network::codes::
{
MessageColors,
StoredMessage,
},
};
#[derive(SchemaWrite, SchemaRead, Clone)]
struct Record {
username: String,
text: String,
image: Option<[u8; 32]>,
}
static HISTORY: LazyLock<Mutex<Vec<Record>>> = LazyLock::new(|| Mutex::new(load())); static KEYS: LazyLock<SharedKeys> = LazyLock::new(crypto::history_keys);
fn path() -> String {
super::config_path(consts::SERVER_MESSAGES_FILE)
}
fn load() -> Vec<Record> {
let Ok(bytes) = fs::read(path()) else
{
log::info!("No message history on disk, starting empty");
return Vec::new();
};
let Some(plaintext) = crypto::decrypt_packet::
<{ why2_consts::DEFAULT_GRID_WIDTH }, { why2_consts::DEFAULT_GRID_HEIGHT }>(bytes, &KEYS)
else
{
log::error!("Message history failed verification, it is being ignored");
return Vec::new();
};
match wincode::config::deserialize::<Vec<Record>, _>(&plaintext, consts::PACKET_CONFIG)
{
Ok(history) =>
{
log::info!("Loaded {} stored messages", history.len());
history
},
Err(_) => migrate(&plaintext), }
}
fn migrate(plaintext: &[u8]) -> Vec<Record>
{
let Ok(history) = wincode::config::deserialize::<Vec<StoredMessage>, _>(plaintext, consts::PACKET_CONFIG) else
{
log::error!("Message history is of an older format, it is being ignored");
return Vec::new();
};
log::info!("Migrated {} stored messages, their colors dropped", history.len());
history.into_iter().map(|message| Record
{
username: message.username,
text: message.text,
image: message.image,
}).collect()
}
pub fn store(username: &str, text: &str) {
push(Record
{
username: username.to_string(),
text: text.to_string(),
image: None,
});
}
pub fn store_image(username: &str, filename: &str, hash: &[u8; 32])
{
push(Record
{
username: username.to_string(),
text: filename.to_string(),
image: Some(*hash),
});
}
fn push(message: Record) {
let limit: usize = super::read_config("max_persistent_messages");
if limit == 0 { return; }
let mut history = HISTORY.lock().unwrap();
history.push(message);
let over = history.len().saturating_sub(limit);
let dropped: Vec<[u8; 32]> = history.drain(..over).filter_map(|message| message.image).collect();
let orphans: Vec<[u8; 32]> = dropped.into_iter()
.filter(|hash| !history.iter().any(|message| message.image.as_ref() == Some(hash)))
.collect();
let bytes = wincode::config::serialize(&*history, consts::PACKET_CONFIG).expect("Encoding message history failed");
let sealed = crypto::encrypt_packet::<{ why2_consts::DEFAULT_GRID_WIDTH }, { why2_consts::DEFAULT_GRID_HEIGHT }>(&bytes, &KEYS);
fs::write(path(), sealed).expect("Saving message history failed");
drop(history);
if !orphans.is_empty() { log::info!("Dropping {} stored images with no history entry left", orphans.len()); }
for hash in orphans { let _ = fs::remove_file(misc::get_image_dir().join(misc::hex(&hash))); }
}
pub fn has_image(hash: &[u8; 32]) -> bool {
HISTORY.lock().unwrap().iter().any(|message| message.image.as_ref() == Some(hash))
}
pub fn sweep_images()
{
let Ok(directory) = fs::read_dir(misc::get_image_dir()) else { return };
let files: Vec<_> = directory.flatten().map(|entry| entry.path()).collect();
if files.is_empty() { return; }
let kept: HashSet<String> = HISTORY.lock().unwrap().iter()
.filter_map(|message| message.image.as_ref().map(|hash| misc::hex(hash)))
.collect();
let mut swept = 0;
for file in files
{
let named = file.file_name().and_then(|name| name.to_str())
.map(|name| kept.contains(name)).unwrap_or(false);
if !named && fs::remove_file(&file).is_ok() { swept += 1; }
}
if swept > 0 { log::info!("Swept {swept} stored images nothing names any more"); }
}
pub fn all() -> Vec<StoredMessage>
{
let history = HISTORY.lock().unwrap().clone();
let mut looked_up: HashMap<String, MessageColors> = HashMap::new();
history.into_iter().map(|message|
{
let stored = looked_up.entry(message.username.clone())
.or_insert_with(|| super::users::colors(&message.username));
StoredMessage
{
username: message.username,
text: message.text,
colors: match message.image.is_some()
{
true => MessageColors { username_color: stored.username_color, message_color: None },
false => stored.clone(),
},
image: message.image,
}
}).collect()
}