This commit adds robust diagnostic tools to help users identify and fix authentication issues with Twitch OAuth and Perplexity API. New Features: - test_credentials.py: Comprehensive credential validator with detailed diagnostics - Tests Twitch OAuth token with IRC authentication - Validates Perplexity API key and model access - Provides specific error messages and actionable fixes - Automatically tests fallback models (sonar-pro → sonar) - TROUBLESHOOTING.md: Complete troubleshooting guide - Common error messages and solutions - Step-by-step diagnostic procedures - Quick reference for file locations and commands Improvements to setup_wizard.py: - Enhanced Twitch token validation with detailed error messages - Added Perplexity model fallback (sonar-pro → sonar) - Better error handling with specific solutions - Recommends working model if primary model unavailable Documentation Updates: - README.md: Added credential testing section with examples - CLAUDE.md: Updated with new tools and testing procedures - Comprehensive troubleshooting section in README This addresses the common authentication failures users encounter during initial setup and provides clear paths to resolution.
336 lines
12 KiB
Python
336 lines
12 KiB
Python
"""
|
||
Credential Testing Utility for Eugen Bot
|
||
Tests and validates Twitch OAuth tokens and Perplexity API keys
|
||
"""
|
||
import asyncio
|
||
import socket
|
||
import sys
|
||
import os
|
||
import httpx
|
||
from dotenv import load_dotenv
|
||
|
||
|
||
class Colors:
|
||
"""ANSI color codes"""
|
||
HEADER = '\033[95m'
|
||
OKBLUE = '\033[94m'
|
||
OKCYAN = '\033[96m'
|
||
OKGREEN = '\033[92m'
|
||
WARNING = '\033[93m'
|
||
FAIL = '\033[91m'
|
||
ENDC = '\033[0m'
|
||
BOLD = '\033[1m'
|
||
|
||
|
||
def print_header(text):
|
||
"""Print colored header"""
|
||
print(f"\n{Colors.HEADER}{Colors.BOLD}{'='*70}{Colors.ENDC}")
|
||
print(f"{Colors.HEADER}{Colors.BOLD}{text.center(70)}{Colors.ENDC}")
|
||
print(f"{Colors.HEADER}{Colors.BOLD}{'='*70}{Colors.ENDC}\n")
|
||
|
||
|
||
def print_success(text):
|
||
"""Print success message"""
|
||
print(f"{Colors.OKGREEN}✓ {text}{Colors.ENDC}")
|
||
|
||
|
||
def print_error(text):
|
||
"""Print error message"""
|
||
print(f"{Colors.FAIL}✗ {text}{Colors.ENDC}")
|
||
|
||
|
||
def print_info(text):
|
||
"""Print info message"""
|
||
print(f"{Colors.OKCYAN}ℹ {text}{Colors.ENDC}")
|
||
|
||
|
||
def print_warning(text):
|
||
"""Print warning message"""
|
||
print(f"{Colors.WARNING}⚠ {text}{Colors.ENDC}")
|
||
|
||
|
||
async def test_twitch_token(token, nickname):
|
||
"""
|
||
Test Twitch OAuth token with detailed diagnostics
|
||
|
||
Args:
|
||
token (str): OAuth token
|
||
nickname (str): Bot nickname
|
||
|
||
Returns:
|
||
bool: True if valid
|
||
"""
|
||
print_header("TESTING TWITCH CREDENTIALS")
|
||
|
||
# Check token format
|
||
if not token:
|
||
print_error("Token is empty!")
|
||
print_info("Get a token from: https://twitchtokengenerator.com")
|
||
return False
|
||
|
||
if not token.startswith("oauth:"):
|
||
print_warning(f"Token doesn't start with 'oauth:' (got: {token[:10]}...)")
|
||
print_info("Token should look like: oauth:abcd1234...")
|
||
return False
|
||
|
||
print_info(f"Token format: ✓ Starts with 'oauth:'")
|
||
print_info(f"Token length: {len(token)} characters")
|
||
print_info(f"Bot nickname: {nickname}")
|
||
|
||
# Test IRC connection
|
||
print_info("\nConnecting to Twitch IRC (irc.chat.twitch.tv:6667)...")
|
||
|
||
try:
|
||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||
sock.settimeout(10)
|
||
|
||
# Connect
|
||
try:
|
||
sock.connect(('irc.chat.twitch.tv', 6667))
|
||
print_success("Connected to IRC server")
|
||
except socket.timeout:
|
||
print_error("Connection timed out!")
|
||
print_info("Check your internet connection and firewall settings")
|
||
return False
|
||
except Exception as e:
|
||
print_error(f"Connection failed: {e}")
|
||
return False
|
||
|
||
# Send authentication
|
||
print_info("Sending authentication...")
|
||
sock.send(f"PASS {token}\r\n".encode())
|
||
sock.send(f"NICK {nickname}\r\n".encode())
|
||
|
||
# Wait for response
|
||
response = ""
|
||
try:
|
||
response = sock.recv(2048).decode()
|
||
except socket.timeout:
|
||
print_error("No response from server (timeout)")
|
||
return False
|
||
|
||
# Parse response
|
||
print_info(f"\nServer response received ({len(response)} bytes)")
|
||
|
||
if "Login authentication failed" in response:
|
||
print_error("AUTHENTICATION FAILED!")
|
||
print_info("\nPossible reasons:")
|
||
print(f" 1. Token is expired or invalid")
|
||
print(f" 2. Token doesn't match the bot nickname '{nickname}'")
|
||
print(f" 3. Account that generated the token is banned/restricted")
|
||
print_info("\nHow to fix:")
|
||
print(f" → Go to: https://twitchtokengenerator.com")
|
||
print(f" → Make sure you're logged into Twitch as '{nickname}'")
|
||
print(f" → Generate a new 'Bot Chat Token'")
|
||
print(f" → Update TWITCH_OAUTH_TOKEN in .env")
|
||
return False
|
||
|
||
if ":tmi.twitch.tv 001" in response or "Welcome" in response:
|
||
print_success("AUTHENTICATION SUCCESSFUL!")
|
||
print_info(f"Bot '{nickname}' can connect to Twitch IRC")
|
||
return True
|
||
|
||
# Ambiguous response
|
||
print_warning("Received unexpected response:")
|
||
for line in response.split('\r\n'):
|
||
if line:
|
||
print(f" {line[:100]}")
|
||
|
||
# If we got this far without clear failure, consider it success
|
||
if "authentication failed" not in response.lower():
|
||
print_success("Authentication appears successful (no error detected)")
|
||
return True
|
||
|
||
return False
|
||
|
||
except Exception as e:
|
||
print_error(f"Error testing Twitch token: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return False
|
||
|
||
|
||
async def test_perplexity_key(api_key, model="sonar-pro"):
|
||
"""
|
||
Test Perplexity API key with model fallback
|
||
|
||
Args:
|
||
api_key (str): Perplexity API key
|
||
model (str): Model to test (will fallback to 'sonar' if needed)
|
||
|
||
Returns:
|
||
tuple: (success, recommended_model)
|
||
"""
|
||
print_header("TESTING PERPLEXITY API")
|
||
|
||
# Check key format
|
||
if not api_key:
|
||
print_error("API key is empty!")
|
||
print_info("Get a key from: https://www.perplexity.ai/settings/api")
|
||
return False, None
|
||
|
||
if not api_key.startswith("pplx-"):
|
||
print_warning(f"API key doesn't start with 'pplx-' (got: {api_key[:10]}...)")
|
||
print_info("Key should look like: pplx-abc123...")
|
||
|
||
print_info(f"API key format: Starts with '{api_key[:5]}...'")
|
||
print_info(f"API key length: {len(api_key)} characters")
|
||
|
||
# Test with different models
|
||
models_to_test = [model]
|
||
if model != "sonar":
|
||
models_to_test.append("sonar")
|
||
|
||
for test_model in models_to_test:
|
||
print_info(f"\nTesting with model: {test_model}")
|
||
|
||
try:
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
response = await client.post(
|
||
"https://api.perplexity.ai/chat/completions",
|
||
json={
|
||
"model": test_model,
|
||
"messages": [{"role": "user", "content": "Say 'test successful' and nothing else"}],
|
||
"max_tokens": 10
|
||
},
|
||
headers={
|
||
"Authorization": f"Bearer {api_key}",
|
||
"Content-Type": "application/json"
|
||
}
|
||
)
|
||
|
||
print_info(f"Response status: {response.status_code}")
|
||
|
||
if response.status_code == 200:
|
||
print_success(f"API KEY VALID with model '{test_model}'!")
|
||
|
||
# Try to parse response
|
||
try:
|
||
data = response.json()
|
||
if "choices" in data and len(data["choices"]) > 0:
|
||
content = data["choices"][0]["message"]["content"]
|
||
print_info(f"Test response: '{content}'")
|
||
except:
|
||
pass
|
||
|
||
return True, test_model
|
||
|
||
elif response.status_code == 401:
|
||
print_error("API key is INVALID (401 Unauthorized)")
|
||
print_info("\nHow to fix:")
|
||
print(f" → Go to: https://www.perplexity.ai/settings/api")
|
||
print(f" → Generate a new API key")
|
||
print(f" → Update PERPLEXITY_API_KEY in .env")
|
||
return False, None
|
||
|
||
elif response.status_code == 400:
|
||
try:
|
||
error_data = response.json()
|
||
error_msg = error_data.get("error", {}).get("message", "Unknown error")
|
||
print_warning(f"400 Bad Request: {error_msg}")
|
||
|
||
# Check if it's a model access issue
|
||
if "model" in error_msg.lower():
|
||
print_info(f"Model '{test_model}' not available on your plan")
|
||
if test_model != models_to_test[-1]:
|
||
print_info("Trying fallback model...")
|
||
continue
|
||
else:
|
||
print_info(f"Error details: {error_msg}")
|
||
except:
|
||
print_warning(f"400 Bad Request (couldn't parse error)")
|
||
|
||
# If this was the last model, fail
|
||
if test_model == models_to_test[-1]:
|
||
print_error("All models failed!")
|
||
return False, None
|
||
|
||
elif response.status_code == 429:
|
||
print_error("Rate limited! Too many requests.")
|
||
print_info("Wait a few minutes and try again")
|
||
return False, None
|
||
|
||
else:
|
||
print_warning(f"Unexpected status code: {response.status_code}")
|
||
try:
|
||
print_info(f"Response: {response.text[:200]}")
|
||
except:
|
||
pass
|
||
|
||
# Try next model if available
|
||
if test_model != models_to_test[-1]:
|
||
continue
|
||
|
||
return False, None
|
||
|
||
except httpx.TimeoutException:
|
||
print_error("Request timed out!")
|
||
print_info("Check your internet connection")
|
||
return False, None
|
||
except Exception as e:
|
||
print_error(f"Error testing API: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return False, None
|
||
|
||
return False, None
|
||
|
||
|
||
async def main():
|
||
"""Main test runner"""
|
||
print(f"""
|
||
{Colors.HEADER}{Colors.BOLD}╔════════════════════════════════════════════════════════════════════╗
|
||
║ EUGEN BOT - CREDENTIAL VALIDATOR ║
|
||
║ Tests Twitch & Perplexity Credentials ║
|
||
╚════════════════════════════════════════════════════════════════════╝{Colors.ENDC}
|
||
""")
|
||
|
||
# Load .env file
|
||
if not os.path.exists('.env'):
|
||
print_error("No .env file found!")
|
||
print_info("Please run: python setup_wizard.py")
|
||
sys.exit(1)
|
||
|
||
load_dotenv()
|
||
|
||
# Get credentials
|
||
twitch_token = os.getenv('TWITCH_OAUTH_TOKEN', '')
|
||
twitch_nickname = os.getenv('TWITCH_BOT_NICKNAME', '')
|
||
twitch_channel = os.getenv('TWITCH_CHANNEL', '')
|
||
perplexity_key = os.getenv('PERPLEXITY_API_KEY', '')
|
||
perplexity_model = os.getenv('PERPLEXITY_MODEL', 'sonar-pro')
|
||
|
||
print_info("Loaded configuration from .env:")
|
||
print(f" • Bot Nickname: {twitch_nickname}")
|
||
print(f" • Channel: {twitch_channel}")
|
||
print(f" • Model: {perplexity_model}")
|
||
|
||
# Test credentials
|
||
results = {}
|
||
|
||
# Test Twitch
|
||
results['twitch'] = await test_twitch_token(twitch_token, twitch_nickname)
|
||
|
||
# Test Perplexity
|
||
results['perplexity'], recommended_model = await test_perplexity_key(perplexity_key, perplexity_model)
|
||
|
||
# Final summary
|
||
print_header("VALIDATION SUMMARY")
|
||
|
||
print(f"Twitch IRC: {'✓ PASS' if results['twitch'] else '✗ FAIL'}")
|
||
print(f"Perplexity API: {'✓ PASS' if results['perplexity'] else '✗ FAIL'}")
|
||
|
||
if recommended_model and recommended_model != perplexity_model:
|
||
print_warning(f"\nRecommendation: Update PERPLEXITY_MODEL to '{recommended_model}' in .env")
|
||
|
||
if all(results.values()):
|
||
print_success("\n🎉 All credentials valid! Bot is ready to run.")
|
||
print_info("Start the bot with: python chatbot.py")
|
||
sys.exit(0)
|
||
else:
|
||
print_error("\n❌ Some credentials are invalid. Please fix and re-test.")
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main())
|