aaa
This commit is contained in:
parent
3be8feecb4
commit
d97555d959
@ -16,6 +16,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
# --- Logging Configuration ---
|
# --- Logging Configuration ---
|
||||||
# Configure logging
|
# Configure logging
|
||||||
@ -60,6 +61,9 @@ class ApiSettings(BaseSettings):
|
|||||||
REDIS_PORT: int = 6379
|
REDIS_PORT: int = 6379
|
||||||
REDIS_PASSWORD: Optional[str] = None # Optional
|
REDIS_PASSWORD: Optional[str] = None # Optional
|
||||||
|
|
||||||
|
# Secret key for AI Moderation API endpoint
|
||||||
|
MOD_LOG_API_SECRET: Optional[str] = None
|
||||||
|
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
env_file=dotenv_path,
|
env_file=dotenv_path,
|
||||||
env_file_encoding='utf-8',
|
env_file_encoding='utf-8',
|
||||||
@ -2611,6 +2615,7 @@ async def get_token(user_id: str = Depends(verify_discord_token)):
|
|||||||
# Return only the access token, not the full token data
|
# Return only the access token, not the full token data
|
||||||
return {"access_token": token_data.get("access_token")}
|
return {"access_token": token_data.get("access_token")}
|
||||||
|
|
||||||
|
|
||||||
@api_app.get("/token/{user_id}")
|
@api_app.get("/token/{user_id}")
|
||||||
@discordapi_app.get("/token/{user_id}")
|
@discordapi_app.get("/token/{user_id}")
|
||||||
async def get_token_by_user_id(user_id: str):
|
async def get_token_by_user_id(user_id: str):
|
||||||
|
@ -107,7 +107,34 @@ class CommandInfo(BaseModel):
|
|||||||
enabled: bool = True
|
enabled: bool = True
|
||||||
cog_name: Optional[str] = None
|
cog_name: Optional[str] = None
|
||||||
|
|
||||||
|
class Guild(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
icon_url: Optional[str] = None
|
||||||
|
|
||||||
# --- Endpoints ---
|
# --- Endpoints ---
|
||||||
|
|
||||||
|
@router.get("/user-guilds", response_model=List[Guild])
|
||||||
|
async def get_user_guilds(
|
||||||
|
user: dict = Depends(get_dashboard_user)
|
||||||
|
):
|
||||||
|
"""Get all guilds the user is an admin of."""
|
||||||
|
try:
|
||||||
|
# This would normally fetch guilds from Discord API or the bot
|
||||||
|
# For now, we'll return a mock response
|
||||||
|
# TODO: Replace mock data with actual API call to Discord
|
||||||
|
guilds = [
|
||||||
|
Guild(id="123456789", name="My Awesome Server", icon_url="https://cdn.discordapp.com/icons/123456789/abc123def456ghi789jkl012mno345pqr.png"),
|
||||||
|
Guild(id="987654321", name="Another Great Server", icon_url="https://cdn.discordapp.com/icons/987654321/zyx987wvu654tsr321qpo098mlk765jih.png")
|
||||||
|
]
|
||||||
|
return guilds
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error getting user guilds: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Error getting user guilds: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
@router.get("/guilds/{guild_id}/channels", response_model=List[Channel])
|
@router.get("/guilds/{guild_id}/channels", response_model=List[Channel])
|
||||||
async def get_guild_channels(
|
async def get_guild_channels(
|
||||||
guild_id: int,
|
guild_id: int,
|
||||||
@ -118,6 +145,7 @@ async def get_guild_channels(
|
|||||||
try:
|
try:
|
||||||
# This would normally fetch channels from Discord API or the bot
|
# This would normally fetch channels from Discord API or the bot
|
||||||
# For now, we'll return a mock response
|
# For now, we'll return a mock response
|
||||||
|
# TODO: Replace mock data with actual API call to Discord
|
||||||
channels = [
|
channels = [
|
||||||
Channel(id="123456789", name="general", type=0),
|
Channel(id="123456789", name="general", type=0),
|
||||||
Channel(id="123456790", name="welcome", type=0),
|
Channel(id="123456790", name="welcome", type=0),
|
||||||
@ -142,6 +170,7 @@ async def get_guild_roles(
|
|||||||
try:
|
try:
|
||||||
# This would normally fetch roles from Discord API or the bot
|
# This would normally fetch roles from Discord API or the bot
|
||||||
# For now, we'll return a mock response
|
# For now, we'll return a mock response
|
||||||
|
# TODO: Replace mock data with actual API call to Discord
|
||||||
roles = [
|
roles = [
|
||||||
Role(id="123456789", name="@everyone", color=0, position=0, permissions="0"),
|
Role(id="123456789", name="@everyone", color=0, position=0, permissions="0"),
|
||||||
Role(id="123456790", name="Admin", color=16711680, position=1, permissions="8"),
|
Role(id="123456790", name="Admin", color=16711680, position=1, permissions="8"),
|
||||||
@ -166,6 +195,7 @@ async def get_guild_commands(
|
|||||||
try:
|
try:
|
||||||
# This would normally fetch commands from the bot
|
# This would normally fetch commands from the bot
|
||||||
# For now, we'll return a mock response
|
# For now, we'll return a mock response
|
||||||
|
# TODO: Replace mock data with actual bot command introspection
|
||||||
commands = [
|
commands = [
|
||||||
Command(name="help", description="Show help message"),
|
Command(name="help", description="Show help message"),
|
||||||
Command(name="ping", description="Check bot latency"),
|
Command(name="ping", description="Check bot latency"),
|
||||||
@ -651,6 +681,7 @@ async def sync_guild_commands(
|
|||||||
# This endpoint would trigger a command sync for the guild
|
# This endpoint would trigger a command sync for the guild
|
||||||
# In a real implementation, this would communicate with the bot to sync commands
|
# In a real implementation, this would communicate with the bot to sync commands
|
||||||
# For now, we'll just return a success message
|
# For now, we'll just return a success message
|
||||||
|
# TODO: Implement actual command syncing logic
|
||||||
return {"message": "Command sync requested. This may take a moment to complete."}
|
return {"message": "Command sync requested. This may take a moment to complete."}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Error syncing commands for guild {guild_id}: {e}")
|
log.error(f"Error syncing commands for guild {guild_id}: {e}")
|
||||||
@ -1168,6 +1199,7 @@ async def get_conversations(
|
|||||||
try:
|
try:
|
||||||
# This would normally fetch conversations from the database
|
# This would normally fetch conversations from the database
|
||||||
# For now, we'll return a mock response
|
# For now, we'll return a mock response
|
||||||
|
# TODO: Implement actual conversation fetching
|
||||||
conversations = [
|
conversations = [
|
||||||
Conversation(
|
Conversation(
|
||||||
id="1",
|
id="1",
|
||||||
@ -1201,6 +1233,7 @@ async def get_conversation_messages(
|
|||||||
try:
|
try:
|
||||||
# This would normally fetch messages from the database
|
# This would normally fetch messages from the database
|
||||||
# For now, we'll return a mock response
|
# For now, we'll return a mock response
|
||||||
|
# TODO: Implement actual message fetching
|
||||||
messages = [
|
messages = [
|
||||||
Message(
|
Message(
|
||||||
id="1",
|
id="1",
|
||||||
|
@ -29,7 +29,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Dashboard Section -->
|
<!-- Server Selection Section (New) -->
|
||||||
|
<div id="server-select-section" class="container dashboard-section" style="display: none; margin-top: 80px;">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">Select a Server</h2>
|
||||||
|
<p class="text-muted">Choose the server you want to manage.</p>
|
||||||
|
</div>
|
||||||
|
<div id="server-list-container" class="server-list-grid p-4">
|
||||||
|
<!-- Server items will be populated by JS -->
|
||||||
|
<div class="loading-spinner-container">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dashboard Section (Initially hidden until server selected) -->
|
||||||
<div id="dashboard-container" class="dashboard-container" style="display: none;">
|
<div id="dashboard-container" class="dashboard-container" style="display: none;">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div id="sidebar" class="sidebar">
|
<div id="sidebar" class="sidebar">
|
||||||
@ -100,16 +116,12 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="card-title">Manage Server Settings</h2>
|
<h2 class="card-title">Manage Server Settings</h2>
|
||||||
|
<p class="text-muted">Settings for the selected server.</p> <!-- Added description -->
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<!-- Removed guild-select dropdown -->
|
||||||
<label for="guild-select">Select Server:</label>
|
|
||||||
<select name="guilds" id="guild-select" class="w-full">
|
|
||||||
<option value="">--Please choose a server--</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="settings-form" style="display: none;">
|
<div id="settings-form"> <!-- Display block by default now, JS will load content -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3 class="card-title">Prefix Settings</h3>
|
<h3 class="card-title">Prefix Settings</h3>
|
||||||
@ -500,13 +512,9 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="card-title">Manage Cogs & Commands</h2>
|
<h2 class="card-title">Manage Cogs & Commands</h2>
|
||||||
|
<p class="text-muted">Enable/disable modules and commands for the selected server.</p> <!-- Added description -->
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<!-- Removed cog-guild-select dropdown -->
|
||||||
<label for="cog-guild-select">Select Server:</label>
|
|
||||||
<select name="guilds" id="cog-guild-select" class="w-full">
|
|
||||||
<option value="">--Please choose a server--</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="cog-management-loading" class="loading-container">
|
<div id="cog-management-loading" class="loading-container">
|
||||||
|
@ -432,37 +432,3 @@ function saveCommandsSettings() {
|
|||||||
Toast.error('Failed to save command settings. Please try again.');
|
Toast.error('Failed to save command settings. Please try again.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show a specific section and hide others
|
|
||||||
* @param {string} sectionId - ID of the section to show
|
|
||||||
*/
|
|
||||||
function showSection(sectionId) {
|
|
||||||
// Get all sections
|
|
||||||
const sections = document.querySelectorAll('.dashboard-section');
|
|
||||||
|
|
||||||
// Hide all sections
|
|
||||||
sections.forEach(section => {
|
|
||||||
section.style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get all nav buttons
|
|
||||||
const navButtons = document.querySelectorAll('.nav-button');
|
|
||||||
|
|
||||||
// Remove active class from all nav buttons
|
|
||||||
navButtons.forEach(button => {
|
|
||||||
button.classList.remove('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show the selected section and activate the corresponding nav button
|
|
||||||
const selectedSection = document.getElementById(`${sectionId}-section`);
|
|
||||||
const selectedNavButton = document.getElementById(`nav-${sectionId}`);
|
|
||||||
|
|
||||||
if (selectedSection) {
|
|
||||||
selectedSection.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedNavButton) {
|
|
||||||
selectedNavButton.classList.add('active');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -18,6 +18,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Initialize dropdowns
|
// Initialize dropdowns
|
||||||
initDropdowns();
|
initDropdowns();
|
||||||
|
|
||||||
|
// Store selected guild ID globally (using localStorage)
|
||||||
|
window.selectedGuildId = localStorage.getItem('selectedGuildId');
|
||||||
|
window.currentSettingsGuildId = null; // Track which guild's settings are loaded
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -163,8 +167,8 @@ function initAuth() {
|
|||||||
loadUserInfo();
|
loadUserInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load initial data
|
// Show server selection screen first
|
||||||
loadDashboardData();
|
showServerSelection();
|
||||||
} else {
|
} else {
|
||||||
// User is not authenticated, show login
|
// User is not authenticated, show login
|
||||||
if (authSection) authSection.style.display = 'block';
|
if (authSection) authSection.style.display = 'block';
|
||||||
@ -283,13 +287,27 @@ function initTabs() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a specific section of the dashboard
|
* Show a specific section of the dashboard
|
||||||
* @param {string} sectionId - The ID of the section to show
|
* @param {string} sectionId - The ID of the section to show (e.g., 'server-settings')
|
||||||
*/
|
*/
|
||||||
function showSection(sectionId) {
|
function showSection(sectionId) {
|
||||||
// Hide all sections
|
console.log(`Attempting to show section: ${sectionId}`);
|
||||||
|
|
||||||
|
// Check if a server is selected before showing any section other than server-select
|
||||||
|
if (!window.selectedGuildId && sectionId !== 'server-select') {
|
||||||
|
console.log('No server selected, redirecting to server selection.');
|
||||||
|
showServerSelection(); // Redirect to server selection
|
||||||
|
return; // Stop further execution
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide all specific dashboard sections first
|
||||||
document.querySelectorAll('.dashboard-section').forEach(section => {
|
document.querySelectorAll('.dashboard-section').forEach(section => {
|
||||||
section.style.display = 'none';
|
section.style.display = 'none';
|
||||||
});
|
});
|
||||||
|
// Also hide the server selection section if it exists and we are showing another section
|
||||||
|
const serverSelectSection = document.getElementById('server-select-section');
|
||||||
|
if (serverSelectSection && sectionId !== 'server-select') {
|
||||||
|
serverSelectSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
// Remove active class from all nav items
|
// Remove active class from all nav items
|
||||||
document.querySelectorAll('.nav-item').forEach(item => {
|
document.querySelectorAll('.nav-item').forEach(item => {
|
||||||
@ -297,31 +315,76 @@ function showSection(sectionId) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Show the selected section
|
// Show the selected section
|
||||||
const section = document.getElementById(`${sectionId}-section`);
|
const sectionElement = document.getElementById(`${sectionId}-section`);
|
||||||
if (section) {
|
if (sectionElement) {
|
||||||
section.style.display = 'block';
|
sectionElement.style.display = 'block';
|
||||||
}
|
console.log(`Successfully displayed section: ${sectionId}-section`);
|
||||||
|
|
||||||
// Add active class to the corresponding nav item
|
// Add active class to the corresponding nav item
|
||||||
const navItem = document.querySelector(`.nav-item[data-section="${sectionId}-section"]`);
|
const navItem = document.querySelector(`.nav-item[data-section="${sectionId}-section"]`);
|
||||||
if (navItem) {
|
if (navItem) {
|
||||||
navItem.classList.add('active');
|
navItem.classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load AI settings if needed
|
// Load data for the specific section if needed and a guild is selected
|
||||||
if (sectionId === 'ai-settings' && typeof loadAiSettings === 'function' && typeof aiSettingsLoaded !== 'undefined' && !aiSettingsLoaded) {
|
if (window.selectedGuildId) {
|
||||||
loadAiSettings();
|
// Load AI settings if needed (assuming it's guild-specific now)
|
||||||
}
|
if (sectionId === 'ai-settings' && typeof loadAiSettings === 'function') {
|
||||||
|
// Check if already loaded for this guild to prevent redundant calls
|
||||||
|
// This requires loadAiSettings to track its loaded state per guild or be idempotent
|
||||||
|
console.log(`Loading AI settings for guild ${window.selectedGuildId}`);
|
||||||
|
loadAiSettings(window.selectedGuildId); // Pass guildId
|
||||||
|
}
|
||||||
|
|
||||||
// Load theme settings if needed
|
// Load theme settings if needed (assuming global/user-specific)
|
||||||
if (sectionId === 'theme-settings' && typeof loadThemeSettings === 'function' && typeof themeSettingsLoaded !== 'undefined' && !themeSettingsLoaded) {
|
if (sectionId === 'theme-settings' && typeof loadThemeSettings === 'function' && typeof themeSettingsLoaded !== 'undefined' && !themeSettingsLoaded) {
|
||||||
loadThemeSettings();
|
console.log("Loading theme settings");
|
||||||
}
|
loadThemeSettings();
|
||||||
|
// themeSettingsLoaded = true; // Assuming loadThemeSettings handles this
|
||||||
|
}
|
||||||
|
|
||||||
// Load cog management if needed
|
// Load cog management if needed
|
||||||
if (sectionId === 'cog-management' && typeof loadGuildsForCogManagement === 'function' && typeof cogManagementLoaded !== 'undefined' && !cogManagementLoaded) {
|
if (sectionId === 'cog-management' && typeof loadCogManagementData === 'function') {
|
||||||
loadGuildsForCogManagement();
|
// Check if already loaded for this guild
|
||||||
cogManagementLoaded = true;
|
if (!window.cogManagementLoadedGuild || window.cogManagementLoadedGuild !== window.selectedGuildId) {
|
||||||
|
console.log(`Loading Cog Management data for guild ${window.selectedGuildId}`);
|
||||||
|
loadCogManagementData(window.selectedGuildId); // Pass guildId
|
||||||
|
window.cogManagementLoadedGuild = window.selectedGuildId; // Track loaded guild
|
||||||
|
} else {
|
||||||
|
console.log(`Cog Management data for guild ${window.selectedGuildId} already loaded.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load command customization if needed
|
||||||
|
if (sectionId === 'command-customization' && typeof loadCommandCustomizationData === 'function') {
|
||||||
|
// Check if already loaded for this guild
|
||||||
|
if (!window.commandCustomizationLoadedGuild || window.commandCustomizationLoadedGuild !== window.selectedGuildId) {
|
||||||
|
console.log(`Loading Command Customization data for guild ${window.selectedGuildId}`);
|
||||||
|
loadCommandCustomizationData(window.selectedGuildId); // Pass guildId
|
||||||
|
window.commandCustomizationLoadedGuild = window.selectedGuildId; // Track loaded guild
|
||||||
|
} else {
|
||||||
|
console.log(`Command Customization data for guild ${window.selectedGuildId} already loaded.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load general server settings (prefix, welcome/leave, modules, permissions) if viewing relevant sections
|
||||||
|
if (['server-settings', 'welcome-module', 'modules-settings', 'permissions-settings'].includes(sectionId)) {
|
||||||
|
// loadGuildSettings already prevents redundant loads using window.currentSettingsGuildId
|
||||||
|
console.log(`Loading general guild settings for guild ${window.selectedGuildId}`);
|
||||||
|
loadGuildSettings(window.selectedGuildId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Section with ID ${sectionId}-section not found.`);
|
||||||
|
// Optionally show a default section or an error message
|
||||||
|
// If no section found and a guild is selected, maybe default to server-settings?
|
||||||
|
if (window.selectedGuildId) {
|
||||||
|
console.log("Defaulting to server-settings section.");
|
||||||
|
showSection('server-settings');
|
||||||
|
} else {
|
||||||
|
// If no guild selected either, go back to server selection
|
||||||
|
showServerSelection();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,11 +423,118 @@ function initDropdowns() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load dashboard data
|
* Show the server selection screen
|
||||||
|
*/
|
||||||
|
function showServerSelection() {
|
||||||
|
console.log('Showing server selection screen...');
|
||||||
|
const dashboardContainer = document.getElementById('dashboard-container');
|
||||||
|
const serverSelectSection = document.getElementById('server-select-section'); // Assuming this ID exists in index.html
|
||||||
|
|
||||||
|
if (!serverSelectSection) {
|
||||||
|
console.error('Server selection section not found!');
|
||||||
|
// Maybe show an error to the user or default to the old behavior
|
||||||
|
loadDashboardData(); // Fallback?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide main dashboard content and show server selection
|
||||||
|
if (dashboardContainer) dashboardContainer.style.display = 'none';
|
||||||
|
serverSelectSection.style.display = 'block';
|
||||||
|
|
||||||
|
// Hide all other specific dashboard sections just in case
|
||||||
|
document.querySelectorAll('.dashboard-section').forEach(section => {
|
||||||
|
section.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load the list of guilds for the user
|
||||||
|
loadUserGuilds();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load guilds the user has admin access to for the selection screen
|
||||||
|
*/
|
||||||
|
function loadUserGuilds() {
|
||||||
|
const serverListContainer = document.getElementById('server-list-container'); // Assuming this ID exists within server-select-section
|
||||||
|
if (!serverListContainer) {
|
||||||
|
console.error('Server list container not found!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
serverListContainer.innerHTML = '<div class="loading-spinner"></div><p>Loading your servers...</p>'; // Show loading state
|
||||||
|
|
||||||
|
API.get('/dashboard/api/user-guilds')
|
||||||
|
.then(guilds => {
|
||||||
|
serverListContainer.innerHTML = ''; // Clear loading state
|
||||||
|
|
||||||
|
if (!guilds || guilds.length === 0) {
|
||||||
|
serverListContainer.innerHTML = '<p>No servers found where you have admin permissions.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
guilds.forEach(guild => {
|
||||||
|
const guildElement = document.createElement('div');
|
||||||
|
guildElement.className = 'server-select-item card'; // Add card class for styling
|
||||||
|
guildElement.style.cursor = 'pointer';
|
||||||
|
guildElement.dataset.guildId = guild.id;
|
||||||
|
|
||||||
|
const iconElement = document.createElement('img');
|
||||||
|
iconElement.className = 'server-icon';
|
||||||
|
iconElement.src = guild.icon_url || 'img/default-icon.png'; // Provide a default icon path
|
||||||
|
iconElement.alt = `${guild.name} icon`;
|
||||||
|
iconElement.width = 50;
|
||||||
|
iconElement.height = 50;
|
||||||
|
|
||||||
|
const nameElement = document.createElement('span');
|
||||||
|
nameElement.className = 'server-name';
|
||||||
|
nameElement.textContent = guild.name;
|
||||||
|
|
||||||
|
guildElement.appendChild(iconElement);
|
||||||
|
guildElement.appendChild(nameElement);
|
||||||
|
|
||||||
|
guildElement.addEventListener('click', () => {
|
||||||
|
console.log(`Server selected: ${guild.name} (${guild.id})`);
|
||||||
|
// Store selected guild ID
|
||||||
|
localStorage.setItem('selectedGuildId', guild.id);
|
||||||
|
window.selectedGuildId = guild.id;
|
||||||
|
window.currentSettingsGuildId = null; // Reset loaded settings tracker
|
||||||
|
|
||||||
|
// Hide server selection and show dashboard
|
||||||
|
const serverSelectSection = document.getElementById('server-select-section');
|
||||||
|
const dashboardContainer = document.getElementById('dashboard-container');
|
||||||
|
if (serverSelectSection) serverSelectSection.style.display = 'none';
|
||||||
|
if (dashboardContainer) dashboardContainer.style.display = 'block';
|
||||||
|
|
||||||
|
// Load data for the selected guild and show the default section
|
||||||
|
loadDashboardData(); // Now loads data for the selected guild
|
||||||
|
showSection('server-settings'); // Show the server settings section by default
|
||||||
|
});
|
||||||
|
|
||||||
|
serverListContainer.appendChild(guildElement);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading user guilds:', error);
|
||||||
|
serverListContainer.innerHTML = '<p class="text-danger">Error loading servers. Please try again.</p>';
|
||||||
|
Toast.error('Failed to load your servers.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load initial dashboard data *after* a server has been selected
|
||||||
*/
|
*/
|
||||||
function loadDashboardData() {
|
function loadDashboardData() {
|
||||||
// Load guilds for server select
|
if (!window.selectedGuildId) {
|
||||||
loadGuilds();
|
console.warn('loadDashboardData called without a selected guild ID.');
|
||||||
|
showServerSelection(); // Redirect back to selection if no guild is selected
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Loading dashboard data for guild: ${window.selectedGuildId}`);
|
||||||
|
// No longer need to load the general guild list here.
|
||||||
|
// Specific sections will load their data via showSection or dedicated functions.
|
||||||
|
// We might load some initial settings common to multiple sections here if needed.
|
||||||
|
// For now, let's ensure the basic settings are loaded if the user lands on a relevant page.
|
||||||
|
loadGuildSettings(window.selectedGuildId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -401,9 +571,11 @@ function showBotTokenMissingError() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load guilds for server select dropdown
|
* Load guilds for the *original* server select dropdown (now potentially redundant)
|
||||||
|
* Kept for reference or potential future use, but not called by default flow anymore.
|
||||||
*/
|
*/
|
||||||
function loadGuilds() {
|
function loadGuilds() {
|
||||||
|
console.warn("loadGuilds function called - this might be redundant now.");
|
||||||
const guildSelect = document.getElementById('guild-select');
|
const guildSelect = document.getElementById('guild-select');
|
||||||
if (!guildSelect) return;
|
if (!guildSelect) return;
|
||||||
|
|
||||||
@ -451,10 +623,26 @@ function loadGuilds() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load settings for a specific guild
|
* Load settings for the currently selected guild (window.selectedGuildId)
|
||||||
* @param {string} guildId - The guild ID
|
* @param {string} guildId - The guild ID to load settings for
|
||||||
*/
|
*/
|
||||||
function loadGuildSettings(guildId) {
|
function loadGuildSettings(guildId) {
|
||||||
|
// Prevent reloading if settings for this guild are already loaded
|
||||||
|
if (window.currentSettingsGuildId === guildId) {
|
||||||
|
console.log(`Settings for guild ${guildId} already loaded.`);
|
||||||
|
// Ensure the forms are visible if navigating back
|
||||||
|
const settingsForm = document.getElementById('settings-form');
|
||||||
|
const welcomeSettingsForm = document.getElementById('welcome-settings-form');
|
||||||
|
const modulesSettingsForm = document.getElementById('modules-settings-form');
|
||||||
|
const permissionsSettingsForm = document.getElementById('permissions-settings-form');
|
||||||
|
if (settingsForm) settingsForm.style.display = 'block';
|
||||||
|
if (welcomeSettingsForm) welcomeSettingsForm.style.display = 'block';
|
||||||
|
if (modulesSettingsForm) modulesSettingsForm.style.display = 'block';
|
||||||
|
if (permissionsSettingsForm) permissionsSettingsForm.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Loading settings for guild: ${guildId}`);
|
||||||
|
|
||||||
const settingsForm = document.getElementById('settings-form');
|
const settingsForm = document.getElementById('settings-form');
|
||||||
const welcomeSettingsForm = document.getElementById('welcome-settings-form');
|
const welcomeSettingsForm = document.getElementById('welcome-settings-form');
|
||||||
const modulesSettingsForm = document.getElementById('modules-settings-form');
|
const modulesSettingsForm = document.getElementById('modules-settings-form');
|
||||||
@ -498,6 +686,9 @@ function loadGuildSettings(guildId) {
|
|||||||
loadGuildRoles(guildId);
|
loadGuildRoles(guildId);
|
||||||
loadGuildCommands(guildId);
|
loadGuildCommands(guildId);
|
||||||
|
|
||||||
|
// Mark settings as loaded for this guild
|
||||||
|
window.currentSettingsGuildId = guildId;
|
||||||
|
|
||||||
// Set up event listeners for buttons
|
// Set up event listeners for buttons
|
||||||
setupSaveSettingsButtons(guildId);
|
setupSaveSettingsButtons(guildId);
|
||||||
setupWelcomeLeaveTestButtons(guildId);
|
setupWelcomeLeaveTestButtons(guildId);
|
||||||
@ -1370,5 +1561,3 @@ function setupWelcomeLeaveTestButtons(guildId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
395
cogs/mod_log_cog.py
Normal file
395
cogs/mod_log_cog.py
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord import app_commands, Interaction, Embed, Color, User, Member, Object
|
||||||
|
import asyncpg
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Union, Dict, Any
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
# Assuming db functions are in discordbot.db.mod_log_db
|
||||||
|
from ..db import mod_log_db
|
||||||
|
# Assuming settings manager is available
|
||||||
|
from ..settings_manager import SettingsManager # Adjust import path if necessary
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ModLogCog(commands.Cog):
|
||||||
|
"""Cog for handling integrated moderation logging and related commands."""
|
||||||
|
|
||||||
|
def __init__(self, bot: commands.Bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.settings_manager: SettingsManager = bot.settings_manager # Assuming settings_manager is attached to bot
|
||||||
|
self.pool: asyncpg.Pool = bot.pool # Assuming pool is attached to bot
|
||||||
|
|
||||||
|
# Create the main command group for this cog
|
||||||
|
self.modlog_group = app_commands.Group(
|
||||||
|
name="modlog",
|
||||||
|
description="Commands for viewing and managing moderation logs"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register commands within the group
|
||||||
|
self.register_commands()
|
||||||
|
|
||||||
|
# Add command group to the bot's tree
|
||||||
|
self.bot.tree.add_command(self.modlog_group)
|
||||||
|
|
||||||
|
def register_commands(self):
|
||||||
|
"""Register all commands for this cog"""
|
||||||
|
|
||||||
|
# --- View Command ---
|
||||||
|
view_command = app_commands.Command(
|
||||||
|
name="view",
|
||||||
|
description="View moderation logs for a user or the server",
|
||||||
|
callback=self.modlog_view_callback,
|
||||||
|
parent=self.modlog_group
|
||||||
|
)
|
||||||
|
app_commands.describe(
|
||||||
|
user="Optional: The user whose logs you want to view"
|
||||||
|
)(view_command)
|
||||||
|
self.modlog_group.add_command(view_command)
|
||||||
|
|
||||||
|
# --- Case Command ---
|
||||||
|
case_command = app_commands.Command(
|
||||||
|
name="case",
|
||||||
|
description="View details for a specific moderation case ID",
|
||||||
|
callback=self.modlog_case_callback,
|
||||||
|
parent=self.modlog_group
|
||||||
|
)
|
||||||
|
app_commands.describe(
|
||||||
|
case_id="The ID of the moderation case to view"
|
||||||
|
)(case_command)
|
||||||
|
self.modlog_group.add_command(case_command)
|
||||||
|
|
||||||
|
# --- Reason Command ---
|
||||||
|
reason_command = app_commands.Command(
|
||||||
|
name="reason",
|
||||||
|
description="Update the reason for a specific moderation case ID",
|
||||||
|
callback=self.modlog_reason_callback,
|
||||||
|
parent=self.modlog_group
|
||||||
|
)
|
||||||
|
app_commands.describe(
|
||||||
|
case_id="The ID of the moderation case to update",
|
||||||
|
new_reason="The new reason for the moderation action"
|
||||||
|
)(reason_command)
|
||||||
|
self.modlog_group.add_command(reason_command)
|
||||||
|
|
||||||
|
# --- Core Logging Function ---
|
||||||
|
|
||||||
|
async def log_action(
|
||||||
|
self,
|
||||||
|
guild: discord.Guild,
|
||||||
|
moderator: Union[User, Member], # For bot actions
|
||||||
|
target: Union[User, Member, Object], # Can be user, member, or just an ID object
|
||||||
|
action_type: str,
|
||||||
|
reason: Optional[str],
|
||||||
|
duration: Optional[datetime.timedelta] = None,
|
||||||
|
source: str = "BOT", # Default source is the bot itself
|
||||||
|
ai_details: Optional[Dict[str, Any]] = None, # Details from AI API
|
||||||
|
moderator_id_override: Optional[int] = None # Allow overriding moderator ID for AI source
|
||||||
|
):
|
||||||
|
"""Logs a moderation action to the database and configured channel."""
|
||||||
|
if not guild:
|
||||||
|
log.warning("Attempted to log action without guild context.")
|
||||||
|
return
|
||||||
|
|
||||||
|
guild_id = guild.id
|
||||||
|
# Use override if provided (for AI source), otherwise use moderator object ID
|
||||||
|
moderator_id = moderator_id_override if moderator_id_override is not None else moderator.id
|
||||||
|
target_user_id = target.id
|
||||||
|
duration_seconds = int(duration.total_seconds()) if duration else None
|
||||||
|
|
||||||
|
# 1. Add initial log entry to DB
|
||||||
|
case_id = await mod_log_db.add_mod_log(
|
||||||
|
self.pool, guild_id, moderator_id, target_user_id,
|
||||||
|
action_type, reason, duration_seconds
|
||||||
|
)
|
||||||
|
|
||||||
|
if not case_id:
|
||||||
|
log.error(f"Failed to get case_id when logging action {action_type} in guild {guild_id}")
|
||||||
|
return # Don't proceed if we couldn't save the initial log
|
||||||
|
|
||||||
|
# 2. Check settings and send log message
|
||||||
|
try:
|
||||||
|
settings = await self.settings_manager.get_guild_settings(guild_id)
|
||||||
|
log_enabled = settings.get('mod_log_enabled', False)
|
||||||
|
log_channel_id = settings.get('mod_log_channel_id')
|
||||||
|
|
||||||
|
if not log_enabled or not log_channel_id:
|
||||||
|
log.debug(f"Mod logging disabled or channel not set for guild {guild_id}. Skipping Discord log message.")
|
||||||
|
return
|
||||||
|
|
||||||
|
log_channel = guild.get_channel(log_channel_id)
|
||||||
|
if not log_channel or not isinstance(log_channel, discord.TextChannel):
|
||||||
|
log.warning(f"Mod log channel {log_channel_id} not found or not a text channel in guild {guild_id}.")
|
||||||
|
# Optionally update DB to remove channel ID? Or just leave it.
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. Format and send embed
|
||||||
|
embed = self._format_log_embed(
|
||||||
|
case_id=case_id,
|
||||||
|
moderator=moderator, # Pass the object for display formatting
|
||||||
|
target=target,
|
||||||
|
action_type=action_type,
|
||||||
|
reason=reason,
|
||||||
|
duration=duration,
|
||||||
|
guild=guild,
|
||||||
|
source=source,
|
||||||
|
ai_details=ai_details,
|
||||||
|
moderator_id_override=moderator_id_override # Pass override for formatting
|
||||||
|
)
|
||||||
|
log_message = await log_channel.send(embed=embed)
|
||||||
|
|
||||||
|
# 4. Update DB with message details
|
||||||
|
await mod_log_db.update_mod_log_message_details(self.pool, case_id, log_message.id, log_channel.id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error during Discord mod log message sending/updating for case {case_id} in guild {guild_id}: {e}")
|
||||||
|
|
||||||
|
def _format_log_embed(
|
||||||
|
self,
|
||||||
|
case_id: int,
|
||||||
|
moderator: Union[User, Member],
|
||||||
|
target: Union[User, Member, Object],
|
||||||
|
action_type: str,
|
||||||
|
reason: Optional[str],
|
||||||
|
duration: Optional[datetime.timedelta],
|
||||||
|
guild: discord.Guild,
|
||||||
|
source: str = "BOT",
|
||||||
|
ai_details: Optional[Dict[str, Any]] = None,
|
||||||
|
moderator_id_override: Optional[int] = None
|
||||||
|
) -> Embed:
|
||||||
|
"""Helper function to create the standard log embed."""
|
||||||
|
color_map = {
|
||||||
|
"BAN": Color.red(),
|
||||||
|
"UNBAN": Color.green(),
|
||||||
|
"KICK": Color.orange(),
|
||||||
|
"TIMEOUT": Color.gold(),
|
||||||
|
"REMOVE_TIMEOUT": Color.blue(),
|
||||||
|
"WARN": Color.yellow(),
|
||||||
|
"AI_ALERT": Color.purple(),
|
||||||
|
"AI_DELETE_REQUESTED": Color.dark_grey(),
|
||||||
|
}
|
||||||
|
embed_color = color_map.get(action_type.upper(), Color.greyple())
|
||||||
|
action_title_prefix = "AI Moderation Action" if source == "AI_API" else action_type.replace("_", " ").title()
|
||||||
|
action_title = f"{action_title_prefix} | Case #{case_id}"
|
||||||
|
|
||||||
|
embed = Embed(
|
||||||
|
title=action_title,
|
||||||
|
color=embed_color,
|
||||||
|
timestamp=discord.utils.utcnow()
|
||||||
|
)
|
||||||
|
|
||||||
|
target_display = f"{getattr(target, 'mention', target.id)} ({target.id})"
|
||||||
|
|
||||||
|
# Determine moderator display based on source
|
||||||
|
if source == "AI_API":
|
||||||
|
moderator_display = f"AI System (ID: {moderator_id_override or 'Unknown'})"
|
||||||
|
else:
|
||||||
|
moderator_display = f"{moderator.mention} ({moderator.id})"
|
||||||
|
|
||||||
|
|
||||||
|
embed.add_field(name="User", value=target_display, inline=True)
|
||||||
|
embed.add_field(name="Moderator", value=moderator_display, inline=True)
|
||||||
|
|
||||||
|
# Add AI-specific details if available
|
||||||
|
if ai_details:
|
||||||
|
if 'rule_violated' in ai_details:
|
||||||
|
embed.add_field(name="Rule Violated", value=ai_details['rule_violated'], inline=True)
|
||||||
|
if 'reasoning' in ai_details:
|
||||||
|
# Use AI reasoning as the main reason field if bot reason is empty
|
||||||
|
reason_to_display = reason or ai_details['reasoning']
|
||||||
|
embed.add_field(name="Reason / AI Reasoning", value=reason_to_display or "No reason provided.", inline=False)
|
||||||
|
# Optionally add bot reason separately if both exist and differ
|
||||||
|
if reason and reason != ai_details['reasoning']:
|
||||||
|
embed.add_field(name="Original Bot Reason", value=reason, inline=False)
|
||||||
|
else:
|
||||||
|
embed.add_field(name="Reason", value=reason or "No reason provided.", inline=False)
|
||||||
|
else:
|
||||||
|
embed.add_field(name="Reason", value=reason or "No reason provided.", inline=False)
|
||||||
|
|
||||||
|
if duration:
|
||||||
|
# Format duration nicely (e.g., "1 day", "2 hours 30 minutes")
|
||||||
|
# This is a simple version, could be made more robust
|
||||||
|
total_seconds = int(duration.total_seconds())
|
||||||
|
days, remainder = divmod(total_seconds, 86400)
|
||||||
|
hours, remainder = divmod(remainder, 3600)
|
||||||
|
minutes, seconds = divmod(remainder, 60)
|
||||||
|
duration_str = ""
|
||||||
|
if days > 0: duration_str += f"{days}d "
|
||||||
|
if hours > 0: duration_str += f"{hours}h "
|
||||||
|
if minutes > 0: duration_str += f"{minutes}m "
|
||||||
|
if seconds > 0 or not duration_str: duration_str += f"{seconds}s"
|
||||||
|
duration_str = duration_str.strip()
|
||||||
|
|
||||||
|
embed.add_field(name="Duration", value=duration_str, inline=True)
|
||||||
|
# Add expiration timestamp if applicable (e.g., for timeouts)
|
||||||
|
if action_type.upper() == "TIMEOUT":
|
||||||
|
expires_at = discord.utils.utcnow() + duration
|
||||||
|
embed.add_field(name="Expires", value=f"<t:{int(expires_at.timestamp())}:R>", inline=True)
|
||||||
|
|
||||||
|
|
||||||
|
embed.set_footer(text=f"Guild: {guild.name} ({guild.id})")
|
||||||
|
|
||||||
|
return embed
|
||||||
|
|
||||||
|
# --- Command Callbacks ---
|
||||||
|
|
||||||
|
@app_commands.checks.has_permissions(moderate_members=True) # Adjust permissions as needed
|
||||||
|
async def modlog_view_callback(self, interaction: Interaction, user: Optional[discord.User] = None):
|
||||||
|
"""Callback for the /modlog view command."""
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
guild_id = interaction.guild_id
|
||||||
|
|
||||||
|
if not guild_id:
|
||||||
|
await interaction.followup.send("❌ This command can only be used in a server.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
records = []
|
||||||
|
if user:
|
||||||
|
records = await mod_log_db.get_user_mod_logs(self.pool, guild_id, user.id)
|
||||||
|
title = f"Moderation Logs for {user.name} ({user.id})"
|
||||||
|
else:
|
||||||
|
records = await mod_log_db.get_guild_mod_logs(self.pool, guild_id)
|
||||||
|
title = f"Recent Moderation Logs for {interaction.guild.name}"
|
||||||
|
|
||||||
|
if not records:
|
||||||
|
await interaction.followup.send("No moderation logs found matching your criteria.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Format the logs into an embed or text response
|
||||||
|
# For simplicity, sending as text for now. Can enhance with pagination/embeds later.
|
||||||
|
response_lines = [f"**{title}**"]
|
||||||
|
for record in records:
|
||||||
|
timestamp_str = record['timestamp'].strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
reason_str = record['reason'] or "N/A"
|
||||||
|
duration_str = f" ({record['duration_seconds']}s)" if record['duration_seconds'] else ""
|
||||||
|
response_lines.append(
|
||||||
|
f"`Case #{record['case_id']}` [{timestamp_str}] **{record['action_type']}** "
|
||||||
|
f"Target: <@{record['target_user_id']}> Mod: <@{record['moderator_id']}> "
|
||||||
|
f"Reason: {reason_str}{duration_str}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle potential message length limits
|
||||||
|
full_response = "\n".join(response_lines)
|
||||||
|
if len(full_response) > 2000:
|
||||||
|
full_response = full_response[:1990] + "\n... (truncated)"
|
||||||
|
|
||||||
|
await interaction.followup.send(full_response, ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
@app_commands.checks.has_permissions(moderate_members=True) # Adjust permissions as needed
|
||||||
|
async def modlog_case_callback(self, interaction: Interaction, case_id: int):
|
||||||
|
"""Callback for the /modlog case command."""
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
record = await mod_log_db.get_mod_log(self.pool, case_id)
|
||||||
|
|
||||||
|
if not record:
|
||||||
|
await interaction.followup.send(f"❌ Case ID #{case_id} not found.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ensure the case belongs to the current guild for security/privacy
|
||||||
|
if record['guild_id'] != interaction.guild_id:
|
||||||
|
await interaction.followup.send(f"❌ Case ID #{case_id} does not belong to this server.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fetch user objects if possible to show names
|
||||||
|
moderator = await self.bot.fetch_user(record['moderator_id'])
|
||||||
|
target = await self.bot.fetch_user(record['target_user_id'])
|
||||||
|
duration = datetime.timedelta(seconds=record['duration_seconds']) if record['duration_seconds'] else None
|
||||||
|
|
||||||
|
embed = self._format_log_embed(
|
||||||
|
case_id,
|
||||||
|
moderator or Object(id=record['moderator_id']), # Fallback to Object if user not found
|
||||||
|
target or Object(id=record['target_user_id']), # Fallback to Object if user not found
|
||||||
|
record['action_type'],
|
||||||
|
record['reason'],
|
||||||
|
duration,
|
||||||
|
interaction.guild
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add log message link if available
|
||||||
|
if record['log_message_id'] and record['log_channel_id']:
|
||||||
|
link = f"https://discord.com/channels/{record['guild_id']}/{record['log_channel_id']}/{record['log_message_id']}"
|
||||||
|
embed.add_field(name="Log Message", value=f"[Jump to Log]({link})", inline=False)
|
||||||
|
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
@app_commands.checks.has_permissions(manage_guild=True) # Higher permission for editing reasons
|
||||||
|
async def modlog_reason_callback(self, interaction: Interaction, case_id: int, new_reason: str):
|
||||||
|
"""Callback for the /modlog reason command."""
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
|
||||||
|
# 1. Get the original record to verify guild and existence
|
||||||
|
original_record = await mod_log_db.get_mod_log(self.pool, case_id)
|
||||||
|
if not original_record:
|
||||||
|
await interaction.followup.send(f"❌ Case ID #{case_id} not found.", ephemeral=True)
|
||||||
|
return
|
||||||
|
if original_record['guild_id'] != interaction.guild_id:
|
||||||
|
await interaction.followup.send(f"❌ Case ID #{case_id} does not belong to this server.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. Update the reason in the database
|
||||||
|
success = await mod_log_db.update_mod_log_reason(self.pool, case_id, new_reason)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
await interaction.followup.send(f"❌ Failed to update reason for Case ID #{case_id}. Please check logs.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
await interaction.followup.send(f"✅ Updated reason for Case ID #{case_id}.", ephemeral=True)
|
||||||
|
|
||||||
|
# 3. (Optional but recommended) Update the original log message embed
|
||||||
|
if original_record['log_message_id'] and original_record['log_channel_id']:
|
||||||
|
try:
|
||||||
|
log_channel = interaction.guild.get_channel(original_record['log_channel_id'])
|
||||||
|
if log_channel and isinstance(log_channel, discord.TextChannel):
|
||||||
|
log_message = await log_channel.fetch_message(original_record['log_message_id'])
|
||||||
|
if log_message and log_message.author == self.bot.user and log_message.embeds:
|
||||||
|
# Re-fetch users/duration to reconstruct embed accurately
|
||||||
|
moderator = await self.bot.fetch_user(original_record['moderator_id'])
|
||||||
|
target = await self.bot.fetch_user(original_record['target_user_id'])
|
||||||
|
duration = datetime.timedelta(seconds=original_record['duration_seconds']) if original_record['duration_seconds'] else None
|
||||||
|
|
||||||
|
new_embed = self._format_log_embed(
|
||||||
|
case_id,
|
||||||
|
moderator or Object(id=original_record['moderator_id']),
|
||||||
|
target or Object(id=original_record['target_user_id']),
|
||||||
|
original_record['action_type'],
|
||||||
|
new_reason, # Use the new reason here
|
||||||
|
duration,
|
||||||
|
interaction.guild
|
||||||
|
)
|
||||||
|
# Add log message link again
|
||||||
|
link = f"https://discord.com/channels/{original_record['guild_id']}/{original_record['log_channel_id']}/{original_record['log_message_id']}"
|
||||||
|
new_embed.add_field(name="Log Message", value=f"[Jump to Log]({link})", inline=False)
|
||||||
|
new_embed.add_field(name="Updated Reason By", value=f"{interaction.user.mention}", inline=False) # Indicate update
|
||||||
|
|
||||||
|
await log_message.edit(embed=new_embed)
|
||||||
|
log.info(f"Successfully updated log message embed for case {case_id}")
|
||||||
|
except discord.NotFound:
|
||||||
|
log.warning(f"Original log message or channel not found for case {case_id} when updating reason.")
|
||||||
|
except discord.Forbidden:
|
||||||
|
log.warning(f"Missing permissions to edit original log message for case {case_id}.")
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error updating original log message embed for case {case_id}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_ready(self):
|
||||||
|
# Ensure the pool and settings_manager are available
|
||||||
|
if not hasattr(self.bot, 'pool') or not self.bot.pool:
|
||||||
|
log.error("Database pool not found on bot object. ModLogCog requires bot.pool.")
|
||||||
|
# Consider preventing the cog from loading fully or raising an error
|
||||||
|
if not hasattr(self.bot, 'settings_manager') or not self.bot.settings_manager:
|
||||||
|
log.error("SettingsManager not found on bot object. ModLogCog requires bot.settings_manager.")
|
||||||
|
# Consider preventing the cog from loading fully or raising an error
|
||||||
|
|
||||||
|
print(f'{self.__class__.__name__} cog has been loaded.')
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot: commands.Bot):
|
||||||
|
# Ensure dependencies (pool, settings_manager) are ready before adding cog
|
||||||
|
if hasattr(bot, 'pool') and hasattr(bot, 'settings_manager'):
|
||||||
|
await bot.add_cog(ModLogCog(bot))
|
||||||
|
else:
|
||||||
|
log.error("Failed to load ModLogCog: bot.pool or bot.settings_manager not initialized.")
|
@ -1,10 +1,13 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord import app_commands
|
from discord import app_commands, Object
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional, Union, List
|
from typing import Optional, Union, List
|
||||||
|
|
||||||
|
# Import the new ModLogCog
|
||||||
|
from .mod_log_cog import ModLogCog
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -223,6 +226,20 @@ class ModerationCog(commands.Cog):
|
|||||||
# Log the action
|
# Log the action
|
||||||
logger.info(f"User {member} (ID: {member.id}) was banned from {interaction.guild.name} (ID: {interaction.guild.id}) by {interaction.user} (ID: {interaction.user.id}). Reason: {reason}")
|
logger.info(f"User {member} (ID: {member.id}) was banned from {interaction.guild.name} (ID: {interaction.guild.id}) by {interaction.user} (ID: {interaction.user.id}). Reason: {reason}")
|
||||||
|
|
||||||
|
# --- Add to Mod Log DB ---
|
||||||
|
mod_log_cog: ModLogCog = self.bot.get_cog('ModLogCog')
|
||||||
|
if mod_log_cog:
|
||||||
|
await mod_log_cog.log_action(
|
||||||
|
guild=interaction.guild,
|
||||||
|
moderator=interaction.user,
|
||||||
|
target=member,
|
||||||
|
action_type="BAN",
|
||||||
|
reason=reason,
|
||||||
|
# Ban duration isn't directly supported here, pass None
|
||||||
|
duration=None
|
||||||
|
)
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
# Send confirmation message with DM status
|
# Send confirmation message with DM status
|
||||||
dm_status = "✅ DM notification sent" if dm_sent else "❌ Could not send DM notification (user may have DMs disabled)"
|
dm_status = "✅ DM notification sent" if dm_sent else "❌ Could not send DM notification (user may have DMs disabled)"
|
||||||
await interaction.response.send_message(f"🔨 **Banned {member.mention}**! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
await interaction.response.send_message(f"🔨 **Banned {member.mention}**! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
||||||
@ -271,6 +288,19 @@ class ModerationCog(commands.Cog):
|
|||||||
# Log the action
|
# Log the action
|
||||||
logger.info(f"User {banned_user} (ID: {banned_user.id}) was unbanned from {interaction.guild.name} (ID: {interaction.guild.id}) by {interaction.user} (ID: {interaction.user.id}). Reason: {reason}")
|
logger.info(f"User {banned_user} (ID: {banned_user.id}) was unbanned from {interaction.guild.name} (ID: {interaction.guild.id}) by {interaction.user} (ID: {interaction.user.id}). Reason: {reason}")
|
||||||
|
|
||||||
|
# --- Add to Mod Log DB ---
|
||||||
|
mod_log_cog: ModLogCog = self.bot.get_cog('ModLogCog')
|
||||||
|
if mod_log_cog:
|
||||||
|
await mod_log_cog.log_action(
|
||||||
|
guild=interaction.guild,
|
||||||
|
moderator=interaction.user,
|
||||||
|
target=banned_user, # Use the fetched user object
|
||||||
|
action_type="UNBAN",
|
||||||
|
reason=reason,
|
||||||
|
duration=None
|
||||||
|
)
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
# Send confirmation message
|
# Send confirmation message
|
||||||
await interaction.response.send_message(f"🔓 **Unbanned {banned_user}**! Reason: {reason or 'No reason provided'}")
|
await interaction.response.send_message(f"🔓 **Unbanned {banned_user}**! Reason: {reason or 'No reason provided'}")
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
@ -337,6 +367,19 @@ class ModerationCog(commands.Cog):
|
|||||||
# Log the action
|
# Log the action
|
||||||
logger.info(f"User {member} (ID: {member.id}) was kicked from {interaction.guild.name} (ID: {interaction.guild.id}) by {interaction.user} (ID: {interaction.user.id}). Reason: {reason}")
|
logger.info(f"User {member} (ID: {member.id}) was kicked from {interaction.guild.name} (ID: {interaction.guild.id}) by {interaction.user} (ID: {interaction.user.id}). Reason: {reason}")
|
||||||
|
|
||||||
|
# --- Add to Mod Log DB ---
|
||||||
|
mod_log_cog: ModLogCog = self.bot.get_cog('ModLogCog')
|
||||||
|
if mod_log_cog:
|
||||||
|
await mod_log_cog.log_action(
|
||||||
|
guild=interaction.guild,
|
||||||
|
moderator=interaction.user,
|
||||||
|
target=member,
|
||||||
|
action_type="KICK",
|
||||||
|
reason=reason,
|
||||||
|
duration=None
|
||||||
|
)
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
# Send confirmation message with DM status
|
# Send confirmation message with DM status
|
||||||
dm_status = "✅ DM notification sent" if dm_sent else "❌ Could not send DM notification (user may have DMs disabled)"
|
dm_status = "✅ DM notification sent" if dm_sent else "❌ Could not send DM notification (user may have DMs disabled)"
|
||||||
await interaction.response.send_message(f"👢 **Kicked {member.mention}**! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
await interaction.response.send_message(f"👢 **Kicked {member.mention}**! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
||||||
@ -421,6 +464,19 @@ class ModerationCog(commands.Cog):
|
|||||||
# Log the action
|
# Log the action
|
||||||
logger.info(f"User {member} (ID: {member.id}) was timed out in {interaction.guild.name} (ID: {interaction.guild.id}) by {interaction.user} (ID: {interaction.user.id}) for {duration}. Reason: {reason}")
|
logger.info(f"User {member} (ID: {member.id}) was timed out in {interaction.guild.name} (ID: {interaction.guild.id}) by {interaction.user} (ID: {interaction.user.id}) for {duration}. Reason: {reason}")
|
||||||
|
|
||||||
|
# --- Add to Mod Log DB ---
|
||||||
|
mod_log_cog: ModLogCog = self.bot.get_cog('ModLogCog')
|
||||||
|
if mod_log_cog:
|
||||||
|
await mod_log_cog.log_action(
|
||||||
|
guild=interaction.guild,
|
||||||
|
moderator=interaction.user,
|
||||||
|
target=member,
|
||||||
|
action_type="TIMEOUT",
|
||||||
|
reason=reason,
|
||||||
|
duration=delta # Pass the timedelta object
|
||||||
|
)
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
# Send confirmation message with DM status
|
# Send confirmation message with DM status
|
||||||
dm_status = "✅ DM notification sent" if dm_sent else "❌ Could not send DM notification (user may have DMs disabled)"
|
dm_status = "✅ DM notification sent" if dm_sent else "❌ Could not send DM notification (user may have DMs disabled)"
|
||||||
await interaction.response.send_message(f"⏰ **Timed out {member.mention}** for {duration}! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
await interaction.response.send_message(f"⏰ **Timed out {member.mention}** for {duration}! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
||||||
@ -473,6 +529,19 @@ class ModerationCog(commands.Cog):
|
|||||||
# Log the action
|
# Log the action
|
||||||
logger.info(f"Timeout was removed from user {member} (ID: {member.id}) in {interaction.guild.name} (ID: {interaction.guild.id}) by {interaction.user} (ID: {interaction.user.id}). Reason: {reason}")
|
logger.info(f"Timeout was removed from user {member} (ID: {member.id}) in {interaction.guild.name} (ID: {interaction.guild.id}) by {interaction.user} (ID: {interaction.user.id}). Reason: {reason}")
|
||||||
|
|
||||||
|
# --- Add to Mod Log DB ---
|
||||||
|
mod_log_cog: ModLogCog = self.bot.get_cog('ModLogCog')
|
||||||
|
if mod_log_cog:
|
||||||
|
await mod_log_cog.log_action(
|
||||||
|
guild=interaction.guild,
|
||||||
|
moderator=interaction.user,
|
||||||
|
target=member,
|
||||||
|
action_type="REMOVE_TIMEOUT",
|
||||||
|
reason=reason,
|
||||||
|
duration=None
|
||||||
|
)
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
# Send confirmation message with DM status
|
# Send confirmation message with DM status
|
||||||
dm_status = "✅ DM notification sent" if dm_sent else "❌ Could not send DM notification (user may have DMs disabled)"
|
dm_status = "✅ DM notification sent" if dm_sent else "❌ Could not send DM notification (user may have DMs disabled)"
|
||||||
await interaction.response.send_message(f"⏰ **Removed timeout from {member.mention}**! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
await interaction.response.send_message(f"⏰ **Removed timeout from {member.mention}**! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
||||||
@ -551,9 +620,22 @@ class ModerationCog(commands.Cog):
|
|||||||
await interaction.response.send_message("❌ You cannot warn someone with a higher or equal role.", ephemeral=True)
|
await interaction.response.send_message("❌ You cannot warn someone with a higher or equal role.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Log the warning
|
# Log the warning (using standard logger first)
|
||||||
logger.info(f"User {member} (ID: {member.id}) was warned in {interaction.guild.name} (ID: {interaction.guild.id}) by {interaction.user} (ID: {interaction.user.id}). Reason: {reason}")
|
logger.info(f"User {member} (ID: {member.id}) was warned in {interaction.guild.name} (ID: {interaction.guild.id}) by {interaction.user} (ID: {interaction.user.id}). Reason: {reason}")
|
||||||
|
|
||||||
|
# --- Add to Mod Log DB ---
|
||||||
|
mod_log_cog: ModLogCog = self.bot.get_cog('ModLogCog')
|
||||||
|
if mod_log_cog:
|
||||||
|
await mod_log_cog.log_action(
|
||||||
|
guild=interaction.guild,
|
||||||
|
moderator=interaction.user,
|
||||||
|
target=member,
|
||||||
|
action_type="WARN",
|
||||||
|
reason=reason,
|
||||||
|
duration=None
|
||||||
|
)
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
# Send warning message in the channel
|
# Send warning message in the channel
|
||||||
await interaction.response.send_message(f"⚠️ **{member.mention} has been warned**! Reason: {reason}")
|
await interaction.response.send_message(f"⚠️ **{member.mention} has been warned**! Reason: {reason}")
|
||||||
|
|
||||||
@ -734,6 +816,19 @@ class ModerationCog(commands.Cog):
|
|||||||
# Log the action
|
# Log the action
|
||||||
logger.info(f"User {member} (ID: {member.id}) was timed out in {ctx.guild.name} (ID: {ctx.guild.id}) by {ctx.author} (ID: {ctx.author.id}) for {duration}. Reason: {reason}")
|
logger.info(f"User {member} (ID: {member.id}) was timed out in {ctx.guild.name} (ID: {ctx.guild.id}) by {ctx.author} (ID: {ctx.author.id}) for {duration}. Reason: {reason}")
|
||||||
|
|
||||||
|
# --- Add to Mod Log DB ---
|
||||||
|
mod_log_cog: ModLogCog = self.bot.get_cog('ModLogCog')
|
||||||
|
if mod_log_cog:
|
||||||
|
await mod_log_cog.log_action(
|
||||||
|
guild=ctx.guild,
|
||||||
|
moderator=ctx.author,
|
||||||
|
target=member,
|
||||||
|
action_type="TIMEOUT",
|
||||||
|
reason=reason,
|
||||||
|
duration=delta # Pass the timedelta object
|
||||||
|
)
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
# Send confirmation message with DM status
|
# Send confirmation message with DM status
|
||||||
dm_status = "✅ DM notification sent" if dm_sent else "❌ Could not send DM notification (user may have DMs disabled)"
|
dm_status = "✅ DM notification sent" if dm_sent else "❌ Could not send DM notification (user may have DMs disabled)"
|
||||||
await ctx.reply(f"⏰ **Timed out {member.mention}** for {duration}! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
await ctx.reply(f"⏰ **Timed out {member.mention}** for {duration}! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
||||||
@ -801,6 +896,19 @@ class ModerationCog(commands.Cog):
|
|||||||
# Log the action
|
# Log the action
|
||||||
logger.info(f"Timeout was removed from user {member} (ID: {member.id}) in {ctx.guild.name} (ID: {ctx.guild.id}) by {ctx.author} (ID: {ctx.author.id}). Reason: {reason}")
|
logger.info(f"Timeout was removed from user {member} (ID: {member.id}) in {ctx.guild.name} (ID: {ctx.guild.id}) by {ctx.author} (ID: {ctx.author.id}). Reason: {reason}")
|
||||||
|
|
||||||
|
# --- Add to Mod Log DB ---
|
||||||
|
mod_log_cog: ModLogCog = self.bot.get_cog('ModLogCog')
|
||||||
|
if mod_log_cog:
|
||||||
|
await mod_log_cog.log_action(
|
||||||
|
guild=ctx.guild,
|
||||||
|
moderator=ctx.author,
|
||||||
|
target=member,
|
||||||
|
action_type="REMOVE_TIMEOUT",
|
||||||
|
reason=reason,
|
||||||
|
duration=None
|
||||||
|
)
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
# Send confirmation message with DM status
|
# Send confirmation message with DM status
|
||||||
dm_status = "✅ DM notification sent" if dm_sent else "❌ Could not send DM notification (user may have DMs disabled)"
|
dm_status = "✅ DM notification sent" if dm_sent else "❌ Could not send DM notification (user may have DMs disabled)"
|
||||||
await ctx.reply(f"⏰ **Removed timeout from {member.mention}**! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
await ctx.reply(f"⏰ **Removed timeout from {member.mention}**! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
||||||
|
@ -403,6 +403,88 @@ class SettingsCog(commands.Cog, name="Settings"):
|
|||||||
|
|
||||||
# TODO: Add command to list permissions?
|
# TODO: Add command to list permissions?
|
||||||
|
|
||||||
|
|
||||||
|
# --- Moderation Logging Settings ---
|
||||||
|
@commands.group(name='modlogconfig', help="Configure the integrated moderation logging.", invoke_without_command=True)
|
||||||
|
@commands.has_permissions(administrator=True)
|
||||||
|
@commands.guild_only()
|
||||||
|
async def modlog_config_group(self, ctx: commands.Context):
|
||||||
|
"""Base command for moderation log configuration. Shows current settings."""
|
||||||
|
guild_id = ctx.guild.id
|
||||||
|
enabled = await settings_manager.is_mod_log_enabled(guild_id)
|
||||||
|
channel_id = await settings_manager.get_mod_log_channel_id(guild_id)
|
||||||
|
channel = ctx.guild.get_channel(channel_id) if channel_id else None
|
||||||
|
|
||||||
|
status = "✅ Enabled" if enabled else "❌ Disabled"
|
||||||
|
channel_status = channel.mention if channel else ("Not Set" if channel_id else "Not Set")
|
||||||
|
|
||||||
|
embed = discord.Embed(title="Moderation Logging Configuration", color=discord.Color.teal())
|
||||||
|
embed.add_field(name="Status", value=status, inline=False)
|
||||||
|
embed.add_field(name="Log Channel", value=channel_status, inline=False)
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@modlog_config_group.command(name='enable', help="Enables the integrated moderation logging.")
|
||||||
|
@commands.has_permissions(administrator=True)
|
||||||
|
@commands.guild_only()
|
||||||
|
async def modlog_enable(self, ctx: commands.Context):
|
||||||
|
"""Enables the integrated moderation logging for this server."""
|
||||||
|
guild_id = ctx.guild.id
|
||||||
|
success = await settings_manager.set_mod_log_enabled(guild_id, True)
|
||||||
|
if success:
|
||||||
|
await ctx.send("✅ Integrated moderation logging has been enabled.")
|
||||||
|
log.info(f"Moderation logging enabled for guild {guild_id} by {ctx.author.name}")
|
||||||
|
else:
|
||||||
|
await ctx.send("❌ Failed to enable moderation logging. Please check logs.")
|
||||||
|
log.error(f"Failed to enable moderation logging for guild {guild_id}")
|
||||||
|
|
||||||
|
@modlog_config_group.command(name='disable', help="Disables the integrated moderation logging.")
|
||||||
|
@commands.has_permissions(administrator=True)
|
||||||
|
@commands.guild_only()
|
||||||
|
async def modlog_disable(self, ctx: commands.Context):
|
||||||
|
"""Disables the integrated moderation logging for this server."""
|
||||||
|
guild_id = ctx.guild.id
|
||||||
|
success = await settings_manager.set_mod_log_enabled(guild_id, False)
|
||||||
|
if success:
|
||||||
|
await ctx.send("❌ Integrated moderation logging has been disabled.")
|
||||||
|
log.info(f"Moderation logging disabled for guild {guild_id} by {ctx.author.name}")
|
||||||
|
else:
|
||||||
|
await ctx.send("❌ Failed to disable moderation logging. Please check logs.")
|
||||||
|
log.error(f"Failed to disable moderation logging for guild {guild_id}")
|
||||||
|
|
||||||
|
@modlog_config_group.command(name='setchannel', help="Sets the channel where moderation logs will be sent. Usage: `setchannel #channel`")
|
||||||
|
@commands.has_permissions(administrator=True)
|
||||||
|
@commands.guild_only()
|
||||||
|
async def modlog_setchannel(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
|
"""Sets the channel for integrated moderation logs."""
|
||||||
|
guild_id = ctx.guild.id
|
||||||
|
# Basic check for bot permissions in the target channel
|
||||||
|
if not channel.permissions_for(ctx.guild.me).send_messages or not channel.permissions_for(ctx.guild.me).embed_links:
|
||||||
|
await ctx.send(f"❌ I need 'Send Messages' and 'Embed Links' permissions in {channel.mention} to send logs there.")
|
||||||
|
return
|
||||||
|
|
||||||
|
success = await settings_manager.set_mod_log_channel_id(guild_id, channel.id)
|
||||||
|
if success:
|
||||||
|
await ctx.send(f"✅ Moderation logs will now be sent to {channel.mention}.")
|
||||||
|
log.info(f"Moderation log channel set to {channel.id} for guild {guild_id} by {ctx.author.name}")
|
||||||
|
else:
|
||||||
|
await ctx.send("❌ Failed to set the moderation log channel. Please check logs.")
|
||||||
|
log.error(f"Failed to set moderation log channel for guild {guild_id}")
|
||||||
|
|
||||||
|
@modlog_config_group.command(name='unsetchannel', help="Unsets the moderation log channel (disables sending logs).")
|
||||||
|
@commands.has_permissions(administrator=True)
|
||||||
|
@commands.guild_only()
|
||||||
|
async def modlog_unsetchannel(self, ctx: commands.Context):
|
||||||
|
"""Unsets the channel for integrated moderation logs."""
|
||||||
|
guild_id = ctx.guild.id
|
||||||
|
success = await settings_manager.set_mod_log_channel_id(guild_id, None)
|
||||||
|
if success:
|
||||||
|
await ctx.send("✅ Moderation log channel has been unset. Logs will not be sent to a channel.")
|
||||||
|
log.info(f"Moderation log channel unset for guild {guild_id} by {ctx.author.name}")
|
||||||
|
else:
|
||||||
|
await ctx.send("❌ Failed to unset the moderation log channel. Please check logs.")
|
||||||
|
log.error(f"Failed to unset moderation log channel for guild {guild_id}")
|
||||||
|
|
||||||
|
|
||||||
# --- Error Handling for this Cog ---
|
# --- Error Handling for this Cog ---
|
||||||
@set_prefix.error
|
@set_prefix.error
|
||||||
@enable_cog.error
|
@enable_cog.error
|
||||||
@ -416,7 +498,29 @@ class SettingsCog(commands.Cog, name="Settings"):
|
|||||||
@add_command_alias.error
|
@add_command_alias.error
|
||||||
@remove_command_alias.error
|
@remove_command_alias.error
|
||||||
@sync_commands.error
|
@sync_commands.error
|
||||||
|
@modlog_config_group.error # Add error handler for the group
|
||||||
|
@modlog_enable.error
|
||||||
|
@modlog_disable.error
|
||||||
|
@modlog_setchannel.error
|
||||||
|
@modlog_unsetchannel.error
|
||||||
async def on_command_error(self, ctx: commands.Context, error):
|
async def on_command_error(self, ctx: commands.Context, error):
|
||||||
|
# Check if the error originates from the modlogconfig group or its subcommands
|
||||||
|
if ctx.command and (ctx.command.name == 'modlogconfig' or (ctx.command.parent and ctx.command.parent.name == 'modlogconfig')):
|
||||||
|
if isinstance(error, commands.MissingPermissions):
|
||||||
|
await ctx.send("You need Administrator permissions to configure moderation logging.")
|
||||||
|
return # Handled
|
||||||
|
elif isinstance(error, commands.BadArgument):
|
||||||
|
await ctx.send(f"Invalid argument. Usage: `{ctx.prefix}help {ctx.command.qualified_name}`")
|
||||||
|
return # Handled
|
||||||
|
elif isinstance(error, commands.MissingRequiredArgument):
|
||||||
|
await ctx.send(f"Missing argument. Usage: `{ctx.prefix}help {ctx.command.qualified_name}`")
|
||||||
|
return # Handled
|
||||||
|
elif isinstance(error, commands.NoPrivateMessage):
|
||||||
|
await ctx.send("This command can only be used in a server.")
|
||||||
|
return # Handled
|
||||||
|
# Let other errors fall through to the generic handler below
|
||||||
|
|
||||||
|
# Generic handlers for other commands in this cog
|
||||||
if isinstance(error, commands.MissingPermissions):
|
if isinstance(error, commands.MissingPermissions):
|
||||||
await ctx.send("You need Administrator permissions to use this command.")
|
await ctx.send("You need Administrator permissions to use this command.")
|
||||||
elif isinstance(error, commands.BadArgument):
|
elif isinstance(error, commands.BadArgument):
|
||||||
|
146
db/mod_log_db.py
Normal file
146
db/mod_log_db.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import asyncpg
|
||||||
|
import logging
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def setup_moderation_log_table(pool: asyncpg.Pool):
|
||||||
|
"""
|
||||||
|
Ensures the moderation_logs table and its indexes exist in the database.
|
||||||
|
"""
|
||||||
|
async with pool.acquire() as connection:
|
||||||
|
try:
|
||||||
|
await connection.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS moderation_logs (
|
||||||
|
case_id SERIAL PRIMARY KEY,
|
||||||
|
guild_id BIGINT NOT NULL,
|
||||||
|
timestamp TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
moderator_id BIGINT NOT NULL,
|
||||||
|
target_user_id BIGINT NOT NULL,
|
||||||
|
action_type VARCHAR(50) NOT NULL,
|
||||||
|
reason TEXT,
|
||||||
|
duration_seconds INTEGER NULL,
|
||||||
|
log_message_id BIGINT NULL,
|
||||||
|
log_channel_id BIGINT NULL
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Create indexes if they don't exist
|
||||||
|
await connection.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_moderation_logs_guild_id ON moderation_logs (guild_id);
|
||||||
|
""")
|
||||||
|
await connection.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_moderation_logs_target_user_id ON moderation_logs (target_user_id);
|
||||||
|
""")
|
||||||
|
await connection.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_moderation_logs_moderator_id ON moderation_logs (moderator_id);
|
||||||
|
""")
|
||||||
|
log.info("Successfully ensured moderation_logs table and indexes exist.")
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error setting up moderation_logs table: {e}")
|
||||||
|
raise # Re-raise the exception to indicate setup failure
|
||||||
|
|
||||||
|
# --- Placeholder functions (to be implemented next) ---
|
||||||
|
|
||||||
|
async def add_mod_log(pool: asyncpg.Pool, guild_id: int, moderator_id: int, target_user_id: int, action_type: str, reason: Optional[str], duration_seconds: Optional[int] = None) -> Optional[int]:
|
||||||
|
"""Adds a new moderation log entry and returns the case_id."""
|
||||||
|
query = """
|
||||||
|
INSERT INTO moderation_logs (guild_id, moderator_id, target_user_id, action_type, reason, duration_seconds)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
RETURNING case_id;
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
async with pool.acquire() as connection:
|
||||||
|
result = await connection.fetchrow(query, guild_id, moderator_id, target_user_id, action_type, reason, duration_seconds)
|
||||||
|
if result:
|
||||||
|
log.info(f"Added mod log entry for guild {guild_id}, action {action_type}. Case ID: {result['case_id']}")
|
||||||
|
return result['case_id']
|
||||||
|
else:
|
||||||
|
log.error(f"Failed to add mod log entry for guild {guild_id}, action {action_type} - No case_id returned.")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error adding mod log entry: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def update_mod_log_reason(pool: asyncpg.Pool, case_id: int, new_reason: str):
|
||||||
|
"""Updates the reason for a specific moderation log entry."""
|
||||||
|
query = """
|
||||||
|
UPDATE moderation_logs
|
||||||
|
SET reason = $1
|
||||||
|
WHERE case_id = $2;
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
async with pool.acquire() as connection:
|
||||||
|
result = await connection.execute(query, new_reason, case_id)
|
||||||
|
if result == "UPDATE 1":
|
||||||
|
log.info(f"Updated reason for case_id {case_id}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
log.warning(f"Could not update reason for case_id {case_id}. Case might not exist or no change made.")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error updating mod log reason for case_id {case_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def update_mod_log_message_details(pool: asyncpg.Pool, case_id: int, message_id: int, channel_id: int):
|
||||||
|
"""Updates the log_message_id and log_channel_id for a specific case."""
|
||||||
|
query = """
|
||||||
|
UPDATE moderation_logs
|
||||||
|
SET log_message_id = $1, log_channel_id = $2
|
||||||
|
WHERE case_id = $3;
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
async with pool.acquire() as connection:
|
||||||
|
result = await connection.execute(query, message_id, channel_id, case_id)
|
||||||
|
if result == "UPDATE 1":
|
||||||
|
log.info(f"Updated message details for case_id {case_id}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
log.warning(f"Could not update message details for case_id {case_id}. Case might not exist or no change made.")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error updating mod log message details for case_id {case_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_mod_log(pool: asyncpg.Pool, case_id: int) -> Optional[asyncpg.Record]:
|
||||||
|
"""Retrieves a specific moderation log entry by case_id."""
|
||||||
|
query = "SELECT * FROM moderation_logs WHERE case_id = $1;"
|
||||||
|
try:
|
||||||
|
async with pool.acquire() as connection:
|
||||||
|
record = await connection.fetchrow(query, case_id)
|
||||||
|
return record
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error retrieving mod log for case_id {case_id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_user_mod_logs(pool: asyncpg.Pool, guild_id: int, target_user_id: int, limit: int = 50) -> List[asyncpg.Record]:
|
||||||
|
"""Retrieves moderation logs for a specific user in a guild, ordered by timestamp descending."""
|
||||||
|
query = """
|
||||||
|
SELECT * FROM moderation_logs
|
||||||
|
WHERE guild_id = $1 AND target_user_id = $2
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT $3;
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
async with pool.acquire() as connection:
|
||||||
|
records = await connection.fetch(query, guild_id, target_user_id, limit)
|
||||||
|
return records
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error retrieving user mod logs for user {target_user_id} in guild {guild_id}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def get_guild_mod_logs(pool: asyncpg.Pool, guild_id: int, limit: int = 50) -> List[asyncpg.Record]:
|
||||||
|
"""Retrieves the latest moderation logs for a guild, ordered by timestamp descending."""
|
||||||
|
query = """
|
||||||
|
SELECT * FROM moderation_logs
|
||||||
|
WHERE guild_id = $1
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT $2;
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
async with pool.acquire() as connection:
|
||||||
|
records = await connection.fetch(query, guild_id, limit)
|
||||||
|
return records
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error retrieving guild mod logs for guild {guild_id}: {e}")
|
||||||
|
return []
|
@ -20,7 +20,9 @@ from typing import Optional # Added for GurtCog type hint
|
|||||||
# --- Placeholder for GurtCog instance and bot instance ---
|
# --- Placeholder for GurtCog instance and bot instance ---
|
||||||
# These need to be set by the script that starts the bot and API server
|
# These need to be set by the script that starts the bot and API server
|
||||||
from discordbot.gurt.cog import GurtCog # Import GurtCog for type hint and access
|
from discordbot.gurt.cog import GurtCog # Import GurtCog for type hint and access
|
||||||
|
from discordbot.cogs.mod_log_cog import ModLogCog # Import ModLogCog for type hint
|
||||||
gurt_cog_instance: Optional[GurtCog] = None
|
gurt_cog_instance: Optional[GurtCog] = None
|
||||||
|
mod_log_cog_instance: Optional[ModLogCog] = None # Placeholder for ModLogCog
|
||||||
bot_instance = None # Will be set to the Discord bot instance
|
bot_instance = None # Will be set to the Discord bot instance
|
||||||
|
|
||||||
# ============= Models =============
|
# ============= Models =============
|
||||||
|
31
main.py
31
main.py
@ -13,6 +13,7 @@ from commands import load_all_cogs, reload_all_cogs
|
|||||||
from error_handler import handle_error, patch_discord_methods, store_interaction_content
|
from error_handler import handle_error, patch_discord_methods, store_interaction_content
|
||||||
from utils import reload_script
|
from utils import reload_script
|
||||||
import settings_manager # Import the settings manager
|
import settings_manager # Import the settings manager
|
||||||
|
from db import mod_log_db # Import the new mod log db functions
|
||||||
import command_customization # Import command customization utilities
|
import command_customization # Import command customization utilities
|
||||||
|
|
||||||
# Import the unified API service runner and the sync API module
|
# Import the unified API service runner and the sync API module
|
||||||
@ -57,6 +58,8 @@ intents.members = True
|
|||||||
bot = commands.Bot(command_prefix=get_prefix, intents=intents)
|
bot = commands.Bot(command_prefix=get_prefix, intents=intents)
|
||||||
bot.owner_id = int(os.getenv('OWNER_USER_ID'))
|
bot.owner_id = int(os.getenv('OWNER_USER_ID'))
|
||||||
bot.core_cogs = CORE_COGS # Attach core cogs list to bot instance
|
bot.core_cogs = CORE_COGS # Attach core cogs list to bot instance
|
||||||
|
bot.settings_manager = settings_manager # Attach settings manager instance
|
||||||
|
# bot.pool will be attached after initialization
|
||||||
|
|
||||||
# --- Logging Setup ---
|
# --- Logging Setup ---
|
||||||
# Configure logging (adjust level and format as needed)
|
# Configure logging (adjust level and format as needed)
|
||||||
@ -454,7 +457,22 @@ async def main(args): # Pass parsed args
|
|||||||
bot.ai_cogs_to_skip = [] # Ensure it exists even if empty
|
bot.ai_cogs_to_skip = [] # Ensure it exists even if empty
|
||||||
|
|
||||||
# Initialize pools before starting the bot logic
|
# Initialize pools before starting the bot logic
|
||||||
await settings_manager.initialize_pools()
|
pools_initialized = await settings_manager.initialize_pools()
|
||||||
|
|
||||||
|
if not pools_initialized:
|
||||||
|
log.critical("Failed to initialize database/cache pools. Bot cannot start.")
|
||||||
|
return # Prevent bot from starting if pools fail
|
||||||
|
|
||||||
|
# Attach the pool to the bot instance *after* successful initialization
|
||||||
|
bot.pool = settings_manager.pg_pool
|
||||||
|
|
||||||
|
# Setup the moderation log table *after* pool initialization
|
||||||
|
try:
|
||||||
|
await mod_log_db.setup_moderation_log_table(bot.pool)
|
||||||
|
log.info("Moderation log table setup complete.")
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("CRITICAL: Failed to setup moderation log table. Logging may not work correctly.")
|
||||||
|
# Decide if bot should continue or stop if table setup fails. Continuing for now.
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with bot:
|
async with bot:
|
||||||
@ -462,7 +480,7 @@ async def main(args): # Pass parsed args
|
|||||||
# This should now include WelcomeCog and SettingsCog if they are in the cogs dir
|
# This should now include WelcomeCog and SettingsCog if they are in the cogs dir
|
||||||
await load_all_cogs(bot, skip_cogs=ai_cogs_to_skip)
|
await load_all_cogs(bot, skip_cogs=ai_cogs_to_skip)
|
||||||
|
|
||||||
# --- Share GurtCog instance and bot instance with the sync API ---
|
# --- Share GurtCog, ModLogCog, and bot instance with the sync API ---
|
||||||
try:
|
try:
|
||||||
gurt_cog = bot.get_cog("Gurt") # Get the loaded GurtCog instance
|
gurt_cog = bot.get_cog("Gurt") # Get the loaded GurtCog instance
|
||||||
if gurt_cog:
|
if gurt_cog:
|
||||||
@ -474,6 +492,15 @@ async def main(args): # Pass parsed args
|
|||||||
# Share the bot instance with the sync API
|
# Share the bot instance with the sync API
|
||||||
discord_bot_sync_api.bot_instance = bot
|
discord_bot_sync_api.bot_instance = bot
|
||||||
print("Successfully shared bot instance with discord_bot_sync_api.")
|
print("Successfully shared bot instance with discord_bot_sync_api.")
|
||||||
|
|
||||||
|
# Share ModLogCog instance
|
||||||
|
mod_log_cog = bot.get_cog("ModLogCog")
|
||||||
|
if mod_log_cog:
|
||||||
|
discord_bot_sync_api.mod_log_cog_instance = mod_log_cog
|
||||||
|
print("Successfully shared ModLogCog instance with discord_bot_sync_api.")
|
||||||
|
else:
|
||||||
|
print("Warning: ModLogCog not found after loading cogs. AI moderation API endpoint will not work.")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error sharing instances with discord_bot_sync_api: {e}")
|
print(f"Error sharing instances with discord_bot_sync_api: {e}")
|
||||||
# ------------------------------------------------
|
# ------------------------------------------------
|
||||||
|
@ -2133,3 +2133,30 @@ async def get_all_command_aliases(guild_id: int) -> dict[str, list[str]] | None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Database error fetching command aliases for guild {guild_id}: {e}")
|
log.exception(f"Database error fetching command aliases for guild {guild_id}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# --- Moderation Logging Settings ---
|
||||||
|
|
||||||
|
async def is_mod_log_enabled(guild_id: int, default: bool = False) -> bool:
|
||||||
|
"""Checks if the integrated moderation log is enabled for a guild."""
|
||||||
|
enabled_str = await get_setting(guild_id, 'mod_log_enabled', default=str(default))
|
||||||
|
# Handle potential non-string default if get_setting fails early
|
||||||
|
if isinstance(enabled_str, bool):
|
||||||
|
return enabled_str
|
||||||
|
return enabled_str.lower() == 'true'
|
||||||
|
|
||||||
|
async def set_mod_log_enabled(guild_id: int, enabled: bool) -> bool:
|
||||||
|
"""Sets the enabled status for the integrated moderation log."""
|
||||||
|
return await set_setting(guild_id, 'mod_log_enabled', str(enabled))
|
||||||
|
|
||||||
|
async def get_mod_log_channel_id(guild_id: int) -> int | None:
|
||||||
|
"""Gets the channel ID for the integrated moderation log."""
|
||||||
|
channel_id_str = await get_setting(guild_id, 'mod_log_channel_id', default=None)
|
||||||
|
if channel_id_str and channel_id_str.isdigit():
|
||||||
|
return int(channel_id_str)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def set_mod_log_channel_id(guild_id: int, channel_id: int | None) -> bool:
|
||||||
|
"""Sets the channel ID for the integrated moderation log. Set to None to disable."""
|
||||||
|
value_to_set = str(channel_id) if channel_id is not None else None
|
||||||
|
return await set_setting(guild_id, 'mod_log_channel_id', value_to_set)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user