diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-04 01:31:45 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-04 01:31:45 +0100 |
| commit | a8c1d47132d4c47863ae7dc5b6f4816d024d3de4 (patch) | |
| tree | 505211909c9caa36ceb3cba2050e4d10cb65598d | |
| parent | music: reject live streams on the yt-dlp path (diff) | |
| download | senator-sperma-a8c1d47132d4c47863ae7dc5b6f4816d024d3de4.tar.gz senator-sperma-a8c1d47132d4c47863ae7dc5b6f4816d024d3de4.zip | |
cron: validate expressions and allocate ids atomically
| -rw-r--r-- | src/commands/cron.rs | 14 | ||||
| -rw-r--r-- | src/database/mod.rs | 19 | ||||
| -rw-r--r-- | src/lang.rs | 3 | ||||
| -rw-r--r-- | 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( | |||
| 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"; | |||
| 46 | pub fn fmt_cron_audio_queued(cron: &str) -> String { | 46 | pub 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 | } |
| 49 | pub fn fmt_cron_invalid(cron: &str, err: &str) -> String { | ||
| 50 | format!("Nieprawidłowe wyrażenie cron `{cron}`: {err}") | ||
| 51 | } | ||
| 49 | pub fn fmt_cron_label_audio(name: &str) -> String { | 52 | pub 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). |
| 25 | fn parse_cron(raw: &str) -> Result<Schedule, cron::error::Error> { | 25 | pub(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(), |
