Merge work into master
This commit is contained in:
commit
2805a06b70
62
AGENTS.md
62
AGENTS.md
@ -8,4 +8,64 @@
|
|||||||
- You are using python 3.11.
|
- You are using python 3.11.
|
||||||
- All pip dependencies are installed in the environment. (Except packages related to locally running AI, due to their large size.)
|
- All pip dependencies are installed in the environment. (Except packages related to locally running AI, due to their large size.)
|
||||||
- You have the ability to directly push to the git repo over https (url already set). And you should when you finish your task and have tested it.
|
- You have the ability to directly push to the git repo over https (url already set). And you should when you finish your task and have tested it.
|
||||||
- If you encounter errors when pushing just leave it to the user to fix them.
|
- If you encounter any hard to fix errors when pushing just leave it to the user to fix them. Try fixing merge conflicts/not having work from the remote by yourself.
|
||||||
|
- You have a python script "tavilytool.py" in the project root, that you can use to search the web.
|
||||||
|
|
||||||
|
# Tavily API Script Usage Instructions
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
Search for information using simple queries:
|
||||||
|
```bash
|
||||||
|
python tavilytool.py "your search query"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
```bash
|
||||||
|
python tavilytool.py "latest AI developments 2024"
|
||||||
|
python tavilytool.py "how to make chocolate chip cookies"
|
||||||
|
python tavilytool.py "current weather in New York"
|
||||||
|
python tavilytool.py "best programming practices Python"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Options
|
||||||
|
|
||||||
|
### Search Depth
|
||||||
|
- **Basic search**: `python tavilytool.py "query"` (default)
|
||||||
|
- **Advanced search**: `python tavilytool.py "query" --depth advanced`
|
||||||
|
|
||||||
|
### Control Results
|
||||||
|
- **Limit results**: `python tavilytool.py "query" --max-results 3`
|
||||||
|
- **Include images**: `python tavilytool.py "query" --include-images`
|
||||||
|
- **Skip AI answer**: `python tavilytool.py "query" --no-answer`
|
||||||
|
|
||||||
|
### Domain Filtering
|
||||||
|
- **Include specific domains**: `python tavilytool.py "query" --include-domains reddit.com stackoverflow.com`
|
||||||
|
- **Exclude domains**: `python tavilytool.py "query" --exclude-domains wikipedia.org`
|
||||||
|
|
||||||
|
### Output Format
|
||||||
|
- **Formatted output**: `python tavilytool.py "query"` (default - human readable)
|
||||||
|
- **Raw JSON**: `python tavilytool.py "query" --raw` (for programmatic processing)
|
||||||
|
|
||||||
|
## Output Structure
|
||||||
|
The default formatted output includes:
|
||||||
|
- 🤖 **AI Answer**: Direct answer to your query
|
||||||
|
- 🔍 **Search Results**: Titles, URLs, and content snippets
|
||||||
|
- 🖼️ **Images**: Relevant images (when `--include-images` is used)
|
||||||
|
|
||||||
|
## Command Combinations
|
||||||
|
```bash
|
||||||
|
# Advanced search with images, limited results
|
||||||
|
python tavilytool.py "machine learning tutorials" --depth advanced --include-images --max-results 3
|
||||||
|
|
||||||
|
# Search specific sites only, raw output
|
||||||
|
python tavilytool.py "Python best practices" --include-domains github.com stackoverflow.com --raw
|
||||||
|
|
||||||
|
# Quick search without AI answer
|
||||||
|
python tavilytool.py "today's news" --no-answer --max-results 5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
- Always quote your search queries to handle spaces and special characters
|
||||||
|
- Use `--max-results` to control response length and API usage
|
||||||
|
- Use `--raw` when you need to parse results programmatically
|
||||||
|
- Combine options as needed for specific use cases
|
@ -1,5 +1,6 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands, tasks
|
from discord.ext import commands, tasks
|
||||||
|
from discord import ui, AllowedMentions
|
||||||
import datetime
|
import datetime
|
||||||
import asyncio
|
import asyncio
|
||||||
import aiohttp # Added for webhook sending
|
import aiohttp # Added for webhook sending
|
||||||
@ -54,6 +55,83 @@ class LoggingCog(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
asyncio.create_task(self.start_audit_log_poller_when_ready()) # Keep this for initial start
|
asyncio.create_task(self.start_audit_log_poller_when_ready()) # Keep this for initial start
|
||||||
|
|
||||||
|
class LogView(ui.LayoutView):
|
||||||
|
"""Simple view for log messages with helper methods."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
bot: commands.Bot,
|
||||||
|
title: str,
|
||||||
|
description: str,
|
||||||
|
color: discord.Color,
|
||||||
|
author: Optional[discord.abc.User],
|
||||||
|
footer: Optional[str],
|
||||||
|
):
|
||||||
|
super().__init__(timeout=None)
|
||||||
|
self.container = ui.Container(accent_colour=color)
|
||||||
|
self.add_item(self.container)
|
||||||
|
|
||||||
|
self.header = ui.Section(
|
||||||
|
accessory=(
|
||||||
|
ui.Thumbnail(media=author.display_avatar.url)
|
||||||
|
if author
|
||||||
|
else ui.Button(label="\u200b", disabled=True)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.header.add_item(ui.TextDisplay(f"**{title}**"))
|
||||||
|
if description:
|
||||||
|
self.header.add_item(ui.TextDisplay(description))
|
||||||
|
self.container.add_item(self.header)
|
||||||
|
|
||||||
|
# Placeholder for future field sections. They are inserted before
|
||||||
|
# the separator when the first field is added.
|
||||||
|
self._field_sections: list[ui.Section] = []
|
||||||
|
|
||||||
|
self.separator = ui.Separator(spacing=discord.SeparatorSpacing.small)
|
||||||
|
|
||||||
|
footer_text = footer or f"Bot ID: {bot.user.id}" + (
|
||||||
|
f" | User ID: {author.id}" if author else ""
|
||||||
|
)
|
||||||
|
self.footer_display = ui.TextDisplay(footer_text)
|
||||||
|
|
||||||
|
self.container.add_item(self.separator)
|
||||||
|
self.container.add_item(self.footer_display)
|
||||||
|
|
||||||
|
# --- Compatibility helpers ---
|
||||||
|
def add_field(self, name: str, value: str, inline: bool = False):
|
||||||
|
"""Mimic Embed.add_field by appending a bolded name/value line."""
|
||||||
|
if not self._field_sections or len(self._field_sections[-1].children) >= 3:
|
||||||
|
section = ui.Section(accessory=ui.Button(label="\u200b", disabled=True))
|
||||||
|
self._insert_field_section(section)
|
||||||
|
self._field_sections.append(section)
|
||||||
|
self._field_sections[-1].add_item(ui.TextDisplay(f"**{name}:** {value}"))
|
||||||
|
|
||||||
|
def _insert_field_section(self, section: ui.Section) -> None:
|
||||||
|
"""Insert a field section before the footer separator."""
|
||||||
|
self.container.remove_item(self.separator)
|
||||||
|
self.container.remove_item(self.footer_display)
|
||||||
|
self.container.add_item(section)
|
||||||
|
self.container.add_item(self.separator)
|
||||||
|
self.container.add_item(self.footer_display)
|
||||||
|
|
||||||
|
def set_footer(self, text: str):
|
||||||
|
"""Mimic Embed.set_footer by replacing the footer text display."""
|
||||||
|
self.footer_display.content = text
|
||||||
|
|
||||||
|
def set_author(self, name: str, icon_url: Optional[str] = None):
|
||||||
|
"""Mimic Embed.set_author by adjusting the header section."""
|
||||||
|
self.header.clear_items()
|
||||||
|
if icon_url:
|
||||||
|
self.header.accessory = ui.Thumbnail(media=icon_url)
|
||||||
|
else:
|
||||||
|
self.header.accessory = ui.Button(label="\u200b", disabled=True)
|
||||||
|
self.header.add_item(ui.TextDisplay(name))
|
||||||
|
def _user_display(self, user: Union[discord.Member, discord.User]) -> str:
|
||||||
|
"""Return display name, username and ID string for a user."""
|
||||||
|
display = user.display_name if isinstance(user, discord.Member) else user.name
|
||||||
|
username = f"{user.name}#{user.discriminator}"
|
||||||
|
return f"{display} ({username}) [ID: {user.id}]"
|
||||||
|
|
||||||
async def initialize_cog(self):
|
async def initialize_cog(self):
|
||||||
"""Asynchronous initialization tasks."""
|
"""Asynchronous initialization tasks."""
|
||||||
log.info("Initializing LoggingCog...")
|
log.info("Initializing LoggingCog...")
|
||||||
@ -103,8 +181,8 @@ class LoggingCog(commands.Cog):
|
|||||||
await self.session.close()
|
await self.session.close()
|
||||||
log.info("aiohttp ClientSession closed for LoggingCog.")
|
log.info("aiohttp ClientSession closed for LoggingCog.")
|
||||||
|
|
||||||
async def _send_log_embed(self, guild: discord.Guild, embed: discord.Embed):
|
async def _send_log_embed(self, guild: discord.Guild, embed: ui.LayoutView) -> None:
|
||||||
"""Sends the log embed via the configured webhook for the guild."""
|
"""Sends the log view via the configured webhook for the guild."""
|
||||||
if not self.session or self.session.closed:
|
if not self.session or self.session.closed:
|
||||||
log.error(f"aiohttp session not available or closed in LoggingCog for guild {guild.id}. Cannot send log.")
|
log.error(f"aiohttp session not available or closed in LoggingCog for guild {guild.id}. Cannot send log.")
|
||||||
return
|
return
|
||||||
@ -116,15 +194,20 @@ class LoggingCog(commands.Cog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
webhook = discord.Webhook.from_url(webhook_url, session=self.session)
|
webhook = discord.Webhook.from_url(
|
||||||
|
webhook_url,
|
||||||
|
session=self.session,
|
||||||
|
client=self.bot,
|
||||||
|
)
|
||||||
await webhook.send(
|
await webhook.send(
|
||||||
embed=embed,
|
view=embed,
|
||||||
username=f"{self.bot.user.name} Logs", # Optional: Customize webhook appearance
|
username=f"{self.bot.user.name} Logs",
|
||||||
avatar_url=self.bot.user.display_avatar.url # Optional: Use bot's avatar
|
avatar_url=self.bot.user.display_avatar.url,
|
||||||
|
allowed_mentions=AllowedMentions.none(),
|
||||||
)
|
)
|
||||||
# log.debug(f"Sent log embed via webhook for guild {guild.id}") # Can be noisy
|
# log.debug(f"Sent log embed via webhook for guild {guild.id}") # Can be noisy
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
log.error(f"Invalid logging webhook URL configured for guild {guild.id}.")
|
log.exception(f"Invalid logging webhook URL configured for guild {guild.id}. Error: {e}")
|
||||||
# Consider notifying an admin or disabling logging for this guild temporarily
|
# Consider notifying an admin or disabling logging for this guild temporarily
|
||||||
# await settings_manager.set_logging_webhook(guild.id, None) # Example: Auto-disable on invalid URL
|
# await settings_manager.set_logging_webhook(guild.id, None) # Example: Auto-disable on invalid URL
|
||||||
except (discord.Forbidden, discord.NotFound):
|
except (discord.Forbidden, discord.NotFound):
|
||||||
@ -139,26 +222,38 @@ class LoggingCog(commands.Cog):
|
|||||||
log.exception(f"Unexpected error sending log via webhook for guild {guild.id}: {e}")
|
log.exception(f"Unexpected error sending log via webhook for guild {guild.id}: {e}")
|
||||||
|
|
||||||
|
|
||||||
def _create_log_embed(self, title: str, description: str = "", color: discord.Color = discord.Color.blue(), author: Optional[Union[discord.User, discord.Member]] = None, footer: Optional[str] = None) -> discord.Embed:
|
def _create_log_embed(
|
||||||
"""Creates a standardized log embed."""
|
self,
|
||||||
embed = discord.Embed(title=title, description=description, color=color, timestamp=datetime.datetime.now(datetime.timezone.utc))
|
title: str,
|
||||||
if author:
|
description: str = "",
|
||||||
embed.set_author(name=str(author), icon_url=author.display_avatar.url)
|
color: discord.Color = discord.Color.blue(),
|
||||||
if footer:
|
author: Optional[Union[discord.User, discord.Member]] = None,
|
||||||
embed.set_footer(text=footer)
|
footer: Optional[str] = None,
|
||||||
else:
|
) -> ui.LayoutView:
|
||||||
# Add User ID to footer if author is present and footer isn't custom
|
"""Creates a standardized log view."""
|
||||||
user_id_str = f" | User ID: {author.id}" if author else ""
|
return self.LogView(self.bot, title, description, color, author, footer)
|
||||||
embed.set_footer(text=f"Bot ID: {self.bot.user.id}{user_id_str}")
|
|
||||||
return embed
|
|
||||||
|
|
||||||
def _add_id_footer(self, embed: discord.Embed, obj: Union[discord.Member, discord.User, discord.Role, discord.abc.GuildChannel, discord.Message, discord.Invite, None] = None, obj_id: Optional[int] = None, id_name: str = "ID"):
|
def _add_id_footer(
|
||||||
"""Adds an ID to the embed footer if possible."""
|
self,
|
||||||
|
embed: ui.LayoutView,
|
||||||
|
obj: Union[
|
||||||
|
discord.Member,
|
||||||
|
discord.User,
|
||||||
|
discord.Role,
|
||||||
|
discord.abc.GuildChannel,
|
||||||
|
discord.Message,
|
||||||
|
discord.Invite,
|
||||||
|
None,
|
||||||
|
] = None,
|
||||||
|
obj_id: Optional[int] = None,
|
||||||
|
id_name: str = "ID",
|
||||||
|
) -> None:
|
||||||
|
"""Adds an ID to the footer text if possible."""
|
||||||
target_id = obj_id or (obj.id if obj else None)
|
target_id = obj_id or (obj.id if obj else None)
|
||||||
if target_id:
|
if target_id and hasattr(embed, "footer_display"):
|
||||||
existing_footer = embed.footer.text or ""
|
existing_footer = embed.footer_display.content or ""
|
||||||
separator = " | " if existing_footer else ""
|
separator = " | " if existing_footer else ""
|
||||||
embed.set_footer(text=f"{existing_footer}{separator}{id_name}: {target_id}")
|
embed.footer_display.content = f"{existing_footer}{separator}{id_name}: {target_id}"
|
||||||
|
|
||||||
async def _check_log_enabled(self, guild_id: int, event_key: str) -> bool:
|
async def _check_log_enabled(self, guild_id: int, event_key: str) -> bool:
|
||||||
"""Checks if logging is enabled for a specific event key in a guild."""
|
"""Checks if logging is enabled for a specific event key in a guild."""
|
||||||
@ -191,10 +286,16 @@ class LoggingCog(commands.Cog):
|
|||||||
|
|
||||||
# 1. Check bot permissions
|
# 1. Check bot permissions
|
||||||
if not channel.permissions_for(me).manage_webhooks:
|
if not channel.permissions_for(me).manage_webhooks:
|
||||||
await ctx.send(f"❌ I don't have the 'Manage Webhooks' permission in {channel.mention}. Please grant it and try again.")
|
await ctx.send(
|
||||||
|
f"❌ I don't have the 'Manage Webhooks' permission in {channel.mention}. Please grant it and try again.",
|
||||||
|
allowed_mentions=AllowedMentions.none(),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
if not channel.permissions_for(me).send_messages:
|
if not channel.permissions_for(me).send_messages:
|
||||||
await ctx.send(f"❌ I don't have the 'Send Messages' permission in {channel.mention}. Please grant it and try again (needed for webhook creation confirmation).")
|
await ctx.send(
|
||||||
|
f"❌ I don't have the 'Send Messages' permission in {channel.mention}. Please grant it and try again (needed for webhook creation confirmation).",
|
||||||
|
allowed_mentions=AllowedMentions.none(),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# 2. Check existing webhook setting
|
# 2. Check existing webhook setting
|
||||||
@ -205,15 +306,27 @@ class LoggingCog(commands.Cog):
|
|||||||
if not self.session or self.session.closed: self.session = aiohttp.ClientSession() # Ensure session exists
|
if not self.session or self.session.closed: self.session = aiohttp.ClientSession() # Ensure session exists
|
||||||
existing_webhook = await discord.Webhook.from_url(existing_url, session=self.session).fetch()
|
existing_webhook = await discord.Webhook.from_url(existing_url, session=self.session).fetch()
|
||||||
if existing_webhook.channel_id == channel.id:
|
if existing_webhook.channel_id == channel.id:
|
||||||
await ctx.send(f"✅ Logging is already configured for {channel.mention} using webhook `{existing_webhook.name}`.")
|
await ctx.send(
|
||||||
|
f"✅ Logging is already configured for {channel.mention} using webhook `{existing_webhook.name}`.",
|
||||||
|
allowed_mentions=AllowedMentions.none(),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
await ctx.send(f"⚠️ Logging webhook is currently set for a different channel (<#{existing_webhook.channel_id}>). I will create a new one for {channel.mention}.")
|
await ctx.send(
|
||||||
|
f"⚠️ Logging webhook is currently set for a different channel (<#{existing_webhook.channel_id}>). I will create a new one for {channel.mention}.",
|
||||||
|
allowed_mentions=AllowedMentions.none(),
|
||||||
|
)
|
||||||
except (discord.NotFound, discord.Forbidden, ValueError, aiohttp.ClientError):
|
except (discord.NotFound, discord.Forbidden, ValueError, aiohttp.ClientError):
|
||||||
await ctx.send(f"⚠️ Could not verify the existing webhook URL. It might be invalid or deleted. I will create a new one for {channel.mention}.")
|
await ctx.send(
|
||||||
|
f"⚠️ Could not verify the existing webhook URL. It might be invalid or deleted. I will create a new one for {channel.mention}.",
|
||||||
|
allowed_mentions=AllowedMentions.none(),
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Error fetching existing webhook during setup for guild {guild.id}")
|
log.exception(f"Error fetching existing webhook during setup for guild {guild.id}")
|
||||||
await ctx.send(f"⚠️ An error occurred while checking the existing webhook. Proceeding to create a new one for {channel.mention}.")
|
await ctx.send(
|
||||||
|
f"⚠️ An error occurred while checking the existing webhook. Proceeding to create a new one for {channel.mention}.",
|
||||||
|
allowed_mentions=AllowedMentions.none(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# 3. Create new webhook
|
# 3. Create new webhook
|
||||||
@ -230,27 +343,51 @@ class LoggingCog(commands.Cog):
|
|||||||
log.info(f"Created logging webhook '{webhook_name}' in channel {channel.id} for guild {guild.id}")
|
log.info(f"Created logging webhook '{webhook_name}' in channel {channel.id} for guild {guild.id}")
|
||||||
except discord.HTTPException as e:
|
except discord.HTTPException as e:
|
||||||
log.error(f"Failed to create webhook in {channel.mention} for guild {guild.id}: {e}")
|
log.error(f"Failed to create webhook in {channel.mention} for guild {guild.id}: {e}")
|
||||||
await ctx.send(f"❌ Failed to create webhook. Error: {e}. This could be due to hitting the channel webhook limit (15).")
|
await ctx.send(
|
||||||
|
f"❌ Failed to create webhook. Error: {e}. This could be due to hitting the channel webhook limit (15).",
|
||||||
|
allowed_mentions=AllowedMentions.none(),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Unexpected error creating webhook in {channel.mention} for guild {guild.id}")
|
log.exception(f"Unexpected error creating webhook in {channel.mention} for guild {guild.id}")
|
||||||
await ctx.send("❌ An unexpected error occurred while creating the webhook.")
|
await ctx.send(
|
||||||
|
"❌ An unexpected error occurred while creating the webhook.",
|
||||||
|
allowed_mentions=AllowedMentions.none(),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# 4. Save webhook URL
|
# 4. Save webhook URL
|
||||||
success = await settings_manager.set_logging_webhook(guild.id, new_webhook.url)
|
success = await settings_manager.set_logging_webhook(guild.id, new_webhook.url)
|
||||||
if success:
|
if success:
|
||||||
await ctx.send(f"✅ Successfully configured logging to send messages to {channel.mention} via the new webhook `{new_webhook.name}`.")
|
await ctx.send(
|
||||||
|
f"✅ Successfully configured logging to send messages to {channel.mention} via the new webhook `{new_webhook.name}`.",
|
||||||
|
allowed_mentions=AllowedMentions.none(),
|
||||||
|
)
|
||||||
# Test send (optional)
|
# Test send (optional)
|
||||||
try:
|
try:
|
||||||
test_embed = self._create_log_embed("✅ Logging Setup Complete", f"Logs will now be sent to this channel via the webhook `{new_webhook.name}`.", color=discord.Color.green())
|
test_view = self._create_log_embed(
|
||||||
await new_webhook.send(embed=test_embed, username=webhook_name, avatar_url=self.bot.user.display_avatar.url)
|
"✅ Logging Setup Complete",
|
||||||
|
f"Logs will now be sent to this channel via the webhook `{new_webhook.name}`.",
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
await new_webhook.send(
|
||||||
|
view=test_view,
|
||||||
|
username=webhook_name,
|
||||||
|
avatar_url=self.bot.user.display_avatar.url,
|
||||||
|
allowed_mentions=AllowedMentions.none(),
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Failed to send test message via new webhook for guild {guild.id}: {e}")
|
log.error(f"Failed to send test message via new webhook for guild {guild.id}: {e}")
|
||||||
await ctx.send("⚠️ Could not send a test message via the new webhook, but the URL has been saved.")
|
await ctx.send(
|
||||||
|
"⚠️ Could not send a test message via the new webhook, but the URL has been saved.",
|
||||||
|
allowed_mentions=AllowedMentions.none(),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
log.error(f"Failed to save webhook URL {new_webhook.url} to database for guild {guild.id}")
|
log.error(f"Failed to save webhook URL {new_webhook.url} to database for guild {guild.id}")
|
||||||
await ctx.send("❌ Successfully created the webhook, but failed to save its URL to my settings. Please try again or contact support.")
|
await ctx.send(
|
||||||
|
"❌ Successfully created the webhook, but failed to save its URL to my settings. Please try again or contact support.",
|
||||||
|
allowed_mentions=AllowedMentions.none(),
|
||||||
|
)
|
||||||
# Attempt to delete the created webhook to avoid orphans
|
# Attempt to delete the created webhook to avoid orphans
|
||||||
try:
|
try:
|
||||||
await new_webhook.delete(reason="Failed to save URL to settings")
|
await new_webhook.delete(reason="Failed to save URL to settings")
|
||||||
@ -272,7 +409,10 @@ class LoggingCog(commands.Cog):
|
|||||||
event_key = event_key.lower() # Ensure case-insensitivity
|
event_key = event_key.lower() # Ensure case-insensitivity
|
||||||
|
|
||||||
if event_key not in ALL_EVENT_KEYS:
|
if event_key not in ALL_EVENT_KEYS:
|
||||||
await ctx.send(f"❌ Invalid event key: `{event_key}`. Use `{ctx.prefix}log list_keys` to see valid keys.")
|
await ctx.send(
|
||||||
|
f"❌ Invalid event key: `{event_key}`. Use `{ctx.prefix}log list_keys` to see valid keys.",
|
||||||
|
allowed_mentions=AllowedMentions.none(),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Determine the new status
|
# Determine the new status
|
||||||
@ -288,9 +428,15 @@ class LoggingCog(commands.Cog):
|
|||||||
|
|
||||||
if success:
|
if success:
|
||||||
status_str = "ENABLED" if new_status else "DISABLED"
|
status_str = "ENABLED" if new_status else "DISABLED"
|
||||||
await ctx.send(f"✅ Logging for event `{event_key}` is now **{status_str}**.")
|
await ctx.send(
|
||||||
|
f"✅ Logging for event `{event_key}` is now **{status_str}**.",
|
||||||
|
allowed_mentions=AllowedMentions.none(),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(f"❌ Failed to update setting for event `{event_key}`. Please check logs or try again.")
|
await ctx.send(
|
||||||
|
f"❌ Failed to update setting for event `{event_key}`. Please check logs or try again.",
|
||||||
|
allowed_mentions=AllowedMentions.none(),
|
||||||
|
)
|
||||||
|
|
||||||
@log_group.command(name="status")
|
@log_group.command(name="status")
|
||||||
@commands.has_permissions(administrator=True)
|
@commands.has_permissions(administrator=True)
|
||||||
@ -312,7 +458,7 @@ class LoggingCog(commands.Cog):
|
|||||||
for line in lines:
|
for line in lines:
|
||||||
if len(description) + len(line) + 1 > 4000: # Embed description limit (approx)
|
if len(description) + len(line) + 1 > 4000: # Embed description limit (approx)
|
||||||
embed.description = description
|
embed.description = description
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed, allowed_mentions=AllowedMentions.none())
|
||||||
description = line + "\n" # Start new description
|
description = line + "\n" # Start new description
|
||||||
embed = discord.Embed(color=discord.Color.blue()) # New embed for continuation
|
embed = discord.Embed(color=discord.Color.blue()) # New embed for continuation
|
||||||
else:
|
else:
|
||||||
@ -320,7 +466,7 @@ class LoggingCog(commands.Cog):
|
|||||||
|
|
||||||
if description: # Send the last embed page
|
if description: # Send the last embed page
|
||||||
embed.description = description.strip()
|
embed.description = description.strip()
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed, allowed_mentions=AllowedMentions.none())
|
||||||
|
|
||||||
|
|
||||||
@log_group.command(name="list_keys")
|
@log_group.command(name="list_keys")
|
||||||
@ -344,12 +490,15 @@ class LoggingCog(commands.Cog):
|
|||||||
parts.append(current_part)
|
parts.append(current_part)
|
||||||
|
|
||||||
embed.description = parts[0]
|
embed.description = parts[0]
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed, allowed_mentions=AllowedMentions.none())
|
||||||
for part in parts[1:]:
|
for part in parts[1:]:
|
||||||
await ctx.send(embed=discord.Embed(description=part, color=discord.Color.purple()))
|
await ctx.send(
|
||||||
|
embed=discord.Embed(description=part, color=discord.Color.purple()),
|
||||||
|
allowed_mentions=AllowedMentions.none(),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
embed.description = keys_text
|
embed.description = keys_text
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed, allowed_mentions=AllowedMentions.none())
|
||||||
|
|
||||||
|
|
||||||
# --- Thread Events ---
|
# --- Thread Events ---
|
||||||
@ -418,7 +567,7 @@ class LoggingCog(commands.Cog):
|
|||||||
user = await self.bot.fetch_user(member.id) # Get user object
|
user = await self.bot.fetch_user(member.id) # Get user object
|
||||||
embed = self._create_log_embed(
|
embed = self._create_log_embed(
|
||||||
title="➕ Member Joined Thread",
|
title="➕ Member Joined Thread",
|
||||||
description=f"{user.mention} joined thread {thread.mention}.",
|
description=f"{self._user_display(user)} joined thread {thread.mention}.",
|
||||||
color=discord.Color.dark_green(),
|
color=discord.Color.dark_green(),
|
||||||
author=user,
|
author=user,
|
||||||
footer=f"Thread ID: {thread.id} | User ID: {user.id}"
|
footer=f"Thread ID: {thread.id} | User ID: {user.id}"
|
||||||
@ -435,7 +584,7 @@ class LoggingCog(commands.Cog):
|
|||||||
user = await self.bot.fetch_user(member.id) # Get user object
|
user = await self.bot.fetch_user(member.id) # Get user object
|
||||||
embed = self._create_log_embed(
|
embed = self._create_log_embed(
|
||||||
title="➖ Member Left Thread",
|
title="➖ Member Left Thread",
|
||||||
description=f"{user.mention} left thread {thread.mention}.",
|
description=f"{self._user_display(user)} left thread {thread.mention}.",
|
||||||
color=discord.Color.dark_orange(),
|
color=discord.Color.dark_orange(),
|
||||||
author=user,
|
author=user,
|
||||||
footer=f"Thread ID: {thread.id} | User ID: {user.id}"
|
footer=f"Thread ID: {thread.id} | User ID: {user.id}"
|
||||||
@ -509,7 +658,7 @@ class LoggingCog(commands.Cog):
|
|||||||
|
|
||||||
embed = self._create_log_embed(
|
embed = self._create_log_embed(
|
||||||
title="📥 Member Joined",
|
title="📥 Member Joined",
|
||||||
description=f"{member.mention} ({member.id}) joined the server.",
|
description=f"{self._user_display(member)} joined the server.",
|
||||||
color=discord.Color.green(),
|
color=discord.Color.green(),
|
||||||
author=member
|
author=member
|
||||||
# Footer already includes User ID via _create_log_embed
|
# Footer already includes User ID via _create_log_embed
|
||||||
@ -527,7 +676,7 @@ class LoggingCog(commands.Cog):
|
|||||||
# We log it as a generic "left" event here.
|
# We log it as a generic "left" event here.
|
||||||
embed = self._create_log_embed(
|
embed = self._create_log_embed(
|
||||||
title="📤 Member Left",
|
title="📤 Member Left",
|
||||||
description=f"{member.mention} left the server.",
|
description=f"{self._user_display(member)} left the server.",
|
||||||
color=discord.Color.orange(),
|
color=discord.Color.orange(),
|
||||||
author=member
|
author=member
|
||||||
)
|
)
|
||||||
@ -542,7 +691,7 @@ class LoggingCog(commands.Cog):
|
|||||||
# Note: Ban reason isn't available directly in this event. Audit log might have it.
|
# Note: Ban reason isn't available directly in this event. Audit log might have it.
|
||||||
embed = self._create_log_embed(
|
embed = self._create_log_embed(
|
||||||
title="🔨 Member Banned (Event)", # Clarify this is the event, audit log has more details
|
title="🔨 Member Banned (Event)", # Clarify this is the event, audit log has more details
|
||||||
description=f"{user.mention} was banned.\n*Audit log may contain moderator and reason.*",
|
description=f"{self._user_display(user)} was banned.\n*Audit log may contain moderator and reason.*",
|
||||||
color=discord.Color.red(),
|
color=discord.Color.red(),
|
||||||
author=user # User who was banned
|
author=user # User who was banned
|
||||||
)
|
)
|
||||||
@ -556,7 +705,7 @@ class LoggingCog(commands.Cog):
|
|||||||
|
|
||||||
embed = self._create_log_embed(
|
embed = self._create_log_embed(
|
||||||
title="🔓 Member Unbanned",
|
title="🔓 Member Unbanned",
|
||||||
description=f"{user.mention} was unbanned.",
|
description=f"{self._user_display(user)} was unbanned.",
|
||||||
color=discord.Color.blurple(),
|
color=discord.Color.blurple(),
|
||||||
author=user # User who was unbanned
|
author=user # User who was unbanned
|
||||||
)
|
)
|
||||||
@ -841,7 +990,7 @@ class LoggingCog(commands.Cog):
|
|||||||
|
|
||||||
embed = self._create_log_embed(
|
embed = self._create_log_embed(
|
||||||
title="👍 Reaction Added",
|
title="👍 Reaction Added",
|
||||||
description=f"{user.mention} added {reaction.emoji} to a message by {reaction.message.author.mention} in {reaction.message.channel.mention} [Jump to Message]({reaction.message.jump_url})",
|
description=f"{self._user_display(user)} added {reaction.emoji} to a message by {self._user_display(reaction.message.author)} in {reaction.message.channel.mention} [Jump to Message]({reaction.message.jump_url})",
|
||||||
color=discord.Color.gold(),
|
color=discord.Color.gold(),
|
||||||
author=user
|
author=user
|
||||||
)
|
)
|
||||||
@ -860,7 +1009,7 @@ class LoggingCog(commands.Cog):
|
|||||||
|
|
||||||
embed = self._create_log_embed(
|
embed = self._create_log_embed(
|
||||||
title="👎 Reaction Removed",
|
title="👎 Reaction Removed",
|
||||||
description=f"{user.mention} removed {reaction.emoji} from a message by {reaction.message.author.mention} in {reaction.message.channel.mention} [Jump to Message]({reaction.message.jump_url})",
|
description=f"{self._user_display(user)} removed {reaction.emoji} from a message by {self._user_display(reaction.message.author)} in {reaction.message.channel.mention} [Jump to Message]({reaction.message.jump_url})",
|
||||||
color=discord.Color.dark_gold(),
|
color=discord.Color.dark_gold(),
|
||||||
author=user
|
author=user
|
||||||
)
|
)
|
||||||
@ -959,7 +1108,7 @@ class LoggingCog(commands.Cog):
|
|||||||
|
|
||||||
embed = self._create_log_embed(
|
embed = self._create_log_embed(
|
||||||
title=action,
|
title=action,
|
||||||
description=f"{member.mention}\n{details}",
|
description=f"{self._user_display(member)}\n{details}",
|
||||||
color=color,
|
color=color,
|
||||||
author=member
|
author=member
|
||||||
)
|
)
|
||||||
@ -1264,21 +1413,21 @@ class LoggingCog(commands.Cog):
|
|||||||
audit_event_key = "audit_ban"
|
audit_event_key = "audit_ban"
|
||||||
if not await self._check_log_enabled(guild.id, audit_event_key): return
|
if not await self._check_log_enabled(guild.id, audit_event_key): return
|
||||||
title = "🛡️ Audit Log: Member Banned"
|
title = "🛡️ Audit Log: Member Banned"
|
||||||
action_desc = f"{user.mention} banned {target.mention}"
|
action_desc = f"{self._user_display(user)} banned {self._user_display(target)}"
|
||||||
color = discord.Color.red()
|
color = discord.Color.red()
|
||||||
# self._add_id_footer(embed, target, id_name="Target ID") # Footer set later
|
# self._add_id_footer(embed, target, id_name="Target ID") # Footer set later
|
||||||
elif entry.action == discord.AuditLogAction.unban:
|
elif entry.action == discord.AuditLogAction.unban:
|
||||||
audit_event_key = "audit_unban"
|
audit_event_key = "audit_unban"
|
||||||
if not await self._check_log_enabled(guild.id, audit_event_key): return
|
if not await self._check_log_enabled(guild.id, audit_event_key): return
|
||||||
title = "🛡️ Audit Log: Member Unbanned"
|
title = "🛡️ Audit Log: Member Unbanned"
|
||||||
action_desc = f"{user.mention} unbanned {target.mention}"
|
action_desc = f"{self._user_display(user)} unbanned {self._user_display(target)}"
|
||||||
color = discord.Color.blurple()
|
color = discord.Color.blurple()
|
||||||
# self._add_id_footer(embed, target, id_name="Target ID") # Footer set later
|
# self._add_id_footer(embed, target, id_name="Target ID") # Footer set later
|
||||||
elif entry.action == discord.AuditLogAction.kick:
|
elif entry.action == discord.AuditLogAction.kick:
|
||||||
audit_event_key = "audit_kick"
|
audit_event_key = "audit_kick"
|
||||||
if not await self._check_log_enabled(guild.id, audit_event_key): return
|
if not await self._check_log_enabled(guild.id, audit_event_key): return
|
||||||
title = "🛡️ Audit Log: Member Kicked"
|
title = "🛡️ Audit Log: Member Kicked"
|
||||||
action_desc = f"{user.mention} kicked {target.mention}"
|
action_desc = f"{self._user_display(user)} kicked {self._user_display(target)}"
|
||||||
color = discord.Color.brand_red()
|
color = discord.Color.brand_red()
|
||||||
# self._add_id_footer(embed, target, id_name="Target ID") # Footer set later
|
# self._add_id_footer(embed, target, id_name="Target ID") # Footer set later
|
||||||
elif entry.action == discord.AuditLogAction.member_prune:
|
elif entry.action == discord.AuditLogAction.member_prune:
|
||||||
@ -1287,7 +1436,7 @@ class LoggingCog(commands.Cog):
|
|||||||
title = "🛡️ Audit Log: Member Prune"
|
title = "🛡️ Audit Log: Member Prune"
|
||||||
days = entry.extra.get('delete_member_days')
|
days = entry.extra.get('delete_member_days')
|
||||||
count = entry.extra.get('members_removed')
|
count = entry.extra.get('members_removed')
|
||||||
action_desc = f"{user.mention} pruned {count} members inactive for {days} days."
|
action_desc = f"{self._user_display(user)} pruned {count} members inactive for {days} days."
|
||||||
color = discord.Color.dark_red()
|
color = discord.Color.dark_red()
|
||||||
# No specific target ID here
|
# No specific target ID here
|
||||||
|
|
||||||
@ -1301,7 +1450,7 @@ class LoggingCog(commands.Cog):
|
|||||||
added = [r.mention for r in after_roles if r not in before_roles]
|
added = [r.mention for r in after_roles if r not in before_roles]
|
||||||
removed = [r.mention for r in before_roles if r not in after_roles]
|
removed = [r.mention for r in before_roles if r not in after_roles]
|
||||||
if added or removed: # Only log if roles actually changed
|
if added or removed: # Only log if roles actually changed
|
||||||
action_desc = f"{user.mention} updated roles for {target.mention} ({target.id}):"
|
action_desc = f"{self._user_display(user)} updated roles for {self._user_display(target)} ({target.id}):"
|
||||||
if added: action_desc += f"\n**Added:** {', '.join(added)}"
|
if added: action_desc += f"\n**Added:** {', '.join(added)}"
|
||||||
if removed: action_desc += f"\n**Removed:** {', '.join(removed)}"
|
if removed: action_desc += f"\n**Removed:** {', '.join(removed)}"
|
||||||
color = discord.Color.blue()
|
color = discord.Color.blue()
|
||||||
@ -1317,10 +1466,10 @@ class LoggingCog(commands.Cog):
|
|||||||
title = "🛡️ Audit Log: Member Timeout Update"
|
title = "🛡️ Audit Log: Member Timeout Update"
|
||||||
if after_timed_out:
|
if after_timed_out:
|
||||||
timeout_duration = discord.utils.format_dt(after_timed_out, style='R')
|
timeout_duration = discord.utils.format_dt(after_timed_out, style='R')
|
||||||
action_desc = f"{user.mention} timed out {target.mention} ({target.id}) until {timeout_duration}"
|
action_desc = f"{self._user_display(user)} timed out {self._user_display(target)} ({target.id}) until {timeout_duration}"
|
||||||
color = discord.Color.orange()
|
color = discord.Color.orange()
|
||||||
else:
|
else:
|
||||||
action_desc = f"{user.mention} removed timeout from {target.mention} ({target.id})"
|
action_desc = f"{self._user_display(user)} removed timeout from {self._user_display(target)} ({target.id})"
|
||||||
color = discord.Color.green()
|
color = discord.Color.green()
|
||||||
# self._add_id_footer(embed, target, id_name="Target ID") # Footer set later
|
# self._add_id_footer(embed, target, id_name="Target ID") # Footer set later
|
||||||
else:
|
else:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord import app_commands, Interaction, Embed, Color, User, Member, Object
|
from discord import app_commands, Interaction, Embed, Color, User, Member, Object, ui
|
||||||
import asyncpg
|
import asyncpg
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional, Union, Dict, Any
|
from typing import Optional, Union, Dict, Any
|
||||||
@ -32,6 +32,49 @@ class ModLogCog(commands.Cog):
|
|||||||
# Add command group to the bot's tree
|
# Add command group to the bot's tree
|
||||||
self.bot.tree.add_command(self.modlog_group)
|
self.bot.tree.add_command(self.modlog_group)
|
||||||
|
|
||||||
|
class LogView(ui.LayoutView):
|
||||||
|
"""View used for moderation log messages."""
|
||||||
|
|
||||||
|
def __init__(self, bot: commands.Bot, title: str, color: discord.Color, lines: list[str], footer: str):
|
||||||
|
super().__init__(timeout=None)
|
||||||
|
container = ui.Container(accent_colour=color)
|
||||||
|
self.add_item(container)
|
||||||
|
container.add_item(ui.TextDisplay(f"**{title}**"))
|
||||||
|
container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
|
||||||
|
for line in lines:
|
||||||
|
container.add_item(ui.TextDisplay(line))
|
||||||
|
container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
|
||||||
|
self.footer_display = ui.TextDisplay(footer)
|
||||||
|
container.add_item(self.footer_display)
|
||||||
|
|
||||||
|
def _format_user(self, user: Union[Member, User, Object], guild: Optional[discord.Guild] = None) -> str:
|
||||||
|
"""Return a string with display name, username and ID for a user-like object."""
|
||||||
|
if isinstance(user, Object):
|
||||||
|
return f"Unknown User (ID: {user.id})"
|
||||||
|
if isinstance(user, Member):
|
||||||
|
display = user.display_name
|
||||||
|
elif guild and isinstance(user, User):
|
||||||
|
member = guild.get_member(user.id)
|
||||||
|
display = member.display_name if member else user.name
|
||||||
|
else:
|
||||||
|
display = user.name
|
||||||
|
username = f"{user.name}#{user.discriminator}" if isinstance(user, (Member, User)) else "Unknown"
|
||||||
|
return f"{display} ({username}) [ID: {user.id}]"
|
||||||
|
|
||||||
|
async def _fetch_user_display(self, user_id: int, guild: discord.Guild) -> str:
|
||||||
|
"""Fetch and format a user by ID for display."""
|
||||||
|
member = guild.get_member(user_id)
|
||||||
|
if member:
|
||||||
|
return self._format_user(member, guild)
|
||||||
|
user = self.bot.get_user(user_id)
|
||||||
|
if user:
|
||||||
|
return self._format_user(user, guild)
|
||||||
|
try:
|
||||||
|
user = await self.bot.fetch_user(user_id)
|
||||||
|
return self._format_user(user, guild)
|
||||||
|
except discord.HTTPException:
|
||||||
|
return f"Unknown User (ID: {user_id})"
|
||||||
|
|
||||||
def register_commands(self):
|
def register_commands(self):
|
||||||
"""Register all commands for this cog"""
|
"""Register all commands for this cog"""
|
||||||
|
|
||||||
@ -192,8 +235,8 @@ class ModLogCog(commands.Cog):
|
|||||||
# Optionally update DB to remove channel ID? Or just leave it.
|
# Optionally update DB to remove channel ID? Or just leave it.
|
||||||
return
|
return
|
||||||
|
|
||||||
# 3. Format and send embed
|
# 3. Format and send view
|
||||||
embed = self._format_log_embed(
|
view = self._format_log_embed(
|
||||||
case_id=case_id,
|
case_id=case_id,
|
||||||
moderator=moderator, # Pass the object for display formatting
|
moderator=moderator, # Pass the object for display formatting
|
||||||
target=target,
|
target=target,
|
||||||
@ -205,7 +248,7 @@ class ModLogCog(commands.Cog):
|
|||||||
ai_details=ai_details,
|
ai_details=ai_details,
|
||||||
moderator_id_override=moderator_id_override # Pass override for formatting
|
moderator_id_override=moderator_id_override # Pass override for formatting
|
||||||
)
|
)
|
||||||
log_message = await log_channel.send(embed=embed)
|
log_message = await log_channel.send(view=view)
|
||||||
|
|
||||||
# 4. Update DB with message details
|
# 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)
|
await mod_log_db.update_mod_log_message_details(self.pool, case_id, log_message.id, log_channel.id)
|
||||||
@ -213,6 +256,7 @@ class ModLogCog(commands.Cog):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Error during Discord mod log message sending/updating for case {case_id} in guild {guild_id}: {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(
|
def _format_log_embed(
|
||||||
self,
|
self,
|
||||||
case_id: int,
|
case_id: int,
|
||||||
@ -224,9 +268,9 @@ class ModLogCog(commands.Cog):
|
|||||||
guild: discord.Guild,
|
guild: discord.Guild,
|
||||||
source: str = "BOT",
|
source: str = "BOT",
|
||||||
ai_details: Optional[Dict[str, Any]] = None,
|
ai_details: Optional[Dict[str, Any]] = None,
|
||||||
moderator_id_override: Optional[int] = None
|
moderator_id_override: Optional[int] = None,
|
||||||
) -> Embed:
|
) -> ui.LayoutView:
|
||||||
"""Helper function to create the standard log embed."""
|
"""Helper function to create the standard log view."""
|
||||||
color_map = {
|
color_map = {
|
||||||
"BAN": Color.red(),
|
"BAN": Color.red(),
|
||||||
"UNBAN": Color.green(),
|
"UNBAN": Color.green(),
|
||||||
@ -237,98 +281,56 @@ class ModLogCog(commands.Cog):
|
|||||||
"AI_ALERT": Color.purple(),
|
"AI_ALERT": Color.purple(),
|
||||||
"AI_DELETE_REQUESTED": Color.dark_grey(),
|
"AI_DELETE_REQUESTED": Color.dark_grey(),
|
||||||
}
|
}
|
||||||
# Use a distinct color for AI actions
|
embed_color = Color.blurple() if source == "AI_API" else color_map.get(action_type.upper(), Color.greyple())
|
||||||
if source == "AI_API":
|
|
||||||
embed_color = Color.blurple()
|
|
||||||
else:
|
|
||||||
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_prefix = "🤖 AI Moderation Action" if source == "AI_API" else action_type.replace("_", " ").title()
|
||||||
action_title = f"{action_title_prefix} | Case #{case_id}"
|
action_title = f"{action_title_prefix} | Case #{case_id}"
|
||||||
|
target_display = self._format_user(target, guild)
|
||||||
embed = Embed(
|
moderator_display = (
|
||||||
title=action_title,
|
f"AI System (ID: {moderator_id_override or 'Unknown'})" if source == "AI_API" else self._format_user(moderator, guild)
|
||||||
color=embed_color,
|
|
||||||
timestamp=discord.utils.utcnow()
|
|
||||||
)
|
)
|
||||||
|
lines = [f"**User:** {target_display}", f"**Moderator:** {moderator_display}"]
|
||||||
# Handle target display - check if it's a Discord Object or User/Member
|
|
||||||
if isinstance(target, discord.Object):
|
|
||||||
# For Object, we only have the ID
|
|
||||||
target_display = f"<@{target.id}> ({target.id})"
|
|
||||||
else:
|
|
||||||
# For User/Member, we can use mention
|
|
||||||
target_display = f"{target.mention} ({target.id})"
|
|
||||||
|
|
||||||
# Determine moderator display based on source
|
|
||||||
if source == "AI_API":
|
|
||||||
moderator_display = f"AI System (ID: {moderator_id_override or 'Unknown'})"
|
|
||||||
elif isinstance(moderator, discord.Object):
|
|
||||||
# For Object, we only have the ID
|
|
||||||
moderator_display = f"<@{moderator.id}> ({moderator.id})"
|
|
||||||
else:
|
|
||||||
# For User/Member, we can use mention
|
|
||||||
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 ai_details:
|
||||||
if 'rule_violated' in ai_details:
|
if "rule_violated" in ai_details:
|
||||||
embed.add_field(name="Rule Violated", value=ai_details['rule_violated'], inline=True)
|
lines.append(f"**Rule Violated:** {ai_details['rule_violated']}")
|
||||||
if 'reasoning' in ai_details:
|
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"]
|
||||||
reason_to_display = reason or ai_details['reasoning']
|
lines.append(f"**Reason / AI Reasoning:** {reason_to_display or 'No reason provided.'}")
|
||||||
embed.add_field(name="Reason / AI Reasoning", value=reason_to_display or "No reason provided.", inline=False)
|
if reason and reason != ai_details["reasoning"]:
|
||||||
# Optionally add bot reason separately if both exist and differ
|
lines.append(f"**Original Bot Reason:** {reason}")
|
||||||
if reason and reason != ai_details['reasoning']:
|
|
||||||
embed.add_field(name="Original Bot Reason", value=reason, inline=False)
|
|
||||||
else:
|
else:
|
||||||
embed.add_field(name="Reason", value=reason or "No reason provided.", inline=False)
|
lines.append(f"**Reason:** {reason or 'No reason provided.'}")
|
||||||
|
if "message_content" in ai_details:
|
||||||
# Add full message content if available
|
message_content = ai_details["message_content"]
|
||||||
if 'message_content' in ai_details:
|
|
||||||
# Truncate if too long (Discord has a 1024 character limit for embed fields)
|
|
||||||
message_content = ai_details['message_content']
|
|
||||||
if len(message_content) > 1000:
|
if len(message_content) > 1000:
|
||||||
message_content = message_content[:997] + "..."
|
message_content = message_content[:997] + "..."
|
||||||
embed.add_field(name="Message Content", value=message_content, inline=False)
|
lines.append(f"**Message Content:** {message_content}")
|
||||||
else:
|
else:
|
||||||
embed.add_field(name="Reason", value=reason or "No reason provided.", inline=False)
|
lines.append(f"**Reason:** {reason or 'No reason provided.'}")
|
||||||
|
|
||||||
if duration:
|
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())
|
total_seconds = int(duration.total_seconds())
|
||||||
days, remainder = divmod(total_seconds, 86400)
|
days, remainder = divmod(total_seconds, 86400)
|
||||||
hours, remainder = divmod(remainder, 3600)
|
hours, remainder = divmod(remainder, 3600)
|
||||||
minutes, seconds = divmod(remainder, 60)
|
minutes, seconds = divmod(remainder, 60)
|
||||||
duration_str = ""
|
duration_str = ""
|
||||||
if days > 0: duration_str += f"{days}d "
|
if days > 0:
|
||||||
if hours > 0: duration_str += f"{hours}h "
|
duration_str += f"{days}d "
|
||||||
if minutes > 0: duration_str += f"{minutes}m "
|
if hours > 0:
|
||||||
if seconds > 0 or not duration_str: duration_str += f"{seconds}s"
|
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()
|
duration_str = duration_str.strip()
|
||||||
|
lines.append(f"**Duration:** {duration_str}")
|
||||||
embed.add_field(name="Duration", value=duration_str, inline=True)
|
|
||||||
# Add expiration timestamp if applicable (e.g., for timeouts)
|
|
||||||
if action_type.upper() == "TIMEOUT":
|
if action_type.upper() == "TIMEOUT":
|
||||||
expires_at = discord.utils.utcnow() + duration
|
expires_at = discord.utils.utcnow() + duration
|
||||||
embed.add_field(name="Expires", value=f"<t:{int(expires_at.timestamp())}:R>", inline=True)
|
lines.append(f"**Expires:** <t:{int(expires_at.timestamp())}:R>")
|
||||||
|
footer = (
|
||||||
|
f"AI Moderation Action • {guild.name} ({guild.id})" + (f" • Model: {ai_details.get('ai_model')}" if ai_details and ai_details.get('ai_model') else "")
|
||||||
if source == "AI_API":
|
if source == "AI_API"
|
||||||
ai_model = ai_details.get("ai_model") if ai_details else None
|
else f"Guild: {guild.name} ({guild.id})"
|
||||||
embed.set_footer(
|
)
|
||||||
text=f"AI Moderation Action • {guild.name} ({guild.id})" + (f" • Model: {ai_model}" if ai_model else ""),
|
return self.LogView(self.bot, action_title, embed_color, lines, footer)
|
||||||
icon_url="https://cdn-icons-png.flaticon.com/512/4712/4712035.png"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
embed.set_footer(text=f"Guild: {guild.name} ({guild.id})")
|
|
||||||
|
|
||||||
return embed
|
|
||||||
|
|
||||||
# --- View Command Callback ---
|
# --- View Command Callback ---
|
||||||
@app_commands.checks.has_permissions(moderate_members=True) # Adjust permissions as needed
|
@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):
|
async def modlog_view_callback(self, interaction: Interaction, user: Optional[discord.User] = None):
|
||||||
@ -359,9 +361,14 @@ class ModLogCog(commands.Cog):
|
|||||||
timestamp_str = record['timestamp'].strftime('%Y-%m-%d %H:%M:%S')
|
timestamp_str = record['timestamp'].strftime('%Y-%m-%d %H:%M:%S')
|
||||||
reason_str = record['reason'] or "N/A"
|
reason_str = record['reason'] or "N/A"
|
||||||
duration_str = f" ({record['duration_seconds']}s)" if record['duration_seconds'] else ""
|
duration_str = f" ({record['duration_seconds']}s)" if record['duration_seconds'] else ""
|
||||||
|
target_disp = await self._fetch_user_display(record['target_user_id'], interaction.guild)
|
||||||
|
if record['moderator_id'] == 0:
|
||||||
|
mod_disp = "AI System"
|
||||||
|
else:
|
||||||
|
mod_disp = await self._fetch_user_display(record['moderator_id'], interaction.guild)
|
||||||
response_lines.append(
|
response_lines.append(
|
||||||
f"`Case #{record['case_id']}` [{timestamp_str}] **{record['action_type']}** "
|
f"`Case #{record['case_id']}` [{timestamp_str}] **{record['action_type']}** "
|
||||||
f"Target: <@{record['target_user_id']}> Mod: <@{record['moderator_id']}> "
|
f"Target: {target_disp} Mod: {mod_disp} "
|
||||||
f"Reason: {reason_str}{duration_str}"
|
f"Reason: {reason_str}{duration_str}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -408,7 +415,7 @@ class ModLogCog(commands.Cog):
|
|||||||
|
|
||||||
duration = datetime.timedelta(seconds=record['duration_seconds']) if record['duration_seconds'] else None
|
duration = datetime.timedelta(seconds=record['duration_seconds']) if record['duration_seconds'] else None
|
||||||
|
|
||||||
embed = self._format_log_embed(
|
view = self._format_log_embed(
|
||||||
case_id,
|
case_id,
|
||||||
moderator or Object(id=record['moderator_id']), # Fallback to Object if user not found
|
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
|
target or Object(id=record['target_user_id']), # Fallback to Object if user not found
|
||||||
@ -421,9 +428,10 @@ class ModLogCog(commands.Cog):
|
|||||||
# Add log message link if available
|
# Add log message link if available
|
||||||
if record['log_message_id'] and record['log_channel_id']:
|
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']}"
|
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)
|
# Append jump link as extra line
|
||||||
|
view.footer_display.content += f" | [Jump to Log]({link})"
|
||||||
|
|
||||||
await interaction.followup.send(embed=embed, ephemeral=True)
|
await interaction.followup.send(view=view, ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
@app_commands.checks.has_permissions(manage_guild=True) # Higher permission for editing reasons
|
@app_commands.checks.has_permissions(manage_guild=True) # Higher permission for editing reasons
|
||||||
@ -455,7 +463,7 @@ class ModLogCog(commands.Cog):
|
|||||||
log_channel = interaction.guild.get_channel(original_record['log_channel_id'])
|
log_channel = interaction.guild.get_channel(original_record['log_channel_id'])
|
||||||
if log_channel and isinstance(log_channel, discord.TextChannel):
|
if log_channel and isinstance(log_channel, discord.TextChannel):
|
||||||
log_message = await log_channel.fetch_message(original_record['log_message_id'])
|
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:
|
if log_message and log_message.author == self.bot.user:
|
||||||
# Re-fetch users/duration to reconstruct embed accurately
|
# Re-fetch users/duration to reconstruct embed accurately
|
||||||
# Special handling for AI moderator (ID 0) to avoid Discord API 404 error
|
# Special handling for AI moderator (ID 0) to avoid Discord API 404 error
|
||||||
if original_record['moderator_id'] == 0:
|
if original_record['moderator_id'] == 0:
|
||||||
@ -476,7 +484,7 @@ class ModLogCog(commands.Cog):
|
|||||||
|
|
||||||
duration = datetime.timedelta(seconds=original_record['duration_seconds']) if original_record['duration_seconds'] else None
|
duration = datetime.timedelta(seconds=original_record['duration_seconds']) if original_record['duration_seconds'] else None
|
||||||
|
|
||||||
new_embed = self._format_log_embed(
|
new_view = self._format_log_embed(
|
||||||
case_id,
|
case_id,
|
||||||
moderator or Object(id=original_record['moderator_id']),
|
moderator or Object(id=original_record['moderator_id']),
|
||||||
target or Object(id=original_record['target_user_id']),
|
target or Object(id=original_record['target_user_id']),
|
||||||
@ -485,13 +493,11 @@ class ModLogCog(commands.Cog):
|
|||||||
duration,
|
duration,
|
||||||
interaction.guild
|
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']}"
|
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_view.footer_display.content += f" | [Jump to Log]({link}) | Updated By: {interaction.user.mention}"
|
||||||
new_embed.add_field(name="Updated Reason By", value=f"{interaction.user.mention}", inline=False) # Indicate update
|
|
||||||
|
|
||||||
await log_message.edit(embed=new_embed)
|
await log_message.edit(view=new_view)
|
||||||
log.info(f"Successfully updated log message embed for case {case_id}")
|
log.info(f"Successfully updated log message view for case {case_id}")
|
||||||
except discord.NotFound:
|
except discord.NotFound:
|
||||||
log.warning(f"Original log message or channel not found for case {case_id} when updating reason.")
|
log.warning(f"Original log message or channel not found for case {case_id} when updating reason.")
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
|
@ -30,6 +30,12 @@ class ModerationCog(commands.Cog):
|
|||||||
# Add command group to the bot's tree
|
# Add command group to the bot's tree
|
||||||
self.bot.tree.add_command(self.moderate_group)
|
self.bot.tree.add_command(self.moderate_group)
|
||||||
|
|
||||||
|
def _user_display(self, user: Union[discord.Member, discord.User]) -> str:
|
||||||
|
"""Return display name, username and ID string for a user."""
|
||||||
|
display = user.display_name if isinstance(user, discord.Member) else user.name
|
||||||
|
username = f"{user.name}#{user.discriminator}"
|
||||||
|
return f"{display} ({username}) [ID: {user.id}]"
|
||||||
|
|
||||||
def register_commands(self):
|
def register_commands(self):
|
||||||
"""Register all commands for this cog"""
|
"""Register all commands for this cog"""
|
||||||
|
|
||||||
@ -285,11 +291,12 @@ class ModerationCog(commands.Cog):
|
|||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
# Send confirmation message with DM status
|
# Send confirmation message with DM status
|
||||||
|
target_text = self._user_display(member)
|
||||||
if send_dm:
|
if send_dm:
|
||||||
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 {target_text}**! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
||||||
else:
|
else:
|
||||||
await interaction.response.send_message(f"🔨 **Banned {member.mention}**! Reason: {reason or 'No reason provided'}\n⚠️ DM notification was disabled")
|
await interaction.response.send_message(f"🔨 **Banned {target_text}**! Reason: {reason or 'No reason provided'}\n⚠️ DM notification was disabled")
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await interaction.response.send_message("❌ I don't have permission to ban this member.", ephemeral=True)
|
await interaction.response.send_message("❌ I don't have permission to ban this member.", ephemeral=True)
|
||||||
except discord.HTTPException as e:
|
except discord.HTTPException as e:
|
||||||
@ -349,7 +356,7 @@ class ModerationCog(commands.Cog):
|
|||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
# 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 {self._user_display(banned_user)}**! Reason: {reason or 'No reason provided'}")
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await interaction.response.send_message("❌ I don't have permission to unban this user.", ephemeral=True)
|
await interaction.response.send_message("❌ I don't have permission to unban this user.", ephemeral=True)
|
||||||
except discord.HTTPException as e:
|
except discord.HTTPException as e:
|
||||||
@ -429,7 +436,7 @@ class ModerationCog(commands.Cog):
|
|||||||
|
|
||||||
# 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 {self._user_display(member)}**! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await interaction.response.send_message("❌ I don't have permission to kick this member.", ephemeral=True)
|
await interaction.response.send_message("❌ I don't have permission to kick this member.", ephemeral=True)
|
||||||
except discord.HTTPException as e:
|
except discord.HTTPException as e:
|
||||||
@ -541,7 +548,7 @@ class ModerationCog(commands.Cog):
|
|||||||
|
|
||||||
# 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 safe_followup(f"⏰ **Timed out {member.mention}** for {duration}! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
await safe_followup(f"⏰ **Timed out {self._user_display(member)}** for {duration}! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await safe_followup("❌ I don't have permission to timeout this member.", ephemeral=True)
|
await safe_followup("❌ I don't have permission to timeout this member.", ephemeral=True)
|
||||||
except discord.HTTPException as e:
|
except discord.HTTPException as e:
|
||||||
@ -606,7 +613,7 @@ class ModerationCog(commands.Cog):
|
|||||||
|
|
||||||
# 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 {self._user_display(member)}**! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await interaction.response.send_message("❌ I don't have permission to remove the timeout from this member.", ephemeral=True)
|
await interaction.response.send_message("❌ I don't have permission to remove the timeout from this member.", ephemeral=True)
|
||||||
except discord.HTTPException as e:
|
except discord.HTTPException as e:
|
||||||
@ -645,7 +652,7 @@ class ModerationCog(commands.Cog):
|
|||||||
logger.info(f"{len(deleted)} messages from user {user} (ID: {user.id}) were purged from channel {interaction.channel.name} (ID: {interaction.channel.id}) in {interaction.guild.name} (ID: {interaction.guild.id}) by {interaction.user} (ID: {interaction.user.id}).")
|
logger.info(f"{len(deleted)} messages from user {user} (ID: {user.id}) were purged from channel {interaction.channel.name} (ID: {interaction.channel.id}) in {interaction.guild.name} (ID: {interaction.guild.id}) by {interaction.user} (ID: {interaction.user.id}).")
|
||||||
|
|
||||||
# Send confirmation message
|
# Send confirmation message
|
||||||
await interaction.followup.send(f"🧹 **Purged {len(deleted)} messages** from {user.mention}!", ephemeral=True)
|
await interaction.followup.send(f"🧹 **Purged {len(deleted)} messages** from {self._user_display(user)}!", ephemeral=True)
|
||||||
else:
|
else:
|
||||||
# Delete messages from anyone
|
# Delete messages from anyone
|
||||||
deleted = await interaction.channel.purge(limit=amount)
|
deleted = await interaction.channel.purge(limit=amount)
|
||||||
@ -699,7 +706,7 @@ class ModerationCog(commands.Cog):
|
|||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
# 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"⚠️ **{self._user_display(member)} has been warned**! Reason: {reason}")
|
||||||
|
|
||||||
# Try to DM the user about the warning
|
# Try to DM the user about the warning
|
||||||
try:
|
try:
|
||||||
@ -788,7 +795,7 @@ class ModerationCog(commands.Cog):
|
|||||||
infractions = await mod_log_db.get_user_mod_logs(self.bot.pg_pool, interaction.guild.id, member.id)
|
infractions = await mod_log_db.get_user_mod_logs(self.bot.pg_pool, interaction.guild.id, member.id)
|
||||||
|
|
||||||
if not infractions:
|
if not infractions:
|
||||||
await interaction.response.send_message(f"No infractions found for {member.mention}.", ephemeral=True)
|
await interaction.response.send_message(f"No infractions found for {self._user_display(member)}.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
@ -901,11 +908,11 @@ class ModerationCog(commands.Cog):
|
|||||||
reason=f"Cleared {deleted_count} infractions. Reason: {reason or 'Not specified'}",
|
reason=f"Cleared {deleted_count} infractions. Reason: {reason or 'Not specified'}",
|
||||||
duration=None
|
duration=None
|
||||||
)
|
)
|
||||||
await interaction_confirm.response.edit_message(content=f"✅ Successfully cleared {deleted_count} infractions for {member.mention}. Reason: {reason or 'Not specified'}", view=None)
|
await interaction_confirm.response.edit_message(content=f"✅ Successfully cleared {deleted_count} infractions for {self._user_display(member)}. Reason: {reason or 'Not specified'}", view=None)
|
||||||
elif deleted_count == 0:
|
elif deleted_count == 0:
|
||||||
await interaction_confirm.response.edit_message(content=f"ℹ️ No infractions found for {member.mention} to clear.", view=None)
|
await interaction_confirm.response.edit_message(content=f"ℹ️ No infractions found for {self._user_display(member)} to clear.", view=None)
|
||||||
else: # Should not happen if 0 is returned for no logs
|
else: # Should not happen if 0 is returned for no logs
|
||||||
await interaction_confirm.response.edit_message(content=f"❌ Failed to clear infractions for {member.mention}. An error occurred.", view=None)
|
await interaction_confirm.response.edit_message(content=f"❌ Failed to clear infractions for {self._user_display(member)}. An error occurred.", view=None)
|
||||||
|
|
||||||
async def cancel_callback(interaction_cancel: discord.Interaction):
|
async def cancel_callback(interaction_cancel: discord.Interaction):
|
||||||
if interaction_cancel.user.id != interaction.user.id:
|
if interaction_cancel.user.id != interaction.user.id:
|
||||||
@ -919,7 +926,7 @@ class ModerationCog(commands.Cog):
|
|||||||
view.add_item(cancel_button)
|
view.add_item(cancel_button)
|
||||||
|
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
f"⚠️ Are you sure you want to clear **ALL** infractions for {member.mention}?\n"
|
f"⚠️ Are you sure you want to clear **ALL** infractions for {self._user_display(member)}?\n"
|
||||||
f"This action is irreversible. Reason: {reason or 'Not specified'}",
|
f"This action is irreversible. Reason: {reason or 'Not specified'}",
|
||||||
view=view,
|
view=view,
|
||||||
ephemeral=True
|
ephemeral=True
|
||||||
@ -1044,7 +1051,7 @@ class ModerationCog(commands.Cog):
|
|||||||
|
|
||||||
# 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 {self._user_display(member)}** for {duration}! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await ctx.reply("❌ I don't have permission to timeout this member.")
|
await ctx.reply("❌ I don't have permission to timeout this member.")
|
||||||
except discord.HTTPException as e:
|
except discord.HTTPException as e:
|
||||||
@ -1124,7 +1131,7 @@ class ModerationCog(commands.Cog):
|
|||||||
|
|
||||||
# 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 {self._user_display(member)}**! Reason: {reason or 'No reason provided'}\n{dm_status}")
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await ctx.reply("❌ I don't have permission to remove the timeout from this member.")
|
await ctx.reply("❌ I don't have permission to remove the timeout from this member.")
|
||||||
except discord.HTTPException as e:
|
except discord.HTTPException as e:
|
||||||
|
@ -439,9 +439,10 @@ class RoleManagementCog(commands.Cog):
|
|||||||
|
|
||||||
# Attempt to DM the user
|
# Attempt to DM the user
|
||||||
try:
|
try:
|
||||||
|
role_info = f"{role.name} (ID: {role.id})"
|
||||||
dm_embed = discord.Embed(
|
dm_embed = discord.Embed(
|
||||||
title="Role Added",
|
title="Role Added",
|
||||||
description=f"The role {role.mention} was added to you in **{interaction.guild.name}**.",
|
description=f"The role {role_info} was added to you in **{interaction.guild.name}**.",
|
||||||
color=role.color
|
color=role.color
|
||||||
)
|
)
|
||||||
dm_embed.add_field(name="Added by", value=interaction.user.mention, inline=True)
|
dm_embed.add_field(name="Added by", value=interaction.user.mention, inline=True)
|
||||||
@ -506,9 +507,10 @@ class RoleManagementCog(commands.Cog):
|
|||||||
|
|
||||||
# Attempt to DM the user
|
# Attempt to DM the user
|
||||||
try:
|
try:
|
||||||
|
role_info = f"{role.name} (ID: {role.id})"
|
||||||
dm_embed = discord.Embed(
|
dm_embed = discord.Embed(
|
||||||
title="Role Removed",
|
title="Role Removed",
|
||||||
description=f"The role {role.mention} was removed from you in **{interaction.guild.name}**.",
|
description=f"The role {role_info} was removed from you in **{interaction.guild.name}**.",
|
||||||
color=role.color
|
color=role.color
|
||||||
)
|
)
|
||||||
dm_embed.add_field(name="Removed by", value=interaction.user.mention, inline=True)
|
dm_embed.add_field(name="Removed by", value=interaction.user.mention, inline=True)
|
||||||
|
152
tavily.py
Normal file
152
tavily.py
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Tavily API Script for AI Agents
|
||||||
|
Execute with: python tavily.py "your search query"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import argparse
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
class TavilyAPI:
|
||||||
|
def __init__(self, api_key: str):
|
||||||
|
self.api_key = api_key
|
||||||
|
self.base_url = "https://api.tavily.com"
|
||||||
|
|
||||||
|
def search(self,
|
||||||
|
query: str,
|
||||||
|
search_depth: str = "basic",
|
||||||
|
include_answer: bool = True,
|
||||||
|
include_images: bool = False,
|
||||||
|
include_raw_content: bool = False,
|
||||||
|
max_results: int = 5,
|
||||||
|
include_domains: Optional[List[str]] = None,
|
||||||
|
exclude_domains: Optional[List[str]] = None) -> Dict:
|
||||||
|
"""
|
||||||
|
Perform a search using Tavily API
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Search query string
|
||||||
|
search_depth: "basic" or "advanced"
|
||||||
|
include_answer: Include AI-generated answer
|
||||||
|
include_images: Include images in results
|
||||||
|
include_raw_content: Include raw HTML content
|
||||||
|
max_results: Maximum number of results (1-20)
|
||||||
|
include_domains: List of domains to include
|
||||||
|
exclude_domains: List of domains to exclude
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing search results
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/search"
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"api_key": self.api_key,
|
||||||
|
"query": query,
|
||||||
|
"search_depth": search_depth,
|
||||||
|
"include_answer": include_answer,
|
||||||
|
"include_images": include_images,
|
||||||
|
"include_raw_content": include_raw_content,
|
||||||
|
"max_results": max_results
|
||||||
|
}
|
||||||
|
|
||||||
|
if include_domains:
|
||||||
|
payload["include_domains"] = include_domains
|
||||||
|
if exclude_domains:
|
||||||
|
payload["exclude_domains"] = exclude_domains
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, json=payload, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {"error": f"API request failed: {str(e)}"}
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {"error": "Invalid JSON response from API"}
|
||||||
|
|
||||||
|
def format_results(results: Dict) -> str:
|
||||||
|
"""Format search results for display"""
|
||||||
|
if "error" in results:
|
||||||
|
return f"❌ Error: {results['error']}"
|
||||||
|
|
||||||
|
output = []
|
||||||
|
|
||||||
|
# Add answer if available
|
||||||
|
if results.get("answer"):
|
||||||
|
output.append("🤖 AI Answer:")
|
||||||
|
output.append(f" {results['answer']}")
|
||||||
|
output.append("")
|
||||||
|
|
||||||
|
# Add search results
|
||||||
|
if results.get("results"):
|
||||||
|
output.append("🔍 Search Results:")
|
||||||
|
for i, result in enumerate(results["results"], 1):
|
||||||
|
output.append(f" {i}. {result.get('title', 'No title')}")
|
||||||
|
output.append(f" URL: {result.get('url', 'No URL')}")
|
||||||
|
if result.get("content"):
|
||||||
|
# Truncate content to first 200 chars
|
||||||
|
content = result["content"][:200] + "..." if len(result["content"]) > 200 else result["content"]
|
||||||
|
output.append(f" Content: {content}")
|
||||||
|
output.append("")
|
||||||
|
|
||||||
|
# Add images if available
|
||||||
|
if results.get("images"):
|
||||||
|
output.append("🖼️ Images:")
|
||||||
|
for img in results["images"][:3]: # Show first 3 images
|
||||||
|
output.append(f" {img}")
|
||||||
|
output.append("")
|
||||||
|
|
||||||
|
return "\n".join(output)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Search using Tavily API")
|
||||||
|
parser.add_argument("query", help="Search query")
|
||||||
|
parser.add_argument("--depth", choices=["basic", "advanced"], default="basic",
|
||||||
|
help="Search depth (default: basic)")
|
||||||
|
parser.add_argument("--max-results", type=int, default=5,
|
||||||
|
help="Maximum number of results (default: 5)")
|
||||||
|
parser.add_argument("--include-images", action="store_true",
|
||||||
|
help="Include images in results")
|
||||||
|
parser.add_argument("--no-answer", action="store_true",
|
||||||
|
help="Don't include AI-generated answer")
|
||||||
|
parser.add_argument("--include-domains", nargs="+",
|
||||||
|
help="Include only these domains")
|
||||||
|
parser.add_argument("--exclude-domains", nargs="+",
|
||||||
|
help="Exclude these domains")
|
||||||
|
parser.add_argument("--raw", action="store_true",
|
||||||
|
help="Output raw JSON response")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Get API key from environment
|
||||||
|
api_key = os.getenv("TAVILY_API_KEY")
|
||||||
|
if not api_key:
|
||||||
|
print("❌ Error: TAVILY_API_KEY environment variable not set")
|
||||||
|
print("Set it with: export TAVILY_API_KEY='your-api-key-here'")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Initialize Tavily API
|
||||||
|
tavily = TavilyAPI(api_key)
|
||||||
|
|
||||||
|
# Perform search
|
||||||
|
results = tavily.search(
|
||||||
|
query=args.query,
|
||||||
|
search_depth=args.depth,
|
||||||
|
include_answer=not args.no_answer,
|
||||||
|
include_images=args.include_images,
|
||||||
|
max_results=args.max_results,
|
||||||
|
include_domains=args.include_domains,
|
||||||
|
exclude_domains=args.exclude_domains
|
||||||
|
)
|
||||||
|
|
||||||
|
# Output results
|
||||||
|
if args.raw:
|
||||||
|
print(json.dumps(results, indent=2))
|
||||||
|
else:
|
||||||
|
print(format_results(results))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Loading…
x
Reference in New Issue
Block a user