import requests import config import twitch_auth from config import BROADCASTER_ID import websocket import threading import config import re import pygame import json # Initialize pygame for sound effects pygame.mixer.init() CLAP_SOUND = pygame.mixer.Sound("clap.wav") # This variable will be set by app.py handle_chat_message = None def delete_existing_subscriptions(): """Deletes all existing Twitch EventSub subscriptions to prevent duplicates.""" access_token = twitch_auth.get_app_access_token() url = "https://api.twitch.tv/helix/eventsub/subscriptions" headers = { "Client-ID": config.CLIENT_ID, "Authorization": f"Bearer {access_token}" } response = requests.get(url, headers=headers).json() for sub in response.get("data", []): delete_url = f"{url}?id={sub['id']}" requests.delete(delete_url, headers=headers) print(f"🗑 Deleted subscription: {sub['id']}") def subscribe_to_twitch_events(): """Subscribes to Twitch EventSub webhooks for follow, sub, cheer, raid, and channel points.""" delete_existing_subscriptions() app_access_token = twitch_auth.get_app_access_token() user_access_token = config.USER_ACCESS_TOKEN if not user_access_token: print("❌ Error: User access token is missing. Please re-authenticate.") return url = "https://api.twitch.tv/helix/eventsub/subscriptions" def subscribe(event_type, version, condition, token): headers = { "Client-ID": config.CLIENT_ID, "Authorization": f"Bearer {token}", "Content-Type": "application/json" } data = { "type": event_type, "version": version, "condition": condition, "transport": { "method": "webhook", "callback": config.WEBHOOK_CALLBACK, "secret": config.WEBHOOK_SECRET } } response = requests.post(url, json=data, headers=headers) print(f"✅ Subscribed to {event_type}: {response.json()}") subscribe("channel.follow", "2", {"broadcaster_user_id": BROADCASTER_ID, "moderator_user_id": BROADCASTER_ID}, app_access_token) subscribe("channel.subscribe", "1", {"broadcaster_user_id": BROADCASTER_ID}, app_access_token) subscribe("channel.cheer", "1", {"broadcaster_user_id": BROADCASTER_ID}, app_access_token) subscribe("channel.raid", "1", {"to_broadcaster_user_id": BROADCASTER_ID}, app_access_token) subscribe("channel.channel_points_custom_reward_redemption.add", "1", {"broadcaster_user_id": BROADCASTER_ID}, app_access_token) def parse_irc_tags(tag_string): """Parse IRC tags from the message prefix.""" if not tag_string: return {} # Remove the @ prefix if tag_string.startswith('@'): tag_string = tag_string[1:] tags = {} for tag in tag_string.split(';'): if '=' in tag: key, value = tag.split('=', 1) tags[key] = value if value else None return tags def on_message(ws, message): """Handles incoming messages from Twitch chat and responds to PING.""" if message.startswith("PING"): ws.send("PONG :tmi.twitch.tv") return message_lines = message.split("\r\n") for line in message_lines: if not line.strip(): continue if "NOTICE" in line or "JOIN" in line or "PART" in line: print(f"📢 System Alert: {line}") if "PRIVMSG" in line: try: # Parse more complex IRC message format with tags tags = {} if line.startswith('@'): tags_part, rest = line.split(' ', 1) tags = parse_irc_tags(tags_part) line = rest # Extract username and message match = re.search(r":(\w+)!\w+@\w+\.tmi\.twitch\.tv PRIVMSG #\w+ :(.+)", line) if match: user, message_content = match.groups() # Handle clap emote if "procptClap" in message_content: print(f"👏 {user} clapped!") CLAP_SOUND.play() # If handle_chat_message function is set (by app.py), use it if handle_chat_message: # Create a copy of data to pass to the main thread username = user content = message_content tags_copy = tags.copy() if tags else {} # Use threading to avoid blocking the WebSocket thread threading.Thread( target=lambda: handle_chat_message(username, content, tags_copy), daemon=True ).start() print(f"💬 {user}: {message_content}") except Exception as e: print(f"Error parsing chat message: {e}") def on_open(ws): """Handles connection opening to Twitch Chat.""" print("✅ Connected to Twitch Chat") # Use the token directly from config ws.send(f"PASS {config.BOT_OAUTH_TOKEN}") ws.send(f"NICK {config.TWITCH_BOT_NICKNAME}") ws.send(f"JOIN #{config.TWITCH_CHANNEL}") # Request capabilities for additional message data ws.send("CAP REQ :twitch.tv/tags twitch.tv/commands twitch.tv/membership") print(f"🔗 Joined #{config.TWITCH_CHANNEL} as {config.TWITCH_BOT_NICKNAME}") def connect_twitch_chat(): """Creates a WebSocket connection to Twitch Chat.""" ws = websocket.WebSocketApp( config.TWITCH_CHAT_URL, on_message=on_message, on_open=on_open ) ws.run_forever() def start_twitch_chat(): chat_thread = threading.Thread(target=connect_twitch_chat, daemon=True) chat_thread.start() if __name__ == "__main__": subscribe_to_twitch_events()