Skip to content

Commit 9861ef5

Browse files
committed
feat:ctd: add inactivity timer
1 parent 9a08ab1 commit 9861ef5

File tree

1 file changed

+166
-17
lines changed

1 file changed

+166
-17
lines changed

src/main.rs

Lines changed: 166 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@ use dotenv::dotenv;
22

33
use poise::serenity_prelude::GatewayIntents;
44
use reqwest::Client as HttpClient;
5-
use serenity::client::Client;
6-
use serenity::model::id::GuildId;
5+
use serenity::async_trait;
6+
use serenity::client::{Client, Context, EventHandler};
7+
use serenity::model::gateway::Ready;
8+
use serenity::model::id::{ChannelId, GuildId};
9+
use serenity::model::voice::VoiceState;
710
use serenity::prelude::TypeMapKey;
11+
use songbird::{Event, EventContext, EventHandler as SongbirdEventHandler, TrackEvent};
12+
// use songbird::id::ChannelId;
13+
814
use songbird::{
915
SerenityInit,
1016
input::{Compose, YoutubeDl},
@@ -18,8 +24,8 @@ use std::time::{Duration, Instant};
1824
use std::{collections::HashMap, env, sync::Arc};
1925
type Error = Box<dyn std::error::Error + Send + Sync>;
2026
type Contx<'a> = poise::Context<'a, Data, Error>;
21-
const INACTIVITY_TIMEOUT: Duration = Duration::from_secs(5 * 60); // 5 minutes
22-
const ALONE_TIMEOUT: Duration = Duration::from_secs(2 * 60); // 2 minutes when alone
27+
const INACTIVITY_TIMEOUT: Duration = Duration::from_secs(300); // 5 minutes
28+
const ALONE_TIMEOUT: Duration = Duration::from_secs(120); // 2 minutes when alone
2329
#[poise::command(prefix_command, slash_command)]
2430
async fn help(
2531
ctx: Contx<'_>,
@@ -64,7 +70,7 @@ impl AutoDisconnectManager {
6470
guild_timer: RwLock::new(HashMap::new()),
6571
}
6672
}
67-
async fn start_timer(&self, ctx: Contx<'_>, guild_id: GuildId, duration: Duration) {
73+
async fn start_timer(&self, ctx: Context, guild_id: GuildId, duration: Duration) {
6874
let mut timers = self.guild_timer.write().await;
6975

7076
//abort if already existing timer
@@ -85,7 +91,7 @@ impl AutoDisconnectManager {
8591

8692
guild_timer.last_activity = Instant::now();
8793

88-
let ctx_ser_clone = ctx.serenity_context().clone();
94+
let ctx_ser_clone = ctx.clone();
8995

9096
// let ctx_clone = ctx.clone();
9197
let task =
@@ -100,13 +106,7 @@ impl AutoDisconnectManager {
100106
if queue_check {
101107
drop(handler);
102108
let _ = manager.remove(guild_id).await;
103-
// ctx_clone
104-
// .say(format!(
105-
// "🔇 Left voice channel due to {} minutes of {}",
106-
// duration.as_secs() / 60,
107-
// "time name"
108-
// ))
109-
// .await;
109+
110110
if let Ok(channels) =
111111
ctx_ser_clone.http.get_channels(guild_id.into()).await
112112
{
@@ -144,14 +144,109 @@ impl AutoDisconnectManager {
144144
}
145145

146146
// Start inactivity timer when queue becomes empty
147-
pub async fn start_inactivity_timer(&self, guild_id: GuildId, ctx: Contx<'_>) {
147+
pub async fn start_inactivity_timer(&self, guild_id: GuildId, ctx: Context) {
148148
self.start_timer(ctx, guild_id, INACTIVITY_TIMEOUT).await;
149149
}
150150

151151
// Start timer when bot is alone in voice channel
152-
pub async fn start_alone_timer(&self, guild_id: GuildId, ctx: Contx<'_>) {
152+
pub async fn start_alone_timer(&self, guild_id: GuildId, ctx: Context) {
153153
self.start_timer(ctx, guild_id, ALONE_TIMEOUT).await;
154154
}
155+
156+
pub async fn cancel_timers(&self, guild_id: GuildId) {
157+
let mut timers = self.guild_timer.write().await;
158+
if let Some(guild_timer) = timers.remove(&guild_id) {
159+
if let Some(task) = guild_timer.timer_task {
160+
task.abort();
161+
}
162+
info!("Cancelled timers for guild {}", guild_id);
163+
}
164+
}
165+
pub async fn check_if_alone(&self, ctx: &Context, guild_id: GuildId) {
166+
if let Some(manager) = songbird::get(ctx).await {
167+
if let Some(handler_lock) = manager.get(guild_id) {
168+
let handler = handler_lock.lock().await;
169+
170+
if let Some(current_channel) = handler.current_channel() {
171+
let channel_id = ChannelId::from(current_channel.0);
172+
173+
// Get current bot's user ID
174+
let bot_user_id = ctx.cache.current_user().id;
175+
176+
// Count non-bot users in the voice channel
177+
let human_count = match ctx.cache.guild(guild_id) {
178+
Some(guild) => {
179+
guild
180+
.voice_states
181+
.values()
182+
.filter(|vs| vs.channel_id == Some(channel_id))
183+
.filter(|vs| vs.user_id != bot_user_id) // Exclude our bot
184+
.filter_map(|vs| guild.members.get(&vs.user_id))
185+
.filter(|member| !member.user.bot) // Exclude all bots
186+
.count()
187+
}
188+
None => {
189+
// If guild not in cache, make HTTP request
190+
info!(
191+
"Guild {} not in cache, cannot determine voice states reliably",
192+
guild_id
193+
);
194+
return;
195+
}
196+
};
197+
198+
if human_count == 0 {
199+
info!(
200+
"Bot is alone in voice channel (found {} humans), starting alone timer",
201+
human_count
202+
);
203+
drop(handler);
204+
self.start_alone_timer(guild_id, ctx.clone()).await;
205+
} else {
206+
info!("Found {} human(s) in voice channel, not alone", human_count);
207+
}
208+
}
209+
}
210+
}
211+
}
212+
213+
pub async fn handle_voice_join(&self, guild_id: GuildId, ctx: &Context) {
214+
// Update activity first (cancels any existing timers)
215+
self.update_activity(guild_id).await;
216+
217+
// Check if alone (starts 2-minute timer if alone)
218+
self.check_if_alone(ctx, guild_id).await;
219+
220+
// If not alone, start inactivity timer (5 minutes) since no music is playing
221+
// Note: check_if_alone will override this with a shorter timer if bot is alone
222+
self.start_inactivity_timer(guild_id, ctx.clone()).await;
223+
}
224+
}
225+
226+
pub struct BotEventHandler {
227+
auto_disconnect: Arc<AutoDisconnectManager>,
228+
}
229+
230+
impl BotEventHandler {
231+
pub fn new(auto_disconnect: Arc<AutoDisconnectManager>) -> Self {
232+
Self { auto_disconnect }
233+
}
234+
}
235+
236+
#[async_trait]
237+
impl EventHandler for BotEventHandler {
238+
async fn ready(&self, _ctx: Context, ready: Ready) {
239+
info!("{} is connected!", ready.user.name);
240+
}
241+
242+
async fn voice_state_update(&self, ctx: Context, _old: Option<VoiceState>, new: VoiceState) {
243+
// Check if someone left and if bot might be alone now
244+
if let Some(guild_id) = new.guild_id {
245+
// Small delay to let voice states update
246+
tokio::time::sleep(Duration::from_millis(500)).await;
247+
self.auto_disconnect.check_if_alone(&ctx, guild_id).await;
248+
}
249+
}
155250
}
156251

157252
fn format_duration(duration: Duration) -> String {
@@ -166,6 +261,35 @@ fn format_duration(duration: Duration) -> String {
166261
}
167262
}
168263

264+
pub struct MusicEventHandler {
265+
pub guild_id: GuildId,
266+
pub auto_disconnect: Arc<AutoDisconnectManager>,
267+
pub ctx: Context,
268+
}
269+
270+
#[async_trait]
271+
impl SongbirdEventHandler for MusicEventHandler {
272+
async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> {
273+
info!("Track ended in guild {}", self.guild_id);
274+
275+
// Check if queue is now empty
276+
if let Some(manager) = songbird::get(&self.ctx).await {
277+
if let Some(handler_lock) = manager.get(self.guild_id) {
278+
let handler = handler_lock.lock().await;
279+
if handler.queue().is_empty() {
280+
info!("Queue is empty after track end, starting inactivity timer");
281+
drop(handler); // Release lock before async call
282+
self.auto_disconnect
283+
.start_inactivity_timer(self.guild_id, self.ctx.clone())
284+
.await;
285+
}
286+
}
287+
}
288+
289+
None
290+
}
291+
}
292+
169293
/// Join your voice channel
170294
#[poise::command(slash_command, guild_only)]
171295
async fn join(ctx: Contx<'_>) -> Result<(), Error> {
@@ -195,7 +319,21 @@ async fn join(ctx: Contx<'_>) -> Result<(), Error> {
195319
Ok(handler_lock) => {
196320
let mut handler = handler_lock.lock().await;
197321
handler.deafen(true).await?;
322+
handler.add_global_event(
323+
Event::Track(TrackEvent::End),
324+
MusicEventHandler {
325+
guild_id,
326+
auto_disconnect: ctx.data().auto_disconnect.clone(),
327+
ctx: ctx.serenity_context().clone(),
328+
},
329+
);
330+
331+
drop(handler); // Release lock
198332

333+
ctx.data()
334+
.auto_disconnect
335+
.handle_voice_join(guild_id, ctx.serenity_context())
336+
.await;
199337
ctx.say(format!("✅ Joined <#{}>!", connect_to)).await?;
200338
}
201339
Err(e) => {
@@ -244,6 +382,15 @@ async fn play(
244382
Ok(handler_lock) => {
245383
let mut handler = handler_lock.lock().await;
246384
handler.deafen(true).await?;
385+
386+
handler.add_global_event(
387+
Event::Track(TrackEvent::End),
388+
MusicEventHandler {
389+
guild_id,
390+
auto_disconnect: ctx.data().auto_disconnect.clone(),
391+
ctx: ctx.serenity_context().clone(),
392+
},
393+
);
247394
}
248395
Err(e) => {
249396
ctx.say(format!("❌ Couldn't join your voice channel: {:?}", e))
@@ -341,7 +488,7 @@ async fn stop(ctx: Contx<'_>) -> Result<(), Error> {
341488
drop(handler);
342489
ctx.data()
343490
.auto_disconnect
344-
.start_inactivity_timer(guild_id, ctx)
491+
.start_inactivity_timer(guild_id, ctx.serenity_context().clone())
345492
.await;
346493

347494
ctx.say("⏹️ Stopped playback and cleared queue").await?;
@@ -362,7 +509,9 @@ async fn main() -> Result<()> {
362509

363510
let intents = GatewayIntents::non_privileged()
364511
| GatewayIntents::GUILD_VOICE_STATES
365-
| GatewayIntents::MESSAGE_CONTENT;
512+
| GatewayIntents::MESSAGE_CONTENT
513+
| GatewayIntents::GUILDS;
514+
366515
let framework = poise::Framework::builder()
367516
.options(poise::FrameworkOptions {
368517
commands: vec![help(), join(), play(), stop()],

0 commit comments

Comments
 (0)