use poise::serenity_prelude as serenity; 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, guild_only)] 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, guild_id).await, } } async fn record_impl(ctx: Context<'_>, guild_id: serenity::GuildId) -> Result<()> { let data = ctx.data(); let Some(rec) = data.voice_manager.get_recorder(guild_id).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()] }