Token and controller commands work
This commit is contained in:
parent
ec1c5958ce
commit
1a2df3e539
8 changed files with 1137 additions and 376 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,5 +1,8 @@
|
||||||
|
game_stats.json
|
||||||
|
input_mode.txt
|
||||||
.env
|
.env
|
||||||
.venv
|
.venv
|
||||||
|
*.db*
|
||||||
default_config.json
|
default_config.json
|
||||||
data
|
data
|
||||||
*.db
|
*.db
|
||||||
|
|
|
||||||
47
main.py
47
main.py
|
|
@ -114,6 +114,7 @@ def main() -> None:
|
||||||
parser.add_argument('--admin-users', type=str, help='Comma-separated list of additional admin users who have the same permissions as the channel owner')
|
parser.add_argument('--admin-users', type=str, help='Comma-separated list of additional admin users who have the same permissions as the channel owner')
|
||||||
parser.add_argument('--config', type=str, default=DEFAULT_CONFIG_PATH,
|
parser.add_argument('--config', type=str, default=DEFAULT_CONFIG_PATH,
|
||||||
help=f'Path to configuration file (default: {DEFAULT_CONFIG_PATH})')
|
help=f'Path to configuration file (default: {DEFAULT_CONFIG_PATH})')
|
||||||
|
parser.add_argument('--force-new-token', action='store_true', help='Force generation of a new token even if a cached one exists')
|
||||||
args: argparse.Namespace = parser.parse_args()
|
args: argparse.Namespace = parser.parse_args()
|
||||||
|
|
||||||
# Load configuration from file if it exists
|
# Load configuration from file if it exists
|
||||||
|
|
@ -180,7 +181,24 @@ def main() -> None:
|
||||||
admin_users = config["admin_users"]
|
admin_users = config["admin_users"]
|
||||||
print(f"Additional admin users from config file: {', '.join(admin_users)}")
|
print(f"Additional admin users from config file: {', '.join(admin_users)}")
|
||||||
|
|
||||||
# Create bot instance
|
# Make sure the cache directory exists
|
||||||
|
cache_dir = os.path.dirname(args.token_cache)
|
||||||
|
os.makedirs(cache_dir, exist_ok=True)
|
||||||
|
|
||||||
|
print(f"Using token cache file: {args.token_cache}")
|
||||||
|
if args.force_new_token:
|
||||||
|
print("Forcing new token generation (any cached token will be ignored)")
|
||||||
|
# Remove cached token if forcing new one
|
||||||
|
if os.path.exists(args.token_cache):
|
||||||
|
try:
|
||||||
|
os.remove(args.token_cache)
|
||||||
|
print("Removed existing token cache file")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not remove existing token cache: {e}")
|
||||||
|
else:
|
||||||
|
print("Using cached token if available")
|
||||||
|
|
||||||
|
# Create bot instance - explicitly pass None for access_token to avoid using env vars
|
||||||
bot: TwitchBot = TwitchBot(
|
bot: TwitchBot = TwitchBot(
|
||||||
username=username,
|
username=username,
|
||||||
client_id=client_id,
|
client_id=client_id,
|
||||||
|
|
@ -188,7 +206,10 @@ def main() -> None:
|
||||||
channel=channel,
|
channel=channel,
|
||||||
use_queue=args.use_queue,
|
use_queue=args.use_queue,
|
||||||
token_cache_file=args.token_cache,
|
token_cache_file=args.token_cache,
|
||||||
admin_users=admin_users
|
admin_users=admin_users,
|
||||||
|
force_new_token=args.force_new_token,
|
||||||
|
access_token=None, # Explicitly set to None to avoid using env vars
|
||||||
|
refresh_token=None # Explicitly set to None to avoid using env vars
|
||||||
)
|
)
|
||||||
|
|
||||||
# Register basic commands
|
# Register basic commands
|
||||||
|
|
@ -206,9 +227,13 @@ def main() -> None:
|
||||||
# Start the Huey consumer if requested
|
# Start the Huey consumer if requested
|
||||||
if args.start_consumer:
|
if args.start_consumer:
|
||||||
try:
|
try:
|
||||||
from src.queue.server import start_consumer
|
from src.queue.server import start_consumer, set_input_mode, get_input_mode
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
# Make sure input mode is set before starting consumer
|
||||||
|
if args.game_control and args.input_type:
|
||||||
|
print(f"Consumer will use input mode: {get_input_mode()}")
|
||||||
|
|
||||||
consumer_thread = threading.Thread(target=start_consumer)
|
consumer_thread = threading.Thread(target=start_consumer)
|
||||||
consumer_thread.daemon = True
|
consumer_thread.daemon = True
|
||||||
consumer_thread.start()
|
consumer_thread.start()
|
||||||
|
|
@ -251,6 +276,18 @@ def main() -> None:
|
||||||
print("Falling back to keyboard/mouse input.")
|
print("Falling back to keyboard/mouse input.")
|
||||||
args.input_type = 'keyboard'
|
args.input_type = 'keyboard'
|
||||||
|
|
||||||
|
# Persist the input type to make it available to the queue consumer process
|
||||||
|
try:
|
||||||
|
from src.queue.server import set_input_mode, INPUT_MODE_KEYBOARD, INPUT_MODE_CONTROLLER
|
||||||
|
if args.input_type == 'keyboard':
|
||||||
|
set_input_mode(INPUT_MODE_KEYBOARD)
|
||||||
|
print(f"Set persistent input mode to keyboard (this will be used by queue consumer)")
|
||||||
|
else:
|
||||||
|
set_input_mode(INPUT_MODE_CONTROLLER)
|
||||||
|
print(f"Set persistent input mode to controller (this will be used by queue consumer)")
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Warning: Could not set persistent input mode: {e}")
|
||||||
|
|
||||||
print(f"Enabling game control in {args.game_mode} mode with {args.input_type} input...")
|
print(f"Enabling game control in {args.game_mode} mode with {args.input_type} input...")
|
||||||
|
|
||||||
if args.input_type == 'keyboard' and not PYNPUT_AVAILABLE:
|
if args.input_type == 'keyboard' and not PYNPUT_AVAILABLE:
|
||||||
|
|
@ -282,6 +319,7 @@ def main() -> None:
|
||||||
if args.input_type == 'controller':
|
if args.input_type == 'controller':
|
||||||
print(" pip install vgamepad # For controller support")
|
print(" pip install vgamepad # For controller support")
|
||||||
|
|
||||||
|
print("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxinput type: ", args.input_type)
|
||||||
# Start the bot
|
# Start the bot
|
||||||
print(f"Starting bot for channel #{channel}")
|
print(f"Starting bot for channel #{channel}")
|
||||||
print("Basic commands: !hello, !dice [sides], !echo [message], !8ball")
|
print("Basic commands: !hello, !dice [sides], !echo [message], !8ball")
|
||||||
|
|
@ -291,6 +329,9 @@ def main() -> None:
|
||||||
print("Queue mode enabled, commands will be processed by the queue system")
|
print("Queue mode enabled, commands will be processed by the queue system")
|
||||||
print("Use !qstats to see queue statistics")
|
print("Use !qstats to see queue statistics")
|
||||||
|
|
||||||
|
if args.force_new_token:
|
||||||
|
print("Note: Forcing generation of a new authentication token")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Make sure requests library is installed
|
# Make sure requests library is installed
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,7 @@ This script helps users generate a Twitch user access token with proper IRC scop
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
import json
|
|
||||||
import argparse
|
import argparse
|
||||||
import time
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Load environment variables if available
|
# Load environment variables if available
|
||||||
|
|
@ -25,43 +23,7 @@ from src.core.auth import TwitchAuth
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
|
||||||
logger = logging.getLogger('twitch-auth-setup')
|
logger = logging.getLogger('twitch-auth-setup')
|
||||||
|
|
||||||
# Path to default config
|
def setup_twitch_auth(client_id=None, client_secret=None, scopes=None, manual_mode=False, force_new_token=False):
|
||||||
DEFAULT_CONFIG_PATH = os.path.join("data", "default_config.json")
|
|
||||||
|
|
||||||
def load_default_config():
|
|
||||||
"""Load default configuration from JSON file"""
|
|
||||||
if os.path.exists(DEFAULT_CONFIG_PATH):
|
|
||||||
try:
|
|
||||||
with open(DEFAULT_CONFIG_PATH, 'r') as f:
|
|
||||||
return json.load(f)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
logger.warning(f"Invalid JSON in {DEFAULT_CONFIG_PATH}, ignoring")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def save_default_config(config_data):
|
|
||||||
"""Save configuration to default config JSON file"""
|
|
||||||
# Ensure directory exists
|
|
||||||
os.makedirs(os.path.dirname(DEFAULT_CONFIG_PATH), exist_ok=True)
|
|
||||||
|
|
||||||
# If file exists, load current data and update it
|
|
||||||
existing_data = {}
|
|
||||||
if os.path.exists(DEFAULT_CONFIG_PATH):
|
|
||||||
try:
|
|
||||||
with open(DEFAULT_CONFIG_PATH, 'r') as f:
|
|
||||||
existing_data = json.load(f)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
logger.warning(f"Invalid JSON in {DEFAULT_CONFIG_PATH}, overwriting")
|
|
||||||
|
|
||||||
# Update with new data
|
|
||||||
existing_data.update(config_data)
|
|
||||||
|
|
||||||
# Save back to file
|
|
||||||
with open(DEFAULT_CONFIG_PATH, 'w') as f:
|
|
||||||
json.dump(existing_data, f, indent=2)
|
|
||||||
|
|
||||||
logger.info(f"Configuration saved to {DEFAULT_CONFIG_PATH}")
|
|
||||||
|
|
||||||
def setup_twitch_auth(client_id=None, client_secret=None, scopes=None, manual_mode=False, force_new_token=False, use_cached_token=False):
|
|
||||||
"""
|
"""
|
||||||
Setup Twitch authentication by generating a user access token
|
Setup Twitch authentication by generating a user access token
|
||||||
|
|
||||||
|
|
@ -71,46 +33,21 @@ def setup_twitch_auth(client_id=None, client_secret=None, scopes=None, manual_mo
|
||||||
scopes: Space-separated list of scopes
|
scopes: Space-separated list of scopes
|
||||||
manual_mode: Whether to use manual mode (no browser)
|
manual_mode: Whether to use manual mode (no browser)
|
||||||
force_new_token: Force generation of a new token even if a cached one exists
|
force_new_token: Force generation of a new token even if a cached one exists
|
||||||
use_cached_token: Whether to use a cached token if available (default: False)
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if successful, False otherwise
|
bool: True if successful, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Load default config
|
# Use provided values, then env vars
|
||||||
default_config = load_default_config()
|
client_id = client_id or os.environ.get('TWITCH_CLIENT_ID')
|
||||||
|
client_secret = client_secret or os.environ.get('TWITCH_CLIENT_SECRET')
|
||||||
|
|
||||||
# Priority of values:
|
# Default scopes for IRC chat
|
||||||
# 1. Function arguments
|
|
||||||
# 2. Environment variables
|
|
||||||
# 3. Default config
|
|
||||||
|
|
||||||
# Use provided values, then env vars, then default config
|
|
||||||
client_id = client_id or os.environ.get('TWITCH_CLIENT_ID') or default_config.get('TWITCH_CLIENT_ID')
|
|
||||||
client_secret = client_secret or os.environ.get('TWITCH_CLIENT_SECRET') or default_config.get('TWITCH_CLIENT_SECRET')
|
|
||||||
|
|
||||||
if not client_id:
|
|
||||||
client_id = input("Enter your Twitch Client ID: ")
|
|
||||||
|
|
||||||
if not client_secret:
|
|
||||||
client_secret = input("Enter your Twitch Client Secret: ")
|
|
||||||
|
|
||||||
if not client_id or not client_secret:
|
|
||||||
logger.error("Client ID and Client Secret are required")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Default scopes for IRC chat - prioritize env var, then default config
|
|
||||||
default_scopes = "chat:read chat:edit"
|
default_scopes = "chat:read chat:edit"
|
||||||
scopes = scopes or os.environ.get('TWITCH_SCOPES') or default_config.get('TWITCH_SCOPES', default_scopes)
|
scopes = scopes or os.environ.get('TWITCH_SCOPES', default_scopes)
|
||||||
|
|
||||||
# Define redirect URI
|
# Define redirect URI
|
||||||
redirect_uri = os.environ.get('TWITCH_REDIRECT_URI') or default_config.get('TWITCH_REDIRECT_URI', "https://localhost:3000")
|
redirect_uri = os.environ.get('TWITCH_REDIRECT_URI', "https://localhost:3000")
|
||||||
|
|
||||||
print("\n===== Twitch Authentication Setup =====")
|
|
||||||
print(f"Client ID: {client_id}")
|
|
||||||
print(f"Redirect URI: {redirect_uri} (must match your Twitch app settings)")
|
|
||||||
print(f"Requested Scopes: {scopes}")
|
|
||||||
print("=====================================\n")
|
|
||||||
|
|
||||||
# Make sure the cache directory exists
|
# Make sure the cache directory exists
|
||||||
cache_dir = os.path.join("data", "cache")
|
cache_dir = os.path.join("data", "cache")
|
||||||
|
|
@ -119,11 +56,6 @@ def setup_twitch_auth(client_id=None, client_secret=None, scopes=None, manual_mo
|
||||||
# Create the token cache file path
|
# Create the token cache file path
|
||||||
token_cache_file = os.path.join(cache_dir, "token_cache.json")
|
token_cache_file = os.path.join(cache_dir, "token_cache.json")
|
||||||
|
|
||||||
# By default, clear any cached tokens unless explicitly told to use them
|
|
||||||
if (not use_cached_token or force_new_token) and os.path.exists(token_cache_file):
|
|
||||||
logger.info("Clearing cached token to generate fresh credentials")
|
|
||||||
os.remove(token_cache_file)
|
|
||||||
|
|
||||||
# Create auth handler
|
# Create auth handler
|
||||||
auth = TwitchAuth(
|
auth = TwitchAuth(
|
||||||
client_id=client_id,
|
client_id=client_id,
|
||||||
|
|
@ -133,97 +65,12 @@ def setup_twitch_auth(client_id=None, client_secret=None, scopes=None, manual_mo
|
||||||
scopes=scopes
|
scopes=scopes
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if a valid token is already available
|
# Get the token, which will automatically trigger setup if needed
|
||||||
has_valid_token = False
|
try:
|
||||||
if use_cached_token and auth.access_token and auth.token_expiry and time.time() < (auth.token_expiry - 60):
|
oauth_token = auth.get_oauth_token(manual_auth=manual_mode, force_new_token=force_new_token)
|
||||||
has_valid_token = True
|
|
||||||
# Using cached token
|
|
||||||
if not force_new_token:
|
|
||||||
print("\nUsing cached token (valid and not expired).")
|
|
||||||
print("If you want to force a new token, run with --force-new-token")
|
|
||||||
try:
|
|
||||||
# Try to validate the token to make sure it's working
|
|
||||||
token_info = auth.validate_token()
|
|
||||||
print(f"Token belongs to user: {token_info.get('login', 'unknown')}")
|
|
||||||
expiry_hours = (auth.token_expiry - time.time()) / 3600
|
|
||||||
print(f"Token expires in {expiry_hours:.1f} hours")
|
|
||||||
|
|
||||||
# Skip browser flow and continue with cached token
|
|
||||||
oauth_token = f"oauth:{auth.access_token}"
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error validating cached token: {e}")
|
|
||||||
logger.warning("Will attempt to get a new token")
|
|
||||||
has_valid_token = False
|
|
||||||
|
|
||||||
# If no valid token, go through the auth flow
|
|
||||||
if not has_valid_token or force_new_token:
|
|
||||||
# Show security warning about certificates
|
|
||||||
if not manual_mode:
|
|
||||||
print("\n⚠️ IMPORTANT SECURITY NOTE ⚠️")
|
|
||||||
print("This application uses a self-signed certificate for HTTPS, which is required by Twitch.")
|
|
||||||
print("When your browser opens, you will see a security warning.")
|
|
||||||
print("This is expected for local development. Please proceed to the site anyway:")
|
|
||||||
print(" • In Chrome: Click 'Advanced' and then 'Proceed to localhost (unsafe)'")
|
|
||||||
print(" • In Firefox: Click 'Advanced', then 'Accept the Risk and Continue'")
|
|
||||||
print(" • In Edge: Click 'Details' and then 'Go on to the webpage'\n")
|
|
||||||
print("NOTE: If the browser doesn't open automatically, a URL will be provided for you to copy and paste.")
|
|
||||||
input("Press Enter to continue...")
|
|
||||||
|
|
||||||
# Start OAuth flow
|
|
||||||
logger.info("Starting OAuth authentication flow...")
|
|
||||||
oauth_token = auth.get_oauth_token(manual_auth=manual_mode)
|
|
||||||
|
|
||||||
# At this point, we should have a valid token
|
|
||||||
if auth.access_token:
|
|
||||||
# Get token info
|
|
||||||
try:
|
|
||||||
token_info = auth.validate_token()
|
|
||||||
|
|
||||||
print("\n===== Authentication Successful =====")
|
|
||||||
print(f"Access Token: {auth.access_token}")
|
|
||||||
if auth.refresh_token:
|
|
||||||
print(f"Refresh Token: {auth.refresh_token}")
|
|
||||||
print(f"Token Scopes: {token_info.get('scopes', [])}")
|
|
||||||
print(f"User Name: {token_info.get('login', 'unknown')}")
|
|
||||||
print("=====================================\n")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to validate token: {e}")
|
|
||||||
print("\n===== Authentication Status =====")
|
|
||||||
print(f"Access Token: {auth.access_token}")
|
|
||||||
if auth.refresh_token:
|
|
||||||
print(f"Refresh Token: {auth.refresh_token}")
|
|
||||||
print("Warning: Could not validate token")
|
|
||||||
print("============================\n")
|
|
||||||
|
|
||||||
# Store token info in default_config.json
|
|
||||||
if input("Would you like to save these credentials to default_config.json? (y/n): ").lower() == 'y':
|
|
||||||
config_data = {
|
|
||||||
"TWITCH_CLIENT_ID": client_id,
|
|
||||||
"TWITCH_CLIENT_SECRET": client_secret,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Try to use the validated user info
|
|
||||||
if 'token_info' in locals():
|
|
||||||
username = token_info.get('login', '')
|
|
||||||
config_data["TWITCH_USERNAME"] = username
|
|
||||||
|
|
||||||
# Get channel name
|
|
||||||
default_channel = username if 'username' in locals() else os.environ.get('TWITCH_CHANNEL', '') or default_config.get('TWITCH_CHANNEL', '')
|
|
||||||
channel = input(f"Enter Twitch channel to join (default: {default_channel}): ") or default_channel
|
|
||||||
config_data["TWITCH_CHANNEL"] = channel
|
|
||||||
|
|
||||||
# Save access and refresh tokens
|
|
||||||
config_data["TWITCH_ACCESS_TOKEN"] = auth.access_token
|
|
||||||
if auth.refresh_token:
|
|
||||||
config_data["TWITCH_REFRESH_TOKEN"] = auth.refresh_token
|
|
||||||
|
|
||||||
# Save to default_config.json
|
|
||||||
save_default_config(config_data)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
except ValueError as e:
|
||||||
else:
|
logger.error(f"Authentication failed: {e}")
|
||||||
logger.error("Failed to get OAuth token")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -238,11 +85,10 @@ def main():
|
||||||
parser.add_argument("--scopes", help="Space-separated list of scopes")
|
parser.add_argument("--scopes", help="Space-separated list of scopes")
|
||||||
parser.add_argument("--manual", action="store_true", help="Use manual mode (no browser)")
|
parser.add_argument("--manual", action="store_true", help="Use manual mode (no browser)")
|
||||||
parser.add_argument("--force-new-token", action="store_true", help="Force generation of a new token even if a cached one exists")
|
parser.add_argument("--force-new-token", action="store_true", help="Force generation of a new token even if a cached one exists")
|
||||||
parser.add_argument("--use-cached-token", action="store_true", help="Use cached token if available (default is to clear cached tokens)")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if setup_twitch_auth(args.client_id, args.client_secret, args.scopes, args.manual, args.force_new_token, args.use_cached_token):
|
if setup_twitch_auth(args.client_id, args.client_secret, args.scopes, args.manual, args.force_new_token):
|
||||||
print("Setup completed successfully!")
|
print("Setup completed successfully!")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
131
src/core/auth.py
131
src/core/auth.py
|
|
@ -175,38 +175,49 @@ class TwitchAuth:
|
||||||
logger.error(f"Failed to exchange authorization code: {e}")
|
logger.error(f"Failed to exchange authorization code: {e}")
|
||||||
return False, f"Failed to exchange authorization code: {str(e)}"
|
return False, f"Failed to exchange authorization code: {str(e)}"
|
||||||
|
|
||||||
def get_oauth_token(self, manual_auth: bool = False) -> str:
|
def _setup_auth(self, manual_mode: bool = False) -> bool:
|
||||||
"""
|
"""
|
||||||
Get OAuth token for Twitch authentication
|
Interactive setup for Twitch authentication
|
||||||
|
|
||||||
If no valid token is available:
|
|
||||||
1. Try to refresh the token if a refresh token is available
|
|
||||||
2. If refresh fails or no refresh token, require user auth
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
manual_auth: If True, provide instructions for manual authentication
|
manual_mode: Whether to use manual mode (no browser)
|
||||||
instead of automatic browser launch
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: The OAuth token with 'oauth:' prefix
|
bool: True if successful, False otherwise
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If authentication fails
|
|
||||||
"""
|
"""
|
||||||
# Check if we have a valid token
|
# Verify we have the required credentials
|
||||||
if self.access_token and self.token_expiry and time.time() < (self.token_expiry - 60):
|
if not self.client_id:
|
||||||
expiry_min = (self.token_expiry - time.time()) / 60
|
self.client_id = input("Enter your Twitch Client ID: ")
|
||||||
logger.debug(f"Using cached token, expires in {expiry_min:.1f} minutes")
|
|
||||||
return f"oauth:{self.access_token}"
|
|
||||||
|
|
||||||
# Try to refresh the token
|
if not self.client_secret:
|
||||||
if self.refresh_token and self._refresh_token():
|
self.client_secret = input("Enter your Twitch Client Secret: ")
|
||||||
return f"oauth:{self.access_token}"
|
|
||||||
|
|
||||||
# Need user authentication - use our auth server to handle the flow
|
if not self.client_id or not self.client_secret:
|
||||||
logger.info("Starting OAuth authentication flow")
|
logger.error("Client ID and Client Secret are required")
|
||||||
|
return False
|
||||||
|
|
||||||
if manual_auth:
|
print("\n===== Twitch Authentication Setup =====")
|
||||||
|
print(f"Client ID: {self.client_id}")
|
||||||
|
print(f"Redirect URI: {self.redirect_uri} (must match your Twitch app settings)")
|
||||||
|
print(f"Requested Scopes: {self.scopes}")
|
||||||
|
print("=====================================\n")
|
||||||
|
|
||||||
|
# Show security warning about certificates
|
||||||
|
if not manual_mode:
|
||||||
|
print("\n⚠️ IMPORTANT SECURITY NOTE ⚠️")
|
||||||
|
print("This application uses a self-signed certificate for HTTPS, which is required by Twitch.")
|
||||||
|
print("When your browser opens, you will see a security warning.")
|
||||||
|
print("This is expected for local development. Please proceed to the site anyway:")
|
||||||
|
print(" • In Chrome: Click 'Advanced' and then 'Proceed to localhost (unsafe)'")
|
||||||
|
print(" • In Firefox: Click 'Advanced', then 'Accept the Risk and Continue'")
|
||||||
|
print(" • In Edge: Click 'Details' and then 'Go on to the webpage'\n")
|
||||||
|
print("NOTE: If the browser doesn't open automatically, a URL will be provided for you to copy and paste.")
|
||||||
|
input("Press Enter to continue...")
|
||||||
|
|
||||||
|
# Start OAuth flow
|
||||||
|
logger.info("Starting OAuth authentication flow...")
|
||||||
|
|
||||||
|
if manual_mode:
|
||||||
# For environments where browser launch isn't possible
|
# For environments where browser launch isn't possible
|
||||||
auth_url = (
|
auth_url = (
|
||||||
f"https://id.twitch.tv/oauth2/authorize"
|
f"https://id.twitch.tv/oauth2/authorize"
|
||||||
|
|
@ -231,14 +242,82 @@ class TwitchAuth:
|
||||||
)
|
)
|
||||||
|
|
||||||
if not auth_code:
|
if not auth_code:
|
||||||
raise ValueError("Failed to get authorization code")
|
logger.error("Failed to get authorization code")
|
||||||
|
return False
|
||||||
|
|
||||||
# Exchange the auth code for tokens
|
# Exchange the auth code for tokens
|
||||||
success, message = self._handle_auth_code(auth_code)
|
success, message = self._handle_auth_code(auth_code)
|
||||||
if success:
|
if success:
|
||||||
return f"oauth:{self.access_token}"
|
# At this point, we should have a valid token
|
||||||
|
try:
|
||||||
|
token_info = self.validate_token()
|
||||||
|
|
||||||
|
print("\n===== Authentication Successful =====")
|
||||||
|
print(f"Token Scopes: {token_info.get('scopes', [])}")
|
||||||
|
print(f"User Name: {token_info.get('login', 'unknown')}")
|
||||||
|
print("=====================================\n")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to validate token: {e}")
|
||||||
|
print("\n===== Authentication Status =====")
|
||||||
|
print("Warning: Could not validate token")
|
||||||
|
print("============================\n")
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Authentication failed: {message}")
|
logger.error(f"Authentication failed: {message}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_oauth_token(self, manual_auth: bool = False, force_new_token: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Get OAuth token for Twitch authentication
|
||||||
|
|
||||||
|
If no valid token is available:
|
||||||
|
1. Try to refresh the token if a refresh token is available
|
||||||
|
2. If refresh fails or no refresh token, automatically trigger setup
|
||||||
|
|
||||||
|
Args:
|
||||||
|
manual_auth: If True, provide instructions for manual authentication
|
||||||
|
instead of automatic browser launch
|
||||||
|
force_new_token: Force generation of a new token even if a cached one exists
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The OAuth token with 'oauth:' prefix
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If authentication fails
|
||||||
|
"""
|
||||||
|
# If force_new_token is set, clear any cached tokens first
|
||||||
|
if force_new_token and os.path.exists(self.token_cache_file):
|
||||||
|
logger.info("Clearing cached token to generate fresh credentials")
|
||||||
|
os.remove(self.token_cache_file)
|
||||||
|
self.access_token = None
|
||||||
|
self.refresh_token = None
|
||||||
|
self.token_expiry = None
|
||||||
|
|
||||||
|
# Check if we have a valid token (with an extra safety margin of 5 minutes)
|
||||||
|
if not force_new_token and self.access_token and self.token_expiry:
|
||||||
|
# Add extra safety margin for token expiry to avoid edge cases
|
||||||
|
safety_margin = 300 # 5 minutes in seconds
|
||||||
|
if time.time() < (self.token_expiry - safety_margin):
|
||||||
|
expiry_min = (self.token_expiry - time.time()) / 60
|
||||||
|
logger.debug(f"Using cached token, expires in {expiry_min:.1f} minutes")
|
||||||
|
return f"oauth:{self.access_token}"
|
||||||
|
else:
|
||||||
|
logger.warning(f"Token expiring soon (within safety margin), attempting refresh")
|
||||||
|
|
||||||
|
# Try to refresh the token
|
||||||
|
if not force_new_token and self.refresh_token and self._refresh_token():
|
||||||
|
logger.info("Successfully refreshed token")
|
||||||
|
return f"oauth:{self.access_token}"
|
||||||
|
|
||||||
|
# If token refresh failed or no refresh token is available, trigger setup
|
||||||
|
logger.info("No valid token available, starting authentication flow")
|
||||||
|
if self._setup_auth(manual_mode=manual_auth):
|
||||||
|
if self.access_token:
|
||||||
|
return f"oauth:{self.access_token}"
|
||||||
|
|
||||||
|
# If we got here, authentication failed
|
||||||
|
raise ValueError("Failed to obtain a valid OAuth token")
|
||||||
|
|
||||||
def validate_token(self) -> Dict[str, Any]:
|
def validate_token(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ class TwitchBot:
|
||||||
access_token: str = None, refresh_token: str = None,
|
access_token: str = None, refresh_token: str = None,
|
||||||
channel: str = None, use_queue: bool = False,
|
channel: str = None, use_queue: bool = False,
|
||||||
token_cache_file: str = "data/cache/token_cache.json",
|
token_cache_file: str = "data/cache/token_cache.json",
|
||||||
admin_users: List[str] = None) -> None:
|
admin_users: List[str] = None,
|
||||||
|
force_new_token: bool = False) -> None:
|
||||||
self.username: str = username.lower()
|
self.username: str = username.lower()
|
||||||
self.client_id: Optional[str] = client_id or os.environ.get("TWITCH_CLIENT_ID")
|
self.client_id: Optional[str] = client_id or os.environ.get("TWITCH_CLIENT_ID")
|
||||||
self.client_secret: Optional[str] = client_secret or os.environ.get("TWITCH_CLIENT_SECRET")
|
self.client_secret: Optional[str] = client_secret or os.environ.get("TWITCH_CLIENT_SECRET")
|
||||||
|
|
@ -36,26 +37,28 @@ class TwitchBot:
|
||||||
self.commands: Dict[str, CommandCallback] = {}
|
self.commands: Dict[str, CommandCallback] = {}
|
||||||
self.running: bool = False
|
self.running: bool = False
|
||||||
self.last_command: str = ""
|
self.last_command: str = ""
|
||||||
|
self.force_new_token: bool = force_new_token
|
||||||
|
self.connection_attempts: int = 0
|
||||||
|
|
||||||
# Admin users who have the same privileges as the channel owner
|
# Admin users who have the same privileges as the channel owner
|
||||||
self.admin_users: List[str] = [self.channel] # Channel owner is always an admin
|
self.admin_users: List[str] = [self.channel] # Channel owner is always an admin
|
||||||
if admin_users:
|
if admin_users:
|
||||||
self.admin_users.extend([user.lower() for user in admin_users])
|
self.admin_users.extend([user.lower() for user in admin_users])
|
||||||
|
|
||||||
# Use access_token from args or environment
|
# Make sure the cache directory exists
|
||||||
direct_access_token = access_token or os.environ.get("TWITCH_ACCESS_TOKEN")
|
os.makedirs(os.path.dirname(token_cache_file), exist_ok=True)
|
||||||
direct_refresh_token = refresh_token or os.environ.get("TWITCH_REFRESH_TOKEN")
|
|
||||||
|
|
||||||
# Auth token handling
|
# Initialize auth handler - only use token cache for tokens
|
||||||
self.auth = TwitchAuth(self.client_id, self.client_secret, token_cache_file)
|
self.auth = TwitchAuth(self.client_id, self.client_secret, token_cache_file)
|
||||||
self.token_cache_file: str = token_cache_file
|
self.token_cache_file: str = token_cache_file
|
||||||
|
|
||||||
# If tokens provided directly, update the auth handler
|
# Only use directly provided tokens (not from env vars) for backward compatibility
|
||||||
if direct_access_token:
|
# This is only used during testing or if explicitly passed to the constructor
|
||||||
logger.info("Using provided access token")
|
if access_token is not None:
|
||||||
|
logger.info("Using provided access token parameter (not from environment)")
|
||||||
self.auth.update_manually(
|
self.auth.update_manually(
|
||||||
access_token=direct_access_token,
|
access_token=access_token,
|
||||||
refresh_token=direct_refresh_token
|
refresh_token=refresh_token
|
||||||
)
|
)
|
||||||
|
|
||||||
# Queue integration
|
# Queue integration
|
||||||
|
|
@ -76,18 +79,92 @@ class TwitchBot:
|
||||||
"""Get the token expiry from the auth handler"""
|
"""Get the token expiry from the auth handler"""
|
||||||
return self.auth.token_expiry
|
return self.auth.token_expiry
|
||||||
|
|
||||||
def get_oauth_token(self) -> str:
|
def validate_token_with_twitch(self) -> bool:
|
||||||
"""Get an OAuth token for authenticating with Twitch"""
|
"""
|
||||||
return self.auth.get_oauth_token()
|
Validate the current token with Twitch API to ensure it's still valid
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if token is valid, False otherwise
|
||||||
|
"""
|
||||||
|
if not self.auth.access_token:
|
||||||
|
logger.warning("No access token available to validate")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate_url = "https://id.twitch.tv/oauth2/validate"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"OAuth {self.auth.access_token}"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(validate_url, headers=headers)
|
||||||
|
if response.status_code == 200:
|
||||||
|
token_info = response.json()
|
||||||
|
logger.info(f"Token valid for user: {token_info.get('login', 'unknown')}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"Token validation failed with status {response.status_code}: {response.text}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error validating token with Twitch: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_oauth_token(self, force_refresh: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Get an OAuth token for authenticating with Twitch
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force_refresh: Force a token refresh regardless of expiry
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The OAuth token in 'oauth:xxx' format
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# If forcing a refresh, temporarily set force_new_token
|
||||||
|
original_force_new_token = self.force_new_token
|
||||||
|
if force_refresh:
|
||||||
|
logger.info("Forcing token refresh due to connection issues")
|
||||||
|
self.force_new_token = True
|
||||||
|
|
||||||
|
# The updated TwitchAuth will automatically handle token refresh or setup if needed
|
||||||
|
token = self.auth.get_oauth_token(force_new_token=self.force_new_token)
|
||||||
|
|
||||||
|
# Restore original force_new_token setting
|
||||||
|
if force_refresh:
|
||||||
|
self.force_new_token = original_force_new_token
|
||||||
|
|
||||||
|
# Reset connection attempts counter on successful token retrieval
|
||||||
|
self.connection_attempts = 0
|
||||||
|
return token
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get OAuth token: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
def connect(self) -> bool:
|
def connect(self) -> bool:
|
||||||
"""Connect to Twitch IRC server"""
|
"""Connect to Twitch IRC server"""
|
||||||
try:
|
try:
|
||||||
# Get the OAuth token
|
# Increment connection attempts
|
||||||
oauth_token = self.get_oauth_token()
|
self.connection_attempts += 1
|
||||||
|
|
||||||
|
# If we've had multiple connection attempts, validate the token with Twitch first
|
||||||
|
if self.connection_attempts > 1 and self.auth.access_token:
|
||||||
|
if not self.validate_token_with_twitch():
|
||||||
|
logger.warning("Token validation failed, forcing refresh before connection attempt")
|
||||||
|
# Force a token refresh since validation failed
|
||||||
|
oauth_token = self.get_oauth_token(force_refresh=True)
|
||||||
|
else:
|
||||||
|
logger.info("Token validation successful, proceeding with connection")
|
||||||
|
oauth_token = self.get_oauth_token()
|
||||||
|
else:
|
||||||
|
# First attempt, use normal token flow
|
||||||
|
oauth_token = self.get_oauth_token()
|
||||||
|
|
||||||
self.oauth_token = oauth_token # Store for queue use
|
self.oauth_token = oauth_token # Store for queue use
|
||||||
|
|
||||||
|
# Create a new socket for each connection attempt
|
||||||
|
self.socket = socket.socket()
|
||||||
self.socket.connect((self.server, self.port))
|
self.socket.connect((self.server, self.port))
|
||||||
|
|
||||||
# Add a timeout so we can process keyboard interrupts
|
# Add a timeout so we can process keyboard interrupts
|
||||||
self.socket.settimeout(1.0)
|
self.socket.settimeout(1.0)
|
||||||
self.socket.send(f"PASS {oauth_token}\n".encode('utf-8'))
|
self.socket.send(f"PASS {oauth_token}\n".encode('utf-8'))
|
||||||
|
|
@ -96,8 +173,30 @@ class TwitchBot:
|
||||||
|
|
||||||
# Wait for confirmation
|
# Wait for confirmation
|
||||||
data = self.socket.recv(2048).decode('utf-8')
|
data = self.socket.recv(2048).decode('utf-8')
|
||||||
|
|
||||||
|
# Check for authentication error
|
||||||
|
if ":tmi.twitch.tv NOTICE * :Login authentication failed" in data:
|
||||||
|
logger.error("Authentication failed! Forcing token refresh for next attempt")
|
||||||
|
# Force a new token on the next attempt - clear the token cache
|
||||||
|
if os.path.exists(self.token_cache_file):
|
||||||
|
try:
|
||||||
|
os.remove(self.token_cache_file)
|
||||||
|
logger.info(f"Removed invalid token cache: {self.token_cache_file}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to remove token cache: {e}")
|
||||||
|
|
||||||
|
# If we've tried multiple times, let's be more explicit about the error
|
||||||
|
if self.connection_attempts >= 3:
|
||||||
|
logger.error("Multiple authentication failures. Please check your Twitch credentials and permissions.")
|
||||||
|
logger.error("You may need to revoke any existing tokens in the Twitch Developer Console.")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check for successful connection
|
||||||
if "Welcome" in data or "You are in a maze of twisty passages" in data:
|
if "Welcome" in data or "You are in a maze of twisty passages" in data:
|
||||||
logger.info(f"Connected to {self.channel}'s chat")
|
logger.info(f"Connected to {self.channel}'s chat")
|
||||||
|
# Reset connection attempts counter on successful connection
|
||||||
|
self.connection_attempts = 0
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.error(f"Failed to connect: {data}")
|
logger.error(f"Failed to connect: {data}")
|
||||||
|
|
@ -138,49 +237,50 @@ class TwitchBot:
|
||||||
content: str = message_data['content'].strip()
|
content: str = message_data['content'].strip()
|
||||||
username: str = message_data['username']
|
username: str = message_data['username']
|
||||||
|
|
||||||
# Check if we should use the queue
|
# If this is a regular message (not a command), handle it
|
||||||
if self.use_queue:
|
if not content.startswith('!'):
|
||||||
# If this is a regular message, add it to the message queue
|
|
||||||
if not content.startswith('!'):
|
|
||||||
enqueue_message(username, content)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check if message is a command (starts with !)
|
|
||||||
if content.startswith('!'):
|
|
||||||
parts: List[str] = content.split()
|
|
||||||
command: str = parts[0][1:].lower() # Remove ! and convert to lowercase
|
|
||||||
args: List[str] = parts[1:] if len(parts) > 1 else []
|
|
||||||
|
|
||||||
# Store the last executed command
|
|
||||||
self.last_command = command
|
|
||||||
|
|
||||||
# Check if we should use the queue
|
|
||||||
if self.use_queue:
|
if self.use_queue:
|
||||||
# Add the command to the queue and return
|
# If using queue, send the message to the queue
|
||||||
# We need to create a simple command registry to pass to the queue
|
enqueue_message(username, content)
|
||||||
command_registry = {cmd: cmd for cmd in self.commands.keys()}
|
return
|
||||||
|
|
||||||
# Make sure we have an OAuth token
|
# At this point, we know this is a command (starts with !)
|
||||||
if not self.oauth_token:
|
parts: List[str] = content.split()
|
||||||
self.oauth_token = self.get_oauth_token()
|
command: str = parts[0][1:].lower() # Remove ! and convert to lowercase
|
||||||
|
args: List[str] = parts[1:] if len(parts) > 1 else []
|
||||||
|
|
||||||
enqueue_command(
|
# Store the last executed command
|
||||||
username,
|
self.last_command = command
|
||||||
command,
|
|
||||||
args,
|
|
||||||
channel=self.channel,
|
|
||||||
oauth_token=self.oauth_token,
|
|
||||||
bot_username=self.username,
|
|
||||||
command_registry=command_registry
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Direct execution (no queue)
|
# Log if this is a controller command
|
||||||
if command in self.commands:
|
if command in self.commands:
|
||||||
try:
|
logger.info(f"Recognized command: !{command}")
|
||||||
self.commands[command](username, args, self)
|
|
||||||
except Exception as e:
|
# If using queue, just enqueue the command without any processing
|
||||||
logger.error(f"Error executing command {command}: {e}")
|
if self.use_queue:
|
||||||
|
# Make sure we have an OAuth token
|
||||||
|
if not self.oauth_token:
|
||||||
|
self.oauth_token = self.get_oauth_token()
|
||||||
|
|
||||||
|
# Simply pass the command name and let the queue worker handle all processing
|
||||||
|
logger.info(f"Enqueueing command '{command}' for processing by queue worker")
|
||||||
|
|
||||||
|
enqueue_command(
|
||||||
|
username,
|
||||||
|
command,
|
||||||
|
args,
|
||||||
|
channel=self.channel,
|
||||||
|
oauth_token=self.oauth_token,
|
||||||
|
bot_username=self.username
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Direct execution (no queue)
|
||||||
|
if command in self.commands:
|
||||||
|
try:
|
||||||
|
self.commands[command](username, args, self)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error executing command {command}: {e}")
|
||||||
|
|
||||||
def start(self, use_queue: bool = False) -> None:
|
def start(self, use_queue: bool = False) -> None:
|
||||||
"""Start the bot and listen for messages"""
|
"""Start the bot and listen for messages"""
|
||||||
|
|
@ -188,9 +288,44 @@ class TwitchBot:
|
||||||
self.use_queue = use_queue
|
self.use_queue = use_queue
|
||||||
logger.info(f"Starting bot with queue {'enabled' if use_queue else 'disabled'}")
|
logger.info(f"Starting bot with queue {'enabled' if use_queue else 'disabled'}")
|
||||||
|
|
||||||
if not self.connect():
|
# Try to connect with up to 3 attempts, possibly with token refreshes
|
||||||
logger.error("Failed to start bot: Connection error")
|
max_attempts = 3
|
||||||
return
|
for attempt in range(1, max_attempts + 1):
|
||||||
|
logger.info(f"Connection attempt {attempt}/{max_attempts}")
|
||||||
|
if self.connect():
|
||||||
|
break
|
||||||
|
elif attempt < max_attempts:
|
||||||
|
# Wait a moment before retrying
|
||||||
|
time.sleep(2)
|
||||||
|
logger.info("Retrying connection...")
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to connect after {max_attempts} attempts")
|
||||||
|
|
||||||
|
# Clean up any existing token cache
|
||||||
|
if os.path.exists(self.token_cache_file):
|
||||||
|
try:
|
||||||
|
os.remove(self.token_cache_file)
|
||||||
|
logger.info(f"Removed token cache after failed connection attempts: {self.token_cache_file}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to remove token cache: {e}")
|
||||||
|
|
||||||
|
# Force a completely new authentication process
|
||||||
|
logger.info("Running authentication setup after failed connection attempts...")
|
||||||
|
try:
|
||||||
|
# Force a new token and run the setup process
|
||||||
|
self.oauth_token = self.get_oauth_token(force_refresh=True)
|
||||||
|
|
||||||
|
# Try one final connection
|
||||||
|
logger.info("Attempting connection with fresh authentication...")
|
||||||
|
if self.connect():
|
||||||
|
logger.info("Successfully connected with new authentication!")
|
||||||
|
else:
|
||||||
|
logger.error("Still unable to connect even with fresh authentication")
|
||||||
|
logger.error("Please check your Twitch credentials and network connection")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Authentication setup failed: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
self.running = True
|
self.running = True
|
||||||
buffer: str = ""
|
buffer: str = ""
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@ except ImportError:
|
||||||
|
|
||||||
# Try to import the VirtualController
|
# Try to import the VirtualController
|
||||||
try:
|
try:
|
||||||
from src.game.controller import VirtualController, VGAMEPAD_AVAILABLE
|
from src.game.input.gamepad import VirtualController, VGAMEPAD_AVAILABLE
|
||||||
except ImportError:
|
except ImportError as e:
|
||||||
|
print(e)
|
||||||
VGAMEPAD_AVAILABLE = False
|
VGAMEPAD_AVAILABLE = False
|
||||||
print("controller_support.py not found or error importing it.")
|
print("controller_support.py not found or error importing it.")
|
||||||
print("Virtual controller support will be disabled.")
|
print("Virtual controller support will be disabled.")
|
||||||
|
|
@ -105,10 +106,10 @@ class GameController:
|
||||||
"shoot": self.controller.press_right_trigger,
|
"shoot": self.controller.press_right_trigger,
|
||||||
|
|
||||||
# D-pad for items/quick slots
|
# D-pad for items/quick slots
|
||||||
"item1": self.controller.press_dpad_up,
|
"dpad_up": self.controller.press_dpad_up,
|
||||||
"item2": self.controller.press_dpad_right,
|
"dpad_right": self.controller.press_dpad_right,
|
||||||
"item3": self.controller.press_dpad_down,
|
"dpad_down": self.controller.press_dpad_down,
|
||||||
"item4": self.controller.press_dpad_left,
|
"dpad_left": self.controller.press_dpad_left,
|
||||||
|
|
||||||
# Menu buttons
|
# Menu buttons
|
||||||
"start": self.controller.press_start,
|
"start": self.controller.press_start,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ class QueueConsumer:
|
||||||
self.running = False
|
self.running = False
|
||||||
self.consumer_thread = None
|
self.consumer_thread = None
|
||||||
self.handlers = {}
|
self.handlers = {}
|
||||||
|
self.input_type = None # Track the current input type: "keyboard" or "controller"
|
||||||
|
|
||||||
logger.info("Queue consumer initialized")
|
logger.info("Queue consumer initialized")
|
||||||
|
|
||||||
|
|
@ -46,6 +47,42 @@ class QueueConsumer:
|
||||||
self.handlers[task_type] = handler
|
self.handlers[task_type] = handler
|
||||||
logger.info(f"Registered handler for {task_type}")
|
logger.info(f"Registered handler for {task_type}")
|
||||||
|
|
||||||
|
def set_input_type(self, input_type: str) -> None:
|
||||||
|
"""
|
||||||
|
Set the current input type
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_type: Either "keyboard" or "controller"
|
||||||
|
"""
|
||||||
|
if input_type not in ["keyboard", "controller"]:
|
||||||
|
logger.error(f"Invalid input type: {input_type}")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.input_type = input_type
|
||||||
|
logger.info(f"Input type set to: {input_type}")
|
||||||
|
|
||||||
|
def validate_command(self, command: str, requested_type: str) -> bool:
|
||||||
|
"""
|
||||||
|
Validate if a command can be executed with the requested input type
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: The command to validate
|
||||||
|
requested_type: The input type being requested ("keyboard" or "controller")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the command is valid for the requested input type
|
||||||
|
"""
|
||||||
|
# If no input type is set yet, allow the request to establish it
|
||||||
|
if self.input_type is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Ensure command matches current input type
|
||||||
|
if self.input_type != requested_type:
|
||||||
|
logger.warning(f"Command type mismatch: Current input type is {self.input_type}, but {requested_type} command received")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
"""Start the consumer in a background thread"""
|
"""Start the consumer in a background thread"""
|
||||||
if self.running:
|
if self.running:
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,74 @@ import os
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
from typing import Dict, Any, Optional, List, Callable, Union
|
import random
|
||||||
|
import cloudpickle
|
||||||
|
from typing import Dict, Any, Optional, List, Callable, Union, Tuple
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
|
||||||
logger = logging.getLogger('twitch-queue')
|
logger = logging.getLogger('twitch-queue')
|
||||||
|
|
||||||
|
# Define constants for input modes
|
||||||
|
INPUT_MODE_KEYBOARD = "keyboard"
|
||||||
|
INPUT_MODE_CONTROLLER = "controller"
|
||||||
|
INPUT_MODE_FILE = "input_mode.txt"
|
||||||
|
|
||||||
|
# Function to get/set the current input mode using persistent storage
|
||||||
|
def get_input_mode():
|
||||||
|
"""Read the current input mode from persistent storage"""
|
||||||
|
try:
|
||||||
|
if os.path.exists(INPUT_MODE_FILE):
|
||||||
|
with open(INPUT_MODE_FILE, 'r') as f:
|
||||||
|
mode = f.read().strip()
|
||||||
|
if mode in [INPUT_MODE_KEYBOARD, INPUT_MODE_CONTROLLER]:
|
||||||
|
return mode
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error reading input mode: {e}")
|
||||||
|
|
||||||
|
# Default to keyboard mode if not set or error
|
||||||
|
return INPUT_MODE_KEYBOARD
|
||||||
|
|
||||||
|
def set_input_mode(mode):
|
||||||
|
"""Save the current input mode to persistent storage"""
|
||||||
|
if mode not in [INPUT_MODE_KEYBOARD, INPUT_MODE_CONTROLLER, None]:
|
||||||
|
logger.error(f"Invalid input mode: {mode}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if mode is None:
|
||||||
|
# Remove the file if mode is None (reset)
|
||||||
|
if os.path.exists(INPUT_MODE_FILE):
|
||||||
|
os.remove(INPUT_MODE_FILE)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Write mode to file
|
||||||
|
with open(INPUT_MODE_FILE, 'w') as f:
|
||||||
|
f.write(mode)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error writing input mode: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# CloudPickle serializer for Huey - wraps cloudpickle to provide the interface Huey expects
|
||||||
|
class CloudPickleSerializer:
|
||||||
|
@staticmethod
|
||||||
|
def serialize(data):
|
||||||
|
return cloudpickle.dumps(data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def deserialize(data):
|
||||||
|
if data:
|
||||||
|
return cloudpickle.loads(data)
|
||||||
|
return None
|
||||||
|
|
||||||
# Create a Huey instance with SQLite storage
|
# Create a Huey instance with SQLite storage
|
||||||
huey = SqliteHuey(
|
huey = SqliteHuey(
|
||||||
name='twitch_commands',
|
name='twitch_commands',
|
||||||
filename='twitch_queue.db',
|
filename='twitch_queue.db',
|
||||||
results=True, # Store task results
|
results=False, # Store task results
|
||||||
immediate=False # Don't execute tasks immediately (use consumer)
|
immediate=False, # Don't execute tasks immediately (use consumer)
|
||||||
|
serializer=CloudPickleSerializer # Use cloudpickle for better serialization
|
||||||
)
|
)
|
||||||
|
|
||||||
# Queue statistics
|
# Queue statistics
|
||||||
|
|
@ -60,120 +116,190 @@ queue_stats = QueueStats()
|
||||||
IRC_SERVER = 'irc.chat.twitch.tv'
|
IRC_SERVER = 'irc.chat.twitch.tv'
|
||||||
IRC_PORT = 6667
|
IRC_PORT = 6667
|
||||||
|
|
||||||
# Task definitions
|
# Command handler type
|
||||||
@huey.task()
|
CommandHandler = Callable[[str, List[str], str, str, str], bool]
|
||||||
def process_chat_command(username: str, command: str, args: List[str],
|
|
||||||
channel: str, oauth_token: str, bot_username: str,
|
|
||||||
command_registry: Dict[str, str]) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Process a chat command and return the result
|
|
||||||
|
|
||||||
This task will be executed by the Huey consumer
|
# Command registry - maps command names to handler functions
|
||||||
"""
|
command_registry: Dict[str, CommandHandler] = {}
|
||||||
logger.info(f"Processing command '{command}' from {username} with args: {args}")
|
|
||||||
|
|
||||||
# Record stats
|
# Try to import the game controller module for controller support
|
||||||
queue_stats.log_processed(f"command:{command}")
|
try:
|
||||||
|
# Since we're running in a separate process, we need to initialize our own controller
|
||||||
|
# Avoid importing GameController directly to prevent circular imports
|
||||||
|
from src.game.input.keyboard import KeyboardController
|
||||||
|
from src.game.input.gamepad import VirtualController, VGAMEPAD_AVAILABLE
|
||||||
|
|
||||||
# Check if the command is registered
|
# Create controller instances - these will be initialized when needed
|
||||||
if command not in command_registry:
|
keyboard_controller = None
|
||||||
logger.warning(f"Command '{command}' not found in registry")
|
gamepad_controller = None
|
||||||
return {
|
|
||||||
'success': False,
|
CONTROLLER_SUPPORT = True
|
||||||
'error': f"Command '{command}' not found",
|
logger.info("Game controller support loaded in queue worker")
|
||||||
'username': username,
|
except ImportError as e:
|
||||||
'command': command,
|
logger.warning(f"Game controller support not available in queue worker: {e}")
|
||||||
'processed_at': time.time()
|
CONTROLLER_SUPPORT = False
|
||||||
}
|
|
||||||
|
# ---- CONTROLLER MANAGEMENT ----
|
||||||
|
|
||||||
|
def initialize_controller(controller_type=INPUT_MODE_KEYBOARD):
|
||||||
|
"""Initialize the appropriate controller type"""
|
||||||
|
global keyboard_controller, gamepad_controller
|
||||||
|
|
||||||
|
if not CONTROLLER_SUPPORT:
|
||||||
|
logger.warning("Controller support not available")
|
||||||
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# For built-in commands we handle here
|
if controller_type == INPUT_MODE_KEYBOARD:
|
||||||
if command == "hello":
|
if keyboard_controller is None:
|
||||||
send_twitch_message(channel, oauth_token, bot_username, f"Hello, {username}!")
|
logger.info("Initializing keyboard controller in queue worker")
|
||||||
elif command == "dice":
|
keyboard_controller = KeyboardController()
|
||||||
import random
|
return keyboard_controller is not None
|
||||||
sides = 6 # Default to 6-sided dice
|
elif controller_type == INPUT_MODE_CONTROLLER:
|
||||||
if args and args[0].isdigit():
|
if not VGAMEPAD_AVAILABLE:
|
||||||
sides = int(args[0])
|
logger.warning("vgamepad not available, can't initialize controller")
|
||||||
result = random.randint(1, sides)
|
return False
|
||||||
send_twitch_message(channel, oauth_token, bot_username, f"@{username} rolled a {result} (d{sides})")
|
|
||||||
elif command == "echo":
|
if gamepad_controller is None:
|
||||||
if args:
|
logger.info("Initializing gamepad controller in queue worker")
|
||||||
message = " ".join(args)
|
gamepad_controller = VirtualController()
|
||||||
send_twitch_message(channel, oauth_token, bot_username, f"Echo: {message}")
|
# Give it a moment to initialize
|
||||||
else:
|
time.sleep(0.2)
|
||||||
send_twitch_message(channel, oauth_token, bot_username, f"@{username}, you didn't provide a message to echo!")
|
|
||||||
elif command == "8ball":
|
# Check if controller was properly initialized
|
||||||
import random
|
if not gamepad_controller.is_available():
|
||||||
responses = [
|
logger.error("VirtualController was created but is not available")
|
||||||
"It is certain.",
|
return False
|
||||||
"It is decidedly so.",
|
|
||||||
"Without a doubt.",
|
logger.info("Virtual gamepad controller successfully initialized")
|
||||||
"Yes, definitely.",
|
|
||||||
"You may rely on it.",
|
return gamepad_controller is not None and gamepad_controller.is_available()
|
||||||
"As I see it, yes.",
|
|
||||||
"Most likely.",
|
|
||||||
"Outlook good.",
|
|
||||||
"Yes.",
|
|
||||||
"Signs point to yes.",
|
|
||||||
"Reply hazy, try again.",
|
|
||||||
"Ask again later.",
|
|
||||||
"Better not tell you now.",
|
|
||||||
"Cannot predict now.",
|
|
||||||
"Concentrate and ask again.",
|
|
||||||
"Don't count on it.",
|
|
||||||
"My reply is no.",
|
|
||||||
"My sources say no.",
|
|
||||||
"Outlook not so good.",
|
|
||||||
"Very doubtful."
|
|
||||||
]
|
|
||||||
send_twitch_message(channel, oauth_token, bot_username, f"@{username}, {random.choice(responses)}")
|
|
||||||
elif command == "qstats":
|
|
||||||
stats = get_queue_stats()
|
|
||||||
send_twitch_message(channel, oauth_token, bot_username,
|
|
||||||
f"Queue stats: {stats['total_processed']}/{stats['total_enqueued']} processed, {stats['pending']} pending")
|
|
||||||
else:
|
else:
|
||||||
# For any custom commands, we'd need to implement them here
|
logger.warning(f"Unknown controller type: {controller_type}")
|
||||||
send_twitch_message(channel, oauth_token, bot_username,
|
return False
|
||||||
f"Command '{command}' is registered but not implemented in the queue worker.")
|
|
||||||
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'username': username,
|
|
||||||
'command': command,
|
|
||||||
'args': args,
|
|
||||||
'processed_at': time.time()
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error executing command {command}: {e}")
|
logger.error(f"Error initializing controller: {e}")
|
||||||
return {
|
return False
|
||||||
'success': False,
|
|
||||||
'error': str(e),
|
|
||||||
'username': username,
|
|
||||||
'command': command,
|
|
||||||
'args': args,
|
|
||||||
'processed_at': time.time()
|
|
||||||
}
|
|
||||||
|
|
||||||
@huey.task()
|
# Maintain the current input mode - either "keyboard" or "controller"
|
||||||
def process_chat_message(username: str, message: str) -> Dict[str, Any]:
|
# This global variable is no longer used - see get_input_mode() and set_input_mode() functions
|
||||||
"""
|
# for the persistent file-based approach that works across processes
|
||||||
Process a regular chat message
|
|
||||||
|
|
||||||
This task will be executed by the Huey consumer
|
def execute_game_command(command, controller_type=INPUT_MODE_KEYBOARD):
|
||||||
"""
|
"""Execute a game controller command"""
|
||||||
logger.info(f"Processing message from {username}: {message}")
|
logger.info(f"execute_game_command called for '{command}' with controller_type={controller_type}")
|
||||||
|
|
||||||
# Record stats
|
if not CONTROLLER_SUPPORT:
|
||||||
queue_stats.log_processed("message")
|
logger.warning("Controller support not available")
|
||||||
|
return False
|
||||||
|
|
||||||
# Return information about the processed message
|
try:
|
||||||
return {
|
if controller_type == INPUT_MODE_KEYBOARD:
|
||||||
'success': True,
|
# Make sure keyboard controller is initialized
|
||||||
'username': username,
|
if keyboard_controller is None and not initialize_controller(INPUT_MODE_KEYBOARD):
|
||||||
'message': message,
|
logger.error("Failed to initialize keyboard controller")
|
||||||
'processed_at': time.time()
|
return False
|
||||||
}
|
|
||||||
|
if keyboard_controller is None:
|
||||||
|
logger.error("Keyboard controller initialization failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Map commands to keyboard controller methods
|
||||||
|
command_mapping = {
|
||||||
|
"up": lambda: keyboard_controller.press_key('w'),
|
||||||
|
"down": lambda: keyboard_controller.press_key('s'),
|
||||||
|
"left": lambda: keyboard_controller.press_key('a'),
|
||||||
|
"right": lambda: keyboard_controller.press_key('d'),
|
||||||
|
"jump": lambda: keyboard_controller.press_key('space'),
|
||||||
|
"attack": lambda: keyboard_controller.press_mouse_button('left'),
|
||||||
|
"interact": lambda: keyboard_controller.press_key('e'),
|
||||||
|
"inventory": lambda: keyboard_controller.press_key('i'),
|
||||||
|
"skill1": lambda: keyboard_controller.press_key('1'),
|
||||||
|
"skill2": lambda: keyboard_controller.press_key('2'),
|
||||||
|
"skill3": lambda: keyboard_controller.press_key('3'),
|
||||||
|
"ultimate": lambda: keyboard_controller.press_key('r'),
|
||||||
|
}
|
||||||
|
|
||||||
|
if command in command_mapping:
|
||||||
|
logger.info(f"Executing keyboard command: {command}")
|
||||||
|
try:
|
||||||
|
command_mapping[command]()
|
||||||
|
# Release key after a short delay
|
||||||
|
time.sleep(0.1)
|
||||||
|
keyboard_controller.release_all()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error executing keyboard command {command}: {e}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.warning(f"Unknown keyboard command: {command}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif controller_type == INPUT_MODE_CONTROLLER:
|
||||||
|
# Make sure gamepad controller is initialized
|
||||||
|
if gamepad_controller is None and not initialize_controller(INPUT_MODE_CONTROLLER):
|
||||||
|
logger.error("Failed to initialize gamepad controller")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if gamepad_controller is None:
|
||||||
|
logger.error("Gamepad controller initialization failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Map commands to gamepad controller methods
|
||||||
|
command_mapping = {
|
||||||
|
"up": lambda: gamepad_controller.move_left_stick_up(),
|
||||||
|
"down": lambda: gamepad_controller.move_left_stick_down(),
|
||||||
|
"left": lambda: gamepad_controller.move_left_stick_left(),
|
||||||
|
"right": lambda: gamepad_controller.move_left_stick_right(),
|
||||||
|
"look_up": lambda: gamepad_controller.move_right_stick_up(),
|
||||||
|
"look_down": lambda: gamepad_controller.move_right_stick_down(),
|
||||||
|
"look_left": lambda: gamepad_controller.move_right_stick_left(),
|
||||||
|
"look_right": lambda: gamepad_controller.move_right_stick_right(),
|
||||||
|
"jump": lambda: gamepad_controller.press_a(),
|
||||||
|
"action": lambda: gamepad_controller.press_b(),
|
||||||
|
"interact": lambda: gamepad_controller.press_x(),
|
||||||
|
"menu": lambda: gamepad_controller.press_y(),
|
||||||
|
"block": lambda: gamepad_controller.press_left_shoulder(),
|
||||||
|
"attack": lambda: gamepad_controller.press_right_shoulder(),
|
||||||
|
"aim": lambda: gamepad_controller.press_left_trigger(),
|
||||||
|
"shoot": lambda: gamepad_controller.press_right_trigger(),
|
||||||
|
"dup": lambda: gamepad_controller.press_dpad_up(),
|
||||||
|
"dright": lambda: gamepad_controller.press_dpad_right(),
|
||||||
|
"ddown": lambda: gamepad_controller.press_dpad_down(),
|
||||||
|
"dleft": lambda: gamepad_controller.press_dpad_left(),
|
||||||
|
"start": lambda: gamepad_controller.press_start(),
|
||||||
|
"select": lambda: gamepad_controller.press_back(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if command in command_mapping:
|
||||||
|
logger.info(f"Executing controller command: {command}")
|
||||||
|
try:
|
||||||
|
command_mapping[command]()
|
||||||
|
# Release buttons after a short delay
|
||||||
|
time.sleep(0.1)
|
||||||
|
gamepad_controller.reset()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error executing controller command {command}: {e}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.warning(f"Unknown controller command: {command}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.warning(f"Unknown controller type: {controller_type}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error executing game command: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Function to reset input mode (e.g., when switching games)
|
||||||
|
def reset_input_mode():
|
||||||
|
"""Reset the current input mode to allow switching between keyboard and controller"""
|
||||||
|
# Use persistent storage for input mode
|
||||||
|
set_input_mode(None)
|
||||||
|
logger.info("Input mode has been reset")
|
||||||
|
|
||||||
|
# ---- COMMUNICATION UTILITIES ----
|
||||||
|
|
||||||
def send_twitch_message(channel: str, oauth_token: str, username: str, message: str) -> bool:
|
def send_twitch_message(channel: str, oauth_token: str, username: str, message: str) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
@ -204,20 +330,508 @@ def send_twitch_message(channel: str, oauth_token: str, username: str, message:
|
||||||
logger.error(f"Error sending message to Twitch: {e}")
|
logger.error(f"Error sending message to Twitch: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# ---- COMMAND HANDLERS ----
|
||||||
|
|
||||||
|
# Define command handler for each specific command
|
||||||
|
# Each will be registered as a separate Huey task
|
||||||
|
|
||||||
|
@huey.task()
|
||||||
|
def handle_hello_task(username: str, args: List[str], channel: str, oauth_token: str, bot_username: str) -> Dict[str, Any]:
|
||||||
|
"""Handler for !hello command"""
|
||||||
|
success = send_twitch_message(channel, oauth_token, bot_username, f"Hello, {username}!")
|
||||||
|
return {
|
||||||
|
'success': success,
|
||||||
|
'username': username,
|
||||||
|
'command': 'hello',
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
@huey.task()
|
||||||
|
def handle_dice_task(username: str, args: List[str], channel: str, oauth_token: str, bot_username: str) -> Dict[str, Any]:
|
||||||
|
"""Handler for !dice command"""
|
||||||
|
sides = 6 # Default to 6-sided dice
|
||||||
|
if args and args[0].isdigit():
|
||||||
|
sides = int(args[0])
|
||||||
|
result = random.randint(1, sides)
|
||||||
|
success = send_twitch_message(channel, oauth_token, bot_username, f"@{username} rolled a {result} (d{sides})")
|
||||||
|
return {
|
||||||
|
'success': success,
|
||||||
|
'username': username,
|
||||||
|
'command': 'dice',
|
||||||
|
'result': result,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
@huey.task()
|
||||||
|
def handle_echo_task(username: str, args: List[str], channel: str, oauth_token: str, bot_username: str) -> Dict[str, Any]:
|
||||||
|
"""Handler for !echo command"""
|
||||||
|
if args:
|
||||||
|
message = " ".join(args)
|
||||||
|
success = send_twitch_message(channel, oauth_token, bot_username, f"Echo: {message}")
|
||||||
|
else:
|
||||||
|
success = send_twitch_message(channel, oauth_token, bot_username, f"@{username}, you didn't provide a message to echo!")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': success,
|
||||||
|
'username': username,
|
||||||
|
'command': 'echo',
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
@huey.task()
|
||||||
|
def handle_8ball_task(username: str, args: List[str], channel: str, oauth_token: str, bot_username: str) -> Dict[str, Any]:
|
||||||
|
"""Handler for !8ball command"""
|
||||||
|
responses = [
|
||||||
|
"It is certain.",
|
||||||
|
"It is decidedly so.",
|
||||||
|
"Without a doubt.",
|
||||||
|
"Yes, definitely.",
|
||||||
|
"You may rely on it.",
|
||||||
|
"As I see it, yes.",
|
||||||
|
"Most likely.",
|
||||||
|
"Outlook good.",
|
||||||
|
"Yes.",
|
||||||
|
"Signs point to yes.",
|
||||||
|
"Reply hazy, try again.",
|
||||||
|
"Ask again later.",
|
||||||
|
"Better not tell you now.",
|
||||||
|
"Cannot predict now.",
|
||||||
|
"Concentrate and ask again.",
|
||||||
|
"Don't count on it.",
|
||||||
|
"My reply is no.",
|
||||||
|
"My sources say no.",
|
||||||
|
"Outlook not so good.",
|
||||||
|
"Very doubtful."
|
||||||
|
]
|
||||||
|
response = random.choice(responses)
|
||||||
|
success = send_twitch_message(channel, oauth_token, bot_username, f"@{username}, {response}")
|
||||||
|
return {
|
||||||
|
'success': success,
|
||||||
|
'username': username,
|
||||||
|
'command': '8ball',
|
||||||
|
'response': response,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
@huey.task()
|
||||||
|
def handle_qstats_task(username: str, args: List[str], channel: str, oauth_token: str, bot_username: str) -> Dict[str, Any]:
|
||||||
|
"""Handler for !qstats command"""
|
||||||
|
stats = get_queue_stats()
|
||||||
|
success = send_twitch_message(channel, oauth_token, bot_username,
|
||||||
|
f"Queue stats: {stats['total_processed']}/{stats['total_enqueued']} processed, {stats['pending']} pending")
|
||||||
|
|
||||||
|
# Only return simple picklable stats, not the whole QueueStats object
|
||||||
|
safe_stats = {
|
||||||
|
'total_enqueued': stats['total_enqueued'],
|
||||||
|
'total_processed': stats['total_processed'],
|
||||||
|
'pending': stats['pending']
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': success,
|
||||||
|
'username': username,
|
||||||
|
'command': 'qstats',
|
||||||
|
'stats': safe_stats,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
@huey.task()
|
||||||
|
def handle_keyboard_command_task(username: str, args: List[str], channel: str, oauth_token: str, bot_username: str, command: str) -> Dict[str, Any]:
|
||||||
|
"""Handler for keyboard mode game commands"""
|
||||||
|
try:
|
||||||
|
# Check if current input mode is compatible
|
||||||
|
current_mode = get_input_mode()
|
||||||
|
if current_mode != INPUT_MODE_KEYBOARD:
|
||||||
|
message = f"@{username}, unable to use keyboard commands while controller mode is active"
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, message)
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'username': username,
|
||||||
|
'command': command,
|
||||||
|
'command_type': 'keyboard',
|
||||||
|
'error': 'input_mode_mismatch',
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep the controller operations completely isolated
|
||||||
|
execute_success = False
|
||||||
|
if initialize_controller(INPUT_MODE_KEYBOARD):
|
||||||
|
# Execute the command
|
||||||
|
execute_success = execute_game_command(command, INPUT_MODE_KEYBOARD)
|
||||||
|
if execute_success:
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, f"@{username} used {command}")
|
||||||
|
else:
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, f"@{username}, failed to execute {command}")
|
||||||
|
else:
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, f"@{username}, keyboard controller not available")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in keyboard command task: {e}")
|
||||||
|
execute_success = False
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, f"@{username}, error executing {command}: {str(e)}")
|
||||||
|
|
||||||
|
# Only return simple, picklable data with no references to any controller objects
|
||||||
|
return {
|
||||||
|
'success': execute_success,
|
||||||
|
'username': username,
|
||||||
|
'command': command,
|
||||||
|
'command_type': 'keyboard',
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
@huey.task()
|
||||||
|
def handle_controller_command_task(username: str, args: List[str], channel: str, oauth_token: str, bot_username: str, command: str) -> Dict[str, Any]:
|
||||||
|
"""Handler for controller mode game commands"""
|
||||||
|
logger.info(f"Controller command task executing for '{command}'")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if current input mode is compatible
|
||||||
|
current_mode = get_input_mode()
|
||||||
|
if current_mode != INPUT_MODE_CONTROLLER:
|
||||||
|
message = f"@{username}, unable to use controller commands while keyboard mode is active"
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, message)
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'username': username,
|
||||||
|
'command': command,
|
||||||
|
'command_type': 'controller',
|
||||||
|
'error': 'input_mode_mismatch',
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep the controller operations completely isolated
|
||||||
|
execute_success = False
|
||||||
|
# Regular command flow for all controller commands
|
||||||
|
if initialize_controller(INPUT_MODE_CONTROLLER):
|
||||||
|
execute_success = execute_game_command(command, INPUT_MODE_CONTROLLER)
|
||||||
|
if execute_success:
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, f"@{username} used {command}")
|
||||||
|
else:
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, f"@{username}, failed to execute {command}")
|
||||||
|
else:
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, f"@{username}, controller support not available")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in controller command task: {e}")
|
||||||
|
execute_success = False
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, f"@{username}, error executing {command}: {str(e)}")
|
||||||
|
|
||||||
|
# Only return simple, picklable data with no references to any controller objects
|
||||||
|
return {
|
||||||
|
'success': execute_success,
|
||||||
|
'username': username,
|
||||||
|
'command': command,
|
||||||
|
'command_type': 'controller',
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
@huey.task()
|
||||||
|
def handle_unknown_command_task(username: str, command: str, args: List[str], channel: str, oauth_token: str, bot_username: str) -> Dict[str, Any]:
|
||||||
|
"""Handle commands that aren't explicitly registered"""
|
||||||
|
logger.info(f"Attempting to handle unknown command: '{command}'")
|
||||||
|
|
||||||
|
# Define valid command lists
|
||||||
|
keyboard_commands = [
|
||||||
|
"up", "down", "left", "right", "jump", "attack", "interact",
|
||||||
|
"inventory", "skill1", "skill2", "skill3", "ultimate"
|
||||||
|
]
|
||||||
|
|
||||||
|
controller_commands = [
|
||||||
|
"look_up", "look_down", "look_left", "look_right",
|
||||||
|
"action", "menu", "block", "aim", "shoot",
|
||||||
|
"dup", "dright", "ddown", "dleft", "start", "select"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Check if it's a keyboard command
|
||||||
|
if command in keyboard_commands and CONTROLLER_SUPPORT:
|
||||||
|
return handle_keyboard_command_task(username, args, channel, oauth_token, bot_username, command)
|
||||||
|
|
||||||
|
# Check if it's a controller command
|
||||||
|
if command in controller_commands and CONTROLLER_SUPPORT:
|
||||||
|
return handle_controller_command_task(username, args, channel, oauth_token, bot_username, command)
|
||||||
|
|
||||||
|
# Handle a few standard commands that might not be registered
|
||||||
|
if command == "help":
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username,
|
||||||
|
f"@{username}, available commands: !hello, !dice, !echo, !8ball, !qstats")
|
||||||
|
if CONTROLLER_SUPPORT:
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username,
|
||||||
|
f"Game commands: !up, !down, !left, !right, !jump, etc.")
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username,
|
||||||
|
f"Admins can use !switchmode [keyboard|controller|reset] to change input mode")
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'username': username,
|
||||||
|
'command': 'help',
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
elif command == "ping":
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, f"@{username}, Pong!")
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'username': username,
|
||||||
|
'command': 'ping',
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Command not recognized
|
||||||
|
logger.warning(f"Command '{command}' not recognized as a known command")
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, f"Sorry @{username}, the command !{command} is not supported.")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f"Command '{command}' not found",
|
||||||
|
'username': username,
|
||||||
|
'command': command,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
@huey.task()
|
||||||
|
def handle_switchmode_task(username: str, args: List[str], channel: str, oauth_token: str, bot_username: str) -> Dict[str, Any]:
|
||||||
|
"""Handler for !switchmode command to switch between keyboard and controller modes"""
|
||||||
|
# Only allow admins to switch modes
|
||||||
|
from src.core.twitch import is_admin
|
||||||
|
if not is_admin(username, channel):
|
||||||
|
message = f"@{username}, only channel admins can switch input modes"
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, message)
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'username': username,
|
||||||
|
'command': 'switchmode',
|
||||||
|
'error': 'not_admin',
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if a new mode is specified
|
||||||
|
if not args:
|
||||||
|
current = get_input_mode()
|
||||||
|
message = f"@{username}, current input mode is: {current}. Use !switchmode keyboard or !switchmode controller to change"
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, message)
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'username': username,
|
||||||
|
'command': 'switchmode',
|
||||||
|
'current_mode': current,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the requested mode
|
||||||
|
requested_mode = args[0].lower()
|
||||||
|
if requested_mode not in [INPUT_MODE_KEYBOARD, INPUT_MODE_CONTROLLER, "reset"]:
|
||||||
|
message = f"@{username}, invalid mode: {requested_mode}. Use 'keyboard', 'controller', or 'reset'"
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, message)
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'username': username,
|
||||||
|
'command': 'switchmode',
|
||||||
|
'error': 'invalid_mode',
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
# If "reset" is specified, reset the mode
|
||||||
|
if requested_mode == "reset":
|
||||||
|
reset_input_mode()
|
||||||
|
message = f"@{username} reset input mode. Users can now use either keyboard or controller commands"
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, message)
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'username': username,
|
||||||
|
'command': 'switchmode',
|
||||||
|
'new_mode': None,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set the new mode
|
||||||
|
old_mode = get_input_mode()
|
||||||
|
logger.info(f"Switching input mode from {old_mode} to {requested_mode}")
|
||||||
|
|
||||||
|
# First, save the mode to the persistent storage
|
||||||
|
set_input_mode(requested_mode)
|
||||||
|
|
||||||
|
# Then initialize the controller based on the saved mode
|
||||||
|
initialize_success = initialize_controller(requested_mode)
|
||||||
|
|
||||||
|
if initialize_success:
|
||||||
|
message = f"@{username} switched input mode to {requested_mode}"
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, message)
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'username': username,
|
||||||
|
'command': 'switchmode',
|
||||||
|
'old_mode': old_mode,
|
||||||
|
'new_mode': requested_mode,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# If initialization failed, revert to the old mode
|
||||||
|
set_input_mode(old_mode)
|
||||||
|
message = f"@{username}, failed to switch to {requested_mode} mode. Staying with {old_mode or 'no'} mode"
|
||||||
|
send_twitch_message(channel, oauth_token, bot_username, message)
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'username': username,
|
||||||
|
'command': 'switchmode',
|
||||||
|
'error': 'initialization_failed',
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- TASK PROCESSING ----
|
||||||
|
|
||||||
|
@huey.task()
|
||||||
|
def process_chat_command(username: str, command: str, args: List[str],
|
||||||
|
channel: str, oauth_token: str, bot_username: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Process a chat command and return the result
|
||||||
|
|
||||||
|
This task will be executed by the Huey consumer
|
||||||
|
"""
|
||||||
|
logger.info(f"Processing command '{command}' from {username} with args: {args}")
|
||||||
|
|
||||||
|
# Record stats
|
||||||
|
queue_stats.log_processed(f"command:{command}")
|
||||||
|
|
||||||
|
# Determine the current input mode from persistent storage
|
||||||
|
mode = get_input_mode()
|
||||||
|
logger.info(f"Current input mode: {mode}")
|
||||||
|
|
||||||
|
# Create command map based on the current mode
|
||||||
|
command_task_map = {
|
||||||
|
# Standard commands - always available regardless of mode
|
||||||
|
"hello": handle_hello_task,
|
||||||
|
"dice": handle_dice_task,
|
||||||
|
"echo": handle_echo_task,
|
||||||
|
"8ball": handle_8ball_task,
|
||||||
|
"qstats": handle_qstats_task,
|
||||||
|
"switchmode": handle_switchmode_task,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add mode-specific commands
|
||||||
|
if mode == INPUT_MODE_KEYBOARD:
|
||||||
|
# Add keyboard-specific commands
|
||||||
|
keyboard_commands = {
|
||||||
|
# Directional and common commands
|
||||||
|
"up": lambda u, a, c, o, b: handle_keyboard_command_task(u, a, c, o, b, "up"),
|
||||||
|
"down": lambda u, a, c, o, b: handle_keyboard_command_task(u, a, c, o, b, "down"),
|
||||||
|
"left": lambda u, a, c, o, b: handle_keyboard_command_task(u, a, c, o, b, "left"),
|
||||||
|
"right": lambda u, a, c, o, b: handle_keyboard_command_task(u, a, c, o, b, "right"),
|
||||||
|
"jump": lambda u, a, c, o, b: handle_keyboard_command_task(u, a, c, o, b, "jump"),
|
||||||
|
"attack": lambda u, a, c, o, b: handle_keyboard_command_task(u, a, c, o, b, "attack"),
|
||||||
|
"interact": lambda u, a, c, o, b: handle_keyboard_command_task(u, a, c, o, b, "interact"),
|
||||||
|
|
||||||
|
# Keyboard-only commands
|
||||||
|
"inventory": lambda u, a, c, o, b: handle_keyboard_command_task(u, a, c, o, b, "inventory"),
|
||||||
|
"skill1": lambda u, a, c, o, b: handle_keyboard_command_task(u, a, c, o, b, "skill1"),
|
||||||
|
"skill2": lambda u, a, c, o, b: handle_keyboard_command_task(u, a, c, o, b, "skill2"),
|
||||||
|
"skill3": lambda u, a, c, o, b: handle_keyboard_command_task(u, a, c, o, b, "skill3"),
|
||||||
|
"ultimate": lambda u, a, c, o, b: handle_keyboard_command_task(u, a, c, o, b, "ultimate"),
|
||||||
|
}
|
||||||
|
command_task_map.update(keyboard_commands)
|
||||||
|
else: # mode == INPUT_MODE_CONTROLLER
|
||||||
|
# Add controller-specific commands
|
||||||
|
controller_commands = {
|
||||||
|
# Directional and common commands
|
||||||
|
"up": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "up"),
|
||||||
|
"down": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "down"),
|
||||||
|
"left": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "left"),
|
||||||
|
"right": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "right"),
|
||||||
|
"jump": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "jump"),
|
||||||
|
"attack": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "attack"),
|
||||||
|
"interact": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "interact"),
|
||||||
|
|
||||||
|
# Controller-only commands
|
||||||
|
"look_up": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "look_up"),
|
||||||
|
"look_down": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "look_down"),
|
||||||
|
"look_left": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "look_left"),
|
||||||
|
"look_right": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "look_right"),
|
||||||
|
"action": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "action"),
|
||||||
|
"menu": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "menu"),
|
||||||
|
"block": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "block"),
|
||||||
|
"aim": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "aim"),
|
||||||
|
"shoot": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "shoot"),
|
||||||
|
"dup": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "dup"),
|
||||||
|
"dright": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "dright"),
|
||||||
|
"ddown": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "ddown"),
|
||||||
|
"dleft": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "dleft"),
|
||||||
|
"start": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "start"),
|
||||||
|
"select": lambda u, a, c, o, b: handle_controller_command_task(u, a, c, o, b, "select"),
|
||||||
|
}
|
||||||
|
command_task_map.update(controller_commands)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if this is a known command
|
||||||
|
if command in command_task_map:
|
||||||
|
task_handler = command_task_map[command]
|
||||||
|
try:
|
||||||
|
# Execute the dedicated task handler
|
||||||
|
logger.info(f"Found handler for '{command}' in {mode} mode, executing...")
|
||||||
|
return task_handler(username, args, channel, oauth_token, bot_username)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error executing command {command}: {e}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': str(e),
|
||||||
|
'username': username,
|
||||||
|
'command': command,
|
||||||
|
'args': args,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Unknown command - try the fallback handler
|
||||||
|
try:
|
||||||
|
return handle_unknown_command_task(username, command, args, channel, oauth_token, bot_username)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error handling unknown command {command}: {e}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': str(e),
|
||||||
|
'username': username,
|
||||||
|
'command': command,
|
||||||
|
'args': args,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error in process_chat_command: {e}")
|
||||||
|
# Make sure we return a safely picklable object
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': str(e),
|
||||||
|
'username': username,
|
||||||
|
'command': command,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
@huey.task()
|
||||||
|
def process_chat_message(username: str, message: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Process a regular chat message
|
||||||
|
|
||||||
|
This task will be executed by the Huey consumer
|
||||||
|
"""
|
||||||
|
logger.info(f"Processing message from {username}: {message}")
|
||||||
|
|
||||||
|
# Record stats
|
||||||
|
queue_stats.log_processed("message")
|
||||||
|
|
||||||
|
# Return information about the processed message
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'username': username,
|
||||||
|
'message': message,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
# Helper functions for enqueueing tasks
|
# Helper functions for enqueueing tasks
|
||||||
def enqueue_command(username: str, command: str, args: List[str],
|
def enqueue_command(username: str, command: str, args: List[str],
|
||||||
channel: str = None, oauth_token: str = None,
|
channel: str = None, oauth_token: str = None,
|
||||||
bot_username: str = None, command_registry: Dict[str, str] = None) -> None:
|
bot_username: str = None) -> None:
|
||||||
"""Add a command to the processing queue"""
|
"""Add a command to the processing queue"""
|
||||||
logger.info(f"Enqueueing command '{command}' from {username} with args: {args}")
|
logger.info(f"Enqueueing command '{command}' from {username} with args: {args}")
|
||||||
queue_stats.log_enqueue(f"command:{command}", username)
|
queue_stats.log_enqueue(f"command:{command}", username)
|
||||||
|
|
||||||
if not all([channel, oauth_token, bot_username, command_registry]):
|
if not all([channel, oauth_token, bot_username]):
|
||||||
logger.error("Missing required parameters for enqueue_command")
|
logger.error("Missing required parameters for enqueue_command")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return process_chat_command(
|
return process_chat_command(
|
||||||
username, command, args, channel, oauth_token, bot_username, command_registry
|
username, command, args, channel, oauth_token, bot_username
|
||||||
)
|
)
|
||||||
|
|
||||||
def enqueue_message(username: str, message: str) -> None:
|
def enqueue_message(username: str, message: str) -> None:
|
||||||
|
|
@ -239,6 +853,11 @@ def start_consumer() -> None:
|
||||||
python -m huey.bin.huey_consumer src.queue.server.huey
|
python -m huey.bin.huey_consumer src.queue.server.huey
|
||||||
"""
|
"""
|
||||||
from huey.consumer import Consumer
|
from huey.consumer import Consumer
|
||||||
|
|
||||||
|
# Log current input mode at startup
|
||||||
|
current_mode = get_input_mode()
|
||||||
|
logger.info(f"Starting consumer with input mode: {current_mode}")
|
||||||
|
|
||||||
consumer = Consumer(huey)
|
consumer = Consumer(huey)
|
||||||
consumer.start()
|
consumer.start()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue