aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-04 01:31:45 +0100
committersalmonik <salmonik@tuta.io>2026-11-04 01:31:45 +0100
commita8c1d47132d4c47863ae7dc5b6f4816d024d3de4 (patch)
tree505211909c9caa36ceb3cba2050e4d10cb65598d
parentmusic: reject live streams on the yt-dlp path (diff)
downloadsenator-sperma-a8c1d47132d4c47863ae7dc5b6f4816d024d3de4.tar.gz
senator-sperma-a8c1d47132d4c47863ae7dc5b6f4816d024d3de4.zip
cron: validate expressions and allocate ids atomically
-rw-r--r--src/commands/cron.rs14
-rw-r--r--src/database/mod.rs19
-rw-r--r--src/lang.rs3
-rw-r--r--src/tasks/mod.rs2
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(
44 ctx.data().copyparty.build_url_no_pw(&audio_filename) 44 ctx.data().copyparty.build_url_no_pw(&audio_filename)
45 }; 45 };
46 46
47 let guild_id = ctx.guild_id().ok_or_else(lang::err_not_in_guild)?; 47 if let Err(e) = crate::tasks::parse_cron(&cron_expression) {
48 ctx.say(lang::fmt_cron_invalid(&cron_expression, &e.to_string()))
49 .await?;
50 return Ok(());
51 }
52 let guild_id = ctx.guild_id().ok_or_else(|| lang::err_not_in_guild())?;
48 if !ctx.data().copyparty.is_own_url(&full_url) { 53 if !ctx.data().copyparty.is_own_url(&full_url) {
49 crate::net::ensure_public_http_url(&full_url).await?; 54 crate::net::ensure_public_http_url(&full_url).await?;
50 } 55 }
@@ -77,7 +82,12 @@ pub async fn dodaj_wiadomość(
77 _ => return Err(lang::err_bad_channel_type()), 82 _ => return Err(lang::err_bad_channel_type()),
78 }; 83 };
79 84
80 let guild_id = ctx.guild_id().ok_or_else(lang::err_not_in_guild)?; 85 if let Err(e) = crate::tasks::parse_cron(&cron_expression) {
86 ctx.say(lang::fmt_cron_invalid(&cron_expression, &e.to_string()))
87 .await?;
88 return Ok(());
89 }
90 let guild_id = ctx.guild_id().ok_or_else(|| lang::err_not_in_guild())?;
81 database::add_cron_job( 91 database::add_cron_job(
82 &ctx.data().db_pool, 92 &ctx.data().db_pool,
83 guild_id.get() as i64, 93 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(
361 audio_filename: Option<&str>, 361 audio_filename: Option<&str>,
362 message: Option<&str>, 362 message: Option<&str>,
363) -> Result<i64> { 363) -> Result<i64> {
364 // Find the lowest unused ID to reuse freed slots 364 // Let SQLite allocate the id atomically; a SELECT-then-INSERT scheme
365 let existing: Vec<i64> = sqlx::query_scalar("SELECT id FROM cron_jobs ORDER BY id") 365 // races when two users create jobs at the same time.
366 .fetch_all(pool)
367 .await?;
368
369 let next_id = existing.iter().enumerate().find_map(|(i, &id)| {
370 let expected = (i as i64) + 1;
371 if id != expected {
372 Some(expected)
373 } else {
374 None
375 }
376 }).unwrap_or(existing.len() as i64 + 1);
377
378 let result = sqlx::query( 366 let result = sqlx::query(
379 "INSERT INTO cron_jobs (id, guild_id, cron_expression, type, channel_id, audio_filename, message) VALUES (?, ?, ?, ?, ?, ?, ?)", 367 "INSERT INTO cron_jobs (guild_id, cron_expression, type, channel_id, audio_filename, message) VALUES (?, ?, ?, ?, ?, ?)",
380 ) 368 )
381 .bind(next_id)
382 .bind(guild_id) 369 .bind(guild_id)
383 .bind(cron_expression) 370 .bind(cron_expression)
384 .bind(r#type) 371 .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";
46pub fn fmt_cron_audio_queued(cron: &str) -> String { 46pub fn fmt_cron_audio_queued(cron: &str) -> String {
47 format!("Praca audio zakolejkowana z cron'em `{cron}`") 47 format!("Praca audio zakolejkowana z cron'em `{cron}`")
48} 48}
49pub fn fmt_cron_invalid(cron: &str, err: &str) -> String {
50 format!("Nieprawidłowe wyrażenie cron `{cron}`: {err}")
51}
49pub fn fmt_cron_label_audio(name: &str) -> String { 52pub fn fmt_cron_label_audio(name: &str) -> String {
50 format!("Audio: `{name}`") 53 format!("Audio: `{name}`")
51} 54}
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 {
22 22
23/// Normalize user-friendly cron expressions to the 7-field format the `cron` crate expects. 23/// Normalize user-friendly cron expressions to the 7-field format the `cron` crate expects.
24/// Supports: 5-field (min hour dom month dow) and 6-field (sec min hour dom month dow). 24/// Supports: 5-field (min hour dom month dow) and 6-field (sec min hour dom month dow).
25fn parse_cron(raw: &str) -> Result<Schedule, cron::error::Error> { 25pub(crate) fn parse_cron(raw: &str) -> Result<Schedule, cron::error::Error> {
26 let fields: Vec<&str> = raw.split_whitespace().collect(); 26 let fields: Vec<&str> = raw.split_whitespace().collect();
27 let normalized = match fields.len() { 27 let normalized = match fields.len() {
28 7 => raw.to_string(), 28 7 => raw.to_string(),