From 2a7ace8c0924c8f8349ff7dead8e044563e70ed1 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Mon, 21 Sep 2026 21:40:29 +0200 Subject: [PATCH] feat(ticket): cooldown 5m after close (DB ticket_cooldowns + check on create) --- src/db/migration.rs | 16 ++++++++-- src/feature/ticket/handler/claim.rs | 10 ++++++ src/feature/ticket/handler/interaction.rs | 14 +++++++++ src/feature/ticket/service/mod.rs | 37 +++++++++++++++++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/db/migration.rs b/src/db/migration.rs index 8f948fb..1a8882d 100644 --- a/src/db/migration.rs +++ b/src/db/migration.rs @@ -12,8 +12,12 @@ pub async fn run(pool: &SqlitePool) -> anyhow::Result<()> { .execute(pool) .await?; - let migrations = - [("V1__initial", V1_INITIAL), ("V2__tickets", V2_TICKETS), ("V3__reminders", V3_REMINDERS)]; + let migrations = [ + ("V1__initial", V1_INITIAL), + ("V2__tickets", V2_TICKETS), + ("V3__reminders", V3_REMINDERS), + ("V4__ticket_cooldown", V4_COOLDOWN), + ]; for (name, sql) in migrations { let exists: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM _migrations WHERE name = ?") .bind(name) @@ -76,3 +80,11 @@ CREATE TABLE IF NOT EXISTS reminders ( sent INTEGER NOT NULL DEFAULT 0 ); "; +const V4_COOLDOWN: &str = " +CREATE TABLE IF NOT EXISTS ticket_cooldowns ( + user_id TEXT NOT NULL, + guild_id TEXT NOT NULL, + expires_at INTEGER NOT NULL, + PRIMARY KEY (user_id, guild_id) +); +"; diff --git a/src/feature/ticket/handler/claim.rs b/src/feature/ticket/handler/claim.rs index f9c8697..c0eaa1a 100644 --- a/src/feature/ticket/handler/claim.rs +++ b/src/feature/ticket/handler/claim.rs @@ -146,8 +146,18 @@ pub async fn handle_close_modal(ctx: Context, interaction: serenity::all::ModalI haste_url.clone(), ) .await; + let opener_for_cooldown = if let Some(pool) = service::pool() { + service::get_opener(pool, channel_id.get()).await + } else { + None + }; if let Some(pool) = service::pool() { service::cleanup_ticket(pool, channel_id.get()).await; + if let Some(opener) = opener_for_cooldown { + if let Some(gid) = interaction.guild_id { + service::set_cooldown(pool, gid.get(), opener, 300).await; + } + } } let control_row = serenity::all::CreateActionRow::Buttons(vec![ serenity::all::CreateButton::new("ticket_reopen") diff --git a/src/feature/ticket/handler/interaction.rs b/src/feature/ticket/handler/interaction.rs index 7352d22..9409ffb 100644 --- a/src/feature/ticket/handler/interaction.rs +++ b/src/feature/ticket/handler/interaction.rs @@ -136,6 +136,20 @@ pub async fn on_modal(ctx: Context, interaction: ModalInteraction) { .await; return; } + if let Some(rem) = service::is_on_cooldown(pool, guild_id.get(), user.id.get()).await { + let _ = interaction + .create_followup( + &ctx.http, + serenity::all::CreateInteractionResponseFollowup::new() + .content(format!( + "Cooldown: wait {}s before new ticket (5m after close).", + rem + )) + .ephemeral(true), + ) + .await; + return; + } } if let Ok(channels) = guild_id.channels(&ctx.http).await { let exists = channels.values().any(|c| { diff --git a/src/feature/ticket/service/mod.rs b/src/feature/ticket/service/mod.rs index e91a993..82942f4 100644 --- a/src/feature/ticket/service/mod.rs +++ b/src/feature/ticket/service/mod.rs @@ -65,6 +65,43 @@ pub async fn cleanup_ticket(p: &SqlitePool, c: u64) { let _ = sqlx::query("DELETE FROM tickets WHERE channel_id=?").bind(c.to_string()).execute(p).await; let _ = sqlx::query("DELETE FROM ticket_claims WHERE channel_id=?").bind(c.to_string()).execute(p).await; } +pub async fn is_on_cooldown(p: &SqlitePool, g: u64, u: u64) -> Option { + let _ = sqlx::query("CREATE TABLE IF NOT EXISTS ticket_cooldowns (user_id TEXT NOT NULL, guild_id TEXT NOT NULL, expires_at INTEGER NOT NULL, PRIMARY KEY (user_id, guild_id))").execute(p).await; + let row: Option<(i64,)> = + sqlx::query_as("SELECT expires_at FROM ticket_cooldowns WHERE user_id=? AND guild_id=?") + .bind(u.to_string()) + .bind(g.to_string()) + .fetch_optional(p) + .await + .unwrap_or(None); + if let Some((exp,)) = row { + let now = + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() + as i64; + if exp > now { + return Some((exp - now) as u64); + } + let _ = sqlx::query("DELETE FROM ticket_cooldowns WHERE user_id=? AND guild_id=?") + .bind(u.to_string()) + .bind(g.to_string()) + .execute(p) + .await; + } + None +} +pub async fn set_cooldown(p: &SqlitePool, g: u64, u: u64, secs: u64) { + let _ = sqlx::query("CREATE TABLE IF NOT EXISTS ticket_cooldowns (user_id TEXT NOT NULL, guild_id TEXT NOT NULL, expires_at INTEGER NOT NULL, PRIMARY KEY (user_id, guild_id))").execute(p).await; + let exp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() + + secs; + let _ = sqlx::query( + "INSERT OR REPLACE INTO ticket_cooldowns(user_id, guild_id, expires_at) VALUES(?,?,?)", + ) + .bind(u.to_string()) + .bind(g.to_string()) + .bind(exp as i64) + .execute(p) + .await; +} pub async fn add_user_to_ticket( http: &Http, channel_id: ChannelId,