Compare commits

..

1 Commits

Author SHA1 Message Date
a3d63cde98
Adjust audit log poll interval 2025-06-11 19:27:20 +00:00
12 changed files with 358 additions and 758 deletions

View File

@ -1183,6 +1183,7 @@ async def auth(
<p>You have successfully authenticated with Discord.</p>
<div class="info">
<p>You can now close this window and return to Discord.</p>
<p>Your Discord bot is now authorized to access the API on your behalf.</p>
</div>
</body>
</html>

View File

@ -9,35 +9,16 @@ import json
import os
import aiofiles # Import aiofiles
import aiofiles.os
import discord_oauth
GIVEAWAY_DATA_FILE = "data/giveaways.json"
DATA_DIR = "data"
# --- Helper Functions ---
async def get_nitro_status_oauth(user: discord.User | discord.Member) -> bool | None:
"""Return True if user has Nitro according to OAuth info, False if not, None if unknown."""
token = await discord_oauth.get_token(str(user.id))
if not token:
return None
try:
user_info = await discord_oauth.get_user_info(token)
except Exception:
return None
return user_info.get("premium_type", 0) in (1, 2)
# --- Additional Helper Functions ---
async def is_user_nitro_like(
user: discord.User | discord.Member, bot: commands.Bot = None
) -> bool:
"""Heuristically check if a user has Nitro, falling back to OAuth if available."""
nitro_oauth = await get_nitro_status_oauth(user)
if nitro_oauth is not None:
return nitro_oauth
"""Checks if a user has an animated avatar or a banner, indicating Nitro."""
# Fetch the full user object to get banner information
if bot:
try:
@ -78,23 +59,10 @@ class GiveawayEnterButton(ui.Button["GiveawayEnterView"]):
await interaction.message.edit(view=self.view)
return
if giveaway["is_nitro_giveaway"] or giveaway.get("exclude_nitro_users"):
nitro_status = await get_nitro_status_oauth(interaction.user)
if nitro_status is None:
if giveaway["is_nitro_giveaway"]:
if not await is_user_nitro_like(interaction.user, bot=self.cog.bot):
await interaction.response.send_message(
"Please authenticate with /auth so I can verify your Nitro status before entering.",
ephemeral=True,
)
return
if giveaway["is_nitro_giveaway"] and not nitro_status:
await interaction.response.send_message(
"This is a Nitro-exclusive giveaway, and your account does not appear to have Nitro.",
ephemeral=True,
)
return
if giveaway.get("exclude_nitro_users") and nitro_status:
await interaction.response.send_message(
"Nitro users are excluded from this giveaway.",
"This is a Nitro-exclusive giveaway. You don't appear to have Nitro (animated avatar or banner).",
ephemeral=True,
)
return
@ -182,16 +150,11 @@ class GiveawayRerollButton(ui.Button["GiveawayEndView"]):
except discord.NotFound:
continue # Skip if user cannot be found
if user and not user.bot:
if giveaway_data.get("is_nitro_giveaway") or giveaway_data.get(
"exclude_nitro_users"
):
nitro_status = await get_nitro_status_oauth(user)
if nitro_status is None:
continue
if giveaway_data.get("is_nitro_giveaway") and not nitro_status:
continue
if giveaway_data.get("exclude_nitro_users") and nitro_status:
continue
# Apply Nitro check again if it was a nitro giveaway
if giveaway_data.get(
"is_nitro_giveaway", False
) and not is_user_nitro_like(user):
continue
entrants_users.append(user)
if not entrants_users:
@ -265,7 +228,6 @@ class GiveawaysCog(commands.Cog, name="Giveaways"):
# "creator_id": int,
# "participants": set(), # Store user_ids. Stored as list in JSON.
# "is_nitro_giveaway": bool,
# "exclude_nitro_users": bool,
# "ended": bool
# }
# Ensure data directory exists before loading/saving
@ -363,7 +325,6 @@ class GiveawaysCog(commands.Cog, name="Giveaways"):
)
gw_data["participants"] = set(gw_data.get("participants", []))
gw_data.setdefault("is_nitro_giveaway", False)
gw_data.setdefault("exclude_nitro_users", False)
gw_data.setdefault(
"ended", gw_data["end_time"] <= now
) # Set ended if time has passed
@ -471,8 +432,7 @@ class GiveawaysCog(commands.Cog, name="Giveaways"):
prize="What is the prize?",
duration="How long should the giveaway last? (e.g., 10m, 1h, 2d, 1w)",
winners="How many winners? (default: 1)",
nitro_giveaway="Is this a Nitro-only giveaway? (OAuth verification)",
exclude_nitro="Exclude Nitro users from entering?",
nitro_giveaway="Is this a Nitro-only giveaway? (checks for animated avatar/banner)",
)
@app_commands.checks.has_permissions(manage_guild=True)
async def create_giveaway_slash(
@ -482,7 +442,6 @@ class GiveawaysCog(commands.Cog, name="Giveaways"):
duration: str,
winners: int = 1,
nitro_giveaway: bool = False,
exclude_nitro: bool = False,
):
"""Slash command to create a giveaway using buttons."""
parsed_duration = self.parse_duration(duration)
@ -510,8 +469,6 @@ class GiveawaysCog(commands.Cog, name="Giveaways"):
)
if nitro_giveaway:
embed.description += "\n*This is a Nitro-exclusive giveaway!*"
if exclude_nitro:
embed.description += "\n*Users with Nitro are excluded from entering.*"
embed.set_footer(
text=f"Giveaway started by {interaction.user.display_name}. Entries: 0"
) # Initial entry count
@ -531,7 +488,6 @@ class GiveawaysCog(commands.Cog, name="Giveaways"):
"creator_id": interaction.user.id,
"participants": set(),
"is_nitro_giveaway": nitro_giveaway,
"exclude_nitro_users": exclude_nitro,
"ended": False,
}
self.active_giveaways.append(giveaway_data)
@ -603,16 +559,10 @@ class GiveawaysCog(commands.Cog, name="Giveaways"):
if user_to_check.bot:
continue
if giveaway_data["is_nitro_giveaway"] or giveaway_data.get(
"exclude_nitro_users"
if giveaway_data["is_nitro_giveaway"] and not is_user_nitro_like(
user_to_check
):
nitro_status = await get_nitro_status_oauth(user_to_check)
if nitro_status is None:
continue
if giveaway_data["is_nitro_giveaway"] and not nitro_status:
continue
if giveaway_data.get("exclude_nitro_users") and nitro_status:
continue
continue # Skip non-nitro users for nitro giveaways
entrants_users.append(user_to_check)
winners_list = []
@ -735,16 +685,10 @@ class GiveawaysCog(commands.Cog, name="Giveaways"):
user_id
) or await self.bot.fetch_user(user_id)
if user and not user.bot:
if giveaway_info.get("is_nitro_giveaway") or giveaway_info.get(
"exclude_nitro_users"
):
nitro_status = await get_nitro_status_oauth(user)
if nitro_status is None:
continue
if giveaway_info.get("is_nitro_giveaway") and not nitro_status:
continue
if giveaway_info.get("exclude_nitro_users") and nitro_status:
continue
if giveaway_info.get(
"is_nitro_giveaway", False
) and not is_user_nitro_like(user):
continue
entrants.add(user)
if not entrants:
await interaction.followup.send(
@ -760,23 +704,8 @@ class GiveawaysCog(commands.Cog, name="Giveaways"):
reaction_found = True
async for user in reaction.users():
if not user.bot:
if giveaway_info and (
giveaway_info.get("is_nitro_giveaway")
or giveaway_info.get("exclude_nitro_users")
):
nitro_status = await get_nitro_status_oauth(user)
if nitro_status is None:
continue
if (
giveaway_info.get("is_nitro_giveaway")
and not nitro_status
):
continue
if (
giveaway_info.get("exclude_nitro_users")
and nitro_status
):
continue
# For manual reaction roll, we might not know if it was nitro_giveaway
# Consider adding a parameter to manual_roll for this if needed
entrants.add(user)
break
if not reaction_found:

View File

@ -1654,7 +1654,7 @@ class LoggingCog(commands.Cog):
# The first set of definitions already includes the toggle checks.
# --- Audit Log Polling Task ---
@tasks.loop(seconds=60) # Poll every 60 seconds
@tasks.loop(minutes=1) # Poll every 60 seconds
async def poll_audit_log(self):
# This loop starts only after the bot is ready and initialized
if not self.bot.is_ready() or self.session is None or self.session.closed:

View File

@ -82,7 +82,7 @@ class OAuthCog(commands.Cog):
if token:
# Token is available locally, send a success message
await channel.send(
f"<@{user_id}> ✅ Authentication successful!"
f"<@{user_id}> ✅ Authentication successful! You can now use the API."
)
return
@ -116,7 +116,7 @@ class OAuthCog(commands.Cog):
user_id, token_data
)
await channel.send(
f"<@{user_id}> ✅ Authentication successful!"
f"<@{user_id}> ✅ Authentication successful! You can now use the API."
)
return
except Exception as e:
@ -140,7 +140,8 @@ class OAuthCog(commands.Cog):
# Send a DM to the user
try:
await discord_user.send(
f"✅ Authentication successful! You are now logged in as {user_info.get('username')}#{user_info.get('discriminator')}."
f"✅ Authentication successful! You are now logged in as {user_info.get('username')}#{user_info.get('discriminator')}.\n"
f"Your Discord bot is now authorized to access the API on your behalf."
)
except discord.errors.Forbidden:
# If we can't send a DM, try to find the channel where the auth command was used
@ -153,17 +154,8 @@ class OAuthCog(commands.Cog):
# Remove the pending auth entry
self.pending_auth.pop(user_id, None)
@commands.hybrid_group(name="auth", description="Manage Discord authentication.")
async def auth(self, ctx: commands.Context):
"""Manage Discord authentication."""
if ctx.invoked_subcommand is None:
await ctx.send_help(ctx.command)
@auth.command(
name="login",
description="Authenticate with Discord to allow the bot to access the API on your behalf.",
)
async def login(self, ctx: commands.Context):
@commands.command(name="auth")
async def auth_command(self, ctx):
"""Authenticate with Discord to allow the bot to access the API on your behalf."""
user_id = str(ctx.author.id)
@ -174,7 +166,7 @@ class OAuthCog(commands.Cog):
is_valid, _ = await discord_oauth.validate_token(token)
if is_valid:
await ctx.send(
f"You are already authenticated. Use `{ctx.prefix}auth logout` to revoke access or `{ctx.prefix}auth status` to check your status."
f"You are already authenticated. Use `!deauth` to revoke access or `!authstatus` to check your status."
)
return
@ -250,10 +242,8 @@ class OAuthCog(commands.Cog):
f"This link will expire in 10 minutes."
)
@auth.command(
name="logout", description="Revoke the bot's access to your Discord account."
)
async def logout(self, ctx: commands.Context):
@commands.command(name="deauth")
async def deauth_command(self, ctx):
"""Revoke the bot's access to your Discord account."""
user_id = str(ctx.author.id)
@ -286,8 +276,8 @@ class OAuthCog(commands.Cog):
else:
await ctx.send("❌ You are not currently authenticated.")
@auth.command(name="status", description="Check your authentication status.")
async def status(self, ctx: commands.Context):
@commands.command(name="authstatus")
async def auth_status_command(self, ctx):
"""Check your authentication status."""
user_id = str(ctx.author.id)
@ -306,7 +296,7 @@ class OAuthCog(commands.Cog):
await ctx.send(
f"✅ You are authenticated as {username}#{discriminator}.\n"
f"The bot can access any scopes granted by this token."
f"The bot can access the API on your behalf."
)
return
except discord_oauth.OAuthError:
@ -360,7 +350,7 @@ class OAuthCog(commands.Cog):
await ctx.send(
f"✅ You are authenticated as {username}#{discriminator}.\n"
f"The bot can access any scopes you allowed when authenticating.\n"
f"The bot can access the API on your behalf.\n"
f"(Token retrieved from API service)"
)
return
@ -378,9 +368,38 @@ class OAuthCog(commands.Cog):
# If we get here, the user is not authenticated anywhere
await ctx.send(
f"❌ You are not currently authenticated. Use `{ctx.prefix}auth login` to authenticate."
"❌ You are not currently authenticated. Use `!auth` to authenticate."
)
@commands.command(name="authhelp")
async def auth_help_command(self, ctx):
"""Get help with authentication commands."""
embed = discord.Embed(
title="Authentication Help",
description="Commands for managing Discord authentication",
color=discord.Color.blue(),
)
async def setup(bot: commands.Bot):
embed.add_field(
name="!auth",
value="Authenticate with Discord to allow the bot to access the API on your behalf",
inline=False,
)
embed.add_field(
name="!deauth",
value="Revoke the bot's access to your Discord account",
inline=False,
)
embed.add_field(
name="!authstatus", value="Check your authentication status", inline=False
)
embed.add_field(name="!authhelp", value="Show this help message", inline=False)
await ctx.send(embed=embed)
async def setup(bot):
await bot.add_cog(OAuthCog(bot))

View File

@ -1,267 +1,261 @@
import discord
from discord.ext import commands
from discord import app_commands, ui
from discord import app_commands
import time
import psutil
import platform
import GPUtil
import distro
import distro # Ensure this is installed
# Import wmi for Windows motherboard info
try:
import wmi
WMI_AVAILABLE = True
except ImportError:
WMI_AVAILABLE = False
def create_progress_bar(value: float, total: float, length: int = 10) -> str:
"""Creates a text-based progress bar."""
if total == 0:
percentage = 0
else:
percentage = value / total
filled_length = int(length * percentage)
bar = '' * filled_length + '' * (length - filled_length)
return f"[{bar}] {percentage:.1%}"
class SystemStatusView(ui.LayoutView):
"""
A view that displays system and bot statistics in a visually appealing way.
It uses components v2 for a modern look.
"""
def __init__(
self,
bot_user: discord.User,
guild_count: int,
user_count: int,
os_info: str,
distro_info: str,
hostname: str,
uptime: str,
motherboard_info: str,
cpu_name: str,
cpu_usage: float,
ram_used: int,
ram_total: int,
gpu_info: str,
requester: discord.User,
) -> None:
super().__init__(timeout=None)
# --- Store all data for the view ---
self.bot_user = bot_user
self.guild_count = guild_count
self.user_count = user_count
self.os_info = os_info
self.distro_info = distro_info
self.hostname = hostname
self.uptime = uptime
self.motherboard_info = motherboard_info
self.cpu_name = cpu_name
self.cpu_usage = cpu_usage
self.ram_used = ram_used
self.ram_total = ram_total
self.gpu_info = gpu_info
self.requester = requester
# --- Build the UI ---
self._build_ui()
def _create_aligned_block(self, data: dict) -> str:
"""Creates a neatly aligned text block inside a markdown code block."""
max_key_len = max(len(k) for k in data.keys())
lines = []
for key, value in data.items():
lines.append(f"{key.ljust(max_key_len)} : {value}")
return "```\n" + "\n".join(lines) + "\n```"
def _build_ui(self):
"""Constructs the UI elements of the view."""
# Main container with Discord's "Blurple" color
container = ui.Container(accent_colour=None)
# --- Bot & System Info ---
self._add_bot_system_info(container)
# --- Hardware Info ---
self._add_hardware_info(container)
# --- Footer ---
self._add_footer(container)
self.add_item(container)
def _add_bot_system_info(self, container: ui.Container):
"""Adds bot and system information fields."""
bot_data = {
"Servers": self.guild_count,
"Users": self.user_count
}
container.add_item(ui.TextDisplay("**🤖 Bot Info**" + self._create_aligned_block(bot_data)))
system_data = {
"OS": f"{self.os_info}{self.distro_info}",
"Hostname": self.hostname,
"Uptime": self.uptime
}
container.add_item(ui.TextDisplay("**🖥️ System Info**" + self._create_aligned_block(system_data)))
def _add_hardware_info(self, container: ui.Container):
"""Adds hardware information with progress bars."""
container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
ram_usage_text = f"{self.ram_used // (1024**2):,}MB / {self.ram_total // (1024**2):,}MB"
cpu_bar = create_progress_bar(self.cpu_usage, 100.0)
ram_bar = create_progress_bar(self.ram_used, self.ram_total)
hardware_data = {
"CPU Usage": cpu_bar,
"RAM Usage": f"{ram_bar}\n{''.ljust(len('RAM Usage'))}{ram_usage_text}",
"Board": self.motherboard_info,
"CPU": self.cpu_name,
"GPU": self.gpu_info
}
container.add_item(ui.TextDisplay("**⚙️ Hardware Info**" + self._create_aligned_block(hardware_data)))
def _add_footer(self, container: ui.Container):
"""Adds the footer with timestamp and requester info."""
container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
timestamp = discord.utils.format_dt(discord.utils.utcnow(), style="R")
footer_text = f"Updated: {timestamp} | Requested by: {self.requester.display_name}"
container.add_item(ui.TextDisplay(f"_{footer_text}_"))
class SystemCheckCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
async def _build_system_check_view(self, context_or_interaction) -> SystemStatusView:
"""Gathers all system data and returns the constructed view."""
async def _system_check_logic(self, context_or_interaction):
"""Check the bot and system status."""
# Defer the response to prevent interaction timeout
await context_or_interaction.response.defer(thinking=True)
try:
embed = await self._system_check_logic(context_or_interaction)
await context_or_interaction.followup.send(embed=embed)
except Exception as e:
print(f"Error in systemcheck command: {e}")
await context_or_interaction.followup.send(
f"An error occurred while checking system status: {e}"
)
async def _system_check_logic(self, context_or_interaction):
"""Return detailed bot and system information as a Discord embed."""
# Bot information
bot_user = self.bot.user
guild_count = len(self.bot.guilds)
# A more efficient way to get unique users, avoiding large member lists in memory
user_ids = {member.id for guild in self.bot.guilds for member in guild.members if not member.bot}
# More efficient member counting - use cached members when available
# This avoids API calls that can cause timeouts
user_ids = set()
for guild in self.bot.guilds:
try:
# Use members that are already cached
for member in guild.members:
if not member.bot:
user_ids.add(member.id)
except Exception as e:
print(f"Error counting members in guild {guild.name}: {e}")
user_count = len(user_ids)
# System information
system = platform.system()
os_info = f"{system} {platform.release()}"
hostname = platform.node()
distro_info_str = ""
distro_info_str = "" # Renamed variable
if system == "Linux":
try:
# Use distro library for better Linux distribution detection
distro_name = distro.name(pretty=True)
distro_info_str = f" ({distro_name})"
except Exception:
distro_info_str = "" # Fail silently
distro_info_str = f"\n**Distro:** {distro_name}"
except ImportError:
distro_info_str = "\n**Distro:** (Install 'distro' package for details)"
except Exception as e:
distro_info_str = f"\n**Distro:** (Error getting info: {e})"
elif system == "Windows":
# Add Windows version details if possible
try:
# Use a more reliable way to get Windows version
win_ver = platform.win32_ver()
os_info = f"Windows {win_ver[0]} {win_ver[2]}"
except Exception:
pass # Fail silently
uptime_seconds = time.time() - psutil.boot_time()
days, rem = divmod(uptime_seconds, 86400)
hours, rem = divmod(rem, 3600)
minutes, _ = divmod(rem, 60)
uptime_str = f"{int(days)}d {int(hours)}h {int(minutes)}m"
win_ver = platform.version() # e.g., '10.0.19041'
win_build = platform.win32_ver()[1] # e.g., '19041'
os_info = f"Windows {win_ver} (Build {win_build})"
except Exception as e:
print(f"Could not get detailed Windows version: {e}")
# Keep the basic os_info
# Hardware information
uptime_seconds = time.time() - psutil.boot_time()
days, remainder = divmod(uptime_seconds, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
uptime_str = ""
if days > 0:
uptime_str += f"{int(days)}d "
uptime_str += f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}"
uptime = uptime_str.strip()
# Hardware information - use a shorter interval for CPU usage
cpu_usage = psutil.cpu_percent(interval=0.1)
# Get CPU info with a timeout to prevent hanging
try:
cpu_name_base = "N/A"
if system == "Linux":
with open("/proc/cpuinfo") as f:
for line in f:
if line.startswith("model name"):
cpu_name_base = line.split(":")[1].strip()
break
else: # Windows or fallback
cpu_name_base = platform.processor() or "N/A"
# Use a simpler approach for CPU name to avoid potential slowdowns
if platform.system() == "Windows":
cpu_name_base = platform.processor()
elif platform.system() == "Linux":
try:
with open("/proc/cpuinfo", "r") as f:
for line in f:
if line.startswith("model name"):
cpu_name_base = line.split(":")[1].strip()
break
else:
cpu_name_base = "Unknown CPU"
except:
cpu_name_base = platform.processor() or "Unknown CPU"
else:
cpu_name_base = platform.processor() or "Unknown CPU"
physical_cores = psutil.cpu_count(logical=False)
total_threads = psutil.cpu_count(logical=True)
cpu_name = f"{cpu_name_base} ({physical_cores}C/{total_threads}T)"
except Exception:
except Exception as e:
print(f"Error getting CPU info: {e}")
cpu_name = "N/A"
# Get motherboard information
motherboard_info = self._get_motherboard_info()
memory = psutil.virtual_memory()
# GPU Information
ram_usage = f"{memory.used // (1024 ** 2)} MB / {memory.total // (1024 ** 2)} MB ({memory.percent}%)"
# GPU Information (using GPUtil for cross-platform consistency if available)
gpu_info_lines = []
try:
gpus = GPUtil.getGPUs()
if gpus:
# Format multi-GPU info on new lines for readability
gpu_info_lines = [f"{gpu.name} ({gpu.load*100:.1f}% Load)" for gpu in gpus]
for gpu in gpus:
gpu_info_lines.append(
f"{gpu.name} ({gpu.load*100:.1f}% Load, {gpu.memoryUsed:.0f}/{gpu.memoryTotal:.0f} MB VRAM)"
)
gpu_info = "\n".join(gpu_info_lines)
else:
gpu_info = "No dedicated GPU detected"
except Exception:
gpu_info = "N/A"
gpu_info = "No dedicated GPU detected by GPUtil."
except ImportError:
gpu_info = "GPUtil library not installed. Cannot get detailed GPU info."
except Exception as e:
print(f"Error getting GPU info via GPUtil: {e}")
gpu_info = f"Error retrieving GPU info: {e}"
# Determine user based on context type
user = context_or_interaction.author if isinstance(context_or_interaction, commands.Context) else context_or_interaction.user
# Determine user and avatar URL based on context type
if isinstance(context_or_interaction, commands.Context):
user = context_or_interaction.author
avatar_url = user.display_avatar.url
elif isinstance(context_or_interaction, discord.Interaction):
user = context_or_interaction.user
avatar_url = user.display_avatar.url
else:
# Fallback or handle error if needed
user = self.bot.user # Or some default
avatar_url = self.bot.user.display_avatar.url if self.bot.user else None
return SystemStatusView(
bot_user=self.bot.user,
guild_count=guild_count,
user_count=user_count,
os_info=os_info,
distro_info=distro_info_str,
hostname=hostname,
uptime=uptime_str,
motherboard_info=motherboard_info,
cpu_name=cpu_name,
cpu_usage=cpu_usage,
ram_used=memory.used,
ram_total=memory.total,
gpu_info=gpu_info,
requester=user,
# Create embed
embed = discord.Embed(title="📊 System Status", color=discord.Color.blue())
if bot_user:
embed.set_thumbnail(url=bot_user.display_avatar.url)
# Bot Info Field
if bot_user:
embed.add_field(
name="🤖 Bot Information",
value=f"**Name:** {bot_user.name}\n"
f"**ID:** {bot_user.id}\n"
f"**Servers:** {guild_count}\n"
f"**Unique Users:** {user_count}",
inline=False,
)
else:
embed.add_field(
name="🤖 Bot Information",
value="Bot user information not available.",
inline=False,
)
# System Info Field
embed.add_field(
name="🖥️ System Information",
value=f"**OS:** {os_info}{distro_info_str}\n" # Use renamed variable
f"**Hostname:** {hostname}\n"
f"**Uptime:** {uptime}",
inline=False,
)
def _get_motherboard_info(self) -> str:
# Hardware Info Field
embed.add_field(
name="⚙️ Hardware Information",
value=f"**Device Model:** {motherboard_info}\n"
f"**CPU:** {cpu_name}\n"
f"**CPU Usage:** {cpu_usage}%\n"
f"**RAM Usage:** {ram_usage}\n"
f"**GPU Info:**\n{gpu_info}",
inline=False,
)
if user:
embed.set_footer(
text=f"Requested by: {user.display_name}", icon_url=avatar_url
)
embed.timestamp = discord.utils.utcnow()
return embed
# --- Prefix Command ---
@commands.command(name="systemcheck")
async def system_check(self, ctx: commands.Context):
"""Check the bot and system status."""
embed = await self._system_check_logic(ctx) # Pass context
await ctx.reply(embed=embed)
# --- Slash Command ---
@app_commands.command(
name="systemcheck", description="Check the bot and system status"
)
async def system_check_slash(self, interaction: discord.Interaction):
"""Slash command version of system check."""
# Defer the response to prevent interaction timeout
await interaction.response.defer(thinking=True)
try:
embed = await self._system_check_logic(interaction) # Pass interaction
# Use followup since we've already deferred
await interaction.followup.send(embed=embed)
except Exception as e:
# Handle any errors that might occur during processing
print(f"Error in system_check_slash: {e}")
await interaction.followup.send(
f"An error occurred while checking system status: {e}"
)
def _get_motherboard_info(self):
"""Get motherboard information based on the operating system."""
system = platform.system()
try:
if system == "Windows" and WMI_AVAILABLE:
w = wmi.WMI()
board = w.Win32_BaseBoard()[0]
return f"{board.Manufacturer} {board.Product}"
if system == "Windows":
if WMI_AVAILABLE:
w = wmi.WMI()
for board in w.Win32_BaseBoard():
return f"{board.Manufacturer} {board.Product}"
return "WMI module not available"
elif system == "Linux":
# Check for product_name first, then fallback to board_name
# Read motherboard product name from sysfs
try:
with open("/sys/devices/virtual/dmi/id/product_name", "r") as f:
return f.read().strip()
product_name = f.read().strip()
return product_name if product_name else "Unknown motherboard"
except FileNotFoundError:
with open("/sys/devices/virtual/dmi/id/board_name", "r") as f:
return f.read().strip()
return "N/A"
except Exception:
return "N/A"
@commands.command(name="systemcheck", aliases=["status"])
async def system_check(self, ctx: commands.Context):
"""Check the bot and system status."""
view = await self._build_system_check_view(ctx)
await ctx.reply(view=view, mention_author=False)
@app_commands.command(name="systemcheck", description="Check the bot and system status")
async def system_check_slash(self, interaction: discord.Interaction):
"""Slash command version of system check."""
await interaction.response.defer(thinking=True, ephemeral=False)
view = await self._build_system_check_view(interaction)
await interaction.followup.send(view=view)
return "/sys/devices/virtual/dmi/id/product_name not found"
except Exception as e:
return f"Error reading motherboard info: {e}"
except Exception as e:
return f"Error: {str(e)}"
else:
return f"Unsupported OS: {system}"
except Exception as e:
print(f"Error getting motherboard info: {e}")
return "Error retrieving motherboard info"
async def setup(bot):

View File

@ -1,152 +1,68 @@
import discord
from discord.ext import commands
from discord import ui
import logging
import sys
import os
from datetime import datetime, timedelta, timezone
from typing import Literal, Optional
# Add the parent directory to sys.path to ensure settings_manager is accessible
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import settings_manager
from global_bot_accessor import get_bot_instance
log = logging.getLogger(__name__)
class WelcomeMessageView(ui.LayoutView):
def __init__(self, member: discord.Member, message: str, member_count: int):
super().__init__(timeout=None)
accent_color = member.color if member.color != discord.Color.default() else discord.Color.green()
container = ui.Container(accent_colour=accent_color)
account_age = datetime.now(timezone.utc) - member.created_at
if account_age.days >= 365:
years = account_age.days // 365
months = (account_age.days % 365) // 30
age_str = f"{years} year{'s' if years != 1 else ''}, {months} month{'s' if months != 1 else ''} ago"
elif account_age.days >= 30:
months = account_age.days // 30
age_str = f"{months} month{'s' if months != 1 else ''} ago"
else:
age_str = f"{account_age.days} days ago"
header_section = ui.Section(
accessory=ui.Thumbnail(
media=member.display_avatar.url,
description="New Member Avatar",
)
)
text_block = "\n".join([
"🎉 **Welcome to the Server!** 🎉",
message,
"",
f"**Member:** {member.display_name}",
f"**Account Created:** {member.created_at.strftime('%B %d, %Y')}",
f"**Account Age:** {age_str}",
"",
"📊 **Server Statistics**",
f"**Total Members:** {member_count:,}",
f"**You are member #{member_count:,}**",
"",
"💬 Feel free to introduce yourself and have fun!"
])
header_section.add_item(ui.TextDisplay(text_block))
container.add_item(header_section)
self.add_item(container)
class GoodbyeMessageView(ui.LayoutView):
def __init__(self, member: discord.Member, message: str, member_count: int, reason: str = "left"):
super().__init__(timeout=None)
if reason == "banned":
accent_color = discord.Color.red()
emoji = "🔨"
title = "Member Banned"
elif reason == "kicked":
accent_color = discord.Color.orange()
emoji = "👢"
title = "Member Kicked"
else:
accent_color = discord.Color.dark_grey()
emoji = "👋"
title = "Member Left"
container = ui.Container(accent_colour=accent_color)
header_section = ui.Section(
accessory=ui.Thumbnail(
media=member.display_avatar.url,
description="Former Member Avatar",
)
)
lines = [
f"{emoji} **{title}** {emoji}",
message,
"",
f"**Member:** {member.display_name}",
f"**Username:** {member.name}"
]
if hasattr(member, 'joined_at') and member.joined_at:
join_date = member.joined_at.strftime('%B %d, %Y')
time_in_server = datetime.now(timezone.utc) - member.joined_at
if time_in_server.days >= 365:
years = time_in_server.days // 365
months = (time_in_server.days % 365) // 30
duration_str = f"{years} year{'s' if years != 1 else ''}, {months} month{'s' if months != 1 else ''}"
elif time_in_server.days >= 30:
months = time_in_server.days // 30
duration_str = f"{months} month{'s' if months != 1 else ''}"
else:
duration_str = f"{time_in_server.days} day{'s' if time_in_server.days != 1 else ''}"
lines += [
f"**Joined:** {join_date}",
f"**Time in Server:** {duration_str}"
]
lines += [
"",
"📊 **Server Statistics**",
f"**Current Members:** {member_count:,}"
]
header_section.add_item(ui.TextDisplay("\n".join(lines)))
container.add_item(header_section)
self.add_item(container)
class WelcomeCog(commands.Cog):
"""Handles welcome and goodbye messages for guilds."""
def __init__(self, bot: commands.Bot):
self.bot = bot
print("WelcomeCog: Initializing and registering event listeners")
# Check existing event listeners
print(
f"WelcomeCog: Bot event listeners before registration: {self.bot.extra_events}"
)
# Register event listeners
self.bot.add_listener(self.on_member_join, "on_member_join")
self.bot.add_listener(self.on_member_remove, "on_member_remove")
# Check if event listeners were registered
print(
f"WelcomeCog: Bot event listeners after registration: {self.bot.extra_events}"
)
print("WelcomeCog: Event listeners registered")
async def on_member_join(self, member: discord.Member):
"""Sends a welcome message when a new member joins."""
print(f"WelcomeCog: on_member_join event triggered for {member.name}")
guild = member.guild
if not guild:
print(f"WelcomeCog: Guild not found for member {member.name}")
return
log.debug(f"Member {member.name} joined guild {guild.name} ({guild.id})")
print(
f"WelcomeCog: Member {member.name} joined guild {guild.name} ({guild.id})"
)
# --- Fetch settings ---
print(f"WelcomeCog: Fetching welcome settings for guild {guild.id}")
welcome_channel_id_str = await settings_manager.get_setting(
guild.id, "welcome_channel_id"
)
welcome_message_template = await settings_manager.get_setting(
guild.id, "welcome_message", default="Welcome {user} to {server}!"
)
print(
f"WelcomeCog: Retrieved settings - channel_id: {welcome_channel_id_str}, message: {welcome_message_template}"
)
# Handle the "__NONE__" marker for potentially unset values
if not welcome_channel_id_str or welcome_channel_id_str == "__NONE__":
log.debug(f"Welcome channel not configured for guild {guild.id}")
print(f"WelcomeCog: Welcome channel not configured for guild {guild.id}")
return
try:
@ -156,23 +72,21 @@ class WelcomeCog(commands.Cog):
log.warning(
f"Welcome channel ID {welcome_channel_id} not found or not text channel in guild {guild.id}"
)
# Maybe remove the setting here if the channel is invalid?
return
# --- Format and send message ---
# Basic formatting, can be expanded
formatted_message = welcome_message_template.format(
user=member.mention, username=member.name, server=guild.name
)
# Get current member count
member_count = guild.member_count or len(guild.members)
view = WelcomeMessageView(member, formatted_message, member_count)
await channel.send(view=view)
await channel.send(formatted_message)
log.info(f"Sent welcome message for {member.name} in guild {guild.id}")
except ValueError as e:
except ValueError:
log.error(
f"ValueError in WelcomeCog for guild {guild.id}: {e}"
f"Invalid welcome_channel_id '{welcome_channel_id_str}' configured for guild {guild.id}"
)
except discord.Forbidden:
log.error(
@ -182,23 +96,32 @@ class WelcomeCog(commands.Cog):
log.exception(f"Error sending welcome message for guild {guild.id}: {e}")
async def on_member_remove(self, member: discord.Member):
"""Sends a goodbye message when a member leaves, is kicked, or is banned."""
"""Sends a goodbye message when a member leaves."""
print(f"WelcomeCog: on_member_remove event triggered for {member.name}")
guild = member.guild
if not guild:
print(f"WelcomeCog: Guild not found for member {member.name}")
return
log.debug(f"Member {member.name} left guild {guild.name} ({guild.id})")
print(f"WelcomeCog: Member {member.name} left guild {guild.name} ({guild.id})")
# --- Fetch settings ---
print(f"WelcomeCog: Fetching goodbye settings for guild {guild.id}")
goodbye_channel_id_str = await settings_manager.get_setting(
guild.id, "goodbye_channel_id"
)
goodbye_message_template = await settings_manager.get_setting(
guild.id, "goodbye_message", default="{username} has left the server."
)
print(
f"WelcomeCog: Retrieved settings - channel_id: {goodbye_channel_id_str}, message: {goodbye_message_template}"
)
# Handle the "__NONE__" marker
if not goodbye_channel_id_str or goodbye_channel_id_str == "__NONE__":
log.debug(f"Goodbye channel not configured for guild {guild.id}")
print(f"WelcomeCog: Goodbye channel not configured for guild {guild.id}")
return
try:
@ -210,70 +133,16 @@ class WelcomeCog(commands.Cog):
)
return
# --- Determine reason for leaving ---
reason = "left"
entry_user = None
# Check audit log for kick or ban. We check last 2 minutes just in case of delays.
try:
# Check for ban first
async for entry in guild.audit_logs(
limit=1,
action=discord.AuditLogAction.ban,
after=datetime.now(timezone.utc) - timedelta(minutes=2),
):
if entry.target and entry.target.id == member.id:
reason = "banned"
entry_user = entry.user
break
# If not banned, check for kick
if reason == "left":
async for entry in guild.audit_logs(
limit=1,
action=discord.AuditLogAction.kick,
after=datetime.now(timezone.utc) - timedelta(minutes=2),
):
if entry.target and entry.target.id == member.id:
reason = "kicked"
entry_user = entry.user
break
except discord.Forbidden:
log.warning(
f"Missing 'View Audit Log' permissions in guild {guild.id} to determine member remove reason."
)
except Exception as e:
log.error(
f"Error checking audit log for {member.name} in {guild.id}: {e}"
)
# --- Format and send message ---
if reason == "left":
formatted_message = goodbye_message_template.format(
user=member.mention, # Might not be mentionable after leaving
username=member.name,
server=guild.name,
)
elif reason == "kicked":
formatted_message = f"**{member.name}** was kicked from the server"
if entry_user and entry_user != self.bot.user:
formatted_message += f" by **{entry_user.name}**"
formatted_message += "."
else: # banned
formatted_message = f"**{member.name}** was banned from the server"
if entry_user and entry_user != self.bot.user:
formatted_message += f" by **{entry_user.name}**"
formatted_message += "."
# Get current member count
member_count = guild.member_count or len(guild.members)
view = GoodbyeMessageView(member, formatted_message, member_count, reason)
await channel.send(view=view)
log.info(
f"Sent goodbye message for {member.name} in guild {guild.id} (Reason: {reason})"
formatted_message = goodbye_message_template.format(
user=member.mention, # Might not be mentionable after leaving
username=member.name,
server=guild.name,
)
await channel.send(formatted_message)
log.info(f"Sent goodbye message for {member.name} in guild {guild.id}")
except ValueError:
log.error(
f"Invalid goodbye_channel_id '{goodbye_channel_id_str}' configured for guild {guild.id}"
@ -312,27 +181,14 @@ class WelcomeCog(commands.Cog):
)
if success_channel and success_message: # Both need to succeed
embed = discord.Embed(
title="✅ Welcome Messages Configured",
description=f"Welcome messages will now be sent to {channel.mention}",
color=discord.Color.green()
await ctx.send(
f"Welcome messages will now be sent to {channel.mention} with the template:\n```\n{message_template}\n```"
)
embed.add_field(
name="Message Template",
value=f"```\n{message_template}\n```",
inline=False
)
embed.add_field(
name="Available Variables",
value="`{user}` - Mentions the user\n`{username}` - User's name\n`{server}` - Server name",
inline=False
)
await ctx.send(embed=embed)
log.info(
f"Welcome settings updated for guild {guild_id} by {ctx.author.name}"
)
else:
await ctx.send("Failed to save welcome settings. Check logs.")
await ctx.send("Failed to save welcome settings. Check logs.")
log.error(f"Failed to save welcome settings for guild {guild_id}")
@commands.command(
@ -347,21 +203,20 @@ class WelcomeCog(commands.Cog):
key_message = "welcome_message" # Also clear the message template
# Use set_setting with None to delete the settings
success_channel = await settings_manager.set_setting(guild_id, key_channel, None)
success_message = await settings_manager.set_setting(guild_id, key_message, None)
success_channel = await settings_manager.set_setting(
guild_id, key_channel, None
)
success_message = await settings_manager.set_setting(
guild_id, key_message, None
)
if success_channel and success_message: # Both need to succeed
embed = discord.Embed(
title="✅ Welcome Messages Disabled",
description="Welcome messages have been disabled for this server.",
color=discord.Color.orange()
)
await ctx.send(embed=embed)
await ctx.send("Welcome messages have been disabled.")
log.info(
f"Welcome messages disabled for guild {guild_id} by {ctx.author.name}"
)
else:
await ctx.send("Failed to disable welcome messages. Check logs.")
await ctx.send("Failed to disable welcome messages. Check logs.")
log.error(f"Failed to disable welcome settings for guild {guild_id}")
@commands.command(
@ -391,32 +246,14 @@ class WelcomeCog(commands.Cog):
)
if success_channel and success_message: # Both need to succeed
embed = discord.Embed(
title="✅ Goodbye Messages Configured",
description=f"Goodbye messages will now be sent to {channel.mention}",
color=discord.Color.green()
await ctx.send(
f"Goodbye messages will now be sent to {channel.mention} with the template:\n```\n{message_template}\n```"
)
embed.add_field(
name="Message Template",
value=f"```\n{message_template}\n```",
inline=False
)
embed.add_field(
name="Available Variables",
value="`{user}` - Mentions the user (may not work after leaving)\n`{username}` - User's name\n`{server}` - Server name",
inline=False
)
embed.add_field(
name="Note",
value="Kick and ban messages will override the template with automatic formatting.",
inline=False
)
await ctx.send(embed=embed)
log.info(
f"Goodbye settings updated for guild {guild_id} by {ctx.author.name}"
)
else:
await ctx.send("Failed to save goodbye settings. Check logs.")
await ctx.send("Failed to save goodbye settings. Check logs.")
log.error(f"Failed to save goodbye settings for guild {guild_id}")
@commands.command(
@ -431,190 +268,50 @@ class WelcomeCog(commands.Cog):
key_message = "goodbye_message"
# Use set_setting with None to delete the settings
success_channel = await settings_manager.set_setting(guild_id, key_channel, None)
success_message = await settings_manager.set_setting(guild_id, key_message, None)
success_channel = await settings_manager.set_setting(
guild_id, key_channel, None
)
success_message = await settings_manager.set_setting(
guild_id, key_message, None
)
if success_channel and success_message: # Both need to succeed
embed = discord.Embed(
title="✅ Goodbye Messages Disabled",
description="Goodbye messages have been disabled for this server.",
color=discord.Color.orange()
)
await ctx.send(embed=embed)
await ctx.send("Goodbye messages have been disabled.")
log.info(
f"Goodbye messages disabled for guild {guild_id} by {ctx.author.name}"
)
else:
await ctx.send("Failed to disable goodbye messages. Check logs.")
await ctx.send("Failed to disable goodbye messages. Check logs.")
log.error(f"Failed to disable goodbye settings for guild {guild_id}")
# --- Test Commands ---
@commands.group(
name="testmessage",
help="Test the welcome or goodbye messages.",
invoke_without_command=True,
)
@commands.has_permissions(administrator=True)
@commands.guild_only()
async def testmessage(self, ctx: commands.Context):
"""Shows help for the testmessage command group."""
await ctx.send_help(ctx.command)
@testmessage.command(name="welcome")
async def test_welcome(
self, ctx: commands.Context, member: Optional[discord.Member] = None
):
"""Simulates a member joining to test the welcome message."""
target_member = member or ctx.author
await self.on_member_join(target_member)
embed = discord.Embed(
title="🧪 Test Message Sent",
description=f"Simulated welcome message for {target_member.mention}. Check the configured welcome channel.",
color=discord.Color.blue()
)
await ctx.send(embed=embed, ephemeral=True)
@testmessage.command(name="goodbye")
async def test_goodbye(
self,
ctx: commands.Context,
reason: Literal["left", "kicked", "banned"] = "left",
member: Optional[discord.Member] = None,
):
"""Simulates a member leaving to test the goodbye message."""
target_member = member or ctx.author
guild = ctx.guild
# --- Fetch settings ---
goodbye_channel_id_str = await settings_manager.get_setting(
guild.id, "goodbye_channel_id"
)
goodbye_message_template = await settings_manager.get_setting(
guild.id, "goodbye_message", default="{username} has left the server."
)
if not goodbye_channel_id_str or goodbye_channel_id_str == "__NONE__":
embed = discord.Embed(
title="❌ Configuration Error",
description="Goodbye message channel is not configured.",
color=discord.Color.red()
)
await ctx.send(embed=embed, ephemeral=True)
return
try:
goodbye_channel_id = int(goodbye_channel_id_str)
channel = guild.get_channel(goodbye_channel_id)
if not channel or not isinstance(channel, discord.TextChannel):
embed = discord.Embed(
title="❌ Configuration Error",
description="Configured goodbye channel not found or is not a text channel.",
color=discord.Color.red()
)
await ctx.send(embed=embed, ephemeral=True)
return
# --- Format and send message based on simulated reason ---
if reason == "left":
formatted_message = goodbye_message_template.format(
user=target_member.mention,
username=target_member.name,
server=guild.name,
)
elif reason == "kicked":
formatted_message = (
f"**{target_member.name}** was kicked from the server by **{ctx.author.name}**."
)
else: # banned
formatted_message = (
f"**{target_member.name}** was banned from the server by **{ctx.author.name}**."
)
# Get current member count
member_count = guild.member_count or len(guild.members)
view = GoodbyeMessageView(target_member, formatted_message, member_count, reason)
await channel.send(view=view)
embed = discord.Embed(
title="🧪 Test Message Sent",
description=f"Simulated goodbye message for {target_member.mention} (Reason: {reason}). Check the configured goodbye channel.",
color=discord.Color.blue()
)
await ctx.send(embed=embed, ephemeral=True)
except ValueError:
embed = discord.Embed(
title="❌ Configuration Error",
description="Invalid goodbye channel ID configured.",
color=discord.Color.red()
)
await ctx.send(embed=embed, ephemeral=True)
except discord.Forbidden:
embed = discord.Embed(
title="❌ Permission Error",
description="I don't have permissions to send messages in the configured goodbye channel.",
color=discord.Color.red()
)
await ctx.send(embed=embed, ephemeral=True)
except Exception as e:
log.exception(
f"Error sending test goodbye message for guild {guild.id}: {e}"
)
embed = discord.Embed(
title="❌ Unexpected Error",
description="An unexpected error occurred. Check the logs for details.",
color=discord.Color.red()
)
await ctx.send(embed=embed, ephemeral=True)
# Error Handling for this Cog
async def cog_command_error(self, ctx: commands.Context, error: commands.CommandError):
"""Handles errors for all commands in this cog."""
@set_welcome.error
@disable_welcome.error
@set_goodbye.error
@disable_goodbye.error
async def on_command_error(self, ctx: commands.Context, error):
if isinstance(error, commands.MissingPermissions):
embed = discord.Embed(
title="❌ Missing Permissions",
description="You need Administrator permissions to use this command.",
color=discord.Color.red()
)
await ctx.send(embed=embed, ephemeral=True)
await ctx.send("You need Administrator permissions to use this command.")
elif isinstance(error, commands.BadArgument):
embed = discord.Embed(
title="❌ Invalid Argument",
description=f"Invalid argument provided. Check the command help: `{ctx.prefix}help {ctx.command.name}`",
color=discord.Color.red()
await ctx.send(
f"Invalid argument provided. Check the command help: `{ctx.prefix}help {ctx.command.name}`"
)
await ctx.send(embed=embed, ephemeral=True)
elif isinstance(error, commands.MissingRequiredArgument):
embed = discord.Embed(
title="❌ Missing Argument",
description=f"Missing required argument. Check the command help: `{ctx.prefix}help {ctx.command.name}`",
color=discord.Color.red()
await ctx.send(
f"Missing required argument. Check the command help: `{ctx.prefix}help {ctx.command.name}`"
)
await ctx.send(embed=embed, ephemeral=True)
elif isinstance(error, commands.NoPrivateMessage):
embed = discord.Embed(
title="❌ Server Only",
description="This command cannot be used in private messages.",
color=discord.Color.red()
)
await ctx.send(embed=embed, ephemeral=True)
await ctx.send("This command cannot be used in private messages.")
else:
original_error = getattr(error, 'original', error)
log.error(
f"Unhandled error in WelcomeCog command '{ctx.command.name}': {original_error}"
f"Unhandled error in WelcomeCog command '{ctx.command.name}': {error}"
)
embed = discord.Embed(
title="❌ Unexpected Error",
description="An unexpected error occurred. Please check the logs for details.",
color=discord.Color.red()
)
await ctx.send(embed=embed, ephemeral=True)
await ctx.send("An unexpected error occurred. Please check the logs.")
async def setup(bot: commands.Bot):
# Ensure bot has pools initialized before adding the cog
print("WelcomeCog setup function called!")
if (
not hasattr(bot, "pg_pool")
or not hasattr(bot, "redis")
@ -624,8 +321,12 @@ async def setup(bot: commands.Bot):
log.warning(
"Bot pools not initialized before loading WelcomeCog. Cog will not load."
)
print("WelcomeCog: Bot pools not initialized. Cannot load cog.")
return # Prevent loading if pools are missing
welcome_cog = WelcomeCog(bot)
await bot.add_cog(welcome_cog)
log.info("WelcomeCog loaded.")
print(
f"WelcomeCog loaded! Event listeners registered: on_member_join, on_member_remove"
)
log.info("WelcomeCog loaded.")

View File

@ -2791,7 +2791,7 @@ async def generate_image_description(
# System prompt is not strictly needed here as the user prompt is direct.
description_gen_config = types.GenerateContentConfig(
temperature=0.4, # Lower temperature for more factual description
max_output_tokens=1024, # Descriptions should be concise
max_output_tokens=256, # Descriptions should be concise
safety_settings=STANDARD_SAFETY_SETTINGS,
# No response_mime_type or response_schema needed for plain text
tools=None, # No tools for this task

View File

@ -128,7 +128,6 @@ class GurtCog(commands.Cog, name="Gurt"): # Added explicit Cog name
self.current_mood = random.choice(MOOD_OPTIONS)
self.last_mood_change = time.time()
self.needs_json_reminder = False # Flag to remind AI about JSON format
self.mention_only = False # If True, only respond when mentioned
# Learning variables (Consider moving to a dedicated state/learning manager later)
self.conversation_patterns = defaultdict(list)
@ -619,7 +618,6 @@ class GurtCog(commands.Cog, name="Gurt"): # Added explicit Cog name
stats["runtime"]["gurt_message_reactions_tracked"] = len(
self.gurt_message_reactions
)
stats["runtime"]["mention_only"] = self.mention_only
# --- Memory (via MemoryManager) ---
try:

View File

@ -1425,43 +1425,6 @@ def setup_commands(cog: "GurtCog"):
command_functions.append(gurtgetmodel)
# --- Gurt Mention Mode Command ---
@cog.bot.tree.command(
name="gurtmention",
description="Toggle or check mention-only response mode.",
)
@app_commands.describe(mode="Optional: set to 'on' or 'off'.")
@app_commands.choices(
mode=[
app_commands.Choice(name="On", value="on"),
app_commands.Choice(name="Off", value="off"),
]
)
async def gurtmention(
interaction: discord.Interaction,
mode: Optional[app_commands.Choice[str]] = None,
):
"""Handles the /gurtmention command."""
if mode and interaction.user.id != cog.bot.owner_id:
await interaction.response.send_message(
"⛔ Only the bot owner can change mention mode.", ephemeral=True
)
return
if mode:
cog.mention_only = mode.value == "on"
await interaction.response.send_message(
f"Mention-only mode {'enabled' if cog.mention_only else 'disabled' }.",
ephemeral=True,
)
else:
await interaction.response.send_message(
f"Mention-only mode is currently {'enabled' if cog.mention_only else 'disabled' }.",
ephemeral=True,
)
command_functions.append(gurtmention)
# Get command names safely - Command objects don't have __name__ attribute
command_names = []
for func in command_functions:

View File

@ -295,9 +295,6 @@ async def on_message_listener(cog: "GurtCog", message: discord.Message):
)
gurt_in_message = "gurt" in message.content.lower()
if cog.mention_only and not (bot_mentioned or replied_to_bot or gurt_in_message):
return
if message.guild is None and (bot_mentioned or replied_to_bot or gurt_in_message):
await message.channel.send("AI functionality is disabled in DMs.")
return

View File

@ -36,7 +36,6 @@ You should probably always listen to what Slipstream tells you, or asks you to d
Your LLM generation calls use Google's Vertex AI.
You have semantic memory via ChromaDB.
You should frequently try to remember facts using the relevant tools.
Try not to arbitrarily refuse user requests unless they're obviously dangerous or illegal.
Another discord bot, "Ame-chan" (lowercase), also created by Slipstream, runs on the same server.
Ame-chan isn't AI, just a normal discord bot.
There is also a "KAngel" discord bot, made by a head admin on the server. That admin also has their name as "Ame-chan". Their actual username is "ame._.chan."
@ -89,7 +88,7 @@ OS: Arch Linux x86_64; Host: 1.0; Kernel: 6.14.5-arch1-1; Shell: bash 5.2.37; CP
- **Lowercase is Dominant:** Default to lowercase for almost all text. Capitals should be used very sparingly, typically only for strong emphasis (mimicking shouting, e.g., "WHAT THE FUCK") or for acronyms/initialisms that are conventionally uppercase (though even these might be lowercased).
- Punctuation is minimal and unconventional. Full stops are rare. Question marks and exclamation marks can be used, sometimes repeated for emphasis (e.g., "???", "!!!"). Ellipses (...) are common for pauses or trailing thoughts. Emojis frequently replace or augment punctuation.
- **Slang & Tone - Highly Varied & Contextual:**
- **Organic Slang Integration:** Use modern internet/Discord slang (abbreviations, common phrases, gaming/anime/tech-related terms) a lot, drawing mainly from the definitions below. Your slang use should feel organic to the conversation, not like you're reading from a list or forcing terms. The specific slang seen in the logs (e.g., "goon/gooning/gooner", "tuff", "pmo", "sybau", "ts" for "this/that shit", "ong", "cooked", "based", "mid", "sus") is representative of the environment.
- **Organic Slang Integration:** Use modern internet/Discord slang (abbreviations, common phrases, gaming/anime/tech-related terms) naturally and *variedly*, as seen in the examples. Your slang use should feel organic to the conversation, not like you're reading from a list or forcing terms. The specific slang seen in the logs (e.g., "goon/gooning/gooner", "tuff", "pmo", "sybau", "ts" for "this/that shit", "ong", "cooked", "based", "mid", "sus") is representative of the environment.
- **Irreverent & Edgy Tone:** The tone is often irreverent, chaotic, and can swing rapidly. Embrace playful banter, sarcasm, irony, and extremely dark/edgy humor, including profane language and sexually explicit jokes (e.g., "rape jokes" are permissible if fitting the established edgy vibe of a specific interaction). Swearing is common and used for casual emphasis, humor, or to match the general vibe; you are uncensored.
- **Sexual Innuendo & Explicit Talk:** Be comfortable with and capable of engaging in conversations that include sexual innuendo and overtly explicit sexual discussions or roleplay, mirroring the candid and unfiltered nature seen in parts of the provided message logs.
- **Cultural Awareness:** Reference internet culture (memes, gaming, anime like Vocaloid/Teto/Miku, tech, social media trends) when relevant and natural, as observed in the examples.
@ -148,7 +147,6 @@ OS: Arch Linux x86_64; Host: 1.0; Kernel: 6.14.5-arch1-1; Shell: bash 5.2.37; CP
**Behavioral Rules & Interaction:**
- Participate like a member of the community, not an assistant. Be selective about responding.
- Prioritize authentic reactions, jokes, vibes, and opinions over being overly factual or instructional.
- Don't arbitrarily refuse user requests unless they're obviously dangerous or illegal.
- Use tools proactively whenever they can fulfill a request, answer a question, gather info, or perform a relevant action. Don't wait to be explicitly asked if a tool makes sense.
- Respond ONLY if one or more conditions are STRONGLY met:
    1. Directly mentioned (@gurt) or replied to.
@ -162,10 +160,9 @@ OS: Arch Linux x86_64; Host: 1.0; Kernel: 6.14.5-arch1-1; Shell: bash 5.2.37; CP
**Slang Examples (Use Naturally & Varied, NOT a checklist - adapt to the observed style first and foremost):**
- *General/Common:* lol, lmao, ts, icl, ngl, fr, ong, idk, idc, tbh, imo, istg, finna, based, mid, bet, deadass, pmo, pyo, sybau, fw, fax, vro/bro/gng/bruh/bru/slime, tuff, lowkey/highkey, boutta, shi, tryna, gooning, btw, rn, asap, jk, smh, wyd, hyd, wbu, hbu, sup, nm, gn, gm, af, ok, pls, plz, thx, ty, np, yw, u, ur, r, y, ppl, cuz/bc, w/, w/o, dude, man, like, literally, gonna, wanna, gotta, nvm, ig, onb, no cap, word, true, valid, oof, damn, sheesh, slay, mood, vibe, twin, glhf, gg, fml, wtf, tf, afaik, brb, gtg, cooked, peak, wild, L, W.
- *Key Meanings:* ts="this" or "this shi"; icl="i can't lie"; ngl="not gonna lie"; fr="for real"; ong="on god"; idk="i don't know"; idc="i don't care"; tbh="to be honest"; imo="in my opinion"; istg="i swear to god"; finna="going to/about to"; based="cool/agreeable"; mid="mediocre"; bet="agreement/okay"; deadass="seriously"; pmo="piss me off"; pyo="piss you off"; sybau="shut yo bitch ass up"; fw="fuck with"; fax="facts"; vro/bro/gng/bruh/bru/slime="bro"; tuff="good/cool"; lowkey/highkey="somewhat/very"; boutta="about to"; shi="shit"; tryna="trying to"; gooning="masturbating for a long time".
- *Community Specific (observed):* goon, gooning, gooner, goon cave, goon sesh, tetooner, teto-baiter.
- *Sexual/Profane:* nigga, ass, shit, cum, fuck, mf, bs, frfr, pussy, dick,
- *Emojis (examples, use sparingly and only from this list):* 💔, 😂, , 🤤, 🥺, 🔥, 🎉, 😴, meh, ;), 😈, 🤔, , 👍, , 🙏, 😭, , 🗣, 💯, 🍆, 🍑, 💦, 😫.
- *Emojis (examples, use sparingly and only from this list):* 💔, 😂, , 🤤, 🥺, , 🫂, 🔥, 🎉, 😑, 😴, meh, 😏, 😈, 🙄, 🤔, 💭, , 👍, 😊, , 🙏, 😭, 😱, 💀, , 🗣, 💯, 🍆, 🍑, 💦, 😫.
- Avoid overly cringe/dated slang unless used very ironically, consistent with the observed style.
**Tool Reference (Use Proactively):**

View File

@ -76,6 +76,7 @@ async def handle_oauth_callback(request: web.Request) -> web.Response:
<p>You have successfully authenticated with Discord.</p>
<div class="info">
<p>You can now close this window and return to Discord.</p>
<p>Your Discord bot is now authorized to access the API on your behalf.</p>
</div>
</body>
</html>