diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-04 01:09:53 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-04 01:09:53 +0100 |
| commit | 76c1824bd6f0dcb157544862652b65af8f6539ce (patch) | |
| tree | 1b89a5394d2cfb207d3fe37ae6d40d4a86dfd04a | |
| parent | triggers: scope groups to the guild that created them (diff) | |
| download | senator-sperma-76c1824bd6f0dcb157544862652b65af8f6539ce.tar.gz senator-sperma-76c1824bd6f0dcb157544862652b65af8f6539ce.zip | |
recording: keep one buffer per guild
| -rw-r--r-- | src/commands/record.rs | 7 | ||||
| -rw-r--r-- | src/recording/mod.rs | 21 | ||||
| -rw-r--r-- | src/voice/mod.rs | 47 |
3 files changed, 41 insertions, 34 deletions
diff --git a/src/commands/record.rs b/src/commands/record.rs index 48cbc59..dfe736d 100644 --- a/src/commands/record.rs +++ b/src/commands/record.rs | |||
| @@ -1,3 +1,4 @@ | |||
| 1 | use poise::serenity_prelude as serenity; | ||
| 1 | use tracing::warn; | 2 | use tracing::warn; |
| 2 | 3 | ||
| 3 | use crate::error::Result; | 4 | use crate::error::Result; |
| @@ -52,14 +53,14 @@ pub async fn record(ctx: Context<'_>) -> Result<()> { | |||
| 52 | ctx.say(lang::L_RECORD_NOT_SAME_VC).await?; | 53 | ctx.say(lang::L_RECORD_NOT_SAME_VC).await?; |
| 53 | Ok(()) | 54 | Ok(()) |
| 54 | } | 55 | } |
| 55 | Some(_) => record_impl(ctx).await, | 56 | Some(_) => record_impl(ctx, guild_id).await, |
| 56 | } | 57 | } |
| 57 | } | 58 | } |
| 58 | 59 | ||
| 59 | async fn record_impl(ctx: Context<'_>) -> Result<()> { | 60 | async fn record_impl(ctx: Context<'_>, guild_id: serenity::GuildId) -> Result<()> { |
| 60 | let data = ctx.data(); | 61 | let data = ctx.data(); |
| 61 | 62 | ||
| 62 | let Some(rec) = data.voice_manager.get_recorder().await else { | 63 | let Some(rec) = data.voice_manager.get_recorder(guild_id).await else { |
| 63 | ctx.say(lang::L_RECORD_NOT_RECORDING).await?; | 64 | ctx.say(lang::L_RECORD_NOT_RECORDING).await?; |
| 64 | return Ok(()); | 65 | return Ok(()); |
| 65 | }; | 66 | }; |
diff --git a/src/recording/mod.rs b/src/recording/mod.rs index d0a94be..4cd2891 100644 --- a/src/recording/mod.rs +++ b/src/recording/mod.rs | |||
| @@ -304,22 +304,21 @@ pub async fn on_bot_voice_state( | |||
| 304 | if !voice_manager.recording_enabled() { | 304 | if !voice_manager.recording_enabled() { |
| 305 | return; | 305 | return; |
| 306 | } | 306 | } |
| 307 | let Some(guild_id) = guild_id else { | ||
| 308 | return; | ||
| 309 | }; | ||
| 307 | // Already torn down by `VoiceManager::disconnect` in the `/stop` path; this | 310 | // Already torn down by `VoiceManager::disconnect` in the `/stop` path; this |
| 308 | // is the catch-all for leaves `disconnect` didn't initiate (kicks, drops). | 311 | // is the catch-all for leaves `disconnect` didn't initiate (kicks, drops). |
| 309 | if new_channel.is_none() { | 312 | if new_channel.is_none() { |
| 310 | voice_manager.stop_recording().await; | 313 | voice_manager.stop_recording(guild_id).await; |
| 311 | return; | 314 | return; |
| 312 | } | 315 | } |
| 313 | // Joined or moved into a channel. If the recorder is missing (e.g. a move | 316 | // Joined or moved into a channel. If this guild's recorder is missing |
| 314 | // broke the prior Call), try to (re)register on the current Call. | 317 | // (e.g. a move broke the prior Call), try to (re)register on the current |
| 315 | if new_channel != old_channel { | 318 | // Call. |
| 316 | let existing = voice_manager.get_recorder().await.is_some(); | 319 | if new_channel != old_channel && voice_manager.get_recorder(guild_id).await.is_none() { |
| 317 | if !existing { | 320 | if let Some(call) = voice_manager.get_call(guild_id) { |
| 318 | if let Some(gid) = guild_id { | 321 | voice_manager.start_recording(guild_id, &call).await; |
| 319 | if let Some(call) = voice_manager.get_call(gid) { | ||
| 320 | voice_manager.start_recording(&call).await; | ||
| 321 | } | ||
| 322 | } | ||
| 323 | } | 322 | } |
| 324 | } | 323 | } |
| 325 | } | 324 | } |
diff --git a/src/voice/mod.rs b/src/voice/mod.rs index 0b6022c..bbb73a0 100644 --- a/src/voice/mod.rs +++ b/src/voice/mod.rs | |||
| @@ -1,3 +1,4 @@ | |||
| 1 | use std::collections::HashMap; | ||
| 1 | use std::io::Cursor; | 2 | use std::io::Cursor; |
| 2 | use std::pin::Pin; | 3 | use std::pin::Pin; |
| 3 | use std::sync::Arc; | 4 | use std::sync::Arc; |
| @@ -28,11 +29,13 @@ const RECORDING_SAMPLE_RATE: u32 = 48000; | |||
| 28 | #[derive(Clone)] | 29 | #[derive(Clone)] |
| 29 | pub struct VoiceManager { | 30 | pub struct VoiceManager { |
| 30 | pub songbird: Arc<Songbird>, | 31 | pub songbird: Arc<Songbird>, |
| 31 | /// Live recorder for the bot's current voice connection, if any. | 32 | /// Live recorder per guild, if any. |
| 32 | /// | 33 | /// |
| 33 | /// `None` whenever the bot is not connected (or recording is disabled) — | 34 | /// Entries exist only while the bot is connected (and recording) in that |
| 34 | /// so the ~86 MB ring only exists while actually recording a 15 min window. | 35 | /// guild, so the ~86 MB ring only exists while actually recording. Keying |
| 35 | pub recorder: Arc<Mutex<Option<Arc<Recorder>>>>, | 36 | /// by guild prevents one guild's connection from replacing or stopping |
| 37 | /// another guild's buffer. | ||
| 38 | pub recorders: Arc<Mutex<HashMap<GuildId, Arc<Recorder>>>>, | ||
| 36 | pub recording_enabled: bool, | 39 | pub recording_enabled: bool, |
| 37 | pub recording_window_seconds: u64, | 40 | pub recording_window_seconds: u64, |
| 38 | } | 41 | } |
| @@ -50,7 +53,7 @@ impl VoiceManager { | |||
| 50 | ))?; | 53 | ))?; |
| 51 | Ok(Self { | 54 | Ok(Self { |
| 52 | songbird, | 55 | songbird, |
| 53 | recorder: Arc::new(Mutex::new(None)), | 56 | recorders: Arc::new(Mutex::new(HashMap::new())), |
| 54 | recording_enabled, | 57 | recording_enabled, |
| 55 | recording_window_seconds, | 58 | recording_window_seconds, |
| 56 | }) | 59 | }) |
| @@ -60,10 +63,10 @@ impl VoiceManager { | |||
| 60 | self.recording_enabled | 63 | self.recording_enabled |
| 61 | } | 64 | } |
| 62 | 65 | ||
| 63 | /// Construct a `Recorder`, register its `VoiceTick` handler on `call`, and | 66 | /// Construct a `Recorder` for `guild_id`, register its `VoiceTick` handler |
| 64 | /// store the strong `Arc` in the slot. Idempotent: if the slot already | 67 | /// on `call`, and store the strong `Arc`. Replacing an existing entry drops |
| 65 | /// holds a recorder, the caller is responsible for not double-registering. | 68 | /// the previous ring; its handler's `Weak` then no-ops. |
| 66 | pub async fn start_recording(&self, call: &Arc<Mutex<Call>>) { | 69 | pub async fn start_recording(&self, guild_id: GuildId, call: &Arc<Mutex<Call>>) { |
| 67 | if !self.recording_enabled { | 70 | if !self.recording_enabled { |
| 68 | return; | 71 | return; |
| 69 | } | 72 | } |
| @@ -79,25 +82,29 @@ impl VoiceManager { | |||
| 79 | handler, | 82 | handler, |
| 80 | ); | 83 | ); |
| 81 | } | 84 | } |
| 82 | *self.recorder.lock().await = Some(recorder); | 85 | self.recorders |
| 86 | .lock() | ||
| 87 | .await | ||
| 88 | .insert(guild_id, recorder); | ||
| 83 | info!( | 89 | info!( |
| 90 | guild = %guild_id, | ||
| 84 | window_secs = self.recording_window_seconds, | 91 | window_secs = self.recording_window_seconds, |
| 85 | "recording: started rolling buffer for this voice connection" | 92 | "recording: started rolling buffer for this voice connection" |
| 86 | ); | 93 | ); |
| 87 | } | 94 | } |
| 88 | 95 | ||
| 89 | /// Drop the live recorder (frees the ring immediately). The songbird `Call` | 96 | /// Drop this guild's live recorder (frees the ring immediately). The |
| 90 | /// may still exist briefly; the handler's `Weak` upgrades to `None` thereafter. | 97 | /// songbird `Call` may still exist briefly; the handler's `Weak` then no-ops. |
| 91 | pub async fn stop_recording(&self) { | 98 | pub async fn stop_recording(&self, guild_id: GuildId) { |
| 92 | let prev = self.recorder.lock().await.take(); | 99 | let prev = self.recorders.lock().await.remove(&guild_id); |
| 93 | if prev.is_some() { | 100 | if prev.is_some() { |
| 94 | info!("recording: stopped, ring buffer freed"); | 101 | info!(guild = %guild_id, "recording: stopped, ring buffer freed"); |
| 95 | } | 102 | } |
| 96 | } | 103 | } |
| 97 | 104 | ||
| 98 | /// Clone of the live recorder `Arc`, if any — used by `/record` to snapshot. | 105 | /// Clone of this guild's live recorder, if any — used by `/record`. |
| 99 | pub async fn get_recorder(&self) -> Option<Arc<Recorder>> { | 106 | pub async fn get_recorder(&self, guild_id: GuildId) -> Option<Arc<Recorder>> { |
| 100 | self.recorder.lock().await.clone() | 107 | self.recorders.lock().await.get(&guild_id).cloned() |
| 101 | } | 108 | } |
| 102 | 109 | ||
| 103 | pub async fn connect( | 110 | pub async fn connect( |
| @@ -115,7 +122,7 @@ impl VoiceManager { | |||
| 115 | // (The bot's own VoiceStateUpdate also reconciles, but may | 122 | // (The bot's own VoiceStateUpdate also reconciles, but may |
| 116 | // race songbird's Call creation — this never misses.) | 123 | // race songbird's Call creation — this never misses.) |
| 117 | if self.recording_enabled { | 124 | if self.recording_enabled { |
| 118 | self.start_recording(&call).await; | 125 | self.start_recording(guild_id, &call).await; |
| 119 | } | 126 | } |
| 120 | return Ok(call); | 127 | return Ok(call); |
| 121 | } | 128 | } |
| @@ -143,7 +150,7 @@ impl VoiceManager { | |||
| 143 | pub async fn disconnect(&self, guild_id: GuildId) -> Result<()> { | 150 | pub async fn disconnect(&self, guild_id: GuildId) -> Result<()> { |
| 144 | // Free the ring before tearing down the Call — the handler's `Weak` | 151 | // Free the ring before tearing down the Call — the handler's `Weak` |
| 145 | // will then no-op any in-flight ticks until songbird drops it. | 152 | // will then no-op any in-flight ticks until songbird drops it. |
| 146 | self.stop_recording().await; | 153 | self.stop_recording(guild_id).await; |
| 147 | self.songbird | 154 | self.songbird |
| 148 | .remove(guild_id) | 155 | .remove(guild_id) |
| 149 | .await | 156 | .await |
