From 25115ac447e5356ca865e625515eb7c1f55b39bc Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 9 Sep 2026 13:42:38 +0200 Subject: first draft --- src/commands/record.rs | 102 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/commands/record.rs (limited to 'src/commands/record.rs') diff --git a/src/commands/record.rs b/src/commands/record.rs new file mode 100644 index 0000000..6f2cfcd --- /dev/null +++ b/src/commands/record.rs @@ -0,0 +1,102 @@ +use tracing::warn; + +use crate::error::Result; +use crate::lang; +use crate::recording; + +type Context<'a> = poise::Context<'a, crate::Data, crate::error::Error>; + +/// Dump the last (up to 15 min) of voice activity as an mp3 to Copyparty. +/// +/// The bot continuously records (into a rolling in-memory ring) while it is +/// connected to a voice channel, if `RECORDING_ENABLED=true`. This command is a +/// read-only snapshot — the buffer keeps recording afterwards. Only callable by +/// someone in the same voice channel as the bot. +#[poise::command(slash_command)] +pub async fn record(ctx: Context<'_>) -> Result<()> { + ctx.defer().await?; + let data = ctx.data(); + + if !data.voice_manager.recording_enabled() { + ctx.say(lang::L_RECORD_DISABLED).await?; + return Ok(()); + } + + let guild_id = ctx.guild_id().ok_or_else(|| lang::err_not_in_guild())?; + + // ── Same-VC gate (mirrors /join's voice-state check) ─────────────── + let caller_channel = ctx.serenity_context().cache.guild(guild_id).and_then(|g| { + g.voice_states + .get(&ctx.author().id) + .and_then(|vs| vs.channel_id) + }); + let caller_channel = match caller_channel { + Some(c) => c, + None => { + ctx.say(lang::L_RECORD_NOT_IN_VOICE).await?; + return Ok(()); + } + }; + + let bot_channel = match data.voice_manager.get_call(guild_id) { + Some(call) => call.lock().await.current_channel(), + None => None, + }; + + match bot_channel { + None => { + ctx.say(lang::L_RECORD_NOT_RECORDING).await?; + Ok(()) + } + Some(bc) if bc.0.get() != caller_channel.get() => { + ctx.say(lang::L_RECORD_NOT_SAME_VC).await?; + Ok(()) + } + Some(_) => record_impl(ctx).await, + } +} + +async fn record_impl(ctx: Context<'_>) -> Result<()> { + let data = ctx.data(); + + let Some(rec) = data.voice_manager.get_recorder().await else { + ctx.say(lang::L_RECORD_NOT_RECORDING).await?; + return Ok(()); + }; + + let pcm = rec.flush(); + let sample_rate = rec.sample_rate(); + if pcm.is_empty() { + ctx.say(lang::L_RECORD_NOTHING).await?; + return Ok(()); + } + + let mp3 = match recording::encode_mp3(&pcm, sample_rate).await { + Ok(b) => b, + Err(e) => { + warn!("recording mp3 encode failed: {e}"); + ctx.say(lang::L_RECORD_ENCODE_FAILED).await?; + return Ok(()); + } + }; + + let filename = format!( + "nagranie-{}.mp3", + chrono::Local::now().format("%Y-%m-%d_%H-%M-%S") + ); + + match data.copyparty.upload(&filename, mp3).await { + Ok(url) => { + ctx.say(lang::fmt_record_uploaded(&url)).await?; + } + Err(e) => { + warn!("recording upload failed: {e}"); + ctx.say(lang::L_RECORD_UPLOAD_FAILED).await?; + } + } + Ok(()) +} + +pub fn commands() -> Vec> { + vec![record()] +} -- cgit v1.2.3