# discord_tools/scripts/voice_call_keeper.py import os import sys import asyncio import websockets import json from datetime import datetime # Add the parent directory to the Python path script_dir = os.path.dirname(os.path.abspath(__file__)) project_root = os.path.dirname(os.path.dirname(script_dir)) sys.path.insert(0, project_root) from discord_tools.config.settings import DISCORD_TOKEN from discord_tools.utils.api_utils import make_discord_request class VoiceCallKeeper: def __init__(self): self.active = True self.current_channel_id = None self.current_guild_id = None self.session_id = None self.token = None self.endpoint = None self.my_user_id = None self.start_time = None self.total_reconnects = 0 async def find_current_voice_channel(self, ws): # Wait for READY and get current voice state ready = False current_voice = None while not ready: msg = json.loads(await ws.recv()) if msg.get('t') == 'READY': ready_data = msg['d'] self.my_user_id = ready_data['user']['id'] # Check private calls private_calls = ready_data.get('private_calls', []) for call in private_calls: # Check if we're in this call voice_states = call.get('voice_states', []) for vs in voice_states: if vs.get('user_id') == self.my_user_id: current_voice = { 'channel_id': call['channel_id'], 'guild_id': None, 'type': 'DM Call' } break if current_voice: break # Check guild voice states if not current_voice: guilds = ready_data.get('guilds', []) for guild in guilds: voice_states = guild.get('voice_states', []) for vs in voice_states: if vs.get('user_id') == self.my_user_id and vs.get('channel_id'): current_voice = { 'channel_id': vs['channel_id'], 'guild_id': guild['id'], 'type': 'Server VC' } break if current_voice: break ready = True return current_voice async def maintain_voice_connection(self, channel_id=None, guild_id=None, auto_join=False): # Connect to gateway and maintain voice connection with auto-reconnect gateway_url = "wss://gateway.discord.gg/?v=9&encoding=json" # Set start time on first connection if self.start_time is None: self.start_time = datetime.now() while self.active: try: await self._connect_and_run(gateway_url, channel_id, guild_id, auto_join) except (websockets.exceptions.ConnectionClosed, websockets.exceptions.ConnectionClosedOK, websockets.exceptions.ConnectionClosedError) as e: if self.active: self.total_reconnects += 1 elapsed = (datetime.now() - self.start_time).total_seconds() print(f"\n[!] Connection lost after {elapsed:.0f}s: {e.reason if hasattr(e, 'reason') else str(e)}") print(f"[!] Reconnecting... (attempt #{self.total_reconnects})") await asyncio.sleep(3) # After reconnect, don't auto-join again, use stored IDs auto_join = False else: break except KeyboardInterrupt: print("\n\nDisconnecting...") self.active = False break except Exception as e: if self.active: self.total_reconnects += 1 print(f"\n[!] Unexpected error: {e}") print(f"[!] Reconnecting... (attempt #{self.total_reconnects})") await asyncio.sleep(3) auto_join = False else: break async def _connect_and_run(self, gateway_url, channel_id, guild_id, auto_join): async with websockets.connect(gateway_url, max_size=16 * 1024 * 1024) as ws: # Receive Hello hello = json.loads(await ws.recv()) heartbeat_interval = hello['d']['heartbeat_interval'] # Send Identify identify = { "op": 2, "d": { "token": DISCORD_TOKEN, "properties": { "$os": "windows", "$browser": "chrome", "$device": "pc" }, "compress": False } } await ws.send(json.dumps(identify)) # Heartbeat task async def heartbeat(): while self.active: try: await asyncio.sleep(heartbeat_interval / 1000) if self.active: await ws.send(json.dumps({"op": 1, "d": None})) except Exception: break heartbeat_task = asyncio.create_task(heartbeat()) try: # If auto-join, find current voice channel if auto_join: print("Detecting current voice channel...") current_voice = await self.find_current_voice_channel(ws) if not current_voice: print("Not currently in any voice channel/call") return channel_id = current_voice['channel_id'] guild_id = current_voice['guild_id'] print(f"Found: {current_voice['type']}") # Store for reconnections self.current_channel_id = channel_id self.current_guild_id = guild_id else: # Wait for READY ready = False while not ready: msg = json.loads(await ws.recv()) if msg.get('t') == 'READY': ready = True self.my_user_id = msg['d']['user']['id'] if self.total_reconnects == 0: print("Connected to Gateway") else: elapsed = (datetime.now() - self.start_time).total_seconds() print(f"[✓] Reconnected successfully (total uptime: {elapsed:.0f}s)") # Use stored or provided IDs if self.current_channel_id is None: self.current_channel_id = channel_id self.current_guild_id = guild_id # Join/maintain voice channel/call if self.total_reconnects == 0: print(f"Maintaining voice connection...") print("Call keeper active - Press Ctrl+C to disconnect") print("=" * 60) voice_state_update = { "op": 4, "d": { "guild_id": self.current_guild_id, "channel_id": self.current_channel_id, "self_mute": False, "self_deaf": False } } await ws.send(json.dumps(voice_state_update)) # Monitor voice state and keep connection alive while self.active: msg = await ws.recv() data = json.loads(msg) event_type = data.get('t') # Track voice state changes if event_type == 'VOICE_STATE_UPDATE': voice_data = data['d'] # Check if it's related to our channel if voice_data.get('channel_id') == self.current_channel_id or voice_data.get('user_id') == self.my_user_id: user_id = voice_data.get('user_id') member = voice_data.get('member', {}) user = member.get('user', {}) username = user.get('username', 'Unknown') if voice_data.get('channel_id') is None and user_id != self.my_user_id: print(f"[-] {username} left") elif voice_data.get('channel_id') == self.current_channel_id and user_id != self.my_user_id: elapsed = (datetime.now() - self.start_time).total_seconds() print(f"[+] {username} joined (running {elapsed:.0f}s)") # Voice server update elif event_type == 'VOICE_SERVER_UPDATE': self.token = data['d'].get('token') self.endpoint = data['d'].get('endpoint') if self.total_reconnects == 0: print(f"[✓] Connection established") else: print(f"[✓] Voice connection re-established") elif event_type == 'CALL_CREATE': print("[📞] Call created") elif event_type == 'CALL_UPDATE': ringing = data['d'].get('ringing', []) if ringing: print(f"[📞] Ringing: {len(ringing)} user(s)") finally: heartbeat_task.cancel() # Only leave voice if we're truly shutting down if not self.active: try: leave_voice = { "op": 4, "d": { "guild_id": self.current_guild_id, "channel_id": None, "self_mute": False, "self_deaf": False } } await ws.send(json.dumps(leave_voice)) except Exception: pass # Stats total_time = (datetime.now() - self.start_time).total_seconds() hours = total_time / 3600 minutes = (total_time % 3600) / 60 print("\n" + "=" * 60) print(f"Duration: {int(hours)}h {int(minutes)}m") print(f"Total reconnections: {self.total_reconnects}") print("=" * 60) def main(): print("Voice Call Keeper") print("=" * 60) print("1. Auto-join current call/VC") print("2. Join specific channel") print("=" * 60) choice = input("Choice: ").strip() if choice == '1': # Auto-join current voice keeper = VoiceCallKeeper() asyncio.run(keeper.maintain_voice_connection(auto_join=True)) elif choice == '2': # Manual join channel_id = input("\nChannel ID: ").strip() if not channel_id.isdigit(): print("Invalid channel ID") return # Ask if it's a server VC is_server = input("Server VC? (y/n): ").strip().lower() guild_id = None if is_server == 'y': guild_id = input("Guild ID: ").strip() if not guild_id.isdigit(): print("Invalid guild ID") return keeper = VoiceCallKeeper() asyncio.run(keeper.maintain_voice_connection(channel_id, guild_id=guild_id)) else: print("Invalid choice") if __name__ == "__main__": main()