aboutsummaryrefslogtreecommitdiffstats
path: root/src/commands/record.rs
blob: 6f2cfcdd081726354e2fce2656004b2d084598b9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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<poise::Command<crate::Data, crate::error::Error>> {
    vec![record()]
}