aa
This commit is contained in:
parent
6dd60e2c5b
commit
05d5394da7
71
dashboard_web/index.html
Normal file
71
dashboard_web/index.html
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<!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:</label>
|
||||||
|
<select id="welcome-channel" name="welcome_channel_id">
|
||||||
|
<!-- Channel options will be populated by JS -->
|
||||||
|
</select><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:</label>
|
||||||
|
<select id="goodbye-channel" name="goodbye_channel_id">
|
||||||
|
<!-- Channel options will be populated by JS -->
|
||||||
|
</select><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>
|
||||||
|
|
||||||
|
<!-- TODO: Add section for Command Permissions -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
351
dashboard_web/script.js
Normal file
351
dashboard_web/script.js
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
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'
|
||||||
|
const API_BASE_URL = '/api'; // Relative path if served by the same server
|
||||||
|
|
||||||
|
// --- 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 {
|
||||||
|
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
|
||||||
|
window.location.href = `${API_BASE_URL}/auth/login`;
|
||||||
|
});
|
||||||
|
|
||||||
|
logoutButton.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
await fetchAPI('/auth/logout', { method: 'POST' });
|
||||||
|
showLogin();
|
||||||
|
} catch (error) {
|
||||||
|
alert('Logout failed. Please try again.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Guild Loading and Settings ---
|
||||||
|
async function loadGuilds() {
|
||||||
|
try {
|
||||||
|
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 = '';
|
||||||
|
document.getElementById('welcome-channel').innerHTML = '';
|
||||||
|
document.getElementById('welcome-message').value = '';
|
||||||
|
document.getElementById('goodbye-channel').innerHTML = '';
|
||||||
|
document.getElementById('goodbye-message').value = '';
|
||||||
|
document.getElementById('cogs-list').innerHTML = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const settings = await fetchAPI(`/guilds/${guildId}/settings`);
|
||||||
|
console.log("Received settings:", settings);
|
||||||
|
|
||||||
|
// Populate Prefix
|
||||||
|
document.getElementById('prefix-input').value = settings.prefix || '';
|
||||||
|
|
||||||
|
// Populate Welcome/Goodbye IDs (Dropdown population is not feasible from API alone)
|
||||||
|
// We'll just display the ID if set, or allow input? Let's stick to the select for now,
|
||||||
|
// but it won't be populated dynamically. The user needs to know the channel ID.
|
||||||
|
// We can pre-select the stored value if it exists.
|
||||||
|
const wcSelect = document.getElementById('welcome-channel');
|
||||||
|
wcSelect.innerHTML = '<option value="">-- Select Channel --</option>'; // Clear previous options
|
||||||
|
if (settings.welcome_channel_id) {
|
||||||
|
// Add the stored ID as an option, maybe mark it as potentially invalid if needed
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = settings.welcome_channel_id;
|
||||||
|
option.textContent = `#? (ID: ${settings.welcome_channel_id})`; // Indicate it's just the ID
|
||||||
|
option.selected = true;
|
||||||
|
wcSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
document.getElementById('welcome-message').value = settings.welcome_message || '';
|
||||||
|
|
||||||
|
const gcSelect = document.getElementById('goodbye-channel');
|
||||||
|
gcSelect.innerHTML = '<option value="">-- Select Channel --</option>'; // Clear previous options
|
||||||
|
if (settings.goodbye_channel_id) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = settings.goodbye_channel_id;
|
||||||
|
option.textContent = `#? (ID: ${settings.goodbye_channel_id})`;
|
||||||
|
option.selected = true;
|
||||||
|
gcSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
document.getElementById('goodbye-message').value = settings.goodbye_message || '';
|
||||||
|
|
||||||
|
// Populate Cogs - This will only show cogs whose state is known by the API/DB
|
||||||
|
// It won't show all possible cogs unless the API is enhanced.
|
||||||
|
populateCogsList(settings.enabled_cogs || {}); // Use the correct field name
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
displayFeedback('prefix-feedback', `Error loading settings: ${error.message}`, true); // Use a general feedback area?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removed populateChannelSelect as dynamic population isn't feasible from API alone.
|
||||||
|
// Users will need to manage channel IDs directly for now.
|
||||||
|
|
||||||
|
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
|
||||||
|
const CORE_COGS = ['SettingsCog', 'HelpCog']; // Example - needs to match backend
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- 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 {
|
||||||
|
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;
|
||||||
|
// Get channel ID directly. Assume user inputs/knows the ID.
|
||||||
|
// We might change the input type from select later if this is confusing.
|
||||||
|
const channelIdInput = document.getElementById('welcome-channel').value; // Treat select as input for now
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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; // Treat select as input
|
||||||
|
const message = document.getElementById('goodbye-message').value;
|
||||||
|
if (!guildId) return;
|
||||||
|
|
||||||
|
const channelId = channelIdInput && /^\d+$/.test(channelIdInput) ? channelIdInput : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// --- Initial Load ---
|
||||||
|
checkLoginStatus();
|
||||||
|
});
|
98
dashboard_web/style.css
Normal file
98
dashboard_web/style.css
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Feedback messages */
|
||||||
|
p[id$="-feedback"] {
|
||||||
|
font-style: italic;
|
||||||
|
color: green;
|
||||||
|
margin-top: 5px;
|
||||||
|
min-height: 1em; /* Reserve space */
|
||||||
|
}
|
||||||
|
|
||||||
|
p[id$="-feedback"].error {
|
||||||
|
color: red;
|
||||||
|
}
|
70
main.py
70
main.py
@ -65,11 +65,45 @@ log = logging.getLogger(__name__) # Logger for main.py
|
|||||||
# --- Events ---
|
# --- Events ---
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_ready():
|
async def on_ready():
|
||||||
print(f'{bot.user.name} has connected to Discord!')
|
log.info(f'{bot.user.name} has connected to Discord!')
|
||||||
print(f'Bot ID: {bot.user.id}')
|
log.info(f'Bot ID: {bot.user.id}')
|
||||||
# Set the bot's status
|
# Set the bot's status
|
||||||
await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name="!help"))
|
await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name="!help"))
|
||||||
print("Bot status set to 'Listening to !help'")
|
log.info("Bot status set to 'Listening to !help'")
|
||||||
|
|
||||||
|
# --- Add current guilds to DB ---
|
||||||
|
if settings_manager and settings_manager.pg_pool:
|
||||||
|
log.info("Syncing guilds with database...")
|
||||||
|
try:
|
||||||
|
async with settings_manager.pg_pool.acquire() as conn:
|
||||||
|
# Get guilds bot is currently in
|
||||||
|
current_guild_ids = {guild.id for guild in bot.guilds}
|
||||||
|
log.debug(f"Bot is currently in {len(current_guild_ids)} guilds.")
|
||||||
|
|
||||||
|
# Get guilds currently in DB
|
||||||
|
db_records = await conn.fetch("SELECT guild_id FROM guilds")
|
||||||
|
db_guild_ids = {record['guild_id'] for record in db_records}
|
||||||
|
log.debug(f"Found {len(db_guild_ids)} guilds in database.")
|
||||||
|
|
||||||
|
# Add guilds bot joined while offline
|
||||||
|
guilds_to_add = current_guild_ids - db_guild_ids
|
||||||
|
if guilds_to_add:
|
||||||
|
log.info(f"Adding {len(guilds_to_add)} new guilds to database: {guilds_to_add}")
|
||||||
|
await conn.executemany("INSERT INTO guilds (guild_id) VALUES ($1) ON CONFLICT DO NOTHING;",
|
||||||
|
[(guild_id,) for guild_id in guilds_to_add])
|
||||||
|
|
||||||
|
# Remove guilds bot left while offline
|
||||||
|
guilds_to_remove = db_guild_ids - current_guild_ids
|
||||||
|
if guilds_to_remove:
|
||||||
|
log.info(f"Removing {len(guilds_to_remove)} guilds from database: {guilds_to_remove}")
|
||||||
|
await conn.execute("DELETE FROM guilds WHERE guild_id = ANY($1::bigint[])", list(guilds_to_remove))
|
||||||
|
|
||||||
|
log.info("Guild sync with database complete.")
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Error syncing guilds with database on ready.")
|
||||||
|
else:
|
||||||
|
log.warning("Settings manager not available or pool not initialized, skipping guild sync.")
|
||||||
|
# -----------------------------
|
||||||
|
|
||||||
# Patch Discord methods to store message content
|
# Patch Discord methods to store message content
|
||||||
try:
|
try:
|
||||||
@ -112,6 +146,36 @@ async def on_shard_disconnect(shard_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to reconnect shard {shard_id}: {e}")
|
print(f"Failed to reconnect shard {shard_id}: {e}")
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_guild_join(guild: discord.Guild):
|
||||||
|
"""Adds guild to database when bot joins."""
|
||||||
|
log.info(f"Joined guild: {guild.name} ({guild.id})")
|
||||||
|
if settings_manager and settings_manager.pg_pool:
|
||||||
|
try:
|
||||||
|
async with settings_manager.pg_pool.acquire() as conn:
|
||||||
|
await conn.execute("INSERT INTO guilds (guild_id) VALUES ($1) ON CONFLICT DO NOTHING;", guild.id)
|
||||||
|
log.info(f"Added guild {guild.id} to database.")
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Failed to add guild {guild.id} to database on join.")
|
||||||
|
else:
|
||||||
|
log.warning("Settings manager not available or pool not initialized, cannot add guild on join.")
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_guild_remove(guild: discord.Guild):
|
||||||
|
"""Removes guild from database when bot leaves."""
|
||||||
|
log.info(f"Left guild: {guild.name} ({guild.id})")
|
||||||
|
if settings_manager and settings_manager.pg_pool:
|
||||||
|
try:
|
||||||
|
async with settings_manager.pg_pool.acquire() as conn:
|
||||||
|
# Note: Cascading deletes should handle related settings in other tables
|
||||||
|
await conn.execute("DELETE FROM guilds WHERE guild_id = $1", guild.id)
|
||||||
|
log.info(f"Removed guild {guild.id} from database.")
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Failed to remove guild {guild.id} from database on leave.")
|
||||||
|
else:
|
||||||
|
log.warning("Settings manager not available or pool not initialized, cannot remove guild on leave.")
|
||||||
|
|
||||||
|
|
||||||
# Error handling - Updated to handle custom check failures
|
# Error handling - Updated to handle custom check failures
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_command_error(ctx, error):
|
async def on_command_error(ctx, error):
|
||||||
|
@ -12,8 +12,11 @@ google-cloud-storage
|
|||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
requests
|
requests
|
||||||
fastapi
|
fastapi
|
||||||
uvicorn
|
uvicorn[standard] # Use standard install for performance features like websockets if needed later
|
||||||
python-multipart
|
python-multipart
|
||||||
|
python-jose[cryptography] # For JWT handling in auth
|
||||||
|
itsdangerous # For secure session cookies
|
||||||
|
pydantic-settings # For loading config from .env
|
||||||
tenacity
|
tenacity
|
||||||
cachetools
|
cachetools
|
||||||
pillow
|
pillow
|
||||||
|
@ -494,3 +494,71 @@ async def check_command_permission(guild_id: int, command_name: str, member_role
|
|||||||
else:
|
else:
|
||||||
log.debug(f"Permission denied for '{command_name}' (Guild: {guild_id}). Member roles {member_roles_ids_str} not in allowed roles {allowed_role_ids_str}.")
|
log.debug(f"Permission denied for '{command_name}' (Guild: {guild_id}). Member roles {member_roles_ids_str} not in allowed roles {allowed_role_ids_str}.")
|
||||||
return False # Member has none of the specifically allowed roles
|
return False # Member has none of the specifically allowed roles
|
||||||
|
|
||||||
|
|
||||||
|
async def get_command_permissions(guild_id: int, command_name: str) -> set[int] | None:
|
||||||
|
"""Gets the set of allowed role IDs for a specific command, checking cache first. Returns None on error."""
|
||||||
|
if not pg_pool or not redis_pool:
|
||||||
|
log.warning(f"Pools not initialized, cannot get permissions for command '{command_name}'.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
cache_key = _get_redis_key(guild_id, "cmd_perms", command_name)
|
||||||
|
try:
|
||||||
|
# Check cache first
|
||||||
|
if await redis_pool.exists(cache_key):
|
||||||
|
cached_roles_str = await redis_pool.smembers(cache_key)
|
||||||
|
if cached_roles_str == {"__EMPTY_SET__"}:
|
||||||
|
log.debug(f"Cache hit (empty set) for cmd perms '{command_name}' (Guild: {guild_id}).")
|
||||||
|
return set() # Return empty set if explicitly empty
|
||||||
|
allowed_role_ids = {int(role_id) for role_id in cached_roles_str}
|
||||||
|
log.debug(f"Cache hit for cmd perms '{command_name}' (Guild: {guild_id})")
|
||||||
|
return allowed_role_ids
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Redis error getting cmd perms for '{command_name}' (Guild: {guild_id}): {e}")
|
||||||
|
# Fall through to DB query on Redis error
|
||||||
|
|
||||||
|
log.debug(f"Cache miss for cmd perms '{command_name}' (Guild: {guild_id})")
|
||||||
|
try:
|
||||||
|
async with pg_pool.acquire() as conn:
|
||||||
|
records = await conn.fetch(
|
||||||
|
"SELECT allowed_role_id FROM command_permissions WHERE guild_id = $1 AND command_name = $2",
|
||||||
|
guild_id, command_name
|
||||||
|
)
|
||||||
|
allowed_role_ids = {record['allowed_role_id'] for record in records}
|
||||||
|
|
||||||
|
# Cache the result
|
||||||
|
try:
|
||||||
|
allowed_role_ids_str = {str(role_id) for role_id in allowed_role_ids}
|
||||||
|
async with redis_pool.pipeline(transaction=True) as pipe:
|
||||||
|
pipe.delete(cache_key) # Ensure clean state
|
||||||
|
if allowed_role_ids_str:
|
||||||
|
pipe.sadd(cache_key, *allowed_role_ids_str)
|
||||||
|
else:
|
||||||
|
pipe.sadd(cache_key, "__EMPTY_SET__") # Marker for empty set
|
||||||
|
pipe.expire(cache_key, 3600) # Cache for 1 hour
|
||||||
|
await pipe.execute()
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Redis error setting cache for cmd perms '{command_name}' (Guild: {guild_id}): {e}")
|
||||||
|
|
||||||
|
return allowed_role_ids
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Database error getting cmd perms for '{command_name}' (Guild: {guild_id}): {e}")
|
||||||
|
return None # Indicate error
|
||||||
|
|
||||||
|
|
||||||
|
# --- Bot Guild Information ---
|
||||||
|
|
||||||
|
async def get_bot_guild_ids() -> set[int] | None:
|
||||||
|
"""Gets the set of all guild IDs known to the bot from the guilds table. Returns None on error."""
|
||||||
|
if not pg_pool:
|
||||||
|
log.error("Pools not initialized, cannot get bot guild IDs.")
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
async with pg_pool.acquire() as conn:
|
||||||
|
records = await conn.fetch("SELECT guild_id FROM guilds")
|
||||||
|
guild_ids = {record['guild_id'] for record in records}
|
||||||
|
log.debug(f"Fetched {len(guild_ids)} guild IDs from database.")
|
||||||
|
return guild_ids
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Database error fetching bot guild IDs.")
|
||||||
|
return None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user