a
This commit is contained in:
parent
0d5ec7bc0b
commit
30f6d93d89
9
api_service/.env.example
Normal file
9
api_service/.env.example
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# API Server Configuration
|
||||||
|
API_HOST=0.0.0.0
|
||||||
|
API_PORT=443
|
||||||
|
DATA_DIR=data
|
||||||
|
|
||||||
|
# Discord OAuth Configuration
|
||||||
|
DISCORD_CLIENT_ID=1360717457852993576
|
||||||
|
# No client secret for public clients
|
||||||
|
DISCORD_REDIRECT_URI=https://slipstreamm.dev/api/auth
|
96
api_service/OAUTH_README.md
Normal file
96
api_service/OAUTH_README.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# OAuth2 Implementation in the API Service
|
||||||
|
|
||||||
|
This document explains how OAuth2 authentication is implemented in the API service.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The API service now includes a proper OAuth2 implementation that allows users to:
|
||||||
|
|
||||||
|
1. Authenticate with their Discord account
|
||||||
|
2. Authorize the API service to access Discord resources on their behalf
|
||||||
|
3. Use the resulting token for API authentication
|
||||||
|
|
||||||
|
This implementation uses the OAuth2 Authorization Code flow with PKCE (Proof Key for Code Exchange) for enhanced security, which is the recommended approach for public clients like mobile apps and Discord bots.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### 1. Authorization Flow
|
||||||
|
|
||||||
|
1. The user initiates the OAuth flow by clicking an authorization link (typically from the Discord bot or Flutter app)
|
||||||
|
2. The user is redirected to Discord's authorization page
|
||||||
|
3. After authorizing the application, Discord redirects the user to the API service's `/auth` endpoint with an authorization code
|
||||||
|
4. The API service exchanges the code for an access token
|
||||||
|
5. The token is stored in the database and associated with the user's Discord ID
|
||||||
|
6. The user is shown a success page
|
||||||
|
|
||||||
|
### 2. Token Usage
|
||||||
|
|
||||||
|
1. The user includes the access token in the `Authorization` header of API requests
|
||||||
|
2. The API service verifies the token with Discord
|
||||||
|
3. If the token is valid, the API service identifies the user and processes the request
|
||||||
|
4. If the token is invalid, the API service returns a 401 Unauthorized error
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
- `GET /api/auth?code={code}&state={state}` - Handle OAuth callback from Discord
|
||||||
|
- `GET /api/token` - Get the access token for the authenticated user
|
||||||
|
- `DELETE /api/token` - Delete the access token for the authenticated user
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The OAuth implementation requires the following environment variables:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DISCORD_CLIENT_ID=your_discord_client_id
|
||||||
|
DISCORD_REDIRECT_URI=https://your-domain.com/api/auth
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that we don't use a client secret because this is a public client implementation. Public clients (like mobile apps, single-page applications, or Discord bots) should use PKCE instead of a client secret for security.
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- The API service stores tokens securely in the database
|
||||||
|
- Tokens are never exposed in logs or error messages
|
||||||
|
- The API service verifies tokens with Discord for each request
|
||||||
|
- The API service uses HTTPS to protect tokens in transit
|
||||||
|
|
||||||
|
## Integration with Discord Bot
|
||||||
|
|
||||||
|
The Discord bot can use the API service's OAuth implementation by:
|
||||||
|
|
||||||
|
1. Setting `API_OAUTH_ENABLED=true` in the bot's environment variables
|
||||||
|
2. Setting `API_URL` to the URL of the API service
|
||||||
|
3. Using the `!auth` command to initiate the OAuth flow
|
||||||
|
4. Using the resulting token for API requests
|
||||||
|
|
||||||
|
## Integration with Flutter App
|
||||||
|
|
||||||
|
The Flutter app can use the API service's OAuth implementation by:
|
||||||
|
|
||||||
|
1. Updating the OAuth configuration to use the API service's redirect URI
|
||||||
|
2. Using the resulting token for API requests
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **"Invalid OAuth2 redirect_uri" error**
|
||||||
|
- Make sure the redirect URI in your Discord application settings matches the one in your environment variables
|
||||||
|
- The redirect URI should be `https://your-domain.com/api/auth`
|
||||||
|
|
||||||
|
2. **"Invalid client_id" error**
|
||||||
|
- Make sure the client ID in your environment variables matches the one in your Discord application settings
|
||||||
|
|
||||||
|
3. **"Invalid request" error**
|
||||||
|
- Make sure you're including the code_verifier parameter when exchanging the authorization code
|
||||||
|
- The code_verifier must match the one used to generate the code_challenge
|
||||||
|
|
||||||
|
4. **"Invalid code" error**
|
||||||
|
- The authorization code has expired or has already been used
|
||||||
|
- Authorization codes are one-time use and expire after a short time
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
The API service logs detailed information about the OAuth process. Check the API service's logs for error messages and debugging information.
|
153
api_service/README.md
Normal file
153
api_service/README.md
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# Unified API Service
|
||||||
|
|
||||||
|
This is a centralized API service that both the Discord bot and Flutter app use to store and retrieve data. This ensures consistent data synchronization between both applications.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The API service provides endpoints for:
|
||||||
|
- Managing conversations
|
||||||
|
- Managing user settings
|
||||||
|
- Authentication via Discord OAuth
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### 1. Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install fastapi uvicorn pydantic aiohttp
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Environment Variables
|
||||||
|
|
||||||
|
Create a `.env` file in the `api_service` directory with the following variables:
|
||||||
|
|
||||||
|
```
|
||||||
|
API_HOST=0.0.0.0
|
||||||
|
API_PORT=8000
|
||||||
|
DATA_DIR=data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Start the API Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd api_service
|
||||||
|
python api_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The API server will start on the configured host and port (default: `0.0.0.0:8000`).
|
||||||
|
|
||||||
|
## Discord Bot Integration
|
||||||
|
|
||||||
|
### 1. Update the Discord Bot
|
||||||
|
|
||||||
|
1. Import the API integration in your bot's main file:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from api_integration import init_api_client
|
||||||
|
|
||||||
|
# Initialize the API client
|
||||||
|
api_client = init_api_client("https://your-api-url.com/api")
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Replace the existing AI cog with the updated version:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In your bot.py file
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(AICog(bot))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Discord OAuth
|
||||||
|
|
||||||
|
1. Go to the [Discord Developer Portal](https://discord.com/developers/applications)
|
||||||
|
2. Create a new application or use an existing one
|
||||||
|
3. Go to the OAuth2 section
|
||||||
|
4. Add a redirect URL: `https://your-api-url.com/api/auth`
|
||||||
|
5. Copy the Client ID and Client Secret
|
||||||
|
|
||||||
|
## Flutter App Integration
|
||||||
|
|
||||||
|
### 1. Update the Flutter App
|
||||||
|
|
||||||
|
1. Replace the existing `SyncService` with the new `ApiService`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// In your main.dart file
|
||||||
|
final apiService = ApiService(discordOAuthService);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update your providers:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider(create: (context) => DiscordOAuthService()),
|
||||||
|
ChangeNotifierProxyProvider<DiscordOAuthService, ApiService>(
|
||||||
|
create: (context) => ApiService(Provider.of<DiscordOAuthService>(context, listen: false)),
|
||||||
|
update: (context, authService, previous) => previous!..update(authService),
|
||||||
|
),
|
||||||
|
ChangeNotifierProxyProvider2<OpenRouterService, ApiService, ChatModel>(
|
||||||
|
create: (context) => ChatModel(
|
||||||
|
Provider.of<OpenRouterService>(context, listen: false),
|
||||||
|
Provider.of<ApiService>(context, listen: false),
|
||||||
|
),
|
||||||
|
update: (context, openRouterService, apiService, previous) =>
|
||||||
|
previous!..update(openRouterService, apiService),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Discord OAuth in Flutter
|
||||||
|
|
||||||
|
1. Update the Discord OAuth configuration in your Flutter app:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// In discord_oauth_service.dart
|
||||||
|
const String clientId = 'your-client-id';
|
||||||
|
const String redirectUri = 'openroutergui://auth';
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
- `GET /auth?code={code}&state={state}` - Handle OAuth callback
|
||||||
|
|
||||||
|
### Conversations
|
||||||
|
|
||||||
|
- `GET /conversations` - Get all conversations for the authenticated user
|
||||||
|
- `GET /conversations/{conversation_id}` - Get a specific conversation
|
||||||
|
- `POST /conversations` - Create a new conversation
|
||||||
|
- `PUT /conversations/{conversation_id}` - Update a conversation
|
||||||
|
- `DELETE /conversations/{conversation_id}` - Delete a conversation
|
||||||
|
|
||||||
|
### Settings
|
||||||
|
|
||||||
|
- `GET /settings` - Get settings for the authenticated user
|
||||||
|
- `PUT /settings` - Update settings for the authenticated user
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- The API uses Discord OAuth for authentication
|
||||||
|
- All API requests require a valid Discord token
|
||||||
|
- The API verifies the token with Discord for each request
|
||||||
|
- Consider adding rate limiting and additional security measures for production use
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### API Connection Issues
|
||||||
|
|
||||||
|
- Ensure the API server is running and accessible
|
||||||
|
- Check that the API URL is correctly configured in both the Discord bot and Flutter app
|
||||||
|
- Verify that the Discord OAuth credentials are correct
|
||||||
|
|
||||||
|
### Authentication Issues
|
||||||
|
|
||||||
|
- Make sure the Discord OAuth redirect URL is correctly configured
|
||||||
|
- Check that the client ID and client secret are correct
|
||||||
|
- Ensure the user has granted the necessary permissions
|
||||||
|
|
||||||
|
### Data Synchronization Issues
|
||||||
|
|
||||||
|
- Check the API server logs for errors
|
||||||
|
- Verify that both the Discord bot and Flutter app are using the same API URL
|
||||||
|
- Ensure the user is authenticated in both applications
|
77
api_service/api_models.py
Normal file
77
api_service/api_models.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
from typing import Dict, List, Optional, Any, Union
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
import datetime
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# ============= Data Models =============
|
||||||
|
|
||||||
|
class Message(BaseModel):
|
||||||
|
content: str
|
||||||
|
role: str # "user", "assistant", or "system"
|
||||||
|
timestamp: datetime.datetime
|
||||||
|
reasoning: Optional[str] = None
|
||||||
|
usage_data: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
class Conversation(BaseModel):
|
||||||
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
||||||
|
title: str
|
||||||
|
messages: List[Message] = []
|
||||||
|
created_at: datetime.datetime = Field(default_factory=datetime.datetime.now)
|
||||||
|
updated_at: datetime.datetime = Field(default_factory=datetime.datetime.now)
|
||||||
|
|
||||||
|
# Conversation-specific settings
|
||||||
|
model_id: str = "openai/gpt-3.5-turbo"
|
||||||
|
reasoning_enabled: bool = False
|
||||||
|
reasoning_effort: str = "medium" # "low", "medium", "high"
|
||||||
|
temperature: float = 0.7
|
||||||
|
max_tokens: int = 1000
|
||||||
|
web_search_enabled: bool = False
|
||||||
|
system_message: Optional[str] = None
|
||||||
|
|
||||||
|
class UserSettings(BaseModel):
|
||||||
|
# General settings
|
||||||
|
model_id: str = "openai/gpt-3.5-turbo"
|
||||||
|
temperature: float = 0.7
|
||||||
|
max_tokens: int = 1000
|
||||||
|
|
||||||
|
# Reasoning settings
|
||||||
|
reasoning_enabled: bool = False
|
||||||
|
reasoning_effort: str = "medium" # "low", "medium", "high"
|
||||||
|
|
||||||
|
# Web search settings
|
||||||
|
web_search_enabled: bool = False
|
||||||
|
|
||||||
|
# System message
|
||||||
|
system_message: Optional[str] = None
|
||||||
|
|
||||||
|
# Character settings
|
||||||
|
character: Optional[str] = None
|
||||||
|
character_info: Optional[str] = None
|
||||||
|
character_breakdown: bool = False
|
||||||
|
custom_instructions: Optional[str] = None
|
||||||
|
|
||||||
|
# UI settings
|
||||||
|
advanced_view_enabled: bool = False
|
||||||
|
streaming_enabled: bool = True
|
||||||
|
|
||||||
|
# Last updated timestamp
|
||||||
|
last_updated: datetime.datetime = Field(default_factory=datetime.datetime.now)
|
||||||
|
|
||||||
|
# ============= API Request/Response Models =============
|
||||||
|
|
||||||
|
class GetConversationsResponse(BaseModel):
|
||||||
|
conversations: List[Conversation]
|
||||||
|
|
||||||
|
class GetSettingsResponse(BaseModel):
|
||||||
|
settings: UserSettings
|
||||||
|
|
||||||
|
class UpdateSettingsRequest(BaseModel):
|
||||||
|
settings: UserSettings
|
||||||
|
|
||||||
|
class UpdateConversationRequest(BaseModel):
|
||||||
|
conversation: Conversation
|
||||||
|
|
||||||
|
class ApiResponse(BaseModel):
|
||||||
|
success: bool
|
||||||
|
message: str
|
||||||
|
data: Optional[Any] = None
|
1267
api_service/api_server.py
Normal file
1267
api_service/api_server.py
Normal file
File diff suppressed because it is too large
Load Diff
25
api_service/code_verifier_store.py
Normal file
25
api_service/code_verifier_store.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
"""
|
||||||
|
Code verifier store for the API service.
|
||||||
|
|
||||||
|
This module provides a simple in-memory store for code verifiers used in the OAuth flow.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
# In-memory storage for code verifiers
|
||||||
|
code_verifiers: Dict[str, str] = {}
|
||||||
|
|
||||||
|
def store_code_verifier(state: str, code_verifier: str) -> None:
|
||||||
|
"""Store a code verifier for a state."""
|
||||||
|
code_verifiers[state] = code_verifier
|
||||||
|
print(f"Stored code verifier for state {state}: {code_verifier[:10]}...")
|
||||||
|
|
||||||
|
def get_code_verifier(state: str) -> Optional[str]:
|
||||||
|
"""Get the code verifier for a state."""
|
||||||
|
return code_verifiers.get(state)
|
||||||
|
|
||||||
|
def remove_code_verifier(state: str) -> None:
|
||||||
|
"""Remove a code verifier for a state."""
|
||||||
|
if state in code_verifiers:
|
||||||
|
del code_verifiers[state]
|
||||||
|
print(f"Removed code verifier for state {state}")
|
85
api_service/dashboard_web/index.html
Normal file
85
api_service/dashboard_web/index.html
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Bot Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Discord Bot Dashboard</h1>
|
||||||
|
|
||||||
|
<div id="auth-section">
|
||||||
|
<button id="login-button">Login with Discord</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="dashboard-section" style="display: none;">
|
||||||
|
<h2>Welcome, <span id="username">User</span>!</h2>
|
||||||
|
<button id="logout-button">Logout</button>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h3>Manage Server Settings</h3>
|
||||||
|
<label for="guild-select">Select Server:</label>
|
||||||
|
<select name="guilds" id="guild-select">
|
||||||
|
<option value="">--Please choose a server--</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div id="settings-form" style="display: none;">
|
||||||
|
<h4>Prefix</h4>
|
||||||
|
<label for="prefix-input">Command Prefix:</label>
|
||||||
|
<input type="text" id="prefix-input" name="prefix" maxlength="10">
|
||||||
|
<button id="save-prefix-button">Save Prefix</button>
|
||||||
|
<p id="prefix-feedback"></p>
|
||||||
|
|
||||||
|
<h4>Welcome Messages</h4>
|
||||||
|
<label for="welcome-channel">Welcome Channel ID:</label> <!-- Changed label -->
|
||||||
|
<input type="text" id="welcome-channel" name="welcome_channel_id" placeholder="Enter Channel ID"> <!-- Changed to text input -->
|
||||||
|
<br>
|
||||||
|
<label for="welcome-message">Welcome Message Template:</label><br>
|
||||||
|
<textarea id="welcome-message" name="welcome_message" rows="4" cols="50" placeholder="Use {user} for mention, {username} for name, {server} for server name."></textarea><br>
|
||||||
|
<button id="save-welcome-button">Save Welcome Settings</button>
|
||||||
|
<button id="disable-welcome-button">Disable Welcome</button>
|
||||||
|
<p id="welcome-feedback"></p>
|
||||||
|
|
||||||
|
<h4>Goodbye Messages</h4>
|
||||||
|
<label for="goodbye-channel">Goodbye Channel ID:</label> <!-- Changed label -->
|
||||||
|
<input type="text" id="goodbye-channel" name="goodbye_channel_id" placeholder="Enter Channel ID"> <!-- Changed to text input -->
|
||||||
|
<br>
|
||||||
|
<label for="goodbye-message">Goodbye Message Template:</label><br>
|
||||||
|
<textarea id="goodbye-message" name="goodbye_message" rows="4" cols="50" placeholder="Use {username} for name, {server} for server name."></textarea><br>
|
||||||
|
<button id="save-goodbye-button">Save Goodbye Settings</button>
|
||||||
|
<button id="disable-goodbye-button">Disable Goodbye</button>
|
||||||
|
<p id="goodbye-feedback"></p>
|
||||||
|
|
||||||
|
<h4>Enabled Modules (Cogs)</h4>
|
||||||
|
<div id="cogs-list">
|
||||||
|
<!-- Cog checkboxes will be populated by JS -->
|
||||||
|
</div>
|
||||||
|
<button id="save-cogs-button">Save Module Settings</button>
|
||||||
|
<p id="cogs-feedback"></p>
|
||||||
|
|
||||||
|
<h4>Command Permissions</h4>
|
||||||
|
<label for="command-select">Command:</label>
|
||||||
|
<select id="command-select">
|
||||||
|
<!-- TODO: Populate commands dynamically -->
|
||||||
|
<option value="">-- Select Command --</option>
|
||||||
|
</select>
|
||||||
|
<label for="role-select">Role:</label>
|
||||||
|
<select id="role-select">
|
||||||
|
<!-- TODO: Populate roles dynamically -->
|
||||||
|
<option value="">-- Select Role --</option>
|
||||||
|
</select>
|
||||||
|
<button id="add-perm-button">Allow Role</button>
|
||||||
|
<button id="remove-perm-button">Disallow Role</button>
|
||||||
|
<div id="current-perms">
|
||||||
|
<!-- Current permissions will be listed here -->
|
||||||
|
</div>
|
||||||
|
<p id="perms-feedback"></p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
421
api_service/dashboard_web/script.js
Normal file
421
api_service/dashboard_web/script.js
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const loginButton = document.getElementById('login-button');
|
||||||
|
const logoutButton = document.getElementById('logout-button');
|
||||||
|
const authSection = document.getElementById('auth-section');
|
||||||
|
const dashboardSection = document.getElementById('dashboard-section');
|
||||||
|
const usernameSpan = document.getElementById('username');
|
||||||
|
const guildSelect = document.getElementById('guild-select');
|
||||||
|
const settingsForm = document.getElementById('settings-form');
|
||||||
|
|
||||||
|
// --- API Base URL (Adjust if needed) ---
|
||||||
|
// Assuming the API runs on the same host/port for simplicity,
|
||||||
|
// otherwise, use the full URL like 'http://localhost:8000'
|
||||||
|
// IMPORTANT: This will need to be updated to the new merged endpoint prefix, e.g., /dashboard/api
|
||||||
|
const API_BASE_URL = '/dashboard/api'; // Tentative new prefix
|
||||||
|
|
||||||
|
// --- Helper Functions ---
|
||||||
|
async function fetchAPI(endpoint, options = {}) {
|
||||||
|
// Add authentication headers if needed (e.g., from cookies or localStorage)
|
||||||
|
// For now, assuming cookies handle session management automatically
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, options);
|
||||||
|
if (response.status === 401) { // Unauthorized
|
||||||
|
showLogin();
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
||||||
|
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
if (response.status === 204) { // No Content
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Fetch Error:', error);
|
||||||
|
// Display error to user?
|
||||||
|
throw error; // Re-throw for specific handlers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLogin() {
|
||||||
|
authSection.style.display = 'block';
|
||||||
|
dashboardSection.style.display = 'none';
|
||||||
|
settingsForm.style.display = 'none';
|
||||||
|
guildSelect.value = ''; // Reset guild selection
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDashboard(userData) {
|
||||||
|
authSection.style.display = 'none';
|
||||||
|
dashboardSection.style.display = 'block';
|
||||||
|
usernameSpan.textContent = userData.username;
|
||||||
|
loadGuilds();
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayFeedback(elementId, message, isError = false) {
|
||||||
|
const feedbackElement = document.getElementById(elementId);
|
||||||
|
if (feedbackElement) {
|
||||||
|
feedbackElement.textContent = message;
|
||||||
|
feedbackElement.className = isError ? 'error' : '';
|
||||||
|
// Clear feedback after a few seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
feedbackElement.textContent = '';
|
||||||
|
feedbackElement.className = '';
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Authentication ---
|
||||||
|
async function checkLoginStatus() {
|
||||||
|
try {
|
||||||
|
// Use the new endpoint path
|
||||||
|
const userData = await fetchAPI('/user/me');
|
||||||
|
if (userData) {
|
||||||
|
showDashboard(userData);
|
||||||
|
} else {
|
||||||
|
showLogin();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If fetching /user/me fails (e.g., 401), show login
|
||||||
|
showLogin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loginButton.addEventListener('click', () => {
|
||||||
|
// Redirect to backend login endpoint which will redirect to Discord
|
||||||
|
// Use the new endpoint path
|
||||||
|
window.location.href = `${API_BASE_URL}/auth/login`;
|
||||||
|
});
|
||||||
|
|
||||||
|
logoutButton.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
// Use the new endpoint path
|
||||||
|
await fetchAPI('/auth/logout', { method: 'POST' });
|
||||||
|
showLogin();
|
||||||
|
} catch (error) {
|
||||||
|
alert('Logout failed. Please try again.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Guild Loading and Settings ---
|
||||||
|
async function loadGuilds() {
|
||||||
|
try {
|
||||||
|
// Use the new endpoint path
|
||||||
|
const guilds = await fetchAPI('/user/guilds');
|
||||||
|
guildSelect.innerHTML = '<option value="">--Please choose a server--</option>'; // Reset
|
||||||
|
guilds.forEach(guild => {
|
||||||
|
// Only add guilds where the user is an administrator (assuming API filters this)
|
||||||
|
// Or filter here based on permissions if API doesn't
|
||||||
|
// const isAdmin = (parseInt(guild.permissions) & 0x8) === 0x8; // Check ADMINISTRATOR bit
|
||||||
|
// if (isAdmin) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = guild.id;
|
||||||
|
option.textContent = guild.name;
|
||||||
|
guildSelect.appendChild(option);
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
displayFeedback('guild-select-feedback', `Error loading guilds: ${error.message}`, true); // Add a feedback element if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guildSelect.addEventListener('change', async (event) => {
|
||||||
|
const guildId = event.target.value;
|
||||||
|
if (guildId) {
|
||||||
|
await loadSettings(guildId);
|
||||||
|
settingsForm.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
settingsForm.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadSettings(guildId) {
|
||||||
|
console.log(`Loading settings for guild ${guildId}`);
|
||||||
|
// Clear previous settings?
|
||||||
|
document.getElementById('prefix-input').value = '';
|
||||||
|
// Changed channel inputs to text
|
||||||
|
document.getElementById('welcome-channel').value = '';
|
||||||
|
document.getElementById('welcome-message').value = '';
|
||||||
|
document.getElementById('goodbye-channel').value = '';
|
||||||
|
document.getElementById('goodbye-message').value = '';
|
||||||
|
document.getElementById('cogs-list').innerHTML = '';
|
||||||
|
document.getElementById('current-perms').innerHTML = ''; // Clear permissions list
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the new endpoint path
|
||||||
|
const settings = await fetchAPI(`/guilds/${guildId}/settings`);
|
||||||
|
console.log("Received settings:", settings);
|
||||||
|
|
||||||
|
// Populate Prefix
|
||||||
|
document.getElementById('prefix-input').value = settings.prefix || '';
|
||||||
|
|
||||||
|
// Populate Welcome/Goodbye Channel IDs
|
||||||
|
document.getElementById('welcome-channel').value = settings.welcome_channel_id || '';
|
||||||
|
document.getElementById('welcome-message').value = settings.welcome_message || '';
|
||||||
|
document.getElementById('goodbye-channel').value = settings.goodbye_channel_id || '';
|
||||||
|
document.getElementById('goodbye-message').value = settings.goodbye_message || '';
|
||||||
|
|
||||||
|
// Populate Cogs
|
||||||
|
// TODO: Need a way to get the *full* list of available cogs from the bot/API
|
||||||
|
// For now, just display the ones returned by the settings endpoint
|
||||||
|
populateCogsList(settings.enabled_cogs || {});
|
||||||
|
|
||||||
|
// Populate Command Permissions
|
||||||
|
// TODO: Fetch roles and commands for dropdowns
|
||||||
|
// TODO: Fetch current permissions
|
||||||
|
await loadCommandPermissions(guildId);
|
||||||
|
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
displayFeedback('prefix-feedback', `Error loading settings: ${error.message}`, true); // Use a general feedback area?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateCogsList(cogsStatus) {
|
||||||
|
// This function now only displays cogs whose status is stored in the DB
|
||||||
|
// and returned by the API. It doesn't know about *all* possible cogs.
|
||||||
|
const cogsListDiv = document.getElementById('cogs-list');
|
||||||
|
cogsListDiv.innerHTML = ''; // Clear previous
|
||||||
|
// Assuming CORE_COGS is available globally or passed somehow
|
||||||
|
// TODO: Get this list from the API or config
|
||||||
|
const CORE_COGS = ['SettingsCog', 'HelpCog']; // Example - needs to match backend
|
||||||
|
|
||||||
|
// TODO: Fetch the *full* list of cogs from the bot/API to display all options
|
||||||
|
// For now, only showing cogs already in the settings response
|
||||||
|
Object.entries(cogsStatus).sort().forEach(([cogName, isEnabled]) => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
const checkbox = document.createElement('input');
|
||||||
|
checkbox.type = 'checkbox';
|
||||||
|
checkbox.id = `cog-${cogName}`;
|
||||||
|
checkbox.name = cogName;
|
||||||
|
checkbox.checked = isEnabled;
|
||||||
|
checkbox.disabled = CORE_COGS.includes(cogName); // Disable core cogs
|
||||||
|
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.htmlFor = `cog-${cogName}`;
|
||||||
|
label.textContent = cogName + (CORE_COGS.includes(cogName) ? ' (Core)' : '');
|
||||||
|
|
||||||
|
div.appendChild(checkbox);
|
||||||
|
div.appendChild(label);
|
||||||
|
cogsListDiv.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCommandPermissions(guildId) {
|
||||||
|
const permsDiv = document.getElementById('current-perms');
|
||||||
|
permsDiv.innerHTML = 'Loading permissions...';
|
||||||
|
try {
|
||||||
|
// Use the new endpoint path
|
||||||
|
const permData = await fetchAPI(`/guilds/${guildId}/permissions`);
|
||||||
|
permsDiv.innerHTML = ''; // Clear loading message
|
||||||
|
if (Object.keys(permData.permissions).length === 0) {
|
||||||
|
permsDiv.innerHTML = '<i>No specific command permissions set. All roles can use all enabled commands (unless restricted by default).</i>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Fetch role names from Discord API or bot API to display names instead of IDs
|
||||||
|
for (const [commandName, roleIds] of Object.entries(permData.permissions).sort()) {
|
||||||
|
const rolesStr = roleIds.map(id => `Role ID: ${id}`).join(', '); // Placeholder until role names are fetched
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.innerHTML = `Command <span>${commandName}</span> allowed for: ${rolesStr}`;
|
||||||
|
permsDiv.appendChild(div);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
permsDiv.innerHTML = `<i class="error">Error loading permissions: ${error.message}</i>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Save Settings Event Listeners ---
|
||||||
|
|
||||||
|
document.getElementById('save-prefix-button').addEventListener('click', async () => {
|
||||||
|
const guildId = guildSelect.value;
|
||||||
|
const prefix = document.getElementById('prefix-input').value;
|
||||||
|
if (!guildId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the new endpoint path
|
||||||
|
await fetchAPI(`/guilds/${guildId}/settings`, {
|
||||||
|
method: 'PATCH', // Use PATCH for partial updates
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ prefix: prefix })
|
||||||
|
});
|
||||||
|
displayFeedback('prefix-feedback', 'Prefix saved successfully!');
|
||||||
|
} catch (error) {
|
||||||
|
displayFeedback('prefix-feedback', `Error saving prefix: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('save-welcome-button').addEventListener('click', async () => {
|
||||||
|
const guildId = guildSelect.value;
|
||||||
|
const channelIdInput = document.getElementById('welcome-channel').value;
|
||||||
|
const message = document.getElementById('welcome-message').value;
|
||||||
|
if (!guildId) return;
|
||||||
|
|
||||||
|
// Basic validation for channel ID (numeric)
|
||||||
|
const channelId = channelIdInput && /^\d+$/.test(channelIdInput) ? channelIdInput : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the new endpoint path
|
||||||
|
await fetchAPI(`/guilds/${guildId}/settings`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
welcome_channel_id: channelId, // Send numeric ID or null
|
||||||
|
welcome_message: message
|
||||||
|
})
|
||||||
|
});
|
||||||
|
displayFeedback('welcome-feedback', 'Welcome settings saved!');
|
||||||
|
} catch (error) {
|
||||||
|
displayFeedback('welcome-feedback', `Error saving welcome settings: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('disable-welcome-button').addEventListener('click', async () => {
|
||||||
|
const guildId = guildSelect.value;
|
||||||
|
if (!guildId) return;
|
||||||
|
if (!confirm('Are you sure you want to disable welcome messages?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the new endpoint path
|
||||||
|
await fetchAPI(`/guilds/${guildId}/settings`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
welcome_channel_id: null,
|
||||||
|
welcome_message: null // Also clear message template maybe? Or just channel? Let's clear both.
|
||||||
|
})
|
||||||
|
});
|
||||||
|
// Clear the form fields visually
|
||||||
|
document.getElementById('welcome-channel').value = '';
|
||||||
|
document.getElementById('welcome-message').value = '';
|
||||||
|
displayFeedback('welcome-feedback', 'Welcome messages disabled.');
|
||||||
|
} catch (error) {
|
||||||
|
displayFeedback('welcome-feedback', `Error disabling welcome messages: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('save-goodbye-button').addEventListener('click', async () => {
|
||||||
|
const guildId = guildSelect.value;
|
||||||
|
const channelIdInput = document.getElementById('goodbye-channel').value;
|
||||||
|
const message = document.getElementById('goodbye-message').value;
|
||||||
|
if (!guildId) return;
|
||||||
|
|
||||||
|
const channelId = channelIdInput && /^\d+$/.test(channelIdInput) ? channelIdInput : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the new endpoint path
|
||||||
|
await fetchAPI(`/guilds/${guildId}/settings`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
goodbye_channel_id: channelId,
|
||||||
|
goodbye_message: message
|
||||||
|
})
|
||||||
|
});
|
||||||
|
displayFeedback('goodbye-feedback', 'Goodbye settings saved!');
|
||||||
|
} catch (error) {
|
||||||
|
displayFeedback('goodbye-feedback', `Error saving goodbye settings: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('disable-goodbye-button').addEventListener('click', async () => {
|
||||||
|
const guildId = guildSelect.value;
|
||||||
|
if (!guildId) return;
|
||||||
|
if (!confirm('Are you sure you want to disable goodbye messages?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the new endpoint path
|
||||||
|
await fetchAPI(`/guilds/${guildId}/settings`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
goodbye_channel_id: null,
|
||||||
|
goodbye_message: null
|
||||||
|
})
|
||||||
|
});
|
||||||
|
document.getElementById('goodbye-channel').value = '';
|
||||||
|
document.getElementById('goodbye-message').value = '';
|
||||||
|
displayFeedback('goodbye-feedback', 'Goodbye messages disabled.');
|
||||||
|
} catch (error) {
|
||||||
|
displayFeedback('goodbye-feedback', `Error disabling goodbye messages: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('save-cogs-button').addEventListener('click', async () => {
|
||||||
|
const guildId = guildSelect.value;
|
||||||
|
if (!guildId) return;
|
||||||
|
|
||||||
|
const cogsPayload = {};
|
||||||
|
const checkboxes = document.querySelectorAll('#cogs-list input[type="checkbox"]');
|
||||||
|
checkboxes.forEach(cb => {
|
||||||
|
if (!cb.disabled) { // Don't send status for disabled (core) cogs
|
||||||
|
cogsPayload[cb.name] = cb.checked;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the new endpoint path
|
||||||
|
await fetchAPI(`/guilds/${guildId}/settings`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ cogs: cogsPayload })
|
||||||
|
});
|
||||||
|
displayFeedback('cogs-feedback', 'Module settings saved!');
|
||||||
|
} catch (error) {
|
||||||
|
displayFeedback('cogs-feedback', `Error saving module settings: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Command Permissions Event Listeners ---
|
||||||
|
document.getElementById('add-perm-button').addEventListener('click', async () => {
|
||||||
|
const guildId = guildSelect.value;
|
||||||
|
const commandName = document.getElementById('command-select').value;
|
||||||
|
const roleId = document.getElementById('role-select').value;
|
||||||
|
if (!guildId || !commandName || !roleId) {
|
||||||
|
displayFeedback('perms-feedback', 'Please select a command and a role.', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the new endpoint path
|
||||||
|
await fetchAPI(`/guilds/${guildId}/permissions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ command_name: commandName, role_id: roleId })
|
||||||
|
});
|
||||||
|
displayFeedback('perms-feedback', `Permission added for ${commandName}.`);
|
||||||
|
await loadCommandPermissions(guildId); // Refresh list
|
||||||
|
} catch (error) {
|
||||||
|
displayFeedback('perms-feedback', `Error adding permission: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('remove-perm-button').addEventListener('click', async () => {
|
||||||
|
const guildId = guildSelect.value;
|
||||||
|
const commandName = document.getElementById('command-select').value;
|
||||||
|
const roleId = document.getElementById('role-select').value;
|
||||||
|
if (!guildId || !commandName || !roleId) {
|
||||||
|
displayFeedback('perms-feedback', 'Please select a command and a role to remove.', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirm(`Are you sure you want to remove permission for role ID ${roleId} from command ${commandName}?`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the new endpoint path
|
||||||
|
await fetchAPI(`/guilds/${guildId}/permissions`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ command_name: commandName, role_id: roleId })
|
||||||
|
});
|
||||||
|
displayFeedback('perms-feedback', `Permission removed for ${commandName}.`);
|
||||||
|
await loadCommandPermissions(guildId); // Refresh list
|
||||||
|
} catch (error) {
|
||||||
|
displayFeedback('perms-feedback', `Error removing permission: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// --- Initial Load ---
|
||||||
|
checkLoginStatus();
|
||||||
|
});
|
127
api_service/dashboard_web/style.css
Normal file
127
api_service/dashboard_web/style.css
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
margin: 2em;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4 {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dashboard-section, #settings-form {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 1.5em;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 1em;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
width: 95%;
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
background-color: #5865F2; /* Discord blue */
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #4752C4;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logout-button {
|
||||||
|
background-color: #dc3545; /* Red */
|
||||||
|
}
|
||||||
|
#logout-button:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[id^="disable-"] {
|
||||||
|
background-color: #ffc107; /* Yellow/Orange */
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
button[id^="disable-"]:hover {
|
||||||
|
background-color: #e0a800;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: #ddd;
|
||||||
|
margin: 2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cogs-list div {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cogs-list label {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 5px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Command Permissions Section */
|
||||||
|
#current-perms {
|
||||||
|
margin-top: 1em;
|
||||||
|
padding: 0.5em;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
#current-perms div {
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
#current-perms span {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
#add-perm-button {
|
||||||
|
background-color: #28a745; /* Green */
|
||||||
|
}
|
||||||
|
#add-perm-button:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
}
|
||||||
|
#remove-perm-button {
|
||||||
|
background-color: #dc3545; /* Red */
|
||||||
|
}
|
||||||
|
#remove-perm-button:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Feedback messages */
|
||||||
|
p[id$="-feedback"] {
|
||||||
|
font-style: italic;
|
||||||
|
color: green;
|
||||||
|
margin-top: 5px;
|
||||||
|
min-height: 1em; /* Reserve space */
|
||||||
|
}
|
||||||
|
|
||||||
|
p[id$="-feedback"].error {
|
||||||
|
color: red;
|
||||||
|
}
|
204
api_service/database.py
Normal file
204
api_service/database.py
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import datetime
|
||||||
|
from typing import Dict, List, Optional, Any
|
||||||
|
from api_models import Conversation, UserSettings, Message
|
||||||
|
|
||||||
|
# ============= Database Class =============
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
def __init__(self, data_dir="data"):
|
||||||
|
self.data_dir = data_dir
|
||||||
|
self.conversations_file = os.path.join(data_dir, "conversations.json")
|
||||||
|
self.settings_file = os.path.join(data_dir, "user_settings.json")
|
||||||
|
self.tokens_file = os.path.join(data_dir, "user_tokens.json")
|
||||||
|
|
||||||
|
# Create data directory if it doesn't exist
|
||||||
|
os.makedirs(data_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# In-memory storage
|
||||||
|
self.conversations: Dict[str, Dict[str, Conversation]] = {} # user_id -> conversation_id -> Conversation
|
||||||
|
self.user_settings: Dict[str, UserSettings] = {} # user_id -> UserSettings
|
||||||
|
self.user_tokens: Dict[str, Dict[str, Any]] = {} # user_id -> token_data
|
||||||
|
|
||||||
|
# Load data from files
|
||||||
|
self.load_data()
|
||||||
|
|
||||||
|
def load_data(self):
|
||||||
|
"""Load all data from files"""
|
||||||
|
self.load_conversations()
|
||||||
|
self.load_user_settings()
|
||||||
|
self.load_user_tokens()
|
||||||
|
|
||||||
|
def save_data(self):
|
||||||
|
"""Save all data to files"""
|
||||||
|
self.save_conversations()
|
||||||
|
self.save_all_user_settings()
|
||||||
|
self.save_user_tokens()
|
||||||
|
|
||||||
|
def load_conversations(self):
|
||||||
|
"""Load conversations from file"""
|
||||||
|
if os.path.exists(self.conversations_file):
|
||||||
|
try:
|
||||||
|
with open(self.conversations_file, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
# Convert to Conversation objects
|
||||||
|
self.conversations = {
|
||||||
|
user_id: {
|
||||||
|
conv_id: Conversation.model_validate(conv_data)
|
||||||
|
for conv_id, conv_data in user_convs.items()
|
||||||
|
}
|
||||||
|
for user_id, user_convs in data.items()
|
||||||
|
}
|
||||||
|
print(f"Loaded conversations for {len(self.conversations)} users")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading conversations: {e}")
|
||||||
|
self.conversations = {}
|
||||||
|
|
||||||
|
def save_conversations(self):
|
||||||
|
"""Save conversations to file"""
|
||||||
|
try:
|
||||||
|
# Convert to JSON-serializable format
|
||||||
|
serializable_data = {
|
||||||
|
user_id: {
|
||||||
|
conv_id: conv.model_dump()
|
||||||
|
for conv_id, conv in user_convs.items()
|
||||||
|
}
|
||||||
|
for user_id, user_convs in self.conversations.items()
|
||||||
|
}
|
||||||
|
with open(self.conversations_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(serializable_data, f, indent=2, default=str, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving conversations: {e}")
|
||||||
|
|
||||||
|
def load_user_settings(self):
|
||||||
|
"""Load user settings from file"""
|
||||||
|
if os.path.exists(self.settings_file):
|
||||||
|
try:
|
||||||
|
with open(self.settings_file, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
# Convert to UserSettings objects
|
||||||
|
self.user_settings = {
|
||||||
|
user_id: UserSettings.model_validate(settings_data)
|
||||||
|
for user_id, settings_data in data.items()
|
||||||
|
}
|
||||||
|
print(f"Loaded settings for {len(self.user_settings)} users")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading user settings: {e}")
|
||||||
|
self.user_settings = {}
|
||||||
|
|
||||||
|
def save_all_user_settings(self):
|
||||||
|
"""Save all user settings to file"""
|
||||||
|
try:
|
||||||
|
# Convert to JSON-serializable format
|
||||||
|
serializable_data = {
|
||||||
|
user_id: settings.model_dump()
|
||||||
|
for user_id, settings in self.user_settings.items()
|
||||||
|
}
|
||||||
|
with open(self.settings_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(serializable_data, f, indent=2, default=str, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving user settings: {e}")
|
||||||
|
|
||||||
|
# ============= Conversation Methods =============
|
||||||
|
|
||||||
|
def get_user_conversations(self, user_id: str) -> List[Conversation]:
|
||||||
|
"""Get all conversations for a user"""
|
||||||
|
return list(self.conversations.get(user_id, {}).values())
|
||||||
|
|
||||||
|
def get_conversation(self, user_id: str, conversation_id: str) -> Optional[Conversation]:
|
||||||
|
"""Get a specific conversation for a user"""
|
||||||
|
return self.conversations.get(user_id, {}).get(conversation_id)
|
||||||
|
|
||||||
|
def save_conversation(self, user_id: str, conversation: Conversation) -> Conversation:
|
||||||
|
"""Save a conversation for a user"""
|
||||||
|
# Update the timestamp
|
||||||
|
conversation.updated_at = datetime.datetime.now()
|
||||||
|
|
||||||
|
# Initialize user's conversations dict if it doesn't exist
|
||||||
|
if user_id not in self.conversations:
|
||||||
|
self.conversations[user_id] = {}
|
||||||
|
|
||||||
|
# Save the conversation
|
||||||
|
self.conversations[user_id][conversation.id] = conversation
|
||||||
|
|
||||||
|
# Save to disk
|
||||||
|
self.save_conversations()
|
||||||
|
|
||||||
|
return conversation
|
||||||
|
|
||||||
|
def delete_conversation(self, user_id: str, conversation_id: str) -> bool:
|
||||||
|
"""Delete a conversation for a user"""
|
||||||
|
if user_id in self.conversations and conversation_id in self.conversations[user_id]:
|
||||||
|
del self.conversations[user_id][conversation_id]
|
||||||
|
self.save_conversations()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ============= User Settings Methods =============
|
||||||
|
|
||||||
|
def get_user_settings(self, user_id: str) -> UserSettings:
|
||||||
|
"""Get settings for a user, creating default settings if they don't exist"""
|
||||||
|
if user_id not in self.user_settings:
|
||||||
|
self.user_settings[user_id] = UserSettings()
|
||||||
|
|
||||||
|
return self.user_settings[user_id]
|
||||||
|
|
||||||
|
def save_user_settings(self, user_id: str, settings: UserSettings) -> UserSettings:
|
||||||
|
"""Save settings for a user"""
|
||||||
|
# Update the timestamp
|
||||||
|
settings.last_updated = datetime.datetime.now()
|
||||||
|
|
||||||
|
# Save the settings
|
||||||
|
self.user_settings[user_id] = settings
|
||||||
|
|
||||||
|
# Save to disk
|
||||||
|
self.save_all_user_settings()
|
||||||
|
|
||||||
|
return settings
|
||||||
|
|
||||||
|
# ============= User Tokens Methods =============
|
||||||
|
|
||||||
|
def load_user_tokens(self):
|
||||||
|
"""Load user tokens from file"""
|
||||||
|
if os.path.exists(self.tokens_file):
|
||||||
|
try:
|
||||||
|
with open(self.tokens_file, "r", encoding="utf-8") as f:
|
||||||
|
self.user_tokens = json.load(f)
|
||||||
|
print(f"Loaded tokens for {len(self.user_tokens)} users")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading user tokens: {e}")
|
||||||
|
self.user_tokens = {}
|
||||||
|
|
||||||
|
def save_user_tokens(self):
|
||||||
|
"""Save user tokens to file"""
|
||||||
|
try:
|
||||||
|
with open(self.tokens_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(self.user_tokens, f, indent=2, default=str, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving user tokens: {e}")
|
||||||
|
|
||||||
|
def get_user_token(self, user_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get token data for a user"""
|
||||||
|
return self.user_tokens.get(user_id)
|
||||||
|
|
||||||
|
def save_user_token(self, user_id: str, token_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Save token data for a user"""
|
||||||
|
# Add the time when the token was saved
|
||||||
|
token_data["saved_at"] = datetime.datetime.now().isoformat()
|
||||||
|
|
||||||
|
# Save the token data
|
||||||
|
self.user_tokens[user_id] = token_data
|
||||||
|
|
||||||
|
# Save to disk
|
||||||
|
self.save_user_tokens()
|
||||||
|
|
||||||
|
return token_data
|
||||||
|
|
||||||
|
def delete_user_token(self, user_id: str) -> bool:
|
||||||
|
"""Delete token data for a user"""
|
||||||
|
if user_id in self.user_tokens:
|
||||||
|
del self.user_tokens[user_id]
|
||||||
|
self.save_user_tokens()
|
||||||
|
return True
|
||||||
|
return False
|
207
api_service/discord_client.py
Normal file
207
api_service/discord_client.py
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
import aiohttp
|
||||||
|
import json
|
||||||
|
import datetime
|
||||||
|
from typing import Dict, List, Optional, Any, Union
|
||||||
|
from api_models import Conversation, UserSettings, Message
|
||||||
|
|
||||||
|
class ApiClient:
|
||||||
|
def __init__(self, api_url: str, token: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
Initialize the API client
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_url: The URL of the API server
|
||||||
|
token: The Discord token to use for authentication
|
||||||
|
"""
|
||||||
|
self.api_url = api_url
|
||||||
|
self.token = token
|
||||||
|
|
||||||
|
def set_token(self, token: str):
|
||||||
|
"""Set the Discord token for authentication"""
|
||||||
|
self.token = token
|
||||||
|
|
||||||
|
async def _make_request(self, method: str, endpoint: str, data: Optional[Dict] = None):
|
||||||
|
"""
|
||||||
|
Make a request to the API
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method: The HTTP method to use
|
||||||
|
endpoint: The API endpoint to call
|
||||||
|
data: The data to send with the request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The response data
|
||||||
|
"""
|
||||||
|
if not self.token:
|
||||||
|
raise ValueError("No token set for API client")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.token}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
url = f"{self.api_url}/{endpoint}"
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
if method == "GET":
|
||||||
|
async with session.get(url, headers=headers) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
error_text = await response.text()
|
||||||
|
raise Exception(f"API request failed: {response.status} - {error_text}")
|
||||||
|
response_text = await response.text()
|
||||||
|
return json.loads(response_text)
|
||||||
|
elif method == "POST":
|
||||||
|
# Convert data to JSON with datetime handling
|
||||||
|
json_data = json.dumps(data, default=str, ensure_ascii=False) if data else None
|
||||||
|
# Update headers for manually serialized JSON
|
||||||
|
if json_data:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
async with session.post(url, headers=headers, data=json_data) as response:
|
||||||
|
if response.status not in (200, 201):
|
||||||
|
error_text = await response.text()
|
||||||
|
raise Exception(f"API request failed: {response.status} - {error_text}")
|
||||||
|
response_text = await response.text()
|
||||||
|
return json.loads(response_text)
|
||||||
|
elif method == "PUT":
|
||||||
|
# Convert data to JSON with datetime handling
|
||||||
|
json_data = json.dumps(data, default=str, ensure_ascii=False) if data else None
|
||||||
|
# Update headers for manually serialized JSON
|
||||||
|
if json_data:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
async with session.put(url, headers=headers, data=json_data) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
error_text = await response.text()
|
||||||
|
raise Exception(f"API request failed: {response.status} - {error_text}")
|
||||||
|
response_text = await response.text()
|
||||||
|
return json.loads(response_text)
|
||||||
|
elif method == "DELETE":
|
||||||
|
async with session.delete(url, headers=headers) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
error_text = await response.text()
|
||||||
|
raise Exception(f"API request failed: {response.status} - {error_text}")
|
||||||
|
response_text = await response.text()
|
||||||
|
return json.loads(response_text)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported HTTP method: {method}")
|
||||||
|
|
||||||
|
# ============= Conversation Methods =============
|
||||||
|
|
||||||
|
async def get_conversations(self) -> List[Conversation]:
|
||||||
|
"""Get all conversations for the authenticated user"""
|
||||||
|
response = await self._make_request("GET", "conversations")
|
||||||
|
return [Conversation.model_validate(conv) for conv in response["conversations"]]
|
||||||
|
|
||||||
|
async def get_conversation(self, conversation_id: str) -> Conversation:
|
||||||
|
"""Get a specific conversation"""
|
||||||
|
response = await self._make_request("GET", f"conversations/{conversation_id}")
|
||||||
|
return Conversation.model_validate(response)
|
||||||
|
|
||||||
|
async def create_conversation(self, conversation: Conversation) -> Conversation:
|
||||||
|
"""Create a new conversation"""
|
||||||
|
response = await self._make_request("POST", "conversations", {"conversation": conversation.model_dump()})
|
||||||
|
return Conversation.model_validate(response)
|
||||||
|
|
||||||
|
async def update_conversation(self, conversation: Conversation) -> Conversation:
|
||||||
|
"""Update an existing conversation"""
|
||||||
|
response = await self._make_request("PUT", f"conversations/{conversation.id}", {"conversation": conversation.model_dump()})
|
||||||
|
return Conversation.model_validate(response)
|
||||||
|
|
||||||
|
async def delete_conversation(self, conversation_id: str) -> bool:
|
||||||
|
"""Delete a conversation"""
|
||||||
|
response = await self._make_request("DELETE", f"conversations/{conversation_id}")
|
||||||
|
return response["success"]
|
||||||
|
|
||||||
|
# ============= Settings Methods =============
|
||||||
|
|
||||||
|
async def get_settings(self) -> UserSettings:
|
||||||
|
"""Get settings for the authenticated user"""
|
||||||
|
response = await self._make_request("GET", "settings")
|
||||||
|
return UserSettings.model_validate(response["settings"])
|
||||||
|
|
||||||
|
async def update_settings(self, settings: UserSettings) -> UserSettings:
|
||||||
|
"""Update settings for the authenticated user"""
|
||||||
|
response = await self._make_request("PUT", "settings", {"settings": settings.model_dump()})
|
||||||
|
return UserSettings.model_validate(response)
|
||||||
|
|
||||||
|
# ============= Helper Methods =============
|
||||||
|
|
||||||
|
async def save_discord_conversation(
|
||||||
|
self,
|
||||||
|
messages: List[Dict[str, Any]],
|
||||||
|
model_id: str = "openai/gpt-3.5-turbo",
|
||||||
|
conversation_id: Optional[str] = None,
|
||||||
|
title: str = "Discord Conversation",
|
||||||
|
reasoning_enabled: bool = False,
|
||||||
|
reasoning_effort: str = "medium",
|
||||||
|
temperature: float = 0.7,
|
||||||
|
max_tokens: int = 1000,
|
||||||
|
web_search_enabled: bool = False,
|
||||||
|
system_message: Optional[str] = None
|
||||||
|
) -> Conversation:
|
||||||
|
"""
|
||||||
|
Save a conversation from Discord to the API
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages: List of message dictionaries with 'content', 'role', and 'timestamp'
|
||||||
|
model_id: The model ID to use for the conversation
|
||||||
|
conversation_id: Optional ID for the conversation (will create new if not provided)
|
||||||
|
title: The title of the conversation
|
||||||
|
reasoning_enabled: Whether reasoning is enabled for the conversation
|
||||||
|
reasoning_effort: The reasoning effort level ("low", "medium", "high")
|
||||||
|
temperature: The temperature setting for the model
|
||||||
|
max_tokens: The maximum tokens setting for the model
|
||||||
|
web_search_enabled: Whether web search is enabled for the conversation
|
||||||
|
system_message: Optional system message for the conversation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The saved Conversation object
|
||||||
|
"""
|
||||||
|
# Convert messages to the API format
|
||||||
|
api_messages = []
|
||||||
|
for msg in messages:
|
||||||
|
api_messages.append(Message(
|
||||||
|
content=msg["content"],
|
||||||
|
role=msg["role"],
|
||||||
|
timestamp=msg.get("timestamp", datetime.datetime.now()),
|
||||||
|
reasoning=msg.get("reasoning"),
|
||||||
|
usage_data=msg.get("usage_data")
|
||||||
|
))
|
||||||
|
|
||||||
|
# Create or update the conversation
|
||||||
|
if conversation_id:
|
||||||
|
# Try to get the existing conversation
|
||||||
|
try:
|
||||||
|
conversation = await self.get_conversation(conversation_id)
|
||||||
|
# Update the conversation
|
||||||
|
conversation.messages = api_messages
|
||||||
|
conversation.model_id = model_id
|
||||||
|
conversation.reasoning_enabled = reasoning_enabled
|
||||||
|
conversation.reasoning_effort = reasoning_effort
|
||||||
|
conversation.temperature = temperature
|
||||||
|
conversation.max_tokens = max_tokens
|
||||||
|
conversation.web_search_enabled = web_search_enabled
|
||||||
|
conversation.system_message = system_message
|
||||||
|
conversation.updated_at = datetime.datetime.now()
|
||||||
|
|
||||||
|
return await self.update_conversation(conversation)
|
||||||
|
except Exception:
|
||||||
|
# Conversation doesn't exist, create a new one
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Create a new conversation
|
||||||
|
conversation = Conversation(
|
||||||
|
id=conversation_id if conversation_id else None,
|
||||||
|
title=title,
|
||||||
|
messages=api_messages,
|
||||||
|
model_id=model_id,
|
||||||
|
reasoning_enabled=reasoning_enabled,
|
||||||
|
reasoning_effort=reasoning_effort,
|
||||||
|
temperature=temperature,
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
web_search_enabled=web_search_enabled,
|
||||||
|
system_message=system_message,
|
||||||
|
created_at=datetime.datetime.now(),
|
||||||
|
updated_at=datetime.datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self.create_conversation(conversation)
|
141
api_service/flutter_client.dart
Normal file
141
api_service/flutter_client.dart
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
// Note: This is a reference implementation.
|
||||||
|
// In your actual Flutter app, you would use:
|
||||||
|
// import 'package:flutter/foundation.dart';
|
||||||
|
// import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
// Simple HTTP client for reference purposes
|
||||||
|
class Response {
|
||||||
|
final int statusCode;
|
||||||
|
final String body;
|
||||||
|
|
||||||
|
Response(this.statusCode, this.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple HTTP methods for reference purposes
|
||||||
|
class http {
|
||||||
|
static Future<Response> get(Uri url, {Map<String, String>? headers}) async {
|
||||||
|
// This is a placeholder. In a real implementation, you would use the http package.
|
||||||
|
return Future.value(Response(200, '{}'));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Response> post(Uri url, {Map<String, String>? headers, dynamic body}) async {
|
||||||
|
// This is a placeholder. In a real implementation, you would use the http package.
|
||||||
|
return Future.value(Response(200, '{}'));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Response> put(Uri url, {Map<String, String>? headers, dynamic body}) async {
|
||||||
|
// This is a placeholder. In a real implementation, you would use the http package.
|
||||||
|
return Future.value(Response(200, '{}'));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Response> delete(Uri url, {Map<String, String>? headers}) async {
|
||||||
|
// This is a placeholder. In a real implementation, you would use the http package.
|
||||||
|
return Future.value(Response(200, '{}'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// API client for the unified API service
|
||||||
|
class ApiClient {
|
||||||
|
final String apiUrl;
|
||||||
|
String? _token;
|
||||||
|
|
||||||
|
ApiClient({required this.apiUrl, String? token}) : _token = token;
|
||||||
|
|
||||||
|
/// Set the Discord token for authentication
|
||||||
|
void setToken(String token) {
|
||||||
|
_token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the authorization header for API requests
|
||||||
|
String? getAuthHeader() {
|
||||||
|
if (_token == null) return null;
|
||||||
|
return 'Bearer $_token';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the client is authenticated
|
||||||
|
bool get isAuthenticated => _token != null;
|
||||||
|
|
||||||
|
/// Make a request to the API
|
||||||
|
Future<dynamic> _makeRequest(String method, String endpoint, {Map<String, dynamic>? data}) async {
|
||||||
|
if (_token == null) {
|
||||||
|
throw Exception('No token set for API client');
|
||||||
|
}
|
||||||
|
|
||||||
|
final headers = {'Authorization': 'Bearer $_token', 'Content-Type': 'application/json'};
|
||||||
|
|
||||||
|
final url = Uri.parse('$apiUrl/$endpoint');
|
||||||
|
Response response;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (method == 'GET') {
|
||||||
|
response = await http.get(url, headers: headers);
|
||||||
|
} else if (method == 'POST') {
|
||||||
|
response = await http.post(url, headers: headers, body: data != null ? jsonEncode(data) : null);
|
||||||
|
} else if (method == 'PUT') {
|
||||||
|
response = await http.put(url, headers: headers, body: data != null ? jsonEncode(data) : null);
|
||||||
|
} else if (method == 'DELETE') {
|
||||||
|
response = await http.delete(url, headers: headers);
|
||||||
|
} else {
|
||||||
|
throw Exception('Unsupported HTTP method: $method');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||||
|
throw Exception('API request failed: ${response.statusCode} - ${response.body}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonDecode(response.body);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error making API request: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= Conversation Methods =============
|
||||||
|
|
||||||
|
/// Get all conversations for the authenticated user
|
||||||
|
Future<List<Map<String, dynamic>>> getConversations() async {
|
||||||
|
final response = await _makeRequest('GET', 'conversations');
|
||||||
|
return List<Map<String, dynamic>>.from(response['conversations']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a specific conversation
|
||||||
|
Future<Map<String, dynamic>> getConversation(String conversationId) async {
|
||||||
|
final response = await _makeRequest('GET', 'conversations/$conversationId');
|
||||||
|
return Map<String, dynamic>.from(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new conversation
|
||||||
|
Future<Map<String, dynamic>> createConversation(Map<String, dynamic> conversation) async {
|
||||||
|
final response = await _makeRequest('POST', 'conversations', data: {'conversation': conversation});
|
||||||
|
return Map<String, dynamic>.from(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update an existing conversation
|
||||||
|
Future<Map<String, dynamic>> updateConversation(Map<String, dynamic> conversation) async {
|
||||||
|
final conversationId = conversation['id'];
|
||||||
|
final response = await _makeRequest('PUT', 'conversations/$conversationId', data: {'conversation': conversation});
|
||||||
|
return Map<String, dynamic>.from(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a conversation
|
||||||
|
Future<bool> deleteConversation(String conversationId) async {
|
||||||
|
final response = await _makeRequest('DELETE', 'conversations/$conversationId');
|
||||||
|
return response['success'] as bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= Settings Methods =============
|
||||||
|
|
||||||
|
/// Get settings for the authenticated user
|
||||||
|
Future<Map<String, dynamic>> getSettings() async {
|
||||||
|
final response = await _makeRequest('GET', 'settings');
|
||||||
|
return Map<String, dynamic>.from(response['settings']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update settings for the authenticated user
|
||||||
|
Future<Map<String, dynamic>> updateSettings(Map<String, dynamic> settings) async {
|
||||||
|
final response = await _makeRequest('PUT', 'settings', data: {'settings': settings});
|
||||||
|
return Map<String, dynamic>.from(response);
|
||||||
|
}
|
||||||
|
}
|
19
api_service/run_api_server.py
Normal file
19
api_service/run_api_server.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import os
|
||||||
|
import uvicorn
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Get configuration from environment variables
|
||||||
|
host = os.getenv("API_HOST", "0.0.0.0")
|
||||||
|
port = int(os.getenv("API_PORT", "8000"))
|
||||||
|
data_dir = os.getenv("DATA_DIR", "data")
|
||||||
|
|
||||||
|
# Create data directory if it doesn't exist
|
||||||
|
os.makedirs(data_dir, exist_ok=True)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(f"Starting API server on {host}:{port}")
|
||||||
|
print(f"Data directory: {data_dir}")
|
||||||
|
uvicorn.run("api_server:app", host=host, port=port, reload=True)
|
Loading…
x
Reference in New Issue
Block a user