From a8c1d47132d4c47863ae7dc5b6f4816d024d3de4 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 01:31:45 +0100 Subject: cron: validate expressions and allocate ids atomically --- src/commands/cron.rs | 14 ++++++++++++-- src/database/mod.rs | 19 +++---------------- src/lang.rs | 3 +++ src/tasks/mod.rs | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/commands/cron.rs b/src/commands/cron.rs index 301ae3d..94cf955 100644 --- a/src/commands/cron.rs +++ b/src/commands/cron.rs @@ -44,7 +44,12 @@ pub async fn dodaj_audio( ctx.data().copyparty.build_url_no_pw(&audio_filename) }; - let guild_id = ctx.guild_id().ok_or_else(lang::err_not_in_guild)?; + if let Err(e) = crate::tasks::parse_cron(&cron_expression) { + ctx.say(lang::fmt_cron_invalid(&cron_expression, &e.to_string())) + .await?; + return Ok(()); + } + let guild_id = ctx.guild_id().ok_or_else(|| lang::err_not_in_guild())?; if !ctx.data().copyparty.is_own_url(&full_url) { crate::net::ensure_public_http_url(&full_url).await?; } @@ -77,7 +82,12 @@ pub async fn dodaj_wiadomość( _ => return Err(lang::err_bad_channel_type()), }; - let guild_id = ctx.guild_id().ok_or_else(lang::err_not_in_guild)?; + if let Err(e) = crate::tasks::parse_cron(&cron_expression) { + ctx.say(lang::fmt_cron_invalid(&cron_expression, &e.to_string())) + .await?; + return Ok(()); + } + let guild_id = ctx.guild_id().ok_or_else(|| lang::err_not_in_guild())?; database::add_cron_job( &ctx.data().db_pool, guild_id.get() as i64, diff --git a/src/database/mod.rs b/src/database/mod.rs index d15bb00..192416f 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -361,24 +361,11 @@ pub async fn add_cron_job( audio_filename: Option<&str>, message: Option<&str>, ) -> Result { - // Find the lowest unused ID to reuse freed slots - let existing: Vec = sqlx::query_scalar("SELECT id FROM cron_jobs ORDER BY id") - .fetch_all(pool) - .await?; - - let next_id = existing.iter().enumerate().find_map(|(i, &id)| { - let expected = (i as i64) + 1; - if id != expected { - Some(expected) - } else { - None - } - }).unwrap_or(existing.len() as i64 + 1); - + // Let SQLite allocate the id atomically; a SELECT-then-INSERT scheme + // races when two users create jobs at the same time. let result = sqlx::query( - "INSERT INTO cron_jobs (id, guild_id, cron_expression, type, channel_id, audio_filename, message) VALUES (?, ?, ?, ?, ?, ?, ?)", + "INSERT INTO cron_jobs (guild_id, cron_expression, type, channel_id, audio_filename, message) VALUES (?, ?, ?, ?, ?, ?)", ) - .bind(next_id) .bind(guild_id) .bind(cron_expression) .bind(r#type) diff --git a/src/lang.rs b/src/lang.rs index de6d23f..8f1ecc1 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -46,6 +46,9 @@ pub const L_CRON_PAUSED: &str = "⏸️ Zapauzowana"; pub fn fmt_cron_audio_queued(cron: &str) -> String { format!("Praca audio zakolejkowana z cron'em `{cron}`") } +pub fn fmt_cron_invalid(cron: &str, err: &str) -> String { + format!("Nieprawidłowe wyrażenie cron `{cron}`: {err}") +} pub fn fmt_cron_label_audio(name: &str) -> String { format!("Audio: `{name}`") } diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs index c895a1b..5506487 100644 --- a/src/tasks/mod.rs +++ b/src/tasks/mod.rs @@ -22,7 +22,7 @@ pub struct CronContext { /// Normalize user-friendly cron expressions to the 7-field format the `cron` crate expects. /// Supports: 5-field (min hour dom month dow) and 6-field (sec min hour dom month dow). -fn parse_cron(raw: &str) -> Result { +pub(crate) fn parse_cron(raw: &str) -> Result { let fields: Vec<&str> = raw.split_whitespace().collect(); let normalized = match fields.len() { 7 => raw.to_string(), -- cgit v1.2.3