This commit is contained in:
Slipstream 2025-05-03 18:01:54 -06:00
parent 1b825f3dd6
commit c26fbda939
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
2 changed files with 189 additions and 68 deletions

View File

@ -1,6 +1,7 @@
import os
import json
import sys
import asyncio
from typing import Dict, List, Optional, Any
from fastapi import FastAPI, HTTPException, Depends, Header, Request, Response, status
from fastapi.middleware.cors import CORSMiddleware
@ -44,6 +45,7 @@ class ApiSettings(BaseSettings):
DISCORD_CLIENT_ID: str
DISCORD_CLIENT_SECRET: str
DISCORD_REDIRECT_URI: str
DISCORD_BOT_TOKEN: str # Add bot token for API calls
# Secret key for dashboard session management
DASHBOARD_SECRET_KEY: str = "a_default_secret_key_for_development_only" # Provide a default for dev
@ -569,32 +571,53 @@ async def verify_dashboard_guild_admin(guild_id: int, current_user: dict = Depen
user_headers = {'Authorization': f'Bearer {current_user["access_token"]}'}
try:
log.debug(f"Dashboard: Verifying admin status for user {current_user['user_id']} in guild {guild_id}")
async with http_session.get(DISCORD_USER_GUILDS_URL, headers=user_headers) as resp:
if resp.status == 401:
# Clear session if token is invalid
# request.session.clear() # Cannot access request here directly
raise HTTPException(status_code=401, detail="Discord token invalid or expired. Please re-login.")
resp.raise_for_status()
user_guilds = await resp.json()
ADMINISTRATOR_PERMISSION = 0x8
is_admin = False
for guild in user_guilds:
if int(guild['id']) == guild_id:
permissions = int(guild['permissions'])
if (permissions & ADMINISTRATOR_PERMISSION) == ADMINISTRATOR_PERMISSION:
is_admin = True
break # Found the guild and user is admin
# Add rate limit handling
max_retries = 3
retry_count = 0
retry_after = 0
if not is_admin:
log.warning(f"Dashboard: User {current_user['user_id']} is not admin or not in guild {guild_id}.")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User is not an administrator of this guild.")
while retry_count < max_retries:
if retry_after > 0:
log.warning(f"Dashboard: Rate limited by Discord API, waiting {retry_after} seconds before retry")
await asyncio.sleep(retry_after)
log.debug(f"Dashboard: User {current_user['user_id']} verified as admin for guild {guild_id}.")
return True # Indicate verification success
async with http_session.get(DISCORD_USER_GUILDS_URL, headers=user_headers) as resp:
if resp.status == 429: # Rate limited
retry_count += 1
retry_after = int(resp.headers.get('Retry-After', 1))
continue
if resp.status == 401:
# Clear session if token is invalid
# request.session.clear() # Cannot access request here directly
raise HTTPException(status_code=401, detail="Discord token invalid or expired. Please re-login.")
resp.raise_for_status()
user_guilds = await resp.json()
ADMINISTRATOR_PERMISSION = 0x8
is_admin = False
for guild in user_guilds:
if int(guild['id']) == guild_id:
permissions = int(guild['permissions'])
if (permissions & ADMINISTRATOR_PERMISSION) == ADMINISTRATOR_PERMISSION:
is_admin = True
break # Found the guild and user is admin
if not is_admin:
log.warning(f"Dashboard: User {current_user['user_id']} is not admin or not in guild {guild_id}.")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User is not an administrator of this guild.")
log.debug(f"Dashboard: User {current_user['user_id']} verified as admin for guild {guild_id}.")
return True # Indicate verification success
# If we get here, we've exceeded our retry limit
raise HTTPException(status_code=429, detail="Rate limited by Discord API. Please try again later.")
except aiohttp.ClientResponseError as e:
log.exception(f"Dashboard: HTTP error verifying guild admin status: {e.status} {e.message}")
if e.status == 429:
raise HTTPException(status_code=429, detail="Rate limited by Discord API. Please try again later.")
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="Error communicating with Discord API.")
except Exception as e:
log.exception(f"Dashboard: Generic error verifying guild admin status: {e}")
@ -886,23 +909,44 @@ async def dashboard_get_guild_channels(
try:
# Use Discord Bot Token to fetch channels
bot_headers = {'Authorization': f'Bot {settings.DISCORD_BOT_TOKEN}'}
async with http_session.get(f"https://discord.com/api/v10/guilds/{guild_id}/channels", headers=bot_headers) as resp:
resp.raise_for_status()
channels = await resp.json()
# Filter and format channels
formatted_channels = []
for channel in channels:
formatted_channels.append({
"id": channel["id"],
"name": channel["name"],
"type": channel["type"],
"parent_id": channel.get("parent_id")
})
# Add rate limit handling
max_retries = 3
retry_count = 0
retry_after = 0
return formatted_channels
while retry_count < max_retries:
if retry_after > 0:
log.warning(f"Dashboard: Rate limited by Discord API, waiting {retry_after} seconds before retry")
await asyncio.sleep(retry_after)
async with http_session.get(f"https://discord.com/api/v10/guilds/{guild_id}/channels", headers=bot_headers) as resp:
if resp.status == 429: # Rate limited
retry_count += 1
retry_after = int(resp.headers.get('Retry-After', 1))
continue
resp.raise_for_status()
channels = await resp.json()
# Filter and format channels
formatted_channels = []
for channel in channels:
formatted_channels.append({
"id": channel["id"],
"name": channel["name"],
"type": channel["type"],
"parent_id": channel.get("parent_id")
})
return formatted_channels
# If we get here, we've exceeded our retry limit
raise HTTPException(status_code=429, detail="Rate limited by Discord API. Please try again later.")
except aiohttp.ClientResponseError as e:
log.exception(f"Dashboard: HTTP error fetching guild channels: {e.status} {e.message}")
if e.status == 429:
raise HTTPException(status_code=429, detail="Rate limited by Discord API. Please try again later.")
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="Error communicating with Discord API.")
except Exception as e:
log.exception(f"Dashboard: Generic error fetching guild channels: {e}")
@ -924,31 +968,52 @@ async def dashboard_get_guild_roles(
try:
# Use Discord Bot Token to fetch roles
bot_headers = {'Authorization': f'Bot {settings.DISCORD_BOT_TOKEN}'}
async with http_session.get(f"https://discord.com/api/v10/guilds/{guild_id}/roles", headers=bot_headers) as resp:
resp.raise_for_status()
roles = await resp.json()
# Filter and format roles
formatted_roles = []
for role in roles:
# Skip @everyone role
if role["name"] == "@everyone":
# Add rate limit handling
max_retries = 3
retry_count = 0
retry_after = 0
while retry_count < max_retries:
if retry_after > 0:
log.warning(f"Dashboard: Rate limited by Discord API, waiting {retry_after} seconds before retry")
await asyncio.sleep(retry_after)
async with http_session.get(f"https://discord.com/api/v10/guilds/{guild_id}/roles", headers=bot_headers) as resp:
if resp.status == 429: # Rate limited
retry_count += 1
retry_after = int(resp.headers.get('Retry-After', 1))
continue
formatted_roles.append({
"id": role["id"],
"name": role["name"],
"color": role["color"],
"position": role["position"],
"permissions": role["permissions"]
})
resp.raise_for_status()
roles = await resp.json()
# Sort roles by position (highest first)
formatted_roles.sort(key=lambda r: r["position"], reverse=True)
# Filter and format roles
formatted_roles = []
for role in roles:
# Skip @everyone role
if role["name"] == "@everyone":
continue
return formatted_roles
formatted_roles.append({
"id": role["id"],
"name": role["name"],
"color": role["color"],
"position": role["position"],
"permissions": role["permissions"]
})
# Sort roles by position (highest first)
formatted_roles.sort(key=lambda r: r["position"], reverse=True)
return formatted_roles
# If we get here, we've exceeded our retry limit
raise HTTPException(status_code=429, detail="Rate limited by Discord API. Please try again later.")
except aiohttp.ClientResponseError as e:
log.exception(f"Dashboard: HTTP error fetching guild roles: {e.status} {e.message}")
if e.status == 429:
raise HTTPException(status_code=429, detail="Rate limited by Discord API. Please try again later.")
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="Error communicating with Discord API.")
except Exception as e:
log.exception(f"Dashboard: Generic error fetching guild roles: {e}")
@ -972,27 +1037,51 @@ async def dashboard_get_guild_commands(
bot_headers = {'Authorization': f'Bot {settings.DISCORD_BOT_TOKEN}'}
application_id = settings.DISCORD_CLIENT_ID # This should be the same as your bot's application ID
async with http_session.get(f"https://discord.com/api/v10/applications/{application_id}/guilds/{guild_id}/commands", headers=bot_headers) as resp:
resp.raise_for_status()
commands = await resp.json()
# Add rate limit handling
max_retries = 3
retry_count = 0
retry_after = 0
# Format commands
formatted_commands = []
for cmd in commands:
formatted_commands.append({
"id": cmd["id"],
"name": cmd["name"],
"description": cmd.get("description", ""),
"type": cmd.get("type", 1), # Default to CHAT_INPUT type
"options": cmd.get("options", [])
})
while retry_count < max_retries:
if retry_after > 0:
log.warning(f"Dashboard: Rate limited by Discord API, waiting {retry_after} seconds before retry")
await asyncio.sleep(retry_after)
return formatted_commands
async with http_session.get(f"https://discord.com/api/v10/applications/{application_id}/guilds/{guild_id}/commands", headers=bot_headers) as resp:
if resp.status == 429: # Rate limited
retry_count += 1
retry_after = int(resp.headers.get('Retry-After', 1))
continue
# Handle 404 specially - it's not an error, just means no commands are registered
if resp.status == 404:
return []
resp.raise_for_status()
commands = await resp.json()
# Format commands
formatted_commands = []
for cmd in commands:
formatted_commands.append({
"id": cmd["id"],
"name": cmd["name"],
"description": cmd.get("description", ""),
"type": cmd.get("type", 1), # Default to CHAT_INPUT type
"options": cmd.get("options", [])
})
return formatted_commands
# If we get here, we've exceeded our retry limit
raise HTTPException(status_code=429, detail="Rate limited by Discord API. Please try again later.")
except aiohttp.ClientResponseError as e:
log.exception(f"Dashboard: HTTP error fetching guild commands: {e.status} {e.message}")
if e.status == 404:
# If no commands are registered yet, return an empty list
return []
if e.status == 429:
raise HTTPException(status_code=429, detail="Rate limited by Discord API. Please try again later.")
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="Error communicating with Discord API.")
except Exception as e:
log.exception(f"Dashboard: Generic error fetching guild commands: {e}")

View File

@ -130,9 +130,10 @@ const API = {
* @param {string} url - The API endpoint URL
* @param {Object} options - Fetch options
* @param {HTMLElement} loadingElement - Element to show loading state on
* @param {number} retryCount - Internal parameter for tracking retries
* @returns {Promise} - The fetch promise
*/
async request(url, options = {}, loadingElement = null) {
async request(url, options = {}, loadingElement = null, retryCount = 0) {
// Set default headers
options.headers = options.headers || {};
options.headers['Content-Type'] = options.headers['Content-Type'] || 'application/json';
@ -140,6 +141,9 @@ const API = {
// Always include credentials for session cookies
options.credentials = options.credentials || 'same-origin';
// Maximum number of retries for rate-limited requests
const MAX_RETRIES = 3;
// Add loading state
if (loadingElement) {
if (loadingElement.tagName === 'BUTTON') {
@ -167,10 +171,28 @@ const API = {
}
try {
console.log(`API Request: ${options.method || 'GET'} ${url}`);
console.log(`API Request: ${options.method || 'GET'} ${url}${retryCount > 0 ? ` (Retry ${retryCount}/${MAX_RETRIES})` : ''}`);
const response = await fetch(url, options);
console.log(`API Response: ${response.status} ${response.statusText}`);
// Handle rate limiting with automatic retry
if (response.status === 429 && retryCount < MAX_RETRIES) {
// Get retry-after header or default to increasing backoff
const retryAfter = parseInt(response.headers.get('Retry-After') || Math.pow(2, retryCount));
console.log(`Rate limited. Retrying in ${retryAfter} seconds...`);
// Show toast only on first retry
if (retryCount === 0) {
Toast.warning(`Rate limited by Discord API. Retrying in ${retryAfter} seconds...`, 'Please Wait');
}
// Wait for the specified time
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
// Retry the request
return this.request(url, options, loadingElement, retryCount + 1);
}
// Parse JSON response
let data;
const contentType = response.headers.get('content-type');
@ -202,7 +224,7 @@ const API = {
error.status = response.status;
error.data = data;
// Handle authentication errors
// Handle specific error types
if (response.status === 401) {
console.log('Authentication error detected, redirecting to login');
// Redirect to login after a short delay to show the error
@ -210,6 +232,16 @@ const API = {
window.location.href = '/dashboard/api/auth/login';
}, 2000);
}
else if (response.status === 429) {
// Rate limiting - show a more user-friendly message
Toast.warning('Discord API rate limit reached. Please wait a moment before trying again.', 'Rate Limited');
// If Retry-After header is present, we could use it to show a countdown
const retryAfter = response.headers.get('Retry-After');
if (retryAfter) {
console.log(`Rate limited. Retry after ${retryAfter} seconds`);
}
}
throw error;
}