aboutsummaryrefslogtreecommitdiffstats
path: root/src/commands
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-09-09 13:42:38 +0200
committersalmonik <salmonik@tuta.io>2026-09-09 13:42:38 +0200
commit25115ac447e5356ca865e625515eb7c1f55b39bc (patch)
treecd8b0b62b8ac68e73168f5c43b430c2740b4329a /src/commands
parent/grajcuj plays video-like extensions too (diff)
downloadsenator-sperma-25115ac447e5356ca865e625515eb7c1f55b39bc.tar.gz
senator-sperma-25115ac447e5356ca865e625515eb7c1f55b39bc.zip
first draft
Diffstat (limited to 'src/commands')
-rw-r--r--src/commands/record.rs102
1 files changed, 102 insertions, 0 deletions
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 @@
1use tracing::warn;
2
3use crate::error::Result;
4use crate::lang;
5use crate::recording;
6
7type Context<'a> = poise::Context<'a, crate::Data, crate::error::Error>;
8
9/// Dump the last (up to 15 min) of voice activity as an mp3 to Copyparty.
10///
11/// The bot continuously records (into a rolling in-memory ring) while it is
12/// connected to a voice channel, if `RECORDING_ENABLED=true`. This command is a
13/// read-only snapshot — the buffer keeps recording afterwards. Only callable by
14/// someone in the same voice channel as the bot.
15#[poise::command(slash_command)]
16pub async fn record(ctx: Context<'_>) -> Result<()> {
17 ctx.defer().await?;
18 let data = ctx.data();
19
20 if !data.voice_manager.recording_enabled() {
21 ctx.say(lang::L_RECORD_DISABLED).await?;
22 return Ok(());
23 }
24
25 let guild_id = ctx.guild_id().ok_or_else(|| lang::err_not_in_guild())?;
26
27 // ── Same-VC gate (mirrors /join's voice-state check) ───────────────
28 let caller_channel = ctx.serenity_context().cache.guild(guild_id).and_then(|g| {
29 g.voice_states
30 .get(&ctx.author().id)
31 .and_then(|vs| vs.channel_id)
32 });
33 let caller_channel = match caller_channel {
34 Some(c) => c,
35 None => {
36 ctx.say(lang::L_RECORD_NOT_IN_VOICE).await?;
37 return Ok(());
38 }
39 };
40
41 let bot_channel = match data.voice_manager.get_call(guild_id) {
42 Some(call) => call.lock().await.current_channel(),
43 None => None,
44 };
45
46 match bot_channel {
47 None => {
48 ctx.say(lang::L_RECORD_NOT_RECORDING).await?;
49 Ok(())
50 }
51 Some(bc) if bc.0.get() != caller_channel.get() => {
52 ctx.say(lang::L_RECORD_NOT_SAME_VC).await?;
53 Ok(())
54 }
55 Some(_) => record_impl(ctx).await,
56 }
57}
58
59async fn record_impl(ctx: Context<'_>) -> Result<()> {
60 let data = ctx.data();
61
62 let Some(rec) = data.voice_manager.get_recorder().await else {
63 ctx.say(lang::L_RECORD_NOT_RECORDING).await?;
64 return Ok(());
65 };
66
67 let pcm = rec.flush();
68 let sample_rate = rec.sample_rate();
69 if pcm.is_empty() {
70 ctx.say(lang::L_RECORD_NOTHING).await?;
71 return Ok(());
72 }
73
74 let mp3 = match recording::encode_mp3(&pcm, sample_rate).await {
75 Ok(b) => b,
76 Err(e) => {
77 warn!("recording mp3 encode failed: {e}");
78 ctx.say(lang::L_RECORD_ENCODE_FAILED).await?;
79 return Ok(());
80 }
81 };
82
83 let filename = format!(
84 "nagranie-{}.mp3",
85 chrono::Local::now().format("%Y-%m-%d_%H-%M-%S")
86 );
87
88 match data.copyparty.upload(&filename, mp3).await {
89 Ok(url) => {
90 ctx.say(lang::fmt_record_uploaded(&url)).await?;
91 }
92 Err(e) => {
93 warn!("recording upload failed: {e}");
94 ctx.say(lang::L_RECORD_UPLOAD_FAILED).await?;
95 }
96 }
97 Ok(())
98}
99
100pub fn commands() -> Vec<poise::Command<crate::Data, crate::error::Error>> {
101 vec![record()]
102}