Initial commit: Discord automation tools

This commit is contained in:
2026-01-27 10:13:41 +01:00
commit 7f5d3c2ca7
41 changed files with 983407 additions and 0 deletions

311
scripts/voice_call_keeper.py Executable file
View 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()