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()]
}
|