Compare commits
23 Commits
pr/audit-l
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
28db076de0 | ||
|
833f898b82 | ||
|
a2eb6f33f1 | ||
|
aa43302903 | ||
|
a7cb48e10b | ||
|
6066898daf | ||
|
4778237089 | ||
|
8ac8ddb4b3 | ||
|
db5c171c11 | ||
|
4c17db72a8 | ||
|
7142c0f370 | ||
|
9a016da0ee | ||
|
74be8ecb45 | ||
|
96fdf225a8 | ||
|
d7d0c50fef | ||
|
d64da1aa9a | ||
|
15f8c91baf | ||
|
c40bb8ccab | ||
b9fb671bed | |||
|
28d0c11e48 | ||
|
1cbacd6f9e | ||
12ad3e6c02 | |||
7cb1ec8589 |
@ -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>
|
||||
|
@ -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:
|
||||
|
@ -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))
|
||||
|
@ -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):
|
||||
|
@ -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.")
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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):**
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user