From b3f5f403aa1f78ab4951a2ea7bc66ba933eda4c1 Mon Sep 17 00:00:00 2001 From: lew Date: Fri, 10 Apr 2026 19:38:46 +0100 Subject: [PATCH] refactor: forget epoch and going down to a 4char hex string --- Cargo.lock | 84 ++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 2 +- src/entries.rs | 58 ++++++++---------------------- src/render.rs | 10 ++++-- src/telegram.rs | 8 ++--- src/web.rs | 25 ++++++++----- templates/default.css | 5 --- 7 files changed, 123 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 48a63da..0243e00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -443,6 +443,18 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -451,7 +463,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -465,6 +477,7 @@ dependencies = [ "chrono", "dotenvy", "http-body-util", + "rand", "serde", "teloxide", "tempfile", @@ -474,7 +487,6 @@ dependencies = [ "tracing", "tracing-subscriber", "urlencoding", - "uuid", ] [[package]] @@ -1072,6 +1084,15 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -1124,12 +1145,47 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rc-box" version = "1.3.0" @@ -1588,7 +1644,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -1876,7 +1932,7 @@ version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ - "getrandom", + "getrandom 0.4.2", "js-sys", "wasm-bindgen", ] @@ -2387,6 +2443,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "zerofrom" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index 26e8074..6c6c825 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ teloxide = { version = "0.13", features = ["macros"], optional = true } serde = { version = "1", features = ["derive"] } toml = "0.8" dotenvy = "0.15" -uuid = { version = "1", features = ["v4"] } +rand = "0.9" chrono = "0.4" base64 = "0.22" tracing = "0.1" diff --git a/src/entries.rs b/src/entries.rs index f966b06..fa429fb 100644 --- a/src/entries.rs +++ b/src/entries.rs @@ -50,12 +50,6 @@ impl Entry { }) } - #[cfg(any(feature = "telegram", test))] - /// Return the short ID (UUID portion after the underscore). - pub fn short_id(&self) -> &str { - self.id.split('_').last().unwrap_or(&self.id) - } - /// Serialize entry back to file format. pub fn to_file_contents(&self) -> String { let frontmatter = toml::to_string_pretty(&self.meta).unwrap(); @@ -106,20 +100,11 @@ pub fn read_approved(dir: &Path) -> Vec { } #[cfg(any(feature = "telegram", test))] -/// Find a single entry by short ID (the UUID portion after the underscore). -pub fn find_entry(dir: &Path, short_id: &str) -> Result { - let read_dir = std::fs::read_dir(dir).map_err(|e| e.to_string())?; - for item in read_dir { - let Ok(item) = item else { continue }; - let path = item.path(); - let fname = path.file_name().and_then(|s| s.to_str()).unwrap_or(""); - if fname.contains(short_id) && fname.ends_with(".txt") { - let contents = std::fs::read_to_string(&path).map_err(|e| e.to_string())?; - let id = path.file_stem().and_then(|s| s.to_str()).unwrap_or("").to_string(); - return Entry::parse(&id, &contents); - } - } - Err("Not found.".into()) +/// Find a single entry by ID. +pub fn find_entry(dir: &Path, id: &str) -> Result { + let path = dir.join(format!("{id}.txt")); + let contents = std::fs::read_to_string(&path).map_err(|_| "Not found.".to_string())?; + Entry::parse(id, &contents) } #[cfg(any(feature = "telegram", test))] @@ -334,9 +319,9 @@ Hello!"#; fn test_find_entry() { let dir = tempfile::tempdir().unwrap(); let contents = "+++\nname = \"alice\"\ndate = \"2026-04-10\"\nstatus = \"pending\"\n+++\nhello"; - std::fs::write(dir.path().join("1744300800_abcd1234.txt"), contents).unwrap(); + std::fs::write(dir.path().join("ab12.txt"), contents).unwrap(); - let entry = find_entry(dir.path(), "abcd1234").unwrap(); + let entry = find_entry(dir.path(), "ab12").unwrap(); assert_eq!(entry.meta.name, "alice"); assert!(find_entry(dir.path(), "nonexistent").is_err()); @@ -352,16 +337,16 @@ Hello!"#; std::fs::create_dir_all(&drawings_dir).unwrap(); std::fs::create_dir_all(&vn_dir).unwrap(); - let contents = "+++\nname = \"alice\"\ndate = \"2026-04-10\"\nstatus = \"denied\"\ndrawing = \"1744300800_abcd1234.png\"\nvoice_note = \"1744300800_abcd1234.webm\"\n+++\nhello"; - std::fs::write(entries_dir.join("1744300800_abcd1234.txt"), contents).unwrap(); - std::fs::write(drawings_dir.join("1744300800_abcd1234.png"), b"fake png").unwrap(); - std::fs::write(vn_dir.join("1744300800_abcd1234.webm"), b"fake webm").unwrap(); + let contents = "+++\nname = \"alice\"\ndate = \"2026-04-10\"\nstatus = \"denied\"\ndrawing = \"ab12.png\"\nvoice_note = \"ab12.webm\"\n+++\nhello"; + std::fs::write(entries_dir.join("ab12.txt"), contents).unwrap(); + std::fs::write(drawings_dir.join("ab12.png"), b"fake png").unwrap(); + std::fs::write(vn_dir.join("ab12.webm"), b"fake webm").unwrap(); - let name = delete_entry(dir.path(), "abcd1234").unwrap(); + let name = delete_entry(dir.path(), "ab12").unwrap(); assert_eq!(name, "alice"); - assert!(!entries_dir.join("1744300800_abcd1234.txt").exists()); - assert!(!drawings_dir.join("1744300800_abcd1234.png").exists()); - assert!(!vn_dir.join("1744300800_abcd1234.webm").exists()); + assert!(!entries_dir.join("ab12.txt").exists()); + assert!(!drawings_dir.join("ab12.png").exists()); + assert!(!vn_dir.join("ab12.webm").exists()); } #[test] @@ -371,17 +356,4 @@ Hello!"#; assert!(delete_entry(dir.path(), "nonexistent").is_err()); } - #[test] - fn test_short_id() { - let contents = "+++\nname = \"a\"\ndate = \"2026-04-10\"\nstatus = \"pending\"\n+++\nhi"; - let entry = Entry::parse("1744300800_abcd1234", contents).unwrap(); - assert_eq!(entry.short_id(), "abcd1234"); - } - - #[test] - fn test_short_id_no_underscore() { - let contents = "+++\nname = \"a\"\ndate = \"2026-04-10\"\nstatus = \"pending\"\n+++\nhi"; - let entry = Entry::parse("plainid", contents).unwrap(); - assert_eq!(entry.short_id(), "plainid"); - } } diff --git a/src/render.rs b/src/render.rs index 50fa7ef..b97961f 100644 --- a/src/render.rs +++ b/src/render.rs @@ -257,7 +257,10 @@ fn escape_html(s: &str) -> String { fn render_entries(entries: &[Entry], config: &Config) -> String { let mut html = String::new(); - for entry in entries { + for (i, entry) in entries.iter().enumerate() { + if i > 0 { + html.push_str("
"); + } html.push_str(&render_entry(entry, config)); } html @@ -304,7 +307,8 @@ fn render_entry(entry: &Entry, config: &Config) -> String { String::new() }; format!( - "
{header}{drawing_html}{voice_note_html}
{body}
" + "
{header}{drawing_html}{voice_note_html}
{body}
", + id = escape_html(&entry.id) ) } @@ -397,7 +401,7 @@ mod tests { assert!(html.contains("entry-header")); assert!(html.contains("entry-name")); assert!(html.contains("entry-body")); - assert!(html.contains("
")); + assert!(html.contains("
")); } #[test] diff --git a/src/telegram.rs b/src/telegram.rs index 41ca697..4a41e1f 100644 --- a/src/telegram.rs +++ b/src/telegram.rs @@ -15,7 +15,7 @@ fn format_entry_list(entries: &[Entry], status_label: &str) -> String { let ellipsis = if entry.body.chars().count() > 30 { "..." } else { "" }; lines.push(format!( "- {} ({}) \"{}{}\"\n /view_{}", - entry.meta.name, entry.meta.date, preview, ellipsis, entry.short_id() + entry.meta.name, entry.meta.date, preview, ellipsis, entry.id )); } lines.join("\n") @@ -23,10 +23,9 @@ fn format_entry_list(entries: &[Entry], status_label: &str) -> String { /// Send a notification to Telegram about a new entry. async fn notify(bot: &Bot, chat_id: ChatId, entry: &Entry) { - let short_id = entry.short_id(); let text = format!( "New guestbook entry:\n\nName: {}\nWebsite: {}\n\n{}\n\n/allow_{}\n/deny_{}", - entry.meta.name, entry.meta.website, entry.body, short_id, short_id + entry.meta.name, entry.meta.website, entry.body, entry.id, entry.id ); if let Err(e) = bot.send_message(chat_id, &text).await { tracing::error!("failed to send telegram message: {e}"); @@ -109,11 +108,10 @@ pub async fn bot_task(bot: Bot, chat_id: ChatId, data_dir: PathBuf) { } else if let Some(id) = text.strip_prefix("/view_") { match entries::find_entry(&entries_dir, id) { Ok(entry) => { - let short_id = entry.short_id(); let text = format!( "Entry ({:?}):\n\nName: {}\nWebsite: {}\nDate: {}\n\n{}\n\n/allow_{}\n/deny_{}", entry.meta.status, entry.meta.name, entry.meta.website, - entry.meta.date, entry.body, short_id, short_id + entry.meta.date, entry.body, entry.id, entry.id ); bot.send_message(msg.chat.id, &text).await?; diff --git a/src/web.rs b/src/web.rs index 6d5b992..d5dca4b 100644 --- a/src/web.rs +++ b/src/web.rs @@ -11,7 +11,7 @@ use axum::{ use base64::Engine; use serde::Deserialize; use std::sync::Arc; -use uuid::Uuid; +use rand::Rng; use crate::config::Config; use crate::entries::{self, Entry, EntryMeta, Status}; @@ -49,6 +49,17 @@ pub fn router(state: Arc) -> Router { .with_state(state) } +fn generate_id(entries_dir: &std::path::Path) -> String { + const CHARS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz"; + let mut rng = rand::rng(); + loop { + let id: String = (0..4).map(|_| CHARS[rng.random_range(0..36)] as char).collect(); + if !entries_dir.join(format!("{id}.txt")).exists() { + return id; + } + } +} + async fn security_headers(req: axum::extract::Request, next: Next) -> Response { let is_static = req.uri().path().starts_with("/drawings/") || req.uri().path().starts_with("/voice_notes/"); @@ -260,15 +271,13 @@ async fn submit( }; let now = chrono::Utc::now(); - let epoch = now.timestamp(); - let short_id = &Uuid::new_v4().to_string()[..8]; - let prefix = format!("{epoch}_{short_id}"); let date = now.format("%Y-%m-%dT%H:%M:%S").to_string(); - let filename = format!("{prefix}.txt"); + let id = generate_id(&state.config.data_dir.join("entries")); + let filename = format!("{id}.txt"); - // Save drawing with the same prefix as the entry + // Save drawing with the same ID as the entry let drawing_filename = if let Some(ref bytes) = drawing_bytes { - let drawing_name = format!("{prefix}.png"); + let drawing_name = format!("{id}.png"); let drawings_dir = state.config.data_dir.join("drawings"); if let Err(e) = std::fs::create_dir_all(&drawings_dir) { tracing::error!("failed to create drawings directory: {e}"); @@ -284,7 +293,7 @@ async fn submit( }; let voice_note_filename = if let Some(ref bytes) = voice_note_bytes { - let vn_name = format!("{prefix}.webm"); + let vn_name = format!("{id}.webm"); let vn_dir = state.config.data_dir.join("voice_notes"); if let Err(e) = std::fs::create_dir_all(&vn_dir) { tracing::error!("failed to create voice notes directory: {e}"); diff --git a/templates/default.css b/templates/default.css index 3055b6a..4a5542b 100644 --- a/templates/default.css +++ b/templates/default.css @@ -108,11 +108,6 @@ audio { /* Entries */ .entry { margin: 0.5em 0; - padding-bottom: 0.5em; - border-bottom: 1px solid #ccc; -} -.entry:last-child { - border-bottom: none; } .entry-header { margin-bottom: 0.2em;