Compare commits

...

23 Commits

Author SHA1 Message Date
Slipstreamm
28db076de0 refactor(goodbye): Remove unnecessary blank line in GoodbyeMessageView 2025-06-14 14:29:53 -06:00
Slipstreamm
833f898b82 refactor(welcome): Streamline WelcomeMessageView and GoodbyeMessageView
Consolidate multiple `ui.TextDisplay` items into single text blocks for `WelcomeMessageView` and `GoodbyeMessageView`. This improves readability and simplifies the UI structure.
2025-06-14 14:28:02 -06:00
Slipstreamm
a2eb6f33f1 refactor(welcome): Consolidate info displays in welcome/goodbye messages
Group related member information and server statistics into single
TextDisplay components. This streamlines the UI layout, reduces the
number of elements, and potentially improves rendering efficiency.
2025-06-14 14:14:19 -06:00
Slipstreamm
aa43302903 fix(welcome): Log specific ValueError message
Include the specific error details from ValueError when logging issues in the
welcome message process, making debugging easier.
2025-06-14 14:06:45 -06:00
Slipstreamm
a7cb48e10b refactor(welcome): Consolidate welcome and goodbye message sections for improved layout 2025-06-14 14:03:23 -06:00
Slipstreamm
6066898daf feat(welcome): Enhance welcome and goodbye messages
Welcome and goodbye messages now use a more detailed LayoutView with sections for member info, server statistics, and dynamic styling.
- Welcome messages display total server members and the new member's account age.
- Goodbye messages show current member count, the leaving member's join date and time in server, with dynamic colors and titles for leave, kick, and ban events.
- All command feedback for welcome/goodbye configuration, testing, and error handling has been updated to use rich Discord Embeds for improved user experience.
2025-06-14 13:58:24 -06:00
Slipstreamm
4778237089 refactor(welcome): Remove disable_goodbye error handler
Removes the dedicated error handler for the `disable_goodbye` command.
This allows for more centralized or general error handling to take over.
Adds a log message when the WelcomeCog is successfully loaded.
2025-06-14 13:40:54 -06:00
Slipstreamm
8ac8ddb4b3 refactor(welcome): Adjust shared command error handlers 2025-06-14 13:37:23 -06:00
Slipstreamm
db5c171c11 refactor(welcome-cog): Use cog-wide error handler
Migrates individual command error handlers to the cog-wide `cog_command_error` method. This simplifies error handling by automatically catching errors from all commands within the cog.
Adds `ephemeral=True` to error messages sent to the user and logs the `original_error` for better debugging.
2025-06-14 13:35:30 -06:00
Slipstreamm
4c17db72a8 feat(welcome): Enhance welcome and goodbye messages
Integrate `discord.ui.LayoutView` to provide richer, more structured welcome and goodbye messages with user avatars and accent colors.

The `on_member_remove` event now utilizes audit logs to determine if a member was kicked or banned, providing more context in goodbye messages.

Add `testmessage` command group with `welcome` and `goodbye` subcommands to allow administrators to easily test message configurations.

Replace development-time `print` statements with standard `logging` for cleaner output and better debugging.
2025-06-14 13:31:35 -06:00
Slipstreamm
7142c0f370 style(system-check): Remove separator from bot system info 2025-06-14 13:22:02 -06:00
Slipstreamm
9a016da0ee style(system-check-cog): Remove header section from status view 2025-06-14 13:19:58 -06:00
Slipstreamm
74be8ecb45 refactor(system-check): Improve system status view layout
Refactor the system status view to enhance readability and organization.

- Introduce `_create_aligned_block` helper for consistent, aligned text formatting.
- Consolidate bot, system, and hardware information into new aligned blocks.
- Remove redundant separators and improve overall view structure.
- Adjust progress bar length for better fit.
- Optimize user count gathering using a set for unique users.
- Improve Windows version detection and Linux motherboard info fallback.
2025-06-14 13:18:02 -06:00
Slipstreamm
96fdf225a8 feat(system-check): Revamp status command UI with components v2
Overhauls the output of the `/systemcheck` and `!systemcheck` commands using
Discord UI Components V2 for a more modern and readable display.

- Implemented `ui.LayoutView` for structured information presentation.
- Added progress bars for CPU and RAM utilization for quick visual assessment.
- Standardized display of OS, hardware, and bot statistics.
- Separated data collection from UI rendering logic.
- Added `status` alias for the prefix command.
- Improved error handling for system information retrieval to display 'N/A' on failure.
2025-06-14 13:15:28 -06:00
Slipstreamm
d7d0c50fef refactor(oauth): Convert authentication commands to hybrid group
Migrate `auth`, `deauth`, and `authstatus` commands to subcommands of a new `auth` hybrid command group. This results in commands like `auth login`, `auth logout`, and `auth status`.

This change:
- Improves command organization and discoverability.
- Leverages Discord's built-in help system for command groups, making the custom `authhelp` command redundant.
- Prepares for seamless integration with slash commands by using `commands.hybrid_group`.
2025-06-14 12:43:05 -06:00
Slipstreamm
d64da1aa9a refactor(oauth): Refine authentication success messages
Update various success messages and help text related to OAuth. Instead
of broadly stating the bot gains general API access, the language now
specifies that it gains access to "scopes" or "additional OAuth scopes"
as allowed during the authentication flow, providing a more precise
understanding of the permissions granted.
2025-06-14 12:37:55 -06:00
Slipstreamm
15f8c91baf Merge branch 'master' of git.slipstreamm.dev:slipstream/discordbot 2025-06-14 05:00:13 -06:00
Slipstreamm
c40bb8ccab feat(system-check): Display system status using UI views
Refactors the system check command to utilize `discord.ui.LayoutView` instead of traditional embeds for presenting system and bot information. This provides a more structured and visually distinct layout for the status output.
2025-06-14 04:57:20 -06:00
b9fb671bed Require OAuth Nitro verification for giveaways (#14)
Adds OAuth-based Nitro status checks to giveaways and allows excluding Nitro users.

Reviewed-on: #14
Co-authored-by: Slipstream <me@slipstreamm.dev>
Co-committed-by: Slipstream <me@slipstreamm.dev>
2025-06-13 22:09:55 -06:00
Slipstreamm
28d0c11e48 refactor(image-description): Increase max output tokens to 1024
Previously, the maximum output tokens for image descriptions was set to 256.
This was often too restrictive, leading to truncated or incomplete descriptions.
Increasing the limit to 1024 allows for more comprehensive and detailed image
descriptions while still encouraging conciseness.
2025-06-12 19:27:59 -06:00
Slipstreamm
1cbacd6f9e docs(prompt): Refine allowed emoji list
Removes several less common or redundant emojis to streamline the list.
Adds the text-based smiley `;)`, allowing for more natural expression.
2025-06-12 19:27:14 -06:00
12ad3e6c02 Update Gurt slang definitions (#13)
Add slang glossary and refusal guidance

Reviewed-on: #13
Co-authored-by: Slipstream <me@slipstreamm.dev>
Co-committed-by: Slipstream <me@slipstreamm.dev>
2025-06-12 18:49:48 -06:00
7cb1ec8589 Add mention-only mode toggle (#12)
Adds ability to restrict Gurt responses to direct mentions

Reviewed-on: #12
Co-authored-by: Slipstream <me@slipstreamm.dev>
Co-committed-by: Slipstream <me@slipstreamm.dev>
2025-06-12 11:15:10 -06:00
11 changed files with 759 additions and 359 deletions

View File

@ -1183,7 +1183,6 @@ 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,16 +9,35 @@ 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:
"""Checks if a user has an animated avatar or a banner, indicating Nitro."""
"""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
# Fetch the full user object to get banner information
if bot:
try:
@ -59,10 +78,23 @@ class GiveawayEnterButton(ui.Button["GiveawayEnterView"]):
await interaction.message.edit(view=self.view)
return
if giveaway["is_nitro_giveaway"]:
if not await is_user_nitro_like(interaction.user, bot=self.cog.bot):
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:
await interaction.response.send_message(
"This is a Nitro-exclusive giveaway. You don't appear to have Nitro (animated avatar or banner).",
"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.",
ephemeral=True,
)
return
@ -150,11 +182,16 @@ class GiveawayRerollButton(ui.Button["GiveawayEndView"]):
except discord.NotFound:
continue # Skip if user cannot be found
if user and not user.bot:
# 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
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
entrants_users.append(user)
if not entrants_users:
@ -228,6 +265,7 @@ 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
@ -325,6 +363,7 @@ 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
@ -432,7 +471,8 @@ 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? (checks for animated avatar/banner)",
nitro_giveaway="Is this a Nitro-only giveaway? (OAuth verification)",
exclude_nitro="Exclude Nitro users from entering?",
)
@app_commands.checks.has_permissions(manage_guild=True)
async def create_giveaway_slash(
@ -442,6 +482,7 @@ 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)
@ -469,6 +510,8 @@ 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
@ -488,6 +531,7 @@ 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)
@ -559,10 +603,16 @@ class GiveawaysCog(commands.Cog, name="Giveaways"):
if user_to_check.bot:
continue
if giveaway_data["is_nitro_giveaway"] and not is_user_nitro_like(
user_to_check
if giveaway_data["is_nitro_giveaway"] or giveaway_data.get(
"exclude_nitro_users"
):
continue # Skip non-nitro users for nitro giveaways
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
entrants_users.append(user_to_check)
winners_list = []
@ -685,10 +735,16 @@ 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", False
) and not is_user_nitro_like(user):
continue
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
entrants.add(user)
if not entrants:
await interaction.followup.send(
@ -704,8 +760,23 @@ class GiveawaysCog(commands.Cog, name="Giveaways"):
reaction_found = True
async for user in reaction.users():
if not user.bot:
# For manual reaction roll, we might not know if it was nitro_giveaway
# Consider adding a parameter to manual_roll for this if needed
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
entrants.add(user)
break
if not reaction_found:

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

View File

@ -1,261 +1,267 @@
import discord
from discord.ext import commands
from discord import app_commands
from discord import app_commands, ui
import time
import psutil
import platform
import GPUtil
import distro # Ensure this is installed
import distro
# 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 _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."""
async def _build_system_check_view(self, context_or_interaction) -> SystemStatusView:
"""Gathers all system data and returns the constructed view."""
# Bot information
bot_user = self.bot.user
guild_count = len(self.bot.guilds)
# 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}")
# 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}
user_count = len(user_ids)
# System information
system = platform.system()
os_info = f"{system} {platform.release()}"
hostname = platform.node()
distro_info_str = "" # Renamed variable
distro_info_str = ""
if system == "Linux":
try:
# Use distro library for better Linux distribution detection
distro_name = distro.name(pretty=True)
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})"
distro_info_str = f" ({distro_name})"
except Exception:
distro_info_str = "" # Fail silently
elif system == "Windows":
# Add Windows version details if possible
try:
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
# 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, remainder = divmod(uptime_seconds, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
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"
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
# Hardware information
cpu_usage = psutil.cpu_percent(interval=0.1)
# Get CPU info with a timeout to prevent hanging
try:
# 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"
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"
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 as e:
print(f"Error getting CPU info: {e}")
except Exception:
cpu_name = "N/A"
# Get motherboard information
motherboard_info = self._get_motherboard_info()
memory = psutil.virtual_memory()
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 = []
# GPU Information
try:
gpus = GPUtil.getGPUs()
if 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)"
)
# Format multi-GPU info on new lines for readability
gpu_info_lines = [f"{gpu.name} ({gpu.load*100:.1f}% Load)" for gpu in gpus]
gpu_info = "\n".join(gpu_info_lines)
else:
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}"
gpu_info = "No dedicated GPU detected"
except Exception:
gpu_info = "N/A"
# 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
# Determine user based on context type
user = context_or_interaction.author if isinstance(context_or_interaction, commands.Context) else context_or_interaction.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,
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,
)
# 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):
def _get_motherboard_info(self) -> str:
"""Get motherboard information based on the operating system."""
system = platform.system()
try:
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"
if system == "Windows" and WMI_AVAILABLE:
w = wmi.WMI()
board = w.Win32_BaseBoard()[0]
return f"{board.Manufacturer} {board.Product}"
elif system == "Linux":
# Read motherboard product name from sysfs
# Check for product_name first, then fallback to board_name
try:
with open("/sys/devices/virtual/dmi/id/product_name", "r") as f:
product_name = f.read().strip()
return product_name if product_name else "Unknown motherboard"
return f.read().strip()
except FileNotFoundError:
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"
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)
async def setup(bot):

View File

@ -1,68 +1,152 @@
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:
@ -72,21 +156,23 @@ 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
)
await channel.send(formatted_message)
# 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)
log.info(f"Sent welcome message for {member.name} in guild {guild.id}")
except ValueError:
except ValueError as e:
log.error(
f"Invalid welcome_channel_id '{welcome_channel_id_str}' configured for guild {guild.id}"
f"ValueError in WelcomeCog for guild {guild.id}: {e}"
)
except discord.Forbidden:
log.error(
@ -96,32 +182,23 @@ 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."""
print(f"WelcomeCog: on_member_remove event triggered for {member.name}")
"""Sends a goodbye message when a member leaves, is kicked, or is banned."""
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:
@ -133,15 +210,69 @@ class WelcomeCog(commands.Cog):
)
return
# --- Format and send message ---
formatted_message = goodbye_message_template.format(
user=member.mention, # Might not be mentionable after leaving
username=member.name,
server=guild.name,
)
# --- Determine reason for leaving ---
reason = "left"
entry_user = None
await channel.send(formatted_message)
log.info(f"Sent goodbye message for {member.name} in guild {guild.id}")
# 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})"
)
except ValueError:
log.error(
@ -181,14 +312,27 @@ class WelcomeCog(commands.Cog):
)
if success_channel and success_message: # Both need to succeed
await ctx.send(
f"Welcome messages will now be sent to {channel.mention} with the template:\n```\n{message_template}\n```"
embed = discord.Embed(
title="✅ Welcome Messages Configured",
description=f"Welcome messages will now be sent to {channel.mention}",
color=discord.Color.green()
)
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(
@ -203,20 +347,21 @@ 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
await ctx.send("Welcome messages have been disabled.")
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)
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(
@ -246,14 +391,32 @@ class WelcomeCog(commands.Cog):
)
if success_channel and success_message: # Both need to succeed
await ctx.send(
f"Goodbye messages will now be sent to {channel.mention} with the template:\n```\n{message_template}\n```"
embed = discord.Embed(
title="✅ Goodbye Messages Configured",
description=f"Goodbye messages will now be sent to {channel.mention}",
color=discord.Color.green()
)
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(
@ -268,50 +431,190 @@ 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
await ctx.send("Goodbye messages have been disabled.")
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)
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
@set_welcome.error
@disable_welcome.error
@set_goodbye.error
@disable_goodbye.error
async def on_command_error(self, ctx: commands.Context, error):
async def cog_command_error(self, ctx: commands.Context, error: commands.CommandError):
"""Handles errors for all commands in this cog."""
if isinstance(error, commands.MissingPermissions):
await ctx.send("You need Administrator permissions to use this command.")
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)
elif isinstance(error, commands.BadArgument):
await ctx.send(
f"Invalid argument provided. Check the command help: `{ctx.prefix}help {ctx.command.name}`"
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(embed=embed, ephemeral=True)
elif isinstance(error, commands.MissingRequiredArgument):
await ctx.send(
f"Missing required argument. Check the command help: `{ctx.prefix}help {ctx.command.name}`"
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(embed=embed, ephemeral=True)
elif isinstance(error, commands.NoPrivateMessage):
await ctx.send("This command cannot be used in private messages.")
else:
log.error(
f"Unhandled error in WelcomeCog command '{ctx.command.name}': {error}"
embed = discord.Embed(
title="❌ Server Only",
description="This command cannot be used in private messages.",
color=discord.Color.red()
)
await ctx.send("An unexpected error occurred. Please check the logs.")
await ctx.send(embed=embed, ephemeral=True)
else:
original_error = getattr(error, 'original', error)
log.error(
f"Unhandled error in WelcomeCog command '{ctx.command.name}': {original_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)
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")
@ -321,12 +624,8 @@ 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)
print(
f"WelcomeCog loaded! Event listeners registered: on_member_join, on_member_remove"
)
log.info("WelcomeCog loaded.")
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=256, # Descriptions should be concise
max_output_tokens=1024, # 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,6 +128,7 @@ 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)
@ -618,6 +619,7 @@ 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,6 +1425,43 @@ 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,6 +295,9 @@ 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,6 +36,7 @@ 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."
@ -88,7 +89,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) 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.
- **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.
- **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.
@ -147,6 +148,7 @@ 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.
@ -160,9 +162,10 @@ 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,7 +76,6 @@ 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>