aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-07-31 01:09:07 +0200
committersalmonik <salmonik@tuta.io>2026-07-31 01:09:07 +0200
commit3f1a4d113e71fc412ec05a319d5ffd5a3d7a78bc (patch)
treec0eefa2d0a04c6dab74b471536fe31e7f4d4b559
parentfix: auto completion timing out before ready + fix preexisting warns (diff)
downloadsenator-sperma-3f1a4d113e71fc412ec05a319d5ffd5a3d7a78bc.tar.gz
senator-sperma-3f1a4d113e71fc412ec05a319d5ffd5a3d7a78bc.zip
fir auto completion and send file in the response body upon grajcuj file
Diffstat (limited to '')
-rw-r--r--Cargo.lock2
-rw-r--r--Cargo.toml2
-rw-r--r--src/commands/music.rs36
-rw-r--r--src/events/mod.rs29
-rw-r--r--src/main.rs2
-rw-r--r--src/voice/mod.rs20
6 files changed, 69 insertions, 22 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 5f45947..8bc2ab8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4391,7 +4391,7 @@ dependencies = [
4391 4391
4392[[package]] 4392[[package]]
4393name = "senator-sperma" 4393name = "senator-sperma"
4394version = "0.2.3" 4394version = "0.2.5"
4395dependencies = [ 4395dependencies = [
4396 "chrono", 4396 "chrono",
4397 "cron", 4397 "cron",
diff --git a/Cargo.toml b/Cargo.toml
index a264129..07cb32a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
1[package] 1[package]
2name = "senator-sperma" 2name = "senator-sperma"
3version = "0.2.3" 3version = "0.2.5"
4edition = "2024" 4edition = "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.
418async 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
415pub fn commands() -> Vec<poise::Command<Data, Error>> { 447pub 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
27impl VoiceManager { 27impl 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 }