Initial commit: Discord automation tools
This commit is contained in:
311
scripts/voice_call_keeper.py
Executable file
311
scripts/voice_call_keeper.py
Executable file
@@ -0,0 +1,311 @@
|
||||
# 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()
|
||||
Reference in New Issue
Block a user