diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-31 01:09:07 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-31 01:09:07 +0200 |
| commit | 3f1a4d113e71fc412ec05a319d5ffd5a3d7a78bc (patch) | |
| tree | c0eefa2d0a04c6dab74b471536fe31e7f4d4b559 | |
| parent | fix: auto completion timing out before ready + fix preexisting warns (diff) | |
| download | senator-sperma-3f1a4d113e71fc412ec05a319d5ffd5a3d7a78bc.tar.gz senator-sperma-3f1a4d113e71fc412ec05a319d5ffd5a3d7a78bc.zip | |
fir auto completion and send file in the response body upon grajcuj file
| -rw-r--r-- | Cargo.lock | 2 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | src/commands/music.rs | 36 | ||||
| -rw-r--r-- | src/events/mod.rs | 29 | ||||
| -rw-r--r-- | src/main.rs | 2 | ||||
| -rw-r--r-- | src/voice/mod.rs | 20 |
6 files changed, 69 insertions, 22 deletions
| @@ -4391,7 +4391,7 @@ dependencies = [ | |||
| 4391 | 4391 | ||
| 4392 | [[package]] | 4392 | [[package]] |
| 4393 | name = "senator-sperma" | 4393 | name = "senator-sperma" |
| 4394 | version = "0.2.3" | 4394 | version = "0.2.5" |
| 4395 | dependencies = [ | 4395 | dependencies = [ |
| 4396 | "chrono", | 4396 | "chrono", |
| 4397 | "cron", | 4397 | "cron", |
| @@ -1,6 +1,6 @@ | |||
| 1 | [package] | 1 | [package] |
| 2 | name = "senator-sperma" | 2 | name = "senator-sperma" |
| 3 | version = "0.2.3" | 3 | version = "0.2.5" |
| 4 | edition = "2024" | 4 | edition = "2024" |
| 5 | 5 | ||
| 6 | [profile.dev] | 6 | [profile.dev] |
diff --git a/src/commands/music.rs b/src/commands/music.rs index 05abb9c..6acac00 100644 --- a/src/commands/music.rs +++ b/src/commands/music.rs | |||
| @@ -253,6 +253,7 @@ async fn play_impl( | |||
| 253 | }) | 253 | }) |
| 254 | .ok_or_else(lang::err_not_in_voice)?; | 254 | .ok_or_else(lang::err_not_in_voice)?; |
| 255 | 255 | ||
| 256 | let file_reupload = file.as_ref().map(|f| (f.url.clone(), f.filename.clone())); | ||
| 256 | let src = resolve_source(ctx, melodia, file).await?; | 257 | let src = resolve_source(ctx, melodia, file).await?; |
| 257 | 258 | ||
| 258 | let call; | 259 | let call; |
| @@ -296,7 +297,7 @@ async fn play_impl( | |||
| 296 | .lock() | 297 | .lock() |
| 297 | .await | 298 | .await |
| 298 | .insert(guild_id.get(), handle); | 299 | .insert(guild_id.get(), handle); |
| 299 | ctx.say(lang::fmt_music_playing(&src.display_name)).await?; | 300 | send_music_msg(ctx, &src.display_name, file_reupload).await?; |
| 300 | return Ok(()); | 301 | return Ok(()); |
| 301 | } | 302 | } |
| 302 | info!(url = %redact_url(&src.url), "downloading for compression"); | 303 | info!(url = %redact_url(&src.url), "downloading for compression"); |
| @@ -365,7 +366,7 @@ async fn play_impl( | |||
| 365 | .await | 366 | .await |
| 366 | .insert(guild_id.get(), handle); | 367 | .insert(guild_id.get(), handle); |
| 367 | 368 | ||
| 368 | ctx.say(lang::fmt_music_playing(&src.display_name)).await?; | 369 | send_music_msg(ctx, &src.display_name, file_reupload).await?; |
| 369 | Ok(()) | 370 | Ok(()) |
| 370 | } | 371 | } |
| 371 | 372 | ||
| @@ -412,6 +413,37 @@ pub async fn join(ctx: Context<'_>) -> Result<()> { | |||
| 412 | Ok(()) | 413 | Ok(()) |
| 413 | } | 414 | } |
| 414 | 415 | ||
| 416 | /// Send the "playing" message, optionally reuploading a user-provided file | ||
| 417 | /// so other channel members can download it. | ||
| 418 | async fn send_music_msg( | ||
| 419 | ctx: Context<'_>, | ||
| 420 | display_name: &str, | ||
| 421 | file_reupload: Option<(String, String)>, | ||
| 422 | ) -> Result<()> { | ||
| 423 | if let Some((url, filename)) = file_reupload { | ||
| 424 | match ctx.data().http_client.get(&url).send().await { | ||
| 425 | Ok(resp) => match resp.bytes().await { | ||
| 426 | Ok(bytes) => { | ||
| 427 | if ctx | ||
| 428 | .send(poise::CreateReply::default().content(lang::fmt_music_playing(display_name)).attachment( | ||
| 429 | serenity::CreateAttachment::bytes(bytes.to_vec(), filename.clone()), | ||
| 430 | )) | ||
| 431 | .await | ||
| 432 | .is_ok() | ||
| 433 | { | ||
| 434 | return Ok(()); | ||
| 435 | } | ||
| 436 | error!("send_music_msg: ctx.send with attachment failed, falling back"); | ||
| 437 | } | ||
| 438 | Err(e) => error!("send_music_msg: failed to read file bytes: {e}"), | ||
| 439 | }, | ||
| 440 | Err(e) => error!("send_music_msg: failed to download file: {e}"), | ||
| 441 | } | ||
| 442 | } | ||
| 443 | ctx.say(lang::fmt_music_playing(display_name)).await?; | ||
| 444 | Ok(()) | ||
| 445 | } | ||
| 446 | |||
| 415 | pub fn commands() -> Vec<poise::Command<Data, Error>> { | 447 | pub fn commands() -> Vec<poise::Command<Data, Error>> { |
| 416 | vec![grajcuj(), stop(), join()] | 448 | vec![grajcuj(), stop(), join()] |
| 417 | } | 449 | } |
diff --git a/src/events/mod.rs b/src/events/mod.rs index d166993..82b6f19 100644 --- a/src/events/mod.rs +++ b/src/events/mod.rs | |||
| @@ -130,8 +130,7 @@ pub async fn handle_voice_state_update( | |||
| 130 | let mut tts_channel: Option<serenity::ChannelId> = None; | 130 | let mut tts_channel: Option<serenity::ChannelId> = None; |
| 131 | 131 | ||
| 132 | // ── JOIN ── | 132 | // ── JOIN ── |
| 133 | if old_channel.is_none() && new_channel.is_some() { | 133 | if old_channel.is_none() && let Some(cid) = new_channel { |
| 134 | let cid = new_channel.unwrap(); | ||
| 135 | let is_afk = new.self_mute || new.self_deaf; | 134 | let is_afk = new.self_mute || new.self_deaf; |
| 136 | 135 | ||
| 137 | va.join_time = now; | 136 | va.join_time = now; |
| @@ -332,11 +331,18 @@ pub async fn handle_message( | |||
| 332 | let has_keyword = content_lower.contains(&data.config.assistant_keyword); | 331 | let has_keyword = content_lower.contains(&data.config.assistant_keyword); |
| 333 | 332 | ||
| 334 | if is_mention || has_keyword { | 333 | if is_mention || has_keyword { |
| 334 | // Spawn the LLM call in a background task so it doesn't block the | ||
| 335 | // event loop. Discord interactions must be answered within 3 s; a | ||
| 336 | // 5–10 s OpenRouter call would make every subsequent interaction | ||
| 337 | // expire with "Unknown interaction". | ||
| 338 | let http = ctx.http.clone(); | ||
| 339 | let channel_id = new_message.channel_id; | ||
| 340 | let _reply_to_id = new_message.id; | ||
| 341 | let bot_id = ctx.cache.current_user().id; | ||
| 335 | let (api_url, api_key, model) = data.config.resolve_llm(); | 342 | let (api_url, api_key, model) = data.config.resolve_llm(); |
| 336 | let client = crate::openrouter::OpenRouterClient::new(api_url, api_key, model); | 343 | let assistant_prompt = lang::ASSISTANT_SYSTEM_PROMPT.to_string(); |
| 337 | let assistant_prompt = lang::ASSISTANT_SYSTEM_PROMPT; | ||
| 338 | 344 | ||
| 339 | let bot_id = ctx.cache.current_user().id; | 345 | // Build message history synchronously (fast path). |
| 340 | let mut messages = Vec::new(); | 346 | let mut messages = Vec::new(); |
| 341 | let mut current_msg = Some(new_message.clone()); | 347 | let mut current_msg = Some(new_message.clone()); |
| 342 | for _ in 0..5 { | 348 | for _ in 0..5 { |
| @@ -346,9 +352,9 @@ pub async fn handle_message( | |||
| 346 | } else { | 352 | } else { |
| 347 | "user" | 353 | "user" |
| 348 | }; | 354 | }; |
| 349 | messages.push((role.into(), msg.content.clone())); | 355 | messages.push((role.to_string(), msg.content.clone())); |
| 350 | current_msg = match msg.message_reference.and_then(|r| r.message_id) { | 356 | current_msg = match msg.message_reference.and_then(|r| r.message_id) { |
| 351 | Some(ref_id) => msg.channel_id.message(ctx, ref_id).await.ok(), | 357 | Some(ref_id) => channel_id.message(&http, ref_id).await.ok(), |
| 352 | None => None, | 358 | None => None, |
| 353 | }; | 359 | }; |
| 354 | } else { | 360 | } else { |
| @@ -357,9 +363,12 @@ pub async fn handle_message( | |||
| 357 | } | 363 | } |
| 358 | messages.reverse(); | 364 | messages.reverse(); |
| 359 | 365 | ||
| 360 | if let Ok(response) = client.chat_with_history(assistant_prompt, messages).await { | 366 | tokio::spawn(async move { |
| 361 | new_message.reply(ctx, response).await.ok(); | 367 | let client = crate::openrouter::OpenRouterClient::new(api_url, api_key, model); |
| 362 | } | 368 | if let Ok(response) = client.chat_with_history(&assistant_prompt, messages).await { |
| 369 | let _ = channel_id.say(&http, response).await; | ||
| 370 | } | ||
| 371 | }); | ||
| 363 | } | 372 | } |
| 364 | 373 | ||
| 365 | Ok(()) | 374 | Ok(()) |
diff --git a/src/main.rs b/src/main.rs index 7874c86..d06176d 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -159,7 +159,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { | |||
| 159 | let copyparty = copyparty.clone(); | 159 | let copyparty = copyparty.clone(); |
| 160 | let triggers = triggers.clone(); | 160 | let triggers = triggers.clone(); |
| 161 | Box::pin(async move { | 161 | Box::pin(async move { |
| 162 | let vm = voice::VoiceManager::new(ctx).await; | 162 | let vm = voice::VoiceManager::new(ctx).await?; |
| 163 | 163 | ||
| 164 | let data = Data { | 164 | let data = Data { |
| 165 | config, | 165 | config, |
diff --git a/src/voice/mod.rs b/src/voice/mod.rs index 0d5b89e..4087eba 100644 --- a/src/voice/mod.rs +++ b/src/voice/mod.rs | |||
| @@ -25,12 +25,13 @@ pub struct VoiceManager { | |||
| 25 | } | 25 | } |
| 26 | 26 | ||
| 27 | impl VoiceManager { | 27 | impl VoiceManager { |
| 28 | pub async fn new(ctx: &serenity::client::Context) -> Self { | 28 | pub async fn new(ctx: &serenity::client::Context) -> Result<Self> { |
| 29 | Self { | 29 | let songbird = songbird::get(ctx) |
| 30 | songbird: songbird::get(ctx) | 30 | .await |
| 31 | .await | 31 | .ok_or_else(|| Error::Custom( |
| 32 | .expect("Songbird not registered via register_songbird()"), | 32 | "Songbird not registered via register_songbird()".into(), |
| 33 | } | 33 | ))?; |
| 34 | Ok(Self { songbird }) | ||
| 34 | } | 35 | } |
| 35 | 36 | ||
| 36 | pub async fn connect( | 37 | pub async fn connect( |
| @@ -54,7 +55,12 @@ impl VoiceManager { | |||
| 54 | warn!("Driver error details: {driver_err:?}"); | 55 | warn!("Driver error details: {driver_err:?}"); |
| 55 | } | 56 | } |
| 56 | last_err = Some(e); | 57 | last_err = Some(e); |
| 57 | tokio::time::sleep(Duration::from_secs(1)).await; | 58 | |
| 59 | // Stale voice state: remove the guild from songbird and | ||
| 60 | // wait a few seconds so Discord's voice gateway can fully | ||
| 61 | // process the disconnect before we retry. | ||
| 62 | let _ = self.songbird.remove(guild_id).await; | ||
| 63 | tokio::time::sleep(Duration::from_secs(3)).await; | ||
| 58 | } | 64 | } |
| 59 | } | 65 | } |
| 60 | } | 66 | } |
