feat(ticket): orphan reconcile on boot, presence watching N, slowmode 5s anti-spam
This commit is contained in:
parent
2a7ace8c09
commit
bd54f9be86
4 changed files with 57 additions and 1 deletions
|
|
@ -21,6 +21,8 @@ impl EventHandler for Handler {
|
||||||
let http = ctx.http.clone();
|
let http = ctx.http.clone();
|
||||||
let http2 = ctx.http.clone();
|
let http2 = ctx.http.clone();
|
||||||
let gid_clone = self.guild_id;
|
let gid_clone = self.guild_id;
|
||||||
|
let pool_clone = self.pool.clone();
|
||||||
|
let ctx_clone = ctx.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
// SAFETY: startup delay 2s - not in command path, lets cache fill, no jitter needed (one-shot on boot)
|
// SAFETY: startup delay 2s - not in command path, lets cache fill, no jitter needed (one-shot on boot)
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||||
|
|
@ -49,6 +51,47 @@ impl EventHandler for Handler {
|
||||||
warn!(error=%e, "ensure_forum_channel failed");
|
warn!(error=%e, "ensure_forum_channel failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Orphan reconciliation: purge DB rows whose channel/thread no longer exists
|
||||||
|
let tickets =
|
||||||
|
crate::feature::ticket::service::count::list_tickets(&pool_clone).await;
|
||||||
|
let mut purged = 0;
|
||||||
|
for (cid_str, gid_str, opener) in tickets {
|
||||||
|
if gid_str != gid.to_string() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Ok(cid) = cid_str.parse::<u64>() {
|
||||||
|
let cid = serenity::all::ChannelId::new(cid);
|
||||||
|
if http2.get_channel(cid).await.is_err() {
|
||||||
|
let _ = sqlx::query("DELETE FROM tickets WHERE channel_id=?")
|
||||||
|
.bind(&cid_str)
|
||||||
|
.execute(&pool_clone)
|
||||||
|
.await;
|
||||||
|
let _ = sqlx::query("DELETE FROM ticket_claims WHERE channel_id=?")
|
||||||
|
.bind(&cid_str)
|
||||||
|
.execute(&pool_clone)
|
||||||
|
.await;
|
||||||
|
purged += 1;
|
||||||
|
warn!(channel=%cid_str, opener=%opener, "orphan ticket purged");
|
||||||
|
// Log to ticket-logs without transcript (manually deleted)
|
||||||
|
let _ = serenity::all::ChannelId::new(1509503717127356446)
|
||||||
|
.send_message(&http2, serenity::all::CreateMessage::new().embed(serenity::all::CreateEmbed::new().title("⚠️ Ticket Manually Deleted").description(format!("Ticket <#{cid_str}> (opener <@{opener}>) manually deleted — transcript unavailable.")).color(0x00ED_4245)))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if purged > 0 {
|
||||||
|
info!(purged, "orphan tickets reconciled");
|
||||||
|
}
|
||||||
|
// Presence: watching N tickets
|
||||||
|
let count =
|
||||||
|
crate::feature::ticket::service::count::count_tickets(&pool_clone, gid).await;
|
||||||
|
ctx_clone.set_presence(
|
||||||
|
Some(serenity::all::ActivityData::custom(format!(
|
||||||
|
"watching {} tickets",
|
||||||
|
count
|
||||||
|
))),
|
||||||
|
serenity::all::OnlineStatus::Online,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if let Some(gid) = self.guild_id {
|
if let Some(gid) = self.guild_id {
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,8 @@ pub async fn create_ticket_channel(
|
||||||
CreateChannel::new(&name)
|
CreateChannel::new(&name)
|
||||||
.kind(ChannelType::Text)
|
.kind(ChannelType::Text)
|
||||||
.category(ChannelId::new(SUPPORT_CATEGORY))
|
.category(ChannelId::new(SUPPORT_CATEGORY))
|
||||||
.permissions(overwrites),
|
.permissions(overwrites)
|
||||||
|
.rate_limit_per_user(5),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
if let Some(pool) = service::pool() {
|
if let Some(pool) = service::pool() {
|
||||||
|
|
|
||||||
11
src/feature/ticket/service/count.rs
Normal file
11
src/feature/ticket/service/count.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub async fn list_tickets(p: &SqlitePool) -> Vec<(String, String, String)> {
|
||||||
|
let _ = sqlx::query("CREATE TABLE IF NOT EXISTS tickets (channel_id TEXT PRIMARY KEY, guild_id TEXT NOT NULL, opener_id TEXT NOT NULL)").execute(p).await;
|
||||||
|
sqlx::query_as::<_, (String, String, String)>("SELECT channel_id, guild_id, opener_id FROM tickets").fetch_all(p).await.unwrap_or_default()
|
||||||
|
}
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub async fn count_tickets(p: &SqlitePool, g: u64) -> i64 {
|
||||||
|
let _ = sqlx::query("CREATE TABLE IF NOT EXISTS tickets (channel_id TEXT PRIMARY KEY, guild_id TEXT NOT NULL, opener_id TEXT NOT NULL)").execute(p).await;
|
||||||
|
sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM tickets WHERE guild_id=?").bind(g.to_string()).fetch_one(p).await.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
//! Ticket service
|
//! Ticket service
|
||||||
|
pub mod count;
|
||||||
pub mod forum;
|
pub mod forum;
|
||||||
use serenity::all::{ChannelId, CreateAttachment, CreateMessage, Http, MessageId};
|
use serenity::all::{ChannelId, CreateAttachment, CreateMessage, Http, MessageId};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue