Compare commits

...

7 Commits

Author SHA1 Message Date
TheOnlyMace
4f57cccea5 Add Discord sharding support and related configurations
Some checks failed
CI / Bot (Python) (push) Successful in 12s
CI / Dashboard (Next.js) (push) Failing after 9s
Enhance the bot's architecture by introducing sharding capabilities, allowing for better scalability and performance. Update environment files to include sharding options, and modify the bot's core logic to handle shard connections and latencies. Implement shard-specific logging and API synchronization, ensuring that only the primary shard performs certain tasks. Update API schemas and routes to reflect shard information, improving the overall status reporting and monitoring of the bot's performance across multiple shards.
2026-07-21 23:09:35 +02:00
TheOnlyMace
36b32de34c Enhance AutoMod configuration by introducing event-specific settings and thresholds. Refactor Automod schemas to include event configurations, thresholds, and limits for ignored roles and channels. Update database interactions to support new structure and improve handling of AutoMod rules in cogs, ensuring dynamic thresholds for various events. Update dashboard components to accommodate new configurations and improve user experience in managing AutoMod settings.
Some checks failed
CI / Bot (Python) (push) Successful in 12s
CI / Dashboard (Next.js) (push) Failing after 9s
2026-07-21 23:05:05 +02:00
TheOnlyMace
d24b810164 Refactor command definitions across various cogs to replace hybrid commands with regular commands for improved consistency and clarity. Update command structures in moderation, music, and other cogs to enhance user experience and streamline command handling.
Some checks failed
CI / Bot (Python) (push) Successful in 12s
CI / Dashboard (Next.js) (push) Failing after 9s
2026-07-21 22:59:25 +02:00
TheOnlyMace
a52d84467d Refactor slash command synchronization logic to prevent duplicate commands in Discord. Update environment variables for command sync modes and enhance command clearing functionality. Improve command registration handling in the bot to streamline user experience.
Some checks failed
CI / Bot (Python) (push) Successful in 12s
CI / Dashboard (Next.js) (push) Failing after 9s
2026-07-21 22:53:51 +02:00
TheOnlyMace
1da2085087 Refactor command definitions across multiple cogs to replace hybrid commands with regular commands for consistency. This change enhances clarity and standardizes command handling throughout the bot.
Some checks failed
CI / Bot (Python) (push) Successful in 12s
CI / Dashboard (Next.js) (push) Failing after 8s
2026-07-21 22:50:11 +02:00
TheOnlyMace
20193404e8 Refactor command definitions in owner and moderation cogs for consistency. Change getinvite command to a regular command and update type hints for parameters in enlarge and roleicon commands to improve clarity and type safety.
Some checks failed
CI / Bot (Python) (push) Successful in 12s
CI / Dashboard (Next.js) (push) Failing after 8s
2026-07-21 22:47:29 +02:00
TheOnlyMace
299351463c Refactor welcome message handling in the Autorole cog to enhance user experience. Implement dynamic embed creation for guild join events, including updated branding and links to the dashboard and support server. Improve error handling for direct messages and audit log access.
Some checks failed
CI / Bot (Python) (push) Failing after 12s
CI / Dashboard (Next.js) (push) Failing after 9s
2026-07-21 22:45:50 +02:00
92 changed files with 1844 additions and 528 deletions

View File

@@ -7,6 +7,12 @@ brand_name=Axiom
OWNER_IDS=YOUR_DISCORD_USER_ID_HERE
DASHBOARD_ADMIN_IDS=YOUR_DISCORD_USER_ID_HERE
# Sharding — leave unset for auto (recommended). Multi-process: same TOKEN, shared bot-db volume.
# SHARD_COUNT=
# SHARD_IDS=0,1
# SHARD_PRIMARY=
# Enable API only on the primary shard process.
# Shared secret — must match between bot API and dashboard
DASHBOARD_API_KEY=generate_a_long_random_secret_here
@@ -19,9 +25,9 @@ LAVALINK_PORT=13592
EMOJI_SYNC=false
JISHAKU_ENABLED=false
# Slash (/) command sync — guild sync makes commands appear immediately
SLASH_SYNC_GLOBAL=true
SLASH_SYNC_ALL_GUILDS=true
# Slash (/) command sync — use ONE mode to avoid duplicate commands in Discord
SLASH_SYNC_MODE=global
SLASH_CLEAR_GUILD_COMMANDS=true
# SLASH_GUILD_IDS=
# Traefik (optional override — default in compose: letsencrypt)

View File

@@ -9,6 +9,16 @@ NEXT_PUBLIC_BRAND_NAME='Axiom'
# BOT_INSTANCE_ID=prod-a
# API_PORT=8001
# ── Discord sharding (optional) ───────────────────────────────────────────────
# Default (all unset): AutoShardedBot asks Discord for recommended shard count (one process).
# Multi-process cluster: same TOKEN + shared DB volume; set SHARD_COUNT + SHARD_IDS per process.
# API / slash sync / emoji sync only on the primary (shard 0 in SHARD_IDS, or SHARD_PRIMARY=true).
# SHARD_COUNT=
# SHARD_IDS=0,1
# SHARD_PRIMARY=
# Only enable API on the primary process:
# API_ENABLED=true
# ── Owner / Staff IDs (REQUIRED — comma-separated Discord user IDs) ───────────
# Set your own Discord user ID(s). No foreign IDs are used as fallback.
OWNER_IDS=YOUR_DISCORD_USER_ID_HERE
@@ -27,11 +37,12 @@ LAVALINK_PASSWORD="youshallnotpass"
LAVALINK_SECURE="false"
LAVALINK_PORT="13592"
SLASH_SYNC_GLOBAL=true
# Instant / commands in every connected server (set false if you have many guilds and hit rate limits)
SLASH_SYNC_ALL_GUILDS=true
# Or limit instant sync to specific guild IDs (comma-separated). Overrides ALL_GUILDS when set.
SLASH_SYNC_MODE=global
# global = one set worldwide (default, no duplicates)
# guild = instant only in connected servers (set SLASH_GUILD_IDS or rely on all guilds)
# SLASH_GUILD_IDS=
# Clears old per-server copies that caused duplicate /commands in Discord
SLASH_CLEAR_GUILD_COMMANDS=true
# ── API / Dashboard Backend ───────────────────────────────────────────────────
API_ENABLED="false"

View File

@@ -96,6 +96,17 @@ async def on_ready():
print(f"Logged in as: {client.user}")
print(f"Connected to: {len(client.guilds)} guilds")
print(f"Connected to: {len(client.users)} users")
shard_count = client.shard_count or 1
shard_ids = list(client.shards.keys()) if client.shards else [0]
print(f"Shards: {shard_count} (this process: {shard_ids})")
print(f"Shard primary: {IS_SHARD_PRIMARY}")
if client.latencies:
for sid, lat in client.latencies:
print(f" Shard {sid}: {round(lat * 1000)}ms")
if not IS_SHARD_PRIMARY:
print("\033[33m◈ Non-primary shard process — skipping API-side sync tasks\033[0m")
return
# Sync application emojis on startup
await run_sync(TOKEN)
@@ -112,15 +123,38 @@ async def on_ready():
client.loop.create_task(update_stats())
@client.event
async def on_shard_connect(shard_id: int):
print(f"\033[32m◈ Shard {shard_id}: connected\033[0m")
@client.event
async def on_shard_ready(shard_id: int):
print(f"\033[32m◈ Shard {shard_id}: ready\033[0m")
@client.event
async def on_shard_disconnect(shard_id: int):
print(f"\033[33m◈ Shard {shard_id}: disconnected\033[0m")
@client.event
async def on_shard_resumed(shard_id: int):
print(f"\033[32m◈ Shard {shard_id}: resumed\033[0m")
@client.event
async def on_guild_join(guild: discord.Guild):
# Instant slash commands in the new server
try:
from utils.slash_sync import sync_guild
synced = await sync_guild(client, guild, copy_global=True)
print(f"◈ Slash sync for new guild {guild.id}: {len(synced)} command(s)")
except Exception as e:
print(f"Slash sync on guild join failed: {e}")
# Only guild-mode needs per-server registration; global mode must not
# copy commands into the guild or Discord shows duplicates (/afk twice).
if IS_SHARD_PRIMARY:
try:
from utils.slash_sync import _sync_mode, sync_guild
if _sync_mode() == "guild":
synced = await sync_guild(client, guild, copy_global=True)
print(f"◈ Slash sync for new guild {guild.id}: {len(synced)} command(s)")
except Exception as e:
print(f"Slash sync on guild join failed: {e}")
if not LOG_CHANNEL_ID:
return
@@ -163,7 +197,7 @@ async def on_command_completion(context: commands.Context) -> None:
# --- Utility Commands ---
@client.hybrid_command(name='spotify')
@client.command(name='spotify')
async def spotify(ctx: Context, user: discord.Member = None):
"""Shows what a user is listening to on Spotify."""
user = user or ctx.author
@@ -184,7 +218,7 @@ async def spotify(ctx: Context, user: discord.Member = None):
await ctx.send(embed=embed)
@client.hybrid_command(name='makeinvite', aliases=['createinvite', 'makeinv'])
@client.command(name='makeinvite', aliases=['createinvite', 'makeinv'])
@commands.is_owner()
async def make_invite(ctx: Context, guild_id: int = None):
"""Creates an invite for a specified server (owner only)."""
@@ -214,7 +248,7 @@ async def make_invite(ctx: Context, guild_id: int = None):
# --- Webhook Management Commands ---
@client.hybrid_command(name='create_hook', aliases=['makehook'])
@client.command(name='create_hook', aliases=['makehook'])
@commands.has_permissions(administrator=True)
async def create_hook(ctx: Context, *, name: str = None):
"""Creates a webhook in the current channel."""
@@ -236,7 +270,7 @@ async def create_hook(ctx: Context, *, name: str = None):
await ctx.send(f"Webhook created: **{webhook.name}**\n||{webhook.url}||\n(I could not DM you the URL.)")
@client.hybrid_command(name='delete_hook', aliases=['delhook'])
@client.command(name='delete_hook', aliases=['delhook'])
@commands.has_permissions(administrator=True)
async def delete_hook(ctx: Context, webhook_url: str = None):
"""Deletes a webhook using its URL."""
@@ -252,7 +286,7 @@ async def delete_hook(ctx: Context, webhook_url: str = None):
await ctx.send(f"{ERROR} Webhook not found or URL is invalid.")
@client.hybrid_command(name='list_hooks', aliases=['hooks'])
@client.command(name='list_hooks', aliases=['hooks'])
@commands.has_permissions(administrator=True)
async def list_hooks(ctx: Context):
"""Lists all webhooks in the current channel."""
@@ -270,7 +304,7 @@ async def list_hooks(ctx: Context):
# --- Game Command ---
@client.hybrid_command()
@client.command()
async def reaction(ctx: Context):
"""See how fast you can react to the correct emoji."""
emojis = ["🍪", "🎉", "🧋", "🍒", "🍑", "💸", "🌙", "💕"]
@@ -326,6 +360,9 @@ def run_api():
uvicorn.run(fastapi_app, host=API_HOST, port=API_PORT, log_level="warning")
def keep_alive():
if not IS_SHARD_PRIMARY:
print(f"\033[33m◈ API Server: Skipped (non-primary shard process)\033[0m")
return
if not API_ENABLED:
print(f"\033[33m◈ API Server: Disabled via API_ENABLED=false\033[0m")
return
@@ -339,7 +376,10 @@ keep_alive()
# --- Cloudflare Tunnel (HTTPS for API) — only when explicitly enabled ---
from utils.tunnel import start_tunnel
start_tunnel()
if IS_SHARD_PRIMARY:
start_tunnel()
else:
print("\033[33m◈ Cloudflare Tunnel: Skipped (non-primary shard process)\033[0m")
# --- Main Bot Execution ---
async def main():

View File

@@ -87,7 +87,16 @@ async def get_admin_stats(bot: "Axiom" = Depends(get_bot)):
AdminNodeStatus(
name="Auth Sockets",
status="Healthy",
load=f"Shard: {bot.shard_count} | Latency: {round(bot.latency * 1000)}ms",
load=(
f"Shards: {bot.shard_count or 1} | "
f"Latency: {round(bot.latency * 1000)}ms"
+ (
" | "
+ ", ".join(f"s{sid}:{round(lat * 1000)}ms" for sid, lat in bot.latencies)
if bot.latencies and len(bot.latencies) > 1
else ""
)
),
icon="Lock"
)
]

View File

@@ -28,13 +28,20 @@ async def get_status(bot: "Axiom" = Depends(get_bot)):
"""
Returns the live status of the bot.
"""
shard_ids = list(bot.shards.keys()) if bot.shards else ([0] if bot.shard_count else [])
shard_latencies = [
{"id": sid, "latency_ms": round(lat * 1000, 2)}
for sid, lat in (bot.latencies or [])
]
return BotStatus(
user=str(bot.user),
id=str(bot.user.id) if bot.user else None,
latency=bot.latency * 1000,
guild_count=len(bot.guilds),
user_count=sum(g.member_count or 0 for g in bot.guilds),
shards=bot.shard_count
shards=bot.shard_count,
shard_ids=shard_ids,
shard_latencies=shard_latencies,
)
@router.get("/info", response_model=BotInfo, summary="Get bot info", description="Returns general information about the bot including command count and user reach.")

View File

@@ -17,7 +17,7 @@ from api.dependencies import get_bot, limiter
from api.discord_auth import get_manageable_guild_ids, get_discord_headers
from api.db_manager import db_manager
from api.schemas import (
GuildSummary, GuildDetails, PrefixConfig, AutomodConfig,
GuildSummary, GuildDetails, PrefixConfig, AutomodConfig, AutomodEventConfig,
TicketConfig, LevelingConfig, LoggingConfig, TicketEmbed,
TicketCategory, LevelingEmbedStyle, PrefixUpdate,
AutomodUpdate, LevelingUpdate, LoggingUpdate, TicketUpdate,
@@ -30,11 +30,23 @@ from api.schemas import (
RRConfig, RRUpdate, ReactionRoleEntry,
InviteStat, InvitesLeaderboard
)
from utils.automod_settings import (
EVENT_ID_TO_DB,
THRESHOLD_META,
MAX_IGNORED,
NSFW_KEYWORDS,
normalize_event_id,
normalize_punishment,
ensure_thresholds_table,
get_all_thresholds,
set_thresholds,
)
from typing import TYPE_CHECKING, List, Optional
import math
import aiosqlite
import json
import os
import discord
if TYPE_CHECKING:
from core.axiom import Axiom
@@ -121,78 +133,264 @@ async def get_guild_automod(guild_id: int):
Retrieves the AutoMod configuration for a specific guild.
"""
db = await db_manager.get_connection(db_path('automod.db'))
# Check enabled status
await ensure_thresholds_table(db)
cursor = await db.execute("SELECT enabled FROM automod WHERE guild_id = ?", (guild_id,))
enabled_row = await cursor.fetchone()
enabled = bool(enabled_row[0]) if enabled_row else False
# Get punishments
cursor = await db.execute("SELECT event, punishment FROM automod_punishments WHERE guild_id = ?", (guild_id,))
punishments = {row[0]: row[1] for row in await cursor.fetchall()}
cursor = await db.execute(
"SELECT event, punishment FROM automod_punishments WHERE guild_id = ?",
(guild_id,),
)
raw_punishments = {row[0]: row[1] for row in await cursor.fetchall()}
# Get ignored items
cursor = await db.execute("SELECT type, id FROM automod_ignored WHERE guild_id = ?", (guild_id,))
events: dict[str, AutomodEventConfig] = {}
punishments_out: dict[str, str] = {}
for event_id, db_name in EVENT_ID_TO_DB.items():
present = db_name in raw_punishments
pun = raw_punishments.get(db_name)
if event_id == "anti_nsfw_link":
events[event_id] = AutomodEventConfig(
enabled=present,
punishment="block_message",
readonly_punishment=True,
)
if present:
punishments_out[event_id] = "block_message"
else:
norm = normalize_punishment(pun) if present else None
events[event_id] = AutomodEventConfig(
enabled=present,
punishment=norm or ("Mute" if present else None),
readonly_punishment=False,
)
if present and norm:
punishments_out[event_id] = norm
cursor = await db.execute(
"SELECT type, id FROM automod_ignored WHERE guild_id = ?", (guild_id,)
)
ignored_items = await cursor.fetchall()
ignored_roles = [row[1] for row in ignored_items if row[0] == 'role']
ignored_channels = [row[1] for row in ignored_items if row[0] == 'channel']
ignored_roles = [row[1] for row in ignored_items if row[0] == "role"]
ignored_channels = [row[1] for row in ignored_items if row[0] == "channel"]
# Get logging channel
cursor = await db.execute("SELECT log_channel FROM automod_logging WHERE guild_id = ?", (guild_id,))
cursor = await db.execute(
"SELECT log_channel FROM automod_logging WHERE guild_id = ?", (guild_id,)
)
logging_row = await cursor.fetchone()
logging_channel = logging_row[0] if logging_row else None
thresholds = await get_all_thresholds(guild_id)
return AutomodConfig(
guild_id=guild_id,
enabled=enabled,
punishments=punishments,
punishments=punishments_out,
events=events,
thresholds=thresholds,
threshold_meta=THRESHOLD_META,
ignored_roles=ignored_roles,
ignored_channels=ignored_channels,
logging_channel=logging_channel
logging_channel=logging_channel,
limits={"max_ignored_roles": MAX_IGNORED, "max_ignored_channels": MAX_IGNORED},
)
async def _sync_nsfw_rule(
bot: "Axiom",
guild_id: int,
*,
enabled: bool,
ignored_roles: list[int],
ignored_channels: list[int],
) -> None:
guild = bot.get_guild(guild_id)
if not guild:
return
existing = None
try:
rules = await guild.fetch_automod_rules()
existing = next((r for r in rules if r.name == "Anti NSFW Links"), None)
except (discord.Forbidden, discord.HTTPException):
return
if not enabled:
if existing:
try:
await existing.delete(reason="Automod NSFW disabled via dashboard")
except (discord.Forbidden, discord.HTTPException):
pass
return
exempt_roles = [discord.Object(i) for i in ignored_roles[:MAX_IGNORED]]
exempt_channels = [discord.Object(i) for i in ignored_channels[:MAX_IGNORED]]
try:
if existing:
await existing.edit(
exempt_roles=exempt_roles,
exempt_channels=exempt_channels,
enabled=True,
reason="Automod NSFW updated via dashboard",
)
else:
await guild.create_automod_rule(
name="Anti NSFW Links",
event_type=discord.AutoModRuleEventType.message_send,
trigger=discord.AutoModTrigger(
type=discord.AutoModRuleTriggerType.keyword,
keyword_filter=NSFW_KEYWORDS,
),
actions=[
discord.AutoModRuleAction(type=discord.AutoModRuleActionType.block_message),
],
enabled=True,
exempt_roles=exempt_roles,
exempt_channels=exempt_channels,
reason="Automod NSFW enabled via dashboard",
)
except (discord.Forbidden, discord.HTTPException) as e:
print(f"Automod NSFW sync error for guild {guild_id}: {e}")
@router.patch("/{guild_id}/automod", summary="Update AutoMod config", description="Partially updates the AutoMod configuration components.")
async def patch_guild_automod(guild_id: int, data: AutomodUpdate):
async def patch_guild_automod(
guild_id: int,
data: AutomodUpdate,
bot: "Axiom" = Depends(get_bot),
):
"""
Updates parts of the AutoMod configuration for a specific guild.
"""
db = await db_manager.get_connection(db_path('automod.db'))
await ensure_thresholds_table(db)
if data.enabled is not None:
await db.execute(
"INSERT OR REPLACE INTO automod (guild_id, enabled) VALUES (?, ?)",
(guild_id, 1 if data.enabled else 0)
(guild_id, 1 if data.enabled else 0),
)
if data.punishments is not None:
for event, punishment in data.punishments.items():
await db.execute(
"INSERT OR REPLACE INTO automod_punishments (guild_id, event, punishment) VALUES (?, ?, ?)",
(guild_id, event, punishment)
)
# Build canonical enabled events from events{} and/or legacy punishments{}
events_payload = data.events
if events_payload is None and data.punishments is not None:
events_payload = {}
for raw_key, raw_pun in data.punishments.items():
eid = normalize_event_id(raw_key)
if not eid:
continue
if eid == "anti_nsfw_link":
events_payload[eid] = AutomodEventConfig(
enabled=True, punishment="block_message", readonly_punishment=True
)
else:
pun = normalize_punishment(raw_pun) or "Mute"
events_payload[eid] = AutomodEventConfig(enabled=True, punishment=pun)
if data.ignored_roles is not None:
await db.execute("DELETE FROM automod_ignored WHERE guild_id = ? AND type = 'role'", (guild_id,))
for role_id in data.ignored_roles:
nsfw_enabled: bool | None = None
if events_payload is not None:
await db.execute(
"DELETE FROM automod_punishments WHERE guild_id = ?", (guild_id,)
)
for raw_id, cfg in events_payload.items():
eid = normalize_event_id(raw_id)
if not eid or not cfg.enabled:
if eid == "anti_nsfw_link":
nsfw_enabled = False
continue
db_name = EVENT_ID_TO_DB[eid]
if eid == "anti_nsfw_link":
await db.execute(
"INSERT INTO automod_punishments (guild_id, event, punishment) VALUES (?, ?, ?)",
(guild_id, db_name, "Block message"),
)
nsfw_enabled = True
else:
pun = normalize_punishment(cfg.punishment) or "Mute"
await db.execute(
"INSERT INTO automod_punishments (guild_id, event, punishment) VALUES (?, ?, ?)",
(guild_id, db_name, pun),
)
# Full-replace: omitted/disabled NSFW must drop the Discord rule
if nsfw_enabled is None:
nsfw_enabled = False
if data.thresholds is not None:
await set_thresholds(db, guild_id, data.thresholds)
ignored_roles = data.ignored_roles
ignored_channels = data.ignored_channels
if ignored_roles is not None:
ignored_roles = list(dict.fromkeys(ignored_roles))[:MAX_IGNORED]
await db.execute(
"DELETE FROM automod_ignored WHERE guild_id = ? AND type = 'role'",
(guild_id,),
)
for role_id in ignored_roles:
await db.execute(
"INSERT OR REPLACE INTO automod_ignored (guild_id, type, id) VALUES (?, 'role', ?)",
(guild_id, role_id)
(guild_id, role_id),
)
if data.ignored_channels is not None:
await db.execute("DELETE FROM automod_ignored WHERE guild_id = ? AND type = 'channel'", (guild_id,))
for channel_id in data.ignored_channels:
if ignored_channels is not None:
ignored_channels = list(dict.fromkeys(ignored_channels))[:MAX_IGNORED]
await db.execute(
"DELETE FROM automod_ignored WHERE guild_id = ? AND type = 'channel'",
(guild_id,),
)
for channel_id in ignored_channels:
await db.execute(
"INSERT OR REPLACE INTO automod_ignored (guild_id, type, id) VALUES (?, 'channel', ?)",
(guild_id, channel_id)
(guild_id, channel_id),
)
if data.logging_channel is not None:
if data.clear_logging:
await db.execute(
"INSERT OR REPLACE INTO automod_logging (guild_id, log_channel) VALUES (?, ?)",
(guild_id, data.logging_channel)
"DELETE FROM automod_logging WHERE guild_id = ?", (guild_id,)
)
elif data.logging_channel is not None:
if data.logging_channel == 0:
await db.execute(
"DELETE FROM automod_logging WHERE guild_id = ?", (guild_id,)
)
else:
await db.execute(
"INSERT OR REPLACE INTO automod_logging (guild_id, log_channel) VALUES (?, ?)",
(guild_id, data.logging_channel),
)
await db.commit()
# Resolve current ignore lists for NSFW sync if not in this payload
if ignored_roles is None or ignored_channels is None:
cursor = await db.execute(
"SELECT type, id FROM automod_ignored WHERE guild_id = ?", (guild_id,)
)
items = await cursor.fetchall()
if ignored_roles is None:
ignored_roles = [r[1] for r in items if r[0] == "role"]
if ignored_channels is None:
ignored_channels = [r[1] for r in items if r[0] == "channel"]
if nsfw_enabled is None and (data.ignored_roles is not None or data.ignored_channels is not None):
# Keep Discord rule exempts in sync when ignores change
cursor = await db.execute(
"SELECT 1 FROM automod_punishments WHERE guild_id = ? AND event = ?",
(guild_id, EVENT_ID_TO_DB["anti_nsfw_link"]),
)
nsfw_enabled = bool(await cursor.fetchone())
if nsfw_enabled is not None:
await _sync_nsfw_rule(
bot,
guild_id,
enabled=nsfw_enabled,
ignored_roles=ignored_roles or [],
ignored_channels=ignored_channels or [],
)
return {"status": "success", "guild_id": guild_id}
@router.get("/{guild_id}/tickets", response_model=TicketConfig, summary="Get Ticket config", description="Retrieves the support ticket system setup, categories, and staff roles.")

View File

@@ -12,7 +12,7 @@
# ╚══════════════════════════════════════════════════════════════════╝
from pydantic import BaseModel, HttpUrl
from typing import Dict, List, Optional
from typing import Any, Dict, List, Optional
# --- Bot Schemas ---
@@ -31,6 +31,8 @@ class BotStatus(BaseModel):
guild_count: int
user_count: int
shards: Optional[int]
shard_ids: Optional[List[int]] = None
shard_latencies: Optional[List[Dict[str, Any]]] = None
# --- Guild Schemas ---
@@ -67,13 +69,24 @@ class PrefixConfig(BaseModel):
guild_id: int
prefix: str
class AutomodEventConfig(BaseModel):
enabled: bool = False
punishment: Optional[str] = None # Mute | Kick | Ban | block_message (NSFW)
readonly_punishment: bool = False
class AutomodConfig(BaseModel):
guild_id: int
enabled: bool
punishments: Dict[str, str]
ignored_roles: List[int]
ignored_channels: List[int]
logging_channel: Optional[int]
# Legacy flat map (API event ids or bot names) — kept for older clients
punishments: Dict[str, str] = {}
events: Dict[str, AutomodEventConfig] = {}
thresholds: Dict[str, Dict[str, float]] = {}
threshold_meta: Dict[str, List[Dict[str, Any]]] = {}
ignored_roles: List[int] = []
ignored_channels: List[int] = []
logging_channel: Optional[int] = None
limits: Dict[str, int] = {"max_ignored_roles": 10, "max_ignored_channels": 10}
class TicketCategory(BaseModel):
name: str
@@ -299,10 +312,14 @@ class TicketUpdate(BaseModel):
class AutomodUpdate(BaseModel):
enabled: Optional[bool] = None
punishments: Optional[Dict[str, str]] = None
punishments: Optional[Dict[str, str]] = None # legacy
events: Optional[Dict[str, AutomodEventConfig]] = None
thresholds: Optional[Dict[str, Dict[str, float]]] = None
ignored_roles: Optional[List[int]] = None
ignored_channels: Optional[List[int]] = None
# Use a sentinel-friendly optional: omit = no change; null = clear logging
logging_channel: Optional[int] = None
clear_logging: Optional[bool] = None
class LevelingUpdate(BaseModel):
enabled: Optional[bool] = None

View File

@@ -161,6 +161,7 @@ from .moderation.warn import Warn
from .moderation.role import Role
from .moderation.message import Message
from .moderation.moderation import Moderation
from .moderation.mod_hub import ModHub
from .moderation.topcheck import TopCheck
from .moderation.snipe import Snipe
@@ -187,7 +188,7 @@ COG_CLASSES: tuple[type, ...] = (
AntiWebhookCreate, AntiWebhookDelete,
AntiSpam, AntiCaps, AntiInvite, AntiLink, AntiMassMention, AntiEmojiSpam,
Ban, Unban, Mute, Unmute, Lock, Unlock, Hide, Unhide, Kick, Warn, Role,
Message, Moderation, TopCheck, Snipe,
Message, Moderation, ModHub, TopCheck, Snipe,
)

View File

@@ -11,6 +11,7 @@
# ║ ║
# ╚══════════════════════════════════════════════════════════════════╝
from utils.db_paths import db_path
from utils.automod_settings import get_thresholds
import discord
from utils.emoji import TICK
@@ -23,7 +24,6 @@ import asyncio
class AntiEmojiSpam(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.emoji_threshold = 5
async def is_automod_enabled(self, guild_id):
async with aiosqlite.connect(db_path("automod.db")) as db:
@@ -118,17 +118,20 @@ class AntiEmojiSpam(commands.Cog):
emoji_count = len(emoji_pattern.findall(message.content))
th = await get_thresholds(guild_id, "anti_emoji_spam")
emoji_threshold = int(th.get("max_emojis", 5))
mute_minutes = float(th.get("mute_minutes", 1))
if emoji_count > self.emoji_threshold:
if emoji_count > emoji_threshold:
punishment = await self.get_punishment(guild_id)
action_taken = None
reason = f"Emoji Spam ({emoji_count} emojis)"
try:
if punishment == "Mute":
timeout_duration = discord.utils.utcnow() + timedelta(minutes=1)
timeout_duration = discord.utils.utcnow() + timedelta(minutes=mute_minutes)
await user.edit(timed_out_until=timeout_duration, reason=reason)
action_taken = "Muted for 1 minute"
action_taken = f"Muted for {int(mute_minutes)} minutes"
elif punishment == "Kick":
await user.kick(reason=reason)
action_taken = "Kicked"

View File

@@ -11,6 +11,7 @@
# ║ ║
# ╚══════════════════════════════════════════════════════════════════╝
from utils.db_paths import db_path
from utils.automod_settings import get_thresholds
import discord
from utils.emoji import TICK
@@ -105,15 +106,17 @@ class AntiInvite(commands.Cog):
if any(invite.code == invite_code for invite in invite):
return
th = await get_thresholds(guild_id, "anti_invites")
mute_minutes = float(th.get("mute_minutes", 12))
punishment = await self.get_punishment(guild_id)
action_taken = None
reason = "Posted an invite link"
try:
if punishment == "Mute":
timeout_duration = discord.utils.utcnow() + timedelta(minutes=12)
timeout_duration = discord.utils.utcnow() + timedelta(minutes=mute_minutes)
await user.edit(timed_out_until=timeout_duration, reason="Posted an invite link")
action_taken = "Muted for 12 minutes"
action_taken = f"Muted for {int(mute_minutes)} minutes"
elif punishment == "Kick":
await user.kick(reason="Posted an invite link")
action_taken = "Kicked"

View File

@@ -11,6 +11,7 @@
# ║ ║
# ╚══════════════════════════════════════════════════════════════════╝
from utils.db_paths import db_path
from utils.automod_settings import get_thresholds
import discord
from utils.emoji import TICK
@@ -22,7 +23,6 @@ import asyncio
class AntiMassMention(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.mass_mention_threshold = 5
async def is_automod_enabled(self, guild_id):
async with aiosqlite.connect(db_path("automod.db")) as db:
@@ -97,17 +97,21 @@ class AntiMassMention(commands.Cog):
return
th = await get_thresholds(guild_id, "anti_mass_mention")
mention_threshold = int(th.get("max_mentions", 5))
mute_minutes = float(th.get("mute_minutes", 3))
mention_count = message.content.count("<@")
if mention_count >= self.mass_mention_threshold:
if mention_count >= mention_threshold:
punishment = await self.get_punishment(guild_id)
action_taken = None
reason = f"Mass Mention ({mention_count} mentions)"
try:
if punishment == "Mute":
timeout_duration = discord.utils.utcnow() + timedelta(minutes=3)
timeout_duration = discord.utils.utcnow() + timedelta(minutes=mute_minutes)
await user.edit(timed_out_until=timeout_duration, reason=reason)
action_taken = "Muted for 3 minutes"
action_taken = f"Muted for {int(mute_minutes)} minutes"
elif punishment == "Kick":
await user.kick(reason=reason)
action_taken = "Kicked"

View File

@@ -11,6 +11,7 @@
# ║ ║
# ╚══════════════════════════════════════════════════════════════════╝
from utils.db_paths import db_path
from utils.automod_settings import get_thresholds
import discord
from utils.emoji import TICK
@@ -22,8 +23,6 @@ from datetime import timedelta
class AntiCaps(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.caps_threshold = 70
self.mute_duration = 2 * 60
async def is_automod_enabled(self, guild_id):
async with aiosqlite.connect(db_path("automod.db")) as db:
@@ -74,9 +73,6 @@ class AntiCaps(commands.Cog):
@commands.Cog.listener()
async def on_message(self, message):
if len(message.content) < 45:
return
if message.author.bot:
return
@@ -85,6 +81,14 @@ class AntiCaps(commands.Cog):
channel = message.channel
guild_id = guild.id
th = await get_thresholds(guild_id, "anti_caps")
min_length = int(th.get("min_length", 45))
caps_threshold = float(th.get("percent", 70))
mute_minutes = float(th.get("mute_minutes", 1))
if len(message.content) < min_length:
return
if not await self.is_automod_enabled(guild_id) or not await self.is_anti_caps_enabled(guild_id):
return
@@ -103,16 +107,16 @@ class AntiCaps(commands.Cog):
caps_count = sum(1 for c in message.content if c.isupper())
caps_percentage = (caps_count / len(message.content)) * 100
if caps_percentage > self.caps_threshold:
if caps_percentage > caps_threshold:
punishment = await self.get_punishment(guild_id)
action_taken = None
reason = "Excessive Caps"
try:
if punishment == "Mute":
timeout_duration = discord.utils.utcnow() + timedelta(minutes=1)
timeout_duration = discord.utils.utcnow() + timedelta(minutes=mute_minutes)
await user.edit(timed_out_until=timeout_duration, reason="Excessive Caps")
action_taken = "Muted for 1 minutes"
action_taken = f"Muted for {int(mute_minutes)} minutes"
elif punishment == "Kick":
await user.kick(reason="Excessive Caps")
action_taken = "Kicked"

View File

@@ -11,6 +11,7 @@
# ║ ║
# ╚══════════════════════════════════════════════════════════════════╝
from utils.db_paths import db_path
from utils.automod_settings import get_thresholds
import discord
from utils.emoji import TICK
@@ -107,15 +108,17 @@ class AntiLink(commands.Cog):
if self.spotify_pattern.search(message.content):
return
th = await get_thresholds(guild_id, "anti_link")
mute_minutes = float(th.get("mute_minutes", 7))
punishment = await self.get_punishment(guild_id)
action_taken = None
reason = "Posted a link"
try:
if punishment == "Mute":
timeout_duration = discord.utils.utcnow() + timedelta(minutes=7)
timeout_duration = discord.utils.utcnow() + timedelta(minutes=mute_minutes)
await user.edit(timed_out_until=timeout_duration, reason=reason)
action_taken = "Muted for 7 minutes"
action_taken = f"Muted for {int(mute_minutes)} minutes"
elif punishment == "Kick":
await user.kick(reason=reason)
action_taken = "Kicked"

View File

@@ -11,6 +11,7 @@
# ║ ║
# ╚══════════════════════════════════════════════════════════════════╝
from utils.db_paths import db_path
from utils.automod_settings import get_thresholds
import discord
from utils.emoji import TICK
@@ -22,8 +23,6 @@ from datetime import timedelta
class AntiSpam(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.spam_threshold = 5
self.mute_duration = 12 * 60
self.recent_messages = {}
async def is_automod_enabled(self, guild_id):
@@ -99,21 +98,26 @@ class AntiSpam(commands.Cog):
return
current_time = message.created_at.timestamp()
th = await get_thresholds(guild_id, "anti_spam")
window = float(th.get("window_seconds", 10))
spam_threshold = int(th.get("max_messages", 5))
mute_minutes = float(th.get("mute_minutes", 12))
user_messages = self.recent_messages.get(user.id, [])
user_messages = [msg for msg in user_messages if current_time - msg < 10]
user_messages = [msg for msg in user_messages if current_time - msg < window]
user_messages.append(current_time)
self.recent_messages[user.id] = user_messages
if len(user_messages) > self.spam_threshold:
if len(user_messages) > spam_threshold:
punishment = await self.get_punishment(guild_id)
action_taken = None
reason = "Spamming"
try:
if punishment == "Mute":
timeout_duration = discord.utils.utcnow() + timedelta(minutes=12)
timeout_duration = discord.utils.utcnow() + timedelta(minutes=mute_minutes)
await user.edit(timed_out_until=timeout_duration, reason="Spamming")
action_taken = "Muted for 12 minutes"
action_taken = f"Muted for {int(mute_minutes)} minutes"
elif punishment == "Kick":
await user.kick(reason="Spamming")
action_taken = "Kicked"

View File

@@ -50,7 +50,7 @@ class Birthdays(commands.Cog):
self.client = client
self.check_birthdays.start()
@commands.hybrid_command(
@commands.command(
name="birthdaysetup",
help="Set up the birthday log channel and role.")
@commands.has_permissions(administrator=True)
@@ -69,7 +69,7 @@ class Birthdays(commands.Cog):
await ctx.send(view=CV2("Birthday Setup", f"Birthday log channel set to {channel.mention} and birthday role set to {role.mention}."))
@commands.hybrid_command(
@commands.command(
name="setbirthday",
help="Set your birthday.")
@commands.guild_only()
@@ -112,7 +112,7 @@ class Birthdays(commands.Cog):
except asyncio.TimeoutError:
await ctx.send(view=CV2("Error", "You took too long to respond. Please try again."))
@commands.hybrid_command(
@commands.command(
name="removebirthday",
help="Remove your birthday.")
@commands.guild_only()
@@ -126,7 +126,7 @@ class Birthdays(commands.Cog):
else:
await ctx.send(view=CV2("Error", "You have no birthday set."))
@commands.hybrid_command(
@commands.command(
name="listbirthdays",
help="List all members who have their birthday today.")
@commands.guild_only()
@@ -143,7 +143,7 @@ class Birthdays(commands.Cog):
else:
await ctx.send(view=CV2("Birthdays", "No birthdays today."))
@commands.hybrid_command(
@commands.command(
name="birthday",
help="Check your birthday.")
@commands.guild_only()

View File

@@ -30,10 +30,25 @@ class Games(Cog):
def __init__(self, client: Axiom):
self.client = client
@commands.hybrid_group(
name="game",
description="Play mini-games with friends or the bot.",
fallback="help",
invoke_without_command=True,
)
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@commands.guild_only()
async def game(self, ctx: Context):
"""Games root — use `/game <subcommand>` or `>game <subcommand>`."""
if ctx.subcommand_passed is None:
await ctx.send_help(ctx.command)
ctx.command.reset_cooldown(ctx)
@commands.hybrid_command(name="chess",
@game.command(name="chess",
help="Play Chess with a user.",
usage="Chess <user>")
usage="game chess <user>")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -49,10 +64,10 @@ class Games(Cog):
await game.start(ctx)
@commands.hybrid_command(name="rps",
@game.command(name="rps",
help="Play Rock Paper Scissor with bot/user.",
aliases=["rockpaperscissors"],
usage="Rockpaperscissors")
usage="game rps [user]")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -62,10 +77,10 @@ class Games(Cog):
game = btn.BetaRockPaperScissors(player)
await game.start(ctx, timeout=120)
@commands.hybrid_command(name="tic-tac-toe",
@game.command(name="tictactoe",
help="play tic-tac-toe game with a user.",
aliases=["ttt", "tictactoe"],
usage="Ticktactoe <member>")
aliases=["ttt", "tic-tac-toe"],
usage="game tictactoe <member>")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -80,9 +95,9 @@ class Games(Cog):
game = btn.BetaTictactoe(cross=ctx.author, circle=player)
await game.start(ctx, timeout=30)
@commands.hybrid_command(name="wordle",
@game.command(name="wordle",
help="Wordle Game | Play with bot.",
usage="Wordle")
usage="game wordle")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -92,10 +107,10 @@ class Games(Cog):
game = games.Wordle()
await game.start(ctx, timeout=120)
@commands.hybrid_command(name="2048",
@game.command(name="2048",
help="Play 2048 game with bot.",
aliases=["twenty48"],
usage="2048")
usage="game 2048")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -105,10 +120,10 @@ class Games(Cog):
game = btn.BetaTwenty48()
await game.start(ctx, win_at=2048)
@commands.hybrid_command(name="memory-game",
@game.command(name="memory",
help="How strong is your memory?",
aliases=["memory"],
usage="memory-game")
aliases=["memory-game"],
usage="game memory")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -118,10 +133,10 @@ class Games(Cog):
game = btn.MemoryGame()
await game.start(ctx)
@commands.hybrid_command(name="number-slider",
@game.command(name="slider",
help="slide numbers with bot",
aliases=["slider"],
usage="slider")
aliases=["number-slider"],
usage="game slider")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -131,10 +146,10 @@ class Games(Cog):
game = btn.NumberSlider()
await game.start(ctx)
@commands.hybrid_command(name="battleship",
@game.command(name="battleship",
help="Play battleship game with your friend.",
aliases=["battle-ship"],
usage="battleship <user>")
usage="game battleship <user>")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -144,7 +159,7 @@ class Games(Cog):
game = btn.BetaBattleShip(player1=ctx.author, player2=player)
await game.start(ctx)
@commands.hybrid_group(name="country-guesser",
@commands.group(name="country-guesser",
help="Guess name of the country by flag.",
aliases=["guess", "guesser", "countryguesser"],
usage="country-guesser")
@@ -164,10 +179,10 @@ class Games(Cog):
async def _end_country_guesser(self, ctx: Context):
await self.country_guesser_game.end_game_manually(ctx)"""
@commands.hybrid_command(name="connectfour",
@game.command(name="connectfour",
help="Play Connect Four game with user.",
aliases=["c4", "connect-four", "connect4"],
usage="connectfour <user>")
usage="game connectfour <user>")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -184,10 +199,10 @@ class Games(Cog):
@commands.hybrid_command(name="lights-out",
@game.command(name="lightsout",
help="Play Lights Show game with bot.",
aliases=["lightsout"],
usage="Lights-out")
aliases=["lights-out"],
usage="game lightsout")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)

View File

@@ -36,7 +36,7 @@ class Invcrole(commands.Cog):
''')
await db.commit()
@commands.hybrid_group(name='vcrole', help="Vcrole Setup commands", invoke_without_command=True)
@commands.group(name='vcrole', help="Vcrole Setup commands", invoke_without_command=True)
@blacklist_check()
@ignore_check()
@commands.has_permissions(administrator=True)

View File

@@ -47,7 +47,13 @@ class Media(commands.Cog):
async def on_ready(self):
await self.set_db()
@commands.hybrid_group(name="media", help="Setup Media channel, Media channel will not allow users to send messages other than media files.", invoke_without_command=True)
@commands.hybrid_group(
name="media",
description="Setup Media channel; only media files are allowed.",
help="Setup Media channel, Media channel will not allow users to send messages other than media files.",
fallback="help",
invoke_without_command=True,
)
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)

View File

@@ -57,16 +57,34 @@ class Whitelist(commands.Cog):
''')
await self.db.commit()
@commands.hybrid_command(name='whitelist', aliases=['wl'], help="Whitelists a user from antinuke for a specific action.")
@commands.hybrid_group(
name="whitelist",
aliases=["wl"],
description="Manage antinuke whitelist.",
fallback="help",
invoke_without_command=True,
help="Whitelists a user from antinuke for a specific action.",
)
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 5, commands.BucketType.user)
@commands.max_concurrency(1, per=commands.BucketType.default, wait=False)
@commands.guild_only()
@commands.has_permissions(administrator=True)
async def whitelist(self, ctx):
"""Whitelist root — use `/whitelist <subcommand>` or `>whitelist <subcommand>`."""
if ctx.subcommand_passed is None:
await ctx.send_help(ctx.command)
ctx.command.reset_cooldown(ctx)
async def whitelist(self, ctx, member: discord.Member = None):
@whitelist.command(name="add", help="Whitelists a user from antinuke for a specific action.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 5, commands.BucketType.user)
@commands.max_concurrency(1, per=commands.BucketType.default, wait=False)
@commands.guild_only()
@commands.has_permissions(administrator=True)
async def whitelist_add(self, ctx, member: discord.Member = None):
if ctx.guild.member_count < 2:
view = CV2(f"{CROSS} Error", "Your Server Doesn't Meet My 30 Member Criteria")
return await ctx.send(view=view)
@@ -101,7 +119,7 @@ class Whitelist(commands.Cog):
view = CV2(
"__Whitelist Commands__",
"**Adding a user to the whitelist means that no actions will be taken against them if they trigger the Anti-Nuke Module.**",
f"**Usage**\n{ARROWRED} `{prefix}whitelist @user/id`\n{ARROWRED} `{prefix}wl @user`"
f"**Usage**\n{ARROWRED} `{prefix}whitelist add @user/id`\n{ARROWRED} `{prefix}wl add @user`"
)
return await ctx.send(view=view)
@@ -252,7 +270,7 @@ class Whitelist(commands.Cog):
await msg.edit(view=None)
@commands.hybrid_command(name='whitelisted', aliases=['wlist'], help="Shows the list of whitelisted users.")
@whitelist.command(name="list", aliases=["whitelisted", "wlist"], help="Shows the list of whitelisted users.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 5, commands.BucketType.user)
@@ -308,7 +326,7 @@ class Whitelist(commands.Cog):
await ctx.send(view=view)
@commands.hybrid_command(name="whitelistreset", aliases=['wlreset'], help="Resets the whitelisted users.")
@whitelist.command(name="reset", aliases=["whitelistreset", "wlreset"], help="Resets the whitelisted users.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 10, commands.BucketType.user)

View File

@@ -181,13 +181,30 @@ class Automod(commands.Cog):
PRIMARY KEY (guild_id)
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS automod_thresholds (
guild_id INTEGER,
event TEXT,
key TEXT,
value REAL,
PRIMARY KEY (guild_id, event, key)
)
""")
await db.commit()
@commands.hybrid_group(invoke_without_command=True)
@commands.hybrid_group(
name="automod",
aliases=["am"],
invoke_without_command=True,
with_app_command=True,
description="Configure server Automod (enable, disable, punishments, ignores).",
fallback="help",
)
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 4, commands.BucketType.user)
async def automod(self, ctx):
"""Automod root — use `/automod <subcommand>` or `>automod <subcommand>`."""
if ctx.subcommand_passed is None:
await ctx.send_help(ctx.command)
ctx.command.reset_cooldown(ctx)

View File

@@ -52,7 +52,7 @@ class AutoReaction(commands.Cog):
cursor = await db.execute("SELECT 1 FROM autoreact WHERE guild_id = ? AND trigger = ?", (guild_id, trigger))
return await cursor.fetchone()
@commands.hybrid_group(name="react", aliases=["autoreact"], help="Lists all subcommands of autoreact group.", invoke_without_command=True)
@commands.group(name="react", aliases=["autoreact"], help="Lists all subcommands of autoreact group.", invoke_without_command=True)
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 4, commands.BucketType.user)

View File

@@ -46,7 +46,7 @@ class AutoResponder(commands.Cog):
''')
await db.commit()
@commands.hybrid_group(name="autoresponder", invoke_without_command=True, aliases=['ar'], help="Manage autoresponders in the server.")
@commands.group(name="autoresponder", invoke_without_command=True, aliases=['ar'], help="Manage autoresponders in the server.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 5, commands.BucketType.user)

View File

@@ -92,7 +92,7 @@ class AutoRole(commands.Cog):
@commands.hybrid_group(name="autorole", invoke_without_command=True)
@commands.group(name="autorole", invoke_without_command=True)
@commands.cooldown(1, 5, commands.BucketType.user)
@commands.max_concurrency(1, per=commands.BucketType.default, wait=False)
@commands.guild_only()

View File

@@ -123,7 +123,7 @@ class Blackjack(commands.Cog):
total_sum += 1
return total_sum
@commands.hybrid_command(aliases=['bj', 'blackjacks'], help="Play a simple game of blackjack.", usage="blackjack")
@commands.command(aliases=['bj', 'blackjacks'], help="Play a simple game of blackjack.", usage="blackjack")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)

View File

@@ -207,7 +207,7 @@ class Blacklist(commands.Cog):
await warning_message.delete(delay=3)
break
@commands.hybrid_group(name="blacklistword", aliases=["blword"], invoke_without_command=True)
@commands.group(name="blacklistword", aliases=["blword"], invoke_without_command=True)
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)

View File

@@ -54,7 +54,7 @@ class Block(commands.Cog):
@commands.hybrid_group(name="blacklist", aliases=["bl"], invoke_without_command=True)
@commands.group(name="blacklist", aliases=["bl"], invoke_without_command=True)
@commands.is_owner()
async def blacklist(self, ctx):
if ctx.subcommand_passed is None:

View File

@@ -122,7 +122,7 @@ class calculator(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.hybrid_command(name='calculator', help='Starts a calculator session', aliases=['calc', 'calculate', 'math'])
@commands.command(name='calculator', help='Starts a calculator session', aliases=['calc', 'calculate', 'math'])
async def calculator(self, ctx):
"""Starts a new calculator session."""
# Ensure we pass the author to the view so it knows who triggered it

View File

@@ -74,7 +74,7 @@ class Counting(commands.Cog):
"**counting stats** — View current counting stats"
))
@commands.hybrid_group(name="counting", invoke_without_command=True)
@commands.group(name="counting", invoke_without_command=True)
async def counting(self, ctx):
if not self.is_enabled(ctx.guild.id):
await self.not_enabled_embed(ctx)

View File

@@ -500,7 +500,23 @@ class Customrole(commands.Cog):
@commands.hybrid_command(name="staff",
@commands.hybrid_group(
name="customrole",
aliases=["cr"],
description="Assign or remove custom roles for members.",
fallback="help",
invoke_without_command=True,
)
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
async def customrole(self, context: Context):
"""Customrole root — use `/customrole <subcommand>` or `>customrole <subcommand>`."""
if context.subcommand_passed is None:
await context.send_help(context.command)
context.command.reset_cooldown(context)
@customrole.command(name="staff",
description="Gives the staff role to the user.",
aliases=['official'],
help="Gives the staff role to the user.")
@@ -511,7 +527,7 @@ class Customrole(commands.Cog):
async def _staff(self, context: Context, member: discord.Member) -> None:
await self.handle_role_command(context, member, 'staff')
@commands.hybrid_command(name="girl",
@customrole.command(name="girl",
description="Gives the girl role to the user.",
aliases=['qt'],
help="Gives the girl role to the user.")
@@ -522,7 +538,7 @@ class Customrole(commands.Cog):
async def _girl(self, context: Context, member: discord.Member) -> None:
await self.handle_role_command(context, member, 'girl')
@commands.hybrid_command(name="vip",
@customrole.command(name="vip",
description="Gives the VIP role to the user.",
help="Gives the VIP role to the user.")
@blacklist_check()
@@ -532,7 +548,7 @@ class Customrole(commands.Cog):
async def _vip(self, context: Context, member: discord.Member) -> None:
await self.handle_role_command(context, member, 'vip')
@commands.hybrid_command(name="guest",
@customrole.command(name="guest",
description="Gives the guest role to the user.",
help="Gives the guest role to the user.")
@blacklist_check()
@@ -542,7 +558,7 @@ class Customrole(commands.Cog):
async def _guest(self, context: Context, member: discord.Member) -> None:
await self.handle_role_command(context, member, 'guest')
@commands.hybrid_command(name="friend",
@customrole.command(name="friend",
description="Gives the friend role to the user.",
aliases=['frnd'],
help="Gives the friend role to the user.")

View File

@@ -81,7 +81,7 @@ class StaffDMCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.hybrid_command(name="dmstaff")
@commands.command(name="dmstaff")
async def dm_staff(self, ctx, member: discord.Member, *, message: str):
if ctx.author.id not in STAFF_IDS:
view = PermissionErrorView()

View File

@@ -406,7 +406,7 @@ class Emergency(commands.Cog):
) as cursor:
return await cursor.fetchone() is not None
@commands.hybrid_group(name="emergency", aliases=["emg"], invoke_without_command=True)
@commands.group(name="emergency", aliases=["emg"], invoke_without_command=True)
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 4, commands.BucketType.user)
@@ -614,7 +614,7 @@ class Emergency(commands.Cog):
roles = await cursor.fetchall()
await ctx.reply(view=RoleListView(roles, is_auth))
@commands.hybrid_command(name="emergencysituation", aliases=["emgs"])
@commands.command(name="emergencysituation", aliases=["emgs"])
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 40, commands.BucketType.user)
@@ -736,7 +736,7 @@ class Emergency(commands.Cog):
await anti.commit()
await proc_msg.delete()
@commands.hybrid_command(name="emergencyrestore", aliases=["emgrestore"])
@commands.command(name="emergencyrestore", aliases=["emgrestore"])
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 30, commands.BucketType.user)

View File

@@ -88,12 +88,12 @@ class encryption(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.hybrid_group(invoke_without_command=True)
@commands.group(invoke_without_command=True)
async def encode(self, ctx):
"""All encode methods"""
await ctx.send_help(ctx.command)
@commands.hybrid_group(invoke_without_command=True)
@commands.group(invoke_without_command=True)
async def decode(self, ctx):
"""All decode methods"""
await ctx.send_help(ctx.command)
@@ -204,7 +204,7 @@ class encryption(commands.Cog):
except Exception:
await ctx.send(view=DecodeErrorView("ASCII85"))
@commands.hybrid_command(name="password")
@commands.command(name="password")
async def password(self, ctx):
"""Generates a random secure password for you"""
if hasattr(ctx, "guild") and ctx.guild is not None:

View File

@@ -125,7 +125,12 @@ class Extra(commands.Cog):
self.color = 0xFF0000
self.start_time = datetime.datetime.now()
@commands.hybrid_group(name="banner")
@commands.hybrid_group(
name="banner",
description="Show user or server banners.",
fallback="help",
invoke_without_command=True,
)
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -197,7 +202,7 @@ class Extra(commands.Cog):
@commands.hybrid_command(name="uptime", description="Shows the Bot's Uptime.")
@commands.command(name="uptime", description="Shows the Bot's Uptime.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -447,7 +452,7 @@ class Extra(commands.Cog):
@commands.hybrid_command(name="boostcount",
@commands.command(name="boostcount",
help="Shows boosts count",
usage="boosts",
aliases=["bco"],
@@ -788,7 +793,7 @@ class Extra(commands.Cog):
@commands.hybrid_command(name="joined-at",
@commands.command(name="joined-at",
help="Shows when a user joined",
usage="joined-at [user]",
with_app_command=True)
@@ -799,7 +804,7 @@ class Extra(commands.Cog):
joined = ctx.author.joined_at.strftime("%a, %d %b %Y %I:%M %p")
await ctx.send(view=CV2("joined-at", f"**`{joined}`**"))
@commands.hybrid_command(name="github", usage="github [search]")
@commands.command(name="github", usage="github [search]")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -900,13 +905,18 @@ class Extra(commands.Cog):
f"**API (Roundtrip):** `{api_latency}ms`\n"
f"**Database:** `{db_latency}`"
)
if self.bot.latencies and len(self.bot.latencies) > 1:
shard_lines = "\n".join(
f"**Shard {sid}:** `{round(lat * 1000)}ms`" for sid, lat in self.bot.latencies
)
latency_text += f"\n\n**Shards ({self.bot.shard_count or len(self.bot.latencies)}):**\n{shard_lines}"
await msg.edit(view=CV2("System Latency Report", latency_text))
@commands.hybrid_command(name="permissions", aliases= ["perms"],
@commands.command(name="permissions", aliases= ["perms"],
help="Check and list the key permissions of a specific user",
usage="perms <user>",
with_app_command=True)

View File

@@ -35,7 +35,7 @@ class FastGreet(commands.Cog):
)
""")
@commands.hybrid_command(name="fastgreet_add")
@commands.command(name="fastgreet_add")
@commands.has_permissions(administrator=True)
async def add_greet_channel(self, ctx, channel: discord.TextChannel):
with sqlite3.connect(DB_PATH) as conn:
@@ -45,7 +45,7 @@ class FastGreet(commands.Cog):
""", (ctx.guild.id, channel.id))
await ctx.send(f"{channel.mention} added as a greet channel.")
@commands.hybrid_command(name="fastgreet_remove")
@commands.command(name="fastgreet_remove")
@commands.has_permissions(administrator=True)
async def remove_greet_channel(self, ctx, channel: discord.TextChannel):
with sqlite3.connect(DB_PATH) as conn:
@@ -54,7 +54,7 @@ class FastGreet(commands.Cog):
""", (ctx.guild.id, channel.id))
await ctx.send(f"{channel.mention} removed from greet channels.")
@commands.hybrid_command(name="fastgreet_list")
@commands.command(name="fastgreet_list")
async def list_greet_channels(self, ctx):
with sqlite3.connect(DB_PATH) as conn:
cursor = conn.execute("""

View File

@@ -66,7 +66,12 @@ class FilterCog(commands.Cog):
self.active_filters[ctx.guild.id] = filter_name
await ctx.send(embed=discord.Embed(description=f"Filter set to **{filter_name}**.", color=discord.Color.green()))
@commands.hybrid_group(invoke_without_command=True)
@commands.hybrid_group(
name="filter",
description="Enable or disable audio filters for music.",
fallback="help",
invoke_without_command=True,
)
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)

View File

@@ -56,58 +56,58 @@ class Fun(commands.Cog):
async def meter_command(self, ctx, title, user, text):
await ctx.send(view=CV2(title, text))
@commands.hybrid_command(name="shipp")
@commands.command(name="shipp")
@blacklist_check()
@ignore_check()
async def shipp(self, ctx, user1: discord.Member, user2: discord.Member):
percentage = random.randint(0, 100)
await ctx.send(view=CV2(f"{self.random_emoji()} Ship Result", f"**{user1.mention} x {user2.mention} = {percentage}% Love**"))
@commands.hybrid_command()
@commands.command()
@blacklist_check()
@ignore_check()
async def hug(self, ctx, user: discord.Member):
await self.action_command(ctx, user, "hug")
@commands.hybrid_command()
@commands.command()
@blacklist_check()
@ignore_check()
async def kiss(self, ctx, user: discord.Member):
await self.action_command(ctx, user, "kiss")
@commands.hybrid_command()
@commands.command()
@blacklist_check()
@ignore_check()
async def pat(self, ctx, user: discord.Member):
await self.action_command(ctx, user, "pat")
@commands.hybrid_command()
@commands.command()
@blacklist_check()
@ignore_check()
async def slap(self, ctx, user: discord.Member):
await self.action_command(ctx, user, "slap")
@commands.hybrid_command()
@commands.command()
@blacklist_check()
@ignore_check()
async def tickle(self, ctx, user: discord.Member):
await self.action_command(ctx, user, "tickle")
@commands.hybrid_command()
@commands.command()
@blacklist_check()
@ignore_check()
async def coinflip(self, ctx):
result = random.choice(["Heads", "Tails"])
await ctx.send(view=CV2("🪙 Coin Flip", f"**Result: {result}**"))
@commands.hybrid_command()
@commands.command()
@blacklist_check()
@ignore_check()
async def dice(self, ctx):
result = random.randint(1, 6)
await ctx.send(view=CV2("🎲 Dice Roll", f"**You rolled a {result}!**"))
@commands.hybrid_command(name="8ball")
@commands.command(name="8ball")
@blacklist_check()
@ignore_check()
async def eight_ball(self, ctx, *, question: str):
@@ -116,7 +116,7 @@ class Fun(commands.Cog):
"Don't count on it.", "My sources say no.", "Very doubtful."]
await ctx.send(view=CV2("🎱 Magic 8Ball", f"**Q:** {question}\n**A:** {random.choice(responses)}"))
@commands.hybrid_command()
@commands.command()
@blacklist_check()
@ignore_check()
async def roast(self, ctx, user: discord.Member):
@@ -127,56 +127,56 @@ class Fun(commands.Cog):
]
await ctx.send(view=CV2("🔥 Roast Time", random.choice(roasts)))
@commands.hybrid_command()
@commands.command()
@blacklist_check()
@ignore_check()
async def iq(self, ctx, user: discord.Member = None):
user = user or ctx.author
await ctx.send(view=CV2("🧠 IQ Test", f"**{user.mention} has an IQ of {random.randint(50, 200)}!**"))
@commands.hybrid_command()
@commands.command()
@blacklist_check()
@ignore_check()
async def dumb(self, ctx, user: discord.Member = None):
user = user or ctx.author
await ctx.send(view=CV2("🤪 Dumbness Test", f"**{user.mention} is {random.randint(0, 100)}% dumb!**"))
@commands.hybrid_command()
@commands.command()
@blacklist_check()
@ignore_check()
async def simprate(self, ctx, user: discord.Member = None):
user = user or ctx.author
await ctx.send(view=CV2("😳 Simp Rate", f"**{user.mention} is {random.randint(0, 100)}% simp!**"))
@commands.hybrid_command()
@commands.command()
@blacklist_check()
@ignore_check()
async def toxic(self, ctx, user: discord.Member = None):
user = user or ctx.author
await ctx.send(view=CV2("☠️ Toxic Meter", f"**{user.mention} is {random.randint(0, 100)}% toxic!**"))
@commands.hybrid_command()
@commands.command()
@blacklist_check()
@ignore_check()
async def intelligence(self, ctx, user: discord.Member = None):
user = user or ctx.author
await ctx.send(view=CV2("🧠 Intelligence Meter", f"**{user.mention} has {random.randint(0, 200)} IQ Points!**"))
@commands.hybrid_command()
@commands.command()
@blacklist_check()
@ignore_check()
async def genius(self, ctx, user: discord.Member = None):
user = user or ctx.author
await ctx.send(view=CV2("🤓 Genius Rate", f"**{user.mention} is {random.randint(0, 100)}% genius!**"))
@commands.hybrid_command()
@commands.command()
@blacklist_check()
@ignore_check()
async def brainrate(self, ctx, user: discord.Member = None):
user = user or ctx.author
await ctx.send(view=CV2("🧠 Brain Power", f"**{user.mention} is using {random.randint(0, 100)}% of their brain!**"))
@commands.hybrid_command()
@commands.command()
@blacklist_check()
@ignore_check()
async def howhot(self, ctx, user: discord.Member = None):

View File

@@ -208,7 +208,7 @@ class General(commands.Cog):
await msg.add_reaction(CROSS)
@commands.hybrid_command(name="users", help=f"checks total users of {BotName}.")
@commands.command(name="users", help=f"checks total users of {BotName}.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -251,7 +251,7 @@ class General(commands.Cog):
except:
pass
@commands.hybrid_command(name="rickroll",
@commands.command(name="rickroll",
help="Detects if provided url is a rick-roll",
usage="Rickroll <url>")
@blacklist_check()
@@ -271,7 +271,7 @@ class General(commands.Cog):
title = "Rick Roll {} in webpage".format("was found" if rickRoll else "was not found")
await ctx.reply(view=CV2(title, "🎵 Never gonna give you up..." if rickRoll else "✅ Safe to click!"), mention_author=True)
@commands.hybrid_command(name="hash",
@commands.command(name="hash",
help="Hashes provided text with provided algorithm")
@blacklist_check()
@ignore_check()
@@ -297,7 +297,7 @@ class General(commands.Cog):
hash_lines = f"**{algorithm}:** `{algos[algorithm.lower()]}`"
await ctx.reply(view=CV2(f"Hashed \"{message}\"", hash_lines), mention_author=True)
@commands.hybrid_command(
@commands.command(
name="invite",
aliases=['invite-bot'],
description="Get Support & Bot invite link!"

View File

@@ -159,7 +159,24 @@ class Giveaway(commands.Cog):
@commands.hybrid_command(description="Starts a new giveaway.")
@commands.hybrid_group(
name="giveaway",
aliases=["g"],
description="Manage server giveaways.",
fallback="help",
invoke_without_command=True,
)
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 5, commands.BucketType.user)
@commands.has_guild_permissions(manage_guild=True)
async def giveaway(self, ctx):
"""Giveaway root — use `/giveaway <subcommand>` or `>giveaway <subcommand>`."""
if ctx.subcommand_passed is None:
await ctx.send_help(ctx.command)
ctx.command.reset_cooldown(ctx)
@giveaway.command(name="start", aliases=["gstart"], description="Starts a new giveaway.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 5, commands.BucketType.user)
@@ -244,7 +261,7 @@ class Giveaway(commands.Cog):
print(f"Giveaway message deleted in {message.guild.name} - {message.guild.id}")
await self.connection.commit()
@commands.hybrid_command(name="gend", description="Ends a giveaway before its ending time.", help="Ends a giveaway before its ending time.")
@giveaway.command(name="end", aliases=["gend"], description="Ends a giveaway before its ending time.", help="Ends a giveaway before its ending time.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 5, commands.BucketType.user)
@@ -329,7 +346,7 @@ class Giveaway(commands.Cog):
await ctx.send("Please reply to the giveaway message or provide the giveaway ID.")
await self.connection.commit()
@commands.hybrid_command(description="Rerolls a giveaway on replying the giveaway message.", help="Rerolls a giveaway on replying the giveaway message.")
@giveaway.command(name="reroll", aliases=["greroll"], description="Rerolls a giveaway on replying the giveaway message.", help="Rerolls a giveaway on replying the giveaway message.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 5, commands.BucketType.user)
@@ -389,7 +406,7 @@ class Giveaway(commands.Cog):
return val * time_dict[unit]
@commands.hybrid_command(name="glist", description="Lists all ongoing giveaways.")
@giveaway.command(name="list", aliases=["glist"], description="Lists all ongoing giveaways.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 5, commands.BucketType.user)

View File

@@ -118,7 +118,7 @@ class Ignore(commands.Cog):
)
await db.commit()
@commands.hybrid_group(
@commands.group(
name="ignore",
help="Manage ignored commands, channels, users, and bypassed users.",
invoke_without_command=True,

View File

@@ -53,22 +53,22 @@ class ImageCommands(commands.Cog):
else:
await ctx.send(view=CV2("❌ Error", f"No image found for {title.lower()}."))
@commands.hybrid_command(name="boy")
@commands.command(name="boy")
async def boy_image(self, ctx):
url = await self.fetch_pexels_image("handsome boy")
await self.send_image_view(ctx, "👦 Boy Pic", url)
# @commands.hybrid_command(name="girl")
# @commands.command(name="girl")
# async def girl_image(self, ctx):
# url = await self.fetch_pexels_image("beautiful girl")
# await self.send_image_view(ctx, "👧 Girl Pic", url)
@commands.hybrid_command(name="couple")
@commands.command(name="couple")
async def couple_image(self, ctx):
url = await self.fetch_pexels_image("romantic couple")
await self.send_image_view(ctx, "💑 Couple Pic", url)
@commands.hybrid_command(name="anime")
@commands.command(name="anime")
async def anime_image(self, ctx):
url = await self.fetch_waifu_image("waifu")
await self.send_image_view(ctx, "🧚 Anime Waifu", url)

View File

@@ -190,7 +190,7 @@ class JoinToCreate(commands.Cog):
print(f"Error in J2C on_ready: {e}")
continue
@commands.hybrid_command(name='j2csetup')
@commands.command(name='j2csetup')
@commands.has_permissions(administrator=True)
async def setup_private_channels(self, ctx):
if ctx.guild.id in self.setup_data:
@@ -225,7 +225,7 @@ class JoinToCreate(commands.Cog):
await ctx.send(view=CV2("✅ Success", f"J2C system setup complete! Join {join_channel.mention} to create a private VC."))
@commands.hybrid_command(name='j2creset')
@commands.command(name='j2creset')
@commands.has_permissions(administrator=True)
async def reset_private_channels(self, ctx):
if ctx.guild.id not in self.setup_data:

View File

@@ -129,7 +129,7 @@ class Jail(commands.Cog):
)
await log_channel.send(view=CV2("🔓 Member Unjailed", desc))
@commands.hybrid_command(name="jail")
@commands.command(name="jail")
@commands.has_permissions(manage_roles=True)
async def jail(self, ctx, member: discord.Member, duration: str = None, *, reason="No reason provided"):
jail_role_id = self.get_setting(ctx.guild.id, "jail_role")
@@ -182,13 +182,13 @@ class Jail(commands.Cog):
)
await log_channel.send(view=CV2("🔒 Member Jailed", desc))
@commands.hybrid_command(name="unjail")
@commands.command(name="unjail")
@commands.has_permissions(manage_roles=True)
async def unjail(self, ctx, member: discord.Member):
await self.unjail_member(ctx.guild, member)
await ctx.send(view=CV2("✅ Success", f"{member.mention} has been unjailed."))
@commands.hybrid_command(name="jailhistory")
@commands.command(name="jailhistory")
async def jailhistory(self, ctx, member: discord.Member):
cursor = self.conn.execute("""
SELECT reason, jailed_at, duration, mod_id FROM jailed

View File

@@ -37,7 +37,7 @@ class joindm(commands.Cog):
with open(jsondb_path('joindm_messages.json'), 'w') as f:
json.dump(self.joindm_messages, f)
@commands.hybrid_group(invoke_without_command=True)
@commands.group(invoke_without_command=True)
@commands.has_permissions(administrator=True)
async def joindm(self, ctx):
# Display the current join DM message

View File

@@ -293,7 +293,7 @@ class Leveling (commands .Cog ):
self .last_level_cache ={}
self .db_path =db_path("leveling.db")
@commands.hybrid_group (name ="level",invoke_without_command =True ,description ="Leveling system")
@commands.group (name ="level",invoke_without_command =True ,description ="Leveling system")
async def level (self ,ctx ):
"""Main leveling command"""
if ctx .invoked_subcommand is None :

View File

@@ -1598,7 +1598,12 @@ class Logging(commands.Cog):
embeds.append(config_embed)
return embeds
@commands.hybrid_group(invoke_without_command=True, name="log")
@commands.hybrid_group(
invoke_without_command=True,
name="log",
description="Configure server logging channels and events.",
fallback="help",
)
async def log(self, ctx: commands.Context):
"""Main logging command group. Works with both slash (/) and prefix (!) commands."""

View File

@@ -81,7 +81,7 @@ class Messages(commands.Cog):
conn.commit()
conn.close()
@commands.hybrid_command(name="messages", aliases=["msg"])
@commands.command(name="messages", aliases=["msg"])
async def messages(self, ctx, member: discord.Member = None):
member = member or ctx.author
today = datetime.utcnow().strftime("%Y-%m-%d")

View File

@@ -21,7 +21,7 @@ class Messagespack(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.hybrid_command(name="addmessages", aliases=["addmsg"])
@commands.command(name="addmessages", aliases=["addmsg"])
@commands.has_permissions(manage_messages=True)
async def addmessages(self, ctx, member: discord.Member, amount: int):
if amount <= 0:
@@ -46,7 +46,7 @@ class Messagespack(commands.Cog):
conn.close()
await ctx.send(f"Added {amount} messages to {member.mention} for today.")
@commands.hybrid_command(name="removemessages", aliases=["removemsg"])
@commands.command(name="removemessages", aliases=["removemsg"])
@commands.has_permissions(manage_messages=True)
async def removemessages(self, ctx, member: discord.Member, amount: int):
if amount <= 0:
@@ -70,7 +70,7 @@ class Messagespack(commands.Cog):
await ctx.send(f"{member.mention} has no messages recorded for today.")
conn.close()
@commands.hybrid_command(name="clearmessage", aliases=["clearmsg"])
@commands.command(name="clearmessage", aliases=["clearmsg"])
@commands.has_permissions(manage_messages=True)
async def clearmessage(self, ctx, member: discord.Member):
conn = sqlite3.connect(db_path("messages.db"))

View File

@@ -585,7 +585,23 @@ class Music(commands.Cog):
bar = '' * filled_length + '' * (length - filled_length)
return bar
@commands.hybrid_command(name="play", aliases=['p'], usage="play <query>", help="Plays a song or playlist.")
@commands.hybrid_group(
name="music",
aliases=["m"],
description="Music player commands.",
fallback="help",
invoke_without_command=True,
)
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
async def music(self, ctx: commands.Context):
"""Music root — use `/music <subcommand>` or `>music <subcommand>`."""
if ctx.subcommand_passed is None:
await ctx.send_help(ctx.command)
ctx.command.reset_cooldown(ctx)
@music.command(name="play", aliases=['p'], usage="music play <query>", help="Plays a song or playlist.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -594,7 +610,7 @@ class Music(commands.Cog):
await self.play_source(ctx, query)
@commands.hybrid_command(name="search", usage="search <query>", help="Searches music from multiple platforms.")
@music.command(name="search", usage="music search <query>", help="Searches music from multiple platforms.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -606,7 +622,7 @@ class Music(commands.Cog):
await ctx.send(view=PlatformSelectView(ctx, query))
@commands.hybrid_command(name="nowplaying", aliases=["nop"], usage="nowplaying", help="Shows the info about current playing song.")
@music.command(name="nowplaying", aliases=["nop"], usage="music nowplaying", help="Shows the info about current playing song.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -659,7 +675,7 @@ class Music(commands.Cog):
await ctx.send(view=view)
@commands.hybrid_command(name="autoplay", usage="autoplay", help="Toggles autoplay mode.")
@music.command(name="autoplay", usage="music autoplay", help="Toggles autoplay mode.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -679,7 +695,7 @@ class Music(commands.Cog):
)
await ctx.send(view=CV2(f"{TICK} Autoplay {'enabled' if vc.autoplay == wavelink.AutoPlayMode.enabled else 'disabled'} by {ctx.author.mention}."))
@commands.hybrid_command(name="loop", usage="loop", help="Toggles loop mode.")
@music.command(name="loop", usage="music loop", help="Toggles loop mode.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -700,7 +716,7 @@ class Music(commands.Cog):
await ctx.send(view=CV2("I'm not connected to a voice channel."))
@commands.hybrid_command(name="pause", usage="pause", help="Pauses the current song.")
@music.command(name="pause", usage="music pause", help="Pauses the current song.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -721,7 +737,7 @@ class Music(commands.Cog):
else:
await ctx.send(view=CV2(f"{WARNING} Nothing is playing or already paused."))
@commands.hybrid_command(name="resume", usage="resume", help="Resumes the paused song.")
@music.command(name="resume", usage="music resume", help="Resumes the paused song.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -742,7 +758,7 @@ class Music(commands.Cog):
else:
await ctx.send(view=CV2("Player is not paused."))
@commands.hybrid_command(name="skip", usage="skip", help="Skips the current song.")
@music.command(name="skip", usage="music skip", help="Skips the current song.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -767,7 +783,7 @@ class Music(commands.Cog):
else:
await ctx.send(view=CV2(f"{WARNING} No song is playing or in the queue to skip."))
@commands.hybrid_command(name="shuffle", usage="shuffle", help="Shuffles the queue.")
@music.command(name="shuffle", usage="music shuffle", help="Shuffles the queue.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -787,7 +803,7 @@ class Music(commands.Cog):
else:
await ctx.send(view=CV2("Queue is empty."))
@commands.hybrid_command(name="stop", usage="stop", help="Stops the current song and clears the queue.")
@music.command(name="stop", usage="music stop", help="Stops the current song and clears the queue.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -810,7 +826,7 @@ class Music(commands.Cog):
else:
await ctx.send(view=CV2("Nothing is playing to stop."))
@commands.hybrid_command(name="volume", aliases=["vol"], usage="volume <level>", help="Sets the volume of the player.")
@music.command(name="volume", aliases=["vol"], usage="music volume <level>", help="Sets the volume of the player.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -834,7 +850,7 @@ class Music(commands.Cog):
else:
await ctx.send(view=CV2("Bot is not connected to a voice channel."))
@commands.hybrid_command(name="queue", usage="queue", help="Shows the current queue.")
@music.command(name="queue", usage="music queue", help="Shows the current queue.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -860,7 +876,7 @@ class Music(commands.Cog):
ctx=ctx)
await paginator.paginate()
@commands.hybrid_command(name="clearqueue", usage="clearqueue", help="Clears the queue.")
@music.command(name="clearqueue", usage="music clearqueue", help="Clears the queue.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -881,7 +897,7 @@ class Music(commands.Cog):
else:
await ctx.send(view=CV2("No queue to clear."))
@commands.hybrid_command(name="replay", usage="replay", help="Replays the current song.")
@music.command(name="replay", usage="music replay", help="Replays the current song.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -902,7 +918,7 @@ class Music(commands.Cog):
else:
await ctx.send(view=CV2("No track is currently playing."))
@commands.hybrid_command(name="join", aliases=["connect"], usage="join", help="Joins the voice channel.")
@music.command(name="join", aliases=["connect"], usage="music join", help="Joins the voice channel.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -913,7 +929,7 @@ class Music(commands.Cog):
else:
await ctx.send(view=CV2("You need to join a voice channel first."))
@commands.hybrid_command(name="disconnect", aliases=["dc", "leave"], usage="disconnect", help="Disconnects the bot from the voice channel.")
@music.command(name="disconnect", aliases=["dc", "leave"], usage="music disconnect", help="Disconnects the bot from the voice channel.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -933,7 +949,7 @@ class Music(commands.Cog):
else:
await ctx.send(view=CV2("Bot is not connected to any voice channel."))
@commands.hybrid_command(name="seek", usage="seek <percentage>", help="Seeks to a specific percentage of the song.")
@music.command(name="seek", usage="music seek <percentage>", help="Seeks to a specific percentage of the song.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)

View File

@@ -28,7 +28,7 @@ class Nitro(commands.Cog):
ctx = await self.bot.get_context(message)
await self.bot.invoke(ctx)
@commands.hybrid_command(name="nitro")
@commands.command(name="nitro")
async def nitro(self, ctx):
embed = discord.Embed(color=0x2B2D31)
embed.add_field(

View File

@@ -34,7 +34,7 @@ class NotifCommands(commands.Cog):
channel_id INTEGER NOT NULL)""")
await db.commit()
@commands.hybrid_group(invoke_without_command=True)
@commands.group(invoke_without_command=True)
async def setnotif(self, ctx):
view = CV2(
"Notification Commands",

View File

@@ -465,7 +465,7 @@ class NoPrefix(commands.Cog):
await ctx.reply(view=embed)
@commands.hybrid_group(name="autonp", help="Manage auto no-prefix for partner guilds.")
@commands.group(name="autonp", help="Manage auto no-prefix for partner guilds.")
@commands.is_owner()
async def autonp(self, ctx):
if ctx.invoked_subcommand is None:

View File

@@ -160,7 +160,7 @@ class Owner(commands.Cog):
async with db.execute('SELECT id FROM staff') as cursor:
self.staff = {row[0] for row in await cursor.fetchall()}
@commands.hybrid_command(name="staff_add", aliases=["staffadd", "addstaff"], help="Adds a user to the staff list.")
@commands.command(name="staff_add", aliases=["staffadd", "addstaff"], help="Adds a user to the staff list.")
@commands.is_owner()
async def staff_add(self, ctx, user: discord.User):
if user.id in self.staff:
@@ -174,7 +174,7 @@ class Owner(commands.Cog):
sonu2 = discord.Embed(title=f"{TICK} Success", description=f"Added {user} to the staff list.", color=0xFF0000)
await ctx.reply(embed=sonu2, mention_author=False)
@commands.hybrid_command(name="staff_remove", aliases=["staffremove", "removestaff"], help="Removes a user from the staff list.")
@commands.command(name="staff_remove", aliases=["staffremove", "removestaff"], help="Removes a user from the staff list.")
@commands.is_owner()
async def staff_remove(self, ctx, user: discord.User):
if user.id not in self.staff:
@@ -188,7 +188,7 @@ class Owner(commands.Cog):
sonu2 = discord.Embed(title=f"{TICK} Success", description=f"Removed {user} from the staff list.", color=0xFF0000)
await ctx.reply(embed=sonu2, mention_author=False)
@commands.hybrid_command(name="staff_list", aliases=["stafflist", "liststaff", "staffs"], help="Lists all staff members.")
@commands.command(name="staff_list", aliases=["stafflist", "liststaff", "staffs"], help="Lists all staff members.")
@commands.is_owner()
async def staff_list(self, ctx):
if not self.staff:
@@ -202,7 +202,7 @@ class Owner(commands.Cog):
sonu = discord.Embed(title=f"{TICK} {BotName} Staffs", description=f"\n{staff_display}", color=0xFF0000)
await ctx.send(embed=sonu)
@commands.hybrid_command(name="slist")
@commands.command(name="slist")
@commands.check(is_owner_or_staff)
async def _slist(self, ctx):
servers = sorted(self.client.guilds, key=lambda g: g.member_count, reverse=True)
@@ -220,7 +220,7 @@ class Owner(commands.Cog):
await paginator.paginate()
@commands.hybrid_command(name="mutuals", aliases=["mutual"])
@commands.command(name="mutuals", aliases=["mutual"])
@commands.is_owner()
async def mutuals(self, ctx, user: discord.User):
guilds = [guild for guild in self.client.guilds if user in guild.members]
@@ -237,9 +237,9 @@ class Owner(commands.Cog):
ctx=ctx)
await paginator.paginate()
@commands.hybrid_command(name="getinvite", aliases=["gi", "guildinvite"])
@commands.command(name="getinvite", aliases=["gi", "guildinvite"])
@commands.is_owner()
async def getinvite(self, ctx: Context, guild= discord.Guild):
async def getinvite(self, ctx: Context, guild: discord.Guild):
if not guild:
await ctx.send("Invalid server.")
return
@@ -272,13 +272,13 @@ class Owner(commands.Cog):
await ctx.send("Forbidden.")
@commands.hybrid_command(name="reload", help="Restarts the client.")
@commands.command(name="reload", help="Restarts the client.")
@commands.is_owner()
async def _restart(self, ctx: Context):
await ctx.reply(f"{TICK} | **Successfully Restarting {BotName} It Takes 10 seconds**")
restart_program()
@commands.hybrid_command(name="sync", help="Syncs all database.")
@commands.command(name="sync", help="Syncs all database.")
@commands.is_owner()
async def _sync(self, ctx):
await ctx.reply("Syncing...", mention_author=False)
@@ -303,32 +303,33 @@ class Owner(commands.Cog):
@commands.hybrid_command(name="syncslash", help="Re-register all slash (/) commands with Discord.")
@commands.is_owner()
async def syncslash(self, ctx, scope: str = "all"):
"""Force-sync slash commands. scope: all | global | guild"""
"""Force-sync slash commands. scope: all | global | clear"""
if ctx.interaction:
await ctx.defer(ephemeral=True)
from utils.slash_sync import sync_slash_commands
scope = (scope or "all").lower().strip()
if scope == "global":
result = await sync_slash_commands(self.client, guilds=[], global_sync=True)
elif scope == "guild":
if not ctx.guild:
return await ctx.send("Use this in a server, or pass scope `all` / `global`.")
result = await sync_slash_commands(
self.client, guilds=[ctx.guild], global_sync=False
)
result = await sync_slash_commands(self.client, guilds=list(self.client.guilds), global_sync=True)
elif scope in ("guild", "clear"):
# Clear guild-scoped copies in this server (or all) to remove duplicates
targets = [ctx.guild] if ctx.guild and scope == "clear" else list(self.client.guilds)
if not targets:
return await ctx.send("No guild to clear.")
result = await sync_slash_commands(self.client, guilds=targets, global_sync=(scope != "clear"))
else:
result = await sync_slash_commands(self.client)
guild_n = len(result.get("guilds") or {})
cleared = len(result.get("cleared_guilds") or [])
err_n = len(result.get("errors") or [])
await ctx.send(
f"{TICK} Slash sync done — tree `{result.get('local_tree')}`, "
f"global `{result.get('global')}`, guilds `{guild_n}`, errors `{err_n}`."
f"{TICK} Slash sync done — mode `{result.get('mode')}`, "
f"tree `{result.get('local_tree')}`, global `{result.get('global')}`, "
f"cleared guilds `{cleared}`, errors `{err_n}`."
)
@commands.hybrid_command(name="owners")
@commands.command(name="owners")
@commands.is_owner()
async def own_list(self, ctx):
nplist = OWNER_IDS
@@ -351,7 +352,7 @@ class Owner(commands.Cog):
@commands.hybrid_command()
@commands.command()
@commands.is_owner()
async def dm(self, ctx, user: discord.User, *, message: str):
""" DM the user of your choice """
@@ -363,7 +364,7 @@ class Owner(commands.Cog):
@commands.hybrid_group()
@commands.group()
@commands.is_owner()
async def change(self, ctx):
if ctx.invoked_subcommand is None:
@@ -384,7 +385,7 @@ class Owner(commands.Cog):
await ctx.send(err)
@commands.hybrid_command(name="ownerban", aliases=["forceban", "dna"])
@commands.command(name="ownerban", aliases=["forceban", "dna"])
@commands.is_owner()
async def _ownerban(self, ctx: Context, user_id: int, *, reason: str = "No reason provided"):
@@ -418,7 +419,7 @@ class Owner(commands.Cog):
await ctx.reply("User not found in this guild.", mention_author=False, delete_after=3)
await ctx.message.delete()
@commands.hybrid_command(name="ownerunban", aliases=["forceunban"])
@commands.command(name="ownerunban", aliases=["forceunban"])
@commands.is_owner()
async def _ownerunban(self, ctx: Context, user_id: int, *, reason: str = "No reason provided"):
user = self.client.get_user(user_id)
@@ -450,7 +451,7 @@ class Owner(commands.Cog):
@commands.hybrid_command(name="globalunban")
@commands.command(name="globalunban")
@commands.is_owner()
async def globalunban(self, ctx: Context, user: discord.User):
success_guilds = []
@@ -474,7 +475,7 @@ class Owner(commands.Cog):
await ctx.reply(f"{success_message}\n{error_message}", mention_author=False)
@commands.hybrid_command(name="guildban")
@commands.command(name="guildban")
@commands.is_owner()
async def guildban(self, ctx: Context, guild_id: int, user_id: int, *, reason: str = "No reason provided"):
guild = self.client.get_guild(guild_id)
@@ -494,7 +495,7 @@ class Owner(commands.Cog):
else:
await ctx.reply(f"User not found in the specified guild {guild.name}.", mention_author=False)
@commands.hybrid_command(name="guildunban")
@commands.command(name="guildunban")
@commands.is_owner()
async def guildunban(self, ctx: Context, guild_id: int, user_id: int, *, reason: str = "No reason provided"):
guild = self.client.get_guild(guild_id)
@@ -519,7 +520,7 @@ class Owner(commands.Cog):
await ctx.reply(f"An error occurred while unbanning user ID {user_id} in {guild.name}: {str(e)}", mention_author=False)
@commands.hybrid_command(name="leaveguild", aliases=["leavesv"])
@commands.command(name="leaveguild", aliases=["leavesv"])
@commands.is_owner()
async def leave_guild(self, ctx, guild_id: int):
guild = self.client.get_guild(guild_id)
@@ -530,7 +531,7 @@ class Owner(commands.Cog):
await guild.leave()
await ctx.send(f"Left the guild: {guild.name} ({guild.id})")
@commands.hybrid_command(name="guildinfo")
@commands.command(name="guildinfo")
@commands.check(is_owner_or_staff)
async def guild_info(self, ctx, guild_id: int):
guild = self.client.get_guild(guild_id)
@@ -554,7 +555,7 @@ class Owner(commands.Cog):
await ctx.send(embed=embed)
@commands.hybrid_command()
@commands.command()
@commands.is_owner()
async def servertour(self, ctx, time_in_seconds: int, member: discord.Member):
guild = ctx.guild
@@ -621,7 +622,7 @@ class Owner(commands.Cog):
@commands.hybrid_group()
@commands.group()
@commands.check(is_owner_or_staff)
@blacklist_check()
@ignore_check()
@@ -684,7 +685,7 @@ class Owner(commands.Cog):
await ctx.send(embed=embed)
@commands.hybrid_command(name="forcepurgebots",
@commands.command(name="forcepurgebots",
aliases=["fpb"],
help="Clear recently bot messages in channel (Bot owner only)")
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -700,7 +701,7 @@ class Owner(commands.Cog):
await do_removal(ctx, search, predicate)
@commands.hybrid_command(name="forcepurgeuser",
@commands.command(name="forcepurgeuser",
aliases=["fpu"],
help="Clear recent messages of a user in channel (Bot owner only)")
@commands.cooldown(1, 3, commands.BucketType.user)

View File

@@ -25,14 +25,14 @@ class Global(commands.Cog):
self.local_frozen_nicks = {}
self.client.frozen_nicknames = {}
@commands.hybrid_group(name="global", invoke_without_command=True)
@commands.group(name="global", invoke_without_command=True)
@commands.is_owner()
async def global_command(self, ctx: commands.Context):
if ctx.subcommand_passed is None:
await ctx.send_help(ctx.command)
ctx.command.reset_cooldown(ctx)
@commands.hybrid_command(name="GB",help="Bans the user from all mutual guilds.")
@commands.command(name="gb", help="Bans the user from all mutual guilds.")
@commands.is_owner()
async def global_ban(self, ctx: commands.Context, user: discord.User, reason: str = "Severe violations of Discord's terms of service."):
mutual_guilds = [guild for guild in self.client.guilds if guild.get_member(user.id)]
@@ -563,7 +563,7 @@ class Global(commands.Cog):
await ctx.send(f"✅ | Nickname freezing stopped for {user.name}.")
@commands.hybrid_command(name="freezenick", help="Freezes a member's nickname in the current server.")
@commands.command(name="freezenick", help="Freezes a member's nickname in the current server.")
@commands.has_permissions(manage_nicknames=True)
async def freeze_nickname(self, ctx: commands.Context, member: Member, *, nickname: str):
guild_id = ctx.guild.id
@@ -597,7 +597,7 @@ class Global(commands.Cog):
self.client.loop.create_task(monitor_nickname())
@commands.hybrid_command(name="unfreezenick", help="Unfreezes a member's nickname in the current server.")
@commands.command(name="unfreezenick", help="Unfreezes a member's nickname in the current server.")
@commands.has_permissions(manage_nicknames=True)
async def unfreeze_nickname(self, ctx: commands.Context, member: Member):
guild_id = ctx.guild.id

View File

@@ -19,7 +19,7 @@ class QR(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.hybrid_command(
@commands.command(
name="qr",
aliases=["qrcode"],
help="Sends a QR code image.",

View File

@@ -25,7 +25,7 @@ class Slots(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.hybrid_command(aliases=['slot'])
@commands.command(aliases=['slot'])
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)

View File

@@ -23,7 +23,7 @@ class Status(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.hybrid_command(name="status", help="Shows the status of the user in detail.")
@commands.command(name="status", help="Shows the status of the user in detail.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)

View File

@@ -130,7 +130,7 @@ class StickyMessage(commands.Cog):
)
await db.commit()
@commands.hybrid_group(aliases=['sticky', 'sm'], invoke_without_command=True)
@commands.group(aliases=['sticky', 'sm'], invoke_without_command=True)
@commands.has_permissions(manage_messages=True)
async def stickymessage(self, ctx: commands.Context):
if ctx.invoked_subcommand is None:

View File

@@ -51,10 +51,24 @@ TICKET_LIMIT_PER_USER = 3
# --- Database Class ---
class TicketDatabase:
def __init__(self, path):
self.conn = sqlite3.connect(path, check_same_thread=False)
self.path = path
self.conn = None
self._connect()
def _connect(self):
self.conn = sqlite3.connect(self.path, check_same_thread=False)
self.conn.row_factory = sqlite3.Row
self._create_tables()
def _ensure_open(self):
if self.conn is None:
self._connect()
return
try:
self.conn.execute("SELECT 1")
except (sqlite3.ProgrammingError, sqlite3.OperationalError):
self._connect()
def _create_tables(self):
with self.conn:
self.conn.execute("CREATE TABLE IF NOT EXISTS guild_configs (guild_id INTEGER PRIMARY KEY, panel_channel_id INTEGER, logging_channel_id INTEGER, panel_message_id INTEGER, panel_type TEXT, embed_title TEXT, embed_description TEXT, embed_color INTEGER, embed_image_url TEXT, embed_thumbnail_url TEXT, closed_category_id INTEGER)")
@@ -63,13 +77,29 @@ class TicketDatabase:
self.conn.execute("CREATE TABLE IF NOT EXISTS user_ticket_counts (guild_id INTEGER, user_id INTEGER, ticket_count INTEGER DEFAULT 0, PRIMARY KEY (guild_id, user_id))")
def execute(self, q, p=()):
with self.conn: return self.conn.execute(q, p)
self._ensure_open()
with self.conn:
return self.conn.execute(q, p)
def fetchone(self, q, p=()):
cur = self.conn.cursor(); cur.execute(q, p); return cur.fetchone()
self._ensure_open()
cur = self.conn.cursor()
cur.execute(q, p)
return cur.fetchone()
def fetchall(self, q, p=()):
cur = self.conn.cursor(); cur.execute(q, p); return cur.fetchall()
self._ensure_open()
cur = self.conn.cursor()
cur.execute(q, p)
return cur.fetchall()
def close(self):
if self.conn: self.conn.close()
if self.conn is not None:
try:
self.conn.close()
except Exception:
pass
self.conn = None
# --- Utility Functions ---
async def get_or_create_log_channel(db, guild):
@@ -270,13 +300,30 @@ class CategoryConfigView(discord.ui.View):
class TicketCog(commands.Cog, name="Ticket System"):
def __init__(self, bot):
self.bot, self.db = bot, TicketDatabase(DB_PATH)
self.bot.loop.create_task(self.load_persistent_views())
self.bot = bot
self.db = TicketDatabase(DB_PATH)
self._views_task: asyncio.Task | None = None
async def cog_load(self):
# Start after inject succeeds — avoids closed-DB race when add_cog fails/unloads
self._views_task = asyncio.create_task(self.load_persistent_views())
async def load_persistent_views(self):
await self.bot.wait_until_ready()
for config in self.db.fetchall("SELECT guild_id, panel_message_id FROM guild_configs WHERE panel_message_id IS NOT NULL"):
if view := self.create_panel_view(config['guild_id']): self.bot.add_view(view, message_id=config['panel_message_id'])
try:
await self.bot.wait_until_ready()
configs = self.db.fetchall(
"SELECT guild_id, panel_message_id FROM guild_configs WHERE panel_message_id IS NOT NULL"
)
for config in configs:
if view := self.create_panel_view(config["guild_id"]):
self.bot.add_view(view, message_id=config["panel_message_id"])
except asyncio.CancelledError:
raise
except sqlite3.ProgrammingError:
# Cog unloaded / DB closed before ready — ignore
return
except Exception as e:
print(f"Ticket persistent views load failed: {e}")
def create_panel_view(self, guild_id):
config = self.db.fetchone("SELECT panel_type FROM guild_configs WHERE guild_id=?", (guild_id,))
@@ -290,7 +337,10 @@ class TicketCog(commands.Cog, name="Ticket System"):
for c in categories: view.add_item(discord.ui.Button(label=c['name'], style=discord.ButtonStyle(c['button_style']), emoji=c['emoji'], custom_id=f"create_ticket_{c['category_id']}"))
return view
def cog_unload(self): self.db.close()
def cog_unload(self):
if self._views_task and not self._views_task.done():
self._views_task.cancel()
self.db.close()
@commands.Cog.listener()
async def on_interaction(self, inter):
@@ -328,7 +378,12 @@ class TicketCog(commands.Cog, name="Ticket System"):
await ch.send(content=" ".join(pings), embed=ticket_embed, view=TicketActionsView(self, ch.id, cat_id))
await inter.followup.send(f"Your ticket has been successfully created: {ch.mention}", ephemeral=True)
@commands.hybrid_group(name="ticket", description="Main command group for the ticket system.")
@commands.hybrid_group(
name="ticket",
description="Main command group for the ticket system.",
fallback="help",
invoke_without_command=True,
)
@commands.guild_only()
async def ticket(self, ctx):
if ctx.invoked_subcommand is None: await ctx.send_help(ctx.command)

View File

@@ -136,7 +136,7 @@ class Tracking(commands.Cog):
row = await cursor.fetchone()
return row[0] if row else 0
@commands.hybrid_command(aliases=["inv"])
@commands.command(aliases=["inv"])
async def invites(self, ctx, member: discord.Member = None):
member = member or ctx.author
await self.ensure_tables(ctx.guild.id)
@@ -161,7 +161,7 @@ class Tracking(commands.Cog):
)
await ctx.send(view=CV2(f"Invite Log - {member.name}", desc))
@commands.hybrid_command(aliases=["addinvs"])
@commands.command(aliases=["addinvs"])
@commands.has_permissions(administrator=True)
async def addinvites(self, ctx, member: discord.Member, amount: int):
await self.ensure_tables(ctx.guild.id)
@@ -171,7 +171,7 @@ class Tracking(commands.Cog):
await db.commit()
await ctx.send(view=CV2("✅ Success", f"Added **{amount}** invites to {member.mention}."))
@commands.hybrid_command(aliases=["setinvs"])
@commands.command(aliases=["setinvs"])
@commands.has_permissions(administrator=True)
async def setinvites(self, ctx, member: discord.Member, amount: int):
await self.ensure_tables(ctx.guild.id)
@@ -180,7 +180,7 @@ class Tracking(commands.Cog):
await db.commit()
await ctx.send(view=CV2("✅ Success", f"Set invites of {member.mention} to **{amount}**."))
@commands.hybrid_command(aliases=["resetinvs"])
@commands.command(aliases=["resetinvs"])
@commands.has_permissions(administrator=True)
async def resetinvites(self, ctx, member: discord.Member):
await self.ensure_tables(ctx.guild.id)
@@ -189,7 +189,7 @@ class Tracking(commands.Cog):
await db.commit()
await ctx.send(view=CV2("✅ Success", f"Reset invites of {member.mention}."))
@commands.hybrid_command(aliases=["invlb"])
@commands.command(aliases=["invlb"])
async def invitesleaderboard(self, ctx):
await self.ensure_tables(ctx.guild.id)
async with aiosqlite.connect(INVITE_DB) as db:
@@ -208,7 +208,7 @@ class Tracking(commands.Cog):
await ctx.send(view=CV2("📊 Invite Leaderboard", leaderboard))
@commands.hybrid_command(aliases=["invlog"])
@commands.command(aliases=["invlog"])
@commands.has_permissions(administrator=True)
async def invitelogging(self, ctx, channel: discord.TextChannel):
await self.ensure_tables(ctx.guild.id)

View File

@@ -49,7 +49,7 @@ class VanityRoles(commands.Cog):
await db.execute("ALTER TABLE vanity_roles ADD COLUMN current_status TEXT")
await db.commit()
@commands.hybrid_group(name="vanityroles", invoke_without_command=True)
@commands.group(name="vanityroles", invoke_without_command=True)
@blacklist_check()
@ignore_check()
async def vanityroles(self, ctx):

View File

@@ -29,7 +29,7 @@ class Voice(commands.Cog):
self.bot = bot
self.color = 0xFF0000
@commands.hybrid_group(name="voice", invoke_without_command=True, aliases=['vc'])
@commands.group(name="voice", invoke_without_command=True, aliases=['vc'])
@blacklist_check()
@ignore_check()
async def vc(self, ctx: commands.Context):

View File

@@ -78,7 +78,13 @@ class Welcomer(commands.Cog):
""")
await db.commit()
@commands.hybrid_group(invoke_without_command=True, name="greet", help="Shows all the greet commands.")
@commands.hybrid_group(
invoke_without_command=True,
name="greet",
description="Configure welcome/greet messages for new members.",
help="Shows all the greet commands.",
fallback="help",
)
@blacklist_check()
@ignore_check()
async def greet(self, ctx: commands.Context):

View File

@@ -22,7 +22,7 @@ class Youtube(commands.Cog):
self.bot = bot
@commands.hybrid_command(name='yt', aliases=['youtube'])
@commands.command(name='yt', aliases=['youtube'])
async def search_youtube(self, ctx, *, search_query):
query_string = urllib.parse.urlencode({'search_query': search_query})
html_content = urllib.request.urlopen('http://www.youtube.com/results?' + query_string)

View File

@@ -12,40 +12,107 @@
# ╚══════════════════════════════════════════════════════════════════╝
import discord
from utils.emoji import ARROWRED, ZMODULE
from discord.utils import *
from core import Axiom, Cog
from utils.Tools import *
from utils.config import BotName, serverLink
from utils.config import BotName, BRAND_NAME, DISCORD_CLIENT_ID, serverLink
from discord.ext import commands
from discord.ui import Button, View
# HexaHost brand accent (matches dashboard primary/accent)
EMBED_COLOR = 0xA348FF
DASHBOARD_URL = "https://bot.hexahost.de"
class Autorole(Cog):
def __init__(self, bot: Axiom):
self.bot = bot
self.bot = bot
def _invite_url(self) -> str | None:
client_id = DISCORD_CLIENT_ID or (str(self.bot.user.id) if self.bot.user else "")
if not client_id:
return None
return (
f"https://discord.com/oauth2/authorize?client_id={client_id}"
f"&permissions=8&scope=bot%20applications.commands"
)
def _welcome_embed(self, guild: discord.Guild, adder: discord.abc.User) -> discord.Embed:
bot_avatar = guild.me.display_avatar.url if guild.me else (
self.bot.user.display_avatar.url if self.bot.user else None
)
embed = discord.Embed(
title=f"Thanks for adding {BotName}",
description=(
f"Hey {adder.mention}, **{BotName}** is now in **{guild.name}**.\n\n"
"Here's everything you need to get started:"
),
color=EMBED_COLOR,
)
if bot_avatar:
embed.set_author(name=BotName, icon_url=bot_avatar)
embed.set_thumbnail(url=bot_avatar)
embed.add_field(
name="Prefix",
value="Default prefix is `>`\nExample: `>help`",
inline=True,
)
embed.add_field(
name="Slash commands",
value="Type `/` in your server to browse available commands.",
inline=True,
)
embed.add_field(
name="Dashboard",
value=f"Configure modules online at **[bot.hexahost.de]({DASHBOARD_URL})**",
inline=False,
)
embed.add_field(
name="Need help?",
value=f"Join our **[Support Server]({serverLink})** for guides, FAQ and assistance.",
inline=False,
)
embed.set_footer(text=f"Powered by HexaHost · {BRAND_NAME}")
return embed
def _welcome_view(self) -> View:
view = View()
view.add_item(
Button(label="Open Dashboard", style=discord.ButtonStyle.link, url=DASHBOARD_URL)
)
view.add_item(
Button(label="Support Server", style=discord.ButtonStyle.link, url=serverLink)
)
invite = self._invite_url()
if invite:
view.add_item(
Button(label="Add to another server", style=discord.ButtonStyle.link, url=invite)
)
return view
@commands.Cog.listener(name="on_guild_join")
async def send_msg_to_adder(self, guild: discord.Guild):
async for entry in guild.audit_logs(limit=3):
if entry.action == discord.AuditLogAction.bot_add:
embed = discord.Embed(
description=f"{ZMODULE} **Thanks for adding me.**\n\n{ARROWRED} My default prefix is `>`\n{ARROWRED}> Use the `>help` command to see a list of commands\n{ARROWRED} For detailed guides, FAQ and information, visit our **[Support Server](https://discord.gg/hexahost)**",
color=0xFF0000
)
embed.set_thumbnail(url=entry.user.avatar.url if entry.user.avatar else entry.user.default_avatar.url)
embed.set_author(name=f"{guild.name}", icon_url=guild.me.display_avatar.url)
website_button = Button(label='Website', style=discord.ButtonStyle.link, url='https://.vercel.app')
support_button = Button(label='Support', style=discord.ButtonStyle.link, url='https://discord.gg/hexahost')
vote_button = Button(label='Vote for Me', style=discord.ButtonStyle.link, url=f'https://top.gg/bot/{self.bot.user.id}/vote')
view = View()
view.add_item(support_button)
#view.add_item(website_button)
#view.add_item(vote_button)
if guild.icon:
embed.set_author(name=guild.name, icon_url=guild.icon.url)
try:
async for entry in guild.audit_logs(limit=5, action=discord.AuditLogAction.bot_add):
if entry.target is None or entry.user is None:
continue
if entry.target.id != self.bot.user.id:
continue
embed = self._welcome_embed(guild, entry.user)
view = self._welcome_view()
try:
await entry.user.send(embed=embed, view=view)
except discord.Forbidden:
# User has DMs closed — ignore silently
pass
except Exception as e:
print(e)
print(f"Welcome DM failed: {e}")
break
except discord.Forbidden:
# Missing View Audit Log permission
pass
except Exception as e:
print(f"Welcome DM (audit log) failed: {e}")

View File

@@ -99,25 +99,37 @@ Threads : {len(guild.threads)}
await guild.chunk()
embed = discord.Embed(
description=f"{ARROWRED} Prefix For This Server is `>`\n{ARROWRED} Get Started with `>help`\n{ARROWRED} For detailed guides, FAQ & information, visit our **[Support Server](https://discord.gg/hexahost)**",
color=0xFF0000,
title=f"{BRAND_NAME} joined the server",
description=(
f"**{BRAND_NAME}** is ready to use.\n\n"
f"▸ Default prefix: `>`\n"
f"▸ Get started with `>help` or type `/`\n"
f"▸ Dashboard: **[bot.hexahost.de](https://bot.hexahost.de)**\n"
f"▸ Support: **[discord.gg/hexahost](https://discord.gg/hexahost)**"
),
color=0xA348FF,
)
embed.set_author(
name="Thanks for adding me!", icon_url=guild.me.display_avatar.url
name=BRAND_NAME,
icon_url=guild.me.display_avatar.url if guild.me else None,
)
embed.set_footer(
text=f"Powered by {BRAND_NAME}",
)
if guild.icon:
embed.set_thumbnail(url=guild.icon.url)
embed.set_footer(text=f"Powered by HexaHost · {BRAND_NAME}")
if guild.me:
embed.set_thumbnail(url=guild.me.display_avatar.url)
support = Button(
label="Support",
label="Support Server",
style=discord.ButtonStyle.link,
url=f"https://discord.gg/hexahost",
url="https://discord.gg/hexahost",
)
dashboard = Button(
label="Open Dashboard",
style=discord.ButtonStyle.link,
url="https://bot.hexahost.de",
)
view = View()
view.add_item(dashboard)
view.add_item(support)
channel = discord.utils.get(guild.text_channels, name="general")
if not channel:

View File

@@ -25,7 +25,7 @@ class Ban(commands.Cog):
def get_user_avatar(self, user):
return user.avatar.url if user.avatar else user.default_avatar.url
@commands.hybrid_command(
@commands.command(
name="ban",
help="Bans a user from the Server",
usage="ban <member>",

View File

@@ -20,7 +20,7 @@ class Hide(commands.Cog):
self.bot = bot
self.color = discord.Color.from_rgb(255, 0, 0) # Red color for embeds
@commands.hybrid_command(
@commands.command(
name="hide",
help="Hides a channel from the default role (@everyone).",
usage="hide [channel]",

View File

@@ -22,7 +22,7 @@ class Kick(commands.Cog):
# Color is set to Red (255, 0, 0) as requested
self.color = discord.Color.from_rgb(255, 0, 0)
@commands.hybrid_command(
@commands.command(
name="kick",
help="Kicks a member from the server.",
usage="kick <member> [reason]",

View File

@@ -20,7 +20,7 @@ class Lock(commands.Cog):
self.bot = bot
self.color = discord.Color.red()
@commands.hybrid_command(
@commands.command(
name="lock",
description="Locks a channel to prevent sending messages.",
aliases=["lockchannel"]

View File

@@ -89,7 +89,7 @@ class Message(commands.Cog):
self.color = 0xFF0000
@commands.hybrid_group(invoke_without_command=True, aliases=["purge"], help="Clears the messages")
@commands.group(invoke_without_command=True, aliases=["purge"], help="Clears the messages")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -232,7 +232,7 @@ class Message(commands.Cog):
@commands.hybrid_command(name="purgebots",
@commands.command(name="purgebots",
aliases=["cleanup", "pb", "clearbot", "clearbots"],
help="Clear recently bot messages in channel")
@blacklist_check()
@@ -250,7 +250,7 @@ class Message(commands.Cog):
await do_removal(ctx, search, predicate)
@commands.hybrid_command(name="purgeuser",
@commands.command(name="purgeuser",
aliases=["pu", "cu", "clearuser"],
help="Clear recent messages of a user in channel")
@blacklist_check()

View File

@@ -0,0 +1,224 @@
# ╔══════════════════════════════════════════════════════════════════╗
# ║ ║
# ║ +-+-+-+-+-+-+-+-+ ║
# ║ |H|e|x|a|H|o|s|t| ║
# ║ +-+-+-+-+-+-+-+-+ ║
# ║ ║
# ║ © 2026 HexaHost — All Rights Reserved ║
# ║ ║
# ║ discord ── https://discord.gg/hexahost ║
# ║ github ── https://github.com/theoneandonlymace ║
# ║ ║
# ╚══════════════════════════════════════════════════════════════════╝
from __future__ import annotations
import discord
from discord.ext import commands
class ModHub(commands.Cog):
"""Slash/prefix hub that exposes moderation actions under `/mod <subcommand>`."""
def __init__(self, bot):
self.bot = bot
async def _get_cog(self, ctx: commands.Context, name: str):
cog = self.bot.get_cog(name)
if not cog:
await ctx.send(f"{name} module unavailable.")
return None
return cog
@commands.hybrid_group(
name="mod",
description="Moderation commands",
fallback="help",
invoke_without_command=True,
)
@commands.guild_only()
async def mod(self, ctx: commands.Context):
"""Moderation root — use `/mod <subcommand>` or `>mod <subcommand>`."""
if ctx.subcommand_passed is None:
await ctx.send_help(ctx.command)
@mod.command(name="ban", description="Ban a user")
@commands.has_permissions(ban_members=True)
@commands.bot_has_permissions(ban_members=True)
@commands.guild_only()
async def mod_ban(self, ctx: commands.Context, user: discord.User, *, reason: str = None):
cog = await self._get_cog(ctx, "Ban")
if not cog:
return
await cog.ban(ctx, user, reason=reason)
@mod.command(name="unban", description="Unban a user")
@commands.has_permissions(ban_members=True)
@commands.bot_has_permissions(ban_members=True)
@commands.guild_only()
async def mod_unban(self, ctx: commands.Context, user: discord.User, *, reason: str = None):
cog = await self._get_cog(ctx, "Unban")
if not cog:
return
await cog.unban(ctx, user, reason=reason)
@mod.command(name="kick", description="Kick a member")
@commands.has_permissions(kick_members=True)
@commands.bot_has_permissions(kick_members=True)
@commands.guild_only()
async def mod_kick(self, ctx: commands.Context, member: discord.Member, *, reason: str = None):
cog = await self._get_cog(ctx, "Kick")
if not cog:
return
await cog.kick_command(ctx, member, reason=reason)
@mod.command(name="mute", description="Mute / timeout a member")
@commands.has_permissions(moderate_members=True)
@commands.bot_has_permissions(moderate_members=True)
@commands.guild_only()
async def mod_mute(
self,
ctx: commands.Context,
user: discord.Member,
time: str = None,
*,
reason: str = None,
):
cog = await self._get_cog(ctx, "Mute")
if not cog:
return
await cog.mute(ctx, user, time, reason=reason)
@mod.command(name="unmute", description="Remove a timeout from a member")
@commands.has_permissions(moderate_members=True)
@commands.bot_has_permissions(moderate_members=True)
@commands.guild_only()
async def mod_unmute(self, ctx: commands.Context, user: discord.Member):
cog = await self._get_cog(ctx, "Unmute")
if not cog:
return
await cog.unmute(ctx, user)
@mod.command(name="warn", description="Warn a member")
@commands.has_permissions(moderate_members=True)
@commands.guild_only()
async def mod_warn(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
cog = await self._get_cog(ctx, "Warn")
if not cog:
return
await cog.warn(ctx, user, reason=reason)
@mod.command(name="clearwarns", description="Clear warnings for a member")
@commands.has_permissions(moderate_members=True)
@commands.guild_only()
async def mod_clearwarns(self, ctx: commands.Context, user: discord.Member):
cog = await self._get_cog(ctx, "Warn")
if not cog:
return
await cog.clearwarns(ctx, user)
@mod.command(name="lock", description="Lock a channel")
@commands.has_permissions(manage_channels=True)
@commands.bot_has_permissions(manage_channels=True)
@commands.guild_only()
async def mod_lock(self, ctx: commands.Context, channel: discord.TextChannel = None):
cog = await self._get_cog(ctx, "Lock")
if not cog:
return
await cog.lock_command(ctx, channel)
@mod.command(name="unlock", description="Unlock a channel")
@commands.has_permissions(manage_roles=True)
@commands.bot_has_permissions(manage_roles=True)
@commands.guild_only()
async def mod_unlock(self, ctx: commands.Context, channel: discord.TextChannel = None):
cog = await self._get_cog(ctx, "Unlock")
if not cog:
return
await cog.unlock_command(ctx, channel)
@mod.command(name="hide", description="Hide a channel")
@commands.has_permissions(manage_channels=True)
@commands.bot_has_permissions(manage_channels=True)
@commands.guild_only()
async def mod_hide(self, ctx: commands.Context, channel: discord.TextChannel = None):
cog = await self._get_cog(ctx, "Hide")
if not cog:
return
await cog.hide_command(ctx, channel)
@mod.command(name="unhide", description="Unhide a channel")
@commands.has_permissions(manage_channels=True)
@commands.bot_has_permissions(manage_channels=True)
@commands.guild_only()
async def mod_unhide(self, ctx: commands.Context, channel: discord.TextChannel = None):
cog = await self._get_cog(ctx, "Unhide")
if not cog:
return
await cog.unhide_command(ctx, channel)
@mod.command(name="lockall", description="Lock all channels")
@commands.has_permissions(administrator=True)
@commands.guild_only()
async def mod_lockall(self, ctx: commands.Context):
cog = await self._get_cog(ctx, "Moderation")
if not cog:
return
await cog.lockall(ctx)
@mod.command(name="unlockall", description="Unlock all channels")
@commands.has_permissions(administrator=True)
@commands.guild_only()
async def mod_unlockall(self, ctx: commands.Context):
cog = await self._get_cog(ctx, "Moderation")
if not cog:
return
await cog.unlockall(ctx)
@mod.command(name="nuke", description="Nuke the current channel")
@commands.has_permissions(manage_channels=True)
@commands.guild_only()
async def mod_nuke(self, ctx: commands.Context):
cog = await self._get_cog(ctx, "Moderation")
if not cog:
return
await cog._nuke(ctx)
@mod.command(name="slowmode", description="Set channel slowmode")
@commands.has_permissions(manage_messages=True)
@commands.guild_only()
async def mod_slowmode(self, ctx: commands.Context, seconds: int = 0):
cog = await self._get_cog(ctx, "Moderation")
if not cog:
return
await cog._slowmode(ctx, seconds)
@mod.command(name="nick", description="Change a member's nickname")
@commands.has_permissions(manage_nicknames=True)
@commands.bot_has_permissions(manage_nicknames=True)
@commands.guild_only()
async def mod_nick(
self,
ctx: commands.Context,
member: discord.Member,
*,
name: str = None,
):
cog = await self._get_cog(ctx, "Moderation")
if not cog:
return
await cog.changenickname(ctx, member, name=name)
@mod.command(name="audit", description="View recent audit log entries")
@commands.has_permissions(view_audit_log=True)
@commands.bot_has_permissions(view_audit_log=True)
@commands.guild_only()
async def mod_audit(self, ctx: commands.Context, limit: int):
cog = await self._get_cog(ctx, "Moderation")
if not cog:
return
await cog.auditlog(ctx, limit)
async def setup(bot):
await bot.add_cog(ModHub(bot))

View File

@@ -115,18 +115,18 @@ class Moderation(commands.Cog):
@commands.hybrid_command()
@commands.command()
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
async def enlarge(self, ctx, emoji: Union[discord.Emoji, discord.PartialEmoji, str]):
async def enlarge(self, ctx, emoji: str):
url = emoji.url
await ctx.send(url)
@commands.hybrid_command(name="unlockall",
@commands.command(name="unlockall",
help="Unlocks all channels in the Guild.",
usage="unlockall")
@blacklist_check()
@@ -211,7 +211,7 @@ class Moderation(commands.Cog):
@commands.hybrid_command(name="lockall",
@commands.command(name="lockall",
help="locks all the channels in Guild.",
usage="lockall")
@blacklist_check()
@@ -294,7 +294,7 @@ class Moderation(commands.Cog):
await ctx.send(embed=denied, mention_author=False)
@commands.hybrid_command(name="give",
@commands.command(name="give",
help="Gives the mentioned user a role.",
usage="give <user> <role>",
aliases=["addrole"])
@@ -364,7 +364,7 @@ class Moderation(commands.Cog):
@commands.hybrid_command(name="hideall", help="Hides all the channels .",
@commands.command(name="hideall", help="Hides all the channels .",
usage="hideall")
@blacklist_check()
@ignore_check()
@@ -442,7 +442,7 @@ class Moderation(commands.Cog):
icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url)
await ctx.send(embed=denied, mention_author=False)
@commands.hybrid_command(name="unhideall", help="Unhides all the channels in the server.",
@commands.command(name="unhideall", help="Unhides all the channels in the server.",
usage="unhideall")
@blacklist_check()
@ignore_check()
@@ -522,7 +522,7 @@ class Moderation(commands.Cog):
@commands.hybrid_command(
@commands.command(
name="prefix",
aliases=["setprefix", "prefixset"],
help="Allows you to change the prefix of the bot for this server"
@@ -560,7 +560,7 @@ class Moderation(commands.Cog):
@commands.hybrid_command(name="clone", help="Clones a channel.")
@commands.command(name="clone", help="Clones a channel.")
@blacklist_check()
@ignore_check()
@commands.has_permissions(manage_channels=True)
@@ -604,7 +604,7 @@ class Moderation(commands.Cog):
await ctx.send(embed=error)
@commands.hybrid_command(name="nick",
@commands.command(name="nick",
aliases=['setnick'],
help="To change someone's nickname.",
usage="nick [member]")
@@ -680,7 +680,7 @@ class Moderation(commands.Cog):
await ctx.send(embed=error)
@commands.hybrid_command(name="nuke", help="Nukes a channel", usage="nuke")
@commands.command(name="nuke", help="Nukes a channel", usage="nuke")
@blacklist_check()
@ignore_check()
@top_check()
@@ -744,7 +744,7 @@ class Moderation(commands.Cog):
@commands.hybrid_command(name="slowmode",
@commands.command(name="slowmode",
help="Changes the slowmode",
usage="slowmode [seconds]",
aliases=["slow"])
@@ -774,7 +774,7 @@ class Moderation(commands.Cog):
await ctx.send(embed=embed)
@commands.hybrid_command(name="unslowmode",
@commands.command(name="unslowmode",
help="Disables slowmode",
usage="unslowmode",
aliases=["unslow"])
@@ -793,7 +793,7 @@ class Moderation(commands.Cog):
@commands.hybrid_command(aliases=["deletesticker", "removesticker"], description="Delete the sticker from the server")
@commands.command(aliases=["deletesticker", "removesticker"], description="Delete the sticker from the server")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -815,7 +815,7 @@ class Moderation(commands.Cog):
await ctx.reply("Failed to delete the sticker")
@commands.hybrid_command(aliases=["deleteemoji", "removeemoji"], description="Deletes the emoji from the server")
@commands.command(aliases=["deleteemoji", "removeemoji"], description="Deletes the emoji from the server")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@@ -864,13 +864,13 @@ class Moderation(commands.Cog):
@commands.hybrid_command(description="Changes the icon for the role.")
@commands.command(description="Changes the icon for the role.")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 3, commands.BucketType.user)
@commands.has_permissions(administrator=True)
@commands.bot_has_guild_permissions(manage_roles=True)
async def roleicon(self, ctx: commands.Context, role: discord.Role, *, icon: Union[discord.Emoji, discord.PartialEmoji, str] = None):
async def roleicon(self, ctx: commands.Context, role: discord.Role, *, icon: str = None):
if role.position >= ctx.guild.me.top_role.position:
error_embed = discord.Embed(
@@ -976,11 +976,10 @@ class Moderation(commands.Cog):
@commands.hybrid_command(name="unbanall",
@commands.command(name="unbanall",
help="Unbans Everyone In The Guild!",
aliases=['massunban'],
usage="Unbanall",
with_app_command=True)
usage="Unbanall")
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 30, commands.BucketType.user)
@@ -1041,7 +1040,7 @@ class Moderation(commands.Cog):
view.add_item(button1)
await ctx.reply(embed=embed, view=view, mention_author=False)
@commands.hybrid_command(name="audit",
@commands.command(name="audit",
help="See recents audit log action in the server .")
@blacklist_check()
@ignore_check()

View File

@@ -56,7 +56,7 @@ class Role(commands.Cog):
self.color = 0xFF0000
@commands.hybrid_group(name="role",invoke_without_command=True)
@commands.group(name="role",invoke_without_command=True)
@blacklist_check()
@ignore_check()
@commands.cooldown(1, 5, commands.BucketType.user)
@@ -598,7 +598,7 @@ class Role(commands.Cog):
@commands.hybrid_group(name="removerole",invoke_without_command=True,
@commands.group(name="removerole",invoke_without_command=True,
aliases=['rrole'],
help="remove a role from all members .")
@blacklist_check()

View File

@@ -36,7 +36,7 @@ class Snipe(commands.Cog):
'deleted_at': datetime.utcnow()
}
@commands.hybrid_command(name='snipe', help="Shows the most recently deleted message in the channel.")
@commands.command(name='snipe', help="Shows the most recently deleted message in the channel.")
@commands.has_permissions(manage_messages=True)
async def snipe(self, ctx):
# Check if there is a sniped message for the current channel

View File

@@ -132,7 +132,7 @@ class Mute(commands.Cog):
return timedelta(days=time_value), f"{time_value} days"
return None, None
@commands.hybrid_command(
@commands.command(
name="mute",
help="Mutes a user with optional time and reason",
usage="mute <member> [time] [reason]",

View File

@@ -53,7 +53,7 @@ class TopCheck(commands.Cog):
await db.execute("UPDATE topcheck SET enabled = 0 WHERE guild_id = ?", (guild_id,))
await db.commit()
@commands.hybrid_group(
@commands.group(
name="topcheck",
help="Manage topcheck settings for the server.",
invoke_without_command=True)

View File

@@ -132,7 +132,7 @@ class Unban(commands.Cog):
def get_user_avatar(self, user):
return user.avatar.url if user.avatar else user.default_avatar.url
@commands.hybrid_command(
@commands.command(
name="unban",
help="Unbans a user from the Server",
usage="unban <member>",

View File

@@ -20,7 +20,7 @@ class Unhide(commands.Cog):
self.bot = bot
self.color = discord.Color.from_rgb(255,0, 0) # Green color for success
@commands.hybrid_command(
@commands.command(
name="unhide",
help="Unhides a channel for the default role (@everyone).",
usage="unhide [channel]",

View File

@@ -69,7 +69,7 @@ class Unlock(commands.Cog):
self.bot = bot
self.color = discord.Color.from_rgb(255, 0, 0)
@commands.hybrid_command(
@commands.command(
name="unlock",
help="Unlocks a channel to allow sending messages.",
usage="unlock <channel>",

View File

@@ -140,7 +140,7 @@ class Unmute(commands.Cog):
def get_user_avatar(self, user):
return user.avatar.url if user.avatar else user.default_avatar.url
@commands.hybrid_command(
@commands.command(
name="unmute",
help="Unmutes a user from the Server",
usage="unmute <member>",

View File

@@ -94,7 +94,7 @@ class Warn(commands.Cog):
except Exception as e:
print(f"Error during database setup: {e}")
@commands.hybrid_command(
@commands.command(
name="warn",
help="Warn a user in the server",
usage="warn <user> [reason]",
@@ -156,7 +156,7 @@ class Warn(commands.Cog):
await ctx.send(f"An error occurred: {str(e)}")
print(f"Error during warn command: {e}")
@commands.hybrid_command(
@commands.command(
name="clearwarns",
help="Clear all warnings for a user",
aliases=["clearwarn" , "clearwarnings"],

View File

@@ -19,7 +19,7 @@ import aiohttp
import typing
from typing import List
import aiosqlite
from utils.config import OWNER_IDS, BotName
from utils.config import OWNER_IDS, BotName, get_shard_kwargs
from utils import getConfig, updateConfig
from utils.Tools import setup_db
from .Context import Context
@@ -39,6 +39,11 @@ class Axiom(commands.AutoShardedBot):
intents = discord.Intents.all()
intents.presences = True
intents.members = True
shard_kwargs = get_shard_kwargs()
# Allow callers to override shard settings via kwargs
for key in ("shard_count", "shard_ids"):
if key in kwargs:
shard_kwargs[key] = kwargs.pop(key)
super().__init__(command_prefix=self.get_prefix,
case_insensitive=True,
intents=intents,
@@ -48,7 +53,7 @@ class Axiom(commands.AutoShardedBot):
owner_ids=OWNER_IDS,
allowed_mentions=discord.AllowedMentions(
everyone=False, replied_user=False, roles=False),
shard_count=1)
**shard_kwargs)
self.status_index = 0
self.status_list = []
self._slash_synced = False

Binary file not shown.

View File

@@ -0,0 +1,187 @@
# ╔══════════════════════════════════════════════════════════════════╗
# ║ ║
# ║ +-+-+-+-+-+-+-+-+ ║
# ║ |H|e|x|a|H|o|s|t| ║
# ║ +-+-+-+-+-+-+-+-+ ║
# ║ ║
# ║ © 2026 HexaHost — All Rights Reserved ║
# ║ ║
# ║ discord ── https://discord.gg/hexahost ║
# ║ github ── https://github.com/theoneandonlymace ║
# ║ ║
# ╚══════════════════════════════════════════════════════════════════╝
"""Shared AutoMod settings: event IDs, punishments, thresholds."""
from __future__ import annotations
from typing import Any
import aiosqlite
from utils.db_paths import db_path
# API id -> bot DB event name
EVENT_ID_TO_DB: dict[str, str] = {
"anti_spam": "Anti spam",
"anti_caps": "Anti caps",
"anti_link": "Anti link",
"anti_invites": "Anti invites",
"anti_mass_mention": "Anti mass mention",
"anti_emoji_spam": "Anti emoji spam",
"anti_nsfw_link": "Anti NSFW link",
}
# Also accept legacy UI ids
EVENT_ID_ALIASES: dict[str, str] = {
"anti_links": "anti_link",
"anti_mentions": "anti_mass_mention",
}
DB_EVENT_TO_ID: dict[str, str] = {v: k for k, v in EVENT_ID_TO_DB.items()}
VALID_PUNISHMENTS = frozenset({"Mute", "Kick", "Ban"})
PUNISHMENT_ALIASES: dict[str, str] = {
"mute": "Mute",
"kick": "Kick",
"ban": "Ban",
"Mute": "Mute",
"Kick": "Kick",
"Ban": "Ban",
"delete": "Mute", # legacy dashboard — map to Mute
"warn": "Mute",
}
MAX_IGNORED = 10
NSFW_KEYWORDS = [
"porn", "xxx", "adult", "sex", "nsfw", "xnxx", "onlyfans", "brazzers",
"xhamster", "xvideos", "pornhub", "redtube", "livejasmin", "youporn",
"tube8", "pornhat", "swxvid", "ixxx",
]
DEFAULT_THRESHOLDS: dict[str, dict[str, float]] = {
"anti_spam": {"max_messages": 5, "window_seconds": 10, "mute_minutes": 12},
"anti_caps": {"percent": 70, "min_length": 45, "mute_minutes": 1},
"anti_link": {"mute_minutes": 7},
"anti_invites": {"mute_minutes": 12},
"anti_mass_mention": {"max_mentions": 5, "mute_minutes": 3},
"anti_emoji_spam": {"max_emojis": 5, "mute_minutes": 1},
"anti_nsfw_link": {},
}
THRESHOLD_META: dict[str, list[dict[str, Any]]] = {
"anti_spam": [
{"key": "max_messages", "label": "Max messages", "min": 2, "max": 20, "step": 1},
{"key": "window_seconds", "label": "Window (seconds)", "min": 3, "max": 60, "step": 1},
{"key": "mute_minutes", "label": "Mute minutes", "min": 1, "max": 1440, "step": 1},
],
"anti_caps": [
{"key": "percent", "label": "Caps percent", "min": 40, "max": 100, "step": 1},
{"key": "min_length", "label": "Min message length", "min": 10, "max": 200, "step": 1},
{"key": "mute_minutes", "label": "Mute minutes", "min": 1, "max": 1440, "step": 1},
],
"anti_link": [
{"key": "mute_minutes", "label": "Mute minutes", "min": 1, "max": 1440, "step": 1},
],
"anti_invites": [
{"key": "mute_minutes", "label": "Mute minutes", "min": 1, "max": 1440, "step": 1},
],
"anti_mass_mention": [
{"key": "max_mentions", "label": "Max mentions", "min": 2, "max": 20, "step": 1},
{"key": "mute_minutes", "label": "Mute minutes", "min": 1, "max": 1440, "step": 1},
],
"anti_emoji_spam": [
{"key": "max_emojis", "label": "Max emojis", "min": 2, "max": 30, "step": 1},
{"key": "mute_minutes", "label": "Mute minutes", "min": 1, "max": 1440, "step": 1},
],
}
def normalize_event_id(raw: str) -> str | None:
if raw in EVENT_ID_TO_DB:
return raw
if raw in EVENT_ID_ALIASES:
return EVENT_ID_ALIASES[raw]
if raw in DB_EVENT_TO_ID:
return DB_EVENT_TO_ID[raw]
return None
def normalize_punishment(raw: str | None) -> str | None:
if raw is None:
return None
return PUNISHMENT_ALIASES.get(raw) or PUNISHMENT_ALIASES.get(raw.strip())
async def ensure_thresholds_table(db: aiosqlite.Connection) -> None:
await db.execute(
"""
CREATE TABLE IF NOT EXISTS automod_thresholds (
guild_id INTEGER,
event TEXT,
key TEXT,
value REAL,
PRIMARY KEY (guild_id, event, key)
)
"""
)
async def get_thresholds(guild_id: int, event_id: str) -> dict[str, float]:
"""Return merged defaults + DB overrides for an API event id."""
defaults = dict(DEFAULT_THRESHOLDS.get(event_id, {}))
db_event = EVENT_ID_TO_DB.get(event_id)
if not db_event:
return defaults
async with aiosqlite.connect(db_path("automod.db")) as db:
await ensure_thresholds_table(db)
cursor = await db.execute(
"SELECT key, value FROM automod_thresholds WHERE guild_id = ? AND event = ?",
(guild_id, db_event),
)
for key, value in await cursor.fetchall():
defaults[str(key)] = float(value)
return defaults
async def get_all_thresholds(guild_id: int) -> dict[str, dict[str, float]]:
out: dict[str, dict[str, float]] = {
eid: dict(vals) for eid, vals in DEFAULT_THRESHOLDS.items()
}
async with aiosqlite.connect(db_path("automod.db")) as db:
await ensure_thresholds_table(db)
cursor = await db.execute(
"SELECT event, key, value FROM automod_thresholds WHERE guild_id = ?",
(guild_id,),
)
for event, key, value in await cursor.fetchall():
eid = DB_EVENT_TO_ID.get(event)
if not eid:
continue
out.setdefault(eid, {})[str(key)] = float(value)
return out
async def set_thresholds(
db: aiosqlite.Connection,
guild_id: int,
thresholds: dict[str, dict[str, float]],
) -> None:
await ensure_thresholds_table(db)
for event_id, pairs in thresholds.items():
eid = normalize_event_id(event_id)
if not eid or eid == "anti_nsfw_link":
continue
db_event = EVENT_ID_TO_DB[eid]
allowed = DEFAULT_THRESHOLDS.get(eid, {})
for key, value in pairs.items():
if key not in allowed:
continue
await db.execute(
"""
INSERT OR REPLACE INTO automod_thresholds (guild_id, event, key, value)
VALUES (?, ?, ?, ?)
""",
(guild_id, db_event, key, float(value)),
)

View File

@@ -80,6 +80,48 @@ OWNER_IDS_STR: list[str] = [str(i) for i in OWNER_IDS]
# Discord application / OAuth client id (same as dashboard DISCORD_CLIENT_ID)
DISCORD_CLIENT_ID = os.getenv("DISCORD_CLIENT_ID", "").strip()
# ── Discord sharding ──────────────────────────────────────────────────────────
# SHARD_COUNT empty = AutoShardedBot asks Discord for recommended count (in-process).
# SHARD_IDS (e.g. "0,1") + SHARD_COUNT = this process only runs those shards (cluster).
# Primary process runs API / slash sync / emoji sync / stats (shard 0 or override).
def _parse_optional_int(env_key: str) -> int | None:
raw = os.getenv(env_key, "").strip()
if not raw or not raw.isdigit():
return None
return int(raw)
SHARD_COUNT: int | None = _parse_optional_int("SHARD_COUNT")
SHARD_IDS: list[int] = _parse_ids("SHARD_IDS")
def get_shard_kwargs() -> dict:
"""Kwargs for AutoShardedBot.__init__ (omit keys for full auto-sharding)."""
kwargs: dict = {}
if SHARD_COUNT is not None:
kwargs["shard_count"] = SHARD_COUNT
if SHARD_IDS:
if SHARD_COUNT is None:
raise ValueError("SHARD_IDS requires SHARD_COUNT to be set")
kwargs["shard_ids"] = SHARD_IDS
kwargs["shard_count"] = SHARD_COUNT
return kwargs
def _resolve_is_shard_primary() -> bool:
raw = os.getenv("SHARD_PRIMARY", "").strip().lower()
if raw in ("1", "true", "yes", "on"):
return True
if raw in ("0", "false", "no", "off"):
return False
if not SHARD_IDS:
return True
return 0 in SHARD_IDS
IS_SHARD_PRIMARY: bool = _resolve_is_shard_primary()
# Aliases kept for backwards compatibility with files that import these names
BOT_OWNER_IDS = OWNER_IDS
BOT_OWNER_IDS_STR = OWNER_IDS_STR

View File

@@ -12,8 +12,12 @@
# ╚══════════════════════════════════════════════════════════════════╝
"""Register slash (/) commands with Discord.
Global sync alone can take up to ~1 hour to appear in clients.
Guild sync makes them available immediately in every server the bot is in.
IMPORTANT: Never register the same commands both globally AND per-guild —
Discord shows them twice in that server (e.g. /afk, /2048 duplicated).
Modes (SLASH_SYNC_MODE):
global — default. Sync once worldwide; clears leftover guild copies.
guild — instant in connected servers only (no global registration).
"""
from __future__ import annotations
@@ -31,19 +35,38 @@ if TYPE_CHECKING:
logger = logging.getLogger("axiom.slash_sync")
# Discord hard limit for top-level application commands (global or per-guild)
MAX_TOP_LEVEL = 100
def _env_bool(key: str, default: bool) -> bool:
raw = os.getenv(key)
if raw is None:
return default
return raw.strip().lower() in ("1", "true", "yes", "on")
def _parse_guild_ids(raw: str | None) -> list[int]:
if not raw:
return []
out: list[int] = []
for part in raw.split(","):
part = part.strip()
if part.isdigit():
out.append(int(part))
return out
return [int(p.strip()) for p in raw.split(",") if p.strip().isdigit()]
def _sync_mode() -> str:
"""
Resolve sync mode.
- SLASH_SYNC_MODE=global|guild
- Legacy: SLASH_SYNC_ALL_GUILDS / SLASH_GUILD_IDS imply guild mode if set true/nonempty
while SLASH_SYNC_GLOBAL=false
"""
mode = (os.getenv("SLASH_SYNC_MODE") or "").strip().lower()
if mode in ("global", "guild"):
return mode
# Legacy env compatibility — prefer no duplicates (global) unless guild-only requested
if _env_bool("SLASH_SYNC_ALL_GUILDS", False) or _parse_guild_ids(os.getenv("SLASH_GUILD_IDS")):
if not _env_bool("SLASH_SYNC_GLOBAL", True):
return "guild"
return "global"
def _tree_command_count(bot: "commands.Bot") -> int:
@@ -51,12 +74,10 @@ def _tree_command_count(bot: "commands.Bot") -> int:
def _prune_tree_to_limit(bot: "commands.Bot", limit: int = MAX_TOP_LEVEL) -> list[str]:
"""Drop excess top-level app commands so Discord accepts the sync."""
commands_list = list(bot.tree.get_commands())
if len(commands_list) <= limit:
return []
# Prefer keeping groups (more coverage) over single commands; drop from the end
singles = [c for c in commands_list if not isinstance(c, app_commands.Group)]
groups = [c for c in commands_list if isinstance(c, app_commands.Group)]
ordered = groups + singles
@@ -68,6 +89,15 @@ def _prune_tree_to_limit(bot: "commands.Bot", limit: int = MAX_TOP_LEVEL) -> lis
return removed
async def clear_guild_commands(
bot: "commands.Bot",
guild: discord.abc.Snowflake,
) -> None:
"""Remove guild-scoped slash commands so only global ones remain (fixes duplicates)."""
bot.tree.clear_commands(guild=guild)
await bot.tree.sync(guild=guild)
async def sync_guild(
bot: "commands.Bot",
guild: discord.abc.Snowflake,
@@ -86,99 +116,105 @@ async def sync_slash_commands(
global_sync: bool = True,
) -> dict:
"""
Sync application commands.
Sync application commands without creating global+guild duplicates.
Env:
SLASH_SYNC_GLOBAL=true|false (default true)
SLASH_GUILD_IDS=id,id (optional; if set, only these guilds get instant sync)
SLASH_SYNC_ALL_GUILDS=true (default true — sync every connected guild for instant /)
SLASH_SYNC_MODE=global|guild (default: global)
SLASH_GUILD_IDS=id,id (optional guild list for guild mode)
SLASH_CLEAR_GUILD_COMMANDS=true (default true in global mode — removes old guild copies)
"""
env_global = os.getenv("SLASH_SYNC_GLOBAL", "true").strip().lower() in ("1", "true", "yes", "on")
do_global = global_sync and env_global
sync_all = os.getenv("SLASH_SYNC_ALL_GUILDS", "true").strip().lower() in ("1", "true", "yes", "on")
mode = _sync_mode()
clear_guild = _env_bool("SLASH_CLEAR_GUILD_COMMANDS", True)
env_guild_ids = _parse_guild_ids(os.getenv("SLASH_GUILD_IDS"))
removed = _prune_tree_to_limit(bot)
if removed:
logger.warning(
"Pruned %s top-level slash command(s) to stay under Discord's %s limit: %s",
len(removed),
MAX_TOP_LEVEL,
", ".join(removed[:20]) + ("" if len(removed) > 20 else ""),
)
print(
f"\033[33m◈ Slash tree pruned by {len(removed)} (Discord max {MAX_TOP_LEVEL} top-level)\033[0m"
)
local_count = _tree_command_count(bot)
result: dict = {
"mode": mode,
"local_tree": local_count,
"global": None,
"guilds": {},
"cleared_guilds": [],
"errors": [],
"pruned": removed,
}
async def _try_sync(label: str, factory):
try:
synced = await factory()
return synced, None
return await factory(), None
except Exception as e:
detail = str(e)
logger.error("%s failed: %s", label, detail)
dropped = []
for cmd in list(bot.tree.get_commands()):
if cmd.name in detail or getattr(cmd, "qualified_name", "") in detail:
bot.tree.remove_command(cmd.name)
dropped.append(cmd.name)
if dropped:
print(f"\033[33m◈ Removed invalid slash cmd(s) after error: {', '.join(dropped)}\033[0m")
try:
synced = await factory()
return synced, None
except Exception as e2:
return None, f"{label}: {e2}"
logger.error("%s failed: %s", label, e)
return None, f"{label}: {e}"
if do_global:
synced, err = await _try_sync("Global slash sync", lambda: bot.tree.sync())
if err:
result["errors"].append(err)
print(f"\033[31m◈ {err}\033[0m")
else:
result["global"] = len(synced or [])
print(f"◈ Slash sync (global): {result['global']} command(s)")
target_guilds: list[discord.abc.Snowflake] = []
# Resolve target guilds (for guild mode or for clearing duplicates)
if guilds is not None:
target_guilds = list(guilds)
elif env_guild_ids:
target_guilds = [discord.Object(id=gid) for gid in env_guild_ids]
elif sync_all:
else:
target_guilds = list(bot.guilds)
for guild in target_guilds:
gid = getattr(guild, "id", guild)
if mode == "guild":
# Instant per-server only — do NOT also sync globally (would duplicate)
for guild in target_guilds:
gid = getattr(guild, "id", guild)
def _make_factory(g=guild):
async def _factory():
bot.tree.copy_global_to(guild=g)
return await bot.tree.sync(guild=g)
return _factory
def _make_factory(g=guild):
async def _factory():
bot.tree.copy_global_to(guild=g)
return await bot.tree.sync(guild=g)
synced, err = await _try_sync(f"Guild {gid} slash sync", _make_factory())
if err:
result["errors"].append(err)
print(f"\033[31m◈ {err}\033[0m")
else:
result["guilds"][str(gid)] = len(synced or [])
print(f"◈ Slash sync (guild {gid}): {result['guilds'][str(gid)]} command(s) — instant")
await asyncio.sleep(0.35)
return _factory
synced, err = await _try_sync(f"Guild {gid} slash sync", _make_factory())
if err:
result["errors"].append(err)
print(f"\033[31m◈ {err}\033[0m")
else:
result["guilds"][str(gid)] = len(synced or [])
print(f"◈ Slash sync (guild {gid}): {result['guilds'][str(gid)]} command(s)")
await asyncio.sleep(0.35)
else:
# Global mode — one registry worldwide
if global_sync:
synced, err = await _try_sync("Global slash sync", lambda: bot.tree.sync())
if err:
result["errors"].append(err)
print(f"\033[31m◈ {err}\033[0m")
else:
result["global"] = len(synced or [])
print(f"◈ Slash sync (global): {result['global']} command(s)")
# Remove guild-scoped copies left from older “sync both” runs
if clear_guild and target_guilds:
for guild in target_guilds:
gid = getattr(guild, "id", guild)
def _make_clear(g=guild):
async def _factory():
await clear_guild_commands(bot, g)
return []
return _factory
_, err = await _try_sync(f"Clear guild {gid} slash cmds", _make_clear())
if err:
result["errors"].append(err)
print(f"\033[31m◈ {err}\033[0m")
else:
result["cleared_guilds"].append(str(gid))
print(f"◈ Cleared guild-scoped slash cmds for {gid} (removes duplicates)")
await asyncio.sleep(0.35)
prefix_count = len(list(bot.commands))
print(
f"Synced Total {prefix_count} Client Commands and "
f"{result.get('global') or local_count} Slash Commands"
+ (f" (guild-synced: {len(result['guilds'])})" if result["guilds"] else "")
f"{result.get('global') or local_count} Slash Commands "
f"[mode={mode}]"
)
return result

View File

@@ -22,7 +22,11 @@ const AutomodForm = dynamic(() => import("@/components/dashboard/automod-form").
});
export default async function AutomodPage({ params }: { params: { guildId: string } }) {
const config = await api.getAutomod(params.guildId);
const [config, channels, roles] = await Promise.all([
api.getAutomod(params.guildId),
api.getChannels(params.guildId),
api.getRoles(params.guildId),
]);
if (!config) return null;
@@ -38,7 +42,12 @@ export default async function AutomodPage({ params }: { params: { guildId: strin
</div>
</div>
<AutomodForm initialConfig={config} guildId={params.guildId} />
<AutomodForm
initialConfig={config}
guildId={params.guildId}
channels={channels || []}
roles={roles || []}
/>
</div>
);
}

View File

@@ -14,17 +14,20 @@
*/
"use client";
import React, { useState } from "react";
import {
import React, { useMemo, useState } from "react";
import {
Zap,
Type,
Link as LinkIcon,
MessageSquare,
UserMinus,
ShieldAlert,
Smile,
EyeOff,
Gavel,
RefreshCcw,
Save
Save,
Info,
Hash,
} from "lucide-react";
import { toast } from "sonner";
import { api } from "@/lib/api";
@@ -32,63 +35,162 @@ import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Select } from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { AutomodConfig } from "@/types/api";
import {
AutomodConfig,
AutomodEventConfig,
DiscordChannel,
DiscordRole,
} from "@/types/api";
const PUNISHMENT_OPTIONS = [
{ value: "delete", label: "Delete Message" },
{ value: "warn", label: "Warn User" },
{ value: "mute", label: "Mute User" },
{ value: "kick", label: "Kick User" },
{ value: "ban", label: "Ban User" },
{ value: "Mute", label: "Mute" },
{ value: "Kick", label: "Kick" },
{ value: "Ban", label: "Ban" },
];
const RULES = [
{ id: 'anti_spam', name: 'Anti Spam', desc: 'Detects and removes repetitive messages or rapid firing.', icon: Zap },
{ id: 'anti_caps', name: 'Anti Caps', desc: 'Prevents excessive use of uppercase letters.', icon: Type },
{ id: 'anti_links', name: 'Anti Links', desc: 'Blocks unauthorized external links in channels.', icon: LinkIcon },
{ id: 'anti_invites', name: 'Anti Invites', desc: 'Automatically removes Discord server invite links.', icon: MessageSquare },
{ id: 'anti_mentions', name: 'Anti Mass Mention', desc: 'Protects against mentioned spam (@everyone, @here).', icon: UserMinus },
{ id: "anti_spam", name: "Anti Spam", desc: "Repetitive messages or rapid firing.", icon: Zap },
{ id: "anti_caps", name: "Anti Caps", desc: "Excessive uppercase letters.", icon: Type },
{ id: "anti_link", name: "Anti Links", desc: "Unauthorized external links.", icon: LinkIcon },
{ id: "anti_invites", name: "Anti Invites", desc: "Discord invite links.", icon: MessageSquare },
{ id: "anti_mass_mention", name: "Anti Mass Mention", desc: "Too many user mentions.", icon: UserMinus },
{ id: "anti_emoji_spam", name: "Anti Emoji Spam", desc: "Too many emojis in one message.", icon: Smile },
{ id: "anti_nsfw_link", name: "Anti NSFW Links", desc: "Discord AutoMod keyword filter (blocks message).", icon: EyeOff },
];
function buildInitialEvents(config: AutomodConfig): Record<string, AutomodEventConfig> {
const events: Record<string, AutomodEventConfig> = { ...(config.events || {}) };
for (const rule of RULES) {
if (!events[rule.id]) {
const legacy = config.punishments?.[rule.id];
events[rule.id] = {
enabled: legacy !== undefined,
punishment:
rule.id === "anti_nsfw_link"
? "block_message"
: legacy === "mute" || legacy === "Mute"
? "Mute"
: legacy === "kick" || legacy === "Kick"
? "Kick"
: legacy === "ban" || legacy === "Ban"
? "Ban"
: "Mute",
readonly_punishment: rule.id === "anti_nsfw_link",
};
}
}
return events;
}
interface AutomodFormProps {
initialConfig: AutomodConfig;
guildId: string;
channels: DiscordChannel[];
roles: DiscordRole[];
}
export function AutomodForm({ initialConfig, guildId }: AutomodFormProps) {
const [config, setConfig] = useState<AutomodConfig>(initialConfig);
export function AutomodForm({ initialConfig, guildId, channels, roles }: AutomodFormProps) {
const [enabled, setEnabled] = useState(initialConfig.enabled);
const [events, setEvents] = useState(() => buildInitialEvents(initialConfig));
const [thresholds, setThresholds] = useState<Record<string, Record<string, number>>>(
() => ({ ...(initialConfig.thresholds || {}) })
);
const [ignoredRoles, setIgnoredRoles] = useState<number[]>(initialConfig.ignored_roles || []);
const [ignoredChannels, setIgnoredChannels] = useState<number[]>(
initialConfig.ignored_channels || []
);
const [loggingChannel, setLoggingChannel] = useState<string>(
initialConfig.logging_channel ? String(initialConfig.logging_channel) : ""
);
const [saving, setSaving] = useState(false);
const handleToggle = (key: string) => {
setConfig({
...config,
enabled: key === 'master' ? !config.enabled : config.enabled,
});
const maxIgnored = initialConfig.limits?.max_ignored_roles ?? 10;
const thresholdMeta = initialConfig.threshold_meta || {};
const textChannels = useMemo(
() =>
channels.filter(
(c) => c.type === "0" || c.type === "text" || !c.type
),
[channels]
);
const channelOptions = useMemo(
() => [
{ value: "", label: "No log channel" },
...textChannels.map((c) => ({ value: String(c.id), label: `#${c.name}` })),
],
[textChannels]
);
const toggleIgnore = (
list: number[],
setList: (v: number[]) => void,
id: number,
cap: number
) => {
if (list.includes(id)) {
setList(list.filter((x) => x !== id));
return;
}
if (list.length >= cap) {
toast.error(`Maximum ${cap} entries allowed`);
return;
}
setList([...list, id]);
};
const handlePunishmentChange = (ruleId: string, value: string) => {
const newPunishments = { ...config.punishments };
newPunishments[ruleId] = value;
setConfig({ ...config, punishments: newPunishments });
const setEventEnabled = (ruleId: string, on: boolean) => {
setEvents((prev) => ({
...prev,
[ruleId]: {
...prev[ruleId],
enabled: on,
punishment:
ruleId === "anti_nsfw_link"
? "block_message"
: prev[ruleId]?.punishment || "Mute",
readonly_punishment: ruleId === "anti_nsfw_link",
},
}));
};
const setPunishment = (ruleId: string, value: string) => {
setEvents((prev) => ({
...prev,
[ruleId]: { ...prev[ruleId], enabled: true, punishment: value },
}));
};
const setThresholdValue = (ruleId: string, key: string, value: number) => {
setThresholds((prev) => ({
...prev,
[ruleId]: { ...(prev[ruleId] || {}), [key]: value },
}));
};
const handleSave = async () => {
setSaving(true);
const payload = {
enabled,
events,
thresholds,
ignored_roles: ignoredRoles,
ignored_channels: ignoredChannels,
logging_channel: loggingChannel ? Number(loggingChannel) : 0,
clear_logging: !loggingChannel,
};
const promise = api.updateAutomod(guildId, {
enabled: config.enabled,
punishments: config.punishments,
});
const promise = api.updateAutomod(guildId, payload);
toast.promise(promise, {
loading: 'Saving configuration...',
success: 'Configuration saved successfully!',
error: (err) => err.message || 'Failed to update settings',
loading: "Saving configuration...",
success: "Configuration saved successfully!",
error: (err) => err.message || "Failed to update settings",
});
try {
await promise;
} catch (err: any) {
} catch (err) {
console.error(err);
} finally {
setSaving(false);
@@ -100,78 +202,127 @@ export function AutomodForm({ initialConfig, guildId }: AutomodFormProps) {
<div className="lg:col-span-3 space-y-6">
<div className="bg-[#141B2D] border border-slate-800 rounded-3xl overflow-hidden shadow-xl">
<div className="p-8 space-y-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-black uppercase text-slate-500 tracking-widest">Master Control</h3>
<Switch
checked={config.enabled}
onCheckedChange={() => handleToggle('master')}
/>
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-black uppercase text-slate-500 tracking-widest">
Master Control
</h3>
<Switch checked={enabled} onCheckedChange={setEnabled} />
</div>
<div className="space-y-3 pb-4 border-b border-slate-800">
<label className="text-xs font-bold uppercase text-slate-500 tracking-wider flex items-center gap-2">
<Hash className="h-3.5 w-3.5" /> Logging Channel
</label>
<Select
value={loggingChannel}
onValueChange={setLoggingChannel}
options={channelOptions}
placeholder="Select channel"
className="max-w-md"
disabled={!enabled}
/>
</div>
<div className="space-y-4">
{RULES.map((rule) => {
const isEnabled = config.enabled && config.punishments?.[rule.id] !== undefined;
const cfg = events[rule.id] || { enabled: false, punishment: "Mute" };
const isOn = enabled && cfg.enabled;
const meta = thresholdMeta[rule.id] || [];
const isNsfw = rule.id === "anti_nsfw_link";
return (
<div
<div
key={rule.id}
className={cn(
"p-6 rounded-2xl border transition-all duration-300",
config.enabled ? "bg-slate-900/40 border-slate-800" : "bg-slate-900/10 border-slate-900 opacity-40 grayscale"
enabled
? "bg-slate-900/40 border-slate-800"
: "bg-slate-900/10 border-slate-900 opacity-40 grayscale"
)}
>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-6">
<div className="flex items-center gap-4">
<div className={cn(
"h-12 w-12 rounded-xl flex items-center justify-center transition-colors",
isEnabled ? "bg-primary/20 text-primary" : "bg-slate-800 text-slate-500"
)}>
<div
className={cn(
"h-12 w-12 rounded-xl flex items-center justify-center transition-colors",
isOn ? "bg-primary/20 text-primary" : "bg-slate-800 text-slate-500"
)}
>
<rule.icon className="h-6 w-6" />
</div>
<div>
<h3 className="font-bold text-white">{rule.name}</h3>
<p className="text-xs text-slate-500 max-w-xs">{rule.desc}</p>
<p className="text-xs text-slate-500 max-w-sm">{rule.desc}</p>
</div>
</div>
<div className="flex items-center gap-4">
{isEnabled && (
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-right-2 duration-300">
<Gavel className="h-4 w-4 text-primary" />
<Select
value={config.punishments[rule.id] || "delete"}
onValueChange={(val) => handlePunishmentChange(rule.id, val)}
options={PUNISHMENT_OPTIONS}
className="w-40"
/>
</div>
)}
<div className="h-8 w-[1px] bg-slate-800 hidden sm:block" />
<Switch
disabled={!config.enabled}
checked={config.punishments?.[rule.id] !== undefined}
onCheckedChange={() => {
const newPunishments = { ...config.punishments };
if (newPunishments[rule.id]) {
delete newPunishments[rule.id];
} else {
newPunishments[rule.id] = "delete";
}
setConfig({...config, punishments: newPunishments});
}}
{isOn && !isNsfw && (
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-right-2 duration-300">
<Gavel className="h-4 w-4 text-primary" />
<Select
value={cfg.punishment || "Mute"}
onValueChange={(val) => setPunishment(rule.id, val)}
options={PUNISHMENT_OPTIONS}
className="w-36"
/>
</div>
)}
{isOn && isNsfw && (
<span className="text-xs text-slate-400 font-mono">block_message</span>
)}
<div className="h-8 w-[1px] bg-slate-800 hidden sm:block" />
<Switch
disabled={!enabled}
checked={!!cfg.enabled}
onCheckedChange={(on) => setEventEnabled(rule.id, on)}
/>
</div>
</div>
{isOn && meta.length > 0 && (
<div className="mt-5 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 pt-4 border-t border-slate-800/80">
{meta.map((field) => {
const val =
thresholds[rule.id]?.[field.key] ??
field.min;
return (
<div key={field.key} className="space-y-2">
<div className="flex justify-between text-xs">
<span className="text-slate-400">{field.label}</span>
<span className="text-primary font-mono">{val}</span>
</div>
<input
type="range"
min={field.min}
max={field.max}
step={field.step}
value={val}
onChange={(e) =>
setThresholdValue(rule.id, field.key, Number(e.target.value))
}
className="w-full accent-primary"
/>
</div>
);
})}
</div>
)}
</div>
);
})}
</div>
<Button
<Button
onClick={handleSave}
disabled={saving}
className="w-full h-14 text-base font-bold gap-2"
>
{saving ? <RefreshCcw className="h-5 w-5 animate-spin" /> : <Save className="h-5 w-5" />}
{saving ? (
<RefreshCcw className="h-5 w-5 animate-spin" />
) : (
<Save className="h-5 w-5" />
)}
Save Moderation Rules
</Button>
</div>
@@ -179,28 +330,87 @@ export function AutomodForm({ initialConfig, guildId }: AutomodFormProps) {
</div>
<div className="space-y-6">
<div className="bg-[#141B2D] border border-slate-800 rounded-3xl p-6">
<h3 className="text-sm font-black uppercase text-slate-500 tracking-widest mb-4">Logging Level</h3>
<div className="space-y-3">
<div className="p-3 bg-slate-900/50 rounded-xl border border-white/5 flex items-center justify-between">
<span className="text-sm text-slate-400">Log Channel</span>
<span className="text-xs font-mono text-primary">#{config.logging_channel || 'None'}</span>
</div>
<p className="text-[10px] text-slate-500 italic text-center">Mod logs are automatically sent to the configured channel.</p>
</div>
</div>
<div className="bg-[#141B2D] border border-slate-800 rounded-3xl p-6">
<h3 className="text-sm font-black uppercase text-slate-500 tracking-widest mb-2">
Ignored Roles
</h3>
<p className="text-[11px] text-slate-500 mb-3">
Up to {maxIgnored} roles bypass Automod filters.
</p>
<div className="max-h-48 overflow-y-auto space-y-1 pr-1">
{roles
.filter((r) => r.name !== "@everyone")
.map((role) => {
const id = Number(role.id);
const checked = ignoredRoles.includes(id);
return (
<button
key={role.id}
type="button"
disabled={!enabled}
onClick={() =>
toggleIgnore(ignoredRoles, setIgnoredRoles, id, maxIgnored)
}
className={cn(
"w-full text-left px-3 py-2 rounded-lg text-sm border transition-colors",
checked
? "border-primary/40 bg-primary/10 text-white"
: "border-transparent bg-slate-900/40 text-slate-400 hover:border-slate-700"
)}
>
{role.name}
</button>
);
})}
</div>
</div>
<div className="bg-gradient-to-br from-primary/10 to-transparent border border-primary/20 rounded-3xl p-6 relative overflow-hidden group">
<div className="absolute -right-4 -top-4 opacity-[0.03] group-hover:scale-110 transition-transform">
<ShieldAlert className="h-32 w-32 text-white" />
<div className="bg-[#141B2D] border border-slate-800 rounded-3xl p-6">
<h3 className="text-sm font-black uppercase text-slate-500 tracking-widest mb-2">
Ignored Channels
</h3>
<p className="text-[11px] text-slate-500 mb-3">
Up to {maxIgnored} channels are skipped by filters.
</p>
<div className="max-h-48 overflow-y-auto space-y-1 pr-1">
{textChannels.map((ch) => {
const id = Number(ch.id);
const checked = ignoredChannels.includes(id);
return (
<button
key={ch.id}
type="button"
disabled={!enabled}
onClick={() =>
toggleIgnore(ignoredChannels, setIgnoredChannels, id, maxIgnored)
}
className={cn(
"w-full text-left px-3 py-2 rounded-lg text-sm border transition-colors",
checked
? "border-primary/40 bg-primary/10 text-white"
: "border-transparent bg-slate-900/40 text-slate-400 hover:border-slate-700"
)}
>
#{ch.name}
</button>
);
})}
</div>
</div>
<div className="bg-[#141B2D] border border-slate-800 rounded-3xl p-6">
<div className="flex items-start gap-3">
<Info className="h-5 w-5 text-primary shrink-0 mt-0.5" />
<div>
<h3 className="text-sm font-bold text-white mb-1">How it works</h3>
<p className="text-xs text-slate-400 leading-relaxed">
Enable rules, pick Mute / Kick / Ban, and tune thresholds. NSFW uses Discord&apos;s
native AutoMod and always blocks the message. Prefix commands (
<code className="text-primary">automod</code>) stay in sync with these settings.
</p>
</div>
<h3 className="text-sm font-bold text-white mb-2">Automod AI</h3>
<p className="text-xs text-slate-400 leading-relaxed mb-4">Our neural network analyzes message context to prevent false positives.</p>
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-primary" />
<span className="text-[10px] font-black uppercase text-primary">V2 Active</span>
</div>
</div>
</div>
</div>
</div>
</div>
);

View File

@@ -123,7 +123,7 @@ export const api = {
}),
getAutomod: (guildId: string) => request<AutomodConfig>(`/guilds/${guildId}/automod`),
updateAutomod: (guildId: string, data: Partial<AutomodConfig>) =>
updateAutomod: (guildId: string, data: AutomodUpdate) =>
request<{ status: string }>(`/guilds/${guildId}/automod`, {
method: "PATCH",
body: JSON.stringify(data),

File diff suppressed because one or more lines are too long

View File

@@ -53,13 +53,34 @@ export interface PrefixConfig {
prefix: string;
}
export interface AutomodEventConfig {
enabled: boolean;
punishment?: string | null;
readonly_punishment?: boolean;
}
export interface AutomodThresholdMeta {
key: string;
label: string;
min: number;
max: number;
step: number;
}
export interface AutomodConfig {
guild_id: number;
enabled: boolean;
punishments: Record<string, string>;
events: Record<string, AutomodEventConfig>;
thresholds: Record<string, Record<string, number>>;
threshold_meta: Record<string, AutomodThresholdMeta[]>;
ignored_roles: number[];
ignored_channels: number[];
logging_channel: number | null;
limits?: {
max_ignored_roles: number;
max_ignored_channels: number;
};
}
export interface TicketCategory {
@@ -124,9 +145,12 @@ export interface PrefixUpdate {
export interface AutomodUpdate {
enabled?: boolean;
punishments?: Record<string, string>;
events?: Record<string, AutomodEventConfig>;
thresholds?: Record<string, Record<string, number>>;
ignored_roles?: number[];
ignored_channels?: number[];
logging_channel?: number;
logging_channel?: number | null;
clear_logging?: boolean;
}
export interface LevelingUpdate {

View File

@@ -47,9 +47,13 @@ export interface AutomodConfig {
guild_id: number;
enabled: boolean;
punishments: Record<string, string>;
events?: Record<string, { enabled: boolean; punishment?: string | null; readonly_punishment?: boolean }>;
thresholds?: Record<string, Record<string, number>>;
threshold_meta?: Record<string, { key: string; label: string; min: number; max: number; step: number }[]>;
ignored_roles: number[];
ignored_channels: number[];
logging_channel: number | null;
limits?: { max_ignored_roles: number; max_ignored_channels: number };
}
export interface TicketConfig {