@@ -2,9 +2,15 @@ use dotenv::dotenv;
22
33use poise:: serenity_prelude:: GatewayIntents ;
44use 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 ;
710use serenity:: prelude:: TypeMapKey ;
11+ use songbird:: { Event , EventContext , EventHandler as SongbirdEventHandler , TrackEvent } ;
12+ // use songbird::id::ChannelId;
13+
814use songbird:: {
915 SerenityInit ,
1016 input:: { Compose , YoutubeDl } ,
@@ -18,8 +24,8 @@ use std::time::{Duration, Instant};
1824use std:: { collections:: HashMap , env, sync:: Arc } ;
1925type Error = Box < dyn std:: error:: Error + Send + Sync > ;
2026type 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) ]
2430async 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
157252fn 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) ]
171295async 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