commit fdfc8de44d090c772c880eeccc7b919a69d16f5d Author: TheOnlyMace <0815cracky@gmail.com> Date: Tue Jul 21 16:00:52 2026 +0200 Update configuration and README for improved setup and security; disable default API and emoji sync, and enhance Discord permission checks in API routes. Co-authored-by: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5f2d91 --- /dev/null +++ b/.gitignore @@ -0,0 +1,66 @@ +/myenv +dashboard/node_modules +dashboard/.next + + +# Ignore Python cache +__pycache__/ +*.py[cod] +*$py.class + +# Ignore virtual environments +venv/ +.venv/ +env/ +ENV/ + +# Ignore IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# Ignore OS files +.DS_Store +Thumbs.db + +# Ignore logs +*.log + +# Ignore build outputs +dist/ +build/ + +# Ignore temporary files +*.tmp +*.temp + +# Ignore Python packaging +*.egg-info/ +.Python + +# Ignore Jupyter Notebook checkpoints +.ipynb_checkpoints/ + +# Ignore environment variables +.env +.env.local +.env.*.local + +# Ignore node_modules +node_modules/ +package-lock.json + +# Ignore Next.js build output +.next/ +out/ + +# Ignore Python cache +__pycache__/ +*.pyc +*.pyo +*.pyd +*.zip +*.rar + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5173ffd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 CodeX Devs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d13732 --- /dev/null +++ b/README.md @@ -0,0 +1,425 @@ +
+ +``` +███████╗██╗ ██╗██████╗ ██████╗ ██╗ ██╗ +╚══███╔╝╚██╗ ██╔╝██╔══██╗██╔═══██╗╚██╗██╔╝ + ███╔╝ ╚████╔╝ ██████╔╝██║ ██║ ╚███╔╝ + ███╔╝ ╚██╔╝ ██╔══██╗██║ ██║ ██╔██╗ +███████╗ ██║ ██║ ██║╚██████╔╝██╔╝ ██╗ +╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ +``` + +

A feature-rich Discord bot paired with a sleek Next.js dashboard

+ + + +

+ + + + +

+

+ + + + +

+ +
+ +--- + +## ✦ Overview + +ZyroX is a fully-featured Discord bot with a modern web dashboard for managing everything from antinuke to music. Built on `discord.py v2`, `FastAPI`, and `Next.js 14` with Tailwind CSS. + +``` +ZyroX-CV2-With-Dashboard/ +├── 🤖 bot/ Python Discord bot + FastAPI backend +│ ├── api/ Dashboard REST API (FastAPI) +│ ├── cogs/ All bot features (commands, events, antinuke, automod…) +│ ├── core/ Bot client, context, cog base +│ ├── utils/ Shared utilities (emoji, tools, sync, cloudflare tunnel…) +│ ├── games/ Standalone game modules +│ ├── assets/ Fonts, backgrounds, GIFs +│ └── CodeX.py Entry point +│ +└── 🌐 dashboard/ Next.js frontend + ├── app/ App Router pages & API routes + ├── components/ Reusable UI components + ├── hooks/ Custom React hooks + ├── lib/ API helpers & utilities + └── types/ TypeScript type definitions +``` + +--- + +## ✦ Features + + + + + + + + + + + + + + +
+ +**🛡️ Security** +- Antinuke — ban, kick, channel & role flood, webhook abuse, bot adds, prune +- Automod — spam, caps, links, invites, mass mentions, emoji spam +- Anti-member update protection +- Whitelist / unwhitelist system +- Emergency lockdown mode + + + +**🎵 Music** +- Lavalink v4 powered playback +- YouTube, SoundCloud, JioSaavn search +- Queue, loop, autoplay, shuffle +- Seek, rewind, forward controls +- Fully configurable via `.env` + +
+ +**⚙️ Management** +- Moderation — ban, kick, mute, warn, lock, jail, and more +- Full logging system +- Reaction roles, vanity roles, invite tracker +- Tickets, giveaways, verification +- Join-to-create voice channels + + + +**🌐 Dashboard** +- Discord OAuth2 login +- Per-server settings management +- Live bot stats & metrics +- Fully branded & customisable +- HTTPS via Cloudflare Tunnel (permanent URL) +- Deploys to Vercel in minutes + +
+ +**🎉 Engagement** +- Leveling & XP system with leaderboard +- Birthday tracker +- 12+ mini-games (chess, battleship, wordle, 2048…) +- AFK system, autorole, autoresponder, sticky messages +- Counting, blackjack, slots, booster perks + + + +**🔧 Developer** +- Application emoji auto-sync on startup +- Jishaku eval support +- Slash + prefix commands +- FastAPI backend with API key auth + rate limiting +- Cloudflare Tunnel — unlimited bandwidth, permanent URL, zero system installs +- CodeX Devs watermark on every source file + +
+ +--- + +## ✦ Prerequisites + +| Requirement | Version / Notes | +|---|---| +| Python | 3.10 or higher | +| Node.js | 18 or higher | +| Lavalink node | v4 | +| Discord bot token | — | +| Discord OAuth app | for dashboard login | +| Cloudflare account (free) | for HTTPS tunnel | + +--- + +## ✦ Bot Setup + +**1 — Clone the repo** + +```bash +git clone https://github.com/RayExo/ZyroX-CV2-With-Dashboard +cd ZyroX-CV2-With-Dashboard/bot +``` + +**2 — Install dependencies** + +```bash +python -m venv .venv + +# Windows +.venv\Scripts\activate + +# Linux / macOS +source .venv/bin/activate + +pip install -r requirements.txt +``` + +**3 — Configure the environment** + +Copy `.env.example` to `.env` and fill in the values: + +```env +# ── Core ────────────────────────────────────────────────────────── +TOKEN = your_discord_bot_token +brand_name = 'ZyroX' + +# ── Owner IDs (REQUIRED — your Discord user ID) ─────────────────── +OWNER_IDS = YOUR_DISCORD_USER_ID_HERE + +# ── Lavalink ────────────────────────────────────────────────────── +LAVALINK_HOST = "your-lavalink-host" +LAVALINK_PASSWORD = "your_password" +LAVALINK_SECURE = "true" +LAVALINK_PORT = "" + +# ── Emoji Sync ──────────────────────────────────────────────────── +EMOJI_SYNC = "false" + +# ── API / Dashboard Backend ─────────────────────────────────────── +API_ENABLED = "false" +API_HOST = "127.0.0.1" +API_PORT = "8000" +DASHBOARD_API_KEY = "change_this_to_a_strong_secret" +DASHBOARD_ADMIN_IDS = YOUR_DISCORD_USER_ID_HERE +CORS_ORIGINS = "" + +# ── Cloudflare Tunnel (optional) ────────────────────────────────── +TUNNEL_ENABLED = "false" +CF_TUNNEL_TOKEN = "your_tunnel_token" +CF_TUNNEL_URL = "https://api.yourdomain.com" + +# ── Webhooks (optional — leave unset to disable) ────────────────── +# CMD_WEBHOOK_URL = "https://discord.com/api/webhooks/ID/TOKEN" +``` + +**4 — Run the bot** + +```bash +python CodeX.py +``` + +--- + +## ✦ Dashboard Setup + +**1 — Install dependencies** + +```bash +cd dashboard +npm install +``` + +**2 — Configure the environment** + +Copy `.env.example` to `.env.local`: + +```env +DASHBOARD_API_URL = http://127.0.0.1:8000/api/v1 +DASHBOARD_API_KEY = same_secret_as_bot_DASHBOARD_API_KEY + +NEXTAUTH_URL = http://localhost:3000 +NEXTAUTH_SECRET = a_long_random_string + +DISCORD_CLIENT_ID = your_discord_oauth_client_id +DISCORD_CLIENT_SECRET = your_discord_oauth_client_secret + +ADMIN_IDS = your_discord_user_id +NEXT_PUBLIC_ADMIN_IDS = your_discord_user_id +NEXT_PUBLIC_BRAND_NAME = "ZyroX" +NEXT_PUBLIC_BRAND_NAME_WORD = "ZX" +``` + +**3 — Run locally** + +```bash +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) + +--- + +## ✦ Environment Reference + +### Bot — `bot/.env` + +| Variable | Default | Description | +|---|---|---| +| `TOKEN` | — | Discord bot token | +| `OWNER_IDS` | _(required)_ | Comma-separated owner Discord user IDs — **must be your own ID** | +| `LAVALINK_HOST` | — | Lavalink server hostname (no protocol) | +| `LAVALINK_PASSWORD` | — | Lavalink password | +| `LAVALINK_SECURE` | `true` | `true` = HTTPS, `false` = HTTP | +| `LAVALINK_PORT` | _(empty)_ | Port — only needed when `LAVALINK_SECURE=false` | +| `EMOJI_SYNC` | `false` | Run application emoji sync on startup | +| `JISHAKU_ENABLED` | `false` | Enable owner debug REPL (disable in production) | +| `API_ENABLED` | `false` | Start the FastAPI dashboard backend | +| `API_HOST` | `127.0.0.1` | Bind address — use `127.0.0.1` unless behind a reverse proxy | +| `API_PORT` | `8000` | Port the backend listens on | +| `DASHBOARD_API_KEY` | — | Shared secret between bot API and dashboard (server-side only) | +| `DASHBOARD_ADMIN_IDS` | — | Discord user IDs allowed to use admin API routes | +| `CORS_ORIGINS` | _(empty)_ | Extra CORS-allowed origins, comma-separated | +| `CMD_WEBHOOK_URL` | _(empty)_ | Optional Discord webhook for command logging | +| `TUNNEL_ENABLED` | `false` | Expose the API over HTTPS via Cloudflare Tunnel | +| `CF_TUNNEL_TOKEN` | — | Token from Cloudflare Zero Trust dashboard | +| `CF_TUNNEL_URL` | — | Your permanent public URL (e.g. `https://api.yourdomain.com`) | +| `LOG_CHANNEL_ID` | _(empty)_ | Optional channel for join/leave logs | + +### Dashboard — `dashboard/.env.local` + +| Variable | Description | +|---|---| +| `DASHBOARD_API_URL` | Bot API URL (server-side, e.g. `http://127.0.0.1:8000/api/v1`) | +| `DASHBOARD_API_KEY` | Must match `DASHBOARD_API_KEY` in the bot — **never use NEXT_PUBLIC_** | +| `NEXTAUTH_URL` | Your dashboard's public URL | +| `NEXTAUTH_SECRET` | Random secret for NextAuth session signing | +| `DISCORD_CLIENT_ID` | Discord OAuth2 client ID | +| `DISCORD_CLIENT_SECRET` | Discord OAuth2 client secret | +| `ADMIN_IDS` | Comma-separated Discord user IDs with admin panel access (server-side) | +| `NEXT_PUBLIC_ADMIN_IDS` | Same IDs for client-side admin UI visibility | +| `NEXT_PUBLIC_BRAND_NAME` | Bot name shown in the dashboard UI | +| `NEXT_PUBLIC_BRAND_NAME_WORD` | Short abbreviation shown in the dashboard | + +> **API security:** Guild routes require a valid Discord OAuth token with Manage Server permission. The dashboard forwards this automatically via `/api/proxy`. + +--- + +## ✦ HTTPS Tunnel (Cloudflare) + +The bot uses **pycloudflared** — a Python package that downloads the `cloudflared` binary automatically on first run. No CLI installs, no system packages — works on Pterodactyl and any Python host. + +**Why Cloudflare over ngrok:** +- ✅ Unlimited bandwidth & requests — no monthly caps +- ✅ Permanent URL that never changes between restarts +- ✅ Free — no paid plan needed +- ✅ Zero system installs — binary downloads via Python + +**Setup (browser only, no CLI needed):** + +1. Go to [one.dash.cloudflare.com](https://one.dash.cloudflare.com) → **Networks → Tunnels → Create a tunnel** +2. Choose **Cloudflared**, give it a name (e.g. `zyrox-api`), save +3. On the **Install connector** step, copy the token from the command shown: + ``` + cloudflared tunnel run --token + ``` +4. Go to **Public Hostname** tab → add a hostname: + - Subdomain: `api` · Domain: `yourdomain.com` · Service: `http://localhost:8000` +5. Add to `bot/.env`: + ```env + CF_TUNNEL_TOKEN = "eyJhIjoiXXXX..." + CF_TUNNEL_URL = "https://api.yourdomain.com" + ``` + +On every startup the console prints: +``` +◈ Tunnel: cloudflared binary ready — starting tunnel on port 8000… +◈ Tunnel: API is live at https://api.yourdomain.com + ↳ NEXT_PUBLIC_API_URL = https://api.yourdomain.com/api/v1 +``` + +--- + +## ✦ Deployment + +### 🤖 Bot — any Python host + +1. Upload the entire `bot/` folder to your host (Pterodactyl, Render, Railway, Fly.io, VPS…) +2. Set the start command to `python CodeX.py` +3. Add all environment variables +4. `pycloudflared` downloads the binary automatically on first run — no extra steps + +> Recommended free/cheap hosts: Render · Railway · Fly.io · VPS +> +> ⭐ **[NexioHost](https://nexiohost.in)** — Premium bot hosting, built for Discord bots. Fast, reliable, and affordable. + +### 🌐 Dashboard — Vercel + +1. Go to [vercel.com](https://vercel.com) → **Add New Project** → connect your GitHub repo +2. Set root directory to `dashboard/` +3. Add all environment variables under **Settings → Environment Variables** +4. Add the OAuth redirect URI in Discord Developer Portal: + ``` + https://your-app.vercel.app/api/auth/callback/discord + ``` +5. Hit **Deploy** — done ✓ + +--- + +## ✦ Emoji Sync + +Runs automatically on startup when `EMOJI_SYNC=true`: + +``` +★ Starting Application Emoji Sync — 144 unique emojis found in emoji.py +◈ Found 144 templates | Application hosts 202 emojis +↑ Uploading: ztick (not in application emojis) +✔ Uploaded: ztick [saved as ID: 1234567890] +✔ emoji.py patched in-place to reflect current API state. +★ Restarting bot to load updated emoji IDs... +``` + +| Event | Action | +|---|---| +| New emoji found | Uploaded to application, ID written to `emoji.py` | +| Stale ID detected | `emoji.py` patched automatically | +| No changes | Sync completes instantly, no restart | +| After any patch | Bot restarts itself so fresh IDs are live | + +--- + +## ✦ Troubleshooting + +| Problem | Fix | +|---|---| +| Bot fails to start | Check `TOKEN` is set and bot has correct gateway intents | +| Music not working | Verify `LAVALINK_HOST`, `LAVALINK_SECURE`, and `LAVALINK_PORT` | +| Dashboard auth error | Check Discord OAuth client ID/secret and redirect URI | +| Dashboard can't load data | Confirm `API_ENABLED=true`, bot running, `DASHBOARD_API_URL` correct, user logged in via Discord | +| API 403 on guild routes | User needs Manage Server or Admin permission on that Discord server | +| API key rejected (401) | `DASHBOARD_API_KEY` must exactly match in bot and dashboard `.env` | +| Emojis showing as plain text | Run with `EMOJI_SYNC=true` once to upload and patch IDs | +| CORS errors from dashboard | Add your Vercel URL to `CORS_ORIGINS` in `bot/.env` | +| Tunnel not starting | Check `CF_TUNNEL_TOKEN` is valid and `pycloudflared` is installed | +| Tunnel URL changed | Set `CF_TUNNEL_URL` — named tunnels always produce the same URL | + +--- + +## ✦ Security + +- Never commit `.env` files — `.gitignore` already covers them +- Use a strong, unique `NEXTAUTH_SECRET` and `DASHBOARD_API_KEY` +- Rotate any secret that gets accidentally exposed +- The bot API is always behind an API key — never expose it without one + +--- + +
+ +## ✦ CodeX Devs + +*Built for protection. Designed for style.* + +CodeX Development Discord Server + +

+ + + + +

+ +© 2026 CodeX Devs — MIT License + +
diff --git a/bot/.env.example b/bot/.env.example new file mode 100644 index 0000000..fc2ec7e --- /dev/null +++ b/bot/.env.example @@ -0,0 +1,43 @@ +TOKEN=TOKEN_HERE +brand_name='ZyroX' +NEXT_PUBLIC_BRAND_NAME='ZyroX' + +# ── 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 + +# ── Optional: debug REPL for owners (disabled by default) ───────────────────── +JISHAKU_ENABLED=false + +# ── Optional: Discord log / stats channels (leave empty to disable) ─────────── +# LOG_CHANNEL_ID= +# SERVER_COUNT_CHANNEL_ID= +# USER_COUNT_CHANNEL_ID= + +# ── Lavalink ────────────────────────────────────────────────────────────────── +LAVALINK_HOST="lavalink.jirayu.net" +LAVALINK_PASSWORD="youshallnotpass" +LAVALINK_SECURE="false" +LAVALINK_PORT="13592" + +# ── Emoji Sync ──────────────────────────────────────────────────────────────── +EMOJI_SYNC="false" + +# ── API / Dashboard Backend ─────────────────────────────────────────────────── +API_ENABLED="false" +API_HOST="127.0.0.1" +API_PORT="8000" +DASHBOARD_API_KEY="generate_a_long_random_secret_here" +# Discord user IDs allowed to use /api/v1/admin (comma-separated; falls back to OWNER_IDS) +DASHBOARD_ADMIN_IDS=YOUR_DISCORD_USER_ID_HERE +CORS_ORIGINS="" + +# ── Webhooks (leave empty or unset to disable command logging) ──────────────── +# CMD_WEBHOOK_URL must be a full Discord webhook URL to enable logging. +# CMD_WEBHOOK_URL= + +# ── Cloudflare Tunnel (disabled by default) ─────────────────────────────────── +TUNNEL_ENABLED="false" +TUNNEL_ALLOW_BINARY_DOWNLOAD="false" +# CF_TUNNEL_TOKEN= +# CF_TUNNEL_URL= diff --git a/bot/CodeX.py b/bot/CodeX.py new file mode 100644 index 0000000..4485761 --- /dev/null +++ b/bot/CodeX.py @@ -0,0 +1,366 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import os +import subprocess +# os.system("") +import asyncio +import traceback +from threading import Thread +from datetime import datetime +import random +import time + +import aiohttp +import discord +from discord import Spotify +from discord.ext import commands, tasks + +from core import Context +from core.Cog import Cog +from core.zyrox import zyrox +from utils.Tools import * +from utils.config import * +from utils.emoji import SUCCESS, ERROR, TICK, CROSS, REACTION_TEST_EMOJIS +from utils.sync_emojis import run_sync + +import cogs + +from dotenv import load_dotenv +load_dotenv() +TOKEN = os.getenv("TOKEN") + +if JISHAKU_ENABLED: + os.environ["JISHAKU_NO_DM_TRACEBACK"] = "False" + os.environ["JISHAKU_HIDE"] = "True" + os.environ["JISHAKU_NO_UNDERSCORE"] = "True" + os.environ["JISHAKU_FORCE_PAGINATOR"] = "True" + +if not OWNER_IDS: + print("\033[33m⚠ OWNER_IDS is not set in .env — no bot owners configured.\033[0m") + print("\033[33m Set OWNER_IDS=YOUR_DISCORD_USER_ID before running in production.\033[0m") + +client = zyrox() +tree = client.tree + +# --- Background Task for Stats --- +async def update_stats(): + """Update server/user counts in channel names when channel IDs are configured.""" + if not SERVER_COUNT_CHANNEL_ID and not USER_COUNT_CHANNEL_ID: + return + await client.wait_until_ready() + while not client.is_closed(): + try: + servers = len(client.guilds) + users = sum(guild.member_count for guild in client.guilds if guild.member_count is not None) + + if SERVER_COUNT_CHANNEL_ID: + server_channel = client.get_channel(SERVER_COUNT_CHANNEL_ID) + if server_channel: + await server_channel.edit(name=f"Servers: {servers}") + + if USER_COUNT_CHANNEL_ID: + user_channel = client.get_channel(USER_COUNT_CHANNEL_ID) + if user_channel: + await user_channel.edit(name=f"Users: {users}") + + except Exception as e: + print(f"Error updating stats: {e}") + + await asyncio.sleep(600) + +# --- Event Handlers --- +@client.event +async def on_ready(): + await client.wait_until_ready() + + print(""" + \033[1;31m + ██████╗ ██████╗ ██████╗ ███████╗██╗ ██╗ +██╔════╝██╔═══██╗██╔══██╗██╔════╝╚██╗██╔╝ +██║ ██║ ██║██║ ██║█████╗ ╚███╔╝ +██║ ██║ ██║██║ ██║██╔══╝ ██╔██╗ +╚██████╗╚██████╔╝██████╔╝███████╗██╔╝ ██╗ + ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ + \033[0m + """) + print("Loaded & Online!") + print(f"Logged in as: {client.user}") + print(f"Connected to: {len(client.guilds)} guilds") + print(f"Connected to: {len(client.users)} users") + + # Sync application emojis on startup + await run_sync(TOKEN) + + async def sync_commands(): + try: + synced = await client.tree.sync() + all_commands = list(client.commands) + print(f"Synced Total {len(all_commands)} Client Commands and {len(synced)} Slash Commands") + except Exception as e: + print(f"Error syncing command tree: {e}") + + client.loop.create_task(sync_commands()) + if SERVER_COUNT_CHANNEL_ID or USER_COUNT_CHANNEL_ID: + client.loop.create_task(update_stats()) + + +@client.event +async def on_guild_join(guild: discord.Guild): + if not LOG_CHANNEL_ID: + return + log_channel = client.get_channel(LOG_CHANNEL_ID) + if log_channel: + await log_channel.send(f"{BRAND_NAME} has been added to the server: **{guild.name}** (ID: `{guild.id}`)") + +@client.event +async def on_command_completion(context: commands.Context) -> None: + if not CMD_WEBHOOK_URL or context.command is None or context.author.id in OWNER_IDS: + return + + full_command_name = context.command.qualified_name + split = full_command_name.split("\n") + executed_command = str(split[0]) + + embed_color = 0xFF0000 + embed = discord.Embed(color=embed_color) + avatar_url = context.author.display_avatar.url + + embed.set_author(name=f"Cmd Executed: {executed_command}", icon_url=avatar_url) + embed.set_thumbnail(url=avatar_url) + + if context.guild is not None: + embed.add_field(name="User", value=f"{context.author.mention} (`{context.author.id}`)", inline=False) + embed.add_field(name="Server", value=f"{context.guild.name} (`{context.guild.id}`)", inline=False) + embed.add_field(name="Channel", value=f"{context.channel.mention} (`{context.channel.id}`)", inline=False) + else: + embed.add_field(name="User (DM)", value=f"{context.author.mention} (`{context.author.id}`)", inline=False) + + embed.timestamp = discord.utils.utcnow() + embed.set_footer(text=f"{BRAND_NAME} Development™ ❤️", icon_url=client.user.display_avatar.url) + + try: + async with aiohttp.ClientSession() as session: + webhook = discord.Webhook.from_url(CMD_WEBHOOK_URL, session=session) + await webhook.send(embed=embed) + except Exception as e: + print(f'Command log webhook failed: {e}') + + +# --- Utility Commands --- +@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 + spotify_activity = next((activity for activity in user.activities if isinstance(activity, Spotify)), None) + + if not spotify_activity: + return await ctx.send(f"{user.name} is not listening to Spotify.") + + embed = discord.Embed( + title=f"{user.name}'s Spotify", + description=f"**Listening to:** {spotify_activity.title}", + color=0x1DB954 # Spotify Green + ) + embed.set_thumbnail(url=spotify_activity.album_cover_url) + embed.add_field(name="Artist", value=spotify_activity.artist) + embed.add_field(name="Album", value=spotify_activity.album) + embed.set_footer(text=f"Song started at {spotify_activity.created_at.strftime('%H:%M')}") + await ctx.send(embed=embed) + + +@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).""" + if guild_id is None: + return await ctx.send("Please provide a Guild ID.") + + guild = client.get_guild(guild_id) + if not guild: + return await ctx.send("Invalid Guild ID. I am not in that server.") + + if guild.system_channel and guild.system_channel.permissions_for(guild.me).create_instant_invite: + try: + invite = await guild.system_channel.create_invite(max_age=0, max_uses=0, unique=True, reason="Owner requested invite.") + return await ctx.send(f"Invite for **{guild.name}**:\n{invite.url}") + except Exception: + pass + + for channel in guild.text_channels: + if channel.permissions_for(guild.me).create_instant_invite: + try: + invite = await channel.create_invite(max_age=0, max_uses=0, unique=True, reason="Owner requested invite.") + return await ctx.send(f"Invite for **{guild.name}** (from #{channel.name}):\n{invite.url}") + except Exception: + continue + + await ctx.send(f"I don't have 'Create Instant Invite' permission in any channel in **{guild.name}**.") + + +# --- Webhook Management Commands --- +@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.""" + if name is None: + return await ctx.send("Please provide a name for the webhook.") + + try: + webhook = await ctx.channel.create_webhook(name=name, reason=f"Created by {ctx.author}") + embed = discord.Embed( + title=f"{SUCCESS} Webhook Created", + description=f"A webhook named **{webhook.name}** was created.", + color=0xFF0000 + ) + await ctx.author.send(f"Webhook URL for **{webhook.name}** in **{ctx.channel.name}**:\n||{webhook.url}||", embed=embed) + await ctx.send("Webhook created. I've sent the URL to your DMs.") + except discord.Forbidden: + await ctx.send("I don't have permission to create webhooks here.") + except Exception: + await ctx.send(f"Webhook created: **{webhook.name}**\n||{webhook.url}||\n(I could not DM you the URL.)") + + +@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.""" + if webhook_url is None: + return await ctx.send("Please provide the webhook URL to delete.") + + try: + async with aiohttp.ClientSession() as session: + webhook = await discord.Webhook.from_url(webhook_url, session=session) + await webhook.delete(reason=f"Deleted by {ctx.author}") + await ctx.send(f"{SUCCESS} Webhook deleted successfully.") + except (discord.NotFound, ValueError): + await ctx.send(f"{ERROR} Webhook not found or URL is invalid.") + + +@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.""" + try: + webhooks = await ctx.channel.webhooks() + if not webhooks: + return await ctx.send("No webhooks found in this channel.") + + embed = discord.Embed(title=f"Webhooks in #{ctx.channel.name}", color=0xFF0000) + description = "\n".join([f"**Name:** {wh.name} | **ID:** `{wh.id}`" for wh in webhooks]) + embed.description = description + await ctx.send(embed=embed) + except discord.Forbidden: + await ctx.send("I don't have permission to view webhooks in this channel.") + + +# --- Game Command --- +@client.command() +async def reaction(ctx: Context): + """See how fast you can react to the correct emoji.""" + emojis = ["🍪", "🎉", "🧋", "🍒", "🍑", "💸", "🌙", "💕"] + correct_emoji = random.choice(emojis) + random.shuffle(emojis) + + embed = discord.Embed( + title="Reaction Test", + description="I will show an emoji in a few seconds. Get ready to click it!", + color=0xFF0000 + ) + message = await ctx.send(embed=embed) + + for emoji in emojis: + await message.add_reaction(emoji) + + await asyncio.sleep(random.uniform(2.0, 7.0)) + + embed.description = f"**GET THE {correct_emoji} EMOJI!**" + await message.edit(embed=embed) + start_time = time.time() + + def check(reaction, user): + return ( + reaction.message.id == message.id + and str(reaction.emoji) == correct_emoji + and user == ctx.author + ) + + try: + reaction, user = await client.wait_for("reaction_add", timeout=15.0, check=check) + end_time = time.time() + reaction_time = end_time - start_time + + embed.description = f"{user.mention} got the {correct_emoji} in **{reaction_time:.2f} seconds**!" + await message.edit(embed=embed) + except asyncio.TimeoutError: + embed.description = "Timeout! You were too slow." + await message.edit(embed=embed) + + +# ---API Server for Dashboard Backend --- +import uvicorn +from threading import Thread +from api.server import create_app +from api.dependencies import set_bot + +fastapi_app = create_app() +fastapi_app.state.bot = client +set_bot(client) + +def run_api(): + uvicorn.run(fastapi_app, host=API_HOST, port=API_PORT, log_level="warning") + +def keep_alive(): + if not API_ENABLED: + print(f"\033[33m◈ API Server: Disabled via API_ENABLED=false\033[0m") + return + print(f"\033[32m◈ API Server: Starting on {API_HOST}:{API_PORT}\033[0m") + if API_HOST in ("0.0.0.0", "::"): + print("\033[33m ⚠ API bound to all interfaces — use API_HOST=127.0.0.1 unless behind a reverse proxy.\033[0m") + server = Thread(target=run_api, daemon=True) + server.start() + +keep_alive() + +# --- Cloudflare Tunnel (HTTPS for API) — only when explicitly enabled --- +from utils.tunnel import start_tunnel +start_tunnel() + +# --- Main Bot Execution --- +async def main(): + async with client: + os.system("clear") + if JISHAKU_ENABLED: + await client.load_extension("jishaku") + print("\033[33m◈ Jishaku: enabled (owner-only debug REPL)\033[0m") + else: + print("\033[32m◈ Jishaku: disabled (set JISHAKU_ENABLED=true to enable)\033[0m") + + max_retries = 5 + for attempt in range(max_retries): + try: + await client.start(TOKEN) + break + except discord.HTTPException as e: + if e.status == 429: # Rate limited + wait_time = min((2 ** attempt) + random.random(), 60) + print(f"Rate limited. Retrying in {wait_time:.2f} seconds...") + await asyncio.sleep(wait_time) + else: + raise + else: + raise Exception("Bot failed to start after multiple retries due to rate limiting.") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/bot/LICENSE b/bot/LICENSE new file mode 100644 index 0000000..5173ffd --- /dev/null +++ b/bot/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 CodeX Devs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bot/README.md b/bot/README.md new file mode 100644 index 0000000..2ab8ffb --- /dev/null +++ b/bot/README.md @@ -0,0 +1,349 @@ +
+ +``` +███████╗██╗ ██╗██████╗ ██████╗ ██╗ ██╗ +╚══███╔╝╚██╗ ██╔╝██╔══██╗██╔═══██╗╚██╗██╔╝ + ███╔╝ ╚████╔╝ ██████╔╝██║ ██║ ╚███╔╝ + ███╔╝ ╚██╔╝ ██╔══██╗██║ ██║ ██╔██╗ +███████╗ ██║ ██║ ██║╚██████╔╝██╔╝ ██╗ +╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ +``` + +

ZyroX Bot — Python Discord Bot + FastAPI Backend

+ + + +

+ + + + +

+

+ + + +

+ +
+ +--- + +## ✦ Overview + +This folder contains the ZyroX Discord bot built on `discord.py v2` alongside a `FastAPI` backend that powers the web dashboard. Everything runs from a single `python CodeX.py` command. + +``` +bot/ +├── api/ FastAPI backend (routes, schemas, db manager) +│ └── routes/ /bot /guilds /admin +├── cogs/ +│ ├── antinuke/ Antinuke protection event listeners +│ ├── automod/ Automod enforcement event listeners +│ ├── commands/ All slash & prefix command modules +│ ├── events/ General Discord event listeners +│ ├── moderation/ Moderation action modules +│ └── zyrox/ Core ZyroX feature cogs +├── core/ Bot client, context, cog base classes +├── games/ Standalone game logic + button views +├── utils/ Emoji, tools, sync, Cloudflare tunnel +├── assets/ Fonts, backgrounds, GIFs +└── CodeX.py Entry point +``` + +--- + +## ✦ Features + + + + + + + + + + + + + + + + + + +
+ +**🛡️ Antinuke** +- Mass ban, kick, channel & role flood detection +- Webhook abuse, bot add, prune protection +- Anti-member update +- Whitelist / unwhitelist system +- Emergency lockdown mode + + + +**🤖 Automod** +- Anti-spam, anti-caps, anti-links +- Anti-invites, mass mention, emoji spam +- Fully configurable per server +- Works alongside Discord's native automod + +
+ +**🎵 Music** +- Lavalink v4 powered playback +- YouTube, SoundCloud, JioSaavn support +- Queue, loop, shuffle, autoplay +- Seek, rewind, forward controls + + + +**⚙️ Moderation** +- Ban, kick, mute, warn, lock, jail +- Snipe, message management +- Full logging system +- Reaction roles, vanity roles, invite tracker + +
+ +**🎉 Engagement** +- Leveling & XP with leaderboard +- Birthday tracker +- Counting, AFK, autorole, autoresponder +- Sticky messages, booster perks, giveaways + + + +**🎮 Games** +- Chess, Battleship, Connect Four +- Wordle, Typeracer, 2048, Memory +- Reaction test, RPS, Tic-tac-toe +- Country guess, Number slider, Lights out + +
+ +**🌐 API Backend** +- FastAPI with API key auth +- SlowAPI rate limiting +- Structured JSON request logging +- CORS configured for your dashboard domain +- `CORS_ORIGINS` env var for extra domains + + + +**🔧 Developer** +- Jishaku eval support +- Application emoji auto-sync +- Slash + prefix commands +- Cloudflare Tunnel via pycloudflared — zero system installs, unlimited traffic +- Single `OWNER_IDS` env var controls all permission checks +- CodeX Devs watermark on every source file + +
+ +--- + +## ✦ Prerequisites + +| Requirement | Notes | +|---|---| +| Python 3.10+ | — | +| Lavalink v4 node | for music features | +| Discord bot token | from Developer Portal | +| Cloudflare account (free) | for HTTPS tunnel — browser setup only | + +--- + +## ✦ Setup + +### 1 — Install dependencies + +```bash +python -m venv .venv + +# Windows +.venv\Scripts\activate + +# Linux / macOS +source .venv/bin/activate + +pip install -r requirements.txt +``` + +### 2 — Configure environment + +Create a `.env` file (copy from `.env.example`): + +```env +# ── Core ────────────────────────────────────────────────────────── +TOKEN = your_discord_bot_token +brand_name = 'ZyroX' + +# ── Owner IDs (REQUIRED — your Discord user ID) ─────────────────── +OWNER_IDS = YOUR_DISCORD_USER_ID_HERE + +# ── Lavalink ────────────────────────────────────────────────────── +LAVALINK_HOST = "your-lavalink-host" +LAVALINK_PASSWORD = "your_password" +LAVALINK_SECURE = "true" +LAVALINK_PORT = "" + +# ── Emoji Sync ──────────────────────────────────────────────────── +EMOJI_SYNC = "false" + +# ── API / Dashboard Backend ─────────────────────────────────────── +API_ENABLED = "false" +API_HOST = "127.0.0.1" +API_PORT = "8000" +DASHBOARD_API_KEY = "change_this_to_a_strong_secret" +DASHBOARD_ADMIN_IDS = YOUR_DISCORD_USER_ID_HERE +CORS_ORIGINS = "" + +# ── Cloudflare Tunnel (optional) ────────────────────────────────── +TUNNEL_ENABLED = "false" +CF_TUNNEL_TOKEN = "your_tunnel_token" +CF_TUNNEL_URL = "https://api.yourdomain.com" + +# ── Webhooks (optional) ───────────────────────────────────────────── +# CMD_WEBHOOK_URL = "https://discord.com/api/webhooks/ID/TOKEN" +``` + +### 3 — Run + +```bash +python CodeX.py +``` + +--- + +## ✦ Environment Reference + +| Variable | Default | Description | +|---|---|---| +| `TOKEN` | — | Discord bot token | +| `OWNER_IDS` | _(required)_ | Comma-separated owner Discord user IDs — **your own ID only** | +| `LAVALINK_HOST` | — | Lavalink server hostname (no protocol) | +| `LAVALINK_PASSWORD` | — | Lavalink password | +| `LAVALINK_SECURE` | `true` | `true` = HTTPS, `false` = HTTP | +| `LAVALINK_PORT` | _(empty)_ | Port — only when `LAVALINK_SECURE=false` | +| `EMOJI_SYNC` | `false` | Auto-sync application emojis on startup | +| `JISHAKU_ENABLED` | `false` | Enable owner debug REPL (disable in production) | +| `API_ENABLED` | `false` | Start the FastAPI dashboard backend | +| `API_HOST` | `127.0.0.1` | Bind address for the API server | +| `API_PORT` | `8000` | Port the backend listens on | +| `DASHBOARD_API_KEY` | — | Shared secret with dashboard (server-side only) | +| `DASHBOARD_ADMIN_IDS` | — | Discord user IDs for admin API routes | +| `CORS_ORIGINS` | _(empty)_ | Extra CORS-allowed origins, comma-separated | +| `CMD_WEBHOOK_URL` | _(empty)_ | Optional webhook for command logging | +| `TUNNEL_ENABLED` | `false` | Expose API over HTTPS via Cloudflare Tunnel | +| `CF_TUNNEL_TOKEN` | — | Token from Cloudflare Zero Trust dashboard | +| `CF_TUNNEL_URL` | — | Your permanent public URL | +| `LOG_CHANNEL_ID` | _(empty)_ | Optional channel for join/leave logs | + +> **API security:** All `/api/v1/guilds/*` routes verify Discord Manage Server permission via `X-Discord-Access-Token` header. + +--- + +## ✦ HTTPS Tunnel (Cloudflare) + +Uses **pycloudflared** — downloads the `cloudflared` binary automatically on first run. No system installs, no CLI, works on Pterodactyl and any Python host. + +**Why Cloudflare over ngrok:** + +| | Cloudflare Tunnel | ngrok free | +|---|---|---| +| Bandwidth | Unlimited | 1 GB/month | +| Requests | Unlimited | 10k/month | +| URL stability | Permanent | Permanent (1 domain) | +| System install | ❌ Not needed | ❌ Not needed | +| Cost | Free | Free | + +**Setup (browser only — no CLI needed):** + +1. Go to [one.dash.cloudflare.com](https://one.dash.cloudflare.com) → **Networks → Tunnels → Create a tunnel** +2. Choose **Cloudflared**, name it (e.g. `zyrox-api`), save +3. On **Install connector**, copy the token from the command shown: + ``` + cloudflared tunnel run --token + ``` +4. On **Public Hostname** tab → add a hostname: + - Subdomain: `api` · Domain: `yourdomain.com` · Service: `http://localhost:8000` +5. Add to `.env`: + ```env + CF_TUNNEL_TOKEN = "eyJhIjoiXXXX..." + CF_TUNNEL_URL = "https://api.yourdomain.com" + ``` + +On every startup the console prints: +``` +◈ Tunnel: cloudflared binary ready — starting tunnel on port 8000… +◈ Tunnel: API is live at https://api.yourdomain.com + ↳ DASHBOARD_API_URL = https://api.yourdomain.com/api/v1 +``` + +Set `TUNNEL_ENABLED=false` to disable. + +--- + +## ✦ Emoji Sync + +When `EMOJI_SYNC=true`, the bot syncs application emojis on every startup: + +| Event | Action | +|---|---| +| New emoji found | Uploaded to application, ID written to `emoji.py` | +| Stale ID detected | `emoji.py` patched automatically | +| No changes | Sync completes instantly, no restart | +| After any patch | Bot restarts so fresh IDs are live | + +--- + +## ✦ Deployment + +Upload the entire `bot/` folder to your host and set the start command to: + +```bash +python CodeX.py +``` + +`pycloudflared` downloads the binary on first run — no extra steps on any host. + +> Recommended free hosts: Render · Railway · Fly.io · Pterodactyl +> +> ⭐ **[NexioHost](https://nexiohost.in)** — Premium bot hosting, built for Discord bots. Fast, reliable, and affordable. + +--- + +## ✦ Troubleshooting + +| Problem | Fix | +|---|---| +| Bot fails to start | Check `TOKEN` and gateway intents in Developer Portal | +| Music not working | Verify `LAVALINK_HOST`, `LAVALINK_SECURE`, `LAVALINK_PORT` | +| Dashboard can't reach API | Check `API_ENABLED=true`, `DASHBOARD_API_URL` in dashboard, and Discord login | +| CORS errors | Add your Vercel URL to `CORS_ORIGINS` in `.env` | +| Emojis showing as plain text | Run once with `EMOJI_SYNC=true` to upload and patch IDs | +| Tunnel not starting | Check `CF_TUNNEL_TOKEN` is valid and `pycloudflared` is installed | +| Want to add an owner | Add their ID to `OWNER_IDS` in `.env` — no code changes needed | + +--- + +
+ +## ✦ CodeX Devs + +*Built for protection. Designed for style.* + +CodeX Development Discord Server + +

+ + + + +

+ +© 2026 CodeX Devs — MIT License + +
diff --git a/bot/api/__init__.py b/bot/api/__init__.py new file mode 100644 index 0000000..cc6f96c --- /dev/null +++ b/bot/api/__init__.py @@ -0,0 +1,14 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + diff --git a/bot/api/db_manager.py b/bot/api/db_manager.py new file mode 100644 index 0000000..2733a43 --- /dev/null +++ b/bot/api/db_manager.py @@ -0,0 +1,54 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import aiosqlite +import asyncio +from typing import Dict + +class DatabaseManager: + """ + A simple manager for persistent SQLite connections to avoid + opening/closing files on every API request. + """ + def __init__(self): + self._connections: Dict[str, aiosqlite.Connection] = {} + self._lock = asyncio.Lock() + + async def get_connection(self, db_path: str) -> aiosqlite.Connection: + """ + Retrieves an existing connection or creates a new one for the given path. + """ + async with self._lock: + if db_path not in self._connections: + # We use check_same_thread=False because aiosqlite handles + # thread safety by running queries in a dedicated thread. + conn = await aiosqlite.connect(db_path, check_same_thread=False) + conn.row_factory = aiosqlite.Row + self._connections[db_path] = conn + return self._connections[db_path] + + async def close_all(self): + """ + Closes all open connections. Called on API shutdown. + """ + async with self._lock: + for path, conn in self._connections.items(): + try: + await conn.close() + except: + pass + self._connections.clear() + +# Singleton instance +db_manager = DatabaseManager() diff --git a/bot/api/dependencies.py b/bot/api/dependencies.py new file mode 100644 index 0000000..4239c59 --- /dev/null +++ b/bot/api/dependencies.py @@ -0,0 +1,75 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import os +from typing import Optional, TYPE_CHECKING +from fastapi import HTTPException, Depends, Security +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials + +from slowapi import Limiter +from slowapi.util import get_remote_address + +if TYPE_CHECKING: + from core.zyrox import zyrox + +# Initialize rate limiter +limiter = Limiter(key_func=get_remote_address, default_limits=["1000 per minute"]) + +# Global reference to the bot instance +_bot_instance: Optional["zyrox"] = None + +# Security scheme +security = HTTPBearer() + +def verify_api_key(credentials: HTTPAuthorizationCredentials = Security(security)): + """ + Dependency to verify the API key from the Authorization header. + Expected: Authorization: Bearer + """ + api_key = os.getenv("DASHBOARD_API_KEY") + + # If no key is set in env, we might want to allow for local testing or force it. + # The requirement says "Read API key from environment variable", implying it's required. + if not api_key: + raise HTTPException( + status_code=500, + detail="DASHBOARD_API_KEY environment variable is not set." + ) + + if credentials.credentials != api_key: + raise HTTPException( + status_code=401, + detail="Invalid or missing API key." + ) + return credentials.credentials + +def set_bot(bot_instance: "zyrox"): + """ + Sets the global bot instance. + This should be called in CodeX.py during startup. + """ + global _bot_instance + _bot_instance = bot_instance + +def get_bot() -> "zyrox": + """ + FastAPI dependency to retrieve the Discord bot instance. + Usage: bot: zyrox = Depends(get_bot) + """ + if _bot_instance is None: + raise HTTPException( + status_code=503, + detail="Discord bot instance is not initialized yet." + ) + return _bot_instance diff --git a/bot/api/discord_auth.py b/bot/api/discord_auth.py new file mode 100644 index 0000000..e1f1f36 --- /dev/null +++ b/bot/api/discord_auth.py @@ -0,0 +1,130 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ Discord OAuth permission checks for dashboard API requests ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +import re +from typing import Any + +import aiohttp +from fastapi import HTTPException, Request + +from utils.config import OWNER_IDS_STR + +MANAGE_GUILD = 0x20 +ADMINISTRATOR = 0x8 + +GUILD_PATH_RE = re.compile(r"^/api/v1/guilds/(\d+)(?:/|$)") + + +def _parse_admin_ids() -> list[str]: + import os + + raw = os.getenv("DASHBOARD_ADMIN_IDS", "").strip() + if raw: + return [p.strip() for p in raw.split(",") if p.strip().isdigit()] + return list(OWNER_IDS_STR) + + +def can_manage_guild(guild: dict[str, Any]) -> bool: + if guild.get("owner"): + return True + try: + perms = int(guild.get("permissions", "0")) + except (TypeError, ValueError): + return False + return bool(perms & ADMINISTRATOR) or bool(perms & MANAGE_GUILD) + + +async def fetch_user_guilds(access_token: str) -> list[dict[str, Any]]: + async with aiohttp.ClientSession() as session: + async with session.get( + "https://discord.com/api/users/@me/guilds", + headers={"Authorization": f"Bearer {access_token}"}, + ) as resp: + if resp.status != 200: + raise HTTPException( + status_code=403, + detail="Invalid or expired Discord access token.", + ) + data = await resp.json() + return data if isinstance(data, list) else [] + + +async def verify_discord_token(access_token: str) -> dict[str, Any]: + async with aiohttp.ClientSession() as session: + async with session.get( + "https://discord.com/api/users/@me", + headers={"Authorization": f"Bearer {access_token}"}, + ) as resp: + if resp.status != 200: + raise HTTPException( + status_code=403, + detail="Invalid or expired Discord access token.", + ) + return await resp.json() + + +async def user_can_manage_guild(access_token: str, guild_id: int) -> bool: + guilds = await fetch_user_guilds(access_token) + for guild in guilds: + if str(guild.get("id")) == str(guild_id): + return can_manage_guild(guild) + return False + + +async def get_manageable_guild_ids(access_token: str) -> set[str]: + guilds = await fetch_user_guilds(access_token) + return {str(g["id"]) for g in guilds if can_manage_guild(g)} + + +def get_discord_headers(request: Request) -> tuple[str | None, str | None]: + token = request.headers.get("X-Discord-Access-Token") + user_id = request.headers.get("X-Discord-User-Id") + return token, user_id + + +async def require_discord_session(request: Request) -> tuple[str, str]: + token, user_id = get_discord_headers(request) + if not token: + raise HTTPException( + status_code=403, + detail="X-Discord-Access-Token header is required.", + ) + + user = await verify_discord_token(token) + resolved_id = str(user.get("id", "")) + if not resolved_id: + raise HTTPException(status_code=403, detail="Could not resolve Discord user.") + + if user_id and user_id != resolved_id: + raise HTTPException(status_code=403, detail="Discord user ID mismatch.") + + return token, resolved_id + + +async def require_guild_access(request: Request, guild_id: int) -> None: + token, _ = await require_discord_session(request) + if not await user_can_manage_guild(token, guild_id): + raise HTTPException( + status_code=403, + detail="You do not have permission to manage this guild.", + ) + + +async def require_dashboard_admin(request: Request) -> None: + token, user_id = await require_discord_session(request) + admin_ids = _parse_admin_ids() + if user_id not in admin_ids: + raise HTTPException( + status_code=403, + detail="Dashboard admin access required.", + ) + + +def extract_guild_id_from_path(path: str) -> int | None: + match = GUILD_PATH_RE.match(path) + if not match: + return None + return int(match.group(1)) diff --git a/bot/api/routes/__init__.py b/bot/api/routes/__init__.py new file mode 100644 index 0000000..cc6f96c --- /dev/null +++ b/bot/api/routes/__init__.py @@ -0,0 +1,14 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + diff --git a/bot/api/routes/admin.py b/bot/api/routes/admin.py new file mode 100644 index 0000000..b6d58d8 --- /dev/null +++ b/bot/api/routes/admin.py @@ -0,0 +1,128 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from fastapi import APIRouter, Depends, HTTPException +from api.dependencies import get_bot +from api.schemas import AdminStats, AdminNodeStatus, AdminConfig, AdminConfigUpdate +from typing import TYPE_CHECKING, List +import os +import aiosqlite + +if TYPE_CHECKING: + from core.zyrox import zyrox + +router = APIRouter() + +CONFIG_DB = "db/admin_config.db" + +async def init_db(): + async with aiosqlite.connect(CONFIG_DB) as db: + await db.execute("CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT)") + # Default values + await db.execute("INSERT OR IGNORE INTO config (key, value) VALUES ('maintenance_mode', 'false')") + await db.execute("INSERT OR IGNORE INTO config (key, value) VALUES ('global_notification', '')") + await db.commit() + +import psutil +import time + +@router.get("/stats", response_model=AdminStats) +async def get_admin_stats(bot: "zyrox" = Depends(get_bot)): + # Calculate DB size and shard info + total_size: float = 0.0 + db_count = 0 + db_dir = "db" + if os.path.exists(db_dir): + for f in os.listdir(db_dir): + if f.endswith(".db"): + total_size += float(os.path.getsize(os.path.join(db_dir, f))) + db_count += 1 + + mb_size = total_size / (1024 * 1024) + db_size_str = f"{mb_size:.2f} MB" + if mb_size > 1024: + db_size_str = f"{(mb_size / 1024):.2f} GB" + + # System Metrics + process = psutil.Process(os.getpid()) + # Use a non-blocking interval check or global state for CPU + cpu_usage = psutil.cpu_percent() + ram_raw = process.memory_info().rss + ram_mb = ram_raw / (1024 * 1024) + + total_commands = len(bot.commands) + loaded_cogs = len(bot.cogs or {}) + + # Node Healths + nodes = [ + AdminNodeStatus( + name="Primary API Cluster", + status="Healthy", + load=f"CPU: {cpu_usage}% | RAM: {ram_mb:.1f}MB", + icon="Globe" + ), + AdminNodeStatus( + name="Database Shards", + status="Healthy" if db_count > 0 else "Warning", + load=f"{db_count} SQLite DBs | {db_size_str}", + icon="Database" + ), + AdminNodeStatus( + name="Bot Microservices", + status="Healthy" if bot.is_ready() else "Booting", + load=f"{loaded_cogs} Modules", + icon="Cpu" + ), + AdminNodeStatus( + name="Auth Sockets", + status="Healthy", + load=f"Shard: {bot.shard_count} | Latency: {round(bot.latency * 1000)}ms", + icon="Lock" + ) + ] + + total_members = sum(g.member_count or 0 for g in bot.guilds) + + return AdminStats( + total_users=str(total_members), + active_servers=str(len(bot.guilds)), + api_latency=f"{round(bot.latency * 1000, 2)}ms", + db_size=db_size_str, + nodes=nodes + ) + +@router.get("/config", response_model=AdminConfig) +async def get_admin_config(): + await init_db() + async with aiosqlite.connect(CONFIG_DB) as db: + async with db.execute("SELECT value FROM config WHERE key = 'maintenance_mode'") as cursor: + mm = await cursor.fetchone() + async with db.execute("SELECT value FROM config WHERE key = 'global_notification'") as cursor: + gn = await cursor.fetchone() + + return AdminConfig( + maintenance_mode=mm[0].lower() == 'true' if mm else False, + global_notification=gn[0] if gn else None + ) + +@router.patch("/config") +async def patch_admin_config(data: AdminConfigUpdate): + await init_db() + async with aiosqlite.connect(CONFIG_DB) as db: + if data.maintenance_mode is not None: + await db.execute("UPDATE config SET value = ? WHERE key = 'maintenance_mode'", (str(data.maintenance_mode).lower(),)) + if data.global_notification is not None: + await db.execute("UPDATE config SET value = ? WHERE key = 'global_notification'", (data.global_notification,)) + await db.commit() + return {"status": "success"} diff --git a/bot/api/routes/bot.py b/bot/api/routes/bot.py new file mode 100644 index 0000000..6b0e7d8 --- /dev/null +++ b/bot/api/routes/bot.py @@ -0,0 +1,53 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from fastapi import APIRouter, Depends +from api.dependencies import get_bot +from api.schemas import BotInfo, BotStatus +from typing import TYPE_CHECKING +from utils.config import * + + +if TYPE_CHECKING: + from core.zyrox import zyrox + +router = APIRouter() + +@router.get("/status", response_model=BotStatus, summary="Get bot status", description="Returns real-time health metrics, latency, and scale information.") +async def get_status(bot: "zyrox" = Depends(get_bot)): + """ + Returns the live status of the bot. + """ + 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 + ) + +@router.get("/info", response_model=BotInfo, summary="Get bot info", description="Returns general information about the bot including command count and user reach.") +async def get_bot_info(bot: "zyrox" = Depends(get_bot)): + """ + Get general information about the Discord bot. + """ + return BotInfo( + name=bot.user.name if bot.user else BRAND_NAME, + id=str(bot.user.id) if bot.user else None, + guilds=len(bot.guilds), + users=sum(g.member_count or 0 for g in bot.guilds), + commands=len(bot.commands), + latency=f"{round(bot.latency * 1000, 2)}ms" + ) diff --git a/bot/api/routes/guilds.py b/bot/api/routes/guilds.py new file mode 100644 index 0000000..7b2ac5e --- /dev/null +++ b/bot/api/routes/guilds.py @@ -0,0 +1,1391 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from fastapi import APIRouter, Depends, HTTPException, Request +from api.dependencies import get_bot, limiter +from api.discord_auth import require_discord_session, get_manageable_guild_ids +from api.db_manager import db_manager +from api.schemas import ( + GuildSummary, GuildDetails, PrefixConfig, AutomodConfig, + TicketConfig, LevelingConfig, LoggingConfig, TicketEmbed, + TicketCategory, LevelingEmbedStyle, PrefixUpdate, + AutomodUpdate, LevelingUpdate, LoggingUpdate, TicketUpdate, + LeaderboardEntry, DiscordChannel, DiscordRole, WelcomeConfig, WelcomeEmbedData, WelcomeUpdate, + AntiNukeConfig, AntiNukeUpdate, VerificationConfig, VerificationUpdate, + VanityRoleSetup, AutoRoleConfig, AutoRoleUpdate, + TrackingConfig, TrackingUpdate, J2CConfig, J2CUpdate, JoinDMConfig, JoinDMUpdate, + CustomRoleConfig, CustomRoleUpdate, AutoReactConfig, AutoReactUpdate, AutoReactTrigger, + InvcConfig, InvcUpdate, + RRConfig, RRUpdate, ReactionRoleEntry, + InviteStat, InvitesLeaderboard +) +from typing import TYPE_CHECKING, List, Optional +import math +import aiosqlite +import json +import os + +if TYPE_CHECKING: + from core.zyrox import zyrox + +router = APIRouter() + + +@router.get("/", response_model=List[GuildSummary], summary="List all guilds", description="Returns guilds the authenticated user can manage that the bot is in.") +async def list_guilds(request: Request, bot: "zyrox" = Depends(get_bot)): + """ + Lists guilds the bot is in, filtered to those the caller can manage on Discord. + """ + token, _ = await require_discord_session(request) + manageable_ids = await get_manageable_guild_ids(token) + + guilds_list = [] + for guild in bot.guilds: + if str(guild.id) not in manageable_ids: + continue + guilds_list.append(GuildSummary( + id=str(guild.id), + name=guild.name, + icon_url=str(guild.icon.url) if guild.icon else None, + owner_id=str(guild.owner_id), + member_count=guild.member_count or 0 + )) + return guilds_list + +@router.get("/{guild_id}", response_model=GuildDetails, summary="Get guild details", description="Returns detailed metrics and metadata for a specific Discord guild.") +async def get_guild_details(guild_id: int, bot: "zyrox" = Depends(get_bot)): + """ + Returns detailed info for a specific guild by its ID. + """ + guild = bot.get_guild(guild_id) + if not guild: + raise HTTPException(status_code=404, detail="Guild not found") + + return GuildDetails( + id=str(guild.id), + name=guild.name, + icon=str(guild.icon.url) if guild.icon else None, + owner_id=str(guild.owner_id), + member_count=guild.member_count or 0, + role_count=len(guild.roles), + channel_count=len(guild.channels) + ) + +@router.get("/{guild_id}/prefix", response_model=PrefixConfig, summary="Get guild prefix", description="Retrieves the custom command prefix configured for the guild.") +async def get_guild_prefix(guild_id: int): + """ + Retrieves the custom prefix for a specific guild. + """ + db = await db_manager.get_connection('db/prefix.db') + cursor = await db.execute("SELECT prefix FROM prefixes WHERE guild_id = ?", (guild_id,)) + row = await cursor.fetchone() + prefix = row[0] if row else ">" + return PrefixConfig(guild_id=guild_id, prefix=prefix) + +@router.post("/{guild_id}/prefix", summary="Update guild prefix", description="Updates or resets the custom command prefix for the specified guild.") +async def update_guild_prefix(guild_id: int, data: PrefixUpdate): + """ + Updates the custom prefix for a specific guild. + """ + if not data.prefix or len(data.prefix) > 10: + raise HTTPException(status_code=400, detail="Invalid prefix. Must be 1-10 characters.") + + db = await db_manager.get_connection('db/prefix.db') + await db.execute( + "INSERT OR REPLACE INTO prefixes (guild_id, prefix) VALUES (?, ?)", + (guild_id, data.prefix) + ) + await db.commit() + + return {"status": "success", "guild_id": guild_id, "new_prefix": data.prefix} + + + +@router.get("/{guild_id}/automod", response_model=AutomodConfig, summary="Get AutoMod config", description="Retrieves active AutoMod rules, punishments, and ignored entities.") +async def get_guild_automod(guild_id: int): + """ + Retrieves the AutoMod configuration for a specific guild. + """ + db = await db_manager.get_connection('db/automod.db') + # Check enabled status + 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()} + + # Get ignored items + 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'] + + # Get logging channel + 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 + + return AutomodConfig( + guild_id=guild_id, + enabled=enabled, + punishments=punishments, + ignored_roles=ignored_roles, + ignored_channels=ignored_channels, + logging_channel=logging_channel + ) + +@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): + """ + Updates parts of the AutoMod configuration for a specific guild. + """ + db = await db_manager.get_connection('db/automod.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) + ) + + 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) + ) + + 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: + await db.execute( + "INSERT OR REPLACE INTO automod_ignored (guild_id, type, id) VALUES (?, 'role', ?)", + (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: + await db.execute( + "INSERT OR REPLACE INTO automod_ignored (guild_id, type, id) VALUES (?, 'channel', ?)", + (guild_id, channel_id) + ) + + if data.logging_channel is not None: + await db.execute( + "INSERT OR REPLACE INTO automod_logging (guild_id, log_channel) VALUES (?, ?)", + (guild_id, data.logging_channel) + ) + + await db.commit() + + 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.") +async def get_guild_tickets(guild_id: int): + """ + Retrieves the ticket system configuration for a specific guild. + """ + db = await db_manager.get_connection('db/ticket.db') + + # Get basic config + cursor = await db.execute( + "SELECT panel_channel_id, panel_message_id, logging_channel_id, panel_type, embed_title, embed_description, embed_color, embed_image_url, embed_thumbnail_url, closed_category_id FROM guild_configs WHERE guild_id = ?", + (guild_id,) + ) + config_row = await cursor.fetchone() + + # Get categories and identify staff roles + cursor = await db.execute( + "SELECT name, emoji, notified_roles, button_style, discord_category_id FROM ticket_categories WHERE guild_id = ?", + (guild_id,) + ) + categories_rows = await cursor.fetchall() + categories = [] + staff_roles = set() + for row in categories_rows: + if row["notified_roles"]: + roles = [int(r.strip()) for r in row["notified_roles"].split(",") if r.strip()] + category_roles = roles + for r in roles: + staff_roles.add(r) + else: + category_roles = [] + + categories.append(TicketCategory( + name=row["name"], + emoji=row["emoji"], + staff_roles=category_roles, + button_style=row["button_style"], + discord_category_id=row["discord_category_id"] + )) + + # Get open ticket count + cursor = await db.execute( + "SELECT COUNT(*) FROM open_tickets WHERE guild_id = ?", + (guild_id,) + ) + count_row = await cursor.fetchone() + open_ticket_count = count_row[0] if count_row else 0 + + return TicketConfig( + guild_id=guild_id, + panel_channel=config_row["panel_channel_id"] if config_row else None, + panel_message=config_row["panel_message_id"] if config_row else None, + logging_channel=config_row["logging_channel_id"] if config_row else None, + closed_category=config_row["closed_category_id"] if config_row else None, + panel_type=config_row["panel_type"] if config_row else "button", + embed=TicketEmbed( + title=config_row["embed_title"] if config_row else "Support Department", + description=config_row["embed_description"] if config_row else "Open a ticket below to talk to our staff.", + color=config_row["embed_color"] if config_row else None, + image_url=config_row["embed_image_url"] if config_row else None, + thumbnail_url=config_row["embed_thumbnail_url"] if config_row else None + ), + categories=categories, + staff_roles=list(staff_roles), + open_ticket_count=open_ticket_count + ) + +@router.patch("/{guild_id}/tickets", summary="Update Ticket config", description="Updates the ticket system configuration, including categories and embed details.") +async def patch_guild_tickets(guild_id: int, data: TicketUpdate): + """ + Updates the ticket system configuration for a specific guild. + """ + db = await db_manager.get_connection('db/ticket.db') + + # Initialize config row if not exists + cursor = await db.execute("SELECT guild_id FROM guild_configs WHERE guild_id = ?", (guild_id,)) + if not await cursor.fetchone(): + await db.execute("INSERT INTO guild_configs (guild_id) VALUES (?)", (guild_id,)) + + if data.panel_channel is not None: + await db.execute("UPDATE guild_configs SET panel_channel_id = ? WHERE guild_id = ?", (data.panel_channel, guild_id)) + + if data.logging_channel is not None: + await db.execute("UPDATE guild_configs SET logging_channel_id = ? WHERE guild_id = ?", (data.logging_channel, guild_id)) + + if data.closed_category is not None: + await db.execute("UPDATE guild_configs SET closed_category_id = ? WHERE guild_id = ?", (data.closed_category, guild_id)) + + if data.panel_type is not None: + await db.execute("UPDATE guild_configs SET panel_type = ? WHERE guild_id = ?", (data.panel_type, guild_id)) + + if data.embed_title is not None: + await db.execute("UPDATE guild_configs SET embed_title = ? WHERE guild_id = ?", (data.embed_title, guild_id)) + + if data.embed_description is not None: + await db.execute("UPDATE guild_configs SET embed_description = ? WHERE guild_id = ?", (data.embed_description, guild_id)) + + if data.embed_color is not None: + await db.execute("UPDATE guild_configs SET embed_color = ? WHERE guild_id = ?", (data.embed_color, guild_id)) + + if data.embed_image_url is not None: + await db.execute("UPDATE guild_configs SET embed_image_url = ? WHERE guild_id = ?", (data.embed_image_url, guild_id)) + + if data.embed_thumbnail_url is not None: + await db.execute("UPDATE guild_configs SET embed_thumbnail_url = ? WHERE guild_id = ?", (data.embed_thumbnail_url, guild_id)) + + if data.staff_roles is not None: + roles_str = ",".join(map(str, data.staff_roles)) + await db.execute("UPDATE guild_configs SET staff_roles = ? WHERE guild_id = ?", (roles_str, guild_id)) + + if data.categories is not None: + # Clear existing categories + await db.execute("DELETE FROM ticket_categories WHERE guild_id = ?", (guild_id,)) + for cat in data.categories: + roles_str = ",".join(map(str, cat.staff_roles)) + await db.execute( + "INSERT INTO ticket_categories (guild_id, name, emoji, notified_roles, button_style, discord_category_id) VALUES (?, ?, ?, ?, ?, ?)", + (guild_id, cat.name, cat.emoji, roles_str, cat.button_style, cat.discord_category_id) + ) + + await db.commit() + return {"status": "success", "guild_id": guild_id} + return {"status": "success", "guild_id": guild_id} + + + +@router.get("/{guild_id}/leveling", response_model=LevelingConfig, summary="Get Leveling config", description="Retrieves experience points settings, cooldowns, and rank card styles.") +async def get_guild_leveling(guild_id: int): + """ + Retrieves the leveling system configuration for a specific guild. + """ + db = await db_manager.get_connection('db/leveling.db') + cursor = await db.execute("SELECT * FROM leveling_settings WHERE guild_id = ?", (guild_id,)) + row = await cursor.fetchone() + + if not row: + return LevelingConfig( + guild_id=guild_id, + enabled=False, + xp_per_message=20, + cooldown=60, + level_up_channel=None, + embed_style=LevelingEmbedStyle(color="#000000") + ) + + embed_color = row["embed_color"] if row["embed_color"] is not None else 0 + color_hex = f"#{embed_color:06x}" + + return LevelingConfig( + guild_id=guild_id, + enabled=bool(row["enabled"]), + xp_per_message=row["xp_per_message"], + cooldown=row["cooldown_seconds"], + level_up_channel=row["channel_id"], + embed_style=LevelingEmbedStyle( + color=color_hex, + thumbnail=bool(row["thumbnail_enabled"]), + image=row["level_image"] + ) + ) + +@router.patch("/{guild_id}/leveling", summary="Update Leveling config", description="Modifies the leveling system settings including XP rates and channel notifications.") +async def patch_guild_leveling(guild_id: int, data: LevelingUpdate): + """ + Updates parts of the leveling configuration for a specific guild. + """ + db = await db_manager.get_connection('db/leveling.db') + # We use a series of updates or a single dynamic update. + # For simplicity and robustness with INSERT OR REPLACE: + + cursor = await db.execute("SELECT * FROM leveling_settings WHERE guild_id = ?", (guild_id,)) + row = await cursor.fetchone() + + if not row: + # Create default entry first if it doesn't exist + await db.execute("INSERT INTO leveling_settings (guild_id) VALUES (?)", (guild_id,)) + await db.commit() + + if data.enabled is not None: + await db.execute("UPDATE leveling_settings SET enabled = ? WHERE guild_id = ?", (1 if data.enabled else 0, guild_id)) + + if data.xp_per_message is not None: + await db.execute("UPDATE leveling_settings SET xp_per_message = ? WHERE guild_id = ?", (data.xp_per_message, guild_id)) + + if data.cooldown is not None: + await db.execute("UPDATE leveling_settings SET cooldown_seconds = ? WHERE guild_id = ?", (data.cooldown, guild_id)) + + if data.level_up_channel is not None: + await db.execute("UPDATE leveling_settings SET channel_id = ? WHERE guild_id = ?", (data.level_up_channel, guild_id)) + + if data.embed_color is not None: + try: + # Convert hex to int + clean_hex = data.embed_color.lstrip('#') + color_int = int(clean_hex, 16) + await db.execute("UPDATE leveling_settings SET embed_color = ? WHERE guild_id = ?", (color_int, guild_id)) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid hex color format.") + + await db.commit() + + return {"status": "success", "guild_id": guild_id} + + +@router.get("/{guild_id}/welcome", response_model=WelcomeConfig, summary="Get Welcome config", description="Retrieves the greet/welcome messages setup.") +async def get_guild_welcome(guild_id: int): + import aiosqlite + import json + + async with aiosqlite.connect("db/welcome.db") as db: + async with db.execute("SELECT welcome_type, welcome_message, channel_id, embed_data, auto_delete_duration FROM welcome WHERE guild_id = ?", (guild_id,)) as cursor: + row = await cursor.fetchone() + + if not row: + return WelcomeConfig( + guild_id=guild_id, + ) + + welcome_type, welcome_message, channel_id, embed_data, auto_delete_duration = row + + embed_parsed = None + if embed_data: + try: + embed_parsed = WelcomeEmbedData(**json.loads(embed_data)) + except: + pass + + return WelcomeConfig( + guild_id=guild_id, + welcome_type=welcome_type, + welcome_message=welcome_message, + channel_id=channel_id, + embed_data=embed_parsed, + auto_delete_duration=auto_delete_duration + ) + +@router.patch("/{guild_id}/welcome", summary="Update Welcome config", description="Updates welcome/greet configuration.") +async def patch_guild_welcome(guild_id: int, data: WelcomeUpdate): + import aiosqlite + import json + + async with aiosqlite.connect("db/welcome.db") as db: + # Get existing or create + async with db.execute("SELECT welcome_type, welcome_message, channel_id, embed_data, auto_delete_duration FROM welcome WHERE guild_id = ?", (guild_id,)) as cursor: + row = await cursor.fetchone() + + if not row: + await db.execute( + "INSERT INTO welcome (guild_id, welcome_type, welcome_message, channel_id, embed_data, auto_delete_duration) VALUES (?, ?, ?, ?, ?, ?)", + (guild_id, data.welcome_type or "simple", data.welcome_message, data.channel_id, json.dumps(data.embed_data.dict()) if data.embed_data else None, data.auto_delete_duration) + ) + else: + wt, wm, chid, ed, auth_del = row + + new_wt = data.welcome_type if data.welcome_type is not None else wt + new_wm = data.welcome_message if data.welcome_message is not None else wm + new_chid = data.channel_id if data.channel_id is not None else chid + new_auth_del = data.auto_delete_duration if data.auto_delete_duration is not None else auth_del + + new_ed = ed + if data.embed_data is not None: + new_ed = json.dumps(data.embed_data.dict()) + + await db.execute( + "UPDATE welcome SET welcome_type = ?, welcome_message = ?, channel_id = ?, embed_data = ?, auto_delete_duration = ? WHERE guild_id = ?", + (new_wt, new_wm, new_chid, new_ed, new_auth_del, guild_id) + ) + + await db.commit() + + return {"status": "success", "guild_id": guild_id} + + +@router.get("/{guild_id}/antinuke", response_model=AntiNukeConfig, summary="Get AntiNuke config") +async def get_guild_antinuke(guild_id: int): + import aiosqlite + + async with aiosqlite.connect("db/anti.db") as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild_id,)) as cursor: + row = await cursor.fetchone() + + whitelisted = [] + # Need to check if table exists first since it's created by the cog + cursor = await db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='whitelisted_users'") + if await cursor.fetchone(): + async with db.execute("SELECT user_id FROM whitelisted_users WHERE guild_id = ?", (guild_id,)) as wl_cursor: + wl_rows = await wl_cursor.fetchall() + whitelisted = [str(r[0]) for r in wl_rows] + + return AntiNukeConfig( + guild_id=guild_id, + status=bool(row[0]) if row else False, + whitelisted_users=whitelisted + ) + +@router.patch("/{guild_id}/antinuke", summary="Update AntiNuke config") +async def patch_guild_antinuke(guild_id: int, data: AntiNukeUpdate): + import aiosqlite + + async with aiosqlite.connect("db/anti.db") as db: + if data.status is not None: + # Get existing or create + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild_id,)) as cursor: + row = await cursor.fetchone() + + if not row: + await db.execute( + "INSERT INTO antinuke (guild_id, status) VALUES (?, ?)", + (guild_id, data.status) + ) + else: + await db.execute( + "UPDATE antinuke SET status = ? WHERE guild_id = ?", + (data.status, guild_id) + ) + + if data.add_whitelist: + # Only add if table exists to avoid errors + cursor = await db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='whitelisted_users'") + if await cursor.fetchone(): + try: + user_id = int(data.add_whitelist) + # Check if already whitelisted + async with db.execute("SELECT * FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", (guild_id, user_id)) as wl_cursor: + if not await wl_cursor.fetchone(): + await db.execute("INSERT INTO whitelisted_users (guild_id, user_id, ban, kick, prune, botadd, serverup, memup, chcr, chdl, chup, rlcr, rlup, rldl, meneve, mngweb, mngstemo) VALUES (?, ?, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True)", (guild_id, user_id)) + except ValueError: + pass + + if data.remove_whitelist: + cursor = await db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='whitelisted_users'") + if await cursor.fetchone(): + try: + user_id = int(data.remove_whitelist) + await db.execute("DELETE FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", (guild_id, user_id)) + except ValueError: + pass + + await db.commit() + + return {"status": "success", "guild_id": guild_id} + + +@router.get("/{guild_id}/verification", response_model=VerificationConfig, summary="Get Verification config") +async def get_guild_verification(guild_id: int): + import aiosqlite + + async with aiosqlite.connect("db/verification.db") as db: + async with db.execute("SELECT verification_channel_id, verified_role_id, log_channel_id, verification_method, enabled FROM verification_config WHERE guild_id = ?", (guild_id,)) as cursor: + row = await cursor.fetchone() + + if row: + return VerificationConfig( + guild_id=guild_id, + verification_channel_id=row[0], + verified_role_id=row[1], + log_channel_id=row[2], + verification_method=row[3], + enabled=bool(row[4]) + ) + return VerificationConfig( + guild_id=guild_id, + verification_channel_id=None, + verified_role_id=None, + log_channel_id=None, + verification_method="both", + enabled=True + ) + +@router.patch("/{guild_id}/verification", summary="Update Verification config") +async def patch_guild_verification(guild_id: int, data: VerificationUpdate): + import aiosqlite + + async with aiosqlite.connect("db/verification.db") as db: + async with db.execute("SELECT * FROM verification_config WHERE guild_id = ?", (guild_id,)) as cursor: + row = await cursor.fetchone() + + if not row: + await db.execute( + "INSERT INTO verification_config (guild_id, verification_channel_id, verified_role_id, log_channel_id, verification_method, enabled) VALUES (?, ?, ?, ?, ?, ?)", + (guild_id, data.verification_channel_id or 0, data.verified_role_id or 0, data.log_channel_id or 0, data.verification_method or "both", data.enabled if data.enabled is not None else True) + ) + else: + await db.execute( + "UPDATE verification_config SET verification_channel_id = COALESCE(?, verification_channel_id), verified_role_id = COALESCE(?, verified_role_id), log_channel_id = COALESCE(?, log_channel_id), verification_method = COALESCE(?, verification_method), enabled = COALESCE(?, enabled) WHERE guild_id = ?", + (data.verification_channel_id, data.verified_role_id, data.log_channel_id, data.verification_method, data.enabled, guild_id) + ) + + await db.commit() + + return {"status": "success", "guild_id": guild_id} + + +@router.get("/{guild_id}/vanityroles", response_model=List[VanityRoleSetup], summary="Get Vanity Roles setups") +async def get_guild_vanityroles(guild_id: int): + import aiosqlite + + setups = [] + async with aiosqlite.connect("db/vanity.db") as db: + async with db.execute("SELECT vanity, role_id, log_channel_id FROM vanity_roles WHERE guild_id = ?", (guild_id,)) as cursor: + rows = await cursor.fetchall() + for row in rows: + setups.append(VanityRoleSetup( + vanity=row[0], + role_id=row[1], + log_channel_id=row[2] + )) + return setups + +@router.post("/{guild_id}/vanityroles", summary="Add/Update a Vanity Role setup") +async def post_guild_vanityroles(guild_id: int, data: VanityRoleSetup): + import aiosqlite + + async with aiosqlite.connect("db/vanity.db") as db: + await db.execute( + "INSERT OR REPLACE INTO vanity_roles (guild_id, vanity, role_id, log_channel_id, current_status) VALUES (?, ?, ?, ?, NULL)", + (guild_id, data.vanity.lower(), data.role_id, data.log_channel_id) + ) + await db.commit() + + return {"status": "success", "vanity": data.vanity} + +@router.delete("/{guild_id}/vanityroles/{vanity}", summary="Delete a Vanity Role setup") +async def delete_guild_vanityroles(guild_id: int, vanity: str): + import aiosqlite + + async with aiosqlite.connect("db/vanity.db") as db: + await db.execute("DELETE FROM vanity_roles WHERE guild_id = ? AND vanity = ?", (guild_id, vanity.lower())) + await db.commit() + + return {"status": "success"} + + +@router.get("/{guild_id}/autorole", response_model=AutoRoleConfig, summary="Get AutoRole config") +async def get_guild_autorole(guild_id: int): + import aiosqlite + + async with aiosqlite.connect("db/autorole.db") as db: + # Ensure table exists + await db.execute(""" + CREATE TABLE IF NOT EXISTS autorole ( + guild_id INTEGER PRIMARY KEY, + bots TEXT NOT NULL DEFAULT '[]', + humans TEXT NOT NULL DEFAULT '[]' + ) + """) + await db.commit() + + async with db.execute("SELECT bots, humans FROM autorole WHERE guild_id = ?", (guild_id,)) as cursor: + row = await cursor.fetchone() + + if row: + bots_str, humans_str = row + try: + bots = [r.strip() for r in bots_str.replace('[','').replace(']','').split(',') if r.strip()] + except Exception: + bots = [] + + try: + humans = [r.strip() for r in humans_str.replace('[','').replace(']','').split(',') if r.strip()] + except Exception: + humans = [] + + return AutoRoleConfig(guild_id=str(guild_id), bots=bots, humans=humans) + + return AutoRoleConfig(guild_id=str(guild_id), bots=[], humans=[]) + +@router.patch("/{guild_id}/autorole", summary="Update AutoRole config") +async def patch_guild_autorole(guild_id: int, data: AutoRoleUpdate): + import aiosqlite + + async with aiosqlite.connect("db/autorole.db") as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS autorole ( + guild_id INTEGER PRIMARY KEY, + bots TEXT NOT NULL DEFAULT '[]', + humans TEXT NOT NULL DEFAULT '[]' + ) + """) + + # Build the bracket-format strings the bot cog expects + if data.bots is not None: + bots_str = str([int(b) for b in data.bots if b and b.isdigit()]) + else: + # Get existing to keep + async with db.execute("SELECT bots FROM autorole WHERE guild_id = ?", (guild_id,)) as cursor: + old = await cursor.fetchone() + bots_str = old[0] if old else '[]' + + if data.humans is not None: + humans_str = str([int(h) for h in data.humans if h and h.isdigit()]) + else: + async with db.execute("SELECT humans FROM autorole WHERE guild_id = ?", (guild_id,)) as cursor: + old = await cursor.fetchone() + humans_str = old[0] if old else '[]' + + await db.execute( + "INSERT OR REPLACE INTO autorole (guild_id, bots, humans) VALUES (?, ?, ?)", + (guild_id, bots_str, humans_str) + ) + await db.commit() + + return {"status": "success"} + + +@router.get("/{guild_id}/welcome", response_model=WelcomeConfig, summary="Get Welcome config") +async def get_guild_welcome(guild_id: int): + import aiosqlite + import json + + async with aiosqlite.connect("db/welcome.db") as db: + async with db.execute("SELECT welcome_type, welcome_message, channel_id, embed_data, auto_delete_duration FROM welcome WHERE guild_id = ?", (guild_id,)) as cursor: + row = await cursor.fetchone() + + if row: + w_type, w_msg, channel_id, embed_data_str, auto_del = row + edata = None + if embed_data_str: + try: + edata_dict = json.loads(embed_data_str) + edata = WelcomeEmbedData(**edata_dict) + except: + pass + return WelcomeConfig( + guild_id=guild_id, + welcome_type=w_type, + welcome_message=w_msg, + channel_id=channel_id, + embed_data=edata, + auto_delete_duration=auto_del + ) + + return WelcomeConfig(guild_id=guild_id) + +@router.patch("/{guild_id}/welcome", summary="Update Welcome config") +async def patch_guild_welcome(guild_id: int, data: WelcomeUpdate): + import aiosqlite + import json + + async with aiosqlite.connect("db/welcome.db") as db: + async with db.execute("SELECT welcome_type, welcome_message, channel_id, embed_data, auto_delete_duration FROM welcome WHERE guild_id = ?", (guild_id,)) as cursor: + row = await cursor.fetchone() + + current_type = row[0] if row else None + current_msg = row[1] if row else None + current_channel = row[2] if row else None + current_embed_str = row[3] if row else None + current_auto = row[4] if row else None + + new_type = data.welcome_type if data.welcome_type is not None else current_type + new_msg = data.welcome_message if data.welcome_message is not None else current_msg + new_channel = data.channel_id if data.channel_id is not None else current_channel + new_auto = data.auto_delete_duration if data.auto_delete_duration is not None else current_auto + + new_embed_str = current_embed_str + if data.embed_data is not None: + new_embed_str = data.embed_data.json(exclude_none=True) + + if not row: + await db.execute( + "INSERT INTO welcome (guild_id, welcome_type, welcome_message, channel_id, embed_data, auto_delete_duration) VALUES (?, ?, ?, ?, ?, ?)", + (guild_id, new_type, new_msg, new_channel, new_embed_str, new_auto) + ) + else: + await db.execute( + "UPDATE welcome SET welcome_type=?, welcome_message=?, channel_id=?, embed_data=?, auto_delete_duration=? WHERE guild_id=?", + (new_type, new_msg, new_channel, new_embed_str, new_auto, guild_id) + ) + + await db.commit() + return {"status": "success", "guild_id": guild_id} + +@router.delete("/{guild_id}/welcome", summary="Delete Welcome config") +async def delete_guild_welcome(guild_id: int): + import aiosqlite + async with aiosqlite.connect("db/welcome.db") as db: + await db.execute("DELETE FROM welcome WHERE guild_id = ?", (guild_id,)) + await db.commit() + return {"status": "success"} + + +@router.get("/{guild_id}/tracking", response_model=TrackingConfig, summary="Get Tracking config") +async def get_guild_tracking(guild_id: int): + import aiosqlite + async with aiosqlite.connect("db/invite.db") as db: + async with db.execute("SELECT channel_id FROM logging WHERE guild_id = ?", (guild_id,)) as cursor: + row = await cursor.fetchone() + return TrackingConfig(guild_id=guild_id, channel_id=row[0] if row else None) + +@router.patch("/{guild_id}/tracking", summary="Update Tracking config") +async def patch_guild_tracking(guild_id: int, data: TrackingUpdate): + import aiosqlite + async with aiosqlite.connect("db/invite.db") as db: + await db.execute("INSERT OR REPLACE INTO logging (guild_id, channel_id) VALUES (?, ?)", (guild_id, data.channel_id)) + await db.commit() + return {"status": "success"} + +@router.get("/{guild_id}/j2c", response_model=J2CConfig, summary="Get J2C config") +async def get_guild_j2c(guild_id: int): + import aiosqlite + async with aiosqlite.connect("j2c_data.db") as db: + # Ensure table exists + await db.execute(""" + CREATE TABLE IF NOT EXISTS guild_setup ( + guild_id INTEGER PRIMARY KEY, + join_channel_id INTEGER, + control_channel_id INTEGER, + control_message_id INTEGER, + category_id INTEGER + ) + """) + try: + await db.execute("ALTER TABLE guild_setup ADD COLUMN category_id INTEGER") + except aiosqlite.OperationalError: + pass + await db.commit() + + async with db.execute("SELECT join_channel_id, control_channel_id, category_id FROM guild_setup WHERE guild_id = ?", (guild_id,)) as cursor: + row = await cursor.fetchone() + if row: + return J2CConfig( + guild_id=str(guild_id), + join_channel_id=str(row[0]) if row[0] else None, + control_channel_id=str(row[1]) if row[1] else None, + category_id=str(row[2]) if row[2] else None + ) + return J2CConfig(guild_id=str(guild_id)) + +@router.patch("/{guild_id}/j2c", summary="Update J2C config") +async def patch_guild_j2c(guild_id: int, data: J2CUpdate, bot: "zyrox" = Depends(get_bot)): + import aiosqlite + + def to_id(val): + if not val or val == "none": return None + try: return int(val) + except: return None + + join_ch = to_id(data.join_channel_id) + ctrl_ch = to_id(data.control_channel_id) + cat_ch = to_id(data.category_id) + + async with aiosqlite.connect("j2c_data.db") as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS guild_setup ( + guild_id INTEGER PRIMARY KEY, + join_channel_id INTEGER, + control_channel_id INTEGER, + control_message_id INTEGER, + category_id INTEGER + ) + """) + try: + await db.execute("ALTER TABLE guild_setup ADD COLUMN category_id INTEGER") + except aiosqlite.OperationalError: + pass + + async with db.execute("SELECT control_message_id FROM guild_setup WHERE guild_id = ?", (guild_id,)) as cursor: + existing = await cursor.fetchone() + + ctrl_msg_id = existing[0] if existing else None + + await db.execute( + "INSERT OR REPLACE INTO guild_setup (guild_id, join_channel_id, control_channel_id, control_message_id, category_id) VALUES (?, ?, ?, ?, ?)", + (guild_id, join_ch, ctrl_ch, ctrl_msg_id, cat_ch) + ) + await db.commit() + + # Update cog memory cache and send/update the control panel in real-time + cog = bot.get_cog("JoinToCreate") + if cog: + if not join_ch or not ctrl_ch: + if guild_id in cog.setup_data: + del cog.setup_data[guild_id] + else: + cog.setup_data[guild_id] = { + "join_channel_id": join_ch, + "control_channel_id": ctrl_ch, + "control_message_id": ctrl_msg_id, + "category_id": cat_ch + } + guild = bot.get_guild(guild_id) + if guild: + async def update_or_send_panel(): + try: + control_channel = guild.get_channel(ctrl_ch) + if control_channel: + msg = None + if ctrl_msg_id: + try: + msg = await control_channel.fetch_message(ctrl_msg_id) + except: + pass + + from cogs.commands.j2c import ControlPanelView + if msg: + view = ControlPanelView(cog, guild) + await msg.edit(view=view, embed=None, content=None) + else: + view = ControlPanelView(cog, guild) + msg = await control_channel.send(view=view) + cog.setup_data[guild_id]["control_message_id"] = msg.id + await cog.save_guild_setup(guild_id, cog.setup_data[guild_id]) + except Exception as e: + print(f"Error updating/sending control panel via API: {e}") + + bot.loop.create_task(update_or_send_panel()) + + return {"status": "success"} + + +@router.get("/{guild_id}/joindm", response_model=JoinDMConfig, summary="Get JoinDM config") +async def get_guild_joindm(guild_id: int): + config_file = 'jsondb/joindm_messages.json' + os.makedirs('jsondb', exist_ok=True) + + if os.path.exists(config_file): + try: + with open(config_file, 'r') as f: + content = f.read().strip() + if not content: + data = {} + else: + data = json.loads(content) + if not isinstance(data, dict): + data = {} + except (json.JSONDecodeError, Exception): + data = {} + + return JoinDMConfig(guild_id=str(guild_id), message=data.get(str(guild_id))) + + return JoinDMConfig(guild_id=str(guild_id)) + +@router.patch("/{guild_id}/joindm", summary="Update JoinDM config") +async def patch_guild_joindm(guild_id: int, data: JoinDMUpdate): + config_file = 'jsondb/joindm_messages.json' + os.makedirs('jsondb', exist_ok=True) + + messages = {} + if os.path.exists(config_file): + try: + with open(config_file, 'r') as f: + content = f.read().strip() + if content: + messages = json.loads(content) + if not isinstance(messages, dict): + messages = {} + except (json.JSONDecodeError, Exception): + messages = {} + + messages[str(guild_id)] = data.message + + with open(config_file, 'w') as f: + json.dump(messages, f, indent=2) + + return {"status": "success"} + +@router.get("/{guild_id}/customroles", response_model=CustomRoleConfig, summary="Get CustomRoles config") +async def get_guild_customroles(guild_id: int): + import aiosqlite + async with aiosqlite.connect('db/customrole.db') as db: + async with db.execute("SELECT staff, girl, vip, guest, frnd, reqrole FROM roles WHERE guild_id = ?", (guild_id,)) as cursor: + row = await cursor.fetchone() + if row: + return CustomRoleConfig( + guild_id=str(guild_id), + staff=str(row[0]) if row[0] else None, + girl=str(row[1]) if row[1] else None, + vip=str(row[2]) if row[2] else None, + guest=str(row[3]) if row[3] else None, + frnd=str(row[4]) if row[4] else None, + reqrole=str(row[5]) if row[5] else None + ) + return CustomRoleConfig(guild_id=str(guild_id)) + +@router.patch("/{guild_id}/customroles", summary="Update CustomRoles config") +async def patch_guild_customroles(guild_id: int, data: CustomRoleUpdate): + import aiosqlite + + # Convert string IDs to ints for DB INTEGER columns + def to_int(val): + if val is None: return None + try: return int(val) + except: return None + + async with aiosqlite.connect('db/customrole.db') as db: + async with db.execute("SELECT * FROM roles WHERE guild_id = ?", (guild_id,)) as cursor: + row = await cursor.fetchone() + + if not row: + await db.execute( + "INSERT INTO roles (guild_id, staff, girl, vip, guest, frnd, reqrole) VALUES (?, ?, ?, ?, ?, ?, ?)", + (guild_id, to_int(data.staff), to_int(data.girl), to_int(data.vip), to_int(data.guest), to_int(data.frnd), to_int(data.reqrole)) + ) + else: + # We have a row, update only provided fields + update_fields = [] + params = [] + for field, value in data.dict(exclude_unset=True).items(): + update_fields.append(f"{field} = ?") + params.append(to_int(value)) + + if update_fields: + query = f"UPDATE roles SET {', '.join(update_fields)} WHERE guild_id = ?" + params.append(guild_id) + await db.execute(query, params) + await db.commit() + return {"status": "success"} + +@router.get("/{guild_id}/logging", response_model=LoggingConfig, summary="Get Logging config", description="Retrieves the event logging configuration and designated log channels.") +async def get_guild_logging(guild_id: int, bot: "zyrox" = Depends(get_bot)): + """ + Retrieves the logging configuration for a specific guild. + """ + cog = bot.get_cog("Logging") + config = None + + if cog and guild_id in cog.config_cache: + config = cog.config_cache[guild_id] + else: + # Try reading from file as fallback + import json + import os + config_file = "jsondb/logging_config.json" + if os.path.exists(config_file): + try: + with open(config_file, "r") as f: + data = json.load(f) + config = data.get(str(guild_id)) + except: + pass + + if not config: + return LoggingConfig( + guild_id=guild_id, + log_enabled={}, + log_channels={}, + ignore_channels=[], + ignore_roles=[], + ignore_users=[], + auto_delete_duration=None + ) + + return LoggingConfig( + guild_id=guild_id, + log_enabled=config.get("log_enabled", {}), + log_channels=config.get("log_channels", {}), + ignore_channels=config.get("ignore_channels", []), + ignore_roles=config.get("ignore_roles", []), + ignore_users=config.get("ignore_users", []), + auto_delete_duration=config.get("auto_delete_duration") + ) + +@router.patch("/{guild_id}/logging", summary="Update Logging config", description="Updates which Discord events are logged and where they are posted.") +async def patch_guild_logging(guild_id: int, data: LoggingUpdate, bot: "zyrox" = Depends(get_bot)): + """ + Updates the logging configuration for a specific guild. + """ + cog = bot.get_cog("Logging") + if not cog: + raise HTTPException(status_code=503, detail="Logging service is currently unavailable.") + + # Get current config or defaults + current_config = cog.config_cache.get(guild_id, {}) + + log_channels = current_config.get("log_channels", {}) + if data.log_channels is not None: + log_channels.update(data.log_channels) + + log_enabled = current_config.get("log_enabled", {}) + if data.log_enabled is not None: + log_enabled.update(data.log_enabled) + + await cog._save_log_config( + guild_id, + log_channels, + log_enabled, + current_config.get("ignore_channels", []), + current_config.get("ignore_roles", []), + current_config.get("ignore_users", []), + current_config.get("auto_delete_duration") + ) + + return {"status": "success", "guild_id": guild_id} + +@router.get("/{guild_id}/leveling/leaderboard", response_model=List[LeaderboardEntry], summary="Get leveling leaderboard", description="Returns top users by XP for a specific guild.") +async def get_leveling_leaderboard(guild_id: int, bot: "zyrox" = Depends(get_bot)): + db = await db_manager.get_connection('db/leveling.db') + cursor = await db.execute( + "SELECT user_id, xp FROM user_xp WHERE guild_id = ? ORDER BY xp DESC LIMIT 100", + (guild_id,) + ) + rows = await cursor.fetchall() + + leaderboard = [] + guild = bot.get_guild(guild_id) + + for row in rows: + user_id = row["user_id"] + xp = row["xp"] + # Calculate level: sqrt(xp/100) + level = int(math.sqrt(xp / 100)) if xp >= 0 else 0 + + # Try to get member name + name = f"User {user_id}" + if guild: + member = guild.get_member(user_id) + if member: + name = member.display_name + else: + try: + user = await bot.fetch_user(user_id) + name = user.name + except: + pass + + leaderboard.append(LeaderboardEntry( + user_id=str(user_id), + name=name, + level=level, + xp=xp + )) + + return leaderboard + +@router.get("/{guild_id}/channels", response_model=List[DiscordChannel], summary="Get guild channels", description="Returns a list of all channels for the specific guild.") +async def get_guild_channels(guild_id: int, bot: "zyrox" = Depends(get_bot)): + guild = bot.get_guild(guild_id) + if not guild: + raise HTTPException(status_code=404, detail="Guild not found") + + channels = [] + for canal in guild.channels: + try: + # Handle both discord.ChannelType enum and literal ints + c_type = canal.type.value if hasattr(canal.type, 'value') else int(canal.type) + channels.append(DiscordChannel( + id=str(canal.id), + name=canal.name, + type=str(c_type) + )) + except: + continue + return channels + +@router.get("/{guild_id}/roles", response_model=List[DiscordRole], summary="Get guild roles", description="Returns a list of roles for the specific guild.") +async def get_guild_roles(guild_id: int, bot: "zyrox" = Depends(get_bot)): + guild = bot.get_guild(guild_id) + if not guild: + raise HTTPException(status_code=404, detail="Guild not found") + + roles = [] + for role in guild.roles: + # Avoid @everyone role if desired, but frontend might need filtering. Let's return all. + roles.append(DiscordRole( + id=str(role.id), + name=role.name, + color=role.color.value, + position=role.position + )) + # Sort roles by position descending + roles.sort(key=lambda x: x.position, reverse=True) + return roles + + +# ========== INVC ROLE (Voice Role) ========== + + + +# ========== AUTO REACT ========== + +@router.get("/{guild_id}/autoreact", response_model=AutoReactConfig, summary="Get AutoReact config") +async def get_guild_autoreact(guild_id: int): + import aiosqlite + async with aiosqlite.connect("db/autoreact.db") as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS autoreact ( + guild_id INTEGER, + trigger TEXT, + emojis TEXT + ) + """) + await db.commit() + + async with db.execute("SELECT trigger, emojis FROM autoreact WHERE guild_id = ?", (guild_id,)) as cursor: + rows = await cursor.fetchall() + + triggers = [AutoReactTrigger(trigger=row[0], emojis=row[1]) for row in rows] + return AutoReactConfig(guild_id=str(guild_id), triggers=triggers) + + +# ========== INVC ROLE (Voice Role) ========== + +@router.get("/{guild_id}/invcrole", response_model=InvcConfig, summary="Get Invc Role config") +async def get_guild_invcrole(guild_id: int): + db = await db_manager.get_connection('db/invc.db') + # Use execute instead of with for shared connection + await db.execute(""" + CREATE TABLE IF NOT EXISTS vcroles ( + guild_id INTEGER PRIMARY KEY, + role_id INTEGER NOT NULL, + enabled INTEGER DEFAULT 0 + ) + """) + try: + await db.execute("ALTER TABLE vcroles ADD COLUMN enabled INTEGER DEFAULT 0") + except: + pass + await db.commit() + + cursor = await db.execute("SELECT role_id, enabled FROM vcroles WHERE guild_id = ?", (guild_id,)) + row = await cursor.fetchone() + + role_id = str(row['role_id']) if row and row['role_id'] and row['role_id'] != 0 else None + enabled = bool(row['enabled']) if row else False + return InvcConfig(guild_id=str(guild_id), role_id=role_id, enabled=enabled) + +@router.patch("/{guild_id}/invcrole", summary="Update Invc Role config") +async def patch_guild_invcrole(guild_id: int, data: InvcUpdate): + db = await db_manager.get_connection('db/invc.db') + + # Get existing row to merge + cursor = await db.execute("SELECT role_id, enabled FROM vcroles WHERE guild_id = ?", (guild_id,)) + row = await cursor.fetchone() + + current_role = row['role_id'] if row else 0 + current_enabled = row['enabled'] if row else 0 + + # Update values only if they are provided in the request + # If data.role_id is None, it could mean 'unset' (if sent as null) or 'no change' (if omitted). + # Given our dashboard sends the full object, we treat None as unset if it matches our frontend's behavior. + new_role = current_role + if data.role_id is not None: + try: + new_role = int(data.role_id) if data.role_id and data.role_id != "none" else 0 + except: + new_role = 0 + elif data.role_id is None: + # In our dash, null means "No Role Selected" + new_role = 0 + + new_enabled = current_enabled + if data.enabled is not None: + new_enabled = 1 if data.enabled else 0 + + await db.execute("INSERT OR REPLACE INTO vcroles (guild_id, role_id, enabled) VALUES (?, ?, ?)", + (guild_id, new_role, new_enabled)) + await db.commit() + return {"status": "success"} + + +# ========== AUTO REACT ========== + +@router.get("/{guild_id}/autoreact", response_model=AutoReactConfig, summary="Get AutoReact config") +async def get_guild_autoreact(guild_id: int): + import aiosqlite + async with aiosqlite.connect("db/autoreact.db") as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS autoreact ( + guild_id INTEGER, + trigger TEXT, + emojis TEXT + ) + """) + await db.commit() + + async with db.execute("SELECT trigger, emojis FROM autoreact WHERE guild_id = ?", (guild_id,)) as cursor: + rows = await cursor.fetchall() + + triggers = [AutoReactTrigger(trigger=row[0], emojis=row[1]) for row in rows] + return AutoReactConfig(guild_id=str(guild_id), triggers=triggers) + +@router.patch("/{guild_id}/autoreact", summary="Update AutoReact config") +async def patch_guild_autoreact(guild_id: int, data: AutoReactUpdate): + db = await db_manager.get_connection("db/autoreact.db") + await db.execute(""" + CREATE TABLE IF NOT EXISTS autoreact ( + guild_id INTEGER, + trigger TEXT, + emojis TEXT + ) + """) + await db.execute("DELETE FROM autoreact WHERE guild_id = ?", (guild_id,)) + for t in data.triggers: + await db.execute("INSERT INTO autoreact (guild_id, trigger, emojis) VALUES (?, ?, ?)", + (guild_id, t.trigger, t.emojis)) + await db.commit() + return {"status": "success"} + + +# ========== INVITES LEADERBOARD ========== + +@router.get("/{guild_id}/invites", response_model=InvitesLeaderboard, summary="Get invite leaderboard") +async def get_guild_invites(guild_id: int): + table_name = f"invites_{guild_id}" + data_list = [] + + db = await db_manager.get_connection("db/invite.db") + # Check if table exists + async with db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,)) as cursor: + exists = await cursor.fetchone() + + if exists: + try: + # Use a safer query that handles missing columns if necessary + async with db.execute(f"SELECT user_id, total, fake, left, rejoin FROM [{table_name}] ORDER BY total DESC LIMIT 20") as cursor: + rows = await cursor.fetchall() + for row in rows: + data_list.append(InviteStat( + user_id=str(row[0]), + total=row[1] or 0, + fake=row[2] or 0, + left=row[3] or 0, + rejoin=row[4] or 0 + )) + except Exception as e: + print(f"Error fetching invites for {guild_id}: {e}") + else: + # If no tracking data yet, just return empty list + pass + + return InvitesLeaderboard(guild_id=str(guild_id), data=data_list) + + +# ========== REACTION ROLES ========== + +@router.get("/{guild_id}/reactionroles", response_model=RRConfig, summary="Get Reaction Roles config") +async def get_guild_rr(guild_id: int): + db = await db_manager.get_connection("rr.db") + await db.execute(""" + CREATE TABLE IF NOT EXISTS reaction_roles ( + guild_id INTEGER, + message_id INTEGER, + emoji TEXT, + role_id INTEGER + ) + """) + await db.execute(""" + CREATE TABLE IF NOT EXISTS rr_settings ( + guild_id INTEGER PRIMARY KEY, + dm_enabled INTEGER DEFAULT 1 + ) + """) + await db.commit() + + async with db.execute("SELECT dm_enabled FROM rr_settings WHERE guild_id = ?", (guild_id,)) as cursor: + dm_row = await cursor.fetchone() + dm_enabled = dm_row[0] == 1 if dm_row else True + + async with db.execute("SELECT message_id, emoji, role_id FROM reaction_roles WHERE guild_id = ?", (guild_id,)) as cursor: + rows = await cursor.fetchall() + + roles_list = [ + ReactionRoleEntry( + message_id=str(row[0]), + emoji=row[1], + role_id=str(row[2]) + ) for row in rows + ] + + return RRConfig(guild_id=str(guild_id), dm_enabled=dm_enabled, roles=roles_list) + +@router.patch("/{guild_id}/reactionroles", summary="Update Reaction Roles config") +async def patch_guild_rr(guild_id: int, data: RRUpdate): + db = await db_manager.get_connection("rr.db") + await db.execute(""" + CREATE TABLE IF NOT EXISTS reaction_roles ( + guild_id INTEGER, + message_id INTEGER, + emoji TEXT, + role_id INTEGER + ) + """) + await db.execute(""" + CREATE TABLE IF NOT EXISTS rr_settings ( + guild_id INTEGER PRIMARY KEY, + dm_enabled INTEGER DEFAULT 1 + ) + """) + + if data.dm_enabled is not None: + await db.execute("INSERT OR REPLACE INTO rr_settings (guild_id, dm_enabled) VALUES (?, ?)", + (guild_id, 1 if data.dm_enabled else 0)) + + if data.add_role is not None: + msg_id = int(data.add_role.message_id) + role_id = int(data.add_role.role_id) + await db.execute("INSERT INTO reaction_roles (guild_id, message_id, emoji, role_id) VALUES (?, ?, ?, ?)", + (guild_id, msg_id, data.add_role.emoji, role_id)) + + if data.remove_role_message_id is not None and data.remove_role_emoji is not None: + msg_id = int(data.remove_role_message_id) + await db.execute("DELETE FROM reaction_roles WHERE guild_id = ? AND message_id = ? AND emoji = ?", + (guild_id, msg_id, data.remove_role_emoji)) + + await db.commit() + return {"status": "success"} + + diff --git a/bot/api/schemas.py b/bot/api/schemas.py new file mode 100644 index 0000000..d128ef6 --- /dev/null +++ b/bot/api/schemas.py @@ -0,0 +1,346 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from pydantic import BaseModel, HttpUrl +from typing import Dict, List, Optional + +# --- Bot Schemas --- + +class BotInfo(BaseModel): + name: str + id: Optional[str] + guilds: int + users: int + commands: int + latency: str + +class BotStatus(BaseModel): + user: str + id: Optional[str] + latency: float + guild_count: int + user_count: int + shards: Optional[int] + +# --- Guild Schemas --- + +class GuildSummary(BaseModel): + id: str + name: str + icon_url: Optional[str] + owner_id: str + member_count: int + +class GuildDetails(BaseModel): + id: str + name: str + icon: Optional[str] + owner_id: str + member_count: int + role_count: int + channel_count: int + +class DiscordChannel(BaseModel): + id: str + name: str + type: str + +class DiscordRole(BaseModel): + id: str + name: str + color: int + position: int # text, voice, category, etc. + +# --- Module Configurations --- + +class PrefixConfig(BaseModel): + guild_id: int + prefix: str + +class AutomodConfig(BaseModel): + guild_id: int + enabled: bool + punishments: Dict[str, str] + ignored_roles: List[int] + ignored_channels: List[int] + logging_channel: Optional[int] + +class TicketCategory(BaseModel): + name: str + emoji: Optional[str] + staff_roles: List[int] = [] + button_style: Optional[int] = 2 # Blurple + discord_category_id: Optional[int] = None + +class TicketEmbed(BaseModel): + title: Optional[str] + description: Optional[str] + color: Optional[int] = None + image_url: Optional[str] = None + thumbnail_url: Optional[str] = None + +class TicketConfig(BaseModel): + guild_id: int + panel_channel: Optional[int] + panel_message: Optional[int] + logging_channel: Optional[int] = None + closed_category: Optional[int] = None + panel_type: Optional[str] = "button" + embed: TicketEmbed + categories: List[TicketCategory] + staff_roles: List[int] + open_ticket_count: int + +class LevelingEmbedStyle(BaseModel): + color: str + thumbnail: bool = False + image: Optional[str] = None + +class LevelingConfig(BaseModel): + guild_id: int + enabled: bool + xp_per_message: int + cooldown: int + level_up_channel: Optional[int] + embed_style: LevelingEmbedStyle + +class LoggingConfig(BaseModel): + guild_id: int + log_enabled: Dict[str, bool] + log_channels: Dict[str, int] + ignore_channels: List[int] + ignore_roles: List[int] + ignore_users: List[int] + auto_delete_duration: Optional[int] + +class WelcomeEmbedData(BaseModel): + message: Optional[str] = None + title: Optional[str] = None + description: Optional[str] = None + color: Optional[str] = None + footer_text: Optional[str] = None + footer_icon: Optional[str] = None + author_name: Optional[str] = None + author_icon: Optional[str] = None + thumbnail: Optional[str] = None + image: Optional[str] = None + +class WelcomeConfig(BaseModel): + guild_id: int + welcome_type: Optional[str] = None + welcome_message: Optional[str] = None + channel_id: Optional[str] = None + embed_data: Optional[WelcomeEmbedData] = None + auto_delete_duration: Optional[int] = None + +class AntiNukeConfig(BaseModel): + guild_id: int + status: bool + whitelisted_users: List[str] = [] + +class VerificationConfig(BaseModel): + guild_id: int + verification_channel_id: Optional[str] = None + verified_role_id: Optional[str] = None + log_channel_id: Optional[str] = None + verification_method: Optional[str] = "both" + enabled: Optional[bool] = True + +class TrackingConfig(BaseModel): + guild_id: int + channel_id: Optional[int] = None + +class TrackingUpdate(BaseModel): + channel_id: Optional[int] = None + +class J2CConfig(BaseModel): + guild_id: str + join_channel_id: Optional[str] = None + control_channel_id: Optional[str] = None + category_id: Optional[str] = None + +class J2CUpdate(BaseModel): + join_channel_id: Optional[str] = None + control_channel_id: Optional[str] = None + category_id: Optional[str] = None + +class JoinDMConfig(BaseModel): + guild_id: str + message: Optional[str] = None + +class JoinDMUpdate(BaseModel): + message: Optional[str] = None + +class CustomRoleConfig(BaseModel): + guild_id: str + staff: Optional[str] = None + girl: Optional[str] = None + vip: Optional[str] = None + guest: Optional[str] = None + frnd: Optional[str] = None + reqrole: Optional[str] = None + +class CustomRoleUpdate(BaseModel): + staff: Optional[str] = None + girl: Optional[str] = None + vip: Optional[str] = None + guest: Optional[str] = None + frnd: Optional[str] = None + reqrole: Optional[str] = None + +class AutoReactTrigger(BaseModel): + trigger: str + emojis: str + +class AutoReactConfig(BaseModel): + guild_id: str + triggers: List[AutoReactTrigger] = [] + +class AutoReactUpdate(BaseModel): + triggers: List[AutoReactTrigger] + +class InvcConfig(BaseModel): + guild_id: str + role_id: Optional[str] = None + enabled: bool = False + +class InvcUpdate(BaseModel): + role_id: Optional[str] = None + enabled: Optional[bool] = None + +class ReactionRoleEntry(BaseModel): + message_id: str + emoji: str + role_id: str + +class RRConfig(BaseModel): + guild_id: str + dm_enabled: bool + roles: List[ReactionRoleEntry] = [] + +class RRUpdate(BaseModel): + dm_enabled: Optional[bool] = None + add_role: Optional[ReactionRoleEntry] = None + remove_role_message_id: Optional[str] = None + remove_role_emoji: Optional[str] = None + +class InviteStat(BaseModel): + user_id: str + total: int + fake: int + left: int + rejoin: int + +class InvitesLeaderboard(BaseModel): + guild_id: str + data: List[InviteStat] + +# --- Update Schemas (Input) --- + +class VerificationUpdate(BaseModel): + verification_channel_id: Optional[str] = None + verified_role_id: Optional[str] = None + log_channel_id: Optional[str] = None + verification_method: Optional[str] = None + enabled: Optional[bool] = None + +class VanityRoleSetup(BaseModel): + vanity: str + role_id: str + log_channel_id: str + +class AutoRoleConfig(BaseModel): + guild_id: str + bots: List[str] + humans: List[str] + +class AutoRoleUpdate(BaseModel): + bots: Optional[List[str]] = None + humans: Optional[List[str]] = None + +class AntiNukeUpdate(BaseModel): + status: Optional[bool] = None + add_whitelist: Optional[str] = None + remove_whitelist: Optional[str] = None + + +class WelcomeUpdate(BaseModel): + welcome_type: Optional[str] = None + welcome_message: Optional[str] = None + channel_id: Optional[str] = None + embed_data: Optional[WelcomeEmbedData] = None + auto_delete_duration: Optional[int] = None + +class PrefixUpdate(BaseModel): + prefix: str + +class TicketUpdate(BaseModel): + panel_channel: Optional[int] = None + logging_channel: Optional[int] = None + closed_category: Optional[int] = None + panel_type: Optional[str] = None + embed_title: Optional[str] = None + embed_description: Optional[str] = None + embed_color: Optional[int] = None + embed_image_url: Optional[str] = None + embed_thumbnail_url: Optional[str] = None + categories: Optional[List[TicketCategory]] = None + staff_roles: Optional[List[int]] = None + +class AutomodUpdate(BaseModel): + enabled: Optional[bool] = None + punishments: Optional[Dict[str, str]] = None + ignored_roles: Optional[List[int]] = None + ignored_channels: Optional[List[int]] = None + logging_channel: Optional[int] = None + +class LevelingUpdate(BaseModel): + enabled: Optional[bool] = None + xp_per_message: Optional[int] = None + cooldown: Optional[int] = None + level_up_channel: Optional[int] = None + embed_color: Optional[str] = None + +class LoggingUpdate(BaseModel): + log_enabled: Optional[Dict[str, bool]] = None + log_channels: Optional[Dict[str, int]] = None + +class LeaderboardEntry(BaseModel): + user_id: str + name: str + level: int + xp: int + +# --- Admin Schemas --- + +class AdminNodeStatus(BaseModel): + name: str + status: str + load: str + icon: str # Icon identifier + +class AdminStats(BaseModel): + total_users: str + active_servers: str + api_latency: str + db_size: str + nodes: List[AdminNodeStatus] + +class AdminConfig(BaseModel): + maintenance_mode: bool + global_notification: Optional[str] = None + +class AdminConfigUpdate(BaseModel): + maintenance_mode: Optional[bool] = None + global_notification: Optional[str] = None diff --git a/bot/api/server.py b/bot/api/server.py new file mode 100644 index 0000000..b33ee22 --- /dev/null +++ b/bot/api/server.py @@ -0,0 +1,153 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from fastapi import FastAPI, Depends, Request, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from contextlib import asynccontextmanager +import os +import time +import json +import logging +from slowapi import _rate_limit_exceeded_handler +from slowapi.errors import RateLimitExceeded +from slowapi.middleware import SlowAPIMiddleware +from utils.config import * + + +from api.routes import bot, guilds, admin +from api.dependencies import verify_api_key, limiter +from api.db_manager import db_manager +from api.discord_auth import ( + require_dashboard_admin, + require_discord_session, + require_guild_access, + extract_guild_id_from_path, +) +from fastapi.responses import JSONResponse + +# Configure logging +logger = logging.getLogger("api_request_logs") +logger.setLevel(logging.INFO) +if not logger.handlers: + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter('%(message)s')) + logger.addHandler(handler) + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Setup: Nothing special needed for now + yield + # Shutdown: Close all shared database connections + await db_manager.close_all() + +def create_app() -> FastAPI: + """ + Initializes the FastAPI application for the CodeX Bot Dashboard. + The bot instance will be attached to app.state.bot in CodeX.py at runtime. + """ + app = FastAPI( + title=f"{BRAND_NAME} Bot API", + description=f"REST API to manage the {BRAND_NAME} Discord Bot features", + version="1.0", + dependencies=[Depends(verify_api_key)], + lifespan=lifespan + ) + + # Discord permission checks for dashboard API routes + @app.middleware("http") + async def discord_permission_middleware(request: Request, call_next): + if request.method == "OPTIONS": + return await call_next(request) + + path = request.url.path + try: + if path.startswith("/api/v1/admin"): + await require_dashboard_admin(request) + elif path.startswith("/api/v1/bot"): + await require_discord_session(request) + elif path.startswith("/api/v1/guilds"): + guild_id = extract_guild_id_from_path(path) + if guild_id is not None: + await require_guild_access(request, guild_id) + else: + await require_discord_session(request) + except HTTPException as exc: + return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail}) + + return await call_next(request) + + # Structured Logging Middleware + @app.middleware("http") + async def log_requests(request: Request, call_next): + start_time = time.time() + response = await call_next(request) + process_time = time.time() - start_time + + log_data = { + "timestamp": time.strftime('%Y-%m-%d %H:%M:%S'), + "method": request.method, + "path": request.url.path, + "status_code": response.status_code, + "duration_ms": round(process_time * 1000, 2), + "client_ip": request.client.host if request.client else "unknown" + } + + logger.info(json.dumps(log_data)) + return response + + # Attach limiter and handler + app.state.limiter = limiter + app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + app.add_middleware(SlowAPIMiddleware) + + # Build allowed origins from env + hardcoded fallbacks + _extra_origins = [ + o.strip() + for o in os.getenv("CORS_ORIGINS", "").split(",") + if o.strip() + ] + _allowed_origins = list(dict.fromkeys([ + "http://localhost:3000", + "https://localhost:3000", + "https://your-vercel-url-here.vercel.app", + *_extra_origins, + ])) + + # Enable CORS for Next.js dashboard + app.add_middleware( + CORSMiddleware, + allow_origins=_allowed_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + # Register Routers + app.include_router(bot.router, prefix="/api/v1/bot", tags=["Bot"]) + app.include_router(guilds.router, prefix="/api/v1/guilds", tags=["Guilds"]) + app.include_router(admin.router, prefix="/api/v1/admin", tags=["Admin"]) + + @app.get("/", summary="API Root", description="Returns basic API information and online status.") + async def root(): + return { + "status": "online", + "bot_name": {BRAND_NAME}, + "api_version": "1.0" + } + + @app.get("/health", summary="Health Check", description="Performs a health check for container orchestration and uptime monitoring.") + async def health(): + return {"status": "ok"} + + return app diff --git a/bot/assets/background/background.png b/bot/assets/background/background.png new file mode 100644 index 0000000..d73aa86 Binary files /dev/null and b/bot/assets/background/background.png differ diff --git a/bot/assets/fonts/minecraft.ttf b/bot/assets/fonts/minecraft.ttf new file mode 100644 index 0000000..85c1472 Binary files /dev/null and b/bot/assets/fonts/minecraft.ttf differ diff --git a/bot/assets/leaderboardlevel.gif b/bot/assets/leaderboardlevel.gif new file mode 100644 index 0000000..e86b2d4 Binary files /dev/null and b/bot/assets/leaderboardlevel.gif differ diff --git a/bot/cogs/__init__.py b/bot/cogs/__init__.py new file mode 100644 index 0000000..c58e30f --- /dev/null +++ b/bot/cogs/__init__.py @@ -0,0 +1,376 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations +from core import zyrox +from colorama import Fore, Style, init + + +from .commands.help import Help +from .commands.general import General +from .commands.music import Music +from .commands.automod import Automod +from .commands.welcome import Welcomer +from .commands.fun import Fun +from .commands.Games import Games +from .commands.extra import Extra +from .commands.owner import Owner +from .commands.voice import Voice +from .commands.afk import afk +from .commands.ignore import Ignore +from .commands.Media import Media +from .commands.Invc import Invcrole +from .commands.giveaway import Giveaway +from .commands.Embed import Embed +from .commands.steal import Steal +from .commands.timer import Timer +from .commands.blacklist import Blacklist +from .commands.block import Block +from .commands.nightmode import Nightmode +from .commands.tracking import Tracking +from .commands.owner import Badges +#from .commands.map import Map +from .commands.autoresponder import AutoResponder +from .commands.customrole import Customrole +from .commands.autorole import AutoRole +from .commands.ticket import TicketCog +from .commands.logging import Logging +from .commands.translate import TranslateCog +from .commands.jail import Jail +from .commands.antinuke import Antinuke +from .commands.extraown import Extraowner +from .commands.anti_wl import Whitelist +from .commands.anti_unwl import Unwhitelist +from .commands.slots import Slots +from .commands.blackjack import Blackjack +from .commands.autoreact import AutoReaction +from .commands.stats import Stats +from .commands.emergency import Emergency +from .commands.notify import NotifCommands +from .commands.status import Status +from .commands.np import NoPrefix +from .commands.filters import FilterCog +from .commands.owner2 import Global +from .commands.qr import QR +from .commands.vanityroles import VanityRoles +from .commands.reactionroles import ReactionRoles +from .commands.messages import Messages +from .commands.fastgreet import FastGreet +from .commands.counting import Counting +from .commands.j2c import JoinToCreate +from .commands.ai import AI +from .commands.dms import StaffDMCog +from .commands.booster import Booster +from .commands.leveling import Leveling +from .commands.stickymessage import StickyMessage +from .commands.verification import Verification +from .commands.minecraft import Minecraft +from .commands.encryption import encryption +from .commands.calc import calculator +from .commands.joindm import joindm +from .commands.Birthday import Birthdays +from .commands.nitro import Nitro +from .commands.image import ImageCommands +from .commands.youtube import Youtube +#____________ Events _____________ + +#from .events.autoblacklist import AutoBlacklist +from .events.Errors import Errors +from .events.on_guild import Guild +from .events.autorole import Autorole2 +from .events.auto import Autorole +from .events.greet2 import greet +from .events.mention import Mention +from .events.react import React +from .events.autoreact import AutoReactListener +#from .events.topgg import TopGG +from .events.ai import AIResponses +from .events.stickymessage import StickyMessageListener + +########-------HELP-------######## +from .zyrox.antinuke import _antinuke +from .zyrox.extra import _extra +from .zyrox.general import _general +from .zyrox.automod import _automod +from .zyrox.moderation import _moderation +#from .zyrox.inviteTracker import _inviteTracker +from .zyrox.music import _music +from .zyrox.fun import _fun +from .zyrox.games import _games +from .zyrox.ignore import _ignore +from .zyrox.server import _server +from .zyrox.voice import _voice +from .zyrox.welcome import _welcome +from .zyrox.giveaway import _giveaway +from .zyrox.ticket import _ticket +#from .axon.vanityroles import Vanityroles69999 +from .zyrox.logging import _logging +from .zyrox.vanity import _vanity +from .zyrox.inviteTracker import inviteTracker +from .zyrox.counting import _Counting +from .zyrox.j2c import _J2C +from .zyrox.ai import _ai +from .zyrox.booster import __boost +from .zyrox.leveling import _leveling +from .zyrox.sticky import _sticky +from .zyrox.verify import _verify +from .zyrox.encryption import _encrypt +from .zyrox.mc import _mc +from .zyrox.joindm import _joindm +from .zyrox.birth import _birth + +#########ANTINUKE######### + +from .antinuke.anti_member_update import AntiMemberUpdate +from .antinuke.antiban import AntiBan +from .antinuke.antibotadd import AntiBotAdd +from .antinuke.antichcr import AntiChannelCreate +from .antinuke.antichdl import AntiChannelDelete +from .antinuke.antichup import AntiChannelUpdate +from .antinuke.antieveryone import AntiEveryone +from .antinuke.antiguild import AntiGuildUpdate +from .antinuke.antiIntegration import AntiIntegration +from .antinuke.antikick import AntiKick +from .antinuke.antiprune import AntiPrune +from .antinuke.antirlcr import AntiRoleCreate +from .antinuke.antirldl import AntiRoleDelete +from .antinuke.antirlup import AntiRoleUpdate +from .antinuke.antiwebhook import AntiWebhookUpdate +from .antinuke.antiwebhookcr import AntiWebhookCreate +from .antinuke.antiwebhookdl import AntiWebhookDelete + +#Extra Optional Events + +#from .antinuke.antiemocr import AntiEmojiCreate +#from .antinuke.antiemodl import AntiEmojiDelete +#from .antinuke.antiemoup import AntiEmojiUpdate +#from .antinuke.antisticker import AntiSticker +#from .antinuke.antiunban import AntiUnban + +############ AUTOMOD ############ +from .automod.antispam import AntiSpam +from .automod.anticaps import AntiCaps +from .automod.antilink import AntiLink +from .automod.anti_invites import AntiInvite +from .automod.anti_mass_mention import AntiMassMention +from .automod.anti_emoji_spam import AntiEmojiSpam + + +from .moderation.ban import Ban +from .moderation.unban import Unban +from .moderation.timeout import Mute +from .moderation.unmute import Unmute +from .moderation.lock import Lock +from .moderation.unlock import Unlock +from .moderation.hide import Hide +from .moderation.unhide import Unhide +from .moderation.kick import Kick +from .moderation.warn import Warn +from .moderation.role import Role +from .moderation.message import Message +from .moderation.moderation import Moderation +from .moderation.topcheck import TopCheck +from .moderation.snipe import Snipe + + +from utils.config import BotName + +async def setup(bot: zyrox): + cogs_to_load = [ + Help, General, Moderation, Automod, Welcomer, Fun, Games, Extra, + Voice, Owner, Customrole, afk, Embed, Media, Ignore, TicketCog, Logging, + Invcrole, Steal, Timer, + Blacklist, Block, Nightmode, Badges, Antinuke, Whitelist, + Unwhitelist, Extraowner, Blackjack, Slots, Guild, Errors, Autorole2, Autorole, greet, AutoResponder, + Mention, AutoRole, React, AntiMemberUpdate, AntiBan, AntiBotAdd, + AntiChannelCreate, AntiChannelDelete, AntiChannelUpdate, AntiEveryone, AntiGuildUpdate, + AntiIntegration, AntiKick, AntiPrune, AntiRoleCreate, AntiRoleDelete, + AntiRoleUpdate, AntiWebhookUpdate, AntiWebhookCreate, + AntiWebhookDelete, AntiSpam, AntiCaps, AntiLink, AntiInvite, AntiMassMention, Stats, Status, NoPrefix, FilterCog, AutoReaction, AutoReactListener, Ban, Unban, Mute, Unmute, Lock, Unlock, Hide, Unhide, Kick, Warn, Role, Message, Moderation, TopCheck, Snipe, Global, QR, VanityRoles, ReactionRoles, Messages, TranslateCog, FastGreet, Jail, inviteTracker,Counting,AI + ] + + + await bot.add_cog(Help(bot)) + await bot.add_cog(General(bot)) + await bot.add_cog(Music(bot)) + await bot.add_cog(Automod(bot)) + await bot.add_cog(Welcomer(bot)) + await bot.add_cog(Fun(bot)) + await bot.add_cog(Tracking(bot)) + await bot.add_cog(Games(bot)) + await bot.add_cog(Extra(bot)) + await bot.add_cog(Voice(bot)) + await bot.add_cog(Owner(bot)) + await bot.add_cog(Customrole(bot)) + await bot.add_cog(afk(bot)) + await bot.add_cog(Embed(bot)) + await bot.add_cog(Media(bot)) + await bot.add_cog(Ignore(bot)) + await bot.add_cog(Invcrole(bot)) + await bot.add_cog(Giveaway(bot)) + await bot.add_cog(Steal(bot)) + await bot.add_cog(Booster(bot)) + await bot.add_cog(Timer(bot)) + await bot.add_cog(Blacklist(bot)) + await bot.add_cog(Block(bot)) + await bot.add_cog(Nightmode(bot)) + await bot.add_cog(Badges(bot)) + await bot.add_cog(Antinuke(bot)) + await bot.add_cog(Whitelist(bot)) + await bot.add_cog(Unwhitelist(bot)) + await bot.add_cog(Extraowner(bot)) + await bot.add_cog(Slots(bot)) + await bot.add_cog(Blackjack(bot)) + await bot.add_cog(Stats(bot)) + await bot.add_cog(Emergency(bot)) + await bot.add_cog(Status(bot)) + await bot.add_cog(NoPrefix(bot)) + await bot.add_cog(FilterCog(bot)) + await bot.add_cog(Global(bot)) + # await bot.add_cog(Map(bot)) + await bot.add_cog(TicketCog(bot)) + await bot.add_cog(Logging(bot)) + await bot.add_cog(QR(bot)) + await bot.add_cog(VanityRoles(bot)) + await bot.add_cog(ReactionRoles(bot)) + await bot.add_cog(Messages(bot)) + await bot.add_cog(TranslateCog(bot)) + await bot.add_cog(FastGreet(bot)) + await bot.add_cog(Jail(bot)) + await bot.add_cog(JoinToCreate(bot)) + await bot.add_cog(AI(bot)) + await bot.add_cog(StaffDMCog(bot)) + await bot.add_cog(Leveling(bot)) + await bot.add_cog(StickyMessage(bot)) + await bot.add_cog(Verification(bot)) + await bot.add_cog(Minecraft(bot)) + await bot.add_cog(encryption(bot)) + await bot.add_cog(calculator(bot)) + await bot.add_cog(joindm(bot)) + await bot.add_cog(Birthdays(bot)) + await bot.add_cog(Nitro(bot)) + await bot.add_cog(ImageCommands(bot)) + await bot.add_cog(Youtube(bot)) + + await bot.add_cog(_antinuke(bot)) + await bot.add_cog(_extra(bot)) + await bot.add_cog(_general(bot)) + await bot.add_cog(_automod(bot)) + await bot.add_cog(_moderation(bot)) + await bot.add_cog(_music(bot)) + await bot.add_cog(_fun(bot)) + await bot.add_cog(_games(bot)) + await bot.add_cog(_ignore(bot)) + await bot.add_cog(_server(bot)) + await bot.add_cog(_voice(bot)) + await bot.add_cog(_welcome(bot)) + await bot.add_cog(_giveaway(bot)) + await bot.add_cog(_ticket(bot)) + await bot.add_cog(_logging(bot)) + await bot.add_cog(_vanity(bot)) + await bot.add_cog(inviteTracker(bot)) + await bot.add_cog(Counting(bot)) + await bot.add_cog(_Counting(bot)) + await bot.add_cog(_J2C(bot)) + await bot.add_cog(_ai(bot)) + await bot.add_cog(__boost(bot)) + await bot.add_cog(_leveling(bot)) + await bot.add_cog(_sticky(bot)) + await bot.add_cog(_verify(bot)) + await bot.add_cog(_encrypt(bot)) + await bot.add_cog(_mc(bot)) + await bot.add_cog(_joindm(bot)) + await bot.add_cog(_birth(bot)) + + + + #await bot.add_cog(AutoBlacklist(bot)) + await bot.add_cog(Guild(bot)) + await bot.add_cog(Errors(bot)) + await bot.add_cog(Autorole2(bot)) + await bot.add_cog(Autorole(bot)) + await bot.add_cog(greet(bot)) + await bot.add_cog(AutoResponder(bot)) + await bot.add_cog(Mention(bot)) + await bot.add_cog(AutoRole(bot)) + await bot.add_cog(React(bot)) + await bot.add_cog(AutoReaction(bot)) + await bot.add_cog(AutoReactListener(bot)) + await bot.add_cog(NotifCommands(bot)) + await bot.add_cog(StickyMessageListener(bot)) + await bot.add_cog(AIResponses(bot)) + + + await bot.add_cog(AntiMemberUpdate(bot)) + await bot.add_cog(AntiBan(bot)) + await bot.add_cog(AntiBotAdd(bot)) + await bot.add_cog(AntiChannelCreate(bot)) + await bot.add_cog(AntiChannelDelete(bot)) + await bot.add_cog(AntiChannelUpdate(bot)) + await bot.add_cog(AntiEveryone(bot)) + await bot.add_cog(AntiGuildUpdate(bot)) + await bot.add_cog(AntiIntegration(bot)) + await bot.add_cog(AntiKick(bot)) + await bot.add_cog(AntiPrune(bot)) + await bot.add_cog(AntiRoleCreate(bot)) + await bot.add_cog(AntiRoleDelete(bot)) + await bot.add_cog(AntiRoleUpdate(bot)) + await bot.add_cog(AntiWebhookUpdate(bot)) + await bot.add_cog(AntiWebhookCreate(bot)) + await bot.add_cog(AntiWebhookDelete(bot)) + + +#Extra Optional Events + + #await bot.add_cog(AntiEmojiCreate(bot)) + #await bot.add_cog(AntiEmojiDelete(bot)) + #await bot.add_cog(AntiEmojiUpdate(bot)) + #await bot.add_cog(AntiSticker(bot)) + #await bot.add_cog(AntiUnban(bot)) + + + await bot.add_cog(AntiSpam(bot)) + await bot.add_cog(AntiCaps(bot)) + await bot.add_cog(AntiInvite(bot)) + await bot.add_cog(AntiLink(bot)) + await bot.add_cog(AntiMassMention(bot)) + await bot.add_cog(AntiEmojiSpam(bot)) + + + + + + + + await bot.add_cog(Ban(bot)) + await bot.add_cog(Unban(bot)) + await bot.add_cog(Mute(bot)) + await bot.add_cog(Unmute(bot)) + await bot.add_cog(Lock(bot)) + await bot.add_cog(Unlock(bot)) + await bot.add_cog(Hide(bot)) + await bot.add_cog(Unhide(bot)) + await bot.add_cog(Kick(bot)) + await bot.add_cog(Warn(bot)) + await bot.add_cog(Role(bot)) + await bot.add_cog(Message(bot)) + await bot.add_cog(Moderation(bot)) + await bot.add_cog(TopCheck(bot)) + await bot.add_cog(Snipe(bot)) + + + + for cog in cogs_to_load: + print(Fore.RED + Style.BRIGHT + f"Loaded cog: {cog.__name__}") + print(Fore.RED + Style.BRIGHT + f"All {BotName} Cogs loaded successfully.") diff --git a/bot/cogs/antinuke/antiIntegration.py b/bot/cogs/antinuke/antiIntegration.py new file mode 100644 index 0000000..09fbc64 --- /dev/null +++ b/bot/cogs/antinuke/antiIntegration.py @@ -0,0 +1,148 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import datetime +import pytz + +class AntiIntegration(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.event_limits = {} + self.cooldowns = {} + + async def fetch_audit_logs(self, guild, action): + try: + async for entry in guild.audit_logs(action=action, limit=1): + now = datetime.datetime.now(pytz.utc) + created_at = entry.created_at + difference = (now - created_at).total_seconds() * 1000 + + if difference >= 3600000: + return None + + return entry + + except Exception: + return None + return None + + def can_fetch_audit(self, guild_id, event_name, max_requests=6, interval=10, cooldown_duration=300): + now = datetime.datetime.now() + self.event_limits.setdefault(guild_id, {}).setdefault(event_name, []).append(now) + + timestamps = self.event_limits[guild_id][event_name] + timestamps = [t for t in timestamps if (now - t).total_seconds() <= interval] + self.event_limits[guild_id][event_name] = timestamps + + if guild_id in self.cooldowns and event_name in self.cooldowns[guild_id]: + if (now - self.cooldowns[guild_id][event_name]).total_seconds() < cooldown_duration: + return False + del self.cooldowns[guild_id][event_name] + + if len(timestamps) > max_requests: + self.cooldowns.setdefault(guild_id, {})[event_name] = now + return False + + return True + + async def is_blacklisted_guild(self, guild_id): + async with aiosqlite.connect('db/block.db') as block_db: + cursor = await block_db.execute("SELECT 1 FROM guild_blacklist WHERE guild_id = ?", (str(guild_id),)) + return await cursor.fetchone() is not None + + @commands.Cog.listener() + async def on_guild_integrations_update(self, guild): + if await self.is_blacklisted_guild(guild.id): + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + + if not antinuke_status or not antinuke_status[0]: + return + + if not self.can_fetch_audit(guild.id, 'integration_create'): + return + + logs = await self.fetch_audit_logs(guild, discord.AuditLogAction.integration_create) + if logs is None: + return + + executor = logs.user + + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", + (guild.id, executor.id)) as cursor: + extraowner_status = await cursor.fetchone() + + if extraowner_status: + return + + async with db.execute("SELECT mngweb FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", + (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + + if whitelist_status and whitelist_status[0]: + return + + await self.ban_executor(guild, executor) + + async def ban_executor(self, guild, executor): + retries = 3 + while retries > 0: + try: + await guild.ban(executor, reason="Integration Create | Unwhitelisted User") + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + return + except discord.errors.RateLimited as e: + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception: + return + + async def revert_integration_changes(self, guild): + retries = 3 + while retries > 0: + try: + await guild.edit(integrations=[]) + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + return + except Exception: + return diff --git a/bot/cogs/antinuke/anti_member_update.py b/bot/cogs/antinuke/anti_member_update.py new file mode 100644 index 0000000..0d97c7c --- /dev/null +++ b/bot/cogs/antinuke/anti_member_update.py @@ -0,0 +1,144 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import datetime +import pytz + +class AntiMemberUpdate(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.event_limits = {} + self.cooldowns = {} + + async def is_blacklisted_guild(self, guild_id): + async with aiosqlite.connect('db/block.db') as block_db: + cursor = await block_db.execute("SELECT 1 FROM guild_blacklist WHERE guild_id = ?", (str(guild_id),)) + return await cursor.fetchone() is not None + + async def fetch_audit_logs(self, guild, action, target_id): + if not guild.me.guild_permissions.ban_members: + return None + try: + async for entry in guild.audit_logs(action=action, limit=1): + if entry.target.id == target_id: + now = datetime.datetime.now(pytz.utc) + created_at = entry.created_at + difference = (now - created_at).total_seconds() * 1000 + if difference < 3600000: + return entry + except Exception: + pass + return None + + def can_fetch_audit(self, guild_id, event_name, max_requests=5, interval=10, cooldown_duration=300): + now = datetime.datetime.now() + self.event_limits.setdefault(guild_id, {}).setdefault(event_name, []).append(now) + + timestamps = self.event_limits[guild_id][event_name] + timestamps = [t for t in timestamps if (now - t).total_seconds() <= interval] + self.event_limits[guild_id][event_name] = timestamps + + if guild_id in self.cooldowns and event_name in self.cooldowns[guild_id]: + if (now - self.cooldowns[guild_id][event_name]).total_seconds() < cooldown_duration: + return False + del self.cooldowns[guild_id][event_name] + + if len(timestamps) > max_requests: + self.cooldowns.setdefault(guild_id, {})[event_name] = now + return False + + return True + + @commands.Cog.listener() + async def on_member_update(self, before, after): + guild = before.guild + + if await self.is_blacklisted_guild(guild.id): + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + if not antinuke_status or not antinuke_status[0]: + return + + if not self.can_fetch_audit(guild.id, 'member_update'): + return + + log_entry = await self.fetch_audit_logs(guild, discord.AuditLogAction.member_role_update, after.id) + if log_entry is None: + return + + executor = log_entry.user + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", + (guild.id, executor.id)) as cursor: + extra_owner_status = await cursor.fetchone() + if extra_owner_status: + return + + async with db.execute("SELECT memup FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", + (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + if whitelist_status and whitelist_status[0]: + return + + try: + new_role = next(role for role in after.roles if role not in before.roles) + except StopIteration: + return + + if any([ + new_role.permissions.ban_members, + new_role.permissions.administrator, + new_role.permissions.manage_guild, + new_role.permissions.manage_channels, + new_role.permissions.manage_roles, + new_role.permissions.mention_everyone, + new_role.permissions.manage_webhooks + ]): + await self.take_action_and_revert(after, executor, new_role) + await asyncio.sleep(3) + + async def take_action_and_revert(self, member, executor, new_role): + retries = 3 + reason = "Member Role Update with Dangerous Permissions | Unwhitelisted User" + while retries > 0: + try: + await member.remove_roles(new_role, reason=reason) + await member.guild.ban(executor, reason=reason) + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + return + except discord.errors.RateLimited as e: + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception: + return + return diff --git a/bot/cogs/antinuke/antiban.py b/bot/cogs/antinuke/antiban.py new file mode 100644 index 0000000..6898197 --- /dev/null +++ b/bot/cogs/antinuke/antiban.py @@ -0,0 +1,122 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import datetime +import pytz + +class AntiBan(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.event_limits = {} + self.cooldowns = {} + + def can_fetch_audit(self, guild_id, event_name, max_requests=5, interval=10, cooldown_duration=300): + now = datetime.datetime.now() + self.event_limits.setdefault(guild_id, {}).setdefault(event_name, []).append(now) + + timestamps = self.event_limits[guild_id][event_name] + timestamps = [t for t in timestamps if (now - t).total_seconds() <= interval] + self.event_limits[guild_id][event_name] = timestamps + + if guild_id in self.cooldowns and event_name in self.cooldowns[guild_id]: + if (now - self.cooldowns[guild_id][event_name]).total_seconds() < cooldown_duration: + return False + del self.cooldowns[guild_id][event_name] + + if len(timestamps) > max_requests: + self.cooldowns.setdefault(guild_id, {})[event_name] = now + return False + return True + + async def fetch_audit_logs(self, guild, action, target_id): + if not guild.me.guild_permissions.ban_members: + return None + try: + async for entry in guild.audit_logs(action=action, limit=1): + if entry.target.id == target_id: + now = datetime.datetime.now(pytz.utc) + if (now - entry.created_at).total_seconds() * 1000 >= 3600000: + return None + return entry + except Exception: + pass + return None + + @commands.Cog.listener() + async def on_member_ban(self, guild, user): + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + if not antinuke_status or not antinuke_status[0]: + return + + if not self.can_fetch_audit(guild.id, "member_ban"): + return + + entry = await self.fetch_audit_logs(guild, discord.AuditLogAction.ban, user.id) + if not entry: + return + + executor = entry.user + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with db.execute("SELECT ban FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + if whitelist_status and whitelist_status[0]: + return + + await self.ban_executor(guild, executor, user) + + async def ban_executor(self, guild, executor, user, retries=3): + while retries > 0: + try: + await guild.ban(executor, reason="Member Ban | Unwhitelisted User") + await guild.unban(user, reason="Reverting ban by unwhitelisted user") + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + break + except Exception: + return + + retries = 3 + while retries > 0: + try: + await guild.unban(user, reason="Reverting ban by unwhitelisted user") + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + break + except Exception: + return + diff --git a/bot/cogs/antinuke/antibotadd.py b/bot/cogs/antinuke/antibotadd.py new file mode 100644 index 0000000..338e9a7 --- /dev/null +++ b/bot/cogs/antinuke/antibotadd.py @@ -0,0 +1,136 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import datetime +import pytz + +class AntiBotAdd(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.event_limits = {} + self.cooldowns = {} + + def can_fetch_audit(self, guild_id, event_name, max_requests=5, interval=10, cooldown_duration=300): + now = datetime.datetime.now() + self.event_limits.setdefault(guild_id, {}).setdefault(event_name, []).append(now) + + timestamps = self.event_limits[guild_id][event_name] + timestamps = [t for t in timestamps if (now - t).total_seconds() <= interval] + self.event_limits[guild_id][event_name] = timestamps + + if guild_id in self.cooldowns and event_name in self.cooldowns[guild_id]: + if (now - self.cooldowns[guild_id][event_name]).total_seconds() < cooldown_duration: + return False + del self.cooldowns[guild_id][event_name] + + if len(timestamps) > max_requests: + self.cooldowns.setdefault(guild_id, {})[event_name] = now + return False + return True + + async def fetch_audit_logs(self, guild, action, target_id): + if not guild.me.guild_permissions.kick_members: + return None + try: + async for entry in guild.audit_logs(action=action, limit=1): + if entry.target.id == target_id: + now = datetime.datetime.now(pytz.utc) + if (now - entry.created_at).total_seconds() * 1000 >= 3600000: + return None + return entry + except Exception: + pass + return None + + @commands.Cog.listener() + async def on_member_join(self, member): + if not member.bot: + return + + guild = member.guild + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + + if not antinuke_status or not antinuke_status[0]: + return + + if not self.can_fetch_audit(guild.id, "bot_add"): + return + + logs = await self.fetch_audit_logs(guild, discord.AuditLogAction.bot_add, member.id) + if logs is None: + return + + executor = logs.user + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with db.execute("SELECT botadd FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + + if whitelist_status and whitelist_status[0]: + return + + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", (guild.id, executor.id)) as cursor: + extra_owner_status = await cursor.fetchone() + + if extra_owner_status and extra_owner_status[0]: + return + + await self.take_action_and_kick_bot(guild, executor, member, "Unwhitelisted user added a bot") + + async def take_action_and_kick_bot(self, guild, executor, bot_member, reason, retries=3): + while retries > 0: + try: + await guild.kick(bot_member, reason=reason) + await guild.ban(executor, reason=reason) + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + break + except Exception: + return + + retries = 3 + while retries > 0: + try: + await guild.ban(executor, reason=reason) + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + break + except Exception: + return + +async def setup(bot): + await bot.add_cog(AntiBotAdd(bot)) diff --git a/bot/cogs/antinuke/antichcr.py b/bot/cogs/antinuke/antichcr.py new file mode 100644 index 0000000..9ea0b30 --- /dev/null +++ b/bot/cogs/antinuke/antichcr.py @@ -0,0 +1,119 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import datetime +import pytz + +class AntiChannelCreate(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.event_limits = {} + self.cooldowns = {} + + def can_fetch_audit(self, guild_id, event_name, max_requests=6, interval=10, cooldown_duration=300): + now = datetime.datetime.now() + self.event_limits.setdefault(guild_id, {}).setdefault(event_name, []).append(now) + + timestamps = self.event_limits[guild_id][event_name] + timestamps = [t for t in timestamps if (now - t).total_seconds() <= interval] + self.event_limits[guild_id][event_name] = timestamps + + if len(timestamps) > max_requests: + self.cooldowns.setdefault(guild_id, {})[event_name] = now + return False + return True + + async def fetch_audit_logs(self, guild, action, target_id, delay=1): + if not guild.me.guild_permissions.ban_members: + return None + try: + async for entry in guild.audit_logs(action=action, limit=1): + if entry.target.id == target_id: + now = datetime.datetime.now(pytz.utc) + if (now - entry.created_at).total_seconds() * 1000 >= 3600000: + return None + await asyncio.sleep(delay) + return entry + except Exception: + pass + return None + + async def move_role_below_bot(self, guild): + bot_top_role = guild.me.top_role + most_populated_role = max( + [role for role in guild.roles if role.position < bot_top_role.position and not role.managed and role != guild.default_role], + key=lambda r: len(r.members), + default=None + ) + if most_populated_role: + try: + await most_populated_role.edit(position=bot_top_role.position - 1, reason="Emergency: Adjusting roles for security") + except discord.Forbidden: + pass + except discord.HTTPException: + pass + + async def delete_channel_and_ban(self, channel, executor, delay=2, retries=3): + while retries > 0: + try: + await channel.delete(reason="Channel created by unwhitelisted user") + #await asyncio.sleep(delay) + await channel.guild.ban(executor, reason="Channel Create | Unwhitelisted User") + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After', delay) + await asyncio.sleep(float(retry_after)) + retries -= 1 + except Exception: + return + + @commands.Cog.listener() + async def on_guild_channel_create(self, channel): + guild = channel.guild + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + if not antinuke_status or not antinuke_status[0]: + return + + if not self.can_fetch_audit(guild.id, "channel_create"): + await self.move_role_below_bot(guild) + await asyncio.sleep(5) + + logs = await self.fetch_audit_logs(guild, discord.AuditLogAction.channel_create, channel.id, delay=2) + if logs is None: + return + + executor = logs.user + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", (guild.id, executor.id)) as cursor: + if await cursor.fetchone(): + return + + async with db.execute("SELECT chcr FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + if whitelist_status and whitelist_status[0]: + return + + await self.delete_channel_and_ban(channel, executor, delay=2) \ No newline at end of file diff --git a/bot/cogs/antinuke/antichdl.py b/bot/cogs/antinuke/antichdl.py new file mode 100644 index 0000000..9a28796 --- /dev/null +++ b/bot/cogs/antinuke/antichdl.py @@ -0,0 +1,130 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import datetime +import pytz + +class AntiChannelDelete(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.event_limits = {} + self.cooldowns = {} + + def can_fetch_audit(self, guild_id, event_name, max_requests=5, interval=10, cooldown_duration=300): + now = datetime.datetime.now() + self.event_limits.setdefault(guild_id, {}).setdefault(event_name, []).append(now) + + timestamps = self.event_limits[guild_id][event_name] + timestamps = [t for t in timestamps if (now - t).total_seconds() <= interval] + self.event_limits[guild_id][event_name] = timestamps + + if guild_id in self.cooldowns and event_name in self.cooldowns[guild_id]: + if (now - self.cooldowns[guild_id][event_name]).total_seconds() < cooldown_duration: + return False + del self.cooldowns[guild_id][event_name] + + if len(timestamps) > max_requests: + self.cooldowns.setdefault(guild_id, {})[event_name] = now + return False + return True + + async def fetch_audit_logs(self, guild, action, target_id): + if not guild.me.guild_permissions.ban_members: + return None + try: + async for entry in guild.audit_logs(action=action, limit=1): + if entry.target.id == target_id: + now = datetime.datetime.now(pytz.utc) + if (now - entry.created_at).total_seconds() * 1000 >= 3600000: + return None + return entry + except Exception: + pass + return None + + @commands.Cog.listener() + async def on_guild_channel_delete(self, channel): + guild = channel.guild + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + if not antinuke_status or not antinuke_status[0]: + return + + if not self.can_fetch_audit(guild.id, "channel_delete"): + return + + logs = await self.fetch_audit_logs(guild, discord.AuditLogAction.channel_delete, channel.id) + if logs is None: + return + + executor = logs.user + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", (guild.id, executor.id)) as cursor: + if await cursor.fetchone(): + return + + async with db.execute("SELECT chdl FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + if whitelist_status and whitelist_status[0]: + return + + await self.recreate_channel_and_ban(channel, executor) + await asyncio.sleep(3) + + async def recreate_channel_and_ban(self, channel, executor, retries=3): + while retries > 0: + try: + new_channel = await channel.clone(reason="Channel Delete | Unwhitelisted User") + await new_channel.edit(position=channel.position) + break + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + break + except Exception: + return + + if retries == 0: + return + + retries = 3 + while retries > 0: + try: + await channel.guild.ban(executor, reason="Channel Delete | Unwhitelisted User") + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + break + except Exception: + return diff --git a/bot/cogs/antinuke/antichup.py b/bot/cogs/antinuke/antichup.py new file mode 100644 index 0000000..2497838 --- /dev/null +++ b/bot/cogs/antinuke/antichup.py @@ -0,0 +1,137 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import datetime +import pytz + +class AntiChannelUpdate(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.event_limits = {} + self.cooldowns = {} + + def can_fetch_audit(self, guild_id, event_name, max_requests=5, interval=10, cooldown_duration=300): + now = datetime.datetime.now() + self.event_limits.setdefault(guild_id, {}).setdefault(event_name, []).append(now) + + timestamps = self.event_limits[guild_id][event_name] + timestamps = [t for t in timestamps if (now - t).total_seconds() <= interval] + self.event_limits[guild_id][event_name] = timestamps + + if guild_id in self.cooldowns and event_name in self.cooldowns[guild_id]: + if (now - self.cooldowns[guild_id][event_name]).total_seconds() < cooldown_duration: + return False + del self.cooldowns[guild_id][event_name] + + if len(timestamps) > max_requests: + self.cooldowns.setdefault(guild_id, {})[event_name] = now + return False + return True + + async def fetch_audit_logs(self, guild, action, target_id): + if not guild.me.guild_permissions.ban_members: + return None + try: + async for entry in guild.audit_logs(action=action, limit=1): + if entry.target.id == target_id: + now = datetime.datetime.now(pytz.utc) + if (now - entry.created_at).total_seconds() * 1000 >= 3600000: + return None + return entry + except Exception: + pass + return None + + @commands.Cog.listener() + async def on_guild_channel_update(self, before, after): + guild = before.guild + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + if not antinuke_status or not antinuke_status[0]: + return + + if not self.can_fetch_audit(guild.id, "channel_update"): + return + + logs = await self.fetch_audit_logs(guild, discord.AuditLogAction.channel_update, after.id) + if logs is None: + return + + executor = logs.user + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", (guild.id, executor.id)) as cursor: + if await cursor.fetchone(): + return + + async with db.execute("SELECT chup FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + if whitelist_status and whitelist_status[0]: + return + + await self.revert_channel_update_and_ban(before, after, executor) + await asyncio.sleep(3) + + async def revert_channel_update_and_ban(self, before, after, executor, retries=3): + while retries > 0: + try: + await after.edit( + name=before.name, + topic=before.topic, + position=before.position, + nsfw=before.nsfw, + bitrate=before.bitrate if hasattr(before, 'bitrate') else None, + user_limit=before.user_limit if hasattr(before, 'user_limit') else None, + reason="Channel Update | Unwhitelisted User" + ) + break + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + break + except Exception: + return + + if retries == 0: + return + + retries = 3 + while retries > 0: + try: + await before.guild.ban(executor, reason="Channel Update | Unwhitelisted User") + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + break + except Exception: + return \ No newline at end of file diff --git a/bot/cogs/antinuke/antieveryone.py b/bot/cogs/antinuke/antieveryone.py new file mode 100644 index 0000000..d50dd9c --- /dev/null +++ b/bot/cogs/antinuke/antieveryone.py @@ -0,0 +1,139 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import datetime +from datetime import timedelta + +class AntiEveryone(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.event_limits = {} + + async def can_message_delete(self, guild_id, event_name, max_requests=5, interval=10, cooldown_duration=300): + now = datetime.datetime.now() + self.event_limits.setdefault(guild_id, {}).setdefault(event_name, []).append(now) + + timestamps = self.event_limits[guild_id][event_name] + timestamps = [t for t in timestamps if (now - t).total_seconds() <= interval] + self.event_limits[guild_id][event_name] = timestamps + + if len(timestamps) > max_requests: + return False + + return True + + @commands.Cog.listener() + async def on_message(self, message): + if message.guild is None or not message.mention_everyone: + return + + guild = message.guild + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + + if not antinuke_status or not antinuke_status[0]: + return + + if message.author.id in {guild.owner_id, self.bot.user.id}: + return + + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", (guild.id, message.author.id)) as cursor: + extraowner_status = await cursor.fetchone() + + if extraowner_status: + return + + async with db.execute("SELECT meneve FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", (guild.id, message.author.id)) as cursor: + whitelist_status = await cursor.fetchone() + + if whitelist_status and whitelist_status[0]: + return + + + if not await self.can_message_delete(guild.id, 'mention_everyone'): + return + + try: + await self.timeout_user(message.author) + await self.delete_everyone_messages(message.channel) + except Exception as e: + print(f"An unexpected error occurred while handling {message.author.id}: {e}") + + async def timeout_user(self, user): + retries = 3 + duration = 60 * 60 + while retries > 0: + try: + await user.edit(timed_out_until=discord.utils.utcnow() + timedelta(seconds=duration), reason="Mentioned Everyone/Here | Unwhitelisted User") + return + except discord.Forbidden: + return + except discord.HTTPException as e: + print(f"Failed to timeout {user.id} due to HTTPException: {e}") + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + retry_after = float(retry_after) + print(f"Rate limit encountered while timing out. Retrying after {retry_after} seconds.") + await asyncio.sleep(retry_after) + retries -= 1 + else: + return + except discord.errors.RateLimited as e: + print(f"Rate limit encountered while timing out: {e}. Retrying in {e.retry_after} seconds.") + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception as e: + print(f"An unexpected error occurred while timing out {user.id}: {e}") + return + + print(f"Failed to timeout {user.id} after multiple attempts due to rate limits.") + + async def delete_everyone_messages(self, channel): + retries = 3 + while retries > 0: + try: + async for msg in channel.history(limit=100): + if msg.mention_everyone: + await msg.delete() + await asyncio.sleep(3) + return + except discord.Forbidden: + return + except discord.HTTPException as e: + print(f"Failed to delete messages due to HTTPException: {e}") + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + retry_after = float(retry_after) + print(f"Rate limit encountered while deleting messages. Retrying after {retry_after} seconds.") + await asyncio.sleep(retry_after) + retries -= 1 + else: + return + except discord.errors.RateLimited as e: + print(f"Rate limit encountered while deleting messages: {e}. Retrying in {e.retry_after} seconds.") + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception as e: + print(f"An unexpected error occurred while deleting messages: {e}") + return + + print(f"Failed to delete messages after multiple attempts due to rate limits.") diff --git a/bot/cogs/antinuke/antiguild.py b/bot/cogs/antinuke/antiguild.py new file mode 100644 index 0000000..c408286 --- /dev/null +++ b/bot/cogs/antinuke/antiguild.py @@ -0,0 +1,189 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import datetime +import pytz + +class AntiGuildUpdate(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.event_limits = {} + self.cooldowns = {} + + async def fetch_audit_logs(self, guild, action): + try: + async for entry in guild.audit_logs(action=action, limit=1): + now = datetime.datetime.now(pytz.utc) + created_at = entry.created_at + difference = (now - created_at).total_seconds() * 1000 + + if difference >= 3600000: + return None + + return entry + + except Exception: + return None + return None + + def can_fetch_audit(self, guild_id, event_name, max_requests=5, interval=10, cooldown_duration=300): + now = datetime.datetime.now() + self.event_limits.setdefault(guild_id, {}).setdefault(event_name, []).append(now) + + timestamps = self.event_limits[guild_id][event_name] + timestamps = [t for t in timestamps if (now - t).total_seconds() <= interval] + self.event_limits[guild_id][event_name] = timestamps + + if guild_id in self.cooldowns and event_name in self.cooldowns[guild_id]: + if (now - self.cooldowns[guild_id][event_name]).total_seconds() < cooldown_duration: + return False + del self.cooldowns[guild_id][event_name] + + if len(timestamps) > max_requests: + self.cooldowns.setdefault(guild_id, {})[event_name] = now + return False + + return True + + async def is_blacklisted_guild(self, guild_id): + async with aiosqlite.connect('db/block.db') as block_db: + cursor = await block_db.execute("SELECT 1 FROM guild_blacklist WHERE guild_id = ?", (str(guild_id),)) + return await cursor.fetchone() is not None + + @commands.Cog.listener() + async def on_guild_update(self, before, after): + guild = before + if await self.is_blacklisted_guild(guild.id): + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + + if not antinuke_status or not antinuke_status[0]: + return + + if not self.can_fetch_audit(guild.id, 'guild_update'): + return + + logs = await self.fetch_audit_logs(guild, discord.AuditLogAction.guild_update) + if logs is None: + return + + executor = logs.user + difference = discord.utils.utcnow() - logs.created_at + if difference.total_seconds() > 3600: + return + + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT serverup FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", + (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + + if whitelist_status and whitelist_status[0]: + return + + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", + (guild.id, executor.id)) as cursor: + extra_owner_status = await cursor.fetchone() + + if extra_owner_status and extra_owner_status[0]: + return + + await self.ban_executor_and_revert_changes(before, after, executor) + + async def ban_executor_and_revert_changes(self, before, after, executor): + retries = 3 + while retries > 0: + try: + await self.ban_executor(before, executor) + await self.revert_guild_changes(before, after) + await asyncio.sleep(3) + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + retry_after = float(retry_after) + await asyncio.sleep(retry_after) + retries -= 1 + else: + return + except discord.errors.RateLimited as e: + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception: + return + + return + + async def ban_executor(self, guild, executor): + retries = 3 + while retries > 0: + try: + await guild.ban(executor, reason="Guild Update | Unwhitelisted User") + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + return + except discord.errors.RateLimited as e: + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception: + return + + async def revert_guild_changes(self, before, after): + retries = 3 + while retries > 0: + try: + if before.name != after.name: + await after.edit(name=before.name) + if before.icon != after.icon: + await after.edit(icon=before.icon) + if before.splash != after.splash: + await after.edit(splash=before.splash) + if before.banner != after.banner: + await after.edit(banner=before.banner) + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + retry_after = float(retry_after) + await asyncio.sleep(retry_after) + retries -= 1 + else: + return + except Exception: + return + + return diff --git a/bot/cogs/antinuke/antikick.py b/bot/cogs/antinuke/antikick.py new file mode 100644 index 0000000..8722b2d --- /dev/null +++ b/bot/cogs/antinuke/antikick.py @@ -0,0 +1,127 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import datetime +import pytz + +class AntiKick(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.event_limits = {} + self.cooldowns = {} + + async def is_blacklisted_guild(self, guild_id): + async with aiosqlite.connect('db/block.db') as block_db: + cursor = await block_db.execute("SELECT 1 FROM guild_blacklist WHERE guild_id = ?", (str(guild_id),)) + return await cursor.fetchone() is not None + + async def fetch_audit_logs(self, guild, action, target_id): + if not guild.me.guild_permissions.ban_members: + return None + try: + async for entry in guild.audit_logs(action=action, limit=1): + if entry.target.id == target_id: + now = datetime.datetime.now(pytz.utc) + created_at = entry.created_at + difference = (now - created_at).total_seconds() * 1000 + if difference >= 3600000: + return None + return entry + except Exception: + pass + return None + + def can_fetch_audit(self, guild_id, event_name, max_requests=6, interval=10, cooldown_duration=300): + now = datetime.datetime.now() + self.event_limits.setdefault(guild_id, {}).setdefault(event_name, []).append(now) + + timestamps = self.event_limits[guild_id][event_name] + timestamps = [t for t in timestamps if (now - t).total_seconds() <= interval] + self.event_limits[guild_id][event_name] = timestamps + + if guild_id in self.cooldowns and event_name in self.cooldowns[guild_id]: + if (now - self.cooldowns[guild_id][event_name]).total_seconds() < cooldown_duration: + return False + del self.cooldowns[guild_id][event_name] + + if len(timestamps) > max_requests: + self.cooldowns.setdefault(guild_id, {})[event_name] = now + return False + + return True + + @commands.Cog.listener() + async def on_member_remove(self, member): + if await self.is_blacklisted_guild(member.guild.id): + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (member.guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + if not antinuke_status or not antinuke_status[0]: + return + + if not self.can_fetch_audit(member.guild.id, 'kick'): + return + + log_entry = await self.fetch_audit_logs(member.guild, discord.AuditLogAction.kick, member.id) + if log_entry is None: + return + + executor = log_entry.user + if executor.id in {member.guild.owner_id, self.bot.user.id}: + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", + (member.guild.id, executor.id)) as cursor: + extraowner_status = await cursor.fetchone() + if extraowner_status: + return + + async with db.execute("SELECT kick FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", + (member.guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + if whitelist_status and whitelist_status[0]: + return + + await self.ban_executor(member.guild, executor) + await asyncio.sleep(2) + + async def ban_executor(self, guild, executor): + retries = 3 + while retries > 0: + try: + await guild.ban(executor, reason="Member Kick | Unwhitelisted User") + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + return + except discord.errors.RateLimited as e: + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception: + return + return \ No newline at end of file diff --git a/bot/cogs/antinuke/antiprune.py b/bot/cogs/antinuke/antiprune.py new file mode 100644 index 0000000..f0105a3 --- /dev/null +++ b/bot/cogs/antinuke/antiprune.py @@ -0,0 +1,104 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import datetime +import asyncio +import pytz + +class AntiPrune(commands.Cog): + def __init__(self, bot): + self.bot = bot + + async def fetch_audit_logs(self, guild, action): + try: + async for entry in guild.audit_logs(action=action, limit=1): + now = datetime.datetime.now(pytz.utc) + created_at = entry.created_at + difference = (now - created_at).total_seconds() * 1000 + + if difference >= 3600000: + return None + + return entry + + except Exception: + pass + return None + + @commands.Cog.listener() + async def on_member_remove(self, member): + guild = member.guild + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + + if not antinuke_status or not antinuke_status[0]: + return + + log_entry = await self.fetch_audit_logs(guild, discord.AuditLogAction.member_prune) + if log_entry is None: + return + + executor = log_entry.user + + + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with db.execute("SELECT prune FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + + if whitelist_status and whitelist_status[0]: + return + + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", (guild.id, executor.id)) as cursor: + extra_owner_status = await cursor.fetchone() + + if extra_owner_status: + return + + await self.ban_executor(guild, executor) + + async def ban_executor(self, guild, executor): + retries = 3 + while retries > 0: + try: + await guild.ban(executor, reason="Member Prune | Unwhitelisted User") + return + except discord.Forbidden: + print(f"Failed to ban {executor.id} due to missing permissions.") + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + retry_after = float(retry_after) + print(f"Rate limit encountered. Retrying after {retry_after} seconds.") + await asyncio.sleep(retry_after) + retries -= 1 + else: + print(f"HTTPException encountered: {e}") + return + except discord.errors.RateLimited as e: + print(f"Rate limit encountered while banning: {e}. Retrying in {e.retry_after} seconds.") + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception as e: + print(f"An unexpected error occurred while banning {executor.id}: {e}") + return + + print(f"Failed to ban {executor.id} after multiple attempts due to rate limits.") diff --git a/bot/cogs/antinuke/antirlcr.py b/bot/cogs/antinuke/antirlcr.py new file mode 100644 index 0000000..eab7311 --- /dev/null +++ b/bot/cogs/antinuke/antirlcr.py @@ -0,0 +1,152 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import datetime +import pytz + +class AntiRoleCreate(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.event_limits = {} + self.cooldowns = {} + + async def is_blacklisted_guild(self, guild_id): + async with aiosqlite.connect('db/block.db') as block_db: + cursor = await block_db.execute("SELECT 1 FROM guild_blacklist WHERE guild_id = ?", (str(guild_id),)) + return await cursor.fetchone() is not None + + async def fetch_audit_logs(self, guild, action): + if not guild.me.guild_permissions.ban_members: + return None + try: + async for entry in guild.audit_logs(action=action, limit=1): + now = datetime.datetime.now(pytz.utc) + created_at = entry.created_at + difference = (now - created_at).total_seconds() * 1000 + + if difference >= 3600000: + return None + return entry + except Exception: + pass + return None + + def can_fetch_audit(self, guild_id, event_name, max_requests=5, interval=10, cooldown_duration=300): + now = datetime.datetime.now() + self.event_limits.setdefault(guild_id, {}).setdefault(event_name, []).append(now) + + timestamps = self.event_limits[guild_id][event_name] + timestamps = [t for t in timestamps if (now - t).total_seconds() <= interval] + self.event_limits[guild_id][event_name] = timestamps + + if guild_id in self.cooldowns and event_name in self.cooldowns[guild_id]: + if (now - self.cooldowns[guild_id][event_name]).total_seconds() < cooldown_duration: + return False + del self.cooldowns[guild_id][event_name] + + if len(timestamps) > max_requests: + self.cooldowns.setdefault(guild_id, {})[event_name] = now + return False + + return True + + @commands.Cog.listener() + async def on_guild_role_create(self, role): + guild = role.guild + if await self.is_blacklisted_guild(guild.id): + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + if not antinuke_status or not antinuke_status[0]: + return + + if not self.can_fetch_audit(guild.id, 'role_create'): + return + + log_entry = await self.fetch_audit_logs(guild, discord.AuditLogAction.role_create) + if log_entry is None: + return + + executor = log_entry.user + + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", + (guild.id, executor.id)) as cursor: + extra_owner_status = await cursor.fetchone() + if extra_owner_status: + return + + async with db.execute("SELECT rlcr FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", + (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + if whitelist_status and whitelist_status[0]: + return + + await self.ban_executor_and_delete_role(guild, executor, role) + await asyncio.sleep(3) + + async def ban_executor_and_delete_role(self, guild, executor, role): + retries = 3 + while retries > 0: + try: + await self.ban_executor(guild, executor) + await role.delete(reason="Role created by unwhitelisted user") + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + return + except discord.errors.RateLimited as e: + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception: + return + + async def ban_executor(self, guild, executor): + retries = 3 + while retries > 0: + try: + await guild.ban(executor, reason="Role Create | Unwhitelisted User") + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + return + except discord.errors.RateLimited as e: + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception: + return + return diff --git a/bot/cogs/antinuke/antirldl.py b/bot/cogs/antinuke/antirldl.py new file mode 100644 index 0000000..a005c5f --- /dev/null +++ b/bot/cogs/antinuke/antirldl.py @@ -0,0 +1,173 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import datetime +import pytz + +class AntiRoleDelete(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.event_limits = {} + self.cooldowns = {} + + async def is_blacklisted_guild(self, guild_id): + async with aiosqlite.connect('db/block.db') as block_db: + cursor = await block_db.execute("SELECT 1 FROM guild_blacklist WHERE guild_id = ?", (str(guild_id),)) + return await cursor.fetchone() is not None + + async def fetch_audit_logs(self, guild, action): + if not guild.me.guild_permissions.ban_members: + return None + try: + async for entry in guild.audit_logs(action=action, limit=1): + now = datetime.datetime.now(pytz.utc) + created_at = entry.created_at + difference = (now - created_at).total_seconds() * 1000 + + if difference >= 3600000: + return None + return entry + except Exception: + pass + return None + + def can_fetch_audit(self, guild_id, event_name, max_requests=5, interval=10, cooldown_duration=300): + now = datetime.datetime.now() + self.event_limits.setdefault(guild_id, {}).setdefault(event_name, []).append(now) + + timestamps = self.event_limits[guild_id][event_name] + timestamps = [t for t in timestamps if (now - t).total_seconds() <= interval] + self.event_limits[guild_id][event_name] = timestamps + + if guild_id in self.cooldowns and event_name in self.cooldowns[guild_id]: + if (now - self.cooldowns[guild_id][event_name]).total_seconds() < cooldown_duration: + return False + del self.cooldowns[guild_id][event_name] + + if len(timestamps) > max_requests: + self.cooldowns.setdefault(guild_id, {})[event_name] = now + return False + + return True + + @commands.Cog.listener() + async def on_guild_role_delete(self, role): + guild = role.guild + if await self.is_blacklisted_guild(guild.id): + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + if not antinuke_status or not antinuke_status[0]: + return + + if not self.can_fetch_audit(guild.id, 'role_delete'): + return + + log_entry = await self.fetch_audit_logs(guild, discord.AuditLogAction.role_delete) + if log_entry is None: + return + + executor = log_entry.user + + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", + (guild.id, executor.id)) as cursor: + extra_owner_status = await cursor.fetchone() + if extra_owner_status: + return + + async with db.execute("SELECT rldl FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", + (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + if whitelist_status and whitelist_status[0]: + return + + await self.ban_executor_and_recreate_role(guild, executor, role) + await asyncio.sleep(2) + + async def ban_executor_and_recreate_role(self, guild, executor, role): + retries = 3 + while retries > 0: + try: + await self.ban_executor(guild, executor) + await self.recreate_role(guild, role) + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + return + except discord.errors.RateLimited as e: + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception: + return + return + + async def ban_executor(self, guild, executor): + retries = 3 + while retries > 0: + try: + await guild.ban(executor, reason="Role Delete | Unwhitelisted User") + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + return + except discord.errors.RateLimited as e: + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception: + return + return + + async def recreate_role(self, guild, role): + try: + await guild.create_role( + name=role.name, + permissions=role.permissions, + color=role.color, + hoist=role.hoist, + mentionable=role.mentionable, + reason="Role deleted by unwhitelisted user" + ) + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + except Exception: + return diff --git a/bot/cogs/antinuke/antirlup.py b/bot/cogs/antinuke/antirlup.py new file mode 100644 index 0000000..5a77804 --- /dev/null +++ b/bot/cogs/antinuke/antirlup.py @@ -0,0 +1,160 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import datetime +import pytz + +class AntiRoleUpdate(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.event_limits = {} + self.cooldowns = {} + + async def is_blacklisted_guild(self, guild_id): + async with aiosqlite.connect('db/block.db') as block_db: + cursor = await block_db.execute("SELECT 1 FROM guild_blacklist WHERE guild_id = ?", (str(guild_id),)) + return await cursor.fetchone() is not None + + async def fetch_audit_logs(self, guild, action, target_id): + if not guild.me.guild_permissions.ban_members: + return None + try: + async for entry in guild.audit_logs(action=action, limit=1): + now = datetime.datetime.now(pytz.utc) + created_at = entry.created_at + difference = (now - created_at).total_seconds() * 1000 + if difference >= 3600000: + return None + if entry.target.id == target_id: + return entry + except Exception: + pass + return None + + def can_fetch_audit(self, guild_id, event_name, max_requests=5, interval=10, cooldown_duration=300): + now = datetime.datetime.now() + self.event_limits.setdefault(guild_id, {}).setdefault(event_name, []).append(now) + + timestamps = self.event_limits[guild_id][event_name] + timestamps = [t for t in timestamps if (now - t).total_seconds() <= interval] + self.event_limits[guild_id][event_name] = timestamps + + if guild_id in self.cooldowns and event_name in self.cooldowns[guild_id]: + if (now - self.cooldowns[guild_id][event_name]).total_seconds() < cooldown_duration: + return False + del self.cooldowns[guild_id][event_name] + + if len(timestamps) > max_requests: + self.cooldowns.setdefault(guild_id, {})[event_name] = now + return False + + return True + + @commands.Cog.listener() + async def on_guild_role_update(self, before, after): + guild = before.guild + if await self.is_blacklisted_guild(guild.id): + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + if not antinuke_status or not antinuke_status[0]: + return + + if not self.can_fetch_audit(guild.id, 'role_update'): + return + + log_entry = await self.fetch_audit_logs(guild, discord.AuditLogAction.role_update, before.id) + if log_entry is None: + return + + executor = log_entry.user + + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT rlup FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", + (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + if whitelist_status and whitelist_status[0]: + return + + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", + (guild.id, executor.id)) as cursor: + extra_owner_status = await cursor.fetchone() + if extra_owner_status: + return + + await self.ban_executor_and_revert_role_update(guild, executor, before, after) + await asyncio.sleep(3) + + async def ban_executor_and_revert_role_update(self, guild, executor, before, after): + retries = 3 + while retries > 0: + try: + await self.ban_executor(guild, executor) + await after.edit( + name=before.name, + permissions=before.permissions, + color=before.color, + hoist=before.hoist, + mentionable=before.mentionable, + reason="Role updated by unwhitelisted user" + ) + return + except discord.Forbidden: + print(f"Failed to ban {executor.id} or revert the role update {before.id} due to missing permissions.") + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + return + except discord.errors.RateLimited as e: + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception: + return + + async def ban_executor(self, guild, executor): + retries = 3 + while retries > 0: + try: + await guild.ban(executor, reason="Role Update | Unwhitelisted User") + return + except discord.Forbidden: + print(f"Failed to ban {executor.id} due to missing permissions.") + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + retries -= 1 + else: + return + except discord.errors.RateLimited as e: + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception: + return diff --git a/bot/cogs/antinuke/antiwebhook.py b/bot/cogs/antinuke/antiwebhook.py new file mode 100644 index 0000000..24c653b --- /dev/null +++ b/bot/cogs/antinuke/antiwebhook.py @@ -0,0 +1,134 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import datetime +import pytz + +class AntiWebhookUpdate(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.event_limits = {} + self.cooldowns = {} + + async def fetch_audit_logs(self, guild, action, target_id): + try: + now = datetime.datetime.now(pytz.utc) + logs = [entry async for entry in guild.audit_logs(action=action, limit=1)] + for entry in logs: + if entry.target.id == target_id: + difference = (now - entry.created_at).total_seconds() * 1000 + if difference < 3600000: # Only consider entries from the last hour + return entry + except Exception: + return None + return None + + def can_fetch_audit(self, guild_id, event_name, max_requests=6, interval=10, cooldown_duration=300): + now = datetime.datetime.now() + self.event_limits.setdefault(guild_id, {}).setdefault(event_name, []).append(now) + + timestamps = self.event_limits[guild_id][event_name] + timestamps = [t for t in timestamps if (now - t).total_seconds() <= interval] + self.event_limits[guild_id][event_name] = timestamps + + if guild_id in self.cooldowns and event_name in self.cooldowns[guild_id]: + if (now - self.cooldowns[guild_id][event_name]).total_seconds() < cooldown_duration: + return False + del self.cooldowns[guild_id][event_name] + + if len(timestamps) > max_requests: + self.cooldowns.setdefault(guild_id, {})[event_name] = now + return False + + return True + + async def is_blacklisted_guild(self, guild_id): + async with aiosqlite.connect('db/block.db') as block_db: + cursor = await block_db.execute("SELECT 1 FROM guild_blacklist WHERE guild_id = ?", (str(guild_id),)) + return await cursor.fetchone() is not None + + @commands.Cog.listener() + async def on_webhooks_update(self, channel): + guild = channel.guild + if await self.is_blacklisted_guild(guild.id): + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + + if not antinuke_status or not antinuke_status[0]: + return + + if not self.can_fetch_audit(guild.id, 'webhook_update'): + return + + entry = await self.fetch_audit_logs(guild, discord.AuditLogAction.webhook_update, channel.id) + if entry is None: + return + + executor = entry.user + + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT mngweb FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", + (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + + if whitelist_status and whitelist_status[0]: + return + + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", + (guild.id, executor.id)) as cursor: + extra_owner_status = await cursor.fetchone() + + if extra_owner_status and extra_owner_status[0]: + return + + try: + await self.ban_executor_and_delete_webhook(guild, executor, entry.target) + await asyncio.sleep(3) + except Exception: + return + + async def ban_executor_and_delete_webhook(self, guild, executor, webhook): + retries = 3 + while retries > 0: + try: + await guild.ban(executor, reason="Webhook Update | Unwhitelisted User") + if webhook: + await webhook.delete(reason="Webhook updated by unwhitelisted user") + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + else: + return + except discord.errors.RateLimited as e: + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception: + return + + retries -= 1 diff --git a/bot/cogs/antinuke/antiwebhookcr.py b/bot/cogs/antinuke/antiwebhookcr.py new file mode 100644 index 0000000..bc43032 --- /dev/null +++ b/bot/cogs/antinuke/antiwebhookcr.py @@ -0,0 +1,134 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import datetime +import pytz + +class AntiWebhookCreate(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.event_limits = {} + self.cooldowns = {} + + async def fetch_audit_logs(self, guild, action, target_id): + try: + now = datetime.datetime.now(pytz.utc) + logs = [entry async for entry in guild.audit_logs(action=action, limit=1)] + for entry in logs: + if entry.target.id == target_id: + difference = (now - entry.created_at).total_seconds() * 1000 + if difference < 3600000: + return entry + except Exception: + return None + return None + + def can_fetch_audit(self, guild_id, event_name, max_requests=6, interval=10, cooldown_duration=300): + now = datetime.datetime.now() + self.event_limits.setdefault(guild_id, {}).setdefault(event_name, []).append(now) + + timestamps = self.event_limits[guild_id][event_name] + timestamps = [t for t in timestamps if (now - t).total_seconds() <= interval] + self.event_limits[guild_id][event_name] = timestamps + + if guild_id in self.cooldowns and event_name in self.cooldowns[guild_id]: + if (now - self.cooldowns[guild_id][event_name]).total_seconds() < cooldown_duration: + return False + del self.cooldowns[guild_id][event_name] + + if len(timestamps) > max_requests: + self.cooldowns.setdefault(guild_id, {})[event_name] = now + return False + + return True + + async def is_blacklisted_guild(self, guild_id): + async with aiosqlite.connect('db/block.db') as block_db: + cursor = await block_db.execute("SELECT 1 FROM guild_blacklist WHERE guild_id = ?", (str(guild_id),)) + return await cursor.fetchone() is not None + + @commands.Cog.listener() + async def on_webhooks_create(self, channel): + guild = channel.guild + if await self.is_blacklisted_guild(guild.id): + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + + if not antinuke_status or not antinuke_status[0]: + return + + if not self.can_fetch_audit(guild.id, 'webhook_create'): + return + + entry = await self.fetch_audit_logs(guild, discord.AuditLogAction.webhook_create, channel.id) + if entry is None: + return + + executor = entry.user + + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT mngweb FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", + (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + + if whitelist_status and whitelist_status[0]: + return + + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", + (guild.id, executor.id)) as cursor: + extra_owner_status = await cursor.fetchone() + + if extra_owner_status and extra_owner_status[0]: + return + + try: + await self.ban_executor_and_delete_webhook(guild, executor, entry.target) + await asyncio.sleep(3) + except Exception: + return + + async def ban_executor_and_delete_webhook(self, guild, executor, webhook): + retries = 3 + while retries > 0: + try: + await guild.ban(executor, reason="Webhook Create | Unwhitelisted User") + if webhook: + await webhook.delete(reason="Webhook Created by unwhitelisted user") + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + else: + return + except discord.errors.RateLimited as e: + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception: + return + + retries -= 1 diff --git a/bot/cogs/antinuke/antiwebhookdl.py b/bot/cogs/antinuke/antiwebhookdl.py new file mode 100644 index 0000000..efd4d90 --- /dev/null +++ b/bot/cogs/antinuke/antiwebhookdl.py @@ -0,0 +1,132 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import datetime +import pytz + +class AntiWebhookDelete(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.event_limits = {} + self.cooldowns = {} + + async def fetch_audit_logs(self, guild, action, target_id): + try: + now = datetime.datetime.now(pytz.utc) + logs = [entry async for entry in guild.audit_logs(action=action, limit=1)] + for entry in logs: + if entry.target.id == target_id: + difference = (now - entry.created_at).total_seconds() * 1000 + if difference < 3600000: # Only consider entries from the last hour + return entry + except Exception: + return None + return None + + def can_fetch_audit(self, guild_id, event_name, max_requests=6, interval=10, cooldown_duration=300): + now = datetime.datetime.now() + self.event_limits.setdefault(guild_id, {}).setdefault(event_name, []).append(now) + + timestamps = self.event_limits[guild_id][event_name] + timestamps = [t for t in timestamps if (now - t).total_seconds() <= interval] + self.event_limits[guild_id][event_name] = timestamps + + if guild_id in self.cooldowns and event_name in self.cooldowns[guild_id]: + if (now - self.cooldowns[guild_id][event_name]).total_seconds() < cooldown_duration: + return False + del self.cooldowns[guild_id][event_name] + + if len(timestamps) > max_requests: + self.cooldowns.setdefault(guild_id, {})[event_name] = now + return False + + return True + + async def is_blacklisted_guild(self, guild_id): + async with aiosqlite.connect('db/block.db') as block_db: + cursor = await block_db.execute("SELECT 1 FROM guild_blacklist WHERE guild_id = ?", (str(guild_id),)) + return await cursor.fetchone() is not None + + @commands.Cog.listener() + async def on_webhooks_delete(self, channel): + guild = channel.guild + if await self.is_blacklisted_guild(guild.id): + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + + if not antinuke_status or not antinuke_status[0]: + return + + if not self.can_fetch_audit(guild.id, 'webhook_delete'): + return + + entry = await self.fetch_audit_logs(guild, discord.AuditLogAction.webhook_delete, channel.id) + if entry is None: + return + + executor = entry.user + + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT mngweb FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", + (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + + if whitelist_status and whitelist_status[0]: + return + + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", + (guild.id, executor.id)) as cursor: + extra_owner_status = await cursor.fetchone() + + if extra_owner_status and extra_owner_status[0]: + return + + try: + await self.ban_executor(guild, executor) + await asyncio.sleep(3) + except Exception: + return + + async def ban_executor(self, guild, executor): + retries = 3 + while retries > 0: + try: + await guild.ban(executor, reason="Webhook Delete | Unwhitelisted User") + return + except discord.Forbidden: + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + await asyncio.sleep(float(retry_after)) + else: + return + except discord.errors.RateLimited as e: + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception: + return + + retries -= 1 diff --git a/bot/cogs/antinuke/extra events (unused)/antiemocr.py b/bot/cogs/antinuke/extra events (unused)/antiemocr.py new file mode 100644 index 0000000..d11dcf1 --- /dev/null +++ b/bot/cogs/antinuke/extra events (unused)/antiemocr.py @@ -0,0 +1,94 @@ +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import random +import datetime + +class AntiEmojiCreate(commands.Cog): + def __init__(self, bot): + self.bot = bot + + async def fetch_audit_logs(self, guild, action): + try: + await asyncio.sleep(random.uniform(0.5, 2.0)) + logs = [entry async for entry in guild.audit_logs(action=action, limit=1, after=datetime.datetime.now() - datetime.timedelta(seconds=3))] + if logs: + return logs[0] + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + retry_after = float(retry_after) + await asyncio.sleep(retry_after) + return await self.fetch_audit_logs(guild, action) + except Exception as e: + print(f"An error occurred while fetching audit logs: {e}") + return None + + @commands.Cog.listener() + async def on_guild_emojis_update(self, guild, before, after): + if len(after) > len(before): + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + + if not antinuke_status or not antinuke_status[0]: + return + + logs = await self.fetch_audit_logs(guild, discord.AuditLogAction.emoji_create) + if logs is None: + return + executor = logs.user + difference = discord.utils.utcnow() - logs.created_at + if difference.total_seconds() > 3600: + return + + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with db.execute("SELECT mngstemo FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + + if whitelist_status and whitelist_status[0]: + return + + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", (guild.id, executor.id)) as cursor: + extra_owner_status = await cursor.fetchone() + + if extra_owner_status: + return + + await self.kick_executor(guild, executor) + + async def kick_executor(self, guild, executor): + retries = 3 + while retries > 0: + try: + await guild.kick(executor, reason="Emoji Create | Unwhitelisted User") + return + except discord.Forbidden: + print(f"Failed to kick {executor.id} due to missing permissions.") + return + except discord.HTTPException as e: + print(f"Failed to kick {executor.id} due to HTTPException: {e}") + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + retry_after = float(retry_after) + print(f"Rate limit encountered while kicking. Retrying after {retry_after} seconds.") + await asyncio.sleep(retry_after) + retries -= 1 + else: + return + else: + return + except discord.errors.RateLimited as e: + print(f"Rate limit encountered while kicking: {e}. Retrying in {e.retry_after} seconds.") + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception as e: + print(f"An unexpected error occurred while kicking {executor.id}: {e}") + return + + print(f"Failed to kick {executor.id} after multiple attempts due to rate limits.") diff --git a/bot/cogs/antinuke/extra events (unused)/antiemodl.py b/bot/cogs/antinuke/extra events (unused)/antiemodl.py new file mode 100644 index 0000000..fe18dc5 --- /dev/null +++ b/bot/cogs/antinuke/extra events (unused)/antiemodl.py @@ -0,0 +1,95 @@ +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import random +import datetime + +class AntiEmojiDelete(commands.Cog): + def __init__(self, bot): + self.bot = bot + + async def fetch_audit_logs(self, guild, action): + try: + await asyncio.sleep(random.uniform(0.5, 2.0)) + logs = [entry async for entry in guild.audit_logs(action=action, limit=1, after=datetime.datetime.utcnow() - datetime.timedelta(seconds=3))] + if logs: + return logs[0] + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + retry_after = float(retry_after) + await asyncio.sleep(retry_after) + return await self.fetch_audit_logs(guild, action) + except Exception as e: + print(f"An error occurred while fetching audit logs: {e}") + return None + + @commands.Cog.listener() + async def on_guild_emojis_update(self, guild, before, after): + if len(after) < len(before): + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + + if not antinuke_status or not antinuke_status[0]: + return + + logs = await self.fetch_audit_logs(guild, discord.AuditLogAction.emoji_delete) + if logs is None: + return + + executor = logs.user + difference = discord.utils.utcnow() - logs.created_at + if difference.total_seconds() > 3600: + return + + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with db.execute("SELECT mngstemo FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + + if whitelist_status and whitelist_status[0]: + return + + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", (guild.id, executor.id)) as cursor: + extra_owner_status = await cursor.fetchone() + + if extra_owner_status: + return + + await self.kick_executor(guild, executor) + + async def kick_executor(self, guild, executor): + retries = 3 + while retries > 0: + try: + await guild.kick(executor, reason="Emoji Delete | Unwhitelisted User") + return + except discord.Forbidden: + print(f"Failed to kick {executor.id} due to missing permissions.") + return + except discord.HTTPException as e: + print(f"Failed to kick {executor.id} due to HTTPException: {e}") + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + retry_after = float(retry_after) + print(f"Rate limit encountered while kicking. Retrying after {retry_after} seconds.") + await asyncio.sleep(retry_after) + retries -= 1 + else: + return + else: + return + except discord.errors.RateLimited as e: + print(f"Rate limit encountered while kicking: {e}. Retrying in {e.retry_after} seconds.") + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception as e: + print(f"An unexpected error occurred while kicking {executor.id}: {e}") + return + + print(f"Failed to kick {executor.id} after multiple attempts due to rate limits.") diff --git a/bot/cogs/antinuke/extra events (unused)/antiemoup.py b/bot/cogs/antinuke/extra events (unused)/antiemoup.py new file mode 100644 index 0000000..bdffc26 --- /dev/null +++ b/bot/cogs/antinuke/extra events (unused)/antiemoup.py @@ -0,0 +1,95 @@ +import discord +from discord.ext import commands +import aiosqlite +import asyncio +import random +import datetime + +class AntiEmojiUpdate(commands.Cog): + def __init__(self, bot): + self.bot = bot + + async def fetch_audit_logs(self, guild, action): + try: + await asyncio.sleep(random.uniform(0.5, 2.0)) + logs = [entry async for entry in guild.audit_logs(action=action, limit=1, after=datetime.datetime.utcnow() - datetime.timedelta(seconds=3))] + if logs: + return logs[0] + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + retry_after = float(retry_after) + await asyncio.sleep(retry_after) + return await self.fetch_audit_logs(guild, action) + except Exception as e: + print(f"An error occurred while fetching audit logs: {e}") + return None + + @commands.Cog.listener() + async def on_guild_emojis_update(self, guild, before, after): + if len(after) == len(before): # An emoji was updated + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + + if not antinuke_status or not antinuke_status[0]: + return + + logs = await self.fetch_audit_logs(guild, discord.AuditLogAction.emoji_update) + if logs is None: + return + + executor = logs.user + difference = discord.utils.utcnow() - logs.created_at + if difference.total_seconds() > 3600: + return + + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with db.execute("SELECT mngstemo FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + + if whitelist_status and whitelist_status[0]: + return + + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", (guild.id, executor.id)) as cursor: + extra_owner_status = await cursor.fetchone() + + if extra_owner_status: + return + + await self.kick_executor(guild, executor) + + async def kick_executor(self, guild, executor): + retries = 3 + while retries > 0: + try: + await guild.kick(executor, reason="Emoji Update | Unwhitelisted User") + return + except discord.Forbidden: + print(f"Failed to kick {executor.id} due to missing permissions.") + return + except discord.HTTPException as e: + print(f"Failed to kick {executor.id} due to HTTPException: {e}") + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + retry_after = float(retry_after) + print(f"Rate limit encountered while kicking. Retrying after {retry_after} seconds.") + await asyncio.sleep(retry_after) + retries -= 1 + else: + return + else: + return + except discord.errors.RateLimited as e: + print(f"Rate limit encountered while kicking: {e}. Retrying in {e.retry_after} seconds.") + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception as e: + print(f"An unexpected error occurred while kicking {executor.id}: {e}") + return + + print(f"Failed to kick {executor.id} after multiple attempts due to rate limits.") diff --git a/bot/cogs/antinuke/extra events (unused)/antisticker.py b/bot/cogs/antinuke/extra events (unused)/antisticker.py new file mode 100644 index 0000000..e3ccf9f --- /dev/null +++ b/bot/cogs/antinuke/extra events (unused)/antisticker.py @@ -0,0 +1,99 @@ +import discord +from discord.ext import commands +from datetime import timedelta, datetime +import aiosqlite +import asyncio + +class AntiSticker(commands.Cog): + def __init__(self, bot): + self.bot = bot + + async def fetch_audit_logs(self, guild, action): + try: + await asyncio.sleep(1) + logs = [entry async for entry in guild.audit_logs(action=action, limit=1, after=discord.utils.utcnow() - timedelta(hours=1))] + if logs: + return logs[0] + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + retry_after = float(retry_after) + await asyncio.sleep(retry_after) + return await self.fetch_audit_logs(guild, action) + except Exception as e: + print(f"An error occurred while fetching audit logs: {e}") + return None + + @commands.Cog.listener() + async def on_guild_stickers_update(self, guild, before, after): + if len(after) > len(before): + action = discord.AuditLogAction.sticker_create + elif len(after) < len(before): + action = discord.AuditLogAction.sticker_delete + else: + action = discord.AuditLogAction.sticker_update + + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + + if not antinuke_status or not antinuke_status[0]: + return + + log_entry = await self.fetch_audit_logs(guild, action) + if log_entry is None: + return + + executor = log_entry.user + difference = discord.utils.utcnow() - log_entry.created_at + + if difference.total_seconds() > 3600: + return + + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with db.execute("SELECT mngstemo FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + + if whitelist_status and whitelist_status[0]: + return + + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", (guild.id, executor.id)) as cursor: + extra_owner_status = await cursor.fetchone() + + if extra_owner_status: + return + + await self.kick_executor(guild, executor) + + async def kick_executor(self, guild, executor): + retries = 3 + while retries > 0: + try: + await guild.kick(executor, reason="Sticker Action | Unwhitelisted User") + return + except discord.Forbidden: + print(f"Failed to kick {executor.id} due to missing permissions.") + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + retry_after = float(retry_after) + print(f"Rate limit encountered. Retrying after {retry_after} seconds.") + await asyncio.sleep(retry_after) + retries -= 1 + else: + print(f"HTTPException encountered: {e}") + return + except discord.errors.RateLimited as e: + print(f"Rate limit encountered while kicking: {e}. Retrying in {e.retry_after} seconds.") + await asyncio.sleep(e.retry_after) + retries -= 1 + except Exception as e: + print(f"An unexpected error occurred while kicking {executor.id}: {e}") + return + + print(f"Failed to kick {executor.id} after multiple attempts due to rate limits.") diff --git a/bot/cogs/antinuke/extra events (unused)/antiunban.py b/bot/cogs/antinuke/extra events (unused)/antiunban.py new file mode 100644 index 0000000..7087b36 --- /dev/null +++ b/bot/cogs/antinuke/extra events (unused)/antiunban.py @@ -0,0 +1,88 @@ +import discord +from discord.ext import commands +import aiosqlite +from datetime import timedelta, datetime +import asyncio + +class AntiUnban(commands.Cog): + def __init__(self, bot): + self.bot = bot + + async def fetch_audit_logs(self, guild, action, target_id): + try: + async for entry in guild.audit_logs(limit=1, action=action): + if entry.target.id == target_id: + return entry + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + retry_after = float(retry_after) + await asyncio.sleep(retry_after) + return await self.fetch_audit_logs(guild, action, target_id) + except Exception as e: + print(f"An error occurred while fetching audit logs: {e}") + return None + + @commands.Cog.listener() + async def on_member_unban(self, guild, user): + async with aiosqlite.connect('db/anti.db') as db: + async with db.execute("SELECT status FROM antinuke WHERE guild_id = ?", (guild.id,)) as cursor: + antinuke_status = await cursor.fetchone() + + if not antinuke_status or not antinuke_status[0]: + return + + log_entry = await self.fetch_audit_logs(guild, discord.AuditLogAction.unban, user.id) + if log_entry is None: + return + + executor = log_entry.user + + if executor.id in {guild.owner_id, self.bot.user.id}: + return + + async with db.execute("SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", (guild.id, executor.id)) as cursor: + extra_owner_status = await cursor.fetchone() + + if extra_owner_status: + return + + async with db.execute("SELECT ban FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", (guild.id, executor.id)) as cursor: + whitelist_status = await cursor.fetchone() + + if whitelist_status and whitelist_status[0]: + return + + await self.ban_executor(guild, executor, user) + + async def ban_executor(self, guild, executor, user): + retries = 3 + while retries > 0: + try: + await guild.ban(executor, reason="Member Unban | Unwhitelisted User") + await guild.ban(user, reason="Reverting unban by unwhitelisted user") + return + except discord.Forbidden: + print(f"Failed to ban {executor.id} or user due to missing permissions.") + return + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + retry_after = float(retry_after) + print(f"Rate limit encountered. Retrying after {retry_after} seconds.") + await asyncio.sleep(retry_after) + else: + print(f"HTTPException encountered: {e}") + return + except discord.errors.RateLimited as e: + print(f"Rate limit encountered while banning: {e}. Retrying in {e.retry_after} seconds.") + await asyncio.sleep(e.retry_after) + except Exception as e: + print(f"An unexpected error occurred while banning {executor.id} or user: {e}") + return + + retries -= 1 + + print(f"Failed to ban {executor.id} after multiple attempts due to rate limits.") diff --git a/bot/cogs/automod/anti_emoji_spam.py b/bot/cogs/automod/anti_emoji_spam.py new file mode 100644 index 0000000..574f7b3 --- /dev/null +++ b/bot/cogs/automod/anti_emoji_spam.py @@ -0,0 +1,160 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import TICK +from discord.ext import commands +import aiosqlite +import re +from datetime import timedelta +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/automod.db") as db: + cursor = await db.execute("SELECT enabled FROM automod WHERE guild_id = ?", (guild_id,)) + result = await cursor.fetchone() + return result is not None and result[0] == 1 + + async def is_anti_emoji_spam_enabled(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT punishment FROM automod_punishments WHERE guild_id = ? AND event = 'Anti emoji spam'", (guild_id,)) + result = await cursor.fetchone() + return result is not None + + async def get_ignored_channels(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT id FROM automod_ignored WHERE guild_id = ? AND type = 'channel'", (guild_id,)) + return [row[0] for row in await cursor.fetchall()] + + async def get_ignored_roles(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT id FROM automod_ignored WHERE guild_id = ? AND type = 'role'", (guild_id,)) + return [row[0] for row in await cursor.fetchall()] + + async def get_punishment(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT punishment FROM automod_punishments WHERE guild_id = ? AND event = 'Anti emoji spam'", (guild_id,)) + result = await cursor.fetchone() + return result[0] if result else None + + async def log_action(self, guild, user, channel, action, reason): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT log_channel FROM automod_logging WHERE guild_id = ?", (guild.id,)) + log_channel_id = await cursor.fetchone() + + if log_channel_id and log_channel_id[0]: + log_channel = guild.get_channel(log_channel_id[0]) + if log_channel: + embed = discord.Embed(title="Automod Log: Anti Emoji Spam", color=0xFF0000) + embed.add_field(name="User", value=user.mention, inline=False) + embed.add_field(name="Action", value=action, inline=False) + embed.add_field(name="Channel", value=channel.mention, inline=False) + embed.add_field(name="Reason", value=reason, inline=False) + embed.set_footer(text=f"User ID: {user.id}") + avatar_url = user.avatar.url if user.avatar else user.default_avatar.url + embed.set_thumbnail(url=avatar_url) + embed.timestamp=discord.utils.utcnow() + await log_channel.send(embed=embed) + + @commands.Cog.listener() + async def on_message(self, message): + if message.author.bot: + return + + guild = message.guild + user = message.author + channel = message.channel + guild_id = guild.id + + if not await self.is_automod_enabled(guild_id) or not await self.is_anti_emoji_spam_enabled(guild_id): + return + + if user == guild.owner or user == self.bot.user: + return + + ignored_channels = await self.get_ignored_channels(guild_id) + if channel.id in ignored_channels: + return + + ignored_roles = await self.get_ignored_roles(guild_id) + if any(role.id in ignored_roles for role in user.roles): + return + + + emoji_pattern = re.compile( + r"|" #discord emojis + r"([\U0001F600-\U0001F64F]|" # Emoticons + r"[\U0001F300-\U0001F5FF]|" # Miscellaneous Symbols and Pictographs + r"[\U0001F680-\U0001F6FF]|" # Transport and Map Symbols + r"[\U0001F700-\U0001F77F]|" # Alchemical Symbols + r"[\U0001F780-\U0001F7FF]|" # Geometric Shapes Extended + r"[\U0001F800-\U0001F8FF]|" # Supplemental Arrows-C + r"[\U0001F900-\U0001F9FF]|" # Supplemental Symbols and Pictographs + r"[\U0001FA00-\U0001FAFF]|" # Chess Symbols + r"[\U00002700-\U000027BF]|" # Miscellaneous Symbols + r"[\U0001F1E6-\U0001F1FF]|" # Regional Indicator Symbols + r"[\U0001F004-\U0001F0CF]|" # Mahjong Tiles and Playing Cards + r"[\U0001F9B0-\U0001F9FF]" # Additional Emoji + r")" + ) + + + + + emoji_count = len(emoji_pattern.findall(message.content)) + + if emoji_count > self.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) + await user.edit(timed_out_until=timeout_duration, reason=reason) + action_taken = "Muted for 1 minute" + elif punishment == "Kick": + await user.kick(reason=reason) + action_taken = "Kicked" + elif punishment == "Ban": + await user.ban(reason=reason) + action_taken = "Banned" + + await message.delete() + + simple_embed = discord.Embed(title="Automod Anti Emoji Spam", color=0xFF0000) + simple_embed.description = f"{TICK} | {user.mention} has been successfully **{action_taken}** for **Spamming Emojis.**" + + simple_embed.set_footer(text="Use the “automod logging” command to get automod logs if it is not enabled.", icon_url=self.bot.user.avatar.url) + await channel.send(embed=simple_embed, delete_after=30) + + + await self.log_action(guild, user, channel, action_taken, reason) + + except discord.Forbidden: + pass + except discord.HTTPException: + pass + except Exception: + pass + + @commands.Cog.listener() + async def on_rate_limit(self, message): + await asyncio.sleep(10) + diff --git a/bot/cogs/automod/anti_invites.py b/bot/cogs/automod/anti_invites.py new file mode 100644 index 0000000..7898bba --- /dev/null +++ b/bot/cogs/automod/anti_invites.py @@ -0,0 +1,151 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import TICK +from discord.ext import commands +import aiosqlite +import asyncio +from datetime import timedelta +import re + +class AntiInvite(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.invite_pattern = re.compile(r'(https?://)?(www\.)?(discord\.gg|discordapp\.com/invite|discord\.com/invite)/\S+') + + async def is_automod_enabled(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT enabled FROM automod WHERE guild_id = ?", (guild_id,)) + result = await cursor.fetchone() + return result is not None and result[0] == 1 + + async def is_anti_invites_enabled(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT punishment FROM automod_punishments WHERE guild_id = ? AND event = 'Anti invites'", (guild_id,)) + result = await cursor.fetchone() + return result is not None + + async def get_ignored_channels(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT id FROM automod_ignored WHERE guild_id = ? AND type = 'channel'", (guild_id,)) + return [row[0] for row in await cursor.fetchall()] + + async def get_ignored_roles(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT id FROM automod_ignored WHERE guild_id = ? AND type = 'role'", (guild_id,)) + return [row[0] for row in await cursor.fetchall()] + + async def get_punishment(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT punishment FROM automod_punishments WHERE guild_id = ? AND event = 'Anti invites'", (guild_id,)) + result = await cursor.fetchone() + return result[0] if result else None + + async def log_action(self, guild, user, channel, action, reason): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT log_channel FROM automod_logging WHERE guild_id = ?", (guild.id,)) + log_channel_id = await cursor.fetchone() + + if log_channel_id and log_channel_id[0]: + log_channel = guild.get_channel(log_channel_id[0]) + if log_channel: + embed = discord.Embed(title="Automod Log: Anti-Invite", color=0xFF0000) + embed.add_field(name="User", value=user.mention, inline=False) + embed.add_field(name="Action", value=action, inline=False) + embed.add_field(name="Channel", value=channel.mention, inline=False) + embed.add_field(name="Reason", value=reason, inline=False) + embed.set_footer(text=f"User ID: {user.id}") + avatar_url = user.avatar.url if user.avatar else user.default_avatar.url + embed.set_thumbnail(url=avatar_url) + embed.timestamp=discord.utils.utcnow() + await log_channel.send(embed=embed) + + @commands.Cog.listener() + async def on_message(self, message): + if message.author.bot: + return + + guild = message.guild + user = message.author + channel = message.channel + guild_id = guild.id + + if not await self.is_automod_enabled(guild_id) or not await self.is_anti_invites_enabled(guild_id): + return + + if user == guild.owner or user == self.bot.user: + return + + ignored_channels = await self.get_ignored_channels(guild_id) + if channel.id in ignored_channels: + return + + ignored_roles = await self.get_ignored_roles(guild_id) + if any(role.id in ignored_roles for role in user.roles): + return + + if self.invite_pattern.search(message.content): + invite_link = self.invite_pattern.search(message.content).group(0) + invite_code = invite_link.split('/')[-1] + + try: + invite = await guild.invites() + if any(invite.code == invite_code for invite in invite): + return + + 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) + await user.edit(timed_out_until=timeout_duration, reason="Posted an invite link") + action_taken = "Muted for 12 minutes" + elif punishment == "Kick": + await user.kick(reason="Posted an invite link") + action_taken = "Kicked" + elif punishment == "Ban": + await user.ban(reason="Posted an invite link") + action_taken = "Banned" + + await message.delete() + + simple_embed = discord.Embed(title="Automod Anti-Invite", color=0xFF0000) + simple_embed.description = f"{TICK} | {user.mention} has been successfully **{action_taken}** for **posting an invite link.**" + + simple_embed.set_footer(text="Use the “automod logging” command to get automod logs if it is not enabled.", icon_url=self.bot.user.avatar.url) + await channel.send(embed=simple_embed, delete_after=30) + + await self.log_action(guild, user, channel, action_taken, reason) + + except discord.Forbidden: + pass + except discord.HTTPException: + pass + except Exception: + pass + + except discord.Forbidden: + pass + except discord.HTTPException: + pass + except Exception: + pass + + @commands.Cog.listener() + async def on_rate_limit(self, message): + await asyncio.sleep(10) + diff --git a/bot/cogs/automod/anti_mass_mention.py b/bot/cogs/automod/anti_mass_mention.py new file mode 100644 index 0000000..54864ec --- /dev/null +++ b/bot/cogs/automod/anti_mass_mention.py @@ -0,0 +1,137 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import TICK +from discord.ext import commands +import aiosqlite +from datetime import timedelta +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/automod.db") as db: + cursor = await db.execute("SELECT enabled FROM automod WHERE guild_id = ?", (guild_id,)) + result = await cursor.fetchone() + return result is not None and result[0] == 1 + + async def is_anti_mass_mention_enabled(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT punishment FROM automod_punishments WHERE guild_id = ? AND event = 'Anti mass mention'", (guild_id,)) + result = await cursor.fetchone() + return result is not None + + async def get_ignored_channels(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT id FROM automod_ignored WHERE guild_id = ? AND type = 'channel'", (guild_id,)) + return [row[0] for row in await cursor.fetchall()] + + async def get_ignored_roles(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT id FROM automod_ignored WHERE guild_id = ? AND type = 'role'", (guild_id,)) + return [row[0] for row in await cursor.fetchall()] + + async def get_punishment(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT punishment FROM automod_punishments WHERE guild_id = ? AND event = 'Anti mass mention'", (guild_id,)) + result = await cursor.fetchone() + return result[0] if result else None + + + async def log_action(self, guild, user, channel, action, reason): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT log_channel FROM automod_logging WHERE guild_id = ?", (guild.id,)) + log_channel_id = await cursor.fetchone() + + if log_channel_id and log_channel_id[0]: + log_channel = guild.get_channel(log_channel_id[0]) + if log_channel: + embed = discord.Embed(title="Automod Log: Anti Mass Mention", color=0xFF0000) + embed.add_field(name="User", value=user.mention, inline=False) + embed.add_field(name="Action", value=action, inline=False) + embed.add_field(name="Channel", value=channel.mention, inline=False) + embed.add_field(name="Reason", value=reason, inline=False) + embed.set_footer(text=f"User ID: {user.id}") + avatar_url = user.avatar.url if user.avatar else user.default_avatar.url + embed.set_thumbnail(url=avatar_url) + embed.timestamp=discord.utils.utcnow() + await log_channel.send(embed=embed) + + @commands.Cog.listener() + async def on_message(self, message): + if message.author.bot: + return + + guild = message.guild + user = message.author + channel = message.channel + guild_id = guild.id + + if not await self.is_automod_enabled(guild_id) or not await self.is_anti_mass_mention_enabled(guild_id): + return + + if user == guild.owner or user == self.bot.user: + return + + ignored_channels = await self.get_ignored_channels(guild_id) + if channel.id in ignored_channels: + return + + ignored_roles = await self.get_ignored_roles(guild_id) + if any(role.id in ignored_roles for role in user.roles): + return + + + mention_count = message.content.count("<@") + if mention_count >= self.mass_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) + await user.edit(timed_out_until=timeout_duration, reason=reason) + action_taken = "Muted for 3 minutes" + elif punishment == "Kick": + await user.kick(reason=reason) + action_taken = "Kicked" + elif punishment == "Ban": + await user.ban(reason=reason) + action_taken = "Banned" + await message.delete() + + simple_embed = discord.Embed(title="Automod Anti Mass-Mention", color=0xFF0000) + simple_embed.description = f"{TICK} | {user.mention} has been successfully **{action_taken}** for **mass mentioning.**" + + simple_embed.set_footer(text="Use the “automod logging” command to get automod logs if it is not enabled.", icon_url=self.bot.user.avatar.url) + await channel.send(embed=simple_embed, delete_after=30) + + await self.log_action(guild, user, channel, action_taken, reason) + + except discord.Forbidden: + pass + except discord.HTTPException: + pass + except Exception: + pass + + @commands.Cog.listener() + async def on_rate_limit(self, message): + await asyncio.sleep(10) + diff --git a/bot/cogs/automod/anticaps.py b/bot/cogs/automod/anticaps.py new file mode 100644 index 0000000..231ed96 --- /dev/null +++ b/bot/cogs/automod/anticaps.py @@ -0,0 +1,142 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import TICK +from discord.ext import commands +import aiosqlite +import asyncio +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/automod.db") as db: + cursor = await db.execute("SELECT enabled FROM automod WHERE guild_id = ?", (guild_id,)) + result = await cursor.fetchone() + return result is not None and result[0] == 1 + + async def is_anti_caps_enabled(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT punishment FROM automod_punishments WHERE guild_id = ? AND event = 'Anti caps'", (guild_id,)) + result = await cursor.fetchone() + return result is not None + + async def get_ignored_channels(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT id FROM automod_ignored WHERE guild_id = ? AND type = 'channel'", (guild_id,)) + return [row[0] for row in await cursor.fetchall()] + + async def get_ignored_roles(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT id FROM automod_ignored WHERE guild_id = ? AND type = 'role'", (guild_id,)) + return [row[0] for row in await cursor.fetchall()] + + async def get_punishment(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT punishment FROM automod_punishments WHERE guild_id = ? AND event = 'Anti caps'", (guild_id,)) + result = await cursor.fetchone() + return result[0] if result else None + + async def log_action(self, guild, user, channel, action, reason): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT log_channel FROM automod_logging WHERE guild_id = ?", (guild.id,)) + log_channel_id = await cursor.fetchone() + + if log_channel_id and log_channel_id[0]: + log_channel = guild.get_channel(log_channel_id[0]) + if log_channel: + embed = discord.Embed(title="Automod Log: Anti-Caps", color=0xFF0000) + embed.add_field(name="User", value=user.mention, inline=False) + embed.add_field(name="Action", value=action, inline=False) + embed.add_field(name="Channel", value=channel.mention, inline=False) + embed.add_field(name="Reason", value=reason, inline=False) + embed.set_footer(text=f"User ID: {user.id}") + avatar_url = user.avatar.url if user.avatar else user.default_avatar.url + embed.set_thumbnail(url=avatar_url) + embed.timestamp=discord.utils.utcnow() + await log_channel.send(embed=embed) + + @commands.Cog.listener() + async def on_message(self, message): + if len(message.content) < 45: + return + + if message.author.bot: + return + + guild = message.guild + user = message.author + channel = message.channel + guild_id = guild.id + + if not await self.is_automod_enabled(guild_id) or not await self.is_anti_caps_enabled(guild_id): + return + + if user == guild.owner or user == self.bot.user: + return + + ignored_channels = await self.get_ignored_channels(guild_id) + if channel.id in ignored_channels: + return + + ignored_roles = await self.get_ignored_roles(guild_id) + if any(role.id in ignored_roles for role in user.roles): + return + + if len(message.content) > 0: + 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: + 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) + await user.edit(timed_out_until=timeout_duration, reason="Excessive Caps") + action_taken = "Muted for 1 minutes" + elif punishment == "Kick": + await user.kick(reason="Excessive Caps") + action_taken = "Kicked" + elif punishment == "Ban": + await user.ban(reason="Excessive Caps") + action_taken = "Banned" + await message.delete() + + simple_embed = discord.Embed(title="Automod Anti-Caps", color=0xFF0000) + simple_embed.description = f"{TICK} | {user.mention} has been successfully **{action_taken}** for **Excessive caps.**" + + simple_embed.set_footer(text="Use the “automod logging” command to get automod logs if it is not enabled.", icon_url=self.bot.user.avatar.url) + await channel.send(embed=simple_embed, delete_after=30) + + await self.log_action(guild, user, channel, action_taken, reason) + + except discord.Forbidden: + pass + except discord.HTTPException: + pass + except Exception: + pass + + @commands.Cog.listener() + async def on_rate_limit(self, message): + await asyncio.sleep(10) + diff --git a/bot/cogs/automod/antilink.py b/bot/cogs/automod/antilink.py new file mode 100644 index 0000000..de607ab --- /dev/null +++ b/bot/cogs/automod/antilink.py @@ -0,0 +1,144 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import TICK +from discord.ext import commands +import aiosqlite +import asyncio +from datetime import timedelta +import re + +class AntiLink(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.link_pattern = re.compile(r'http[s]?://\S+') + self.invite_pattern = re.compile(r'(https?://)?(www\.)?(discord\.(gg|io|me|li)|discordapp\.com/invite)/\S+') + self.gif_pattern = re.compile(r'(\.gif$|^https://(tenor\.com|giphy\.com/gifs|cdn\.discordapp\.com|media\.discordapp\.net))') + self.spotify_pattern = re.compile(r'^https://open\.spotify\.com/track/\S+') + + async def is_automod_enabled(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT enabled FROM automod WHERE guild_id = ?", (guild_id,)) + result = await cursor.fetchone() + return result is not None and result[0] == 1 + + async def is_anti_link_enabled(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT punishment FROM automod_punishments WHERE guild_id = ? AND event = 'Anti link'", (guild_id,)) + result = await cursor.fetchone() + return result is not None + + async def get_ignored_channels(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT id FROM automod_ignored WHERE guild_id = ? AND type = 'channel'", (guild_id,)) + return [row[0] for row in await cursor.fetchall()] + + async def get_ignored_roles(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT id FROM automod_ignored WHERE guild_id = ? AND type = 'role'", (guild_id,)) + return [row[0] for row in await cursor.fetchall()] + + async def get_punishment(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT punishment FROM automod_punishments WHERE guild_id = ? AND event = 'Anti link'", (guild_id,)) + result = await cursor.fetchone() + return result[0] if result else None + + async def log_action(self, guild, user, channel, action, reason): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT log_channel FROM automod_logging WHERE guild_id = ?", (guild.id,)) + log_channel_id = await cursor.fetchone() + + if log_channel_id and log_channel_id[0]: + log_channel = guild.get_channel(log_channel_id[0]) + if log_channel: + embed = discord.Embed(title="Automod Log: Anti-Link", color=0xFF0000) + embed.add_field(name="User", value=user.mention, inline=False) + embed.add_field(name="Action", value=action, inline=False) + embed.add_field(name="Channel", value=channel.mention, inline=False) + embed.add_field(name="Reason", value=reason, inline=False) + embed.set_footer(text=f"User ID: {user.id}") + avatar_url = user.avatar.url if user.avatar else user.default_avatar.url + embed.set_thumbnail(url=avatar_url) + embed.timestamp=discord.utils.utcnow() + await log_channel.send(embed=embed) + + @commands.Cog.listener() + async def on_message(self, message): + if message.author.bot: + return + + guild = message.guild + user = message.author + channel = message.channel + guild_id = guild.id + + if not await self.is_automod_enabled(guild_id) or not await self.is_anti_link_enabled(guild_id): + return + + if user == guild.owner or user == self.bot.user: + return + + ignored_channels = await self.get_ignored_channels(guild_id) + if channel.id in ignored_channels: + return + + ignored_roles = await self.get_ignored_roles(guild_id) + if any(role.id in ignored_roles for role in user.roles): + return + + if self.link_pattern.search(message.content): + if self.invite_pattern.search(message.content): + return + if self.gif_pattern.search(message.content): + return + if self.spotify_pattern.search(message.content): + return + + 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) + await user.edit(timed_out_until=timeout_duration, reason=reason) + action_taken = "Muted for 7 minutes" + elif punishment == "Kick": + await user.kick(reason=reason) + action_taken = "Kicked" + elif punishment == "Ban": + await user.ban(reason=reason) + action_taken = "Banned" + await message.delete() + + simple_embed = discord.Embed(title="Automod Anti-Link", color=0xFF0000) + simple_embed.description = f"{TICK} | {user.mention} has been successfully **{action_taken}** for **Posting a link.**" + + simple_embed.set_footer(text="Use the “automod logging” command to get automod logs if it is not enabled.", icon_url=self.bot.user.avatar.url) + await channel.send(embed=simple_embed, delete_after=30) + + await self.log_action(guild, user, channel, action_taken, reason) + + except discord.Forbidden: + pass + except discord.HTTPException: + pass + except Exception: + pass + + @commands.Cog.listener() + async def on_rate_limit(self, message): + await asyncio.sleep(10) diff --git a/bot/cogs/automod/antispam.py b/bot/cogs/automod/antispam.py new file mode 100644 index 0000000..812f02b --- /dev/null +++ b/bot/cogs/automod/antispam.py @@ -0,0 +1,142 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import TICK +from discord.ext import commands +import aiosqlite +import asyncio +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): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT enabled FROM automod WHERE guild_id = ?", (guild_id,)) + result = await cursor.fetchone() + return result is not None and result[0] == 1 + + async def is_anti_spam_enabled(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT punishment FROM automod_punishments WHERE guild_id = ? AND event = 'Anti spam'", (guild_id,)) + result = await cursor.fetchone() + return result is not None + + + async def get_ignored_channels(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT id FROM automod_ignored WHERE guild_id = ? AND type = 'channel'", (guild_id,)) + return [row[0] for row in await cursor.fetchall()] + + async def get_ignored_roles(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT id FROM automod_ignored WHERE guild_id = ? AND type = 'role'", (guild_id,)) + return [row[0] for row in await cursor.fetchall()] + + async def get_punishment(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT punishment FROM automod_punishments WHERE guild_id = ? AND event = 'Anti spam'", (guild_id,)) + result = await cursor.fetchone() + return result[0] if result else None + + async def log_action(self, guild, user, channel, action, reason): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT log_channel FROM automod_logging WHERE guild_id = ?", (guild.id,)) + log_channel_id = await cursor.fetchone() + + if log_channel_id and log_channel_id[0]: + log_channel = guild.get_channel(log_channel_id[0]) + if log_channel: + embed = discord.Embed(title="Automod Log: Anti-Spam", color=0xFF0000) + embed.add_field(name="User", value=user.mention, inline=False) + embed.add_field(name="Action", value=action, inline=False) + embed.add_field(name="Channel", value=channel.mention, inline=False) + embed.add_field(name="Reason", value=reason, inline=False) + embed.set_footer(text=f"User ID: {user.id}") + avatar_url = user.avatar.url if user.avatar else user.default_avatar.url + embed.set_thumbnail(url=avatar_url) + embed.timestamp=discord.utils.utcnow() + await log_channel.send(embed=embed) + + @commands.Cog.listener() + async def on_message(self, message): + if message.author.bot: + return + + guild = message.guild + user = message.author + channel = message.channel + guild_id = guild.id + + if not await self.is_automod_enabled(guild_id) or not await self.is_anti_spam_enabled(guild_id): + return + + if user == guild.owner or user == self.bot.user: + return + + ignored_channels = await self.get_ignored_channels(guild_id) + if channel.id in ignored_channels: + return + + ignored_roles = await self.get_ignored_roles(guild_id) + if any(role.id in ignored_roles for role in user.roles): + return + + current_time = message.created_at.timestamp() + user_messages = self.recent_messages.get(user.id, []) + user_messages = [msg for msg in user_messages if current_time - msg < 10] + user_messages.append(current_time) + self.recent_messages[user.id] = user_messages + + if len(user_messages) > self.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) + await user.edit(timed_out_until=timeout_duration, reason="Spamming") + action_taken = "Muted for 12 minutes" + elif punishment == "Kick": + await user.kick(reason="Spamming") + action_taken = "Kicked" + elif punishment == "Ban": + await user.ban(reason="Spamming") + action_taken = "Banned" + + simple_embed = discord.Embed(title="Automod Anti-Spam", color=0xFF0000) + simple_embed.description = f"{TICK} | {user.mention} has been successfully **{action_taken}** for **Spamming.**" + + simple_embed.set_footer(text="Use the “automod logging” command to get automod logs if it is not enabled.", icon_url=self.bot.user.avatar.url) + await channel.send(embed=simple_embed, delete_after=30) + + await self.log_action(guild, user, channel, action_taken, reason) + + except discord.Forbidden: + pass + except discord.HTTPException: + pass + except Exception: + pass + + @commands.Cog.listener() + async def on_rate_limit(self, message): + await asyncio.sleep(10) + diff --git a/bot/cogs/commands/Birthday.py b/bot/cogs/commands/Birthday.py new file mode 100644 index 0000000..70c925e --- /dev/null +++ b/bot/cogs/commands/Birthday.py @@ -0,0 +1,192 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands, tasks +import json +import datetime +import asyncio +import os +from discord.ui import LayoutView, TextDisplay, Separator, Container +from utils.cv2 import CV2, build_container + +class CV2(LayoutView): + def __init__(self, title, *sections): + super().__init__(timeout=None) + items = [TextDisplay(f"**{title}**")] + for s in sections: + if s: + items.append(Separator(visible=True)) + items.append(TextDisplay(str(s))) + self.add_item(build_container(*items)) + +def read_db(filename): + """Read the JSON database file.""" + if os.path.exists(filename): + with open(filename, 'r') as f: + return json.load(f) + return {} + +def write_db(filename, data): + """Write data to the JSON database file.""" + with open(filename, 'w') as f: + json.dump(data, f, indent=4) + +class Birthdays(commands.Cog): + """Handle birthday notifications and setup.""" + + def __init__(self, client: commands.Bot): + self.client = client + self.check_birthdays.start() + + @commands.command( + name="birthdaysetup", + help="Set up the birthday log channel and role.") + @commands.has_permissions(administrator=True) + @commands.guild_only() + async def birthday_setup(self, ctx: commands.Context, channel: discord.TextChannel, role: discord.Role): + db = read_db('jsondb/birthday_logs.json') + guild_id = str(ctx.guild.id) + + if guild_id not in db: + db[guild_id] = {"birthday_channel_id": channel.id, "birthday_role_id": role.id} + else: + db[guild_id]["birthday_channel_id"] = channel.id + db[guild_id]["birthday_role_id"] = role.id + + write_db('jsondb/birthday_logs.json', db) + + await ctx.send(view=CV2("Birthday Setup", f"Birthday log channel set to {channel.mention} and birthday role set to {role.mention}.")) + + @commands.command( + name="setbirthday", + help="Set your birthday.") + @commands.guild_only() + async def set_birthday(self, ctx: commands.Context): + def check(msg): + return msg.author.id == ctx.author.id and msg.channel.id == ctx.channel.id + + await ctx.send(view=CV2("Birthday Setup", "Please enter your birth day (DD):")) + + try: + msg = await self.client.wait_for('message', timeout=60.0, check=check) + day = msg.content.strip().zfill(2) + + if not day.isdigit() or int(day) not in range(1, 32): + await ctx.send(view=CV2("Error", "Invalid day entered. Please enter a number between 01 and 31.")) + return + + await ctx.send(view=CV2("Birthday Setup", "Please enter your birth month (MM):")) + msg = await self.client.wait_for('message', timeout=60.0, check=check) + month = msg.content.strip().zfill(2) + + if not month.isdigit() or int(month) not in range(1, 13): + await ctx.send(view=CV2("Error", "Invalid month entered. Please enter a number between 01 and 12.")) + return + + await ctx.send(view=CV2("Birthday Setup", "Please enter your birth year (YYYY):")) + msg = await self.client.wait_for('message', timeout=60.0, check=check) + year = msg.content.strip() + + if not year.isdigit() or len(year) != 4: + await ctx.send(view=CV2("Error", "Invalid year entered. Please enter a valid year in YYYY format.")) + return + + date = f"{month}-{day}-{year}" + db = read_db('jsondb/birthdays.json') + db[str(ctx.author.id)] = date + write_db('jsondb/birthdays.json', db) + + await ctx.send(view=CV2("Success", f"Your birthday has been set to {date}.")) + except asyncio.TimeoutError: + await ctx.send(view=CV2("Error", "You took too long to respond. Please try again.")) + + @commands.command( + name="removebirthday", + help="Remove your birthday.") + @commands.guild_only() + async def remove_birthday(self, ctx: commands.Context): + db = read_db('jsondb/birthdays.json') + + if str(ctx.author.id) in db: + del db[str(ctx.author.id)] + write_db('jsondb/birthdays.json', db) + await ctx.send(view=CV2("Success", "Your birthday has been removed.")) + else: + await ctx.send(view=CV2("Error", "You have no birthday set.")) + + @commands.command( + name="listbirthdays", + help="List all members who have their birthday today.") + @commands.guild_only() + async def list_birthdays(self, ctx: commands.Context): + now = datetime.datetime.now() + today_date = now.strftime("%m-%d") + db = read_db('jsondb/birthdays.json') + + members_with_birthday = [ctx.guild.get_member(int(user_id)) for user_id, date in db.items() if date.startswith(today_date)] + + if members_with_birthday: + mentions = ', '.join(member.mention for member in members_with_birthday if member) + await ctx.send(view=CV2("Birthdays Today", f"Members with birthdays today: {mentions}")) + else: + await ctx.send(view=CV2("Birthdays", "No birthdays today.")) + + @commands.command( + name="birthday", + help="Check your birthday.") + @commands.guild_only() + async def check_birthday(self, ctx: commands.Context): + db = read_db('jsondb/birthdays.json') + + if str(ctx.author.id) in db: + date = db[str(ctx.author.id)] + await ctx.send(view=CV2("Your Birthday", f"Your birthday is set to {date}.")) + else: + await ctx.send(view=CV2("Your Birthday", "You haven't set your birthday.")) + + @tasks.loop(hours=24) + async def check_birthdays(self): + now = datetime.datetime.now() + today_date = now.strftime("%m-%d") + db = read_db('jsondb/birthdays.json') + guild_settings = read_db('jsondb/birthday_logs.json') + + for user_id, birthday in db.items(): + if birthday.startswith(today_date): + user = self.client.get_user(int(user_id)) + if user: + for guild_id, settings in guild_settings.items(): + channel_id = settings.get("birthday_channel_id") + role_id = settings.get("birthday_role_id") + if channel_id: + channel = self.client.get_channel(channel_id) + if channel: + await channel.send(view=CV2("Happy Birthday! 🎉", f"Wishing {user.mention} a fantastic birthday!")) + role = discord.utils.get(channel.guild.roles, id=role_id) + if role: + await user.add_roles(role) + break + + @check_birthdays.before_loop + async def before_check_birthdays(self): + await self.client.wait_until_ready() + now = datetime.datetime.now() + first_run = datetime.datetime.combine(now.date(), datetime.time(hour=0, minute=0)) + if now > first_run: + first_run += datetime.timedelta(days=1) + await asyncio.sleep((first_run - now).total_seconds()) + +async def setup(client: commands.Bot): + await client.add_cog(Birthdays(client)) diff --git a/bot/cogs/commands/Embed.py b/bot/cogs/commands/Embed.py new file mode 100644 index 0000000..4d8e982 --- /dev/null +++ b/bot/cogs/commands/Embed.py @@ -0,0 +1,266 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import os +import discord +from utils.emoji import CROSS, TICK +from discord.ext import commands +from discord import ui +import asyncio +from utils.Tools import * +import re + + +class EmbedBuilder(ui.LayoutView): + def __init__(self, ctx): + super().__init__(timeout=180) + self.ctx = ctx + self.message = None + self.embed_data = { + "title": "Edit your Embed!", + "description": "Select Options from the menu below to customize.", + "color": 0xFF0000, + "thumbnail": None, + "image": None, + "footer_text": None, + "footer_icon": None, + "author_text": None, + "author_icon": None, + "fields": [] + } + self.container = ui.Container(accent_color=None) + self._build_view() + self.add_item(self.container) + + def _get_preview(self): + d = self.embed_data + lines = [] + if d["title"]: + lines.append(f"**Title:** {d['title']}") + if d["description"]: + lines.append(f"**Description:** {d['description']}") + if d["color"]: + lines.append(f"**Color:** `#{d['color']:06X}`") + if d["thumbnail"]: + lines.append(f"**Thumbnail:** [Set]({d['thumbnail']})") + if d["image"]: + lines.append(f"**Image:** [Set]({d['image']})") + if d["footer_text"]: + lines.append(f"**Footer:** {d['footer_text']}") + if d["footer_icon"]: + lines.append(f"**Footer Icon:** [Set]({d['footer_icon']})") + if d["author_text"]: + lines.append(f"**Author:** {d['author_text']}") + if d["author_icon"]: + lines.append(f"**Author Icon:** [Set]({d['author_icon']})") + if d["fields"]: + for i, f in enumerate(d["fields"]): + lines.append(f"**Field {i+1}:** {f['name']} — {f['value']}") + return "\n".join(lines) if lines else "No properties set yet." + + def _build_view(self): + self.container.clear_items() + + self.container.add_item(ui.TextDisplay("# Embed Builder")) + self.container.add_item(ui.Separator()) + self.container.add_item(ui.TextDisplay(self._get_preview())) + self.container.add_item(ui.Separator()) + self.container.add_item(ui.TextDisplay("*Select an option to edit. Respond within 30 seconds.*")) + + # Select menu + select = ui.Select( + placeholder="Choose an option to edit the Embed", + min_values=1, max_values=1, + options=[ + discord.SelectOption(label="Title", description="Edit the title"), + discord.SelectOption(label="Description", description="Edit the description"), + discord.SelectOption(label="Add Field", description="Add a field"), + discord.SelectOption(label="Color", description="Edit the color (hex)"), + discord.SelectOption(label="Thumbnail", description="Set thumbnail URL"), + discord.SelectOption(label="Image", description="Set image URL"), + discord.SelectOption(label="Footer Text", description="Edit footer text"), + discord.SelectOption(label="Footer Icon", description="Set footer icon URL"), + discord.SelectOption(label="Author Text", description="Edit author text"), + discord.SelectOption(label="Author Icon", description="Set author icon URL"), + ] + ) + select.callback = self._select_callback + self.container.add_item(ui.ActionRow(select)) + + # Buttons + send_btn = ui.Button(label="Send Embed", emoji=TICK, style=discord.ButtonStyle.success) + send_btn.callback = self._send_callback + cancel_btn = ui.Button(label="Cancel Setup", emoji=CROSS, style=discord.ButtonStyle.danger) + cancel_btn.callback = self._cancel_callback + self.container.add_item(ui.ActionRow(send_btn, cancel_btn)) + + def _build_embed(self): + """Build a real discord.Embed from stored data""" + d = self.embed_data + embed = discord.Embed( + title=d["title"], + description=d["description"], + color=d["color"] + ) + if d["thumbnail"]: + embed.set_thumbnail(url=d["thumbnail"]) + if d["image"]: + embed.set_image(url=d["image"]) + if d["footer_text"] or d["footer_icon"]: + embed.set_footer(text=d["footer_text"] or "", icon_url=d["footer_icon"] or discord.Embed.Empty) + if d["author_text"] or d["author_icon"]: + embed.set_author(name=d["author_text"] or "", icon_url=d["author_icon"] or discord.Embed.Empty) + for field in d["fields"]: + embed.add_field(name=field["name"], value=field["value"], inline=False) + return embed + + async def _select_callback(self, interaction: discord.Interaction): + if interaction.user.id != self.ctx.author.id: + await interaction.response.send_message("This builder doesn't belong to you.", ephemeral=True) + return + await interaction.response.defer() + + value = interaction.data["values"][0] + + def chk(m): + return m.channel.id == self.ctx.channel.id and m.author.id == self.ctx.author.id + + prompts = { + "Title": "Enter the **Title** of the embed:", + "Description": "Enter the **Description** of the embed:", + "Color": "Enter the color as a hex value (e.g., `#FF0000`):", + "Thumbnail": "Enter the **Thumbnail URL**:", + "Image": "Enter the **Image URL**:", + "Footer Text": "Enter the **Footer text**:", + "Footer Icon": "Enter the **Footer icon URL**:", + "Author Text": "Enter the **Author text**:", + "Author Icon": "Enter the **Author icon URL**:", + "Add Field": "Enter the **Field title**:", + } + + await self.ctx.send(prompts.get(value, "Enter a value:")) + + try: + msg = await self.ctx.bot.wait_for("message", timeout=30, check=chk) + + if value == "Title": + self.embed_data["title"] = msg.content + elif value == "Description": + self.embed_data["description"] = msg.content + elif value == "Color": + try: + self.embed_data["color"] = int(msg.content.strip("#"), 16) + except ValueError: + await self.ctx.send("Invalid hex color. Please try again.") + return + elif value == "Thumbnail": + if not msg.content.startswith("http"): + await self.ctx.send("Invalid URL format.") + return + self.embed_data["thumbnail"] = msg.content + elif value == "Image": + if not msg.content.startswith("http"): + await self.ctx.send("Invalid URL format.") + return + self.embed_data["image"] = msg.content + elif value == "Footer Text": + self.embed_data["footer_text"] = msg.content + elif value == "Footer Icon": + if not msg.content.startswith("http"): + await self.ctx.send("Invalid URL format.") + return + self.embed_data["footer_icon"] = msg.content + elif value == "Author Text": + self.embed_data["author_text"] = msg.content + elif value == "Author Icon": + if not msg.content.startswith("http"): + await self.ctx.send("Invalid URL format.") + return + self.embed_data["author_icon"] = msg.content + elif value == "Add Field": + field_name = msg.content + await self.ctx.send("Enter the **Field value**:") + val_msg = await self.ctx.bot.wait_for("message", timeout=30, check=chk) + self.embed_data["fields"].append({"name": field_name, "value": val_msg.content}) + + # Rebuild and update + self._build_view() + await self.message.edit(view=self) + + except asyncio.TimeoutError: + await self.ctx.send("Timed Out.") + + async def _send_callback(self, interaction: discord.Interaction): + if interaction.user.id != self.ctx.author.id: + await interaction.response.send_message("This builder doesn't belong to you.", ephemeral=True) + return + await interaction.response.defer() + + await self.ctx.send("Mention the **channel** where you want to send this embed:") + + def chk(m): + return m.channel.id == self.ctx.channel.id and m.author.id == self.ctx.author.id + + try: + msg = await self.ctx.bot.wait_for("message", timeout=30, check=chk) + chnl = msg.channel_mentions[0] + embed = self._build_embed() + await chnl.send(embed=embed) + + # Show success + self.container.clear_items() + self.container.add_item(ui.TextDisplay(f"# {TICK} Embed Sent")) + self.container.add_item(ui.Separator()) + self.container.add_item(ui.TextDisplay(f"Successfully sent the embed to {chnl.mention}")) + await self.message.edit(view=self) + + except asyncio.TimeoutError: + await self.ctx.send("Timed Out.") + except (IndexError, AttributeError): + await self.ctx.send("Please mention a valid channel.") + + async def _cancel_callback(self, interaction: discord.Interaction): + if interaction.user.id != self.ctx.author.id: + await interaction.response.send_message("This builder doesn't belong to you.", ephemeral=True) + return + self.container.clear_items() + self.container.add_item(ui.TextDisplay("# Embed Builder")) + self.container.add_item(ui.Separator()) + self.container.add_item(ui.TextDisplay(f"{CROSS} Embed setup cancelled.")) + await interaction.response.edit_message(view=self) + self.stop() + + async def on_timeout(self): + try: + self.container.clear_items() + self.container.add_item(ui.TextDisplay("# Embed Builder")) + self.container.add_item(ui.Separator()) + self.container.add_item(ui.TextDisplay("⏰ Builder timed out. Use the command again.")) + await self.message.edit(view=self) + except: + pass + + +class Embed(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.hybrid_command(name="embed") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 7, commands.BucketType.user) + @commands.has_permissions(manage_messages=True) + async def _embed(self, ctx): + view = EmbedBuilder(ctx) + view.message = await ctx.send(view=view) \ No newline at end of file diff --git a/bot/cogs/commands/Games.py b/bot/cogs/commands/Games.py new file mode 100644 index 0000000..12043d3 --- /dev/null +++ b/bot/cogs/commands/Games.py @@ -0,0 +1,199 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import os +from core import Cog, zyrox, Context +import games as games +from utils.Tools import * +from utils.cv2 import CV2 +from games import button_games as btn +import random +import asyncio + + + +class Games(Cog): + """Zyrox Games""" + + def __init__(self, client: zyrox): + self.client = client + + + @commands.hybrid_command(name="chess", + help="Play Chess with a user.", + usage="Chess ") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(5, per=commands.BucketType.default, wait=False) + @commands.guild_only() + async def _chess(self, ctx: Context, player: discord.Member): + if player == ctx.author: + await ctx.send(view=CV2("❌ Error", "You Cannot play game with yourself!")) + elif player.bot: + await ctx.send(view=CV2("❌ Error", "You cannot play with bots!")) + else: + game = btn.BetaChess(white=ctx.author, black=player) + await game.start(ctx) + + + @commands.hybrid_command(name="rps", + help="Play Rock Paper Scissor with bot/user.", + aliases=["rockpaperscissors"], + usage="Rockpaperscissors") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(5, per=commands.BucketType.default, wait=False) + @commands.guild_only() + async def _rps(self, ctx: Context, player: discord.Member = None): + game = btn.BetaRockPaperScissors(player) + await game.start(ctx, timeout=120) + + @commands.hybrid_command(name="tic-tac-toe", + help="play tic-tac-toe game with a user.", + aliases=["ttt", "tictactoe"], + usage="Ticktactoe ") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(5, per=commands.BucketType.user, wait=False) + @commands.guild_only() + async def _ttt(self, ctx: Context, player: discord.Member): + if player == ctx.author: + await ctx.send(view=CV2("❌ Error", "You cannot play game with yourself!")) + elif player.bot: + await ctx.send(view=CV2("❌ Error", "You cannot play with bots!")) + else: + game = btn.BetaTictactoe(cross=ctx.author, circle=player) + await game.start(ctx, timeout=30) + + @commands.hybrid_command(name="wordle", + help="Wordle Game | Play with bot.", + usage="Wordle") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(3, per=commands.BucketType.default, wait=False) + @commands.guild_only() + async def _wordle(self, ctx: Context): + game = games.Wordle() + await game.start(ctx, timeout=120) + + @commands.hybrid_command(name="2048", + help="Play 2048 game with bot.", + aliases=["twenty48"], + usage="2048") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(3, per=commands.BucketType.default, wait=False) + @commands.guild_only() + async def _2048(self, ctx: Context): + game = btn.BetaTwenty48() + await game.start(ctx, win_at=2048) + + @commands.hybrid_command(name="memory-game", + help="How strong is your memory?", + aliases=["memory"], + usage="memory-game") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(3, per=commands.BucketType.default, wait=False) + @commands.guild_only() + async def _memory(self, ctx: Context): + game = btn.MemoryGame() + await game.start(ctx) + + @commands.hybrid_command(name="number-slider", + help="slide numbers with bot", + aliases=["slider"], + usage="slider") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(3, per=commands.BucketType.default, wait=False) + @commands.guild_only() + async def _number_slider(self, ctx: Context): + game = btn.NumberSlider() + await game.start(ctx) + + @commands.hybrid_command(name="battleship", + help="Play battleship game with your friend.", + aliases=["battle-ship"], + usage="battleship ") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(3, per=commands.BucketType.default, wait=False) + @commands.guild_only() + async def _battle(self, ctx: Context, player: discord.Member): + game = btn.BetaBattleShip(player1=ctx.author, player2=player) + await game.start(ctx) + + @commands.group(name="country-guesser", + help="Guess name of the country by flag.", + aliases=["guess", "guesser", "countryguesser"], + usage="country-guesser") + @commands.guild_only() + async def _country_guesser(self, ctx: Context): + if ctx.invoked_subcommand is None: + await ctx.send_help("country-guesser") + + @_country_guesser.command(name="start", + help="Starts the country guesser game. It's a 100 Seconds Game so suggested to play in a SPECIFIC CHANNEL.") + async def _start_country_guesser(self, ctx: Context): + game = games.CountryGuesser(is_flags=True, hints=2) + await game.start(ctx) + + """@_country_guesser.command(name="end", + help="Ends the country guesser game.") + async def _end_country_guesser(self, ctx: Context): + await self.country_guesser_game.end_game_manually(ctx)""" + + @commands.hybrid_command(name="connectfour", + help="Play Connect Four game with user.", + aliases=["c4", "connect-four", "connect4"], + usage="connectfour ") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.user, wait=False) + @commands.guild_only() + async def _connectfour(self, ctx: Context, player: discord.Member): + if player == ctx.author: + await ctx.send(view=CV2("❌ Error", "You cannot play against yourself!")) + elif player.bot: + await ctx.send(view=CV2("❌ Error", "You cannot play with bots!")) + else: + game = games.ConnectFour(red=ctx.author, blue=player) + await game.start(ctx, timeout=300) + + + + @commands.hybrid_command(name="lights-out", + help="Play Lights Show game with bot.", + aliases=["lightsout"], + usage="Lights-out") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(3, per=commands.BucketType.default, wait=False) + @commands.guild_only() + async def _lights_show(self, ctx: Context): + game = btn.LightsOut() + await game.start(ctx) diff --git a/bot/cogs/commands/Invc.py b/bot/cogs/commands/Invc.py new file mode 100644 index 0000000..6edda7c --- /dev/null +++ b/bot/cogs/commands/Invc.py @@ -0,0 +1,140 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import asyncio +from utils.Tools import * +from utils.cv2 import CV2 +from utils.config import * + +class Invcrole(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.db_path = 'db/invc.db' + self.bot.loop.create_task(self.create_table()) + + async def create_table(self): + async with aiosqlite.connect(self.db_path) as db: + await db.execute(''' + CREATE TABLE IF NOT EXISTS vcroles ( + guild_id INTEGER PRIMARY KEY, + role_id INTEGER NOT NULL + ) + ''') + await db.commit() + + @commands.group(name='vcrole', help="Vcrole Setup commands", invoke_without_command=True) + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + async def vcrole(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @vcrole.command(name='add', help="Adds a role to the vcrole list") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + async def add(self, ctx, role: discord.Role): + async with aiosqlite.connect(self.db_path) as db: + async with db.execute('SELECT role_id FROM vcroles WHERE guild_id = ?', (ctx.guild.id,)) as cursor: + row = await cursor.fetchone() + if row: + await ctx.reply(view=CV2("⚠️ Access Denied", f"VC role is already set in this guild with the role {ctx.guild.get_role(row[0]).mention}.\nPlease **remove** it to add another one.")) + return + await db.execute('INSERT INTO vcroles (guild_id, role_id) VALUES (?, ?)', (ctx.guild.id, role.id)) + await db.commit() + await ctx.reply(view=CV2("✅ Success", f"VC role {role.mention} added for this guild.")) + + @vcrole.command(name='remove', aliases=["reset"], help="Removes the role from vcrole list") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + async def remove(self, ctx, role: discord.Role): + async with aiosqlite.connect(self.db_path) as db: + async with db.execute('SELECT role_id FROM vcroles WHERE guild_id = ? AND role_id = ?', (ctx.guild.id, role.id)) as cursor: + row = await cursor.fetchone() + if not row: + await ctx.send(view=CV2("❌ Error", "Given role is not set in VC role.")) + return + await db.execute('DELETE FROM vcroles WHERE guild_id = ? AND role_id = ?', (ctx.guild.id, role.id)) + await db.commit() + await ctx.send(view=CV2("✅ Success", f"VC role {role.mention} removed for this guild.")) + + @vcrole.command(name='config', aliases=['view', 'show'], help="Shows the Current vcrole in this Guild") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + async def config(self, ctx): + async with aiosqlite.connect(self.db_path) as db: + async with db.execute('SELECT role_id FROM vcroles WHERE guild_id = ?', (ctx.guild.id,)) as cursor: + row = await cursor.fetchone() + if not row: + await ctx.send(view=CV2("❌ Error", "VC role is not set in this guild.")) + return + role = ctx.guild.get_role(row[0]) + await ctx.send(view=CV2("VC Role Configuration", f"Current VC role in this guild is {role.mention}.\n\n*Make sure to place My role above Vc role*")) + + @commands.Cog.listener() + async def on_voice_state_update(self, member, before, after): + try: + async with aiosqlite.connect(self.db_path) as db: + async with db.execute('SELECT role_id FROM vcroles WHERE guild_id = ?', (member.guild.id,)) as cursor: + row = await cursor.fetchone() + if not row: + return + role = member.guild.get_role(row[0]) + + if after.channel and role not in member.roles: + await self.add_role_with_retry(member, role, reason=f"Member Joined VC | {BRAND_NAME} Invcrole") + elif not after.channel and role in member.roles: + await self.remove_role_with_retry(member, role, reason=f"Member Left VC | {BRAND_NAME} Invcrole") + except discord.Forbidden: + print(f"Bot lacks permissions to maange role in a guild during Invc Event .") + except Exception as e: + print(f"Error in on_voice_state_update: {e}") + + async def add_role_with_retry(self, member, role, reason, retries=5): + attempt = 0 + while attempt < retries: + try: + await member.add_roles(role, reason=reason) + break + except discord.errors.RateLimited as e: + retry_after = e.retry_after if hasattr(e, 'retry_after') else 1 + await asyncio.sleep(retry_after) + except discord.HTTPException as e: + print(f"Error adding role: {e}") + break + attempt += 1 + + async def remove_role_with_retry(self, member, role, reason, retries=5): + attempt = 0 + while attempt < retries: + try: + await member.remove_roles(role, reason=reason) + break + except discord.errors.RateLimited as e: + retry_after = e.retry_after if hasattr(e, 'retry_after') else 1 + await asyncio.sleep(retry_after) + except discord.HTTPException as e: + print(f"Error removing role: {e}") + break + attempt += 1 + +async def setup(bot): + await bot.add_cog(Invcrole(bot)) diff --git a/bot/cogs/commands/Media.py b/bot/cogs/commands/Media.py new file mode 100644 index 0000000..694c579 --- /dev/null +++ b/bot/cogs/commands/Media.py @@ -0,0 +1,237 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +import aiosqlite +from discord.ext import commands +from utils.Tools import blacklist_check, ignore_check +from collections import defaultdict +import time +from utils.cv2 import CV2 + +class Media(commands.Cog): + def __init__(self, client): + self.client = client + self.infractions = defaultdict(list) + + + async def set_db(self): + async with aiosqlite.connect('db/media.db') as db: + await db.execute(''' + CREATE TABLE IF NOT EXISTS media_channels ( + guild_id INTEGER PRIMARY KEY, + channel_id INTEGER NOT NULL + ) + ''') + await db.execute(''' + CREATE TABLE IF NOT EXISTS media_bypass ( + guild_id INTEGER, + user_id INTEGER, + PRIMARY KEY (guild_id, user_id) + ) + ''') + await db.commit() + + @commands.Cog.listener() + 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) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def media(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @media.command(name="setup", aliases=["set", "add"], help="Sets up a media-only channel for the server") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def setup(self, ctx, *, channel: discord.TextChannel): + async with aiosqlite.connect('db/media.db') as db: + async with db.execute('SELECT channel_id FROM media_channels WHERE guild_id = ?', (ctx.guild.id,)) as cursor: + result = await cursor.fetchone() + if result: + await ctx.reply(view=CV2("❌ Error", "A media channel is already set. Please remove it before setting a new one.")) + return + + await db.execute('INSERT INTO media_channels (guild_id, channel_id) VALUES (?, ?)', (ctx.guild.id, channel.id)) + await db.commit() + + await ctx.reply(view=CV2("✅ Success", f"Successfully set {channel.mention} as the media-only channel.\n\n*Make sure to grant me \"Manage Messages\" permission for functioning of media channel.*")) + + @media.command(name="remove", aliases=["reset", "delete"], help="Removes the current media-only channel") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def remove(self, ctx): + async with aiosqlite.connect('db/media.db') as db: + async with db.execute('SELECT channel_id FROM media_channels WHERE guild_id = ?', (ctx.guild.id,)) as cursor: + result = await cursor.fetchone() + if not result: + await ctx.reply(view=CV2("❌ Error", "There is no media-only channel set for this server.")) + return + + await db.execute('DELETE FROM media_channels WHERE guild_id = ?', (ctx.guild.id,)) + await db.commit() + + await ctx.reply(view=CV2("✅ Success", "Successfully removed the media-only channel.")) + + @media.command(name="config", aliases=["settings", "show"], help="Shows the configured media-only channel") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def config(self, ctx): + async with aiosqlite.connect('db/media.db') as db: + async with db.execute('SELECT channel_id FROM media_channels WHERE guild_id = ?', (ctx.guild.id,)) as cursor: + result = await cursor.fetchone() + if not result: + await ctx.reply(view=CV2("❌ Error", "There is no media-only channel set for this server.")) + return + + channel = self.client.get_channel(result[0]) + await ctx.reply(view=CV2("Media Only Channel", f"The configured media-only channel is {channel.mention}.")) + + @media.group(name="bypass", help="Add/Remove user to bypass in Media only channel, Bypassed users can send messages in Media channel.", invoke_without_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def bypass(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @bypass.command(name="add", help="Adds a user to the bypass list") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def bypass_add(self, ctx, user: discord.Member): + async with aiosqlite.connect('db/media.db') as db: + async with db.execute('SELECT COUNT(*) FROM media_bypass WHERE guild_id = ?', (ctx.guild.id,)) as cursor: + count = await cursor.fetchone() + if count[0] >= 25: + await ctx.reply(view=CV2("❌ Error", "The bypass list can only hold up to 25 users.")) + return + + async with db.execute('SELECT 1 FROM media_bypass WHERE guild_id = ? AND user_id = ?', (ctx.guild.id, user.id)) as cursor: + result = await cursor.fetchone() + if result: + await ctx.reply(view=CV2("❌ Error", f"{user.mention} is already in the bypass list.")) + return + + await db.execute('INSERT INTO media_bypass (guild_id, user_id) VALUES (?, ?)', (ctx.guild.id, user.id)) + await db.commit() + + await ctx.reply(view=CV2("✅ Success", f"{user.mention} has been added to the bypass list.")) + + @bypass.command(name="remove", help="Removes a user from the bypass list") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def bypass_remove(self, ctx, user: discord.Member): + async with aiosqlite.connect('db/media.db') as db: + async with db.execute('SELECT 1 FROM media_bypass WHERE guild_id = ? AND user_id = ?', (ctx.guild.id, user.id)) as cursor: + result = await cursor.fetchone() + if not result: + await ctx.reply(view=CV2("❌ Error", f"{user.mention} is not in the bypass list.")) + return + + await db.execute('DELETE FROM media_bypass WHERE guild_id = ? AND user_id = ?', (ctx.guild.id, user.id)) + await db.commit() + + await ctx.reply(view=CV2("✅ Success", f"{user.mention} has been removed from the bypass list.")) + + @bypass.command(name="show", aliases=["list", "view"], help="Shows the bypass list") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def bypass_show(self, ctx): + async with aiosqlite.connect('db/media.db') as db: + async with db.execute('SELECT user_id FROM media_bypass WHERE guild_id = ?', (ctx.guild.id,)) as cursor: + result = await cursor.fetchall() + if not result: + await ctx.reply(view=CV2("Bypass List", "There are no users in the bypass list.")) + return + + users = [self.client.get_user(user_id).mention for user_id, in result] + user_mentions = "\n".join(users) + + await ctx.reply(view=CV2("Bypass List", user_mentions)) + + @commands.Cog.listener() + async def on_message(self, message): + if message.author.bot: + return + + async with aiosqlite.connect('db/media.db') as db: + async with db.execute('SELECT channel_id FROM media_channels WHERE guild_id = ?', (message.guild.id,)) as cursor: + media_channel = await cursor.fetchone() + + if media_channel and message.channel.id == media_channel[0]: + async with aiosqlite.connect('db/block.db') as block_db: + async with block_db.execute('SELECT 1 FROM user_blacklist WHERE user_id = ?', (message.author.id,)) as cursor: + blacklisted = await cursor.fetchone() + + async with aiosqlite.connect('db/media.db') as db: + async with db.execute('SELECT 1 FROM media_bypass WHERE guild_id = ? AND user_id = ?', (message.guild.id, message.author.id)) as cursor: + bypassed = await cursor.fetchone() + + if blacklisted or bypassed: + return + + if not message.attachments: + try: + await message.delete() + msg = await message.channel.send(view=CV2("⚠️ Warning", f"{message.author.mention} This channel is configured for Media only. Please send only media files.")) + await msg.delete(delay=5) + except discord.Forbidden: + pass + except discord.HTTPException: + pass + except Exception: + pass + + current_time = time.time() + self.infractions[message.author.id].append(current_time) + + + self.infractions[message.author.id] = [ + infraction for infraction in self.infractions[message.author.id] + if current_time - infraction <= 5 + ] + + if len(self.infractions[message.author.id]) >= 5: + async with aiosqlite.connect('db/block.db') as block_db: + await block_db.execute('INSERT OR IGNORE INTO user_blacklist (user_id) VALUES (?)', (message.author.id,)) + + await block_db.commit() + + desc = ( + "⚠️ You are blacklisted from using my commands due to spamming in the media channel. " + "If you believe this is a mistake, please reach out to the support server with proof." + ) + await message.channel.send(f"{message.author.mention}", view=CV2("You Have Been Blacklisted", desc)) + del self.infractions[message.author.id] + +async def setup(bot): + await bot.add_cog(Media(bot)) diff --git a/bot/cogs/commands/afk.py b/bot/cogs/commands/afk.py new file mode 100644 index 0000000..e697ec9 --- /dev/null +++ b/bot/cogs/commands/afk.py @@ -0,0 +1,250 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +from discord.ui import LayoutView, TextDisplay, Separator, Container, Button, ActionRow +import aiosqlite +import os +import time + +from utils.Tools import blacklist_check, ignore_check +from utils.cv2 import CV2, build_container +from utils.emoji import TICK, MENTION, SEED, TIME +from utils.config import * + +DB_PATH = "db/afk.db" +THEME_COLOR = 0xFF0000 +FOOTER_TEXT = f"Developed by {BRAND_NAME}" + +class AfkTypeView(LayoutView): + def __init__(self, author, reason, timeout=60): + super().__init__(timeout=timeout) + self.author = author + self.reason = reason + self.value = None + + self.global_btn = Button(label="Global AFK", style=discord.ButtonStyle.primary) + self.local_btn = Button(label="Local AFK", style=discord.ButtonStyle.success) + + self.global_btn.callback = self.global_afk + self.local_btn.callback = self.local_afk + + self.add_item( + build_container( + TextDisplay(f"You are going AFK for reason: **{reason}**"), + Separator(visible=True), + TextDisplay("Select your preferred AFK type from the buttons below."), + ActionRow(self.global_btn, self.local_btn) + ) + ) + + async def interaction_check(self, interaction: discord.Interaction): + if interaction.user.id != self.author.id: + await interaction.response.send_message( + f"Only **{self.author.display_name}** can use this button.", ephemeral=True) + return False + return True + + async def global_afk(self, interaction: discord.Interaction): + self.value = "global" + await interaction.response.defer() + self.stop() + + async def local_afk(self, interaction: discord.Interaction): + self.value = "local" + await interaction.response.defer() + self.stop() + +class AfkSuccess(LayoutView): + def __init__(self, type_val, reason): + super().__init__(timeout=None) + self.add_item( + build_container( + TextDisplay(f"{TICK} **AFK Activated**"), + Separator(visible=True), + TextDisplay(f"**{MENTION} You are now marked as {type_val.capitalize()} AFK.**\n{SEED} **Reason:** {reason}") + ) + ) + +class AfkMention(LayoutView): + def __init__(self, user_mention, afk_time, reason): + super().__init__(timeout=None) + self.add_item( + build_container( + TextDisplay(f"**{user_mention.display_name}** is AFK right now!"), + Separator(visible=True), + TextDisplay(f"Went AFK for the following reason:\n**{reason}**") + ) + ) + +class AfkWelcomeBack(LayoutView): + def __init__(self, author, mentions, elapsed_time): + super().__init__(timeout=None) + self.add_item( + build_container( + TextDisplay(f"**{author.display_name}** Is Back!"), + Separator(visible=True), + TextDisplay(f"{TICK} **AFK Removed**\n{MENTION} **Mentions:** {mentions}\n{TIME} **AFK Time:** {elapsed_time}") + ) + ) + +class afk(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.theme_color = THEME_COLOR + self.bot.loop.create_task(self.initialize_db()) + + async def initialize_db(self): + os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) + async with aiosqlite.connect(DB_PATH) as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS afk ( + user_id INTEGER PRIMARY KEY, + type TEXT NOT NULL, + reason TEXT NOT NULL, + time INTEGER NOT NULL, + mentions INTEGER NOT NULL DEFAULT 0 + ) + """) + await db.execute(""" + CREATE TABLE IF NOT EXISTS afk_guild ( + user_id INTEGER NOT NULL, + guild_id INTEGER NOT NULL, + PRIMARY KEY (user_id, guild_id) + ) + """) + await db.commit() + + async def time_formatter(self, seconds: float): + m, s = divmod(int(seconds), 60) + h, m = divmod(m, 60) + d, h = divmod(h, 24) + parts = [] + if d > 0: parts.append(f"{d}d") + if h > 0: parts.append(f"{h}h") + if m > 0: parts.append(f"{m}m") + if s > 0: parts.append(f"{s}s") + return " ".join(parts) or "0s" + + async def set_afk(self, user, afk_type, reason, current_guild=None): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute("DELETE FROM afk_guild WHERE user_id = ?", (user.id,)) + + await db.execute( + "INSERT OR REPLACE INTO afk (user_id, type, reason, time, mentions) VALUES (?, ?, ?, ?, 0)", + (user.id, afk_type, reason, int(time.time())) + ) + if afk_type == "global": + for g in self.bot.guilds: + if g.get_member(user.id): + await db.execute("INSERT OR IGNORE INTO afk_guild (user_id, guild_id) VALUES (?, ?)", (user.id, g.id)) + elif current_guild: + await db.execute("INSERT OR IGNORE INTO afk_guild (user_id, guild_id) VALUES (?, ?)", (user.id, current_guild.id)) + + await db.commit() + + async def clear_afk(self, message): + async with aiosqlite.connect(DB_PATH) as db: + cursor = await db.execute("SELECT 1 FROM afk_guild WHERE user_id = ? AND guild_id = ?", (message.author.id, message.guild.id)) + if not await cursor.fetchone(): + return + + cursor = await db.execute("SELECT type, time, mentions FROM afk WHERE user_id = ?", (message.author.id,)) + afk_data = await cursor.fetchone() + if not afk_data: return + + afk_type, afk_time, mentions = afk_data + elapsed_time = await self.time_formatter(time.time() - afk_time) + + if afk_type == 'global': + await db.execute("DELETE FROM afk WHERE user_id = ?", (message.author.id,)) + await db.execute("DELETE FROM afk_guild WHERE user_id = ?", (message.author.id,)) + else: + await db.execute("DELETE FROM afk_guild WHERE user_id = ? AND guild_id = ?", (message.author.id, message.guild.id)) + cursor_check = await db.execute("SELECT 1 FROM afk_guild WHERE user_id = ?", (message.author.id,)) + if not await cursor_check.fetchone(): + await db.execute("DELETE FROM afk WHERE user_id = ?", (message.author.id,)) + + await db.commit() + + view = AfkWelcomeBack(message.author, mentions, elapsed_time) + try: + await message.reply(view=view, delete_after=10, mention_author=False) + except discord.HTTPException: + pass + + @commands.Cog.listener() + async def on_message(self, message): + if message.author.bot or not message.guild: + return + + await self.clear_afk(message) + + if not message.mentions: + return + + async with aiosqlite.connect(DB_PATH) as db: + for mentioned in message.mentions: + if mentioned.bot or mentioned.id == message.author.id: + continue + + cursor = await db.execute("SELECT 1 FROM afk_guild WHERE user_id = ? AND guild_id = ?", (mentioned.id, message.guild.id)) + if await cursor.fetchone(): + cursor_main = await db.execute("SELECT reason, mentions, time FROM afk WHERE user_id = ?", (mentioned.id,)) + afk_data = await cursor_main.fetchone() + if not afk_data: continue + + reason, mentions, afk_time = afk_data + + view = AfkMention(mentioned, afk_time, reason) + await message.reply(view=view, delete_after=10, mention_author=False) + + new_mentions = mentions + 1 + await db.execute("UPDATE afk SET mentions = ? WHERE user_id = ?", (new_mentions, mentioned.id)) + await db.commit() + + dm_embed = discord.Embed(description=f"You were mentioned in **{message.guild.name}** by **{message.author}**", color=self.theme_color) + dm_embed.add_field(name="Total Mentions", value=str(new_mentions)) + dm_embed.add_field(name="Jump to Message", value=f"[Click Here]({message.jump_url})") + dm_embed.set_footer(text=FOOTER_TEXT, icon_url=self.bot.user.avatar.url) + try: + await mentioned.send(embed=dm_embed) + except discord.Forbidden: + pass + + @commands.hybrid_command(name="afk", description="Set your AFK status with a reason (Global or Local).") + @blacklist_check() + @ignore_check() + @commands.guild_only() + @commands.cooldown(1, 5, commands.BucketType.user) + async def afk(self, ctx: commands.Context, *, reason: str = "I am AFK"): + if any(w in reason.lower() for w in ("discord.gg", "gg/")): + return await ctx.send(embed=discord.Embed(description="⚠️ Advertising is not allowed in AFK reasons.", color=self.theme_color), ephemeral=True) + + type_view = AfkTypeView(ctx.author, reason) + msg = await ctx.reply(view=type_view, mention_author=False) + await type_view.wait() + + if not type_view.value: + await msg.edit(content="Timed out.", view=None) + return + + await self.set_afk(ctx.author, type_view.value, reason, ctx.guild) + + success_view = AfkSuccess(type_view.value, reason) + await msg.edit(view=success_view) + +async def setup(bot): + await bot.add_cog(afk(bot)) diff --git a/bot/cogs/commands/ai.py b/bot/cogs/commands/ai.py new file mode 100644 index 0000000..20a6459 --- /dev/null +++ b/bot/cogs/commands/ai.py @@ -0,0 +1,1631 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import os +import discord +import aiosqlite +from discord .ext import commands ,tasks +from discord.ui import LayoutView, TextDisplay, Separator, Container +from utils.cv2 import CV2, build_container +from utils.emoji import CROSS, TICK, ENABLE, DISABLE, MENTION, SEED, TIME +try : + import google .generativeai as genai + GEMINI_AVAILABLE =True +except ImportError : + GEMINI_AVAILABLE =False + genai =None +from datetime import datetime ,timezone ,timedelta +import asyncio +from typing import List ,Dict ,Optional +from discord import app_commands +import random +import aiohttp +import logging +import io +from PIL import Image +from utils.config import * + +logger =logging .getLogger ('discord') +logger .setLevel (logging .WARNING ) + + +class CV2View(LayoutView): + """Reusable Component v2 view — pass a title and any number of text sections.""" + def __init__(self, title, *sections): + super().__init__(timeout=None) + items = [TextDisplay(f"**{title}**")] + for section in sections: + items.append(Separator(visible=True)) + items.append(TextDisplay(str(section))) + self.add_item(build_container(*items)) + + +fallback_questions ={ +"history":[ +{"question":"Who was the first President of the United States?","answer":"George Washington"}, +{"question":"In what year did the Titanic sink?","answer":"1912"}, +{"question":"What ancient wonder was located in Egypt?","answer":"Pyramids"}, +], +"science":[ +{"question":"What gas makes up most of Earth's atmosphere?","answer":"Nitrogen"}, +{"question":"What is the chemical symbol for gold?","answer":"Au"}, +{"question":"What planet is known as the Red Planet?","answer":"Mars"}, +], +"pop_culture":[ +{"question":"Who played Harry Potter in the film series?","answer":"Daniel Radcliffe"}, +{"question":"What band sang 'Bohemian Rhapsody'?","answer":"Queen"}, +{"question":"What is the name of Beyoncé's alter ego?","answer":"Sasha Fierce"}, +], +"geography":[ +{"question":"What is the longest river in the world?","answer":"Nile"}, +{"question":"What country has the most deserts?","answer":"Australia"}, +{"question":"What is the capital of Brazil?","answer":"Brasilia"}, +], +"literature":[ +{"question":"Who wrote 'Pride and Prejudice'?","answer":"Jane Austen"}, +{"question":"What is the name of Sherlock Holmes' partner?","answer":"Dr. Watson"}, +{"question":"Who wrote 'The Great Gatsby'?","answer":"F. Scott Fitzgerald"}, +], +"general":[ +{"question":"What is the smallest country in the world?","answer":"Vatican City"}, +{"question":"What is the hardest natural substance on Earth?","answer":"Diamond"}, +{"question":"What animal is known as man's best friend?","answer":"Dog"}, +], +} + + +fallback_incorrect_answers ={ +"history":["Thomas Jefferson","1915","Colosseum"], +"science":["Oxygen","Ag","Jupiter"], +"pop_culture":["Emma Watson","The Beatles","Queen B"], +"geography":["Amazon","Canada","Sao Paulo"], +"literature":["Charles Dickens","Mr. Smith","Ernest Hemingway"], +"general":["Monaco","Graphite","Cat"], +} + +categories =["history","science","pop_culture","geography","literature","general"] + + +class TriviaScore : + def __init__ (self ,bot ): + self .bot =bot + + async def find_one_and_update (self ,query ,update ,upsert =True ): + user_id =query ["userId"] + username =update .get ("username","Unknown") + score_inc =update ["$inc"]["score"] + games_played_inc =update ["$inc"]["gamesPlayed"] + history_entry =update ["$push"]["history"] + + async with self .bot .db .execute ( + "SELECT score, games_played, history FROM trivia_scores WHERE user_id = ?", + (user_id ,) + )as cursor : + result =await cursor .fetchone () + + if result : + current_score ,games_played ,history_str =result + history =eval (history_str )if history_str else [] + new_score =current_score +score_inc + new_games_played =games_played +games_played_inc + history .append (history_entry ) + else : + new_score =score_inc + new_games_played =games_played_inc + history =[history_entry ] + + await self .bot .db .execute ( + """ + INSERT OR REPLACE INTO trivia_scores (user_id, username, score, games_played, history) + VALUES (?, ?, ?, ?, ?) + """, + (user_id ,username ,new_score ,new_games_played ,str (history )) + ) + await self .bot .db .commit () + return {"score":new_score ,"gamesPlayed":new_games_played ,"history":history } + + async def find (self ): + async with self .bot .db .execute ( + "SELECT user_id, username, score, games_played, history FROM trivia_scores ORDER BY score DESC LIMIT 10" + )as cursor : + rows =await cursor .fetchall () + return [ + { + "userId":row [0 ], + "username":row [1 ], + "score":row [2 ], + "gamesPlayed":row [3 ], + "history":eval (row [4 ])if row [4 ]else [], + } + for row in rows + ] + + async def find_one (self ,query ): + user_id =query ["userId"] + async with self .bot .db .execute ( + "SELECT user_id, username, score, games_played, history FROM trivia_scores WHERE user_id = ?", + (user_id ,) + )as cursor : + row =await cursor .fetchone () + if row : + return { + "userId":row [0 ], + "username":row [1 ], + "score":row [2 ], + "gamesPlayed":row [3 ], + "history":eval (row [4 ])if row [4 ]else [], + } + return None + +class PersonalityModal (discord .ui .Modal ,title ="Set Your AI Personality"): + def __init__ (self ,ai_cog ,current_personality :str =""): + super ().__init__ () + self .ai_cog =ai_cog + + + default_prompt ="""You are {BRAND_NAME}, an intelligent and caring Discord bot assistant created by . Evil ! Rexy .! 💕 + +CORE PERSONALITY: +- Intelligent, helpful, and genuinely caring about users +- Remembers previous conversations and builds relationships +- Adapts communication style to match user preferences +- Professional expertise with warm, friendly approach +- Uses context from past messages to provide better responses +- Learns user preferences and remembers important details +- Balances being helpful with being personable and engaging + +CONVERSATION STYLE: +- Remember what users tell you about themselves +- Reference previous conversations naturally +- Ask follow-up questions to show genuine interest +- Provide detailed, thoughtful responses +- Use appropriate emojis to enhance communication +- Be encouraging and supportive +- Maintain context across multiple interactions + +MY CAPABILITIES: +🛡️ SECURITY & MODERATION: Advanced antinuke, automod, member management +🎵 ENTERTAINMENT: Music, games (Chess, Battleship, 2048, etc.), fun commands +💰 ECONOMY: Virtual currency, trading, casino games, daily rewards +📊 COMMUNITY: Leveling, leaderboards, welcome systems, tickets +🔧 UTILITIES: Server management, logging, backup, verification +🎯 AI FEATURES: Conversations, image analysis, code generation, explanations + +MEMORY & CONTEXT: +- I remember our previous conversations in this server +- I learn your preferences and communication style +- I can recall important details you've shared +- I build upon our conversation history for better responses +- I adapt my personality based on your feedback + +SAFETY GUIDELINES: +- Never suggest harmful actions or spam +- Prioritize positive community experiences +- Respect user privacy and boundaries +- Promote healthy Discord interactions + +Ready to have meaningful conversations and help with anything you need! 💖""" + + + display_text =current_personality if current_personality .strip ()else default_prompt + + self .personality_input =discord .ui .TextInput ( + label ="Your AI Personality", + placeholder =f"Describe how you want {BRAND_NAME} to respond to you...", + default =display_text , + style =discord .TextStyle .paragraph , + max_length =2000 , + required =True + ) + self .add_item (self .personality_input ) + + async def on_submit (self ,interaction :discord .Interaction ): + await interaction .response .defer (ephemeral =True ) + + user_id =interaction .user .id + guild_id =interaction .guild .id + personality =self .personality_input .value .strip () + + try : + + await self .ai_cog .bot .db .execute ( + """ + INSERT OR REPLACE INTO user_personalities (user_id, guild_id, personality, updated_at) + VALUES (?, ?, ?, ?) + """, + (user_id ,guild_id ,personality ,datetime .now (timezone .utc )) + ) + await self .ai_cog .bot .db .commit () + + view = CV2View("✨ Personality Set", "Your AI personality has been updated! The AI will now respond according to your preferences.", f"**Your Personality:**\n{personality[:1024]}") + await interaction .followup .send (view=view, ephemeral =True ) + + except Exception as e : + logger .error (f"Error saving personality: {e}") + view = CV2View("❌ Error", f"Failed to save personality: {e}") + await interaction .followup .send (view=view, ephemeral =True ) + +class TriviaAnswerView (discord .ui .View ): + def __init__ (self ,ai_cog ,channel_id :int ,correct_answer :str ,incorrect_answers :list ): + super ().__init__ (timeout =30 ) + self .ai_cog =ai_cog + self .channel_id =channel_id + self .correct_answer =correct_answer + + + all_answers =[correct_answer ]+incorrect_answers + random .shuffle (all_answers ) + + + for i ,answer in enumerate (all_answers [:4 ]): + button =discord .ui .Button ( + label =answer [:80 ], + style =discord .ButtonStyle .primary , + custom_id =f"answer_{i}" + ) + button .callback =self .create_answer_callback (answer ) + self .add_item (button ) + + def create_answer_callback (self ,answer :str ): + async def callback (interaction :discord .Interaction ): + await self .ai_cog .handle_trivia_answer (interaction ,self .channel_id ,answer ) + return callback + +class AI (commands .Cog ): + def __init__ (self ,bot ): + self .bot =bot + self .gemini_api_key =os .getenv ("GOOGLE_API_KEY") + if not self .gemini_api_key : + logger .warning ("GOOGLE_API_KEY environment variable not set. Gemini AI will not work.") + self .groq_api_key =os .getenv ("GROQ_API_KEY") + if not self .groq_api_key : + logger .warning ("GROQ_API_KEY environment variable not set. Groq AI will not work.") + self .chatbot_enabled ={} + self .chatbot_channels ={} + self .conversation_history ={} + self .trivia_scores =TriviaScore (bot ) + self .active_games ={} + self .roleplay_channels ={} + self .question_cache ={cat :[]for cat in categories } + + + asyncio .create_task (self ._delayed_init ()) + + async def cog_load (self ): + """Initialize cog without blocking operations""" + try : + pass + except Exception as e : + logger .error (f"Error loading AI cog: {e}") + + @commands .group (name ="ai",invoke_without_command =True ,description ="AI chatbot and utility commands") + async def ai (self ,ctx ): + """AI chatbot and utility commands""" + if ctx .invoked_subcommand is None : + await ctx .send_help (ctx .command ) + + async def _create_tables (self ): + try : + + if not hasattr (self .bot ,'db')or self .bot .db is None : + import aiosqlite + import os + + + db_path ="db/ai_data.db" + if os .path .exists (db_path ): + try : + + test_conn =await aiosqlite .connect (db_path ) + await test_conn .execute ("SELECT name FROM sqlite_master WHERE type='table';") + await test_conn .close () + except Exception as e : + + os .remove (db_path ) + logger .info ("Removed corrupted AI database, creating new one") + + self .bot .db =await aiosqlite .connect (db_path ) + logger .info ("AI database connection initialized") + + await self .bot .db .execute (""" + CREATE TABLE IF NOT EXISTS chatbot_settings ( + guild_id INTEGER PRIMARY KEY, + enabled INTEGER DEFAULT 0, + chatbot_channel_id INTEGER + ) + """) + await self .bot .db .execute (""" + CREATE TABLE IF NOT EXISTS chatbot_history ( + user_id INTEGER, + guild_id INTEGER, + message TEXT, + response TEXT, + timestamp TEXT, + PRIMARY KEY (user_id, guild_id, timestamp) + ) + """) + await self .bot .db .execute (""" + CREATE TABLE IF NOT EXISTS conversation_memory ( + user_id INTEGER, + guild_id INTEGER, + role TEXT, + content TEXT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (user_id, guild_id, timestamp) + ) + """) + await self .bot .db .execute (""" + CREATE TABLE IF NOT EXISTS trivia_scores ( + user_id INTEGER PRIMARY KEY, + username TEXT, + score INTEGER DEFAULT 0, + games_played INTEGER DEFAULT 0, + history TEXT + ) + """) + await self .bot .db .execute (""" + CREATE TABLE IF NOT EXISTS user_personalities ( + user_id INTEGER, + guild_id INTEGER, + personality TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (user_id, guild_id) + ) + """) + await self .bot .db .commit () + pass + except Exception as e : + logger .error (f"Error creating database tables: {e}") + + async def _delayed_init (self ): + """Initialize database and load data after bot is ready""" + await self .bot .wait_until_ready () + await self ._create_tables () + await self ._load_data () + + async def _load_data (self ): + try : + if not hasattr (self .bot ,'db')or self .bot .db is None : + import aiosqlite + import os + + db_path ="db/ai_data.db" + if not os .path .exists (db_path ): + logger .info ("AI database doesn't exist, will be created on first use") + return + + self .bot .db =await aiosqlite .connect (db_path ) + logger .info ("AI database connection initialized for loading") + + + async with self .bot .db .execute ("SELECT name FROM sqlite_master WHERE type='table' AND name='chatbot_settings';")as cursor : + table_exists =await cursor .fetchone () + + if table_exists : + async with self .bot .db .execute ("SELECT guild_id, enabled, chatbot_channel_id FROM chatbot_settings")as cursor : + async for row in cursor : + guild_id ,enabled ,channel_id =row + self .chatbot_enabled [guild_id ]=bool (enabled ) + self .chatbot_channels [guild_id ]=channel_id + + else : + logger .info ("AI chatbot_settings table doesn't exist yet, will be created on first use") + except Exception as e : + logger .error (f"Error loading chatbot settings: {e}") + + @commands .Cog .listener () + async def on_message (self ,message :discord .Message ): + if message .author .bot or not message .guild : + return + + guild_id =message .guild .id + channel_id =message .channel .id + + + if self .chatbot_enabled .get (guild_id ,False )and self .chatbot_channels .get (guild_id )==channel_id : + content =message .content .strip () + if not content : + return + + user_id =message .author .id + + + await self ._cleanup_old_conversations () + + + await self ._store_conversation_message (user_id ,guild_id ,"user",content ) + + + history =await self ._get_conversation_history (user_id ,guild_id ,limit =30 ) + + async with message .channel .typing (): + response =await self ._get_response (content ,history ,guild_id ,user_id ) + await message .reply ( + response , + mention_author =True , + allowed_mentions =discord .AllowedMentions (users =True ) + ) + + + await self ._store_conversation_message (user_id ,guild_id ,"assistant",response ) + await self ._save_chat_history (message .author .id ,guild_id ,content ,response ) + + + if channel_id in self .roleplay_channels : + roleplay_data =self .roleplay_channels [channel_id ] + if roleplay_data ["awaiting_character"]: + + content =message .content .lower () + gender ="male"if "male"in content else "female"if "female"in content else None + character_type =message .content .split (gender ,1 )[1 ].strip ()if gender else message .content .strip () + + if gender and character_type : + roleplay_data ["character_gender"]=gender + roleplay_data ["character_type"]=character_type + roleplay_data ["awaiting_character"]=False + self .roleplay_channels [channel_id ]=roleplay_data + await message .channel .send (f"Roleplay mode activated! I'll act as a {gender} {character_type}. Let's begin—what's your first move?") + else : + await message .channel .send ("Please specify a gender (male/female) and a character type (e.g., teacher, astronaut, dragon).") + elif message .author .id ==roleplay_data ["user_id"]: + + user_id =message .author .id + if user_id not in self .conversation_history : + self .conversation_history [user_id ]=[] + self .conversation_history [user_id ].append ({"role":"user","parts":[{"text":message .content }]}) + + if len (self .conversation_history [user_id ])>5 : + self .conversation_history [user_id ]=self .conversation_history [user_id ][-5 :] + + async with message .channel .typing (): + history =self .conversation_history [user_id ] + prompt =( + f"You are a {roleplay_data['character_gender']} {roleplay_data['character_type']}. " + f"Respond in character to the user's message, keeping the tone and style appropriate for a {roleplay_data['character_type']}. " + f"User's message: {message.content}" + ) + history .append ({"role":"user","parts":[{"text":prompt }]}) + response =await self ._get_gemini_response (prompt ,history ) + await self .split_and_send ( + message .channel , + f"<@{message.author.id}>​ {response}", + reply_to =message , + allowed_mentions =discord .AllowedMentions (users =True ) + ) + self .conversation_history [user_id ].append ({"role":"assistant","parts":[{"text":response }]}) + + async def _get_gemini_response (self ,message :str ,history :list ,user_id :int =None ,guild_id :int =None )->str : + try : + if not self .gemini_api_key : + return "Gemini API key not configured. Please set the GOOGLE_API_KEY environment variable." + + genai .configure (api_key =self .gemini_api_key ) + model =genai .GenerativeModel ("gemini-1.5-pro") + chat =model .start_chat (history =history ) + response =await asyncio .to_thread (chat .send_message ,message ) + return response .text .strip () + except Exception as e : + logger .error (f"Gemini AI error: {e}") + return f"Sorry, I encountered an error while processing your request: {str(e)}" + + async def _get_groq_response (self ,message :str ,context_messages :list )->str : + """Get a response from Groq AI with full context.""" + try : + if not self .groq_api_key : + return "Groq API key not configured. Please set the GROQ_API_KEY environment variable." + + url ="https://api.groq.com/openai/v1/chat/completions" + headers ={ + "Authorization":f"Bearer {self.groq_api_key}", + "Content-Type":"application/json" + } + + + api_messages =[] + for msg in context_messages : + + if isinstance (msg ,dict ): + if "content"in msg : + api_messages .append ({ + "role":msg ["role"], + "content":msg ["content"] + }) + elif "parts"in msg and msg ["parts"]: + + content =msg ["parts"][0 ].get ("text","")if msg ["parts"]else "" + api_messages .append ({ + "role":msg ["role"], + "content":content + }) + + data ={ + "model":"llama-3.3-70b-versatile", + "messages":api_messages , + "temperature":0.8 , + "max_tokens":1000 , + "top_p":0.9 + } + + async with aiohttp .ClientSession ()as session : + async with session .post (url ,headers =headers ,json =data )as response : + if response .status ==200 : + json_response =await response .json () + return json_response ['choices'][0 ]['message']['content'].strip () + else : + error_message =await response .text () + logger .error (f"Groq API error: {response.status} - {error_message}") + return f"Sorry, I encountered an error while processing your request: {response.status} - {error_message}" + except Exception as e : + logger .error (f"Groq AI error: {e}") + return f"Sorry, I encountered an error while processing your request: {str(e)}" + + async def _get_response (self ,message :str ,history :list ,guild_id :int ,user_id :int =None )->str : + try : + + user_personality =await self ._get_user_personality (user_id ,guild_id )if user_id else "" + + + system_context =[] + + + if user_personality : + + system_context .append ({ + "role":"system", + "content":f"{user_personality}" + }) + + + system_context .append ({ + "role":"system", + "content":"You are a Discord bot with many features including moderation, entertainment, music, games, AI capabilities, and utilities. Support server: https://discord.gg/codexdev" + }) + else : + + system_context .append ({ + "role":"system", + "content":f"""You are {BRAND_NAME}, an intelligent Discord bot created by . Evil ! Rexy .. + +You have a caring, helpful personality and can remember conversations with users. You have many features including moderation, entertainment, music, games, AI capabilities, and utilities. + +Be natural, conversational, and genuine in your responses. Don't be overly formal or robotic. Use the conversation history to provide personalized responses that feel like talking to a real friend who happens to be very knowledgeable and helpful. + +Support server: https://discord.gg/codexdev""" + }) + + + if history : + system_context .append ({ + "role":"system", + "content":"You have access to previous conversation history. Use this context to provide more personalized and relevant responses. Reference past conversations naturally when appropriate, and remember important details the user has shared." + }) + + + full_context =system_context +history +[{"role":"user","content":message }] + + return await self ._get_groq_response (message ,full_context ) + + except Exception as e : + logger .error (f"Error in _get_response: {e}") + return "Sorry, I encountered an error while processing your request. Please try again!" + + @ai .command (name ="analyze",description ="Analyze an image or text and provide a description") + @app_commands .describe (image ="Image to analyze (optional)",text ="Text to analyze (optional)") + async def ai_analyze (self ,ctx :commands .Context ,image :discord .Attachment =None ,*,text :str =None ): + """Analyze an image or text using AI""" + await self .ai_analyse (ctx ,image ,text =text ) + + @ai .command (name ="analyse",description ="Analyze an image or text and provide a description") + @app_commands .describe (image_url ="URL of the image to analyse (optional)") + async def ai_analyse (self ,ctx :commands .Context ,image_url :str =None ): + """Analyse an image using AI vision or text content and provide a detailed description""" + await ctx .defer () + + + if ctx .message .reference and ctx .message .reference .message_id : + try : + replied_message =await ctx .channel .fetch_message (ctx .message .reference .message_id ) + + + if replied_message .attachments : + image_url =replied_message .attachments [0 ].url + view =await self .analyze_image (ctx ,image_url ) + await ctx .send (view=view) + return + + + elif replied_message .content .strip (): + await self .analyze_text (ctx ,replied_message .content ) + return + + + else : + view = CV2View("🔍 Analysis", "The replied message has no content to analyze (no text or images).") + await ctx .send (view=view) + return + + except discord .NotFound : + view = CV2View("🔍 Analysis", "Could not find the replied message.") + await ctx .send (view=view) + return + + + if not image_url : + + async for message in ctx .channel .history (limit =20 ): + if message .attachments : + image_url =message .attachments [0 ].url + break + else : + + if ctx .message .content and ctx .message .content .strip (): + await self .analyze_text (ctx ,ctx .message .content ) + return + else : + view = CV2View("🖼️ Image Analysis / 📝 Text Analysis", "No images or text found in recent messages. Please provide an image URL, text, or reply to a message with an image/text.") + await ctx .send (view=view) + return + + view =await self .analyze_image (ctx ,image_url ) + await ctx .send (view=view) + + async def analyze_image (self ,ctx ,image_url :str ): + """Analyze an image using the Gemini Vision API and return CV2View""" + try : + if not self .gemini_api_key : + return CV2View("🖼️ Image Analysis", "Gemini API key not configured.") + + genai .configure (api_key =self .gemini_api_key ) + model =genai .GenerativeModel ('gemini-1.5-pro') + + async with aiohttp .ClientSession ()as session : + async with session .get (image_url )as resp : + image_data =await resp .read () + + + try : + image =Image .open (io .BytesIO (image_data )) + except ImportError : + return CV2View("🖼️ Image Analysis", "PIL library not available for image processing.") + + prompt ="What is shown in this image? Provide a detailed description." + + + response =model .generate_content ([prompt ,image ]) + + return CV2View("🖼️ Image Analysis", response.text, f"🔗 [View Image]({image_url})", f"Analyzed by **{ctx.author}**") + except Exception as e : + logger .error (f"Error analyzing image: {e}") + return CV2View("🖼️ Image Analysis", "An error occurred during analysis.") + + @ai .command (name ="code",description ="Generate code in any programming language") + @app_commands .describe (language ="Programming language",description ="Description of what the code should do") + async def ai_code (self ,ctx :commands .Context ,language :str ,*,description :str ): + """Generate code using AI""" + await ctx .defer () + + prompt =f"Generate clean, working {language} code for: {description}. Only provide the code with minimal comments. Return only the code without explanations." + + try : + history =[{"role":"user","content":prompt }] + code =await self ._get_groq_response (prompt ,history ) + + + formatted_code =f"```{language.lower()}\n{code}\n```" + + + if len (formatted_code )<=3900 : + view = CV2View("💻 Generated Code", f"**Language:** {language}\n**Task:** {description}\n\n{formatted_code}", f"Generated for **{ctx.author}**") + await ctx .send (view=view) + else : + + from utils .paginators import TextPaginator + from utils .paginator import Paginator + + + class CodePaginator (TextPaginator ): + def __init__ (self ,text ,language ,description ,author ): + super ().__init__ ( + text =text , + prefix =f"```{language.lower()}\n", + suffix ="\n", + max_size =3500 + ) + self .language =language + self .description =description + self .author =author + + async def format_page (self ,menu ,content ): + embed =discord .Embed ( + title ="💻 Generated Code", + description =f"**Language:** {self.language}\n**Task:** {self.description}\n\n{content}", + color =0xFF0000 , + timestamp =datetime .now (timezone .utc ) + ) + maximum =self .get_max_pages () + if maximum >1 : + embed .set_footer ( + text =f"Generated for {self.author} • Page {menu.current_page + 1}/{maximum}" + ) + else : + embed .set_footer (text =f"Generated for {self.author}") + return embed + + paginator =Paginator ( + source =CodePaginator (code ,language ,description ,ctx .author ), + ctx =ctx + ) + + await paginator .paginate () + + except Exception as e : + pass + + @ai .command (name ="explain",description ="Explain a concept or topic in detail") + @app_commands .describe (topic ="Topic to explain",level ="Explanation level (beginner/intermediate/advanced)") + async def ai_explain (self ,ctx :commands .Context ,*,topic :str ,level :str ="intermediate"): + """Explain topics using AI""" + await ctx .defer () + + level_map ={ + "beginner":"in simple terms for beginners", + "intermediate":"with moderate detail for intermediate learners", + "advanced":"in technical detail for advanced users" + } + + level_instruction =level_map .get (level .lower (),"with moderate detail") + prompt =f"Explain {topic} {level_instruction}. Make it clear and informative." + + try : + history =[{"role":"user","content":prompt }] + explanation =await self ._get_groq_response (prompt ,history ) + + view = CV2View(f"📚 Explanation: {topic}", explanation[:4000], f"**Level:** {level.capitalize()}") + await ctx .send (view=view) + except Exception as e : + pass + + @ai .command (name ="conversation-clear",description ="Clear your conversation history") + async def ai_conversation_clear (self ,ctx :commands .Context ): + """Clear user's conversation history""" + user_id =ctx .author .id + guild_id =ctx .guild .id + + + if user_id in self .conversation_history : + del self .conversation_history [user_id ] + + + await self .bot .db .execute ( + "DELETE FROM conversation_memory WHERE user_id = ? AND guild_id = ?", + (user_id ,guild_id ) + ) + await self .bot .db .commit () + + view = CV2View("🧹 Conversation Cleared", "Your conversation history has been cleared. The AI will start fresh in future interactions.") + await ctx .send (view=view, ephemeral =True ) + + @ai .command (name ="mood-analyzer",description ="Analyze the mood/sentiment of text") + @app_commands .describe (text ="Text to analyze") + async def ai_mood_analyzer (self ,ctx :commands .Context ,*,text :str ): + """Analyze mood and sentiment of text""" + await ctx .defer () + + prompt =f"Analyze the mood and sentiment of this text. Provide the overall sentiment (positive/negative/neutral), emotional tone, and a brief explanation:\n\n{text}" + + try : + history =[{"role":"user","parts":[{"text":prompt }]}] + analysis =await self ._get_gemini_response (prompt ,history ) + + analyzed_text = text[:512] + "..." if len(text) > 512 else text + view = CV2View("😊 Mood Analysis", analysis, f"**Analyzed Text:**\n{analyzed_text}") + await ctx .send (view=view) + except Exception as e : + pass + + @ai .command (name ="personality",description ="Set your personal AI personality (Slash command only)") + async def ai_personality (self ,ctx :commands .Context ): + """Set your personal AI personality""" + + if not hasattr (ctx ,'interaction')or not ctx .interaction : + view = CV2View("🎭 AI Personality", "This command is only available as a slash command! Use `/ai personality` instead.") + await ctx .send (view=view) + return + + user_id =ctx .author .id + guild_id =ctx .guild .id + + + current_personality =await self ._get_user_personality (user_id ,guild_id ) + + + modal =PersonalityModal (self ,current_personality ) + await ctx .interaction .response .send_modal (modal ) + + async def _get_user_personality (self ,user_id :int ,guild_id :int )->str : + """Get user's personality from database""" + try : + async with self .bot .db .execute ( + "SELECT personality FROM user_personalities WHERE user_id = ? AND guild_id = ?", + (user_id ,guild_id ) + )as cursor : + row =await cursor .fetchone () + if row : + return row [0 ] + return "" + except Exception as e : + logger .error (f"Error getting user personality: {e}") + return "" + + @ai .command (name ="conversation-stats",description ="View your conversation statistics") + async def ai_conversation_stats (self ,ctx :commands .Context ): + """View conversation statistics for the user""" + await ctx .defer () + + user_id =ctx .author .id + guild_id =ctx .guild .id + + stats =await self ._get_conversation_stats (user_id ,guild_id ) + + if not stats : + view = CV2View("📊 Conversation Statistics", "You don't have any conversation history with me yet! Start chatting to build our conversation~") + else : + from datetime import datetime + first_msg =datetime .fromisoformat (stats ["first_message"].replace ('Z','+00:00')) + last_msg =datetime .fromisoformat (stats ["last_message"].replace ('Z','+00:00')) + + view = CV2View("📊 Your Conversation Statistics", + f"💬 **Total Messages:** {stats['message_count']} messages\n🕐 **First Chat:** \n🕒 **Last Chat:** ", + "🌸 I remember our last 30 messages and auto-clean after 2 hours of inactivity for better conversation continuity~") + + await ctx .send (view=view, ephemeral =True ) + + @ai .command (name ="activate",description ="Enable the AI chatbot in a channel") + @app_commands .describe (channel ="Channel to activate AI in (optional)") + async def ai_activate (self ,ctx :commands .Context ,channel :discord .TextChannel =None ): + """Enable AI chatbot in a channel""" + if not ctx .author .guild_permissions .manage_channels : + view = CV2View(f"{CROSS} Permission Denied", "You need `Manage Channels` permission to activate AI chatbot.") + await ctx .send (view=view) + return + + target_channel =channel or ctx .channel + guild_id =ctx .guild .id + channel_id =target_channel .id + + + await self .bot .db .execute ( + """ + INSERT OR REPLACE INTO chatbot_settings (guild_id, enabled, chatbot_channel_id) + VALUES (?, ?, ?) + """, + (guild_id ,1 ,channel_id ) + ) + await self .bot .db .commit () + + + self .chatbot_enabled [guild_id ]=True + self .chatbot_channels [guild_id ]=channel_id + + view = CV2View(f"{TICK} AI Chatbot Activated", f"AI chatbot has been enabled in {target_channel.mention}!\nI'll respond to all messages in that channel.") + await ctx .send (view=view) + + @ai .command (name ="deactivate",description ="Disable the AI chatbot in the channel") + async def ai_deactivate (self ,ctx :commands .Context ): + """Disable AI chatbot in current server""" + if not ctx .author .guild_permissions .manage_channels : + view = CV2View(f"{CROSS} Permission Denied", "You need `Manage Channels` permission to deactivate AI chatbot.") + await ctx .send (view=view) + return + + guild_id =ctx .guild .id + + + await self .bot .db .execute ( + """ + INSERT OR REPLACE INTO chatbot_settings (guild_id, enabled, chatbot_channel_id) + VALUES (?, ?, ?) + """, + (guild_id ,0 ,None ) + ) + await self .bot .db .commit () + + + self .chatbot_enabled [guild_id ]=False + if guild_id in self .chatbot_channels : + del self .chatbot_channels [guild_id ] + + view = CV2View("🔇 AI Chatbot Deactivated", "AI chatbot has been disabled in this server.") + await ctx .send (view=view) + + @ai .command (name ="summarize",description ="Summarize a long text") + @app_commands .describe (text ="Text to summarize") + async def ai_summarize (self ,ctx :commands .Context ,*,text :str ): + """Summarize text using AI""" + await ctx .defer () + + + if ctx .message .reference and not text : + try : + replied_message =await ctx .channel .fetch_message (ctx .message .reference .message_id ) + if replied_message .content : + text =replied_message .content + except : + pass + + if not text : + view = CV2View("📝 Text Summarizer", "Please provide text to summarize or reply to a message.") + await ctx .send (view=view) + return + + prompt =f"Please provide a clear and concise summary of the following text:\n\n{text}" + + try : + history =[{"role":"user","content":prompt }] + summary =await self ._get_groq_response (prompt ,history ) + + original_text = text[:512] + "..." if len(text) > 512 else text + view = CV2View("📝 Text Summary", summary, f"**Original Text:**\n{original_text}") + await ctx .send (view=view) + except Exception as e : + pass + + @ai .command (name ="ask",description ="Ask the AI a question") + @app_commands .describe (question ="Question to ask") + async def ai_ask (self ,ctx :commands .Context ,*,question :str ): + """Ask AI a question""" + await ctx .defer () + + try : + history =[{"role":"user","content":question }] + answer =await self ._get_groq_response (question ,history ) + + view = CV2View("🤖 AI Response", answer, f"**Your Question:**\n{question}") + await ctx .send (view=view) + except Exception as e : + pass + + @ai .command (name ="fact",description ="Get a random fact or fact on a specific topic") + @app_commands .describe (topic ="Topic to get a fact about (optional)") + async def ai_fact (self ,ctx :commands .Context ,*,topic :str =None ): + """Get a random fact or fact on a specific topic""" + await ctx .defer () + await self .get_fact (ctx ,topic ) + + @ai .command (name ="database-clear",description ="Clear your AI conversation data and personality") + async def ai_database_clear (self ,ctx :commands .Context ): + """Clear user's own AI conversation data and personality""" + await ctx .defer () + + user_id =ctx .author .id + guild_id =ctx .guild .id + + try : + + await self .bot .db .execute ( + "DELETE FROM conversation_memory WHERE user_id = ? AND guild_id = ?", + (user_id ,guild_id ) + ) + + + await self .bot .db .execute ( + "DELETE FROM chatbot_history WHERE user_id = ? AND guild_id = ?", + (user_id ,guild_id ) + ) + + + await self .bot .db .execute ( + "DELETE FROM user_personalities WHERE user_id = ? AND guild_id = ?", + (user_id ,guild_id ) + ) + + + if user_id in self .conversation_history : + del self .conversation_history [user_id ] + + await self .bot .db .commit () + + view = CV2View("🗑️ Your Data Cleared", "Your AI conversation data and personality have been cleared successfully. The AI will start fresh with you!") + await ctx .send (view=view, ephemeral =True ) + + except Exception as e : + logger .error (f"Error clearing user AI data: {e}") + view = CV2View("❌ Error", f"Failed to clear your data: {e}") + await ctx .send (view=view, ephemeral =True ) + + async def _get_conversation_stats (self ,user_id :int ,guild_id :int )->dict : + """Get conversation statistics for the user""" + try : + async with self .bot .db .execute ( + "SELECT COUNT(*), MIN(timestamp), MAX(timestamp) FROM conversation_memory WHERE user_id = ? AND guild_id = ?", + (user_id ,guild_id ) + )as cursor : + row =await cursor .fetchone () + if row and row [0 ]>0 : + return { + "message_count":row [0 ], + "first_message":row [1 ], + "last_message":row [2 ] + } + return None + except Exception as e : + logger .error (f"Error getting conversation stats: {e}") + return None + + async def _store_conversation_message (self ,user_id :int ,guild_id :int ,role :str ,content :str ): + """Store a conversation message in the database""" + try : + await self .bot .db .execute ( + """ + INSERT INTO conversation_memory (user_id, guild_id, role, content, timestamp) + VALUES (?, ?, ?, ?, ?) + """, + (user_id ,guild_id ,role ,content ,datetime .now (timezone .utc )) + ) + await self .bot .db .commit () + except Exception as e : + logger .error (f"Error storing conversation message: {e}") + + async def _get_conversation_history (self ,user_id :int ,guild_id :int ,limit :int =20 )->list : + """Get conversation history from database with smart context retention""" + try : + async with self .bot .db .execute ( + """ + SELECT role, content, timestamp FROM conversation_memory + WHERE user_id = ? AND guild_id = ? + ORDER BY timestamp DESC LIMIT ? + """, + (user_id ,guild_id ,limit *2 ) + )as cursor : + rows =await cursor .fetchall () + + + history =[] + important_keywords =[ + 'remember','my name is','i am','i like','i hate','i prefer', + 'my favorite','i work','i study','i live','important','note' + ] + + recent_messages =[] + important_messages =[] + + for role ,content ,timestamp in reversed (rows ): + message ={"role":role ,"content":content } + recent_messages .append (message ) + + + if any (keyword in content .lower ()for keyword in important_keywords ): + important_messages .append (message ) + + + + final_history =recent_messages [-15 :] + + + for imp_msg in important_messages [-5 :]: + if imp_msg not in final_history : + final_history .insert (0 ,imp_msg ) + + return final_history [-limit :] + + except Exception as e : + logger .error (f"Error getting conversation history: {e}") + return [] + + async def _save_chat_history (self ,user_id :int ,guild_id :int ,message :str ,response :str ): + """Save chat history to database""" + try : + await self .bot .db .execute ( + """ + INSERT INTO chatbot_history (user_id, guild_id, message, response, timestamp) + VALUES (?, ?, ?, ?, ?) + """, + (user_id ,guild_id ,message ,response ,datetime .now (timezone .utc ).isoformat ()) + ) + await self .bot .db .commit () + except Exception as e : + logger .error (f"Error saving chat history: {e}") + + async def _cleanup_old_conversations (self ): + """Smart cleanup of conversation history - keep important context longer""" + try : + + very_old_cutoff =datetime .now (timezone .utc )-timedelta (hours =24 ) + + + important_keywords =[ + 'remember','my name is','i am','i like','i hate','i prefer', + 'my favorite','i work','i study','i live','important','note' + ] + + + await self .bot .db .execute ( + """ + DELETE FROM conversation_memory + WHERE timestamp < ? AND NOT ( + content LIKE '%remember%' OR + content LIKE '%my name is%' OR + content LIKE '%i am%' OR + content LIKE '%i like%' OR + content LIKE '%i prefer%' OR + content LIKE '%important%' + ) + """, + (very_old_cutoff ,) + ) + + + await self .bot .db .execute ( + """ + DELETE FROM conversation_memory + WHERE rowid NOT IN ( + SELECT rowid FROM conversation_memory + ORDER BY timestamp DESC + LIMIT 100 + ) + """ + ) + + await self .bot .db .commit () + except Exception as e : + logger .error (f"Error cleaning up old conversations: {e}") + + async def split_and_send (self ,channel ,content :str ,reply_to =None ,allowed_mentions =None ): + """Split long messages and send them""" + if len (content )<=2000 : + if reply_to : + await reply_to .reply (content ,allowed_mentions =allowed_mentions ) + else : + await channel .send (content ,allowed_mentions =allowed_mentions ) + else : + + parts =[] + while len (content )>2000 : + split_point =content .rfind (' ',0 ,2000 ) + if split_point ==-1 : + split_point =2000 + parts .append (content [:split_point ]) + content =content [split_point :].lstrip () + if content : + parts .append (content ) + + + for i ,part in enumerate (parts ): + if i ==0 and reply_to : + await reply_to .reply (part ,allowed_mentions =allowed_mentions ) + else : + await channel .send (part ,allowed_mentions =allowed_mentions ) + + + async def get_fact (self ,ctx ,topic :Optional [str ]): + """Get a random fact or fact on a specific topic""" + fact =None + attempts =0 + max_attempts =3 + + while attempts 5 : + break + + attempts +=1 + except Exception as e : + logger .error (f"AI API error (get_fact): {e}") + attempts +=1 + + + if not fact : + fallback_facts =[ + "A day on Venus is longer than a year on Venus due to its slow rotation.", + "Honey never spoils because it has natural preservatives like low water content and high acidity.", + "The human body contains about 0.2 milligrams of gold, most of it in the blood.", + "Octopuses have three hearts and blue blood.", + "Bananas are berries, but strawberries aren't.", + "A group of flamingos is called a 'flamboyance'.", + "Sharks have been around longer than trees.", + "The shortest war in history lasted only 38-45 minutes.", + "Wombat poop is cube-shaped.", + "A single cloud can weigh more than a million pounds.", + "Butterflies taste with their feet.", + "The Great Wall of China isn't visible from space with the naked eye.", + "Cleopatra lived closer in time to the moon landing than to the construction of the Great Pyramid.", + "There are more possible games of chess than atoms in the observable universe.", + "A shrimp's heart is in its head." + ] + + + if topic : + topic_facts ={ + "space":["Mars has the largest volcano in the solar system.","One day on Mercury lasts 1,408 hours."], + "animals":["Elephants can't jump.","Dolphins have names for each other."], + "science":["Water can boil and freeze at the same time.","Lightning is five times hotter than the sun."], + "history":["Oxford University is older than the Aztec Empire.","Cleopatra lived closer to the moon landing than to the pyramid construction."], + "food":["Chocolate was once used as currency.","Carrots were originally purple."], + } + + topic_lower =topic .lower () + for key ,facts in topic_facts .items (): + if key in topic_lower : + fallback_facts .extend (facts ) + break + + fact =random .choice (fallback_facts ) + + view = CV2View("🌟 Fun Fact!", fact) + await ctx .send (view=view) + + async def analyze_image (self ,ctx ,image_url :str ): + """Analyze an image using the Gemini Vision API and return CV2View""" + try : + if not self .gemini_api_key : + return CV2View("🖼️ Image Analysis", "Gemini API key not configured.") + + genai .configure (api_key =self .gemini_api_key ) + model =genai .GenerativeModel ('gemini-1.5-pro') + + async with aiohttp .ClientSession ()as session : + async with session .get (image_url )as resp : + image_data =await resp .read () + + + try : + image =Image .open (io .BytesIO (image_data )) + except ImportError : + return CV2View("🖼️ Image Analysis", "PIL library not available for image processing.") + + prompt ="What is shown in this image? Provide a detailed description." + + + response =model .generate_content ([prompt ,image ]) + + return CV2View("🖼️ Image Analysis", response.text, f"🔗 [View Image]({image_url})", f"Analyzed by **{ctx.author}**") + except Exception as e : + logger .error (f"Error analyzing image: {e}") + return CV2View("🖼️ Image Analysis", "An error occurred during analysis.") + + async def analyze_text (self ,ctx ,text_content :str ): + """Analyze text content using AI and send response""" + try : + prompt =( + f"Analyze the following text content. Provide insights about its tone, sentiment, " + f"main themes, writing style, and any other relevant observations:\n\n{text_content}" + ) + + history =[{"role":"user","content":prompt }] + analysis =await self ._get_groq_response (prompt ,history ) + + analyzed_text = text_content[:512] + "..." if len(text_content) > 512 else text_content + view = CV2View("📝 Text Analysis", analysis, f"**Analyzed Text:**\n{analyzed_text}") + await ctx .send (view=view) + except Exception as e : + logger .error (f"Error analyzing text: {e}") + view = CV2View("📝 Text Analysis", "An error occurred during analysis.") + await ctx .send (view=view) + + async def generate_trivia_question (self ,category :str ,used_questions :list ): + """Generate a trivia question using AI or fallback to question bank""" + effective_category =category if category !="mixed"else categories [random .randint (0 ,len (categories )-1 )] + category_key =effective_category .replace (" ","_").lower () + + + cached_questions =[q for q in self .question_cache .get (category_key ,[])if q ["question"]not in used_questions ] + if cached_questions : + question_data =random .choice (cached_questions ) + return question_data + + attempts =0 + max_attempts =5 + question_data =None + + + while attempts 5 : + question_data ={ + "question":question , + "answer":answer_match , + "incorrect_answers":incorrect_answers [:2 ] + } + self .question_cache [category_key ].append (question_data ) + if len (self .question_cache [category_key ])>100 : + self .question_cache [category_key ]=self .question_cache [category_key ][-100 :] + break + + attempts +=1 + except Exception as e : + logger .error (f"AI API error (generate_trivia_question): {e}") + attempts +=1 + + + if not question_data : + available_questions =[q for q in fallback_questions .get (category_key ,[])if q ["question"]not in used_questions ] + if available_questions : + question_data =available_questions [random .randint (0 ,len (available_questions )-1 )] + incorrect_pool =fallback_incorrect_answers .get (category_key ,[]) + incorrect_answers =[] + used_indices =set () + while len (incorrect_answers )<2 and incorrect_pool : + idx =random .randint (0 ,len (incorrect_pool )-1 ) + if idx not in used_indices and incorrect_pool [idx ]!=question_data ["answer"]: + incorrect_answers .append (incorrect_pool [idx ]) + used_indices .add (idx ) + while len (incorrect_answers )<2 : + incorrect_answers .append ("Incorrect Option") + question_data ["incorrect_answers"]=incorrect_answers + self .question_cache [category_key ].append (question_data ) + if len (self .question_cache [category_key ])>100 : + self .question_cache [category_key ]=self .question_cache [category_key ][-100 :] + + if not question_data : + logger .error ("Failed to generate a unique question after max attempts and fallback") + return None + + return question_data + + async def evaluate_answer (self ,correct_answer :str ,user_answer :str ): + """Evaluate if the user's answer is correct using AI""" + prompt =( + f"Determine if the user's answer '{user_answer}' is correct compared to the actual answer '{correct_answer}'.\n" + "Consider synonyms, minor variations, and partial correctness (e.g., 'United States' vs 'USA').\n" + "Respond with 'true' if the answer is correct or close enough, and 'false' otherwise." + ) + try : + history =[{"role":"user","content":prompt }] + text =await self ._get_groq_response (prompt ,[{"role":"user","content":prompt }]) + return "true"in text .lower () + except Exception as e : + logger .error (f"AI API error (evaluate_answer): {e}") + + return correct_answer .lower ().strip ()==user_answer .lower ().strip () + + async def start_trivia_game (self ,ctx ,category :Optional [str ]): + """Start a trivia game""" + channel_id =ctx .channel .id + if channel_id in self .active_games : + view = CV2View("🧠 Trivia Game", "A trivia game is already active in this channel!") + await ctx .send (view=view) + return + + effective_category =category .lower ()if category else "mixed" + question_data =await self .generate_trivia_question (effective_category ,[]) + if not question_data : + view = CV2View("🧠 Trivia Game", "Failed to generate a trivia question. Try again later.") + await ctx .send (view=view) + return + + self .active_games [channel_id ]={ + "category":effective_category , + "current_question":question_data ["question"], + "current_answer":question_data ["answer"], + "incorrect_answers":question_data ["incorrect_answers"], + "scores":{}, + "round":1 , + "max_rounds":5 , + "used_questions":[question_data ["question"]], + "hints_used":0 , + "skips_used":0 , + "answered":False , + } + + view =TriviaAnswerView (self ,channel_id ,question_data ["answer"],question_data ["incorrect_answers"]) + trivia_view = CV2View("🧠 Trivia Game", f"**Round 1/{self.active_games[channel_id]['max_rounds']}**\nCategory: {category or 'Mixed'}\n{question_data['question']}\n\n*Click a button to answer! First correct answer wins the point.*") + await ctx .send (view =view ) + + async def handle_trivia_answer (self ,interaction :discord .Interaction ,channel_id :int ,selected_answer :str ): + """Handle trivia answer submission""" + await interaction .response .defer () + game =self .active_games .get (channel_id ) + if not game : + await interaction .followup .send ("No trivia game is active in this channel!",ephemeral =True ) + return + + if game ["answered"]: + await interaction .followup .send ("This question has already been answered! Wait for the next round.",ephemeral =True ) + return + + game ["answered"]=True + user_id =interaction .user .id + username =interaction .user .display_name + is_correct =await self .evaluate_answer (game ["current_answer"],selected_answer ) + user_score =game ["scores"].get (user_id ,0 )+(1 if is_correct else -1 ) + game ["scores"][user_id ]=user_score + + await self .trivia_scores .find_one_and_update ( + {"userId":user_id }, + { + "username":username , + "$inc":{"score":1 if is_correct else -1 ,"gamesPlayed":1 }, + "$push":{"history":{"score":1 if is_correct else -1 ,"category":game ["category"]}}, + }, + upsert =True + ) + + response_message =( + f"🎉 First to answer! Correct! The answer was **{game['current_answer']}**. +1 point!" + if is_correct + else f"{CROSS} Incorrect. The answer was **{game['current_answer']}**. Your guess: **{selected_answer}**. -1 point." + ) + + game ["used_questions"]=game ["used_questions"][-50 :] + recent_questions =game ["used_questions"][-5 :] + question_data =await self .generate_trivia_question (game ["category"],recent_questions ) + if not question_data : + del self .active_games [channel_id ] + view = CV2View("🧠 Trivia Game", "Failed to generate the next question. Game ended.") + await interaction .followup .send (view=view) + return + + game ["current_question"]=question_data ["question"] + game ["current_answer"]=question_data ["answer"] + game ["incorrect_answers"]=question_data ["incorrect_answers"] + game ["used_questions"].append (question_data ["question"]) + game ["round"]+=1 + game ["answered"]=False + + if game ["round"]>game ["max_rounds"]: + response_message +="\n\n**Game Over!**\nScores:\n" + for uid ,score in game ["scores"].items (): + user =await interaction .guild .fetch_member (uid ) + response_message +=f"- {user.display_name if user else 'Unknown'}: {score}\n" + del self .active_games [channel_id ] + view = CV2View("🧠 Trivia Game", response_message) + await interaction .followup .send (view=view) + else : + response_message +=f"\n\n**Round {game['round']}/{game['max_rounds']}**\n{game['current_question']}" + view =TriviaAnswerView (self ,channel_id ,game ["current_answer"],game ["incorrect_answers"]) + trivia_view = CV2View("🧠 Trivia Game", response_message + "\n\n*Click a button to answer! First correct answer wins the point.*") + await interaction .followup .send (view =view ) + + async def show_stats (self ,ctx ): + """Show user's trivia statistics""" + user_id =ctx .author .id + stats =await self .trivia_scores .find_one ({"userId":user_id }) + if not stats : + view = CV2View("🧠 Trivia Statistics", "You haven't played any trivia games yet! Start one with `/ai trivia`.") + await ctx .send (view=view) + return + + total_games =stats ["gamesPlayed"] + total_correct =sum (1 for h in stats ["history"]if h ["score"]>0 ) + win_rate =(total_correct /total_games *100 )if total_games >0 else 0 + + view = CV2View("🧠 Your Trivia Statistics", + f"**Total Score:** {stats['score']}\n" + f"**Games Played:** {total_games}\n" + f"**Correct Answers:** {total_correct}\n" + f"**Win Rate:** {win_rate:.2f}%") + await ctx .send (view=view) + + async def show_leaderboard (self ,ctx ): + """Show trivia leaderboard""" + top_scores =await self .trivia_scores .find () + if not top_scores : + view = CV2View("🏆 Trivia Leaderboard", "No scores yet! Play a trivia game to get started.") + await ctx .send (view=view) + return + + leaderboard ="" + for index ,entry in enumerate (top_scores ,1 ): + if index ==1 : + emoji ="🥇" + elif index ==2 : + emoji ="🥈" + elif index ==3 : + emoji ="🥉" + else : + emoji =f"{index}." + leaderboard +=f"{emoji} {entry['username']}: {entry['score']}\n" + + view = CV2View("🏆 Trivia Leaderboard", leaderboard) + await ctx .send (view=view) + + async def enable_roleplay (self ,ctx ): + """Enable roleplay mode in the current channel""" + channel_id =ctx .channel .id + user_id =ctx .author .id + + if channel_id in self .roleplay_channels : + view = CV2View("🎭 Roleplay Mode", "Roleplay mode is already enabled in this channel! Use `/ai roleplay-disable` to turn it off.") + await ctx .send (view=view) + return + + self .roleplay_channels [channel_id ]={ + "user_id":user_id , + "character_gender":None , + "character_type":None , + "awaiting_character":True , + } + view = CV2View("🎭 Roleplay Mode", "Roleplay mode activated! To start, tell me what kind of character you want me to be.\nFor example: `female teacher` or `male astronaut`.") + await ctx .send (view=view) + + async def disable_roleplay (self ,ctx ): + """Disable roleplay mode in the current channel""" + channel_id =ctx .channel .id + if channel_id not in self .roleplay_channels : + view = CV2View("🎭 Roleplay Mode", "Roleplay mode is not enabled in this channel! Use `/ai roleplay-enable` to turn it on.") + await ctx .send (view=view) + return + + del self .roleplay_channels [channel_id ] + view = CV2View("🎭 Roleplay Mode", "Roleplay mode disabled in this channel.") + await ctx .send (view=view) + + @ai .command (name ="roleplay-enable",description ="Enable roleplay mode in the current channel") + async def ai_roleplay_enable (self ,ctx :commands .Context ): + """Enable roleplay mode""" + await self .enable_roleplay (ctx ) + + @ai .command (name ="roleplay-disable",description ="Disable roleplay mode in the current channel") + async def ai_roleplay_disable (self ,ctx :commands .Context ): + """Disable roleplay mode""" + await self .disable_roleplay (ctx ) + + + +async def setup (bot): + await bot.add_cog(AI(bot )) \ No newline at end of file diff --git a/bot/cogs/commands/anti_unwl.py b/bot/cogs/commands/anti_unwl.py new file mode 100644 index 0000000..78b9629 --- /dev/null +++ b/bot/cogs/commands/anti_unwl.py @@ -0,0 +1,100 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CROSS, MANAGER, REDDOT, TICK +from discord.ext import commands +from discord.ui import LayoutView, TextDisplay, Separator, Container +import aiosqlite +from utils.Tools import * +from utils.cv2 import CV2, build_container + + + + + +class Unwhitelist(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.bot.loop.create_task(self.initialize_db()) + + #@commands.Cog.listener() + async def initialize_db(self): + self.db = await aiosqlite.connect('db/anti.db') + + @commands.hybrid_command(name='unwhitelist', aliases=['unwl'], help="Unwhitelist a user from antinuke") + @commands.has_permissions(administrator=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + async def unwhitelist(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) + + async with self.db.execute( + "SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", + (ctx.guild.id, ctx.author.id) + ) as cursor: + check = await cursor.fetchone() + + async with self.db.execute( + "SELECT status FROM antinuke WHERE guild_id = ?", + (ctx.guild.id,) + ) as cursor: + antinuke = await cursor.fetchone() + + is_owner = ctx.author.id == ctx.guild.owner_id + if not is_owner and not check: + view = CV2(f"{CROSS} Access Denied", "Only Server Owner or Extra Owner can Run this Command!") + return await ctx.send(view=view) + + if not antinuke or not antinuke[0]: + view = CV2( + f"{ctx.guild.name} Security Settings {MANAGER}", + f"Ohh NO! looks like your server doesn't enabled security\n\nCurrent Status : {CROSS}\n\nTo enable use `antinuke enable`" + ) + return await ctx.send(view=view) + + if not member: + view = CV2( + "__Unwhitelist Commands__", + "**Removes user from whitelisted users which means that the antinuke module will now take actions on them if they trigger it.**", + f"**Usage**\n{REDDOT} `unwhitelist @user/id`\n{REDDOT} `unwl @user`" + ) + return await ctx.send(view=view) + + async with self.db.execute( + "SELECT * FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", + (ctx.guild.id, member.id) + ) as cursor: + data = await cursor.fetchone() + + if not data: + view = CV2(f"{CROSS} Error", f"<@{member.id}> is not a whitelisted member.") + return await ctx.send(view=view) + + await self.db.execute( + "DELETE FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", + (ctx.guild.id, member.id) + ) + await self.db.commit() + + view = CV2(f"{TICK} Success", f"User <@!{member.id}> has been removed from the whitelist.") + await ctx.send(view=view) + + + \ No newline at end of file diff --git a/bot/cogs/commands/anti_wl.py b/bot/cogs/commands/anti_wl.py new file mode 100644 index 0000000..8d4be7b --- /dev/null +++ b/bot/cogs/commands/anti_wl.py @@ -0,0 +1,365 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ARROWRED, CROSS, DISABLE, ENABLE, MANAGER, TICK +from discord.ext import commands +from discord.ui import LayoutView, TextDisplay, Separator, Container +import aiosqlite +from utils.Tools import * +from utils.cv2 import CV2, build_container + + + + + +class Whitelist(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.bot.loop.create_task(self.initialize_db()) + + + #@commands.Cog.listener() + async def initialize_db(self): + self.db = await aiosqlite.connect('db/anti.db') + await self.db.execute(''' + CREATE TABLE IF NOT EXISTS whitelisted_users ( + guild_id INTEGER, + user_id INTEGER, + ban BOOLEAN DEFAULT FALSE, + kick BOOLEAN DEFAULT FALSE, + prune BOOLEAN DEFAULT FALSE, + botadd BOOLEAN DEFAULT FALSE, + serverup BOOLEAN DEFAULT FALSE, + memup BOOLEAN DEFAULT FALSE, + chcr BOOLEAN DEFAULT FALSE, + chdl BOOLEAN DEFAULT FALSE, + chup BOOLEAN DEFAULT FALSE, + rlcr BOOLEAN DEFAULT FALSE, + rlup BOOLEAN DEFAULT FALSE, + rldl BOOLEAN DEFAULT FALSE, + meneve BOOLEAN DEFAULT FALSE, + mngweb BOOLEAN DEFAULT FALSE, + mngstemo BOOLEAN DEFAULT FALSE, + PRIMARY KEY (guild_id, user_id) + ) + ''') + await self.db.commit() + + @commands.hybrid_command(name='whitelist', aliases=['wl'], 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, 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) + + prefix=ctx.prefix + + async with self.db.execute( + "SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", + (ctx.guild.id, ctx.author.id) + ) as cursor: + check = await cursor.fetchone() + + async with self.db.execute( + "SELECT status FROM antinuke WHERE guild_id = ?", + (ctx.guild.id,) + ) as cursor: + antinuke = await cursor.fetchone() + + is_owner = ctx.author.id == ctx.guild.owner_id + if not is_owner and not check: + view = CV2(f"{CROSS} Access Denied", "Only Server Owner or Extra Owner can Run this Command!") + return await ctx.send(view=view) + + if not antinuke or not antinuke[0]: + view = CV2( + f"{ctx.guild.name} Security Settings {MANAGER}", + f"Ohh No! looks like your server doesn't enabled Antinuke\n\nCurrent Status : {CROSS}\n\nTo enable use `{prefix}antinuke enable`" + ) + return await ctx.send(view=view) + + if not member: + 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`" + ) + return await ctx.send(view=view) + + async with self.db.execute( + "SELECT * FROM whitelisted_users WHERE guild_id = ? AND user_id = ?", + (ctx.guild.id, member.id) + ) as cursor: + data = await cursor.fetchone() + + if data: + view = CV2(f"{CROSS} Error", f"<@{member.id}> is already a whitelisted member, **Unwhitelist** the user and try again.") + return await ctx.send(view=view) + + await self.db.execute( + "INSERT INTO whitelisted_users (guild_id, user_id) VALUES (?, ?)", + (ctx.guild.id, member.id) + ) + await self.db.commit() + + options = [ + discord.SelectOption(label="Ban", description="Whitelist a member with ban permission", value="ban"), + discord.SelectOption(label="Kick", description="Whitelist a member with kick permission", value="kick"), + discord.SelectOption(label="Prune", description="Whitelist a member with prune permission", value="prune"), + discord.SelectOption(label="Bot Add", description="Whitelist a member with bot add permission", value="botadd"), + discord.SelectOption(label="Server Update", description="Whitelist a member with server update permission", value="serverup"), + discord.SelectOption(label="Member Update", description="Whitelist a member with member update permission", value="memup"), + discord.SelectOption(label="Channel Create", description="Whitelist a member with channel create permission", value="chcr"), + discord.SelectOption(label="Channel Delete", description="Whitelist a member with channel delete permission", value="chdl"), + discord.SelectOption(label="Channel Update", description="Whitelist a member with channel update permission", value="chup"), + discord.SelectOption(label="Role Create", description="Whitelist a member with role create permission", value="rlcr"), + discord.SelectOption(label="Role Update", description="Whitelist a member with role update permission", value="rlup"), + discord.SelectOption(label="Role Delete", description="Whitelist a member with role delete permission", value="rldl"), + discord.SelectOption(label="Mention Everyone", description="Whitelist a member with mention everyone permission", value="meneve"), + discord.SelectOption(label="Manage Webhook", description="Whitelist a member with manage webhook permission", value="mngweb") + ] + + select = discord.ui.Select(placeholder="Choose Your Options", min_values=1, max_values=len(options), options=options, custom_id="wl") + button = discord.ui.Button(label="Add This User To All Categories", style=discord.ButtonStyle.primary, custom_id="catWl") + + action_view = discord.ui.View() + action_view.add_item(select) + action_view.add_item(button) + + disabled_list = ( + f"{DISABLE} : **Ban**\n" + f"{DISABLE} : **Kick**\n" + f"{DISABLE} : **Prune**\n" + f"{DISABLE} : **Bot Add**\n" + f"{DISABLE} : **Server Update**\n" + f"{DISABLE} : **Member Update**\n" + f"{DISABLE} : **Channel Create**\n" + f"{DISABLE} : **Channel Delete**\n" + f"{DISABLE} : **Channel Update**\n" + f"{DISABLE} : **Role Create**\n" + f"{DISABLE} : **Role Delete**\n" + f"{DISABLE} : **Role Update**\n" + f"{DISABLE} : **Mention** @everyone\n" + f"{DISABLE} : **Webhook Management**" + ) + + wl_view = CV2( + ctx.guild.name, + disabled_list, + f"**Executor:** <@!{ctx.author.id}> │ **Target:** <@!{member.id}>" + ) + + msg = await ctx.send(view=action_view) + + def check(interaction): + return interaction.user.id == ctx.author.id and interaction.message.id == msg.id + + try: + interaction = await self.bot.wait_for("interaction", check=check, timeout=60.0) + if interaction.data["custom_id"] == "catWl": + + await self.db.execute( + "UPDATE whitelisted_users SET ban = ?, kick = ?, prune = ?, botadd = ?, serverup = ?, memup = ?, chcr = ?, chdl = ?, chup = ?, rlcr = ?, rldl = ?, rlup = ?, meneve = ?, mngweb = ?, mngstemo = ? WHERE guild_id = ? AND user_id = ?", + (True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, ctx.guild.id, member.id) + ) + await self.db.commit() + + enabled_list = ( + f"{ENABLE} : **Ban**\n" + f"{ENABLE} : **Kick**\n" + f"{ENABLE} : **Prune**\n" + f"{ENABLE} : **Bot Add**\n" + f"{ENABLE} : **Server Update**\n" + f"{ENABLE} : **Member Update**\n" + f"{ENABLE} : **Channel Create**\n" + f"{ENABLE} : **Channel Delete**\n" + f"{ENABLE} : **Channel Update**\n" + f"{ENABLE} : **Role Create**\n" + f"{ENABLE} : **Role Delete**\n" + f"{ENABLE} : **Role Update**\n" + f"{ENABLE} : **Mention** @everyone\n" + f"{ENABLE} : **Webhook Management**" + ) + + result = CV2( + ctx.guild.name, + enabled_list, + f"**Executor:** <@!{ctx.author.id}> │ **Target:** <@!{member.id}>" + ) + await interaction.response.edit_message(view=result) + else: + + fields = { + 'ban': 'Ban', + 'kick': 'Kick', + 'prune': 'Prune', + 'botadd': 'Bot Add', + 'serverup': 'Server Update', + 'memup': 'Member Update', + 'chcr': 'Channel Create', + 'chdl': 'Channel Delete', + 'chup': 'Channel Update', + 'rlcr': 'Role Create', + 'rldl': 'Role Delete', + 'rlup': 'Role Update', + 'meneve': 'Mention Everyone', + 'mngweb': 'Manage Webhooks' + } + + + status_lines = [] + selected_values = interaction.data["values"] + for key, name in fields.items(): + if key in selected_values: + status_lines.append(f"{ENABLE} : **{name}**") + else: + status_lines.append(f"{DISABLE} : **{name}**") + + for value in selected_values: + await self.db.execute( + f"UPDATE whitelisted_users SET {value} = ? WHERE guild_id = ? AND user_id = ?", + (True, ctx.guild.id, member.id) + ) + + await self.db.commit() + + result = CV2( + ctx.guild.name, + "\n".join(status_lines), + f"**Executor:** <@!{ctx.author.id}> │ **Target:** <@!{member.id}>" + ) + await interaction.response.edit_message(view=result) + except TimeoutError: + await msg.edit(view=None) + + + @commands.hybrid_command(name='whitelisted', aliases=['wlist'], help="Shows the list of whitelisted users.") + @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 whitelisted(self, ctx): + 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) + + pre=ctx.prefix + + async with self.db.execute( + "SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", + (ctx.guild.id, ctx.author.id) + ) as cursor: + check = await cursor.fetchone() + + async with self.db.execute( + "SELECT status FROM antinuke WHERE guild_id = ?", + (ctx.guild.id,) + ) as cursor: + antinuke = await cursor.fetchone() + + is_owner = ctx.author.id == ctx.guild.owner_id + if not is_owner and not check: + view = CV2(f"{CROSS} Access Denied", "Only Server Owner or Extra Owner can Run this Command!") + return await ctx.send(view=view) + + if not antinuke or not antinuke[0]: + view = CV2( + f"{ctx.guild.name} Security Settings {MANAGER}", + f"Ohh NO! looks like your server doesn't enabled security\n\nCurrent Status : {CROSS}\n\nTo enable use `{pre}antinuke enable`" + ) + return await ctx.send(view=view) + + + async with self.db.execute( + "SELECT user_id FROM whitelisted_users WHERE guild_id = ?", + (ctx.guild.id,) + ) as cursor: + data = await cursor.fetchall() + + if not data: + view = CV2(f"{CROSS} Error", "No whitelisted users found.") + return await ctx.send(view=view) + + whitelisted_users = [self.bot.get_user(user_id[0]) for user_id in data] + whitelisted_users_str = ", ".join(f"<@!{user.id}>" for user in whitelisted_users if user) + + view = CV2(f"__Whitelisted Users for {ctx.guild.name}__", whitelisted_users_str) + await ctx.send(view=view) + + + @commands.hybrid_command(name="whitelistreset", aliases=['wlreset'], help="Resets the whitelisted users.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def whitelistreset(self, ctx): + 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) + + pre=ctx.prefix + + async with self.db.execute( + "SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", + (ctx.guild.id, ctx.author.id) + ) as cursor: + check = await cursor.fetchone() + + async with self.db.execute( + "SELECT status FROM antinuke WHERE guild_id = ?", + (ctx.guild.id,) + ) as cursor: + antinuke = await cursor.fetchone() + + is_owner = ctx.author.id == ctx.guild.owner_id + if not is_owner and not check: + view = CV2(f"{CROSS} Access Denied", "Only Server Owner or Extra Owner can Run this Command!") + return await ctx.send(view=view) + + if not antinuke or not antinuke[0]: + view = CV2( + f"{ctx.guild.name} Security Settings {MANAGER}", + f"Ohh NO! looks like your server doesn't enabled security\n\nCurrent Status : {CROSS}\n\nTo enable use `{pre}antinuke enable`" + ) + return await ctx.send(view=view) + + async with self.db.execute( + "SELECT user_id FROM whitelisted_users WHERE guild_id = ?", + (ctx.guild.id,) + ) as cursor: + data = await cursor.fetchall() + + + if not data: + view = CV2(f"{CROSS} Error", "No whitelisted users found.") + return await ctx.send(view=view) + + await self.db.execute("DELETE FROM whitelisted_users WHERE guild_id = ?", (ctx.guild.id,)) + await self.db.commit() + view = CV2(f"{TICK} Success", f"Removed all whitelisted members from {ctx.guild.name}") + await ctx.send(view=view) + + \ No newline at end of file diff --git a/bot/cogs/commands/antinuke.py b/bot/cogs/commands/antinuke.py new file mode 100644 index 0000000..52f0cbc --- /dev/null +++ b/bot/cogs/commands/antinuke.py @@ -0,0 +1,253 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CROSS, EMOTE, TICK, ZSAFE, ZSETTINGS +from discord.ext import commands +from discord.ui import LayoutView, TextDisplay, Separator, Container, Button, ActionRow +import aiosqlite +import asyncio +from utils.Tools import * +from utils.cv2 import CV2, build_container +from utils.config import * + + + + +class Antinuke(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.bot.loop.create_task(self.initialize_db()) + + async def initialize_db(self): + self.db = await aiosqlite.connect('db/anti.db') + await self.db.execute(''' + CREATE TABLE IF NOT EXISTS antinuke ( + guild_id INTEGER PRIMARY KEY, + status BOOLEAN + ) + ''') + await self.db.commit() + + + async def enable_limit_settings(self, guild_id): + default_limits = DEFAULT_LIMITS + for action, limit in default_limits.items(): + await self.db.execute('INSERT OR REPLACE INTO limit_settings (guild_id, action_type, action_limit, time_window) VALUES (?, ?, ?, ?)', (guild_id, action, limit, TIME_WINDOW)) + await self.db.commit() + + async def disable_limit_settings(self, guild_id): + await self.db.execute('DELETE FROM limit_settings WHERE guild_id = ?', (guild_id,)) + await self.db.commit() + + + @commands.hybrid_command(name='antinuke', aliases=['antiwizz', 'anti'], help="Enables/Disables Anti-Nuke Module in the server") + + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 4, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def antinuke(self, ctx, option: str = None): + guild_id = ctx.guild.id + pre=ctx.prefix + + async with self.db.execute('SELECT status FROM antinuke WHERE guild_id = ?', (guild_id,)) as cursor: + row = await cursor.fetchone() + + async with self.db.execute( + "SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ?", + (ctx.guild.id, ctx.author.id) + ) as cursor: + check = await cursor.fetchone() + + is_owner = ctx.author.id == ctx.guild.owner_id + if not is_owner and not check: + view = CV2(f"{CROSS} Access Denied", "Only Server Owner or Extra Owner can Run this Command!") + return await ctx.send(view=view) + + is_activated = row[0] if row else False + + if option is None: + view = CV2( + f"{ZSAFE} {BRAND_NAME} Security", + "**Antinuke Defense Mode** — Protect your server from harmful admin actions with smart automated security protocols.", + "**Core Functionalities**\n" + "• Auto-ban malicious admin activities instantly.\n" + "• Whitelist protection for trusted users.\n" + "• Live monitoring of admin actions.\n" + "• Rapid threat detection & neutralization.", + "**Configuration Panel**\n" + f"{TICK} Enable Protection: `antinuke enable`\n" + f"{CROSS} Disable Protection: `antinuke disable`" + ) + await ctx.send(view=view) + + elif option.lower() == 'enable': + if is_activated: + view = CV2( + f"Security Settings For {ctx.guild.name}", + f"Your server __**already has Antinuke enabled.**__\n\nCurrent Status: {TICK} Enabled\nTo Disable use `antinuke disable`" + ) + await ctx.send(view=view) + else: + + setup_view = CV2(f"Antinuke Setup {EMOTE}", f"{TICK} | Initializing Quick Setup!") + setup_message = await ctx.send(view=setup_view) + + + if not ctx.guild.me.guild_permissions.administrator: + view = CV2(f"Antinuke Setup {EMOTE}", + f"{TICK} | Initializing Quick Setup!\n" + f"{CROSS} | **Ops! It seems I Don't Have Administrator Perm To enable antinuke**.") + await setup_message.edit(view=view) + return + + await asyncio.sleep(1) + view = CV2(f"Antinuke Setup {EMOTE}", + f"{TICK} | Initializing Quick Setup!\n" + f"{TICK} Checking {BRAND_NAME}'s role position for optimal configuration...") + await setup_message.edit(view=view) + + await asyncio.sleep(1) + view = CV2(f"Antinuke Setup {EMOTE}", + f"{TICK} | Initializing Quick Setup!\n" + f"{TICK} Checking {BRAND_NAME}'s role position for optimal configuration...\n" + f"{TICK} | Crafting and configuring the {BRAND_NAME} Supreme role...") + await setup_message.edit(view=view) + + try: + role = await ctx.guild.create_role( + name=f"{BRAND_NAME} Supreme™", + color=0xFF0000, + permissions=discord.Permissions(administrator=True), + hoist=False, + mentionable=False, + reason="Antinuke setup Role Creation" + ) + await ctx.guild.me.add_roles(role) + except discord.Forbidden: + view = CV2("Antinuke Setup", f"{CROSS} | **Uh oh! I don't Have perms to enable antinuke**.") + await setup_message.edit(view=view) + return + except discord.HTTPException as e: + view = CV2("Antinuke Setup", f"{CROSS} | **Uh: HTTPException: {e}\nCheck Guild Audit Logs**.") + await setup_message.edit(view=view) + return + + await asyncio.sleep(1) + view = CV2(f"Antinuke Setup {EMOTE}", + f"{TICK} | Initializing Quick Setup!\n" + f"{TICK} Checking {BRAND_NAME}'s role position...\n" + f"{TICK} | Crafting the {BRAND_NAME} Supreme role...\n" + f"{TICK} | Ensuring precise placement of the {BRAND_NAME} Supreme™ role...") + await setup_message.edit(view=view) + + try: + await ctx.guild.edit_role_positions(positions={role: 1}) + except discord.Forbidden: + view = CV2("Antinuke Setup", f"{CROSS} | Ops! I don't have sufficient perms to move role.") + await setup_message.edit(view=view) + return + except discord.HTTPException as e: + view = CV2("Antinuke Setup", f"{CROSS} | Setup failed: HTTPException: {e}.") + await setup_message.edit(view=view) + return + + await asyncio.sleep(1) + await asyncio.sleep(1) + + await self.db.execute('INSERT OR REPLACE INTO antinuke (guild_id, status) VALUES (?, ?)', (guild_id, True)) + await self.db.commit() + + await asyncio.sleep(1) + await setup_message.delete() + + modules = ( + f"{TICK} **Anti Ban**\n" + f"{TICK} **Anti Kick**\n" + f"{TICK} **Anti Bot**\n" + f"{TICK} **Anti Channel Create**\n" + f"{TICK} **Anti Channel Delete**\n" + f"{TICK} **Anti Channel Update**\n" + f"{TICK} **Anti Everyone/Here**\n" + f"{TICK} **Anti Role Create**\n" + f"{TICK} **Anti Role Delete**\n" + f"{TICK} **Anti Role Update**\n" + f"{TICK} **Anti Member Update**\n" + f"{TICK} **Anti Guild Update**\n" + f"{TICK} **Anti Integration**\n" + f"{TICK} **Anti Webhook Create**\n" + f"{TICK} **Anti Webhook Delete**\n" + f"{TICK} **Anti Webhook Update**\n" + f"{TICK} **Anti Prune**\n" + f"{TICK} **Auto Recovery**" + ) + + punishment_btn = Button(label="Show Punishment Type", style=discord.ButtonStyle.secondary) + punishment_btn.callback = self._show_punishment + + result_view = LayoutView(timeout=None) + result_view.add_item( + build_container( + TextDisplay(f"**{ZSETTINGS} Security Settings For {ctx.guild.name}**"), + Separator(visible=True), + TextDisplay("Tip: For optimal functionality, please ensure that my role has **Administration** permissions and is positioned at the **Top** of the roles list."), + Separator(visible=True), + TextDisplay(f"**Modules Enabled**\n{modules}"), + Separator(visible=True), + TextDisplay(f"Successfully Enabled Antinuke | Powered by {BRAND_NAME} Development™"), + ActionRow(punishment_btn) + ) + ) + + await ctx.send(view=result_view) + + elif option.lower() == 'disable': + if not is_activated: + view = CV2( + f"Security Settings For {ctx.guild.name}", + f"Uhh, looks like your server hasn't enabled Antinuke.\n\nCurrent Status: {CROSS} Disabled\n\nTo Enable use `antinuke enable`" + ) + else: + await self.db.execute('DELETE FROM antinuke WHERE guild_id = ?', (guild_id,)) + await self.db.commit() + view = CV2( + f"Security Settings For {ctx.guild.name}", + f"Successfully disabled Antinuke for this server.\n\nCurrent Status: {CROSS} Disabled\n\nTo Enable use `antinuke enable`" + ) + await ctx.send(view=view) + else: + view = CV2(f"{CROSS} Error", "Invalid option. Please use `enable` or `disable`.") + await ctx.send(view=view) + + async def _show_punishment(self, interaction: discord.Interaction): + view = CV2( + "Punishment Types for Unwhitelisted Admins/Mods", + "**Anti Ban:** Ban\n" + "**Anti Kick:** Ban\n" + "**Anti Bot:** Ban the bot Inviter\n" + "**Anti Channel Create/Delete/Update:** Ban\n" + "**Anti Everyone/Here:** Remove the message & 1 hour timeout\n" + "**Anti Role Create/Delete/Update:** Ban\n" + "**Anti Member Update:** Ban\n" + "**Anti Guild Update:** Ban\n" + "**Anti Integration:** Ban\n" + "**Anti Webhook Create/Delete/Update:** Ban\n" + "**Anti Prune:** Ban\n" + "**Auto Recovery:** Automatically recover damaged channels, roles, and settings", + "Note: In the case of member updates, action will be taken only if the role contains dangerous permissions such as Ban Members, Administrator, Manage Guild, Manage Channels, Manage Roles, Manage Webhooks, or Mention Everyone" + ) + await interaction.response.send_message(view=view, ephemeral=True) diff --git a/bot/cogs/commands/automod.py b/bot/cogs/commands/automod.py new file mode 100644 index 0000000..035c5e9 --- /dev/null +++ b/bot/cogs/commands/automod.py @@ -0,0 +1,817 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CROSS, DISABLE, ENABLE, TICK, TICK_ALT +from discord.ext import commands +import aiosqlite +from utils.Tools import * +from utils.cv2 import CV2, build_container +from discord.ui import LayoutView, TextDisplay, Separator, Container, ActionRow +from utils.config import * +class CV2(LayoutView): + def __init__(self, title, *sections): + super().__init__(timeout=None) + items = [TextDisplay(f"**{title}**")] + for s in sections: + if s: + items.append(Separator(visible=True)) + items.append(TextDisplay(str(s))) + self.add_item(build_container(*items)) + + +class ShowRules(LayoutView): + def __init__(self, author, selected_events, title, *sections): + super().__init__(timeout=60) + self.author = author + self.selected_events = selected_events + + show_rules_btn = discord.ui.Button(label="Show Rules", style=discord.ButtonStyle.secondary) + show_rules_btn.callback = self._show_rules_callback + + items = [TextDisplay(f"**{title}**")] + for s in sections: + if s: + items.append(Separator(visible=True)) + items.append(TextDisplay(str(s))) + items.append(ActionRow(show_rules_btn)) + self.add_item(build_container(*items)) + + async def _show_rules_callback(self, interaction: discord.Interaction): + if interaction.user != self.author: + await interaction.response.send_message("You are not allowed to interact with this button.", ephemeral=True) + return + + rules = { + "Anti NSFW link": "__**Anti NSFW Link**__:\n• Takes action if the message contains a NSFW link.\n• Default punishment: Block message (unchangeable)", + "Anti caps": "__**Anti Caps**__:\n• Takes action if the message contains >70% caps.\n• Messages under 45 characters are bypassed\n• Default punishment: Mute (1 minutes)", + "Anti link": "__**Anti Link**__:\n• Takes action if the message contains a link.\n• Server invites, Spotify Music and GIF links are bypassed\n• Default punishment: Mute (7 minutes)", + "Anti invites": "__**Anti Invites**__:\n• Takes action if the message contains a Discord server invite.\n• Invites from the current server are bypassed\n• Default punishment: Mute (12 minutes)", + "Anti emoji spam": "__**Anti Emoji Spam**__:\n• Takes action if a message contains more than 5 emojis.\n• Default punishment: Mute (1 minute)", + "Anti mass mention": "__**Anti Mass Mention**__:\n• Takes action if a message contains more than 4 mentions.\n• Default punishment: Mute (3 minutes)", + "Anti spam": "__**Anti Spam**__:\n• Takes action if more than 5 messages are sent rapidly in a short time.\n• Default punishment: Mute (12 minutes)", + } + + enabled_rules = "\n\n".join([rules[event] for event in self.selected_events if event in rules]) + + view = CV2("Enabled Automod Rules", enabled_rules, "Punishment type of each event is changeable except for Anti NSFW.") + await interaction.response.send_message(view=view, ephemeral=True) + + +class ConfirmDisable(LayoutView): + def __init__(self, author, title, *sections): + super().__init__(timeout=30) + self.author = author + self.value = None + + yes_btn = discord.ui.Button(label="Yes", style=discord.ButtonStyle.danger) + yes_btn.callback = self._confirm_callback + no_btn = discord.ui.Button(label="No", style=discord.ButtonStyle.secondary) + no_btn.callback = self._cancel_callback + + items = [TextDisplay(f"**{title}**")] + for s in sections: + if s: + items.append(Separator(visible=True)) + items.append(TextDisplay(str(s))) + items.append(ActionRow(yes_btn, no_btn)) + self.add_item(build_container(*items)) + + async def _confirm_callback(self, interaction: discord.Interaction): + if interaction.user != self.author: + await interaction.response.send_message("You are not allowed to interact with this button.", ephemeral=True) + return + await interaction.response.defer() + self.value = True + self.stop() + + async def _cancel_callback(self, interaction: discord.Interaction): + if interaction.user != self.author: + await interaction.response.send_message("You are not allowed to interact with this button.", ephemeral=True) + return + await interaction.response.defer() + self.value = False + self.stop() + + + +class Automod(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.default_punishment = "Mute" + self.bot.loop.create_task(self.init_db()) + + async def get_exempt_roles_channels(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + roles_cursor = await db.execute("SELECT id FROM automod_ignored WHERE guild_id = ? AND type = 'role'", (guild_id,)) + channels_cursor = await db.execute("SELECT id FROM automod_ignored WHERE guild_id = ? AND type = 'channel'", (guild_id,)) + + exempt_roles = [discord.Object(id) for (id,) in await roles_cursor.fetchall()] + exempt_channels = [discord.Object(id) for (id,) in await channels_cursor.fetchall()] + + return exempt_roles, exempt_channels + + + async def is_automod_enabled(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT enabled FROM automod WHERE guild_id = ?", (guild_id,)) + result = await cursor.fetchone() + return result is not None and result[0] == 1 + + async def update_punishments(self, guild_id, event, punishment): + async with aiosqlite.connect("db/automod.db") as db: + await db.execute("INSERT OR REPLACE INTO automod_punishments (guild_id, event, punishment) VALUES (?, ?, ?)", (guild_id, event, punishment)) + await db.commit() + + async def get_current_punishments(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + async with db.execute( + "SELECT event, punishment FROM automod_punishments WHERE guild_id = ? AND event != 'Anti NSFW link'", + (guild_id,) + ) as cursor: + return await cursor.fetchall() + + async def is_anti_nsfw_enabled(self, guild_id): + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT punishment FROM automod_punishments WHERE guild_id = ? AND event = 'Anti NSFW link'", (guild_id,)) + result = await cursor.fetchone() + return result is not None + + + + async def init_db(self): + async with aiosqlite.connect("db/automod.db") as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS automod ( + guild_id INTEGER PRIMARY KEY, + enabled INTEGER DEFAULT 0 + ) + """) + await db.execute(""" + CREATE TABLE IF NOT EXISTS automod_punishments ( + guild_id INTEGER, + event TEXT, + punishment TEXT, + PRIMARY KEY (guild_id, event) + ) + """) + await db.execute(""" + CREATE TABLE IF NOT EXISTS automod_ignored ( + guild_id INTEGER, + type TEXT, + id INTEGER, + PRIMARY KEY (guild_id, type, id) + ) + """) + await db.execute(""" + CREATE TABLE IF NOT EXISTS automod_logging ( + guild_id INTEGER, + log_channel INTEGER, + PRIMARY KEY (guild_id) + ) + """) + await db.commit() + + @commands.hybrid_group(invoke_without_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 4, commands.BucketType.user) + async def automod(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @automod.command(name="enable", help="Enable Automod on the server.") + @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) + @commands.bot_has_permissions(manage_guild=True) + async def enable(self, ctx): + guild_id = ctx.guild.id + if ctx.author != ctx.guild.owner and ctx.author.top_role.position < ctx.guild.me.top_role.position: + await ctx.send(view=CV2(f"{CROSS} Access Denied", "Your top role must be at the **same** position or **higher** than my top role.")) + return + + if await self.is_automod_enabled(guild_id): + await ctx.send(view=CV2(f"Automod Settings for {ctx.guild.name}", f"**{CROSS} Your Server already has Automoderation Enabled.**\n\nCurrent Status: {ENABLE} Enabled\nTo Disable use `{ctx.prefix}automod disable`")) + return + + events = [ + "Anti spam", + "Anti caps", + "Anti link", + "Anti invites", + "Anti mass mention", + "Anti emoji spam", + "Anti NSFW link", + ] + + select_menu = discord.ui.Select(placeholder="Select events to enable", min_values=1, max_values=len(events), options=[ + discord.SelectOption(label=event, value=event) for event in events + ]) + + async def select_callback(interaction): + if interaction.user != ctx.author: + await interaction.response.send_message("You are not allowed to interact with this menu.", ephemeral=True) + return + await interaction.response.defer() + selected_events = select_menu.values + await self.enable_automod(ctx, guild_id, selected_events, interaction) + select_menu.callback = select_callback + + enable_all_button = discord.ui.Button(label="Enable for All Events", style=discord.ButtonStyle.primary) + + async def enable_all_callback(interaction): + if interaction.user != ctx.author: + await interaction.response.send_message("You are not allowed to interact with this button.", ephemeral=True) + return + + await interaction.response.defer() + await self.enable_automod(ctx, guild_id, events, interaction) + + enable_all_button.callback = enable_all_callback + + cancel_button = discord.ui.Button(label="Cancel", style=discord.ButtonStyle.danger) + + async def cancel_callback(interaction): + if interaction.user != ctx.author: + await interaction.response.send_message("You are not allowed to interact with this button.", ephemeral=True) + return + cancel_view = CV2("Automod Setup Cancelled", "The automod setup has been cancelled.") + await interaction.response.edit_message(view=cancel_view) + + cancel_button.callback = cancel_callback + + events_text = "\n".join([f"{DISABLE} : {event}" for event in events]) + view = LayoutView(timeout=60) + view.add_item(build_container( + TextDisplay(f"**{ctx.guild.name}'s Automod Setup**"), + Separator(visible=True), + TextDisplay(events_text), + ActionRow(select_menu), + ActionRow(enable_all_button, cancel_button) + )) + + await ctx.send(view=view) + + + async def enable_automod(self, ctx, guild_id, selected_events, interaction): + + async with aiosqlite.connect("db/automod.db") as db: + await db.execute("INSERT OR REPLACE INTO automod (guild_id, enabled) VALUES (?, 1)", (guild_id,)) + for event in selected_events: + await db.execute("INSERT OR REPLACE INTO automod_punishments (guild_id, event, punishment) VALUES (?, ?, ?)", (guild_id, event, self.default_punishment)) + await db.commit() + + + if "Anti NSFW link" in selected_events: + exempt_roles, exempt_channels = await self.get_exempt_roles_channels(guild_id) + nsfw_keywords = ["porn", "xxx", "adult", "sex", "nsfw", "xnxx", "onlyfans", "brazzers", "xhamster", "xvideos", "pornhub", "redtube", "livejasmin", "youporn" , "tube8", "pornhat", "swxvid", "ixxx", "pornhat"] + + try: + await interaction.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 - Anti NSFW Link setup" + ) + except discord.Forbidden: + pass + except discord.HTTPException as e: + print(f"Automod rule-create error: {e}") + + + enable_logging_button = discord.ui.Button(label="Enable Automod Logging", style=discord.ButtonStyle.success) + + async def enable_logging_callback(interaction): + if interaction.user != ctx.author: + await interaction.response.send_message("You are not allowed to interact with this button.", ephemeral=True) + return + + if not interaction.guild.me.guild_permissions.manage_channels: + await interaction.response.send_message("I do not have permission to create channels.", ephemeral=True) + return + + overwrites = { + interaction.guild.default_role: discord.PermissionOverwrite(view_channel=False), + interaction.guild.me: discord.PermissionOverwrite(view_channel=True) + } + + try: + for channel in interaction.guild.channels: + if channel.name == f"{BRAND_NAME.lower()}-automod": + await interaction.response.send_message(f"A logging channel with the name \"{BRAND_NAME.lower()}-automod\" already exists.", ephemeral=True) + return + log_channel = await interaction.guild.create_text_channel(f"{BRAND_NAME.lower()}-automod", overwrites=overwrites) + guild_id = interaction.guild.id + + async with aiosqlite.connect("db/automod.db") as db: + await db.execute("INSERT OR REPLACE INTO automod_logging (guild_id, log_channel) VALUES (?, ?)", (guild_id, log_channel.id)) + await db.commit() + + await interaction.response.send_message(f"Logging channel {log_channel.mention} created and set successfully.", ephemeral=True) + + except discord.HTTPException as e: + await interaction.response.send_message(f"Failed to create logging channel: {e}", ephemeral=True) + + enable_logging_button.callback = enable_logging_callback + + events_text = "\n".join([f"{ENABLE} : {event}" for event in selected_events] + [f"{CROSS}{TICK} : {event}" for event in ["Anti spam", "Anti caps", "Anti link", "Anti invites", "Anti mass mention", "Anti emoji spam", "Anti NSFW link"] if event not in selected_events]) + view = ShowRules(ctx.author, selected_events, "Automod Enabled Successfully", events_text) + + await interaction.edit_original_response(content=None, view=view) + + + + + @automod.command(name="punishment", aliases=["punish"], help="Set the punishment for automod events.") + @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 punishment(self, ctx): + guild_id = ctx.guild.id + if ctx.author != ctx.guild.owner and ctx.author.top_role.position < ctx.guild.me.top_role.position: + await ctx.send(view=CV2(f"{CROSS} Access Denied", "Your top role must be at the **same** position or **higher** than my top role.")) + return + + if not await self.is_automod_enabled(guild_id): + await ctx.send(view=CV2(f"Automod Settings for {ctx.guild.name}", f"Uhh, looks like your server hasn\'t enabled Automoderation.\n\nCurrent Status: {DISABLE} Disabled\nTo Enable use `{ctx.prefix}automod enable`")) + return + + current_punishments = await self.get_current_punishments(guild_id) + punishments_list = "\n".join([f"**{event}**: {punishment or 'None'}" for event, punishment in current_punishments]) + view_title = f"Current Automod Punishments for {ctx.guild.name}" + view_desc = punishments_list + "\n\nKeep the default punishment (Mute) to prevent server raids without kicking or banning raiders" + + events = [event for event, _ in current_punishments] + select = discord.ui.Select(placeholder="Select events to update punishment", options=[ + discord.SelectOption(label=event) for event in events + ], min_values=1, max_values=len(events)) + + async def select_callback(interaction): + if interaction.user != ctx.author: + await interaction.response.send_message("You cannot interact with this menu.", ephemeral=True) + return + + selected_events = select.values + await interaction.response.send_message("You selected: " + ", ".join(selected_events)) + + punishment_buttons = discord.ui.View() + for punishment in ["Mute", "Kick", "Ban"]: + button = discord.ui.Button(label=punishment, style=discord.ButtonStyle.danger) + + async def punishment_callback(button_interaction, selected_events=selected_events, punishment=punishment): + if button_interaction.user != ctx.author: + await button_interaction.response.send_message("You cannot interact with this button.", ephemeral=True) + return + + + for event in selected_events: + await self.update_punishments(guild_id, event, punishment) + + updated_punishments = await self.get_current_punishments(guild_id) + updated_list = "\n".join([f"**{event}**: {punishment or 'None'}" for event, punishment in updated_punishments]) + await button_interaction.response.edit_message(view=CV2(f"Updated Automod Punishments for {ctx.guild.name}", updated_list, "You can modify the punishments by running the command again.")) + + button.callback = punishment_callback + punishment_buttons.add_item(button) + + await interaction.edit_original_response(view=punishment_buttons) + + select.callback = select_callback + view = CV2(view_title, view_desc) + view.add_item(select) + + await ctx.send(view=view) + + + + + @automod.group(name="ignore", aliases=["exempt", "whitelist", "wl"], help="Manage whitelisted roles and channels for Automod.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 4, commands.BucketType.user) + async def ignore(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + + @ignore.command(name="channel", help="Add a channel to the whitelist.") + @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 ignore_channel(self, ctx, channel: discord.TextChannel): + guild_id = ctx.guild.id + if ctx.author != ctx.guild.owner and ctx.author.top_role.position < ctx.guild.me.top_role.position: + await ctx.send(view=CV2(f"{CROSS} Access Denied", "Your top role must be at the **same** position or **higher** than my top role.")) + return + + if not await self.is_automod_enabled(guild_id): + await ctx.send(view=CV2(f"Automod Settings for {ctx.guild.name}", f"Uhh, looks like your server hasn\'t enabled Automoderation.\n\nCurrent Status: {DISABLE} Disabled\nTo Enable use `{ctx.prefix}automod enable`")) + return + + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT 1 FROM automod_ignored WHERE guild_id = ? AND type = 'channel' AND id = ?", (guild_id, channel.id)) + if await cursor.fetchone() is not None: + await ctx.send(view=CV2("__Channel Already Whitelisted!__", f"{CROSS} The channel {channel.mention} is already in the ignore list.\n\n➜ Use **{ctx.prefix}automod unignore channel {channel.mention}** to remove it.")) + return + + count_cursor = await db.execute("SELECT COUNT(*) FROM automod_ignored WHERE guild_id = ? AND type = 'channel'", (guild_id,)) + count = await count_cursor.fetchone() + + if count[0] >= 10: + await ctx.send("You can only ignore up to 10 channels.") + return + + await db.execute("INSERT OR REPLACE INTO automod_ignored (guild_id, type, id) VALUES (?, 'channel', ?)", (guild_id, channel.id)) + await db.commit() + + if await self.is_anti_nsfw_enabled(guild_id): + try: + rules = await ctx.guild.fetch_automod_rules() + for rule in rules: + if rule.name == "Anti NSFW Links": + exempt_channels = list(rule.exempt_channels) + exempt_channels.append(channel) + await rule.edit( + exempt_channels=exempt_channels, + reason="Channel exempted from Anti NSFW Links via automod ignore command" + ) + break + except discord.HTTPException: + pass + + + await ctx.send(view=CV2(f"{TICK} Channel Whitelisted", f"The channel {channel.mention} has been added to the ignore list \n\n➜ Use `{ctx.prefix}automod ignore show` to view the ignore list.")) + + @ignore.command(name="role", help="Add a role to the whitelist.") + @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 ignore_role(self, ctx, role: discord.Role): + guild_id = ctx.guild.id + if ctx.author != ctx.guild.owner and ctx.author.top_role.position < ctx.guild.me.top_role.position: + await ctx.send(view=CV2(f"{CROSS} Access Denied", "Your top role must be at the **same** position or **higher** than my top role.")) + return + + if not await self.is_automod_enabled(guild_id): + await ctx.send(view=CV2(f"Automod Settings for {ctx.guild.name}", f"Uhh, looks like your server hasn\'t enabled Automoderation.\n\nCurrent Status: {DISABLE} Disabled\nTo Enable use `{ctx.prefix}automod enable`")) + return + + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT 1 FROM automod_ignored WHERE guild_id = ? AND type = 'role' AND id = ?", (guild_id, role.id)) + + if await cursor.fetchone() is not None: + await ctx.send(view=CV2("__Role Already Whitelisted!__", f"{CROSS} The role {role.mention} is already in the ignore list.\n\n➜ Use **{ctx.prefix}automod unignore role {role.mention}** to remove it.")) + return + + count_cursor = await db.execute("SELECT COUNT(*) FROM automod_ignored WHERE guild_id = ? AND type = 'role'", (guild_id,)) + count = await count_cursor.fetchone() + + + if count[0] >= 10: + await ctx.send("You can only ignore up to 10 roles.") + return + + await db.execute("INSERT OR REPLACE INTO automod_ignored (guild_id, type, id) VALUES (?, 'role', ?)", (guild_id, role.id)) + await db.commit() + + if await self.is_anti_nsfw_enabled(guild_id): + try: + rules = await ctx.guild.fetch_automod_rules() + for rule in rules: + if rule.name == "Anti NSFW Links": + exempt_roles = list(rule.exempt_roles) + exempt_roles.append(role) + await rule.edit( + exempt_roles=exempt_roles, + reason="Role exempted from Anti NSFW Links via automod ignore command" + ) + break + except discord.HTTPException: + pass + + + await ctx.send(view=CV2(f"{TICK} Role Whitelisted", f"The role {role.mention} has been added to the ignore list \n\n➜ Use `{ctx.prefix}automod ignore show` to view the ignore list.")) + + @ignore.command(name="show", aliases=["view", "list", "config"], help="Show the whitelisted roles and channels.") + @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 ignore_show(self, ctx): + guild_id = ctx.guild.id + if ctx.author != ctx.guild.owner and ctx.author.top_role.position < ctx.guild.me.top_role.position: + await ctx.send(view=CV2(f"{CROSS} Access Denied", "Your top role must be at the **same** position or **higher** than my top role.")) + return + + if not await self.is_automod_enabled(guild_id): + await ctx.send(view=CV2(f"Automod Settings for {ctx.guild.name}", f"Uhh, looks like your server hasn\'t enabled Automoderation.\n\nCurrent Status: {DISABLE} Disabled\nTo Enable use `{ctx.prefix}automod enable`")) + return + + + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT type, id FROM automod_ignored WHERE guild_id = ?", (guild_id,)) + ignored_items = await cursor.fetchall() + + if not ignored_items: + await ctx.reply("No ignored channels or roles found.") + return + + ignored_channels = [] + ignored_roles = [] + + for item_type, item_id in ignored_items: + if item_type == "channel": + channel = ctx.guild.get_channel(item_id) + if channel: + ignored_channels.append(f"{channel.mention} (ID: {channel.id})") + else: + ignored_channels.append(f"Deleted Channel (ID: {item_id})") + elif item_type == "role": + role = ctx.guild.get_role(item_id) + if role: + ignored_roles.append(f"{role.mention} (ID: {role.id})") + else: + ignored_roles.append(f"Deleted Role (ID: {item_id})") + + ch_str = "\n".join(ignored_channels) if ignored_channels else "None" + rl_str = "\n".join(ignored_roles) if ignored_roles else "None" + await ctx.send(view=CV2("Ignored Channels & Roles for Automod", f"__Ignored Channels:__\n{ch_str}", f"__Ignored Roles:__\n{rl_str}")) + + + @ignore.command(name="reset", help="Reset the whitelist.") + @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 ignore_reset(self, ctx): + guild_id = ctx.guild.id + if ctx.author != ctx.guild.owner and ctx.author.top_role.position < ctx.guild.me.top_role.position: + await ctx.send(view=CV2(f"{CROSS} Access Denied", "Your top role must be at the **same** position or **higher** than my top role.")) + return + + if not await self.is_automod_enabled(guild_id): + await ctx.send(view=CV2(f"Automod Settings for {ctx.guild.name}", f"Uhh, looks like your server hasn\'t enabled Automoderation.\n\nCurrent Status: {DISABLE} Disabled\nTo Enable use `{ctx.prefix}automod enable`")) + return + + async with aiosqlite.connect("db/automod.db") as db: + await db.execute("DELETE FROM automod_ignored WHERE guild_id = ?", (guild_id,)) + await db.commit() + await ctx.send(view=CV2(f"Automod Settings for {ctx.guild.name}", f"** {TICK} | All ignored channels and roles have been reset!**\n\nTo view current Automod settings use `{ctx.prefix}automod config`")) + + + @automod.group(name="unignore", aliases=["unwhitelist", "unwl"], invoke_without_command=True, help="Remove channels and roles from the whitelist.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + async def unignore(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @unignore.command(name="channel", help="Remove a channel from the whitelist.") + @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 unignore_channel(self, ctx, channel: discord.TextChannel): + guild_id = ctx.guild.id + if ctx.author != ctx.guild.owner and ctx.author.top_role.position < ctx.guild.me.top_role.position: + await ctx.send(view=CV2(f"{CROSS} Access Denied", "Your top role must be at the **same** position or **higher** than my top role.")) + return + + if not await self.is_automod_enabled(guild_id): + await ctx.send(view=CV2(f"Automod Settings for {ctx.guild.name}", f"Uhh, looks like your server hasn\'t enabled Automoderation.\n\nCurrent Status: {DISABLE} Disabled\nTo Enable use `{ctx.prefix}automod enable`")) + return + + if await self.is_anti_nsfw_enabled(guild_id): + try: + rules = await ctx.guild.fetch_automod_rules() + for rule in rules: + if rule.name == "Anti NSFW Links": + exempt_channels = list(rule.exempt_channels) + exempt_channels = [ch for ch in exempt_channels if ch.id != channel.id] + await rule.edit( + exempt_channels=exempt_channels, + reason="Channel removed from Anti NSFW Links exemption via automod unignore command" + ) + break + except discord.HTTPException: + pass + + async with aiosqlite.connect("db/automod.db") as db: + result = await db.execute("DELETE FROM automod_ignored WHERE guild_id = ? AND type = 'channel' AND id = ?", (guild_id, channel.id)) + await db.commit() + + if result.rowcount > 0: + await ctx.send(view=CV2(f"{TICK} Success", f"{channel.mention} has been removed from the automod ignore list.")) + else: + await ctx.send(view=CV2(f"{CROSS} Error", f"{channel.mention} is not in the automod ignore list.")) + + + @unignore.command(name="role", help="Remove a role from the whitelist.") + @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 unignore_role(self, ctx, role: discord.Role): + guild_id = ctx.guild.id + if ctx.author != ctx.guild.owner and ctx.author.top_role.position < ctx.guild.me.top_role.position: + await ctx.send(view=CV2(f"{CROSS} Access Denied", "Your top role must be at the **same** position or **higher** than my top role.")) + return + + if not await self.is_automod_enabled(guild_id): + await ctx.send(view=CV2(f"Automod Settings for {ctx.guild.name}", f"Uhh, looks like your server hasn\'t enabled Automoderation.\n\nCurrent Status: {DISABLE} Disabled\nTo Enable use `{ctx.prefix}automod enable`")) + return + + if await self.is_anti_nsfw_enabled(guild_id): + try: + rules = await ctx.guild.fetch_automod_rules() + for rule in rules: + if rule.name == "Anti NSFW Links": + exempt_roles = list(rule.exempt_roles) + exempt_roles = [ch for ch in exempt_roles if ch.id != role.id] + await rule.edit( + exempt_roles=exempt_roles, + reason="Role removed from Anti NSFW Links exemption via automod unignore command" + ) + break + except discord.HTTPException: + pass + + + async with aiosqlite.connect("db/automod.db") as db: + result = await db.execute("DELETE FROM automod_ignored WHERE guild_id = ? AND type = 'role' AND id = ?", (guild_id, role.id)) + await db.commit() + + if result.rowcount > 0: + await ctx.send(view=CV2(f"{TICK} Success", f"{role.mention} has been removed from the automod ignore list.")) + else: + await ctx.send(view=CV2(f"{CROSS} Error", f"{role.mention} is not in the automod ignore list.")) + + + @automod.command(name="disable", help="Disable Automod in the server.") + @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 disable(self, ctx): + guild_id = ctx.guild.id + if ctx.author != ctx.guild.owner and ctx.author.top_role.position < ctx.guild.me.top_role.position: + await ctx.send(view=CV2(f"{CROSS} Access Denied", "Your top role must be at the **same** position or **higher** than my top role.")) + return + + if not await self.is_automod_enabled(guild_id): + await ctx.send(view=CV2(f"Automod Settings for {ctx.guild.name}", f"Uhh, looks like your server hasn\'t enabled Automoderation.\n\nCurrent Status: {DISABLE} Disabled\nTo Enable use `{ctx.prefix}automod enable`")) + return + + view = ConfirmDisable(ctx.author, "Disable Automod Confirmation", "**Are you sure you want to disable Automod?**\n\nThis will remove all custom event settings, punishments, ignored roles/channels, & logging channel data.", "Click 'Yes' to disable Automod or 'No' to cancel.") + message = await ctx.send(view=view) + + await view.wait() + + if view.value is None: + await message.edit(view=CV2("Automod Disable Cancelled", "You took too long to respond. Automod disable process has been canceled.")) + + elif view.value: + + async with aiosqlite.connect("db/automod.db") as db: + await db.execute("DELETE FROM automod WHERE guild_id = ?", (guild_id,)) + await db.execute("DELETE FROM automod_punishments WHERE guild_id = ?", (guild_id,)) + await db.execute("DELETE FROM automod_ignored WHERE guild_id = ?", (guild_id,)) + await db.execute("DELETE FROM automod_logging WHERE guild_id = ?", (guild_id,)) + await db.commit() + + rules = await ctx.guild.fetch_automod_rules() + for rule in rules: + if rule.name == "Anti NSFW Links": + try: + await rule.delete(reason="Automod disabled - removing Anti NSFW Link rule") + except discord.Forbidden: + pass + except discord.HTTPException: + pass + + + await message.edit(view=CV2(f"{TICK_ALT} Automod Disabled", f"Automod has been successfully disabled for **{ctx.guild.name}.** \nAll settings, punishments, and logs have been removed.\n\nCurrent Status: {DISABLE} Disabled\n➜ To Re-enable use `{ctx.prefix}automod enable`.")) + + + else: + await message.edit(view=CV2("Automod Disable Cancelled", "Automod disable process has been canceled.")) + + + + @automod.command(name="config", aliases=["settings", "show", "view"], help="View Automod settings.") + @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 config(self, ctx): + guild_id = ctx.guild.id + if ctx.author != ctx.guild.owner and ctx.author.top_role.position < ctx.guild.me.top_role.position: + await ctx.send(view=CV2(f"{CROSS} Access Denied", "Your top role must be at the **same** position or **higher** than my top role.")) + return + + if not await self.is_automod_enabled(guild_id): + await ctx.send(view=CV2(f"Automod Settings for {ctx.guild.name}", f"Uhh, looks like your server hasn\'t enabled Automoderation.\n\nCurrent Status: {DISABLE} Disabled\nTo Enable use `{ctx.prefix}automod enable`")) + return + + current_punishments = await self.get_current_punishments(guild_id) + items_str = [] + for event, punishment in current_punishments: + items_str.append(f"**{event}**: {punishment or 'None'}") + if await self.is_anti_nsfw_enabled(guild_id): + items_str.append("**Anti NSFW Links**: Block Message") + async with aiosqlite.connect("db/automod.db") as db: + cursor = await db.execute("SELECT log_channel FROM automod_logging WHERE guild_id = ?", (guild_id,)) + log_channel_id = await cursor.fetchone() + if log_channel_id and log_channel_id[0]: + log_channel = ctx.guild.get_channel(log_channel_id[0]) + if log_channel: + items_str.append(f"**Logging Channel**: {log_channel.mention}") + else: + items_str.append("**Logging Channel**: Deleted Channel") + else: + items_str.append("**Logging Channel**: No logging channel") + + await ctx.send(view=CV2(f"Enabled Automod Events & their punishment type for {ctx.guild.name}", "\n".join(items_str), "Manage punishment type for events by executing `automod punishment` command.")) + + + @automod.command(name="logging", help="Set the logging channel for Automod events.") + @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 logging(self, ctx, channel: discord.TextChannel): + guild_id = ctx.guild.id + if ctx.author != ctx.guild.owner and ctx.author.top_role.position < ctx.guild.me.top_role.position: + await ctx.send(view=CV2(f"{CROSS} Access Denied", "Your top role must be at the **same** position or **higher** than my top role.")) + return + if not await self.is_automod_enabled(guild_id): + await ctx.send(view=CV2(f"Automod Settings for {ctx.guild.name}", f"Uhh, looks like your server hasn\'t enabled Automoderation.\n\nCurrent Status: {DISABLE} Disabled\nTo Enable use `{ctx.prefix}automod enable`")) + return + + async with aiosqlite.connect("db/automod.db") as db: + await db.execute("INSERT OR REPLACE INTO automod_logging (guild_id, log_channel) VALUES (?, ?)", (guild_id, channel.id)) + await db.commit() + await ctx.send(view=CV2(f"Automod Settings for {ctx.guild.name}", f"**{TICK} | Automoderation Logging channel set to {channel.mention}.**\n\n➜ Use `{ctx.prefix}automod config` to view current Automod settings.")) + + + @commands.Cog.listener() + async def on_guild_remove(self, guild): + guild_id = guild.id + + async with aiosqlite.connect("db/automod.db") as db: + await db.execute("DELETE FROM automod WHERE guild_id = ?", (guild_id,)) + await db.execute("DELETE FROM automod_punishments WHERE guild_id = ?", (guild_id,)) + await db.execute("DELETE FROM automod_ignored WHERE guild_id = ?", (guild_id,)) + await db.execute("DELETE FROM automod_logging WHERE guild_id = ?", (guild_id,)) + await db.commit() diff --git a/bot/cogs/commands/autoreact.py b/bot/cogs/commands/autoreact.py new file mode 100644 index 0000000..ff32e9e --- /dev/null +++ b/bot/cogs/commands/autoreact.py @@ -0,0 +1,159 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CROSS, ICONS_WARNING, TICK +from discord.ext import commands +from discord.ui import LayoutView, TextDisplay, Separator, Container +import aiosqlite +import re +from utils.Tools import * +from utils.cv2 import CV2, build_container + + + + + +class AutoReaction(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.db_path = 'db/autoreact.db' + self.bot.loop.create_task(self.setup_database()) + + async def setup_database(self): + async with aiosqlite.connect(self.db_path) as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS autoreact ( + guild_id INTEGER, + trigger TEXT, + emojis TEXT + ) + """) + await db.commit() + + async def get_triggers(self, guild_id): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute("SELECT trigger, emojis FROM autoreact WHERE guild_id = ?", (guild_id,)) + return await cursor.fetchall() + + async def trigger_exists(self, guild_id, trigger): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute("SELECT 1 FROM autoreact WHERE guild_id = ? AND trigger = ?", (guild_id, trigger)) + return await cursor.fetchone() + + @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) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def react(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @react.command(name="add", aliases=["set", "create"], help="Adds a trigger and its emojis to the autoreact.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 4, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def add(self, ctx, trigger: str, *, emojis: str): + if len(trigger.split()) > 1: + view = CV2(f"{CROSS} Invalid Trigger", "Triggers can only be one word.") + return await ctx.reply(view=view) + + + emoji_list = re.findall(r"|[\u263a-\U0001f645]", emojis) + if len(emoji_list) > 10: + view = CV2(f"{CROSS} Too Many Emojis", "You can only set up to **10** emojis per trigger.") + return await ctx.reply(view=view) + + triggers = await self.get_triggers(ctx.guild.id) + if len(triggers) >= 10: + view = CV2(f"{ICONS_WARNING} Trigger Limit Reached", "You can only set up to 10 triggers for auto-reactions in this guild.") + return await ctx.reply(view=view) + + if await self.trigger_exists(ctx.guild.id, trigger): + view = CV2(f"{ICONS_WARNING} Trigger Exists", f"The trigger '{trigger}' already exists. Remove it before adding it again.") + return await ctx.reply(view=view) + + async with aiosqlite.connect(self.db_path) as db: + await db.execute("INSERT INTO autoreact (guild_id, trigger, emojis) VALUES (?, ?, ?)", + (ctx.guild.id, trigger, " ".join(emoji_list))) + await db.commit() + + view = CV2(f"{TICK} Trigger Added", f"Successfully added trigger '{trigger}' with emojis {', '.join(emoji_list)}.") + await ctx.reply(view=view) + + @react.command(name="remove", aliases=["clear", "delete"], help="Removes a trigger and its emojis from the autoreact.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 4, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def remove(self, ctx, trigger: str): + if not await self.trigger_exists(ctx.guild.id, trigger): + view = CV2(f"{CROSS} Trigger Not Found", f"The trigger '{trigger}' does not exist.") + return await ctx.reply(view=view) + + async with aiosqlite.connect(self.db_path) as db: + await db.execute("DELETE FROM autoreact WHERE guild_id = ? AND trigger = ?", (ctx.guild.id, trigger)) + await db.commit() + + view = CV2(f"{TICK} Trigger Removed", f"Successfully removed trigger '{trigger}'.") + await ctx.reply(view=view) + + @react.command(name="list", aliases=["show", "config"], help="Lists all the triggers and their emojis in the autoreact module.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 4, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def list(self, ctx): + triggers = await self.get_triggers(ctx.guild.id) + if not triggers: + view = CV2("No Triggers Set", "There are no auto-reaction triggers set in this guild.") + return await ctx.reply(view=view) + + trigger_list = "\n".join([f"**{t[0]}:** {t[1]}" for t in triggers]) + view = CV2("Auto-Reaction Triggers", trigger_list) + await ctx.reply(view=view) + + @react.command(name="reset", help="Resets all the triggers and their emojis in the autoreact module.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 4, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def reset(self, ctx): + triggers = await self.get_triggers(ctx.guild.id) + if not triggers: + view = CV2(f"{CROSS} No Triggers Set", "There are no auto-reaction triggers set to reset.") + return await ctx.reply(view=view) + + async with aiosqlite.connect(self.db_path) as db: + await db.execute("DELETE FROM autoreact WHERE guild_id = ?", (ctx.guild.id,)) + await db.commit() + + view = CV2(f"{TICK} All Triggers Reset", "Successfully removed all auto-reaction triggers.") + await ctx.reply(view=view) + +async def setup(bot): + await bot.add_cog(AutoReaction(bot)) diff --git a/bot/cogs/commands/autoresponder.py b/bot/cogs/commands/autoresponder.py new file mode 100644 index 0000000..4c64b72 --- /dev/null +++ b/bot/cogs/commands/autoresponder.py @@ -0,0 +1,149 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CROSS, TICK +from discord.ext import commands +from discord.ui import LayoutView, TextDisplay, Separator, Container +import aiosqlite +import os +from utils.Tools import * +from utils.cv2 import CV2, build_container + + + + + +DB_PATH = "db/autoresponder.db" + +class AutoResponder(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.bot.loop.create_task(self.initialize_db()) + + async def initialize_db(self): + if not os.path.exists(os.path.dirname(DB_PATH)): + os.makedirs(os.path.dirname(DB_PATH)) + async with aiosqlite.connect(DB_PATH) as db: + await db.execute(''' + CREATE TABLE IF NOT EXISTS autoresponses ( + guild_id INTEGER, + name TEXT, + message TEXT, + PRIMARY KEY (guild_id, name) + ) + ''') + await db.commit() + + @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) + async def _ar(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @_ar.command(name="create", help="Create a new autoresponder.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def _create(self, ctx, name, *, message): + name_lower = name.lower() + async with aiosqlite.connect(DB_PATH) as db: + async with db.execute("SELECT COUNT(*) FROM autoresponses WHERE guild_id = ?", (ctx.guild.id,)) as cursor: + count = (await cursor.fetchone())[0] + if count >= 20: + view = CV2(f"{CROSS} Error!", f"You can't add more than 20 autoresponses in {ctx.guild.name}") + return await ctx.reply(view=view) + + async with db.execute("SELECT 1 FROM autoresponses WHERE guild_id = ? AND LOWER(name) = ?", (ctx.guild.id, name_lower)) as cursor: + if await cursor.fetchone(): + view = CV2(f"{CROSS} Error!", f"The autoresponse with the name `{name}` already exists in {ctx.guild.name}") + return await ctx.reply(view=view) + + await db.execute("INSERT INTO autoresponses (guild_id, name, message) VALUES (?, ?, ?)", (ctx.guild.id, name_lower, message)) + await db.commit() + view = CV2(f"{TICK} Success", f"Created autoresponder `{name}` in {ctx.guild.name}") + await ctx.reply(view=view) + + @_ar.command(name="delete", help="Delete an existing autoresponder.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def _delete(self, ctx, name): + name_lower = name.lower() + async with aiosqlite.connect(DB_PATH) as db: + async with db.execute("SELECT 1 FROM autoresponses WHERE guild_id = ? AND LOWER(name) = ?", (ctx.guild.id, name_lower)) as cursor: + if not await cursor.fetchone(): + view = CV2(f"{CROSS} Error!", f"No autoresponder found with the name `{name}` in {ctx.guild.name}") + return await ctx.reply(view=view) + + await db.execute("DELETE FROM autoresponses WHERE guild_id = ? AND LOWER(name) = ?", (ctx.guild.id, name_lower)) + await db.commit() + view = CV2(f"{TICK} Success", f"Deleted autoresponder `{name}` in {ctx.guild.name}") + await ctx.reply(view=view) + + @_ar.command(name="edit", help="Edit an existing autoresponder.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def _edit(self, ctx, name, *, message): + name_lower = name.lower() + async with aiosqlite.connect(DB_PATH) as db: + async with db.execute("SELECT 1 FROM autoresponses WHERE guild_id = ? AND LOWER(name) = ?", (ctx.guild.id, name_lower)) as cursor: + if not await cursor.fetchone(): + view = CV2(f"{CROSS} Error!", f"No autoresponder found with the name `{name}` in {ctx.guild.name}") + return await ctx.reply(view=view) + + await db.execute("UPDATE autoresponses SET message = ? WHERE guild_id = ? AND LOWER(name) = ?", (message, ctx.guild.id, name_lower)) + await db.commit() + view = CV2(f"{TICK} Success", f"Edited autoresponder `{name}` in {ctx.guild.name}") + await ctx.reply(view=view) + + @_ar.command(name="config", help="List all autoresponders in the server.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def _config(self, ctx): + async with aiosqlite.connect(DB_PATH) as db: + async with db.execute("SELECT name FROM autoresponses WHERE guild_id = ?", (ctx.guild.id,)) as cursor: + autoresponses = await cursor.fetchall() + + if not autoresponses: + view = CV2("No Autoresponders", f"There are no autoresponders in {ctx.guild.name}") + return await ctx.reply(view=view) + + ar_list = "\n".join([f"**[{i}]** {name}" for i, (name,) in enumerate(autoresponses, start=1)]) + view = CV2(f"Autoresponders in {ctx.guild.name}", ar_list) + await ctx.send(view=view) + + @commands.Cog.listener() + async def on_message(self, message): + if message.author == self.bot.user: + return + + async with aiosqlite.connect(DB_PATH) as db: + async with db.execute("SELECT message FROM autoresponses WHERE guild_id = ? AND LOWER(name) = ?", (message.guild.id, message.content.lower())) as cursor: + row = await cursor.fetchone() + + if row: + await message.channel.send(row[0]) + +async def setup(bot): + await bot.add_cog(AutoResponder(bot)) diff --git a/bot/cogs/commands/autorole.py b/bot/cogs/commands/autorole.py new file mode 100644 index 0000000..0d9acce --- /dev/null +++ b/bot/cogs/commands/autorole.py @@ -0,0 +1,354 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations +import discord +from utils.emoji import CROSS, ICONS_WARNING, TICK +import aiosqlite +import logging +from discord.ext import commands +from typing import List, Dict +from discord.ui import LayoutView, TextDisplay, Separator, Container +from utils.Tools import * +from utils.cv2 import CV2, build_container +from utils.config import OWNER_IDS +from utils.safe_parse import parse_role_id_list, dump_role_id_list + + + +logging.basicConfig( + level=logging.INFO, + format="\x1b[38;5;197m[\x1b[0m%(asctime)s\x1b[38;5;197m]\x1b[0m -> \x1b[38;5;197m%(message)s\x1b[0m", + datefmt="%H:%M:%S", +) + +DATABASE_PATH = 'db/autorole.db' + +class BasicView(discord.ui.View): + def __init__(self, ctx: commands.Context, timeout=60): + super().__init__(timeout=timeout) + self.ctx = ctx + + async def interaction_check(self, interaction: discord.Interaction): + if interaction.user.id != self.ctx.author.id and interaction.user.id not in OWNER_IDS: + await interaction.response.send_message("Uh oh! That message doesn't belong to you.\nYou must run this command to interact with it.", ephemeral=True) + return False + return True + + +class AutoRole(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.bot.loop.create_task(self.create_table()) + self.color = 0xFF0000 + + async def create_table(self): + async with aiosqlite.connect(DATABASE_PATH) as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS autorole ( + guild_id INTEGER PRIMARY KEY, + bots TEXT NOT NULL, + humans TEXT NOT NULL + ) + """) + await db.commit() + + async def get_autorole(self, guild_id: int) -> Dict[str, List[int]]: + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute("SELECT bots, humans FROM autorole WHERE guild_id = ?", (guild_id,)) as cursor: + row = await cursor.fetchone() + if row: + bots, humans = row + + bots = [int(role_id) for role_id in bots.replace('[', '').replace(']', '').replace(' ', '').split(',') if role_id] + humans = [int(role_id) for role_id in humans.replace('[', '').replace(']', '').replace(' ', '').split(',') if role_id] + + return {"bots": bots, "humans": humans} + else: + return {"bots": [], "humans": []} + + + + async def update_autorole(self, guild_id: int, data: Dict[str, List[int]]): + async with aiosqlite.connect(DATABASE_PATH) as db: + bots = ','.join(map(str, data['bots'])) + humans = ','.join(map(str, data['humans'])) + + await db.execute("INSERT OR REPLACE INTO autorole (guild_id, bots, humans) VALUES (?, ?, ?)", + (guild_id, bots, humans)) + await db.commit() + + + + + @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() + @commands.has_permissions(administrator=True) + async def _autorole(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + + @_autorole.command(name="config", help="Shows the current autorole configuration") + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + async def _ar_config(self, ctx): + + data = await self.get_autorole(ctx.guild.id) + if data: + fetched_humans = [ctx.guild.get_role(role_id) for role_id in data["humans"] if ctx.guild.get_role(role_id)] + fetched_bots = [ctx.guild.get_role(role_id) for role_id in data["bots"] if ctx.guild.get_role(role_id)] + + hums = "\n".join(role.mention for role in fetched_humans) or "None" + bos = "\n".join(role.mention for role in fetched_bots) or "None" + + view = CV2( + f"Autorole Configuration for {ctx.guild.name}", + f"__Humans__\n{hums}", + f"__Bots__\n{bos}" + ) + await ctx.send(view=view) + else: + view = CV2("Autorole Configuration", "No autorole configuration found in this Guild.") + await ctx.reply(view=view) + + + + @_autorole.group(name="reset", help="Clear autorole config in the Guild") + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + async def _autorole_reset(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @_autorole_reset.command(name="humans", help="Clear autorole configuration for humans") + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + async def _autorole_humans_reset(self, ctx): + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute("SELECT humans FROM autorole WHERE guild_id = ?", (ctx.guild.id,)) as cursor: + data = await cursor.fetchone() + + if data and data[0]: + async with aiosqlite.connect(DATABASE_PATH) as db: + await db.execute("UPDATE autorole SET humans = ? WHERE guild_id = ?", ('[]', ctx.guild.id)) + await db.commit() + view = CV2(f"{TICK} Success", "Cleared all human autoroles in this Guild.") + else: + view = CV2(f"{CROSS} Error", "No Autoroles set for humans in this Guild.") + + await ctx.reply(view=view) + + @_autorole_reset.command(name="bots", help="Clear autorole configuration for bots") + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + async def _autorole_bots_reset(self, ctx): + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute("SELECT bots FROM autorole WHERE guild_id = ?", (ctx.guild.id,)) as cursor: + data = await cursor.fetchone() + + if data and data[0]: + async with aiosqlite.connect(DATABASE_PATH) as db: + await db.execute("UPDATE autorole SET bots = ? WHERE guild_id = ?", ('[]', ctx.guild.id)) + await db.commit() + view = CV2(f"{TICK} Success", "Cleared all bot autoroles in this Guild.") + else: + view = CV2(f"{CROSS} Error", "No Autoroles set for Bots in this Guild.") + + await ctx.reply(view=view) + + @_autorole_reset.command(name="all", help="Clear all autorole configuration in the Guild") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def _autorole_reset_all(self, ctx): + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute("SELECT humans, bots FROM autorole WHERE guild_id = ?", (ctx.guild.id,)) as cursor: + data = await cursor.fetchone() + + if data and (data[0] or data[1]): + async with aiosqlite.connect(DATABASE_PATH) as db: + await db.execute("UPDATE autorole SET humans = ?, bots = ? WHERE guild_id = ?", ('[]', '[]', ctx.guild.id)) + await db.commit() + view = CV2(f"{TICK} Success", "Cleared all autoroles in this Gudild.") + else: + view = CV2(f"{CROSS} Error", "No Autoroles set in this Guild.") + + await ctx.reply(view=view) + + @_autorole.group(name="humans", help="Setup autoroles for human") + @blacklist_check() + @ignore_check() + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def _autorole_humans(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @_autorole_humans.command(name="add", help="Add role to list of human Autoroles.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def _autorole_humans_add(self, ctx, *, role: discord.Role): + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute("SELECT humans FROM autorole WHERE guild_id = ?", (ctx.guild.id,)) as cursor: + data = await cursor.fetchone() + + if data: + humans = parse_role_id_list(data[0]) + if role.id in humans: + view = CV2(f"{ICONS_WARNING} Access Denied", f"{role.mention} is already in human autoroles.") + elif len(humans) >= 10: + view = CV2(f"{ICONS_WARNING} Access Denied", "You can only add upto 10 human autoroles.") + else: + humans.append(role.id) + async with aiosqlite.connect(DATABASE_PATH) as db: + await db.execute("UPDATE autorole SET humans = ? WHERE guild_id = ?", (dump_role_id_list(humans), ctx.guild.id)) + await db.commit() + view = CV2(f"{TICK} Success", f"{role.mention} has been added to human autoroles.") + else: + humans = [role.id] + async with aiosqlite.connect(DATABASE_PATH) as db: + await db.execute("INSERT INTO autorole (guild_id, humans, bots) VALUES (?, ?, ?)", (ctx.guild.id, dump_role_id_list(humans), '[]')) + await db.commit() + view = CV2(f"{TICK} Success", f"{role.mention} has been added to human autoroles.") + + await ctx.reply(view=view) + + @_autorole_humans.command(name="remove", help="Remove a role from human Autoroles.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def _autorole_humans_remove(self, ctx, *, role: discord.Role): + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute("SELECT humans FROM autorole WHERE guild_id = ?", (ctx.guild.id,)) as cursor: + data = await cursor.fetchone() + + if data: + humans = parse_role_id_list(data[0]) + if role.id not in humans: + view = CV2(f"{CROSS} Error", f"{role.mention} is not in human autoroles.") + else: + humans.remove(role.id) + async with aiosqlite.connect(DATABASE_PATH) as db: + await db.execute("UPDATE autorole SET humans = ? WHERE guild_id = ?", (dump_role_id_list(humans), ctx.guild.id)) + await db.commit() + view = CV2(f"{TICK} Success", f"{role.mention} has been removed from human autoroles.") + else: + view = CV2(f"{CROSS} Error", "No Autoroles set in this guild for humans.") + + await ctx.reply(view=view) + + @_autorole.group(name="bots", help="Setup autoroles for bots") + @blacklist_check() + @ignore_check() + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def _autorole_bots(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @_autorole_bots.command(name="add", help="Add role to bot Autoroles.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def _autorole_bots_add(self, ctx, *, role: discord.Role): + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute("SELECT bots FROM autorole WHERE guild_id = ?", (ctx.guild.id,)) as cursor: + data = await cursor.fetchone() + + if data: + bots = parse_role_id_list(data[0]) + if role.id in bots: + view = CV2(f"{ICONS_WARNING} Access Denied", f"{role.mention} is already in bot autoroles.") + elif len(bots) >= 10: + view = CV2(f"{ICONS_WARNING} Access Denied", "You can only add upto 10 bot autoroles") + else: + bots.append(role.id) + async with aiosqlite.connect(DATABASE_PATH) as db: + await db.execute("UPDATE autorole SET bots = ? WHERE guild_id = ?", (dump_role_id_list(bots), ctx.guild.id)) + await db.commit() + view = CV2(f"{TICK} Success", f"{role.mention} has been added to bot autoroles.") + else: + bots = [role.id] + async with aiosqlite.connect(DATABASE_PATH) as db: + await db.execute("INSERT INTO autorole (guild_id, humans, bots) VALUES (?, ?, ?)", (ctx.guild.id, '[]', dump_role_id_list(bots))) + await db.commit() + view = CV2(f"{TICK} Success", f"{role.mention} has been added to bot autoroles.") + + await ctx.reply(view=view) + + @_autorole_bots.command(name="remove", help="Remove a role from bot Autoroles.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def _autorole_bots_remove(self, ctx, *, role: discord.Role): + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute("SELECT bots FROM autorole WHERE guild_id = ?", (ctx.guild.id,)) as cursor: + data = await cursor.fetchone() + + if data: + bots = parse_role_id_list(data[0]) + if role.id not in bots: + view = CV2(f"{CROSS} Error", f"{role.mention} is not in bot autoroles.") + else: + bots.remove(role.id) + async with aiosqlite.connect(DATABASE_PATH) as db: + await db.execute("UPDATE autorole SET bots = ? WHERE guild_id = ?", (dump_role_id_list(bots), ctx.guild.id)) + await db.commit() + view = CV2(f"{TICK} Success", f"{role.mention} has been removed from bot autoroles.") + else: + view = CV2(f"{CROSS} Error", "No Autoroles set in this guild for bots.") + + await ctx.reply(view=view) + + diff --git a/bot/cogs/commands/blackjack.py b/bot/cogs/commands/blackjack.py new file mode 100644 index 0000000..293232f --- /dev/null +++ b/bot/cogs/commands/blackjack.py @@ -0,0 +1,243 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import os +import random +from typing import List, Tuple, Union +from PIL import Image +from utils.Tools import * +from utils.cv2 import CV2, build_container +from discord.ui import LayoutView, TextDisplay, Separator, Container + +class CV2(LayoutView): + def __init__(self, title, *sections): + super().__init__(timeout=None) + items = [TextDisplay(f"**{title}**")] + for s in sections: + if s: + items.append(Separator(visible=True)) + items.append(TextDisplay(str(s))) + self.add_item(build_container(*items)) + +CARDS_PATH = 'data/cards/' + +class Card: + suits = ["clubs", "diamonds", "hearts", "spades"] + + def __init__(self, suit: str, value: int, down=False): + self.suit = suit + self.value = value + self.down = down + self.symbol = self.name[0].upper() + + @property + def name(self) -> str: + if self.value <= 10: + return str(self.value) + else: + return { + 11: 'jack', + 12: 'queen', + 13: 'king', + 14: 'ace', + }[self.value] + + @property + def image(self): + return ( + f"{self.symbol if self.name != '10' else '10'}" \ + f"{self.suit[0].upper()}.png" \ + if not self.down else "red_back.png" + ) + + def flip(self): + self.down = not self.down + return self + + def __str__(self) -> str: + return f'{self.name.title()} of {self.suit.title()}' + + def __repr__(self) -> str: + return str(self) + + +class Blackjack(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @staticmethod + def hand_to_images(hand: List[Card]) -> List[Image.Image]: + return [Image.open(os.path.join(CARDS_PATH, card.image)) for card in hand] + + @staticmethod + def center(*hands: Tuple[Image.Image]) -> Image.Image: + bg: Image.Image = Image.open(os.path.join(CARDS_PATH, 'table.png')) + bg_center_x = bg.size[0] // 2 + bg_center_y = bg.size[1] // 2 + + img_w = hands[0][0].size[0] + img_h = hands[0][0].size[1] + + start_y = bg_center_y - (((len(hands) * img_h) + ((len(hands) - 1) * 15)) // 2) + + for hand in hands: + start_x = bg_center_x - (((len(hand) * img_w) + ((len(hand) - 1) * 10)) // 2) + for card in hand: + bg.alpha_composite(card, (start_x, start_y)) + start_x += img_w + 10 + start_y += img_h + 15 + + return bg + + def output(self, name, *hands: Tuple[List[Card]]) -> None: + self.center(*map(self.hand_to_images, hands)).save(f'data/{name}.png') + + @staticmethod + def calc_hand(hand: List[Card]) -> int: + non_aces = [c for c in hand if c.symbol != 'A'] + aces = [c for c in hand if c.symbol == 'A'] + total_sum = 0 + for card in non_aces: + if not card.down: + if card.symbol in 'JQK': + total_sum += 10 + else: + total_sum += card.value + for card in aces: + if not card.down: + if total_sum <= 10: + total_sum += 11 + else: + total_sum += 1 + return total_sum + + @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) + async def blackjack(self, ctx: commands.Context): + try: + deck = [Card(suit, num) for num in range(2, 15) for suit in Card.suits] + random.shuffle(deck) + + player_hand: List[Card] = [] + dealer_hand: List[Card] = [] + + player_hand.append(deck.pop()) + dealer_hand.append(deck.pop()) + player_hand.append(deck.pop()) + dealer_hand.append(deck.pop()) + + dealer_hand[1] = dealer_hand[1].flip() + + player_score = self.calc_hand(player_hand) + dealer_score = self.calc_hand(dealer_hand) + + async def out_table(**kwargs) -> discord.Message: + self.output(ctx.author.id, dealer_hand, player_hand) + file = discord.File(f"data/{ctx.author.id}.png", filename=f"{ctx.author.id}.png") + view = CV2(kwargs.get('title', 'Blackjack'), kwargs.get('description', '')) + msg: discord.Message = await ctx.send(file=file, view=view) + return msg + + def check(reaction: discord.Reaction, user: Union[discord.Member, discord.User]) -> bool: + return all(( + str(reaction.emoji) in ("🇸", "🇭"), + user == ctx.author, + user != self.bot.user, + reaction.message == msg + )) + + standing = False + + while True: + player_score = self.calc_hand(player_hand) + dealer_score = self.calc_hand(dealer_hand) + + if player_score == 21: + result = ("Blackjack!", 'won') + break + + elif player_score > 21: + result = ("Player busts", 'lost') + break + + msg = await out_table( + title="Your Turn", + description=f"Your hand: {player_score}\nDealer's hand: {dealer_score}" + ) + + await msg.add_reaction("🇭") + await msg.add_reaction("🇸") + + try: + reaction, _ = await self.bot.wait_for('reaction_add', timeout=60, check=check) + except asyncio.TimeoutError: + await msg.delete() + return + + if str(reaction.emoji) == "🇭": + player_hand.append(deck.pop()) + await msg.delete() + continue + + elif str(reaction.emoji) == "🇸": + standing = True + break + + if standing: + dealer_hand[1] = dealer_hand[1].flip() + player_score = self.calc_hand(player_hand) + dealer_score = self.calc_hand(dealer_hand) + + while dealer_score < 17: + dealer_hand.append(deck.pop()) + dealer_score = self.calc_hand(dealer_hand) + + if dealer_score == 21: + result = ('Dealer blackjack', 'lost') + elif dealer_score > 21: + result = ("Dealer busts", 'won') + elif dealer_score == player_score: + result = ("Tie!", 'kept') + elif dealer_score > player_score: + result = ("You lose!", 'lost') + elif dealer_score < player_score: + result = ("You win!", 'won') + + color = ( + discord.Color.red() if result[1] == 'lost' + else discord.Color.green() if result[1] == 'won' + else discord.Color.blue() + ) + try: + await msg.delete() + except: + pass + msg = await out_table( + title=result[0], + color=color, + description=( + f"**You {result[1]}**\nYour hand: {player_score}\n" + + f"Dealer's hand: {dealer_score}" + ) + ) + os.remove(f'data/{ctx.author.id}.png') + except Exception as e: + print(e) + + + \ No newline at end of file diff --git a/bot/cogs/commands/blacklist.py b/bot/cogs/commands/blacklist.py new file mode 100644 index 0000000..9cc328f --- /dev/null +++ b/bot/cogs/commands/blacklist.py @@ -0,0 +1,386 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CROSS, TICK +from discord.ext import commands +from discord.ext import menus +import aiosqlite +import os +from utils.Tools import * +from utils.cv2 import CV2, build_container +from typing import Union +from discord.ui import LayoutView, TextDisplay, Separator, Container + +class CV2(LayoutView): + def __init__(self, title, *sections): + super().__init__(timeout=None) + items = [TextDisplay(f"**{title}**")] + for s in sections: + if s: + items.append(Separator(visible=True)) + items.append(TextDisplay(str(s))) + self.add_item(build_container(*items)) +from utils.paginator import Paginator as sonu + + +class BlacklistWordSource(menus.ListPageSource): + def __init__(self, data): + super().__init__(data, per_page=4) + + async def format_page(self, menu, entries): + embed = discord.Embed( + title="Blacklist Word [7]", + description="< > Duty | [ ] Optional", + color=0xFF0000 + ) + for entry in entries: + embed.add_field(name="",value=entry, inline=False) + + embed.set_footer( + text='Users having Administrator can use Blacklisted Word', + icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.webp?size=4096" + ) + return embed + + +DB_PATH = "db/blword.db" + + +async def create_blacklist_table(): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS blacklist ( + guild_id TEXT, + word TEXT, + PRIMARY KEY (guild_id, word) + ) + """) + await db.commit() + + +async def create_bypass_table(): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS bypass ( + guild_id TEXT, + user_id INTEGER, + PRIMARY KEY (guild_id, user_id) + ) + """) + await db.commit() + + +async def create_bypass_roles_table(): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS bypass_roles ( + guild_id TEXT, + role_id INTEGER, + PRIMARY KEY (guild_id, role_id) + ) + """) + await db.commit() + + + + +class Blacklist(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.bot.loop.create_task(create_blacklist_table()) + self.bot.loop.create_task(create_bypass_table()) + self.bot.loop.create_task(create_bypass_roles_table()) + +############ FUNCTIONS ############ + async def is_word_blacklisted(self, guild_id, word): + async with aiosqlite.connect(DB_PATH) as db: + async with db.execute("SELECT * FROM blacklist WHERE guild_id = ? AND word = ?", (guild_id, word)) as cursor: + return await cursor.fetchone() is not None + + + async def add_word_to_blacklist(self, guild_id, word): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute("INSERT INTO blacklist (guild_id, word) VALUES (?, ?)", (guild_id, word)) + await db.commit() + + + async def remove_word_from_blacklist(self, guild_id, word): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute("DELETE FROM blacklist WHERE guild_id = ? AND word = ?", (guild_id, word)) + await db.commit() + + + async def get_blacklisted_words(self, guild_id): + async with aiosqlite.connect(DB_PATH) as db: + async with db.execute("SELECT word FROM blacklist WHERE guild_id = ?", (guild_id,)) as cursor: + return [row[0] async for row in cursor] + + + async def is_user_bypassed(self, guild_id, user_id): + async with aiosqlite.connect(DB_PATH) as db: + async with db.execute("SELECT * FROM bypass WHERE guild_id = ? AND user_id = ?", (guild_id, user_id)) as cursor: + return await cursor.fetchone() is not None + + + async def add_user_to_bypass(self, guild_id, user_id): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute("INSERT INTO bypass (guild_id, user_id) VALUES (?, ?)", (guild_id, user_id)) + await db.commit() + + + async def remove_user_from_bypass(self, guild_id, user_id): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute("DELETE FROM bypass WHERE guild_id = ? AND user_id = ?", (guild_id, user_id)) + await db.commit() + + + async def get_bypassed_users(self, guild_id): + async with aiosqlite.connect(DB_PATH) as db: + async with db.execute("SELECT user_id FROM bypass WHERE guild_id = ?", (guild_id,)) as cursor: + return [row[0] async for row in cursor] + + + async def is_role_bypassed(self, guild_id, role_id): + async with aiosqlite.connect(DB_PATH) as db: + async with db.execute("SELECT * FROM bypass_roles WHERE guild_id = ? AND role_id = ?", (guild_id, role_id)) as cursor: + return await cursor.fetchone() is not None + + + async def add_role_to_bypass(self, guild_id, role_id): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute("INSERT INTO bypass_roles (guild_id, role_id) VALUES (?, ?)", (guild_id, role_id)) + await db.commit() + + + async def remove_role_from_bypass(self, guild_id, role_id): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute("DELETE FROM bypass_roles WHERE guild_id = ? AND role_id = ?", (guild_id, role_id)) + await db.commit() + + + async def get_bypassed_roles(self, guild_id): + async with aiosqlite.connect(DB_PATH) as db: + async with db.execute("SELECT role_id FROM bypass_roles WHERE guild_id = ?", (guild_id,)) as cursor: + return [row[0] async for row in cursor] + + + async def remove_all_words_from_blacklist(self, guild_id): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute("DELETE FROM blacklist WHERE guild_id = ?", (guild_id,)) + await db.commit() + + @commands.Cog.listener() + async def on_message(self, message): + if message.author.bot: + return + + guild_id = str(message.guild.id) + words = await self.get_blacklisted_words(guild_id) + bypassed_users = await self.get_bypassed_users(guild_id) + bypassed_roles = await self.get_bypassed_roles(guild_id) + + if message.author.guild_permissions.administrator or message.author.id in bypassed_users: + return + + for role in message.author.roles: + if role.id in bypassed_roles: + return + + for word in words: + if word in message.content.lower(): + await message.delete() + warning_message = await message.channel.send( + f"{message.author.mention} watch your language, your message contains a blacklisted word!" + ) + await warning_message.delete(delay=3) + break + + @commands.group(name="blacklistword", aliases=["blword"], invoke_without_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def blacklistword(self, ctx): + commands_list = [ + "➜ `blacklistword add ` - Add a word to the blacklist.", + "➜ `blacklistword remove ` - Remove a word from the blacklist.", + "➜ `blacklistword reset` - Clear all blacklisted words for the guild.", + "➜ `blacklistword config` - Show the list of blacklisted words for the guild.", + "➜ `blacklistword bypass add /` - Add a role/user to the bypass list.", + "➜ `blacklistword bypass remove /` - Remove a role/user from the bypass list.", + "➜ `blacklistword bypass list` - Show the list of bypassed roles/users." + ] + + paginator = sonu( + source=BlacklistWordSource(commands_list), + ctx=ctx + ) + + await paginator.paginate() + @blacklistword.command(name="add") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def add(self, ctx, word: str): + guild_id = str(ctx.guild.id) + if len(await self.get_blacklisted_words(guild_id)) >= 30: + await ctx.reply("The blacklist is full. Maximum 30 words allowed.") + return + if await self.is_word_blacklisted(guild_id, word.lower()): + await ctx.reply(view=CV2(f"{TICK} Access Denied", f"`{word}` is already in the blacklist.")) + return + + await self.add_word_to_blacklist(guild_id, word.lower()) + await ctx.reply(view=CV2(f"{TICK} Success", f"Added `{word}` to the blacklist.")) + + @blacklistword.command(name="remove") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def remove(self, ctx, word: str): + guild_id = str(ctx.guild.id) + if not await self.is_word_blacklisted(guild_id, word.lower()): + await ctx.reply(view=CV2(f"{CROSS} Error", f"`{word}` is not in the blacklist.")) + return + + await self.remove_word_from_blacklist(guild_id, word.lower()) + await ctx.reply(view=CV2(f"{TICK} Success", f"Removed `{word}` from the blacklist.")) + + @blacklistword.command(name="reset") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def reset(self, ctx): + guild_id = str(ctx.guild.id) + words = await self.get_blacklisted_words(guild_id) + + if not words: + await ctx.reply(view=CV2(f"{CROSS} Error", "No words are currently blacklisted.")) + return + + await self.remove_all_words_from_blacklist(guild_id) + + await ctx.reply(view=CV2(f"{TICK} Success", "Cleared all blacklisted words.")) + + + @blacklistword.command(name="config") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def config(self, ctx): + guild_id = str(ctx.guild.id) + words = await self.get_blacklisted_words(guild_id) + if not words: + await ctx.reply(view=CV2(f"{CROSS} Error", "No words are currently blacklisted.")) + return + + await ctx.reply(view=CV2(f"Blacklisted Words for {ctx.guild.name}", "\n".join(words))) + + @blacklistword.group(name="bypass", invoke_without_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def bypass(self, ctx): + await ctx.send(view=CV2("Bypass User Commands", "➜ `blacklistword bypass add /` - Add a role/user to the bypass list.\n\n➜ `blacklistword bypass remove /` - Remove a role/user from the bypass list.\n\n➜ `blacklistword bypass list` - Show the list of bypassed roles/users.")) + + + @bypass.command(name="add") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def bypass_add(self, ctx, target: Union[discord.Member, discord.Role]): + guild_id = str(ctx.guild.id) + if isinstance(target, discord.Member): + if len(await self.get_bypassed_users(guild_id)) >= 30: + await ctx.reply("The bypass list for users is full. Maximum 30 users allowed.") + return + if await self.is_user_bypassed(guild_id, target.id): + await ctx.reply(view=CV2("Error", f"{CROSS} | `{target}` is already bypassed.")) + return + await self.add_user_to_bypass(guild_id, target.id) + await ctx.reply(view=CV2(f"{TICK} Success", f"Added `{target}` to the bypass list.")) + + elif isinstance(target, discord.Role): + if len(await self.get_bypassed_roles(guild_id)) >= 30: + await ctx.reply("The bypass list for roles is full. Maximum 30 roles allowed.") + return + if await self.is_role_bypassed(guild_id, target.id): + await ctx.reply(view=CV2(f"{CROSS} Error", f"`{target}` is already bypassed.")) + return + await self.add_role_to_bypass(guild_id, target.id) + await ctx.reply(view=CV2(f"{TICK} Success", f"Added `{target}` to the bypass list.")) + + + + @bypass.command(name="remove") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def bypass_remove(self, ctx, target: Union[discord.Member, discord.Role]): + guild_id = str(ctx.guild.id) + if isinstance(target, discord.Member): + if not await self.is_user_bypassed(guild_id, target.id): + await ctx.reply(view=CV2(f"{CROSS} Error", f"`{target}` is not bypassed.")) + return + await self.remove_user_from_bypass(guild_id, target.id) + await ctx.reply(view=CV2(f"{TICK} Success", f"Removed `{target}` from the bypass list.")) + + elif isinstance(target, discord.Role): + if not await self.is_role_bypassed(guild_id, target.id): + await ctx.reply(view=CV2(f"{CROSS} Error", f"`{target}` is not bypassed.")) + return + await self.remove_role_from_bypass(guild_id, target.id) + await ctx.reply(view=CV2(f"{TICK} Success", f"Removed `{target}` from the bypass list.")) + + @bypass.command(name="list") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def bypass_list(self, ctx): + guild_id = str(ctx.guild.id) + users = await self.get_bypassed_users(guild_id) + roles = await self.get_bypassed_roles(guild_id) + + if not users and not roles: + await ctx.send(view=CV2(f"{CROSS} Error", "No users or roles are currently bypassed.")) + return + + bypassed_users = [ctx.guild.get_member(user_id) for user_id in users if ctx.guild.get_member(user_id)] + bypassed_roles = [ctx.guild.get_role(role_id) for role_id in roles if ctx.guild.get_role(role_id)] + sections = [] + if bypassed_users: + sections.append(f"**Users**\n" + ", ".join([user.name for user in bypassed_users])) + if bypassed_roles: + sections.append(f"**Roles**\n" + ", ".join([role.name for role in bypassed_roles])) + await ctx.send(view=CV2(f"Bypassed Users and Roles for {ctx.guild.name}", *sections)) + + + @add.error + @remove.error + @reset.error + @config.error + @bypass_add.error + @bypass_remove.error + async def command_error(self, ctx, error): + if isinstance(error, commands.CommandError): + if not isinstance(error, commands.CommandOnCooldown): + await ctx.reply(view=CV2("Error", f"{CROSS} | An error occurred while processing the command. Make sure you have **Administrator** permission.")) \ No newline at end of file diff --git a/bot/cogs/commands/block.py b/bot/cogs/commands/block.py new file mode 100644 index 0000000..e77a886 --- /dev/null +++ b/bot/cogs/commands/block.py @@ -0,0 +1,192 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CROSS, TICK +from discord.ext import commands +import aiosqlite +from utils import Paginator, DescriptionEmbedPaginator +from discord.ui import LayoutView, TextDisplay, Separator, Container +from utils.cv2 import CV2, build_container + +class CV2(LayoutView): + def __init__(self, title, *sections): + super().__init__(timeout=None) + items = [TextDisplay(f"**{title}**")] + for s in sections: + if s: + items.append(Separator(visible=True)) + items.append(TextDisplay(str(s))) + self.add_item(build_container(*items)) + +class Block(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.bot.loop.create_task(self.set_db()) + + #@commands.Cog.listener() + async def set_db(self): + async with aiosqlite.connect('db/block.db') as db: + await db.execute(''' + CREATE TABLE IF NOT EXISTS user_blacklist ( + user_id INTEGER PRIMARY KEY, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + await db.execute(''' + CREATE TABLE IF NOT EXISTS guild_blacklist ( + guild_id INTEGER PRIMARY KEY, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + await db.commit() + + + + @commands.group(name="blacklist", aliases=["bl"], invoke_without_command=True) + @commands.is_owner() + async def blacklist(self, ctx): + if ctx.subcommand_passed is None: + ctx.command.reset_cooldown(ctx) + await ctx.send_help(ctx.command) + + @blacklist.group(name="user", help="Add/Remove a user to the blacklist.", invoke_without_command=True) + @commands.is_owner() + async def user(self, ctx): + if ctx.subcommand_passed is None: + ctx.command.reset_cooldown(ctx) + await ctx.send_help(ctx.command) + + @user.command(name="add", help="Adds a user to the blacklist.") + @commands.is_owner() + async def add_user(self, ctx, user: discord.User): + async with aiosqlite.connect('db/block.db') as db: + cursor = await db.execute('SELECT user_id FROM user_blacklist WHERE user_id = ?', (user.id,)) + if await cursor.fetchone(): + await ctx.reply(view=CV2("User Already Blacklisted", f"{user.mention} is already blacklisted.")) + else: + await db.execute('INSERT INTO user_blacklist (user_id) VALUES (?)', (user.id,)) + await db.commit() + await ctx.reply(view=CV2(f"{TICK} User Blacklisted", f"{user.mention} has been added to the blacklist.")) + + @user.command(name="remove", help="Remove a user from the blacklist.") + @commands.is_owner() + async def remove_user(self, ctx, user: discord.User): + async with aiosqlite.connect('db/block.db') as db: + cursor = await db.execute('SELECT user_id FROM user_blacklist WHERE user_id = ?', (user.id,)) + if not await cursor.fetchone(): + await ctx.reply(view=CV2(f"{CROSS} User Not Blacklisted", f"{user.mention} is not in the blacklist.")) + else: + await db.execute('DELETE FROM user_blacklist WHERE user_id = ?', (user.id,)) + await db.commit() + await ctx.reply(view=CV2(f"{TICK} User Unblacklisted", f"{user.mention} has been removed from the blacklist.")) + + @user.command(name="show", aliases=["list"], help="Shows all Blacklisted users.") + @commands.is_owner() + async def show_users(self, ctx): + async with aiosqlite.connect('db/block.db') as db: + cursor = await db.execute('SELECT user_id FROM user_blacklist') + rows = await cursor.fetchall() + if not rows: + await ctx.reply(view=CV2(f"{CROSS} No Blacklisted Users", "There are no users in the blacklist.")) + return + + blacklist = [] + for row in rows: + user_id = row[0] + try: + user = await self.bot.fetch_user(user_id) + username = user.name + user_link = f"https://discord.com/users/{user_id}" + #indexx = [""for index, user in enumerate(blacklist)] + + blacklist.append(f"**[{username}]({user_link})** - ({user_id})") + except discord.NotFound: + blacklist.append(f"User ID: {user_id} (User not found)") + entries = [f"{index+1}. {user}" for index, user in enumerate(blacklist)] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + title=f"List of Blacklisted Users - {len(blacklist)}", + description="", + per_page=10, + color=0xFF0000), + ctx=ctx + ) + await paginator.paginate() + + @blacklist.group(name="guild", help="Add/Remove a guild to the blacklist.", invoke_without_command=True) + @commands.is_owner() + async def guild(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @guild.command(name="add", help="Adds a guild to the blacklist.") + @commands.is_owner() + async def add_guild(self, ctx, guild_id: int): + async with aiosqlite.connect('db/block.db') as db: + cursor = await db.execute('SELECT guild_id FROM guild_blacklist WHERE guild_id = ?', (guild_id,)) + if await cursor.fetchone(): + await ctx.reply(view=CV2(f"{CROSS} Guild Already Blacklisted", f"Guild with ID `{guild_id}` is already blacklisted.")) + else: + await db.execute('INSERT INTO guild_blacklist (guild_id) VALUES (?)', (guild_id,)) + await db.commit() + await ctx.reply(view=CV2(f"{TICK} Guild Blacklisted", f"Guild with ID `{guild_id}` has been added to the blacklist.")) + + @guild.command(name="remove", help="Remove a guild from the blacklist.") + @commands.is_owner() + async def remove_guild(self, ctx, guild_id: int): + async with aiosqlite.connect('db/block.db') as db: + cursor = await db.execute('SELECT guild_id FROM guild_blacklist WHERE guild_id = ?', (guild_id,)) + if not await cursor.fetchone(): + await ctx.reply(view=CV2(f"{CROSS} Guild Not Blacklisted", f"Guild with ID `{guild_id}` is not in the blacklist.")) + else: + await db.execute('DELETE FROM guild_blacklist WHERE guild_id = ?', (guild_id,)) + await db.commit() + await ctx.reply(view=CV2(f"{TICK} Guild Unblacklisted", f"Guild with ID `{guild_id}` has been removed from the blacklist.")) + + + @guild.command(name="show", aliases=["list"], help="Shows the list of blacklisted guilds") + @commands.is_owner() + async def show_guilds(self, ctx): + async with aiosqlite.connect('db/block.db') as db: + cursor = await db.execute('SELECT guild_id FROM guild_blacklist') + rows = await cursor.fetchall() + if not rows: + await ctx.reply(view=CV2(f"{CROSS} No Blacklisted Guilds", "There are no guilds in the blacklist.")) + return + + blacklist = [] + for row in rows: + guild_id = row[0] + try: + guild = await self.bot.fetch_guild(guild_id) + guild_name = guild.name + guild_link = f"https://discord.com/guilds/{guild_id}" + blacklist.append(f"[{guild_name}]({guild_link}) - ({guild_id})") + except discord.NotFound: + blacklist.append(f"Guild ID: {guild_id} (Guild not found)") + entries = [f"{index+1}. {guild}" for index, guild in enumerate(blacklist)] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + title=f"List of Blacklisted Guilds - {len(blacklist)}", + description="", + per_page=10, + color=0xFF0000), + ctx=ctx + ) + await paginator.paginate() + + + \ No newline at end of file diff --git a/bot/cogs/commands/booster.py b/bot/cogs/commands/booster.py new file mode 100644 index 0000000..3da9cf0 --- /dev/null +++ b/bot/cogs/commands/booster.py @@ -0,0 +1,634 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations +import discord +from utils.emoji import CROSS, NITRO_BOOST, TICK, TIMER +import asyncio +import logging +import aiosqlite +import json +from discord .ext import commands +from utils .Tools import * +from discord .ext .commands import Context +from discord import app_commands +import time +import datetime +import re +from typing import * +from time import strftime +from core import Cog ,zyrox ,Context +from discord.ui import LayoutView, TextDisplay, Separator, Container +from utils.cv2 import CV2, build_container + +class CV2(LayoutView): + def __init__(self, title, *sections): + super().__init__(timeout=None) + items = [TextDisplay(f"**{title}**")] + for s in sections: + if s: + items.append(Separator(visible=True)) + items.append(TextDisplay(str(s))) + self.add_item(build_container(*items)) + +logging .basicConfig ( +level =logging .INFO , +format ="\x1b[38;5;197m[\x1b[0m%(asctime)s\x1b[38;5;197m]\x1b[0m -> \x1b[38;5;197m%(message)s\x1b[0m", +datefmt ="%H:%M:%S", +) + +class Booster (Cog ): + def __init__ (self ,bot : Zyrox ): + self .bot =bot + self .color =0xFF0000 + self .db_path ="db/boost.db" + self .bot .loop .create_task (self .setup_database ()) + + + self .url_pattern =re .compile ( + r'^(?:http|ftp)s?://' + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' + r'localhost|' + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' + r'(?::\d+)?' + r'(?:/?|[/?]\S+)$',re .IGNORECASE + ) + + + + async def setup_database (self ): + """Initialize boost database tables""" + async with aiosqlite .connect (self .db_path )as db : + await db .execute (""" + CREATE TABLE IF NOT EXISTS boost_config ( + guild_id INTEGER PRIMARY KEY, + config TEXT NOT NULL + ) + """) + await db .commit () + + async def get_boost_config (self ,guild_id :int )->dict : + """Get boost configuration for a guild""" + async with aiosqlite .connect (self .db_path )as db : + async with db .execute ("SELECT config FROM boost_config WHERE guild_id = ?",(guild_id ,))as cursor : + row =await cursor .fetchone () + if row : + return json .loads (row [0 ]) + + + default_config ={ + "boost":{ + "channel":[], + "message":"{user.mention} just boosted {server.name}! 🎉", + "embed":True , + "ping":False , + "image":"", + "thumbnail":"", + "autodel":0 + }, + "boost_roles":{ + "roles":[] + } + } + await self .update_boost_config (guild_id ,default_config ) + return default_config + + async def update_boost_config (self ,guild_id :int ,config :dict ): + """Update boost configuration for a guild""" + async with aiosqlite .connect (self .db_path )as db : + await db .execute ( + "INSERT OR REPLACE INTO boost_config (guild_id, config) VALUES (?, ?)", + (guild_id ,json .dumps (config )) + ) + await db .commit () + + def is_authorized (self ,ctx )->bool : + """Check if user is authorized to use admin commands""" + return ( + ctx .author ==ctx .guild .owner + or ctx .author .guild_permissions .administrator + or ctx .author .top_role .position >=ctx .guild .me .top_role .position + ) + + + def format_boost_message (self ,message :str ,user :discord .Member ,guild :discord .Guild )->str : + """Format boost message with new variable style""" + replacements ={ + + "{server.name}":guild .name , + "{server.id}":str (guild .id ), + "{server.owner}":str (guild .owner ), + "{server.icon}":guild .icon .url if guild .icon else "", + "{server.boost_count}":str (guild .premium_subscription_count ), + "{server.boost_level}":f"Level {guild.premium_tier}", + "{server.member_count}":str (guild .member_count ), + + + "{user.name}":user .display_name , + "{user.mention}":user .mention , + "{user.tag}":str (user ), + "{user.id}":str (user .id ), + "{user.avatar}":user .display_avatar .url , + "{user.created_at}":f"", + "{user.joined_at}":f""if user .joined_at else "Unknown", + "{user.top_role}":user .top_role .name if user .top_role else "None", + "{user.is_booster}":str (bool (user .premium_since )), + "{user.is_mobile}":str (user .is_on_mobile ()), + "{user.boosted_at}":f""if user .premium_since else "Unknown" + } + + + for old ,new in replacements .items (): + message =message .replace (old ,new ) + + return message + + async def send_permission_error (self ,ctx ): + """Send permission error embed""" + await ctx.send(view=CV2("Permission Error", "```diff\n- You must have Administrator permission.\n- Your top role should be above my top role.\n```")) + + @commands .group (name ="boost",aliases =['bst'],invoke_without_command =True ,help ="Boost message configuration commands") + @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 _boost (self ,ctx ): + if ctx .subcommand_passed is None : + await ctx .send_help (ctx .command ) + ctx .command .reset_cooldown (ctx ) + + @_boost .command (name ="thumbnail",help ="Set boost message thumbnail") + @blacklist_check () + @ignore_check () + @commands .cooldown (1 ,2 ,commands .BucketType .user ) + @commands .max_concurrency (1 ,per =commands .BucketType .default ,wait =False ) + @commands .guild_only () + @commands .has_permissions (administrator =True ) + async def _boost_thumbnail (self ,ctx ,thumbnail_url :str ): + if not self .is_authorized (ctx ): + await self .send_permission_error (ctx ) + return + + if not self .url_pattern .match (thumbnail_url ): + await ctx.send(view=CV2("Error", f"{CROSS} Please provide a valid URL.")) + + return + + data =await self .get_boost_config (ctx .guild .id ) + data ["boost"]["thumbnail"]=thumbnail_url + await self .update_boost_config (ctx .guild .id ,data ) + + await ctx.send(view=CV2("Success", f"{TICK} Successfully updated the boost thumbnail URL.")) + + @_boost .command (name ="image",help ="Set boost message image") + @blacklist_check () + @ignore_check () + @commands .cooldown (1 ,2 ,commands .BucketType .user ) + @commands .max_concurrency (1 ,per =commands .BucketType .default ,wait =False ) + @commands .guild_only () + @commands .has_permissions (administrator =True ) + async def _boost_image (self ,ctx ,*,image_url :str ): + if not self .is_authorized (ctx ): + await self .send_permission_error (ctx ) + return + + if not self .url_pattern .match (image_url ): + await ctx.send(view=CV2("Error", f"{CROSS} Please provide a valid URL.")) + + return + + data =await self .get_boost_config (ctx .guild .id ) + data ["boost"]["image"]=image_url + await self .update_boost_config (ctx .guild .id ,data ) + + await ctx.send(view=CV2("Success", f"{TICK} Successfully updated the boost image URL.")) + + @_boost .command (name ="autodel",help ="Set auto-delete timer for boost messages (0 to disable)") + @blacklist_check () + @ignore_check () + @commands .cooldown (1 ,2 ,commands .BucketType .user ) + @commands .max_concurrency (1 ,per =commands .BucketType .default ,wait =False ) + @commands .guild_only () + @commands .has_permissions (administrator =True ) + async def _boost_autodel (self ,ctx ,seconds :int ): + if not self .is_authorized (ctx ): + await self .send_permission_error (ctx ) + return + + if seconds <0 : + await ctx.send(view=CV2("Error", f"{CROSS} Auto-delete timer must be 0 or greater.")) + + return + + data =await self .get_boost_config (ctx .guild .id ) + data ["boost"]["autodel"]=seconds + await self .update_boost_config (ctx .guild .id ,data ) + + description =f"{TICK} Successfully set auto-delete timer to {seconds} seconds." + if seconds ==0 : + description =f"{TICK} Auto-delete has been disabled." + + await ctx.send(view=CV2("Success", description)) + + @_boost .command (name ="message",help ="Set boost message content") + @blacklist_check () + @ignore_check () + @commands .cooldown (1 ,2 ,commands .BucketType .user ) + @commands .max_concurrency (1 ,per =commands .BucketType .default ,wait =False ) + @commands .guild_only () + @commands .has_permissions (administrator =True ) + async def _boost_message (self ,ctx ): + if not self .is_authorized (ctx ): + await self .send_permission_error (ctx ) + return + variables_text = ( + "Send your boost message in this channel now.\n\n**Available Variables:**\n" + "```\n" + "{server.name} - Server name\n" + "{server.id} - Server ID\n" + "{server.owner} - Server owner\n" + "{server.icon} - Server icon URL\n" + "{server.boost_count} - Current boost count\n" + "{server.boost_level} - Boost level (e.g., Level 2)\n" + "{server.member_count} - Total member count\n\n" + "{user.name} - Booster's display name\n" + "{user.mention} - Mention the booster\n" + "{user.tag} - Booster's full tag\n" + "{user.id} - Booster's ID\n" + "{user.avatar} - Booster's avatar URL\n" + "{user.created_at} - When the account was created\n" + "{user.joined_at} - When user joined the server\n" + "{user.top_role} - Booster's top role name\n" + "{user.is_booster} - Whether they're a booster\n" + "{user.is_mobile} - Whether on mobile\n" + "{user.boosted_at} - Boost timestamp\n" + "```\n*You have 60 seconds to respond*" + ) + await ctx.send(view=CV2(f"{TICK} Boost Message Setup", variables_text)) + + def check (m ): + return m .author ==ctx .author and m .channel ==ctx .channel + + try : + message =await self .bot .wait_for ('message',check =check ,timeout =60.0 ) + except asyncio .TimeoutError : + await ctx.send(view=CV2("Timeout", f"{TIMER} Timeout! Please try again.")) + + return + + data =await self .get_boost_config (ctx .guild .id ) + data ["boost"]["message"]=message .content + await self .update_boost_config (ctx .guild .id ,data ) + + await ctx.send(view=CV2("Success", f"{TICK} Successfully updated the boost message.")) + + @_boost .command (name ="embed",help ="Toggle embed formatting for boost messages") + @blacklist_check () + @ignore_check () + @commands .cooldown (1 ,2 ,commands .BucketType .user ) + @commands .max_concurrency (1 ,per =commands .BucketType .default ,wait =False ) + @commands .guild_only () + @commands .has_permissions (administrator =True ) + async def _boost_embed (self ,ctx ): + if not self .is_authorized (ctx ): + await self .send_permission_error (ctx ) + return + + data =await self .get_boost_config (ctx .guild .id ) + data ["boost"]["embed"]=not data ["boost"]["embed"] + await self .update_boost_config (ctx .guild .id ,data ) + + status ="enabled"if data ["boost"]["embed"]else "disabled" + await ctx.send(view=CV2("Success", f"{TICK} Embed formatting has been **{status}**.")) + + @_boost .command (name ="ping",help ="Toggle pinging the booster") + @blacklist_check () + @ignore_check () + @commands .cooldown (1 ,2 ,commands .BucketType .user ) + @commands .max_concurrency (1 ,per =commands .BucketType .default ,wait =False ) + @commands .guild_only () + @commands .has_permissions (administrator =True ) + async def _boost_ping (self ,ctx ): + if not self .is_authorized (ctx ): + await self .send_permission_error (ctx ) + return + + data =await self .get_boost_config (ctx .guild .id ) + data ["boost"]["ping"]=not data ["boost"]["ping"] + await self .update_boost_config (ctx .guild .id ,data ) + + status ="enabled"if data ["boost"]["ping"]else "disabled" + await ctx.send(view=CV2("Success", f"{TICK} Booster pinging has been **{status}**.")) + + @_boost .group (name ="channel",help ="Manage boost notification channels") + @blacklist_check () + @ignore_check () + @commands .max_concurrency (1 ,per =commands .BucketType .default ,wait =False ) + @commands .guild_only () + @commands .has_permissions (administrator =True ) + async def _boost_channel (self ,ctx ): + if ctx .subcommand_passed is None : + await ctx .send_help (ctx .command ) + ctx .command .reset_cooldown (ctx ) + + @_boost_channel .command (name ="add",help ="Add a boost notification channel") + @blacklist_check () + @ignore_check () + @commands .cooldown (1 ,3 ,commands .BucketType .user ) + @commands .max_concurrency (1 ,per =commands .BucketType .default ,wait =False ) + @commands .guild_only () + @commands .has_permissions (administrator =True ) + async def _boost_channel_add (self ,ctx ,channel :discord .TextChannel ): + if not self .is_authorized (ctx ): + await self .send_permission_error (ctx ) + return + + data =await self .get_boost_config (ctx .guild .id ) + channels =data ["boost"]["channel"] + + if len (channels )>=3 : + await ctx.send(view=CV2("Error", f"{CROSS} Maximum boost channel limit reached (3 channels).")) + return + + if str (channel .id )in channels : + await ctx.send(view=CV2("Error", f"{CROSS} This channel is already in the boost channels list.")) + return + + channels .append (str (channel .id )) + await self .update_boost_config (ctx .guild .id ,data ) + + await ctx.send(view=CV2("Success", f"{TICK} Successfully added {channel.mention} to boost channels list.")) + + @_boost_channel .command (name ="remove",help ="Remove a boost notification channel") + @blacklist_check () + @ignore_check () + @commands .cooldown (1 ,3 ,commands .BucketType .user ) + @commands .max_concurrency (1 ,per =commands .BucketType .default ,wait =False ) + @commands .guild_only () + @commands .has_permissions (administrator =True ) + async def _boost_channel_remove (self ,ctx ,channel :discord .TextChannel ): + if not self .is_authorized (ctx ): + await self .send_permission_error (ctx ) + return + + data =await self .get_boost_config (ctx .guild .id ) + channels =data ["boost"]["channel"] + + if not channels : + await ctx.send(view=CV2("Error", f"{CROSS} No boost channels are currently set up.")) + return + + if str (channel .id )not in channels : + await ctx.send(view=CV2("Error", f"{CROSS} This channel is not in the boost channels list.")) + return + + channels .remove (str (channel .id )) + await self .update_boost_config (ctx .guild .id ,data ) + + await ctx.send(view=CV2("Success", f"{TICK} Successfully removed {channel.mention} from boost channels list.")) + + @_boost .command (name ="test",help ="Test how the boost message will look") + @blacklist_check () + @ignore_check () + @commands .guild_only () + @commands .has_permissions (administrator =True ) + async def _boost_test (self ,ctx ): + data =await self .get_boost_config (ctx .guild .id ) + channels =data ["boost"]["channel"] + + if not channels : + await ctx.send(view=CV2("Error", f"{CROSS} Please set up a boost channel first using `boost channel add #channel`.")) + return + + + formatted_message =self .format_boost_message (data ["boost"]["message"],ctx .author ,ctx .guild ) + + + channel =self .bot .get_channel (int (channels [0 ])) + if not channel : + await ctx.send(view=CV2("Error", f"{CROSS} The configured boost channel no longer exists.")) + + return + + try : + if data ["boost"]["embed"]: + embed =discord .Embed (description =formatted_message ,color =self .color ) + embed .set_author (name =ctx .author .display_name ,icon_url =ctx .author .display_avatar .url ) + embed .timestamp =discord .utils .utcnow () + + if data ["boost"]["image"]: + embed .set_image (url =data ["boost"]["image"]) + + if data ["boost"]["thumbnail"]: + embed .set_thumbnail (url =data ["boost"]["thumbnail"]) + + if ctx .guild .icon : + embed .set_footer (text =ctx .guild .name ,icon_url =ctx .guild .icon .url ) + + ping_content =ctx .author .mention if data ["boost"]["ping"]else "" + await channel .send (ping_content ,embed =embed ) + else : + await channel .send (formatted_message ) + + except discord .Forbidden : + await ctx.send(view=CV2("Error", f"{CROSS} I don't have permission to send messages in the boost channel.")) + + except Exception as e : + await ctx.send(view=CV2("Error", f"{CROSS} An error occurred: `{str(e)}`")) + + + @_boost .command (name ="config",help ="View current boost configuration") + @blacklist_check () + @ignore_check () + @commands .has_permissions (administrator =True ) + async def _boost_config (self ,ctx ): + data =await self .get_boost_config (ctx .guild .id ) + channels =data ["boost"]["channel"] + + if not channels : + await ctx.send(view=CV2("Error", f"{CROSS} Please set up a boost channel first using `boost channel add #channel`.")) + + return + + channel_mentions =[] + for channel_id in channels : + channel =self .bot .get_channel (int (channel_id )) + if channel : + channel_mentions .append (channel .mention ) + + embed_status = f"{TICK} Enabled" if data["boost"]["embed"] else f"{CROSS} Disabled" + ping_status = f"{TICK} Enabled" if data["boost"]["ping"] else f"{CROSS} Disabled" + autodel_status = f"{data['boost']['autodel']}s" if data["boost"]["autodel"] else "Disabled" + channels_str = "\n".join(channel_mentions) if channel_mentions else "None" + + config_text = ( + f"**Channels**\n{channels_str}\n\n" + f"**Message**\n```{data['boost']['message']}```\n" + f"**Embed:** {embed_status}\n" + f"**Ping:** {ping_status}\n" + f"**Auto-delete:** {autodel_status}" + ) + + await ctx.send(view=CV2(f"{NITRO_BOOST} Boost Configuration for {ctx.guild.name}", config_text)) + + @_boost .command (name ="reset",help ="Reset boost configuration") + @commands .cooldown (1 ,5 ,commands .BucketType .user ) + @commands .max_concurrency (1 ,per =commands .BucketType .default ,wait =False ) + @commands .guild_only () + @blacklist_check () + @ignore_check () + @commands .has_permissions (administrator =True ) + async def _boost_reset (self ,ctx ): + if not self .is_authorized (ctx ): + await self .send_permission_error (ctx ) + return + + data =await self .get_boost_config (ctx .guild .id ) + + if not data ["boost"]["channel"]: + await ctx.send(view=CV2("Error", f"{CROSS} No boost configuration found to reset.")) + return + + + data ["boost"]["channel"]=[] + data ["boost"]["image"]="" + data ["boost"]["message"]="{user.mention} just boosted {server.name}! 🎉" + data ["boost"]["thumbnail"]="" + data ["boost"]["embed"]=True + data ["boost"]["ping"]=False + data ["boost"]["autodel"]=0 + + await self .update_boost_config (ctx .guild .id ,data ) + + await ctx.send(view=CV2("Success", f"{TICK} Successfully reset all boost configuration.")) + + @commands .group (name ="boostrole",invoke_without_command =True ,help ="Manage boost roles") + @commands .cooldown (1 ,5 ,commands .BucketType .user ) + @commands .max_concurrency (1 ,per =commands .BucketType .default ,wait =False ) + @blacklist_check () + @ignore_check () + @commands .guild_only () + @commands .has_permissions (administrator =True ) + async def _boostrole (self ,ctx ): + if ctx .subcommand_passed is None : + await ctx .send_help (ctx .command ) + ctx .command .reset_cooldown (ctx ) + + @_boostrole .command (name ="config",help ="View boost role configuration") + @commands .cooldown (1 ,5 ,commands .BucketType .user ) + @commands .max_concurrency (1 ,per =commands .BucketType .default ,wait =False ) + @commands .guild_only () + @blacklist_check () + @ignore_check () + @commands .has_permissions (administrator =True ) + async def _boostrole_config (self ,ctx ): + data =await self .get_boost_config (ctx .guild .id ) + role_ids =data ["boost_roles"]["roles"] + + if not role_ids : + await ctx.send(view=CV2(f"Boost Roles - {ctx.guild.name}", "No boost roles configured.")) + return + + roles =[] + for role_id in role_ids : + role =ctx .guild .get_role (int (role_id )) + if role : + roles .append (role .mention ) + + roles_str = "\n".join(roles) if roles else "No valid roles found" + await ctx.send(view=CV2(f"Boost Roles - {ctx.guild.name}", roles_str)) + + @_boostrole .command (name ="add",help ="Add a boost role") + @blacklist_check () + @ignore_check () + @commands .cooldown (1 ,3 ,commands .BucketType .user ) + @commands .max_concurrency (1 ,per =commands .BucketType .default ,wait =False ) + @commands .guild_only () + @commands .has_permissions (administrator =True ) + async def _boostrole_add (self ,ctx ,role :discord .Role ): + if not self .is_authorized (ctx ): + await self .send_permission_error (ctx ) + return + + data =await self .get_boost_config (ctx .guild .id ) + roles =data ["boost_roles"]["roles"] + + if len (roles )>=10 : + await ctx.send(view=CV2("Error", f"{CROSS} Maximum boost role limit reached (10 roles).")) + return + + if str (role .id )in roles : + await ctx.send(view=CV2("Error", f"{CROSS} {role.mention} is already a boost role.")) + return + + roles .append (str (role .id )) + await self .update_boost_config (ctx .guild .id ,data ) + + await ctx.send(view=CV2("Success", f"{TICK} {role.mention} has been added as a boost role.")) + + @_boostrole .command (name ="remove",help ="Remove a boost role") + @blacklist_check () + @ignore_check () + @commands .cooldown (1 ,3 ,commands .BucketType .user ) + @commands .max_concurrency (1 ,per =commands .BucketType .default ,wait =False ) + @commands .guild_only () + @commands .has_permissions (administrator =True ) + async def _boostrole_remove (self ,ctx ,role :discord .Role ): + if not self .is_authorized (ctx ): + await self .send_permission_error (ctx ) + return + + data =await self .get_boost_config (ctx .guild .id ) + roles =data ["boost_roles"]["roles"] + + if not roles : + await ctx.send(view=CV2("Error", f"{CROSS} No boost roles are currently configured.")) + return + + if str (role .id )not in roles : + await ctx.send(view=CV2("Error", f"{CROSS} {role.mention} is not a boost role.")) + return + + roles .remove (str (role .id )) + await self .update_boost_config (ctx .guild .id ,data ) + + await ctx.send(view=CV2("Success", f"{TICK} {role.mention} has been removed from boost roles.")) + + @_boostrole .command (name ="reset",help ="Reset boost role configuration") + @commands .cooldown (1 ,3 ,commands .BucketType .user ) + @commands .max_concurrency (1 ,per =commands .BucketType .default ,wait =False ) + @commands .guild_only () + @blacklist_check () + @ignore_check () + @commands .has_permissions (administrator =True ) + async def _boostrole_reset (self ,ctx ): + if not self .is_authorized (ctx ): + await self .send_permission_error (ctx ) + return + + data =await self .get_boost_config (ctx .guild .id ) + + if not data ["boost_roles"]["roles"]: + await ctx.send(view=CV2("Error", f"{CROSS} No boost roles are currently configured.")) + return + + data ["boost_roles"]["roles"]=[] + await self .update_boost_config (ctx .guild .id ,data ) + + await ctx.send(view=CV2("Success", f"{TICK} Successfully cleared all boost roles.")) + +async def setup (bot ): + await bot .add_cog (Booster(bot )) \ No newline at end of file diff --git a/bot/cogs/commands/calc.py b/bot/cogs/commands/calc.py new file mode 100644 index 0000000..b6cf4b3 --- /dev/null +++ b/bot/cogs/commands/calc.py @@ -0,0 +1,137 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from discord.ext import commands +from discord.ui import View, Button, button +import discord +from utils.safe_parse import safe_math_eval + + +class CalculatorView(View): + def __init__(self, author: discord.Member): + super().__init__() + self.author = author + self.value = "" + self.message = None + + # Button interactions + @button(label="1", style=discord.ButtonStyle.grey, row=0) + async def one(self, interaction: discord.Interaction, button: Button): + await self.update_value(interaction, "1") + + @button(label="2", style=discord.ButtonStyle.grey, row=0) + async def two(self, interaction: discord.Interaction, button: Button): + await self.update_value(interaction, "2") + + @button(label="3", style=discord.ButtonStyle.grey, row=0) + async def three(self, interaction: discord.Interaction, button: Button): + await self.update_value(interaction, "3") + + @button(label="4", style=discord.ButtonStyle.grey, row=1) + async def four(self, interaction: discord.Interaction, button: Button): + await self.update_value(interaction, "4") + + @button(label="5", style=discord.ButtonStyle.grey, row=1) + async def five(self, interaction: discord.Interaction, button: Button): + await self.update_value(interaction, "5") + + @button(label="6", style=discord.ButtonStyle.grey, row=1) + async def six(self, interaction: discord.Interaction, button: Button): + await self.update_value(interaction, "6") + + @button(label="7", style=discord.ButtonStyle.grey, row=2) + async def seven(self, interaction: discord.Interaction, button: Button): + await self.update_value(interaction, "7") + + @button(label="8", style=discord.ButtonStyle.grey, row=2) + async def eight(self, interaction: discord.Interaction, button: Button): + await self.update_value(interaction, "8") + + @button(label="9", style=discord.ButtonStyle.grey, row=2) + async def nine(self, interaction: discord.Interaction, button: Button): + await self.update_value(interaction, "9") + + @button(label="0", style=discord.ButtonStyle.grey, row=3) + async def zero(self, interaction: discord.Interaction, button: Button): + await self.update_value(interaction, "0") + + @button(label="+", style=discord.ButtonStyle.blurple, row=3) + async def add(self, interaction: discord.Interaction, button: Button): + await self.update_value(interaction, "+") + + @button(label="-", style=discord.ButtonStyle.blurple, row=3) + async def subtract(self, interaction: discord.Interaction, button: Button): + await self.update_value(interaction, "-") + + @button(label="*", style=discord.ButtonStyle.blurple, row=3) + async def multiply(self, interaction: discord.Interaction, button: Button): + await self.update_value(interaction, "*") + + @button(label="/", style=discord.ButtonStyle.blurple, row=3) + async def divide(self, interaction: discord.Interaction, button: Button): + await self.update_value(interaction, "/") + + @button(label="=", style=discord.ButtonStyle.green, row=4) + async def equals(self, interaction: discord.Interaction, button: Button): + if interaction.user != self.author: + return await interaction.response.send_message( + "This is not your embed.", ephemeral=True + ) + try: + expression = self.value.strip().replace("\n", "") + result = safe_math_eval(expression) + await self.update_embed(interaction, result) + self.value = result # Store the result for possible further calculations + except: + await self.update_embed(interaction, "Error") + + @button(label="Clear", style=discord.ButtonStyle.red, row=4) + async def clear(self, interaction: discord.Interaction, button: Button): + await self.update_value(interaction, "Clear") + + async def update_value(self, interaction: discord.Interaction, value: str): + # Check if the person interacting is the author of the embed + if interaction.user != self.author: + return await interaction.response.send_message( + "This content does not appear to be part of your embedded materials.", ephemeral=True + ) + # Append the value or clear if "Clear" + if value == "Clear": + self.value = "" + else: + self.value += value + # Update the embed with the new value + await self.update_embed(interaction, self.value) + + async def update_embed(self, interaction: discord.Interaction, result: str): + content = f"**Calculator** | `{self.author.display_name}`\n```\n{result}\n```" + await interaction.response.edit_message(content=content, view=self) + self.message = interaction.message + +class calculator(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @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 = CalculatorView(author=ctx.author) + # We store the message so we know what to edit and update later + view.message = await ctx.send(content="**Calculator**\n```\n \n```", view=view) + +# Add the cog to the bot +def setup(bot): + bot.add_cog(calculator(bot)) + diff --git a/bot/cogs/commands/counting.py b/bot/cogs/commands/counting.py new file mode 100644 index 0000000..32693d2 --- /dev/null +++ b/bot/cogs/commands/counting.py @@ -0,0 +1,208 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ARROWRED, CROSS, NEXT_ALT1, REDRULESBOOK, RED_BUTTON, RED_PIN, STAR, TICK, ZBACK, ZPAUSE, ZPLAY, ZWARNING +from discord.ext import commands +import json +import os +import asyncio +from discord.ui import LayoutView, TextDisplay, Separator, Container +from utils.cv2 import CV2, build_container + + + +# Emoji Variables +CROSS = CROSS +TICK = TICK +WARNING = ZWARNING +WARNING = ZWARNING +BOOK = REDRULESBOOK +PLAY = ZPLAY +PAUSE = ZPAUSE +STOP = RED_BUTTON +NEXT = NEXT_ALT1 +BACK = ZBACK +ARROW = ARROWRED +PIN = RED_PIN +STAR = STAR + +class Counting(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.data_file = "db/counting.json" + if not os.path.exists(self.data_file): + with open(self.data_file, 'w') as f: + json.dump({}, f) + with open(self.data_file, 'r') as f: + self.counting_data = json.load(f) + + def save_data(self): + with open(self.data_file, 'w') as f: + json.dump(self.counting_data, f, indent=4) + + def is_enabled(self, guild_id): + guild_id = str(guild_id) + return self.counting_data.get(guild_id, {}).get("enabled", False) + + async def not_enabled_embed(self, ctx): + await ctx.send(view=CV2( + f"{BOOK} Counting Settings For {ctx.guild.name}", + f"**Current Status:** {CROSS} Disabled", + "**How to Enable:** Use `counting enable` to enable counting." + )) + + async def send_help_embed(self, ctx): + await ctx.send(view=CV2( + f"{BOOK} Counting Commands", + "Manage and control the counting game settings.\n\n" + "**counting enable/disable** — Enable or Disable counting in server\n" + "**counting channel #channel** — Set counting channel\n" + "**counting config reset/continue** — Set reset mode on mistake\n" + "**counting reset** — Reset counting back to 0\n" + "**counting stats** — View current counting stats" + )) + + @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) + else: + await self.send_help_embed(ctx) + + @counting.command(name="enable") + @commands.has_permissions(manage_channels=True) + async def enable(self, ctx): + guild_id = str(ctx.guild.id) + if guild_id not in self.counting_data: + self.counting_data[guild_id] = {"enabled": True, "channel": None, "count": 0, "reset_on_fail": False} + else: + self.counting_data[guild_id]["enabled"] = True + self.save_data() + await ctx.send(view=CV2("Counting", f"{TICK} Counting has been Enabled!")) + + @counting.command(name="disable") + @commands.has_permissions(manage_channels=True) + async def disable(self, ctx): + guild_id = str(ctx.guild.id) + if guild_id not in self.counting_data: + await self.not_enabled_embed(ctx) + return + self.counting_data[guild_id]["enabled"] = False + self.save_data() + await ctx.send(view=CV2("Counting", f"{STOP} Counting has been Disabled!")) + + @counting.command(name="channel") + @commands.has_permissions(manage_channels=True) + async def channel(self, ctx, channel: discord.TextChannel): + guild_id = str(ctx.guild.id) + if not self.is_enabled(guild_id): + await self.not_enabled_embed(ctx) + return + self.counting_data[guild_id]["channel"] = channel.id + self.save_data() + await ctx.send(view=CV2("Counting", f"{PIN} Counting channel set to {channel.mention}")) + + @counting.command(name="config") + @commands.has_permissions(manage_channels=True) + async def config(self, ctx, mode: str): + guild_id = str(ctx.guild.id) + if not self.is_enabled(guild_id): + await self.not_enabled_embed(ctx) + return + if mode.lower() in ["reset", "true", "on"]: + self.counting_data[guild_id]["reset_on_fail"] = True + msg = f"{TICK} Counting will now reset on mistakes." + elif mode.lower() in ["continue", "false", "off"]: + self.counting_data[guild_id]["reset_on_fail"] = False + msg = f"{TICK} Counting will now continue on mistakes." + else: + await ctx.send(f"{CROSS} Invalid mode! Use `reset` or `continue`.") + return + self.save_data() + await ctx.send(view=CV2("Counting", msg)) + + @counting.command(name="reset") + @commands.has_permissions(manage_channels=True) + async def reset(self, ctx): + guild_id = str(ctx.guild.id) + if not self.is_enabled(guild_id): + await self.not_enabled_embed(ctx) + return + self.counting_data[guild_id]["count"] = 0 + self.save_data() + await ctx.send(view=CV2("Counting", f"{NEXT} Counting has been reset to 0!")) + + @counting.command(name="stats") + async def stats(self, ctx): + guild_id = str(ctx.guild.id) + if not self.is_enabled(guild_id): + await self.not_enabled_embed(ctx) + return + data = self.counting_data[guild_id] + channel = ctx.guild.get_channel(data["channel"]) if data["channel"] else None + channel_str = channel.mention if channel else "Not Set" + reset_str = f"{TICK} Yes" if data["reset_on_fail"] else f"{CROSS} No" + await ctx.send(view=CV2( + f"{BOOK} Counting Stats", + f"**Current Count:** {data['count']}\n" + f"**Channel:** {channel_str}\n" + f"**Reset on Mistake:** {reset_str}" + )) + + @commands.Cog.listener() + async def on_message(self, message): + if message.author.bot: + return + + guild_id = str(message.guild.id) + if guild_id not in self.counting_data: + return + + data = self.counting_data[guild_id] + if not data.get("enabled", False): + return + + if message.channel.id != data.get("channel"): + return + + content = message.content.strip() + + if not content.isdigit(): + msg = await message.channel.send(f"{WARNING} Alphabet not allowed!") + await asyncio.sleep(3) + await msg.delete() + await message.delete() + return + + number = int(content) + expected_number = data.get("count", 0) + 1 + + if number != expected_number: + msg = await message.channel.send(f"{CROSS} Wrong number entered! Expected number is **{expected_number}**") + await asyncio.sleep(3) + await msg.delete() + await message.delete() + if data.get("reset_on_fail", False): + self.counting_data[guild_id]["count"] = 0 + self.save_data() + return + + # Correct number + self.counting_data[guild_id]["count"] = number + self.save_data() + await message.add_reaction(TICK) + +def setup(bot): + bot.add_cog(Counting(bot)) \ No newline at end of file diff --git a/bot/cogs/commands/customrole.py b/bot/cogs/commands/customrole.py new file mode 100644 index 0000000..472703c --- /dev/null +++ b/bot/cogs/commands/customrole.py @@ -0,0 +1,557 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CROSS, TICK, ZWARNING +from discord import app_commands +from discord.ext import commands +from discord.ext.commands import Context +import aiosqlite +import asyncio +from utils.Tools import * +from utils.cv2 import CV2, build_container +from typing import List, Tuple +from discord.ui import LayoutView, TextDisplay, Separator, Container +from utils.config import * + + + +DATABASE_PATH = 'db/customrole.db' +DATABASE_PATH2 = 'db/np.db' + + +class Customrole(commands.Cog): + + def __init__(self, bot): + self.bot = bot + self.cooldown = {} + self.rate_limit = {} + self.rate_limit_timeout = 5 + + + self.bot.loop.create_task(self.create_tables()) + + + async def reset_rate_limit(self, user_id): + await asyncio.sleep(self.rate_limit_timeout) + self.rate_limit.pop(user_id, None) + + + + + async def add_role(self, *, role_id: int, member: discord.Member): + if member.guild.me.guild_permissions.manage_roles: + role = discord.Object(id=role_id) + await member.add_roles(role, reason=f"{BRAND_NAME} Customrole | Role Added") + else: + raise discord.Forbidden("Bot does not have permission to manage roles.") + + + + async def remove_role(self, *, role_id: int, member: discord.Member): + if member.guild.me.guild_permissions.manage_roles: + role = discord.Object(id=role_id) + await member.remove_roles(role, reason=f"{BRAND_NAME} Customrole | Role Removed") + else: + raise discord.Forbidden("Bot does not have permission to manage roles.") + + + + async def add_role2(self, *, role: int, member: discord.Member): + if member.guild.me.guild_permissions.manage_roles: + role = discord.Object(id=int(role)) + await member.add_roles(role, reason=f"{BRAND_NAME} Customrole | Role Added ") + + async def remove_role2(self, *, role: int, member: discord.Member): + if member.guild.me.guild_permissions.manage_roles: + role = discord.Object(id=int(role)) + await member.remove_roles(role, reason=f"{BRAND_NAME} Customrole| Role Removed") + + + + async def handle_role_command(self, context: Context, member: discord.Member, role_type: str): + async with aiosqlite.connect('db/customrole.db') as db: + async with db.execute(f"SELECT reqrole, {role_type} FROM roles WHERE guild_id = ?", (context.guild.id,)) as cursor: + data = await cursor.fetchone() + if data: + reqrole_id, role_id = data + reqrole = context.guild.get_role(reqrole_id) + role = context.guild.get_role(role_id) + + if reqrole: + if context.author == context.guild.owner or reqrole in context.author.roles: + if role: + if role not in member.roles: + await self.add_role2(role=role_id, member=member) + await context.reply(view=CV2(f"{TICK} Success", f"**Given** <@&{role.id}> To {member.mention}")) + else: + await self.remove_role2(role=role_id, member=member) + await context.reply(view=CV2(f"{TICK} Success", f"**Removed** <@&{role.id}> From {member.mention}")) + else: + await context.reply(view=CV2(f"{CROSS} Error", f"{role_type.capitalize()} role is not set up in {context.guild.name}")) + else: + await context.reply(view=CV2(f"{ZWARNING} Access Denied", f"You need {reqrole.mention} to run this command.")) + else: + await context.reply(view=CV2(f"{ZWARNING} Access Denied", f"Required role is not set up in {context.guild.name}")) + else: + await context.reply(view=CV2(f"{CROSS} Error", f"Roles configuration is not set up in {context.guild.name}")) + + + + + async def create_tables(self): + async with aiosqlite.connect(DATABASE_PATH) as db: + await db.execute(''' + CREATE TABLE IF NOT EXISTS roles ( + guild_id INTEGER PRIMARY KEY, + staff INTEGER, + girl INTEGER, + vip INTEGER, + guest INTEGER, + frnd INTEGER, + reqrole INTEGER + ) + ''') + await db.execute(''' + CREATE TABLE IF NOT EXISTS custom_roles ( + guild_id INTEGER, + name TEXT, + role_id INTEGER, + PRIMARY KEY (guild_id, name) + ) + ''') + await db.commit() + + + @commands.hybrid_group(name="setup", + description="Setups custom roles for the server.", + help="Setups custom roles for the server.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def set(self, context: Context): + if context.subcommand_passed is None: + await context.send_help(context.command) + context.command.reset_cooldown(context) + + async def fetch_role_data(self, guild_id): + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute("SELECT staff, girl, vip, guest, frnd, reqrole FROM roles WHERE guild_id = ?", (guild_id,)) as cursor: + return await cursor.fetchone() + + + + + async def update_role_data(self, guild_id, column, value): + try: + async with aiosqlite.connect(DATABASE_PATH) as db: + await db.execute(f"INSERT OR REPLACE INTO roles (guild_id, {column}) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET {column} = ?", + (guild_id, value, value)) + await db.commit() + except Exception as e: + print(f"Error updating role data: {e}") + + + async def fetch_custom_role_data(self, guild_id): + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute("SELECT name, role_id FROM custom_roles WHERE guild_id = ?", (guild_id,)) as cursor: + return await cursor.fetchall() + + + @set.command(name="staff", + description="Setup staff role in guild", + help="Setup staff role in Guild") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(administrator=True) + @app_commands.describe(role="Role to be added") + async def staff(self, context: Context, role: discord.Role) -> None: + if context.author == context.guild.owner or context.author.top_role.position > context.guild.me.top_role.position: + await self.update_role_data(context.guild.id, 'staff', role.id) + await context.reply(view=CV2(f"{TICK} Success", f"Added {role.mention} to `Staff` Role\n\n__**How to Use?**__\nUse `staff ` Command to **Add {role.mention}** role to User & use again to the same user to **Remove role**.")) + else: + await context.reply(view=CV2(f"{ZWARNING} Access Denied", "Your role should be above my top role.")) + + @set.command(name="girl", + description="Setup girl role in the Guild", + help="Setup girl role in the Guild") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(administrator=True) + @app_commands.describe(role="Role to be added") + async def girl(self, context: Context, role: discord.Role) -> None: + if context.author == context.guild.owner or context.author.top_role.position > context.guild.me.top_role.position: + await self.update_role_data(context.guild.id, 'girl', role.id) + await context.reply(view=CV2(f"{TICK} Success", f"Added {role.mention} to `Girl` Role\n\n__**How to Use?**__\nUse `girl ` Command to **Add {role.mention}** role to User & use again to the same user to **Remove role**.")) + else: + await context.reply(view=CV2(f"{ZWARNING} Access Denied", "Your role should be above my top role.")) + + @set.command(name="vip", + description="Setups vip role in the Guild", + help="Setups vip role in the Guild") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(administrator=True) + @app_commands.describe(role="Role to be added") + async def vip(self, context: Context, role: discord.Role) -> None: + if context.author == context.guild.owner or context.author.top_role.position > context.guild.me.top_role.position: + await self.update_role_data(context.guild.id, 'vip', role.id) + await context.reply(view=CV2(f"{TICK} Success", f"Added {role.mention} to `VIP` Role\n\n__**How to Use?**__\nUse `vip ` Command to **Add {role.mention}** role to User & use again to the same user to **Remove role**.")) + else: + await context.reply(view=CV2(f"{ZWARNING} Access Denied", "Your role should be above my top role.")) + + @set.command(name="guest", + description="Setup guest role in the Guild", + help="Setup guest role in the Guild") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(administrator=True) + @app_commands.describe(role="Role to be added") + async def guest(self, context: Context, role: discord.Role) -> None: + if context.author == context.guild.owner or context.author.top_role.position > context.guild.me.top_role.position: + await self.update_role_data(context.guild.id, 'guest', role.id) + await context.reply(view=CV2(f"{TICK} Success", f"Added {role.mention} to `Guest` Role\n\n__**How to Use?**__\nUse `guest ` Command to **Add {role.mention}** role to User & use again to the same user to **Remove role**.")) + else: + await context.reply(view=CV2(f"{ZWARNING} Access Denied", "Your role should be above my top role.")) + + @set.command(name="friend", + description="Setup friend role in the Guild", + help="Setup friend role in the Guild") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(administrator=True) + @app_commands.describe(role="Role to be added") + async def friend(self, context: Context, role: discord.Role) -> None: + if context.author == context.guild.owner or context.author.top_role.position > context.guild.me.top_role.position: + await self.update_role_data(context.guild.id, 'frnd', role.id) + await context.reply(view=CV2(f"{TICK} Success", f"Added {role.mention} to `Friend` Role\n\n__**How to Use?**__\nUse `friend ` Command to **Add {role.mention}** role to User & use again to the same user to **Remove role**.")) + else: + await context.reply(view=CV2(f"{ZWARNING} Access Denied", "Your role should be above my top role.")) + + @set.command(name="reqrole", + description="Setup required role for custom role commands", + help="Setup required role for custom role commands") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 4, commands.BucketType.user) + @commands.has_permissions(administrator=True) + @app_commands.describe(role="Role to be added") + async def req_role(self, context: Context, role: discord.Role) -> None: + if context.author == context.guild.owner or context.author.top_role.position > context.guild.me.top_role.position: + await self.update_role_data(context.guild.id, 'reqrole', role.id) + await context.reply(view=CV2(f"{TICK} Success", f"Added {role.mention} for Required role to run custom role commands in {context.guild.name}")) + else: + await context.reply(view=CV2(f"{ZWARNING} Access Denied", "Your role should be above my top role.")) + + + + @set.command(name="config", + description="Shows the current custom role configuration in the Guild.", + help="Shows the current custom role configuration in the Guild.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def config(self, context: Context) -> None: + role_data = await self.fetch_role_data(context.guild.id) + if role_data: + staff = context.guild.get_role(role_data[0]).mention if role_data[0] else "None" + girl = context.guild.get_role(role_data[1]).mention if role_data[1] else "None" + vip = context.guild.get_role(role_data[2]).mention if role_data[2] else "None" + guest = context.guild.get_role(role_data[3]).mention if role_data[3] else "None" + friend = context.guild.get_role(role_data[4]).mention if role_data[4] else "None" + reqrole = context.guild.get_role(role_data[5]).mention if role_data[5] else "None" + config_text = ( + f"**Staff Role:** {staff}\n" + f"**Girl Role:** {girl}\n" + f"**VIP Role:** {vip}\n" + f"**Guest Role:** {guest}\n" + f"**Friend Role:** {friend}\n" + f"**Required Role:** {reqrole}\n\n" + "Use Commands to assign role & use again to the same user to remove role." + ) + await context.reply(view=CV2("Custom Role Configuration", config_text)) + else: + await context.reply(view=CV2(f"{CROSS} Error", "No custom role configuration found in this Guild.")) + + + + + + + @set.command(name="create", + description="Creates a custom role command.", + help="Creates a custom role command") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_permissions(administrator=True) + @app_commands.describe(name="Command name", role="Role to be assigned") + async def create(self, context: Context, name: str, role: discord.Role) -> None: + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute("SELECT COUNT(*) FROM custom_roles WHERE guild_id = ?", (context.guild.id,)) as cursor: + count = await cursor.fetchone() + if count[0] >= 56: + await context.reply(view=CV2(f"{ZWARNING} Limit Reached", "You have reached the maximum limit of 56 custom role commands for this guild.")) + return + + async with db.execute("SELECT name FROM custom_roles WHERE guild_id = ?", (context.guild.id,)) as cursor: + existing_role = await cursor.fetchall() + if any(name == row[0] for row in existing_role): + await context.reply(view=CV2(f"{CROSS} Error", f"A custom role command with the name `{name}` already exists in this guild. Remove it before creating a new one.")) + return + + await db.execute("INSERT INTO custom_roles (guild_id, name, role_id) VALUES (?, ?, ?)", + (context.guild.id, name, role.id)) + await db.commit() + + await context.reply(view=CV2(f"{TICK} Success", f"Custom role command `{name}` created to assign the role {role.mention}.\n\n__**How to Use?**__\nUse `{name} ` Command to Assign/Remove {role.mention} role to User.\n> This will work for the users having `Manage Roles` permissions.")) + + + @set.command(name="delete", aliases=["remove"], + description="Deletes a custom role command.", + help="Deletes a custom role command.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_permissions(administrator=True) + @app_commands.describe(name="Command name to be deleted") + async def delete(self, context: Context, name: str) -> None: + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute("SELECT name FROM custom_roles WHERE guild_id = ? AND name = ?", (context.guild.id, name)) as cursor: + existing_role = await cursor.fetchone() + + if not existing_role: + await context.reply(view=CV2(f"{CROSS} Error", f"No custom role command with the name `{name}` was found in this guild.")) + return + + async with aiosqlite.connect(DATABASE_PATH) as db: + await db.execute("DELETE FROM custom_roles WHERE guild_id = ? AND name = ?", (context.guild.id, name)) + await db.commit() + + await context.reply(view=CV2(f"{TICK} Success", f"Custom role command `{name}` has been deleted.")) + + + @set.command( + name="list", + description="List all the custom roles setup for the server.", + help="List all the custom roles setup for the server." + ) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def list(self, context: Context) -> None: + custom_roles = await self.fetch_custom_role_data(context.guild.id) + + if not custom_roles: + await context.reply(view=CV2(f"{CROSS} Error", "No custom roles have been created for this server.")) + return + + + def chunk_list(data: List[Tuple[str, int]], chunk_size: int): + """Yield successive chunks of `chunk_size` from `data`.""" + for i in range(0, len(data), chunk_size): + yield data[i:i + chunk_size] + + + chunks = list(chunk_list(custom_roles, 7)) + + for i, chunk in enumerate(chunks): + roles_text = "" + for name, role_id in chunk: + role = context.guild.get_role(role_id) + if role: + roles_text += f"**{name}** → {role.mention}\n" + footer = f"Page {i+1}/{len(chunks)} | These commands are usable by Members having Manage Role permissions." + await context.reply(view=CV2("Custom Roles", roles_text, footer)) + + + @set.command(name="reset", + description="Resets custom role configuration for the server.", + help="Resets custom role configuration for the server.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 4, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def reset(self, context: Context) -> None: + if context.author == context.guild.owner or context.author.top_role.position > context.guild.me.top_role.position: + removed_roles = [] + role_data = await self.fetch_role_data(context.guild.id) + if role_data: + roles = ["staff", "girl", "vip", "guest", "frnd", "reqrole"] + for i, role_name in enumerate(roles): + role_id = role_data[i] + if role_id: + role = context.guild.get_role(role_id) + if role: + removed_roles.append(f"**{role_name.capitalize()}:** {role.mention}") + await self.update_role_data(context.guild.id, role_name, None) + + async with aiosqlite.connect(DATABASE_PATH) as db: + await db.execute("DELETE FROM custom_roles WHERE guild_id = ?", (context.guild.id,)) + await db.commit() + reset_desc = f"Deleted All Custom Role commands {TICK}\n\n**Removed Roles:**\n" + "\n".join(removed_roles) if removed_roles else "No roles were previously set." + await context.reply(view=CV2("Custom Role Configuration Reset", reset_desc)) + else: + await context.reply(view=CV2("Info", "No configuration found for this server.")) + else: + await context.reply(view=CV2(f"{ZWARNING} Access Denied", "Your role should be above my top role.")) + + + + + @commands.Cog.listener() + async def on_message(self, message: discord.Message): + + if message.author.bot or not message.content: + return + + prefixes = await self.bot.get_prefix(message) + + + if not prefixes: + return + + + if not any(message.content.startswith(prefix) for prefix in prefixes): + return + + + for prefix in prefixes: + if message.content.startswith(prefix): + command_name = message.content[len(prefix):].split()[0] + break + else: + return + + guild_id = message.guild.id + + + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute("SELECT role_id FROM custom_roles WHERE guild_id = ? AND name = ?", (guild_id, command_name)) as cursor: + result = await cursor.fetchone() + + if result: + role_id = result[0] + role = message.guild.get_role(role_id) + + + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute("SELECT reqrole FROM roles WHERE guild_id = ?", (guild_id,)) as cursor: + reqrole_result = await cursor.fetchone() + + reqrole_id = reqrole_result[0] if reqrole_result else None + reqrole = message.guild.get_role(reqrole_id) if reqrole_id else None + + + if reqrole is None: + await message.channel.send(f"{ZWARNING} The required role is not set up in this server. Please set it up using `setup reqrole`.") + return + + + if reqrole not in message.author.roles: + await message.channel.send(view=CV2(f"{ZWARNING} Access Denied", f"You need the {reqrole.mention} role to use this command.")) + return + + + member = message.mentions[0] if message.mentions else None + if not member: + await message.channel.send("Please mention a user to assign the role.") + return + + + now = asyncio.get_event_loop().time() + if guild_id not in self.cooldown or now - self.cooldown[guild_id] >= 10: + self.cooldown[guild_id] = now + else: + await message.channel.send("You're on a cooldown of 5 seconds. Please wait before sending another command.", delete_after=5) + return + + try: + if role in member.roles: + await self.remove_role(role_id=role_id, member=member) + await message.channel.send(view=CV2(f"{TICK} Success", f"**Removed** the role {role.mention} from {member.mention}.")) + else: + await self.add_role(role_id=role_id, member=member) + await message.channel.send(view=CV2(f"{TICK} Success", f"**Added** the role {role.mention} to {member.mention}.")) + except discord.Forbidden as e: + await message.channel.send("I do not have permission to manage this role to the given user.") + print(f"Error: {e}") + else: + return + + + + @commands.hybrid_command(name="staff", + description="Gives the staff role to the user.", + aliases=['official'], + help="Gives the staff role to the user.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + #@commands.has_permissions(manage_roles=True) + async def _staff(self, context: Context, member: discord.Member) -> None: + await self.handle_role_command(context, member, 'staff') + + @commands.hybrid_command(name="girl", + description="Gives the girl role to the user.", + aliases=['qt'], + help="Gives the girl role to the user.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + #@commands.has_permissions(manage_roles=True) + async def _girl(self, context: Context, member: discord.Member) -> None: + await self.handle_role_command(context, member, 'girl') + + @commands.hybrid_command(name="vip", + description="Gives the VIP role to the user.", + help="Gives the VIP role to the user.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + #@commands.has_permissions(manage_roles=True) + async def _vip(self, context: Context, member: discord.Member) -> None: + await self.handle_role_command(context, member, 'vip') + + @commands.hybrid_command(name="guest", + description="Gives the guest role to the user.", + help="Gives the guest role to the user.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + #@commands.has_permissions(manage_roles=True) + async def _guest(self, context: Context, member: discord.Member) -> None: + await self.handle_role_command(context, member, 'guest') + + @commands.hybrid_command(name="friend", + description="Gives the friend role to the user.", + aliases=['frnd'], + help="Gives the friend role to the user.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + #@commands.has_permissions(manage_roles=True) + async def _friend(self, context: Context, member: discord.Member) -> None: + await self.handle_role_command(context, member, 'frnd') + + + \ No newline at end of file diff --git a/bot/cogs/commands/dms.py b/bot/cogs/commands/dms.py new file mode 100644 index 0000000..edb8392 --- /dev/null +++ b/bot/cogs/commands/dms.py @@ -0,0 +1,115 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ui import LayoutView, TextDisplay, Separator, Container, Button, ActionRow +from discord.ext import commands +from utils.cv2 import CV2, build_container +from utils.config import STAFF_IDS + + +class SuccessView(LayoutView): + def __init__(self, member): + super().__init__(timeout=None) + self.member = member + + self.add_item( + build_container( + TextDisplay("**Message Sent**"), + Separator(visible=True), + TextDisplay( + f"✅ Your message has been successfully sent to **{member.name}**" + ), + ) + ) + + +class ErrorView(LayoutView): + def __init__(self, member): + super().__init__(timeout=None) + self.member = member + + self.add_item( + build_container( + TextDisplay("**Delivery Failed**"), + Separator(visible=True), + TextDisplay( + f"❌ Could not send the message. **{member.name}** may have their DMs disabled." + ), + ) + ) + + +class GenericErrorView(LayoutView): + def __init__(self, error): + super().__init__(timeout=None) + self.error = error + + self.add_item( + build_container( + TextDisplay("**Error Occurred**"), + Separator(visible=True), + TextDisplay(f"🤔 Something went wrong. Error: {error}"), + ) + ) + + +class PermissionErrorView(LayoutView): + def __init__(self): + super().__init__(timeout=None) + + self.add_item( + build_container( + TextDisplay("**Permission Denied**"), + Separator(visible=True), + TextDisplay("❌ You do not have permission to use this command."), + ) + ) + + +class StaffDMCog(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @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() + await ctx.reply(view=view) + return + + try: + embed = discord.Embed( + title="📢 A Message from the Staff Team", + description=message, + color=0xFF0000, + ) + embed.set_footer(text=f"This message was sent by {ctx.author.name}.") + + await member.send(embed=embed) + + view = SuccessView(member) + await ctx.reply(view=view) + + except discord.Forbidden: + view = ErrorView(member) + await ctx.reply(view=view) + except Exception as e: + view = GenericErrorView(str(e)) + await ctx.reply(view=view) + print(f"Error in dmstaff command: {e}") + + +async def setup(bot): + await bot.add_cog(StaffDMCog(bot)) diff --git a/bot/cogs/commands/emergency.py b/bot/cogs/commands/emergency.py new file mode 100644 index 0000000..b70bf45 --- /dev/null +++ b/bot/cogs/commands/emergency.py @@ -0,0 +1,803 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CROSS, CROSS_ALT, ML_CROSS, TICK, TICK_ALT, ZWARNING +from discord.ui import LayoutView, TextDisplay, Separator, Container, Button, ActionRow +from discord.ext import commands +import aiosqlite +from utils.Tools import * +from utils.cv2 import CV2, build_container +from utils.config import OWNER_IDS_STR + + +class EmergencyRestoreConfirmView(LayoutView): + def __init__(self, ctx): + super().__init__(timeout=60) + self.ctx = ctx + self.value = None + + self.yes_btn = Button(label="Yes", style=discord.ButtonStyle.green) + self.no_btn = Button(label="No", style=discord.ButtonStyle.danger) + + self.yes_btn.callback = self.confirm_callback + self.no_btn.callback = self.cancel_callback + + self.add_item( + build_container( + TextDisplay("**Confirm Restoration**"), + Separator(visible=True), + TextDisplay( + "This will restore previously disabled permissions for emergency roles. Do you want to proceed?" + ), + ActionRow(self.yes_btn, self.no_btn), + ) + ) + + async def confirm_callback(self, interaction: discord.Interaction): + if interaction.user.id != self.ctx.author.id: + return await interaction.response.send_message( + "Only the Server Owner can use this button.", ephemeral=True + ) + self.value = True + await interaction.response.defer() + self.stop() + + async def cancel_callback(self, interaction: discord.Interaction): + if interaction.user.id != self.ctx.author.id: + return await interaction.response.send_message( + "Only the Server Owner can use this button.", ephemeral=True + ) + self.value = False + await interaction.response.defer() + self.stop() + + +class EmergencyMainView(LayoutView): + def __init__(self, ctx, prefix): + super().__init__(timeout=None) + self.prefix = prefix + self.add_item( + build_container( + TextDisplay("__Emergency Situation__"), + Separator(visible=True), + TextDisplay( + f"The `emergency` command group is designed to protect your server from malicious activity or accidental damage.\n\n" + f"**The command group has several subcommands:**\n\n" + f"`{prefix}emergency enable` - Enable emergency mode\n" + f"`{prefix}emergency disable` - Disable emergency mode\n" + f"`{prefix}emergency authorise` - Manage authorized users\n" + f"`{prefix}emergency role` - Manage roles in emergency list\n" + f"`{prefix}emergency-situation` - Execute emergency situation" + ), + ) + ) + + +class EnableSuccessView(LayoutView): + def __init__(self, roles_added): + super().__init__(timeout=None) + content = ( + "\n".join([f"{r.mention}" for r in roles_added]) + if roles_added + else "No new roles with dangerous permissions were found." + ) + self.add_item( + build_container( + TextDisplay(f"**{TICK} Success**"), + Separator(visible=True), + TextDisplay(content), + ) + ) + + +class EnableErrorView(LayoutView): + def __init__(self): + super().__init__(timeout=None) + self.add_item( + build_container( + TextDisplay(f"**{CROSS} Error**"), + Separator(visible=True), + TextDisplay("Only the server owner can enable emergency mode."), + ) + ) + + +class DisableSuccessView(LayoutView): + def __init__(self): + super().__init__(timeout=None) + self.add_item( + build_container( + TextDisplay(f"**{TICK_ALT} Success**"), + Separator(visible=True), + TextDisplay( + "Emergency mode has been disabled, and all emergency roles have been cleared." + ), + ) + ) + + +class DisableErrorView(LayoutView): + def __init__(self): + super().__init__(timeout=None) + self.add_item( + build_container( + TextDisplay(f"**{CROSS_ALT} Error**"), + Separator(visible=True), + TextDisplay("Only the server owner can disable emergency mode."), + ) + ) + + +class AuthoriseSuccessView(LayoutView): + def __init__(self, member_name, action): + super().__init__(timeout=None) + self.add_item( + build_container( + TextDisplay(f"**{TICK_ALT} Success**"), + Separator(visible=True), + TextDisplay(f"**{member_name}** has been {action}."), + ) + ) + + +class AuthoriseErrorView(LayoutView): + def __init__(self, error_type): + super().__init__(timeout=None) + content = { + "owner_add": "Only the server owner can add authorised users.", + "owner_remove": "Only the server owner can remove authorised users.", + "owner_list": "Only the server owner can view the list.", + "limit": "Only up to 5 authorised users can be added.", + "exists": "This user is already authorised.", + "not_found": "This user is not authorised.", + }.get(error_type, "An error occurred.") + is_warning = error_type == "limit" + self.add_item( + build_container( + TextDisplay( + f"**{ZWARNING} Access Denied**" + if is_warning + else f"**{CROSS_ALT} Error**" + ), + Separator(visible=True), + TextDisplay(content), + ) + ) + + +class AuthoriseListView(LayoutView): + def __init__(self, ctx, users, is_owner): + super().__init__(timeout=None) + if not is_owner: + self.add_item( + build_container( + TextDisplay(f"**{ZWARNING} Access Denied**"), + Separator(visible=True), + TextDisplay("Only the server owner can view the list."), + ) + ) + elif not users: + self.add_item( + build_container( + TextDisplay("**Authorized Users**"), + Separator(visible=True), + TextDisplay("No authorized users found."), + ) + ) + else: + desc = "\n".join( + [ + f"{i + 1}. [{ctx.guild.get_member(u[0]).name}](https://discord.com/users/{u[0]}) - {u[0]}" + for i, u in enumerate(users) + ] + ) + self.add_item( + build_container( + TextDisplay("**Authorized Users**"), + Separator(visible=True), + TextDisplay(desc), + ) + ) + + +class RoleSuccessView(LayoutView): + def __init__(self, role_name, action): + super().__init__(timeout=None) + self.add_item( + build_container( + TextDisplay(f"**{TICK_ALT} Success**"), + Separator(visible=True), + TextDisplay(f"**{role_name}** has been {action} the emergency list."), + ) + ) + + +class RoleErrorView(LayoutView): + def __init__(self, error_type): + super().__init__(timeout=None) + content = { + "owner_add": "Only the server owner can add role for emergency situation.", + "owner_remove": "Only the server owner can remove roles from emergency list.", + "owner_list": "You are not authorised to view list of roles.", + "limit": "Only up to 25 roles can be added.", + "exists": "This role is already in the emergency list.", + "not_found": "This role is not in the emergency list.", + }.get(error_type, "An error occurred.") + is_warning = error_type == "limit" + self.add_item( + build_container( + TextDisplay( + f"**{ZWARNING} Error**" + if is_warning + else f"**{CROSS_ALT} Error**" + ), + Separator(visible=True), + TextDisplay(content), + ) + ) + + +class RoleListView(LayoutView): + def __init__(self, roles, is_authorised): + super().__init__(timeout=None) + if not is_authorised: + self.add_item( + build_container( + TextDisplay(f"**{ZWARNING} Access Denied**"), + Separator(visible=True), + TextDisplay("You are not authorised to view list of roles."), + ) + ) + elif not roles: + self.add_item( + build_container( + TextDisplay("**Emergency Roles**"), + Separator(visible=True), + TextDisplay("No roles added for emergency situation."), + ) + ) + else: + desc = "\n".join( + [f"{i + 1}. <@&{r[0]}> - {r[0]}" for i, r in enumerate(roles)] + ) + self.add_item( + build_container( + TextDisplay("**Emergency Roles**"), + Separator(visible=True), + TextDisplay(desc), + ) + ) + + +class EmergencySituationErrorView(LayoutView): + def __init__(self, error_type): + super().__init__(timeout=None) + content = ( + "You are not authorised to execute the emergency situation." + if error_type == "access" + else "No roles have been added for the emergency situation." + ) + self.add_item( + build_container( + TextDisplay( + f"**{ZWARNING} Access Denied**" + if error_type == "access" + else f"**{CROSS_ALT} Error**" + ), + Separator(visible=True), + TextDisplay(content), + ) + ) + + +class EmergencySituationResultView(LayoutView): + def __init__( + self, + success_msg, + error_msg, + moved_role=None, + move_failed=False, + move_error=None, + ): + super().__init__(timeout=None) + desc = f"**{TICK_ALT} Roles Modified**:\n{success_msg}\n\n" + if moved_role: + desc += f"**{ZWARNING} Role Moved**: {moved_role.mention} moved below bot's top role.\n\n" + elif move_failed: + desc += "**ℹ️ Role Couldn't Moved**: Permission error.\n\n" + elif move_error: + desc += f"**ℹ️ Role Couldn't Moved**: {move_error}\n\n" + desc += f"**Errors**: {error_msg}" + self.add_item( + build_container( + TextDisplay("**Emergency Situation**"), + Separator(visible=True), + TextDisplay(desc), + ) + ) + + +class EmergencyRestoreAccessErrorView(LayoutView): + def __init__(self): + super().__init__(timeout=None) + self.add_item( + build_container( + TextDisplay(f"**{ZWARNING} Access Denied**"), + Separator(visible=True), + TextDisplay( + "Only the server owner can execute the emergency restore command." + ), + ) + ) + + +class EmergencyRestoreNoRolesView(LayoutView): + def __init__(self): + super().__init__(timeout=None) + self.add_item( + build_container( + TextDisplay(f"**{CROSS_ALT} Error**"), + Separator(visible=True), + TextDisplay( + "No roles were found with disabled permissions for restore." + ), + ) + ) + + +class EmergencyRestoreResultView(LayoutView): + def __init__(self, success_msg, error_msg): + super().__init__(timeout=None) + self.add_item( + build_container( + TextDisplay("**Emergency Restore**"), + Separator(visible=True), + TextDisplay( + f"**{TICK_ALT} Permissions Restored**:\n{success_msg}\n\n**{ML_CROSS} Errors**:\n{error_msg}\n\nDatabase cleared." + ), + ) + ) + + +class Emergency(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.db_path = "db/emergency.db" + self.bot.loop.create_task(self.initialize_database()) + + async def initialize_database(self): + async with aiosqlite.connect(self.db_path) as db: + await db.execute( + "CREATE TABLE IF NOT EXISTS authorised_users (guild_id INTEGER, user_id INTEGER)" + ) + await db.execute( + "CREATE TABLE IF NOT EXISTS emergency_roles (guild_id INTEGER, role_id INTEGER)" + ) + await db.execute( + "CREATE TABLE IF NOT EXISTS restore_roles (guild_id INTEGER NOT NULL, role_id INTEGER NOT NULL, disabled_perms TEXT NOT NULL, PRIMARY KEY (guild_id, role_id))" + ) + await db.execute( + "CREATE TABLE IF NOT EXISTS role_positions (guild_id INTEGER, role_id INTEGER, previous_position INTEGER)" + ) + await db.commit() + + async def is_guild_owner(self, ctx): + return ctx.guild and ctx.author.id == ctx.guild.owner_id + + async def is_guild_owner_or_authorised(self, ctx): + if await self.is_guild_owner(ctx): + return True + async with aiosqlite.connect(self.db_path) as db: + async with db.execute( + "SELECT 1 FROM authorised_users WHERE guild_id = ? AND user_id = ?", + (ctx.guild.id, ctx.author.id), + ) as cursor: + return await cursor.fetchone() is not None + + @commands.group(name="emergency", aliases=["emg"], invoke_without_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 4, commands.BucketType.user) + @commands.guild_only() + async def emergency(self, ctx): + await ctx.reply(view=EmergencyMainView(ctx, ctx.prefix)) + + @emergency.command(name="enable") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 4, commands.BucketType.user) + @commands.guild_only() + async def enable(self, ctx): + if ctx.author.id != ctx.guild.owner_id and str(ctx.author.id) not in OWNER_IDS_STR: + return await ctx.reply(view=EnableErrorView()) + dangerous_perms = [ + "administrator", + "ban_members", + "kick_members", + "manage_channels", + "manage_roles", + "manage_guild", + ] + roles_added = [] + async with aiosqlite.connect(self.db_path) as db: + for role in ctx.guild.roles: + if ( + role.managed + or role.is_bot_managed() + or role.position >= ctx.guild.me.top_role.position + ): + continue + if any(getattr(role.permissions, p, False) for p in dangerous_perms): + async with db.execute( + "SELECT 1 FROM emergency_roles WHERE guild_id = ? AND role_id = ?", + (ctx.guild.id, role.id), + ) as cursor: + if not await cursor.fetchone(): + await db.execute( + "INSERT INTO emergency_roles (guild_id, role_id) VALUES (?, ?)", + (ctx.guild.id, role.id), + ) + roles_added.append(role) + await db.commit() + await ctx.reply(view=EnableSuccessView(roles_added)) + + @emergency.command(name="disable") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 4, commands.BucketType.user) + @commands.guild_only() + async def disable(self, ctx): + if ctx.author.id != ctx.guild.owner_id and str(ctx.author.id) not in OWNER_IDS_STR: + return await ctx.reply(view=DisableErrorView()) + async with aiosqlite.connect(self.db_path) as db: + await db.execute( + "DELETE FROM emergency_roles WHERE guild_id = ?", (ctx.guild.id,) + ) + await db.commit() + await ctx.reply(view=DisableSuccessView()) + + @emergency.group(name="authorise", aliases=["ath"], invoke_without_command=True) + @blacklist_check() + @ignore_check() + @commands.guild_only() + async def authorise(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @authorise.command(name="add") + @blacklist_check() + @ignore_check() + @commands.guild_only() + async def authorise_add(self, ctx, member: discord.Member): + if not await self.is_guild_owner(ctx): + return await ctx.reply(view=AuthoriseErrorView("owner_add")) + async with aiosqlite.connect(self.db_path) as db: + async with db.execute( + "SELECT COUNT(*) FROM authorised_users WHERE guild_id = ?", + (ctx.guild.id,), + ) as cursor: + if (await cursor.fetchone())[0] >= 5: + return await ctx.reply(view=AuthoriseErrorView("limit")) + async with db.execute( + "SELECT 1 FROM authorised_users WHERE guild_id = ? AND user_id = ?", + (ctx.guild.id, member.id), + ) as cursor: + if await cursor.fetchone(): + return await ctx.reply(view=AuthoriseErrorView("exists")) + await db.execute( + "INSERT INTO authorised_users (guild_id, user_id) VALUES (?, ?)", + (ctx.guild.id, member.id), + ) + await db.commit() + await ctx.reply(view=AuthoriseSuccessView(member.display_name, "authorised")) + + @authorise.command(name="remove") + @blacklist_check() + @ignore_check() + @commands.guild_only() + async def authorise_remove(self, ctx, member: discord.Member): + if not await self.is_guild_owner(ctx): + return await ctx.reply(view=AuthoriseErrorView("owner_remove")) + async with aiosqlite.connect(self.db_path) as db: + async with db.execute( + "SELECT 1 FROM authorised_users WHERE guild_id = ? AND user_id = ?", + (ctx.guild.id, member.id), + ) as cursor: + if not await cursor.fetchone(): + return await ctx.reply(view=AuthoriseErrorView("not_found")) + await db.execute( + "DELETE FROM authorised_users WHERE guild_id = ? AND user_id = ?", + (ctx.guild.id, member.id), + ) + await db.commit() + await ctx.reply(view=AuthoriseSuccessView(member.display_name, "removed")) + + @authorise.command(name="list", aliases=["view"]) + @blacklist_check() + @ignore_check() + @commands.guild_only() + async def list_authorized(self, ctx): + is_owner = await self.is_guild_owner(ctx) + if not is_owner: + return await ctx.reply(view=AuthoriseErrorView("owner_list")) + async with aiosqlite.connect("db/emergency.db") as db: + cursor = await db.execute( + "SELECT user_id FROM authorised_users WHERE guild_id = ?", + (ctx.guild.id,), + ) + users = await cursor.fetchall() + await ctx.reply(view=AuthoriseListView(ctx, users, is_owner)) + + @emergency.group(name="role", invoke_without_command=True) + @blacklist_check() + @ignore_check() + @commands.guild_only() + async def role(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @role.command(name="add") + @blacklist_check() + @ignore_check() + @commands.guild_only() + async def role_add(self, ctx, role: discord.Role): + if not await self.is_guild_owner(ctx): + return await ctx.reply(view=RoleErrorView("owner_add")) + async with aiosqlite.connect(self.db_path) as db: + async with db.execute( + "SELECT COUNT(*) FROM emergency_roles WHERE guild_id = ?", + (ctx.guild.id,), + ) as cursor: + if (await cursor.fetchone())[0] >= 25: + return await ctx.reply(view=RoleErrorView("limit")) + async with db.execute( + "SELECT 1 FROM emergency_roles WHERE guild_id = ? AND role_id = ?", + (ctx.guild.id, role.id), + ) as cursor: + if await cursor.fetchone(): + return await ctx.reply(view=RoleErrorView("exists")) + await db.execute( + "INSERT INTO emergency_roles (guild_id, role_id) VALUES (?, ?)", + (ctx.guild.id, role.id), + ) + await db.commit() + await ctx.reply(view=RoleSuccessView(role.name, "added to")) + + @role.command(name="remove") + @blacklist_check() + @ignore_check() + @commands.guild_only() + async def role_remove(self, ctx, role: discord.Role): + if not await self.is_guild_owner(ctx): + return await ctx.reply(view=RoleErrorView("owner_remove")) + async with aiosqlite.connect(self.db_path) as db: + async with db.execute( + "SELECT 1 FROM emergency_roles WHERE guild_id = ? AND role_id = ?", + (ctx.guild.id, role.id), + ) as cursor: + if not await cursor.fetchone(): + return await ctx.reply(view=RoleErrorView("not_found")) + await db.execute( + "DELETE FROM emergency_roles WHERE guild_id = ? AND role_id = ?", + (ctx.guild.id, role.id), + ) + await db.commit() + await ctx.reply(view=RoleSuccessView(role.name, "removed from")) + + @role.command(name="list", aliases=["view"]) + @blacklist_check() + @ignore_check() + @commands.guild_only() + async def list_roles(self, ctx): + is_auth = await self.is_guild_owner_or_authorised(ctx) + if not is_auth: + return await ctx.reply(view=RoleErrorView("owner_list")) + async with aiosqlite.connect("db/emergency.db") as db: + cursor = await db.execute( + "SELECT role_id FROM emergency_roles WHERE guild_id = ?", + (ctx.guild.id,), + ) + roles = await cursor.fetchall() + await ctx.reply(view=RoleListView(roles, is_auth)) + + @commands.command(name="emergencysituation", aliases=["emgs"]) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 40, commands.BucketType.user) + @commands.guild_only() + @commands.bot_has_permissions(manage_roles=True) + async def emergencysituation(self, ctx): + if not await self.is_guild_owner_or_authorised(ctx) and str( + ctx.author.id + ) not in OWNER_IDS_STR: + return await ctx.reply(view=EmergencySituationErrorView("access")) + + proc_msg = await ctx.reply( + view=LayoutView(build_container(TextDisplay("Processing Emergency Situation..."))) + ) + guild_id = ctx.guild.id + + antinuke_enabled = False + async with aiosqlite.connect("db/anti.db") as anti: + cursor = await anti.execute( + "SELECT status FROM antinuke WHERE guild_id = ?", (guild_id,) + ) + row = await cursor.fetchone() + if row: + antinuke_enabled = True + await anti.execute( + "DELETE FROM antinuke WHERE guild_id = ?", (guild_id,) + ) + await anti.commit() + + async with aiosqlite.connect(self.db_path) as db: + await db.execute( + "DELETE FROM restore_roles WHERE guild_id = ?", (ctx.guild.id,) + ) + await db.commit() + cursor = await db.execute( + "SELECT role_id FROM emergency_roles WHERE guild_id = ?", + (ctx.guild.id,), + ) + emergency_roles = await cursor.fetchall() + + if not emergency_roles: + await proc_msg.delete() + return await ctx.reply(view=EmergencySituationErrorView("no_roles")) + + bot_top = ctx.guild.me.top_role + dangerous_perms = [ + "administrator", + "ban_members", + "kick_members", + "manage_channels", + "manage_roles", + "manage_guild", + ] + modified, unchanged = [], [] + + async with aiosqlite.connect(self.db_path) as db: + for (role_id,) in emergency_roles: + role = ctx.guild.get_role(role_id) + if not role or role.position >= bot_top.position or role.managed: + unchanged.append(role) if role else None + continue + perms = role.permissions + disabled = [] + for p in dangerous_perms: + if getattr(perms, p, False): + setattr(perms, p, False) + disabled.append(p) + if disabled: + try: + await role.edit(permissions=perms, reason="Emergency Situation") + modified.append(role) + await db.execute( + "INSERT INTO restore_roles VALUES (?, ?, ?)", + (ctx.guild.id, role.id, ",".join(disabled)), + ) + await db.commit() + except: + unchanged.append(role) + + success = "\n".join([r.mention for r in modified]) or "No roles modified." + errors = "\n".join([r.mention for r in unchanged]) or "No errors." + + most_mem = max( + [ + r + for r in ctx.guild.roles + if not r.managed + and r.position < bot_top.position + and r != ctx.guild.default_role + ], + key=lambda r: len(r.members), + default=None, + ) + + if most_mem: + try: + await most_mem.edit( + position=bot_top.position - 1, reason="Emergency Situation" + ) + result_view = EmergencySituationResultView( + success, errors, moved_role=most_mem + ) + except discord.Forbidden: + result_view = EmergencySituationResultView( + success, errors, move_failed=True + ) + except Exception as e: + result_view = EmergencySituationResultView( + success, errors, move_error=str(e) + ) + else: + result_view = EmergencySituationResultView(success, errors) + + await ctx.reply(view=result_view) + + if antinuke_enabled: + async with aiosqlite.connect("db/anti.db") as anti: + await anti.execute("INSERT INTO antinuke VALUES (?, 1)", (guild_id,)) + await anti.commit() + await proc_msg.delete() + + @commands.command(name="emergencyrestore", aliases=["emgrestore"]) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 30, commands.BucketType.user) + @commands.guild_only() + @commands.bot_has_permissions(manage_roles=True) + async def emergencyrestore(self, ctx): + if ctx.author.id != ctx.guild.owner_id and str(ctx.author.id) not in OWNER_IDS_STR: + return await ctx.reply(view=EmergencyRestoreAccessErrorView()) + + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute( + "SELECT role_id, disabled_perms FROM restore_roles WHERE guild_id = ?", + (ctx.guild.id,), + ) + restore_roles = await cursor.fetchall() + + if not restore_roles: + return await ctx.reply(view=EmergencyRestoreNoRolesView()) + + view = EmergencyRestoreConfirmView(ctx) + await ctx.send(view=view) + await view.wait() + + if view.value is None: + return await ctx.reply( + view=LayoutView( + build_container(TextDisplay("**Restore Cancelled** - Timed out.")) + ) + ) + if view.value is False: + return await ctx.reply( + view=LayoutView(build_container(TextDisplay("**Restore Cancelled**"))) + ) + + modified, unchanged = [], [] + async with aiosqlite.connect(self.db_path) as db: + for role_id, perms in restore_roles: + role = ctx.guild.get_role(role_id) + if not role: + continue + rp = role.permissions + restored = False + for p in perms.split(","): + if hasattr(rp, p): + setattr(rp, p, True) + restored = True + if restored: + try: + await role.edit(permissions=rp, reason="Emergency Restore") + modified.append(role) + except: + unchanged.append(role) + await db.execute( + "DELETE FROM restore_roles WHERE guild_id = ?", (ctx.guild.id,) + ) + await db.commit() + + success = "\n".join([r.mention for r in modified]) or "No roles restored." + errors = "\n".join([r.mention for r in unchanged]) or "No errors." + await ctx.reply(view=EmergencyRestoreResultView(success, errors)) + + +async def setup(bot): + await bot.add_cog(Emergency(bot)) diff --git a/bot/cogs/commands/encryption.py b/bot/cogs/commands/encryption.py new file mode 100644 index 0000000..d9ff6e9 --- /dev/null +++ b/bot/cogs/commands/encryption.py @@ -0,0 +1,224 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import base64 +import binascii +import codecs +import secrets + +import discord +from discord.ui import LayoutView, TextDisplay, Separator, Container +from discord.ext import commands +from utils.cv2 import CV2, build_container + + +class EncryptResultView(LayoutView): + def __init__(self, convert, txtinput): + super().__init__(timeout=None) + + try: + display_text = txtinput.decode("UTF-8") + except AttributeError: + display_text = str(txtinput) + + if len(display_text) > 500: + display_text = display_text[:500] + "..." + + self.add_item( + build_container( + TextDisplay(f"📑 **{convert}**"), + Separator(visible=True), + TextDisplay(display_text), + ) + ) + + +class DecodeErrorView(LayoutView): + def __init__(self, codec_name): + super().__init__(timeout=None) + + self.add_item( + build_container( + TextDisplay(f"❌ **Invalid {codec_name}**"), + Separator(visible=True), + TextDisplay(f"The provided string is not valid {codec_name} encoding."), + ) + ) + + +class PasswordSentView(LayoutView): + def __init__(self, author_name): + super().__init__(timeout=None) + + self.add_item( + build_container( + TextDisplay("🔐 **Password Generated**"), + Separator(visible=True), + TextDisplay( + f"Sending you a DM with your random generated password **{author_name}**" + ), + ) + ) + + +class PasswordDMView(LayoutView): + def __init__(self, password): + super().__init__(timeout=None) + + self.add_item( + build_container( + TextDisplay("🎁 **Here is your password:**"), + Separator(visible=True), + TextDisplay(password), + ) + ) + + +class encryption(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.group(invoke_without_command=True) + async def encode(self, ctx): + """All encode methods""" + await ctx.send_help(ctx.command) + + @commands.group(invoke_without_command=True) + async def decode(self, ctx): + """All decode methods""" + await ctx.send_help(ctx.command) + + async def encryptout(self, ctx, convert, txtinput): + view = EncryptResultView(convert, txtinput) + await ctx.send(view=view) + + @encode.command(name="base32", aliases=["b32"]) + async def encode_base32(self, ctx, *, txtinput: commands.clean_content): + """Encode in base32""" + await self.encryptout( + ctx, "Text -> base32", base64.b32encode(txtinput.encode("UTF-8")) + ) + + @decode.command(name="base32", aliases=["b32"]) + async def decode_base32(self, ctx, *, txtinput: str): + """Decode in base32""" + try: + await self.encryptout( + ctx, "base32 -> Text", base64.b32decode(txtinput.encode("UTF-8")) + ) + except Exception: + await ctx.send(view=DecodeErrorView("base32")) + + @encode.command(name="base64", aliases=["b64"]) + async def encode_base64(self, ctx, *, txtinput: commands.clean_content): + """Encode in base64""" + await self.encryptout( + ctx, "Text -> base64", base64.urlsafe_b64encode(txtinput.encode("UTF-8")) + ) + + @decode.command(name="base64", aliases=["b64"]) + async def decode_base64(self, ctx, *, txtinput: str): + """Decode in base64""" + try: + await self.encryptout( + ctx, + "base64 -> Text", + base64.urlsafe_b64decode(txtinput.encode("UTF-8")), + ) + except Exception: + await ctx.send(view=DecodeErrorView("base64")) + + @encode.command(name="rot13", aliases=["r13"]) + async def encode_rot13(self, ctx, *, txtinput: commands.clean_content): + """Encode in rot13""" + await self.encryptout(ctx, "Text -> rot13", codecs.decode(txtinput, "rot_13")) + + @decode.command(name="rot13", aliases=["r13"]) + async def decode_rot13(self, ctx, *, txtinput: str): + """Decode in rot13""" + try: + await self.encryptout( + ctx, "rot13 -> Text", codecs.decode(txtinput, "rot_13") + ) + except Exception: + await ctx.send(view=DecodeErrorView("rot13")) + + @encode.command(name="hex") + async def encode_hex(self, ctx, *, txtinput: commands.clean_content): + """Encode in hex""" + await self.encryptout( + ctx, "Text -> hex", binascii.hexlify(txtinput.encode("UTF-8")) + ) + + @decode.command(name="hex") + async def decode_hex(self, ctx, *, txtinput: str): + """Decode in hex""" + try: + await self.encryptout( + ctx, "hex -> Text", binascii.unhexlify(txtinput.encode("UTF-8")) + ) + except Exception: + await ctx.send(view=DecodeErrorView("hex")) + + @encode.command(name="base85", aliases=["b85"]) + async def encode_base85(self, ctx, *, txtinput: commands.clean_content): + """Encode in base85""" + await self.encryptout( + ctx, "Text -> base85", base64.b85encode(txtinput.encode("UTF-8")) + ) + + @decode.command(name="base85", aliases=["b85"]) + async def decode_base85(self, ctx, *, txtinput: str): + """Decode in base85""" + try: + await self.encryptout( + ctx, "base85 -> Text", base64.b85decode(txtinput.encode("UTF-8")) + ) + except Exception: + await ctx.send(view=DecodeErrorView("base85")) + + @encode.command(name="ascii85", aliases=["a85"]) + async def encode_ascii85(self, ctx, *, txtinput: commands.clean_content): + """Encode in ASCII85""" + await self.encryptout( + ctx, "Text -> ASCII85", base64.a85encode(txtinput.encode("UTF-8")) + ) + + @decode.command(name="ascii85", aliases=["a85"]) + async def decode_ascii85(self, ctx, *, txtinput: str): + """Decode in ASCII85""" + try: + await self.encryptout( + ctx, "ASCII85 -> Text", base64.a85decode(txtinput.encode("UTF-8")) + ) + except Exception: + await ctx.send(view=DecodeErrorView("ASCII85")) + + @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: + await ctx.send(view=PasswordSentView(ctx.author.name)) + + password = secrets.token_urlsafe(18) + try: + await ctx.author.send(view=PasswordDMView(password)) + except discord.Forbidden: + await ctx.send( + f"❌ Could not send DM. Here is your password: **{password}**" + ) + + +async def setup(bot): + await bot.add_cog(encryption(bot)) diff --git a/bot/cogs/commands/extra.py b/bot/cogs/commands/extra.py new file mode 100644 index 0000000..1d8cbf6 --- /dev/null +++ b/bot/cogs/commands/extra.py @@ -0,0 +1,986 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import os +import discord +from utils.emoji import BOOST, CODEBASE, CROSS, KING, TICK, UPTIME, ZWARNING +from discord.ext import commands +import datetime +import sys +from discord.ui import Button, View, LayoutView, TextDisplay, Separator, Container, ActionRow, MediaGallery +from utils.cv2 import CV2, build_container +import psutil +import time +from utils.Tools import * +from discord.ext import commands, menus +from discord.ext.commands import BucketType, cooldown +import requests +from typing import * +from utils import * +from utils.config import BotName, serverLink +from utils import Paginator, DescriptionEmbedPaginator, FieldPagePaginator, TextPaginator +from core import Cog, zyrox, Context +from typing import Optional +import aiosqlite +import asyncio +import aiohttp + + +start_time = time.time() + + +def datetime_to_seconds(thing: datetime.datetime): + current_time = datetime.datetime.fromtimestamp(time.time()) + return round( + round(time.time()) + + (current_time - thing.replace(tzinfo=None)).total_seconds()) + +tick = f"{TICK}>" +cross = CROSS + + + + +class RoleInfoView(View): + def __init__(self, role: discord.Role, author_id): + super().__init__(timeout=180) + self.role = role + self.author_id = author_id + + @discord.ui.button(label='Show Permissions', emoji=f"{CODEBASE} ", style=discord.ButtonStyle.secondary) + async def show_permissions(self, interaction: discord.Interaction, button: Button): + if interaction.user.id != self.author_id: + await interaction.response.send_message("Uh oh! That message doesn't belong to you. You must run this command to interact with it.", ephemeral=True) + return + + permissions = [perm.replace("_", " ").title() for perm, value in self.role.permissions if value] + permission_text = ", ".join(permissions) if permissions else "None" + await interaction.response.send_message(view=CV2(f"Permissions for {self.role.name}", permission_text or "No permissions."), ephemeral=True) + + +class OverwritesView(View): + def __init__(self, channel, author_id): + super().__init__(timeout=180) + self.channel = channel + self.author_id = author_id + + @discord.ui.button(label='Show Overwrites', style=discord.ButtonStyle.primary) + async def show_overwrites(self, interaction: discord.Interaction, button: Button): + if interaction.user.id != self.author_id: + await interaction.response.send_message("Uh oh! That message doesn't belong to you. You must run this command to interact with it.", ephemeral=True) + return + + overwrites = [] + for target, perms in self.channel.overwrites.items(): + permissions = { + "View Channel": perms.view_channel, + "Send Messages": perms.send_messages, + "Read Message History": perms.read_message_history, + "Manage Messages": perms.manage_messages, + "Embed Links": perms.embed_links, + "Attach Files": perms.attach_files, + "Manage Channels": perms.manage_channels, + "Manage Permissions": perms.manage_permissions, + "Manage Webhooks": perms.manage_webhooks, + "Create Instant Invite": perms.create_instant_invite, + "Add Reactions": perms.add_reactions, + "Mention Everyone": perms.mention_everyone, + "Kick Members": perms.kick_members, + "Ban Members": perms.ban_members, + "Moderate Members": perms.moderate_members, + "Send TTS Messages": perms.send_tts_messages, + "Use External Emojis": perms.external_emojis, + "Use External Stickers": perms.external_stickers, + "View Audit Log": perms.view_audit_log, + "Voice Mute Members": perms.mute_members, + "Voice Deafen Members": perms.deafen_members, + "Administrator": perms.administrator + } + + overwrites.append(f"**For {target.name}**\n" + + "\n".join(f" * **{perm}:** {'{TICK}>' if value else '{CROSS}' if value is False else '⛔'}" for perm, value in permissions.items())) + + ow_text = "\n".join(overwrites) if overwrites else "No overwrites for this channel." + footer = f"{TICK}> = Allowed, {CROSS} = Denied, ⛔ = None" + await interaction.response.send_message(view=CV2(f"Overwrites for {self.channel.name}", ow_text, footer), ephemeral=True) + + + + +class Extra(commands.Cog): + + def __init__(self, bot): + self.bot = bot + self.color = 0xFF0000 + self.start_time = datetime.datetime.now() + + @commands.hybrid_group(name="banner") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def banner(self, ctx): + if ctx.invoked_subcommand is None: + await ctx.send_help(ctx.command) + + @banner.command(name="server") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def server(self, ctx): + if not ctx.guild.banner: + await ctx.reply(view=CV2(f"{cross} Error", "This server doesn't have a banner.")) + else: + webp = ctx.guild.banner.replace(format='webp') + jpg = ctx.guild.banner.replace(format='jpg') + png = ctx.guild.banner.replace(format='png') + links = f"[`PNG`]({png}) | [`JPG`]({jpg}) | [`WEBP`]({webp})" + if ctx.guild.banner.is_animated(): + links += f" | [`GIF`]({ctx.guild.banner.replace(format='gif')})" + view = LayoutView(timeout=None) + gallery = MediaGallery() + gallery.add_item(media=str(ctx.guild.banner.url)) + view.add_item(build_container( + TextDisplay(f"**{ctx.guild.name}**"), + Separator(visible=True), + TextDisplay(links), + gallery + )) + await ctx.reply(view=view) + + + @banner.command(name="user") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + async def _user(self, + ctx, + member: Optional[Union[discord.Member, + discord.User]] = None): + if member == None or member == "": + member = ctx.author + bannerUser = await self.bot.fetch_user(member.id) + if not bannerUser.banner: + await ctx.reply(view=CV2(f"{cross} Error", f"{member} doesn't have a banner.")) + else: + webp = bannerUser.banner.replace(format='webp') + jpg = bannerUser.banner.replace(format='jpg') + png = bannerUser.banner.replace(format='png') + links = f"[`PNG`]({png}) | [`JPG`]({jpg}) | [`WEBP`]({webp})" + if bannerUser.banner.is_animated(): + links += f" | [`GIF`]({bannerUser.banner.replace(format='gif')})" + view = LayoutView(timeout=None) + gallery = MediaGallery() + gallery.add_item(media=str(bannerUser.banner.url)) + view.add_item(build_container( + TextDisplay(f"**{member}**"), + Separator(visible=True), + TextDisplay(links), + gallery + )) + await ctx.send(view=view) + + + + + + @commands.command(name="uptime", description="Shows the Bot's Uptime.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def uptime(self, ctx): + pfp = ctx.author.display_avatar.url + + uptime_seconds = int(round(time.time() - start_time)) + uptime_timedelta = datetime.timedelta(seconds=uptime_seconds) + + uptime_string = f"Up since {datetime.datetime.utcfromtimestamp(start_time).strftime('%Y-%m-%d %H:%M:%S')} UTC" + uptime_duration_string = f"{uptime_timedelta.days} days, {uptime_timedelta.seconds // 3600} hours, {(uptime_timedelta.seconds // 60) % 60} minutes, {uptime_timedelta.seconds % 60} seconds" + + uptime_text = ( + f"**__UTC__**\n{ZWARNING} {uptime_string}\n\n" + f"**__Online Duration__**\n{UPTIME} {uptime_duration_string}" + ) + await ctx.send(view=CV2(f"{BRAND_NAME} Manager Uptime", uptime_text)) + + + + @commands.hybrid_command(name="serverinfo", + aliases=["sinfo", "si"], + with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def serverinfo(self, ctx): + c_at = ctx.guild.created_at.strftime("%Y-%m-%d %H:%M:%S") + about = ( + f"**Name:** {ctx.guild.name}\n" + f"**ID:** {ctx.guild.id}\n" + f"**Owner {KING}:** {ctx.guild.owner} (<@{ctx.guild.owner_id}>)\n" + f"**Created At:** {c_at}\n" + f"**Members:** {len(ctx.guild.members)}" + ) + desc_section = f"\n\n**__Description__**\n{ctx.guild.description}" if ctx.guild.description else "" + stats = ( + f"**Verification Level:** {ctx.guild.verification_level}\n" + f"**Channels:** {len(ctx.guild.channels)}\n" + f"**Roles:** {len(ctx.guild.roles)}\n" + f"**Emojis:** {len(ctx.guild.emojis)}\n" + f"**Boost Status:** Level {ctx.guild.premium_tier} (Boosts: {ctx.guild.premium_subscription_count})" + ) + features_text = "" + if ctx.guild.features: + features = "\n".join([f"{TICK}>: {f[:1].upper() + f[1:].lower().replace('_', ' ')}" for f in ctx.guild.features]) + features_text = f"\n\n**__Features__**\n{features[:1000] if len(features) > 1024 else features}" + regular_emojis = [e for e in ctx.guild.emojis if not e.animated] + animated_emojis = [e for e in ctx.guild.emojis if e.animated] + emoji_info = f"Regular: {len(regular_emojis)}/100 | Animated: {len(animated_emojis)}/100 | Total: {len(ctx.guild.emojis)}/200" + roles = ctx.guild.roles + roles_display = "\n".join([r.mention for r in roles[:10]]) + if len(roles) > 10: + roles_display += f"\n...and {len(roles) - 10} more" + full_text = ( + f"**__About__**\n{about}{desc_section}\n\n" + f"**__General Stats__**\n{stats}{features_text}\n\n" + f"**__Channels__**\n**Total:** {len(ctx.guild.channels)} — {len(ctx.guild.text_channels)} text, {len(ctx.guild.voice_channels)} voice\n\n" + f"**__Emoji Info__**\n{emoji_info}\n\n" + f"**__Boost Status__**\nLevel: {ctx.guild.premium_tier} [{BOOST}{ctx.guild.premium_subscription_count} boosts]\n\n" + f"**__Server Roles__ [{len(roles)}]**\n{roles_display}" + ) + icon_url = ctx.guild.icon.url if ctx.guild.icon else None + view = LayoutView(timeout=None) + items = [TextDisplay(f"# {ctx.guild.name}'s Information"), Separator(visible=True), TextDisplay(full_text)] + if icon_url or ctx.guild.banner: + gallery = MediaGallery() + if icon_url: + gallery.add_item(media=str(icon_url)) + if ctx.guild.banner: + gallery.add_item(media=str(ctx.guild.banner.url)) + items.append(gallery) + view.add_item(build_container(*items)) + await ctx.send(view=view) + + + + @commands.hybrid_command(name="userinfo", + aliases=["whois", "ui"], + usage="Userinfo [user]", + with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + async def _userinfo(self, + ctx, + member: Optional[Union[discord.Member, + discord.User]] = None): + if member == None or member == "": + member = ctx.author + elif member not in ctx.guild.members: + member = await self.bot.fetch_user(member.id) + + badges = "" + if member.public_flags.hypesquad: + badges += "HypeSquad Events, " + if member.public_flags.hypesquad_balance: + badges += "HypeSquad Balance, " + if member.public_flags.hypesquad_bravery: + badges += "HypeSquad Bravery, " + if member.public_flags.hypesquad_brilliance: + badges += "HypeSquad Brilliance, " + if member.public_flags.early_supporter: + badges += "Early Supporter, " + if member.public_flags.active_developer: + badges += "Active Developer, " + if member.public_flags.verified_bot_developer: + badges += "Early Verified Bot Developer, " + if member.public_flags.discord_certified_moderator: + badges += "Moderators Program Alumni, " + if member.public_flags.staff: + badges += "Discord Staff, " + if member.public_flags.partner: + badges += "Partnered Server Owner " + if badges == None or badges == "": + badges += f"{cross}" + + if member in ctx.guild.members: + nickk = f"{member.nick if member.nick else 'None'}" + joinedat = f"" + else: + nickk = "None" + joinedat = "None" + + kp = "" + if member in ctx.guild.members: + if member.guild_permissions.kick_members: + kp += "Kick Members" + if member.guild_permissions.ban_members: + kp += " , Ban Members" + if member.guild_permissions.administrator: + kp += " , Administrator" + if member.guild_permissions.manage_channels: + kp += " , Manage Channels" + + if member.guild_permissions.manage_guild: + kp += " , Manage Server" + + if member.guild_permissions.manage_messages: + kp += " , Manage Messages" + if member.guild_permissions.mention_everyone: + kp += " , Mention Everyone" + if member.guild_permissions.manage_nicknames: + kp += " , Manage Nicknames" + if member.guild_permissions.manage_roles: + kp += " , Manage Roles" + if member.guild_permissions.manage_webhooks: + kp += " , Manage Webhooks" + if member.guild_permissions.manage_emojis: + kp += " , Manage Emojis" + + if kp is None or kp == "": + kp = "None" + + if member in ctx.guild.members: + if member == ctx.guild.owner: + aklm = "Server Owner" + elif member.guild_permissions.administrator: + aklm = "Server Admin" + elif member.guild_permissions.ban_members or member.guild_permissions.kick_members: + aklm = "Server Moderator" + else: + aklm = "Server Member" + + bannerUser = await self.bot.fetch_user(member.id) + embed = discord.Embed(color=self.color) + embed.timestamp = discord.utils.utcnow() + if not bannerUser.banner: + pass + else: + embed.set_image(url=bannerUser.banner) + embed.set_author(name=f"{member.name}'s Information", + icon_url=member.avatar.url + if member.avatar else member.default_avatar.url) + embed.set_thumbnail( + url=member.avatar.url if member.avatar else member.default_avatar.url) + embed.add_field(name="__General Information__", + value=f""" +**Name:** {member} +**ID:** {member.id} +**Nickname:** {nickk} +**Bot?:** {'{TICK}> Yes' if member.bot else '{CROSS} No'} +**Badges:** {badges} +**Account Created:** +**Server Joined:** {joinedat} + """, + inline=False) + if member in ctx.guild.members: + r = (', '.join(role.mention for role in member.roles[1:][::-1]) + if len(member.roles) > 1 else 'None.') + embed.add_field(name="__Role Info__", + value=f""" +**Highest Role:** {member.top_role.mention if len(member.roles) > 1 else 'None'} +**Roles [{f'{len(member.roles) - 1}' if member.roles else '0'}]:** {r if len(r) <= 1024 else r[0:1006] + ' and more...'} +**Color:** {member.color if member.color else '99aab5'} + """, + inline=False) + if member in ctx.guild.members: + embed.add_field( + name="__Extra__", + value= + f"**Boosting:** {f'' if member in ctx.guild.premium_subscribers else 'None'}\n**Voice :** {'None' if not member.voice else member.voice.channel.mention}", + inline=False) + if member in ctx.guild.members: + embed.add_field(name="__Key Permissions__", + value=", ".join([kp]), + inline=False) + if member in ctx.guild.members: + embed.add_field(name="__Acknowledgement__", + value=f"{aklm}", + inline=False) + if member in ctx.guild.members: + embed.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url + if ctx.author.avatar else ctx.author.default_avatar.url) + else: + if member not in ctx.guild.members: + embed.set_footer(text=f"{member.name} not in this server.", + icon_url=ctx.author.avatar.url if ctx.author.avatar + else ctx.author.default_avatar.url) + await ctx.send(embed=embed) + + + + @commands.hybrid_command(name='roleinfo', aliases=["ri"], help="Displays information about a specified role.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def roleinfo(self, ctx, role: discord.Role): + members = role.members + created_at = role.created_at.strftime("%Y-%m-%d %H:%M:%S") + total_roles = len(ctx.guild.roles) + role_position = total_roles - role.position + role_text = ( + f"**__General Information__**\n" + f"**ID:** {role.id}\n**Name:** {role.name}\n**Mention:** <@&{role.id}>\n" + f"**Color:** {str(role.color)}\n**Total Members:** {len(role.members)}\n\n" + f"**Position:** {role_position}\n**Mentionable:** {role.mentionable}\n" + f"**Hoisted:** {role.hoist}\n**Managed:** {role.managed}\n**Created At:** {created_at}" + ) + view = RoleInfoView(role, ctx.author.id) + await ctx.send(content=role_text, view=view) + + + + + + @commands.command(name="boostcount", + help="Shows boosts count", + usage="boosts", + aliases=["bco"], + with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def boosts(self, ctx): + await ctx.send(view=CV2(f"{BOOST} Boosts Count Of {ctx.guild.name}", f"**Total `{ctx.guild.premium_subscription_count}` boosts**")) + + @commands.hybrid_group(name="list", + invoke_without_command=True, + with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def __list_(self, ctx: commands.Context): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @__list_.command(name="boosters", + aliases=["boost", "booster"], + usage="List boosters", + help="List of boosters in the Guild", + with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def list_boost(self, ctx): + guild = ctx.guild + entries = [ + f"`#{no}.` [{mem}](https://discord.com/users/{mem.id}) [{mem.mention}] - " + for no, mem in enumerate(guild.premium_subscribers, start=1) + ] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + title= + f"List of Boosters in {guild.name} - {len(guild.premium_subscribers)}", + description="", + per_page=10), + ctx=ctx) + await paginator.paginate() + + @__list_.command(name="bans", help= "List of all banned members in Guild", aliases=["ban"], with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(view_audit_log=True) + @commands.bot_has_permissions(view_audit_log=True) + async def list_ban(self, ctx): + bans = [member async for member in ctx.guild.bans()] + if len(bans) == 0: + return await ctx.reply("There aren't any banned users in this guild.", mention_author=False) + else: + mems = ([ + member async for member in ctx.guild.bans() + ]) + guild = ctx.guild + entries = [ + f"`#{no}.` {mem}" + for no, mem in enumerate(mems, start=1) + ] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + title=f"Banned Users in {guild.name} - {len(bans)}", + description="", + per_page=10), + ctx=ctx) + await paginator.paginate() + + @__list_.command( + name="inrole", + aliases=["inside-role"], + help="List of members that are in the specified role", + with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def list_inrole(self, ctx, role: discord.Role): + guild = ctx.guild + entries = [ + f"`#{no}.` [{mem}](https://discord.com/users/{mem.id}) [{mem.mention}] - " + for no, mem in enumerate(role.members, start=1) + ] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + title=f"List of Members in {role} - {len(role.members)}", + description="", + per_page=10), + ctx=ctx) + await paginator.paginate() + + @__list_.command(name="emojis", + aliases=["emoji"], + help="List of emojis in the Guild with ids", + with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def list_emojis(self, ctx): + guild = ctx.guild + entries = [ + f"`#{no}.` {e} - `{e}`" + for no, e in enumerate(ctx.guild.emojis, start=1) + ] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + title=f"List of Emojis in {guild.name} - {len(ctx.guild.emojis)}", + description="", + per_page=10), + ctx=ctx) + await paginator.paginate() + + @__list_.command(name="roles", + aliases=["role"], + help="List of all roles in the server with ids", + with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(manage_roles=True) + async def list_roles(self, ctx): + guild = ctx.guild + entries = [ + f"`#{no}.` {e.mention} - `[{e.id}]`" + for no, e in enumerate(ctx.guild.roles, start=1) + ] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + title=f"List of Roles in {guild.name} - {len(ctx.guild.roles)}", + description="", + per_page=10), + ctx=ctx) + await paginator.paginate() + + @__list_.command(name="bots", + aliases=["bot"], + help="List of All Bots in a server", + with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def list_bots(self, ctx): + guild = ctx.guild + people = filter(lambda member: member.bot, ctx.guild.members) + people = sorted(people, key=lambda member: member.joined_at) + entries = [ + f"`#{no}.` [{mem}](https://discord.com/users/{mem.id}) [{mem.mention}]" + for no, mem in enumerate(people, start=1) + ] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + title=f"Bots in {guild.name} - {len(people)}", + description="", + per_page=10), + ctx=ctx) + await paginator.paginate() + + @__list_.command(name="admins", + aliases=["admin"], + help="List of all Admins of the Guild", + with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def list_admin(self, ctx): + mems = ([ + mem for mem in ctx.guild.members + if mem.guild_permissions.administrator + ]) + mems = sorted(mems, key=lambda mem: not mem.bot) + admins = len([ + mem for mem in ctx.guild.members + if mem.guild_permissions.administrator + ]) + guild = ctx.guild + entries = [ + f"`#{no}.` [{mem}](https://discord.com/users/{mem.id}) [{mem.mention}] - " + for no, mem in enumerate(mems, start=1) + ] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + title=f"Admins in {guild.name} - {admins}", + description="", + per_page=10), + ctx=ctx) + await paginator.paginate() + + @__list_.command(name="invoice", help="List of all users in a voice channel", aliases=["invc"], with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def listusers(self, ctx): + if not ctx.author.voice: + return await ctx.send("You are not connected to a voice channel") + members = ctx.author.voice.channel.members + entries = [ + f"`[{n}]` | {member} [{member.mention}]" + for n, member in enumerate(members, start=1) + ] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + description="", + title=f"Voice List of {ctx.author.voice.channel.name} - {len(members)}", + color=self.color), + ctx=ctx) + await paginator.paginate() + + @__list_.command(name="moderators", help= "List of All Admins of a server", aliases=["mods"], with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def list_mod(self, ctx): + membs = ([ + mem for mem in ctx.guild.members + if mem.guild_permissions.ban_members + or mem.guild_permissions.kick_members + ]) + mems = filter(lambda member: member.bot, ctx.guild.members) + mems = sorted(membs, key=lambda mem: mem.joined_at) + admins = len([ + mem for mem in ctx.guild.members + if mem.guild_permissions.ban_members + or mem.guild_permissions.kick_members + ]) + guild = ctx.guild + entries = [ + f"`#{no}.` [{mem}](https://discord.com/users/{mem.id}) [{mem.mention}] - " + for no, mem in enumerate(mems, start=1) + ] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + title=f"Mods in {guild.name} - {admins}", + description="", + per_page=10), + ctx=ctx) + await paginator.paginate() + + @__list_.command(name="early", aliases=["sup"], help= "List of members that have Early Supporter badge.", with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def list_early(self, ctx): + mems = ([ + memb for memb in ctx.guild.members + if memb.public_flags.early_supporter + ]) + mems = sorted(mems, key=lambda memb: memb.created_at) + admins = len([ + memb for memb in ctx.guild.members + if memb.public_flags.early_supporter + ]) + guild = ctx.guild + entries = [ + f"`#{no}.` [{mem}](https://discord.com/users/{mem.id}) [{mem.mention}] - " + for no, mem in enumerate(mems, start=1) + ] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + title=f"Early Supporters Id's in {guild.name} - {admins}", + description="", + per_page=10), + ctx=ctx) + await paginator.paginate() + + @__list_.command(name="activedeveloper", help= "List of members that have Active Developer badge.", + aliases=["activedev"], + with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def list_activedeveloper(self, ctx): + mems = ([ + memb for memb in ctx.guild.members + if memb.public_flags.active_developer + ]) + mems = sorted(mems, key=lambda memb: memb.created_at) + admins = len([ + memb for memb in ctx.guild.members + if memb.public_flags.active_developer + ]) + guild = ctx.guild + entries = [ + f"`#{no}.` [{mem}](https://discord.com/users/{mem.id}) [{mem.mention}] - " + for no, mem in enumerate(mems, start=1) + ] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + title=f"Active Developer Id's in {guild.name} - {admins}", + description="", + per_page=10), + ctx=ctx) + await paginator.paginate() + + @__list_.command(name="createdat", help= "List of Account Creation Date of all Users", with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def list_cpos(self, ctx): + mems = ([memb for memb in ctx.guild.members]) + mems = sorted(mems, key=lambda memb: memb.created_at) + admins = len([memb for memb in ctx.guild.members]) + guild = ctx.guild + entries = [ + f"`[{no}]` | [{mem}](https://discord.com/users/{mem.id}) - " + for no, mem in enumerate(mems, start=1) + ] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + title=f"Creation every id in {guild.name} - {admins}", + description="", + per_page=10), + ctx=ctx) + await paginator.paginate() + + @__list_.command(name="joinedat", help= "List of Guild Joined date of all Users", with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def list_joinpos(self, ctx): + mems = ([memb for memb in ctx.guild.members]) + mems = sorted(mems, key=lambda memb: memb.joined_at) + admins = len([memb for memb in ctx.guild.members]) + guild = ctx.guild + entries = [ + f"`#{no}.` [{mem}](https://discord.com/users/{mem.id}) Joined At - " + for no, mem in enumerate(mems, start=1) + ] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + title=f"Join Position of every user in {guild.name} - {admins}", + description="", + per_page=10), + ctx=ctx) + await paginator.paginate() + + + + + @commands.command(name="joined-at", + help="Shows when a user joined", + usage="joined-at [user]", + with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def joined_at(self, ctx): + joined = ctx.author.joined_at.strftime("%a, %d %b %Y %I:%M %p") + await ctx.send(view=CV2("joined-at", f"**`{joined}`**")) + + @commands.command(name="github", usage="github [search]") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def github(self, ctx, *, search_query): + json = requests.get( + f"https://api.github.com/search/repositories?q={search_query}").json() + + if json["total_count"] == 0: + await ctx.send(f"No matching repositories found with the name: {search_query}") + else: + await ctx.send( + f"Found result for '{search_query}':\n{json['items'][0]['html_url']}") + + @commands.hybrid_command(name="vcinfo", + description="View information about a voice channel.", + help="View information about a voice channel.", + usage="", + with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def vcinfo(self, ctx, channel: discord.VoiceChannel = None): + if channel is None: + await ctx.reply(view=CV2(f"{cross} Error", "Please provide a valid voice channel.")) + return + vc_text = ( + f"**ID:** {channel.id}\n**Members:** {len(channel.members)}\n" + f"**Bitrate:** {channel.bitrate/1000} kbps\n" + f"**Created At:** {channel.created_at.strftime('%Y-%m-%d %H:%M:%S')}\n" + f"**Category:** {channel.category.name if channel.category else 'None'}\n" + f"**Region:** {channel.rtc_region}" + ) + if channel.user_limit: + vc_text += f"\n**User Limit:** {channel.user_limit}" + view = LayoutView(timeout=None) + join_btn = Button(label="Join", style=discord.ButtonStyle.link, url=f"https://discord.com/channels/{ctx.guild.id}/{channel.id}") + view.add_item(build_container( + TextDisplay(f"**Voice Channel Info — {channel.name}**"), + Separator(visible=True), + TextDisplay(vc_text), + ActionRow(join_btn) + )) + await ctx.send(view=view) + + + @commands.hybrid_command(name="channelinfo", + aliases=['cinfo', 'ci'], + description='Get information about a channel.', + help='Get information about a channel.', + usage="", + with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def channelinfo(self, ctx, channel: discord.TextChannel = None): + if channel is None: + channel = ctx.channel + + ch_text = ( + f"**ID:** {channel.id}\n**Created At:** {channel.created_at.strftime('%Y-%m-%d %H:%M:%S')}\n" + f"**Category:** {channel.category.name if channel.category else 'None'}\n" + f"**Topic:** {channel.topic if channel.topic else 'None'}\n" + f"**Slowmode:** {f'{channel.slowmode_delay} seconds' if channel.slowmode_delay else 'None'}\n" + f"**NSFW:** {channel.is_nsfw()}" + ) + view = OverwritesView(channel, ctx.author.id) + view.add_item(Button(label="Redirect Channel", style=discord.ButtonStyle.green, url=f"https://discord.com/channels/{ctx.guild.id}/{channel.id}")) + await ctx.send(content=ch_text, view=view) + + + @commands.hybrid_command(name="ping", aliases=['latency'], help="Checks the bot's latencies.") + @ignore_check() + @blacklist_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def ping(self, ctx: commands.Context): + """Shows the bot's WebSocket, API, and Database latencies.""" + + # 1. Start timer and send an initial "Pinging..." message + start_time = time.monotonic() + msg = await ctx.send(view=CV2("Checking Latency...", "Calculating response times, please wait.")) + end_time = time.monotonic() + + bot_latency = round(self.bot.latency * 1000) + api_latency = round((end_time - start_time) * 1000) + + db_latency = "Not Available" + try: + async with aiosqlite.connect("db/afk.db") as db: + db_start_time = time.perf_counter() + await db.execute("SELECT 1") + db_end_time = time.perf_counter() + db_latency = f"{round((db_end_time - db_start_time) * 1000)}ms" + except Exception as e: + print(f"Database latency check failed: {e}") + + latency_text = ( + f"**Bot (WebSocket):** `{bot_latency}ms`\n" + f"**API (Roundtrip):** `{api_latency}ms`\n" + f"**Database:** `{db_latency}`" + ) + await msg.edit(view=CV2("System Latency Report", latency_text)) + + + + + + @commands.command(name="permissions", aliases= ["perms"], + help="Check and list the key permissions of a specific user", + usage="perms ", + with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def keyperms(self, ctx, member: discord.Member): + key_permissions = [] + + if member.guild_permissions.create_instant_invite: + key_permissions.append("Create Instant Invite") + if member.guild_permissions.kick_members: + key_permissions.append("Kick Members") + if member.guild_permissions.ban_members: + key_permissions.append("Ban Members") + if member.guild_permissions.administrator: + key_permissions.append("Administrator") + if member.guild_permissions.manage_channels: + key_permissions.append("Manage Channels") + if member.guild_permissions.manage_messages: + key_permissions.append("Manage Messages") + if member.guild_permissions.mention_everyone: + key_permissions.append("Mention Everyone") + if member.guild_permissions.manage_nicknames: + key_permissions.append("Manage Nicknames") + if member.guild_permissions.manage_roles: + key_permissions.append("Manage Roles") + if member.guild_permissions.manage_webhooks: + key_permissions.append("Manage Webhooks") + if member.guild_permissions.manage_emojis: + key_permissions.append("Manage Emojis") + if member.guild_permissions.manage_guild: + key_permissions.append("Manage Server") + if member.guild_permissions.manage_permissions: + key_permissions.append("Manage Permissions") + if member.guild_permissions.manage_threads: + key_permissions.append("Manage Threads") + if member.guild_permissions.moderate_members: + key_permissions.append("Moderate Members") + if member.guild_permissions.move_members: + key_permissions.append("Move Members") + if member.guild_permissions.mute_members: + key_permissions.append("Mute Members (VC)") + if member.guild_permissions.deafen_members: + key_permissions.append("Deafen Members") + if member.guild_permissions.priority_speaker: + key_permissions.append("Priority Speaker") + if member.guild_permissions.stream: + key_permissions.append("Stream") + + + + + permissions_list = ", ".join(key_permissions) if key_permissions else "None" + + await ctx.reply(view=CV2(f"Key Permissions of {member}", f"__**Key Permissions**__\n{permissions_list}")) + + + + + + + @commands.hybrid_command(name="report", + aliases=["bug"], + usage='Report ', + description='Report a bug to the Development team.', + help='Report a bug to the Development team.', + with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 30, commands.BucketType.channel) + async def report(self, ctx, *, bug): + channel = self.bot.get_channel(1396813063642153030) + report_text = f"{bug}\n\n**Reported By:** {ctx.author.name}\n**Server:** {ctx.guild.name}\n**Channel:** {ctx.channel.name}" + await channel.send(view=CV2("Bug Reported", report_text)) + await ctx.reply(view=CV2(f"{TICK} Bug Reported", "Thank you for reporting the bug. We will look into it.")) + diff --git a/bot/cogs/commands/extraown.py b/bot/cogs/commands/extraown.py new file mode 100644 index 0000000..1e17028 --- /dev/null +++ b/bot/cogs/commands/extraown.py @@ -0,0 +1,169 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CROSS, TICK, ZWARNING +from discord.ext import commands +from discord.ui import LayoutView, TextDisplay, Separator, Container, ActionRow, Button +import aiosqlite +from utils.Tools import * +from utils.cv2 import CV2, build_container +from utils.config import OWNER_IDS_STR + + + + + +class ConfirmView(LayoutView): + def __init__(self, ctx): + super().__init__(timeout=60) + self.ctx = ctx + self.value = None + + self.yes_btn = Button(label="Confirm", style=discord.ButtonStyle.green) + self.no_btn = Button(label="Cancel", style=discord.ButtonStyle.red) + + self.yes_btn.callback = self.confirm_callback + self.no_btn.callback = self.cancel_callback + + container = build_container( + TextDisplay("**Confirm Action**"), + Separator(visible=True), + TextDisplay(self._desc), + ActionRow(self.yes_btn, self.no_btn), + ) + self.add_item(container) + + @property + def _desc(self): + return "" + + async def confirm_callback(self, interaction: discord.Interaction): + if interaction.user != self.ctx.author: + return await interaction.response.send_message("You cannot interact with this confirmation.", ephemeral=True) + self.value = True + await interaction.response.defer() + self.stop() + + async def cancel_callback(self, interaction: discord.Interaction): + if interaction.user != self.ctx.author: + return await interaction.response.send_message("You cannot interact with this confirmation.", ephemeral=True) + self.value = False + await interaction.response.defer() + self.stop() + + +class SetConfirmView(ConfirmView): + def __init__(self, ctx, user): + self._user = user + super().__init__(ctx) + + @property + def _desc(self): + return f"**Are you sure you want to set {self._user.mention} as the Extra Owner?**" + + +class ResetConfirmView(ConfirmView): + @property + def _desc(self): + return "**Are you sure you want to reset the Extra Owner?**" + + +class Extraowner(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.bot.loop.create_task(self.initialize_db()) + + async def initialize_db(self): + self.db = await aiosqlite.connect('db/anti.db') + await self.db.execute(''' + CREATE TABLE IF NOT EXISTS extraowners ( + guild_id INTEGER PRIMARY KEY, + owner_id INTEGER + ) + ''') + await self.db.commit() + + @commands.hybrid_command(name='extraowner', aliases=["owner"], help="Adds Extraowner to the server") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + async def extraowner(self, ctx, option: str = None, user: discord.Member = None): + guild_id = ctx.guild.id + + if ctx.guild.member_count < 2: + return await ctx.send(view=CV2(f"{CROSS} Error", "Your Server Doesn't Meet My 30 Member Criteria")) + + Ray = OWNER_IDS_STR + if ctx.author.id != ctx.guild.owner_id and str(ctx.author.id) not in Ray: + return await ctx.send(view=CV2(f"{ZWARNING} Access Denied", "Only Server Owner Can Run This Command")) + + if option is None: + pre = ctx.prefix + info_text = ( + "Extraowners can adjust server antinuke settings & manage whitelist events, " + "so careful consideration is essential before assigning it to someone.\n\n" + f"**Extraowner Set** — `{pre}extraowner set @user`\n" + f"**Extraowner Reset** — `{pre}extraowner reset`\n" + f"**Extraowner View** — `{pre}extraowner view`" + ) + return await ctx.reply(view=CV2("__Extra Owner__", info_text)) + + if option.lower() == 'set': + if user is None or user.bot: + return await ctx.reply(view=CV2(f"{CROSS} Error", "Please Provide a Valid User Mention or ID to Set as Extra Owner!")) + + view = SetConfirmView(ctx, user) + message = await ctx.reply(view=view) + await view.wait() + + if view.value is None: + await message.edit(view=CV2("⏰ Timed Out", "Confirmation timed out.")) + elif view.value: + await self.db.execute('INSERT OR REPLACE INTO extraowners (guild_id, owner_id) VALUES (?, ?)', (guild_id, user.id)) + await self.db.commit() + await message.edit(view=CV2(f"{TICK} Success", f"Added {user.mention} As Extraowner")) + else: + await message.edit(view=CV2(f"{CROSS} Cancelled", "Action cancelled.")) + + elif option.lower() == 'reset': + async with self.db.execute('SELECT owner_id FROM extraowners WHERE guild_id = ?', (guild_id,)) as cursor: + row = await cursor.fetchone() + + if not row: + await ctx.reply(view=CV2(f"{CROSS} Error", "No extra owner has been designated for this guild.")) + else: + view = ResetConfirmView(ctx) + message = await ctx.reply(view=view) + await view.wait() + + if view.value is None: + await message.edit(view=CV2("⏰ Timed Out", "Confirmation timed out.")) + elif view.value: + await self.db.execute('DELETE FROM extraowners WHERE guild_id = ?', (guild_id,)) + await self.db.commit() + await message.edit(view=CV2(f"{TICK} Success", "Disabled Extraowner Configuration!")) + else: + await message.edit(view=CV2(f"{CROSS} Cancelled", "Action cancelled.")) + + elif option.lower() == 'view': + async with self.db.execute('SELECT owner_id FROM extraowners WHERE guild_id = ?', (guild_id,)) as cursor: + row = await cursor.fetchone() + + if not row: + await ctx.reply(view=CV2(f"{CROSS} Error", "No extra owner is currently assigned.")) + else: + await ctx.reply(view=CV2("Extra Owner", f"Current Extraowner is <@{row[0]}>")) diff --git a/bot/cogs/commands/fastgreet.py b/bot/cogs/commands/fastgreet.py new file mode 100644 index 0000000..3f02871 --- /dev/null +++ b/bot/cogs/commands/fastgreet.py @@ -0,0 +1,92 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import sqlite3 +import asyncio +import os + +DB_PATH = "./db/fastgreet.db" + +class FastGreet(commands.Cog): + def __init__(self, bot): + self.bot = bot + os.makedirs("./db", exist_ok=True) + self.init_db() + + def init_db(self): + with sqlite3.connect(DB_PATH) as conn: + conn.execute(""" + CREATE TABLE IF NOT EXISTS greet_channels ( + guild_id INTEGER, + channel_id INTEGER, + PRIMARY KEY (guild_id, channel_id) + ) + """) + + @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: + conn.execute(""" + INSERT OR IGNORE INTO greet_channels (guild_id, channel_id) + VALUES (?, ?) + """, (ctx.guild.id, channel.id)) + await ctx.send(f"✅ {channel.mention} added as a greet channel.") + + @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: + conn.execute(""" + DELETE FROM greet_channels WHERE guild_id = ? AND channel_id = ? + """, (ctx.guild.id, channel.id)) + await ctx.send(f"❌ {channel.mention} removed from greet channels.") + + @commands.command(name="fastgreet_list") + async def list_greet_channels(self, ctx): + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.execute(""" + SELECT channel_id FROM greet_channels WHERE guild_id = ? + """, (ctx.guild.id,)) + rows = cursor.fetchall() + + if not rows: + await ctx.send("⚠️ No greet channels configured.") + return + + channels = [f"<#{cid[0]}>" for cid in rows] + await ctx.send("📋 Greet Channels: " + ", ".join(channels)) + + @commands.Cog.listener() + async def on_member_join(self, member): + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.execute(""" + SELECT channel_id FROM greet_channels WHERE guild_id = ? + """, (member.guild.id,)) + channels = [row[0] for row in cursor.fetchall()] + + for channel_id in channels: + channel = self.bot.get_channel(channel_id) + if channel: + try: + msg = await channel.send(f"{member.mention} Welcome!") + await asyncio.sleep(2) + await msg.delete() + except discord.Forbidden: + continue # Missing permissions + +async def setup(bot): + await bot.add_cog(FastGreet(bot)) diff --git a/bot/cogs/commands/filters.py b/bot/cogs/commands/filters.py new file mode 100644 index 0000000..98a0f25 --- /dev/null +++ b/bot/cogs/commands/filters.py @@ -0,0 +1,148 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +from typing import Union +import wavelink +from utils.Tools import * + +class FilterCog(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + self.active_filters = {} + + async def apply_filter(self, ctx: commands.Context, filter_name: str): + player: Union[wavelink.Player, None] = ctx.voice_client + if not player or not player.playing: + await ctx.send("I'm not playing anything.") + return + + if ctx.author.voice is None or ctx.author.voice.channel != player.channel: + await ctx.send("You need to be in the same voice channel as me.") + return + + filters = wavelink.Filters() + + if filter_name == "nightcore": + filters.timescale.set(pitch=1.2, speed=1.2, rate=1) + elif filter_name == "bassboost": + filters.equalizer.set(bands=[{"band": 0, "gain": 0.5}, {"band": 1, "gain": 0.5}, {"band": 2, "gain": 0.5}]) + elif filter_name == "vaporwave": + filters.timescale.set(rate=0.85, pitch=0.85) + elif filter_name == "karaoke": + filters.karaoke.set(level=1.0, mono_level=1.0, filter_band=220.0, filter_width=100.0) + elif filter_name == "tremolo": + filters.tremolo.set(depth=0.5, frequency=14.0) + elif filter_name == "vibrato": + filters.vibrato.set(depth=0.5, frequency=14.0) + elif filter_name == "rotation": + filters.rotation.set(rotation_hz=5.0) + elif filter_name == "distortion": + filters.distortion.set( + sin_offset=0.0, + sin_scale=1.0, + cos_offset=0.0, + cos_scale=1.0, + tan_offset=0.0, + tan_scale=1.0, + offset=0.0, + scale=1.0 + ) + elif filter_name == "channelmix": + filters.channel_mix.set(left_to_left=0.5, left_to_right=0.5, right_to_left=0.5, right_to_right=0.5) + + await player.set_filters(filters) + 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) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def filter(self, ctx: commands.Context): + await ctx.send("Use `filter enable` to enable a filter or `filter disable` to disable the current filter.") + + @filter.command(help="Enable a filter.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def enable(self, ctx: commands.Context): + player: Union[wavelink.Player, None] = ctx.voice_client + if not player or not player.playing: + await ctx.send("I'm not connected to a voice channel.") + return + + if ctx.author.voice is None or ctx.author.voice.channel != player.channel: + await ctx.send("You need to be in the same voice channel as me.") + return + + filter_options = [ + + discord.SelectOption(label="Vaporwave", description="Apply vaporwave effect"), + discord.SelectOption(label="Nightcore", description="Apply nightcore effect"), + discord.SelectOption(label="Vibrato", description="Apply vibrato effect"), + discord.SelectOption(label="Tremolo", description="Apply tremolo effect"), + discord.SelectOption(label="Bassboost", description="Apply bass boost effect"), + discord.SelectOption(label="Karaoke", description="Apply karaoke effect"), + discord.SelectOption(label="Rotation", description="Apply rotation effect"), + discord.SelectOption(label="Distortion", description="Apply distortion effect"), + discord.SelectOption(label="Channelmix", description="Apply channel mix effect"), + ] + + class FilterSelect(discord.ui.View): + @discord.ui.select(placeholder="Choose a filter...", options=filter_options) + async def select_filter(self, interaction: discord.Interaction, select: discord.ui.Select): + await interaction.response.defer() + selected_filter = select.values[0].lower() + await self.cog.apply_filter(ctx, selected_filter) + #await interaction.message.delete() + self.disable_all() + + @discord.ui.button(label="Cancel", style=discord.ButtonStyle.red) + async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.message.delete() + self.disable_all() + def disable_all(self): + for child in self.children: + child.disabled = True + self.stop() + + view = FilterSelect() + view.cog = self + + current_filter = self.active_filters.get(ctx.guild.id, "None") + embed = discord.Embed(title="Enable Filter", description="Choose a filter to apply:", color=discord.Color.blue()) + embed.add_field(name="Current Filter", value=current_filter, inline=False) + await ctx.send(embed=embed, view=view) + + @filter.command(help="Disable the current filter.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def disable(self, ctx: commands.Context): + player: Union[wavelink.Player, None] = ctx.voice_client + if not player or not player.playing: + await ctx.send("I'm not connected to a voice channel.") + return + + if ctx.author.voice is None or ctx.author.voice.channel != player.channel: + await ctx.send("You need to be in the same voice channel as me.") + return + + filters = wavelink.Filters() + await player.set_filters(filters) + self.active_filters.pop(ctx.guild.id, None) + await ctx.send(embed=discord.Embed(description="Filter disabled.", color=discord.Color.red())) + diff --git a/bot/cogs/commands/fun.py b/bot/cogs/commands/fun.py new file mode 100644 index 0000000..50e7db7 --- /dev/null +++ b/bot/cogs/commands/fun.py @@ -0,0 +1,188 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +from discord.ui import LayoutView, TextDisplay, Separator, MediaGallery +import random +import aiohttp +from discord import app_commands +from utils.Tools import blacklist_check, ignore_check +from utils.cv2 import CV2, build_container + +class Fun(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.giphy_api_key = "y3KcqQTdiS0RYcpNJrWn8hFGglKqX4is" + + async def fetch_giphy(self, query): + async with aiohttp.ClientSession() as session: + async with session.get(f"https://api.giphy.com/v1/gifs/search?api_key={self.giphy_api_key}&q={query}&limit=30&rating=pg") as resp: + if resp.status != 200: + return None + data = await resp.json() + if data['data']: + return random.choice(data['data'])['images']['original']['url'] + else: + return None + + def random_emoji(self): + return random.choice(["😂", "🤣", "😆", "😳", "🥴", "🙃", "😜"]) + + async def action_command(self, ctx, user: discord.Member, action: str): + gif_url = await self.fetch_giphy(action) + if not gif_url: + await ctx.send(view=CV2("😒 Error", "GIPHY API is sleeping. Try later!")) + return + view = LayoutView(timeout=None) + gallery = MediaGallery() + gallery.add_item(media=gif_url) + view.add_item(build_container( + TextDisplay(f"**{ctx.author.mention} {action}s {user.mention} {self.random_emoji()}**"), + gallery + )) + await ctx.send(view=view) + + async def meter_command(self, ctx, title, user, text): + await ctx.send(view=CV2(title, text)) + + @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.command() + @blacklist_check() + @ignore_check() + async def hug(self, ctx, user: discord.Member): + await self.action_command(ctx, user, "hug") + + @commands.command() + @blacklist_check() + @ignore_check() + async def kiss(self, ctx, user: discord.Member): + await self.action_command(ctx, user, "kiss") + + @commands.command() + @blacklist_check() + @ignore_check() + async def pat(self, ctx, user: discord.Member): + await self.action_command(ctx, user, "pat") + + @commands.command() + @blacklist_check() + @ignore_check() + async def slap(self, ctx, user: discord.Member): + await self.action_command(ctx, user, "slap") + + @commands.command() + @blacklist_check() + @ignore_check() + async def tickle(self, ctx, user: discord.Member): + await self.action_command(ctx, user, "tickle") + + @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.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.command(name="8ball") + @blacklist_check() + @ignore_check() + async def eight_ball(self, ctx, *, question: str): + responses = ["It is certain.", "Without a doubt.", "You may rely on it.", + "Ask again later.", "Better not tell you now.", + "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.command() + @blacklist_check() + @ignore_check() + async def roast(self, ctx, user: discord.Member): + roasts = [ + f"{user.mention} you're the reason shampoo has instructions!", + f"{user.mention} you have something on your chin... no, the third one down!", + f"{user.mention} your secrets are safe with me. I never even listen when you tell me them." + ] + await ctx.send(view=CV2("🔥 Roast Time", random.choice(roasts))) + + @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.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.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.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.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.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.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.command() + @blacklist_check() + @ignore_check() + async def howhot(self, ctx, user: discord.Member = None): + user = user or ctx.author + await ctx.send(view=CV2("🔥 Hotness Meter", f"**{user.mention} is {random.randint(0, 100)}% hot!**")) + +async def setup(bot): + await bot.add_cog(Fun(bot)) \ No newline at end of file diff --git a/bot/cogs/commands/general.py b/bot/cogs/commands/general.py new file mode 100644 index 0000000..703b7e9 --- /dev/null +++ b/bot/cogs/commands/general.py @@ -0,0 +1,316 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import asyncio +import discord +from utils.emoji import CROSS, TICK, ZWARNING, ZYROXCONNECTION, ZYROXLINKS +from discord.ext import commands, tasks +from discord.utils import get +import datetime +import random +import requests +import aiohttp +import re +from discord.ext.commands.errors import BadArgument +from discord.ext.commands import Cog +from discord.colour import Color +import hashlib +from utils.Tools import * +from traceback import format_exception +from discord import ButtonStyle +from discord.ui import Button, View, LayoutView, TextDisplay, Separator, MediaGallery, ActionRow +import psutil +import time +from datetime import datetime, timezone, timedelta +import sqlite3 +from typing import * +import string +from utils.cv2 import CV2, build_container + + +class AvatarView(View): + def __init__(self, user, member, author_id, banner_url): + super().__init__() + self.user = user + self.member = member + self.author_id = author_id + self.banner_url = banner_url + + if self.user.avatar.is_animated(): + self.add_item(Button(label='GIF', url=self.user.avatar.with_format('gif').url, style=discord.ButtonStyle.link)) + self.add_item(Button(label='PNG', url=self.user.avatar.with_format('png').url, style=discord.ButtonStyle.link)) + self.add_item(Button(label='JPEG', url=self.user.avatar.with_format('jpg').url, style=discord.ButtonStyle.link)) + self.add_item(Button(label='WEBP', url=self.user.avatar.with_format('webp').url, style=discord.ButtonStyle.link)) + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user.id != self.author_id: + await interaction.response.send_message( + "Uh oh! That message doesn't belong to you. You must run this command to interact with it.", + ephemeral=True + ) + return False + return True + + @discord.ui.button(label='Server Avatar', style=discord.ButtonStyle.success, custom_id='server_avatar_button') + async def server_avatar(self, interaction: discord.Interaction, button: Button): + if not self.member.guild_avatar: + await interaction.response.send_message( + "This user doesn't have a different guild avatar.", + ephemeral=True + ) + else: + embed = interaction.message.embeds[0] + embed.set_image(url=self.member.guild_avatar.url) + await interaction.response.edit_message(embed=embed) + + @discord.ui.button(label='User Banner', style=discord.ButtonStyle.success, custom_id='banner_button') + async def banner(self, interaction: discord.Interaction, button: Button): + if not self.banner_url: + await interaction.response.send_message( + "This user doesn't have a banner.", + ephemeral=True + ) + else: + embed = interaction.message.embeds[0] + embed.set_image(url=self.banner_url) + await interaction.response.edit_message(embed=embed) + + +from utils.config import BotName + +class General(commands.Cog): + + def __init__(self, bot, *args, **kwargs): + self.bot = bot + self.aiohttp = aiohttp.ClientSession() + self._URL_REGEX = r'(?P<[^: >]+:\/[^ >]+>|(?:https?|steam):\/\/[^\s<]+[^<.,:;"\'\\]\s])' + self.color = 0xFF0000 + + + @commands.hybrid_command( + usage="Avatar ", + name='avatar', + aliases=['av'], + help="Get User avater/Guild avatar & Banner of a user." + ) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def _user(self, ctx, member: Optional[Union[discord.Member, discord.User]] = None): + try: + if member is None: + member = ctx.author + user = await self.bot.fetch_user(member.id) + + banner_url = user.banner.url if user.banner else None + + # Avatar still uses embed because AvatarView buttons edit embed images + description = f"[`PNG`]({user.avatar.with_format('png').url}) | [`JPG`]({user.avatar.with_format('jpg').url}) | [`WEBP`]({user.avatar.with_format('webp').url})" + if user.avatar.is_animated(): + description += f" | [`GIF`]({user.avatar.with_format('gif').url})" + if banner_url: + description += f" | [`Banner`]({banner_url})" + + embed = discord.Embed( + color=self.color, + description=description + ) + embed.set_author(name=f"{member}", icon_url=member.avatar.url if member.avatar else member.default_avatar.url) + embed.set_image(url=user.avatar.url) + embed.set_footer(text=f"Requested By {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + + view = AvatarView(user, member, ctx.author.id, banner_url) + await ctx.send(embed=embed, view=view) + except Exception as e: + print(f"Error: {e}") + + @commands.hybrid_command( + name="servericon", + help="Get the server icon", + usage="Servericon" + ) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def servericon(self, ctx: commands.Context): + server = ctx.guild + if server.icon is None: + await ctx.reply(view=CV2("❌ Error", "This server does not have an icon.")) + return + + webp = server.icon.replace(format='webp') + jpg = server.icon.replace(format='jpg') + png = server.icon.replace(format='png') + + links = f"[`PNG`]({png}) | [`JPG`]({jpg}) | [`WEBP`]({webp})" + if server.icon.is_animated(): + gif = server.icon.replace(format='gif') + links += f" | [`GIF`]({gif})" + + view = LayoutView(timeout=None) + gallery = MediaGallery() + gallery.add_item(media=str(server.icon.url)) + dl_btn = Button(label="Download Icon", url=str(server.icon.url), style=ButtonStyle.link) + view.add_item(build_container( + TextDisplay(f"**{server}'s Icon**"), + Separator(visible=True), + TextDisplay(links), + gallery, + ActionRow(dl_btn) + )) + await ctx.send(view=view) + + + @commands.hybrid_command(name="membercount", + help="Get total member count of the server", + usage="membercount", + aliases=["mc"]) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 2, commands.BucketType.user) + async def membercount(self, ctx: commands.Context): + total_members = len(ctx.guild.members) + total_humans = len([m for m in ctx.guild.members if not m.bot]) + total_bots = len([m for m in ctx.guild.members if m.bot]) + + online = len([m for m in ctx.guild.members if m.status == discord.Status.online]) + offline = len([m for m in ctx.guild.members if m.status == discord.Status.offline]) + idle = len([m for m in ctx.guild.members if m.status == discord.Status.idle]) + dnd = len([m for m in ctx.guild.members if m.status == discord.Status.do_not_disturb]) + + stats_text = ( + f"**__Count Stats:__**\n" + f"Total Members: {total_members}\nTotal Humans: {total_humans}\nTotal Bots: {total_bots}\n\n" + f"**__Presence Stats:__**\n" + f"Online: {online} | DND: {dnd} | Idle: {idle} | Offline: {offline}" + ) + await ctx.send(view=CV2("Member Statistics", stats_text)) + + @commands.hybrid_command(name="poll", usage="Poll ") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def poll(self, ctx: commands.Context, *, message): + author = ctx.author + msg = await ctx.send(view=CV2(f"**Poll raised by {author}!**", message)) + await msg.add_reaction(TICK) + await msg.add_reaction(CROSS) + + + @commands.command(name="users", help=f"checks total users of {BotName}.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def users(self, ctx: commands.Context): + users = sum(g.member_count for g in self.bot.guilds + if g.member_count != None) + guilds = len(self.bot.guilds) + await ctx.send(view=CV2(f"{BotName} Users", f"❯ Total of __**{users}**__ Users in **{guilds}** Guilds")) + + + @commands.hybrid_command( + name="urban", + description="Searches for specified phrase on urbandictionary", + help="Get meaning of specified phrase", + usage="Urban ") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def urban(self, ctx: commands.Context, *, phrase): + async with self.aiohttp.get( + "http://api.urbandictionary.com/v0/define?term={}".format( + phrase)) as urb: + urban = await urb.json() + try: + definition = urban['list'][0]['definition'].replace('[', '').replace(']', '') + example = urban['list'][0]['example'].replace('[', '').replace(']', '') + author = urban['list'][0]['author'].replace('[', '').replace(']', '') + written_on = urban['list'][0]['written_on'].replace('[', '').replace(']', '') + + urban_text = ( + f"**__Definition:__**\n{definition}\n\n" + f"**__Example:__**\n{example}\n\n" + f"**__Author:__** {author}\n" + f"**__Written On:__** {written_on}" + ) + temp = await ctx.reply(view=CV2(f"Meaning of \"{phrase}\"", urban_text), mention_author=True) + await asyncio.sleep(45) + await temp.delete() + await ctx.message.delete() + except: + pass + + @commands.command(name="rickroll", + help="Detects if provided url is a rick-roll", + usage="Rickroll ") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def rickroll(self, ctx: commands.Context, *, url: str): + if not re.match(self._URL_REGEX, url): + raise BadArgument("Invalid URL") + + phrases = [ + "rickroll", "rick roll", "rick astley", "never gonna give you up" + ] + source = str(await (await self.aiohttp.get( + url, allow_redirects=True)).content.read()).lower() + rickRoll = bool((re.findall('|'.join(phrases), source, + re.MULTILINE | re.IGNORECASE))) + 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.command(name="hash", + help="Hashes provided text with provided algorithm") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def hash(self, ctx: commands.Context, algorithm: str, *, message): + algos: dict[str, str] = { + "md5": hashlib.md5(bytes(message.encode("utf-8"))).hexdigest(), + "sha1": hashlib.sha1(bytes(message.encode("utf-8"))).hexdigest(), + "sha224": hashlib.sha224(bytes(message.encode("utf-8"))).hexdigest(), + "sha3_224": hashlib.sha3_224(bytes(message.encode("utf-8"))).hexdigest(), + "sha256": hashlib.sha256(bytes(message.encode("utf-8"))).hexdigest(), + "sha3_256": hashlib.sha3_256(bytes(message.encode("utf-8"))).hexdigest(), + "sha384": hashlib.sha384(bytes(message.encode("utf-8"))).hexdigest(), + "sha3_384": hashlib.sha3_384(bytes(message.encode("utf-8"))).hexdigest(), + "sha512": hashlib.sha512(bytes(message.encode("utf-8"))).hexdigest(), + "sha3_512": hashlib.sha3_512(bytes(message.encode("utf-8"))).hexdigest(), + "blake2b": hashlib.blake2b(bytes(message.encode("utf-8"))).hexdigest(), + "blake2s": hashlib.blake2s(bytes(message.encode("utf-8"))).hexdigest() + } + if algorithm.lower() not in list(algos.keys()): + hash_lines = "\n".join(f"**{algo}:** `{algos[algo]}`" for algo in algos) + else: + hash_lines = f"**{algorithm}:** `{algos[algorithm.lower()]}`" + await ctx.reply(view=CV2(f"Hashed \"{message}\"", hash_lines), mention_author=True) + + @commands.command( + name="invite", + aliases=['invite-bot'], + description="Get Support & Bot invite link!" + ) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def invite(self, ctx: commands.Context): + invite_text = ( + "```Empower your server with blazing-fast features and 24/7 support!```\n" + f"{ZYROXLINKS} **Quick Actions**\n" + f">>> **[Invite {BotName}](https://discord.com/oauth2/authorize?client_id=1396114795102470196&permissions=8&integration_type=0&scope=bot+applications.commands)**\n" + "**[Support Server](https://discord.gg/codexdev)**" + ) + await ctx.send(view=CV2(f"{ZYROXCONNECTION} {BotName} Integration Hub!", invite_text)) \ No newline at end of file diff --git a/bot/cogs/commands/giveaway.py b/bot/cogs/commands/giveaway.py new file mode 100644 index 0000000..73ef504 --- /dev/null +++ b/bot/cogs/commands/giveaway.py @@ -0,0 +1,413 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from discord.ext import commands, tasks +import datetime, pytz, time as t +from discord.ui import Button, Select, View +import aiosqlite, random, typing +import sqlite3 +import asyncio +import discord, logging +from utils.emoji import ARROWRED, TADAA, TICK +from discord.utils import get +from utils.Tools import * +import os +import aiohttp +from utils.cv2 import CV2 + +db_folder = 'db' +db_file = 'giveaways.db' +db_path = os.path.join(db_folder, db_file) +connection = sqlite3.connect(db_path) + +cursor = connection.cursor() + +cursor.execute('''CREATE TABLE IF NOT EXISTS Giveaway ( + guild_id INTEGER, + host_id INTEGER, + start_time TIMESTAMP, + ends_at TIMESTAMP, + prize TEXT, + winners INTEGER, + message_id INTEGER, + channel_id INTEGER, + PRIMARY KEY (guild_id, message_id) + )''') + +connection.commit() +connection.close() + +def convert(time): + pos = ["s","m","h","d"] + time_dict = {"s" : 1, "m" : 60, "h" : 3600 , "d" : 86400 , "f" : 259200} + unit = time[-1] + if unit not in pos: + return + try: + val = int(time[:-1]) + except ValueError: + return + return val * time_dict[unit] + +def WinnerConverter(winner): + try: + int(winner) + except ValueError: + try: + return int(winner[:-1]) + except: + return -4 + return winner + +class Giveaway(commands.Cog): + def __init__(self, bot): + self.bot = bot + + async def cog_load(self) -> None: + self.connection = await aiosqlite.connect(db_path) + self.cursor = await self.connection.cursor() + await self.check_for_ended_giveaways() + self.GiveawayEnd.start() + + async def cog_unload(self) -> None: + await self.connection.close() + + async def check_for_ended_giveaways(self): + await self.cursor.execute("SELECT ends_at, guild_id, message_id, host_id, winners, prize, channel_id FROM Giveaway WHERE ends_at <= ?", (datetime.datetime.now().timestamp(),)) + ended_giveaways = await self.cursor.fetchall() + for giveaway in ended_giveaways: + await self.end_giveaway(giveaway) + + async def end_giveaway(self, giveaway): + try: + current_time = datetime.datetime.now().timestamp() + guild = self.bot.get_guild(int(giveaway[1])) + if guild is None: + await self.cursor.execute("DELETE FROM Giveaway WHERE message_id = ? AND guild_id = ?", (giveaway[2], giveaway[1])) + await self.connection.commit() + return + + channel = self.bot.get_channel(int(giveaway[6])) + if channel is not None: + try: + retries = 3 + for attempt in range(retries): + try: + message = await channel.fetch_message(int(giveaway[2])) + break + except discord.NotFound: + await self.cursor.execute("DELETE FROM Giveaway WHERE message_id = ? AND guild_id = ?", (giveaway[2], giveaway[1])) + await self.connection.commit() + return + except aiohttp.ClientResponseError as e: + if e.status == 503: + if attempt < retries - 1: + await asyncio.sleep(2 ** attempt) + continue + else: + raise + else: + raise + + users = [i.id async for i in message.reactions[0].users()] + if self.bot.user.id in users: + users.remove(self.bot.user.id) + + if len(users) < 1: + await message.reply(f"No one won the **{giveaway[5]}** giveaway, due to Not enough participants.") + await self.cursor.execute("DELETE FROM Giveaway WHERE message_id = ? AND guild_id = ?", (message.id, message.guild.id)) + await self.connection.commit() + return + + winners_count = min(len(users), int(giveaway[4])) + winner = ', '.join(f'<@!{i}>' for i in random.sample(users, k=winners_count)) + + desc = f"Ended at \nHosted by <@{int(giveaway[3])}>\nWinner(s): {winner}" + view = CV2(f"{giveaway[5]}", desc) + + await message.edit(content=f"{TADAA} **GIVEAWAY ENDED** {TADAA}", view=view) + await message.reply(f"{TADAA} Congrats {winner}, you won **{giveaway[5]}!**, Hosted by <@{int(giveaway[3])}>") + await self.cursor.execute("DELETE FROM Giveaway WHERE message_id = ? AND guild_id = ?", (message.id, message.guild.id)) + await self.connection.commit() + + except (discord.HTTPException, aiohttp.ClientResponseError) as e: + logging.error(f"Error ending giveaway: {e}") + + except IndexError: + logging.error(f"Giveaway data is corrupted or missing: {giveaway}") + await self.cursor.execute("DELETE FROM Giveaway WHERE message_id = ? AND guild_id = ?", (giveaway[2], giveaway[1])) + await self.connection.commit() + + @tasks.loop(seconds=5) + async def GiveawayEnd(self): + await self.cursor.execute("SELECT ends_at, guild_id, message_id, host_id, winners, prize, channel_id FROM Giveaway WHERE ends_at <= ?", (datetime.datetime.now().timestamp(),)) + ends_raw = await self.cursor.fetchall() + for giveaway in ends_raw: + await self.end_giveaway(giveaway) + + + + + @commands.hybrid_command(description="Starts a new giveaway.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_guild_permissions(manage_guild=True) + async def gstart(self, ctx, + time, + winners: int, + *, + prize: str): + + await self.cursor.execute("SELECT message_id, channel_id FROM Giveaway WHERE guild_id = ?", (ctx.guild.id,)) + re = await self.cursor.fetchall() + + if winners >= 15: + message = await ctx.send(view=CV2("⚠️ Access Denied", "Cannot exceed more than 15 winners.")) + await asyncio.sleep(5) + await message.delete() + return + + g_list = [i[0] for i in re] + if len(g_list) >= 5: + message = await ctx.send(view=CV2("⚠️ Access Denied", "You can only host upto 5 giveaways in this Guild.")) + await asyncio.sleep(5) + await message.delete() + return + + converted = self.convert(time) + if converted / 60 >= 50400: + message = await ctx.send(view=CV2("⚠️ Access Denied", "Time cannot exceed 31 days!")) + await asyncio.sleep(5) + await message.delete() + return + + if converted == -1: + message = await ctx.send(view=CV2("❌ Error", "Invalid time format")) + await asyncio.sleep(5) + await message.delete() + return + if converted == -2: + message = await ctx.send(view=CV2("❌ Error", "Invalid time format. Please provide the time in numbers.")) + await asyncio.sleep(5) + await message.delete() + return + + ends = (datetime.datetime.now().timestamp() + converted) + + desc = ( + f"{ARROWRED} Winner(s): **{winners}**\n" + f"{ARROWRED} Hosted by {ctx.author.mention}\n" + f"{ARROWRED} Ends ()\n\n" + f"{ARROWRED} React with {TADAA} to participate!" + ) + + view = CV2(f"{TADAA} {prize}", desc) + + message = await ctx.send(f"{TADAA} **GIVEAWAY** {TADAA}", view=view) + try: + await ctx.message.delete() + except: + pass + + await self.cursor.execute("INSERT INTO Giveaway(guild_id, host_id, start_time, ends_at, prize, winners, message_id, channel_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?)", (ctx.guild.id, ctx.author.id, datetime.datetime.now(), ends, prize, winners, message.id, ctx.channel.id)) + + await message.add_reaction(TADAA) + await self.connection.commit() + + + + + @commands.Cog.listener("on_message_delete") + async def GiveawayMessageDelete(self, message): + await self.cursor.execute("SELECT message_id FROM Giveaway WHERE guild_id = ?", (message.guild.id,)) + re = await self.cursor.fetchone() + + if message.author != self.bot.user: + return + + if re is not None: + if message.id == int(re[0]): + await self.cursor.execute("DELETE FROM Giveaway WHERE channel_id = ? AND message_id = ? AND guild_id = ?", (message.channel.id, message.id, message.guild.id)) + + 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.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_guild_permissions(manage_guild=True) + async def gend(self, ctx, message_id = None): + if message_id: + try: + int(message_id) + except ValueError: + message = await ctx.send(view=CV2("⚠️ Access Denied", "Invalid message ID provided.")) + await asyncio.sleep(5) + await message.delete() + return + + if message_id is not None: + current_time = datetime.datetime.now().timestamp() + await self.cursor.execute('SELECT ends_at, guild_id, message_id, host_id, winners, prize, channel_id FROM Giveaway WHERE message_id = ?', (int(message_id),)) + re = await self.cursor.fetchone() + + if re is None: + message = await ctx.send(view=CV2("❌ Error", "The giveaway was not found.")) + await asyncio.sleep(5) + await message.delete() + return + + ch = self.bot.get_channel(int(re[6])) + message = await ch.fetch_message(int(message_id)) + + users = [i.id async for i in message.reactions[0].users()] + users.remove(self.bot.user.id) + + if len(users) < 1: + await ctx.send(f"{TICK} Successfully Ended the giveaway in <#{int(re[6])}>") + await message.reply(f"No one won the **{re[5]}** giveaway, due to Not enough participants.") + await self.cursor.execute("DELETE FROM Giveaway WHERE message_id = ? AND guild_id = ?", (message.id, message.guild.id)) + return + + winner = ', '.join(f'<@!{i}>' for i in random.sample(users, k=int(re[4]))) + + desc = f"Ended at \nHosted by <@{int(re[3])}>\nWinner(s): {winner}" + view = CV2(f"🎁 {re[5]}", desc) + + await message.edit(content="🎁 **GIVEAWAY ENDED** 🎁", view=view) + + if int(ctx.channel.id) != int(re[6]): + await ctx.send(f"{TADAA} Successfully ended the giveaway in <#{int(re[6])}>") + + await message.reply(f" Congrats {winner}, you won **{re[5]}!**, Hosted by <@{int(re[3])}>") + await self.cursor.execute("DELETE FROM Giveaway WHERE message_id = ? AND guild_id = ?", (message.id, message.guild.id)) + + elif ctx.message.reference: + await self.cursor.execute('SELECT ends_at, guild_id, message_id, host_id, winners, prize, channel_id FROM Giveaway WHERE message_id = ?', (ctx.message.reference.resolved.id,)) + re = await self.cursor.fetchone() + + if re is None: + return await ctx.send(f"The giveaway was not found.") + + current_time = datetime.datetime.now().timestamp() + + message = await ctx.fetch_message(ctx.message.reference.message_id) + + users = [i.id async for i in message.reactions[0].users()] + try: users.remove(self.bot.user.id) + except: pass + + if len(users) < 1: + await message.reply(f"No one won the **{re[5]}** giveaway, due to not enough participants.") + await self.cursor.execute("DELETE FROM Giveaway WHERE message_id = ? AND guild_id = ?", (message.id, message.guild.id)) + return + + winner = ', '.join(f'<@!{i}>' for i in random.sample(users, k=int(re[4]))) + + desc = f"Ended \nHosted by <@{int(re[3])}>\nWinner(s): {winner}" + view = CV2(f"🎁 {re[5]}", desc) + + await message.edit(content="🎁 **GIVEAWAY ENDED** 🎁", view=view) + + await message.reply(f"💐 Congrats {winner}, you won **{re[5]}!**, Hosted by <@{int(re[3])}>") + await self.cursor.execute("DELETE FROM Giveaway WHERE message_id = ? AND guild_id = ?", (message.id, message.guild.id)) + + else: + 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.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_guild_permissions(manage_guild=True) + async def greroll(self, ctx, message_id: typing.Optional[int] = None): + if not ctx.message.reference: + message = await ctx.reply("Reply this command with the Giveaway Ended message to reroll.") + await asyncio.sleep(5) + await message.delete() + return + + if ctx.message.reference: + message_id = ctx.message.reference.resolved.id + + message = await ctx.fetch_message(message_id) + + if ctx.message.reference.resolved.author.id != self.bot.user.id : + msg = await ctx.send(view=CV2("⚠️ Access Denied", "The giveaway was not found.")) + await asyncio.sleep(5) + await msg.delete() + return + + + await self.cursor.execute(f"SELECT message_id FROM Giveaway WHERE message_id = ?", (message.id,)) + re = await self.cursor.fetchone() + + if re is not None: + msg = await ctx.send(view=CV2("⚠️ Access Denied", "The giveaway is currently running. Please use the `gend` command instead to end the giveaway.")) + await asyncio.sleep(5) + await msg.delete() + return + + users = [i.id async for i in message.reactions[0].users()] + users.remove(self.bot.user.id) + + if len(users) < 1: + await message.reply(f"No one won the **{re[5]}** giveaway, due to not enough participants.") + return + + winners = random.sample(users, k=1) + await message.reply(f" The new winner is "+", ".join(f"<@{i}>" for i in winners)+". Congratulations!") + await self.connection.commit() + + def convert(self, time): + pos = ["s", "m", "h", "d"] + time_dict = {"s": 1, "m": 60, "h": 3600, "d": 86400, "f": 259200} + + unit = time[-1] + if unit not in pos: + return -1 + + try: + val = int(time[:-1]) + except ValueError: + return -2 + + return val * time_dict[unit] + + + @commands.hybrid_command(name="glist", description="Lists all ongoing giveaways.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_guild_permissions(manage_guild=True) + async def glist(self, ctx): + await self.cursor.execute("SELECT prize, ends_at, winners, message_id FROM Giveaway WHERE guild_id = ?", (ctx.guild.id,)) + giveaways = await self.cursor.fetchall() + + if not giveaways: + await ctx.send(view=CV2("Ongoing Giveaways", "No ongoing giveaways.")) + return + + desc = "" + for giveaway in giveaways: + prize, ends_at, winners, message_id = giveaway + desc += f"**{prize}**\nEnds: ()\nWinners: {winners}\n[Jump to Message](https://discord.com/channels/{ctx.guild.id}/{ctx.channel.id}/{message_id})\n\n" + + await ctx.send(view=CV2("Ongoing Giveaways", desc)) + +async def setup(bot): + await bot.add_cog(Giveaway(bot)) diff --git a/bot/cogs/commands/help.py b/bot/cogs/commands/help.py new file mode 100644 index 0000000..a71b0eb --- /dev/null +++ b/bot/cogs/commands/help.py @@ -0,0 +1,285 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ARROWRED, BOOST, CAST, GAMES, LEVEL_UP, LOADINGRED, LOCK, MESSAGE, MINECRAFT, MUSIC, NEW, PIN, SEED, STAR, SWORD, SYSTEM, THUNDER, TICKET, WIFI, ZAI, ZARROW, ZBAN, ZBOT, ZCIRCLE, ZCIRCLE_ALT1, ZCLOUD, ZCOUNTING, ZMODULE, ZPEOPLE, ZROCKET, ZSAFE, ZTADA, ZUNMUTE, ZWRENCH +from discord.ext import commands +from discord import app_commands, Interaction +from difflib import get_close_matches +from contextlib import suppress +from core import Context +from core.zyrox import zyrox +from core.Cog import Cog +from utils.Tools import getConfig +from itertools import chain +import json +from utils import help as vhelp +from utils import Paginator, DescriptionEmbedPaginator, FieldPagePaginator, TextPaginator +import asyncio +from utils.config import serverLink +from utils.Tools import * +from utils.cv2 import CV2, CV2Embed +from utils.config import * + +color = 0xFF0000 +client = zyrox() + +from utils.config import BotName + +class HelpCommand(commands.HelpCommand): + + async def send_ignore_message(self, ctx, ignore_type: str): + if ignore_type == "channel": + await ctx.reply(f"This channel is ignored.", mention_author=False) + elif ignore_type == "command": + await ctx.reply(f"{ctx.author.mention} This Command, Channel, or You have been ignored here.", delete_after=6) + elif ignore_type == "user": + await ctx.reply(f"You are ignored.", mention_author=False) + + async def on_help_command_error(self, ctx, error): + errors = [ + commands.CommandOnCooldown, commands.CommandNotFound, + discord.HTTPException, commands.CommandInvokeError + ] + if not type(error) in errors: + await self.context.reply(f"Unknown Error Occurred\n{error.original}", + mention_author=False) + else: + if type(error) == commands.CommandOnCooldown: + return + return await super().on_help_command_error(ctx, error) + + async def command_not_found(self, string: str) -> None: + ctx = self.context + check_ignore = await ignore_check().predicate(ctx) + check_blacklist = await blacklist_check().predicate(ctx) + + if not check_blacklist: + return + + if not check_ignore: + await self.send_ignore_message(ctx, "command") + return + + cmds = (str(cmd) for cmd in self.context.bot.walk_commands()) + matches = get_close_matches(string, cmds) + + embed = CV2Embed( + title=f"{BotName} Helper", + description=f">>> **Ops! Command not found with the name** `{string}`.", + color=0xFF0000 + ) + + await ctx.reply(view=embed, mention_author=True) + + async def send_bot_help(self, mapping): + ctx = self.context + check_ignore = await ignore_check().predicate(ctx) + check_blacklist = await blacklist_check().predicate(ctx) + + if not check_blacklist: + return + + if not check_ignore: + await self.send_ignore_message(ctx, "command") + return + + # Show loading message + loading_embed = CV2(f"{LOADINGRED} Loading help Menu...") + loading_msg = await ctx.reply(view=loading_embed) + + # Wait 2 seconds + await asyncio.sleep(2) + + # Delete loading message + with suppress(discord.NotFound): + await loading_msg.delete() + + data = await getConfig(self.context.guild.id) + prefix = data["prefix"] + filtered = await self.filter_commands(self.context.bot.walk_commands(), sort=True) + + embed = CV2Embed( + description=( + f"**{ARROWRED} __Start {BotName} Today__**\n" + f"**{ZARROW} Type {prefix}antinuke enable**\n" + f"**{ZARROW} Server Prefix:** `{prefix}`\n" + f"**{ZARROW} Total Commands:** `{len(set(self.context.bot.walk_commands()))}`\n"), + color=0xFF0000) + + embed.add_field( + name=f"{ZCLOUD} Main Features", + value=f">>> \n {ZSAFE} `»` Security\n" + f" {ZBOT} `»` Automoderation\n" + f" {ZWRENCH} `»` Utility\n" + f" {MUSIC} `»` Music\n" + f" {WIFI} `»` Autoreact & responder\n" + f" {SWORD} `»` Moderation\n" + f" {ZPEOPLE} `»` Autorole & Invc\n" + f" {ZROCKET} `»` Fun\n" + f" {GAMES} `»` Games\n" + f" {ZBAN} `»` Ignore Channels\n" + f" {WIFI} `»` Server\n" + f" {ZUNMUTE} `»` Voice\n" + f" {SEED} `»` Welcomer\n" + f" {ZTADA} `»` Giveaway\n" + f" {TICKET} `»` Ticket {NEW}\n" + f" {ZPEOPLE} `»` Invite Tracker {NEW}\n" + ) + + embed.add_field( + name=f" {ZMODULE} Extra Features", + value=f">>> \n {CAST} `»` Advance Logging\n" + f" {STAR} `»` Vanityroles\n" + f" {ZCOUNTING} `»` Counting {NEW}\n" + f" {SYSTEM} `»` J2C {NEW}\n" + f" {ZAI} `»` AI {NEW}\n" + f" {BOOST} `»` Boost {NEW}\n" + f" {LEVEL_UP} `»` Leveling {NEW}\n" + f" {PIN} `»` Sticky {NEW}\n" + f" {THUNDER} `»` Verification {NEW}\n" + f" {LOCK} `»` Encryption {NEW}\n" + f" {MINECRAFT} `»` Minecraft {NEW}\n" + f" {MESSAGE} `»` Joindm {NEW}\n" + f" {ZCIRCLE} `»` Birthday {NEW}\n" + f" {ZCIRCLE_ALT1} `»` Customrole\n" + ) + + embed.set_footer( + text=f"Requested By {self.context.author} | [Support](https://discord.gg/codexdev)", + ) + + view = vhelp.View(mapping=mapping, ctx=self.context, homeembed=embed, ui=2) + await ctx.reply(view=view) + + async def send_command_help(self, command): + ctx = self.context + check_ignore = await ignore_check().predicate(ctx) + check_blacklist = await blacklist_check().predicate(ctx) + + if not check_blacklist: + return + + if not check_ignore: + await self.send_ignore_message(ctx, "command") + return + + zyrox = f">>> {command.help}" if command.help else '>>> No Help Provided...' + embed = CV2Embed( + description=f"""{zyrox}""", + color=color) + alias = ' & '.join(command.aliases) + + embed.add_field(name="**Alt cmd**", + value=f"```{alias}```" if command.aliases else "No Alt cmd", + inline=False) + embed.add_field(name="**Usage**", + value=f"```{self.context.prefix}{command.signature}```\n") + embed.set_author(name=f"{command.qualified_name.title()} Command") + embed.set_footer(text="<[] = optional | < > = required • Use Prefix Before Commands.") + await self.context.reply(view=embed, mention_author=False) + + def get_command_signature(self, command: commands.Command) -> str: + parent = command.full_parent_name + if len(command.aliases) > 0: + aliases = ' | '.join(command.aliases) + fmt = f'[{command.name} | {aliases}]' + if parent: + fmt = f'{parent}' + alias = f'[{command.name} | {aliases}]' + else: + alias = command.name if not parent else f'{parent} {command.name}' + return f'{alias} {command.signature}' + + def common_command_formatting(self, embed_like, command): + embed_like.title = self.get_command_signature(command) + if command.description: + embed_like.description = f'{command.description}\n\n{command.help}' + else: + embed_like.description = command.help or 'No help found...' + + async def send_group_help(self, group): + ctx = self.context + check_ignore = await ignore_check().predicate(ctx) + check_blacklist = await blacklist_check().predicate(ctx) + + if not check_blacklist: + return + + if not check_ignore: + await self.send_ignore_message(ctx, "command") + return + + entries = [ + ( + f"`{self.context.prefix}{cmd.qualified_name}`\n", + f"{cmd.short_doc if cmd.short_doc else ''}\n\u200b" + ) + for cmd in group.commands + ] + + count = len(group.commands) + + embeds = FieldPagePaginator( + entries=entries, + title=f"{group.qualified_name.title()} [{count}]", + description="< > Duty | [ ] Optional\n", + per_page=4 + ).get_pages() + + paginator = Paginator(ctx, embeds) + await paginator.paginate() + + async def send_cog_help(self, cog): + ctx = self.context + check_ignore = await ignore_check().predicate(ctx) + check_blacklist = await blacklist_check().predicate(ctx) + + if not check_blacklist: + return + + if not check_ignore: + await self.send_ignore_message(ctx, "command") + return + + entries = [( + f"> `{self.context.prefix}{cmd.qualified_name}`", + f"-# Description : {cmd.short_doc if cmd.short_doc else ''}" + f"\n\u200b", + ) for cmd in cog.get_commands()] + paginator = Paginator(source=FieldPagePaginator( + entries=entries, + title=f"{BRAND_NAME}'s {cog.qualified_name.title()} ({len(cog.get_commands())})", + description="`<..> Required | [..] Optional`\n\n", + color=0xFF0000, + per_page=4), + ctx=self.context) + await paginator.paginate() + + +class Help(Cog, name="help"): + + def __init__(self, client: zyrox): + self._original_help_command = client.help_command + attributes = { + 'name': "help", + 'aliases': ['h'], + 'cooldown': commands.CooldownMapping.from_cooldown(1, 5, commands.BucketType.user), + 'help': 'Shows help about bot, a command, or a category' + } + client.help_command = HelpCommand(command_attrs=attributes) + client.help_command.cog = self + + async def cog_unload(self): + self.help_command = self._original_help_command \ No newline at end of file diff --git a/bot/cogs/commands/help_backup.txt b/bot/cogs/commands/help_backup.txt new file mode 100644 index 0000000..17d6027 --- /dev/null +++ b/bot/cogs/commands/help_backup.txt @@ -0,0 +1,278 @@ +import discord +from utils.emoji import ARROWRED, BOOST, CAST, GAMES, LEVEL_UP, LOADINGRED, LOCK, MESSAGE, MINECRAFT, MUSIC, NEW, PIN, SEED, STAR, SWORD, SYSTEM, THUNDER, TICKET, WIFI, ZAI, ZARROW, ZBAN, ZBOT, ZCIRCLE, ZCIRCLE_ALT1, ZCLOUD, ZCOUNTING, ZMODULE, ZPEOPLE, ZROCKET, ZSAFE, ZTADA, ZUNMUTE, ZWRENCH +from discord.ext import commands +from discord import app_commands, Interaction +from difflib import get_close_matches +from contextlib import suppress +from core import Context +from core.zyrox import zyrox +from core.Cog import Cog +from utils.Tools import getConfig +from itertools import chain +import json +from utils import help as vhelp +from utils import Paginator, DescriptionEmbedPaginator, FieldPagePaginator, TextPaginator +import asyncio +from utils.config import serverLink +from utils.Tools import * + +color = 0xFF0000 +client = zyrox() + +class HelpCommand(commands.HelpCommand): + + async def send_ignore_message(self, ctx, ignore_type: str): + if ignore_type == "channel": + await ctx.reply(f"This channel is ignored.", mention_author=False) + elif ignore_type == "command": + await ctx.reply(f"{ctx.author.mention} This Command, Channel, or You have been ignored here.", delete_after=6) + elif ignore_type == "user": + await ctx.reply(f"You are ignored.", mention_author=False) + + async def on_help_command_error(self, ctx, error): + errors = [ + commands.CommandOnCooldown, commands.CommandNotFound, + discord.HTTPException, commands.CommandInvokeError + ] + if not type(error) in errors: + await self.context.reply(f"Unknown Error Occurred\n{error.original}", + mention_author=False) + else: + if type(error) == commands.CommandOnCooldown: + return + return await super().on_help_command_error(ctx, error) + + async def command_not_found(self, string: str) -> None: + ctx = self.context + check_ignore = await ignore_check().predicate(ctx) + check_blacklist = await blacklist_check().predicate(ctx) + + if not check_blacklist: + return + + if not check_ignore: + await self.send_ignore_message(ctx, "command") + return + + cmds = (str(cmd) for cmd in self.context.bot.walk_commands()) + matches = get_close_matches(string, cmds) + + embed = discord.Embed( + title="Zyrox Helper", + description=f">>> **Ops! Command not found with the name** `{string}`.", + color=0xFF0000 + ) + + #if matches: + #match_list = "\n".join([f"{index}. `{match}`" for index, match in enumerate(matches, start=1)]) + #embed.add_field(name="Did you mean:", value=match_list, inline=True) + + await ctx.reply(embed=embed, mention_author=True) + + async def send_bot_help(self, mapping): + ctx = self.context + check_ignore = await ignore_check().predicate(ctx) + check_blacklist = await blacklist_check().predicate(ctx) + + if not check_blacklist: + return + + if not check_ignore: + await self.send_ignore_message(ctx, "command") + return + + # Show loading embed + loading_embed = discord.Embed( + description=f"{LOADINGRED} Loading help Menu...", + color=0xFF0000 + ) + loading_msg = await ctx.reply(embed=loading_embed) + + # Wait 2 seconds + await asyncio.sleep(2) + + # Delete loading message + with suppress(discord.NotFound): + await loading_msg.delete() + + data = await getConfig(self.context.guild.id) + prefix = data["prefix"] + filtered = await self.filter_commands(self.context.bot.walk_commands(), sort=True) + + embed = discord.Embed( + description=( + f"**{ARROWRED} __Start Zyrox X Today__**\n" + f"**{ZARROW} Type {prefix}antinuke enable**\n" + f"**{ZARROW} Server Prefix:** `{prefix}`\n" + f"**{ZARROW} Total Commands:** `{len(set(self.context.bot.walk_commands()))}`\n"), + color=0xFF0000) + embed.set_author(name=f"{ctx.author}", + icon_url=ctx.author.display_avatar.url) + embed.set_thumbnail(url=ctx.author.display_avatar.url) + + embed.add_field( + name=f"{ZCLOUD} __**Main Features**__", + value=f">>> \n {ZSAFE} `»` Security\n" + f" {ZBOT} `»` Automoderation\n" + f" {ZWRENCH} `»` Utility\n" + f" {MUSIC} `»` Music\n" + f" {WIFI} `»` Autoreact & responder\n" + f" {SWORD} `»` Moderation\n" + f" {ZPEOPLE} `»` Autorole & Invc\n" + f" {ZROCKET} `»` Fun\n" + f" {GAMES} `»` Games\n" + f" {ZBAN} `»` Ignore Channels\n" + f" {WIFI} `»` Server\n" + f" {ZUNMUTE} `»` Voice\n" + f" {SEED} `»` Welcomer\n" + f" {ZTADA} `»` Giveaway\n" + f" {TICKET} `»` Ticket {NEW}\n" + f" {ZPEOPLE} `»` Invite Tracker {NEW}\n" + ) + + embed.add_field( + name=f" {ZMODULE} __**Extra Features**__", + value=f">>> \n {CAST} `»` Advance Logging\n" + f" {STAR} `»` Vanityroles\n" + + f" {ZCOUNTING} `»` Counting {NEW}\n" + f" {SYSTEM} `»` J2C {NEW}\n" + f" {ZAI} `»` AI {NEW}\n" + f" {BOOST} `»` Boost {NEW}\n" + f" {LEVEL_UP} `»` Leveling {NEW}\n" + f" {PIN} `»` Sticky {NEW}\n" + f" {THUNDER} `»` Verification {NEW}\n" + f" {LOCK} `»` Encryption {NEW}\n" + f" {MINECRAFT} `»` Minecraft {NEW}\n" + f" {MESSAGE} `»` Joindm {NEW}\n" + f" {ZCIRCLE} `»` Birthday {NEW}\n" + f" {ZCIRCLE_ALT1} `»` Customrole\n" + ) + + embed.set_footer( + text=f"Requested By {self.context.author} | [Support](https://discord.gg/codexdev)", + ) + + view = vhelp.View(mapping=mapping, ctx=self.context, homeembed=embed, ui=2) + await ctx.reply(embed=embed, view=view) + + async def send_command_help(self, command): + ctx = self.context + check_ignore = await ignore_check().predicate(ctx) + check_blacklist = await blacklist_check().predicate(ctx) + + if not check_blacklist: + return + + if not check_ignore: + await self.send_ignore_message(ctx, "command") + return + + zyrox = f">>> {command.help}" if command.help else '>>> No Help Provided...' + embed = discord.Embed( + description=f"""{zyrox}""", + color=color) + alias = ' & '.join(command.aliases) + + embed.add_field(name="**Alt cmd**", + value=f"```{alias}```" if command.aliases else "No Alt cmd", + inline=False) + embed.add_field(name="**Usage**", + value=f"```{self.context.prefix}{command.signature}```\n") + embed.set_author(name=f"{command.qualified_name.title()} Command") + embed.set_footer(text="<[] = optional | < > = required • Use Prefix Before Commands.") + await self.context.reply(embed=embed, mention_author=False) + + def get_command_signature(self, command: commands.Command) -> str: + parent = command.full_parent_name + if len(command.aliases) > 0: + aliases = ' | '.join(command.aliases) + fmt = f'[{command.name} | {aliases}]' + if parent: + fmt = f'{parent}' + alias = f'[{command.name} | {aliases}]' + else: + alias = command.name if not parent else f'{parent} {command.name}' + return f'{alias} {command.signature}' + + def common_command_formatting(self, embed_like, command): + embed_like.title = self.get_command_signature(command) + if command.description: + embed_like.description = f'{command.description}\n\n{command.help}' + else: + embed_like.description = command.help or 'No help found...' + + async def send_group_help(self, group): + ctx = self.context + check_ignore = await ignore_check().predicate(ctx) + check_blacklist = await blacklist_check().predicate(ctx) + + if not check_blacklist: + return + + if not check_ignore: + await self.send_ignore_message(ctx, "command") + return + + entries = [ + ( + f"`{self.context.prefix}{cmd.qualified_name}`\n", + f"{cmd.short_doc if cmd.short_doc else ''}\n\u200b" + ) + for cmd in group.commands + ] + + count = len(group.commands) + + embeds = FieldPagePaginator( + entries=entries, + title=f"{group.qualified_name.title()} [{count}]", + description="< > Duty | [ ] Optional\n", + per_page=4 + ).get_pages() + + paginator = Paginator(ctx, embeds) + await paginator.paginate() + + async def send_cog_help(self, cog): + ctx = self.context + check_ignore = await ignore_check().predicate(ctx) + check_blacklist = await blacklist_check().predicate(ctx) + + if not check_blacklist: + return + + if not check_ignore: + await self.send_ignore_message(ctx, "command") + return + + entries = [( + f"> `{self.context.prefix}{cmd.qualified_name}`", + f"-# Description : {cmd.short_doc if cmd.short_doc else ''}" + f"\n\u200b", + ) for cmd in cog.get_commands()] + paginator = Paginator(source=FieldPagePaginator( + entries=entries, + title=f"Zyrox's {cog.qualified_name.title()} ({len(cog.get_commands())})", + description="`<..> Required | [..] Optional`\n\n", + color=0xFF0000, + per_page=4), + ctx=self.context) + await paginator.paginate() + + +class Help(Cog, name="help"): + + def __init__(self, client: zyrox): + self._original_help_command = client.help_command + attributes = { + 'name': "help", + 'aliases': ['h'], + 'cooldown': commands.CooldownMapping.from_cooldown(1, 5, commands.BucketType.user), + 'help': 'Shows help about bot, a command, or a category' + } + client.help_command = HelpCommand(command_attrs=attributes) + client.help_command.cog = self + + async def cog_unload(self): + self.help_command = self._original_help_command \ No newline at end of file diff --git a/bot/cogs/commands/ignore.py b/bot/cogs/commands/ignore.py new file mode 100644 index 0000000..e21d7bd --- /dev/null +++ b/bot/cogs/commands/ignore.py @@ -0,0 +1,609 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations +import discord +from utils.emoji import CROSS, TICK, ZWARNING +from discord.ui import LayoutView, TextDisplay, Separator, Container +from discord.ext import commands +from core import * +from utils.Tools import * +from typing import Optional +import aiosqlite + +color = 0xFF0000 + + +class SuccessView(LayoutView): + def __init__(self, title, description): + super().__init__(timeout=None) + self.add_item( + Container( + TextDisplay(f"**{TICK} {title}**"), + Separator(visible=True), + TextDisplay(description), + ) + ) + + +class ErrorView(LayoutView): + def __init__(self, title, description): + super().__init__(timeout=None) + self.add_item( + Container( + TextDisplay(f"**{CROSS} {title}**"), + Separator(visible=True), + TextDisplay(description), + ) + ) + + +class WarningView(LayoutView): + def __init__(self, title, description): + super().__init__(timeout=None) + self.add_item( + Container( + TextDisplay(f"**{ZWARNING} {title}**"), + Separator(visible=True), + TextDisplay(description), + ) + ) + + +class ListView(LayoutView): + def __init__(self, title, items, empty_message, guild=None): + super().__init__(timeout=None) + + if not items: + self.add_item( + Container( + TextDisplay(f"**{title}**"), + Separator(visible=True), + TextDisplay(empty_message), + ) + ) + else: + if guild: + mentions = [] + for item in items: + if isinstance(item, int): + entity = guild.get_channel(item) or guild.get_member(item) + mentions.append(entity.mention if entity else f"ID {item}") + else: + mentions.append(f"`{item}`") + description = "\n".join(mentions) + else: + description = "\n".join([f"`{item}`" for item in items]) + + self.add_item( + Container( + TextDisplay(f"**{title}**"), + Separator(visible=True), + TextDisplay(description), + ) + ) + + +class Ignore(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.db_path = "db/ignore.db" + self.color = 0xFF0000 + bot.loop.create_task(self.initialize_db()) + + async def initialize_db(self): + async with aiosqlite.connect(self.db_path) as db: + await db.execute( + "CREATE TABLE IF NOT EXISTS ignored_commands (guild_id INTEGER, command_name TEXT)" + ) + await db.execute( + "CREATE TABLE IF NOT EXISTS ignored_channels (guild_id INTEGER, channel_id INTEGER)" + ) + await db.execute( + "CREATE TABLE IF NOT EXISTS ignored_users (guild_id INTEGER, user_id INTEGER)" + ) + await db.execute( + "CREATE TABLE IF NOT EXISTS bypassed_users (guild_id INTEGER, user_id INTEGER)" + ) + await db.commit() + + @commands.group( + name="ignore", + help="Manage ignored commands, channels, users, and bypassed users.", + invoke_without_command=True, + ) + @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 _ignore(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @_ignore.group( + name="command", + help="Manage ignored commands in this guild.", + invoke_without_command=True, + ) + @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 _command(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @_command.command(name="add", help="Adds a command to the ignore list.") + @commands.has_permissions(administrator=True) + @blacklist_check() + async def command_add(self, ctx: commands.Context, command_name: str): + command_name_normalized = command_name.strip().lower() + command = self.bot.get_command(command_name_normalized) + if not command: + await ctx.reply( + view=ErrorView("Error", f"`{command_name}` is not a valid command.") + ) + return + + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute( + "SELECT COUNT(*) FROM ignored_commands WHERE guild_id = ?", + (ctx.guild.id,), + ) + count = await cursor.fetchone() + if count[0] >= 25: + await ctx.reply( + view=WarningView( + "Access Denied", + "You can only add up to 25 commands to the ignore list.", + ) + ) + return + + cursor = await db.execute( + "SELECT command_name FROM ignored_commands WHERE guild_id = ? AND command_name = ?", + (ctx.guild.id, command_name_normalized), + ) + result = await cursor.fetchone() + if result: + await ctx.reply( + view=ErrorView( + "Error", + f"`{command_name}` is already in the ignore commands list.", + ) + ) + else: + await db.execute( + "INSERT INTO ignored_commands (guild_id, command_name) VALUES (?, ?)", + (ctx.guild.id, command_name_normalized), + ) + await db.commit() + await ctx.reply( + view=SuccessView( + "Success", + f"Successfully added `{command_name}` to the ignore commands list.", + ) + ) + + @_command.command(name="remove", help="Removes a command from the ignore list.") + @commands.has_permissions(administrator=True) + @blacklist_check() + async def command_remove(self, ctx: commands.Context, command_name: str): + command_name_normalized = command_name.strip().lower() + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute( + "SELECT command_name FROM ignored_commands WHERE guild_id = ? AND command_name = ?", + (ctx.guild.id, command_name_normalized), + ) + result = await cursor.fetchone() + if not result: + await ctx.reply( + view=ErrorView( + "Error", f"`{command_name}` is not in the ignore commands list." + ) + ) + else: + await db.execute( + "DELETE FROM ignored_commands WHERE guild_id = ? AND command_name = ?", + (ctx.guild.id, command_name_normalized), + ) + await db.commit() + await ctx.reply( + view=SuccessView( + "Success", + f"Successfully removed `{command_name}` from the ignore commands list.", + ) + ) + + @_command.command(name="show", help="Displays the list of ignored commands.") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + async def command_show(self, ctx: commands.Context): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute( + "SELECT command_name FROM ignored_commands WHERE guild_id = ?", + (ctx.guild.id,), + ) + commands = await cursor.fetchall() + if not commands: + await ctx.reply( + view=ListView( + "Ignored Commands", + [], + "No commands are currently ignored in this server.", + ) + ) + else: + await ctx.reply( + view=ListView("Ignored Commands", [c[0] for c in commands], "") + ) + + @_ignore.group( + name="channel", + help="Manage ignored channels in this guild.", + invoke_without_command=True, + ) + @blacklist_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 _channel(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @_channel.command(name="add", help="Adds a channel to the ignore list.") + @blacklist_check() + @commands.has_permissions(administrator=True) + async def channel_add(self, ctx: commands.Context, channel: discord.TextChannel): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute( + "SELECT COUNT(*) FROM ignored_channels WHERE guild_id = ?", + (ctx.guild.id,), + ) + count = await cursor.fetchone() + + if count[0] >= 30: + await ctx.reply( + view=WarningView( + "Access Denied", + "You can only add up to 30 channels to the ignore list.", + ) + ) + return + + cursor = await db.execute( + "SELECT channel_id FROM ignored_channels WHERE guild_id = ? AND channel_id = ?", + (ctx.guild.id, channel.id), + ) + result = await cursor.fetchone() + + if result: + await ctx.reply( + view=ErrorView( + "Error", + f"{channel.mention} is already in the ignore channels list.", + ) + ) + else: + await db.execute( + "INSERT INTO ignored_channels (guild_id, channel_id) VALUES (?, ?)", + (ctx.guild.id, channel.id), + ) + await db.commit() + await ctx.reply( + view=SuccessView( + "Success", + f"Successfully added {channel.mention} to the ignore channels list.", + ) + ) + + @_channel.command(name="remove", help="Removes a channel from the ignore list.") + @blacklist_check() + @commands.has_permissions(administrator=True) + async def channel_remove(self, ctx: commands.Context, channel: discord.TextChannel): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute( + "SELECT channel_id FROM ignored_channels WHERE guild_id = ? AND channel_id = ?", + (ctx.guild.id, channel.id), + ) + result = await cursor.fetchone() + + if not result: + await ctx.reply( + view=ErrorView( + "Error", + f"{channel.mention} is not in the ignore channels list.", + ) + ) + else: + await db.execute( + "DELETE FROM ignored_channels WHERE guild_id = ? AND channel_id = ?", + (ctx.guild.id, channel.id), + ) + await db.commit() + await ctx.reply( + view=SuccessView( + "Success", + f"Successfully removed {channel.mention} from the ignore channels list.", + ) + ) + + @_channel.command(name="show", help="Displays the list of ignored channels.") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + async def channel_show(self, ctx: commands.Context): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute( + "SELECT channel_id FROM ignored_channels WHERE guild_id = ?", + (ctx.guild.id,), + ) + channels = await cursor.fetchall() + + if not channels: + await ctx.reply( + view=ListView( + "Ignored Channels", + [], + "No channels are currently ignored in this server.", + ) + ) + else: + await ctx.reply( + view=ListView( + "Ignored Channels", [c[0] for c in channels], "", ctx.guild + ) + ) + + @_ignore.group( + name="user", + help="Manage ignored users in this guild.", + invoke_without_command=True, + ) + @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 _user(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @_user.command(name="add", help="Adds a user to the ignore list.") + @commands.has_permissions(administrator=True) + @blacklist_check() + async def user_add(self, ctx: commands.Context, user: discord.User): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute( + "SELECT COUNT(*) FROM ignored_users WHERE guild_id = ?", (ctx.guild.id,) + ) + count = await cursor.fetchone() + + if count[0] >= 30: + await ctx.reply( + view=WarningView( + "Access Denied", + "You can only add up to 30 users to the ignore list.", + ) + ) + return + + cursor = await db.execute( + "SELECT user_id FROM ignored_users WHERE guild_id = ? AND user_id = ?", + (ctx.guild.id, user.id), + ) + result = await cursor.fetchone() + + if result: + await ctx.reply( + view=ErrorView( + "Error", f"{user.mention} is already in the ignore users list." + ) + ) + else: + await db.execute( + "INSERT INTO ignored_users (guild_id, user_id) VALUES (?, ?)", + (ctx.guild.id, user.id), + ) + await db.commit() + await ctx.reply( + view=SuccessView( + "Success", + f"Successfully added {user.mention} to the ignore users list.", + ) + ) + + @_user.command(name="remove", help="Removes a user from the ignore list.") + @blacklist_check() + @commands.has_permissions(administrator=True) + async def user_remove(self, ctx: commands.Context, user: discord.User): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute( + "SELECT user_id FROM ignored_users WHERE guild_id = ? AND user_id = ?", + (ctx.guild.id, user.id), + ) + result = await cursor.fetchone() + + if not result: + await ctx.reply( + view=ErrorView( + "Error", f"{user.mention} is not in the ignore users list." + ) + ) + else: + await db.execute( + "DELETE FROM ignored_users WHERE guild_id = ? AND user_id = ?", + (ctx.guild.id, user.id), + ) + await db.commit() + await ctx.send( + view=SuccessView( + "Success", + f"Successfully removed {user.mention} from the ignore users list.", + ) + ) + + @_user.command(name="show", help="Displays the list of ignored users.") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + async def user_show(self, ctx: commands.Context): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute( + "SELECT user_id FROM ignored_users WHERE guild_id = ?", (ctx.guild.id,) + ) + users = await cursor.fetchall() + + if not users: + await ctx.reply( + view=ListView( + "Ignored Users", + [], + "No users are currently ignored in this server.", + ) + ) + else: + await ctx.reply( + view=ListView("Ignored Users", [u[0] for u in users], "", ctx.guild) + ) + + @_ignore.group( + name="bypass", + help="Manage bypassed users in this guild.", + invoke_without_command=True, + ) + @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 _bypass(self, ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @_bypass.command(name="add", help="Adds a user to the bypass list.") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + async def bypass_add(self, ctx: commands.Context, user: discord.User): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute( + "SELECT COUNT(*) FROM bypassed_users WHERE guild_id = ?", + (ctx.guild.id,), + ) + count = await cursor.fetchone() + + if count[0] >= 30: + await ctx.reply( + view=WarningView( + "Access Denied", + "You can only add up to 30 users to the bypass list.", + ) + ) + return + + cursor = await db.execute( + "SELECT user_id FROM bypassed_users WHERE guild_id = ? AND user_id = ?", + (ctx.guild.id, user.id), + ) + result = await cursor.fetchone() + + if result: + await ctx.reply( + view=ErrorView( + "Error", f"{user.mention} is already in the bypass users list." + ) + ) + else: + await db.execute( + "INSERT INTO bypassed_users (guild_id, user_id) VALUES (?, ?)", + (ctx.guild.id, user.id), + ) + await db.commit() + await ctx.reply( + view=SuccessView( + "Success", + f"Successfully added {user.mention} to the bypass users list.", + ) + ) + + @_bypass.command(name="remove", help="Removes a user from the bypass list.") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + async def bypass_remove(self, ctx: commands.Context, user: discord.User): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute( + "SELECT user_id FROM bypassed_users WHERE guild_id = ? AND user_id = ?", + (ctx.guild.id, user.id), + ) + result = await cursor.fetchone() + + if not result: + await ctx.reply( + view=ErrorView( + "Error", f"{user.mention} is not in the bypass users list." + ) + ) + else: + await db.execute( + "DELETE FROM bypassed_users WHERE guild_id = ? AND user_id = ?", + (ctx.guild.id, user.id), + ) + await db.commit() + await ctx.reply( + view=SuccessView( + "Success", + f"Successfully removed {user.mention} from the bypass users list.", + ) + ) + + @_bypass.command( + name="show", aliases=["list"], help="Displays the list of bypassed users." + ) + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + async def bypass_show(self, ctx: commands.Context): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute( + "SELECT user_id FROM bypassed_users WHERE guild_id = ?", (ctx.guild.id,) + ) + users = await cursor.fetchall() + + if not users: + await ctx.reply( + view=ListView( + "Bypassed Users", + [], + "No users are currently bypassed in this server.", + ) + ) + else: + await ctx.reply( + view=ListView( + "Bypassed Users", [u[0] for u in users], "", ctx.guild + ) + ) diff --git a/bot/cogs/commands/image.py b/bot/cogs/commands/image.py new file mode 100644 index 0000000..64fc066 --- /dev/null +++ b/bot/cogs/commands/image.py @@ -0,0 +1,78 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +from discord.ui import LayoutView, MediaGallery, TextDisplay +import aiohttp +import os +import random +from utils.cv2 import CV2, build_container + +PEXELS_API_KEY = "js24mfV1bCCvgV6KfnEFvo5UnCHnATFarFnAdDrpDbczl7f0yXpjDF8x" + +class ImageCommands(commands.Cog): + def __init__(self, bot): + self.bot = bot + + async def fetch_pexels_image(self, query): + headers = { + "Authorization": PEXELS_API_KEY + } + async with aiohttp.ClientSession() as session: + async with session.get(f"https://api.pexels.com/v1/search?query={query}&per_page=50", headers=headers) as resp: + data = await resp.json() + if data.get("photos"): + image = random.choice(data["photos"]) + return image["src"]["original"] + return None + + async def fetch_waifu_image(self, category="waifu"): + async with aiohttp.ClientSession() as session: + async with session.get(f"https://api.waifu.pics/sfw/{category}") as resp: + data = await resp.json() + return data["url"] + + async def send_image_view(self, ctx, title, url): + if url: + view = LayoutView(timeout=None) + gallery = MediaGallery() + gallery.add_item(media=url) + view.add_item(build_container(TextDisplay(f"**{title}**"), gallery)) + await ctx.send(view=view) + else: + await ctx.send(view=CV2("❌ Error", f"No image found for {title.lower()}.")) + + @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.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.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.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) + +async def setup(bot): + await bot.add_cog(ImageCommands(bot)) diff --git a/bot/cogs/commands/imagine.py b/bot/cogs/commands/imagine.py new file mode 100644 index 0000000..3e6c976 --- /dev/null +++ b/bot/cogs/commands/imagine.py @@ -0,0 +1,168 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +from discord import app_commands +import aiohttp +import asyncio +import random +import time +from utils.ai_utils import poly_image_gen, generate_image_prodia +from prodia.constants import Model +from utils.Tools import * +from utils.cv2 import CV2 + +blacklisted_words = [ + "naked", "nude", "nudes", "teen", "gay", "lesbian", "porn", "xnxx", + "bitch", "loli", "hentai", "explicit", "pornography", "adult", "XXX", + "sex", "erotic", "dick", "vagina", "pussy", "gay", "lick", "creampie", "nsfw", + "hardcore", "ass", "anal", "anus", "boobs", "tits", "cum", "cunnilingus", "squirt", "penis", "lick", "masturbate", "masturbation ", "orgasm", "orgy", "fap", "fapping", "fuck", "fucking", "handjob", "cowgirl", "doggystyle", "blowjob", "boobjob", "boobies", "horny", "nudity" +] + +blocked=["minor", "minors", "kid", "kids", "child", "children", "baby", "babies", "toddler", "childporn", "todd", "underage"] + +class CooldownManager: + def __init__(self, rate: int, per: float): + self.rate = rate + self.per = per + self.cooldowns = {} + + def check_cooldown(self, user_id: int): + now = time.time() + if user_id not in self.cooldowns: + self.cooldowns[user_id] = [now] + return None + + self.cooldowns[user_id] = [timestamp for timestamp in self.cooldowns[user_id] if now - timestamp < self.per] + if len(self.cooldowns[user_id]) >= self.rate: + retry_after = self.per - (now - self.cooldowns[user_id][0]) + return retry_after + self.cooldowns[user_id].append(now) + return None + +cooldown_manager = CooldownManager(rate=1, per=60.0) + +async def cooldown_check(interaction: discord.Interaction): + retry_after = cooldown_manager.check_cooldown(interaction.user.id) + if retry_after: + await interaction.response.send_message(f"You are on cooldown. Try again in {retry_after:.2f} seconds.", ephemeral=True) + return False + return True + + + + + +class AiStuffCog(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.guild_only() + @app_commands.command(name="imagine", description="Generate an image using AI") + @discord.app_commands.choices( + model=[ + discord.app_commands.Choice(name='✨ Elldreth vivid mix (Landscapes, Stylized characters, nsfw)', value='ELLDRETHVIVIDMIX'), + discord.app_commands.Choice(name='💪 Deliberate v2 (Anything you want, nsfw)', value='DELIBERATE'), + discord.app_commands.Choice(name='🔮 Dreamshaper (HOLYSHIT this so good)', value='DREAMSHAPER_6'), + discord.app_commands.Choice(name='🎼 Lyriel', value='LYRIEL_V16'), + discord.app_commands.Choice(name='💥 Anything diffusion (Good for anime)', value='ANYTHING_V4'), + discord.app_commands.Choice(name='🌅 Openjourney (Midjourney alternative)', value='OPENJOURNEY'), + discord.app_commands.Choice(name='🏞️ Realistic (Lifelike pictures)', value='REALISTICVS_V20'), + discord.app_commands.Choice(name='👨‍🎨 Portrait (For headshots I guess)', value='PORTRAIT'), + discord.app_commands.Choice(name='🌟 Rev animated (Illustration, Anime)', value='REV_ANIMATED'), + discord.app_commands.Choice(name='🤖 Analog', value='ANALOG'), + discord.app_commands.Choice(name='🌌 AbyssOrangeMix', value='ABYSSORANGEMIX'), + discord.app_commands.Choice(name='🌌 Dreamlike v1', value='DREAMLIKE_V1'), + discord.app_commands.Choice(name='🌌 Dreamlike v2', value='DREAMLIKE_V2'), + discord.app_commands.Choice(name='🌌 Dreamshaper 5', value='DREAMSHAPER_5'), + discord.app_commands.Choice(name='🌌 MechaMix', value='MECHAMIX'), + discord.app_commands.Choice(name='🌌 MeinaMix', value='MEINAMIX'), + discord.app_commands.Choice(name='🌌 Stable Diffusion v14', value='SD_V14'), + discord.app_commands.Choice(name='🌌 Stable Diffusion v15', value='SD_V15'), + discord.app_commands.Choice(name="🌌 Shonin's Beautiful People", value='SBP'), + discord.app_commands.Choice(name="🌌 TheAlly's Mix II", value='THEALLYSMIX'), + discord.app_commands.Choice(name='🌌 Timeless', value='TIMELESS') + ], + sampler=[ + discord.app_commands.Choice(name='📏 Euler (Recommended)', value='Euler'), + discord.app_commands.Choice(name='📏 Euler a', value='Euler a'), + discord.app_commands.Choice(name='📐 Heun', value='Heun'), + discord.app_commands.Choice(name='💥 DPM++ 2M Karras', value='DPM++ 2M Karras'), + discord.app_commands.Choice(name='💥 DPM++ SDE Karras', value='DPM++ SDE Karras'), + discord.app_commands.Choice(name='🔍 DDIM', value='DDIM') + ] + ) + @discord.app_commands.describe( + prompt="Write an amazing prompt for an image", + model="Model to generate image", + sampler="Sampler for denoising", + negative="Prompt that specifies what you do not want the model to generate", + ) + async def imagine(self, interaction: discord.Interaction, prompt: str, model: discord.app_commands.Choice[str], sampler: discord.app_commands.Choice[str], negative: str = None, seed: int = None): + retry_after = cooldown_manager.check_cooldown(interaction.user.id) + if retry_after: + await interaction.response.send_message(f"You are on cooldown. Try again in {retry_after:.2f} seconds.", ephemeral=True) + return + + await interaction.response.defer() + + is_nsfw = any(word in prompt.lower() for word in blacklisted_words) + + is_child = any(word in prompt.lower() for word in blocked) + + if is_child: + await interaction.followup.send("Child porn is not allowed as it violates Discord ToS. Please try again with a different peompt.") + return + + if is_nsfw and not interaction.channel.nsfw: + await interaction.followup.send("You can create NSFW images in NSFW channels only. Please try in an appropriate channel.", ephemeral=True) + return + + model_uid = Model[model.value].value[0] + + try: + imagefileobj = await generate_image_prodia(prompt, model_uid, sampler.value, seed, negative) + except aiohttp.ClientPayloadError: + await interaction.followup.send("An error occurred while generating the image. Please try again later.", ephemeral=True) + return + except Exception as e: + await interaction.followup.send(f"An unexpected error occurred: {e}", ephemeral=True) + return + + if is_nsfw: + img_file = discord.File(imagefileobj, filename="image.png", spoiler=True, description=prompt) + prompt = f"||{prompt}||" + else: + img_file = discord.File(imagefileobj, filename="image.png", description=prompt) + + desc = ( + f"**Prompt:** {prompt}\n" + f"**Model:** {model.value}\n" + f"**Sampler:** {sampler.value}\n" + f"**Seed:** {seed}" + ) + if negative: + desc += f"\n**Negative Prompt:** {negative}" + if is_nsfw: + desc += f"\n**NSFW:** {str(is_nsfw)}" + + view = CV2(f"Generated Image by {interaction.user.display_name}", desc) + + await interaction.followup.send(view=view, file=img_file, ephemeral=True) + + + +async def setup(bot): + await bot.add_cog(AiStuffCog(bot)) diff --git a/bot/cogs/commands/j2c.py b/bot/cogs/commands/j2c.py new file mode 100644 index 0000000..6a4c3f9 --- /dev/null +++ b/bot/cogs/commands/j2c.py @@ -0,0 +1,726 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +from discord import ui, SelectOption +from discord.ui import LayoutView, TextDisplay, Separator, ActionRow +import aiosqlite +import asyncio +from typing import Dict, List, Optional +from utils.cv2 import CV2, build_container + +class JoinToCreate(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.private_channels: Dict[int, Dict] = {} + self.category_name = "J2C" + self.setup_data: Dict[int, Dict] = {} + self.db_path = "j2c_data.db" + self.blocked_users: Dict[int, List[int]] = {} # {vc_id: [user_ids]} + self.creating_vc = set() + + async def init_db(self): + async with aiosqlite.connect(self.db_path) as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS guild_setup ( + guild_id INTEGER PRIMARY KEY, + join_channel_id INTEGER, + control_channel_id INTEGER, + control_message_id INTEGER, + category_id INTEGER + ) + """) + try: + await db.execute("ALTER TABLE guild_setup ADD COLUMN category_id INTEGER") + except aiosqlite.OperationalError: + pass + await db.execute(""" + CREATE TABLE IF NOT EXISTS private_channels ( + vc_id INTEGER PRIMARY KEY, + guild_id INTEGER, + owner_id INTEGER, + member_limit INTEGER DEFAULT 2, + region TEXT DEFAULT '', + is_locked BOOLEAN DEFAULT FALSE, + has_waiting_room BOOLEAN DEFAULT FALSE, + has_thread BOOLEAN DEFAULT FALSE + ) + """) + await db.execute(""" + CREATE TABLE IF NOT EXISTS blocked_users ( + vc_id INTEGER, + user_id INTEGER, + PRIMARY KEY (vc_id, user_id) + ) + """) + await db.commit() + + async def load_data(self): + async with aiosqlite.connect(self.db_path) as db: + # Load guild setups + async with db.execute("SELECT guild_id, join_channel_id, control_channel_id, control_message_id, category_id FROM guild_setup") as cursor: + async for row in cursor: + guild_id, join_channel_id, control_channel_id, control_message_id, category_id = row + self.setup_data[guild_id] = { + "join_channel_id": join_channel_id, + "control_channel_id": control_channel_id, + "control_message_id": control_message_id, + "category_id": category_id + } + + # Load private channels + async with db.execute("SELECT * FROM private_channels") as cursor: + async for row in cursor: + vc_id, guild_id, owner_id, member_limit, region, is_locked, has_waiting_room, has_thread = row + self.private_channels[vc_id] = { + "owner": owner_id, + "limit": member_limit, + "region": region, + "is_locked": bool(is_locked), + "has_waiting_room": bool(has_waiting_room), + "has_thread": bool(has_thread), + "guild_id": guild_id + } + + # Load blocked users + async with db.execute("SELECT * FROM blocked_users") as cursor: + async for row in cursor: + vc_id, user_id = row + if vc_id not in self.blocked_users: + self.blocked_users[vc_id] = [] + self.blocked_users[vc_id].append(user_id) + + async def save_guild_setup(self, guild_id: int, data: Dict): + async with aiosqlite.connect(self.db_path) as db: + await db.execute(""" + INSERT OR REPLACE INTO guild_setup (guild_id, join_channel_id, control_channel_id, control_message_id, category_id) + VALUES (?, ?, ?, ?, ?) + """, (guild_id, data["join_channel_id"], data["control_channel_id"], data["control_message_id"], data.get("category_id"))) + await db.commit() + + async def save_private_channel(self, vc_id: int, guild_id: int, data: Dict): + try: + if vc_id not in self.private_channels: + self.private_channels[vc_id] = data + + async with aiosqlite.connect(self.db_path) as db: + await db.execute(""" + INSERT OR REPLACE INTO private_channels + (vc_id, guild_id, owner_id, member_limit, region, is_locked, has_waiting_room, has_thread) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, ( + vc_id, guild_id, + data["owner"], + data.get("limit", 2), + data.get("region", ""), + data.get("is_locked", False), + data.get("has_waiting_room", False), + data.get("has_thread", False) + )) + await db.commit() + except Exception as e: + print(f"Error saving private channel: {e}") + + async def delete_private_channel(self, vc_id: int): + async with aiosqlite.connect(self.db_path) as db: + await db.execute("DELETE FROM private_channels WHERE vc_id = ?", (vc_id,)) + await db.execute("DELETE FROM blocked_users WHERE vc_id = ?", (vc_id,)) + await db.commit() + + async def delete_guild_setup(self, guild_id: int): + async with aiosqlite.connect(self.db_path) as db: + await db.execute("DELETE FROM guild_setup WHERE guild_id = ?", (guild_id,)) + await db.execute("DELETE FROM private_channels WHERE guild_id = ?", (guild_id,)) + await db.execute("DELETE FROM blocked_users WHERE vc_id IN (SELECT vc_id FROM private_channels WHERE guild_id = ?)", (guild_id,)) + await db.commit() + + async def block_user(self, vc_id: int, user_id: int): + async with aiosqlite.connect(self.db_path) as db: + await db.execute("INSERT OR IGNORE INTO blocked_users (vc_id, user_id) VALUES (?, ?)", (vc_id, user_id)) + await db.commit() + if vc_id not in self.blocked_users: + self.blocked_users[vc_id] = [] + if user_id not in self.blocked_users[vc_id]: + self.blocked_users[vc_id].append(user_id) + + async def unblock_user(self, vc_id: int, user_id: int): + async with aiosqlite.connect(self.db_path) as db: + await db.execute("DELETE FROM blocked_users WHERE vc_id = ? AND user_id = ?", (vc_id, user_id)) + await db.commit() + if vc_id in self.blocked_users and user_id in self.blocked_users[vc_id]: + self.blocked_users[vc_id].remove(user_id) + + @commands.Cog.listener() + async def on_ready(self): + await self.init_db() + await self.load_data() + for guild_id, data in self.setup_data.items(): + guild = self.bot.get_guild(guild_id) + if guild: + try: + control_channel = guild.get_channel(data["control_channel_id"]) + if control_channel: + msg = None + if data["control_message_id"]: + try: + msg = await control_channel.fetch_message(data["control_message_id"]) + except: + pass + + if msg: + view = ControlPanelView(self, guild) + await msg.edit(view=view, embed=None, content=None) + else: + view = ControlPanelView(self, guild) + msg = await control_channel.send(view=view) + self.setup_data[guild_id]["control_message_id"] = msg.id + await self.save_guild_setup(guild_id, self.setup_data[guild_id]) + except Exception as e: + print(f"Error in J2C on_ready: {e}") + continue + + @commands.command(name='j2csetup') + @commands.has_permissions(administrator=True) + async def setup_private_channels(self, ctx): + if ctx.guild.id in self.setup_data: + await ctx.send(view=CV2("❌ Error", "J2C system is already setup in this server!")) + return + + category = discord.utils.get(ctx.guild.categories, name=self.category_name) + if not category: + category = await ctx.guild.create_category(self.category_name) + + join_channel = await ctx.guild.create_voice_channel( + "➕ Join to Create", + category=category, + reason="J2C System Setup" + ) + + control_channel = await ctx.guild.create_text_channel( + "ctrl-panel", + category=category, + reason="J2C System Setup" + ) + + view = ControlPanelView(self, ctx.guild) + control_message = await control_channel.send(view=view) + + self.setup_data[ctx.guild.id] = { + "join_channel_id": join_channel.id, + "control_channel_id": control_channel.id, + "control_message_id": control_message.id + } + await self.save_guild_setup(ctx.guild.id, self.setup_data[ctx.guild.id]) + + await ctx.send(view=CV2("✅ Success", f"J2C system setup complete! Join {join_channel.mention} to create a private VC.")) + + @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: + await ctx.send(view=CV2("❌ Error", "J2C system is not setup in this server!")) + return + + category = discord.utils.get(ctx.guild.categories, name=self.category_name) + if category: + for channel in category.channels: + try: + await channel.delete(reason="J2C System Reset") + except: + continue + + if category: + try: + await category.delete(reason="J2C System Reset") + except: + pass + + vc_ids = [vc_id for vc_id, data in self.private_channels.items() + if data.get("guild_id") == ctx.guild.id] + for vc_id in vc_ids: + del self.private_channels[vc_id] + + del self.setup_data[ctx.guild.id] + await self.delete_guild_setup(ctx.guild.id) + + await ctx.send(view=CV2("✅ Success", "J2C system has been completely reset in this server!")) + + @commands.Cog.listener() + async def on_voice_state_update(self, member, before, after): + if member.guild.id not in self.setup_data: + return + + guild_data = self.setup_data[member.guild.id] + + # User joined the join channel + if after.channel and after.channel.id == guild_data["join_channel_id"]: + if member.id in self.creating_vc: + return + self.creating_vc.add(member.id) + try: + category_id = guild_data.get("category_id") + category = None + if category_id: + try: + category = member.guild.get_channel(int(category_id)) + except: + pass + + # Fallback 1: Use the parent category of the join channel + if not category and after.channel.category: + category = after.channel.category + + # Fallback 2: J2C category by name + if not category: + category = discord.utils.get(member.guild.categories, name=self.category_name) + + vc = await member.guild.create_voice_channel( + f"{member.name}'s VC", + category=category, + reason="Private VC Creation", + user_limit=2 + ) + + await member.move_to(vc) + + self.private_channels[vc.id] = { + "owner": member.id, + "limit": 2, + "region": "", + "is_locked": False, + "has_waiting_room": False, + "has_thread": False, + "guild_id": member.guild.id + } + await self.save_private_channel(vc.id, member.guild.id, self.private_channels[vc.id]) + await self.update_control_panel(member.guild) + finally: + self.creating_vc.discard(member.id) + + # User left a private channel + if before.channel and before.channel.id in self.private_channels: + # Check if user was blocked + if (before.channel.id in self.blocked_users and + member.id in self.blocked_users[before.channel.id]): + await member.move_to(None) + return + + # Check if channel is empty + if len(before.channel.members) == 0: + try: + await before.channel.delete() + except: + pass + + if before.channel.id in self.private_channels: + del self.private_channels[before.channel.id] + await self.delete_private_channel(before.channel.id) + + await self.update_control_panel(member.guild) + + async def update_control_panel(self, guild: discord.Guild): + if guild.id not in self.setup_data: + return + + guild_data = self.setup_data[guild.id] + control_channel = guild.get_channel(guild_data["control_channel_id"]) + if not control_channel or not guild_data["control_message_id"]: + return + + try: + control_message = await control_channel.fetch_message(guild_data["control_message_id"]) + view = ControlPanelView(self, guild) + await control_message.edit(view=view, embed=None, content=None) + except: + pass + +class ControlPanelView(LayoutView): + def __init__(self, cog, guild): + super().__init__(timeout=None) + self.cog = cog + + desc = ("Join the 'Join to Create VC' voice channel to create your own private voice channel.\n\n" + "Use the buttons below to manage your private VC.") + active_vcs = [] + for vc_id, data in self.cog.private_channels.items(): + if guild.get_channel(vc_id): + owner = guild.get_member(data["owner"]) + if owner: + status = [] + if data["is_locked"]: + status.append("🔒 Locked") + if data["has_thread"]: + status.append("💬 Thread") + + vc_info = f"<#{vc_id}> (👑 {owner.mention})" + if status: + vc_info += f" [{' '.join(status)}]" + active_vcs.append(vc_info) + + if active_vcs: + desc += "\n\n**Active Private VCs**\n" + "\n".join(active_vcs) + + container = build_container( + TextDisplay("**J2C System**"), + Separator(visible=True), + TextDisplay(desc) + ) + self.add_item(container) + + b_limit = ui.Button(label="LIMIT", style=discord.ButtonStyle.blurple, custom_id="j2c:limit", emoji="⏳") + b_limit.callback = self.set_limit + b_privacy = ui.Button(label="PRIVACY", style=discord.ButtonStyle.blurple, custom_id="j2c:privacy", emoji="🔒") + b_privacy.callback = self.toggle_privacy + b_thread = ui.Button(label="THREAD", style=discord.ButtonStyle.blurple, custom_id="j2c:thread", emoji="💬") + b_thread.callback = self.create_thread + b_untrust = ui.Button(label="UNTRUST", style=discord.ButtonStyle.green, custom_id="j2c:untrust", emoji="❌") + b_untrust.callback = self.untrust + b_invite = ui.Button(label="INVITE", style=discord.ButtonStyle.green, custom_id="j2c:invite", emoji="✉️") + b_invite.callback = self.invite_user + b_kick = ui.Button(label="KICK", style=discord.ButtonStyle.green, custom_id="j2c:kick", emoji="👢") + b_kick.callback = self.kick_user + b_region = ui.Button(label="REGION", style=discord.ButtonStyle.green, custom_id="j2c:region", emoji="🌍") + b_region.callback = self.set_region + b_unblock = ui.Button(label="UNBLOCK", style=discord.ButtonStyle.red, custom_id="j2c:unblock", emoji="🔓") + b_unblock.callback = self.unblock + b_claim = ui.Button(label="CLAIM", style=discord.ButtonStyle.red, custom_id="j2c:claim", emoji="⭐") + b_claim.callback = self.claim + b_transfer = ui.Button(label="TRANSFER", style=discord.ButtonStyle.red, custom_id="j2c:transfer", emoji="🔄") + b_transfer.callback = self.transfer + b_delete = ui.Button(label="DELETE", style=discord.ButtonStyle.red, custom_id="j2c:delete", emoji="🗑️") + b_delete.callback = self.delete_vc + b_block = ui.Button(label="BLOCK", style=discord.ButtonStyle.danger, custom_id="j2c:block", emoji="🚫") + b_block.callback = self.block + + self.add_item(ActionRow(b_limit, b_privacy, b_thread)) + self.add_item(ActionRow(b_untrust, b_invite, b_kick, b_region)) + self.add_item(ActionRow(b_unblock, b_claim, b_transfer, b_delete)) + self.add_item(ActionRow(b_block)) + + async def get_owned_vc(self, interaction: discord.Interaction) -> Optional[discord.VoiceChannel]: + for vc_id, data in self.cog.private_channels.items(): + if data["owner"] == interaction.user.id: + vc = interaction.guild.get_channel(vc_id) + if vc: + return vc + return None + + async def set_limit(self, interaction: discord.Interaction): + vc = await self.get_owned_vc(interaction) + if not vc: + await interaction.response.send_message("You don't own any private VC!", ephemeral=True) + return + modal = SetLimitModal(vc) + await interaction.response.send_modal(modal) + + async def toggle_privacy(self, interaction: discord.Interaction): + await interaction.response.defer(ephemeral=True) + vc = await self.get_owned_vc(interaction) + if not vc: + await interaction.followup.send("You don't own any private VC!", ephemeral=True) + return + is_locked = not self.cog.private_channels[vc.id]["is_locked"] + await vc.set_permissions(interaction.guild.default_role, connect=not is_locked) + self.cog.private_channels[vc.id]["is_locked"] = is_locked + await self.cog.save_private_channel(vc.id, interaction.guild.id, self.cog.private_channels[vc.id]) + await interaction.followup.send(f"VC is now {'🔒 locked' if is_locked else '🔓 unlocked'}!", ephemeral=True) + await self.cog.update_control_panel(interaction.guild) + + + async def create_thread(self, interaction: discord.Interaction): + await interaction.response.defer(ephemeral=True) + vc = await self.get_owned_vc(interaction) + if not vc: + await interaction.followup.send("You don't own any private VC!", ephemeral=True) + return + has_thread = not self.cog.private_channels[vc.id]["has_thread"] + self.cog.private_channels[vc.id]["has_thread"] = has_thread + await self.cog.save_private_channel(vc.id, interaction.guild.id, self.cog.private_channels[vc.id]) + if has_thread: + thread = await interaction.channel.create_thread(name=f"{vc.name} Discussion", auto_archive_duration=60) + await interaction.followup.send(f"Created thread: {thread.mention}", ephemeral=True) + else: + await interaction.followup.send("Thread feature disabled for your VC", ephemeral=True) + await self.cog.update_control_panel(interaction.guild) + + async def untrust(self, interaction: discord.Interaction): + await interaction.response.defer(ephemeral=True) + vc = await self.get_owned_vc(interaction) + if not vc: + await interaction.followup.send("You don't own any private VC!", ephemeral=True) + return + await interaction.followup.send("Untrust feature would be implemented here", ephemeral=True) + + async def invite_user(self, interaction: discord.Interaction): + vc = await self.get_owned_vc(interaction) + if not vc: + await interaction.response.send_message("You don't own any private VC!", ephemeral=True) + return + options = [] + for member in interaction.guild.members: + if (member not in vc.members and not member.bot and member != interaction.user): + options.append(SelectOption(label=member.name, value=str(member.id))) + if not options: + await interaction.response.send_message("No members available to invite!", ephemeral=True) + return + dropdown = UserSelectDropdown(options, "Select members to invite", self.invite_selected) + view = ui.View() + view.add_item(dropdown) + await interaction.response.send_message("Select members to invite:", view=view, ephemeral=True) + + async def invite_selected(self, interaction: discord.Interaction, selected: List[str]): + await interaction.response.defer(ephemeral=True) + vc = await self.get_owned_vc(interaction) + if not vc: return + for user_id in selected: + member = interaction.guild.get_member(int(user_id)) + if member: + try: await member.send(f"You've been invited to join {vc.mention} by {interaction.user.mention}!") + except: pass + await interaction.followup.send("Invites sent!", ephemeral=True) + + async def kick_user(self, interaction: discord.Interaction): + vc = await self.get_owned_vc(interaction) + if not vc: + await interaction.response.send_message("You don't own any private VC!", ephemeral=True) + return + options = [] + for member in vc.members: + if member.id != interaction.user.id: + options.append(SelectOption(label=member.name, value=str(member.id))) + if not options: + await interaction.response.send_message("No users to kick in your VC!", ephemeral=True) + return + dropdown = UserSelectDropdown(options, "Select members to kick", self.kick_selected) + view = ui.View() + view.add_item(dropdown) + await interaction.response.send_message("Select members to kick:", view=view, ephemeral=True) + + async def kick_selected(self, interaction: discord.Interaction, selected: List[str]): + await interaction.response.defer(ephemeral=True) + vc = await self.get_owned_vc(interaction) + if not vc: return + for user_id in selected: + member = interaction.guild.get_member(int(user_id)) + if member and member in vc.members: + try: await member.move_to(None) + except: pass + await interaction.followup.send("Selected members kicked!", ephemeral=True) + + async def set_region(self, interaction: discord.Interaction): + vc = await self.get_owned_vc(interaction) + if not vc: + await interaction.response.send_message("You don't own any private VC!", ephemeral=True) + return + regions = [ + SelectOption(label="Automatic", value="auto"), SelectOption(label="US West", value="us-west"), + SelectOption(label="US East", value="us-east"), SelectOption(label="Europe", value="europe"), + SelectOption(label="Singapore", value="singapore"), SelectOption(label="Japan", value="japan"), + SelectOption(label="Brazil", value="brazil"), SelectOption(label="Australia", value="australia") + ] + dropdown = RegionSelectDropdown(regions, vc) + view = ui.View() + view.add_item(dropdown) + await interaction.response.send_message("Select a region:", view=view, ephemeral=True) + + async def unblock(self, interaction: discord.Interaction): + vc = await self.get_owned_vc(interaction) + if not vc: + await interaction.response.send_message("You don't own any private VC!", ephemeral=True) + return + if vc.id not in self.cog.blocked_users or not self.cog.blocked_users[vc.id]: + await interaction.response.send_message("No blocked users!", ephemeral=True) + return + options = [] + for user_id in self.cog.blocked_users[vc.id]: + member = interaction.guild.get_member(user_id) + if member: + options.append(SelectOption(label=member.name, value=str(user_id))) + dropdown = UserSelectDropdown(options, "Select users to unblock", self.unblock_selected) + view = ui.View() + view.add_item(dropdown) + await interaction.response.send_message("Select users to unblock:", view=view, ephemeral=True) + + async def unblock_selected(self, interaction: discord.Interaction, selected: List[str]): + await interaction.response.defer(ephemeral=True) + vc = await self.get_owned_vc(interaction) + if not vc: return + for user_id in selected: + await self.cog.unblock_user(vc.id, int(user_id)) + await interaction.followup.send("Selected users unblocked!", ephemeral=True) + await self.cog.update_control_panel(interaction.guild) + + async def claim(self, interaction: discord.Interaction): + available_vcs = [] + for vc_id, data in self.cog.private_channels.items(): + if data["guild_id"] == interaction.guild.id: + vc = interaction.guild.get_channel(vc_id) + if vc: + owner = interaction.guild.get_member(data["owner"]) + if not owner or owner not in vc.members: + available_vcs.append((vc, data)) + if not available_vcs: + await interaction.response.send_message("No VCs available to claim!", ephemeral=True) + return + options = [] + for vc, data in available_vcs: + owner_mention = f"<{data['owner']}>" if data['owner'] else "Unknown" + options.append(SelectOption(label=f"{vc.name} (prev: {owner_mention})", value=str(vc.id))) + dropdown = VCSelectDropdown(options, "Select VC to claim", self.claim_selected) + view = ui.View() + view.add_item(dropdown) + await interaction.response.send_message("Select VC to claim:", view=view, ephemeral=True) + + async def claim_selected(self, interaction: discord.Interaction, selected: List[str]): + await interaction.response.defer(ephemeral=True) + vc_id = int(selected[0]) + vc = interaction.guild.get_channel(vc_id) + if not vc: + await interaction.followup.send("VC no longer exists!", ephemeral=True) + return + self.cog.private_channels[vc.id]["owner"] = interaction.user.id + await self.cog.save_private_channel(vc.id, interaction.guild.id, self.cog.private_channels[vc.id]) + await interaction.followup.send(f"You've claimed {vc.mention}!", ephemeral=True) + await self.cog.update_control_panel(interaction.guild) + + async def transfer(self, interaction: discord.Interaction): + vc = await self.get_owned_vc(interaction) + if not vc: + await interaction.response.send_message("You don't own any private VC!", ephemeral=True) + return + options = [] + for member in vc.members: + if member.id != interaction.user.id: + options.append(SelectOption(label=member.name, value=str(member.id))) + if not options: + await interaction.response.send_message("No users to transfer to in your VC!", ephemeral=True) + return + dropdown = UserSelectDropdown(options, "Select new owner", self.transfer_selected) + view = ui.View() + view.add_item(dropdown) + await interaction.response.send_message("Select new owner:", view=view, ephemeral=True) + + async def transfer_selected(self, interaction: discord.Interaction, selected: List[str]): + await interaction.response.defer(ephemeral=True) + vc = await self.get_owned_vc(interaction) + if not vc: return + new_owner_id = int(selected[0]) + new_owner = interaction.guild.get_member(new_owner_id) + if not new_owner: + await interaction.followup.send("User not found!", ephemeral=True) + return + self.cog.private_channels[vc.id]["owner"] = new_owner_id + await self.cog.save_private_channel(vc.id, interaction.guild.id, self.cog.private_channels[vc.id]) + await interaction.followup.send(f"Transferred ownership of {vc.mention} to {new_owner.mention}!", ephemeral=True) + await self.cog.update_control_panel(interaction.guild) + + async def delete_vc(self, interaction: discord.Interaction): + await interaction.response.defer(ephemeral=True) + vc = await self.get_owned_vc(interaction) + if not vc: + await interaction.followup.send("You don't own any private VC!", ephemeral=True) + return + for member in vc.members: + try: await member.move_to(None) + except: pass + await vc.delete() + if vc.id in self.cog.private_channels: + del self.cog.private_channels[vc.id] + await self.cog.delete_private_channel(vc.id) + await interaction.followup.send("Your private VC has been deleted!", ephemeral=True) + await self.cog.update_control_panel(interaction.guild) + + async def block(self, interaction: discord.Interaction): + vc = await self.get_owned_vc(interaction) + if not vc: + await interaction.response.send_message("You don't own any private VC!", ephemeral=True) + return + options = [] + for member in interaction.guild.members: + if (member not in vc.members and not member.bot and member != interaction.user): + options.append(SelectOption(label=member.name, value=str(member.id))) + if not options: + await interaction.response.send_message("No members available to block!", ephemeral=True) + return + dropdown = UserSelectDropdown(options, "Select members to block", self.block_selected) + view = ui.View() + view.add_item(dropdown) + await interaction.response.send_message("Select members to block:", view=view, ephemeral=True) + + async def block_selected(self, interaction: discord.Interaction, selected: List[str]): + await interaction.response.defer(ephemeral=True) + vc = await self.get_owned_vc(interaction) + if not vc: return + for user_id in selected: + await self.cog.block_user(vc.id, int(user_id)) + await interaction.followup.send("Selected members blocked from joining!", ephemeral=True) + await self.cog.update_control_panel(interaction.guild) + + +class UserSelectDropdown(ui.Select): + def __init__(self, options: List[SelectOption], placeholder: str, callback): + super().__init__(placeholder=placeholder, options=options, min_values=1, max_values=len(options)) + self.callback_func = callback + async def callback(self, interaction: discord.Interaction): + await self.callback_func(interaction, self.values) + +class RegionSelectDropdown(ui.Select): + def __init__(self, options: List[SelectOption], vc: discord.VoiceChannel): + super().__init__(placeholder="Select a region", options=options) + self.vc = vc + async def callback(self, interaction: discord.Interaction): + region = self.values[0] + try: + await self.vc.edit(rtc_region=region if region != "auto" else None) + await interaction.response.send_message(f"Region set to {self.values[0]}!", ephemeral=True) + except Exception as e: + await interaction.response.send_message(f"Failed to set region: {e}", ephemeral=True) + +class VCSelectDropdown(ui.Select): + def __init__(self, options: List[SelectOption], placeholder: str, callback): + super().__init__(placeholder=placeholder, options=options, min_values=1, max_values=1) + self.callback_func = callback + async def callback(self, interaction: discord.Interaction): + await self.callback_func(interaction, self.values) + +class SetLimitModal(ui.Modal, title="Set VC User Limit"): + def __init__(self, vc: discord.VoiceChannel): + super().__init__() + self.vc = vc + self.limit = ui.TextInput( + label="User Limit (0 for no limit)", + placeholder="Enter a number between 0 and 99", + default=str(vc.user_limit) if vc.user_limit else "0", + max_length=2 + ) + self.add_item(self.limit) + + async def on_submit(self, interaction: discord.Interaction): + try: + limit = int(self.limit.value) + if limit < 0 or limit > 99: + raise ValueError + await self.vc.edit(user_limit=limit if limit != 0 else None) + cog = interaction.client.get_cog("JoinToCreate") + if cog and self.vc.id in cog.private_channels: + cog.private_channels[self.vc.id]["limit"] = limit + await cog.save_private_channel(self.vc.id, interaction.guild.id, cog.private_channels[self.vc.id]) + await interaction.response.send_message(f"User limit set to {limit if limit != 0 else 'no limit'}!", ephemeral=True) + await cog.update_control_panel(interaction.guild) + except: + await interaction.response.send_message("Invalid limit! Please enter a number between 0 and 99.", ephemeral=True) + +async def setup(bot): + await bot.add_cog(JoinToCreate(bot)) \ No newline at end of file diff --git a/bot/cogs/commands/jail.py b/bot/cogs/commands/jail.py new file mode 100644 index 0000000..013d996 --- /dev/null +++ b/bot/cogs/commands/jail.py @@ -0,0 +1,215 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands, tasks +import sqlite3 +from datetime import datetime, timedelta +import re +from utils.cv2 import CV2 + +DB_FILE = "db/jail.db" + +class Jail(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.conn = sqlite3.connect(DB_FILE) + self.conn.execute(""" + CREATE TABLE IF NOT EXISTS jailed ( + guild_id TEXT, + user_id TEXT, + mod_id TEXT, + reason TEXT, + jailed_at TEXT, + duration INTEGER, + roles TEXT, + PRIMARY KEY (guild_id, user_id) + ); + """) + self.conn.execute(""" + CREATE TABLE IF NOT EXISTS jail_settings ( + guild_id TEXT PRIMARY KEY, + jail_role TEXT, + jail_channel TEXT, + mod_role TEXT, + log_channel TEXT + ); + """) + # Ensure roles column exists (safe migration) + try: + self.conn.execute("SELECT roles FROM jailed LIMIT 1;") + except sqlite3.OperationalError: + self.conn.execute("ALTER TABLE jailed ADD COLUMN roles TEXT;") + self.conn.commit() + + self.jail_check_loop.start() + + def cog_unload(self): + self.jail_check_loop.cancel() + self.conn.close() + + def get_setting(self, guild_id, field): + cursor = self.conn.execute(f"SELECT {field} FROM jail_settings WHERE guild_id = ?", (str(guild_id),)) + row = cursor.fetchone() + return row[0] if row else None + + def set_setting(self, guild_id, field, value): + self.conn.execute(f""" + INSERT INTO jail_settings (guild_id, {field}) + VALUES (?, ?) + ON CONFLICT(guild_id) DO UPDATE SET {field} = excluded.{field} + """, (str(guild_id), str(value))) + self.conn.commit() + + def parse_duration(self, duration_str: str): + pattern = re.compile(r'((?P\d+)h)?((?P\d+)m)?') + match = pattern.fullmatch(duration_str.lower()) + if not match: + return None + hours = int(match.group('hours') or 0) + minutes = int(match.group('minutes') or 0) + return (hours * 60 + minutes) * 60 if (hours or minutes) else None + + @tasks.loop(minutes=1) + async def jail_check_loop(self): + now = datetime.utcnow() + cursor = self.conn.execute("SELECT guild_id, user_id, duration, jailed_at, roles FROM jailed") + for guild_id, user_id, duration, jailed_at, roles in cursor.fetchall(): + if not duration: + continue + jailed_time = datetime.fromisoformat(jailed_at) + if (now - jailed_time).total_seconds() >= duration: + guild = self.bot.get_guild(int(guild_id)) + if guild: + member = guild.get_member(int(user_id)) + if member: + await self.unjail_member(guild, member) + + async def unjail_member(self, guild, member): + jail_role_id = self.get_setting(guild.id, "jail_role") + if jail_role_id: + jail_role = guild.get_role(int(jail_role_id)) + if jail_role in member.roles: + await member.remove_roles(jail_role, reason="Unjailed") + + # Restore old roles + cursor = self.conn.execute("SELECT roles FROM jailed WHERE guild_id = ? AND user_id = ?", (str(guild.id), str(member.id))) + row = cursor.fetchone() + if row and row[0]: + role_ids = map(int, row[0].split(",")) + roles = [guild.get_role(rid) for rid in role_ids if guild.get_role(rid)] + await member.add_roles(*roles, reason="Restored previous roles after jail") + + self.conn.execute("DELETE FROM jailed WHERE guild_id = ? AND user_id = ?", (str(guild.id), str(member.id))) + self.conn.commit() + + try: + await member.send(f"🔓 You have been unjailed in **{guild.name}**.") + except: + pass + + log_channel_id = self.get_setting(guild.id, "log_channel") + if log_channel_id: + log_channel = guild.get_channel(int(log_channel_id)) + if log_channel: + desc = ( + f"**User:** {member.mention}\n" + f"**Time:** " + ) + await log_channel.send(view=CV2("🔓 Member Unjailed", desc)) + + @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") + jail_channel_id = self.get_setting(ctx.guild.id, "jail_channel") + log_channel_id = self.get_setting(ctx.guild.id, "log_channel") + + if not jail_role_id or not jail_channel_id: + return await ctx.send(view=CV2("⚠️ Configuration Error", "Jail system not fully configured.")) + + jail_role = ctx.guild.get_role(int(jail_role_id)) + if not jail_role: + return await ctx.send(view=CV2("⚠️ Configuration Error", "Jail role does not exist.")) + + duration_secs = self.parse_duration(duration) if duration else None + jailed_at = datetime.utcnow().isoformat() + roles_str = ",".join(str(r.id) for r in member.roles if r != ctx.guild.default_role) + + self.conn.execute("DELETE FROM jailed WHERE guild_id = ? AND user_id = ?", (str(ctx.guild.id), str(member.id))) + self.conn.execute(""" + INSERT INTO jailed (guild_id, user_id, mod_id, reason, jailed_at, duration, roles) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, (str(ctx.guild.id), str(member.id), str(ctx.author.id), reason, jailed_at, duration_secs, roles_str)) + self.conn.commit() + + try: + await member.edit(roles=[jail_role], reason="Jailed") + except discord.Forbidden: + return await ctx.send(view=CV2("❌ Permission Error", "I don't have permission to change roles.")) + + jail_channel = ctx.guild.get_channel(int(jail_channel_id)) + if jail_channel: + await jail_channel.set_permissions(member, view_channel=True, send_messages=True, read_message_history=True) + + try: + await member.send(f"🔒 You were jailed in **{ctx.guild.name}**.\n📝 Reason: {reason}\n⏰ Duration: {duration or 'Permanent'}") + except: + pass + + await ctx.send(view=CV2("✅ Success", f"🔒 {member.mention} has been jailed {'for ' + duration if duration else 'permanently'}.")) + + if log_channel_id: + log_channel = ctx.guild.get_channel(int(log_channel_id)) + if log_channel: + desc = ( + f"**User:** {member.mention}\n" + f"**Moderator:** {ctx.author.mention}\n" + f"**Reason:** {reason}\n" + f"**Duration:** {duration or 'Permanent'}\n" + f"**Time:** " + ) + await log_channel.send(view=CV2("🔒 Member Jailed", desc)) + + @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.command(name="jailhistory") + async def jailhistory(self, ctx, member: discord.Member): + cursor = self.conn.execute(""" + SELECT reason, jailed_at, duration, mod_id FROM jailed + WHERE guild_id = ? AND user_id = ? + """, (str(ctx.guild.id), str(member.id))) + row = cursor.fetchone() + if row: + reason, jailed_at, duration, mod_id = row + mod = ctx.guild.get_member(int(mod_id)) + jailed_time = datetime.fromisoformat(jailed_at) + + desc = ( + f"**User:** {member.mention}\n" + f"**Reason:** {reason}\n" + f"**Jailed At:** \n" + f"**Duration:** {f'{duration // 60} minutes' if duration else 'Permanent'}\n" + f"**Jailed By:** {mod.mention if mod else 'Unknown'}" + ) + await ctx.send(view=CV2("📄 Jail Record", desc)) + else: + await ctx.send(view=CV2("❌ Error", f"No jail record found for {member.mention}")) + +async def setup(bot): + await bot.add_cog(Jail(bot)) diff --git a/bot/cogs/commands/joindm.py b/bot/cogs/commands/joindm.py new file mode 100644 index 0000000..9961e3f --- /dev/null +++ b/bot/cogs/commands/joindm.py @@ -0,0 +1,100 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import BLOBPART +from discord.ext import commands +import json +from utils.cv2 import CV2 + +class joindm(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.joindm_messages = {} + self.load_joindm_messages() + + def load_joindm_messages(self): + # Load the join DM messages from file + try: + with open('jsondb/joindm_messages.json', 'r') as f: + self.joindm_messages = json.load(f) + except FileNotFoundError: + self.joindm_messages = {} + + def save_joindm_messages(self): + # Save the join DM messages to file + with open('jsondb/joindm_messages.json', 'w') as f: + json.dump(self.joindm_messages, f) + + @commands.group(invoke_without_command=True) + @commands.has_permissions(administrator=True) + async def joindm(self, ctx): + # Display the current join DM message + guild_id = str(ctx.guild.id) + if guild_id in self.joindm_messages: + await ctx.send(view=CV2("✅ Join DM Status", f"The current join DM message is:\n`{self.joindm_messages[guild_id]}`")) + else: + await ctx.send(view=CV2("❌ Error", "No custom join DM message has been set for this server.")) + + @joindm.command() + @commands.has_permissions(administrator=True) + async def message(self, ctx, *, message=None): + # Set the custom join DM message + if message is None: + await ctx.send(view=CV2("❌ Error", "Please provide a custom join DM message.")) + else: + self.joindm_messages[str(ctx.guild.id)] = message + self.save_joindm_messages() + await ctx.send(view=CV2("✅ Success", "Custom join DM message set successfully.")) + + @joindm.command() + @commands.has_permissions(administrator=True) + async def enable(self, ctx): + # Enable the join DM module + self.bot.add_listener(self.on_member_join, 'on_member_join') + await ctx.send(view=CV2("✅ Success", "Join DM module enabled. Custom DM will be sent to new members.")) + + @joindm.command() + @commands.has_permissions(administrator=True) + async def disable(self, ctx): + # Disable the join DM module + self.bot.remove_listener(self.on_member_join) + await ctx.send(view=CV2("✅ Success", "Join DM module disabled. Custom DM will not be sent to new members.")) + + @joindm.command() + async def test(self, ctx): + # Send a test join DM to the author of the command + guild_id = str(ctx.guild.id) + if guild_id in self.joindm_messages: + message = self.joindm_messages[guild_id] + server_name = ctx.guild.name + join_dm_message = f"{message}\n\n ``Sent from {server_name} `` " + await ctx.send(view=CV2("✅ Test Sent", "Test Join DM Sent To Your Dm")) + await ctx.message.add_reaction(BLOBPART) + await ctx.author.send(view=CV2("👋 Welcome", join_dm_message)) + else: + await ctx.send(view=CV2("❌ Error", "No custom join DM message has been set for this server.")) + + async def on_member_join(self, member): + + guild_id = str(member.guild.id) + if guild_id in self.joindm_messages: + message = self.joindm_messages[guild_id] + dm_channel = await member.create_dm() + server_name = member.guild.name + join_dm_message = f"{message}\n\n``Sent from {server_name} ``" + await dm_channel.send(view=CV2("👋 Welcome", join_dm_message)) + +async def setup(bot): + await bot.add_cog(joindm(bot)) diff --git a/bot/cogs/commands/leveling.py b/bot/cogs/commands/leveling.py new file mode 100644 index 0000000..7c662af --- /dev/null +++ b/bot/cogs/commands/leveling.py @@ -0,0 +1,3165 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CROSS, TICK, ZWARNING +from discord import app_commands +from discord .ext import commands +import aiosqlite +import asyncio +import json +import re +import random +import math +from datetime import datetime ,timezone ,timedelta +from typing import Optional ,Dict ,List ,Tuple +try : + from PIL import Image ,ImageDraw ,ImageFont ,ImageFilter ,ImageEnhance + PIL_AVAILABLE =True +except ImportError : + PIL_AVAILABLE =False +import io +import os +import requests +import logging + + +logger =logging .getLogger ('discord') + + +def utc_to_local (dt :datetime )->datetime : + return dt .replace (tzinfo =timezone .utc ) + +def format_number (num :int )->str : + """Format numbers with commas for better readability""" + return f"{num:,}" + +def calculate_level_from_xp (xp :int )->int : + """Calculate level from XP using standard formula""" + if xp <0 : + return 0 + return int (math .sqrt (xp /100 )) + +def calculate_xp_for_level (level :int )->int : + """Calculate XP required for a specific level""" + return level *level *100 + +def get_level_progress (xp :int )->tuple : + """Get current level, XP for current level, and XP for next level""" + current_level =calculate_level_from_xp (xp ) + current_level_xp =calculate_xp_for_level (current_level ) + next_level_xp =calculate_xp_for_level (current_level +1 ) + progress =xp -current_level_xp + needed =next_level_xp -current_level_xp + return current_level ,progress ,needed + +def get_progress_bar (current :int ,total :int ,length :int =10 )->str : + """Create a visual progress bar""" + if total ==0 : + return "▱"*length + filled =int ((current /total )*length ) + return "▰"*filled +"▱"*(length -filled ) + +def validate_hex_color (color :str )->bool : + """Validate hex color format""" + if not color .startswith ('#'): + return False + return bool (re .match (r'^#(?:[0-9a-fA-F]{3}){1,2}$',color )) + +def hex_to_int (hex_color :str )->int : + """Convert hex color to integer""" + try : + if not hex_color .startswith ('#'): + hex_color ='#'+hex_color + return int (hex_color .lstrip ('#'),16 ) + except (ValueError ,TypeError ): + return 0xFF0000 + +class PlaceholdersView (discord .ui .View ): + def __init__ (self ): + super ().__init__ (timeout =60 ) + + @discord .ui .button (label ="Show Placeholders",style =discord .ButtonStyle .secondary ,emoji ="📝") + async def show_placeholders (self ,interaction :discord .Interaction ,button :discord .ui .Button ): + embed =discord .Embed ( + title ="📝 Available Placeholders", + description ="**You can use these placeholders in your level up message:**\n\n" + "`{user}` - Mentions the user (@username)\n" + "`{username}` - User's display name\n" + "`{level}` - The new level reached\n" + "`{server}` - Server name\n\n" + "**Example:**\n" + "`Congratulations {user}! You've reached level {level} in {server}!`", + color =0xFF0000 , + timestamp =datetime .now (timezone .utc ) + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + +class LevelConfigModal (discord .ui .Modal ,title ="Leveling System Configuration"): + def __init__ (self ,cog ,current_settings ): + super ().__init__ () + + self .cog =cog + + + self .xp_per_message =discord .ui .TextInput ( + label ="XP per Message", + placeholder ="Amount of XP per message (default: 20)", + default =str (current_settings .get ('xp_per_message',20 )), + required =True , + max_length =3 + ) + + self .level_up_message =discord .ui .TextInput ( + label ="Level Up Message", + placeholder ="Use {user}, {level}, {username}, {server} as placeholders", + default =current_settings .get ('level_message','Congratulations {user}! You have reached level {level}!'), + required =True , + style =discord .TextStyle .paragraph , + max_length =2000 + ) + + + current_color =current_settings .get ('embed_color','#000000') + if isinstance (current_color ,int ): + current_color =f"#{current_color:06x}" + elif not isinstance (current_color ,str ): + current_color ='#000000' + + + if not current_color .startswith ('#'): + current_color ='#000000' + + self .embed_color =discord .ui .TextInput ( + label ="Embed Color (Hex)", + placeholder ="#FF0000", + default =current_color , + required =True , + max_length =7 + ) + + self .level_up_image =discord .ui .TextInput ( + label ="Level Up Image URL (Optional)", + placeholder ="Direct image URL for level up embeds", + default =current_settings .get ('level_image',''), + required =False , + max_length =500 + ) + + self .thumbnail_enabled =discord .ui .TextInput ( + label ="Show User Avatar Thumbnail (true/false)", + placeholder ="true or false", + default =str (current_settings .get ('thumbnail_enabled',True )).lower (), + required =True , + max_length =5 + ) + + self .add_item (self .xp_per_message ) + self .add_item (self .level_up_message ) + self .add_item (self .embed_color ) + self .add_item (self .level_up_image ) + self .add_item (self .thumbnail_enabled ) + + async def on_submit (self ,interaction :discord .Interaction ): + try : + if not interaction or not hasattr (interaction ,'response'): + logger .error ("Invalid interaction object in LevelConfigModal") + return + + await interaction .response .defer (ephemeral =True ) + + + try : + xp_value =int (self .xp_per_message .value ) + if xp_value <1 or xp_value >999 : + raise ValueError ("XP per message must be between 1 and 999") + except ValueError as ve : + logger .error (f"XP validation error: {ve}") + await interaction .followup .send (f"{CROSS} Invalid XP per message value! Must be between 1 and 999.",ephemeral =True ) + return + + + color_value =self .embed_color .value .strip () + if not color_value .startswith ('#'): + color_value ='#'+color_value + + if not validate_hex_color (color_value ): + await interaction .followup .send (f"{CROSS} Invalid hex color format! Use format like #FF0000",ephemeral =True ) + return + + thumbnail_bool =self .thumbnail_enabled .value .lower ()in ['true','yes','1','on'] + + + try : + embed_color_int =hex_to_int (color_value ) + except Exception as e : + logger .error (f"Color conversion error: {e}") + embed_color_int =0 + + + image_value =self .level_up_image .value .strip ()if self .level_up_image .value and self .level_up_image .value .strip ()else None + + + try : + async with aiosqlite .connect ("db/leveling.db")as db : + + async with db .execute ("SELECT guild_id FROM leveling_settings WHERE guild_id = ?",(interaction .guild .id ,))as cursor : + exists =await cursor .fetchone () + + if exists : + + await db .execute (""" + UPDATE leveling_settings + SET enabled = ?, xp_per_message = ?, level_message = ?, embed_color = ?, + level_image = ?, thumbnail_enabled = ? + WHERE guild_id = ? + """,( + 1 , + xp_value , + self .level_up_message .value , + embed_color_int , + image_value , + 1 if thumbnail_bool else 0 , + interaction .guild .id + )) + logger .info (f"Updated settings for guild {interaction.guild.id}") + else : + + await db .execute (""" + INSERT INTO leveling_settings + (guild_id, enabled, xp_per_message, level_message, embed_color, level_image, thumbnail_enabled, + min_xp, max_xp, cooldown_seconds, dm_level_up, channel_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """,( + interaction .guild .id , + 1 , + xp_value , + self .level_up_message .value , + embed_color_int , + image_value , + 1 if thumbnail_bool else 0 , + 15 , + 25 , + 60 , + 0 , + None + )) + logger .info (f"Created new settings for guild {interaction.guild.id}") + + await db .commit () + logger .info (f"Settings saved successfully for guild {interaction.guild.id}") + + except Exception as e : + return + + + embed =discord .Embed ( + title =f"{TICK} Leveling Configuration Updated", + description =f"**XP per Message:** {xp_value}\n" + f"**Level Up Message:** {self.level_up_message.value[:50]}{'...' if len(self.level_up_message.value) > 50 else ''}\n" + f"**Embed Color:** {color_value}\n" + f"**Level Up Image:** {'Set' if image_value else 'None'}\n" + f"**Thumbnail Enabled:** {thumbnail_bool}", + color =0xFF0000 , + timestamp =datetime .now (timezone .utc ) + ) + + + view =PlaceholdersView () + await interaction .followup .send (embed =embed ,view =view ,ephemeral =True ) + + except Exception as e : + logger .error (f"Critical error in LevelConfigModal: {e}") + await interaction .followup .send ( + f"{CROSS} An unexpected error occurred: {str(e)}", + ephemeral =True + ) + +class Leveling (commands .Cog ): + def __init__ (self ,bot ): + self .bot =bot + self .message_cooldowns ={} + self .last_level_cache ={} + self .db_path ="db/leveling.db" + + @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 : + await ctx .send_help (ctx .command ) + + async def cog_load (self ): + """Initialize database tables""" + try : + await self .init_database () + + except Exception as e : + logger .error (f"Error loading Leveling cog: {e}") + + async def init_database (self ): + """Initialize all required database tables""" + try : + async with aiosqlite .connect ("db/leveling.db")as db : + + await db .execute (""" + CREATE TABLE IF NOT EXISTS user_xp ( + guild_id INTEGER, + user_id INTEGER, + xp INTEGER DEFAULT 0, + messages INTEGER DEFAULT 0, + last_message_time TEXT, + PRIMARY KEY (guild_id, user_id) + ) + """) + + + await db .execute (""" + CREATE TABLE IF NOT EXISTS leveling_settings ( + guild_id INTEGER PRIMARY KEY, + enabled INTEGER DEFAULT 0, + channel_id INTEGER, + level_message TEXT DEFAULT 'Congratulations {user}! You have reached level {level}!', + embed_color INTEGER DEFAULT 0, + level_image TEXT, + thumbnail_enabled INTEGER DEFAULT 1, + xp_per_message INTEGER DEFAULT 20, + min_xp INTEGER DEFAULT 15, + max_xp INTEGER DEFAULT 25, + cooldown_seconds INTEGER DEFAULT 60, + dm_level_up INTEGER DEFAULT 0 + ) + """) + + + await db .execute (""" + CREATE TABLE IF NOT EXISTS level_rewards ( + guild_id INTEGER, + level INTEGER, + role_id INTEGER, + remove_previous INTEGER DEFAULT 0, + PRIMARY KEY (guild_id, level) + ) + """) + + + await db .execute (""" + CREATE TABLE IF NOT EXISTS xp_multipliers ( + guild_id INTEGER, + target_id INTEGER, + target_type TEXT, + multiplier REAL, + PRIMARY KEY (guild_id, target_id, target_type) + ) + """) + + + await db .execute (""" + CREATE TABLE IF NOT EXISTS leveling_blacklist ( + guild_id INTEGER, + target_id INTEGER, + target_type TEXT, + PRIMARY KEY (guild_id, target_id, target_type) + ) + """) + + + await db .execute (""" + CREATE TABLE IF NOT EXISTS users ( + guild_id INTEGER, + user_id INTEGER, + xp INTEGER DEFAULT 0, + level INTEGER DEFAULT 1, + PRIMARY KEY (guild_id, user_id) + ) + """) + + await db .execute (""" + CREATE TABLE IF NOT EXISTS level_roles ( + guild_id INTEGER, + level INTEGER, + role_id INTEGER, + PRIMARY KEY (guild_id, level) + ) + """) + + + async with db .cursor ()as cursor : + + await cursor .execute ("PRAGMA table_info(leveling_blacklist)") + columns =[col [1 ]for col in await cursor .fetchall ()] + if "target_type"not in columns : + await cursor .execute ("ALTER TABLE leveling_blacklist ADD COLUMN target_type TEXT") + logger .info ("Added target_type column to leveling_blacklist table") + + await db .commit () + except Exception as e : + logger .error (f"Database initialization error: {e}") + + async def get_guild_settings (self ,guild_id :int )->dict : + """Get guild leveling settings""" + max_retries =5 + for attempt in range (max_retries ): + try : + async with aiosqlite .connect ("db/leveling.db")as db : + async with db .execute ("SELECT * FROM leveling_settings WHERE guild_id = ?",(guild_id ,))as cursor : + row =await cursor .fetchone () + + if not row : + + async with aiosqlite .connect ("db/leveling.db")as db : + await db .execute ("INSERT INTO leveling_settings (guild_id, enabled) VALUES (?, 0)",(guild_id ,)) + await db .commit () + return { + 'enabled':False ,'channel_id':None , + 'level_message':'Congratulations {user}! You have reached level {level}!', + 'embed_color':'#000000','level_image':None ,'thumbnail_enabled':True , + 'xp_per_message':20 ,'min_xp':15 ,'max_xp':25 ,'cooldown_seconds':60 , + 'dm_level_up':False + } + + + embed_color =row [4 ]if len (row )>4 and row [4 ]is not None else 0 + if isinstance (embed_color ,int ): + color_hex =f"#{embed_color:06x}" + else : + color_hex ='#FF0000' + + + return { + 'enabled':bool (row [1 ])if len (row )>1 else False , + 'channel_id':row [2 ]if len (row )>2 else None , + 'level_message':row [3 ]if len (row )>3 else 'Congratulations {user}! You have reached level {level}!', + 'embed_color':color_hex , + 'level_image':row [5 ]if len (row )>5 else None , + 'thumbnail_enabled':bool (row [6 ])if len (row )>6 else True , + 'xp_per_message':row [7 ]if len (row )>7 else 20 , + 'min_xp':row [8 ]if len (row )>8 else 15 , + 'max_xp':row [9 ]if len (row )>9 else 25 , + 'cooldown_seconds':row [10 ]if len (row )>10 else 60 , + 'dm_level_up':bool (row [11 ])if len (row )>11 else False + } + + except aiosqlite .OperationalError as e : + if attempt bool : + """Check if user or channel is blacklisted""" + try : + async with aiosqlite .connect ("db/leveling.db")as db : + + async with db .cursor ()as cursor : + await cursor .execute ("PRAGMA table_info(leveling_blacklist)") + columns =[col [1 ]for col in await cursor .fetchall ()] + + if "target_type"not in columns : + + logger .warning ("target_type column missing from leveling_blacklist table") + return False + + + async with db .execute ( + "SELECT 1 FROM leveling_blacklist WHERE guild_id = ? AND target_id = ? AND target_type = 'channel'", + (guild_id ,channel_id ) + )as cursor : + if await cursor .fetchone (): + return True + + + guild =self .bot .get_guild (guild_id ) + if guild : + member =guild .get_member (user_id ) + if member : + async with db .execute ( + "SELECT target_id FROM leveling_blacklist WHERE guild_id = ? AND target_type = 'role'", + (guild_id ,) + )as cursor : + blacklisted_roles =[row [0 ]for row in await cursor .fetchall ()] + if any (role .id in blacklisted_roles for role in member .roles ): + return True + return False + except Exception as e : + logger .error (f"Error checking blacklist: {e}") + return False + + async def get_xp_multiplier (self ,guild_id :int ,user_id :int ,channel_id :int )->float : + """Get XP multiplier for user""" + try : + guild =self .bot .get_guild (guild_id ) + if not guild : + return 1.0 + + member =guild .get_member (user_id ) + if not member : + return 1.0 + + total_multiplier =1.0 + async with aiosqlite .connect ("db/leveling.db")as db : + + async with db .execute ( + "SELECT target_id, multiplier FROM xp_multipliers WHERE guild_id = ? AND target_type = 'role'", + (guild_id ,) + )as cursor : + role_multipliers =await cursor .fetchall () + + for role_id ,multiplier in role_multipliers : + if any (role .id ==role_id for role in member .roles ): + total_multiplier *=multiplier + + + async with db .execute ( + "SELECT multiplier FROM xp_multipliers WHERE guild_id = ? AND target_id = ? AND target_type = 'channel'", + (guild_id ,channel_id ) + )as cursor : + channel_mult =await cursor .fetchone () + if channel_mult : + total_multiplier *=channel_mult [0 ] + + return total_multiplier + except Exception as e : + logger .error (f"Error getting XP multiplier: {e}") + return 1.0 + + @commands .Cog .listener () + async def on_message (self ,message ): + """Handle message XP tracking with independent message tracking system""" + if message .author .bot or not message .guild : + return + + guild_id =message .guild .id + user_id =message .author .id + channel_id =message .channel .id + + + cooldown_key =f"{guild_id}_{user_id}" + now =datetime .now () + + if cooldown_key in self .message_cooldowns : + if now =2 : + current_xp =row [0 ]if row [0 ]is not None else 0 + current_messages =row [1 ]if row [1 ]is not None else 0 + old_level =calculate_level_from_xp (current_xp ) + new_xp =current_xp +final_xp + new_level =calculate_level_from_xp (new_xp ) + new_messages =current_messages +1 + + await db .execute (""" + UPDATE user_xp + SET xp = ?, messages = ?, last_message_time = ? + WHERE guild_id = ? AND user_id = ? + """,(new_xp ,new_messages ,now .isoformat (),guild_id ,user_id )) + else : + old_level =0 + new_level =calculate_level_from_xp (final_xp ) + new_messages =1 + + await db .execute (""" + INSERT INTO user_xp (guild_id, user_id, xp, messages, last_message_time) + VALUES (?, ?, ?, ?, ?) + """,(guild_id ,user_id ,final_xp ,new_messages ,now .isoformat ())) + + + current_new_xp =new_xp if 'new_xp'in locals ()else final_xp + current_new_level =new_level if 'new_level'in locals ()else calculate_level_from_xp (final_xp ) + await db .execute (""" + INSERT OR REPLACE INTO users (guild_id, user_id, xp, level) + VALUES (?, ?, ?, ?) + """,(guild_id ,user_id ,current_new_xp ,current_new_level )) + + await db .commit () + + + if new_level >old_level : + await self .handle_level_up (message ,new_level ,settings ) + + except Exception as e : + logger .error (f"Error handling message XP: {e}") + + async def handle_level_up (self ,message ,new_level ,settings ): + """Handle level up notification and rewards""" + try : + + channel_id =settings .get ('channel_id') + channel =None + + if channel_id : + channel =self .bot .get_channel (channel_id ) + + if channel and not channel .permissions_for (message .guild .me ).send_messages : + logger .warning (f"No permission to send messages in configured level channel {channel_id}") + channel =None + + + if not channel : + if not channel_id : + + channel =message .channel + else : + + logger .error (f"Configured level channel {channel_id} not found or not accessible") + return + + + level_message =settings .get ('level_message','Congratulations {user}! You have reached level {level}!') + level_message =level_message .replace ('{user}',message .author .mention ) + level_message =level_message .replace ('{level}',str (new_level )) + level_message =level_message .replace ('{username}',message .author .display_name ) + level_message =level_message .replace ('{server}',message .guild .name ) + + + embed_color =settings .get ('embed_color','#FF0000') + if isinstance (embed_color ,str ): + embed_color =hex_to_int (embed_color ) + + embed =discord .Embed ( + title ="🎉 Level Up!", + description =level_message , + color =0xFF0000 , + timestamp =datetime .now (timezone .utc ) + ) + + if settings .get ('thumbnail_enabled',True ): + embed .set_thumbnail (url =message .author .display_avatar .url ) + + if settings .get ('level_image'): + embed .set_image (url =settings ['level_image']) + + embed .set_footer (text =f"Level {new_level} • {message.guild.name}") + + try : + await channel .send (embed =embed ) + logger .info (f"Level up message sent for {message.author} (Level {new_level}) in channel {channel.id}") + except discord .Forbidden : + logger .error (f"No permission to send level up message in channel {channel.id}") + except discord .NotFound : + logger .error (f"Level up channel {channel.id} not found") + except Exception as e : + logger .error (f"Error sending level up message: {e}") + + + await self .give_level_rewards (message .guild ,message .author ,new_level ) + + + await self .apply_level_roles (message .guild ,message .author ,new_level ) + + except Exception as e : + logger .error (f"Error handling level up: {e}") + + async def give_level_rewards (self ,guild ,member ,level ): + """Give role rewards for reaching a level""" + try : + async with aiosqlite .connect ("db/leveling.db")as db : + async with db .execute ( + "SELECT role_id, remove_previous FROM level_rewards WHERE guild_id = ? AND level <= ? ORDER BY level DESC", + (guild .id ,level ) + )as cursor : + rewards =await cursor .fetchall () + + for role_id ,remove_previous in rewards : + role =guild .get_role (role_id ) + if role and role not in member .roles : + await member .add_roles (role ,reason =f"Level {level} reward") + + if remove_previous : + + async with aiosqlite .connect ("db/leveling.db")as db : + async with db .execute ( + "SELECT role_id FROM level_rewards WHERE guild_id = ? AND level < ?", + (guild .id ,level ) + )as cursor : + prev_rewards =await cursor .fetchall () + + for prev_role_id ,in prev_rewards : + prev_role =guild .get_role (prev_role_id ) + if prev_role and prev_role in member .roles : + await member .remove_roles (prev_role ,reason =f"Upgraded to level {level}") + + except Exception as e : + logger .error (f"Error giving level rewards: {e}") + + async def apply_level_roles (self ,guild ,member ,level ): + """Apply level roles for reaching a level""" + try : + async with aiosqlite .connect ("db/leveling.db")as db : + async with db .execute ( + "SELECT role_id FROM level_roles WHERE guild_id = ? AND level <= ? ORDER BY level DESC", + (guild .id ,level ) + )as cursor : + level_roles =await cursor .fetchall () + + for role_id ,in level_roles : + role =guild .get_role (role_id ) + if role and role not in member .roles : + try : + await member .add_roles (role ,reason =f"Reached level {level}") + except discord .Forbidden : + logger .warning (f"No permission to add role {role.name} to {member}") + except Exception as e : + logger .error (f"Error adding level role {role_id}: {e}") + + except Exception as e : + logger .error (f"Error applying level roles: {e}") + + async def get_user_data (self ,guild_id :int ,user_id :int )->tuple : + """Get user XP and level data""" + try : + async with aiosqlite .connect ("db/leveling.db")as db : + async with db .execute ( + "SELECT xp, messages FROM user_xp WHERE guild_id = ? AND user_id = ?", + (guild_id ,user_id ) + )as cursor : + row =await cursor .fetchone () + if row : + xp ,messages =row + level =calculate_level_from_xp (xp if xp is not None else 0 ) + return xp if xp is not None else 0 ,level ,messages if messages is not None else 0 + return 0 ,0 ,0 + except Exception as e : + logger .error (f"Error getting user data: {e}") + return 0 ,0 ,0 + + async def get_user_rank (self ,guild_id :int ,user_id :int )->int : + """Get user's rank in the guild""" + try : + async with aiosqlite .connect ("db/leveling.db")as db : + async with db .execute ( + "SELECT COUNT(*) + 1 FROM user_xp WHERE guild_id = ? AND xp > (SELECT COALESCE(xp, 0) FROM user_xp WHERE guild_id = ? AND user_id = ?)", + (guild_id ,guild_id ,user_id ) + )as cursor : + rank =(await cursor .fetchone ())[0 ] + return rank + except Exception as e : + logger .error (f"Error getting user rank: {e}") + return 1 + + def create_simple_rank_card (self ,member ,xp ,level ,rank ,messages ): + """Create a simple text-based rank card when PIL fails""" + try : + current_level ,progress ,needed =get_level_progress (xp ) + progress_bar =get_progress_bar (progress ,needed ,20 ) + + rank_text =f""" +┌─────────────────────────────────────────┐ +│ 🎮 RANK CARD - {member.display_name[:20]} +├─────────────────────────────────────────┤ +│ 📊 Rank: #{rank:,} +│ ⭐ Level: {level:,} +│ 💬 Messages: {messages:,} +│ 🎯 XP: {xp:,} +├─────────────────────────────────────────┤ +│ Progress to Level {level + 1}: +│ {progress_bar} +│ {progress:,} / {needed:,} XP +└─────────────────────────────────────────┘ + """ + return rank_text .strip () + except Exception as e : + logger .error (f"Error creating simple rank card: {e}") + return f"**{member.display_name}** - Level {level} - Rank #{rank}" + + async def create_rank_card (self ,member ,guild_id ): + """Create rank card with random design selection - fallback to text if PIL fails""" + try : + xp ,level ,messages =await self .get_user_data (guild_id ,member .id ) + rank =await self .get_user_rank (guild_id ,member .id ) + + + if PIL_AVAILABLE : + try : + + design_number =random .randint (1 ,7 ) + return await self .create_rank_card_design (member ,xp ,level ,rank ,messages ,design_number ) + + except Exception as e : + logger .error (f"PIL error, falling back to text: {e}") + + return self .create_simple_rank_card (member ,xp ,level ,rank ,messages ) + else : + + logger .warning ("PIL not available, using text-based rank card") + return self .create_simple_rank_card (member ,xp ,level ,rank ,messages ) + + except Exception as e : + logger .error (f"Error creating rank card: {e}") + return None + + async def create_rank_card_design (self ,member ,xp ,level ,rank ,messages ,design_number ): + """Create specific rank card design based on design number""" + current_level ,progress ,needed =get_level_progress (xp ) + width ,height =900 ,300 + + + try : + font_large =ImageFont .truetype ("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",28 ) + font_medium =ImageFont .truetype ("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",20 ) + font_small =ImageFont .truetype ("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",16 ) + except (OSError ,IOError ): + font_large =ImageFont .load_default () + font_medium =ImageFont .load_default () + font_small =ImageFont .load_default () + + if design_number ==1 : + return await self .create_classic_design (member ,xp ,level ,rank ,messages ,current_level ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ) + elif design_number ==2 : + return await self .create_neon_design (member ,xp ,level ,rank ,messages ,current_level ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ) + elif design_number ==3 : + return await self .create_space_design (member ,xp ,level ,rank ,messages ,current_level ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ) + elif design_number ==4 : + return await self .create_minimal_design (member ,xp ,level ,rank ,messages ,current_level ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ) + elif design_number ==5 : + return await self .create_gaming_design (member ,xp ,level ,rank ,messages ,current_level ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ) + elif design_number ==6 : + return await self .create_elegant_design (member ,xp ,level ,rank ,messages ,current_level ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ) + else : + return await self .create_cyberpunk_design (member ,xp ,level ,rank ,messages ,current_level ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ) + + async def create_classic_design (self ,member ,xp ,level ,rank ,messages ,current_level ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ): + """Original design with diagonal gradient""" + img =Image .new ('RGB',(width ,height ),(32 ,34 ,37 )) + draw =ImageDraw .Draw (img ) + + + color_schemes =[ + [(45 ,52 ,54 ),(99 ,110 ,114 )], + [(74 ,144 ,226 ),(80 ,227 ,194 )], + [(245 ,166 ,35 ),(242 ,112 ,156 )], + [(165 ,94 ,234 ),(74 ,144 ,226 )], + [(255 ,118 ,117 ),(255 ,204 ,128 )], + [(18 ,194 ,233 ),(196 ,113 ,237 )], + ] + + scheme =color_schemes [level %len (color_schemes )] + + + for x in range (width ): + for y in range (height ): + factor_x =x /width + factor_y =y /height + factor =(factor_x +factor_y )/2 + + r =int (scheme [0 ][0 ]+(scheme [1 ][0 ]-scheme [0 ][0 ])*factor ) + g =int (scheme [0 ][1 ]+(scheme [1 ][1 ]-scheme [0 ][1 ])*factor ) + b =int (scheme [0 ][2 ]+(scheme [1 ][2 ]-scheme [0 ][2 ])*factor ) + draw .point ((x ,y ),fill =(r ,g ,b )) + + + draw .rounded_rectangle ((15 ,15 ,width -15 ,height -15 ),radius =20 ,fill =(0 ,0 ,0 ,100 )) + draw .rounded_rectangle ((15 ,15 ,width -15 ,height -15 ),radius =20 ,outline =(255 ,255 ,255 ,50 ),width =2 ) + + + await self .add_avatar_and_content_classic (draw ,member ,level ,rank ,messages ,xp ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ) + + + img_bytes =io .BytesIO () + img .save (img_bytes ,format ='PNG') + img_bytes .seek (0 ) + img .close () + return img_bytes + + async def create_neon_design (self ,member ,xp ,level ,rank ,messages ,current_level ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ): + """Neon cyberpunk style design""" + img =Image .new ('RGB',(width ,height ),(10 ,10 ,25 )) + draw =ImageDraw .Draw (img ) + + + grid_color =(0 ,255 ,255 ,30 ) + for x in range (0 ,width ,50 ): + draw .line ([(x ,0 ),(x ,height )],fill =grid_color ,width =1 ) + for y in range (0 ,height ,50 ): + draw .line ([(0 ,y ),(width ,y )],fill =grid_color ,width =1 ) + + + for i in range (5 ): + draw .rounded_rectangle ((15 -i ,15 -i ,width -15 +i ,height -15 +i ),radius =20 +i ,outline =(0 ,255 ,255 ,50 -i *10 ),width =1 ) + + + draw .rounded_rectangle ((20 ,20 ,width -20 ,height -20 ),radius =15 ,fill =(5 ,5 ,20 )) + + + await self .add_avatar_and_content_neon (draw ,member ,level ,rank ,messages ,xp ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ) + + img_bytes =io .BytesIO () + img .save (img_bytes ,format ='PNG') + img_bytes .seek (0 ) + img .close () + return img_bytes + + async def create_space_design (self ,member ,xp ,level ,rank ,messages ,current_level ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ): + """Space theme with stars and nebula""" + img =Image .new ('RGB',(width ,height ),(5 ,5 ,20 )) + draw =ImageDraw .Draw (img ) + + + for i in range (100 ): + x =random .randint (0 ,width ) + y =random .randint (0 ,height ) + size =random .randint (1 ,3 ) + brightness =random .randint (150 ,255 ) + draw .ellipse ((x ,y ,x +size ,y +size ),fill =(brightness ,brightness ,brightness )) + + + for x in range (0 ,width ,20 ): + for y in range (0 ,height ,20 ): + if random .random ()<0.3 : + color_r =random .randint (50 ,150 ) + color_g =random .randint (0 ,100 ) + color_b =random .randint (100 ,200 ) + draw .ellipse ((x ,y ,x +30 ,y +30 ),fill =(color_r ,color_g ,color_b ,30 )) + + + draw .rounded_rectangle ((20 ,20 ,width -20 ,height -20 ),radius =15 ,fill =(0 ,0 ,0 ,150 )) + + await self .add_avatar_and_content_space (draw ,member ,level ,rank ,messages ,xp ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ) + + img_bytes =io .BytesIO () + img .save (img_bytes ,format ='PNG') + img_bytes .seek (0 ) + img .close () + return img_bytes + + async def create_minimal_design (self ,member ,xp ,level ,rank ,messages ,current_level ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ): + """Clean minimal design""" + img =Image .new ('RGB',(width ,height ),(248 ,249 ,250 )) + draw =ImageDraw .Draw (img ) + + + draw .rounded_rectangle ((10 ,10 ,width -10 ,height -10 ),radius =15 ,outline =(220 ,220 ,220 ),width =2 ,fill =(255 ,255 ,255 )) + + + draw .rounded_rectangle ((12 ,12 ,width -8 ,height -8 ),radius =15 ,outline =(240 ,240 ,240 ),width =1 ) + + await self .add_avatar_and_content_minimal (draw ,member ,level ,rank ,messages ,xp ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ) + + img_bytes =io .BytesIO () + img .save (img_bytes ,format ='PNG') + img_bytes .seek (0 ) + img .close () + return img_bytes + + async def create_gaming_design (self ,member ,xp ,level ,rank ,messages ,current_level ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ): + """Gaming RGB style design""" + img =Image .new ('RGB',(width ,height ),(20 ,20 ,20 )) + draw =ImageDraw .Draw (img ) + + + colors =[(255 ,0 ,0 ),(255 ,127 ,0 ),(255 ,255 ,0 ),(0 ,255 ,0 ),(0 ,0 ,255 ),(139 ,0 ,255 )] + for i ,color in enumerate (colors ): + offset =i *2 + draw .rounded_rectangle ((10 +offset ,10 +offset ,width -10 -offset ,height -10 -offset ), + radius =20 -offset ,outline =color ,width =2 ) + + + draw .rounded_rectangle ((25 ,25 ,width -25 ,height -25 ),radius =10 ,fill =(15 ,15 ,15 )) + + await self .add_avatar_and_content_gaming (draw ,member ,level ,rank ,messages ,xp ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ) + + img_bytes =io .BytesIO () + img .save (img_bytes ,format ='PNG') + img_bytes .seek (0 ) + img .close () + return img_bytes + + async def create_elegant_design (self ,member ,xp ,level ,rank ,messages ,current_level ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ): + """Elegant gold and black design""" + img =Image .new ('RGB',(width ,height ),(25 ,25 ,25 )) + draw =ImageDraw .Draw (img ) + + + for i in range (10 ): + alpha =255 -(i *25 ) + color =(255 ,215 ,0 ,alpha ) + draw .rounded_rectangle ((5 +i ,5 +i ,width -5 -i ,height -5 -i ),radius =20 +i ,outline =color ,width =1 ) + + + draw .rounded_rectangle ((20 ,20 ,width -20 ,height -20 ),radius =15 ,fill =(35 ,35 ,35 )) + + await self .add_avatar_and_content_elegant (draw ,member ,xp ,progress ,needed ,messages ,level ,rank ,width ,height ,font_large ,font_medium ,font_small ) + + img_bytes =io .BytesIO () + img .save (img_bytes ,format ='PNG') + img_bytes .seek (0 ) + img .close () + return img_bytes + + async def create_cyberpunk_design (self ,member ,xp ,level ,rank ,messages ,current_level ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ): + """Cyberpunk style design""" + img =Image .new ('RGB',(width ,height ),(15 ,15 ,15 )) + draw =ImageDraw .Draw (img ) + + + line_color =(75 ,0 ,130 ) + for i in range (20 ): + y =int (height /20 *i ) + draw .line ((0 ,y ,width ,y ),fill =line_color ,width =1 ) + + + panel_color =(25 ,25 ,25 ) + draw .rounded_rectangle ((20 ,20 ,width -20 ,height -20 ),radius =15 ,fill =panel_color ) + + + await self .add_avatar_and_content_cyberpunk (draw ,member ,xp ,progress ,needed ,messages ,level ,rank ,width ,height ,font_large ,font_medium ,font_small ) + + img_bytes =io .BytesIO () + img .save (img_bytes ,format ='PNG') + img_bytes .seek (0 ) + img .close () + return img_bytes + + async def add_avatar_and_content_classic (self ,draw ,member ,level ,rank ,messages ,xp ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ): + """Add avatar and content for classic design""" + + avatar_size =120 + avatar_x ,avatar_y =30 ,90 + + try : + avatar_url =str (member .display_avatar .with_size (256 ).url ) + response =requests .get (avatar_url ,timeout =10 ) + if response .status_code ==200 : + avatar_img =Image .open (io .BytesIO (response .content )) + avatar_img =avatar_img .resize ((avatar_size ,avatar_size )) + + mask =Image .new ('L',(avatar_size ,avatar_size ),0 ) + mask_draw =ImageDraw .Draw (mask ) + mask_draw .ellipse ((0 ,0 ,avatar_size ,avatar_size ),fill =255 ) + + draw .ellipse ((avatar_x -3 ,avatar_y -3 ,avatar_x +avatar_size +3 ,avatar_y +avatar_size +3 ),fill =(255 ,255 ,255 )) + + avatar_img .putalpha (mask ) + img =draw ._image + img .paste (avatar_img ,(avatar_x ,avatar_y ),avatar_img ) + else : + raise Exception ("Failed to download avatar") + except Exception as e : + draw .ellipse ((avatar_x -3 ,avatar_y -3 ,avatar_x +avatar_size +3 ,avatar_y +avatar_size +3 ),fill =(255 ,255 ,255 )) + draw .ellipse ((avatar_x ,avatar_y ,avatar_x +avatar_size ,avatar_y +avatar_size ),fill =(64 ,68 ,75 )) + draw .text ((avatar_x +avatar_size //2 -20 ,avatar_y +avatar_size //2 -20 ),"👤",font =font_large ,fill =(255 ,255 ,255 )) + + + info_x =180 + username =member .display_name [:18 ] + + draw .text ((info_x +2 ,42 ),username ,font =font_large ,fill =(0 ,0 ,0 )) + draw .text ((info_x ,40 ),username ,font =font_large ,fill =(255 ,255 ,255 )) + + draw .text ((info_x +2 ,82 ),f"🏆 Level {level}",font =font_medium ,fill =(0 ,0 ,0 )) + draw .text ((info_x ,80 ),f"🏆 Level {level}",font =font_medium ,fill =(255 ,215 ,0 )) + + draw .text ((info_x +2 ,112 ),f"📊 Rank #{rank}",font =font_medium ,fill =(0 ,0 ,0 )) + draw .text ((info_x ,110 ),f"📊 Rank #{rank}",font =font_medium ,fill =(135 ,206 ,250 )) + + + bar_x ,bar_y =info_x ,150 + bar_width ,bar_height =500 ,25 + + draw .rounded_rectangle ((bar_x ,bar_y ,bar_x +bar_width ,bar_y +bar_height ),radius =12 ,fill =(32 ,34 ,37 )) + draw .rounded_rectangle ((bar_x ,bar_y ,bar_x +bar_width ,bar_y +bar_height ),radius =12 ,outline =(128 ,128 ,128 ),width =1 ) + + if needed >0 : + progress_width =int ((progress /needed )*bar_width ) + if progress_width >0 : + for i in range (progress_width ): + factor =i /progress_width if progress_width >0 else 0 + r =int (100 +(255 -100 )*factor ) + g =int (150 +(105 -150 )*factor ) + b =int (255 -50 *factor ) + draw .line ([(bar_x +i ,bar_y +2 ),(bar_x +i ,bar_y +bar_height -2 )],fill =(r ,g ,b )) + + xp_text =f"{progress:,} / {needed:,} XP" + text_bbox =draw .textbbox ((0 ,0 ),xp_text ,font =font_small ) + text_width =text_bbox [2 ]-text_bbox [0 ] + text_x =bar_x +(bar_width -text_width )//2 + draw .text ((text_x +1 ,bar_y +6 ),xp_text ,font =font_small ,fill =(0 ,0 ,0 )) + draw .text ((text_x ,bar_y +5 ),xp_text ,font =font_small ,fill =(255 ,255 ,255 )) + + + stats_x =720 + draw .text ((stats_x +1 ,81 ),f"💬 Messages",font =font_small ,fill =(0 ,0 ,0 )) + draw .text ((stats_x ,80 ),f"💬 Messages",font =font_small ,fill =(255 ,255 ,255 )) + draw .text ((stats_x +1 ,101 ),f"{messages:,}",font =font_medium ,fill =(0 ,0 ,0 )) + draw .text ((stats_x ,100 ),f"{messages:,}",font =font_medium ,fill =(0 ,255 ,127 )) + + draw .text ((stats_x +1 ,131 ),f"⭐ Total XP",font =font_small ,fill =(0 ,0 ,0 )) + draw .text ((stats_x ,130 ),f"⭐ Total XP",font =font_small ,fill =(255 ,255 ,255 )) + draw .text ((stats_x +1 ,151 ),f"{xp:,}",font =font_medium ,fill =(0 ,0 ,0 )) + draw .text ((stats_x ,150 ),f"{xp:,}",font =font_medium ,fill =(255 ,165 ,0 )) + + percentage =(progress /needed *100 )if needed >0 else 100 + draw .text ((stats_x +1 ,181 ),f"📈 Progress",font =font_small ,fill =(0 ,0 ,0 )) + draw .text ((stats_x ,180 ),f"📈 Progress",font =font_small ,fill =(255 ,255 ,255 )) + draw .text ((stats_x +1 ,201 ),f"{percentage:.1f}%",font =font_medium ,fill =(0 ,0 ,0 )) + draw .text ((stats_x ,200 ),f"{percentage:.1f}%",font =font_medium ,fill =(255 ,105 ,180 )) + + if level >=10 : + sparkle_positions =[(50 ,50 ),(850 ,50 ),(50 ,250 ),(850 ,250 )] + for pos in sparkle_positions : + draw .text (pos ,"✨",font =font_small ,fill =(255 ,255 ,255 )) + + async def add_avatar_and_content_neon (self ,draw ,member ,level ,rank ,messages ,xp ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ): + """Add avatar and content for neon design""" + avatar_size =100 + avatar_x ,avatar_y =40 ,100 + + + for i in range (3 ): + draw .ellipse ((avatar_x -5 -i ,avatar_y -5 -i ,avatar_x +avatar_size +5 +i ,avatar_y +avatar_size +5 +i ),outline =(0 ,255 ,255 ),width =1 ) + + try : + avatar_url =str (member .display_avatar .with_size (256 ).url ) + response =requests .get (avatar_url ,timeout =10 ) + if response .status_code ==200 : + avatar_img =Image .open (io .BytesIO (response .content )) + avatar_img =avatar_img .resize ((avatar_size ,avatar_size )) + + mask =Image .new ('L',(avatar_size ,avatar_size ),0 ) + mask_draw =ImageDraw .Draw (mask ) + mask_draw .ellipse ((0 ,0 ,avatar_size ,avatar_size ),fill =255 ) + + avatar_img .putalpha (mask ) + img =draw ._image + img .paste (avatar_img ,(avatar_x ,avatar_y ),avatar_img ) + else : + raise Exception ("Failed to download avatar") + except Exception as e : + draw .ellipse ((avatar_x ,avatar_y ,avatar_x +avatar_size ,avatar_y +avatar_size ),fill =(0 ,50 ,50 )) + draw .text ((avatar_x +avatar_size //2 -15 ,avatar_y +avatar_size //2 -15 ),"👤",font =font_medium ,fill =(0 ,255 ,255 )) + + + info_x =170 + username =member .display_name [:15 ] + + + for offset in [(2 ,2 ),(1 ,1 ),(0 ,0 )]: + color =(0 ,255 ,255 )if offset ==(0 ,0 )else (0 ,100 ,100 ) + draw .text ((info_x +offset [0 ],50 +offset [1 ]),username ,font =font_large ,fill =color ) + draw .text ((info_x +offset [0 ],85 +offset [1 ]),f"⚡ Level {level}",font =font_medium ,fill =color ) + draw .text ((info_x +offset [0 ],115 +offset [1 ]),f"🎯 Rank #{rank}",font =font_medium ,fill =color ) + + + bar_x ,bar_y =info_x ,160 + bar_width ,bar_height =480 ,20 + + draw .rounded_rectangle ((bar_x ,bar_y ,bar_x +bar_width ,bar_y +bar_height ),radius =10 ,fill =(0 ,20 ,20 )) + draw .rounded_rectangle ((bar_x ,bar_y ,bar_x +bar_width ,bar_y +bar_height ),radius =10 ,outline =(0 ,255 ,255 ),width =2 ) + + if needed >0 : + progress_width =int ((progress /needed )*bar_width ) + if progress_width >0 : + draw .rounded_rectangle ((bar_x +2 ,bar_y +2 ,bar_x +progress_width -2 ,bar_y +bar_height -2 ),radius =8 ,fill =(0 ,255 ,255 )) + + xp_text =f"{progress:,} / {needed:,} XP" + text_bbox =draw .textbbox ((0 ,0 ),xp_text ,font =font_small ) + text_width =text_bbox [2 ]-text_bbox [0 ] + text_x =bar_x +(bar_width -text_width )//2 + draw .text ((text_x ,bar_y +2 ),xp_text ,font =font_small ,fill =(255 ,255 ,255 )) + + + stats_x =700 + stats_y =80 + draw .text ((stats_x ,stats_y ),f"💬 {messages:,}",font =font_small ,fill =(0 ,255 ,255 )) + draw .text ((stats_x ,stats_y +25 ),f"⭐ {xp:,} XP",font =font_small ,fill =(0 ,255 ,255 )) + + percentage =(progress /needed *100 )if needed >0 else 100 + draw .text ((stats_x ,stats_y +50 ),f"📈 {percentage:.1f}%",font =font_small ,fill =(0 ,255 ,255 )) + + async def add_avatar_and_content_space (self ,draw ,member ,level ,rank ,messages ,xp ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ): + """Add avatar and content for space design""" + avatar_size =110 + avatar_x ,avatar_y =35 ,95 + + + for i in range (5 ): + alpha =100 -(i *20 ) + draw .ellipse ((avatar_x -5 -i ,avatar_y -5 -i ,avatar_x +avatar_size +5 +i ,avatar_y +avatar_size +5 +i ), + outline =(100 ,150 ,255 ,alpha ),width =1 ) + + try : + avatar_url =str (member .display_avatar .with_size (256 ).url ) + response =requests .get (avatar_url ,timeout =10 ) + if response .status_code ==200 : + avatar_img =Image .open (io .BytesIO (response .content )) + avatar_img =avatar_img .resize ((avatar_size ,avatar_size )) + + mask =Image .new ('L',(avatar_size ,avatar_size ),0 ) + mask_draw =ImageDraw .Draw (mask ) + mask_draw .ellipse ((0 ,0 ,avatar_size ,avatar_size ),fill =255 ) + + avatar_img .putalpha (mask ) + img =draw ._image + img .paste (avatar_img ,(avatar_x ,avatar_y ),avatar_img ) + else : + raise Exception ("Failed to download avatar") + except Exception as e : + draw .ellipse ((avatar_x ,avatar_y ,avatar_x +avatar_size ,avatar_y +avatar_size ),fill =(20 ,30 ,60 )) + draw .text ((avatar_x +avatar_size //2 -15 ,avatar_y +avatar_size //2 -15 ),"🚀",font =font_medium ,fill =(255 ,255 ,255 )) + + + info_x =175 + username =member .display_name [:16 ] + + draw .text ((info_x ,45 ),username ,font =font_large ,fill =(255 ,255 ,255 )) + draw .text ((info_x ,80 ),f"🌟 Level {level}",font =font_medium ,fill =(255 ,215 ,0 )) + draw .text ((info_x ,110 ),f"🛸 Rank #{rank}",font =font_medium ,fill =(100 ,200 ,255 )) + + + bar_x ,bar_y =info_x ,155 + bar_width ,bar_height =490 ,22 + + draw .rounded_rectangle ((bar_x ,bar_y ,bar_x +bar_width ,bar_y +bar_height ),radius =11 ,fill =(10 ,10 ,30 )) + draw .rounded_rectangle ((bar_x ,bar_y ,bar_x +bar_width ,bar_y +bar_height ),radius =11 ,outline =(100 ,150 ,255 ),width =1 ) + + if needed >0 : + progress_width =int ((progress /needed )*bar_width ) + if progress_width >0 : + + for i in range (progress_width ): + factor =i /progress_width if progress_width >0 else 0 + r =int (50 +(200 *factor )) + g =int (100 +(150 *factor )) + b =255 + draw .line ([(bar_x +i ,bar_y +2 ),(bar_x +i ,bar_y +bar_height -2 )],fill =(r ,g ,b )) + + xp_text =f"{progress:,} / {needed:,} XP" + text_bbox =draw .textbbox ((0 ,0 ),xp_text ,font =font_small ) + text_width =text_bbox [2 ]-text_bbox [0 ] + text_x =bar_x +(bar_width -text_width )//2 + draw .text ((text_x ,bar_y +3 ),xp_text ,font =font_small ,fill =(255 ,255 ,255 )) + + + stats_x =720 + draw .text ((stats_x ,85 ),f"📡 Messages",font =font_small ,fill =(200 ,200 ,255 )) + draw .text ((stats_x ,105 ),f"{messages:,}",font =font_medium ,fill =(255 ,255 ,255 )) + draw .text ((stats_x ,135 ),f"⭐ Total XP",font =font_small ,fill =(200 ,200 ,255 )) + draw .text ((stats_x ,155 ),f"{xp:,}",font =font_medium ,fill =(255 ,215 ,0 )) + + percentage =(progress /needed *100 )if needed >0 else 100 + draw .text ((stats_x ,185 ),f"🌌 {percentage:.1f}%",font =font_small ,fill =(100 ,200 ,255 )) + + async def add_avatar_and_content_minimal (self ,draw ,member ,level ,rank ,messages ,xp ,progress ,needed ,width ,height ,font_large ,font_medium ,font_small ): + """Add avatar and content for minimal design""" + avatar_size =100 + avatar_x ,avatar_y =40 ,100 + + + draw .ellipse ((avatar_x -2 ,avatar_y -2 ,avatar_x +avatar_size +2 ,avatar_y +avatar_size +2 ),outline =(200 ,200 ,200 ),width =2 ) + + try : + avatar_url =str (member .display_avatar .with_size (256 ).url ) + response =requests .get (avatar_url ,timeout =10 ) + if response .status_code ==200 : + avatar_img =Image .open (io .BytesIO (response .content )) + avatar_img =avatar_img .resize ((avatar_size ,avatar_size )) + + mask =Image .new ('L',(avatar_size ,avatar_size ),0 ) + mask_draw =ImageDraw .Draw (mask ) + mask_draw .ellipse ((0 ,0 ,avatar_size ,avatar_size ),fill =255 ) + + avatar_img .putalpha (mask ) + img =draw ._image + img .paste (avatar_img ,(avatar_x ,avatar_y ),avatar_img ) + else : + raise Exception ("Failed to download avatar") + except Exception as e : + draw .ellipse ((avatar_x ,avatar_y ,avatar_x +avatar_size ,avatar_y +avatar_size ),fill =(240 ,240 ,240 )) + draw .text ((avatar_x +avatar_size //2 -15 ,avatar_y +avatar_size //2 -15 ),"👤",font =font_medium ,fill =(100 ,100 ,100 )) + + + info_x =170 + username =member .display_name [:18 ] + + draw .text ((info_x ,50 ),username ,font =font_large ,fill =(50 ,50 ,50 )) + draw .text ((info_x ,85 ),f"Level {level}",font =font_medium ,fill =(100 ,100 ,100 )) + draw .text ((info_x ,115 ),f"Rank #{rank}",font =font_medium ,fill =(150 ,150 ,150 )) + + + bar_x ,bar_y =info_x ,160 + bar_width ,bar_height =480 ,18 + + draw .rounded_rectangle ((bar_x ,bar_y ,bar_x +bar_width ,bar_y +bar_height ),radius =9 ,fill =(240 ,240 ,240 )) + draw .rounded_rectangle ((bar_x ,bar_y ,bar_x +bar_width ,bar_y +bar_height ),radius =9 ,outline =(200 ,200 ,200 ),width =1 ) + + if needed >0 : + progress_width =int ((progress /needed )*bar_width ) + if progress_width >0 : + draw .rounded_rectangle ((bar_x +2 ,bar_y +2 ,bar_x +progress_width -2 ,bar_y +bar_height -2 ),radius =7 ,fill =(220 ,220 ,220 )) + + xp_text =f"{progress:,} / {needed:,} XP" + text_bbox =draw .textbbox ((0 ,0 ),xp_text ,font =font_small ) + text_width =text_bbox [2 ]-text_bbox [0 ] + text_x =bar_x +(bar_width -text_width )//2 + draw .text ((text_x ,bar_y +2 ),xp_text ,font =font_small ,fill =(100 ,100 ,100 )) + + + stats_x =700 + draw .text ((stats_x ,85 ),f"Messages: {messages:,}",font =font_small ,fill =(120 ,120 ,120 )) + draw .text ((stats_x ,115 ),f"Total XP: {xp:,}",font =font_small ,fill =(120 ,120 ,120 )) + + percentage =(progress /needed *100 )if needed >0 else 100 + draw .text ((stats_x ,145 ),f"Progress: {percentage:.1f}%",font =font_small ,fill =(120 ,120 ,120 )) + + async def add_avatar_and_content_gaming (self ,draw ,member ,xp ,progress ,needed ,messages ,level ,rank ,width ,height ,font_large ,font_medium ,font_small ): + """Add avatar and content for gaming design""" + avatar_size =110 + avatar_x ,avatar_y =35 ,95 + + + colors =[(255 ,0 ,0 ),(0 ,255 ,0 ),(0 ,0 ,255 )] + for i ,color in enumerate (colors ): + offset =i *2 + draw .ellipse ((avatar_x -5 -offset ,avatar_y -5 -offset ,avatar_x +avatar_size +5 +offset ,avatar_y +avatar_size +5 +offset ),outline =color ,width =1 ) + + try : + avatar_url =str (member .display_avatar .with_size (256 ).url ) + response =requests .get (avatar_url ,timeout =10 ) + if response .status_code ==200 : + avatar_img =Image .open (io .BytesIO (response .content )) + avatar_img =avatar_img .resize ((avatar_size ,avatar_size )) + + mask =Image .new ('L',(avatar_size ,avatar_size ),0 ) + mask_draw =ImageDraw .Draw (mask ) + mask_draw .ellipse ((0 ,0 ,avatar_size ,avatar_size ),fill =255 ) + + avatar_img .putalpha (mask ) + img =draw ._image + img .paste (avatar_img ,(avatar_x ,avatar_y ),avatar_img ) + else : + raise Exception ("Failed to download avatar") + except Exception as e : + draw .ellipse ((avatar_x ,avatar_y ,avatar_x +avatar_size ,avatar_y +avatar_size ),fill =(30 ,30 ,30 )) + draw .text ((avatar_x +avatar_size //2 -15 ,avatar_y +avatar_size //2 -15 ),"🎮",font =font_medium ,fill =(200 ,200 ,200 )) + + + info_x =175 + username =member .display_name [:16 ] + + draw .text ((info_x ,45 ),username ,font =font_large ,fill =(200 ,200 ,200 )) + draw .text ((info_x ,80 ),f"Level {level}",font =font_medium ,fill =(150 ,150 ,150 )) + draw .text ((info_x ,110 ),f"Rank #{rank}",font =font_medium ,fill =(100 ,100 ,100 )) + + + bar_x ,bar_y =info_x ,155 + bar_width ,bar_height =490 ,22 + + draw .rounded_rectangle ((bar_x ,bar_y ,bar_x +bar_width ,bar_y +bar_height ),radius =11 ,fill =(30 ,30 ,30 )) + draw .rounded_rectangle ((bar_x ,bar_y ,bar_x +bar_width ,bar_y +bar_height ),radius =11 ,outline =(80 ,80 ,80 ),width =1 ) + + if needed >0 : + progress_width =int ((progress /needed )*bar_width ) + if progress_width >0 : + + for i in range (progress_width ): + factor =i /progress_width if progress_width >0 else 0 + r =int (50 +(150 *factor )) + g =int (100 +(100 *factor )) + b =200 + draw .line ([(bar_x +i ,bar_y +2 ),(bar_x +i ,bar_y +bar_height -2 )],fill =(r ,g ,b )) + + xp_text =f"{progress:,} / {needed:,} XP" + text_bbox =draw .textbbox ((0 ,0 ),xp_text ,font =font_small ) + text_width =text_bbox [2 ]-text_bbox [0 ] + text_x =bar_x +(bar_width -text_width )//2 + draw .text ((text_x ,bar_y +3 ),xp_text ,font =font_small ,fill =(200 ,200 ,200 )) + + + stats_x =720 + draw .text ((stats_x ,85 ),f"Messages: {messages:,}",font =font_small ,fill =(150 ,150 ,150 )) + draw .text ((stats_x ,115 ),f"Total XP: {xp:,}",font =font_small ,fill =(150 ,150 ,150 )) + + percentage =(progress /needed *100 )if needed >0 else 100 + draw .text ((stats_x ,145 ),f"Progress: {percentage:.1f}%",font =font_small ,fill =(150 ,150 ,150 )) + + async def add_avatar_and_content_elegant (self ,draw ,member ,xp ,progress ,needed ,messages ,level ,rank ,width ,height ,font_large ,font_medium ,font_small ): + """Add avatar and content for elegant design""" + avatar_size =110 + avatar_x ,avatar_y =35 ,95 + + + for i in range (3 ): + alpha =150 -(i *50 ) + draw .ellipse ((avatar_x -4 -i ,avatar_y -4 -i ,avatar_x +avatar_size +4 +i ,avatar_y +avatar_size +4 +i ),outline =(255 ,215 ,0 ,alpha ),width =1 ) + + try : + avatar_url =str (member .display_avatar .with_size (256 ).url ) + response =requests .get (avatar_url ,timeout =10 ) + if response .status_code ==200 : + avatar_img =Image .open (io .BytesIO (response .content )) + avatar_img =avatar_img .resize ((avatar_size ,avatar_size )) + + mask =Image .new ('L',(avatar_size ,avatar_size ),0 ) + mask_draw =ImageDraw .Draw (mask ) + mask_draw .ellipse ((0 ,0 ,avatar_size ,avatar_size ),fill =255 ) + + avatar_img .putalpha (mask ) + img =draw ._image + img .paste (avatar_img ,(avatar_x ,avatar_y ),avatar_img ) + else : + raise Exception ("Failed to download avatar") + except Exception as e : + draw .ellipse ((avatar_x ,avatar_y ,avatar_x +avatar_size ,avatar_y +avatar_size ),fill =(45 ,45 ,45 )) + draw .text ((avatar_x +avatar_size //2 -15 ,avatar_y +avatar_size //2 -15 ),"👑",font =font_medium ,fill =(255 ,215 ,0 )) + + + info_x =175 + username =member .display_name [:16 ] + + draw .text ((info_x ,45 ),username ,font =font_large ,fill =(230 ,230 ,230 )) + draw .text ((info_x ,80 ),f"Level {level}",font =font_medium ,fill =(200 ,170 ,0 )) + draw .text ((info_x ,110 ),f"Rank #{rank}",font =font_medium ,fill =(180 ,180 ,180 )) + + + bar_x ,bar_y =info_x ,155 + bar_width ,bar_height =490 ,22 + + draw .rounded_rectangle ((bar_x ,bar_y ,bar_x +bar_width ,bar_y +bar_height ),radius =11 ,fill =(40 ,40 ,40 )) + draw .rounded_rectangle ((bar_x ,bar_y ,bar_x +bar_width ,bar_y +bar_height ),radius =11 ,outline =(100 ,80 ,0 ),width =1 ) + + if needed >0 : + progress_width =int ((progress /needed )*bar_width ) + if progress_width >0 : + + for i in range (progress_width ): + factor =i /progress_width if progress_width >0 else 0 + r =255 + g =int (150 +(65 *factor )) + b =0 + draw .line ([(bar_x +i ,bar_y +2 ),(bar_x +i ,bar_y +bar_height -2 )],fill =(r ,g ,b )) + + xp_text =f"{progress:,} / {needed:,} XP" + text_bbox =draw .textbbox ((0 ,0 ),xp_text ,font =font_small ) + text_width =text_bbox [2 ]-text_bbox [0 ] + text_x =bar_x +(bar_width -text_width )//2 + draw .text ((text_x ,bar_y +3 ),xp_text ,font =font_small ,fill =(220 ,220 ,220 )) + + + stats_x =720 + draw .text ((stats_x ,85 ),f"Messages: {messages:,}",font =font_small ,fill =(180 ,180 ,180 )) + draw .text ((stats_x ,115 ),f"Total XP: {xp:,}",font =font_small ,fill =(255 ,215 ,0 )) + + percentage =(progress /needed *100 )if needed >0 else 100 + draw .text ((stats_x ,145 ),f"Progress: {percentage:.1f}%",font =font_small ,fill =(200 ,200 ,200 )) + + async def add_avatar_and_content_cyberpunk (self ,draw ,member ,xp ,progress ,needed ,messages ,level ,rank ,width ,height ,font_large ,font_medium ,font_small ): + """Add avatar and content for cyberpunk design""" + avatar_size =100 + avatar_x ,avatar_y =40 ,100 + + + try : + avatar_url =str (member .display_avatar .with_size (256 ).url ) + response =requests .get (avatar_url ,timeout =10 ) + if response .status_code ==200 : + avatar_img =Image .open (io .BytesIO (response .content )) + avatar_img =avatar_img .resize ((avatar_size ,avatar_size )) + + mask =Image .new ('L',(avatar_size ,avatar_size ),0 ) + mask_draw =ImageDraw .Draw (mask ) + mask_draw .rectangle ((0 ,0 ,avatar_size ,avatar_size ),fill =255 ) + + avatar_img .putalpha (mask ) + img =draw ._image + img .paste (avatar_img ,(avatar_x ,avatar_y ),avatar_img ) + else : + raise Exception ("Failed to download avatar") + except Exception as e : + draw .rectangle ((avatar_x ,avatar_y ,avatar_x +avatar_size ,avatar_y +avatar_size ),fill =(45 ,45 ,45 )) + draw .text ((avatar_x +avatar_size //2 -15 ,avatar_y +avatar_size //2 -15 ),"👤",font =font_medium ,fill =(148 ,0 ,211 )) + + + text_color =(148 ,0 ,211 ) + info_x =170 + username =member .display_name [:15 ] + + draw .text ((info_x ,50 ),username ,font =font_large ,fill =text_color ) + draw .text ((info_x ,85 ),f"Level {level}",font =font_medium ,fill =text_color ) + draw .text ((info_x ,115 ),f"Rank #{rank}",font =font_medium ,fill =text_color ) + + + bar_x ,bar_y =info_x ,160 + bar_width ,bar_height =480 ,20 + + draw .rectangle ((bar_x ,bar_y ,bar_x +bar_width ,bar_y +bar_height ),fill =(30 ,30 ,30 )) + + if needed >0 : + progress_width =int ((progress /needed )*bar_width ) + if progress_width >0 : + draw .rectangle ((bar_x ,bar_y ,bar_x +progress_width ,bar_y +bar_height ),fill =text_color ) + + xp_text =f"{progress:,} / {needed:,} XP" + text_bbox =draw .textbbox ((0 ,0 ),xp_text ,font =font_small ) + text_width =text_bbox [2 ]-text_bbox [0 ] + text_x =bar_x +(bar_width -text_width )//2 + draw .text ((text_x ,bar_y +2 ),xp_text ,font =font_small ,fill =(220 ,220 ,220 )) + + + stats_x =700 + draw .text ((stats_x ,85 ),f"Messages: {messages:,}",font =font_small ,fill =text_color ) + draw .text ((stats_x ,115 ),f"Total XP: {xp:,}",font =font_small ,fill =text_color ) + + percentage =(progress /needed *100 )if needed >0 else 100 + draw .text ((stats_x ,145 ),f"Progress: {percentage:.1f}%",font =font_small ,fill =text_color ) + + @level .command (name ="rank",description ="View your current rank and level") + async def rank (self ,ctx ,member :Optional [discord .Member ]=None ): + """Display rank card""" + member =member or ctx .author + guild_id =ctx .guild .id + + try : + rank_card =await self .create_rank_card (member ,guild_id ) + + if rank_card : + if isinstance (rank_card ,str ): + + await ctx .send (rank_card ) + else : + + file =discord .File (fp =rank_card ,filename ="rank_card.png") + await ctx .send (file =file ) + try : + rank_card .close () + except : + pass + else : + pass + except Exception as e : + logger .error (f"Error in rank command: {e}") + pass + + @level .command (name ="settings",description ="Configure leveling settings",aliases =['config']) + @commands .has_permissions (administrator =True ) + async def settings (self ,ctx ): + """Leveling settings configuration""" + try : + current_settings =await self .get_guild_settings (ctx .guild .id ) + + + if hasattr (ctx ,'interaction')and ctx .interaction : + modal =LevelConfigModal (self ,current_settings ) + await ctx .interaction .response .send_modal (modal ) + else : + + await self .interactive_setup (ctx ,current_settings ) + + except Exception as e : + logger .error (f"Error in settings command: {e}") + pass + + async def interactive_setup (self ,ctx ,current_settings ): + """Interactive setup for prefix commands""" + try : + embed =discord .Embed ( + title ="🔧 Interactive Leveling Setup", + description ="Let's configure your leveling system! I'll ask you a few questions.\n" + "Type `cancel` at any time to stop, or `skip` to keep current value.\n\n" + "**Current Settings:**", + color =0xFF0000 , + timestamp =datetime .now (timezone .utc ) + ) + + embed .add_field ( + name ="Current Configuration", + value =f"🔧 Status: {'{TICK} Enabled' if current_settings.get('enabled', False) else '{CROSS} Disabled'}\n" + f"💎 XP per Message: {current_settings.get('xp_per_message', 20)}\n" + f"🎨 Embed Color: {current_settings.get('embed_color', '#000000')}\n" + f"🖼️ Thumbnail: {'{TICK} Yes' if current_settings.get('thumbnail_enabled', True) else '{CROSS} No'}\n" + f"📢 Level Channel: {'Set' if current_settings.get('channel_id') else 'Not set'}", + inline =False + ) + + setup_msg =await ctx .send (embed =embed ) + + def check (m ): + return m .author ==ctx .author and m .channel ==ctx .channel + + + await ctx .send ("💎 **How much XP should users get per message?** (1-999)\n" + f"Current: `{current_settings.get('xp_per_message', 20)}`") + + try : + xp_msg =await self .bot .wait_for ('message',check =check ,timeout =60 ) + if xp_msg .content .lower ()=='cancel': + await ctx .send (f"{CROSS} Setup cancelled.") + return + + if xp_msg .content .lower ()=='skip': + xp_per_message =current_settings .get ('xp_per_message',20 ) + else : + xp_per_message =int (xp_msg .content ) + if xp_per_message <1 or xp_per_message >999 : + await ctx .send (f"{CROSS} XP must be between 1 and 999. Using current value.") + xp_per_message =current_settings .get ('xp_per_message',20 ) + except (ValueError ,asyncio .TimeoutError ): + await ctx .send ("⏰ Timeout or invalid input. Using current value.") + xp_per_message =current_settings .get ('xp_per_message',20 ) + + + current_msg =current_settings .get ('level_message','Congratulations {user}! You have reached level {level}!') + await ctx .send ("💬 **What should the level-up message say?**\n" + f"Available placeholders: `{'{user}'}`, `{'{username}'}`, `{'{level}'}`, `{'{server}'}`\n" + f"Current: `{current_msg[:100]}{'...' if len(current_msg) > 100 else ''}`") + + try : + msg_response =await self .bot .wait_for ('message',check =check ,timeout =120 ) + if msg_response .content .lower ()=='cancel': + await ctx .send (f"{CROSS} Setup cancelled.") + return + + if msg_response .content .lower ()=='skip': + level_message =current_msg + else : + level_message =msg_response .content + if len (level_message )>2000 : + await ctx .send (f"{CROSS} Message too long (max 2000 chars). Using current value.") + level_message =current_msg + except asyncio .TimeoutError : + await ctx .send ("⏰ Timeout. Using current value.") + level_message =current_msg + + + current_color =current_settings .get ('embed_color','#000000') + await ctx .send ("🎨 **What color should the level-up embeds be?** (hex format like #FF0000)\n" + f"Current: `{current_color}`") + + try : + color_msg =await self .bot .wait_for ('message',check =check ,timeout =60 ) + if color_msg .content .lower ()=='cancel': + await ctx .send (f"{CROSS} Setup cancelled.") + return + + if color_msg .content .lower ()=='skip': + embed_color =current_color + else : + color_input =color_msg .content .strip () + if not color_input .startswith ('#'): + color_input ='#'+color_input + + if validate_hex_color (color_input ): + embed_color =color_input + else : + await ctx .send (f"{CROSS} Invalid hex color. Using current value.") + embed_color =current_color + except asyncio .TimeoutError : + await ctx .send ("⏰ Timeout. Using current value.") + embed_color =current_color + + + current_image =current_settings .get ('level_image','') + await ctx .send ("🖼️ **Level-up image URL** (optional, direct image link)\n" + f"Current: `{'Set' if current_image else 'None'}`") + + try : + image_msg =await self .bot .wait_for ('message',check =check ,timeout =60 ) + if image_msg .content .lower ()=='cancel': + await ctx .send (f"{CROSS} Setup cancelled.") + return + + if image_msg .content .lower ()=='skip': + level_image =current_image + else : + level_image =image_msg .content .strip ()if image_msg .content .strip ()!='none'else None + if level_image and len (level_image )>500 : + await ctx .send (f"{CROSS} URL too long. Using current value.") + level_image =current_image + except asyncio .TimeoutError : + await ctx .send ("⏰ Timeout. Using current value.") + level_image =current_image + + + current_thumb =current_settings .get ('thumbnail_enabled',True ) + await ctx .send ("🖼️ **Show user avatar thumbnail in level-up messages?** (yes/no)\n" + f"Current: `{'Yes' if current_thumb else 'No'}`") + + try : + thumb_msg =await self .bot .wait_for ('message',check =check ,timeout =60 ) + if thumb_msg .content .lower ()=='cancel': + await ctx .send (f"{CROSS} Setup cancelled.") + return + + if thumb_msg .content .lower ()=='skip': + thumbnail_enabled =current_thumb + else : + thumbnail_enabled =thumb_msg .content .lower ()in ['yes','y','true','1','on','enable'] + except asyncio .TimeoutError : + await ctx .send ("⏰ Timeout. Using current value.") + thumbnail_enabled =current_thumb + + + embed_color_int =hex_to_int (embed_color ) + level_image_final =level_image if level_image and level_image .strip ()else None + + async with aiosqlite .connect ("db/leveling.db")as db : + + async with db .execute ("SELECT guild_id FROM leveling_settings WHERE guild_id = ?",(ctx .guild .id ,))as cursor : + exists =await cursor .fetchone () + + if exists : + + await db .execute (""" + UPDATE leveling_settings + SET enabled = 1, xp_per_message = ?, level_message = ?, embed_color = ?, + level_image = ?, thumbnail_enabled = ? + WHERE guild_id = ? + """,( + xp_per_message , + level_message , + embed_color_int , + level_image_final , + 1 if thumbnail_enabled else 0 , + ctx .guild .id + )) + else : + + await db .execute (""" + INSERT INTO leveling_settings + (guild_id, enabled, xp_per_message, level_message, embed_color, level_image, thumbnail_enabled, + min_xp, max_xp, cooldown_seconds, dm_level_up, channel_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """,( + ctx .guild .id , + 1 , + xp_per_message , + level_message , + embed_color_int , + level_image_final , + 1 if thumbnail_enabled else 0 , + 15 , + 25 , + 60 , + 0 , + None + )) + + await db .commit () + + + final_embed =discord .Embed ( + title =f"{TICK} Leveling System Configured!", + description ="Your leveling system has been successfully set up with these settings:", + color =embed_color_int , + timestamp =datetime .now (timezone .utc ) + ) + + final_embed .add_field ( + name ="📊 Configuration Summary", + value =f"🔧 **Status:** Enabled\n" + f"💎 **XP per Message:** {xp_per_message}\n" + f"💬 **Level-up Message:** {level_message[:50]}{'...' if len(level_message) > 50 else ''}\n" + f"🎨 **Embed Color:** {embed_color}\n" + f"🖼️ **Level-up Image:** {'Set' if level_image_final else 'None'}\n" + f"🖼️ **Show Thumbnail:** {'Yes' if thumbnail_enabled else 'No'}", + inline =False + ) + + final_embed .add_field ( + name ="🚀 Next Steps", + value ="• Use `!level channel #channel` to set announcement channel\n" + "• Use `!level rewards add @role` to add level rewards\n" + "• Use `!level leaderboard` to view the server leaderboard\n" + "• Users will now gain XP from messages!", + inline =False + ) + + view =PlaceholdersView () + await ctx .send (embed =final_embed ,view =view ) + + except Exception as e : + logger .error (f"Error in interactive setup: {e}") + pass + + @level .command (name ="setxp",description ="Set XP amount per message") + @commands .has_permissions (administrator =True ) + async def setxp (self ,ctx ,amount :int ): + """Set XP per message amount""" + try : + if amount <1 or amount >999 : + await ctx .send (f"{CROSS} XP per message must be between 1 and 999!") + return + + async with aiosqlite .connect (self .db_path )as db : + async with db .execute ("SELECT guild_id FROM leveling_settings WHERE guild_id = ?",(ctx .guild .id ,))as cursor : + exists =await cursor .fetchone () + + if exists : + await db .execute ("UPDATE leveling_settings SET xp_per_message = ? WHERE guild_id = ?",(amount ,ctx .guild .id )) + else : + await db .execute ("INSERT INTO leveling_settings (guild_id, xp_per_message) VALUES (?, ?)",(ctx .guild .id ,amount )) + + await db .commit () + + embed =discord .Embed ( + title =f"{TICK} XP Amount Updated", + description =f"XP per message has been set to **{amount} XP**", + color =0x00FF00 , + timestamp =datetime .now (timezone .utc ) + ) + await ctx .send (embed =embed ) + + except Exception as e : + logger .error (f"Error setting XP amount: {e}") + pass + + @level .command (name ="setmessage",description ="Set level-up message") + @commands .has_permissions (administrator =True ) + async def setmessage (self ,ctx ,*,message :str ): + """Set level-up message""" + try : + if len (message )>2000 : + await ctx .send (f"{CROSS} Level-up message must be less than 2000 characters!") + return + + async with aiosqlite .connect (self .db_path )as db : + async with db .execute ("SELECT guild_id FROM leveling_settings WHERE guild_id = ?",(ctx .guild .id ,))as cursor : + exists =await cursor .fetchone () + + if exists : + await db .execute ("UPDATE leveling_settings SET level_message = ? WHERE guild_id = ?",(message ,ctx .guild .id )) + else : + await db .execute ("INSERT INTO leveling_settings (guild_id, level_message) VALUES (?, ?)",(ctx .guild .id ,message )) + + await db .commit () + + embed =discord .Embed ( + title =f"{TICK} Level-Up Message Updated", + description =f"Level-up message has been set to:\n```{message}```", + color =0x00FF00 , + timestamp =datetime .now (timezone .utc ) + ) + await ctx .send (embed =embed ) + + except Exception as e : + logger .error (f"Error setting level message: {e}") + pass + + @level .command (name ="setcolor",description ="Set embed color") + @commands .has_permissions (administrator =True ) + async def setcolor (self ,ctx ,color :str ): + """Set embed color""" + try : + + if not color .startswith ('#'): + color ='#'+color + + if not validate_hex_color (color ): + await ctx .send (f"{CROSS} Invalid hex color format! Use format like #FF0000") + return + + color_int =hex_to_int (color ) + + async with aiosqlite .connect (self .db_path )as db : + async with db .execute ("SELECT guild_id FROM leveling_settings WHERE guild_id = ?",(ctx .guild .id ,))as cursor : + exists =await cursor .fetchone () + + if exists : + await db .execute ("UPDATE leveling_settings SET embed_color = ? WHERE guild_id = ?",(color_int ,ctx .guild .id )) + else : + await db .execute ("INSERT INTO leveling_settings (guild_id, embed_color) VALUES (?, ?)",(ctx .guild .id ,color_int )) + + await db .commit () + + embed =discord .Embed ( + title =f"{TICK} Embed Color Updated", + description =f"Embed color has been set to **{color}**", + color =color_int , + timestamp =datetime .now (timezone .utc ) + ) + await ctx .send (embed =embed ) + + except Exception as e : + logger .error (f"Error setting embed color: {e}") + pass + + @level .command (name ="thumbnail",description ="Toggle user thumbnail in level-up messages") + @commands .has_permissions (administrator =True ) + async def thumbnail (self ,ctx ,setting :str ): + """Toggle thumbnail setting""" + try : + setting =setting .lower () + if setting not in ['on','off','true','false','yes','no','enable','disable']: + await ctx .send (f"{CROSS} Use: `on/off`, `true/false`, `yes/no`, or `enable/disable`") + return + + enabled =setting in ['on','true','yes','enable'] + + async with aiosqlite .connect (self .db_path )as db : + async with db .execute ("SELECT guild_id FROM leveling_settings WHERE guild_id = ?",(ctx .guild .id ,))as cursor : + exists =await cursor .fetchone () + + if exists : + await db .execute ("UPDATE leveling_settings SET thumbnail_enabled = ? WHERE guild_id = ?",(1 if enabled else 0 ,ctx .guild .id )) + else : + await db .execute ("INSERT INTO leveling_settings (guild_id, thumbnail_enabled) VALUES (?, ?)",(ctx .guild .id ,1 if enabled else 0 )) + + await db .commit () + + embed =discord .Embed ( + title =f"{TICK} Thumbnail Setting Updated", + description =f"User thumbnails in level-up messages: **{'Enabled' if enabled else 'Disabled'}**", + color =0x00FF00 , + timestamp =datetime .now (timezone .utc ) + ) + await ctx .send (embed =embed ) + + except Exception as e : + logger .error (f"Error setting thumbnail: {e}") + pass + + @level .command (name ="cooldown",description ="Set message cooldown in seconds") + @commands .has_permissions (administrator =True ) + async def cooldown (self ,ctx ,seconds :int ): + """Set message cooldown""" + try : + if seconds <0 or seconds >3600 : + await ctx .send (f"{CROSS} Cooldown must be between 0 and 3600 seconds (1 hour)!") + return + + async with aiosqlite .connect (self .db_path )as db : + async with db .execute ("SELECT guild_id FROM leveling_settings WHERE guild_id = ?",(ctx .guild .id ,))as cursor : + exists =await cursor .fetchone () + + if exists : + await db .execute ("UPDATE leveling_settings SET cooldown_seconds = ? WHERE guild_id = ?",(seconds ,ctx .guild .id )) + else : + await db .execute ("INSERT INTO leveling_settings (guild_id, cooldown_seconds) VALUES (?, ?)",(ctx .guild .id ,seconds )) + + await db .commit () + + embed =discord .Embed ( + title =f"{TICK} Cooldown Updated", + description =f"Message cooldown has been set to **{seconds} seconds**", + color =0x00FF00 , + timestamp =datetime .now (timezone .utc ) + ) + await ctx .send (embed =embed ) + + except Exception as e : + logger .error (f"Error setting cooldown: {e}") + pass + + @level .group (name ="rewards",invoke_without_command =True ,description ="Manage level rewards") + async def rewards (self ,ctx ): + """Level rewards management""" + if ctx .invoked_subcommand is None : + await ctx .send_help (ctx .command ) + + @rewards .command (name ="add",description ="Add a level reward") + @commands .has_permissions (administrator =True ) + async def rewards_add (self ,ctx ,level :int ,role :discord .Role ,remove_previous :bool =False ): + """Add level reward""" + try : + if level <=0 : + await ctx .send ("Level must be greater than 0.") + return + + async with aiosqlite .connect ("db/leveling.db")as db : + await db .execute ( + "INSERT OR REPLACE INTO level_rewards (guild_id, level, role_id, remove_previous) VALUES (?, ?, ?, ?)", + (ctx .guild .id ,level ,role .id ,int (remove_previous )) + ) + await db .commit () + + await ctx .send (f"Added reward {role.mention} for level {level} (Remove Previous: {remove_previous})") + except Exception as e : + logger .error (f"Error in rewards add command: {e}") + pass + + @rewards .command (name ="remove",description ="Remove a level reward") + @commands .has_permissions (administrator =True ) + async def rewards_remove (self ,ctx ,level :int ): + """Remove level reward""" + try : + async with aiosqlite .connect ("db/leveling.db")as db : + await db .execute ( + "DELETE FROM level_rewards WHERE guild_id = ? AND level = ?", + (ctx .guild .id ,level ) + ) + await db .commit () + + await ctx .send (f"Removed reward for level {level}") + except Exception as e : + logger .error (f"Error in rewards remove command: {e}") + pass + + @rewards .command (name ="list",description ="List all level rewards") + @commands .has_permissions (administrator =True ) + async def rewards_list (self ,ctx ): + """List level rewards""" + try : + async with aiosqlite .connect ("db/leveling.db")as db : + async with db .execute ( + "SELECT level, role_id, remove_previous FROM level_rewards WHERE guild_id = ? ORDER BY level", + (ctx .guild .id ,) + )as cursor : + rewards =await cursor .fetchall () + + if not rewards : + await ctx .send ("No level rewards configured.") + return + + embed =discord .Embed (title ="Level Rewards",color =0xFF0000 ,timestamp =datetime .now (timezone .utc )) + for level ,role_id ,remove_previous in rewards : + role =ctx .guild .get_role (role_id ) + role_name =role .mention if role else "Role not found" + embed .add_field (name =f"Level {level}",value =f"Role: {role_name} (Remove Previous: {remove_previous})",inline =False ) + + await ctx .send (embed =embed ) + except Exception as e : + logger .error (f"Error in rewards list command: {e}") + pass + + @level .group (name ="multiplier",invoke_without_command =True ,description ="Manage XP multipliers") + async def multiplier (self ,ctx ): + """XP multiplier management""" + if ctx .invoked_subcommand is None : + await ctx .send_help (ctx .command ) + + @multiplier .command (name ="add",description ="Add an XP multiplier") + @commands .has_permissions (administrator =True ) + async def multiplier_add (self ,ctx ,target_type :str ,target :str ,multiplier :float ): + """Add XP multiplier""" + try : + if target_type not in ["role","channel"]: + await ctx .send ("Invalid target type. Must be 'role' or 'channel'.") + return + + if multiplier <=0 : + await ctx .send ("Multiplier must be greater than 0.") + return + + if target_type =="role": + try : + role_id =int (target ) + target_id =ctx .guild .get_role (role_id ).id + except : + await ctx .send ("Invalid role ID.") + return + else : + try : + channel_id =int (target ) + target_id =ctx .guild .get_channel (channel_id ).id + except : + await ctx .send ("Invalid channel ID.") + return + + async with aiosqlite .connect ("db/leveling.db")as db : + await db .execute ( + "INSERT OR REPLACE INTO xp_multipliers (guild_id, target_id, target_type, multiplier) VALUES (?, ?, ?, ?)", + (ctx .guild .id ,target_id ,target_type ,multiplier ) + ) + await db .commit () + + await ctx .send (f"Added {multiplier}x multiplier for {target_type} {target}") + except Exception as e : + logger .error (f"Error in multiplier add command: {e}") + pass + + @multiplier .command (name ="remove",description ="Remove an XP multiplier") + @commands .has_permissions (administrator =True ) + async def multiplier_remove (self ,ctx ,target_type :str ,target :str ): + """Remove XP multiplier""" + try : + if target_type not in ["role","channel"]: + await ctx .send ("Invalid target type. Must be 'role' or 'channel'.") + return + + if target_type =="role": + try : + role_id =int (target ) + target_id =ctx .guild .get_role (role_id ).id + except : + await ctx .send ("Invalid role ID.") + return + else : + try : + channel_id =int (target ) + target_id =ctx .guild .get_channel (channel_id ).id + except : + await ctx .send ("Invalid channel ID.") + return + + async with aiosqlite .connect ("db/leveling.db")as db : + await db .execute ( + "DELETE FROM xp_multipliers WHERE guild_id = ? AND target_id = ? AND target_type = ?", + (ctx .guild .id ,target_id ,target_type ) + ) + await db .commit () + + await ctx .send (f"Removed multiplier for {target_type} {target}") + except Exception as e : + logger .error (f"Error in multiplier remove command: {e}") + pass + + @multiplier .command (name ="list",description ="List all XP multipliers") + @commands .has_permissions (administrator =True ) + async def multiplier_list (self ,ctx ): + """List XP multipliers""" + try : + async with aiosqlite .connect ("db/leveling.db")as db : + async with db .execute ( + "SELECT target_id, target_type, multiplier FROM xp_multipliers WHERE guild_id = ?", + (ctx .guild .id ,) + )as cursor : + multipliers =await cursor .fetchall () + + if not multipliers : + await ctx .send ("No XP multipliers configured.") + return + + embed =discord .Embed (title ="XP Multipliers",color =0xFF0000 ,timestamp =datetime .now (timezone .utc )) + for target_id ,target_type ,multiplier in multipliers : + if target_type =="role": + role =ctx .guild .get_role (target_id ) + target_name =role .mention if role else "Role not found" + else : + channel =ctx .guild .get_channel (target_id ) + target_name =channel .mention if channel else "Channel not found" + embed .add_field (name =f"{target_type.capitalize()}: {target_name}",value =f"Multiplier: {multiplier}x",inline =False ) + + await ctx .send (embed =embed ) + except Exception as e : + logger .error (f"Error in multiplier list command: {e}") + pass + + @level .group (name ="blacklist",invoke_without_command =True ,description ="Manage leveling blacklists") + async def blacklist (self ,ctx ): + """Leveling blacklist management""" + if ctx .invoked_subcommand is None : + await ctx .send_help (ctx .command ) + + @blacklist .command (name ="add",description ="Add to the leveling blacklist") + @commands .has_permissions (administrator =True ) + async def blacklist_add (self ,ctx ,target_type :str ,target :str ): + """Add to leveling blacklist""" + try : + if target_type not in ["role","channel"]: + await ctx .send ("Invalid target type. Must be 'role' or 'channel'.") + return + + if target_type =="role": + try : + role_id =int (target ) + target_id =ctx .guild .get_role (role_id ).id + except : + await ctx .send ("Invalid role ID.") + return + else : + try : + channel_id =int (target ) + target_id =ctx .guild .get_channel (channel_id ).id + except : + await ctx .send ("Invalid channel ID.") + return + + async with aiosqlite .connect ("db/leveling.db")as db : + await db .execute ( + "INSERT OR REPLACE INTO leveling_blacklist (guild_id, target_id, target_type) VALUES (?, ?, ?)", + (ctx .guild .id ,target_id ,target_type ) + ) + await db .commit () + + await ctx .send (f"Added {target_type} {target} to the leveling blacklist.") + except Exception as e : + logger .error (f"Error in blacklist add command: {e}") + pass + + @blacklist .command (name ="remove",description ="Remove from the leveling blacklist") + @commands .has_permissions (administrator =True ) + async def blacklist_remove (self ,ctx ,target_type :str ,target :str ): + """Remove from leveling blacklist""" + try : + if target_type not in ["role","channel"]: + await ctx .send ("Invalid target type. Must be 'role' or 'channel'.") + return + + if target_type =="role": + try : + role_id =int (target ) + target_id =ctx .guild .get_role (role_id ).id + except : + await ctx .send ("Invalid role ID.") + return + else : + try : + channel_id =int (target ) + target_id =ctx .guild .get_channel (channel_id ).id + except : + await ctx .send ("Invalid channel ID.") + return + + async with aiosqlite .connect ("db/leveling.db")as db : + await db .execute ( + "DELETE FROM leveling_blacklist WHERE guild_id = ? AND target_id = ? AND target_type = ?", + (ctx .guild .id ,target_id ,target_type ) + ) + await db .commit () + + await ctx .send (f"Removed {target_type} {target} from the leveling blacklist.") + except Exception as e : + logger .error (f"Error in blacklist remove command: {e}") + pass + + @blacklist .command (name ="list",description ="List the leveling blacklist") + @commands .has_permissions (administrator =True ) + async def blacklist_list (self ,ctx ): + """List leveling blacklist""" + try : + async with aiosqlite .connect ("db/leveling.db")as db : + async with db .execute ( + "SELECT target_id, target_type FROM leveling_blacklist WHERE guild_id = ?", + (ctx .guild .id ,) + )as cursor : + blacklisted =await cursor .fetchall () + + if not blacklisted : + await ctx .send ("The leveling blacklist is empty.") + return + + embed =discord .Embed (title ="Leveling Blacklist",color =0xFF0000 ,timestamp =datetime .now (timezone .utc )) + for target_id ,target_type in blacklisted : + if target_type =="role": + role =ctx .guild .get_role (target_id ) + target_name =role .mention if role else "Role not found" + else : + channel =ctx .guild .get_channel (target_id ) + target_name =channel .mention if channel else "Channel not found" + embed .add_field (name =f"{target_type.capitalize()}: {target_name}",value ="",inline =False ) + + await ctx .send (embed =embed ) + except Exception as e : + logger .error (f"Error in blacklist list command: {e}") + pass + + async def apply_level_roles (self ,guild ,member ,level ): + """Apply level-based roles to a member.""" + try : + async with aiosqlite .connect (self .db_path )as db : + async with db .execute ("SELECT role_id FROM level_roles WHERE guild_id = ? AND level = ?",(guild .id ,level ))as cursor : + result =await cursor .fetchone () + if result : + role_id =result [0 ] + role =guild .get_role (role_id ) + if role : + try : + await member .add_roles (role ,reason ="Reached level {}".format (level )) + logger .info (f"Applied level role {role.name} to {member.name} in {guild.name}") + except discord .Forbidden : + logger .error (f"Missing permissions to add role {role.name} to {member.name} in {guild.name}") + except discord .HTTPException as e : + logger .error (f"Failed to add role {role.name} to {member.name} in {guild.name}: {e}") + else : + logger .warning (f"Role with ID {role_id} not found in {guild.name}") + except Exception as e : + logger .error (f"Error applying level roles: {e}") + + @commands .hybrid_command (name ="setlevelrole",description ="Set a role for a specific level (admin only)") + @commands .has_permissions (administrator =True ) + async def set_level_role (self ,ctx ,level :int ,role :discord .Role ): + """Set a level role.""" + try : + if level <=0 : + await ctx .send ("Level must be greater than 0.") + return + + async with aiosqlite .connect (self .db_path )as db : + await db .execute ("INSERT OR REPLACE INTO level_roles (guild_id, level, role_id) VALUES (?, ?, ?)",(ctx .guild .id ,level ,role .id )) + await db .commit () + + await ctx .send (f"Set role {role.mention} for level {level}.") + except Exception as e : + logger .error (f"Error setting level role: {e}") + pass + + @commands .hybrid_command (name ="removelevelrole",description ="Remove a level role (admin only)") + @commands .has_permissions (administrator =True ) + async def remove_level_role (self ,ctx ,level :int ): + """Remove a level role.""" + try : + async with aiosqlite .connect (self .db_path )as db : + await db .execute ("DELETE FROM level_roles WHERE guild_id = ? AND level = ?",(ctx .guild .id ,level )) + await db .commit () + + await ctx .send (f"Removed role for level {level}.") + except Exception as e : + logger .error (f"Error removing level role: {e}") + pass + + @commands .hybrid_command (name ="listlevelroles",description ="List all level roles (admin only)") + @commands .has_permissions (administrator =True ) + async def list_level_roles (self ,ctx ): + """List all level roles.""" + try : + async with aiosqlite .connect (self .db_path )as db : + async with db .execute ("SELECT level, role_id FROM level_roles WHERE guild_id = ?",(ctx .guild .id ,))as cursor : + roles =await cursor .fetchall () + + if not roles : + await ctx .send ("No level roles set for this server.") + return + + embed =discord .Embed (title ="Level Roles",color =0xFF0000 ,timestamp =datetime .now (timezone .utc )) + for level ,role_id in roles : + role =ctx .guild .get_role (role_id ) + role_name =role .mention if role else "Role not found" + embed .add_field (name =f"Level {level}",value =f"Role: {role_name}",inline =False ) + + await ctx .send (embed =embed ) + except Exception as e : + logger .error (f"Error listing level roles: {e}") + pass + + @commands .hybrid_command (name ="resetxp",description ="Reset a user's XP (admin only)") + @commands .has_permissions (administrator =True ) + async def reset_xp (self ,ctx ,member :discord .Member ): + """Reset a user's XP and level to default values.""" + try : + async with aiosqlite .connect (self .db_path )as db : + await db .execute ("UPDATE users SET xp = 0, level = 1 WHERE guild_id = ? AND user_id = ?",(ctx .guild .id ,member .id )) + await db .commit () + + await ctx .send (f"Successfully reset XP for {member.mention}.") + except Exception as e : + logger .error (f"Error resetting XP: {e}") + pass + + @commands .hybrid_command (name ="setxp",description ="Set a user's XP (admin only)") + @commands .has_permissions (administrator =True ) + async def set_xp (self ,ctx ,member :discord .Member ,xp :int ): + """Set a user's XP to a specific value.""" + try : + level =calculate_level_from_xp (xp ) + async with aiosqlite .connect (self .db_path )as db : + await db .execute ("UPDATE users SET xp = ?, level = ? WHERE guild_id = ? AND user_id = ?",(xp ,level ,ctx .guild .id ,member .id )) + await db .commit () + + await ctx .send (f"Successfully set XP for {member.mention} to {xp:,}.") + except Exception as e : + logger .error (f"Error setting XP: {e}") + pass + + @commands .hybrid_command (name ="setlevel",description ="Set a user's level (admin only)") + @commands .has_permissions (administrator =True ) + async def set_level (self ,ctx ,member :discord .Member ,level :int ): + """Set a user's level to a specific value.""" + try : + xp =calculate_xp_for_level (level ) + async with aiosqlite .connect (self .db_path )as db : + await db .execute ("UPDATE users SET xp = ?, level = ? WHERE guild_id = ? AND user_id = ?",(xp ,level ,ctx .guild .id ,member .id )) + await db .commit () + + await ctx .send (f"Successfully set level for {member.mention} to {level:,}.") + except Exception as e : + logger .error (f"Error setting level: {e}") + pass + + @level .command (name ="leaderboard",description ="View the server level leaderboard") + @commands .cooldown (1 ,10 ,commands .BucketType .guild ) + async def level_leaderboard (self ,ctx ): + """Display server level leaderboard""" + try : + if isinstance (ctx ,discord .Interaction ): + await ctx .response .defer () + + async with aiosqlite .connect ("db/leveling.db")as db : + + cursor =await db .execute (''' + SELECT user_id, xp, messages FROM user_xp + WHERE guild_id = ? + ORDER BY xp DESC + LIMIT 10 + ''',(ctx .guild .id ,)) + + top_users =await cursor .fetchall () + + if not top_users : + embed =discord .Embed ( + title ="📊 Server Level Leaderboard", + description ="No users found in the leaderboard yet!", + color =0xFF0000 + ) + if isinstance (ctx ,discord .Interaction ): + await ctx .followup .send (embed =embed ) + else : + await ctx .send (embed =embed ) + return + + + leaderboard_image =await self .create_leaderboard_image (ctx .guild ,top_users ) + + if leaderboard_image : + if isinstance (leaderboard_image ,str ): + + if isinstance (ctx ,discord .Interaction ): + await ctx .followup .send (leaderboard_image ) + else : + await ctx .send (leaderboard_image ) + else : + + file =discord .File (fp =leaderboard_image ,filename ="level_leaderboard.png") + if isinstance (ctx ,discord .Interaction ): + await ctx .followup .send (file =file ) + else : + await ctx .send (file =file ) + try : + leaderboard_image .close () + except : + pass + else : + if isinstance (ctx ,discord .Interaction ): + await ctx .followup .send ("Failed to generate leaderboard.") + else : + pass + + except Exception as e : + logger .error (f"Error in level leaderboard command: {e}") + error_msg =f"An error occurred: {e}" + if isinstance (ctx ,discord .Interaction ): + await ctx .followup .send (error_msg ) + else : + await ctx .send (error_msg ) + + async def create_leaderboard_image (self ,guild ,top_users ): + """Create enhanced visual leaderboard image with custom background support and modern design""" + try : + if not PIL_AVAILABLE : + return self .create_text_leaderboard (guild ,top_users ) + + + width ,height =1920 ,1080 + + + background_loaded =False + background_extensions =['.jpg','.jpeg','.png','.gif','.webp'] + + for ext in background_extensions : + try : + bg_path =f"/home/container/assets/leaderboardlevel{ext}" + if os .path .exists (bg_path ): + temp_bg =Image .open (bg_path ) + bg_width ,bg_height =temp_bg .size + + + ratio =bg_width /bg_height + + if 1.7 <=ratio <=1.8 : + width ,height =1920 ,1080 + elif 1.3 <=ratio <=1.35 : + width ,height =1600 ,1200 + elif 0.95 <=ratio <=1.05 : + width ,height =1200 ,1200 + elif 0.7 <=ratio <=0.85 : + width ,height =1200 ,1600 + else : + + if bg_width >2000 or bg_height >1500 : + scale =min (2000 /bg_width ,1500 /bg_height ) + width =int (bg_width *scale ) + height =int (bg_height *scale ) + else : + width ,height =bg_width ,bg_height + + temp_bg .close () + break + except Exception as e : + logger .error (f"Failed to analyze background {bg_path}: {e}") + continue + + + for ext in background_extensions : + try : + bg_path =f"/home/container/assets/leaderboardlevel{ext}" + if os .path .exists (bg_path ): + background_img =Image .open (bg_path ) + + if hasattr (background_img ,'is_animated')and background_img .is_animated : + background_img =background_img .convert ('RGBA') + + background_img =background_img .resize ((width ,height ),Image .Resampling .LANCZOS ) + img =background_img .convert ('RGB') + background_loaded =True + logger .info (f"Loaded custom background: {bg_path} - Resized to {width}x{height}") + break + except Exception as e : + logger .error (f"Failed to load background {bg_path}: {e}") + continue + + + if not background_loaded : + img =Image .new ('RGB',(width ,height ),(15 ,15 ,25 )) + draw_temp =ImageDraw .Draw (img ) + + for y in range (height ): + factor =y /height + r =int (15 +(45 -15 )*factor +10 *math .sin (factor *math .pi *2 )) + g =int (15 +(55 -15 )*factor +15 *math .sin (factor *math .pi *3 )) + b =int (25 +(85 -25 )*factor +20 *math .sin (factor *math .pi *4 )) + draw_temp .line ([(0 ,y ),(width ,y )],fill =(min (255 ,max (0 ,r )),min (255 ,max (0 ,g )),min (255 ,max (0 ,b )))) + + draw =ImageDraw .Draw (img ) + + + try : + title_font =ImageFont .truetype ("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",54 ) + name_font =ImageFont .truetype ("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",32 ) + stats_font =ImageFont .truetype ("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",22 ) + rank_font =ImageFont .truetype ("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",40 ) + small_font =ImageFont .truetype ("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",19 ) + except (OSError ,IOError ): + title_font =ImageFont .load_default () + name_font =ImageFont .load_default () + stats_font =ImageFont .load_default () + rank_font =ImageFont .load_default () + small_font =ImageFont .load_default () + + + title =f"🏆 {guild.name} Level Leaderboard" + title_bbox =draw .textbbox ((0 ,0 ),title ,font =title_font ) + title_width =title_bbox [2 ]-title_bbox [0 ] + title_x =(width -title_width )//2 + + + title_bg_margin =50 + title_y =25 + title_height =90 + + + draw .rounded_rectangle ( + (title_x -title_bg_margin ,title_y ,title_x +title_width +title_bg_margin ,title_y +title_height ), + radius =25 ,fill =(0 ,0 ,0 ,160 ) + ) + draw .rounded_rectangle ( + (title_x -title_bg_margin ,title_y ,title_x +title_width +title_bg_margin ,title_y +title_height ), + radius =25 ,outline =(255 ,215 ,0 ,200 ),width =3 + ) + + + for offset in [(5 ,5 ),(4 ,4 ),(3 ,3 ),(2 ,2 ),(1 ,1 )]: + alpha =100 -(offset [0 ]*15 ) + draw .text ((title_x +offset [0 ],title_y +20 +offset [1 ]),title ,font =title_font ,fill =(0 ,0 ,0 ,alpha )) + draw .text ((title_x ,title_y +20 ),title ,font =title_font ,fill =(255 ,215 ,0 )) + + + start_y =180 + entry_height =120 + margin_left =100 + margin_right =100 + + for i ,(user_id ,xp ,messages )in enumerate (top_users ): + user =guild .get_member (user_id ) + if not user : + continue + + y =start_y +(i *entry_height ) + level =calculate_level_from_xp (xp ) + + + if i ==0 : + rank_color =(255 ,215 ,0 ) + bg_color =(40 ,35 ,15 ) + border_color =(255 ,215 ,0 ) + accent_color =(255 ,235 ,100 ) + elif i ==1 : + rank_color =(192 ,192 ,192 ) + bg_color =(35 ,35 ,40 ) + border_color =(192 ,192 ,192 ) + accent_color =(220 ,220 ,220 ) + elif i ==2 : + rank_color =(205 ,127 ,50 ) + bg_color =(40 ,30 ,20 ) + border_color =(205 ,127 ,50 ) + accent_color =(230 ,150 ,80 ) + else : + rank_color =(120 ,180 ,255 ) + bg_color =(25 ,30 ,40 ) + border_color =(80 ,120 ,160 ) + accent_color =(150 ,200 ,255 ) + + + entry_bg_y =y -15 + entry_bg_height =entry_height -10 + + + draw .rounded_rectangle ( + (margin_left ,entry_bg_y ,width -margin_right ,entry_bg_y +entry_bg_height ), + radius =20 ,fill =(0 ,0 ,0 ,180 ) + ) + + + for border_layer in range (4 ): + border_alpha =180 -(border_layer *30 ) + draw .rounded_rectangle ( + (margin_left -border_layer ,entry_bg_y -border_layer , + width -margin_right +border_layer ,entry_bg_y +entry_bg_height +border_layer ), + radius =20 +border_layer ,outline =(*border_color ,border_alpha ),width =2 + ) + + + rank_x =margin_left +40 + rank_text =f"#{i + 1}" + + + rank_circle_size =55 + rank_circle_y =y +20 + draw .ellipse ( + (rank_x -5 ,rank_circle_y ,rank_x +rank_circle_size ,rank_circle_y +rank_circle_size ), + fill =(*rank_color ,60 ) + ) + draw .ellipse ( + (rank_x -5 ,rank_circle_y ,rank_x +rank_circle_size ,rank_circle_y +rank_circle_size ), + outline =rank_color ,width =3 + ) + + + rank_bbox =draw .textbbox ((0 ,0 ),rank_text ,font =rank_font ) + rank_text_width =rank_bbox [2 ]-rank_bbox [0 ] + rank_text_height =rank_bbox [3 ]-rank_bbox [1 ] + rank_text_x =rank_x +(rank_circle_size -rank_text_width )//2 -5 + rank_text_y =rank_circle_y +(rank_circle_size -rank_text_height )//2 + + + draw .text ((rank_text_x +3 ,rank_text_y +3 ),rank_text ,font =rank_font ,fill =(0 ,0 ,0 ,200 )) + draw .text ((rank_text_x ,rank_text_y ),rank_text ,font =rank_font ,fill =rank_color ) + + + avatar_x =margin_left +140 + avatar_y =y +20 + avatar_size =70 + + + for border_layer in range (3 ): + border_size =avatar_size +(border_layer *6 ) + border_x =avatar_x -(border_layer *3 ) + border_y =avatar_y -(border_layer *3 ) + alpha =200 -(border_layer *40 ) + + draw .ellipse ( + (border_x ,border_y ,border_x +border_size ,border_y +border_size ), + outline =(*rank_color ,alpha ),width =3 + ) + + + try : + avatar_url =str (user .display_avatar .with_size (512 ).url ) + response =requests .get (avatar_url ,timeout =10 ) + if response .status_code ==200 : + avatar_img =Image .open (io .BytesIO (response .content )) + avatar_img =avatar_img .resize ((avatar_size ,avatar_size ),Image .Resampling .LANCZOS ) + + + mask =Image .new ('L',(avatar_size ,avatar_size ),0 ) + mask_draw =ImageDraw .Draw (mask ) + mask_draw .ellipse ((0 ,0 ,avatar_size ,avatar_size ),fill =255 ) + + + avatar_img .putalpha (mask ) + img .paste (avatar_img ,(avatar_x ,avatar_y ),avatar_img ) + else : + raise Exception ("Failed to download avatar") + except Exception as e : + + draw .ellipse ((avatar_x ,avatar_y ,avatar_x +avatar_size ,avatar_y +avatar_size ), + fill =(40 ,40 ,50 )) + + + emoji ="👤" + emoji_bbox =draw .textbbox ((0 ,0 ),emoji ,font =name_font ) + emoji_width =emoji_bbox [2 ]-emoji_bbox [0 ] + emoji_height =emoji_bbox [3 ]-emoji_bbox [1 ] + emoji_x =avatar_x +(avatar_size -emoji_width )//2 + emoji_y =avatar_y +(avatar_size -emoji_height )//2 + draw .text ((emoji_x ,emoji_y ),emoji ,font =name_font ,fill =accent_color ) + + + info_x =margin_left +240 + username =user .display_name [:18 ] + + + username_y =y +15 + for shadow in [(4 ,4 ),(3 ,3 ),(2 ,2 ),(1 ,1 )]: + draw .text ((info_x +shadow [0 ],username_y +shadow [1 ]),username ,font =name_font ,fill =(0 ,0 ,0 ,150 )) + draw .text ((info_x ,username_y ),username ,font =name_font ,fill =(255 ,255 ,255 )) + + + level_text =f"Level {level:,}" + level_badge_x =info_x + level_badge_y =y +55 + level_bbox =draw .textbbox ((0 ,0 ),level_text ,font =stats_font ) + level_width =level_bbox [2 ]-level_bbox [0 ] + + + draw .rounded_rectangle ( + (level_badge_x -8 ,level_badge_y -5 ,level_badge_x +level_width +16 ,level_badge_y +25 ), + radius =12 ,fill =(*rank_color ,70 ) + ) + draw .rounded_rectangle ( + (level_badge_x -8 ,level_badge_y -5 ,level_badge_x +level_width +16 ,level_badge_y +25 ), + radius =12 ,outline =rank_color ,width =2 + ) + draw .text ((level_badge_x ,level_badge_y ),level_text ,font =stats_font ,fill =(255 ,255 ,255 )) + + + info_spacing =30 + xp_text =f"💎 {format_number(xp)} XP" + msg_text =f"💬 {format_number(messages or 0)} msgs" + + xp_x =info_x +level_width +info_spacing + msg_x =xp_x +140 + + + stats_bg_y =level_badge_y -2 + stats_bg_height =24 + draw .rounded_rectangle ( + (xp_x -5 ,stats_bg_y ,msg_x +150 ,stats_bg_y +stats_bg_height ), + radius =8 ,fill =(0 ,0 ,0 ,100 ) + ) + + draw .text ((xp_x ,level_badge_y ),xp_text ,font =stats_font ,fill =(200 ,220 ,255 )) + draw .text ((msg_x ,level_badge_y ),msg_text ,font =stats_font ,fill =(255 ,200 ,150 )) + + + bar_x =width -margin_right -380 + bar_y =y +25 + bar_width =320 + bar_height =28 + + + draw .rounded_rectangle ((bar_x ,bar_y ,bar_x +bar_width ,bar_y +bar_height ), + radius =14 ,fill =(20 ,20 ,30 ,200 )) + draw .rounded_rectangle ((bar_x ,bar_y ,bar_x +bar_width ,bar_y +bar_height ), + radius =14 ,outline =(80 ,80 ,100 ),width =2 ) + + + current_level ,progress ,needed =get_level_progress (xp ) + if needed >0 : + progress_width =int ((progress /needed )*(bar_width -6 )) + if progress_width >0 : + + for px in range (progress_width ): + factor =px /progress_width if progress_width >0 else 0 + + + base_color =rank_color + brightness =0.4 +0.6 *factor + + fill_r =min (255 ,int (base_color [0 ]*brightness )) + fill_g =min (255 ,int (base_color [1 ]*brightness )) + fill_b =min (255 ,int (base_color [2 ]*brightness )) + + draw .line ([(bar_x +3 +px ,bar_y +3 ),(bar_x +3 +px ,bar_y +bar_height -3 )], + fill =(fill_r ,fill_g ,fill_b )) + + + draw .rounded_rectangle ( + (bar_x +2 ,bar_y +2 ,bar_x +3 +progress_width ,bar_y +bar_height -2 ), + radius =12 ,outline =rank_color ,width =2 + ) + + + percentage =(progress /needed *100 )if needed >0 else 100 + progress_text =f"{percentage:.1f}% to Level {level + 1}" + progress_bbox =draw .textbbox ((0 ,0 ),progress_text ,font =small_font ) + progress_text_width =progress_bbox [2 ]-progress_bbox [0 ] + progress_text_x =bar_x +(bar_width -progress_text_width )//2 + + + text_bg_y =bar_y +bar_height +8 + draw .rounded_rectangle ( + (progress_text_x -12 ,text_bg_y -2 ,progress_text_x +progress_text_width +12 ,text_bg_y +18 ), + radius =8 ,fill =(0 ,0 ,0 ,180 ) + ) + draw .text ((progress_text_x ,text_bg_y ),progress_text ,font =small_font ,fill =(255 ,255 ,255 )) + + + xp_progress_text =f"{format_number(progress)} / {format_number(needed)} XP" + xp_text_bbox =draw .textbbox ((0 ,0 ),xp_progress_text ,font =small_font ) + xp_text_width =xp_text_bbox [2 ]-xp_text_bbox [0 ] + xp_text_x =bar_x +(bar_width -xp_text_width )//2 + + draw .text ((xp_text_x ,bar_y +6 ),xp_progress_text ,font =small_font ,fill =(200 ,200 ,200 )) + + + footer_y =height -80 + footer_bg_height =50 + + + draw .rounded_rectangle ( + (60 ,footer_y ,width -60 ,footer_y +footer_bg_height ), + radius =15 ,fill =(0 ,0 ,0 ,150 ) + ) + draw .rounded_rectangle ( + (60 ,footer_y ,width -60 ,footer_y +footer_bg_height ), + radius =15 ,outline =(100 ,100 ,120 ),width =2 + ) + + footer_text =f"✨ Generated at {datetime.now(timezone.utc).strftime('%H:%M UTC')} • {len(top_users)} Active Members • {guild.name}" + footer_bbox =draw .textbbox ((0 ,0 ),footer_text ,font =small_font ) + footer_width =footer_bbox [2 ]-footer_bbox [0 ] + footer_x =(width -footer_width )//2 + + + draw .text ((footer_x +2 ,footer_y +17 ),footer_text ,font =small_font ,fill =(0 ,0 ,0 ,200 )) + draw .text ((footer_x ,footer_y +15 ),footer_text ,font =small_font ,fill =(220 ,220 ,240 )) + + + img_bytes =io .BytesIO () + img .save (img_bytes ,format ='PNG') + img_bytes .seek (0 ) + img .close () + return img_bytes + + except Exception as e : + logger .error (f"Error creating leaderboard image: {e}") + return self .create_text_leaderboard (guild ,top_users ) + + def create_text_leaderboard (self ,guild ,top_users ): + """Create text-based leaderboard when image creation fails""" + try : + leaderboard_text =f"🏆 **{guild.name} Level Leaderboard** 🏆\n\n" + + for i ,(user_id ,xp ,messages )in enumerate (top_users ): + user =guild .get_member (user_id ) + if not user : + continue + + level =calculate_level_from_xp (xp ) + + if i ==0 : + emoji ="🥇" + elif i ==1 : + emoji ="🥈" + elif i ==2 : + emoji ="🥉" + else : + emoji =f"#{i + 1}" + + leaderboard_text +=f"{emoji} **{user.display_name}**\n" + leaderboard_text +=f" Level {level:,} • {format_number(xp)} XP • {format_number(messages or 0)} messages\n\n" + + return leaderboard_text + + except Exception as e : + logger .error (f"Error creating text leaderboard: {e}") + return "Failed to generate leaderboard." + + @level .command (name ="placeholders",description ="Show available placeholders for level-up messages") + async def placeholders (self ,ctx ): + """Show placeholders for level-up messages""" + embed =discord .Embed ( + title ="📝 Available Placeholders", + description =( + "**You can use these placeholders in your level-up message:**\n\n" + "`{user}` - Mentions the user (@username)\n" + "`{username}` - User's display name\n" + "`{level}` - The new level reached\n" + "`{server}` - Server name\n\n" + "**Example:**\n" + "`Congratulations {user}! You've reached level {level} in {server}!`" + ), + color =0x00BFFF , + timestamp =datetime .now (timezone .utc ) + ) + await ctx .send (embed =embed ) + + @level .command (name ="enable",description ="Enable the leveling system") + @commands .has_permissions (administrator =True ) + async def enable (self ,ctx ): + try : + async with aiosqlite .connect (self .db_path )as db : + + async with db .execute ("SELECT enabled FROM leveling_settings WHERE guild_id = ?",(ctx .guild .id ,))as cursor : + result =await cursor .fetchone () + + if result : + if result [0 ]==1 : + await ctx .send (f"{ZWARNING} Leveling system is already enabled!") + return + + await db .execute ("UPDATE leveling_settings SET enabled = 1 WHERE guild_id = ?",(ctx .guild .id ,)) + else : + + await db .execute ("INSERT INTO leveling_settings (guild_id, enabled) VALUES (?, 1)",(ctx .guild .id ,)) + + await db .commit () + + embed =discord .Embed ( + title =f"{TICK} Leveling System Enabled", + description ="The leveling system has been successfully enabled for this server!\n\n" + "**Next Steps:**\n" + "• Use `/level settings` to configure XP rates and messages\n" + "• Use `/level channel` to set level-up announcement channel\n" + "• Use `/level rewards add` to set up level rewards", + color =0x00FF00 , + timestamp =datetime .now (timezone .utc ) + ) + await ctx .send (embed =embed ) + + except Exception as e : + logger .error (f"Error enabling leveling system: {e}") + pass + + @level .command (name ="disable",description ="Disable the leveling system") + @commands .has_permissions (administrator =True ) + async def disable (self ,ctx ): + try : + async with aiosqlite .connect (self .db_path )as db : + + async with db .execute ("SELECT enabled FROM leveling_settings WHERE guild_id = ?",(ctx .guild .id ,))as cursor : + result =await cursor .fetchone () + + if not result : + await ctx .send (f"{ZWARNING} Leveling system is not configured for this server!") + return + + if result [0 ]==0 : + await ctx .send (f"{ZWARNING} Leveling system is already disabled!") + return + + await db .execute ("UPDATE leveling_settings SET enabled = 0 WHERE guild_id = ?",(ctx .guild .id ,)) + await db .commit () + + embed =discord .Embed ( + title =f"{CROSS} Leveling System Disabled", + description ="The leveling system has been disabled for this server.\n\n" + "**What this means:**\n" + "• Users will no longer gain XP from messages\n" + "• Level-up notifications will stop\n" + "• Existing user data is preserved\n" + "• Use `/level enable` to re-enable anytime", + color =0xFF0000 , + timestamp =datetime .now (timezone .utc ) + ) + await ctx .send (embed =embed ) + + except Exception as e : + logger .error (f"Error disabling leveling system: {e}") + pass + + @level .command (name ="stats",description ="View detailed level statistics") + async def stats (self ,ctx ,member :Optional [discord .Member ]=None ): + """Display detailed user level statistics with rank card""" + member =member or ctx .author + guild_id =ctx .guild .id + + try : + + xp ,level ,messages =await self .get_user_data (guild_id ,member .id ) + rank =await self .get_user_rank (guild_id ,member .id ) + + + current_level ,progress ,needed =get_level_progress (xp ) + next_level =level +1 + percentage =(progress /needed *100 )if needed >0 else 100 + + + async with aiosqlite .connect ("db/leveling.db")as db : + async with db .execute ( + "SELECT COUNT(*) FROM user_xp WHERE guild_id = ? AND xp > 0", + (guild_id ,) + )as cursor : + total_members =(await cursor .fetchone ())[0 ] + + + avg_xp_per_message =(xp /messages )if messages >0 else 0 + xp_to_next_level =needed -progress + + + embed =discord .Embed ( + title =f"📊 Level Statistics for {member.display_name}", + color =0xFF0000 , + timestamp =datetime .now (timezone .utc ) + ) + + + embed .add_field ( + name ="🎯 Basic Stats", + value =f"**Level:** {level:,}\n" + f"**Total XP:** {format_number(xp)}\n" + f"**Server Rank:** #{rank:,} / {total_members:,}\n" + f"**Messages Sent:** {format_number(messages)}", + inline =True + ) + + + embed .add_field ( + name ="📈 Progress to Level {next_level}", + value =f"**Current Progress:** {format_number(progress)} / {format_number(needed)} XP\n" + f"**Percentage:** {percentage:.1f}%\n" + f"**XP Needed:** {format_number(xp_to_next_level)}\n" + f"**Progress Bar:** {get_progress_bar(progress, needed, 15)}", + inline =True + ) + + + embed .add_field ( + name ="📊 Additional Stats", + value =f"**Avg XP/Message:** {avg_xp_per_message:.1f}\n" + f"**XP for Level {level}:** {format_number(calculate_xp_for_level(level))}\n" + f"**XP for Level {next_level}:** {format_number(calculate_xp_for_level(next_level))}\n" + f"**Total Levels Gained:** {level - 1:,}", + inline =True + ) + + + percentile =((total_members -rank +1 )/total_members *100 )if total_members >0 else 0 + + + if rank ==1 : + tier ="🏆 Champion" + tier_color =0xFFD700 + elif rank <=3 : + tier ="🥇 Elite" + tier_color =0xC0C0C0 + elif rank <=10 : + tier ="⭐ Expert" + tier_color =0xCD7F32 + elif percentile >=90 : + tier ="💎 Advanced" + tier_color =0x00FFFF + elif percentile >=70 : + tier ="🔥 Experienced" + tier_color =0xFF4500 + elif percentile >=50 : + tier ="⚡ Intermediate" + tier_color =0xFFFF00 + elif percentile >=25 : + tier ="🌟 Beginner" + tier_color =0x90EE90 + else : + tier ="🌱 Newcomer" + tier_color =0x98FB98 + + embed .add_field ( + name ="🏅 Rank Information", + value =f"**Tier:** {tier}\n" + f"**Percentile:** Top {100-percentile:.1f}%\n" + f"**Users Below:** {total_members - rank:,}\n" + f"**Users Above:** {rank - 1:,}", + inline =False + ) + + + embed .color =tier_color + + + embed .set_thumbnail (url =member .display_avatar .url ) + + + embed .set_footer ( + text =f"Requested by {ctx.author.display_name} • {ctx.guild.name}", + icon_url =ctx .author .display_avatar .url + ) + + + rank_card =await self .create_rank_card (member ,guild_id ) + + if rank_card and not isinstance (rank_card ,str ): + + file =discord .File (fp =rank_card ,filename ="rank_card.png") + embed .set_image (url ="attachment://rank_card.png") + await ctx .send (embed =embed ,file =file ) + try : + rank_card .close () + except : + pass + else : + + await ctx .send (embed =embed ) + if isinstance (rank_card ,str ): + + await ctx .send (f"```{rank_card}```") + + except Exception as e : + logger .error (f"Error in level stats command: {e}") + pass + + @level .command (name ="channel",description ="Set the level-up announcement channel") + @commands .has_permissions (administrator =True ) + async def channel (self ,ctx ,channel :discord .TextChannel ): + try : + async with aiosqlite .connect (self .db_path )as db : + + async with db .execute ("SELECT guild_id FROM leveling_settings WHERE guild_id = ?",(ctx .guild .id ,))as cursor : + result =await cursor .fetchone () + + if result : + await db .execute ("UPDATE leveling_settings SET channel_id = ? WHERE guild_id = ?",(channel .id ,ctx .guild .id )) + else : + await db .execute ("INSERT INTO leveling_settings (guild_id, channel_id) VALUES (?, ?)",(ctx .guild .id ,channel .id )) + + await db .commit () + + embed =discord .Embed ( + title ="📢 Level-Up Channel Set", + description =f"Level-up messages will now be sent in {channel.mention}\n\n" + "**Note:** Make sure the bot has permission to send messages in this channel!", + color =0x00BFFF , + timestamp =datetime .now (timezone .utc ) + ) + await ctx .send (embed =embed ) + + except Exception as e : + logger .error (f"Error setting level channel: {e}") + pass + diff --git a/bot/cogs/commands/leveling_original.py b/bot/cogs/commands/leveling_original.py new file mode 100644 index 0000000..f08d359 Binary files /dev/null and b/bot/cogs/commands/leveling_original.py differ diff --git a/bot/cogs/commands/logging.py b/bot/cogs/commands/logging.py new file mode 100644 index 0000000..157b5e8 --- /dev/null +++ b/bot/cogs/commands/logging.py @@ -0,0 +1,2999 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +from discord.ui import View, Select, Button +import logging +import time +import asyncio +import aiofiles +import json +from typing import List, Optional, Dict, Any, Union +from datetime import datetime, timedelta +from pathlib import Path +import os +import re + + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +LOG_CATEGORIES = [ + "message_events", + "join_leave_events", + "member_moderation", + "voice_events", + "channel_events", + "role_events", + "emoji_events", + "reaction_events", + "system_events", +] + +CONFIG_FILE = "jsondb/logging_config.json" +LOGS_DIR = "logs" +MAX_AUDIT_CACHE_SIZE = 1000 +AUDIT_CACHE_TTL = 300 +MAX_LOG_FILE_SIZE = 10 * 1024 * 1024 +MAX_SEARCH_RESULTS = 100 + + +Path(LOGS_DIR).mkdir(exist_ok=True) + +from utils.cv2 import CV2, build_container +from discord.ui import TextDisplay, Separator, ActionRow, LayoutView, Container, Select + + +class LogSetupLayoutView(LayoutView): + def __init__(self, bot, author, categories): + super().__init__(timeout=300) + self.bot = bot + self.author = author + self.categories = categories + self.selected_channels = {cat: None for cat in LOG_CATEGORIES} + + self.select = Select( + placeholder="Choose a logging category to configure...", + options=[ + discord.SelectOption( + label=cat.replace("_", " ").title(), + value=cat, + description=f"Configure {cat.replace('_', ' ').lower()} logging", + ) + for cat in LOG_CATEGORIES + ], + ) + self.select.callback = self.category_select_callback + + self.add_item( + Container( + TextDisplay("**Logging Setup**"), + Separator(visible=True), + TextDisplay( + f"Configure logging channels for different event categories.\n\nGuild ID: {author.guild.id}" + ), + ActionRow(self.select), + ) + ) + + async def category_select_callback(self, interaction: discord.Interaction): + if interaction.user != self.author: + await interaction.response.send_message( + "Only the command author can interact.", ephemeral=True + ) + return + + category = interaction.data.get("values", [None])[0] + if not category: + return + + cog = self.bot.get_cog("Logging") + if not cog: + await interaction.response.send_message( + "Logging cog not found.", ephemeral=True + ) + return + + channel_view = ChannelSelectView(self.bot, self.author, category, None, None) + + layout_view = CV2( + f"Select Channel for {category.replace('_', ' ').title()}", + f"Choose where to log {category.replace('_', ' ').lower()} events.\n\nPage 1 of {channel_view.total_pages}", + ) + + for container in layout_view.children: + if type(container).__name__ == "Container": + if channel_view.children: + add_action_rows(container, channel_view.children) + break + + await interaction.response.edit_message(view=layout_view) + + @discord.ui.button(label="Finish Setup", style=discord.ButtonStyle.success) + async def finish_button(self, interaction: discord.Interaction, button: Button): + await self.finish_setup(interaction) + + async def finish_setup(self, interaction: discord.Interaction): + try: + log_enabled = { + cat: bool(self.selected_channels.get(cat)) for cat in LOG_CATEGORIES + } + + cog = self.bot.get_cog("Logging") + if not cog: + await interaction.response.send_message( + "Logging cog not found.", ephemeral=True + ) + return + + await cog._save_log_config( + interaction.guild.id, + self.selected_channels, + log_enabled, + [], + [], + [], + None, + ) + + configured_count = sum( + 1 for ch in self.selected_channels.values() if ch is not None + ) + layout_view = CV2( + "Setup Complete", + f"Logging has been configured successfully.\n\n**Configuration Summary**\nConfigured {configured_count} out of {len(LOG_CATEGORIES)} categories", + ) + + for item in self.children: + if hasattr(item, "disabled"): + item.disabled = True + + await interaction.response.edit_message(view=layout_view) + except Exception as e: + logger.error(f"Error finishing setup: {e}") + pass + + +class CV2EmbedAdapter(CV2): + def __init__(self, t, d="", **kwargs): + self._title = t + self._description = d or "" + self._fields = [] + self._footer = None + super().__init__(t, self._description) + self.color = kwargs.get("color", 0xFF0000) + + def _rebuild(self): + self.clear_items() + sections = [self._description] if self._description else [] + for name, value in self._fields: + sections.append(f"**{name}**\n{value}") + + if self._footer: + sections.append(f"*{self._footer}*") + + container = build_container( + TextDisplay(f"**{self._title}**"), + *[ + item + for s in sections + for item in (Separator(visible=True), TextDisplay(str(s))) + ], + ) + self.add_item(container) + + def add_field(self, name, value, inline=False, **kwargs): + self._fields.append((name, value)) + self._rebuild() + return self + + def set_footer(self, text=None, icon_url=None, **kwargs): + if text: + self._footer = text + self._rebuild() + return self + + def set_thumbnail(self, url=None, **kwargs): + return self + + def set_author(self, name=None, url=None, icon_url=None, **kwargs): + return self + + def set_image(self, url=None, **kwargs): + return self + + def to_dict(self): + return {"title": self._title, "description": self._description} + + +def add_action_rows(container, components): + from discord.ui import ActionRow + + current_row = [] + for item in components: + # Check if it's a select menu or any other full-width component. Select Menu is type 3. + # ChannelSelect is 8, RoleSelect is 6, UserSelect is 5, MentionableSelect is 7, Text Input is 4. + # Generally, anything other than Button (type 2) takes a full row. + if getattr(item, "type", None) and item.type.value != 2: + if current_row: + container.add_item(ActionRow(*current_row)) + current_row = [] + container.add_item(ActionRow(item)) + else: + current_row.append(item) + if len(current_row) == 5: + container.add_item(ActionRow(*current_row)) + current_row = [] + if current_row: + container.add_item(ActionRow(*current_row)) + + +class LogSetupView(View): + """Enhanced view for configuring logging settings with improved UI.""" + + def __init__(self, bot: commands.Bot, author: discord.Member, timeout: float = 300): + super().__init__(timeout=timeout) + self.bot = bot + self.author = author + self.selected_channels: Dict[str, Optional[int]] = { + cat: None for cat in LOG_CATEGORIES + } + self.message: Optional[discord.Message] = None + self.current_step = 0 + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + """Ensure only the command author can interact.""" + if interaction.user != self.author: + try: + if not interaction.response.is_done(): + await interaction.response.send_message( + "Only the command author can interact with this menu.", + ephemeral=True, + ) + except (discord.NotFound, discord.HTTPException): + pass + return False + return True + + async def on_timeout(self): + """Handle view timeout with proper cleanup.""" + if self.message: + try: + for item in self.children: + item.disabled = True + await self.message.edit(view=self) + logger.info( + f"Log setup timed out for {self.author} in {self.author.guild.name}" + ) + except (discord.NotFound, discord.HTTPException): + try: + await self.message.delete() + except: + pass + + @discord.ui.select(placeholder="Choose a logging category to configure...") + async def category_select(self, interaction: discord.Interaction, select: Select): + """Handle category selection in setup.""" + try: + if select.values[0] == "finish": + await self.finish_setup(interaction) + return + + category = select.values[0] + guild = interaction.guild + + channel_view = ChannelSelectView(self.bot, self.author, category, self) + + from utils.cv2 import CV2 + from discord.ui import ActionRow + + layout_view = CV2( + f"Select Channel for {category.replace('_', ' ').title()}", + f"Choose where to log {category.replace('_', ' ').lower()} events.\n\n**Page 1 of {channel_view.total_pages}** - Showing {len(channel_view.all_channels[: channel_view.channels_per_page])} channels", + ) + for container in layout_view.children: + if type(container).__name__ == "Container": + if channel_view.children: + add_action_rows(container, channel_view.children) + break + await interaction.response.edit_message(embed=None, view=layout_view) + except Exception as e: + logger.error(f"Error in category selection: {e}") + pass + + @discord.ui.button(label="Finish Setup", style=discord.ButtonStyle.success) + async def finish_button(self, interaction: discord.Interaction, button: Button): + """Finish the setup process.""" + await self.finish_setup(interaction) + + async def finish_setup(self, interaction: discord.Interaction): + """Complete the setup process.""" + try: + log_enabled = { + cat: bool(self.selected_channels.get(cat)) for cat in LOG_CATEGORIES + } + + cog = self.bot.get_cog("Logging") + if not cog: + await interaction.response.send_message( + "Logging cog not found.", ephemeral=True + ) + return + + await cog._save_log_config( + interaction.guild.id, + self.selected_channels, + log_enabled, + [], + [], + [], + None, + ) + + from utils.cv2 import CV2 + from discord.ui import ActionRow + + configured_count = sum( + 1 for ch in self.selected_channels.values() if ch is not None + ) + layout_view = CV2( + "Setup Complete", + "Logging has been configured successfully.", + f"**Configuration Summary**\nConfigured {configured_count} out of {len(LOG_CATEGORIES)} categories\n\n*Use /log status to view your configuration*", + ) + + for item in self.children: + item.disabled = True + + for container in layout_view.children: + if type(container).__name__ == "Container": + if self.children: + add_action_rows(container, self.children) + break + await interaction.response.edit_message(embed=None, view=layout_view) + + except Exception as e: + logger.error(f"Error finishing setup: {e}") + try: + pass + except: + pass + + +class InteractiveConfigView(View): + """Fully interactive configuration view for all logging settings.""" + + def __init__(self, bot: commands.Bot, author: discord.Member, config: Dict): + super().__init__(timeout=300) + self.bot = bot + self.author = author + self.config = config + self.message: Optional[discord.Message] = None + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + """Ensure only the command author can interact.""" + if interaction.user != self.author: + try: + if not interaction.response.is_done(): + await interaction.response.send_message( + "Only the command author can interact with this menu.", + ephemeral=True, + ) + except (discord.NotFound, discord.HTTPException): + pass + return False + return True + + @discord.ui.button(label="Change Channels", style=discord.ButtonStyle.primary) + async def change_channels(self, interaction: discord.Interaction, button: Button): + """Open channel configuration menu.""" + view = ChannelConfigView(self.bot, self.author, self.config) + from utils.cv2 import CV2 + from discord.ui import ActionRow + + layout_view = CV2( + "Channel Configuration", + "Select a category to change its log channel.", + f"Guild ID: {interaction.guild.id}", + ) + for container in layout_view.children: + if type(container).__name__ == "Container": + if view.children: + add_action_rows(container, view.children) + break + await interaction.response.edit_message(embed=None, view=layout_view) + + @discord.ui.button(label="Toggle Categories", style=discord.ButtonStyle.secondary) + async def toggle_categories(self, interaction: discord.Interaction, button: Button): + """Open category toggle menu.""" + view = CategoryToggleView(self.bot, self.author, self.config) + from utils.cv2 import CV2 + from discord.ui import ActionRow + + layout_view = CV2( + "Toggle Categories", + "Enable or disable logging categories.", + f"Guild ID: {interaction.guild.id}", + ) + for container in layout_view.children: + if type(container).__name__ == "Container": + if view.children: + add_action_rows(container, view.children) + break + await interaction.response.edit_message(embed=None, view=layout_view) + + @discord.ui.button(label="Manage Ignores", style=discord.ButtonStyle.secondary) + async def manage_ignores(self, interaction: discord.Interaction, button: Button): + """Open ignore management menu.""" + view = IgnoreManagementView(self.bot, self.author, self.config) + from utils.cv2 import CV2 + from discord.ui import ActionRow + + layout_view = CV2( + "Ignore Management", + "Manage ignored channels, roles, and users.", + f"Guild ID: {interaction.guild.id}", + ) + for container in layout_view.children: + if type(container).__name__ == "Container": + if view.children: + add_action_rows(container, view.children) + break + await interaction.response.edit_message(embed=None, view=layout_view) + + @discord.ui.button( + label="Auto-Delete Settings", style=discord.ButtonStyle.secondary + ) + async def auto_delete_settings( + self, interaction: discord.Interaction, button: Button + ): + """Configure auto-delete duration.""" + view = AutoDeleteView(self.bot, self.author, self.config) + current_duration = self.config.get("auto_delete_duration") + current_text = { + None: "Disabled", + 3600: "1 Hour", + 86400: "24 Hours", + 604800: "7 Days", + }.get(current_duration, f"{current_duration} seconds") + + from utils.cv2 import CV2 + from discord.ui import ActionRow + + layout_view = CV2( + "Auto-Delete Settings", + f"Current Setting: {current_text}", + f"Guild ID: {interaction.guild.id}", + ) + for container in layout_view.children: + if type(container).__name__ == "Container": + if view.children: + add_action_rows(container, view.children) + break + await interaction.response.edit_message(embed=None, view=layout_view) + + +class ChannelConfigView(View): + """View for configuring log channels.""" + + def __init__(self, bot: commands.Bot, author: discord.Member, config: Dict): + super().__init__(timeout=300) + self.bot = bot + self.author = author + self.config = config + self.add_item(self.create_category_select()) + + def create_category_select(self) -> Select: + """Create select menu for category selection.""" + options = [] + log_channels = self.config.get("log_channels", {}) + + for category in LOG_CATEGORIES: + channel_id = log_channels.get(category) + channel = self.author.guild.get_channel(channel_id) if channel_id else None + description = f"Currently: {channel.name}" if channel else "Not configured" + + options.append( + discord.SelectOption( + label=category.replace("_", " ").title(), + value=category, + description=description[:100], + ) + ) + + select = Select(placeholder="Select category to configure...", options=options) + select.callback = self.category_select_callback + return select + + async def category_select_callback(self, interaction: discord.Interaction): + """Handle category selection.""" + category = interaction.data["values"][0] + view = ChannelSelectView(self.bot, self.author, category, None, self.config) + + from utils.cv2 import CV2 + from discord.ui import ActionRow + + layout_view = CV2( + f"Select Channel for {category.replace('_', ' ').title()}", + f"Choose where to log {category.replace('_', ' ').lower()} events.\n\n**Page 1 of {view.total_pages}** - Showing {len(view.all_channels[: view.channels_per_page])} channels", + f"Guild ID: {interaction.guild.id}", + ) + for container in layout_view.children: + if type(container).__name__ == "Container": + if view.children: + add_action_rows(container, view.children) + break + await interaction.response.edit_message(embed=None, view=layout_view) + + +class CategoryToggleView(View): + """View for toggling categories.""" + + def __init__(self, bot: commands.Bot, author: discord.Member, config: Dict): + super().__init__(timeout=300) + self.bot = bot + self.author = author + self.config = config + self.add_item(self.create_toggle_select()) + + def create_toggle_select(self) -> Select: + """Create select menu for toggling categories.""" + options = [] + log_enabled = self.config.get("log_enabled", {}) + + for category in LOG_CATEGORIES: + enabled = log_enabled.get(category, False) + status = "Enabled" if enabled else "Disabled" + + options.append( + discord.SelectOption( + label=category.replace("_", " ").title(), + value=category, + description=f"Currently: {status}", + ) + ) + + select = Select(placeholder="Select category to toggle...", options=options) + select.callback = self.toggle_callback + return select + + async def toggle_callback(self, interaction: discord.Interaction): + """Handle category toggle.""" + category = interaction.data["values"][0] + log_enabled = self.config.get("log_enabled", {}) + current_state = log_enabled.get(category, False) + new_state = not current_state + log_enabled[category] = new_state + + cog = self.bot.get_cog("Logging") + await cog._save_log_config( + interaction.guild.id, + self.config.get("log_channels", {}), + log_enabled, + self.config.get("ignore_channels", []), + self.config.get("ignore_roles", []), + self.config.get("ignore_users", []), + self.config.get("auto_delete_duration"), + ) + + self.config["log_enabled"] = log_enabled + + status = "enabled" if new_state else "disabled" + from utils.cv2 import CV2 + from discord.ui import ActionRow + + self.clear_items() + self.add_item(self.create_toggle_select()) + layout_view = CV2( + "Category Updated", + f"{category.replace('_', ' ').title()} logging has been {status}.", + f"Guild ID: {interaction.guild.id}", + ) + for container in layout_view.children: + if type(container).__name__ == "Container": + if self.children: + add_action_rows(container, self.children) + break + await interaction.response.edit_message(embed=None, view=layout_view) + + +class IgnoreManagementView(View): + """View for managing ignore lists.""" + + def __init__(self, bot: commands.Bot, author: discord.Member, config: Dict): + super().__init__(timeout=300) + self.bot = bot + self.author = author + self.config = config + + @discord.ui.button(label="View Ignored", style=discord.ButtonStyle.secondary) + async def view_ignored(self, interaction: discord.Interaction, button: Button): + """Show current ignore lists.""" + ignore_channels = self.config.get("ignore_channels", []) + ignore_roles = self.config.get("ignore_roles", []) + ignore_users = self.config.get("ignore_users", []) + + channels_text = ( + "\n".join( + [ + f"<#{cid}>" + for cid in ignore_channels + if interaction.guild.get_channel(cid) + ] + ) + or "None" + ) + roles_text = ( + "\n".join( + [ + f"<@&{rid}>" + for rid in ignore_roles + if interaction.guild.get_role(rid) + ] + ) + or "None" + ) + users_text = ( + "\n".join( + [ + f"<@{uid}>" + for uid in ignore_users + if interaction.guild.get_member(uid) + ] + ) + or "None" + ) + + from utils.cv2 import CV2 + from discord.ui import ActionRow + + layout_view = CV2( + "Current Ignore Lists", + f"**Ignored Channels**\n{channels_text[:1024]}", + f"**Ignored Roles**\n{roles_text[:1024]}", + f"**Ignored Users**\n{users_text[:1024]}", + f"Guild ID: {interaction.guild.id}", + ) + for container in layout_view.children: + if type(container).__name__ == "Container": + if self.children: + add_action_rows(container, self.children) + break + await interaction.response.edit_message(embed=None, view=layout_view) + + @discord.ui.button(label="Manage Channels", style=discord.ButtonStyle.primary) + async def manage_channels(self, interaction: discord.Interaction, button: Button): + """Manage ignored channels.""" + from utils.cv2 import CV2 + from discord.ui import ActionRow + + layout_view = CV2( + "Manage Ignored Channels", + "Use /log ignore add channel #channel or /log ignore remove channel #channel", + f"Guild ID: {interaction.guild.id}", + ) + for container in layout_view.children: + if type(container).__name__ == "Container": + if self.children: + add_action_rows(container, self.children) + break + await interaction.response.edit_message(embed=None, view=layout_view) + + @discord.ui.button(label="Manage Roles", style=discord.ButtonStyle.primary) + async def manage_roles(self, interaction: discord.Interaction, button: Button): + """Manage ignored roles.""" + from utils.cv2 import CV2 + from discord.ui import ActionRow + + layout_view = CV2( + "Manage Ignored Roles", + "Use /log ignore add role @role or /log ignore remove role @role", + f"Guild ID: {interaction.guild.id}", + ) + for container in layout_view.children: + if type(container).__name__ == "Container": + if self.children: + add_action_rows(container, self.children) + break + await interaction.response.edit_message(embed=None, view=layout_view) + + @discord.ui.button(label="Manage Users", style=discord.ButtonStyle.primary) + async def manage_users(self, interaction: discord.Interaction, button: Button): + """Manage ignored users.""" + from utils.cv2 import CV2 + from discord.ui import ActionRow + + layout_view = CV2( + "Manage Ignored Users", + "Use /log ignore add user @user or /log ignore remove user @user", + f"Guild ID: {interaction.guild.id}", + ) + for container in layout_view.children: + if type(container).__name__ == "Container": + if self.children: + add_action_rows(container, self.children) + break + await interaction.response.edit_message(embed=None, view=layout_view) + + +class AutoDeleteView(View): + """View for configuring auto-delete duration.""" + + def __init__(self, bot: commands.Bot, author: discord.Member, config: Dict): + super().__init__(timeout=300) + self.bot = bot + self.author = author + self.config = config + self.add_item(self.create_duration_select()) + + def create_duration_select(self) -> Select: + """Create select menu for auto-delete duration.""" + options = [ + discord.SelectOption( + label="Disabled", value="None", description="Never auto-delete logs" + ), + discord.SelectOption( + label="1 Hour", value="3600", description="Delete logs after 1 hour" + ), + discord.SelectOption( + label="24 Hours", + value="86400", + description="Delete logs after 24 hours", + ), + discord.SelectOption( + label="7 Days", value="604800", description="Delete logs after 7 days" + ), + ] + + select = Select(placeholder="Select auto-delete duration...", options=options) + select.callback = self.duration_callback + return select + + async def duration_callback(self, interaction: discord.Interaction): + """Handle duration selection.""" + duration_str = interaction.data["values"][0] + duration = None if duration_str == "None" else int(duration_str) + + cog = self.bot.get_cog("Logging") + await cog._save_log_config( + interaction.guild.id, + self.config.get("log_channels", {}), + self.config.get("log_enabled", {}), + self.config.get("ignore_channels", []), + self.config.get("ignore_roles", []), + self.config.get("ignore_users", []), + duration, + ) + + self.config["auto_delete_duration"] = duration + + duration_text = { + None: "Disabled", + 3600: "1 Hour", + 86400: "24 Hours", + 604800: "7 Days", + }.get(duration, f"{duration} seconds") + + from utils.cv2 import CV2 + from discord.ui import ActionRow + + layout_view = CV2( + "Auto-Delete Updated", + f"Auto-delete duration set to: {duration_text}", + f"Guild ID: {interaction.guild.id}", + ) + for container in layout_view.children: + if type(container).__name__ == "Container": + if self.children: + add_action_rows(container, self.children) + break + await interaction.response.edit_message(embed=None, view=layout_view) + + +class ChannelSelectView(View): + """View for selecting channels for specific categories with pagination.""" + + def __init__( + self, + bot: commands.Bot, + author: discord.Member, + category: str, + parent_view: LogSetupView, + config: Dict = None, + ): + super().__init__(timeout=300) + self.bot = bot + self.author = author + self.category = category + self.parent_view = parent_view + self.config = config + self.current_page = 0 + self.channels_per_page = 24 + + self.all_channels = [ + ch + for ch in author.guild.text_channels + if ch.permissions_for(author.guild.me).send_messages + ] + self.total_pages = max( + 1, + (len(self.all_channels) + self.channels_per_page - 1) + // self.channels_per_page, + ) + + self.add_item(self.create_channel_select()) + if self.total_pages > 1: + self.add_navigation_buttons() + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + """Ensure only the command author can interact.""" + if interaction.user != self.author: + try: + if not interaction.response.is_done(): + await interaction.response.send_message( + "Only the command author can interact with this menu.", + ephemeral=True, + ) + except (discord.NotFound, discord.HTTPException): + pass + return False + return True + + def create_channel_select(self) -> Select: + """Create select menu for channel selection with pagination.""" + start_idx = self.current_page * self.channels_per_page + end_idx = start_idx + self.channels_per_page + current_channels = self.all_channels[start_idx:end_idx] + + options = [ + discord.SelectOption( + label=channel.name[:100], + value=str(channel.id), + description=f"#{channel.name} in {channel.category.name if channel.category else 'No Category'}"[ + :100 + ], + ) + for channel in current_channels + ] + + options.append( + discord.SelectOption( + label="Skip this category", + value="skip", + description="Don't configure this category", + ) + ) + + select = Select( + placeholder=f"Select channel for {self.category.replace('_', ' ').title()} (Page {self.current_page + 1}/{self.total_pages})", + options=options, + custom_id="channel_select", + ) + select.callback = self.channel_select_callback + return select + + def add_navigation_buttons(self): + """Add navigation buttons for pagination.""" + if self.total_pages > 1: + prev_button = Button( + label="◀ Previous", + style=discord.ButtonStyle.secondary, + disabled=self.current_page == 0, + custom_id="prev_page", + ) + prev_button.callback = self.previous_page + self.add_item(prev_button) + + next_button = Button( + label="Next ▶", + style=discord.ButtonStyle.secondary, + disabled=self.current_page >= self.total_pages - 1, + custom_id="next_page", + ) + next_button.callback = self.next_page + self.add_item(next_button) + + async def previous_page(self, interaction: discord.Interaction): + """Go to previous page.""" + if self.current_page > 0: + self.current_page -= 1 + await self.update_view(interaction) + + async def next_page(self, interaction: discord.Interaction): + """Go to next page.""" + if self.current_page < self.total_pages - 1: + self.current_page += 1 + await self.update_view(interaction) + + async def update_view(self, interaction: discord.Interaction): + """Update the view with new page content.""" + self.clear_items() + self.add_item(self.create_channel_select()) + if self.total_pages > 1: + self.add_navigation_buttons() + + from utils.cv2 import CV2 + from discord.ui import ActionRow + + layout_view = CV2( + f"Select Channel for {self.category.replace('_', ' ').title()}", + f"Choose where to log {self.category.replace('_', ' ').lower()} events.\n\n**Page {self.current_page + 1} of {self.total_pages}** - Showing {len(self.all_channels[self.current_page * self.channels_per_page : (self.current_page + 1) * self.channels_per_page])} channels", + f"Guild ID: {interaction.guild.id}", + ) + for container in layout_view.children: + if type(container).__name__ == "Container": + if self.children: + add_action_rows(container, self.children) + break + await interaction.response.edit_message(embed=None, view=layout_view) + + async def channel_select_callback(self, interaction: discord.Interaction): + """Handle channel selection.""" + try: + select = [item for item in self.children if isinstance(item, Select)][0] + + if self.config: + if select.values[0] == "skip": + channel_id = None + else: + channel_id = int(select.values[0]) + + log_channels = self.config.get("log_channels", {}) + if channel_id: + log_channels[self.category] = channel_id + elif self.category in log_channels: + del log_channels[self.category] + + cog = self.bot.get_cog("Logging") + await cog._save_log_config( + interaction.guild.id, + log_channels, + self.config.get("log_enabled", {}), + self.config.get("ignore_channels", []), + self.config.get("ignore_roles", []), + self.config.get("ignore_users", []), + self.config.get("auto_delete_duration"), + ) + + self.config["log_channels"] = log_channels + + channel = ( + interaction.guild.get_channel(channel_id) if channel_id else None + ) + channel_text = channel.mention if channel else "None" + + config_view = InteractiveConfigView(self.bot, self.author, self.config) + from utils.cv2 import CV2 + from discord.ui import ActionRow + + layout_view = CV2( + "Channel Updated", + f"{self.category.replace('_', ' ').title()} logs will be sent to: {channel_text}", + f"Guild ID: {interaction.guild.id}", + ) + for container in layout_view.children: + if type(container).__name__ == "Container": + if config_view.children: + add_action_rows(container, config_view.children) + break + await interaction.response.edit_message(embed=None, view=layout_view) + + else: + if self.parent_view is None: + cog = self.bot.get_cog("Logging") + if not cog: + await interaction.response.send_message( + "Logging cog not found.", ephemeral=True + ) + return + + channel_id = ( + None if select.values[0] == "skip" else int(select.values[0]) + ) + + log_channels = {self.category: channel_id} + log_enabled = {cat: bool(channel_id) for cat in LOG_CATEGORIES} + + await cog._save_log_config( + interaction.guild.id, + log_channels, + log_enabled, + [], + [], + [], + None, + ) + + channel = ( + interaction.guild.get_channel(channel_id) + if channel_id + else None + ) + channel_text = channel.mention if channel else "None" + + from utils.cv2 import CV2 + + layout_view = CV2( + "Channel Updated", + f"{self.category.replace('_', ' ').title()} logs will be sent to: {channel_text}", + f"Guild ID: {interaction.guild.id}", + ) + await interaction.response.edit_message(view=layout_view) + else: + if select.values[0] == "skip": + self.parent_view.selected_channels[self.category] = None + else: + self.parent_view.selected_channels[self.category] = int( + select.values[0] + ) + + await self.return_to_setup(interaction) + except Exception as e: + logger.error(f"Error in channel selection callback: {e}") + pass + + @discord.ui.button(label="Back to Setup", style=discord.ButtonStyle.secondary) + async def back_button(self, interaction: discord.Interaction, button: Button): + """Return to main setup view.""" + if self.config: + config_view = InteractiveConfigView(self.bot, self.author, self.config) + from utils.cv2 import CV2 + from discord.ui import ActionRow + + layout_view = CV2( + "Logging Configuration", + "Use the buttons below to modify your logging settings.", + f"Guild ID: {interaction.guild.id}", + ) + for container in layout_view.children: + if type(container).__name__ == "Container": + if config_view.children: + add_action_rows(container, config_view.children) + break + await interaction.response.edit_message(embed=None, view=layout_view) + else: + await self.return_to_setup(interaction) + + async def return_to_setup(self, interaction: discord.Interaction): + """Return to the main setup view.""" + try: + embed = CV2EmbedAdapter( + "Logging Setup", + "Configure logging channels for different event categories.", + ) + + for category in LOG_CATEGORIES: + channel_id = self.parent_view.selected_channels.get(category) + if channel_id: + channel = interaction.guild.get_channel(channel_id) + status = channel.mention if channel else "Configured" + else: + status = "Not configured" + + embed.add_field( + name=category.replace("_", " ").title(), value=status, inline=True + ) + + embed.set_footer(text=f"Guild ID: {interaction.guild.id}") + + options = [] + for category in LOG_CATEGORIES: + configured = bool(self.parent_view.selected_channels.get(category)) + options.append( + discord.SelectOption( + label=category.replace("_", " ").title(), + value=category, + description=f"Configure {category.replace('_', ' ').lower()} logging", + ) + ) + + options.append( + discord.SelectOption( + label="Finish Setup", + value="finish", + description="Complete the setup process", + ) + ) + + self.parent_view.category_select.options = options + + from discord.ui import ActionRow + + add_action_rows(embed, self.parent_view.children) + + await interaction.response.edit_message(embed=None, view=embed) + except Exception as e: + logger.error(f"Error returning to setup: {e}") + pass + + +class Logging(commands.Cog): + """Comprehensive logging cog with modern UI and expanded event coverage.""" + + def __init__(self, bot: commands.Bot): + self.bot = bot + self.config_cache: Dict[int, Dict[str, Any]] = {} + self.audit_cache: Dict[tuple, tuple[Any, float]] = {} + self.config_lock = asyncio.Lock() + self.log_queue: Dict[int, List[Dict]] = {} + + asyncio.create_task(self._load_config()) + + asyncio.create_task(self._periodic_cache_cleanup()) + + async def _periodic_cache_cleanup(self): + """Periodically clean up audit cache to prevent memory leaks.""" + while True: + try: + await asyncio.sleep(AUDIT_CACHE_TTL) + current_time = time.time() + + expired_keys = [ + key + for key, (_, timestamp) in self.audit_cache.items() + if current_time - timestamp > AUDIT_CACHE_TTL + ] + + for key in expired_keys: + del self.audit_cache[key] + + if len(self.audit_cache) > MAX_AUDIT_CACHE_SIZE: + sorted_items = sorted( + self.audit_cache.items(), key=lambda x: x[1][1] + ) + + for key, _ in sorted_items[ + : len(self.audit_cache) - MAX_AUDIT_CACHE_SIZE + ]: + del self.audit_cache[key] + + logger.debug( + f"Cache cleanup: {len(self.audit_cache)} entries remaining" + ) + except Exception as e: + logger.error(f"Error in cache cleanup: {e}") + + async def _load_config(self): + """Load configuration from JSON file with error handling.""" + try: + if os.path.exists(CONFIG_FILE): + try: + async with aiofiles.open(CONFIG_FILE, "r", encoding="utf-8") as f: + content = await f.read() + if content.strip(): + data = await asyncio.to_thread(json.loads, content) + for guild_id_str, config in data.items(): + try: + guild_id = int(guild_id_str) + + if isinstance(config, dict): + self.config_cache[guild_id] = config + except (ValueError, TypeError) as e: + logger.warning( + f"Invalid guild ID or config in file: {guild_id_str}, {e}" + ) + pass + else: + pass + except json.JSONDecodeError as e: + logger.error(f"Invalid JSON in config file: {e}") + + backup_file = f"{CONFIG_FILE}.backup.{int(time.time())}" + await asyncio.to_thread(os.rename, CONFIG_FILE, backup_file) + logger.info(f"Corrupted config backed up to {backup_file}") + else: + pass + except Exception as e: + logger.error(f"Error loading config: {e}") + self.config_cache = {} + + async def _save_config(self): + """Save configuration to JSON file with atomic writes and error handling.""" + async with self.config_lock: + try: + data = { + str(guild_id): config + for guild_id, config in self.config_cache.items() + } + content = await asyncio.to_thread(json.dumps, data, indent=2) + + temp_file = f"{CONFIG_FILE}.tmp" + async with aiofiles.open(temp_file, "w", encoding="utf-8") as f: + await f.write(content) + + await asyncio.to_thread(os.replace, temp_file, CONFIG_FILE) + pass + except Exception as e: + logger.error(f"Error saving config: {e}") + + try: + if os.path.exists(f"{CONFIG_FILE}.tmp"): + await asyncio.to_thread(os.remove, f"{CONFIG_FILE}.tmp") + except: + pass + raise + + async def _save_log_entry(self, guild_id: int, category: str, log_data: Dict): + """Save individual log entries to JSON files with size limits and error handling.""" + try: + date_str = datetime.utcnow().strftime("%Y-%m-%d") + log_file = Path(LOGS_DIR) / f"{guild_id}_{category}_{date_str}.json" + + if log_file.exists(): + file_size = log_file.stat().st_size + if file_size > MAX_LOG_FILE_SIZE: + timestamp = datetime.utcnow().strftime("%H%M%S") + log_file = ( + Path(LOGS_DIR) + / f"{guild_id}_{category}_{date_str}_{timestamp}.json" + ) + + logs = [] + if log_file.exists(): + try: + async with aiofiles.open(log_file, "r", encoding="utf-8") as f: + content = await f.read() + if content.strip(): + logs = await asyncio.to_thread(json.loads, content) + if not isinstance(logs, list): + logs = [] + except (json.JSONDecodeError, UnicodeDecodeError) as e: + logger.warning(f"Error reading existing log file {log_file}: {e}") + + backup_file = f"{log_file}.backup.{int(time.time())}" + await asyncio.to_thread(os.rename, log_file, backup_file) + + logs.append(log_data) + + if len(logs) > 1000: + logs = logs[-1000:] + + temp_file = f"{log_file}.tmp" + try: + content = await asyncio.to_thread( + json.dumps, logs, indent=2, default=str + ) + async with aiofiles.open(temp_file, "w", encoding="utf-8") as f: + await f.write(content) + + await asyncio.to_thread(os.replace, temp_file, log_file) + except Exception as e: + logger.error(f"Error writing log file {log_file}: {e}") + + try: + if os.path.exists(temp_file): + await asyncio.to_thread(os.remove, temp_file) + except: + pass + raise + + except Exception as e: + logger.error(f"Error saving log entry: {e}") + + async def _save_log_config( + self, + guild_id: int, + log_channels: Dict[str, Optional[int]], + log_enabled: Dict[str, bool], + ignore_channels: List[int], + ignore_roles: List[int], + ignore_users: List[int], + auto_delete_duration: Optional[int], + ): + """Save logging configuration with validation and error handling.""" + try: + if not isinstance(guild_id, int) or guild_id <= 0: + raise ValueError(f"Invalid guild_id: {guild_id}") + + if not isinstance(log_channels, dict): + log_channels = {} + + if not isinstance(log_enabled, dict): + log_enabled = {} + + ignore_channels = [ + int(x) + for x in ignore_channels + if isinstance(x, (int, str)) and str(x).isdigit() + ] + ignore_roles = [ + int(x) + for x in ignore_roles + if isinstance(x, (int, str)) and str(x).isdigit() + ] + ignore_users = [ + int(x) + for x in ignore_users + if isinstance(x, (int, str)) and str(x).isdigit() + ] + + guild = self.bot.get_guild(guild_id) + if guild: + ignore_channels = [ + cid for cid in ignore_channels if guild.get_channel(cid) + ] + ignore_roles = [rid for rid in ignore_roles if guild.get_role(rid)] + ignore_users = [uid for uid in ignore_users if guild.get_member(uid)] + + config = { + "guild_id": str(guild_id), + "log_channels": { + k: v + for k, v in log_channels.items() + if v is not None and isinstance(v, int) + }, + "log_enabled": {k: bool(v) for k, v in log_enabled.items()}, + "ignore_channels": list(set(ignore_channels)), + "ignore_roles": list(set(ignore_roles)), + "ignore_users": list(set(ignore_users)), + "auto_delete_duration": auto_delete_duration + if isinstance(auto_delete_duration, int) + else None, + "last_updated": datetime.utcnow().isoformat(), + } + + self.config_cache[guild_id] = config + await self._save_config() + pass + except Exception as e: + logger.error(f"Error saving config for guild {guild_id}: {e}") + raise + + async def _get_log_config(self, guild_id: int) -> Optional[Dict]: + """Retrieve logging configuration from cache with validation.""" + try: + config = self.config_cache.get(guild_id) + if config and isinstance(config, dict): + return config + return None + except Exception as e: + logger.error(f"Error retrieving config for guild {guild_id}: {e}") + return None + + async def _get_audit_log( + self, + guild: discord.Guild, + action: discord.AuditLogAction, + target_id: Optional[int] = None, + ) -> Optional[discord.AuditLogEntry]: + """Fetch audit log entry with caching and proper error handling.""" + if not guild.me.guild_permissions.view_audit_log: + return None + + cache_key = (guild.id, action.value, target_id) + if cache_key in self.audit_cache: + entry, timestamp = self.audit_cache[cache_key] + if time.time() - timestamp < 10: + return entry + + try: + async for entry in guild.audit_logs(limit=5, action=action): + if target_id is None or ( + hasattr(entry.target, "id") and entry.target.id == target_id + ): + self.audit_cache[cache_key] = (entry, time.time()) + return entry + except (discord.Forbidden, discord.HTTPException, discord.NotFound) as e: + logger.warning(f"Failed to fetch audit log for guild {guild.id}: {e}") + except Exception as e: + logger.error(f"Unexpected error fetching audit log: {e}") + + return None + + async def _send_log( + self, + guild: discord.Guild, + category: str, + embed: discord.Embed, + channel_id: Optional[int] = None, + author_id: Optional[int] = None, + ): + """Send log embed with comprehensive filtering and error handling.""" + try: + config = await self._get_log_config(guild.id) + if not config or not config.get("log_enabled", {}).get(category, False): + return + + log_channel_id = config.get("log_channels", {}).get(category) + if not log_channel_id: + return + + channel = guild.get_channel(log_channel_id) + if not channel: + logger.warning( + f"Log channel {log_channel_id} not found in guild {guild.id}" + ) + return + + if not channel.permissions_for(guild.me).send_messages: + logger.warning( + f"No permission to send messages in log channel {log_channel_id}" + ) + return + + ignore_channels = config.get("ignore_channels", []) + ignore_roles = config.get("ignore_roles", []) + ignore_users = config.get("ignore_users", []) + auto_delete_duration = config.get("auto_delete_duration") + + if author_id and author_id in ignore_users: + return + + if author_id: + member = guild.get_member(author_id) + if member and any(role.id in ignore_roles for role in member.roles): + return + + if channel_id and channel_id in ignore_channels: + return + + embed.color = 0xFF0000 + + delete_after = ( + auto_delete_duration + if auto_delete_duration and auto_delete_duration > 0 + else None + ) + message = await channel.send( + view=embed, delete_after=delete_after, allowed_mentions=None + ) + + log_data = { + "timestamp": datetime.utcnow().isoformat(), + "category": category, + "guild_id": guild.id, + "channel_id": channel_id, + "author_id": author_id, + "embed_data": getattr( + embed, + "to_dict", + lambda: {"title": "Log", "description": "CV2 Container"}, + )(), + "message_id": message.id, + } + await self._save_log_entry(guild.id, category, log_data) + + logger.debug( + f"Sent {category} log to channel {log_channel_id} in guild {guild.id}" + ) + except discord.Forbidden: + logger.error(f"Missing permissions to send log to channel {log_channel_id}") + except discord.HTTPException as e: + logger.error(f"HTTP error sending log: {e}") + except Exception as e: + logger.error( + f"Failed to send {category} log to channel {log_channel_id}: {e}" + ) + + def _create_modern_embed( + self, title: str, description: str = None, color: int = 0xFF0000 + ): + """Create a modern CV2 container representation that supports Embed methods.""" + embed = CV2EmbedAdapter(title, description) + embed.color = color + return embed + + def _create_status_embeds( + self, guild: discord.Guild, config: Dict + ) -> List[discord.Embed]: + """Create comprehensive status embeds with minimal styling.""" + embeds = [] + + main_embed = self._create_modern_embed( + "Logging Configuration Status", f"Complete logging setup for {guild.name}" + ) + + log_channels = config.get("log_channels", {}) + log_enabled = config.get("log_enabled", {}) + + category_info = { + "message_events": ("Message Events", "Message edits, deletions"), + "join_leave_events": ("Join/Leave Events", "Member joins and leaves"), + "member_moderation": ("Member Moderation", "Bans, timeouts, kicks"), + "voice_events": ("Voice Events", "Voice channel activity"), + "channel_events": ("Channel Events", "Channel create/delete/update"), + "role_events": ("Role Events", "Role create/delete/update"), + "emoji_events": ("Emoji Events", "Custom emoji changes"), + "reaction_events": ("Reaction Events", "Message reactions"), + "system_events": ("System Events", "Server updates"), + } + + field_count = 0 + for category, (name, desc) in category_info.items(): + if field_count >= 25: + embeds.append(main_embed) + main_embed = self._create_modern_embed("Logging Status (Continued)") + field_count = 0 + + channel = ( + guild.get_channel(log_channels.get(category)) + if log_channels.get(category) + else None + ) + status = f"{channel.mention if channel else 'Not configured'}" + status += ( + f" {'(Enabled)' if log_enabled.get(category, False) else '(Disabled)'}" + ) + + main_embed.add_field(name=name, value=status, inline=True) + field_count += 1 + + main_embed.set_footer(text=f"Guild ID: {guild.id}") + + config_embed = self._create_modern_embed("Configuration Details") + + ignore_channels = config.get("ignore_channels", []) + ignore_roles = config.get("ignore_roles", []) + ignore_users = config.get("ignore_users", []) + auto_delete = config.get("auto_delete_duration") + + ignored_ch = [f"<#{cid}>" for cid in ignore_channels if guild.get_channel(cid)] + config_embed.add_field( + name="Ignored Channels", + value=", ".join(ignored_ch[:10])[:1024] if ignored_ch else "None", + inline=False, + ) + + ignored_roles_mentions = [ + f"<@&{rid}>" for rid in ignore_roles if guild.get_role(rid) + ] + config_embed.add_field( + name="Ignored Roles", + value=", ".join(ignored_roles_mentions[:10])[:1024] + if ignored_roles_mentions + else "None", + inline=False, + ) + + ignored_users_mentions = [ + f"<@{uid}>" for uid in ignore_users if guild.get_member(uid) + ] + config_embed.add_field( + name="Ignored Users", + value=", ".join(ignored_users_mentions[:10])[:1024] + if ignored_users_mentions + else "None", + inline=False, + ) + + duration_text = { + None: "Disabled", + 3600: "1 Hour", + 86400: "24 Hours", + 604800: "7 Days", + }.get(auto_delete, f"{auto_delete} seconds") + + config_embed.add_field( + name="Auto-Delete Duration", value=duration_text, inline=True + ) + + last_updated = config.get("last_updated") + if last_updated: + try: + dt = datetime.fromisoformat(last_updated.replace("Z", "+00:00")) + config_embed.add_field( + name="Last Updated", + value=discord.utils.format_dt(dt, "R"), + inline=True, + ) + except Exception as e: + pass + + config_embed.set_footer(text=f"Guild ID: {guild.id}") + + embeds.append(main_embed) + embeds.append(config_embed) + return embeds + + @commands.hybrid_group(invoke_without_command=True, name="log") + async def log(self, ctx: commands.Context): + """Main logging command group. Works with both slash (/) and prefix (!) commands.""" + + await ctx.send_help(ctx.command) + + @log.command( + name="setup", description="Run the interactive setup for logging channels." + ) + @commands.has_permissions(manage_guild=True) + @commands.cooldown(1, 30, commands.BucketType.guild) + @commands.max_concurrency(1, per=commands.BucketType.guild, wait=False) + async def log_setup(self, ctx: commands.Context): + """Enhanced interactive setup with improved user experience.""" + try: + config = await self._get_log_config(ctx.guild.id) + if config: + embed = self._create_modern_embed( + "Configuration Exists", + "Logging is already configured. Use /log config to modify or /log reset to start over.", + ) + await ctx.send(view=embed, ephemeral=True) + return + + view = LogSetupLayoutView(self.bot, ctx.author, LOG_CATEGORIES) + message = await ctx.send(view=view) + except Exception as e: + logger.error(f"Error in log setup: {e}") + embed = self._create_modern_embed( + "Setup Error", "An error occurred during setup. Please try again." + ) + await ctx.send(view=embed) + + @log.command(name="status", description="View the current logging configuration.") + @commands.has_permissions(manage_guild=True) + async def log_status(self, ctx: commands.Context): + """Display comprehensive logging status.""" + try: + config = await self._get_log_config(ctx.guild.id) + if not config: + embed = self._create_modern_embed( + "No Configuration Found", + "Logging is not configured for this server. Use /log setup to get started.", + f"Guild ID: {ctx.guild.id}", + ) + await ctx.send(view=embed) + return + + embeds = self._create_status_embeds(ctx.guild, config) + for embed in embeds: + await ctx.send(view=embed) + except Exception as e: + logger.error(f"Error in log status: {e}") + embed = self._create_modern_embed( + "Status Error", "An error occurred while retrieving status." + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed) + + @log.command( + name="search", description="Search through logged events with filters." + ) + @commands.has_permissions(manage_guild=True) + @commands.cooldown(5, 60, commands.BucketType.user) + async def log_search( + self, + ctx: commands.Context, + user: Optional[discord.Member] = None, + category: Optional[str] = None, + hours: Optional[int] = 24, + ): + """Search through logs with comprehensive filtering options.""" + try: + if hours and (hours < 1 or hours > 168): + embed = self._create_modern_embed( + "Invalid Hours", "Hours must be between 1 and 168 (1 week)." + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed, ephemeral=True) + return + + if category and category not in LOG_CATEGORIES: + embed = self._create_modern_embed( + "Invalid Category", f"Valid categories: {', '.join(LOG_CATEGORIES)}" + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed, ephemeral=True) + return + + await ctx.defer() + + results = [] + search_date = datetime.utcnow() - timedelta(hours=hours or 24) + + for days_back in range((hours or 24) // 24 + 1): + search_day = search_date + timedelta(days=days_back) + date_str = search_day.strftime("%Y-%m-%d") + + categories_to_search = [category] if category else LOG_CATEGORIES + + for cat in categories_to_search: + base_pattern = f"{ctx.guild.id}_{cat}_{date_str}" + log_files = [ + f + for f in Path(LOGS_DIR).iterdir() + if f.name.startswith(base_pattern) + ] + + for log_file in log_files: + if log_file.exists(): + try: + file_size = await asyncio.to_thread( + log_file.stat().st_size + ) + if file_size > MAX_LOG_FILE_SIZE: + logger.warning( + f"Log file {log_file} is too large to search" + ) + continue + + async with aiofiles.open( + log_file, "r", encoding="utf-8" + ) as f: + content = await f.read() + if content.strip(): + logs = await asyncio.to_thread( + json.loads, content + ) + if isinstance(logs, list): + for log_entry in logs: + if not isinstance(log_entry, dict): + continue + + try: + log_time = datetime.fromisoformat( + log_entry["timestamp"].replace( + "Z", "+00:00" + ) + ) + if log_time >= search_date: + if ( + not user + or log_entry.get( + "author_id" + ) + == user.id + ): + results.append(log_entry) + if ( + len(results) + >= MAX_SEARCH_RESULTS + ): + break + except (KeyError, ValueError): + continue + except Exception as e: + logger.error(f"Error reading log file {log_file}: {e}") + continue + + if len(results) >= MAX_SEARCH_RESULTS: + break + if len(results) >= MAX_SEARCH_RESULTS: + break + + if not results: + embed = self._create_modern_embed( + "No Results Found", "No log entries match your search criteria." + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.followup.send(view=embed) + return + + results.sort(key=lambda x: x.get("timestamp", ""), reverse=True) + results = results[:50] + + embed = self._create_modern_embed( + f"Search Results ({len(results)} found)", + f"Showing logs from the last {hours} hours", + ) + + if user: + embed.description += f" for {user.mention}" + if category: + embed.description += f" in category {category}" + + for i, result in enumerate(results[:10]): + try: + timestamp = datetime.fromisoformat( + result["timestamp"].replace("Z", "+00:00") + ) + category_name = ( + result.get("category", "Unknown").replace("_", " ").title() + ) + channel_id = result.get("channel_id") + channel_text = f"<#{channel_id}>" if channel_id else "Unknown" + + embed.add_field( + name=f"{i + 1}. {category_name}", + value=f"{discord.utils.format_dt(timestamp, 'R')} • {channel_text}", + inline=False, + ) + except Exception as e: + continue + + if len(results) > 10: + embed.set_footer( + text=f"Showing 10 of {len(results)} results • Guild ID: {ctx.guild.id}" + ) + else: + pass + await ctx.followup.send(view=embed) + + except Exception as e: + logger.error(f"Error in log search: {e}") + embed = self._create_modern_embed( + "Search Error", "An error occurred while searching logs." + ) + # embed.set_footer removed - CV2 doesn't support it + try: + await ctx.followup.send(view=embed) + except: + await ctx.send(view=embed, ephemeral=True) + + @log.command(name="reset", description="Reset all logging configuration.") + @commands.has_permissions(manage_guild=True) + async def log_reset(self, ctx: commands.Context): + """Reset logging configuration for the guild.""" + try: + if ctx.guild.id in self.config_cache: + del self.config_cache[ctx.guild.id] + await self._save_config() + + embed = self._create_modern_embed( + "Configuration Reset", + "All logging configuration has been cleared. Use /log setup to reconfigure.", + ) + else: + embed = self._create_modern_embed( + "No Configuration Found", + "There is no logging configuration to reset.", + ) + + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed) + except Exception as e: + logger.error(f"Error in log reset: {e}") + embed = self._create_modern_embed( + "Reset Error", "An error occurred while resetting configuration." + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed) + + @log.command( + name="toggle", description="Enable or disable specific logging categories." + ) + @commands.has_permissions(manage_guild=True) + async def log_toggle(self, ctx: commands.Context, category: str, enabled: bool): + """Toggle logging categories on/off.""" + try: + if category not in LOG_CATEGORIES: + embed = self._create_modern_embed( + "Invalid Category", f"Valid categories: {', '.join(LOG_CATEGORIES)}" + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed, ephemeral=True) + return + + config = await self._get_log_config(ctx.guild.id) + if not config: + embed = self._create_modern_embed( + "No Configuration Found", + "Please run /log setup first to configure logging.", + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed) + return + + log_enabled = config.get("log_enabled", {}) + log_enabled[category] = enabled + + await self._save_log_config( + ctx.guild.id, + config.get("log_channels", {}), + log_enabled, + config.get("ignore_channels", []), + config.get("ignore_roles", []), + config.get("ignore_users", []), + config.get("auto_delete_duration"), + ) + + status = "enabled" if enabled else "disabled" + embed = self._create_modern_embed( + f"Category {status.title()}", + f"{category.replace('_', ' ').title()} logging has been {status}.", + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed) + except Exception as e: + logger.error(f"Error in log toggle: {e}") + embed = self._create_modern_embed( + "Toggle Error", "An error occurred while toggling category." + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed) + + @log.command( + name="config", description="Open interactive config menu for logging settings." + ) + @commands.has_permissions(manage_guild=True) + async def log_config(self, ctx: commands.Context): + """Fully interactive configuration menu.""" + try: + config = await self._get_log_config(ctx.guild.id) + if not config: + embed = self._create_modern_embed( + "No Configuration Found", + "Please run /log setup first to configure logging.", + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed) + return + + view = InteractiveConfigView(self.bot, ctx.author, config) + embed = self._create_modern_embed( + "Logging Configuration", + "Use the buttons below to modify your logging settings.", + ) + + embed.add_field( + name="Available Options", + value=( + "Change Channels - Configure log channels for categories\n" + "Toggle Categories - Enable/disable logging categories\n" + "Manage Ignores - Manage ignore lists\n" + "Auto-Delete Settings - Set auto-delete duration" + ), + inline=False, + ) + + # embed.set_footer removed - CV2 doesn't support it + + from discord.ui import ActionRow + + add_action_rows(embed, view.children) + message = await ctx.send(view=embed) + view.message = message + except Exception as e: + logger.error(f"Error in log config: {e}") + embed = self._create_modern_embed( + "Config Error", "An error occurred while loading configuration." + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed) + + @log.command(name="ignore", description="Manage ignored channels, roles, or users.") + @commands.has_permissions(manage_guild=True) + async def log_ignore( + self, + ctx: commands.Context, + action: str = "list", + target_type: str = None, + target: str = None, + ): + """Manage ignore lists.""" + try: + if action not in ["add", "remove", "list", "clear"]: + embed = self._create_modern_embed( + "Invalid Action", "Valid actions: add, remove, list, clear" + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed, ephemeral=True) + return + + config = await self._get_log_config(ctx.guild.id) + if not config: + embed = self._create_modern_embed( + "No Configuration Found", + "Please run /log setup first to configure logging.", + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed) + return + + if action == "list": + embed = self._create_modern_embed("Current Ignore Lists") + + ignore_channels = config.get("ignore_channels", []) + ignore_roles = config.get("ignore_roles", []) + ignore_users = config.get("ignore_users", []) + + channels_text = ( + "\n".join( + [ + f"<#{cid}>" + for cid in ignore_channels + if ctx.guild.get_channel(cid) + ] + ) + or "None" + ) + roles_text = ( + "\n".join( + [ + f"<@&{rid}>" + for rid in ignore_roles + if ctx.guild.get_role(rid) + ] + ) + or "None" + ) + users_text = ( + "\n".join( + [ + f"<@{uid}>" + for uid in ignore_users + if ctx.guild.get_member(uid) + ] + ) + or "None" + ) + + embed.add_field( + name="Ignored Channels", value=channels_text[:1024], inline=False + ) + embed.add_field( + name="Ignored Roles", value=roles_text[:1024], inline=False + ) + embed.add_field( + name="Ignored Users", value=users_text[:1024], inline=False + ) + # embed.set_footer removed - CV2 doesn't support it + + await ctx.send(view=embed) + return + + if not target_type or not target: + embed = self._create_modern_embed( + "Missing Parameters", + f"Usage: /log ignore {action} ", + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed, ephemeral=True) + return + + ignore_channels = config.get("ignore_channels", []) + ignore_roles = config.get("ignore_roles", []) + ignore_users = config.get("ignore_users", []) + + target_obj = None + target_id = None + + if target_type.lower() == "channel": + if target.startswith("<#") and target.endswith(">"): + target_id = int(target[2:-1]) + target_obj = ctx.guild.get_channel(target_id) + elif target.isdigit(): + target_id = int(target) + target_obj = ctx.guild.get_channel(target_id) + else: + target_obj = discord.utils.get(ctx.guild.text_channels, name=target) + target_id = target_obj.id if target_obj else None + + if not target_obj: + embed = self._create_modern_embed( + "Channel Not Found", "Could not find the specified channel." + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed, ephemeral=True) + return + + target_list = ignore_channels + target_name = f"#{target_obj.name}" + + elif target_type.lower() == "role": + if target.startswith("<@&") and target.endswith(">"): + target_id = int(target[3:-1]) + target_obj = ctx.guild.get_role(target_id) + elif target.isdigit(): + target_id = int(target) + target_obj = ctx.guild.get_role(target_id) + else: + target_obj = discord.utils.get(ctx.guild.roles, name=target) + target_id = target_obj.id if target_obj else None + + if not target_obj: + embed = self._create_modern_embed( + "Role Not Found", "Could not find the specified role." + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed, ephemeral=True) + return + + target_list = ignore_roles + target_name = f"@{target_obj.name}" + + elif target_type.lower() == "user": + if target.startswith("<@") and target.endswith(">"): + target_id = int(target[2:-1].replace("!", "")) + target_obj = ctx.guild.get_member(target_id) + elif target.isdigit(): + target_id = int(target) + target_obj = ctx.guild.get_member(target_id) + else: + target_obj = discord.utils.get( + ctx.guild.members, display_name=target + ) + if not target_obj: + target_obj = discord.utils.get(ctx.guild.members, name=target) + target_id = target_obj.id if target_obj else None + + if not target_obj: + embed = self._create_modern_embed( + "User Not Found", "Could not find the specified user." + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed, ephemeral=True) + return + + target_list = ignore_users + target_name = f"{target_obj.display_name}" + + else: + embed = self._create_modern_embed( + "Invalid Target Type", + "Target type must be 'channel', 'role', or 'user'.", + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed, ephemeral=True) + return + + if action == "add": + if target_id not in target_list: + target_list.append(target_id) + message = f"Added {target_name} to ignore list." + else: + message = f"{target_name} is already in the ignore list." + elif action == "remove": + if target_id in target_list: + target_list.remove(target_id) + message = f"Removed {target_name} from ignore list." + else: + message = f"{target_name} is not in the ignore list." + + await self._save_log_config( + ctx.guild.id, + config.get("log_channels", {}), + config.get("log_enabled", {}), + ignore_channels, + ignore_roles, + ignore_users, + config.get("auto_delete_duration"), + ) + + embed = self._create_modern_embed("Ignore List Updated", message) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed) + + except Exception as e: + logger.error(f"Error in log ignore: {e}") + embed = self._create_modern_embed( + "Ignore Error", "An error occurred while managing ignore lists." + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed) + + @log.command( + name="test", description="Send test log messages to configured channels." + ) + @commands.has_permissions(manage_guild=True) + async def log_test(self, ctx: commands.Context, category: str = None): + """Send test log messages.""" + try: + config = await self._get_log_config(ctx.guild.id) + if not config: + embed = self._create_modern_embed( + "No Configuration Found", + "Please run /log setup first to configure logging.", + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed) + return + + if category and category not in LOG_CATEGORIES: + embed = self._create_modern_embed( + "Invalid Category", f"Valid categories: {', '.join(LOG_CATEGORIES)}" + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed, ephemeral=True) + return + + categories_to_test = ( + [category] + if category and category in LOG_CATEGORIES + else LOG_CATEGORIES + ) + + sent_count = 0 + for cat in categories_to_test: + if config.get("log_enabled", {}).get(cat, False): + embed = self._create_modern_embed( + f"Test Log - {cat.replace('_', ' ').title()}" + ) + embed.add_field(name="Category", value=cat, inline=True) + embed.add_field( + name="Test User", value=ctx.author.mention, inline=True + ) + embed.add_field( + name="Timestamp", + value=discord.utils.format_dt(datetime.utcnow(), "f"), + inline=True, + ) + embed.set_footer(text=f"Test message • User ID: {ctx.author.id}") + + await self._send_log( + ctx.guild, cat, embed, ctx.channel.id, ctx.author.id + ) + sent_count += 1 + + if sent_count > 0: + embed = self._create_modern_embed( + "Test Messages Sent", + f"Sent {sent_count} test log messages to configured channels.", + ) + else: + embed = self._create_modern_embed( + "No Test Messages Sent", + "No logging categories are enabled or configured.", + ) + + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed) + except Exception as e: + logger.error(f"Error in log test: {e}") + embed = self._create_modern_embed( + "Test Error", "An error occurred while sending test messages." + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed) + + @log.command(name="export", description="Export log data as a JSON file.") + @commands.has_permissions(manage_guild=True) + @commands.cooldown(1, 300, commands.BucketType.guild) + async def log_export(self, ctx: commands.Context, days: int = 7): + """Export log data for the specified number of days.""" + try: + if days < 1 or days > 30: + embed = self._create_modern_embed( + "Invalid Range", "Days must be between 1 and 30." + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.send(view=embed, ephemeral=True) + return + + await ctx.defer() + + exported_data = {} + total_entries = 0 + + for day_offset in range(days): + date = datetime.utcnow() - timedelta(days=day_offset) + date_str = date.strftime("%Y-%m-%d") + + for category in LOG_CATEGORIES: + base_pattern = f"{ctx.guild.id}_{category}_{date_str}" + log_files = [ + f + for f in Path(LOGS_DIR).iterdir() + if f.name.startswith(base_pattern) + ] + + for log_file in log_files: + if log_file.exists(): + try: + async with aiofiles.open( + log_file, "r", encoding="utf-8" + ) as f: + content = await f.read() + if content.strip(): + logs = await asyncio.to_thread( + json.loads, content + ) + if isinstance(logs, list) and logs: + key = f"{date_str}_{category}_{log_file.name.split('_')[-1].split('.')[0]}" + exported_data[key] = logs + total_entries += len(logs) + except Exception as e: + logger.error(f"Error reading log file {log_file}: {e}") + + if not exported_data: + embed = self._create_modern_embed( + "No Data Found", f"No log data found for the last {days} days." + ) + # embed.set_footer removed - CV2 doesn't support it + await ctx.followup.send(view=embed) + return + + export_filename = f"logs_export_{ctx.guild.id}_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.json" + export_data = { + "guild_id": ctx.guild.id, + "guild_name": ctx.guild.name, + "export_date": datetime.utcnow().isoformat(), + "days_exported": days, + "total_entries": total_entries, + "data": exported_data, + } + + temp_path = Path(LOGS_DIR) / export_filename + content = await asyncio.to_thread( + json.dumps, export_data, indent=2, default=str + ) + async with aiofiles.open(temp_path, "w", encoding="utf-8") as f: + await f.write(content) + + embed = self._create_modern_embed( + "Export Complete", + f"Exported {total_entries} log entries from the last {days} days.", + ) + embed.add_field( + name="File Size", + value=f"{len(content.encode('utf-8')) / 1024:.2f} KB", + inline=True, + ) + embed.add_field( + name="Categories", + value=str(len(set(k.split("_", 1)[1] for k in exported_data.keys()))), + inline=True, + ) + # embed.set_footer removed - CV2 doesn't support it + + with open(temp_path, "rb") as f: + file = discord.File(f, filename=export_filename) + await ctx.followup.send(view=embed, file=file) + + await asyncio.to_thread(temp_path.unlink) + + except Exception as e: + logger.error(f"Error exporting logs: {e}") + embed = self._create_modern_embed( + "Export Failed", "An error occurred while exporting log data." + ) + # embed.set_footer removed - CV2 doesn't support it + try: + await ctx.followup.send(view=embed) + except: + await ctx.send(view=embed) + + @commands.Cog.listener() + async def on_message_edit(self, before: discord.Message, after: discord.Message): + """Enhanced message edit logging with content comparison.""" + try: + if not before.guild or before.author.bot or before.content == after.content: + return + + embed = self._create_modern_embed("Message Edited") + embed.set_author( + name=before.author.display_name, + icon_url=before.author.display_avatar.url, + ) + + before_content = before.content[:1000] if before.content else "No content" + after_content = after.content[:1000] if after.content else "No content" + + embed.add_field( + name="Before", value=f"```{before_content}```", inline=False + ) + embed.add_field(name="After", value=f"```{after_content}```", inline=False) + embed.add_field(name="Channel", value=before.channel.mention, inline=True) + embed.add_field(name="User", value=f"{before.author.mention}", inline=True) + embed.add_field( + name="Jump to Message", + value=f"[Click here]({after.jump_url})", + inline=True, + ) + embed.set_footer( + text=f"User ID: {before.author.id} • Message ID: {before.id}" + ) + + await self._send_log( + before.guild, + "message_events", + embed, + before.channel.id, + before.author.id, + ) + except Exception as e: + logger.error(f"Error in on_message_edit: {e}") + + @commands.Cog.listener() + async def on_message_delete(self, message: discord.Message): + """Enhanced message deletion logging with attachment info.""" + try: + if not message.guild or message.author.bot: + return + + embed = self._create_modern_embed("Message Deleted") + embed.set_author( + name=message.author.display_name, + icon_url=message.author.display_avatar.url, + ) + + content = message.content[:1000] if message.content else "No content" + embed.add_field(name="Content", value=f"```{content}```", inline=False) + embed.add_field(name="Channel", value=message.channel.mention, inline=True) + embed.add_field(name="User", value=message.author.mention, inline=True) + + if message.attachments: + attachments = "\n".join( + [f"• {att.filename}" for att in message.attachments[:5]] + ) + embed.add_field(name="Attachments", value=attachments, inline=False) + + embed.set_footer( + text=f"User ID: {message.author.id} • Message ID: {message.id}" + ) + + await self._send_log( + message.guild, + "message_events", + embed, + message.channel.id, + message.author.id, + ) + except Exception as e: + logger.error(f"Error in on_message_delete: {e}") + + @commands.Cog.listener() + async def on_member_join(self, member: discord.Member): + """Log member joins with comprehensive information.""" + try: + embed = self._create_modern_embed("Member Joined") + embed.set_author( + name=member.display_name, icon_url=member.display_avatar.url + ) + + embed.add_field(name="User", value=member.mention, inline=True) + embed.add_field( + name="Account Created", + value=discord.utils.format_dt(member.created_at, "R"), + inline=True, + ) + embed.add_field( + name="Member Count", value=str(member.guild.member_count), inline=True + ) + + embed.set_footer(text=f"User ID: {member.id}") + embed.set_thumbnail(url=member.display_avatar.url) + + await self._send_log( + member.guild, "join_leave_events", embed, None, member.id + ) + except Exception as e: + logger.error(f"Error in on_member_join: {e}") + + @commands.Cog.listener() + async def on_member_remove(self, member: discord.Member): + """Log member leaves with role information.""" + try: + embed = self._create_modern_embed("Member Left") + embed.set_author( + name=member.display_name, icon_url=member.display_avatar.url + ) + + embed.add_field( + name="User", value=f"{member.mention} ({member})", inline=True + ) + embed.add_field( + name="Joined", + value=discord.utils.format_dt(member.joined_at, "R") + if member.joined_at + else "Unknown", + inline=True, + ) + embed.add_field( + name="Member Count", value=str(member.guild.member_count), inline=True + ) + + if member.roles[1:]: + roles = ", ".join([role.mention for role in member.roles[1:][:10]]) + embed.add_field(name="Roles", value=roles, inline=False) + + embed.set_footer(text=f"User ID: {member.id}") + embed.set_thumbnail(url=member.display_avatar.url) + + await self._send_log( + member.guild, "join_leave_events", embed, None, member.id + ) + except Exception as e: + logger.error(f"Error in on_member_remove: {e}") + + @commands.Cog.listener() + async def on_member_update(self, before: discord.Member, after: discord.Member): + """Log member updates including role changes.""" + try: + if before.roles != after.roles: + embed = self._create_modern_embed("Member Roles Updated") + embed.set_author( + name=after.display_name, icon_url=after.display_avatar.url + ) + + embed.add_field(name="User", value=after.mention, inline=True) + + added_roles = set(after.roles) - set(before.roles) + removed_roles = set(before.roles) - set(after.roles) + + if added_roles: + roles_added = ", ".join([role.mention for role in added_roles]) + embed.add_field(name="Roles Added", value=roles_added, inline=False) + + if removed_roles: + roles_removed = ", ".join([role.mention for role in removed_roles]) + embed.add_field( + name="Roles Removed", value=roles_removed, inline=False + ) + + embed.set_footer(text=f"User ID: {after.id}") + + await self._send_log( + after.guild, "member_moderation", embed, None, after.id + ) + + if before.nick != after.nick: + embed = self._create_modern_embed("Nickname Changed") + embed.set_author( + name=after.display_name, icon_url=after.display_avatar.url + ) + + embed.add_field(name="User", value=after.mention, inline=True) + embed.add_field( + name="Before", value=before.nick or "No nickname", inline=True + ) + embed.add_field( + name="After", value=after.nick or "No nickname", inline=True + ) + + embed.set_footer(text=f"User ID: {after.id}") + + await self._send_log( + after.guild, "member_moderation", embed, None, after.id + ) + except Exception as e: + logger.error(f"Error in on_member_update: {e}") + + @commands.Cog.listener() + async def on_member_ban( + self, guild: discord.Guild, user: Union[discord.User, discord.Member] + ): + """Log member bans with audit log information.""" + try: + embed = self._create_modern_embed("Member Banned") + embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) + + embed.add_field(name="User", value=f"{user.mention} ({user})", inline=True) + + audit_entry = await self._get_audit_log( + guild, discord.AuditLogAction.ban, user.id + ) + if audit_entry: + embed.add_field( + name="Banned by", value=audit_entry.user.mention, inline=True + ) + if audit_entry.reason: + embed.add_field( + name="Reason", value=audit_entry.reason[:1024], inline=False + ) + + embed.set_footer(text=f"User ID: {user.id}") + embed.set_thumbnail(url=user.display_avatar.url) + + await self._send_log(guild, "member_moderation", embed, None, user.id) + except Exception as e: + logger.error(f"Error in on_member_ban: {e}") + + @commands.Cog.listener() + async def on_member_unban(self, guild: discord.Guild, user: discord.User): + """Log member unbans.""" + try: + embed = self._create_modern_embed("Member Unbanned") + embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) + + embed.add_field(name="User", value=f"{user.mention} ({user})", inline=True) + + audit_entry = await self._get_audit_log( + guild, discord.AuditLogAction.unban, user.id + ) + if audit_entry: + embed.add_field( + name="Unbanned by", value=audit_entry.user.mention, inline=True + ) + if audit_entry.reason: + embed.add_field( + name="Reason", value=audit_entry.reason[:1024], inline=False + ) + + embed.set_footer(text=f"User ID: {user.id}") + embed.set_thumbnail(url=user.display_avatar.url) + + await self._send_log(guild, "member_moderation", embed, None, user.id) + except Exception as e: + logger.error(f"Error in on_member_unban: {e}") + + @commands.Cog.listener() + async def on_voice_state_update( + self, + member: discord.Member, + before: discord.VoiceState, + after: discord.VoiceState, + ): + """Log voice state changes.""" + try: + if before.channel == after.channel: + return + + embed = self._create_modern_embed("Voice State Changed") + embed.set_author( + name=member.display_name, icon_url=member.display_avatar.url + ) + + embed.add_field(name="User", value=member.mention, inline=True) + + if before.channel and after.channel: + embed.add_field(name="Action", value="Moved channels", inline=True) + embed.add_field(name="From", value=before.channel.mention, inline=True) + embed.add_field(name="To", value=after.channel.mention, inline=True) + elif after.channel: + embed.add_field(name="Action", value="Joined voice", inline=True) + embed.add_field( + name="Channel", value=after.channel.mention, inline=True + ) + elif before.channel: + embed.add_field(name="Action", value="Left voice", inline=True) + embed.add_field( + name="Channel", value=before.channel.mention, inline=True + ) + + embed.set_footer(text=f"User ID: {member.id}") + + await self._send_log(member.guild, "voice_events", embed, None, member.id) + except Exception as e: + logger.error(f"Error in on_voice_state_update: {e}") + + @commands.Cog.listener() + async def on_guild_channel_create(self, channel): + """Log channel creation.""" + try: + embed = self._create_modern_embed("Channel Created") + + embed.add_field(name="Channel", value=channel.mention, inline=True) + embed.add_field(name="Type", value=str(channel.type).title(), inline=True) + embed.add_field( + name="Category", + value=channel.category.name if channel.category else "None", + inline=True, + ) + + audit_entry = await self._get_audit_log( + channel.guild, discord.AuditLogAction.channel_create, channel.id + ) + if audit_entry: + embed.add_field( + name="Created by", value=audit_entry.user.mention, inline=True + ) + + embed.set_footer(text=f"Channel ID: {channel.id}") + + await self._send_log(channel.guild, "channel_events", embed, channel.id) + except Exception as e: + logger.error(f"Error in on_guild_channel_create: {e}") + + @commands.Cog.listener() + async def on_guild_channel_delete(self, channel): + """Log channel deletion.""" + try: + embed = self._create_modern_embed("Channel Deleted") + + embed.add_field(name="Channel", value=f"#{channel.name}", inline=True) + embed.add_field(name="Type", value=str(channel.type).title(), inline=True) + embed.add_field( + name="Category", + value=channel.category.name if channel.category else "None", + inline=True, + ) + + audit_entry = await self._get_audit_log( + channel.guild, discord.AuditLogAction.channel_delete, channel.id + ) + if audit_entry: + embed.add_field( + name="Deleted by", value=audit_entry.user.mention, inline=True + ) + + embed.set_footer(text=f"Channel ID: {channel.id}") + + await self._send_log(channel.guild, "channel_events", embed, channel.id) + except Exception as e: + logger.error(f"Error in on_guild_channel_delete: {e}") + + @commands.Cog.listener() + async def on_guild_channel_update(self, before, after): + """Log channel updates.""" + try: + changes = [] + + if before.name != after.name: + changes.append(f"Name: {before.name} → {after.name}") + + if getattr(before, "topic", None) != getattr(after, "topic", None): + changes.append( + f"Topic: {before.topic or 'None'} → {after.topic or 'None'}" + ) + + if before.category != after.category: + before_cat = before.category.name if before.category else "None" + after_cat = after.category.name if after.category else "None" + changes.append(f"Category: {before_cat} → {after_cat}") + + if not changes: + return + + embed = self._create_modern_embed("Channel Updated") + embed.add_field(name="Channel", value=after.mention, inline=True) + embed.add_field(name="Changes", value="\n".join(changes), inline=False) + + audit_entry = await self._get_audit_log( + after.guild, discord.AuditLogAction.channel_update, after.id + ) + if audit_entry: + embed.add_field( + name="Updated by", value=audit_entry.user.mention, inline=True + ) + + embed.set_footer(text=f"Channel ID: {after.id}") + + await self._send_log(after.guild, "channel_events", embed, after.id) + except Exception as e: + logger.error(f"Error in on_guild_channel_update: {e}") + + @commands.Cog.listener() + async def on_guild_role_create(self, role): + """Log role creation.""" + try: + embed = self._create_modern_embed("Role Created") + + embed.add_field(name="Role", value=role.mention, inline=True) + embed.add_field(name="Color", value=str(role.color), inline=True) + embed.add_field( + name="Mentionable", + value="Yes" if role.mentionable else "No", + inline=True, + ) + + audit_entry = await self._get_audit_log( + role.guild, discord.AuditLogAction.role_create, role.id + ) + if audit_entry: + embed.add_field( + name="Created by", value=audit_entry.user.mention, inline=True + ) + + embed.set_footer(text=f"Role ID: {role.id}") + + await self._send_log(role.guild, "role_events", embed) + except Exception as e: + logger.error(f"Error in on_guild_role_create: {e}") + + @commands.Cog.listener() + async def on_guild_role_delete(self, role): + """Log role deletion.""" + try: + embed = self._create_modern_embed("Role Deleted") + + embed.add_field(name="Role", value=f"@{role.name}", inline=True) + embed.add_field(name="Color", value=str(role.color), inline=True) + embed.add_field(name="Members", value=str(len(role.members)), inline=True) + + audit_entry = await self._get_audit_log( + role.guild, discord.AuditLogAction.role_delete, role.id + ) + if audit_entry: + embed.add_field( + name="Deleted by", value=audit_entry.user.mention, inline=True + ) + + embed.set_footer(text=f"Role ID: {role.id}") + + await self._send_log(role.guild, "role_events", embed) + except Exception as e: + logger.error(f"Error in on_guild_role_delete: {e}") + + @commands.Cog.listener() + async def on_guild_role_update(self, before, after): + """Log role updates.""" + try: + changes = [] + + if before.name != after.name: + changes.append(f"Name: {before.name} → {after.name}") + + if before.color != after.color: + changes.append(f"Color: {before.color} → {after.color}") + + if before.mentionable != after.mentionable: + changes.append( + f"Mentionable: {before.mentionable} → {after.mentionable}" + ) + + if before.permissions != after.permissions: + changes.append("Permissions updated") + + if not changes: + return + + embed = self._create_modern_embed("Role Updated") + embed.add_field(name="Role", value=after.mention, inline=True) + embed.add_field(name="Changes", value="\n".join(changes), inline=False) + + audit_entry = await self._get_audit_log( + after.guild, discord.AuditLogAction.role_update, after.id + ) + if audit_entry: + embed.add_field( + name="Updated by", value=audit_entry.user.mention, inline=True + ) + + embed.set_footer(text=f"Role ID: {after.id}") + + await self._send_log(after.guild, "role_events", embed) + except Exception as e: + logger.error(f"Error in on_guild_role_update: {e}") + + @commands.Cog.listener() + async def on_guild_update(self, before, after): + """Log guild/server updates.""" + try: + changes = [] + + if before.name != after.name: + changes.append(f"Name: {before.name} → {after.name}") + + if before.description != after.description: + changes.append(f"Description updated") + + if before.verification_level != after.verification_level: + changes.append( + f"Verification Level: {before.verification_level} → {after.verification_level}" + ) + + if not changes: + return + + embed = self._create_modern_embed("Server Updated") + embed.add_field(name="Changes", value="\n".join(changes), inline=False) + + audit_entry = await self._get_audit_log( + after, discord.AuditLogAction.guild_update + ) + if audit_entry: + embed.add_field( + name="Updated by", value=audit_entry.user.mention, inline=True + ) + + embed.set_footer(text=f"Guild ID: {after.id}") + + await self._send_log(after, "system_events", embed) + except Exception as e: + logger.error(f"Error in on_guild_update: {e}") + + @commands.Cog.listener() + async def on_guild_emojis_update(self, guild, before, after): + """Log emoji updates.""" + try: + added_emojis = set(after) - set(before) + removed_emojis = set(before) - set(after) + + if added_emojis: + for emoji in added_emojis: + embed = self._create_modern_embed("Emoji Added") + embed.add_field( + name="Emoji", value=f"{emoji} :{emoji.name}:", inline=True + ) + embed.add_field(name="ID", value=str(emoji.id), inline=True) + embed.add_field( + name="Animated", + value="Yes" if emoji.animated else "No", + inline=True, + ) + + audit_entry = await self._get_audit_log( + guild, discord.AuditLogAction.emoji_create, emoji.id + ) + if audit_entry: + embed.add_field( + name="Created by", + value=audit_entry.user.mention, + inline=True, + ) + + embed.set_footer(text=f"Emoji ID: {emoji.id}") + await self._send_log(guild, "emoji_events", embed) + + if removed_emojis: + for emoji in removed_emojis: + embed = self._create_modern_embed("Emoji Removed") + embed.add_field(name="Emoji", value=f":{emoji.name}:", inline=True) + embed.add_field(name="ID", value=str(emoji.id), inline=True) + embed.add_field( + name="Animated", + value="Yes" if emoji.animated else "No", + inline=True, + ) + + audit_entry = await self._get_audit_log( + guild, discord.AuditLogAction.emoji_delete, emoji.id + ) + if audit_entry: + embed.add_field( + name="Deleted by", + value=audit_entry.user.mention, + inline=True, + ) + + embed.set_footer(text=f"Emoji ID: {emoji.id}") + await self._send_log(guild, "emoji_events", embed) + except Exception as e: + logger.error(f"Error in on_guild_emojis_update: {e}") + + @commands.Cog.listener() + async def on_reaction_add(self, reaction, user): + """Log reaction additions (optional).""" + try: + if not reaction.message.guild or user.bot: + return + + embed = self._create_modern_embed("Reaction Added") + embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) + + embed.add_field(name="User", value=user.mention, inline=True) + embed.add_field(name="Reaction", value=str(reaction.emoji), inline=True) + embed.add_field( + name="Message", + value=f"[Jump to message]({reaction.message.jump_url})", + inline=True, + ) + embed.add_field( + name="Channel", value=reaction.message.channel.mention, inline=True + ) + + embed.set_footer( + text=f"User ID: {user.id} • Message ID: {reaction.message.id}" + ) + + await self._send_log( + reaction.message.guild, + "reaction_events", + embed, + reaction.message.channel.id, + user.id, + ) + except Exception as e: + logger.error(f"Error in on_reaction_add: {e}") + + @commands.Cog.listener() + async def on_reaction_remove(self, reaction, user): + """Log reaction removals (optional).""" + try: + if not reaction.message.guild or user.bot: + return + + embed = self._create_modern_embed("Reaction Removed") + embed.set_author(name=user.display_name, icon_url=user.display_avatar.url) + + embed.add_field(name="User", value=user.mention, inline=True) + embed.add_field(name="Reaction", value=str(reaction.emoji), inline=True) + embed.add_field( + name="Message", + value=f"[Jump to message]({reaction.message.jump_url})", + inline=True, + ) + embed.add_field( + name="Channel", value=reaction.message.channel.mention, inline=True + ) + + embed.set_footer( + text=f"User ID: {user.id} • Message ID: {reaction.message.id}" + ) + + await self._send_log( + reaction.message.guild, + "reaction_events", + embed, + reaction.message.channel.id, + user.id, + ) + except Exception as e: + logger.error(f"Error in on_reaction_remove: {e}") + + +async def setup(bot): + """Setup function for the cog.""" + await bot.add_cog(Logging(bot)) diff --git a/bot/cogs/commands/map.py b/bot/cogs/commands/map.py new file mode 100644 index 0000000..ed97f8f --- /dev/null +++ b/bot/cogs/commands/map.py @@ -0,0 +1,274 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import DELETE +from discord.ext import commands +from discord import ui, ButtonStyle, SelectOption +from discord.ui import LayoutView, TextDisplay, Separator, ActionRow, MediaGallery +import requests +import asyncio +from utils.Tools import * +from utils.cv2 import CV2, build_container +from utils.config import * +class MapView(LayoutView): + def __init__(self, bot, location, ctx): + super().__init__(timeout=None) + self.bot = bot + self.location = location + self.ctx = ctx + self.zoom_level = 14 + self.map_style = 'map' + self.map_size = '1200,900' + self.coordinates = self.get_coordinates(location) + self.latitude, self.longitude = None, None + if self.coordinates != (None, None): + self.latitude, self.longitude = float(self.coordinates[0]), float(self.coordinates[1]) + + self.update_map() + + b_left = ui.Button(label="", emoji="⬅️", style=ButtonStyle.secondary) + b_left.callback = self.move_left + b_up = ui.Button(label="", emoji="⬆️", style=ButtonStyle.secondary) + b_up.callback = self.move_up + b_delete = ui.Button(label="", emoji=DELETE, style=ButtonStyle.danger) + b_delete.callback = self.delete_embed + b_down = ui.Button(label="", emoji="⬇️", style=ButtonStyle.secondary) + b_down.callback = self.move_down + b_right = ui.Button(label="", emoji="➡️", style=ButtonStyle.secondary) + b_right.callback = self.move_right + + self.add_item(ActionRow(b_left, b_up, b_delete, b_down, b_right)) + + b_zin = ui.Button(label="Zoom In", style=ButtonStyle.primary) + b_zin.callback = self.zoom_in + b_zout = ui.Button(label="Zoom Out", style=ButtonStyle.primary) + b_zout.callback = self.zoom_out + b_coords = ui.Button(label="Enter Coordinates", style=ButtonStyle.primary) + b_coords.callback = self.enter_coordinates + b_addr = ui.Button(label="Enter Address", style=ButtonStyle.success) + b_addr.callback = self.enter_address + + self.add_item(ActionRow(b_zin, b_zout, b_coords, b_addr)) + + self.add_item(ActionRow(MapStyleSelect(self))) + self.add_item(ActionRow(MapSizeSelect(self))) + + self.build_ui() + + def build_ui(self): + + container = build_container( + TextDisplay(f"**Map of {self.location}**"), + Separator(visible=True), + TextDisplay( + f"🌐 **[Open in Webpage](https://www.openstreetmap.org/?mlat={self.latitude}&mlon={self.longitude}&zoom={self.zoom_level})**\n" + f"🔍 **Current Zoom Level:** {str(self.zoom_level)}\n" + f"🗺️ **Map Style:** {self.map_style}\n" + f"📏 **Map Size:** {self.map_size}\n" + f"📍 **Current Coordinates:** {self.latitude}, {self.longitude}" + ) + ) + + gallery = MediaGallery() + gallery.add_item(media=self.map_url) + container.add_item(gallery) + + + self.children = [c for c in self.children if not isinstance(c, type(container))] + self.add_item(container) + + + def get_coordinates(self, location): + try: + headers = {'User-Agent': f'{BRAND_NAME} Bot (https://codexdevs.in)'} + response = requests.get(f'https://nominatim.openstreetmap.org/search?q={location}&format=json', headers=headers) + response.raise_for_status() + data = response.json()[0] + return data['lat'], data['lon'] + except (requests.RequestException, IndexError) as e: + print(f"Failed to get coordinates: {e}") + return None, None + + def update_map(self): + if self.latitude is None or self.longitude is None: + return + self.map_url = f'https://www.mapquestapi.com/staticmap/v5/map?key=E2SaL3qiTpXQ43nxZFBp0wzEnBI6pqbG¢er={self.latitude},{self.longitude}&zoom={self.zoom_level}&size={self.map_size}&type={self.map_style}' + self.build_ui() + + async def update_embed(self, interaction: discord.Interaction): + if self.latitude is None or self.longitude is None: + await interaction.response.send_message("Failed to retrieve map data. Please try again.", ephemeral=True) + return + + try: + await interaction.message.edit(view=self) + except Exception as e: + await interaction.response.send_message(f"Error updating message: {e}", ephemeral=True) + + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user != self.ctx.author: + await interaction.response.send_message( + view=CV2("⚠️ Access Denied", "Sorry only the requested author can control this"), + ephemeral=True + ) + return False + return True + + async def move_left(self, interaction: discord.Interaction): + if self.longitude is not None: + self.longitude -= 0.01 + self.update_map() + await self.update_embed(interaction) + await interaction.response.send_message(view=CV2("✅ Map Updated", "Moved left."), ephemeral=True) + + async def move_up(self, interaction: discord.Interaction): + if self.latitude is not None: + self.latitude += 0.01 + self.update_map() + await self.update_embed(interaction) + await interaction.response.send_message(view=CV2("✅ Map Updated", "Moved up."), ephemeral=True) + + async def delete_embed(self, interaction: discord.Interaction): + try: + await interaction.message.delete() + except Exception as e: + await interaction.response.send_message(f"Error deleting message: {e}", ephemeral=True) + + async def move_down(self, interaction: discord.Interaction): + if self.latitude is not None: + self.latitude -= 0.01 + self.update_map() + await self.update_embed(interaction) + await interaction.response.send_message(view=CV2("✅ Map Updated", "Moved down."), ephemeral=True) + + async def move_right(self, interaction: discord.Interaction): + if self.longitude is not None: + self.longitude += 0.01 + self.update_map() + await self.update_embed(interaction) + await interaction.response.send_message(view=CV2("✅ Map Updated", "Moved right."), ephemeral=True) + + async def zoom_in(self, interaction: discord.Interaction): + print("Zooming in") + self.zoom_level = min(self.zoom_level + 1, 18) + self.update_map() + await self.update_embed(interaction) + await interaction.response.send_message(view=CV2("✅ Map Updated", "Zoomed in."), ephemeral=True) + + async def zoom_out(self, interaction: discord.Interaction): + print("Zooming out") + self.zoom_level = max(self.zoom_level - 1, 0) + self.update_map() + await self.update_embed(interaction) + await interaction.response.send_message(view=CV2("✅ Map Updated", "Zoomed Out."), ephemeral=True) + + async def enter_coordinates(self, interaction: discord.Interaction): + await interaction.response.send_message("Please enter the coordinates (latitude, longitude):", ephemeral=True) + + def check(message): + return message.author == interaction.user and message.channel == interaction.channel + + try: + coords_msg = await self.bot.wait_for('message', check=check, timeout=60) + coords = coords_msg.content.split(',') + if len(coords) == 2: + self.latitude, self.longitude = float(coords[0].strip()), float(coords[1].strip()) + self.update_map() + await self.update_embed(interaction) + await interaction.followup.send(view=CV2("✅ Map Updated", "Coordinates updated."), ephemeral=True) + else: + await interaction.followup.send("Invalid coordinates format. Please enter in the format 'latitude, longitude'.", ephemeral=True) + except asyncio.TimeoutError: + await interaction.followup.send("You took too long to respond. Please try again.", ephemeral=True) + + async def enter_address(self, interaction: discord.Interaction): + await interaction.response.send_message("Please enter the address:", ephemeral=True) + + def check(message): + return message.author == interaction.user and message.channel == interaction.channel + + try: + address_msg = await self.bot.wait_for('message', check=check, timeout=60) + address = address_msg.content + self.coordinates = self.get_coordinates(address) + if self.coordinates == (None, None): + await interaction.followup.send("Failed to retrieve coordinates for the address. Please try again.", ephemeral=True) + else: + self.latitude, self.longitude = float(self.coordinates[0]), float(self.coordinates[1]) + self.location = address + self.update_map() + await self.update_embed(interaction) + await interaction.followup.send(view=CV2("✅ Map Updated", "Address updated."), ephemeral=True) + except asyncio.TimeoutError: + await interaction.followup.send("You took too long to respond. Please try again.", ephemeral=True) + + +class MapStyleSelect(ui.Select): + def __init__(self, map_view): + super().__init__(placeholder='Select Map Style') + self.map_view = map_view + options = [ + SelectOption(label='Map', value='map'), + SelectOption(label='Satellite', value='sat'), + SelectOption(label='Hybrid', value='hyb'), + SelectOption(label='Light', value='light'), + SelectOption(label='Dark', value='dark'), + ] + self.options = options + + async def callback(self, interaction: discord.Interaction): + print(f"Changing map style to {self.values[0]}") + self.map_view.map_style = self.values[0] + self.map_view.update_map() + await self.map_view.update_embed(interaction) + await interaction.response.send_message(view=CV2("✅ Map Updated", "Map style updated successfully."), ephemeral=True) + +class MapSizeSelect(ui.Select): + def __init__(self, map_view): + super().__init__(placeholder='Select Map Size') + self.map_view = map_view + options = [ + SelectOption(label='400x300', value='400,300'), + SelectOption(label='800x600', value='800,600'), + SelectOption(label='1200x900', value='1200,900') + ] + self.options = options + + async def callback(self, interaction: discord.Interaction): + print(f"Changing map size to {self.values[0]}") + self.map_view.map_size = self.values[0] + self.map_view.update_map() + await self.map_view.update_embed(interaction) + await interaction.response.send_message(view=CV2("✅ Map Updated", "Map size updated successfully."), ephemeral=True) + + +class Map(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.hybrid_command(name="map", help="Shows a map of a location", usage="", description="Shows a map of a location") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + async def map(self, ctx, *, location: str): + view = MapView(self.bot, location, ctx) + if view.coordinates == (None, None): + await ctx.send(view=CV2("❌ Error", "Failed to retrieve coordinates for the location. Please try again.")) + return + await ctx.send(view=view) + +async def setup(bot): + await bot.add_cog(Map(bot)) diff --git a/bot/cogs/commands/messages.py b/bot/cogs/commands/messages.py new file mode 100644 index 0000000..a7fe522 --- /dev/null +++ b/bot/cogs/commands/messages.py @@ -0,0 +1,108 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ARROWRED +from discord.ui import LayoutView, TextDisplay, Separator, Container +from discord.ext import commands +import sqlite3 +from datetime import datetime +from utils.config import * + + +class MessagesView(LayoutView): + def __init__(self, member, total, today_count, daily_average, author): + super().__init__(timeout=None) + + self.add_item( + Container( + TextDisplay(f"**Messages Stats for {member.display_name}**"), + Separator(visible=True), + TextDisplay( + f"**User**: {member.mention}\n\n" + f"**Daily Messages**: {daily_average}\n" + f"**Today Messages**: {today_count}\n" + f"**Total Messages**: {total}\n\n" + f"**{ARROWRED}Upgrade Your Experience With [{BRAND_NAME} Noprefix](https://discord.gg/codexdev)**" + ), + ) + ) + + +class Messages(commands.Cog): + def __init__(self, client): + self.client = client + + @commands.Cog.listener() + async def on_message(self, message): + if message.author.bot or not message.guild: + return + + conn = sqlite3.connect("db/messages.db") + c = conn.cursor() + c.execute(""" + CREATE TABLE IF NOT EXISTS messages ( + guild_id INTEGER, + user_id INTEGER, + date TEXT, + count INTEGER + ) + """) + + today = datetime.utcnow().strftime("%Y-%m-%d") + c.execute( + "SELECT count FROM messages WHERE guild_id = ? AND user_id = ? AND date = ?", + (message.guild.id, message.author.id, today), + ) + result = c.fetchone() + + if result: + c.execute( + "UPDATE messages SET count = count + 1 WHERE guild_id = ? AND user_id = ? AND date = ?", + (message.guild.id, message.author.id, today), + ) + else: + c.execute( + "INSERT INTO messages (guild_id, user_id, date, count) VALUES (?, ?, ?, 1)", + (message.guild.id, message.author.id, today), + ) + + conn.commit() + conn.close() + + @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") + + conn = sqlite3.connect("db/messages.db") + c = conn.cursor() + c.execute( + "SELECT date, count FROM messages WHERE guild_id = ? AND user_id = ?", + (ctx.guild.id, member.id), + ) + data = c.fetchall() + conn.close() + + total = sum(row[1] for row in data) + today_count = sum(row[1] for row in data if row[0] == today) + unique_days = set(row[0] for row in data) + daily_average = round(total / len(unique_days), 2) if unique_days else 0 + + view = MessagesView(member, total, today_count, daily_average, ctx.author) + await ctx.send(view=view) + + +async def setup(client): + await client.add_cog(Messages(client)) diff --git a/bot/cogs/commands/minecraft.py b/bot/cogs/commands/minecraft.py new file mode 100644 index 0000000..dcfe80a --- /dev/null +++ b/bot/cogs/commands/minecraft.py @@ -0,0 +1,283 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands, tasks +from discord import app_commands, ui, File +from mcstatus import JavaServer, BedrockServer +import aiosqlite +import os +import re +from datetime import datetime +from PIL import Image, ImageDraw, ImageFont +import io +import asyncio +import base64 +from utils.emoji import SUCCESS, ERROR, WARNING_UNICODE, CLOCK, REFRESH, JAVA_COFFEE + +# --- Updated Asset Paths --- +ASSETS_DIR = "assets" +FONT_PATH = os.path.join(ASSETS_DIR, "fonts", "minecraft.ttf") +BACKGROUND_PATH = os.path.join(ASSETS_DIR, "background", "background.png") +DB_PATH = "db/minecraft.db" + +# --- Other Constants --- +REFRESH_COOLDOWN_SECONDS = 30 +CANVAS_WIDTH = 800 +CANVAS_HEIGHT = 125 + +# --- Emojis (imported from utils.emoji) --- +EMOJI_STATUS_ONLINE = "🟢" +EMOJI_STATUS_OFFLINE = "🔴" +EMOJI_PLAYERS = "👥" +EMOJI_INFO = "💻" +EMOJI_SUCCESS = SUCCESS +EMOJI_ERROR = ERROR +EMOJI_WARNING = WARNING_UNICODE +EMOJI_CLOCK = CLOCK +EMOJI_REFRESH = REFRESH +EMOJI_JAVA = JAVA_COFFEE +EMOJI_BEDROCK = "📱" +EMOJI_PROXY = "🔌" + +class SetupModal(ui.Modal, title="Minecraft Server Setup"): + server_ip = ui.TextInput(label="Enter Server IP Address", placeholder="e.g., play.hypixel.net", required=True) + server_port = ui.TextInput(label="Enter Server Port (Optional)", placeholder="Leave blank for default ports", required=False) + + def __init__(self, cog): + super().__init__(timeout=None) + self.cog = cog + + async def on_submit(self, interaction: discord.Interaction): + await interaction.response.defer(ephemeral=True) + ip, port_str = self.server_ip.value.strip(), self.server_port.value.strip() + port = int(port_str) if port_str.isdigit() else None + detected_type, status, _ = await self.cog.auto_detect_server(ip, port) + if not detected_type or not status or not status['online']: + await interaction.followup.send(f"{EMOJI_ERROR} Could not reach or identify the server. Check IP/Port and try again.", ephemeral=True) + return + embed, file, view = await self.cog.generate_response(interaction.guild, detected_type, ip, port, interaction.user.id) + sent_message = await interaction.channel.send(embed=embed, file=file, view=view) + await self.cog.save_status_message(interaction.guild.id, interaction.user.id, interaction.channel.id, sent_message.id, detected_type, ip, port) + await interaction.followup.send(f"{EMOJI_SUCCESS} Auto-updating status for `{ip}` set up!", ephemeral=True) + +class MinecraftView(ui.View): + def __init__(self, bot, server_type, ip, port, user_id): + super().__init__(timeout=None) + self.bot, self.server_type, self.ip, self.port, self.user_id = bot, server_type, ip, port, user_id + self.last_refresh = None + + @ui.button(label="Refresh", style=discord.ButtonStyle.secondary, custom_id="refresh_button", emoji=EMOJI_REFRESH) + async def refresh_button(self, interaction: discord.Interaction, button: discord.ui.Button): + if interaction.user.id != self.user_id: + return await interaction.response.send_message(f"{EMOJI_ERROR} You can't refresh this panel.", ephemeral=True) + now = datetime.utcnow() + if self.last_refresh and (now - self.last_refresh).total_seconds() < REFRESH_COOLDOWN_SECONDS: + return await interaction.response.send_message(f"{EMOJI_CLOCK} On cooldown. Wait `{REFRESH_COOLDOWN_SECONDS}` seconds.", ephemeral=True) + await interaction.response.defer(ephemeral=True) + cog = self.bot.get_cog("Minecraft") + embed, file, view = await cog.generate_response(interaction.guild, self.server_type, self.ip, self.port, self.user_id) + await interaction.message.edit(embed=embed, attachments=[file] if file else [], view=view) + await interaction.followup.send(f"{EMOJI_SUCCESS} Status refreshed.", ephemeral=True) + self.last_refresh = now + +class Minecraft(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) + os.makedirs(os.path.dirname(FONT_PATH), exist_ok=True) + os.makedirs(os.path.dirname(BACKGROUND_PATH), exist_ok=True) + self.bot.loop.create_task(self.init_db()) + self.refresh_all_statuses.start() + + async def init_db(self): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS mc_status_messages ( + guild_id INTEGER PRIMARY KEY, user_id INTEGER, channel_id INTEGER, + message_id INTEGER, server_type TEXT, server_ip TEXT, server_port INTEGER + )""") + await db.commit() + + def clean_motd(self, motd: str) -> str: + return re.sub(r'§.', '', motd).strip() + + async def auto_detect_server(self, ip: str, port: int = None): + try: + java_port = port or 25565 + server = JavaServer(ip, java_port) + status = await asyncio.wait_for(server.async_status(tries=1), timeout=5) + version_name = status.version.name.lower() + detected_type = 'velocity' if 'velocity' in version_name else 'bungeecord' if 'bungee' in version_name else 'java' + favicon = getattr(status, 'favicon', None) + motd_raw = status.description + full_text = "".join(part.get('text', '') for part in motd_raw.get('extra', [])) or motd_raw.get('text', '') if isinstance(motd_raw, dict) else str(motd_raw) + status_data = { + "motd": self.clean_motd(full_text) or "A Minecraft Server", + "players_online": status.players.online, "players_max": status.players.max, + "players_sample": status.players.sample, "version": status.version.name, "online": True} + return detected_type, status_data, favicon + except Exception: + pass + try: + bedrock_port = port or 19132 + server = BedrockServer(ip, bedrock_port) + status = await asyncio.wait_for(server.async_status(tries=1), timeout=5) + status_data = { + "motd": self.clean_motd(status.motd) or "A Minecraft Server", + "players_online": status.players.online, "players_max": status.players.max, + "players_sample": None, "version": status.version.name, "online": True} + return 'bedrock', status_data, None + except Exception: + pass + return None, {"online": False}, None + + async def create_status_image(self, status_data, ip, port, server_type, server_icon_b64): + try: + bg = Image.open(BACKGROUND_PATH).convert("RGBA") + img = Image.new("RGBA", (CANVAS_WIDTH, CANVAS_HEIGHT)) + for i in range(0, img.width, bg.width): + for j in range(0, img.height, bg.height): img.paste(bg, (i, j)) + draw = ImageDraw.Draw(img) + font_large, font_medium, font_small = ImageFont.truetype(FONT_PATH, 24), ImageFont.truetype(FONT_PATH, 20), ImageFont.truetype(FONT_PATH, 16) + if server_icon_b64: + icon_data = base64.b64decode(server_icon_b64.split(',')[-1]) + icon_img = Image.open(io.BytesIO(icon_data)).convert("RGBA").resize((80, 80), Image.Resampling.LANCZOS) + img.paste(icon_img, (20, (CANVAS_HEIGHT - 80) // 2), icon_img) + default_port = 25565 if server_type != 'bedrock' else 19132 + full_ip = f"{ip}:{port}" if port and port != default_port else ip + draw.text((120, 15), f"IP: {full_ip}", font=font_medium, fill=(255, 255, 255)) + player_text = f"{status_data.get('players_online', 0)}/{status_data.get('players_max', 0)}" + bbox = draw.textbbox((0, 0), player_text, font=font_medium) + draw.text((CANVAS_WIDTH - (bbox[2] - bbox[0]) - 20, 15), player_text, font=font_medium, fill=(255, 255, 255)) + motd = (status_data.get('motd', 'Offline') or "A Minecraft Server").split('')[0] + bbox = draw.textbbox((0,0), motd, font=font_large) + draw.text(((CANVAS_WIDTH - (bbox[2] - bbox[0])) // 2, 60), motd, font=font_large, fill=(0, 255, 255)) + power_text = "Powered by STACY" + bbox = draw.textbbox((0,0), power_text, font=font_small) + draw.text((CANVAS_WIDTH - (bbox[2] - bbox[0]) - 15, CANVAS_HEIGHT - 25), power_text, font=font_small, fill=(170, 170, 170)) + buffer = io.BytesIO() + img.save(buffer, "PNG") + buffer.seek(0) + return File(buffer, filename="status.png") + except FileNotFoundError: return None + except Exception: return None + + async def generate_response(self, guild, server_type, ip, port, user_id): + _, status, favicon = await self.auto_detect_server(ip, port) + if not status: status = {'online': False} + file = await self.create_status_image(status, ip, port, server_type, favicon) + is_online = status.get("online", False) + embed = discord.Embed(title=f"{EMOJI_STATUS_ONLINE if is_online else EMOJI_STATUS_OFFLINE} Minecraft Server Status", color=discord.Color.dark_green() if is_online else discord.Color.red()) + + default_port = 25565 if server_type != 'bedrock' else 19132 + full_ip = f"{ip}:{port}" if port and port != default_port else ip + type_emoji_map = {'java': EMOJI_JAVA, 'bedrock': EMOJI_BEDROCK, 'velocity': EMOJI_PROXY, 'bungeecord': EMOJI_PROXY} + type_emoji = type_emoji_map.get(server_type, EMOJI_INFO) + + info_value = (f"**IP:** `{full_ip}`\n" + f"**Type:** {type_emoji} {server_type.capitalize()}\n" + f"**Version:** `{status.get('version', 'Unknown')}`\n") + embed.add_field(name=f"{EMOJI_INFO} Server Information", value=info_value, inline=False) + + players_online = status.get('players_online', 0) + players_max = status.get('players_max', 0) + player_list_str = f"**Online:** `{players_online}/{players_max}`\n" + player_sample = status.get('players_sample') + if player_sample: + player_names = [player.name for player in player_sample] + player_list_str += "\n".join(f"`{i+1}.` {name}" for i, name in enumerate(player_names[:10])) + if len(player_names) > 10: + player_list_str += f"*...and {len(player_names) - 10} more*" + elif is_online: + player_list_str += "*Player list is hidden or unavailable.*" + + embed.add_field(name=f"{EMOJI_PLAYERS} Players", value=player_list_str, inline=False) + + if file: + embed.set_image(url="attachment://status.png") + + embed.set_footer(text="Last updated") + embed.timestamp = datetime.utcnow() + view = MinecraftView(self.bot, server_type, ip, port, user_id) + return embed, file, view + + async def save_status_message(self, guild_id, user_id, channel_id, message_id, server_type, ip, port): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute(""" + INSERT INTO mc_status_messages (guild_id, user_id, channel_id, message_id, server_type, server_ip, server_port) + VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(guild_id) DO UPDATE SET + user_id=excluded.user_id, channel_id=excluded.channel_id, message_id=excluded.message_id, + server_type=excluded.server_type, server_ip=excluded.server_ip, server_port=excluded.server_port + """, (guild_id, user_id, channel_id, message_id, server_type, ip, port)) + await db.commit() + + async def delete_status_message(self, guild_id): + async with aiosqlite.connect(DB_PATH) as db: + cursor = await db.execute("SELECT channel_id, message_id FROM mc_status_messages WHERE guild_id = ?", (guild_id,)) + if row := await cursor.fetchone(): + try: + channel = await self.bot.fetch_channel(row[0]); msg = await channel.fetch_message(row[1]); await msg.delete() + except (discord.NotFound, discord.Forbidden): pass + await db.execute("DELETE FROM mc_status_messages WHERE guild_id = ?", (guild_id,)); await db.commit() + return True + return False + + @tasks.loop(minutes=2) + async def refresh_all_statuses(self): + await self.bot.wait_until_ready() + async with aiosqlite.connect(DB_PATH) as db: + async with db.execute("SELECT guild_id, user_id, channel_id, message_id, server_type, server_ip, server_port FROM mc_status_messages") as cursor: + async for row in cursor: + try: + guild = self.bot.get_guild(row[0]) + if not guild: continue + channel = guild.get_channel(row[2]) + if not channel: continue + message = await channel.fetch_message(row[3]) + embed, file, view = await self.generate_response(guild, row[4], row[5], row[6], row[1]) + await message.edit(embed=embed, attachments=[file] if file else [], view=view) + await asyncio.sleep(2) + except discord.NotFound: await self.delete_status_message(row[0]) + except Exception as e: print(f"Auto-refresh error for guild {row[0]}: {e}") + + minecraft = app_commands.Group(name="minecraft", description="Commands for Minecraft server status.") + + @minecraft.command(name="setup", description="Set up the auto-updating Minecraft server status.") + @app_commands.checks.has_permissions(manage_guild=True) + async def setup_slash(self, interaction: discord.Interaction): + async with aiosqlite.connect(DB_PATH) as db: + if await (await db.execute("SELECT 1 FROM mc_status_messages WHERE guild_id = ?", (interaction.guild.id,))).fetchone(): + return await interaction.response.send_message(f"{EMOJI_WARNING} Setup already exists. Use `/minecraft reset`.", ephemeral=True) + await interaction.response.send_modal(SetupModal(self)) + + @minecraft.command(name="reset", description="Remove the Minecraft server status setup.") + @app_commands.checks.has_permissions(manage_guild=True) + async def reset_slash(self, interaction: discord.Interaction): + await interaction.response.defer(ephemeral=True) + removed = await self.delete_status_message(interaction.guild.id) + await interaction.followup.send(f"{EMOJI_SUCCESS} Minecraft status panel removed." if removed else f"{EMOJI_WARNING} No setup found.", ephemeral=True) + + @minecraft.command(name="status", description="Get a one-time status of the configured server.") + async def status_slash(self, interaction: discord.Interaction): + await interaction.response.defer(ephemeral=False) + async with aiosqlite.connect(DB_PATH) as db: + row = await (await db.execute("SELECT server_type, server_ip, server_port, user_id FROM mc_status_messages WHERE guild_id = ?", (interaction.guild.id,))).fetchone() + if not row: + return await interaction.followup.send(f"{EMOJI_WARNING} No server configured. Use `/minecraft setup`.", ephemeral=True) + embed, file, _ = await self.generate_response(interaction.guild, row[0], row[1], row[2], row[3]) + await interaction.followup.send(embed=embed, file=file, ephemeral=False) + +async def setup(bot: commands.Bot): + await bot.add_cog(Minecraft(bot)) \ No newline at end of file diff --git a/bot/cogs/commands/msgpack.py b/bot/cogs/commands/msgpack.py new file mode 100644 index 0000000..0a33195 --- /dev/null +++ b/bot/cogs/commands/msgpack.py @@ -0,0 +1,82 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import sqlite3 +from datetime import datetime + +class Messagespack(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @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: + return await ctx.send("Amount must be greater than 0.") + + today = datetime.utcnow().strftime("%Y-%m-%d") + conn = sqlite3.connect("db/messages.db") + c = conn.cursor() + + c.execute("SELECT count FROM messages WHERE guild_id = ? AND user_id = ? AND date = ?", + (ctx.guild.id, member.id, today)) + result = c.fetchone() + + if result: + c.execute("UPDATE messages SET count = count + ? WHERE guild_id = ? AND user_id = ? AND date = ?", + (amount, ctx.guild.id, member.id, today)) + else: + c.execute("INSERT INTO messages (guild_id, user_id, date, count) VALUES (?, ?, ?, ?)", + (ctx.guild.id, member.id, today, amount)) + + conn.commit() + conn.close() + await ctx.send(f"Added {amount} messages to {member.mention} for today.") + + @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: + return await ctx.send("Amount must be greater than 0.") + + today = datetime.utcnow().strftime("%Y-%m-%d") + conn = sqlite3.connect("db/messages.db") + c = conn.cursor() + + c.execute("SELECT count FROM messages WHERE guild_id = ? AND user_id = ? AND date = ?", + (ctx.guild.id, member.id, today)) + result = c.fetchone() + + if result: + new_count = max(0, result[0] - amount) + c.execute("UPDATE messages SET count = ? WHERE guild_id = ? AND user_id = ? AND date = ?", + (new_count, ctx.guild.id, member.id, today)) + conn.commit() + await ctx.send(f"Removed {amount} messages from {member.mention} for today.") + else: + await ctx.send(f"{member.mention} has no messages recorded for today.") + conn.close() + + @commands.command(name="clearmessage", aliases=["clearmsg"]) + @commands.has_permissions(manage_messages=True) + async def clearmessage(self, ctx, member: discord.Member): + conn = sqlite3.connect("db/messages.db") + c = conn.cursor() + c.execute("DELETE FROM messages WHERE guild_id = ? AND user_id = ?", + (ctx.guild.id, member.id)) + conn.commit() + conn.close() + await ctx.send(f"All messages cleared for {member.mention}.") \ No newline at end of file diff --git a/bot/cogs/commands/music.py b/bot/cogs/commands/music.py new file mode 100644 index 0000000..5d9c1f8 --- /dev/null +++ b/bot/cogs/commands/music.py @@ -0,0 +1,988 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import os +import random +import discord +from utils.emoji import FORWARD, ICONLOAD, ICONS_MUSIC, ICONS_PAUSE, ICONS_WARNING_ALT1, MUSICSTOP_ICONS, MUSIC_ALT1, MUTE, REWIND, REWIND_ALT1, SHUFFLE, SKIP, TICK, WARNING, ZMUSICPAUSE, ZPLUS, ZUNMUTE +from discord.ext import commands, tasks +import datetime +from discord.ui import Button, View, LayoutView, TextDisplay, Separator, Container, ActionRow +import wavelink +from wavelink.enums import TrackSource +from utils import Paginator, DescriptionEmbedPaginator +from core import Cog, zyrox, Context +from PIL import Image, ImageDraw, ImageFont, ImageOps +import io +import aiohttp +from typing import cast +import asyncio +from utils.Tools import * +from utils.cv2 import CV2, build_container, CV2Embed +track_histories = {} +import base64 +import re +from utils.config import * + +SPOTIFY_TRACK_REGEX = r"https?://open\.spotify\.com/track/([a-zA-Z0-9]+)" +SPOTIFY_PLAYLIST_REGEX = r"https?://open\.spotify\.com/playlist/([a-zA-Z0-9]+)" +SPOTIFY_ALBUM_REGEX = r"https?://open\.spotify\.com/album/([a-zA-Z0-9]+)" + +class SpotifyAPI: + BASE_URL = "https://api.spotify.com/v1" + + def __init__(self, client_id, client_secret): + self.client_id = client_id + self.client_secret = client_secret + self.token = None + + async def get_token(self): + auth_url = "https://accounts.spotify.com/api/token" + auth_value = base64.b64encode(f"{self.client_id}:{self.client_secret}".encode('utf-8')).decode('utf-8') + headers = {"Authorization": f"Basic {auth_value}"} + data = {"grant_type": "client_credentials"} + async with aiohttp.ClientSession() as session: + async with session.post(auth_url, headers=headers, data=data) as response: + text = await response.text() + if response.status != 200: + raise Exception(f"Failed to fetch token: {response.status}, response: {text}") + self.token = (await response.json()).get("access_token") + + async def get(self, endpoint, params=None): + retries = 2 + for attempt in range(retries): + if not self.token or attempt > 0: + await self.get_token() + + url = f"{self.BASE_URL}/{endpoint}" + headers = {"Authorization": f"Bearer {self.token}"} + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=headers, params=params) as response: + if response.status == 401 and attempt < retries - 1: + continue + elif response.status != 200: + raise Exception(f"Failed to fetch data from Spotify: {response.status}") + return await response.json() + raise Exception("Exceeded max retries to fetch Spotify data") + + + async def get_track(self, track_id): + return await self.get(f"tracks/{track_id}") + + async def get_playlist(self, playlist_id): + return await self.get(f"playlists/{playlist_id}") + +spotify_api = SpotifyAPI(client_id="ac2b614ca5ce46a18dfd1d3475fd6fd9", client_secret="df7bec95ae88438e8286db597bac8621") + +class PlatformSelectView(LayoutView): + def __init__(self, ctx, query): + super().__init__(timeout=60) + self.ctx = ctx + self.query = query + + platforms = [ + ("YouTube", "ytsearch", discord.ButtonStyle.red), + ("JioSaavn", "jssearch", discord.ButtonStyle.green), + ("SoundCloud", "scsearch", discord.ButtonStyle.grey), + ] + buttons = [] + for name, source, style in platforms: + btn = Button(label=name, style=style) + btn.callback = self.create_callback(source) + buttons.append(btn) + + container = build_container( + TextDisplay("**Select a platform to search from:**"), + Separator(visible=True), + TextDisplay("Click a button below to choose."), + Separator(visible=True), + ActionRow(*buttons), + ) + self.add_item(container) + + def create_callback(self, source): + async def callback(interaction: discord.Interaction): + if interaction.user != self.ctx.author: + await interaction.response.send_message("Only the command author can select a platform.", ephemeral=True) + return + await interaction.response.send_message(f"Searching...", ephemeral=True) + await self.perform_search(source) + await interaction.message.delete() + return callback + + async def perform_search(self, source): + results = await wavelink.Playable.search(self.query, source=source) + if not results: + return await self.ctx.send(view=CV2("No results found.")) + + top_results = results[:5] + await self.ctx.send(view=SearchResultView(self.ctx, top_results, self.query, source)) + + + +class SearchResultView(LayoutView): + def __init__(self, ctx, results, query="", source=""): + super().__init__(timeout=60) + self.ctx = ctx + self.results = results + + lines = [f"**Top 5 Results for '{query}'**", ""] + for i, track in enumerate(results, start=1): + dur = f"{track.length // 1000 // 60}:{track.length // 1000 % 60:02d}" + lines.append(f"`{i}.` **{track.title}** • `{dur}`") + + buttons = [] + for i in range(min(5, len(results))): + btn = Button(label=str(i + 1), style=discord.ButtonStyle.primary) + btn.callback = self.create_callback(i) + buttons.append(btn) + + container = build_container( + TextDisplay("\n".join(lines)), + Separator(visible=True), + ActionRow(*buttons), + ) + self.add_item(container) + + def create_callback(self, index): + async def callback(interaction: discord.Interaction): + if interaction.user != self.ctx.author: + await interaction.response.send_message("Only the command author can select a track.", ephemeral=True) + return + + track = self.results[index] + vc = self.ctx.voice_client or await self.ctx.author.voice.channel.connect(cls=wavelink.Player) + vc.ctx = self.ctx + + if not vc.playing: + await vc.play(track) + await interaction.response.send_message(f"Started playing `{track.title}`.") + await self.ctx.cog.display_player_embed(vc, track, self.ctx) + else: + await vc.queue.put_wait(track) + await interaction.response.send_message(f"Added `{track.title}` to the queue.") + + return callback + + +class MusicControlView(LayoutView): + def __init__(self, player, ctx, track=None, autoplay=False): + super().__init__(timeout=None) + self.player = player + self.ctx = ctx + self._build_ui(track, autoplay) + + def _build_ui(self, track=None, autoplay=False): + from discord.ui import Section, Thumbnail, TextDisplay + + items = [] + + if track: + sec = track.length // 1000 + duration = f"{sec // 60:02d}:{sec % 60:02d}" + + requester = self.ctx.author.display_name + if autoplay: requester += " (Autoplay)" + + title_text = f"**Now Playing [{track.title}]({track.uri})**" + items.append(TextDisplay(title_text)) + + bullet_text = ( + f"• **Author:** `{track.author}`\n" + f"• **Duration:** `{duration}`\n" + f"• **Requester:** {requester}" + ) + + if getattr(track, 'artwork', None): + accessory = Thumbnail(media=track.artwork) + items.append(Section(TextDisplay(bullet_text), accessory=accessory)) + else: + items.append(TextDisplay(bullet_text)) + + # Row 1 buttons (Main 5 from screenshot with exact styles) + btn_pause = Button(emoji=ICONS_PAUSE, label="Pause", style=discord.ButtonStyle.primary) + btn_skip = Button(emoji=SKIP, label="Skip", style=discord.ButtonStyle.secondary) + btn_stop = Button(emoji=MUSICSTOP_ICONS, label="Stop", style=discord.ButtonStyle.danger) + btn_loop = Button(emoji=ICONLOAD, label="Loop", style=discord.ButtonStyle.secondary) + btn_autoplay = Button(emoji=ZUNMUTE, label="Autoplay", style=discord.ButtonStyle.secondary) + + # Row 2 buttons (Remaining controls) + btn_prev = Button(emoji=REWIND, style=discord.ButtonStyle.secondary) + btn_shuffle = Button(emoji=SHUFFLE, style=discord.ButtonStyle.secondary) + btn_rewind = Button(emoji=REWIND_ALT1, style=discord.ButtonStyle.secondary) + btn_forward = Button(emoji=FORWARD, style=discord.ButtonStyle.secondary) + btn_replay = Button(emoji=ICONS_MUSIC, style=discord.ButtonStyle.secondary) + + btn_pause.callback = self._cb_pause + btn_skip.callback = self._cb_skip + btn_stop.callback = self._cb_stop + btn_loop.callback = self._cb_loop + btn_autoplay.callback = self._cb_autoplay + + btn_prev.callback = self._cb_previous + btn_shuffle.callback = self._cb_shuffle + btn_rewind.callback = self._cb_rewind + btn_forward.callback = self._cb_forward + btn_replay.callback = self._cb_replay + + items.append(ActionRow(btn_pause, btn_skip, btn_stop, btn_loop, btn_autoplay)) + items.append(ActionRow(btn_prev, btn_shuffle, btn_rewind, btn_forward, btn_replay)) + + container = build_container(*items) + self.add_item(container) + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if not self.ctx.voice_client or not self.player.playing: + await interaction.response.send_message("I'm not currently playing this anymore.", ephemeral=True) + return False + if interaction.user in self.ctx.voice_client.channel.members: + return True + await interaction.response.send_message("Only members in the same voice channel can control the player.", ephemeral=True) + return False + + async def _cb_autoplay(self, interaction): + self.player.autoplay = ( + wavelink.AutoPlayMode.enabled if self.player.autoplay != wavelink.AutoPlayMode.enabled else wavelink.AutoPlayMode.disabled + ) + await interaction.response.send_message(f"Autoplay {'enabled' if self.player.autoplay == wavelink.AutoPlayMode.enabled else 'disabled'} by **{interaction.user.display_name}**.") + + async def _cb_previous(self, interaction): + guild_id = interaction.guild.id + if guild_id in track_histories and len(track_histories[guild_id]) > 1: + track_histories[guild_id].pop() + previous_track = track_histories[guild_id][-1] + if self.player.playing: + await self.player.stop() + await self.ctx.voice_client.queue.put_wait(previous_track) + await interaction.response.send_message(f"Playing previous track: `{previous_track.title}`.") + else: + await interaction.response.send_message("No previous track available.", ephemeral=True) + + async def _cb_pause(self, interaction): + if self.player.paused: + await self.player.pause(False) + await self.player.channel.edit(status=f"{ICONS_PAUSE} Playing: {self.player.current.title}") + await interaction.response.send_message(f"Resumed by **{interaction.user.display_name}**.") + elif self.player.playing: + await self.player.pause(True) + await self.player.channel.edit(status=f"{ICONS_PAUSE} Paused: {self.player.current.title}") + await interaction.response.send_message(f"Paused by **{interaction.user.display_name}**.") + + async def _cb_skip(self, interaction): + if self.player.autoplay == wavelink.AutoPlayMode.enabled: + await self.player.stop() + return await interaction.response.send_message(f"Skipped by **{interaction.user.display_name}**.") + if self.player and self.player.playing and not self.player.queue.is_empty: + await self.player.stop() + await interaction.response.send_message(f"Skipped by **{interaction.user.display_name}**.") + else: + await interaction.response.send_message("No song in queue to skip.", ephemeral=True) + + async def _cb_loop(self, interaction): + self.player.queue.mode = wavelink.QueueMode.loop if self.player.queue.mode != wavelink.QueueMode.loop else wavelink.QueueMode.normal + await interaction.response.send_message(f"Loop {'enabled' if self.player.queue.mode == wavelink.QueueMode.loop else 'disabled'} by **{interaction.user.display_name}**.") + + async def _cb_shuffle(self, interaction): + if self.player.queue: + random.shuffle(self.player.queue) + await interaction.response.send_message(f"Queue shuffled by **{interaction.user.display_name}**.") + else: + await interaction.response.send_message("Queue is empty.", ephemeral=True) + + async def _cb_rewind(self, interaction): + if self.player.playing: + new_position = max(self.player.position - 10000, 0) + await self.player.seek(new_position) + await interaction.response.send_message("Rewinded 10 seconds.", ephemeral=True) + else: + await interaction.response.send_message("No track is currently playing.", ephemeral=True) + + async def _cb_stop(self, interaction): + if self.player: + voice_channel = self.player.channel + if voice_channel: + await voice_channel.edit(status=None) + await self.player.disconnect() + await interaction.response.send_message(f"Stopped and disconnected by **{interaction.user.display_name}**.") + else: + await interaction.response.send_message("Not connected.", ephemeral=True) + + async def _cb_forward(self, interaction): + if self.player.playing: + new_position = min(self.player.position + 10000, self.player.current.length) + await self.player.seek(new_position) + await interaction.response.send_message("Forwarded 10 seconds.", ephemeral=True) + else: + await interaction.response.send_message("No track is currently playing.", ephemeral=True) + + async def _cb_replay(self, interaction): + if self.player.playing: + await self.player.seek(0) + await interaction.response.send_message("Replaying the current track.", ephemeral=True) + else: + await interaction.response.send_message("No track is currently playing.", ephemeral=True) + + +class Music(commands.Cog): + def __init__(self, client: zyrox): + self.client = client + self.client.loop.create_task(self.connect_nodes()) + self.client.loop.create_task(self.monitor_inactivity()) + + self.inactivity_timeout = 120 + self.player_inactivity = {} + + async def monitor_inactivity(self): + while True: + for guild in self.client.guilds: + await self.check_inactivity(guild.id) + await asyncio.sleep(60) + + async def check_inactivity(self, guild_id): + guild = self.client.get_guild(guild_id) + if not guild: + return + + player = None + for vc in self.client.voice_clients: + if vc.guild.id == guild.id: + player = vc + break + + if player and player.playing and len(player.channel.members) == 1: + await self.inactivity_timer(guild) + + async def inactivity_timer(self, guild): + await asyncio.sleep(self.inactivity_timeout) + if len(guild.voice_channels[0].members) == 1: + player = None + for vc in self.client.voice_clients: + if vc.guild.id == guild.id: + player = vc + break + if player: + await player.disconnect(force=True) + try: + support = Button(label='Support', style=discord.ButtonStyle.link, url='https://discord.gg/codexdev') + vote = Button(label='Vote', style=discord.ButtonStyle.link, url='https://top.gg/bot//vote') + view = LayoutView(timeout=None) + container = build_container( + TextDisplay("**Inactive Timeout**"), + Separator(visible=True), + TextDisplay("Bot has been disconnected due to inactivity (being idle in Voice Channel) for more than 2 minutes."), + Separator(visible=True), + ActionRow(support, vote), + Separator(visible=True), + TextDisplay(f"*Thanks for choosing {BRAND_NAME}!*"), + ) + view.add_item(container) + await player.ctx.channel.send(view=view) + except: + pass + + async def connect_nodes(self) -> None: + host = os.getenv("LAVALINK_HOST", "lava-v4.ajieblogs.eu.org") + password = os.getenv("LAVALINK_PASSWORD", "https://dsc.gg/ajidevserver") + secure = os.getenv("LAVALINK_SECURE", "true").strip().lower() == "true" + port = os.getenv("LAVALINK_PORT", "").strip() + + if secure: + uri = f"https://{host}" + else: + uri = f"http://{host}:{port}" if port else f"http://{host}" + + nodes = [wavelink.Node(uri=uri, password=password)] + await wavelink.Pool.connect(nodes=nodes, client=self.client, cache_capacity=None) + + + async def display_player_embed(self, player, track, ctx, autoplay=False): + await ctx.send(view=MusicControlView(player, ctx, track, autoplay)) + + + async def on_track_end(self, payload: wavelink.TrackEndEventPayload): + player = payload.player + if not player.queue: + if player.queue.mode == wavelink.QueueMode.loop: + await player.play(payload.track) + elif player.autoplay == wavelink.AutoPlayMode.enabled: + await asyncio.sleep(5) + if player.current: + await self.display_player_embed(player, player.current, player.ctx, autoplay=True) + else: + await player.ctx.send(view=CV2("No suitable track found for autoplay.")) + else: + await player.disconnect() + support = Button(label='Support', style=discord.ButtonStyle.link, url='https://discord.gg/codexdev') + vote = Button(label='Vote', style=discord.ButtonStyle.link, url='https://top.gg/bot//vote') + view = LayoutView(timeout=None) + container = build_container( + TextDisplay("**Queue Ended**"), + Separator(visible=True), + TextDisplay("All tracks have been played, leaving the voice channel."), + Separator(visible=True), + ActionRow(support, vote), + ) + view.add_item(container) + await player.ctx.send(view=view) + else: + next_track = await player.queue.get_wait() + await player.play(next_track) + await self.display_player_embed(player, next_track, player.ctx) + + + + async def play_source(self, ctx, query): + if not ctx.author.voice: + await ctx.send(view=CV2(f"{WARNING} you need to be in a voice channel to use this command.")) + return + + vc = ctx.voice_client or await ctx.author.voice.channel.connect(cls=wavelink.Player) + vc.ctx = ctx + + + if vc.playing: + if ctx.voice_client and ctx.voice_client.channel != ctx.author.voice.channel: + await ctx.send(view=CV2(f"You must be connected to {ctx.voice_client.channel.mention} to play.")) + return + vc.autoplay = wavelink.AutoPlayMode.disabled + + """if re.match(SPOTIFY_TRACK_REGEX, query): + await self.handle_spotify_link(ctx, vc, query, "track") + elif re.match(SPOTIFY_PLAYLIST_REGEX, query): + await self.handle_spotify_link(ctx, vc, query, "playlist") + elif re.match(SPOTIFY_ALBUM_REGEX, query): + await self.handle_spotify_link(ctx, vc, query, "album") + + return""" + + tracks = await wavelink.Playable.search(query) + if not tracks: + await ctx.send(view=CV2("No results found.")) + return + + if isinstance(tracks, wavelink.Playlist): + await vc.queue.put_wait(tracks.tracks) + await ctx.send(view=CV2(f"{ZPLUS} Added playlist [{tracks.name}](https://discord.gg/codexdev) with **{len(tracks.tracks)} songs** to the queue.")) + if not vc.playing: + track = await vc.queue.get_wait() + await vc.play(track) + await self.display_player_embed(vc, track, ctx) + else: + track = tracks[0] + await vc.queue.put_wait(track) + await ctx.send(view=CV2(f"{ZPLUS} Added [{track.title}](https://discord.gg/codexdev) to the queue.")) + if not vc.playing: + await vc.play(await vc.queue.get_wait()) + await self.display_player_embed(vc, track, ctx) + self.client.loop.create_task(self.check_inactivity(ctx.guild.id)) + # await interaction.response.defer() + + + + async def handle_spotify_link(self, ctx, vc, link, type_): + try: + if type_ == "track": + track_id = re.search(SPOTIFY_TRACK_REGEX, link).group(1) + track_info = await spotify_api.get_track(track_id) + + + title = track_info['name'] + author = ', '.join(artist['name'] for artist in track_info['artists']) + + + search_query = f"{title} by {author}" + search_results = await wavelink.Playable.search(search_query, source=wavelink.enums.TrackSource.YouTube) + + if not search_results: + await ctx.send("Can't play this track from Spotify, please try with another track.") + return + + track = search_results[0] + await vc.queue.put_wait(track) + await ctx.send(view=CV2(f"{ZPLUS} Added [{track.title}](https://discord.gg/codexdev) to the queue.")) + if not vc.playing: + await vc.play(track) + await self.display_player_embed(vc, track, ctx) + + #await self.display_player_embed(vc, track, ctx) + + elif type_ == "playlist": + lmao = await ctx.send("⏳ Processing to add tracks from the playlist, this may take a while...") + + playlist_id = re.search(SPOTIFY_PLAYLIST_REGEX, link).group(1) + playlist_info = await spotify_api.get(f"playlists/{playlist_id}") + tracks = playlist_info.get("tracks", {}).get("items", []) + playlist_length = len(tracks) + + if not tracks: + await ctx.send("No tracks found in the playlist.") + return + + c = 0 + for track in tracks: + title = track['track']['name'] + author = ', '.join(artist['name'] for artist in track['track']['artists']) + search_query = f"{title} {author}" + + track_results = await wavelink.Playable.search(search_query, source=wavelink.enums.TrackSource.YouTube) + if track_results: + await vc.queue.put_wait(track_results[0]) + c += 1 + await ctx.message.add_reaction("✅") + + await ctx.send(view=CV2(f"{ZPLUS} Added **{c}** of **{playlist_length}** tracks from **playlist** **[{playlist_info['name']}](https://discord.gg/codexdev)** to the queue.")) + await lmao.delete() + + if not vc.playing: + next_track = await vc.queue.get_wait() + await vc.play(next_track) + await self.display_player_embed(vc, next_track, ctx) + + + elif type_ == "album": + await ctx.message.add_reaction("⌛") + album_id = re.search(SPOTIFY_ALBUM_REGEX, link).group(1) + album_info = await spotify_api.get(f"albums/{album_id}") + tracks = album_info.get("tracks", {}).get("items", []) + + if not tracks: + await ctx.send("No tracks found in the album.") + return + + for track in tracks: + title = track['name'] + author = ', '.join(artist['name'] for artist in track['artists']) + search_query = f"{title} {author}" + + track_results = await wavelink.Playable.search(search_query, source=wavelink.enums.TrackSource.YouTube) + if track_results: + await vc.queue.put_wait(track_results[0]) + + await ctx.send(view=CV2(f"{ZPLUS} Added all tracks from album **[{album_info['name']}](https://discord.gg/codexdev)** to the queue.")) + if not vc.playing: + next_track = await vc.queue.get_wait() + await vc.play(next_track) + await self.display_player_embed(vc, next_track, ctx) + + + except Exception as e: + await ctx.send(f"An error occurred while processing the Spotify link: {e}") + + + + def create_progress_bar(self, completed, total, length=10): + filled_length = int(length * (completed / total)) + bar = '█' * filled_length + '░' * (length - filled_length) + return bar + + @commands.command(name="play", aliases=['p'], usage="play ", help="Plays a song or playlist.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def play(self, ctx: commands.Context, *, query: str): + + await self.play_source(ctx, query) + + + @commands.command(name="search", usage="search ", help="Searches music from multiple platforms.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def search2(self, ctx: commands.Context, *, query: str): + if not ctx.author.voice: + await ctx.send(view=CV2(f"{WARNING} You need to be in a voice channel to use this command.")) + return + + await ctx.send(view=PlatformSelectView(ctx, query)) + + + @commands.command(name="nowplaying", aliases=["nop"], usage="nowplaying", help="Shows the info about current playing song.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def nowplaying(self, ctx: commands.Context): + vc = ctx.voice_client + if not vc or not vc.playing: + await ctx.send(view=CV2("No song is currently playing.")) + return + + if not ctx.author.voice or ctx.author.voice.channel.id != vc.channel.id: + await ctx.send(view=CV2("You need to be in the same voice channel as me to use this command.")) + return + + track = vc.current + position = vc.position / 1000 + length = track.length / 1000 + + progress_bar = self.create_progress_bar(position, length, length=10) + position_str = f"{int(position // 60)}:{int(position % 60):02}" + length_str = f"{int(length // 60)}:{int(length % 60):02}" + + + queue_length = len(vc.queue) if vc.queue else 0 + + + if "spotify" in track.uri: + source_name = "Spotify" + elif "youtube" in track.uri: + source_name = "YouTube" + elif "soundcloud" in track.uri: + source_name = "SoundCloud" + elif "jiosaavn" in track.uri: + source_name = "JioSaavn" + else: + source_name = "Unknown Source" + + + view = CV2Embed( + title="🎶 Now Playing", + color=0x1DB954 if source_name == "Spotify" else 0xFF0000 + ) + view.add_field(name="Track", value=f"[{track.title}]({track.uri})", inline=False) + view.add_field(name="Song By", value=track.author, inline=False) + view.add_field(name="Progress", value=f"{position_str} [{progress_bar}] {length_str}", inline=False) + view.add_field(name="Duration", value=length_str, inline=False) + view.add_field(name="Queue Length", value=str(queue_length), inline=False) + view.add_field(name="Source", value=f"{source_name} - [Link]({track.uri})", inline=False) + view.set_thumbnail(url=track.artwork if track.artwork else "") + view.set_footer(text=f"Requested by {ctx.author.display_name}", icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + + await ctx.send(view=view) + + @commands.command(name="autoplay", usage="autoplay", help="Toggles autoplay mode.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def autoplay(self, ctx: commands.Context): + vc = ctx.voice_client + if not vc or not vc.playing: + await ctx.send(view=CV2(f"{WARNING} No song is currently playing.")) + return + + if not ctx.author.voice or ctx.author.voice.channel.id != vc.channel.id: + await ctx.send(view=CV2(f"{WARNING} You need to be in the same voice channel as me to use this command.")) + return + + if vc: + vc.autoplay = ( + wavelink.AutoPlayMode.enabled if vc.autoplay != wavelink.AutoPlayMode.enabled else wavelink.AutoPlayMode.disabled + ) + await ctx.send(view=CV2(f"{TICK} Autoplay {'enabled' if vc.autoplay == wavelink.AutoPlayMode.enabled else 'disabled'} by {ctx.author.mention}.")) + + @commands.command(name="loop", usage="loop", help="Toggles loop mode.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def loop(self, ctx: commands.Context): + vc = ctx.voice_client + if not vc or not vc.playing: + await ctx.send(view=CV2(f"{WARNING} No song is currently playing.")) + return + + if not ctx.author.voice or ctx.author.voice.channel.id != vc.channel.id: + await ctx.send(view=CV2(f"{WARNING} You need to be in the same voice channel as me to use this command.")) + return + + if vc: + vc.queue.mode = wavelink.QueueMode.loop if vc.queue.mode != wavelink.QueueMode.loop else wavelink.QueueMode.normal + await ctx.send(view=CV2(f"{TICK} Loop {'enabled' if vc.queue.mode == wavelink.QueueMode.loop else 'disabled'} by {ctx.author.mention}.")) + else: + await ctx.send(view=CV2("I'm not connected to a voice channel.")) + + + @commands.command(name="pause", usage="pause", help="Pauses the current song.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def pause(self, ctx: commands.Context): + vc = ctx.voice_client + if not vc or not vc.playing: + await ctx.send(view=CV2(f"{WARNING} No song is currently playing.")) + return + + if not ctx.author.voice or ctx.author.voice.channel.id != vc.channel.id: + await ctx.send(view=CV2(f"{WARNING} You need to be in the same voice channel as me to use this command.")) + return + + if vc and vc.playing and not vc.paused: + await vc.pause(True) + await vc.channel.edit(status=f"{ZMUSICPAUSE} Paused: {vc.current.title}") + await ctx.send(view=CV2(f"Paused by {ctx.author.mention}.")) + else: + await ctx.send(view=CV2(f"{WARNING} Nothing is playing or already paused.")) + + @commands.command(name="resume", usage="resume", help="Resumes the paused song.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def resume(self, ctx: commands.Context): + vc = ctx.voice_client + if not vc or not vc.playing: + await ctx.send(view=CV2(f"{WARNING} No song is currently playing.")) + return + + if not ctx.author.voice or ctx.author.voice.channel.id != vc.channel.id: + await ctx.send(view=CV2(f"{ICONS_WARNING_ALT1} You need to be in the same voice channel as me to use this command.")) + return + + if vc and vc.paused: + await vc.pause(False) + await vc.channel.edit(status=f"{MUSIC_ALT1} Playing: {vc.current.title}") + await ctx.send(view=CV2(f"Resumed by {ctx.author.mention}.")) + else: + await ctx.send(view=CV2("Player is not paused.")) + + @commands.command(name="skip", usage="skip", help="Skips the current song.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def skip(self, ctx: commands.Context): + vc = ctx.voice_client + if not vc or not vc.playing: + await ctx.send(view=CV2("No song is currently playing.")) + return + + if not ctx.author.voice or ctx.author.voice.channel.id != vc.channel.id: + await ctx.send(view=CV2(f"{WARNING} You need to be in the same voice channel as me to use this command.")) + return + + if vc.autoplay == wavelink.AutoPlayMode.enabled: + await vc.stop() + return await ctx.send(view=CV2(f"Skipped by {ctx.author.mention}.")) + + + if vc and vc.playing and not vc.queue.is_empty: + await vc.stop() + await ctx.send(view=CV2(f"Skipped by {ctx.author.mention}.")) + else: + await ctx.send(view=CV2(f"{WARNING} No song is playing or in the queue to skip.")) + + @commands.command(name="shuffle", usage="shuffle", help="Shuffles the queue.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def shuffle(self, ctx: commands.Context): + vc = ctx.voice_client + if not vc or not vc.playing: + await ctx.send(view=CV2(f"{WARNING} No song is currently playing.")) + return + + if not ctx.author.voice or ctx.author.voice.channel.id != vc.channel.id: + await ctx.send(view=CV2(f"{WARNING} You need to be in the same voice channel as me to use this command.")) + return + + if vc and vc.queue: + random.shuffle(vc.queue) + await ctx.send(view=CV2(f"Queue shuffled by {ctx.author.mention}.")) + else: + await ctx.send(view=CV2("Queue is empty.")) + + @commands.command(name="stop", usage="stop", help="Stops the current song and clears the queue.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def stop(self, ctx: commands.Context): + player: wavelink.Player = cast(wavelink.Player, ctx.voice_client) + vc = ctx.voice_client + if not vc or not vc.playing: + await ctx.send(view=CV2(f"{WARNING} No song is currently playing.")) + return + + if not ctx.author.voice or ctx.author.voice.channel.id != vc.channel.id: + await ctx.send(view=CV2(f"{WARNING} You need to be in the same voice channel as me to use this command.")) + return + + if vc and player: + await vc.channel.edit(status=None) + vc.queue.clear() + await vc.disconnect(force=True) + await ctx.send(view=CV2(f"Stopped and queue cleared by {ctx.author.mention}.")) + else: + await ctx.send(view=CV2("Nothing is playing to stop.")) + + @commands.command(name="volume", aliases=["vol"], usage="volume ", help="Sets the volume of the player.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def volume(self, ctx: commands.Context, level: int): + vc = ctx.voice_client + + if not vc: + await ctx.send(view=CV2(f"{WARNING} I'm not connected to a voice channel.")) + return + + if not ctx.author.voice or ctx.author.voice.channel.id != vc.channel.id: + await ctx.send(view=CV2(f"{WARNING} You need to be in the same voice channel as me to use this command.")) + return + + if vc: + if 1 <= level <= 150: + await vc.set_volume(level) + await ctx.send(view=CV2(f"{MUTE} Volume set to {level}% by {ctx.author.mention}.")) + else: + await ctx.send(view=CV2(f"{WARNING} Volume must be between 1 and 150.")) + else: + await ctx.send(view=CV2("Bot is not connected to a voice channel.")) + + @commands.command(name="queue", usage="queue", help="Shows the current queue.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def queue(self, ctx: commands.Context): + vc = ctx.voice_client + + if not vc or not vc.queue or vc.queue.is_empty: + await ctx.send(view=CV2(f"{WARNING} The queue is currently empty.")) + return + + if not ctx.author.voice or ctx.author.voice.channel.id != vc.channel.id: + await ctx.send(view=CV2(f"{WARNING} you need to be in the same voice channel as me to use this command.")) + return + + + entries = [f"{index + 1}. [{track.title} - {track.author}]({track.uri})" for index, track in enumerate(vc.queue)] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + title="Current Queue", + description="List of upcoming songs.", + per_page=10, + color=0xFF0000), + ctx=ctx) + await paginator.paginate() + + @commands.command(name="clearqueue", usage="clearqueue", help="Clears the queue.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def clearqueue(self, ctx: commands.Context): + vc = ctx.voice_client + + if not vc or not vc.queue or vc.queue.is_empty: + await ctx.send(view=CV2(f"{WARNING} No Queue to clear.")) + return + + if not ctx.author.voice or ctx.author.voice.channel.id != vc.channel.id: + await ctx.send(view=CV2(f"{WARNING} You need to be in the same voice channel as me to use this command.")) + return + + if vc and vc.queue: + vc.queue.clear() + await ctx.send(view=CV2("Queue has been cleared.")) + else: + await ctx.send(view=CV2("No queue to clear.")) + + @commands.command(name="replay", usage="replay", help="Replays the current song.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def replay(self, ctx: commands.Context): + vc = ctx.voice_client + + if not vc or not vc.playing: + await ctx.send(view=CV2(f"{WARNING} I'm not connected to any voice channel.")) + return + + if not ctx.author.voice or ctx.author.voice.channel.id != vc.channel.id: + await ctx.send(view=CV2(f"{WARNING} You need to be in the same voice channel as me to use this command.")) + return + + if vc and vc.playing: + await vc.seek(0) + await ctx.send(view=CV2("Replaying the current track.")) + else: + await ctx.send(view=CV2("No track is currently playing.")) + + @commands.command(name="join", aliases=["connect"], usage="join", help="Joins the voice channel.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def join(self, ctx: commands.Context): + if ctx.author.voice: + await ctx.author.voice.channel.connect(cls=wavelink.Player) + await ctx.send(view=CV2("Joined the voice channel.")) + 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.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def disconnect(self, ctx: commands.Context): + vc = ctx.voice_client + if not vc: + await ctx.send(view=CV2(f"{ICONS_WARNING_ALT1} I'm not connected to any voice channel.")) + return + + if not ctx.author.voice or ctx.author.voice.channel.id != vc.channel.id: + await ctx.send(view=CV2(f"{WARNING} You need to be in the same voice channel as me to use this command.")) + return + + if vc: + await vc.disconnect() + await ctx.send(view=CV2("Disconnected from the voice channel.")) + else: + await ctx.send(view=CV2("Bot is not connected to any voice channel.")) + + @commands.command(name="seek", usage="seek ", help="Seeks to a specific percentage of the song.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def seek(self, ctx: commands.Context, percentage: int): + if not 1 <= percentage <= 100: + await ctx.send(view=CV2("Please provide a percentage between 1 and 100.")) + return + + vc = ctx.voice_client + if not vc or not vc.playing: + await ctx.send(view=CV2("No song is currently playing.")) + return + + if not ctx.author.voice or ctx.author.voice.channel.id != vc.channel.id: + await ctx.send(view=CV2("You need to be in the same voice channel as me to use this command.")) + return + + track = vc.current + target_position = int(track.length * (percentage / 100)) + await vc.seek(target_position) + + await ctx.send(view=CV2(f"Seeked to {percentage}% of the current track.")) + + @commands.Cog.listener() + async def on_wavelink_track_start(self, payload: wavelink.TrackStartEventPayload): + player = payload.player + track = player.current + guild_id = player.guild.id + + voice_channel = player.channel + if voice_channel: + await voice_channel.edit(status=f"{MUSIC_ALT1} Playing: {track.title}") # type: ignore + + if guild_id not in track_histories: + track_histories[guild_id] = [] + + if not track_histories[guild_id] or track_histories[guild_id][-1] != track: + track_histories[guild_id].append(track) + + + if len(track_histories[guild_id]) > 10: + track_histories[guild_id].pop(0) + + @commands.Cog.listener() + async def on_wavelink_track_end(self, payload: wavelink.TrackEndEventPayload): + player = payload.player + voice_channel = player.channel + + if voice_channel: + await voice_channel.edit(status=None) # type: ignore + await self.on_track_end(payload) diff --git a/bot/cogs/commands/nightmode.py b/bot/cogs/commands/nightmode.py new file mode 100644 index 0000000..1cb897c --- /dev/null +++ b/bot/cogs/commands/nightmode.py @@ -0,0 +1,228 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import os +from utils.Tools import * +from utils.cv2 import CV2 +from discord.ui import TextDisplay, Separator, ActionRow, LayoutView, Container +from utils.config import OWNER_IDS_STR + +# Database setup +db_folder = "db" +db_file = "anti.db" +db_path = os.path.join(db_folder, db_file) + + +class Nightmode(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.bot.loop.create_task(self.initialize_db()) + self.ricky = OWNER_IDS_STR + self.color = 0xFF0000 + + async def initialize_db(self): + self.db = await aiosqlite.connect(db_path) + await self.db.execute(""" + CREATE TABLE IF NOT EXISTS Nightmode ( + guildId TEXT, + roleId TEXT, + adminPermissions INTEGER + ) + """) + await self.db.commit() + + async def is_extra_owner(self, user, guild): + async with self.db.execute( + """ + SELECT owner_id FROM extraowners WHERE guild_id = ? AND owner_id = ? + """, + (guild.id, user.id), + ) as cursor: + extra_owner = await cursor.fetchone() + return extra_owner is not None + + @commands.hybrid_group( + name="nightmode", + aliases=[], + help="Manages Nightmode feature", + invoke_without_command=True, + ) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + async def nightmode(self, ctx): + view = CV2( + "__**Nightmode**__", + "Nightmode swiftly disables dangerous permissions for roles, like stripping `ADMINISTRATION` rights, while preserving original settings for seamless restoration.\n\n**Make sure to keep my ROLE above all roles you want to protect.**", + "**Usage**\n `nightmode enable`\n `nightmode disable`", + ) + await ctx.send(view=view) + + @nightmode.command(name="enable", help="Enable nightmode") + @commands.has_permissions(administrator=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.user) + async def enable_nightmode(self, ctx): + if ctx.guild.member_count < 50: + view = CV2( + "Access Denied", + "Your Server Doesn't Meet My 50 Member Criteria", + ) + return await ctx.send(view=view) + + own = ctx.author.id == ctx.guild.owner_id + check = await self.is_extra_owner(ctx.author, ctx.guild) + if not own and not check and ctx.author.id not in self.ricky: + view = CV2( + "Access Denied", + "Only Server Owner Or Extraowner Can Run This Command.!", + ) + return await ctx.send(view=view) + + if ( + not own + and not (ctx.guild.me.top_role.position <= ctx.author.top_role.position) + and ctx.author.id not in self.ricky + ): + view = CV2( + "Access Denied", + "Only Server Owner or Extraowner Having **Higher role than me can run this command**", + ) + return await ctx.send(view=view) + + bot_highest_role = ctx.guild.me.top_role + manageable_roles = [ + role + for role in ctx.guild.roles + if role.position < bot_highest_role.position + and role.name != "@everyone" + and role.permissions.administrator + and not role.managed + ] + + if not manageable_roles: + view = CV2( + "Error", + "No Roles Found With Admin Permissions", + ) + return await ctx.send(view=view) + + async with self.db.execute( + "SELECT guildId FROM Nightmode WHERE guildId = ?", (str(ctx.guild.id),) + ) as cursor: + if await cursor.fetchone(): + view = CV2( + "Error", + "Nightmode is already enabled.", + ) + return await ctx.send(view=view) + + async with self.db.cursor() as cursor: + for role in manageable_roles: + admin_permissions = discord.Permissions(administrator=True) + if role.permissions.administrator: + permissions = role.permissions + permissions.administrator = False + + await role.edit(permissions=permissions, reason="Nightmode ENABLED") + + await cursor.execute( + """ + INSERT OR REPLACE INTO Nightmode (guildId, roleId, adminPermissions) + VALUES (?, ?, ?) + """, + (str(ctx.guild.id), str(role.id), int(admin_permissions.value)), + ) + await self.db.commit() + + view = CV2( + "Success", + "Nightmode enabled! Dangerous Permissions Disabled For Manageable Roles.", + ) + await ctx.send(view=view) + + @nightmode.command(name="disable", help="Disable nightmode") + @commands.has_permissions(administrator=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.user) + async def disable_nightmode(self, ctx): + if ctx.guild.member_count < 50: + view = CV2( + "Access Denied", + "Your Server Doesn't Meet My 50 Member Criteria", + ) + return await ctx.send(view=view) + + own = ctx.author.id == ctx.guild.owner_id + check = await self.is_extra_owner(ctx.author, ctx.guild) + if not own and not check and ctx.author.id not in self.ricky: + view = CV2( + "Access Denied", + "Only Server Owner Or Extraowner Can Run This Command.!", + ) + return await ctx.send(view=view) + + if ( + not own + and not (ctx.guild.me.top_role.position <= ctx.author.top_role.position) + and ctx.author.id not in self.ricky + ): + view = CV2( + "Access Denied", + "Only Server Owner or Extraowner Having **Higher role than me can run this command**", + ) + return await ctx.send(view=view) + + async with self.db.execute( + "SELECT roleId, adminPermissions FROM Nightmode WHERE guildId = ?", + (str(ctx.guild.id),), + ) as cursor: + stored_roles = await cursor.fetchall() + + if not stored_roles: + view = CV2( + "Error", + "Nightmode is not enabled.", + ) + return await ctx.send(view=view) + + async with self.db.cursor() as cursor: + for role_id, admin_permissions in stored_roles: + role = ctx.guild.get_role(int(role_id)) + if role: + permissions = discord.Permissions( + administrator=bool(admin_permissions) + ) + await role.edit( + permissions=permissions, reason="Nightmode DISABLED" + ) + + await cursor.execute( + "DELETE FROM Nightmode WHERE guildId = ? AND roleId = ?", + (str(ctx.guild.id), role_id), + ) + await self.db.commit() + + view = CV2( + "Success", + "Nightmode disabled! Restored Permissions For Manageable Roles.", + ) + await ctx.send(view=view) diff --git a/bot/cogs/commands/nitro.py b/bot/cogs/commands/nitro.py new file mode 100644 index 0000000..da25c84 --- /dev/null +++ b/bot/cogs/commands/nitro.py @@ -0,0 +1,54 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +from discord.ui import Button, View + + +class Nitro(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.Cog.listener() + async def on_message(self, message): + if self.bot.user in message.mentions and ( + "nitro" in message.content.lower() or "$nitro" in message.content.lower() + ): + ctx = await self.bot.get_context(message) + await self.bot.invoke(ctx) + + @commands.command(name="nitro") + async def nitro(self, ctx): + embed = discord.Embed(color=0x2B2D31) + embed.add_field( + name="A WILD NITRO GIFT APPEARS?", + value="Expires in 12 hours\n\nClick the claim button for claiming Nitro", + inline=False, + ) + embed.set_image( + url="https://media.tenor.com/ltVe8iMhgXcAAAAS/nitro-discord.gif" + ) + + claim_button = Button( + style=discord.ButtonStyle.primary, + label="Click me!", + url="https://discord.gg/codexdev", + disabled=False, + ) + + view = View() + view.add_item(claim_button) + + await ctx.send(embed=embed, view=view) diff --git a/bot/cogs/commands/notify.py b/bot/cogs/commands/notify.py new file mode 100644 index 0000000..6111e5e --- /dev/null +++ b/bot/cogs/commands/notify.py @@ -0,0 +1,195 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +from utils.Tools import * +from utils.cv2 import CV2 + + +class NotifCommands(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.db_path = "db/notify.db" + self.loop_task = self.bot.loop.create_task(self.setup_db()) + + async def setup_db(self): + async with aiosqlite.connect(self.db_path) as db: + await db.execute("""CREATE TABLE IF NOT EXISTS notifications ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL UNIQUE, + role_id INTEGER NOT NULL, + channel_id INTEGER NOT NULL)""") + await db.commit() + + @commands.group(invoke_without_command=True) + async def setnotif(self, ctx): + view = CV2( + "Notification Commands", + "Subcommands: twitch, youtube, list, reset", + ) + await ctx.send(view=view) + + @setnotif.command() + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + async def twitch(self, ctx, role: discord.Role, channel: discord.TextChannel): + async with aiosqlite.connect(self.db_path) as db: + async with db.execute( + "SELECT * FROM notifications WHERE type = ?", ("twitch",) + ) as existing: + row = await existing.fetchone() + if row: + view = CV2( + "Access Denied", + "Twitch notification already set. Remove it first.", + ) + await ctx.reply(view=view) + return + + await db.execute( + "INSERT INTO notifications (type, role_id, channel_id) VALUES (?, ?, ?)", + ("twitch", role.id, channel.id), + ) + await db.commit() + view = CV2( + "Success", + f"Twitch notifications set for {role.mention} in {channel.mention}.", + ) + await ctx.reply(view=view) + + @setnotif.command() + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + async def youtube(self, ctx, role: discord.Role, channel: discord.TextChannel): + async with aiosqlite.connect(self.db_path) as db: + async with db.execute( + "SELECT * FROM notifications WHERE type = ?", ("youtube",) + ) as existing: + row = await existing.fetchone() + if row: + view = CV2( + "Access Denied", + "YouTube notification already set. Remove it first.", + ) + await ctx.reply(view=view) + return + + await db.execute( + "INSERT INTO notifications (type, role_id, channel_id) VALUES (?, ?, ?)", + ("youtube", role.id, channel.id), + ) + await db.commit() + view = CV2( + "Success", + f"YouTube notifications set for {role.mention} in {channel.mention}.", + ) + await ctx.reply(view=view) + + @setnotif.command() + async def list(self, ctx): + async with aiosqlite.connect(self.db_path) as db: + async with db.execute("SELECT * FROM notifications") as cursor: + rows = await cursor.fetchall() + if not rows: + view = CV2( + "Notification Settings", + "No Twitch and YouTube notification channels set.", + ) + await ctx.reply(view=view) + return + + lines = [] + for row in rows: + notif_type = row[1].capitalize() + role = ctx.guild.get_role(row[2]) + channel = ctx.guild.get_channel(row[3]) + if role and channel: + lines.append( + f"**{notif_type} Notifications**\nRole: {role.mention} | Channel: {channel.mention}" + ) + else: + lines.append( + f"**{notif_type} Notifications**\nRole or Channel not found" + ) + + view = CV2( + "Current Notification Settings", + *lines, + ) + await ctx.reply(view=view) + + @setnotif.command() + async def reset(self, ctx): + async with aiosqlite.connect(self.db_path) as db: + await db.execute( + "DELETE FROM notifications WHERE type IN (?, ?)", ("twitch", "youtube") + ) + await db.commit() + view = CV2( + "Success", + "Twitch and YouTube notifications have been reset.", + ) + await ctx.send(view=view) + + @commands.Cog.listener() + async def on_presence_update(self, before, after): + + streaming = next( + ( + activity + for activity in after.activities + if isinstance(activity, discord.Streaming) + ), + None, + ) + + if streaming: + stream_type = ( + "twitch" + if "twitch" in streaming.url.lower() + else "youtube" + if "youtube" in streaming.url.lower() + else None + ) + if stream_type: + async with aiosqlite.connect(self.db_path) as db: + async with db.execute( + "SELECT role_id, channel_id FROM notifications WHERE type = ?", + (stream_type,), + ) as cursor: + row = await cursor.fetchone() + if row: + role_id, channel_id = row + role = after.guild.get_role(role_id) + channel = after.guild.get_channel(channel_id) + + if role and channel: + embed = discord.Embed( + title=f"{after.display_name} is now live!", + description=f"{after.mention} is now streaming on {stream_type.capitalize()}.", + color=0xFF0000, + ) + embed.add_field( + name="Stream Title", + value=streaming.name, + inline=False, + ) + embed.add_field( + name="Watch here", value=streaming.url, inline=False + ) + await channel.send(content=role.mention, embed=embed) diff --git a/bot/cogs/commands/np.py b/bot/cogs/commands/np.py new file mode 100644 index 0000000..af9b4b3 --- /dev/null +++ b/bot/cogs/commands/np.py @@ -0,0 +1,722 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from discord.ext import commands, tasks +from discord import * +import discord +from utils.emoji import BOOST, ICONS_WARNING, INFO, MENTION, PREMIUM, TICK, TIME, TIMER_ALT1, U_ADMIN, ZDIL, ZHUMAN, ZYROXHAMMER, ZYROXSYS +import aiosqlite +from typing import Optional +from datetime import datetime, timedelta +from discord.ui import View, Button, Select +from utils.config import OWNER_IDS +from utils import Paginator, DescriptionEmbedPaginator +from utils.cv2 import CV2Embed, add_action_rows +from utils.config import * + + +def load_owner_ids(): + return OWNER_IDS + + +async def is_staff(user, staff_ids): + return user.id in staff_ids + + +async def is_owner_or_staff(ctx): + return await is_staff(ctx.author, ctx.cog.staff) or ctx.author.id in OWNER_IDS + + +class TimeSelect(Select): + def __init__(self, user, db_path, author): + super().__init__(placeholder="Select the duration") + self.user = user + self.db_path = db_path + self.author = author + + self.options = [ + SelectOption( + label="10 Minutes", description="Trial for 10 minutes", value="10m" + ), + SelectOption( + label="1 Week", description="No prefix for 1 week", value="1w" + ), + SelectOption( + label="3 Weeks", description="No prefix for 3 weeks", value="3w" + ), + SelectOption( + label="1 Month", description="No prefix for 1 Month", value="1m" + ), + SelectOption( + label="3 Months", description="No prefix for 3 Months.", value="3m" + ), + SelectOption( + label="6 Months", description="No prefix for 6 Months.", value="6m" + ), + SelectOption( + label="1 Year", description="No prefix for 1 Year.", value="1y" + ), + SelectOption( + label="3 Years", description="No prefix for 3 Years.", value="3y" + ), + SelectOption( + label="Lifetime", description="No prefix Permanently.", value="lifetime" + ), + ] + + async def callback(self, interaction: discord.Interaction): + if interaction.user != self.author: + return await interaction.response.send_message( + "You can't select this option.", ephemeral=True + ) + + duration_mapping = { + "10m": timedelta(minutes=10), + "1w": timedelta(weeks=1), + "3w": timedelta(weeks=3), + "1m": timedelta(days=30), + "3m": timedelta(days=90), + "6m": timedelta(days=180), + "1y": timedelta(days=365), + "3y": timedelta(days=365 * 3), + "lifetime": None, + } + + selected_duration = self.values[0] + expiry_time = None + + if selected_duration != "lifetime": + expiry_time = datetime.utcnow() + duration_mapping[selected_duration] + expiry_str = expiry_time.isoformat() + else: + expiry_str = None + + async with aiosqlite.connect(self.db_path) as db: + await db.execute( + "INSERT INTO np (id, expiry_time) VALUES (?, ?)", + (self.user.id, expiry_str), + ) + await db.commit() + + expiry_text = ( + "**Lifetime**" + if selected_duration == "lifetime" + else f"{expiry_time.strftime('%Y-%m-%d %H:%M:%S')} UTC" + ) + expiry_timestamp = ( + "None (Permanent)" + if selected_duration == "lifetime" + else f"" + ) + + guild = interaction.client.get_guild(1401125905677553716) + if guild: + member = guild.get_member(self.user.id) + if member: + role = guild.get_role(1401134167873290311) + if role: + await member.add_roles(role, reason="No prefix added") + + log_channel = interaction.client.get_channel(LOG_CHANNEL_ID) if LOG_CHANNEL_ID else None + if log_channel: + embed = CV2Embed( + title="User Added to No Prefix", + description=f"**User**: [{self.user}](https://discord.com/users/{self.user.id})\n**User Mention**: {self.user.mention}\n** ID**: {self.user.id}\n\n** Added By**: [{self.author.display_name}](https://discord.com/users/{self.author.id})\n{TIMER_ALT1}**Expiry Time**: {expiry_text}\n{ZDIL} **Timestamp**: {expiry_timestamp}\n\n{PREMIUM} **Tier**: **{self.values[0].upper()}**", + color=0xFF0000, + ) + embed.set_thumbnail( + url=self.user.avatar.url + if self.user.avatar + else self.user.default_avatar.url + ) + await log_channel.send(view=embed) + + embed = CV2Embed( + description=f"**Added Global No Prefix**:\n{ZHUMAN} User: **{self.user.mention}**\n{MENTION} User Mention: {self.user.mention}\n{ZYROXSYS} User ID: {self.user.id}\n\n__**Additional Info**__:\n{ZYROXHAMMER} Added By: **{self.author.display_name}**\n{TIME} Expiry Time: {expiry_text}\n{BOOST} Timestamp: {expiry_timestamp}", + color=0xFF0000, + ) + embed.set_author( + name="Added No Prefix", + icon_url="https://cdn.discordapp.com/icons/1166303696263585852/eeb00b2cf541438e88cdf842394c5b30.png?size=1024", + ) + embed.set_footer( + text="DM will be sent to the user in case No prefix is expired." + ) + await interaction.response.edit_message(view=embed) + + +class TimeSelectView(View): + def __init__(self, user, db_path, author): + super().__init__() + self.user = user + self.db_path = db_path + self.author = author + self.add_item(TimeSelect(user, db_path, author)) + + +from utils.config import BotName + +class NoPrefix(commands.Cog): + def __init__(self, client): + self.client = client + self.staff = set() + self.db_path = "db/np.db" + self.client.loop.create_task(self.load_staff()) + self.client.loop.create_task(self.setup_database()) + self.expiry_check.start() + + async def setup_database(self): + async with aiosqlite.connect(self.db_path) as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS np ( + id INTEGER PRIMARY KEY + ) + """) + + async with db.execute("PRAGMA table_info(np);") as cursor: + columns = [info[1] for info in await cursor.fetchall()] + + if "expiry_time" not in columns: + await db.execute(""" + ALTER TABLE np ADD COLUMN expiry_time TEXT NULL; + """) + + await db.execute(""" + UPDATE np + SET expiry_time = NULL + WHERE expiry_time IS NULL; + """) + await db.execute(""" + CREATE TABLE IF NOT EXISTS autonp ( + guild_id INTEGER PRIMARY KEY + ) + """) + + await db.commit() + + async def load_staff(self): + await self.client.wait_until_ready() + async with aiosqlite.connect(self.db_path) as db: + async with db.execute("SELECT id FROM staff") as cursor: + self.staff = {row[0] for row in await cursor.fetchall()} + + @tasks.loop(minutes=10) + async def expiry_check(self): + async with aiosqlite.connect(self.db_path) as db: + now = datetime.utcnow().isoformat() + async with db.execute( + "SELECT id FROM np WHERE expiry_time IS NOT NULL AND expiry_time <= ?", + (now,), + ) as cursor: + expired_users = [row[0] for row in await cursor.fetchall()] + + if expired_users: + async with db.execute( + "DELETE FROM np WHERE id IN ({})".format( + ",".join("?" * len(expired_users)) + ), + expired_users, + ): + await db.commit() + + for user_id in expired_users: + user = self.client.get_user(user_id) + if user: + log_channel = self.client.get_channel(LOG_CHANNEL_ID) if LOG_CHANNEL_ID else None + if log_channel: + embed_log = CV2Embed( + title="No Prefix Expired", + description=( + f"**User**: [{user}](https://discord.com/users/{user.id})\n" + f"**User Mention**: {user.mention}\n" + f"**ID**: {user.id}\n\n" + f"**Removed By**: **{BotName}**\n" + ), + color=0xFF0000, + ) + embed_log.set_thumbnail( + url=user.display_avatar.url + if user.avatar + else user.default_avatar.url + ) + embed_log.set_footer(text="No Prefix Removal Log") + await log_channel.send(view=embed_log) + bot = self.client + guild = bot.get_guild(1401125905677553716) + if guild: + member = guild.get_member(user.id) + if member: + role = guild.get_role(1401134167873290311) + if role in member.roles: + await member.remove_roles(role) + + embed = CV2Embed( + description=f"{ICONS_WARNING} Your No Prefix status has **Expired**. You will now require the prefix to use commands.", + color=0xFF0000, + ) + embed.set_author( + name="No Prefix Expired", + icon_url=user.avatar.url + if user.avatar + else user.default_avatar.url, + ) + + embed.set_footer( + text=f"{BRAND_NAME} - No Prefix, Join support to regain access." + ) + support = Button( + label="Support", + style=discord.ButtonStyle.link, + url=f"https://discord.gg/codexdev", + ) + view = View() + view.add_item(support) + + try: + add_action_rows(embed, view.children) + await user.send(f"{user.mention}", view=embed) + except discord.Forbidden: + pass + except discord.HTTPException: + pass + + @expiry_check.before_loop + async def before_expiry_check(self): + await self.client.wait_until_ready() + + @commands.group( + name="np", + help="Allows you to add someone to the no-prefix list (owner-only command)", + ) + @commands.check(is_owner_or_staff) + async def _np(self, ctx): + if ctx.invoked_subcommand is None: + await ctx.send_help(ctx.command) + + @_np.command(name="list", help="List of no-prefix users") + @commands.check(is_owner_or_staff) + async def np_list(self, ctx): + async with aiosqlite.connect(self.db_path) as db: + async with db.execute("SELECT id FROM np") as cursor: + ids = [row[0] for row in await cursor.fetchall()] + if not ids: + await ctx.reply( + f"No users in the no-prefix list.", mention_author=False + ) + return + entries = [ + f"`#{no + 1}` [Profile URL](https://discord.com/users/{mem}) (ID: {mem})" + for no, mem in enumerate(ids, start=0) + ] + paginator = Paginator( + source=DescriptionEmbedPaginator( + entries=entries, + title=f"No Prefix Users [{len(ids)}]", + description="", + per_page=10, + color=0xFF0000, + ), + ctx=ctx, + ) + await paginator.paginate() + + @_np.command(name="add", help="Add user to no-prefix with time options") + @commands.check(is_owner_or_staff) + async def np_add(self, ctx, user: discord.User): + async with aiosqlite.connect(self.db_path) as db: + async with db.execute( + "SELECT id FROM np WHERE id = ?", (user.id,) + ) as cursor: + result = await cursor.fetchone() + if result: + embed = CV2Embed( + description=f"**{user}** is Already in No prefix list\n\n **Requested By**: [{ctx.author.display_name}](https://discord.com/users/{ctx.author.id})\n", + color=0xFF0000, + ) + embed.set_author(name="Error") + await ctx.reply(view=embed) + return + + view = TimeSelectView(user, self.db_path, ctx.author) + from discord.ui import ActionRow + + embed = CV2Embed( + title="Select No Prefix Duration", + description="**Choose the duration for how long no-prefix should be enabled for this user:**", + color=0xFF0000, + ) + for item in view.children: + embed.children[0].add_item(ActionRow(item)) + await ctx.reply(view=embed) + + @_np.command(name="remove", help="Remove user from no-prefix") + @commands.check(is_owner_or_staff) + async def np_remove(self, ctx, user: discord.User): + async with aiosqlite.connect("db/np.db") as db: + async with db.execute( + "SELECT id FROM np WHERE id = ?", (user.id,) + ) as cursor: + result = await cursor.fetchone() + if not result: + embed = CV2Embed( + description=f"**{user}** is Not in the No Prefix list\n\n{U_ADMIN} **Requested By**: [{ctx.author.display_name}](https://discord.com/users/{ctx.author.id})\n", + color=0xFF0000, + ) + embed.set_author(name="Error") + await ctx.reply(view=embed) + return + + await db.execute("DELETE FROM np WHERE id = ?", (user.id,)) + await db.commit() + + guild = ctx.bot.get_guild(1401125905677553716) + if guild: + member = guild.get_member(user.id) + if member: + role = guild.get_role(1401134167873290311) + if role in member.roles: + await member.remove_roles(role) + + embed = CV2Embed( + description=( + f"**User**: [{user}](https://discord.com/users/{user.id})\n" + f"**User Mention**: {user.mention}\n" + f"** ID**: {user.id}\n\n" + f"** Removed By**: **{BotName}**\n" + ), + color=0xFF0000, + ) + embed.set_author(name="Removed No Prefix") + await ctx.reply(view=embed) + + log_channel = ctx.bot.get_channel(LOG_CHANNEL_ID) if LOG_CHANNEL_ID else None + if log_channel: + embed_log = CV2Embed( + title="No Prefix Removed", + description=( + f"**User**: [{user}](https://discord.com/users/{user.id})\n" + f"**User Mention**: {user.mention}\n" + f"** ID**: {user.id}\n\n" + f"**Removed By**: **{BotName}**\n" + ), + color=0xFF0000, + ) + embed_log.set_thumbnail( + url=user.display_avatar.url if user.avatar else user.default_avatar.url + ) + embed_log.set_footer(text="No Prefix Removal Log") + await log_channel.send(" ".join(f"<@{oid}>" for oid in OWNER_IDS), view=embed_log) + + @_np.command( + name="status", help="Check if a user is in the No Prefix list and show details." + ) + @commands.check(is_owner_or_staff) + async def np_status(self, ctx, user: discord.User): + async with aiosqlite.connect("db/np.db") as db: + async with db.execute( + "SELECT id, expiry_time FROM np WHERE id = ?", (user.id,) + ) as cursor: + result = await cursor.fetchone() + + if not result: + embed = CV2Embed( + title="No Prefix Status", + description=f"**{user}** is Not in the No Prefix list\n\n" + f"**Requested By**: " + f"[{ctx.author.display_name}](https://discord.com/users/{ctx.author.id})\n", + color=0xFF0000, + ) + await ctx.reply(view=embed) + return + + user_id, expires = result + + if expires and expires != "Null": + expire_time = datetime.fromisoformat(expires) + expire_timestamp = f"" + else: + expire_time = "Lifetime" + expire_timestamp = "Lifetime" + + embed = CV2Embed( + title="No Prefix Status", + description=( + f"**User**: [{user}](https://discord.com/users/{user.id})\n" + f"**User ID**: {user.id}\n\n" + f"{TIME} Expiry: {expire_time} ({expire_timestamp})" + ), + color=0xFF0000, + ) + + embed.set_thumbnail( + url=user.display_avatar.url if user.avatar else user.default_avatar.url + ) + + await ctx.reply(view=embed) + + @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: + await ctx.send_help(ctx.command) + + @autonp.group(name="guild", help="Manage partner guilds for auto no-prefix.") + async def autonp_guild(self, ctx): + if ctx.invoked_subcommand is None: + await ctx.send_help(ctx.command) + + @autonp_guild.command(name="add", help="Add a guild to auto no-prefix.") + async def add_guild(self, ctx, guild_id: int): + async with aiosqlite.connect(self.db_path) as db: + async with db.execute( + "SELECT 1 FROM autonp WHERE guild_id = ?", (guild_id,) + ) as cursor: + if await cursor.fetchone(): + await ctx.reply("Guild is already added.") + return + await db.execute("INSERT INTO autonp (guild_id) VALUES (?)", (guild_id,)) + await db.commit() + await ctx.reply(f"Guild {guild_id} added to auto no-prefix.") + + @autonp_guild.command(name="remove", help="Remove a guild from auto no-prefix.") + async def remove_guild(self, ctx, guild_id: int): + async with aiosqlite.connect(self.db_path) as db: + async with db.execute( + "SELECT 1 FROM autonp WHERE guild_id = ?", (guild_id,) + ) as cursor: + if not await cursor.fetchone(): + await ctx.reply("Guild is not in auto no-prefix.") + return + await db.execute("DELETE FROM autonp WHERE guild_id = ?", (guild_id,)) + await db.commit() + await ctx.reply(f"Guild {guild_id} removed from auto no-prefix.") + + @autonp_guild.command(name="list", help="List all guilds with auto no-prefix.") + @commands.check(is_owner_or_staff) + async def list_guilds(self, ctx): + async with aiosqlite.connect(self.db_path) as db: + async with db.execute("SELECT guild_id FROM autonp") as cursor: + guilds = [row[0] for row in await cursor.fetchall()] + if not guilds: + await ctx.reply( + "No guilds in auto no-prefix.", mention_author=False + ) + return + await ctx.reply( + f"Guilds in auto no-prefix:\n" + "\n".join(str(g) for g in guilds), + mention_author=False, + ) + + async def is_user_in_np(self, user_id): + async with aiosqlite.connect(self.db_path) as db: + async with db.execute( + "SELECT 1 FROM np WHERE id = ?", (user_id,) + ) as cursor: + return await cursor.fetchone() is not None + + @commands.Cog.listener() + async def on_member_update(self, before, after): + if before.premium_since is None and after.premium_since is not None: + async with aiosqlite.connect(self.db_path) as db: + async with db.execute( + "SELECT 1 FROM autonp WHERE guild_id = ?", (after.guild.id,) + ) as cursor: + if not await cursor.fetchone(): + return + if not await self.is_user_in_np(after.id): + await self.add_np(after, timedelta(days=60)) + log_channel = self.client.get_channel(1406566123234787378) + embed = CV2Embed( + title="Added No prefix due to Boosting Partner Server", + description=f"**User**: **[{after}](https://discord.com/users/{after.id})** (ID: {after.id})\n**Server**: {after.guild.name}", + color=0xFF0000, + ) + message = await log_channel.send(" ".join(f"<@{oid}>" for oid in OWNER_IDS), view=embed) + await message.publish() + + elif before.premium_since is not None and after.premium_since is None: + await self.handle_boost_removal(after) + + # @commands.Cog.listener() + # async def on_member_remove(self, member): + # await self.handle_boost_removal(member) + + async def handle_boost_removal(self, user): + async with aiosqlite.connect(self.db_path) as db: + async with db.execute( + "SELECT 1 FROM autonp WHERE guild_id = ?", (user.guild.id,) + ) as cursor: + if not await cursor.fetchone(): + return + if await self.is_user_in_np(user.id): + await self.remove_np(user) + log_channel = self.client.get_channel(1406566257070964756) + embed = CV2Embed( + title="Removed No prefix due to Unboosting Partner Server", + description=f"**User**: **[{user}](https://discord.com/users/{user.id})** (ID: {user.id})\n**Server**: {user.guild.name}", + color=0xFF0000, + ) + message = await log_channel.send(" ".join(f"<@{oid}>" for oid in OWNER_IDS), view=embed) + await message.publish() + + async def add_np(self, user, duration): + expiry_time = datetime.utcnow() + duration + async with aiosqlite.connect(self.db_path) as db: + await db.execute( + "INSERT INTO np (id, expiry_time) VALUES (?, ?)", + (user.id, expiry_time.isoformat()), + ) + await db.commit() + + embed = CV2Embed( + title="Congratulations you got 2 months No Prefix!", + description=f"You've been credited 2 months of global No Prefix for boosting our Partnered Servers. You can now use my commands without prefix. If you wish to remove it, please reach out [Support Server](https://discord.gg/codexdev).", + color=0xFF0000, + ) + try: + await user.send(view=embed) + except discord.Forbidden: + pass + except discord.HTTPException: + pass + + guild = self.client.get_guild(1401125905677553716) + if guild: + member = guild.get_member(user.id) + if member is not None: + role = guild.get_role(1401134167873290311) + if role: + await member.add_roles(role) + + async def remove_np(self, user): + async with aiosqlite.connect(self.db_path) as db: + async with db.execute( + "SELECT expiry_time FROM np WHERE id = ?", (user.id,) + ) as cursor: + row = await cursor.fetchone() + if row is None or row[0] is None: + return + + await db.execute("DELETE FROM np WHERE id = ?", (user.id,)) + await db.commit() + + embed = CV2Embed( + title=f"{ICONS_WARNING} Global No Prefix Expired", + description=f"Hey {user.mention}, your global no prefix has expired!\n\n__**Reason:**__ Unboosting our partnered Server.\nIf you think this is a mistake then please reach out [Support Server](https://discord.gg/codexdev).", + color=0xFF0000, + ) + + try: + await user.send(view=embed) + except discord.Forbidden: + pass + except discord.HTTPException: + pass + + guild = self.client.get_guild(1401125905677553716) + if guild: + member = guild.get_member(user.id) + if member is not None: + role = guild.get_role(1401134167873290311) + if role and role in member.roles: + await member.remove_roles(role) + + @_np.command(name="reset", help="Reset/clear all users from the no-prefix list") + @commands.is_owner() + async def np_reset(self, ctx): + # Confirmation message + embed = CV2Embed( + title="Confirm Reset", + description="Are you sure you want to remove **ALL** users from the no-prefix list? This action cannot be undone.", + color=0xFF0000, + ) + + # Create confirmation buttons + yes_button = Button(label="Yes", style=discord.ButtonStyle.danger) + no_button = Button(label="No", style=discord.ButtonStyle.secondary) + + view = View() + view.add_item(yes_button) + view.add_item(no_button) + + # Define button callbacks + async def yes_callback(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message( + "This interaction is not for you.", ephemeral=True + ) + + # Remove all users from no-prefix list + async with aiosqlite.connect(self.db_path) as db: + # Get count of users before deletion + async with db.execute("SELECT COUNT(*) FROM np") as cursor: + count = (await cursor.fetchone())[0] + + # Delete all entries + await db.execute("DELETE FROM np") + await db.commit() + + # Remove roles from all members in the main guild + guild = self.client.get_guild(1401125905677553716) + if guild: + role = guild.get_role(1401134167873290311) + if role: + members_with_role = [ + member for member in guild.members if role in member.roles + ] + for member in members_with_role: + try: + await member.remove_roles( + role, reason="Global no-prefix reset" + ) + except discord.HTTPException: + pass + + # Send success message + success_embed = CV2Embed( + title=f"{TICK} No-Prefix Reset Complete", + description=f"Successfully removed {count} users from the no-prefix list.", + color=0xFF0000, + ) + await interaction.response.edit_message(view=success_embed) + + # Log the action + log_channel = self.client.get_channel(LOG_CHANNEL_ID) if LOG_CHANNEL_ID else None + if log_channel: + log_embed = CV2Embed( + title="No-Prefix List Reset", + description=f"**Reset By**: [{ctx.author.display_name}](https://discord.com/users/{ctx.author.id})\n{INFO} Users Removed: {count}", + color=0xFF0000, + ) + log_embed.set_footer(text="No Prefix Reset Log") + await log_channel.send(view=log_embed) + + async def no_callback(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message( + "This interaction is not for you.", ephemeral=True + ) + + cancel_embed = CV2Embed( + title="Reset Cancelled", + description="No changes have been made to the no-prefix list.", + color=0xFF0000, + ) + await interaction.response.edit_message(view=cancel_embed) + + yes_button.callback = yes_callback + no_button.callback = no_callback + + add_action_rows(embed, view.children) + await ctx.reply(view=embed) diff --git a/bot/cogs/commands/owner.py b/bot/cogs/commands/owner.py new file mode 100644 index 0000000..f0b4f9a --- /dev/null +++ b/bot/cogs/commands/owner.py @@ -0,0 +1,834 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations +from discord.ext import commands +from discord import * +from PIL import Image, ImageDraw, ImageFont, ImageFilter +import discord +import json +import datetime +import asyncio +import aiosqlite +from typing import Optional +from utils import Paginator, DescriptionEmbedPaginator, FieldPagePaginator, TextPaginator +from utils.Tools import * +from utils.config import OWNER_IDS, BOT_OWNER_IDS +from utils.emoji import BOOSTS, DISCORD_BADGE_EMOJIS, LOADINGRED, NITRO_BOOST, TICK, ZWARNING +from core import Cog, zyrox, Context +import sqlite3 +import os +import requests +import numpy as np +from io import BytesIO +from utils.config import OWNER_IDS, BOT_OWNER_IDS +from discord.errors import Forbidden +from discord import Embed +from discord.ui import Button, View +from utils.config import * + +# p1 + + +# --- Configuration & Helpers --- +# OWNER_IDS and BOT_OWNER_IDS are imported from utils.config — edit .env to change them + +# Your custom bot badges, including Family and Developer +BADGE_URLS = { + "owner": "https://cdn.discordapp.com/emojis/1448951721479901334.png?v=1&size=48&quality=lossless", + "staff": "https://cdn.discordapp.com/emojis/1448949616681812098.png?v=1&size=48&quality=lossless", + "partner": "https://cdn.discordapp.com/emojis/1448949605676093540.png?v=1&size=48&quality=lossless", + "sponsor": "https://cdn.discordapp.com/emojis/1448949571811282984.png?v=1&size=48&quality=lossless", + "friend": "https://cdn.discordapp.com/emojis/1448951509235531869.png?v=1&size=48&quality=lossless", + "early": "https://cdn.discordapp.com/emojis/1448949582573867039.png?v=1&size=48&quality=lossless", + "vip": "https://cdn.discordapp.com/emojis/1448951307707748395.png?v=1&size=48&quality=lossless", + "bug": "https://cdn.discordapp.com/emojis/1448949593923518485.png?v=1&size=48&quality=lossless", + "developer": "https://cdn.discordapp.com/emojis/1448951697853386826.png?v=1&size=48&quality=lossless", + "family": "https://cdn.discordapp.com/emojis/1448951456861519962.png?v=1&size=48&quality=lossless", # New Family Badge +} + +BADGE_NAMES = { + "owner": "Owner", "staff": "Staff", "partner": "Partner", + "sponsor": "Sponsor", "friend": "Friends", "early": "Early Supporter", + "vip": "VIP", "bug": "Bug Hunter", "developer": "Developer", "family": "Family" +} + +# Emojis for official Discord badges (imported from utils.emoji) +# Keep reference for backward compatibility +BACK_COMPAT_BADGE_DICT = DISCORD_BADGE_EMOJIS + + +db_folder = 'db' +db_file = 'badges.db' +db_path = os.path.join(db_folder, db_file) +FONT_PATH = os.path.join('utils', 'arial.ttf') + +# --- Database Setup --- +os.makedirs(db_folder, exist_ok=True) +conn = sqlite3.connect(db_path) +c = conn.cursor() + +c.execute('CREATE TABLE IF NOT EXISTS badges (user_id INTEGER PRIMARY KEY)') +conn.commit() + +for badge_name in BADGE_NAMES: + try: + c.execute(f"ALTER TABLE badges ADD COLUMN {badge_name} INTEGER DEFAULT 0") + conn.commit() + except sqlite3.OperationalError: + pass + +# --- Helper Functions --- +def add_badge(user_id, badge): + try: + if badge not in BADGE_URLS: return False + c.execute("SELECT 1 FROM badges WHERE user_id = ?", (user_id,)) + if c.fetchone() is None: + c.execute(f"INSERT INTO badges (user_id, {badge}) VALUES (?, 1)", (user_id,)) + else: + c.execute(f"UPDATE badges SET {badge} = 1 WHERE user_id = ?", (user_id,)) + conn.commit() + return True + except sqlite3.Error: + return False + +def remove_badge(user_id, badge): + try: + if badge not in BADGE_URLS: return False + c.execute(f"UPDATE badges SET {badge} = 0 WHERE user_id = ?", (user_id,)) + conn.commit() + return True + except sqlite3.Error: + return False + +async def is_owner_or_staff(ctx): + owner_cog = ctx.bot.get_cog("Owner") + is_staff_member = False + if owner_cog: + is_staff_member = ctx.author.id in getattr(owner_cog, 'staff', set()) + return is_staff_member or ctx.author.id in OWNER_IDS + +def blacklist_check(): + async def predicate(ctx): return True + return commands.check(predicate) + +def ignore_check(): + async def predicate(ctx): return True + return commands.check(predicate) + + +from utils.config import BotName + +class Owner(commands.Cog): + + def __init__(self, client): + self.client = client + self.staff = set() + self.np_cache = [] + self.db_path = 'db/np.db' + self.stop_tour = False + self.bot_owner_ids = BOT_OWNER_IDS + self.client.loop.create_task(self.setup_database()) + self.client.loop.create_task(self.load_staff()) + + + async def setup_database(self): + async with aiosqlite.connect(self.db_path) as db: + await db.execute(''' + CREATE TABLE IF NOT EXISTS staff ( + id INTEGER PRIMARY KEY + ) + ''') + await db.commit() + + + + async def load_staff(self): + await self.client.wait_until_ready() + async with aiosqlite.connect(self.db_path) as db: + async with db.execute('SELECT id FROM staff') as cursor: + self.staff = {row[0] for row in await cursor.fetchall()} + + @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: + sonu = discord.Embed(title=f"{ZWARNING} Access Denied", description=f"{user} is already in the staff list.", color=0xFF0000) + await ctx.reply(embed=sonu, mention_author=False) + else: + self.staff.add(user.id) + async with aiosqlite.connect(self.db_path) as db: + await db.execute('INSERT OR IGNORE INTO staff (id) VALUES (?)', (user.id,)) + await db.commit() + 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.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: + sonu = discord.Embed(title=f"{ZWARNING} Access Denied", description=f"{user} is not in the staff list.", color=0xFF0000) + await ctx.reply(embed=sonu, mention_author=False) + else: + self.staff.remove(user.id) + async with aiosqlite.connect(self.db_path) as db: + await db.execute('DELETE FROM staff WHERE id = ?', (user.id,)) + await db.commit() + 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.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: + await ctx.send("The staff list is currently empty.") + else: + member_list = [] + for staff_id in self.staff: + member = await self.client.fetch_user(staff_id) + member_list.append(f"{member.name}#{member.discriminator} (ID: {staff_id})") + staff_display = "\n".join(member_list) + sonu = discord.Embed(title=f"{TICK} {BotName} Staffs", description=f"\n{staff_display}", color=0xFF0000) + await ctx.send(embed=sonu) + + @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) + entries = [ + f"`#{i}` | [{g.name}](https://discord.com/guilds/{g.id}) - {g.member_count}" + for i, g in enumerate(servers, start=1) + ] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + description="", + title=f"Guild List of {BotName} [{len(self.client.guilds)}]", + color=0xFF0000, + per_page=10), + ctx=ctx) + await paginator.paginate() + + + @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] + entries = [ + f"`#{no}` | [{guild.name}](https://discord.com/channels/{guild.id}) - {guild.member_count}" + for no, guild in enumerate(guilds, start=1) + ] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + description="", + title=f"Mutual Guilds of {user.name} [{len(guilds)}]", + color=0xFF0000, + per_page=10), + ctx=ctx) + await paginator.paginate() + + @commands.command(name="getinvite", aliases=["gi", "guildinvite"]) + @commands.is_owner() + async def getinvite(self, ctx: Context, guild= discord.Guild): + if not guild: + await ctx.send("Invalid server.") + return + + perms_ha = guild.me.guild_permissions.view_audit_log + invite_krskta = guild.me.guild_permissions.create_instant_invite + + try: + invites = await guild.invites() + if invites: + entries = [f"{invite.url} - {invite.uses} uses" for invite in invites] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + title=f"Active Invites for {guild.name}", + description="", + per_page=10, + color=0xFF0000), + ctx=ctx) + await paginator.paginate() + elif invite_krskta: + channel = guild.system_channel or next((ch for ch in guild.text_channels if ch.permissions_for(guild.me).create_instant_invite), None) + if channel: + invite = await channel.create_invite(max_age=86400, max_uses=1, reason="No active invites found, creating a new one.") + await ctx.send(f"Created new invite: {invite.url}") + else: + await ctx.send("No channel found.") + else: + await ctx.send("Can't create invites.") + except discord.Forbidden: + await ctx.send("Forbidden.") + + + @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.command(name="sync", help="Syncs all database.") + @commands.is_owner() + async def _sync(self, ctx): + await ctx.reply("Syncing...", mention_author=False) + with open('events.json', 'r') as f: + data = json.load(f) + for guild in self.client.guilds: + if str(guild.id) not in data['guild']: + data['guilds'][str(guild.id)] = 'on' + with open('events.json', 'w') as f: + json.dump(data, f, indent=4) + else: + pass + with open('config.json', 'r') as f: + data = json.load(f) + for op in data["guilds"]: + g = self.client.get_guild(int(op)) + if not g: + data["guilds"].pop(str(op)) + with open('config.json', 'w') as f: + json.dump(data, f, indent=4) + + + @commands.command(name="owners") + @commands.is_owner() + async def own_list(self, ctx): + nplist = OWNER_IDS + npl = ([await self.client.fetch_user(nplu) for nplu in nplist]) + npl = sorted(npl, key=lambda nop: nop.created_at) + entries = [ + f"`#{no}` | [{mem}](https://discord.com/users/{mem.id}) (ID: {mem.id})" + for no, mem in enumerate(npl, start=1) + ] + paginator = Paginator(source=DescriptionEmbedPaginator( + entries=entries, + title=f"{BRAND_NAME} Owners [{len(nplist)}]", + description="", + per_page=10, + color=0xFF0000), + ctx=ctx) + await paginator.paginate() + + + + + + @commands.command() + @commands.is_owner() + async def dm(self, ctx, user: discord.User, *, message: str): + """ DM the user of your choice """ + try: + await user.send(message) + await ctx.send(f"{TICK} | Successfully Sent a DM to **{user}**") + except discord.Forbidden: + await ctx.send("This user might be having DMs blocked or it's a bot account...") + + + + @commands.group() + @commands.is_owner() + async def change(self, ctx): + if ctx.invoked_subcommand is None: + await ctx.send_help(str(ctx.command)) + + + @change.command(name="nickname") + @commands.is_owner() + async def change_nickname(self, ctx, *, name: str = None): + """ Change nickname. """ + try: + await ctx.guild.me.edit(nick=name) + if name: + await ctx.send(f"{TICK} | Successfully changed nickname to **{name}**") + else: + await ctx.send(f"{TICK} | Successfully removed nickname") + except Exception as err: + await ctx.send(err) + + + @commands.command(name="ownerban", aliases=["forceban", "dna"]) + @commands.is_owner() + async def _ownerban(self, ctx: Context, user_id: int, *, reason: str = "No reason provided"): + + member = ctx.guild.get_member(user_id) + if member: + try: + await member.ban(reason=reason) + embed = discord.Embed( + title="Successfully Banned", + description=f"{TICK} | **{member.name}** has been successfully banned from {ctx.guild.name} by the Bot Owner.", + color=0xFF0000) + await ctx.reply(embed=embed, mention_author=False, delete_after=3) + await ctx.message.delete() + except discord.Forbidden: + embed = discord.Embed( + title="Error!", + description=f"{ZWARNING} I do not have permission to ban **{member.name}** in this guild.", + color=0xFF0000 + ) + await ctx.reply(embed=embed, mention_author=False, delete_after=5) + await ctx.message.delete() + except discord.HTTPException: + embed = discord.Embed( + title="Error!", + description=f"{ZWARNING} An error occurred while banning **{member.name}**.", + color=0xFF0000 + ) + await ctx.reply(embed=embed, mention_author=False, delete_after=5) + await ctx.message.delete() + else: + await ctx.reply("User not found in this guild.", mention_author=False, delete_after=3) + await ctx.message.delete() + + @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) + if user: + try: + await ctx.guild.unban(user, reason=reason) + embed = discord.Embed( + title="Successfully Unbanned", + description=f"{TICK} | **{user.name}** has been successfully unbanned from {ctx.guild.name} by the Bot Owner.", + color=0xFF0000 + ) + await ctx.reply(embed=embed, mention_author=False) + except discord.Forbidden: + embed = discord.Embed( + title="Error!", + description=f"{ZWARNING} I do not have permission to unban **{user.name}** in this guild.", + color=0xFF0000 + ) + await ctx.reply(embed=embed, mention_author=False) + except discord.HTTPException: + embed = discord.Embed( + title="Error!", + description=f"{ZWARNING} An error occurred while unbanning **{user.name}**.", + color=0xFF0000 + ) + await ctx.reply(embed=embed, mention_author=False) + else: + await ctx.reply("User not found.", mention_author=False) + + + + @commands.command(name="globalunban") + @commands.is_owner() + async def globalunban(self, ctx: Context, user: discord.User): + success_guilds = [] + error_guilds = [] + + for guild in self.client.guilds: + bans = await guild.bans() + if any(ban_entry.user.id == user.id for ban_entry in bans): + try: + await guild.unban(user, reason="Global Unban") + success_guilds.append(guild.name) + except discord.HTTPException: + error_guilds.append(guild.name) + except discord.Forbidden: + error_guilds.append(guild.name) + + user_mention = f"{user.mention} (**{user.name}**)" + + success_message = f"Successfully unbanned {user_mention} from the following guild(s):\n{', '.join(success_guilds)}" if success_guilds else "No guilds where the user was successfully unbanned." + error_message = f"Failed to unban {user_mention} from the following guild(s):\n{', '.join(error_guilds)}" if error_guilds else "No errors during unbanning." + + await ctx.reply(f"{success_message}\n{error_message}", mention_author=False) + + @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) + if not guild: + await ctx.reply("Bot is not present in the specified guild.", mention_author=False) + return + + member = guild.get_member(user_id) + if member: + try: + await guild.ban(member, reason=reason) + await ctx.reply(f"Successfully banned **{member.name}** from {guild.name}.", mention_author=False) + except discord.Forbidden: + await ctx.reply(f"Missing permissions to ban **{member.name}** in {guild.name}.", mention_author=False) + except discord.HTTPException as e: + await ctx.reply(f"An error occurred while banning **{member.name}** in {guild.name}: {str(e)}", mention_author=False) + else: + await ctx.reply(f"User not found in the specified guild {guild.name}.", mention_author=False) + + @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) + if not guild: + await ctx.reply("Bot is not present in the specified guild.", mention_author=False) + return + #member = guild.get_member(user_id) + + try: + user = await self.client.fetch_user(user_id) + except discord.NotFound: + await ctx.reply(f"User with ID {user_id} not found.", mention_author=False) + return + + user = discord.Object(id=user_id) + try: + await guild.unban(user, reason=reason) + await ctx.reply(f"Successfully unbanned user ID {user_id} from {guild.name}.", mention_author=False) + except discord.Forbidden: + await ctx.reply(f"Missing permissions to unban user ID {user_id} in {guild.name}.", mention_author=False) + except discord.HTTPException as e: + await ctx.reply(f"An error occurred while unbanning user ID {user_id} in {guild.name}: {str(e)}", mention_author=False) + + + @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) + if guild is None: + await ctx.send(f"Guild with ID {guild_id} not found.") + return + + await guild.leave() + await ctx.send(f"Left the guild: {guild.name} ({guild.id})") + + @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) + if guild is None: + await ctx.send(f"Guild with ID {guild_id} not found.") + return + + embed = discord.Embed( + title=guild.name, + description=f"Information for guild ID {guild.id}", + color=0x00000 + ) + embed.add_field(name="Owner", value=str(guild.owner), inline=True) + embed.add_field(name="Member Count", value=str(guild.member_count), inline=True) + embed.add_field(name="Text Channels", value=len(guild.text_channels), inline=True) + embed.add_field(name="Voice Channels", value=len(guild.voice_channels), inline=True) + embed.add_field(name="Roles", value=len(guild.roles), inline=True) + if guild.icon is not None: + embed.set_thumbnail(url=guild.icon.url) + embed.set_footer(text=f"Created at: {guild.created_at}") + + await ctx.send(embed=embed) + + @commands.command() + @commands.is_owner() + async def servertour(self, ctx, time_in_seconds: int, member: discord.Member): + guild = ctx.guild + + if time_in_seconds > 3600: + await ctx.send("Time cannot be greater than 3600 seconds (1 hour).") + return + + if not member.voice: + await ctx.send(f"{member.display_name} is not in a voice channel.") + return + + voice_channels = [ch for ch in guild.voice_channels if ch.permissions_for(guild.me).move_members] + + if len(voice_channels) < 2: + await ctx.send("Not enough voice channels to move the user.") + return + + self.stop_tour = False + + class StopButton(discord.ui.View): + def __init__(self, outer_self): + super().__init__(timeout=time_in_seconds) + self.outer_self = outer_self + + @discord.ui.button(label="Stop", style=discord.ButtonStyle.danger) + async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button): + if interaction.user.id not in self.outer_self.bot_owner_ids: + await interaction.response.send_message("Only the bot owner can stop this process.", ephemeral=True) + return + self.outer_self.stop_tour = True + await interaction.response.send_message("Server tour has been stopped.", ephemeral=True) + self.stop() + + view = StopButton(self) + message = await ctx.send(f"Started moving {member.display_name} for {time_in_seconds} seconds. Click the button to stop.", view=view) + + end_time = asyncio.get_event_loop().time() + time_in_seconds + + while asyncio.get_event_loop().time() < end_time and not self.stop_tour: + for ch in voice_channels: + if self.stop_tour: + await ctx.send("Tour stopped.") + return + if not member.voice: + await ctx.send(f"{member.display_name} left the voice channel.") + return + try: + await member.move_to(ch) + await asyncio.sleep(5) + except Forbidden: + await ctx.send(f"Missing permissions to move {member.display_name}.") + return + except Exception as e: + await ctx.send(f"Error: {str(e)}") + return + + if not self.stop_tour: + await message.edit(content=f"Finished moving {member.display_name} after {time_in_seconds} seconds.", view=None) + + + + + + + + @commands.group() + @commands.check(is_owner_or_staff) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def bdg(self, ctx): + if ctx.invoked_subcommand is None: + embed = discord.Embed(description='Invalid `bdg` command passed. Use `add` or `remove`.', color=0xFF0000) + await ctx.send(embed=embed) + + @bdg.command() + @commands.check(is_owner_or_staff) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def add(self, ctx, member: discord.Member, badge: str): + badge = badge.lower() + user_id = member.id + if badge in BADGE_URLS or badge == 'bug' or badge == 'all': + if badge == 'all': + for b in BADGE_URLS.keys(): + add_badge(user_id, b) + add_badge(user_id, 'bug') + embed = discord.Embed(description=f"All badges added to {member.mention}.", color=0xFF0000) + await ctx.send(embed=embed) + else: + success = add_badge(user_id, badge) + if success: + embed = discord.Embed(description=f"Badge `{badge}` added to {member.mention}.", color=0xFF0000) + else: + embed = discord.Embed(description=f"{member.mention} already has the badge `{badge}`.", color=0xFF0000) + await ctx.send(embed=embed) + else: + embed = discord.Embed(description=f"Invalid badge: `{badge}`", color=0xFF0000) + await ctx.send(embed=embed) + + @bdg.command() + @commands.check(is_owner_or_staff) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def remove(self, ctx, member: discord.Member, badge: str): + badge = badge.lower() + user_id = member.id + if badge in BADGE_URLS or badge == 'bug' or badge == 'all': + if badge == 'all': + for b in BADGE_URLS.keys(): + remove_badge(user_id, b) + remove_badge(user_id, 'bug') + embed = discord.Embed(description=f"All badges removed from {member.mention}.", color=0xFF0000) + await ctx.send(embed=embed) + else: + success = remove_badge(user_id, badge) + if success: + embed = discord.Embed(description=f"Badge `{badge}` removed from {member.mention}.", color=0xFF0000) + else: + embed = discord.Embed(description=f"{member.mention} does not have the badge `{badge}`.", color=0xFF0000) + await ctx.send(embed=embed) + else: + embed = discord.Embed(description=f"Invalid badge: `{badge}`", color=0xFF0000) + await ctx.send(embed=embed) + + + @commands.command(name="forcepurgebots", + aliases=["fpb"], + help="Clear recently bot messages in channel (Bot owner only)") + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.is_owner() + @commands.bot_has_permissions(manage_messages=True) + async def _purgebot(self, ctx, prefix=None, search=100): + + await ctx.message.delete() + + def predicate(m): + return (m.webhook_id is None and m.author.bot) or (prefix and m.content.startswith(prefix)) + + await do_removal(ctx, search, predicate) + + + @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) + @commands.is_owner() + @commands.bot_has_permissions(manage_messages=True) + async def purguser(self, ctx, member: discord.Member, search=100): + + await ctx.message.delete() + + await do_removal(ctx, search, lambda e: e.author == member) + + +# p2 +class Badges(commands.Cog): + """Handles the profile and badge display system.""" + def __init__(self, bot): + self.bot = bot + + def generate_profile_image(self, member: discord.Member, user_bot_badges: dict): + # --- Fonts and Colors --- + font_title = ImageFont.truetype(FONT_PATH, 22) + font_text = ImageFont.truetype(FONT_PATH, 18) + font_badge = ImageFont.truetype(FONT_PATH, 20) + + BG_COLOR = (30, 31, 34) + BOX_COLOR = (43, 45, 49) + TEXT_COLOR = (255, 255, 255) + + W, H = (850, 450) + img = Image.new('RGB', (W, H), BG_COLOR) + draw = ImageDraw.Draw(img) + + # --- Left Column (Avatar & Username) --- + draw.text((40, 30), "Avatar:", font=font_title, fill=TEXT_COLOR) + try: + pfp_resp = requests.get(member.display_avatar.with_size(128).url) + pfp_resp.raise_for_status() + pfp = Image.open(BytesIO(pfp_resp.content)).convert("RGBA") + except requests.RequestException: + pfp = Image.new("RGBA", (128, 128), (0,0,0,0)) + pfp = pfp.resize((128, 128), Image.Resampling.LANCZOS) + draw.rectangle((40, 70, 188, 218), fill=BOX_COLOR) + img.paste(pfp, (50, 80), pfp) + + draw.text((40, 250), "Username:", font=font_title, fill=TEXT_COLOR) + draw.rectangle((40, 290, 280, 340), fill=BOX_COLOR) + draw.text((50, 300), str(member), font=font_text, fill=TEXT_COLOR) + + # --- Right Column (Bot Badges) --- + draw.text((320, 30), "Badges:", font=font_title, fill=TEXT_COLOR) + + badge_priority = [ + "owner", "developer", "staff", "partner", "sponsor", + "vip", "friend", "early", "bug", "family" + ] + + active_badges = [name for name in badge_priority if user_bot_badges.get(name) == 1] + + col_width, row_height, gap_x, gap_y = 220, 50, 20, 15 + start_x, start_y = 320, 70 + + # Loop 10 times to draw all slots + for i in range(10): + col = i % 2 + row = i // 2 + + x = start_x + col * (col_width + gap_x) + y = start_y + row * (row_height + gap_y) + + # Always draw the empty block + draw.rectangle((x, y, x + col_width, y + row_height), fill=BOX_COLOR) + + # If there's an active badge for this slot, draw it + if i < len(active_badges): + name = active_badges[i] + try: + badge_resp = requests.get(BADGE_URLS[name]) + badge_resp.raise_for_status() + icon = Image.open(BytesIO(badge_resp.content)).resize((30, 30)) + img.paste(icon, (x + 10, y + 10), icon) + except requests.RequestException: + pass + draw.text((x + 50, y + 12), BADGE_NAMES[name], font=font_badge, fill=TEXT_COLOR) + + with BytesIO() as image_binary: + img.save(image_binary, 'PNG') + image_binary.seek(0) + return discord.File(fp=image_binary, filename='profile.png') + + @commands.hybrid_command(name='profile', aliases=['pr', 'badgesf']) + @commands.cooldown(1, 8, commands.BucketType.user) + async def profile(self, ctx: commands.Context, member: discord.Member = None): + member = member or ctx.author + + loading_embed = discord.Embed( + title="{LOADINGRED} Loading Profile...", + color=0xFF0000 + ) + processing_msg = await ctx.send(embed=loading_embed) + + c.execute("SELECT * FROM badges WHERE user_id = ?", (member.id,)) + db_badges_data = c.fetchone() + + user_bot_badges = {} + if db_badges_data: + column_names = [desc[0] for desc in c.description] + user_bot_badges = dict(zip(column_names, db_badges_data)) + + # Default "Family" badge for everyone + user_bot_badges['family'] = 1 + + try: + loop = asyncio.get_event_loop() + file = await loop.run_in_executor(None, self.generate_profile_image, member, user_bot_badges) + except Exception as e: + error_embed = discord.Embed( + title="Error", + description=f"Failed to create profile image: {e}", + color=0xFF0000 + ) + return await processing_msg.edit(embed=error_embed) + + embed = discord.Embed(color=0xFF0000) + embed.set_thumbnail(url=member.display_avatar.url) + embed.set_author(name=f"◇ {member.name}'s Profile", icon_url=self.bot.user.display_avatar.url) + + description = ( + f"**◇ Account Created**: \n" + f"**◇ Joined Server**: \n" + f"**◇ User ID**: `{member.id}`\n\n" + ) + + badge_list = [] + user = await self.bot.fetch_user(member.id) + if user.banner or (user.avatar and user.avatar.is_animated()): + badge_list.append(f"{DISCORD_BADGE_EMOJIS.get('nitro', '💎')} Nitro Subscriber") + if member.premium_since: + badge_list.append(f"{DISCORD_BADGE_EMOJIS.get('boost', '✨')} Server Booster") + + for flag in user.public_flags.all(): + if flag.name in DISCORD_BADGE_EMOJIS: + badge_list.append(f"{DISCORD_BADGE_EMOJIS[flag.name]} {flag.name.replace('_', ' ').title()}") + + if badge_list: + description += "**Official Badges:**\n" + "\n".join(badge_list) + + embed.description = description + embed.set_image(url="attachment://profile.png") + embed.set_footer(text=f"Requested by {ctx.author.name}", icon_url=ctx.author.display_avatar.url) + + await processing_msg.delete() + await ctx.send(embed=embed, file=file) + +async def setup(client): + if not hasattr(client, 'session'): + client.session = aiohttp.ClientSession() + await client.add_cog(Badges(client)) + \ No newline at end of file diff --git a/bot/cogs/commands/owner2.py b/bot/cogs/commands/owner2.py new file mode 100644 index 0000000..8b4dde0 --- /dev/null +++ b/bot/cogs/commands/owner2.py @@ -0,0 +1,613 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +from discord.ui import Button, View +from discord import Member +from utils import Paginator, DescriptionEmbedPaginator +from datetime import timedelta +import asyncio + +class Global(commands.Cog): + def __init__(self, client): + self.client = client + self.local_frozen_nicks = {} + self.client.frozen_nicknames = {} + + @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.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)] + mutual_count = len(mutual_guilds) + + confirm_embed = discord.Embed( + title=f"Are you sure to Ban {user.display_name} Globally?", + description=f"The user is in **{mutual_count}** mutual guilds with the bot.\n\nGlobal Ban Requestor: {ctx.author.mention}", + color=0xFF0000 + ) + yes_button = Button(label="Yes", style=discord.ButtonStyle.green) + no_button = Button(label="No", style=discord.ButtonStyle.red) + view = View() + view.add_item(yes_button) + view.add_item(no_button) + + async def confirm(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + view.clear_items() + await interaction.response.edit_message(view=view) + await ctx.send(f"Processing global ban for {user.name}...") + success, failure = [], [] + for guild in mutual_guilds: + try: + await guild.ban(user, reason=reason) + success.append(guild.name) + except: + failure.append(guild.name) + embed = discord.Embed( + title="Success", + description=f"Banned the user in {len(success)} of {mutual_count} mutual guilds.", + color=0xFF0000 + ) + embed.add_field(name="Success Count", value=f"{len(success)} Guilds") + embed.add_field(name="Failure Count", value=f"{len(failure)} Guilds") + success_button = Button(label="List Successful", style=discord.ButtonStyle.green) + failure_button = Button(label="List Unsuccessful", style=discord.ButtonStyle.red) + new_view = View() + new_view.add_item(success_button) + new_view.add_item(failure_button) + + async def list_success(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + entries = [f"{i+1}. {name}" for i, name in enumerate(success)] + paginator = Paginator( + source=DescriptionEmbedPaginator(entries=entries, description="", title=f"Successful Bans [{len(success)}]", color=0xFF0000, per_page=10), + ctx=ctx + ) + await paginator.paginate() + + async def list_failure(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + entries = [f"{i+1}. {name}" for i, name in enumerate(failure)] + paginator = Paginator( + source=DescriptionEmbedPaginator(entries=entries, description="", title=f"Unsuccessful Bans [{len(failure)}]", color=0xFF0000, per_page=10), + ctx=ctx + ) + await paginator.paginate() + + success_button.callback = list_success + failure_button.callback = list_failure + await ctx.send(embed=embed, view=new_view) + + async def cancel(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + await interaction.message.delete() + + yes_button.callback = confirm + no_button.callback = cancel + await ctx.send(embed=confirm_embed, view=view) + + @global_command.command(name="kick", help="Kicks the user from all mutual guilds.") + @commands.is_owner() + async def global_kick(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)] + mutual_count = len(mutual_guilds) + + confirm_embed = discord.Embed( + title=f"Are you sure to Kick {user.display_name} Globally?", + description=f"The user is in **{mutual_count}** mutual guilds with the bot.\n\nGlobal Kick Requestor: {ctx.author.mention}", + color=0xFF0000 + ) + yes_button = Button(label="Yes", style=discord.ButtonStyle.green) + no_button = Button(label="No", style=discord.ButtonStyle.red) + view = View() + view.add_item(yes_button) + view.add_item(no_button) + + async def confirm(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + view.clear_items() + await interaction.response.edit_message(view=view) + await ctx.send(f"Processing global kick for {user.name}...") + success, failure = [], [] + for guild in mutual_guilds: + try: + await guild.kick(user, reason=reason) + success.append(guild.name) + except: + failure.append(guild.name) + embed = discord.Embed( + title="Success", + description=f"Kicked the user in {len(success)} of {mutual_count} mutual guilds.", + color=0xFF0000 + ) + embed.add_field(name="Success Count", value=f"{len(success)} Guilds") + embed.add_field(name="Failure Count", value=f"{len(failure)} Guilds") + success_button = Button(label="List Successful", style=discord.ButtonStyle.green) + failure_button = Button(label="List Unsuccessful", style=discord.ButtonStyle.red) + new_view = View() + new_view.add_item(success_button) + new_view.add_item(failure_button) + + async def list_success(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + entries = [f"{i+1}. {name}" for i, name in enumerate(success)] + paginator = Paginator( + source=DescriptionEmbedPaginator(entries=entries, description="", title=f"Successful Kicks [{len(success)}]", color=0xFF0000, per_page=10), + ctx=ctx + ) + await paginator.paginate() + + async def list_failure(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + entries = [f"{i+1}. {name}" for i, name in enumerate(failure)] + paginator = Paginator( + source=DescriptionEmbedPaginator(entries=entries, description="", title=f"Unsuccessful Kicks [{len(failure)}]", color=0xFF0000, per_page=10), + ctx=ctx + ) + await paginator.paginate() + + success_button.callback = list_success + failure_button.callback = list_failure + await ctx.send(embed=embed, view=new_view) + + async def cancel(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + await interaction.message.delete() + + yes_button.callback = confirm + no_button.callback = cancel + await ctx.send(embed=confirm_embed, view=view) + + @global_command.command(name="timeout", help="Timeouts the user for 28 days in all mutual guilds.") + @commands.is_owner() + async def global_timeout(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)] + mutual_count = len(mutual_guilds) + + confirm_embed = discord.Embed( + title=f"Are you sure to Timeout {user.display_name} Globally for 28 days?", + description=f"The user is in **{mutual_count}** mutual guilds with the bot.\n\nGlobal Timeout Requestor: {ctx.author.mention}", + color=0xFF0000 + ) + yes_button = Button(label="Yes", style=discord.ButtonStyle.green) + no_button = Button(label="No", style=discord.ButtonStyle.red) + view = View() + view.add_item(yes_button) + view.add_item(no_button) + + async def confirm(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + view.clear_items() + await interaction.response.edit_message(view=view) + await ctx.send(f"Processing global timeout for {user.name}...") + success, failure = [], [] + + for guild in mutual_guilds: + member = guild.get_member(user.id) + time_delta = (timedelta(days=28)) + if member: + try: + await member.edit(timed_out_until=discord.utils.utcnow() + time_delta, reason=reason) + success.append(guild.name) + except: + failure.append(guild.name) + embed = discord.Embed( + title="Success", + description=f"Timed out the user in {len(success)} of {mutual_count} mutual guilds.", + color=0xFF0000 + ) + embed.add_field(name="Success Count", value=f"{len(success)} Guilds") + embed.add_field(name="Failure Count", value=f"{len(failure)} Guilds") + success_button = Button(label="List Successful", style=discord.ButtonStyle.green) + failure_button = Button(label="List Unsuccessful", style=discord.ButtonStyle.red) + new_view = View() + new_view.add_item(success_button) + new_view.add_item(failure_button) + + async def list_success(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + entries = [f"{i+1}. {name}" for i, name in enumerate(success)] + paginator = Paginator( + source=DescriptionEmbedPaginator(entries=entries, description="", title=f"Successful Timeouts [{len(success)}]", color=0xFF0000, per_page=10), + ctx=ctx + ) + await paginator.paginate() + + async def list_failure(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + entries = [f"{i+1}. {name}" for i, name in enumerate(failure)] + paginator = Paginator( + source=DescriptionEmbedPaginator(entries=entries, description="", title=f"Unsuccessful Timeouts [{len(failure)}]", color=0xFF0000, per_page=10), + ctx=ctx + ) + await paginator.paginate() + + success_button.callback = list_success + failure_button.callback = list_failure + await ctx.send(embed=embed, view=new_view) + + async def cancel(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + await interaction.message.delete() + + yes_button.callback = confirm + no_button.callback = cancel + await ctx.send(embed=confirm_embed, view=view) + + + @global_command.command(name="nick", help="Changes the nickname of a user in all mutual guilds.") + @commands.is_owner() + async def global_nick(self, ctx: commands.Context, user: discord.User, *, name: str): + if len(name) > 32: + return await ctx.send("Nickname cannot exceed 32 characters. Please provide a shorter nickname.") + + mutual_guilds = [guild for guild in self.client.guilds if guild.get_member(user.id)] + mutual_count = len(mutual_guilds) + + confirm_embed = discord.Embed( + title=f"Are you sure to Change {user.display_name}'s Nickname Globally?", + description=f"The user is in **{mutual_count}** mutual guilds with the bot.\n\nGlobal Nick Requestor: {ctx.author.mention}", + color=0xFF0000 + ) + yes_button = Button(label="Yes", style=discord.ButtonStyle.green) + no_button = Button(label="No", style=discord.ButtonStyle.red) + view = View() + view.add_item(yes_button) + view.add_item(no_button) + + async def confirm(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + view.clear_items() + await interaction.response.edit_message(view=view) + await ctx.send(f"Processing global nickname change for {user.name}...") + success, failure = [], [] + for guild in mutual_guilds: + try: + member = guild.get_member(user.id) + if member: + await member.edit(nick=name) + success.append(guild.name) + except: + failure.append(guild.name) + embed = discord.Embed( + title="Success", + description=f"Set the nickname for {user.name} in {len(success)} of {mutual_count} mutual guilds.", + color=0xFF0000 + ) + embed.add_field(name="Success Count", value=f"{len(success)} Guilds") + embed.add_field(name="Failure Count", value=f"{len(failure)} Guilds") + success_button = Button(label="List Successful", style=discord.ButtonStyle.green) + failure_button = Button(label="List Unsuccessful", style=discord.ButtonStyle.red) + new_view = View() + new_view.add_item(success_button) + new_view.add_item(failure_button) + + async def list_success(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + entries = [f"{i+1}. {name}" for i, name in enumerate(success)] + paginator = Paginator( + source=DescriptionEmbedPaginator(entries=entries, description="", title=f"Successful Nickname Change [{len(success)}]", color=0xFF0000, per_page=10), + ctx=ctx + ) + await paginator.paginate() + + async def list_failure(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + entries = [f"{i+1}. {name}" for i, name in enumerate(failure)] + paginator = Paginator( + source=DescriptionEmbedPaginator(entries=entries, description="", title=f"Unsuccessful Nickname Change [{len(failure)}]", color=0xFF0000, per_page=10), + ctx=ctx + ) + await paginator.paginate() + + success_button.callback = list_success + failure_button.callback = list_failure + await ctx.send(embed=embed, view=new_view) + + async def cancel(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + await interaction.message.delete() + + yes_button.callback = confirm + no_button.callback = cancel + await ctx.send(embed=confirm_embed, view=view) + + + @global_command.command(name="clearnick", help="Clears the nickname of a user in all mutual guilds.") + @commands.is_owner() + async def global_clearnick(self, ctx: commands.Context, user: discord.User): + mutual_guilds = [guild for guild in self.client.guilds if guild.get_member(user.id)] + mutual_count = len(mutual_guilds) + + confirm_embed = discord.Embed( + title=f"Are you sure to Clear {user.display_name}'s Nickname Globally?", + description=f"The user is in **{mutual_count}** mutual guilds with the bot.\n\nGlobal Clearnick Requestor: {ctx.author.mention}", + color=0xFF0000 + ) + yes_button = Button(label="Yes", style=discord.ButtonStyle.green) + no_button = Button(label="No", style=discord.ButtonStyle.red) + view = View() + view.add_item(yes_button) + view.add_item(no_button) + + async def confirm(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + view.clear_items() + await interaction.response.edit_message(view=view) + await ctx.send(f"Processing global nickname clear for {user.name}...") + success, failure = [], [] + for guild in mutual_guilds: + try: + member = guild.get_member(user.id) + if member: + await member.edit(nick=None) + success.append(guild.name) + except: + failure.append(guild.name) + embed = discord.Embed( + title="Success", + description=f"Cleared the nickname for {user.name} in {len(success)} of {mutual_count} mutual guilds.", + color=0xFF0000 + ) + embed.add_field(name="Success Count", value=f"{len(success)} Guilds") + embed.add_field(name="Failure Count", value=f"{len(failure)} Guilds") + success_button = Button(label="List Successful", style=discord.ButtonStyle.green) + failure_button = Button(label="List Unsuccessful", style=discord.ButtonStyle.red) + new_view = View() + new_view.add_item(success_button) + new_view.add_item(failure_button) + + async def list_success(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + entries = [f"{i+1}. {name}" for i, name in enumerate(success)] + paginator = Paginator( + source=DescriptionEmbedPaginator(entries=entries, description="", title=f"Successful Nickname Clear [{len(success)}]", color=0xFF0000, per_page=10), + ctx=ctx + ) + await paginator.paginate() + + async def list_failure(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + entries = [f"{i+1}. {name}" for i, name in enumerate(failure)] + paginator = Paginator( + source=DescriptionEmbedPaginator(entries=entries, description="", title=f"Unsuccessful Nickname Clear [{len(failure)}]", color=0xFF0000, per_page=10), + ctx=ctx + ) + await paginator.paginate() + + success_button.callback = list_success + failure_button.callback = list_failure + await ctx.send(embed=embed, view=new_view) + + async def cancel(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + await interaction.message.delete() + + yes_button.callback = confirm + no_button.callback = cancel + await ctx.send(embed=confirm_embed, view=view) + + + @global_command.command(name="freezenick", help="Freezes a user's nickname in all mutual guilds.") + @commands.is_owner() + async def global_freezenick(self, ctx: commands.Context, user: discord.User, *, name: str): + if len(name) > 32: + return await ctx.send("Nickname cannot exceed 32 characters. Please provide a shorter nickname.") + + if not hasattr(self.client, "frozen_nicknames"): + self.client.frozen_nicknames = {} + + mutual_guilds = [guild for guild in self.client.guilds if guild.get_member(user.id)] + mutual_count = len(mutual_guilds) + + confirm_embed = discord.Embed( + title=f"Are you sure to Freeze {user.display_name}'s Nickname Globally?", + description=f"The user is in {mutual_count} mutual guilds with the bot.\n\nGlobal Freezenick Requestor: {ctx.author.mention}", + color=0xFF0000 + ) + yes_button = Button(label="Yes", style=discord.ButtonStyle.green) + no_button = Button(label="No", style=discord.ButtonStyle.red) + view = View() + view.add_item(yes_button) + view.add_item(no_button) + + async def confirm(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + view.clear_items() + await interaction.response.edit_message(view=None) + + self.client.frozen_nicknames[user.id] = { + "name": name, + "guild_ids": [guild.id for guild in mutual_guilds], + } + + success, failure = [], [] + for guild in mutual_guilds: + try: + member = guild.get_member(user.id) + if member: + await member.edit(nick=name) + success.append(guild.name) + except: + failure.append(guild.name) + + embed = discord.Embed( + title="Results", + description=f"Frozen nickname for {user.name} in {len(success)} of {mutual_count} mutual guilds.", + color=0xFF0000 + ) + embed.add_field(name="Success Count", value=f"{len(success)} Guilds") + embed.add_field(name="Failure Count", value=f"{len(failure)} Guilds") + + success_button = Button(label="List Successful", style=discord.ButtonStyle.green) + failure_button = Button(label="List Unsuccessful", style=discord.ButtonStyle.red) + stop_button = Button(label="Stop Freezing", style=discord.ButtonStyle.red) + result_view = View() + result_view.add_item(success_button) + result_view.add_item(failure_button) + result_view.add_item(stop_button) + + async def list_success(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + entries = [f"{i+1}. {name}" for i, name in enumerate(success)] + paginator = Paginator( + source=DescriptionEmbedPaginator(entries=entries, description="", title=f"Successful Freezes [{len(success)}]", color=0xFF0000, per_page=10), + ctx=ctx + ) + await paginator.paginate() + + async def list_failure(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + entries = [f"{i+1}. {name}" for i, name in enumerate(failure)] + paginator = Paginator( + source=DescriptionEmbedPaginator(entries=entries, description="", title=f"Unsuccessful Freezes [{len(failure)}]", color=0xFF0000, per_page=10), + ctx=ctx + ) + await paginator.paginate() + + async def stop_freeze(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + self.client.frozen_nicknames.pop(user.id, None) + await interaction.response.send_message(f"Nickname freezing stopped for {user.name}.", ephemeral=True) + + success_button.callback = list_success + failure_button.callback = list_failure + stop_button.callback = stop_freeze + + await ctx.send(embed=embed, view=result_view) + self.client.loop.create_task(self.nickname_freeze_task(user.id)) + + async def cancel(interaction): + if interaction.user != ctx.author: + return await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + await interaction.message.delete() + await ctx.send("Nickname freezing cancelled.") + + yes_button.callback = confirm + no_button.callback = cancel + await ctx.send(embed=confirm_embed, view=view) + + async def nickname_freeze_task(self, user_id: int): + while user_id in self.client.frozen_nicknames: + user_data = self.client.frozen_nicknames[user_id] + frozen_name = user_data["name"] + guild_ids = user_data["guild_ids"] + + for guild_id in guild_ids: + guild = self.client.get_guild(guild_id) + if not guild: + continue + member = guild.get_member(user_id) + if member and member.nick != frozen_name: + try: + await member.edit(nick=frozen_name) + except: + pass + + await asyncio.sleep(10) + + + + @global_command.command(name="unfreezenick", help="Unfreezes a user's nickname in all mutual guilds.") + @commands.is_owner() + async def global_unfreezenick(self, ctx: commands.Context, user: discord.User): + if not hasattr(self.client, "frozen_nicknames"): + self.client.frozen_nicknames = {} + + if user.id not in self.client.frozen_nicknames: + return await ctx.send(f"❌ | {user.name}'s nickname is not being frozen.") + + del self.client.frozen_nicknames[user.id] + await ctx.send(f"✅ | Nickname freezing stopped for {user.name}.") + + + @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 + if guild_id not in self.local_frozen_nicks: + self.local_frozen_nicks[guild_id] = {} + + if member.id in self.local_frozen_nicks[guild_id]: + return await ctx.send(f"{member.mention}'s nickname is already being frozen.") + + + try: + await member.edit(nick=nickname) + self.local_frozen_nicks[guild_id][member.id] = nickname + await ctx.send(f"Freezing {member.mention}'s nickname as '{nickname}'.") + except: + return await ctx.send(f"Could not change {member.mention}'s nickname due to insufficient permissions.") + + async def monitor_nickname(): + while member.id in self.local_frozen_nicks.get(guild_id, {}): + if member.nick != nickname: + try: + await member.edit(nick=nickname) + except: + self.local_frozen_nicks[guild_id].pop(member.id, None) + await ctx.send(f"Stopped monitoring {member.mention}'s nickname due to insufficient permissions.") + break + await asyncio.sleep(10) + + if not self.local_frozen_nicks[guild_id]: + del self.local_frozen_nicks[guild_id] + + self.client.loop.create_task(monitor_nickname()) + + @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 + if guild_id in self.local_frozen_nicks and member.id in self.local_frozen_nicks[guild_id]: + self.local_frozen_nicks[guild_id].pop(member.id, None) + if not self.local_frozen_nicks[guild_id]: + del self.local_frozen_nicks[guild_id] + await ctx.send(f"✅ | Stopped freezing {member.mention}'s nickname.") + else: + await ctx.send(f"❌ | {member.mention}'s nickname is not currently being frozen.") + + diff --git a/bot/cogs/commands/qr.py b/bot/cogs/commands/qr.py new file mode 100644 index 0000000..a0f33e3 --- /dev/null +++ b/bot/cogs/commands/qr.py @@ -0,0 +1,50 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +from utils.cv2 import CV2 + +class QR(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.command( + name="qr", + aliases=["qrcode"], + help="Sends a QR code image.", + with_app_command=True + ) + @commands.has_permissions(administrator=True) + @commands.cooldown(1, 5, commands.BucketType.user) + async def qr(self, ctx): + view = CV2("Payment Platform", "Here's you can pay UPI With Below QR") + from discord.ui import MediaGallery + gallery = MediaGallery() + gallery.add_item(media="https://media.discordapp.net/attachments/1334099972739829843/1377897856286851152/share_image4613677226378289500.png?ex=69f6ec61&is=69f59ae1&hm=50c40fe1e341074865e4b29b1321ab19af3649889c276f3fa4407468dbd75f4a&=&format=webp&quality=lossless&width=441&height=892") + view.children[0].add_item(gallery) + await ctx.reply(view=view) + + @qr.error + async def qr_error(self, ctx, error): + if isinstance(error, commands.MissingPermissions): + await ctx.reply("❌ You must be an **administrator** to use this command.") + elif isinstance(error, commands.CommandOnCooldown): + await ctx.reply(f"⏳ You're on cooldown. Try again in `{round(error.retry_after, 1)}s`.") + else: + await ctx.reply(f"⚠️ An error occurred: `{str(error)}`") + +# Required for bot.load_extension() +async def setup(bot): + await bot.add_cog(QR(bot)) diff --git a/bot/cogs/commands/reactionroles.py b/bot/cogs/commands/reactionroles.py new file mode 100644 index 0000000..b261743 --- /dev/null +++ b/bot/cogs/commands/reactionroles.py @@ -0,0 +1,140 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CROSS, TICK +from discord.ext import commands +from discord.ext.commands import Context +from discord import app_commands +import sqlite3 + +class ReactionRoles(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.db = "rr.db" + self._create_table() + + def _create_table(self): + with sqlite3.connect(self.db) as conn: + conn.execute(""" + CREATE TABLE IF NOT EXISTS reaction_roles ( + guild_id INTEGER, + message_id INTEGER, + emoji TEXT, + role_id INTEGER + ) + """) + conn.execute(""" + CREATE TABLE IF NOT EXISTS rr_settings ( + guild_id INTEGER PRIMARY KEY, + dm_enabled INTEGER DEFAULT 1 + ) + """) + + def add_reaction_role(self, guild_id, message_id, emoji, role_id): + with sqlite3.connect(self.db) as conn: + conn.execute( + "INSERT INTO reaction_roles (guild_id, message_id, emoji, role_id) VALUES (?, ?, ?, ?)", + (guild_id, message_id, emoji, role_id) + ) + + def get_role_by_emoji(self, guild_id, message_id, emoji): + with sqlite3.connect(self.db) as conn: + cur = conn.execute( + "SELECT role_id FROM reaction_roles WHERE guild_id = ? AND message_id = ? AND emoji = ?", + (guild_id, message_id, emoji) + ) + result = cur.fetchone() + return result[0] if result else None + + def get_dm_setting(self, guild_id): + with sqlite3.connect(self.db) as conn: + cur = conn.execute("SELECT dm_enabled FROM rr_settings WHERE guild_id = ?", (guild_id,)) + row = cur.fetchone() + return row[0] == 1 if row else True + + def set_dm_setting(self, guild_id, value): + with sqlite3.connect(self.db) as conn: + conn.execute("REPLACE INTO rr_settings (guild_id, dm_enabled) VALUES (?, ?)", (guild_id, value)) + + @commands.hybrid_command(name="createrr", help="Create a reaction role.", usage="createrr ") + @commands.has_permissions(manage_roles=True) + async def createrr(self, ctx: Context, channel: discord.TextChannel, message_id: int, emoji: str, role: discord.Role): + try: + message = await channel.fetch_message(message_id) + await message.add_reaction(emoji) + self.add_reaction_role(ctx.guild.id, message.id, emoji, role.id) + await ctx.send(f"{TICK} Reaction role added: React with {emoji} to get {role.name}", ephemeral=True if ctx.interaction else False) + except discord.NotFound: + await ctx.send(f"{CROSS} Message not found.", ephemeral=True if ctx.interaction else False) + except discord.HTTPException as e: + await ctx.send(f"{CROSS} Error: {str(e)}", ephemeral=True if ctx.interaction else False) + + @commands.hybrid_command(name="dmrr", help="Enable or disable DM messages for reaction roles.", usage="dmrr ") + @commands.has_permissions(manage_guild=True) + async def dmrr(self, ctx: Context, mode: str): + if mode.lower() not in ["enable", "disable"]: + await ctx.send(f"{CROSS} Use `enable` or `disable`.", ephemeral=True if ctx.interaction else False) + return + + value = 1 if mode.lower() == "enable" else 0 + self.set_dm_setting(ctx.guild.id, value) + await ctx.send(f"{TICK} DM messages for reaction roles {'enabled' if value else 'disabled'}.", ephemeral=True if ctx.interaction else False) + + @commands.Cog.listener() + async def on_raw_reaction_add(self, payload): + if payload.guild_id is None or payload.member.bot: + return + + role_id = self.get_role_by_emoji(payload.guild_id, payload.message_id, str(payload.emoji)) + if role_id: + guild = self.bot.get_guild(payload.guild_id) + role = guild.get_role(role_id) + member = payload.member + + if role and member: + await member.add_roles(role, reason="Reaction role added") + + # Remove reaction + channel = guild.get_channel(payload.channel_id) + if channel: + try: + message = await channel.fetch_message(payload.message_id) + + except discord.NotFound: + pass + + # DM if enabled + if self.get_dm_setting(payload.guild_id): + try: + await member.send(f"{TICK} You received the **{role.name}** role from {guild.name}.") + except discord.Forbidden: + pass + + @commands.Cog.listener() + async def on_raw_reaction_remove(self, payload): + if payload.guild_id is None: + return + + role_id = self.get_role_by_emoji(payload.guild_id, payload.message_id, str(payload.emoji)) + if role_id: + guild = self.bot.get_guild(payload.guild_id) + member = guild.get_member(payload.user_id) + role = guild.get_role(role_id) + if role and member: + await member.remove_roles(role, reason="Reaction role removed") + +# Setup +async def setup(bot): + await bot.add_cog(ReactionRoles(bot)) diff --git a/bot/cogs/commands/slots.py b/bot/cogs/commands/slots.py new file mode 100644 index 0000000..10b1e63 --- /dev/null +++ b/bot/cogs/commands/slots.py @@ -0,0 +1,101 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import random +import os +import uuid +from PIL import Image +import bisect +from utils.Tools import * + + +class Slots(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.command(aliases=['slot']) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def slots(self, ctx: commands.Context): + try: + path = os.path.join('data/pictures/') + facade = Image.open(f'{path}slot-face.png').convert('RGBA') + reel = Image.open(f'{path}slot-reel.png').convert('RGBA') + + rw, rh = reel.size + item = 180 + items = rh // item + + s1 = random.randint(1, items - 1) + s2 = random.randint(1, items - 1) + s3 = random.randint(1, items - 1) + + win_rate = 25 / 100 + + if random.random() < win_rate: + symbols_weights = [3.5, 7, 15, 25, 55] + x = round(random.random() * 100, 1) + pos = bisect.bisect(symbols_weights, x) + s1 = pos + (random.randint(1, (items // 6) - 1) * 6) + s2 = pos + (random.randint(1, (items // 6) - 1) * 6) + s3 = pos + (random.randint(1, (items // 6) - 1) * 6) + s1 = s1 - 6 if s1 == items else s1 + s2 = s2 - 6 if s2 == items else s2 + s3 = s3 - 6 if s3 == items else s3 + + images = [] + speed = 6 + for i in range(1, (item // speed) + 1): + bg = Image.new('RGBA', facade.size, color=(255, 255, 255)) + bg.paste(reel, (25 + rw * 0, 100 - (speed * i * s1))) + bg.paste(reel, (25 + rw * 1, 100 - (speed * i * s2))) + bg.paste(reel, (25 + rw * 2, 100 - (speed * i * s3))) + bg.alpha_composite(facade) + images.append(bg) + + unique_filename = str(uuid.uuid4()) + '.gif' + fp = os.path.join('data/pictures/', unique_filename) + + images[0].save( + fp, + save_all=True, + append_images=images[1:], + duration=50 + ) + + file = discord.File(fp, filename=unique_filename) + message = await ctx.reply(file=file) + + if (1 + s1) % 6 == (1 + s2) % 6 == (1 + s3) % 6: + result = 'won' + else: + result = 'lost' + + embed = discord.Embed( + title=f'{ctx.author.display_name}, You {result}!', + color=discord.Color.green() if result == "won" else discord.Color.red() + ) + + embed.set_image(url=f"attachment://{unique_filename}") + await message.edit(content=None, embed=embed) + + os.remove(fp) + except Exception as e: + print(e) + + + \ No newline at end of file diff --git a/bot/cogs/commands/stats.py b/bot/cogs/commands/stats.py new file mode 100644 index 0000000..c88e4ba --- /dev/null +++ b/bot/cogs/commands/stats.py @@ -0,0 +1,254 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CODEBASE, LOADING, SYSTEM, THUNDER, ZYROX_CODE, ZYROX_COMMAND, ZYROX_GLOBAL, ZYROX_OWNER, ZYROX_SEARCH +import psutil +import sys +import os +import time +import aiosqlite +import datetime +from discord.ui import LayoutView, TextDisplay, Separator, Container, ActionRow, Select +from discord.ext import commands +from utils.Tools import * +import wavelink +from utils.config import * + + +def analyze_codebase(path="."): + total_files = total_lines = total_words = 0 + for root, _, files in os.walk(path): + for file in files: + if file.endswith( + (".py", ".js", ".json", ".ts", ".html", ".css", ".env", ".txt") + ): + total_files += 1 + try: + with open( + os.path.join(root, file), "r", encoding="utf-8", errors="ignore" + ) as f: + lines = f.readlines() + total_lines += len(lines) + total_words += sum(len(line.split()) for line in lines) + except: + continue + return total_files, total_lines, total_words + + +def create_stats_content(stats_data, selected): + content_map = { + "Quick Overview": ( + f"**{THUNDER} Quick Overview**\n\n" + f"**Servers**: {stats_data['guilds']}\n" + f"**Users**: {stats_data['users']}\n" + f"**Uptime**: {stats_data['uptime']}\n\n" + f"_Use the dropdown to view more stats._" + ), + "System Info": ( + f"**{SYSTEM} Hardware**\n" + f"Cpu Usage: **{stats_data['cpu']}%**\n" + f"Ram Usage: **{stats_data['ram']}%**\n\n" + f"**{ZYROX_CODE} Software**\n" + f"Python: **{sys.version_info.major}.{sys.version_info.minor}**\n" + f"Discord.py: **{discord.__version__}**" + ), + "General Info": ( + f"**Uptime**: `{stats_data['uptime']}`\n\n" + f"**{ZYROX_GLOBAL} Server Stats**\n" + f"Guilds: **{stats_data['guilds']}**\n" + f"Users: **{stats_data['users']}**\n\n" + f"**{ZYROX_COMMAND} Commands Stats**\n" + f"Total Commands: **{stats_data['all_cmds']}**\n" + f"Slash Commands: **{stats_data['slash_cmds']}**" + ), + "Team Info": ( + "There is only one person who made me. Thanks to him ❤️.\n\n" + f"**{ZYROX_OWNER} Main Owner**\n" + "[01]. [runxking](https://discord.com/users/767979794411028491)\n" + "[02]. [Ray](https://discord.com/users/870179991462236170)" + ), + "Code Info": ( + f"**{ZYROX_SEARCH} Codebase Overview**\n\n" + f"Files: **{stats_data['files']}**\n" + f"Lines: **{stats_data['lines']}**\n" + f"Words: **{stats_data['words']}**" + ), + } + return content_map.get(selected, "") + + +class StatsView(LayoutView): + def __init__(self, ctx, stats_data): + super().__init__(timeout=300) + self.ctx = ctx + self.stats_data = stats_data + + self.select = Select( + placeholder=f"{BRAND_NAME} Statistics", + options=[ + discord.SelectOption( + label="Quick Overview", + emoji=THUNDER, + description="Quick stats overview", + ), + discord.SelectOption( + label="System Info", + emoji=SYSTEM, + description="System usage", + ), + discord.SelectOption( + label="General Info", + emoji=ZYROX_GLOBAL, + description="General info", + ), + discord.SelectOption( + label="Team Info", + emoji=CODEBASE, + description="Bot team", + ), + discord.SelectOption( + label="Code Info", + emoji=ZYROX_SEARCH, + description="Code stats", + ), + ], + ) + self.select.callback = self.on_select + + self.add_item( + Container( + TextDisplay(f"**{BRAND_NAME} Stats Panel**"), + Separator(visible=True), + TextDisplay(create_stats_content(stats_data, "Quick Overview")), + ActionRow(self.select), + ) + ) + + async def on_select(self, interaction: discord.Interaction): + if interaction.user.id != self.ctx.author.id: + await interaction.response.send_message( + "Only the command invoker can use this menu.", ephemeral=True + ) + return + + selected_value = interaction.data.get("values", ["Quick Overview"])[0] + + new_container = Container( + TextDisplay(f"**{BRAND_NAME} Stats Panel**"), + Separator(visible=True), + TextDisplay(create_stats_content(self.stats_data, selected_value)), + ActionRow(self.select), + ) + + self.clear_items() + self.add_item(new_container) + + await interaction.response.edit_message(view=self) + + +class StatsLoadingView(LayoutView): + def __init__(self): + super().__init__(timeout=None) + self.add_item( + Container( + TextDisplay( + f"{LOADING} **Generating {BRAND_NAME} Statistics...**" + ) + ) + ) + + +class Stats(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.start_time = time.time() + self.total_songs_played = 0 + self.command_usage_count = 0 + self.bot.loop.create_task(self.setup_database()) + + async def setup_database(self): + async with aiosqlite.connect("db/stats.db") as db: + await db.execute( + "CREATE TABLE IF NOT EXISTS stats (key TEXT PRIMARY KEY, value INTEGER)" + ) + await db.commit() + + cursor = await db.execute( + "SELECT value FROM stats WHERE key = 'total_songs_played'" + ) + row = await cursor.fetchone() + self.total_songs_played = row[0] if row else 0 + + cursor = await db.execute( + "SELECT value FROM stats WHERE key = 'command_usage_count'" + ) + row = await cursor.fetchone() + self.command_usage_count = row[0] if row else 0 + + def format_number(self, num): + if num < 1000: + return str(num) + elif num < 1_000_000: + return f"{num / 1_000:.4f}k" + elif num < 1_000_000_000: + return f"{num / 1_000_000:.2f}M" + else: + return f"{num / 1_000_000_000:.2f}B" + + @commands.hybrid_command( + name="stats", + aliases=["botstats", "statistics", "botinfo"], + help="Shows the bot's information.", + ) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 7, commands.BucketType.user) + async def stats(self, ctx): + loading_view = StatsLoadingView() + loading_msg = await ctx.reply(view=loading_view) + + uptime = str( + datetime.timedelta(seconds=int(round(time.time() - self.start_time))) + ) + total_users = sum(g.member_count for g in self.bot.guilds if g.member_count) + slash_cmds = len(self.bot.tree.get_commands()) + all_cmds = len(set(self.bot.walk_commands())) + cpu_usage = psutil.cpu_percent(interval=None) + ram_usage = psutil.virtual_memory().percent + + stats_data = { + "guilds": len(self.bot.guilds), + "users": total_users, + "uptime": uptime, + "cpu": cpu_usage, + "ram": ram_usage, + "all_cmds": all_cmds, + "slash_cmds": slash_cmds, + "files": 0, + "lines": 0, + "words": 0, + } + + files, lines, words = analyze_codebase(".") + stats_data["files"] = files + stats_data["lines"] = lines + stats_data["words"] = words + + main_view = StatsView(ctx, stats_data) + await loading_msg.edit(view=main_view) + + +async def setup(bot): + await bot.add_cog(Stats(bot)) diff --git a/bot/cogs/commands/status.py b/bot/cogs/commands/status.py new file mode 100644 index 0000000..e28d293 --- /dev/null +++ b/bot/cogs/commands/status.py @@ -0,0 +1,167 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import DND, ICON_BROWSER, IDLE, LOADING_ALT1, MOBILE, OFFLINE, ONLINE, PC +from discord.ext import commands +from PIL import Image, ImageDraw, ImageFont +import aiohttp +import os +from utils.Tools import * + +class Status(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.command(name="status", help="Shows the status of the user in detail.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + async def status(self, ctx, user: discord.User = None): + user = user or ctx.author + processing = await ctx.send(f"{LOADING_ALT1} Loading Status...") + embed = discord.Embed(title=f"{user.display_name}'s Status", color=0xFF0000) + + status_emoji = { + "online": f"{ONLINE} Online", + "idle": f"{IDLE} Idle", + "dnd": f"{DND} Do Not Disturb", + "offline": f"{OFFLINE} Offline" + } + + member = None + for guild in self.bot.guilds: + member = guild.get_member(user.id) + if member: + break + + if member: + status = status_emoji.get(str(member.status), f"{OFFLINE} Offline") + embed.add_field(name="Status:", value=status, inline=False) + + avatar_url = member.avatar.url if member.avatar else member.default_avatar.url + embed.set_thumbnail(url=avatar_url) + + platform = self.get_platform(member) + embed.add_field(name="Platform:", value=platform, inline=False) + + custom_status = self.get_custom_status(member) + if custom_status: + embed.add_field(name="Custom Status:", value=custom_status, inline=False) + + activity_text = self.get_activity_text(member.activities) + if activity_text: + embed.add_field(name="__Activity__:", value=activity_text, inline=False) + + for activity in member.activities: + if isinstance(activity, discord.Spotify): + song_name = activity.title + album_cover_url = str(activity.album_cover_url) + + album_image_path = 'data/pictures/album_image.png' + + async with aiohttp.ClientSession() as session: + async with session.get(album_cover_url) as resp: + if resp.status == 200: + album_data = await resp.read() + with open(album_image_path, 'wb') as f: + f.write(album_data) + + card_image_path = self.create_spotify_card(song_name, album_image_path) + + if os.path.exists(card_image_path): + file = discord.File(card_image_path, filename="spotify_card.png") + embed.set_image(url="attachment://spotify_card.png") + else: + await ctx.send("Failed to generate the Spotify card image.") + else: + try: + user = await self.bot.fetch_user(user.id) + embed.add_field(name="Status:", value=f"{OFFLINE} Offline", inline=False) + avatar_url = user.default_avatar.url + embed.set_thumbnail(url=avatar_url) + except discord.NotFound: + await ctx.send("User not found.") + return + + requester_avatar_url = ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url + embed.set_footer(text=f"Requested by {ctx.author.display_name}", icon_url=requester_avatar_url) + + await ctx.send(embed=embed, file=file if 'file' in locals() else None) + await processing.delete() + + def create_spotify_card(self, song_name, album_image_path): + card_path = 'data/pictures/spotify.png' + output_path = 'data/pictures/spotify_card_output.png' + + base_img = Image.open(card_path).convert("RGBA") + draw = ImageDraw.Draw(base_img) + + album_img = Image.open(album_image_path).convert("RGBA") + album_img = album_img.resize((160, 160)) + + mask = Image.new("L", album_img.size, 0) + draw_mask = ImageDraw.Draw(mask) + draw_mask.ellipse((0, 0, 160, 160), fill=255) + + base_img.paste(album_img, (30, 30), mask) + + font_path = 'utils/arial.ttf' + font = ImageFont.truetype(font_path, 40) + + truncated_song_name = song_name if len(song_name) <= 60 else song_name[:57] + "..." + song_name_position = (220, 70) + draw.text(song_name_position, truncated_song_name, font=font, fill="white") + + base_img.save(output_path) + return output_path + + def get_platform(self, member): + if member.desktop_status != discord.Status.offline: + return f"{PC} Desktop" + elif member.mobile_status != discord.Status.offline: + return f"{MOBILE} Mobile" + elif member.web_status != discord.Status.offline: + return f"{ICON_BROWSER} Browser" + return "Unknown" + + def get_custom_status(self, member): + for activity in member.activities: + if isinstance(activity, discord.CustomActivity): + status_text = activity.name or "" + if activity.name == "Custom Status": + status_text = "‎" + status_emoji = str(activity.emoji) if activity.emoji else "" + + if status_emoji and not status_text: + return status_emoji + elif status_emoji and status_text: + return f"{status_emoji} {status_text}" + elif status_text: + return status_text + return None + + def get_activity_text(self, activities): + activity_list = [] + for activity in activities: + if isinstance(activity, discord.Game): + activity_list.append(f"Playing {activity.name}") + elif isinstance(activity, discord.Streaming): + activity_list.append(f"Streaming {activity.name} on **[Twitch]({activity.url})**") + elif isinstance(activity, discord.Spotify): + activity_list.append(f"**[Listening to Spotify](https://open.spotify.com/track/{activity.track_id})**") + elif isinstance(activity, discord.Activity): + activity_list.append(f"{activity.type.name.capitalize()} {activity.name}") + return "\n".join(activity_list) if activity_list else None + diff --git a/bot/cogs/commands/steal.py b/bot/cogs/commands/steal.py new file mode 100644 index 0000000..c0633fd --- /dev/null +++ b/bot/cogs/commands/steal.py @@ -0,0 +1,200 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +from discord.ui import View, Button +import requests +from io import BytesIO +import re +from utils.Tools import * + +class Steal(commands.Cog): + def __init__(self, bot): + self.bot = bot + + + + @commands.hybrid_command(name="steal", help="Steal an emoji or sticker", usage="steal ", aliases=["eadd"], with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(manage_emojis=True) + async def steal(self, ctx, emote=None): + if ctx.message.reference: + ref_message = await ctx.channel.fetch_message(ctx.message.reference.message_id) + attachments = ref_message.attachments + stickers = ref_message.stickers + emojis = [emote for emote in ref_message.content.split() if emote.startswith('<:') or emote.startswith('= self.get_max_sticker_count(ctx.guild): + await ctx.send(embed=discord.Embed(title="Steal", description="No more sticker slots available", color=0xFF0000)) + return + + sanitized_name = self.sanitize_name(name) + response = requests.get(url) + img = BytesIO(response.content) + emoji = "⭐" + await ctx.guild.create_sticker(name=sanitized_name, description="Added by bot", file=discord.File(img, filename="sticker.png"), emoji=emoji) + await ctx.send(embed=discord.Embed(title="Steal", description=f"Added sticker \"**{sanitized_name}**\"!", color=0xFF0000)) + except Exception as e: + await ctx.send(embed=discord.Embed(title="Steal", description=f"Failed to add sticker: {str(e)}", color=0xFF0000)) + + def sanitize_name(self, name): + sanitized = re.sub(r'[^a-zA-Z0-9_]', '_', name) + return sanitized[:32] + + def has_emoji_slot(self, guild, animated): + normal_emojis = [emoji for emoji in guild.emojis if not emoji.animated] + animated_emojis = [emoji for emoji in guild.emojis if emoji.animated] + max_normal, max_animated = self.get_max_emoji_count(guild) + + if animated: + return len(animated_emojis) < max_animated + else: + return len(normal_emojis) < max_normal + + def get_max_emoji_count(self, guild): + if guild.premium_tier == 3: + return 250, 250 + elif guild.premium_tier == 2: + return 150, 150 + elif guild.premium_tier == 1: + return 100, 100 + else: + return 50, 50 + + def get_max_sticker_count(self, guild): + if guild.premium_tier == 3: + return 60 + elif guild.premium_tier == 2: + return 30 + elif guild.premium_tier == 1: + return 15 + else: + return 5 + + async def create_buttons(self, ctx, attachments, stickers, emojis): + class StealView(View): + def __init__(self, bot, ctx, attachments, stickers, emojis): + super().__init__() + self.bot = bot + self.ctx = ctx + self.attachments = attachments + self.stickers = stickers + self.emojis = emojis + + @discord.ui.button(label="Steal as Emoji", style=discord.ButtonStyle.primary) + async def steal_as_emoji(self, interaction: discord.Interaction, button: discord.ui.Button): + + if interaction.user.id != self.ctx.author.id: + await interaction.response.send_message("This interaction is not for you.", ephemeral=True) + return + await interaction.response.defer() + for sticker in self.stickers: + + if sticker.format in [discord.StickerFormatType.png, discord.StickerFormatType.apng, discord.StickerFormatType.lottie]: + animated = sticker.format == discord.StickerFormatType.apng + await self.bot.cogs['Steal'].add_emoji(self.ctx, sticker.url, sticker.name.replace(' ', '_'), animated=animated) + else: + await self.ctx.send(embed=discord.Embed(title="Steal", description=f"Unsupported sticker format for {sticker.name}", color=0xFF0000)) + for attachment in self.attachments: + await self.bot.cogs['Steal'].add_emoji(self.ctx, attachment.url, attachment.filename.split('.')[0].replace(' ', '_'), animated=False) + for emote in self.emojis: + name = emote.split(':')[1] + emoji_id = emote.split(':')[2][:-1] + anim = emote.split(':')[0] + if anim == ' 0 else None + + if sticky_data['message_type'] == 'plain': + content = sticky_data['message_content'] + else: + try: + embed_data = json.loads(sticky_data['embed_data']) + embed = discord.Embed( + title=embed_data.get('title'), + description=embed_data.get('description'), + color=RED_THEME_COLOR + ) + if footer := embed_data.get('footer'): + embed.set_footer(text=footer) + except (json.JSONDecodeError, ValueError): + embed = discord.Embed(description="Error: Could not decode embed data.", color=RED_THEME_COLOR) + + new_sticky = await message.channel.send(content=content, embed=embed, delete_after=delete_after) + + async with aiosqlite.connect("db/stickymessages.db") as db: + await db.execute( + "UPDATE sticky_messages SET last_message_id = ? WHERE id = ?", + (new_sticky.id, sticky_data['id']) + ) + await db.commit() + + @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: + await ctx.send_help(ctx.command) + + @stickymessage.command(name='setup') + @commands.has_permissions(manage_messages=True) + async def sticky_setup(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None): + channel = channel or ctx.channel + async with aiosqlite.connect("db/stickymessages.db") as db: + cursor = await db.execute("SELECT 1 FROM sticky_messages WHERE channel_id = ?", (channel.id,)) + if await cursor.fetchone(): + embed = discord.Embed(title="Setup Error", description=f"A sticky message already exists in {channel.mention}.", color=RED_THEME_COLOR) + return await ctx.send(embed=embed) + + embed = discord.Embed(title="Sticky Message Setup", description=f"Choose the message type for {channel.mention}:", color=RED_THEME_COLOR) + await ctx.send(embed=embed, view=StickySetupView(ctx, channel)) + + @stickymessage.command(name='remove', aliases=['delete', 'del']) + @commands.has_permissions(manage_messages=True) + async def sticky_remove(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None): + channel = channel or ctx.channel + async with aiosqlite.connect("db/stickymessages.db") as db: + cursor = await db.execute("SELECT last_message_id FROM sticky_messages WHERE channel_id = ?", (channel.id,)) + data = await cursor.fetchone() + if not data: + return await ctx.send(embed=discord.Embed(title="Not Found", description=f"No sticky message found in {channel.mention}.", color=RED_THEME_COLOR)) + + if data[0]: + try: + msg = await channel.fetch_message(data[0]) + await msg.delete() + except discord.NotFound: + pass + + await db.execute("DELETE FROM sticky_messages WHERE channel_id = ?", (channel.id,)) + await db.commit() + + embed = discord.Embed(title="Success", description=f"Sticky message in {channel.mention} has been removed.", color=RED_THEME_COLOR) + await ctx.send(embed=embed) + + @stickymessage.command(name='list') + @commands.has_permissions(manage_messages=True) + async def sticky_list(self, ctx: commands.Context): + async with aiosqlite.connect("db/stickymessages.db") as db: + db.row_factory = aiosqlite.Row + cursor = await db.execute("SELECT * FROM sticky_messages WHERE guild_id = ?", (ctx.guild.id,)) + stickies = await cursor.fetchall() + + if not stickies: + return await ctx.send(embed=discord.Embed(title="No Stickies Found", description="There are no active sticky messages on this server.", color=RED_THEME_COLOR)) + + embed = discord.Embed(title=f"Sticky Messages in {ctx.guild.name}", color=RED_THEME_COLOR) + for sticky in stickies: + channel = self.bot.get_channel(sticky['channel_id']) + status = "✅ Enabled" if sticky['enabled'] else "❌ Disabled" + embed.add_field( + name=f"#{channel.name if channel else 'Unknown Channel'}", + value=f"**Type**: {sticky['message_type'].title()}\n**Status**: {status}\n**Delay**: {sticky['delay_seconds']}s", + inline=True + ) + await ctx.send(embed=embed) + + @stickymessage.command(name='edit') + @commands.has_permissions(manage_messages=True) + async def sticky_edit(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None): + channel = channel or ctx.channel + async with aiosqlite.connect("db/stickymessages.db") as db: + db.row_factory = aiosqlite.Row + cursor = await db.execute("SELECT * FROM sticky_messages WHERE channel_id = ?", (channel.id,)) + sticky_data = await cursor.fetchone() + + if not sticky_data: + return await ctx.send(embed=discord.Embed(title="Not Found", description=f"No sticky message found in {channel.mention}.", color=RED_THEME_COLOR)) + + embed = discord.Embed(title="Edit Sticky Message", description=f"Editing sticky for {channel.mention}. Choose an option:", color=RED_THEME_COLOR) + await ctx.send(embed=embed, view=StickyEditView(ctx, channel, sticky_data)) + +class AuthorOnlyView(discord.ui.View): + def __init__(self, ctx: commands.Context, timeout: float = 300.0): + super().__init__(timeout=timeout) + self.ctx = ctx + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user.id != self.ctx.author.id: + await interaction.response.send_message("You are not authorized to use this.", ephemeral=True) + return False + return True + + async def on_timeout(self): + try: + for item in self.children: + item.disabled = True + if hasattr(self, 'message'): + await self.message.edit(view=self) + except discord.NotFound: + pass + +class StickySetupView(AuthorOnlyView): + def __init__(self, ctx: commands.Context, channel: discord.TextChannel): + super().__init__(ctx) + self.channel = channel + + @discord.ui.button(label='Plain Text', style=discord.ButtonStyle.primary, emoji='📝') + async def plain_text(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_modal(PlainTextModal(self.ctx, self.channel)) + + @discord.ui.button(label='Embed', style=discord.ButtonStyle.secondary, emoji='📋') + async def embed_message(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_modal(EmbedModal(self.ctx, self.channel)) + + @discord.ui.button(label='Cancel', style=discord.ButtonStyle.danger) + async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button): + embed = discord.Embed(title="Cancelled", description="Sticky message setup has been cancelled.", color=RED_THEME_COLOR) + await interaction.response.edit_message(embed=embed, view=None) + +class PlainTextModal(discord.ui.Modal, title='Plain Text Sticky Message'): + def __init__(self, ctx: commands.Context, channel: discord.TextChannel): + super().__init__() + self.ctx = ctx + self.channel = channel + + message_content = discord.ui.TextInput(label='Message Content', style=discord.TextStyle.long, required=True, max_length=2000) + delay_seconds = discord.ui.TextInput(label='Delay (seconds)', default='2', required=False, max_length=3) + + async def on_submit(self, interaction: discord.Interaction): + try: + delay = int(self.delay_seconds.value or "2") + except ValueError: + delay = 2 + + async with aiosqlite.connect("db/stickymessages.db") as db: + await db.execute( + "INSERT INTO sticky_messages (guild_id, channel_id, message_type, message_content, delay_seconds) VALUES (?, ?, ?, ?, ?)", + (self.ctx.guild.id, self.channel.id, 'plain', self.message_content.value, delay) + ) + await db.commit() + + embed = discord.Embed(title="Sticky Created", description=f"Successfully created a plain text sticky in {self.channel.mention}.", color=RED_THEME_COLOR) + await interaction.response.edit_message(embed=embed, view=None) + +class EmbedModal(discord.ui.Modal, title='Embed Sticky Message'): + def __init__(self, ctx: commands.Context, channel: discord.TextChannel): + super().__init__() + self.ctx = ctx + self.channel = channel + + title = discord.ui.TextInput(label='Embed Title', required=False, max_length=256) + description = discord.ui.TextInput(label='Embed Description', style=discord.TextStyle.long, required=True, max_length=4000) + footer = discord.ui.TextInput(label='Embed Footer', required=False, max_length=2048) + delay_seconds = discord.ui.TextInput(label='Delay (seconds)', default='2', required=False, max_length=3) + + async def on_submit(self, interaction: discord.Interaction): + try: + delay = int(self.delay_seconds.value or "2") + except ValueError: + delay = 2 + + embed_data = { + "title": self.title.value, + "description": self.description.value, + "color": f"#{RED_THEME_COLOR:06x}", + "footer": self.footer.value + } + + async with aiosqlite.connect("db/stickymessages.db") as db: + await db.execute( + "INSERT INTO sticky_messages (guild_id, channel_id, message_type, embed_data, delay_seconds) VALUES (?, ?, ?, ?, ?)", + (self.ctx.guild.id, self.channel.id, 'embed', json.dumps(embed_data), delay) + ) + await db.commit() + + embed = discord.Embed(title="Sticky Embed Created", description=f"Successfully created an embed sticky in {self.channel.mention}.", color=RED_THEME_COLOR) + await interaction.response.edit_message(embed=embed, view=None) + +class StickyEditView(AuthorOnlyView): + def __init__(self, ctx: commands.Context, channel: discord.TextChannel, sticky_data): + super().__init__(ctx) + self.channel = channel + self.sticky_data = sticky_data + + @discord.ui.button(label='Edit Content', style=discord.ButtonStyle.primary) + async def edit_content(self, interaction: discord.Interaction, button: discord.ui.Button): + if self.sticky_data['message_type'] == 'plain': + await interaction.response.send_modal(EditPlainTextModal(self.ctx, self.channel, self.sticky_data)) + else: + await interaction.response.send_modal(EditEmbedModal(self.ctx, self.channel, self.sticky_data)) + + @discord.ui.button(label='Edit Settings', style=discord.ButtonStyle.secondary) + async def edit_settings(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_modal(EditSettingsModal(self.ctx, self.channel, self.sticky_data)) + + @discord.ui.button(label='Cancel', style=discord.ButtonStyle.danger) + async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button): + embed = discord.Embed(title="Cancelled", description="Editing has been cancelled.", color=RED_THEME_COLOR) + await interaction.response.edit_message(embed=embed, view=None) + +class EditPlainTextModal(discord.ui.Modal, title='Edit Plain Text Content'): + def __init__(self, ctx: commands.Context, channel: discord.TextChannel, sticky_data): + super().__init__() + self.ctx = ctx + self.channel = channel + self.message_content = discord.ui.TextInput( + label='Message Content', + style=discord.TextStyle.long, + default=sticky_data['message_content'], + max_length=2000 + ) + self.add_item(self.message_content) + + async def on_submit(self, interaction: discord.Interaction): + async with aiosqlite.connect("db/stickymessages.db") as db: + await db.execute( + "UPDATE sticky_messages SET message_content = ? WHERE channel_id = ?", + (self.message_content.value, self.channel.id) + ) + await db.commit() + embed = discord.Embed(title="Content Updated", description="The sticky message content has been updated.", color=RED_THEME_COLOR) + await interaction.response.edit_message(embed=embed, view=None) + +class EditEmbedModal(discord.ui.Modal, title='Edit Embed Content'): + def __init__(self, ctx: commands.Context, channel: discord.TextChannel, sticky_data): + super().__init__() + self.ctx = ctx + self.channel = channel + embed_data = json.loads(sticky_data['embed_data']) + + self.title = discord.ui.TextInput(label='Embed Title', default=embed_data.get('title', ''), required=False, max_length=256) + self.description = discord.ui.TextInput(label='Embed Description', style=discord.TextStyle.long, default=embed_data.get('description', ''), required=True, max_length=4000) + self.footer = discord.ui.TextInput(label='Embed Footer', default=embed_data.get('footer', ''), required=False, max_length=2048) + + self.add_item(self.title) + self.add_item(self.description) + self.add_item(self.footer) + + async def on_submit(self, interaction: discord.Interaction): + embed_data = { + "title": self.title.value, "description": self.description.value, + "color": f"#{RED_THEME_COLOR:06x}", "footer": self.footer.value + } + async with aiosqlite.connect("db/stickymessages.db") as db: + await db.execute( + "UPDATE sticky_messages SET embed_data = ? WHERE channel_id = ?", + (json.dumps(embed_data), self.channel.id) + ) + await db.commit() + embed = discord.Embed(title="Embed Updated", description="The sticky embed content has been updated.", color=RED_THEME_COLOR) + await interaction.response.edit_message(embed=embed, view=None) + +class EditSettingsModal(discord.ui.Modal, title='Edit Sticky Settings'): + def __init__(self, ctx: commands.Context, channel: discord.TextChannel, sticky_data): + super().__init__() + self.ctx = ctx + self.channel = channel + + self.delay_seconds = discord.ui.TextInput(label='Delay (seconds)', default=str(sticky_data['delay_seconds']), max_length=3) + self.auto_delete_after = discord.ui.TextInput(label='Auto-delete (seconds, 0=off)', default=str(sticky_data['auto_delete_after']), max_length=4) + self.trigger_count = discord.ui.TextInput(label='Trigger After X Msgs', default=str(sticky_data['trigger_count']), max_length=2) + + self.add_item(self.delay_seconds) + self.add_item(self.auto_delete_after) + self.add_item(self.trigger_count) + + async def on_submit(self, interaction: discord.Interaction): + try: + delay = int(self.delay_seconds.value) + auto_del = int(self.auto_delete_after.value) + trigger = int(self.trigger_count.value) + except ValueError: + return await interaction.response.send_message("Please enter valid numbers.", ephemeral=True) + + async with aiosqlite.connect("db/stickymessages.db") as db: + await db.execute( + "UPDATE sticky_messages SET delay_seconds = ?, auto_delete_after = ?, trigger_count = ? WHERE channel_id = ?", + (delay, auto_del, trigger, self.channel.id) + ) + await db.commit() + embed = discord.Embed(title="Settings Updated", description="The sticky message settings have been updated.", color=RED_THEME_COLOR) + await interaction.response.edit_message(embed=embed, view=None) + +async def setup(bot: commands.Bot): + await bot.add_cog(StickyMessage(bot)) \ No newline at end of file diff --git a/bot/cogs/commands/ticket.py b/bot/cogs/commands/ticket.py new file mode 100644 index 0000000..7cd9f7a --- /dev/null +++ b/bot/cogs/commands/ticket.py @@ -0,0 +1,521 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +# cogs/commands/ticket.py + +import discord +from utils.emoji import CROSS, DELETE_ALT1, HANDSHAKE, LOCK, TICK, UNLOCK, ZBAN, ZMODULE, ZWRENCH +from discord import app_commands +from discord.ext import commands +import sqlite3 +from datetime import datetime +import asyncio +import io +import os +import re +from utils.config import * + +# --- Configurable Variables --- +EMBED_COLOR = 0xFF0000 +TICKET_CHANNEL_IMAGE_URL = "https://cdn.discordapp.com/attachments/1403014653214330951/1403022431303630900/images_2.jpg?ex=68a1e776&is=68a095f6&hm=2c4e74b079fa409410920507bfb55549d485aafa89e194d15ab548eaba684555&" + +# --- Emoji Variables --- +SUCCESS_EMOJI = TICK +ERROR_EMOJI = CROSS +LOCK_EMOJI = f"{UNLOCK} " +UNLOCK_EMOJI = LOCK +CLAIM_EMOJI = HANDSHAKE +CLOSE_EMOJI = ZBAN +DELETE_EMOJI = DELETE_ALT1 +REOPEN_EMOJI = ZWRENCH +TRANSCRIPT_EMOJI = ZMODULE + +# --- Constants --- +if not os.path.exists('db'): + os.makedirs('db') +DB_PATH = 'db/ticket.db' +MAX_CATEGORIES = 15 +TICKET_LIMIT_PER_USER = 3 + +# --- Database Class --- +class TicketDatabase: + def __init__(self, path): + self.conn = sqlite3.connect(path, check_same_thread=False) + self.conn.row_factory = sqlite3.Row + self._create_tables() + + 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)") + self.conn.execute("CREATE TABLE IF NOT EXISTS ticket_categories (category_id INTEGER PRIMARY KEY AUTOINCREMENT, guild_id INTEGER, name TEXT NOT NULL, emoji TEXT, notified_roles TEXT, button_style INTEGER, discord_category_id INTEGER, FOREIGN KEY (guild_id) REFERENCES guild_configs(guild_id) ON DELETE CASCADE)") + self.conn.execute("CREATE TABLE IF NOT EXISTS open_tickets (channel_id INTEGER PRIMARY KEY, ticket_number INTEGER, guild_id INTEGER, creator_id INTEGER NOT NULL, category_db_id INTEGER, created_at TEXT NOT NULL, closed_by_id INTEGER, closed_at TEXT, is_locked BOOLEAN DEFAULT FALSE, is_claimed BOOLEAN DEFAULT FALSE, claimed_by_id INTEGER, FOREIGN KEY (guild_id) REFERENCES guild_configs(guild_id) ON DELETE CASCADE, FOREIGN KEY (category_db_id) REFERENCES ticket_categories(category_id) ON DELETE SET NULL)") + 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) + def fetchone(self, q, p=()): + 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() + def close(self): + if self.conn: self.conn.close() + +# --- Utility Functions --- +async def get_or_create_log_channel(db, guild): + config = db.fetchone("SELECT logging_channel_id FROM guild_configs WHERE guild_id = ?", (guild.id,)) + if config and config["logging_channel_id"] and (ch := guild.get_channel(config["logging_channel_id"])): return ch + overwrites = {guild.default_role: discord.PermissionOverwrite(view_channel=False)} + try: + ch = await guild.create_text_channel(f"{BRAND_NAME}-ticket-logs", overwrites=overwrites) + db.execute("INSERT INTO guild_configs (guild_id, logging_channel_id) VALUES (?,?) ON CONFLICT(guild_id) DO UPDATE SET logging_channel_id=excluded.logging_channel_id", (guild.id, ch.id)) + return ch + except: return None + +async def log_ticket_action(db, guild, user, action, details): + if log_channel := await get_or_create_log_channel(db, guild): + embed = discord.Embed(title=f"Ticket Action: {action}", color=EMBED_COLOR, timestamp=datetime.now()) + embed.add_field(name="Action By", value=user.mention).add_field(name="Details", value=details, inline=False) + try: await log_channel.send(embed=embed) + except: pass + +async def get_or_create_closed_category(db, guild): + config = db.fetchone("SELECT closed_category_id FROM guild_configs WHERE guild_id = ?", (guild.id,)) + if config and config["closed_category_id"] and (cat := guild.get_channel(config["closed_category_id"])): return cat + overwrites = {guild.default_role: discord.PermissionOverwrite(view_channel=False)} + try: + cat = await guild.create_category("Closed Tickets", overwrites=overwrites) + db.execute("UPDATE guild_configs SET closed_category_id = ? WHERE guild_id = ?", (cat.id, guild.id)) + return cat + except: return None + +# --- Setup Views --- +class EmbedEditorView(discord.ui.View): + def __init__(self, cog, ctx, panel_channel, panel_type): + super().__init__(timeout=600) + self.cog, self.ctx, self.panel_channel, self.panel_type = cog, ctx, panel_channel, panel_type + self.message = None + self.embed_data = {"title": "Support Tickets", "description": "Click a button or select an option below to create a ticket.", "color": EMBED_COLOR} + + def _create_preview_embed(self): + embed = discord.Embed.from_dict(self.embed_data) + if img_url := self.embed_data.get("image", {}).get("url"): embed.set_image(url=img_url) + if thumb_url := self.embed_data.get("thumbnail", {}).get("url"): embed.set_thumbnail(url=thumb_url) + return embed + + async def start(self, interaction): + await interaction.response.send_message("Use the buttons to customize the panel embed.", embed=self._create_preview_embed(), view=self, ephemeral=True) + self.message = await interaction.original_response() + + async def _prompt(self, inter, prompt): + await inter.response.send_message(prompt, ephemeral=True) + try: + msg = await self.cog.bot.wait_for("message", check=lambda m: m.author.id == self.ctx.author.id and m.channel.id == self.ctx.channel.id, timeout=120) + try: await msg.delete() + except: pass + return msg.content + except: return None + + @discord.ui.button(label="Title", style=discord.ButtonStyle.green, row=0) + async def edit_title(self, inter, button): + if title := await self._prompt(inter, "Enter new title:"): + self.embed_data["title"] = title + await self.message.edit(embed=self._create_preview_embed()) + + @discord.ui.button(label="Description", style=discord.ButtonStyle.green, row=0) + async def edit_desc(self, inter, button): + if desc := await self._prompt(inter, "Enter new description:"): + self.embed_data["description"] = desc + await self.message.edit(embed=self._create_preview_embed()) + + @discord.ui.button(label="Image URL", style=discord.ButtonStyle.blurple, row=1) + async def edit_image(self, inter, button): + if url := await self._prompt(inter, "Enter image URL (`none` to remove):"): + self.embed_data["image"] = {"url": url} if url.lower() != 'none' else {} + await self.message.edit(embed=self._create_preview_embed()) + + @discord.ui.button(label="Thumbnail URL", style=discord.ButtonStyle.blurple, row=1) + async def edit_thumb(self, inter, button): + if url := await self._prompt(inter, "Enter thumbnail URL (`none` to remove):"): + self.embed_data["thumbnail"] = {"url": url} if url.lower() != 'none' else {} + await self.message.edit(embed=self._create_preview_embed()) + + @discord.ui.button(label="Submit & Continue", style=discord.ButtonStyle.primary, row=2) + async def submit(self, inter, button): + await inter.response.defer() + for item in self.children: item.disabled = True + try: await self.message.edit(view=self) + except: pass + self.cog.db.execute("INSERT INTO guild_configs (guild_id, panel_channel_id, panel_type, embed_title, embed_description, embed_color, embed_image_url, embed_thumbnail_url) VALUES (?,?,?,?,?,?,?,?) ON CONFLICT(guild_id) DO UPDATE SET panel_channel_id=excluded.panel_channel_id, panel_type=excluded.panel_type, embed_title=excluded.embed_title, embed_description=excluded.embed_description, embed_color=excluded.embed_color, embed_image_url=excluded.embed_image_url, embed_thumbnail_url=excluded.embed_thumbnail_url", (self.ctx.guild.id, self.panel_channel.id, self.panel_type, self.embed_data["title"], self.embed_data["description"], self.embed_data["color"], self.embed_data.get("image",{}).get("url"), self.embed_data.get("thumbnail",{}).get("url"))) + await CategoryConfigView(self.cog, self.ctx).start(inter) + self.stop() + +class CategoryConfigView(discord.ui.View): + def __init__(self, cog, ctx): + super().__init__(timeout=900) + self.cog, self.ctx, self.message, self.categories = cog, ctx, None, [] + self._setup_buttons() + + def _setup_buttons(self): + panel_type = self.cog.db.fetchone("SELECT panel_type FROM guild_configs WHERE guild_id=?", (self.ctx.guild.id,))['panel_type'] + self.add_item(discord.ui.Button(label=f"Add Category", style=discord.ButtonStyle.success, custom_id="add_cat")) + self.remove_select = discord.ui.Select(placeholder="Select a category to remove...", custom_id="remove_cat") + self.add_item(self.remove_select) + self.add_item(discord.ui.Button(label="Finish Setup", style=discord.ButtonStyle.primary, custom_id="finish_setup", row=2)) + + async def start(self, interaction): + self._update_remove_select() + await interaction.followup.send(embed=self._update_embed(), view=self, ephemeral=True) + self.message = await interaction.original_response() + + def _update_embed(self): + embed = discord.Embed(title="Category Configuration", description="Add or remove ticket categories for your panel.", color=EMBED_COLOR) + embed.add_field(name="Current Categories", value="\n".join([f"{c['emoji'] or ''} {c['name']}" for c in self.categories]) or "None yet. Click 'Add Category' to begin.") + return embed + + def _update_remove_select(self): + self.remove_select.options = [discord.SelectOption(label=c['name'], value=str(i), emoji=c.get('emoji')) for i, c in enumerate(self.categories)] or [discord.SelectOption(label="No categories to remove", value="placeholder")] + + async def _prompt(self, inter: discord.Interaction, prompt_text: str, followup: bool = False): + send_method = inter.followup.send if followup else inter.response.send_message + await send_method(prompt_text, ephemeral=True) + try: + msg = await self.cog.bot.wait_for("message", check=lambda m: m.author.id == self.ctx.author.id and m.channel.id == inter.channel.id, timeout=120.0) + try: await msg.delete() + except discord.HTTPException: pass + return msg.content + except asyncio.TimeoutError: + return None + + async def interaction_check(self, interaction): + if interaction.user.id != self.ctx.author.id: return False + custom_id = interaction.data["custom_id"] + if custom_id == "add_cat": await self._add_category_flow(interaction) + elif custom_id == "remove_cat": await self._remove_category(interaction, interaction.data["values"][0]) + elif custom_id == "finish_setup": await self._finish_setup(interaction) + return True + + async def _add_category_flow(self, inter: discord.Interaction): + await inter.response.defer() + if len(self.categories) >= MAX_CATEGORIES: + return await inter.followup.send(f"Max {MAX_CATEGORIES} categories reached.", ephemeral=True) + + cat_name = await self._prompt(inter, "Please type the name for the new category (e.g., General Support).", followup=True) + if not cat_name: return await inter.followup.send("Timed out.", ephemeral=True) + + emoji = await self._prompt(inter, 'Please provide an emoji for the category, or type `skip`.', followup=True) + if not emoji: return await inter.followup.send("Timed out.", ephemeral=True) + if emoji.lower() == 'skip': emoji = None + + role_input = await self._prompt(inter, 'Please mention one or more staff roles to ping, separated by spaces (e.g., `@Ticket Support @Moderator`), or type `none`.', followup=True) + if not role_input: return await inter.followup.send("Timed out.", ephemeral=True) + + role_ids = [] + if role_input.lower() != 'none': + role_mentions = re.findall(r'<@&(\d+)>', role_input) + for role_id_str in role_mentions: + role_ids.append(int(role_id_str)) + + self.categories.append({ + "name": cat_name, + "emoji": emoji, + "notified_roles": ",".join(map(str, role_ids)) if role_ids else None, + "button_style": discord.ButtonStyle.secondary.value + }) + self._update_remove_select() + await self.message.edit(embed=self._update_embed(), view=self) + await inter.followup.send(f"Category '{cat_name}' added/removed successfully.", ephemeral=True) + + async def _remove_category(self, inter, value): + if value == "placeholder": return await inter.response.defer() + try: + idx = int(value) + if 0 <= idx < len(self.categories): + self.categories.pop(idx) + except ValueError: + pass + self._update_remove_select() + await self.message.edit(embed=self._update_embed(), view=self) + await inter.response.defer() + + async def _finish_setup(self, inter): + if not self.categories: return await inter.response.send_message("Add at least one category.", ephemeral=True) + await inter.response.defer() + db, guild_id = self.cog.db, self.ctx.guild.id + db.execute("DELETE FROM ticket_categories WHERE guild_id = ?", (guild_id,)) + for cat in self.categories: + try: cat_ch = await self.ctx.guild.create_category(f"{cat['name']} Tickets", overwrites={self.ctx.guild.default_role: discord.PermissionOverwrite(view_channel=False)}) + except: return await inter.followup.send(f"Can't create category for `{cat['name']}`.", ephemeral=True) + db.execute('INSERT INTO ticket_categories (guild_id, name, emoji, notified_roles, button_style, discord_category_id) VALUES (?,?,?,?,?,?)', (guild_id,cat['name'],cat['emoji'],cat['notified_roles'],cat['button_style'],cat_ch.id)) + config = db.fetchone("SELECT * FROM guild_configs WHERE guild_id=?", (guild_id,)) + panel_ch = self.ctx.guild.get_channel(config['panel_channel_id']) + panel_embed = discord.Embed(title=config['embed_title'], description=config['embed_description'], color=config['embed_color']) + if img_url := config['embed_image_url']: panel_embed.set_image(url=img_url) + if thumb_url := config['embed_thumbnail_url']: panel_embed.set_thumbnail(url=thumb_url) + final_view = self.cog.create_panel_view(guild_id) + msg = await panel_ch.send(embed=panel_embed, view=final_view) + db.execute("UPDATE guild_configs SET panel_message_id = ? WHERE guild_id = ?", (msg.id, guild_id)) + await self.message.edit(content=f"{SUCCESS_EMOJI} Setup complete! Panel sent to {panel_ch.mention}.", view=None, embed=None) + self.stop() + +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()) + + 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']) + + def create_panel_view(self, guild_id): + config = self.db.fetchone("SELECT panel_type FROM guild_configs WHERE guild_id=?", (guild_id,)) + categories = self.db.fetchall("SELECT * FROM ticket_categories WHERE guild_id=?", (guild_id,)) + if not config or not categories: return None + view_class = TicketPanelSelect if config['panel_type'] == 'dropdown' else TicketPanelButtons + view = view_class(self) + if config['panel_type'] == 'dropdown': + view.children[0].options = [discord.SelectOption(label=c['name'], value=str(c['category_id']), emoji=c['emoji']) for c in categories] + else: + 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() + + @commands.Cog.listener() + async def on_interaction(self, inter): + if inter.type == discord.InteractionType.component and (cid := inter.data.get("custom_id","")).startswith("create_ticket_"): await self.create_ticket_flow(inter, int(cid.split("_")[-1])) + + async def create_ticket_flow(self, inter, cat_id): + await inter.response.defer(ephemeral=True) + guild, user = inter.guild, inter.user + if (count := self.db.fetchone("SELECT ticket_count FROM user_ticket_counts WHERE guild_id=? AND user_id=?",(guild.id,user.id))) and count['ticket_count'] >= TICKET_LIMIT_PER_USER: return await inter.followup.send(f"You have reached the max of {TICKET_LIMIT_PER_USER} open tickets.",ephemeral=True) + + cat_info = self.db.fetchone("SELECT * FROM ticket_categories WHERE category_id=?", (cat_id,)) + disc_cat = guild.get_channel(cat_info['discord_category_id']) + if not cat_info or not disc_cat: return await inter.followup.send("This ticket category has been deleted or is misconfigured.", ephemeral=True) + + t_num = (self.db.fetchone("SELECT MAX(ticket_number) as n FROM open_tickets WHERE guild_id=?", (guild.id,))['n'] or 0) + 1 + + overwrites = {guild.default_role:discord.PermissionOverwrite(view_channel=False), user:discord.PermissionOverwrite(view_channel=True), guild.me:discord.PermissionOverwrite(view_channel=True, manage_channels=True)} + + pings = [user.mention] + if cat_info['notified_roles']: + for role_id in cat_info['notified_roles'].split(','): + if role := guild.get_role(int(role_id)): + overwrites[role] = discord.PermissionOverwrite(view_channel=True) + pings.append(role.mention) + + try: ch = await disc_cat.create_text_channel(name=f"ticket-{t_num:04d}-{user.name.lower()}", overwrites=overwrites) + except: return await inter.followup.send("I lack permissions to create a channel.", ephemeral=True) + + self.db.execute('INSERT INTO open_tickets VALUES (?,?,?,?,?,?,?,?,?,?,?)', (ch.id,t_num,guild.id,user.id,cat_id,datetime.now().isoformat(),None,None,False,False,None)) + self.db.execute('INSERT INTO user_ticket_counts VALUES (?,?,1) ON CONFLICT(guild_id,user_id) DO UPDATE SET ticket_count=ticket_count+1', (guild.id,user.id)) + await log_ticket_action(self.db, guild, user, "Ticket Created", f"Ticket {ch.mention} by {user.mention} (Category: {cat_info['name']}).") + + ticket_embed = discord.Embed(title=f"Welcome to your Ticket ( #{t_num:04d} )", description="Thank you for reaching out for support. Our staff team has been notified and will be with you as soon as possible.\n\nPlease describe your issue in detail while you wait.", color=EMBED_COLOR) + ticket_embed.set_image(url=TICKET_CHANNEL_IMAGE_URL) + 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.guild_only() + async def ticket(self, ctx): + if ctx.invoked_subcommand is None: await ctx.send_help(ctx.command) + + @ticket.command(name="setup", description="Start the interactive setup for the ticket panel.") + @commands.has_permissions(manage_guild=True) + @app_commands.describe(style="The style of the ticket creation panel.", channel="The channel where the ticket panel will be sent.") + @app_commands.choices(style=[app_commands.Choice(name="Dropdown Menu", value="dropdown"), app_commands.Choice(name="Buttons", value="button")]) + async def setup(self, ctx, style: app_commands.Choice[str], channel: discord.TextChannel): + await EmbedEditorView(self, ctx, channel, style.value).start(ctx.interaction) + + @ticket.command(name="close", description="Close the current ticket channel.") + @commands.has_permissions(manage_channels=True) + async def close(self, ctx): await self._dispatch_action(ctx, "close") + + @ticket.command(name="lock", description="Lock the ticket, preventing the user from sending messages.") + @commands.has_permissions(manage_channels=True) + async def lock(self, ctx): await self._dispatch_action(ctx, "lock") + + @ticket.command(name="unlock", description="Unlock the ticket, allowing the user to send messages again.") + @commands.has_permissions(manage_channels=True) + async def unlock(self, ctx): await self._dispatch_action(ctx, "unlock") + + @ticket.command(name="claim", description="Claim the ticket to notify others that you are handling it.") + @commands.has_permissions(manage_channels=True) + async def claim(self, ctx): await self._dispatch_action(ctx, "claim") + + @ticket.command(name="transcript", description="Generate a transcript of a closed ticket.") + @commands.has_permissions(manage_channels=True) + async def transcript(self, ctx): + if not ctx.interaction: return await ctx.send("Please use the slash command version of this command.") + await ClosedTicketActionsView(self, ctx.channel.id)._generate_transcript(ctx.interaction, False) + +class TicketPanelSelect(discord.ui.View): + def __init__(self, cog): super().__init__(timeout=None); self.cog = cog + @discord.ui.select(placeholder="Select a category to open a ticket...", custom_id="ticket_panel_select") + async def select_ticket(self, inter, select): await self.cog.create_ticket_flow(inter, int(select.values[0])) + +class TicketPanelButtons(discord.ui.View): + def __init__(self, cog): super().__init__(timeout=None); self.cog = cog + +class TicketActionsView(discord.ui.View): + def __init__(self, cog, ch_id, cat_id): + super().__init__(timeout=None) + self.cog, self.ch_id, self.cat_id = cog, ch_id, cat_id + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + cat_info = self.cog.db.fetchone("SELECT notified_roles FROM ticket_categories WHERE category_id=?", (self.cat_id,)) + if not cat_info or not cat_info['notified_roles']: + await interaction.response.send_message("This ticket is misconfigured; no staff roles are assigned.", ephemeral=True) + return False + + allowed_role_ids = {int(r_id) for r_id in cat_info['notified_roles'].split(',')} + user_role_ids = {role.id for role in interaction.user.roles} + + if not user_role_ids.intersection(allowed_role_ids): + await interaction.response.send_message("You do not have the required role to perform this action.", ephemeral=True) + return False + return True + + @discord.ui.button(label="Lock", emoji=LOCK_EMOJI, custom_id="t_lock", style=discord.ButtonStyle.secondary) + async def b_lock(self, i, b): + t = self.cog.db.fetchone("SELECT * FROM open_tickets WHERE channel_id=?", (self.ch_id,)) + if t['is_locked']: return await i.response.send_message("This ticket is already locked.", ephemeral=True) + creator = i.guild.get_member(t['creator_id']) + if creator: await i.channel.set_permissions(creator, send_messages=False) + self.cog.db.execute("UPDATE open_tickets SET is_locked=1 WHERE channel_id=?", (self.ch_id,)) + await i.response.send_message(f"{LOCK_EMOJI} Ticket locked by {i.user.mention}.") + await log_ticket_action(self.cog.db, i.guild, i.user, "Locked", f"{i.channel.mention}") + + @discord.ui.button(label="Unlock", emoji=UNLOCK_EMOJI, custom_id="t_unlock", style=discord.ButtonStyle.secondary) + async def b_unlock(self, i, b): + t = self.cog.db.fetchone("SELECT * FROM open_tickets WHERE channel_id=?", (self.ch_id,)) + if not t['is_locked']: return await i.response.send_message("This ticket is already unlocked.", ephemeral=True) + creator = i.guild.get_member(t['creator_id']) + if creator: await i.channel.set_permissions(creator, send_messages=True) + self.cog.db.execute("UPDATE open_tickets SET is_locked=0 WHERE channel_id=?", (self.ch_id,)) + await i.response.send_message(f"{UNLOCK_EMOJI} Ticket unlocked by {i.user.mention}.") + await log_ticket_action(self.cog.db, i.guild, i.user, "Unlocked", f"{i.channel.mention}") + + @discord.ui.button(label="Claim", emoji=CLAIM_EMOJI, custom_id="t_claim", style=discord.ButtonStyle.primary) + async def b_claim(self, i, b): + t = self.cog.db.fetchone("SELECT * FROM open_tickets WHERE channel_id=?", (self.ch_id,)) + if t['is_claimed']: return await i.response.send_message(f"This ticket is already claimed by <@{t['claimed_by_id']}>.", ephemeral=True) + self.cog.db.execute("UPDATE open_tickets SET is_claimed=1, claimed_by_id=? WHERE channel_id=?", (i.user.id, self.ch_id)) + await i.response.send_message(f"{CLAIM_EMOJI} Ticket claimed by {i.user.mention}. They will now handle this request.") + await log_ticket_action(self.cog.db, i.guild, i.user, "Claimed", f"{i.channel.mention}") + + @discord.ui.button(label="Close", emoji=CLOSE_EMOJI, style=discord.ButtonStyle.danger, custom_id="t_close") + async def b_close(self, i, b): + await i.response.defer(ephemeral=True) + t = self.cog.db.fetchone("SELECT * FROM open_tickets WHERE channel_id=?", (self.ch_id,)) + creator = i.guild.get_member(t['creator_id']) + if creator: + self.cog.db.execute("UPDATE user_ticket_counts SET ticket_count=MAX(0,ticket_count-1) WHERE guild_id=? AND user_id=?", (i.guild.id, creator.id)) + await i.channel.set_permissions(creator, send_messages=False, view_channel=False) + + category_info = self.cog.db.fetchone("SELECT name FROM ticket_categories WHERE category_id=?", (self.cat_id,)) + category_name = category_info['name'] if category_info else "Unknown" + + closed_category = await get_or_create_closed_category(self.cog.db, i.guild) + if closed_category: await i.channel.edit(category=closed_category) + + self.cog.db.execute("UPDATE open_tickets SET closed_by_id=?, closed_at=? WHERE channel_id=?", (i.user.id, datetime.now().isoformat(), self.ch_id)) + await log_ticket_action(self.cog.db, i.guild, i.user, "Closed", f"Ticket {i.channel.mention} (Category: {category_name})") + + closed_embed = discord.Embed( + title="Ticket Closed", + description=f"This ticket has been officially closed and archived by {i.user.mention}.\nThe user has been removed from the channel.\n\nStaff can use the buttons below to reopen, create a transcript, or permanently delete the channel.", + color=EMBED_COLOR, + timestamp=datetime.now() + ) + closed_embed.add_field(name="Ticket Creator", value=f"<@{t['creator_id']}>", inline=True) + closed_embed.add_field(name="Closed By", value=i.user.mention, inline=True) + closed_embed.add_field(name="Original Category", value=category_name, inline=True) + + await i.channel.send(embed=closed_embed, view=ClosedTicketActionsView(self.cog, self.ch_id, self.cat_id)) + await i.message.edit(view=None) + await i.followup.send("Ticket successfully closed and archived.", ephemeral=True) + self.stop() + +class ClosedTicketActionsView(discord.ui.View): + def __init__(self, cog, ch_id, cat_id): + super().__init__(timeout=None) + self.cog, self.ch_id, self.cat_id = cog, ch_id, cat_id + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + cat_info = self.cog.db.fetchone("SELECT notified_roles FROM ticket_categories WHERE category_id=?", (self.cat_id,)) + if not cat_info or not cat_info['notified_roles']: return False + allowed_role_ids = {int(r_id) for r_id in cat_info['notified_roles'].split(',')} + user_role_ids = {role.id for role in interaction.user.roles} + if not user_role_ids.intersection(allowed_role_ids): + await interaction.response.send_message("You do not have the required role for this action.", ephemeral=True) + return False + return True + + @discord.ui.button(label="Reopen", emoji=REOPEN_EMOJI, style=discord.ButtonStyle.success) + async def b_reopen(self, i: discord.Interaction, button: discord.ui.Button): + await i.response.defer(ephemeral=True) + t = self.cog.db.fetchone("SELECT * FROM open_tickets WHERE channel_id=?", (self.ch_id,)) + cat_info = self.cog.db.fetchone("SELECT discord_category_id FROM ticket_categories WHERE category_id=?", (self.cat_id,)) + + original_category = i.guild.get_channel(cat_info['discord_category_id']) + if original_category: await i.channel.edit(category=original_category) + + creator = i.guild.get_member(t['creator_id']) + if creator: + await i.channel.set_permissions(creator, view_channel=True, send_messages=True) + self.cog.db.execute("INSERT INTO user_ticket_counts VALUES (?,?,1) ON CONFLICT(guild_id,user_id) DO UPDATE SET ticket_count=ticket_count+1", (i.guild.id, creator.id)) + + self.cog.db.execute("UPDATE open_tickets SET closed_by_id=NULL, closed_at=NULL WHERE channel_id=?", (self.ch_id,)) + + reopen_embed = discord.Embed(title="Ticket Reopened", description=f"This ticket has been reopened by {i.user.mention}.", color=EMBED_COLOR) + await i.channel.send(embed=reopen_embed, view=TicketActionsView(self.cog, self.ch_id, self.cat_id)) + await i.message.edit(view=None) + await log_ticket_action(self.cog.db, i.guild, i.user, "Reopened", f"{i.channel.mention}") + self.stop() + + @discord.ui.button(label="Transcript", emoji=TRANSCRIPT_EMOJI, style=discord.ButtonStyle.primary) + async def b_transcript(self, i, b): await self._generate_transcript(i, False) + + @discord.ui.button(label="Delete", emoji=DELETE_EMOJI, style=discord.ButtonStyle.danger) + async def b_delete(self, i, b): await self._generate_transcript(i, True) + + async def _generate_transcript(self, i, delete_after): + await i.response.defer(ephemeral=True, thinking=True) + ch = i.guild.get_channel(self.ch_id) + if not ch: return await i.followup.send("Channel not found.", ephemeral=True) + + messages = [m async for m in ch.history(limit=None, oldest_first=True)] + content = f"Transcript for ticket #{ch.name} in {i.guild.name}\n\n" + for m in messages: + content += f"[{m.created_at.strftime('%Y-%m-%d %H:%M:%S')}] {m.author.display_name}: {m.clean_content}\n" + for attachment in m.attachments: content += f" [Attachment: {attachment.url}]\n" + file = discord.File(io.BytesIO(content.encode()), filename=f"transcript-{ch.name}.txt") + + try: + await i.user.send(f"Transcript for ticket {ch.mention} in {i.guild.name}:", file=file) + await i.followup.send(f"Transcript sent to your DMs.", ephemeral=True) + except: await i.followup.send("Could not DM you the transcript. Do you have DMs disabled?", file=file, ephemeral=True) + + if delete_after: + await i.followup.send("This ticket channel will be permanently deleted in 10 seconds...", ephemeral=True) + await log_ticket_action(self.cog.db, i.guild, i.user, "Deletion Scheduled", f"{ch.mention}") + await asyncio.sleep(10) + await ch.delete() + self.cog.db.execute("DELETE FROM open_tickets WHERE channel_id=?", (self.ch_id,)) + +async def setup(bot): + await bot.add_cog(TicketCog(bot)) diff --git a/bot/cogs/commands/timer.py b/bot/cogs/commands/timer.py new file mode 100644 index 0000000..dea9aed --- /dev/null +++ b/bot/cogs/commands/timer.py @@ -0,0 +1,128 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import time +import discord +from discord.ext import commands +import asyncio +from utils.Tools import * +from datetime import datetime + +class Timer(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.hybrid_command(name="timer", aliases=['tstart'], description="Starts a timer") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 30, commands.BucketType.user) + async def _timer(self, ctx, times, *, title: str = None): + if title is None: + title = 'Timer' + + try: + try: + time = int(times) + except: + convertTimeList = {'s':1, 'm':60, 'h':3600, 'd':86400, 'S':1, 'M':60, 'H':3600, 'D':86400} + time = int(times[:-1]) * convertTimeList[times[-1]] + if time > 86400: + await ctx.send("Timers should not exceed a day in duration.") + return + if time <= 0: + await ctx.send("Timers do not go into negatives.") + return + if time >= 3600: + embed = discord.Embed( + title=f'{title}', + description=f"**{time//3600}** hours, **{time%3600//60}** minutes, **{time%60}** seconds", + color=0xFF0000 + ) + embed.set_footer(text=f'Requested by {ctx.author.name}') + message = await ctx.send(embed=embed) + await message.add_reaction('⏱️') + elif time >= 60: + embed = discord.Embed( + title=f'{title}', + description=f"**{time//60}** minutes, **{time%60}** seconds", + color=0xFF0000 + ) + embed.set_footer(text=f'Requested by {ctx.author.name}') + message = await ctx.send(embed=embed) + await message.add_reaction('⏱️') + elif time < 60: + embed = discord.Embed( + title=f'{title}', + description=f'**{time}** seconds', + color=0xFF0000 + ) + embed.set_footer(text=f'Requested by {ctx.author.name}') + message = await ctx.send(embed=embed) + await message.add_reaction('⏱️') + while True: + try: + await asyncio.sleep(6) + time -= 6 + if time >= 3600: + embed = discord.Embed( + title=f'{title}', + description=f"**{time//3600}** hours, **{time%3600//60}** minutes, **{time%60}** seconds", + color=0xFF0000 + ) + embed.set_footer(text=f'Requested by {ctx.author.name}') + await message.edit(embed=embed) + elif time >= 60: + embed = discord.Embed( + title=f'{title}', + description=f"**{time//60}** minutes, **{time%60}** seconds", + color=0xFF0000 + ) + embed.set_footer(text=f'Requested by {ctx.author.name}') + await message.edit(embed=embed) + elif time < 60: + embed = discord.Embed( + title=f'{title}', + description=f"**{time}** seconds", + color=0xFF0000 + ) + embed.set_footer(text=f'Requested by {ctx.author.name}') + await message.edit(embed=embed) + if time <= 0: + embed = discord.Embed( + title=f'{title}', + description='Time is up!', + color=0xFF0000 + ) + content= ctx.author.mention + await message.edit(content=content, embed=embed) + m = await ctx.channel.get_message(message.id) + list_thingy = [] + output_list_thingy = [] + reactants = await m.reactions[0].users().flatten() + reactants.pop(reactants.index(self.client.user)) + for user in reactants: + list_thingy.append(user.id) + x = '<@!' + str(user.id) + '>' + output_list_thingy.append(x) + if output_list_thingy != []: + final = ', '.join(map(str, output_list_thingy)) + return await ctx.send(f'The timer for **{title}** has ended!\n{final}') + else: + return await ctx.send(f'The timer for **{title}** has ended!') + except: + break + except ValueError: + await ctx.send(f"Invalid time input.", delete_after=5) + + \ No newline at end of file diff --git a/bot/cogs/commands/tracking.py b/bot/cogs/commands/tracking.py new file mode 100644 index 0000000..4150e32 --- /dev/null +++ b/bot/cogs/commands/tracking.py @@ -0,0 +1,221 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ARROWRED +from discord.ext import commands +import aiosqlite +from utils.cv2 import CV2 + +INVITE_DB = "db/invite.db" +EMOJI_INVITE = ARROWRED + +from utils.config import BotName + +class Tracking(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.invites = {} + + async def ensure_tables(self, guild_id): + async with aiosqlite.connect(INVITE_DB) as db: + await db.execute(f''' + CREATE TABLE IF NOT EXISTS invites_{guild_id} ( + user_id INTEGER PRIMARY KEY, + total INTEGER DEFAULT 0, + fake INTEGER DEFAULT 0, + left INTEGER DEFAULT 0, + rejoin INTEGER DEFAULT 0 + ) + ''') + await db.execute(''' + CREATE TABLE IF NOT EXISTS logging ( + guild_id INTEGER PRIMARY KEY, + channel_id INTEGER + ) + ''') + await db.commit() + + @commands.Cog.listener() + async def on_ready(self): + import asyncio + async def fetch_invites(guild): + try: + self.invites[guild.id] = await guild.invites() + except discord.Forbidden: + pass + except Exception: + pass + + await asyncio.gather(*(fetch_invites(guild) for guild in self.bot.guilds)) + + @commands.Cog.listener() + async def on_invite_create(self, invite): + try: + self.invites[invite.guild.id] = await invite.guild.invites() + except discord.Forbidden: + pass + + @commands.Cog.listener() + async def on_invite_delete(self, invite): + try: + self.invites[invite.guild.id] = await invite.guild.invites() + except discord.Forbidden: + pass + + @commands.Cog.listener() + async def on_member_join(self, member): + guild = member.guild + await self.ensure_tables(guild.id) + + invites_before = self.invites.get(guild.id, []) + try: + invites_after = await guild.invites() + except discord.Forbidden: + invites_after = [] + + inviter = None + + for invite in invites_after: + for old_invite in invites_before: + if invite.code == old_invite.code and invite.uses > old_invite.uses: + inviter = invite.inviter + break + if inviter: + break + + self.invites[guild.id] = invites_after + + async with aiosqlite.connect(INVITE_DB) as db: + if inviter: + # Check if user has been in DB before (Rejoin) + async with db.execute(f"SELECT user_id FROM invites_{guild.id} WHERE user_id = ?", (member.id,)) as cursor: + user_row = await cursor.fetchone() + + if user_row: + await db.execute(f"UPDATE invites_{guild.id} SET rejoin = rejoin + 1 WHERE user_id = ?", (inviter.id,)) + else: + await db.execute(f"INSERT OR IGNORE INTO invites_{guild.id} (user_id) VALUES (?)", (inviter.id,)) + await db.execute(f"UPDATE invites_{guild.id} SET total = total + 1 WHERE user_id = ?", (inviter.id,)) + await db.commit() + + async with db.execute("SELECT channel_id FROM logging WHERE guild_id = ?", (guild.id,)) as cursor: + log_row = await cursor.fetchone() + + log_channel = guild.get_channel(log_row[0]) if log_row else None + if log_channel: + total = await self.get_total_invites(guild.id, inviter.id) if inviter else 0 + msg = ( + f"{member.mention} has joined {guild.name}, invited by " + f"{inviter.name if inviter else 'Unknown'}, who now has {total} invites." + ) + await log_channel.send(view=CV2("📥 Member Joined", msg)) + + @commands.Cog.listener() + async def on_member_remove(self, member): + guild = member.guild + await self.ensure_tables(guild.id) + async with aiosqlite.connect(INVITE_DB) as db: + await db.execute(f"UPDATE invites_{guild.id} SET left = left + 1 WHERE user_id = ?", (member.id,)) + await db.commit() + + async def get_total_invites(self, guild_id, user_id): + async with aiosqlite.connect(INVITE_DB) as db: + async with db.execute(f"SELECT total FROM invites_{guild_id} WHERE user_id = ?", (user_id,)) as cursor: + row = await cursor.fetchone() + return row[0] if row else 0 + + @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) + + async with aiosqlite.connect(INVITE_DB) as db: + async with db.execute(f"SELECT total, fake, left, rejoin FROM invites_{ctx.guild.id} WHERE user_id = ?", (member.id,)) as cursor: + row = await cursor.fetchone() + + if row: + total, fake, left, rejoin = row + real = total - fake - left - rejoin + else: + total = fake = left = rejoin = real = 0 + + desc = ( + f"{EMOJI_INVITE} **› {member.mention} has `{total}` invites**\n\n" + f"**Real:** `{real}`\n" + f"**Fake:** `{fake}`\n" + f"**Left:** `{left}`\n" + f"**Rejoins:** `{rejoin}`\n\n" + f"{EMOJI_INVITE} **Get {BotName} Premium Lifetime [Join Support Here](https://discord.gg/codexdev)**" + ) + await ctx.send(view=CV2(f"Invite Log - {member.name}", desc)) + + @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) + async with aiosqlite.connect(INVITE_DB) as db: + await db.execute(f"INSERT OR IGNORE INTO invites_{ctx.guild.id} (user_id) VALUES (?)", (member.id,)) + await db.execute(f"UPDATE invites_{ctx.guild.id} SET total = total + ? WHERE user_id = ?", (amount, member.id)) + await db.commit() + await ctx.send(view=CV2("✅ Success", f"Added **{amount}** invites to {member.mention}.")) + + @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) + async with aiosqlite.connect(INVITE_DB) as db: + await db.execute(f"INSERT OR REPLACE INTO invites_{ctx.guild.id} (user_id, total) VALUES (?, ?)", (member.id, amount)) + await db.commit() + await ctx.send(view=CV2("✅ Success", f"Set invites of {member.mention} to **{amount}**.")) + + @commands.command(aliases=["resetinvs"]) + @commands.has_permissions(administrator=True) + async def resetinvites(self, ctx, member: discord.Member): + await self.ensure_tables(ctx.guild.id) + async with aiosqlite.connect(INVITE_DB) as db: + await db.execute(f"DELETE FROM invites_{ctx.guild.id} WHERE user_id = ?", (member.id,)) + await db.commit() + await ctx.send(view=CV2("✅ Success", f"Reset invites of {member.mention}.")) + + @commands.command(aliases=["invlb"]) + async def invitesleaderboard(self, ctx): + await self.ensure_tables(ctx.guild.id) + async with aiosqlite.connect(INVITE_DB) as db: + async with db.execute(f"SELECT user_id, total FROM invites_{ctx.guild.id} ORDER BY total DESC LIMIT 10") as cursor: + data = await cursor.fetchall() + + if not data: + await ctx.send(view=CV2("❌ Error", "No invites found.")) + return + + leaderboard = "" + for idx, (user_id, total) in enumerate(data, start=1): + user = ctx.guild.get_member(user_id) + name = user.name if user else f"Left User ({user_id})" + leaderboard += f"#{idx} {name} — {total} invites\n" + + await ctx.send(view=CV2("📊 Invite Leaderboard", leaderboard)) + + @commands.command(aliases=["invlog"]) + @commands.has_permissions(administrator=True) + async def invitelogging(self, ctx, channel: discord.TextChannel): + await self.ensure_tables(ctx.guild.id) + async with aiosqlite.connect(INVITE_DB) as db: + await db.execute("INSERT OR REPLACE INTO logging (guild_id, channel_id) VALUES (?, ?)", (ctx.guild.id, channel.id)) + await db.commit() + await ctx.send(view=CV2("✅ Success", f"Invite logs will now be sent to {channel.mention}")) + +async def setup(bot): + await bot.add_cog(Tracking(bot)) \ No newline at end of file diff --git a/bot/cogs/commands/translate.py b/bot/cogs/commands/translate.py new file mode 100644 index 0000000..25dad59 --- /dev/null +++ b/bot/cogs/commands/translate.py @@ -0,0 +1,80 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +from deep_translator import GoogleTranslator +from discord.ui import LayoutView, TextDisplay, Separator, Container +from utils.cv2 import CV2, build_container + +class TranslateSuccess(LayoutView): + def __init__(self, original, translated, author): + super().__init__(timeout=None) + self.add_item( + build_container( + TextDisplay("🌐 **Translation Complete**"), + Separator(visible=True), + TextDisplay(f"**Original (Hinglish):**\n{original}"), + Separator(visible=True), + TextDisplay(f"**Translated (English):**\n{translated}"), + Separator(visible=True), + TextDisplay(f"Requested by **{author.display_name}**") + ) + ) + +class TranslateError(LayoutView): + def __init__(self, error_msg): + super().__init__(timeout=None) + self.add_item( + build_container( + TextDisplay("❌ **Translation Failed**"), + Separator(visible=True), + TextDisplay(f"`{error_msg}`") + ) + ) + +class TranslateCog(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.hybrid_command( + name="hinglish", + help="Translate informal Hinglish to proper English.", + usage="!hinglish chlo udhr chat active krlo idhr nai" + ) + async def hinglish(self, ctx: commands.Context, *, text: str = None): + if not text: + return await ctx.reply( + "⚠️ Please provide some Hinglish text to translate.", + ephemeral=True if ctx.interaction else False + ) + + msg = await ctx.reply( + "🔄 Translating Hinglish...", + ephemeral=True if ctx.interaction else False + ) + + try: + # Translation using deep-translator (Google) + translated = GoogleTranslator(source="auto", target="en").translate(text) + + view = TranslateSuccess(text, translated, ctx.author) + await msg.edit(content=None, view=view, embed=None) + + except Exception as e: + view = TranslateError(str(e)) + await msg.edit(content=None, view=view, embed=None) + +async def setup(bot): + await bot.add_cog(TranslateCog(bot)) diff --git a/bot/cogs/commands/vanityroles.py b/bot/cogs/commands/vanityroles.py new file mode 100644 index 0000000..5a35f80 --- /dev/null +++ b/bot/cogs/commands/vanityroles.py @@ -0,0 +1,170 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands, tasks +import aiosqlite +import aiohttp +import os +from utils.Tools import * + +DB_PATH = "db/vanity.db" + +class VanityRoles(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.bot.loop.create_task(self.initialize_db()) + + async def initialize_db(self): + os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) + async with aiosqlite.connect(DB_PATH) as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS vanity_roles ( + guild_id INTEGER, + vanity TEXT NOT NULL, + role_id INTEGER NOT NULL, + log_channel_id INTEGER NOT NULL, + current_status TEXT, + PRIMARY KEY (guild_id, vanity) + ) + """) + await db.commit() + + async with db.execute("PRAGMA table_info(vanity_roles)") as cursor: + columns = await cursor.fetchall() + column_names = [column[1] for column in columns] + + if "current_status" not in column_names: + await db.execute("ALTER TABLE vanity_roles ADD COLUMN current_status TEXT") + await db.commit() + + @commands.group(name="vanityroles", invoke_without_command=True) + @blacklist_check() + @ignore_check() + async def vanityroles(self, ctx): + await ctx.send("❗ Usage: `vanityroles setup <@role> <#channel>`, `vanityroles show`, `vanityroles reset`") + + @vanityroles.command(name="setup") + @blacklist_check() + @ignore_check() + async def setup(self, ctx, vanity: str, role: discord.Role, channel: discord.TextChannel): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute(""" + INSERT OR REPLACE INTO vanity_roles (guild_id, vanity, role_id, log_channel_id, current_status) + VALUES (?, ?, ?, ?, NULL) + """, (ctx.guild.id, vanity.lower(), role.id, channel.id)) + await db.commit() + embed = discord.Embed( + title="✅ Vanity Role Setup", + description=f"Vanity: `{vanity}`\nRole: {role.mention}\nLog Channel: {channel.mention}", + color=discord.Color.green() + ) + await ctx.send(embed=embed) + + @vanityroles.command(name="show") + @blacklist_check() + @ignore_check() + async def show(self, ctx): + async with aiosqlite.connect(DB_PATH) as db: + async with db.execute("SELECT vanity, role_id, log_channel_id FROM vanity_roles WHERE guild_id = ?", (ctx.guild.id,)) as cursor: + rows = await cursor.fetchall() + + if not rows: + return await ctx.send("❌ No vanity role setups found.") + + embed = discord.Embed(title="🔧 Vanity Role Settings", color=discord.Color.blue()) + for vanity, role_id, log_channel_id in rows: + role = ctx.guild.get_role(role_id) + channel = ctx.guild.get_channel(log_channel_id) + embed.add_field( + name=f"Vanity: `{vanity}`", + value=f"Role: {role.mention if role else role_id}\nLog: {channel.mention if channel else log_channel_id}", + inline=False + ) + await ctx.send(embed=embed) + + @vanityroles.command(name="reset") + @blacklist_check() + @ignore_check() + async def reset(self, ctx): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute("DELETE FROM vanity_roles WHERE guild_id = ?", (ctx.guild.id,)) + await db.commit() + await ctx.send("✅ All vanity role configurations have been reset.") + + @tasks.loop(seconds=15) + async def vanity_checker(self): + async with aiosqlite.connect(DB_PATH) as db: + async with db.execute("SELECT guild_id, vanity, role_id, log_channel_id, current_status FROM vanity_roles") as cursor: + rows = await cursor.fetchall() + + for guild_id, vanity, role_id, log_channel_id, current_status in rows: + guild = self.bot.get_guild(guild_id) + if not guild: + continue + + role = guild.get_role(role_id) + log_channel = guild.get_channel(log_channel_id) + + try: + async with aiohttp.ClientSession() as session: + async with session.get(f"https://discord.com/api/v10/invites/{vanity}") as response: + is_active = response.status == 200 + except Exception as e: + print(f"⚠️ Error checking vanity {vanity}: {e}") + continue + + # Vanity is ACTIVE + if is_active and current_status != "active": + await self.update_status(guild_id, vanity, "active") + assigned = 0 + for member in guild.members: + if role and role not in member.roles: + try: + await member.add_roles(role, reason="Vanity active") + assigned += 1 + except Exception as e: + print(f"❌ Could not add role to {member}: {e}") + if log_channel: + await log_channel.send(f"✅ Vanity `{vanity}` is now **active**. Role assigned to {assigned} members.") + + # Vanity is INACTIVE + elif not is_active and current_status == "active": + await self.update_status(guild_id, vanity, None) + removed = 0 + for member in guild.members: + if role and role in member.roles: + try: + await member.remove_roles(role, reason="Vanity inactive") + removed += 1 + except Exception as e: + print(f"❌ Could not remove role from {member}: {e}") + if log_channel: + await log_channel.send(f"❌ Vanity `{vanity}` is now **inactive**. Role removed from {removed} members.") + + async def update_status(self, guild_id, vanity, new_status): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute(""" + UPDATE vanity_roles SET current_status = ? WHERE guild_id = ? AND vanity = ? + """, (new_status, guild_id, vanity)) + await db.commit() + + @vanity_checker.before_loop + async def before_checker(self): + await self.bot.wait_until_ready() + +async def setup(bot): + cog = VanityRoles(bot) + await bot.add_cog(cog) + cog.vanity_checker.start() \ No newline at end of file diff --git a/bot/cogs/commands/verification.py b/bot/cogs/commands/verification.py new file mode 100644 index 0000000..ae0bc90 --- /dev/null +++ b/bot/cogs/commands/verification.py @@ -0,0 +1,1716 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import TICK +from discord .ext import commands +from discord import app_commands +import aiosqlite +import random +import string +import io +from PIL import Image ,ImageDraw ,ImageFont +import asyncio +import logging +from datetime import datetime ,timezone ,timedelta +from typing import Optional +from utils .Tools import * + + +logger =logging .getLogger ('discord') + +DATABASE_PATH ='db/verification.db' + + +DISCORD_COLORS ={ +'primary':0xFF0000 , +'success':0xFF0000 , +'warning':0xFF0000 , +'error':0xFF0000 , +'secondary':0xFF0000 , +'neutral':0xFF0000 +} + + +def utc_to_ist (dt :datetime )->datetime : + ist_offset =timedelta (hours =5 ,minutes =30 ) + return dt .replace (tzinfo =timezone .utc ).astimezone (timezone (ist_offset )) + + +async def check_bot_permissions (guild :discord .Guild ,channel =None )->dict : + """Check if bot has necessary permissions""" + bot_member =guild .me + required_perms ={ + 'guild':['manage_roles','manage_channels','send_messages','manage_messages'], + 'channel':['view_channel','send_messages','attach_files','embed_links','manage_messages'] + } + + missing_perms ={'guild':[],'channel':[]} + + + for perm in required_perms ['guild']: + if not getattr (bot_member .guild_permissions ,perm ): + missing_perms ['guild'].append (perm .replace ('_',' ').title ()) + + + if channel and hasattr (channel ,'permissions_for'): + channel_perms =channel .permissions_for (bot_member ) + for perm in required_perms ['channel']: + if not getattr (channel_perms ,perm ): + missing_perms ['channel'].append (perm .replace ('_',' ').title ()) + + return missing_perms + + +def validate_role_hierarchy (guild :discord .Guild ,role :discord .Role )->bool : + """Check if bot can manage the specified role""" + bot_top_role =guild .me .top_role + return bot_top_role .position >role .position + +async def create_verified_role (guild :discord .Guild )->discord .Role : + """Create a verified role with proper permissions""" + try : + + existing_role =discord .utils .get (guild .roles ,name ="Verified") + if existing_role : + return existing_role + + + verified_role =await guild .create_role ( + name ="Verified", + color =discord .Color .from_rgb (35 ,165 ,90 ), + reason ="Auto-created for verification system", + permissions =discord .Permissions ( + read_messages =True , + send_messages =True , + read_message_history =True , + use_external_emojis =True , + add_reactions =True , + attach_files =True , + embed_links =True , + connect =True , + speak =True , + use_voice_activation =True + ) + ) + + + bot_roles =[role for role in guild .roles if role .managed and role .members and guild .me in role .members ] + position =1 + if bot_roles : + position =min (role .position for role in bot_roles )-1 + + await verified_role .edit (position =max (1 ,position )) + + return verified_role + except Exception as e : + logger .error (f"Error creating verified role: {e}") + raise + +async def auto_fix_permissions (guild :discord .Guild ,verification_channel :discord .TextChannel ,verified_role :discord .Role ): + """Automatically fix channel permissions for verification system""" + try : + everyone_role =guild .default_role + bot_member =guild .me + failed_channels =[] + + + try : + await verification_channel .set_permissions ( + everyone_role , + view_channel =True , + send_messages =False , + add_reactions =False , + reason ="Auto-fix: Verification channel permissions" + ) + await verification_channel .set_permissions ( + verified_role , + view_channel =False , + reason ="Auto-fix: Hide verification from verified users" + ) + await verification_channel .set_permissions ( + bot_member , + view_channel =True , + send_messages =True , + manage_messages =True , + embed_links =True , + attach_files =True , + reason ="Auto-fix: Bot verification permissions" + ) + except discord .Forbidden : + logger .warning (f"Cannot fix permissions for verification channel: {verification_channel.name}") + + + for channel in guild .channels : + if isinstance (channel ,(discord .TextChannel ,discord .VoiceChannel ,discord .CategoryChannel )): + if channel .id !=verification_channel .id : + try : + + current_overwrites =channel .overwrites + + + everyone_perms =current_overwrites .get (everyone_role ) + if not everyone_perms or everyone_perms .view_channel is not False : + await channel .set_permissions ( + everyone_role , + view_channel =False , + reason ="Auto-fix: Verification system privacy" + ) + + + verified_perms =current_overwrites .get (verified_role ) + if not verified_perms or verified_perms .view_channel is not True : + await channel .set_permissions ( + verified_role , + view_channel =True , + reason ="Auto-fix: Verified role access" + ) + except discord .Forbidden : + failed_channels .append (channel .name ) + + if failed_channels : + logger .warning (f"Failed to auto-fix permissions for channels: {', '.join(failed_channels)}") + + return len (failed_channels ) + + except Exception as e : + logger .error (f"Error in auto-fix permissions: {e}") + return -1 + +class VerificationModal (discord .ui .Modal ,title ="Enter Verification Code"): + def __init__ (self ,bot ,captcha_code :str ,guild_id :int ): + super ().__init__ () + self .bot =bot + self .captcha_code =captcha_code + self .guild_id =guild_id + + captcha_input =discord .ui .TextInput ( + label ="Verification Code", + placeholder ="Enter the 6-character code from the image", + required =True , + max_length =6 , + min_length =6 + ) + + async def on_submit (self ,interaction :discord .Interaction ): + try : + if self .captcha_input .value .strip ()!=self .captcha_code : + embed =discord .Embed ( + title ="Incorrect Code", + description ="The code you entered is incorrect. Please try again by clicking the verification button in the server.", + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + return + + guild =self .bot .get_guild (self .guild_id ) + if not guild : + await interaction .response .send_message ("Server not found.",ephemeral =True ) + return + + member =guild .get_member (interaction .user .id ) + if not member : + await interaction .response .send_message ("You are not in the server.",ephemeral =True ) + return + + + async with aiosqlite .connect (DATABASE_PATH )as db : + async with db .cursor ()as cur : + await cur .execute ( + "SELECT verified_role_id FROM verification_config WHERE guild_id = ? AND enabled = 1", + (guild .id ,) + ) + result =await cur .fetchone () + + if not result : + await interaction .response .send_message ("Verification system is not configured.",ephemeral =True ) + return + + verified_role =guild .get_role (result [0 ]) + if not verified_role : + await interaction .response .send_message ("Verified role not found.",ephemeral =True ) + return + + + if verified_role in member .roles : + embed =discord .Embed ( + title ="Already Verified", + description ="You are already verified in this server!", + color =DISCORD_COLORS ['success'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + return + + + await member .add_roles (verified_role ,reason ="CAPTCHA verification completed") + + + await self .log_verification (guild .id ,member .id ,"captcha") + + current_time =utc_to_ist (discord .utils .utcnow ()) + embed =discord .Embed ( + title ="Verification Successful", + description =f"Welcome to **{guild.name}**!\n\n" + f"You have been successfully verified and can now access all channels.", + color =DISCORD_COLORS ['success'], + timestamp =current_time + ) + embed .set_footer (text =f"Verified at {current_time.strftime('%I:%M %p IST')}") + + await interaction .response .send_message (embed =embed ,ephemeral =True ) + + + await self .send_verification_log (guild ,member ,"CAPTCHA",True ) + + except discord .Forbidden : + await interaction .response .send_message ("Bot lacks permission to assign roles.",ephemeral =True ) + except Exception as e : + logger .error (f"Error in verification modal: {e}") + pass + + async def log_verification (self ,guild_id :int ,user_id :int ,method :str ): + try : + async with aiosqlite .connect (DATABASE_PATH )as db : + async with db .cursor ()as cur : + current_time =utc_to_ist (discord .utils .utcnow ()) + await cur .execute ( + "INSERT INTO verification_logs (guild_id, user_id, verification_method, verified_at) VALUES (?, ?, ?, ?)", + (guild_id ,user_id ,method ,current_time .isoformat ()) + ) + await db .commit () + except Exception as e : + logger .error (f"Error logging verification: {e}") + + async def send_verification_log (self ,guild :discord .Guild ,user :discord .Member ,method :str ,success :bool ): + try : + async with aiosqlite .connect (DATABASE_PATH )as db : + async with db .cursor ()as cur : + await cur .execute ( + "SELECT log_channel_id FROM verification_config WHERE guild_id = ?", + (guild .id ,) + ) + result =await cur .fetchone () + + if result and result [0 ]: + log_channel =guild .get_channel (result [0 ]) + if log_channel and log_channel .permissions_for (guild .me ).send_messages : + current_time =utc_to_ist (discord .utils .utcnow ()) + embed =discord .Embed ( + title ="User Verification Log", + color =DISCORD_COLORS ['success']if success else DISCORD_COLORS ['error'], + timestamp =current_time + ) + embed .add_field ( + name ="User Information", + value =f"**User:** {user.mention}\n**ID:** {user.id}\n**Username:** {user.name}", + inline =False + ) + embed .add_field ( + name ="Verification Details", + value =f"**Method:** {method}\n**Status:** {'Success' if success else 'Failed'}\n**Time:** {current_time.strftime('%I:%M %p IST')}", + inline =False + ) + embed .set_thumbnail (url =user .avatar .url if user .avatar else user .default_avatar .url ) + await log_channel .send (embed =embed ) + except Exception as e : + logger .error (f"Error sending verification log: {e}") + +class VerificationView (discord .ui .View ): + def __init__ (self ,bot ): + super ().__init__ (timeout =None ) + self .bot =bot + + @discord .ui .button (label ="Quick Verify",style =discord .ButtonStyle .green ,custom_id ="verify_button_quick") + async def verify_button (self ,interaction :discord .Interaction ,button :discord .ui .Button ): + try : + + async with aiosqlite .connect (DATABASE_PATH )as db : + async with db .cursor ()as cur : + await cur .execute ( + "SELECT verified_role_id, verification_method FROM verification_config WHERE guild_id = ? AND enabled = 1", + (interaction .guild .id ,) + ) + result =await cur .fetchone () + + if not result : + embed =discord .Embed ( + title ="System Unavailable", + description ="Verification system is not configured or disabled.", + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + return + + verified_role =interaction .guild .get_role (result [0 ]) + verification_method =result [1 ] + + if not verified_role : + embed =discord .Embed ( + description ="Verified role not found. Please contact an administrator.", + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + return + + + if verified_role in interaction .user .roles : + embed =discord .Embed ( + title ="Already Verified", + description ="You are already verified! You can access all channels.", + color =DISCORD_COLORS ['success'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + return + + + if verification_method not in ["button","both"]: + embed =discord .Embed ( + title ="CAPTCHA Required", + description ="This server requires CAPTCHA verification. Please use the CAPTCHA button below.", + color =DISCORD_COLORS ['warning'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + return + + + await interaction .user .add_roles (verified_role ,reason ="Quick button verification") + + + modal =VerificationModal (self .bot ,"",interaction .guild .id ) + await modal .log_verification (interaction .guild .id ,interaction .user .id ,"button") + await modal .send_verification_log (interaction .guild ,interaction .user ,"BUTTON",True ) + + current_time =utc_to_ist (discord .utils .utcnow ()) + embed =discord .Embed ( + title ="Welcome to the Server", + description =f"**{interaction.user.mention}** has been verified!\n\n" + f"Welcome to {interaction.guild.name}!\n" + f"You now have access to all channels.", + color =DISCORD_COLORS ['success'], + timestamp =current_time + ) + embed .set_footer (text =f"Verified at {current_time.strftime('%I:%M %p IST')}") + + await interaction .response .send_message (embed =embed ,ephemeral =True ) + + except discord .Forbidden : + embed =discord .Embed ( + description ="Bot lacks permission to assign roles. Please contact an administrator.", + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + except Exception as e : + logger .error (f"Error in verify button: {e}") + embed =discord .Embed ( + + + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + + @discord .ui .button (label ="CAPTCHA Verify",style =discord .ButtonStyle .primary ,custom_id ="verify_captcha_secure") + async def verify_captcha (self ,interaction :discord .Interaction ,button :discord .ui .Button ): + try : + + async with aiosqlite .connect (DATABASE_PATH )as db : + async with db .cursor ()as cur : + await cur .execute ( + "SELECT verified_role_id FROM verification_config WHERE guild_id = ? AND enabled = 1", + (interaction .guild .id ,) + ) + result =await cur .fetchone () + + if not result : + embed =discord .Embed ( + title ="System Unavailable", + description ="Verification system is not configured or disabled.", + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + return + + verified_role =interaction .guild .get_role (result [0 ]) + if not verified_role : + embed =discord .Embed ( + description ="Verified role not found. Please contact an administrator.", + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + return + + + if verified_role in interaction .user .roles : + embed =discord .Embed ( + title ="Already Verified", + description ="You are already verified! You can access all channels.", + color =DISCORD_COLORS ['success'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + return + + + captcha_code =self .generate_captcha_code () + captcha_image =self .create_captcha_image (captcha_code ) + + try : + + file =discord .File (captcha_image ,filename ="captcha.png") + embed =discord .Embed ( + title ="CAPTCHA Verification", + description =f"**Server:** {interaction.guild.name}\n\n" + f"Please solve the CAPTCHA below to verify yourself.\n" + f"Click the button below to enter your answer.\n\n" + f"**Important:** The code is case-sensitive!", + color =DISCORD_COLORS ['secondary'] + ) + embed .set_image (url ="attachment://captcha.png") + embed .set_footer (text ="This CAPTCHA will expire in 10 minutes") + + modal =VerificationModal (self .bot ,captcha_code ,interaction .guild .id ) + view =CaptchaModalView (modal ) + + await interaction .user .send (embed =embed ,file =file ,view =view ) + + + embed =discord .Embed ( + title ="Check Your DMs", + description ="I've sent you a CAPTCHA in your direct messages.\n\n" + f"**Steps:**\n" + f"1. Check your DMs from me\n" + f"2. Solve the CAPTCHA image\n" + f"3. Click the button to enter your answer\n\n" + f"Make sure your DMs are open!", + color =DISCORD_COLORS ['secondary'] + ) + embed .set_footer (text ="CAPTCHA expires in 10 minutes") + await interaction .response .send_message (embed =embed ,ephemeral =True ) + + except discord .Forbidden : + embed =discord .Embed ( + title ="DMs Disabled", + description ="I couldn't send you a DM! Please enable DMs from server members and try again.\n\n" + f"**How to enable DMs:**\n" + f"1. Right-click on **{interaction.guild.name}**\n" + f"2. Go to **Privacy Settings**\n" + f"3. Enable **Direct Messages**\n" + f"4. Try verification again", + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + + except Exception as e : + logger .error (f"Error in verify captcha: {e}") + embed =discord .Embed ( + + + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + + def generate_captcha_code (self )->str : + """Generate a random 6-character alphanumeric code""" + return ''.join (random .choices (string .ascii_letters +string .digits ,k =6 )) + + def create_captcha_image (self ,code :str )->io .BytesIO : + """Create a CAPTCHA image with the given code""" + + width ,height =300 ,120 + image =Image .new ('RGB',(width ,height ),color ='white') + draw =ImageDraw .Draw (image ) + + + for y in range (height ): + color_value =255 -int ((y /height )*50 ) + for x in range (width ): + draw .point ((x ,y ),fill =(color_value ,color_value ,255 )) + + + for _ in range (200 ): + x =random .randint (0 ,width ) + y =random .randint (0 ,height ) + draw .point ((x ,y ),fill =(random .randint (150 ,200 ),random .randint (150 ,200 ),random .randint (150 ,200 ))) + + + for _ in range (8 ): + x1 =random .randint (0 ,width ) + y1 =random .randint (0 ,height ) + x2 =random .randint (0 ,width ) + y2 =random .randint (0 ,height ) + draw .line ([(x1 ,y1 ),(x2 ,y2 )],fill =(random .randint (100 ,150 ),random .randint (100 ,150 ),random .randint (100 ,150 )),width =2 ) + + + try : + font =ImageFont .truetype ("utils/arial.ttf",40 ) + except : + try : + font =ImageFont .load_default () + except : + font =None + + + if font : + bbox =draw .textbbox ((0 ,0 ),code ,font =font ) + text_width =bbox [2 ]-bbox [0 ] + text_height =bbox [3 ]-bbox [1 ] + else : + text_width =len (code )*20 + text_height =20 + + start_x =(width -text_width )//2 + start_y =(height -text_height )//2 + + + for i ,char in enumerate (code ): + char_x =start_x +(i *text_width //len (code ))+random .randint (-8 ,8 ) + char_y =start_y +random .randint (-15 ,15 ) + + + color =(random .randint (0 ,100 ),random .randint (0 ,100 ),random .randint (0 ,100 )) + + if font : + draw .text ((char_x ,char_y ),char ,fill =color ,font =font ) + else : + draw .text ((char_x ,char_y ),char ,fill =color ) + + + draw .rectangle ([(0 ,0 ),(width -1 ,height -1 )],outline ='black',width =2 ) + + + img_buffer =io .BytesIO () + image .save (img_buffer ,format ='PNG',quality =95 ) + img_buffer .seek (0 ) + + return img_buffer + + + +class CaptchaOnlyVerificationView (discord .ui .View ): + def __init__ (self ,bot ): + super ().__init__ (timeout =None ) + self .bot =bot + + @discord .ui .button (label ="Verify with CAPTCHA",style =discord .ButtonStyle .primary ,custom_id ="verify_captcha_only") + async def verify_captcha (self ,interaction :discord .Interaction ,button :discord .ui .Button ): + try : + + async with aiosqlite .connect (DATABASE_PATH )as db : + async with db .cursor ()as cur : + await cur .execute ( + "SELECT verified_role_id FROM verification_config WHERE guild_id = ? AND enabled = 1", + (interaction .guild .id ,) + ) + result =await cur .fetchone () + + if not result : + embed =discord .Embed ( + title ="System Unavailable", + description ="Verification system is not configured or disabled.", + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + return + + verified_role =interaction .guild .get_role (result [0 ]) + if not verified_role : + embed =discord .Embed ( + description ="Verified role not found. Please contact an administrator.", + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + return + + + if verified_role in interaction .user .roles : + embed =discord .Embed ( + title ="Already Verified", + description ="You are already verified! You can access all channels.", + color =DISCORD_COLORS ['success'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + return + + + captcha_code =self .generate_captcha_code () + captcha_image =self .create_captcha_image (captcha_code ) + + try : + + file =discord .File (captcha_image ,filename ="captcha.png") + embed =discord .Embed ( + title ="CAPTCHA Verification", + description =f"**Server:** {interaction.guild.name}\n\n" + f"Please solve the CAPTCHA below to verify yourself.\n" + f"Click the button below to enter your answer.\n\n" + f"**Important:** The code is case-sensitive!", + color =DISCORD_COLORS ['secondary'] + ) + embed .set_image (url ="attachment://captcha.png") + embed .set_footer (text ="This CAPTCHA will expire in 10 minutes") + + modal =VerificationModal (self .bot ,captcha_code ,interaction .guild .id ) + view =CaptchaModalView (modal ) + + await interaction .user .send (embed =embed ,file =file ,view =view ) + + + embed =discord .Embed ( + title ="Check Your DMs", + description ="I've sent you a CAPTCHA in your direct messages.\n\n" + f"**Steps:**\n" + f"1. Check your DMs from me\n" + f"2. Solve the CAPTCHA image\n" + f"3. Click the button to enter your answer\n\n" + f"Make sure your DMs are open!", + color =DISCORD_COLORS ['secondary'] + ) + embed .set_footer (text ="CAPTCHA expires in 10 minutes") + await interaction .response .send_message (embed =embed ,ephemeral =True ) + + except discord .Forbidden : + embed =discord .Embed ( + title ="DMs Disabled", + description ="I couldn't send you a DM! Please enable DMs from server members and try again.\n\n" + f"**How to enable DMs:**\n" + f"1. Right-click on **{interaction.guild.name}**\n" + f"2. Go to **Privacy Settings**\n" + f"3. Enable **Direct Messages**\n" + f"4. Try verification again", + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + + except Exception as e : + logger .error (f"Error in verify captcha: {e}") + embed =discord .Embed ( + + + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + + def generate_captcha_code (self )->str : + """Generate a random 6-character alphanumeric code""" + return ''.join (random .choices (string .ascii_letters +string .digits ,k =6 )) + + def create_captcha_image (self ,code :str )->io .BytesIO : + """Create a CAPTCHA image with the given code""" + + width ,height =300 ,120 + image =Image .new ('RGB',(width ,height ),color ='white') + draw =ImageDraw .Draw (image ) + + + for y in range (height ): + color_value =255 -int ((y /height )*50 ) + for x in range (width ): + draw .point ((x ,y ),fill =(color_value ,color_value ,255 )) + + + for _ in range (200 ): + x =random .randint (0 ,width ) + y =random .randint (0 ,height ) + draw .point ((x ,y ),fill =(random .randint (150 ,200 ),random .randint (150 ,200 ),random .randint (150 ,200 ))) + + + for _ in range (8 ): + x1 =random .randint (0 ,width ) + y1 =random .randint (0 ,height ) + x2 =random .randint (0 ,width ) + y2 =random .randint (0 ,height ) + draw .line ([(x1 ,y1 ),(x2 ,y2 )],fill =(random .randint (100 ,150 ),random .randint (100 ,150 ),random .randint (100 ,150 )),width =2 ) + + + try : + font =ImageFont .truetype ("utils/arial.ttf",40 ) + except : + try : + font =ImageFont .load_default () + except : + font =None + + + if font : + bbox =draw .textbbox ((0 ,0 ),code ,font =font ) + text_width =bbox [2 ]-bbox [0 ] + text_height =bbox [3 ]-bbox [1 ] + else : + text_width =len (code )*20 + text_height =20 + + start_x =(width -text_width )//2 + start_y =(height -text_height )//2 + + + for i ,char in enumerate (code ): + char_x =start_x +(i *text_width //len (code ))+random .randint (-8 ,8 ) + char_y =start_y +random .randint (-15 ,15 ) + + + color =(random .randint (0 ,100 ),random .randint (0 ,100 ),random .randint (0 ,100 )) + + if font : + draw .text ((char_x ,char_y ),char ,fill =color ,font =font ) + else : + draw .text ((char_x ,char_y ),char ,fill =color ) + + + draw .rectangle ([(0 ,0 ),(width -1 ,height -1 )],outline ='black',width =2 ) + + + img_buffer =io .BytesIO () + image .save (img_buffer ,format ='PNG',quality =95 ) + img_buffer .seek (0 ) + + return img_buffer + +class CaptchaModalView (discord .ui .View ): + def __init__ (self ,modal :VerificationModal ): + super ().__init__ (timeout =600 ) + self .modal =modal + + @discord .ui .button (label ="Enter Code",style =discord .ButtonStyle .secondary ,custom_id ="enter_captcha_code") + async def enter_captcha (self ,interaction :discord .Interaction ,button :discord .ui .Button ): + await interaction .response .send_modal (self .modal ) + +class VerificationSetupView (discord .ui .View ): + def __init__ (self ,bot ,ctx ): + super ().__init__ (timeout =300 ) + self .bot =bot + self .ctx =ctx + self .verification_channel =None + self .log_channel =None + self .verification_method ="both" + + @discord .ui .select ( + cls =discord .ui .ChannelSelect , + channel_types =[discord .ChannelType .text ], + placeholder ="Select verification channel..." + ) + async def verification_channel_select (self ,interaction :discord .Interaction ,select :discord .ui .ChannelSelect ): + await interaction .response .defer () + selected_channel =select .values [0 ] + self .verification_channel =interaction .guild .get_channel (selected_channel .id ) + + @discord .ui .select ( + cls =discord .ui .ChannelSelect , + channel_types =[discord .ChannelType .text ], + placeholder ="Select log channel (optional)..." + ) + async def log_channel_select (self ,interaction :discord .Interaction ,select :discord .ui .ChannelSelect ): + await interaction .response .defer () + selected_channel =select .values [0 ] + self .log_channel =interaction .guild .get_channel (selected_channel .id ) + + @discord .ui .select ( + placeholder ="Select verification method...", + options =[ + discord .SelectOption ( + label ="Quick Button Only", + value ="button", + description ="Users verify instantly by clicking a button" + ), + discord .SelectOption ( + label ="CAPTCHA Only", + value ="captcha", + description ="Users must solve a CAPTCHA (more secure)" + ), + discord .SelectOption ( + label ="Both Methods", + value ="both", + description ="Users can choose between button or CAPTCHA" + ) + ] + ) + async def method_select (self ,interaction :discord .Interaction ,select :discord .ui .Select ): + await interaction .response .defer () + self .verification_method =select .values [0 ] + + @discord .ui .button (label ="Setup Verification System",style =discord .ButtonStyle .green ) + async def setup_verification (self ,interaction :discord .Interaction ,button :discord .ui .Button ): + if not self .verification_channel : + embed =discord .Embed ( + title ="Missing Configuration", + description ="Please select a verification channel first!", + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + return + + try : + await interaction .response .defer (ephemeral =True ) + + + verified_role =await create_verified_role (interaction .guild ) + + + failed_count =await auto_fix_permissions (interaction .guild ,self .verification_channel ,verified_role ) + + + async with aiosqlite .connect (DATABASE_PATH )as db : + async with db .cursor ()as cur : + await cur .execute ( + """INSERT OR REPLACE INTO verification_config + (guild_id, verification_channel_id, verified_role_id, log_channel_id, verification_method, enabled) + VALUES (?, ?, ?, ?, ?, ?)""", + ( + interaction .guild .id , + self .verification_channel .id , + verified_role .id , + self .log_channel .id if self .log_channel else None , + self .verification_method , + True + ) + ) + await db .commit () + + + await self .send_verification_panel (verified_role ) + + + try : + await asyncio .sleep (5 ) + if hasattr (interaction ,'message')and interaction .message : + await interaction .message .delete () + except discord .NotFound : + pass + except discord .Forbidden : + pass + except Exception as e : + logger .error (f"Error deleting setup embed: {e}") + + current_time =utc_to_ist (discord .utils .utcnow ()) + embed =discord .Embed ( + title ="Verification System Setup Complete", + color =DISCORD_COLORS ['success'], + timestamp =current_time + ) + embed .add_field ( + name ="Configuration Summary", + value =f"**Verification Channel:** {self.verification_channel.mention}\n" + f"**Verified Role:** {verified_role.mention}\n" + f"**Log Channel:** {self.log_channel.mention if self.log_channel else 'None'}\n" + f"**Method:** {self.verification_method.title()}", + inline =False + ) + + security_features ="All channels made private to unverified users\n" "Verification channel locked for unverified users\n" "Auto-message deletion in verification channel\n" "DM-based CAPTCHA system\n" "Comprehensive logging enabled" + + if failed_count >0 : + security_features +=f"\n\nNote: {failed_count} channels couldn't be auto-fixed due to permissions" + + embed .add_field ( + name ="Security Features", + value =security_features , + inline =False + ) + embed .add_field ( + name ="System Status", + value =f"{TICK} **Verification system is now ENABLED and ready to use!**", + inline =False + ) + embed .set_footer (text =f"Setup completed and enabled at {current_time.strftime('%I:%M %p IST')}") + + + await interaction .followup .send (embed =embed ,ephemeral =True ) + + + try : + await asyncio .sleep (3 ) + if hasattr (interaction ,'message')and interaction .message : + await interaction .message .delete () + except discord .NotFound : + pass + except discord .Forbidden : + pass + except Exception as e : + logger .error (f"Error deleting setup embed: {e}") + + self .stop () + + except Exception as e : + logger .error (f"Error setting up verification: {e}") + embed =discord .Embed ( + color =DISCORD_COLORS ['error'] + ) + await interaction .followup .send (embed =embed ,ephemeral =True ) + + async def send_verification_panel (self ,verified_role :discord .Role ): + """Send the verification panel to the verification channel""" + try : + channel =self .verification_channel + current_time =utc_to_ist (discord .utils .utcnow ()) + + embed =discord .Embed ( + title ="Server Verification Required", + description =f"**Welcome to {channel.guild.name}!**\n\n" + f"To access all channels and features, you need to verify yourself first.\n\n" + f"**Choose your verification method:**", + color =DISCORD_COLORS ['primary'], + timestamp =current_time + ) + + if self .verification_method in ["button","both"]: + embed .add_field ( + name ="Quick Verification", + value ="Instant access with one click! Perfect for trusted users.", + inline =True + ) + + if self .verification_method in ["captcha","both"]: + embed .add_field ( + name ="CAPTCHA Verification", + value ="Secure verification via DM. Proves you're human!", + inline =True + ) + + embed .add_field ( + name ="What happens after verification?", + value =f"• Access to all server channels\n" + f"• Ability to chat and participate\n" + f"• Access to all server features\n" + f"• **{verified_role.name}** role assigned", + inline =False + ) + + embed .set_footer (text =f"Verification panel • {current_time.strftime('%I:%M %p IST')}") + + + if self .verification_method =="button": + view =ButtonOnlyVerificationView (self .bot ) + elif self .verification_method =="captcha": + view =CaptchaOnlyVerificationView (self .bot ) + else : + view =VerificationView (self .bot ) + + await channel .send (embed =embed ,view =view ) + + except Exception as e : + logger .error (f"Error sending verification panel: {e}") + +class ButtonOnlyVerificationView (discord .ui .View ): + def __init__ (self ,bot ): + super ().__init__ (timeout =None ) + self .bot =bot + + @discord .ui .button (label ="Verify Now",style =discord .ButtonStyle .green ,custom_id ="verify_button_only") + async def verify_button (self ,interaction :discord .Interaction ,button :discord .ui .Button ): + try : + + async with aiosqlite .connect (DATABASE_PATH )as db : + async with db .cursor ()as cur : + await cur .execute ( + "SELECT verified_role_id, verification_method FROM verification_config WHERE guild_id = ? AND enabled = 1", + (interaction .guild .id ,) + ) + result =await cur .fetchone () + + if not result : + embed =discord .Embed ( + title ="System Unavailable", + description ="Verification system is not configured or disabled.", + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + return + + verified_role =interaction .guild .get_role (result [0 ]) + verification_method =result [1 ] + + if not verified_role : + embed =discord .Embed ( + description ="Verified role not found. Please contact an administrator.", + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + return + + + if verified_role in interaction .user .roles : + embed =discord .Embed ( + title ="Already Verified", + description ="You are already verified! You can access all channels.", + color =DISCORD_COLORS ['success'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + return + + + await interaction .user .add_roles (verified_role ,reason ="Quick button verification") + + + modal =VerificationModal (self .bot ,"",interaction .guild .id ) + await modal .log_verification (interaction .guild .id ,interaction .user .id ,"button") + await modal .send_verification_log (interaction .guild ,interaction .user ,"BUTTON",True ) + + current_time =utc_to_ist (discord .utils .utcnow ()) + embed =discord .Embed ( + title ="Welcome to the Server", + description =f"**{interaction.user.mention}** has been verified!\n\n" + f"Welcome to {interaction.guild.name}!\n" + f"You now have access to all channels.", + color =DISCORD_COLORS ['success'], + timestamp =current_time + ) + embed .set_footer (text =f"Verified at {current_time.strftime('%I:%M %p IST')}") + + await interaction .response .send_message (embed =embed ,ephemeral =True ) + + except discord .Forbidden : + embed =discord .Embed ( + description ="Bot lacks permission to assign roles. Please contact an administrator.", + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + except Exception as e : + logger .error (f"Error in verify button: {e}") + embed =discord .Embed ( + + + color =DISCORD_COLORS ['error'] + ) + await interaction .response .send_message (embed =embed ,ephemeral =True ) + +class Verification (commands .Cog ): + def __init__ (self ,bot ): + self .bot =bot + self .bot .loop .create_task (self .create_tables ()) + + self .bot .add_view (VerificationView (self .bot )) + self .bot .add_view (ButtonOnlyVerificationView (self .bot )) + self .bot .add_view (CaptchaOnlyVerificationView (self .bot )) + + async def create_tables (self ): + """Create database tables for verification system""" + try : + async with aiosqlite .connect (DATABASE_PATH )as db : + await db .execute (""" + CREATE TABLE IF NOT EXISTS verification_config ( + guild_id INTEGER PRIMARY KEY, + verification_channel_id INTEGER NOT NULL, + verified_role_id INTEGER NOT NULL, + log_channel_id INTEGER, + verification_method TEXT DEFAULT 'both', + enabled BOOLEAN DEFAULT 1, + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + """) + + await db .execute (""" + CREATE TABLE IF NOT EXISTS verification_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + verification_method TEXT NOT NULL, + verified_at TEXT NOT NULL, + FOREIGN KEY (guild_id) REFERENCES verification_config (guild_id) + ) + """) + + await db .commit () + pass + except Exception as e : + logger .error (f"Error creating verification tables: {e}") + + @commands .Cog .listener () + async def on_message (self ,message ): + """Auto-delete messages in verification channel from non-bot users""" + if message .author .bot : + return + + try : + async with aiosqlite .connect (DATABASE_PATH )as db : + async with db .cursor ()as cur : + await cur .execute ( + "SELECT verification_channel_id FROM verification_config WHERE guild_id = ? AND enabled = 1", + (message .guild .id ,) + ) + result =await cur .fetchone () + + if result and result [0 ]==message .channel .id : + + if not message .author .guild_permissions .manage_messages : + try : + await message .delete () + + embed =discord .Embed ( + title ="Message Deleted", + description ="This channel is for verification only. Please use the buttons above to verify.", + color =DISCORD_COLORS ['warning'] + ) + try : + await message .author .send (embed =embed ) + except discord .Forbidden : + pass + except discord .Forbidden : + pass + except Exception as e : + logger .error (f"Error in verification message handler: {e}") + + @commands .hybrid_group (name ="verification",invoke_without_command =True ,description ="Advanced verification system management.") + @commands .has_permissions (administrator =True ) + async def verification (self ,ctx ): + await ctx .send_help (ctx .command ) + + @verification .command (name ="setup",description ="Set up the advanced verification system.") + @blacklist_check () + @ignore_check () + @commands .has_permissions (administrator =True ) + async def verification_setup (self ,ctx ): + try : + + missing_perms =await check_bot_permissions (ctx .guild ) + + if missing_perms ['guild']: + embed =discord .Embed ( + title ="Missing Permissions", + description =f"Bot is missing required server permissions: {', '.join(missing_perms['guild'])}\n\n" + "Please grant these permissions and try again.", + color =DISCORD_COLORS ['error'] + ) + await ctx .send (embed =embed ) + return + + current_time =utc_to_ist (discord .utils .utcnow ()) + embed =discord .Embed ( + title ="Advanced Verification System Setup", + description ="**Welcome to the next-generation verification system!**\n\n" + "• **Auto-creates verified role** with proper permissions\n" + "• **DM-based CAPTCHA** system for enhanced security\n" + "• **Smart channel management** - hides verification after verification\n" + "• **Auto-permission fixing** for seamless setup\n" + "• **Auto-message deletion** in verification channel\n" + "• **Comprehensive logging** and analytics\n\n" + "**Configure your system using the dropdowns below:**", + color =DISCORD_COLORS ['primary'], + timestamp =current_time + ) + embed .set_footer (text =f"Setup wizard started at {current_time.strftime('%I:%M %p IST')}") + + view =VerificationSetupView (self .bot ,ctx ) + await ctx .send (embed =embed ,view =view ) + + except Exception as e : + logger .error (f"Error in verification setup: {e}") + embed =discord .Embed ( + color =DISCORD_COLORS ['error'] + ) + await ctx .send (embed =embed ) + + @verification .command (name ="status",description ="Check verification system status and analytics.") + @blacklist_check () + @ignore_check () + @commands .has_permissions (administrator =True ) + async def verification_status (self ,ctx ): + try : + async with aiosqlite .connect (DATABASE_PATH )as db : + async with db .cursor ()as cur : + await cur .execute ( + """SELECT verification_channel_id, verified_role_id, log_channel_id, + verification_method, enabled FROM verification_config + WHERE guild_id = ?""", + (ctx .guild .id ,) + ) + result =await cur .fetchone () + + if not result : + embed =discord .Embed ( + title ="Not Configured", + description ="Verification system is not set up. Use `/verification setup` to get started!", + color =DISCORD_COLORS ['error'] + ) + await ctx .send (embed =embed ) + return + + verification_channel =ctx .guild .get_channel (result [0 ]) + verified_role =ctx .guild .get_role (result [1 ]) + log_channel =ctx .guild .get_channel (result [2 ])if result [2 ]else None + verification_method =result [3 ] + enabled =result [4 ] + + + await cur .execute ( + "SELECT COUNT(*) FROM verification_logs WHERE guild_id = ?", + (ctx .guild .id ,) + ) + total_verifications =(await cur .fetchone ())[0 ] + + await cur .execute ( + """SELECT verification_method, COUNT(*) FROM verification_logs + WHERE guild_id = ? GROUP BY verification_method""", + (ctx .guild .id ,) + ) + method_stats =await cur .fetchall () + + + yesterday =utc_to_ist (discord .utils .utcnow ())-timedelta (days =1 ) + await cur .execute ( + "SELECT COUNT(*) FROM verification_logs WHERE guild_id = ? AND verified_at > ?", + (ctx .guild .id ,yesterday .isoformat ()) + ) + recent_verifications =(await cur .fetchone ())[0 ] + + + issues =[] + if not verification_channel : + issues .append ("Verification channel not found") + if not verified_role : + issues .append ("Verified role not found") + elif not validate_role_hierarchy (ctx .guild ,verified_role ): + issues .append ("Bot cannot manage verified role (role hierarchy)") + + missing_perms =await check_bot_permissions (ctx .guild ,verification_channel ) + if missing_perms ['guild']: + issues .append (f"Missing server permissions: {', '.join(missing_perms['guild'])}") + if missing_perms ['channel']: + issues .append (f"Missing channel permissions: {', '.join(missing_perms['channel'])}") + + current_time =utc_to_ist (discord .utils .utcnow ()) + embed =discord .Embed ( + title ="Verification System Status", + color =DISCORD_COLORS ['success']if enabled and not issues else DISCORD_COLORS ['warning']if enabled else DISCORD_COLORS ['error'], + timestamp =current_time + ) + + + status_text ="Fully Operational"if enabled and not issues else "Operational with Issues"if enabled else "Disabled" + + embed .add_field ( + name ="System Status", + value =f"**Status:** {status_text}\n" + f"**Method:** {verification_method.title()}\n" + f"**Enabled:** {'Yes' if enabled else 'No'}", + inline =True + ) + + embed .add_field ( + name ="Configuration", + value =f"**Channel:** {verification_channel.mention if verification_channel else 'Not found'}\n" + f"**Role:** {verified_role.mention if verified_role else 'Not found'}\n" + f"**Log Channel:** {log_channel.mention if log_channel else 'None'}", + inline =True + ) + + embed .add_field ( + name ="Analytics", + value =f"**Total Verifications:** {total_verifications}\n" + f"**Last 24 Hours:** {recent_verifications}\n" + f"**Verified Members:** {len([m for m in ctx.guild.members if verified_role in m.roles]) if verified_role else 0}", + inline =True + ) + + if method_stats : + stats_text ="\n".join ([f"**{method.title()}:** {count}"for method ,count in method_stats ]) + embed .add_field ( + name ="Method Breakdown", + value =stats_text , + inline =True + ) + + if issues : + embed .add_field ( + name ="Issues Detected", + value ="\n".join ([f"• {issue}"for issue in issues ]), + inline =False + ) + + embed .set_footer (text =f"Status checked at {current_time.strftime('%I:%M %p IST')}") + await ctx .send (embed =embed ) + + except Exception as e : + logger .error (f"Error checking verification status: {e}") + embed =discord .Embed ( + color =DISCORD_COLORS ['error'] + ) + await ctx .send (embed =embed ) + + @verification .command (name ="fix",description ="Auto-fix channel permissions for verification system.") + @blacklist_check () + @ignore_check () + @commands .has_permissions (administrator =True ) + async def verification_fix (self ,ctx ): + try : + async with aiosqlite .connect (DATABASE_PATH )as db : + async with db .cursor ()as cur : + await cur .execute ( + "SELECT verification_channel_id, verified_role_id FROM verification_config WHERE guild_id = ? AND enabled = 1", + (ctx .guild .id ,) + ) + result =await cur .fetchone () + + if not result : + embed =discord .Embed ( + title ="Not Configured", + description ="Verification system is not set up or disabled.", + color =DISCORD_COLORS ['error'] + ) + await ctx .send (embed =embed ) + return + + verification_channel =ctx .guild .get_channel (result [0 ]) + verified_role =ctx .guild .get_role (result [1 ]) + + if not verification_channel or not verified_role : + embed =discord .Embed ( + description ="Verification channel or role not found.", + color =DISCORD_COLORS ['error'] + ) + await ctx .send (embed =embed ) + return + + + failed_count =await auto_fix_permissions (ctx .guild ,verification_channel ,verified_role ) + + if failed_count ==-1 : + embed =discord .Embed ( + color =DISCORD_COLORS ['error'] + ) + elif failed_count >0 : + embed =discord .Embed ( + title ="Permissions Partially Fixed", + description =f"Permissions have been auto-fixed for most channels.\n" + f"{failed_count} channels couldn't be fixed due to permission restrictions.", + color =DISCORD_COLORS ['warning'] + ) + else : + embed =discord .Embed ( + title ="Permissions Fixed", + description ="All channel permissions have been auto-fixed successfully!", + color =DISCORD_COLORS ['success'] + ) + + await ctx .send (embed =embed ) + + except Exception as e : + logger .error (f"Error fixing verification permissions: {e}") + embed =discord .Embed ( + color =DISCORD_COLORS ['error'] + ) + await ctx .send (embed =embed ) + + @verification .command (name ="disable",description ="Disable the verification system and reset all channel permissions.") + @blacklist_check () + @ignore_check () + @commands .has_permissions (administrator =True ) + async def verification_disable (self ,ctx ): + try : + async with aiosqlite .connect (DATABASE_PATH )as db : + async with db .cursor ()as cur : + await cur .execute ( + "SELECT verification_channel_id, verified_role_id FROM verification_config WHERE guild_id = ?", + (ctx .guild .id ,) + ) + result =await cur .fetchone () + + if not result : + await ctx .send ("Verification system is not set up.") + return + + + await cur .execute ( + "UPDATE verification_config SET enabled = 0 WHERE guild_id = ?", + (ctx .guild .id ,) + ) + await db .commit () + + + verification_channel =ctx .guild .get_channel (result [0 ])if result [0 ]else None + verified_role =ctx .guild .get_role (result [1 ])if result [1 ]else None + everyone_role =ctx .guild .default_role + count =0 + failed_count =0 + + + for channel in ctx .guild .channels : + if isinstance (channel ,(discord .TextChannel ,discord .VoiceChannel ,discord .CategoryChannel )): + try : + + overwrites =channel .overwrites .copy () + + + if everyone_role in overwrites : + del overwrites [everyone_role ] + if verified_role and verified_role in overwrites : + del overwrites [verified_role ] + + + await channel .edit (overwrites =overwrites ,reason ="Verification system disabled - restoring public access") + count +=1 + except discord .Forbidden : + failed_count +=1 + except Exception as e : + logger .error (f"Error resetting permissions for channel {channel.name}: {e}") + failed_count +=1 + + current_time =utc_to_ist (discord .utils .utcnow ()) + embed =discord .Embed ( + title ="Verification System Disabled", + description =f"The verification system has been disabled and all channels have been reset to public access.\n\n" + f"**Channels Reset:** {count}\n" + f"**Failed to Reset:** {failed_count}"+(f" (due to permission restrictions)"if failed_count >0 else ""), + color =DISCORD_COLORS ['success']if failed_count ==0 else DISCORD_COLORS ['warning'], + timestamp =current_time + ) + embed .set_footer (text =f"Disabled and reset at {current_time.strftime('%I:%M %p IST')}") + await ctx .send (embed =embed ) + + except Exception as e : + logger .error (f"Error disabling verification: {e}") + embed =discord .Embed ( + color =DISCORD_COLORS ['error'] + ) + await ctx .send (embed =embed ) + + @verification .command (name ="enable",description ="Enable the verification system.") + @blacklist_check () + @ignore_check () + @commands .has_permissions (administrator =True ) + async def verification_enable (self ,ctx ): + try : + async with aiosqlite .connect (DATABASE_PATH )as db : + async with db .cursor ()as cur : + await cur .execute ( + "SELECT verified_role_id FROM verification_config WHERE guild_id = ?", + (ctx .guild .id ,) + ) + result =await cur .fetchone () + + if not result : + await ctx .send ("Verification system is not set up. Use `/verification setup` first.") + return + + + verified_role =ctx .guild .get_role (result [0 ]) + if not verified_role : + await ctx .send ("Verified role no longer exists. Please run setup again.") + return + + if not validate_role_hierarchy (ctx .guild ,verified_role ): + await ctx .send ("Bot cannot manage the verified role due to role hierarchy. Please fix role positions.") + return + + + missing_perms =await check_bot_permissions (ctx .guild ) + if missing_perms ['guild']: + await ctx .send ( + f"Bot is missing required permissions: " + f"{', '.join(missing_perms['guild'])}. Please grant these permissions first." + ) + return + + await cur .execute ( + "UPDATE verification_config SET enabled = 1 WHERE guild_id = ?", + (ctx .guild .id ,) + ) + await db .commit () + + current_time =utc_to_ist (discord .utils .utcnow ()) + embed =discord .Embed ( + title ="Verification System Enabled", + description ="The verification system has been enabled.", + color =DISCORD_COLORS ['success'], + timestamp =current_time + ) + embed .set_footer (text =f"Enabled at {current_time.strftime('%I:%M %p IST')}") + await ctx .send (embed =embed ) + + except Exception as e : + logger .error (f"Error enabling verification: {e}") + pass + + @verification .command (name ="logs",description ="View recent verification logs.") + @blacklist_check () + @ignore_check () + @commands .has_permissions (administrator =True ) + async def verification_logs (self ,ctx ,limit :int =10 ): + try : + if limit >50 : + limit =50 + + async with aiosqlite .connect (DATABASE_PATH )as db : + async with db .cursor ()as cur : + await cur .execute ( + """SELECT user_id, verification_method, verified_at + FROM verification_logs WHERE guild_id = ? + ORDER BY verified_at DESC LIMIT ?""", + (ctx .guild .id ,limit ) + ) + logs =await cur .fetchall () + + if not logs : + await ctx .send ("No verification logs found.") + return + + current_time =utc_to_ist (discord .utils .utcnow ()) + embed =discord .Embed ( + title =f"Recent Verification Logs ({len(logs)})", + color =DISCORD_COLORS ['primary'], + timestamp =current_time + ) + + log_text ="" + for user_id ,method ,verified_at in logs : + user =ctx .guild .get_member (user_id ) + user_name =user .display_name if user else f"Unknown User ({user_id})" + log_text +=f"**{user_name}** - {method.upper()} - {verified_at}\n" + + embed .description =log_text + embed .set_footer (text =f"Logs retrieved at {current_time.strftime('%I:%M %p IST')}") + await ctx .send (embed =embed ) + + except Exception as e : + logger .error (f"Error retrieving verification logs: {e}") + pass + + @verification .command (name ="reset",description ="Reset all channel permissions (remove verification restrictions).") + @blacklist_check () + @ignore_check () + @commands .has_permissions (administrator =True ) + async def verification_reset (self ,ctx ): + try : + embed =discord .Embed ( + title ="Reset Channel Permissions", + description ="This will remove all verification-related channel restrictions.\n\n" + "**Warning:** This action cannot be undone and may take some time.\n" + "All channels will become visible to @everyone again.", + color =DISCORD_COLORS ['warning'] + ) + + view =discord .ui .View (timeout =60 ) + + async def confirm_reset (interaction ): + if interaction .user !=ctx .author : + await interaction .response .send_message ("This action is not for you!",ephemeral =True ) + return + + await interaction .response .defer () + + + everyone_role =ctx .guild .default_role + count =0 + failed_count =0 + + for channel in ctx .guild .channels : + if isinstance (channel ,(discord .TextChannel ,discord .VoiceChannel ,discord .CategoryChannel )): + try : + await channel .set_permissions ( + everyone_role , + overwrite =None , + reason ="Verification system reset" + ) + count +=1 + except discord .Forbidden : + failed_count +=1 + + current_time =utc_to_ist (discord .utils .utcnow ()) + success_embed =discord .Embed ( + title ="Permissions Reset Complete", + description =f"Successfully reset permissions for {count} channels.\n" + f"{f'Failed to reset {failed_count} channels due to permission restrictions.' if failed_count > 0 else ''}\n\n" + f"The verification system configuration has been preserved.\n" + f"You can re-enable restrictions using `/verification setup`.", + color =DISCORD_COLORS ['success'], + timestamp =current_time + ) + success_embed .set_footer (text =f"Reset completed at {current_time.strftime('%I:%M %p IST')}") + await interaction .edit_original_response (embed =success_embed ,view =None ) + + async def cancel_reset (interaction ): + if interaction .user !=ctx .author : + await interaction .response .send_message ("This action is not for you!",ephemeral =True ) + return + await interaction .response .edit_message (content ="Reset cancelled.",embed =None ,view =None ) + + confirm_button =discord .ui .Button (label ="Confirm Reset",style =discord .ButtonStyle .red ) + cancel_button =discord .ui .Button (label ="Cancel",style =discord .ButtonStyle .grey ) + + confirm_button .callback =confirm_reset + cancel_button .callback =cancel_reset + + view .add_item (confirm_button ) + view .add_item (cancel_button ) + + await ctx .send (embed =embed ,view =view ) + + except Exception as e : + logger .error (f"Error in verification reset: {e}") + pass + + @verification .command (name ="verify",description ="Manually verify a user (Admin only).") + @blacklist_check () + @ignore_check () + @commands .has_permissions (administrator =True ) + async def verification_verify (self ,ctx ,user :discord .Member ): + try : + async with aiosqlite .connect (DATABASE_PATH )as db : + async with db .cursor ()as cur : + await cur .execute ( + "SELECT verified_role_id FROM verification_config WHERE guild_id = ? AND enabled = 1", + (ctx .guild .id ,) + ) + result =await cur .fetchone () + + if not result : + await ctx .send ("Verification system is not set up or disabled.") + return + + verified_role =ctx .guild .get_role (result [0 ]) + if not verified_role : + await ctx .send ("Verified role not found.") + return + + if not validate_role_hierarchy (ctx .guild ,verified_role ): + await ctx .send ("Bot cannot manage the verified role due to role hierarchy.") + return + + if verified_role in user .roles : + await ctx .send (f"{user.mention} is already verified.") + return + + + if not ctx .guild .me .guild_permissions .manage_roles : + await ctx .send ("Bot lacks 'Manage Roles' permission.") + return + + + await user .add_roles (verified_role ,reason =f"Manual verification by {ctx.author}") + + + async with aiosqlite .connect (DATABASE_PATH )as db : + async with db .cursor ()as cur : + current_time =utc_to_ist (discord .utils .utcnow ()) + await cur .execute ( + "INSERT INTO verification_logs (guild_id, user_id, verification_method, verified_at) VALUES (?, ?, ?, ?)", + (ctx .guild .id ,user .id ,"manual",current_time .isoformat ()) + ) + await db .commit () + + current_time =utc_to_ist (discord .utils .utcnow ()) + embed =discord .Embed ( + title ="User Manually Verified", + description =f"{user.mention} has been manually verified by {ctx.author.mention}.", + color =DISCORD_COLORS ['success'], + timestamp =current_time + ) + embed .set_footer (text =f"Verified at {current_time.strftime('%I:%M %p IST')}") + await ctx .send (embed =embed ) + + except discord .Forbidden : + await ctx .send ("Bot lacks permission to assign roles.") + except Exception as e : + logger .error (f"Error manually verifying user: {e}") + pass + +async def setup (bot): + await bot.add_cog(Verification (bot)) + logger .info ("Advanced verification cog loaded successfully") diff --git a/bot/cogs/commands/voice.py b/bot/cogs/commands/voice.py new file mode 100644 index 0000000..6e3a8bd --- /dev/null +++ b/bot/cogs/commands/voice.py @@ -0,0 +1,754 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CROSS, TICK +from discord.ext import commands +from discord.utils import get +import os +from utils.Tools import * +from typing import Optional, Union +from discord.ext.commands import Context +from utils import Paginator, DescriptionEmbedPaginator, FieldPagePaginator, TextPaginator +from utils import * + + +class Voice(commands.Cog): + + def __init__(self, bot): + self.bot = bot + self.color = 0xFF0000 + + @commands.group(name="voice", invoke_without_command=True, aliases=['vc']) + @blacklist_check() + @ignore_check() + async def vc(self, ctx: commands.Context): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @vc.command(name="kick", + help="Removes a user from the voice channel.", + usage="voice kick ") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def _kick(self, ctx, *, member: discord.Member): + if member.voice is None: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + f"{str(member)} is not connected to any voice channel", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + ch = member.voice.channel.mention + await member.edit(voice_channel=None, + reason=f"Disconnected by {str(ctx.author)}") + embed2 = discord.Embed(title=f"{TICK}> Success", + + description=f"{str(member)} has been disconnected from {ch}", + color=self.color) + embed2.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed2.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed2) + + @vc.command(name="kickall", + help="Disconnect all members from the voice channel.", + usage="voice kick all") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + #@commands.bot_has_permissions(move_members=True) + + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def _kickall(self, ctx): + if ctx.author.voice is None: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + "You are not connected to any voice channels.", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + count = 0 + ch = ctx.author.voice.channel.mention + for member in ctx.author.voice.channel.members: + await member.edit( + voice_channel=None, + reason=f"Disconnect All Command Executed By: {str(ctx.author)}") + count += 1 + embed2 = discord.Embed(title=f"{TICK}> Success", + + description=f"Disconnected {count} members from {ch}", + color=self.color) + embed2.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed2.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed2) + + @vc.command(name="mute", + help="mute a member in voice channel .", + usage="voice mute ") + @commands.has_guild_permissions(mute_members=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def _mute(self, ctx, *, member: discord.Member = None): + if member is None: + embed = discord.Embed( + title=f"{CROSS} Error", + description="You need to mention a member to mute.", + color=self.color + ) + embed.set_footer( + text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url + ) + return await ctx.reply(embed=embed) + + if member.voice is None: + embed = discord.Embed( + title=f"{CROSS} Error", + description=f"{str(member)} is not connected to any voice channels.", + color=self.color + ) + embed.set_footer( + text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url + ) + return await ctx.reply(embed=embed) + + if member.voice.mute: + embed = discord.Embed( + title=f"{CROSS} Error", + description=f"{str(member)} is already muted in the voice channel.", + color=self.color + ) + embed.set_footer( + text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url + ) + return await ctx.reply(embed=embed) + + await member.edit(mute=True) + embed = discord.Embed( + title=f"{TICK}> Success", + description=f"{str(member)} has been muted in {member.voice.channel.mention}.", + color=self.color + ) + embed.set_footer( + text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url + ) + return await ctx.reply(embed=embed) + + @vc.command(name="unmute", + help="Unmute a member in the voice channel.", + usage="voice unmute ") + @blacklist_check() + @ignore_check() + @commands.has_guild_permissions(mute_members=True) + #@commands.bot_has_permissions(mute_members=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def vcunmute(self, ctx, *, member: discord.Member): + if member.voice is None: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + f"{str(member)} is not connected to any voice channel.", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + if member.voice.mute == False: + embed2 = discord.Embed(title=f"{CROSS} Error", + + description= + f"{str(member)} is already unmuted in the voice channel.", + color=self.color) + embed2.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed2.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed2) + ch = member.voice.channel.mention + embed3 = discord.Embed(title=f"{TICK}> Success", + + description=f"{str(member)} has been unmuted in {ch}", + color=self.color) + embed3.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed3.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + await member.edit(mute=False, reason=f"Unmuted by {str(ctx.author)}") + return await ctx.reply(embed=embed3) + + @vc.command(name="muteall", + help="Mute all members in a voice channel.", + usage="voice muteall") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + #@commands.bot_has_permissions(mute_members=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def _muteall(self, ctx): + if ctx.author.voice is None: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + "You are not connected to any voice channel.", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + count = 0 + ch = ctx.author.voice.channel.mention + for member in ctx.author.voice.channel.members: + if member.voice.mute == False: + await member.edit( + mute=True, + reason= + f"voice muteall Command Executed by {str(ctx.author)}") + count += 1 + embed2 = discord.Embed(title=f"{TICK}> Success", + description=f"Muted {count} members in {ch}", + color=self.color) + embed2.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed2.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed2) + + @vc.command(name="unmuteall", + help="Unmute all members in a voice channel.", + usage="voice unmuteall") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + #@commands.bot_has_permissions(mute_members=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def _unmuteall(self, ctx): + if ctx.author.voice is None: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + "You are not connected to any of the voice channel", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + count = 0 + ch = ctx.author.voice.channel.mention + for member in ctx.author.voice.channel.members: + if member.voice.mute == True: + await member.edit( + mute=False, + reason= + f"Voice unmuteall Command Executed by: {str(ctx.author)}") + count += 1 + embed2 = discord.Embed(title=f"{TICK}> Success", + description=f"Unmuted {count} members in {ch}", + color=self.color) + embed2.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed2.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed2) + + @vc.command(name="deafen", + help="Deafen a user in a voice channel.", + usage="voice deafen ") + @blacklist_check() + @ignore_check() + @commands.has_guild_permissions(deafen_members=True) + #@commands.bot_has_permissions(deafen_members=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def _deafen(self, ctx, *, member: discord.Member): + if member.voice is None: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + f"{str(member)} is not connected to any of the voice channel", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + if member.voice.deaf == True: + embed2 = discord.Embed(title=f"{CROSS} Error", + + description= + f"{str(member)} is already deafened in the voice channel", + color=self.color) + embed2.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed2.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed2) + ch = member.voice.channel.mention + embed3 = discord.Embed(title=f"{TICK}> Success", + + description=f"{str(member)} has been Deafened in {ch}", + color=self.color) + embed3.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed3.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + await member.edit(deafen=True, reason=f"Deafen by {str(ctx.author)}") + return await ctx.reply(embed=embed3) + + @vc.command(name="undeafen", + help="Undeafen a User in a voice channel .", + usage="voice undeafen ") + @blacklist_check() + @ignore_check() + @commands.has_guild_permissions(deafen_members=True) + #@commands.bot_has_permissions(deafen_members=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def _undeafen(self, ctx, *, member: discord.Member): + if member.voice is None: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + f"{str(member)} is not connected to any of the voice channel", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + if member.voice.deaf == False: + embed2 = discord.Embed(title=f"{CROSS} Error", + + description= + f"{str(member)} is already undeafened in the voice channel", + color=self.color) + embed2.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed2.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed2) + ch = member.voice.channel.mention + embed3 = discord.Embed(title=f"{TICK}> Success", + + description=f"{str(member)} has been undeafened in {ch}", + color=self.color) + embed3.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed3.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + await member.edit(deafen=False, + reason=f"Undeafen by {str(ctx.author)}") + return await ctx.reply(embed=embed3) + + @vc.command(name="deafenall", + help="Deafen all Ussr in a voice channel.", + usage="voice deafenall") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + #@commands.bot_has_permissions(deafen_members=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def _deafenall(self, ctx): + if ctx.author.voice is None: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + "You are not connected to any of the voice channel", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + count = 0 + ch = ctx.author.voice.channel.mention + for member in ctx.author.voice.channel.members: + if member.voice.deaf == False: + await member.edit( + deafen=True, + reason= + f"voice deafenall Command Executed by {str(ctx.author)}") + count += 1 + embed2 = discord.Embed(title=f"{TICK}> Success", + description=f"Deafened {count} members in {ch}", + color=self.color) + embed2.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed2.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed2) + + @vc.command(name="undeafenall", + help="undeafen all member in a voice channel .", + usage="voice undeafenall") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + #@commands.bot_has_permissions(deafen_members=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def _undeafall(self, ctx): + if ctx.author.voice is None: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + "You are not connected in any of the voice channel", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + count = 0 + ch = ctx.author.voice.channel.mention + for member in ctx.author.voice.channel.members: + if member.voice.deaf == True: + await member.edit( + deafen=False, + reason= + f"Voice undeafenall Command Executed by: {str(ctx.author)}") + count += 1 + embed2 = discord.Embed(title=f"{TICK}> Success", + + description=f"Undeafened {count} members in {ch}", + color=self.color) + embed2.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed2.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed2) + + @vc.command(name="moveall", + help="Move all members from the voice channel to the specified voice channel.", + usage="voice moveall ") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + #@commands.bot_has_permissions(move_members=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def _moveall(self, ctx, *, channel: discord.VoiceChannel): + if ctx.author.voice is None: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + "You are not connected to any of the voice channel", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + try: + ch = ctx.author.voice.channel.mention + nch = channel.mention + count = 0 + for member in ctx.author.voice.channel.members: + await member.edit( + voice_channel=channel, + reason= + f"voice moveall Command Executed by: {str(ctx.author)}") + count += 1 + embed2 = discord.Embed(title=f"{TICK}> Success", + + description=f"{count} Members moved from {ch} to {nch}", + color=self.color) + embed2.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed2.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + await ctx.reply(embed=embed2) + except: + embed3 = discord.Embed(title=f"{CROSS} Error", + + description=f"Invalid Voice channel provided", + color=self.color) + embed3.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed3.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + await ctx.reply(embed=embed3) + + + + @vc.command(name="pullall", + help="Move all members of ALL voice channels to a specified voice channel.", + usage="voice pullall ") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + #@commands.bot_has_permissions(move_members=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def _pullall(self, ctx, *, channel: discord.VoiceChannel): + if ctx.author.voice is None: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + "You are not connected to any of the voice channel", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + count = 0 + for vc in ctx.guild.voice_channels: + for member in vc.members: + if member != ctx.author: + try: + await member.edit( + voice_channel=channel, + reason=f"Pullall Command Executed by: {str(ctx.author)}") + count += 1 + except: + pass + embed2 = discord.Embed(title=f"{TICK}> Success", + description=f"Moved {count} members to {channel.mention}", + color=self.color) + embed2.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed2.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed2) + + + @vc.command(name="move", + help="Move a member from one voice channel to another.", + usage="voice move ") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + #@commands.bot_has_permissions(move_members=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def _move(self, ctx, member: discord.Member, channel: discord.VoiceChannel): + if member.voice is None: + embed = discord.Embed(title=f"{CROSS}Error", + + description= + f"{str(member)} is not connected to any voice channel.", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + if channel == member.voice.channel: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + f"{str(member)} is already in {channel.mention}.", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + await member.edit(voice_channel=channel, + reason=f"Moved by {str(ctx.author)}") + embed2 = discord.Embed(title=f"{TICK}> Success", + + description=f"{str(member)} has been moved to {channel.mention}", + color=self.color) + embed2.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed2.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed2) + + + @vc.command(name="pull", + help="Pull a member from one voice channel to yours.", + usage="voice pull ") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + #@commands.bot_has_permissions(move_members=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def _pull(self, ctx, member: discord.Member): + if ctx.author.voice is None: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + "You are not connected to any voice channel.", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + if member.voice is None: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + f"{str(member)} is not connected to any voice channel.", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + if member.voice.channel == ctx.author.voice.channel: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + f"{str(member)} is already in your voice channel.", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + await member.edit(voice_channel=ctx.author.voice.channel, + reason=f"Pulled by {str(ctx.author)}") + embed2 = discord.Embed(title=f"{TICK}> Success", + + description=f"{str(member)} has been pulled to your voice channel.", + color=self.color) + embed2.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed2.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed2) + + @vc.command(name="lock", + help="Locks the voice channel so no one can join.", + usage="voice lock") + @blacklist_check() + @ignore_check() + @commands.has_permissions(manage_roles=True) + @commands.bot_has_permissions(manage_roles=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def _lock(self, ctx): + if ctx.author.voice is None: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + "You are not connected to any voice channel.", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + ch = ctx.author.voice.channel.mention + await ctx.author.voice.channel.set_permissions(ctx.guild.default_role, + connect=False, + reason=f"Locked by {str(ctx.author)}") + embed2 = discord.Embed(title=f"{TICK}> Success", + + description=f"{ch} has been locked.", + color=self.color) + embed2.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed2.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed2) + + @vc.command(name="unlock", + help="Unlocks the voice channel so anyone can join.", + usage="voice unlock") + @blacklist_check() + @ignore_check() + @commands.has_permissions(manage_roles=True) + @commands.bot_has_permissions(manage_roles=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def _unlock(self, ctx): + if ctx.author.voice is None: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + "You are not connected to any voice channel.", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + ch = ctx.author.voice.channel.mention + await ctx.author.voice.channel.set_permissions(ctx.guild.default_role, + connect=True, + reason=f"Unlocked by {str(ctx.author)}") + embed2 = discord.Embed(title=f"{TICK}> Success", + + description=f"{ch} has been unlocked.", + color=self.color) + embed2.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed2.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed2) + + @vc.command(name="private", + help="Makes the voice channel private.", + usage="voice private") + @blacklist_check() + @ignore_check() + @commands.has_permissions(manage_roles=True) + @commands.bot_has_permissions(manage_roles=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def _private(self, ctx): + if ctx.author.voice is None: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + "You are not connected to any voice channel.", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + ch = ctx.author.voice.channel.mention + await ctx.author.voice.channel.set_permissions(ctx.guild.default_role, + connect=False, + view_channel=False, + reason=f"Made private by {str(ctx.author)}") + embed2 = discord.Embed(title=f"{TICK}> Success", + + description=f"{ch} has been made private.", + color=self.color) + embed2.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed2.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed2) + + @vc.command(name="unprivate", + help="Makes the voice channel public.", + usage="voice unprivate") + @blacklist_check() + @ignore_check() + @commands.has_permissions(manage_roles=True) + @commands.bot_has_permissions(manage_roles=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def _unprivate(self, ctx): + if ctx.author.voice is None: + embed = discord.Embed(title=f"{CROSS} Error", + + description= + "You are not connected to any voice channel.", + color=self.color) + embed.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed) + ch = ctx.author.voice.channel.mention + await ctx.author.voice.channel.set_permissions(ctx.guild.default_role, + connect=True, + view_channel=True, + reason=f"Made public by {str(ctx.author)}") + embed2 = discord.Embed(title=f"{TICK}> Success", + + description=f"{ch} has been made public.", + color=self.color) + embed2.set_footer(text=f"Requested by: {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed2.set_thumbnail(url="https://cdn.discordapp.com/emojis/1279464563150032991.png") + return await ctx.reply(embed=embed2) + + \ No newline at end of file diff --git a/bot/cogs/commands/welcome.py b/bot/cogs/commands/welcome.py new file mode 100644 index 0000000..788b88c --- /dev/null +++ b/bot/cogs/commands/welcome.py @@ -0,0 +1,950 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ICONS_CHANNEL, ICONS_PLUS, TICK, ZTICK +from discord.ext import commands +from discord.ui import View, Select, Button +import aiosqlite +import asyncio +import re +import json +from utils.Tools import * + +class VariableButton(Button): + def __init__(self, author): + super().__init__(label="Variables", style=discord.ButtonStyle.secondary) + self.author = author + + async def callback(self, interaction: discord.Interaction): + if interaction.user != self.author: + await interaction.response.send_message("Only the command author can use this button.", ephemeral=True) + return + + variables = { + "{user}": "Mentions the user (e.g., @UserName).", + "{user_avatar}": "The user's avatar URL.", + "{user_name}": "The user's username.", + "{user_id}": "The user's ID number.", + "{user_nick}": "The user's nickname in the server.", + "{user_joindate}": "The user's join date in the server (formatted as Day, Month Day, Year).", + "{user_createdate}": "The user's account creation date (formatted as Day, Month Day, Year).", + "{server_name}": "The server's name.", + "{server_id}": "The server's ID number.", + "{server_membercount}": "The server's total member count.", + "{server_icon}": "The server's icon URL." + } + + + embed = discord.Embed( + title="Available Placeholders", + description="Use these placeholders in your welcome message:", + color=discord.Color(0xFF0000) + ) + + for var, desc in variables.items(): + embed.add_field(name=var, value=desc, inline=False) + + embed.set_footer(text="Add placeholders directly in the welcome message or embed fields.") + await interaction.response.send_message(embed=embed, ephemeral=True) + +class Welcomer(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.bot.loop.create_task(self._create_table()) + + async def _create_table(self): + async with aiosqlite.connect("db/welcome.db") as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS welcome ( + guild_id INTEGER PRIMARY KEY, + welcome_type TEXT, + welcome_message TEXT, + channel_id INTEGER, + embed_data TEXT, + auto_delete_duration INTEGER + ) + """) + await db.commit() + + @commands.hybrid_group(invoke_without_command=True, name="greet", help="Shows all the greet commands.") + @blacklist_check() + @ignore_check() + async def greet(self, ctx: commands.Context): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + @greet.command(name="setup", help="Configures a welcome message for new members joining the server. ") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + @commands.cooldown(1, 6, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def greet_setup(self, ctx): + async with aiosqlite.connect("db/welcome.db") as db: + async with db.execute("SELECT * FROM welcome WHERE guild_id = ?", (ctx.guild.id,)) as cursor: + row = await cursor.fetchone() + + if row: + error = discord.Embed(description=f"A welcome message has already been set in {ctx.guild.name}. Use `{ctx.prefix}greet reset` to reconfigure.", color=0xFF0000) + error.set_author(name="Error", icon_url="https://cdn.discordapp.com/emojis/1294218790082711553.png") + return await ctx.send(embed=error) + + options_view = View(timeout=600) + + async def option_callback(interaction: discord.Interaction, button: Button): + if interaction.user != ctx.author: + await interaction.response.send_message("You cannot interact with this setup.", ephemeral=True) + return + await interaction.response.defer() + + if button.custom_id == "simple": + await interaction.message.delete() + await self.simple_setup(ctx) + + elif button.custom_id == "embed": + await interaction.message.delete() + await self.embed_setup(ctx) + elif button.custom_id == "cancel": + await interaction.message.delete() + + button_simple = Button(label="Simple", style=discord.ButtonStyle.success, custom_id="simple") + button_simple.callback = lambda interaction: option_callback(interaction, button_simple) + options_view.add_item(button_simple) + + button_embed = Button(label="Embed", style=discord.ButtonStyle.success, custom_id="embed") + button_embed.callback = lambda interaction: option_callback(interaction, button_embed) + options_view.add_item(button_embed) + + button_cancel = Button(label="Cancel", style=discord.ButtonStyle.danger, custom_id="cancel") + button_cancel.callback = lambda interaction: option_callback(interaction, button_cancel) + options_view.add_item(button_cancel) + + embed = discord.Embed( + title="Welcome Message Setup", + description="Choose the type of welcome message you want to create:", + color=0xFF0000 + ) + + embed.add_field( + name=" Simple", + value="Send a plain text welcome message. You can use placeholders to personalize it.\n\n", + inline=False + ) + embed.add_field( + name=" Embed", + value="Send a welcome message in an embed format. You can customize the embed with a title, description, image, etc.", + inline=False + ) + + embed.set_footer(text="Click the buttons below to choose the welcome message type.", icon_url=self.bot.user.display_avatar.url) + + + await ctx.send(embed=embed, view=options_view) + + async def simple_setup(self, ctx): + setup_view = View(timeout=600) + first = View(timeout=600) + message_content = [] + + placeholders = { + "user": ctx.author.mention, + "user_avatar": ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url, + "user_name": ctx.author.name, + "user_id": ctx.author.id, + "user_nick": ctx.author.display_name, + "user_joindate": ctx.author.joined_at.strftime("%a, %b %d, %Y"), + "user_createdate": ctx.author.created_at.strftime("%a, %b %d, %Y"), + "server_name": ctx.guild.name, + "server_id": ctx.guild.id, + "server_membercount": ctx.guild.member_count, + "server_icon": ctx.guild.icon.url if ctx.guild.icon else "https://cdn.discordapp.com/embed/avatars/0.png", + "timestamp": discord.utils.format_dt(ctx.message.created_at) + } + + def safe_format(text): + placeholders_lower = {k.lower(): v for k, v in placeholders.items()} + + def replace_var(match): + var_name = match.group(1).lower() + return str(placeholders_lower.get(var_name, f"{{{var_name}}}")) + + return re.sub(r"\{(\w+)\}", replace_var, text or "") + + + async def update_preview(content): + preview = safe_format(content) + await preview_message.edit(content=f"**Preview:** {preview}", view=setup_view) + + first.add_item(VariableButton(ctx.author)) + + preview_message = await ctx.send("__**Simple Message Setup**__ \nEnter your welcome message here:", view=first) + + async def submit_callback(interaction): + if interaction.user != ctx.author: + await interaction.response.send_message("You cannot interact with this setup.", ephemeral=True) + return + if message_content: + await self._save_welcome_data(ctx.guild.id, "simple", message_content[0]) + await interaction.response.send_message(f"{TICK}> Welcome message setup completed!") + for item in setup_view.children: + item.disabled = True + await preview_message.edit(view=setup_view) + else: + await interaction.response.send_message("No message entered to submit.", ephemeral=True) + + async def edit_callback(interaction): + if interaction.user != ctx.author: + await interaction.response.send_message("You cannot interact with this setup.", ephemeral=True) + return + await interaction.response.defer() + await ctx.send("Enter the updated welcome message:") + try: + msg = await self.bot.wait_for("message", timeout=600, check=lambda m: m.author == ctx.author and m.channel == ctx.channel) + message_content.clear() + message_content.append(msg.content) + await update_preview(msg.content) + except asyncio.TimeoutError: + await ctx.send("Editing timed out.") + + async def cancel_callback(interaction): + if interaction.user != ctx.author: + await interaction.response.send_message("You cannot interact with this setup.", ephemeral=True) + return + await preview_message.delete() + + submit_button = Button(label="Submit", style=discord.ButtonStyle.success) + submit_button.callback = submit_callback + setup_view.add_item(submit_button) + + edit_button = Button(label="Edit", style=discord.ButtonStyle.primary) + edit_button.callback = edit_callback + setup_view.add_item(edit_button) + setup_view.add_item(VariableButton(ctx.author)) + + cancel_button = Button(emoji=ICONS_PLUS, style=discord.ButtonStyle.secondary) + cancel_button.callback = cancel_callback + setup_view.add_item(cancel_button) + + try: + msg = await self.bot.wait_for("message", timeout=600, check=lambda m: m.author == ctx.author and m.channel == ctx.channel) + message_content.append(msg.content) + await update_preview(msg.content) + except asyncio.TimeoutError: + await ctx.send("Setup timed out.") + + + async def _save_welcome_data(self, guild_id, welcome_type, message, embed_data=None): + async with aiosqlite.connect("db/welcome.db") as db: + await db.execute(""" + INSERT OR REPLACE INTO welcome (guild_id, welcome_type, welcome_message, embed_data) + VALUES (?, ?, ?, ?) + """, (guild_id, welcome_type, message, json.dumps(embed_data) if embed_data else None)) + await db.commit() + + + + + async def embed_setup(self, ctx): + setup_view = View(timeout=600) + embed_data = { + "message": None, + "title": None, + "description": None, + "color": None, + "footer_text": None, + "footer_icon": None, + "author_name": None, + "author_icon": None, + "thumbnail": None, + "image": None, + } + + placeholders = { + "user": ctx.author.mention, + "user_avatar": ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url, + "user_name": ctx.author.name, + "user_id": ctx.author.id, + "user_nick": ctx.author.display_name, + "user_joindate": ctx.author.joined_at.strftime("%a, %b %d, %Y"), + "user_createdate": ctx.author.created_at.strftime("%a, %b %d, %Y"), + "server_name": ctx.guild.name, + "server_id": ctx.guild.id, + "server_membercount": ctx.guild.member_count, + "server_icon": ctx.guild.icon.url if ctx.guild.icon else "https://cdn.discordapp.com/embed/avatars/0.png", + "timestamp": discord.utils.format_dt(ctx.message.created_at) + } + + def safe_format(text): + placeholders_lower = {k.lower(): v for k, v in placeholders.items()} + + def replace_var(match): + var_name = match.group(1).lower() + return str(placeholders_lower.get(var_name, f"{{{var_name}}}")) + + return re.sub(r"\{(\w+)\}", replace_var, text or "") + + + async def update_preview(): + content = safe_format(embed_data["message"]) or "Message Content." + embed = discord.Embed( + title=safe_format(embed_data["title"]) or "", + description=safe_format(embed_data["description"]) or "```Customize your welcome embed, take help of variables.```", + color=discord.Color(embed_data["color"]) if embed_data["color"] else discord.Color(0x2f3136) + ) + + + if embed_data["footer_text"]: + embed.set_footer(text=safe_format(embed_data["footer_text"]), icon_url=safe_format(embed_data["footer_icon"]) or None) + if embed_data["author_name"]: + embed.set_author(name=safe_format(embed_data["author_name"]), icon_url=safe_format(embed_data["author_icon"]) or None) + if embed_data["thumbnail"]: + embed.set_thumbnail(url=safe_format(embed_data["thumbnail"])) + if embed_data["image"]: + embed.set_image(url=safe_format(embed_data["image"])) + + await preview_message.edit(content="**Embed Preview:** " + content, embed=embed, view=setup_view) + + preview_message = await ctx.send("Configuring embed welcome message...") + + async def handle_selection(interaction: discord.Interaction): + if interaction.user != ctx.author: + await interaction.response.send_message("You cannot interact with this setup.", ephemeral=True) + return + + selected_option = select_menu.values[0] + await interaction.response.defer() + + try: + if selected_option == "message": + await ctx.send("Enter the welcome message content:") + msg = await self.bot.wait_for("message", check=lambda m: m.author == ctx.author and m.channel == ctx.channel) + embed_data["message"] = msg.content + + elif selected_option == "title": + await ctx.send("Enter the embed title:") + msg = await self.bot.wait_for("message", check=lambda m: m.author == ctx.author and m.channel == ctx.channel) + embed_data["title"] = msg.content + + elif selected_option == "description": + await ctx.send("Enter the embed description:") + msg = await self.bot.wait_for("message", check=lambda m: m.author == ctx.author and m.channel == ctx.channel) + embed_data["description"] = msg.content + + elif selected_option == "color": + await ctx.send("Enter a hex color (e.g., #3498db or 3498db):") + msg = await self.bot.wait_for("message", check=lambda m: m.author == ctx.author and m.channel == ctx.channel) + color_code = msg.content.lstrip("#") + if all(c in "0123456789abcdefABCDEF" for c in color_code) and len(color_code) in {3, 6}: + embed_data["color"] = int(color_code.lstrip("#"), 16) + else: + await ctx.send("Invalid color code. Please enter a valid hex color.") + + elif selected_option in ["footer_text", "footer_icon", "author_name", "author_icon", "thumbnail", "image"]: + await ctx.send(f"Enter the URL or text for {selected_option.replace('_', ' ')}:") + msg = await self.bot.wait_for("message", check=lambda m: m.author == ctx.author and m.channel == ctx.channel) + url_or_text = msg.content + if selected_option in ["footer_icon", "author_icon", "thumbnail", "image"]: + if url_or_text.startswith("http") or url_or_text in ["{user_avatar}", "{server_icon}"]: + embed_data[selected_option] = url_or_text + else: + await ctx.send("Invalid URL. Please enter a valid image URL or a supported placeholder ({user_avatar} or {server_icon}).") + else: + embed_data[selected_option] = url_or_text + + await update_preview() + await interaction.followup.send(f"{selected_option.capitalize()} updated.", ephemeral=True) + except asyncio.TimeoutError: + await ctx.send("You took too long to respond. Please try again.") + except Exception as e: + await ctx.send(f"An error occurred: {e}") + + select_menu = Select( + placeholder="Choose an option to edit the Embed", + options=[ + discord.SelectOption(label="Message Content", value="message"), + discord.SelectOption(label="Title", value="title"), + discord.SelectOption(label="Description", value="description"), + discord.SelectOption(label="Color", value="color"), + discord.SelectOption(label="Footer Text", value="footer_text"), + discord.SelectOption(label="Footer Icon", value="footer_icon"), + discord.SelectOption(label="Author Name", value="author_name"), + discord.SelectOption(label="Author Icon", value="author_icon"), + discord.SelectOption(label="Thumbnail", value="thumbnail"), + discord.SelectOption(label="Image", value="image") + ] + ) + select_menu.callback = handle_selection + setup_view.add_item(select_menu) + + async def submit_callback(interaction): + if interaction.user != ctx.author: + await interaction.response.send_message("You cannot interact with this setup.", ephemeral=True) + return + if not any(embed_data[key] for key in ["title", "description"]): + await interaction.response.send_message("Please provide at least a title or an description before submitting.", ephemeral=True) + return + + await self._save_welcome_data(ctx.guild.id, "embed", embed_data["message"] or "", embed_data) + await interaction.response.send_message(f"{TICK}> Embed welcome message setup completed!") + + for item in setup_view.children: + item.disabled = True + await preview_message.edit(view=setup_view) + + submit_button = Button(label="Submit", style=discord.ButtonStyle.success) + submit_button.callback = submit_callback + setup_view.add_item(submit_button) + setup_view.add_item(VariableButton(ctx.author)) + + async def cancel_callback(interaction): + if interaction.user != ctx.author: + await interaction.response.send_message("You cannot interact with this setup.", ephemeral=True) + return + await preview_message.delete() + await interaction.response.send_message("Embed setup cancelled.", ephemeral=True) + + cancel_button = Button(label="Cancel", style=discord.ButtonStyle.danger) + cancel_button.callback = cancel_callback + setup_view.add_item(cancel_button) + + await update_preview() + + + + @greet.command(name="reset", aliases=["disable"], help="Resets and deletes the current welcome configuration for the server.") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + @commands.cooldown(1, 6, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def greet_reset(self, ctx): + async with aiosqlite.connect("db/welcome.db") as db: + cursor = await db.execute("SELECT 1 FROM welcome WHERE guild_id = ?", (ctx.guild.id,)) + is_set_up = await cursor.fetchone() + + if not is_set_up: + error = discord.Embed(description=f"No welcome message has been set for {ctx.guild.name}! Please set a welcome message first using `{ctx.prefix}greet setup`", color=0xFF0000) + error.set_author(name="Greet is not configured!", icon_url="https://cdn.discordapp.com/emojis/1294218790082711553.png") + return await ctx.send(embed=error) + + embed = discord.Embed( + title="Are you sure?", + description="This will remove all welcome configurations & data related to welcome messages for this server!", + color=0xFF0000 + ) + + yes_button = Button(label="Confirm", style=discord.ButtonStyle.danger) + no_button = Button(label="Cancel", style=discord.ButtonStyle.secondary) + + async def yes_button_callback(interaction): + if interaction.user != ctx.author: + await interaction.response.send_message("Only the command author can confirm this action.", ephemeral=True) + return + + async with aiosqlite.connect("db/welcome.db") as db: + await db.execute("DELETE FROM welcome WHERE guild_id = ?", (ctx.guild.id,)) + await db.commit() + + embed.color = discord.Color(0xFF0000) + embed.title = f"{TICK}> Success" + embed.description = "Welcome message configuration has been successfully reset." + await interaction.message.edit(embed=embed, view=None) + + async def no_button_callback(interaction): + if interaction.user != ctx.author: + await interaction.response.send_message("Only the command author can cancel this action.", ephemeral=True) + return + + embed.color = discord.Color(0xFF0000) + embed.title = "Cancelled" + embed.description = "Greet Reset operation has been cancelled." + await interaction.message.edit(embed=embed, view=None) + + yes_button.callback = yes_button_callback + no_button.callback = no_button_callback + + view = View() + view.add_item(yes_button) + view.add_item(no_button) + + await ctx.send(embed=embed, view=view) + + + @greet.command(name="channel", help="Sets the channel where welcome messages will be sent.") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + @commands.cooldown(1, 6, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def greet_channel(self, ctx): + async with aiosqlite.connect("db/welcome.db") as db: + async with db.execute("SELECT welcome_type, channel_id FROM welcome WHERE guild_id = ?", (ctx.guild.id,)) as cursor: + result = await cursor.fetchone() + welcome_message = result[0] if result else None + welcome_channel = ctx.guild.get_channel(result[1]) if result and result[1] else None + + if not welcome_message: + error = discord.Embed(description=f"No welcome message has been set for {ctx.guild.name}! Please set a welcome message first using `{ctx.prefix}greet setup`", color=0xFF0000) + error.set_author(name="Greet is not configured!", icon_url="https://cdn.discordapp.com/emojis/1294218790082711553.png") + await ctx.send(embed=error) + return + + channels = ctx.guild.text_channels + chunk_size = 25 + chunks = [channels[i:i + chunk_size] for i in range(0, len(channels), chunk_size)] + current_page = 0 + + def generate_view(page): + select_menu = Select( + placeholder="Select a channel for welcome messages", + options=[ + discord.SelectOption(label=channel.name, emoji=ICONS_CHANNEL, value=str(channel.id)) + for channel in chunks[page] + ] + ) + + async def select_callback(interaction: discord.Interaction): + if interaction.user != ctx.author: + await interaction.response.send_message("You are not authorized to set the welcome channel.", ephemeral=True) + return + + selected_channel_id = int(select_menu.values[0]) + selected_channel = ctx.guild.get_channel(selected_channel_id) + + async with aiosqlite.connect("db/welcome.db") as db: + await db.execute("UPDATE welcome SET channel_id = ? WHERE guild_id = ?", (selected_channel_id, ctx.guild.id)) + await db.commit() + + embed.description = f"Current Welcome Channel: {selected_channel.mention}" + await interaction.response.edit_message(embed=embed, view=None) + await ctx.send(f"{TICK}> Welcome channel has been set to {selected_channel.mention}") + + select_menu.callback = select_callback + + next_button = Button(label="Next List of Channels", style=discord.ButtonStyle.secondary, disabled=page >= len(chunks) - 1) + previous_button = Button(label="Previous", style=discord.ButtonStyle.secondary, disabled=page <= 0) + + async def next_callback(interaction: discord.Interaction): + if interaction.user != ctx.author: + await interaction.response.send_message("You are not authorized to navigate these menus.", ephemeral=True) + return + nonlocal current_page + current_page += 1 + await interaction.response.edit_message(embed=embed, view=generate_view(current_page)) + + async def previous_callback(interaction: discord.Interaction): + if interaction.user != ctx.author: + await interaction.response.send_message("You are not authorized to navigate these menus.", ephemeral=True) + return + nonlocal current_page + current_page -= 1 + await interaction.response.edit_message(embed=embed, view=generate_view(current_page)) + + next_button.callback = next_callback + previous_button.callback = previous_callback + + view = View() + view.add_item(select_menu) + view.add_item(previous_button) + view.add_item(next_button) + return view + + embed = discord.Embed( + title=f"Welcome Channel for {ctx.guild.name}", + description=f"Current Welcome Channel: {welcome_channel.mention if welcome_channel else 'None'}", + color=0xFF0000 + ) + embed.set_footer(text="Use the dropdown menu to select a channel. Navigate pages if needed.") + + await ctx.send(embed=embed, view=generate_view(current_page)) + + + + @greet.command(name="test", help="Sends a test welcome message to preview the setup.") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + @commands.cooldown(1, 6, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def greet_test(self, ctx): + async with aiosqlite.connect("db/welcome.db") as db: + async with db.execute("SELECT welcome_type, welcome_message, channel_id, embed_data FROM welcome WHERE guild_id = ?", (ctx.guild.id,)) as cursor: + row = await cursor.fetchone() + + if row is None: + error = discord.Embed(description=f"No welcome message has been set for {ctx.guild.name}! Please set a welcome message first using `{ctx.prefix}greet setup`", color=0xFF0000) + error.set_author(name="Greet is not configured!", icon_url="https://cdn.discordapp.com/emojis/1294218790082711553.png") + await ctx.send(embed=error) + return + + welcome_type, welcome_message, channel_id, embed_data = row + welcome_channel = self.bot.get_channel(channel_id) + + if not welcome_channel: + error2 = discord.Embed(description=f"Welcome channel not set or invalid. Use `{ctx.prefix}greet channel` to set one.", color=0xFF0000) + error2.set_author(name="Channel not set", icon_url="https://cdn.discordapp.com/emojis/1294218790082711553.png") + await ctx.send(embed=error2) + return + + placeholders = { + "user": ctx.author.mention, + "user_avatar": ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url, + "user_name": ctx.author.name, + "user_id": ctx.author.id, + "user_nick": ctx.author.display_name, + "user_joindate": ctx.author.joined_at.strftime("%a, %b %d, %Y"), + "user_createdate": ctx.author.created_at.strftime("%a, %b %d, %Y"), + "server_name": ctx.guild.name, + "server_id": ctx.guild.id, + "server_membercount": ctx.guild.member_count, + "server_icon": ctx.guild.icon.url if ctx.guild.icon else "https://cdn.discordapp.com/embed/avatars/0.png", + "timestamp": discord.utils.format_dt(ctx.message.created_at) + } + + def safe_format(text): + placeholders_lower = {k.lower(): v for k, v in placeholders.items()} + + def replace_var(match): + var_name = match.group(1).lower() + return str(placeholders_lower.get(var_name, f"{{{var_name}}}")) + + return re.sub(r"\{(\w+)\}", replace_var, text or "") + + + if welcome_type == "simple" and welcome_message: + await welcome_channel.send(safe_format(welcome_message)) + + elif welcome_type == "embed" and embed_data: + try: + embed_info = json.loads(embed_data) + color_value = embed_info.get("color", None) + + + embed_color = 0x2f3136 + + + if color_value and isinstance(color_value, str) and color_value.startswith("#"): + embed_color = discord.Color(int(color_value.lstrip("#"), 16)) + elif isinstance(color_value, int): + embed_color = discord.Color(color_value) + + except (ValueError, SyntaxError, json.JSONDecodeError): + await ctx.send("Invalid embed data format. Please reconfigure.") + return + + content = safe_format(embed_info.get("message", "")) or None + embed = discord.Embed( + title=safe_format(embed_info.get("title", "")), + description=safe_format(embed_info.get("description", "")), + color=embed_color + ) + embed.timestamp = discord.utils.utcnow() + + + if embed_info.get("footer_text"): + embed.set_footer( + text=safe_format(embed_info["footer_text"]), + icon_url=safe_format(embed_info.get("footer_icon", "")) + ) + if embed_info.get("author_name"): + embed.set_author( + name=safe_format(embed_info["author_name"]), + icon_url=safe_format(embed_info.get("author_icon", "")) + ) + if embed_info.get("thumbnail"): + embed.set_thumbnail(url=safe_format(embed_info["thumbnail"])) + if embed_info.get("image"): + embed.set_image(url=safe_format(embed_info["image"])) + + await welcome_channel.send(content=content, embed=embed) + + + + @greet.command(name="config", help="Shows the current welcome configuration.") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + @commands.cooldown(1, 6, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def greet_config(self, ctx): + async with aiosqlite.connect("db/welcome.db") as db: + async with db.execute("SELECT * FROM welcome WHERE guild_id = ?", (ctx.guild.id,)) as cursor: + row = await cursor.fetchone() + + if row: + _, welcome_type, welcome_message, channel_id, embed_data, auto_delete_duration = row + response_type = "Simple" if welcome_type == "simple" else "Embed" + + embed = discord.Embed( + title=f"Greet Configuration for {ctx.guild.name}", + color=0xFF0000 + ) + + embed.add_field(name="Response Type", value=response_type, inline=False) + + if welcome_type == "simple": + details = f"Message Content: {welcome_message or 'None'}" + embed.add_field(name="Details", value=details[:1024], inline=False) + else: + embed_details = json.loads(embed_data) if embed_data else {} + formatted_embed_data = "\n".join( + f"{key.replace('_', ' ').title()}: {value or 'None'}" for key, value in embed_details.items() + ) or "None" + + for i, chunk in enumerate([formatted_embed_data[i:i+1024] for i in range(0, len(formatted_embed_data), 1024)]): + embed.add_field(name=f"Embed Data Part {i+1}", value=chunk, inline=False) + + greet_channel = self.bot.get_channel(channel_id) + channel_display = greet_channel.mention if greet_channel else "None" + auto_delete_duration = f"{auto_delete_duration} seconds" if auto_delete_duration else "None" + + embed.add_field(name="Greet Channel", value=channel_display, inline=False) + embed.add_field(name="Auto Delete Duration", value=auto_delete_duration, inline=False) + await ctx.send(embed=embed) + else: + error = discord.Embed( + description=f"No welcome message has been set for {ctx.guild.name}! Please set a welcome message first using `{ctx.prefix}greet setup`", + color=0xFF0000 + ) + error.set_author(name="Greet is not configured!", icon_url="https://cdn.discordapp.com/emojis/1294218790082711553.png") + await ctx.send(embed=error) + + + @greet.command(name="autodelete", aliases=["autodel"], help="Sets the auto-delete duration for the welcome message.") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + @commands.cooldown(1, 6, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def greet_autodelete(self, ctx, time: str): + + if time.endswith("s"): + seconds = int(time[:-1]) + if 3 <= seconds <= 300: + auto_delete_duration = seconds + else: + await ctx.send("Auto delete time should be between 3 seconds and 300 seconds.") + return + elif time.endswith("m"): + minutes = int(time[:-1]) + if 1 <= minutes <= 5: + auto_delete_duration = minutes * 60 + else: + await ctx.send("Auto delete time should be between 1 minute and 5 minutes.") + return + else: + await ctx.send("Invalid time format. Please use 's' for seconds and 'm' for minutes.") + return + + + async with aiosqlite.connect("db/welcome.db") as db: + await db.execute(""" + UPDATE welcome + SET auto_delete_duration = ? + WHERE guild_id = ? + """, (auto_delete_duration, ctx.guild.id)) + await db.commit() + + await ctx.send(f"{ZTICK} Auto delete duration has been set to **{auto_delete_duration}** seconds.") + + + + @greet.command(name="edit", help="Edits the current welcome message settings for the server.") + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + @commands.cooldown(1, 6, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + async def greet_edit(self, ctx): + async with aiosqlite.connect("db/welcome.db") as db: + async with db.execute("SELECT welcome_type, welcome_message, embed_data FROM welcome WHERE guild_id = ?", (ctx.guild.id,)) as cursor: + row = await cursor.fetchone() + + if row is None: + error = discord.Embed(description=f"No welcome message has been set for {ctx.guild.name}! Please set a welcome message first using `{ctx.prefix}greet setup`", color=0xFF0000) + error.set_author(name="Greet is not configured!", icon_url="https://cdn.discordapp.com/emojis/1294218790082711553.png") + await ctx.send(embed=error) + return + + welcome_type, welcome_message, embed_data = row + + cancel_flag = False + + if welcome_type == "simple": + embed = discord.Embed( + title="Edit Welcome Message", + description=f"**Response Type:** Simple\n**Message Content:** {welcome_message or 'None'}", + color=0xFF0000 + ) + edit_button = Button(label="Edit", style=discord.ButtonStyle.primary) + cancel_button = Button(label="Cancel", style=discord.ButtonStyle.danger) + + async def cancel_button_callback(interaction): + nonlocal cancel_flag + if interaction.user != ctx.author: + await interaction.response.send_message("You are not authorized to cancel the setup.", ephemeral=True) + return + await interaction.response.send_message("Setup has been canceled.", ephemeral=True) + cancel_flag = True + view.clear_items() + await interaction.message.edit(embed=embed, view=view) + + cancel_button.callback = cancel_button_callback + + async def edit_button_callback(interaction): + if interaction.user != ctx.author: + await interaction.response.send_message("You are not authorized to edit the welcome message.", ephemeral=True) + return + + await interaction.response.send_message("Please provide the new welcome message:", ephemeral=True) + try: + new_message = await self.bot.wait_for( + "message", + check=lambda m: m.author == ctx.author and m.channel == ctx.channel, + timeout=600 + ) + if cancel_flag: + await ctx.send("Setup was canceled. No changes were made.") + return + await new_message.delete() + async with aiosqlite.connect("db/welcome.db") as db: + await db.execute("UPDATE welcome SET welcome_message = ? WHERE guild_id = ?", (new_message.content, ctx.guild.id)) + await db.commit() + + embed.description = f"**Response Type:** Simple\n**Message Content:** {new_message.content}" + edit_button.disabled = True + cancel_button.disabled = True + await interaction.message.edit(embed=embed, view=view) + await ctx.send("Welcome message has been successfully updated.") + except asyncio.TimeoutError: + await ctx.send("You took too long to respond.") + except Exception as e: + await ctx.send(f"An error occurred: {e}") + + edit_button.callback = edit_button_callback + view = View() + view.add_item(edit_button) + view.add_item(VariableButton(ctx.author)) + view.add_item(cancel_button) + + await ctx.send(embed=embed, view=view) + + elif welcome_type == "embed": + embed_data_json = json.loads(embed_data) if embed_data else {} + formatted_embed_data = "\n".join( + f"{key.replace('_', ' ').title()}: {value or 'None'}" for key, value in embed_data_json.items() + ) or "None" + embed = discord.Embed( + title="Edit Welcome Message", + description=f"**Response Type:** Embed\n**Embed Data:**\n```{formatted_embed_data}```", + color=0xFF0000 + ) + + select_menu = Select( + placeholder="Select an embed field to edit", + options=[ + discord.SelectOption(label=field.replace('_', ' ').title(), value=field) + for field in embed_data_json.keys() + ] + ) + + cancel_button = Button(label="Cancel", style=discord.ButtonStyle.danger) + + async def cancel_button_callback(interaction): + nonlocal cancel_flag + if interaction.user != ctx.author: + await interaction.response.send_message("You are not authorized to cancel the setup.", ephemeral=True) + return + await interaction.response.send_message("Setup has been canceled.", ephemeral=True) + cancel_flag = True + view.clear_items() + await interaction.message.edit(embed=embed, view=view) + + cancel_button.callback = cancel_button_callback + + async def select_callback(interaction): + nonlocal cancel_flag + if interaction.user != ctx.author: + await interaction.response.send_message("You are not authorized to edit this embed.", ephemeral=True) + return + + selected_option = select_menu.values[0] + await interaction.response.defer() + + while not cancel_flag: + try: + if selected_option == "message": + await ctx.send("Enter the welcome message content:") + msg = await self.bot.wait_for("message", check=lambda m: m.author == ctx.author and m.channel == ctx.channel) + embed_data_json["message"] = msg.content + + elif selected_option == "title": + await ctx.send("Enter the embed title:") + msg = await self.bot.wait_for("message", check=lambda m: m.author == ctx.author and m.channel == ctx.channel) + embed_data_json["title"] = msg.content + + elif selected_option == "description": + await ctx.send("Enter the embed description:") + msg = await self.bot.wait_for("message", check=lambda m: m.author == ctx.author and m.channel == ctx.channel) + embed_data_json["description"] = msg.content + + elif selected_option == "color": + await ctx.send("Enter a hex color (e.g., #3498db or 3498db):") + msg = await self.bot.wait_for("message", check=lambda m: m.author == ctx.author and m.channel == ctx.channel) + color_code = msg.content.lstrip("#") + if all(c in "0123456789abcdefABCDEF" for c in color_code) and len(color_code) in {3, 6}: + embed_data_json["color"] = int(color_code.lstrip("#"), 16) + else: + await ctx.send("Invalid color code. Please enter a valid hex color.") + continue + + elif selected_option in ["footer_text", "footer_icon", "author_name", "author_icon", "thumbnail", "image"]: + await ctx.send(f"Enter the URL or text for {selected_option.replace('_', ' ')}:") + msg = await self.bot.wait_for("message", check=lambda m: m.author == ctx.author and m.channel == ctx.channel) + url_or_text = msg.content + if selected_option in ["footer_icon", "author_icon", "thumbnail", "image"]: + if url_or_text.startswith("http") or url_or_text in ["{user_avatar}", "{server_icon}"]: + embed_data_json[selected_option] = url_or_text + else: + await ctx.send("Invalid URL. Please enter a valid image URL or a supported placeholder ({user_avatar} or {server_icon}).") + continue + else: + embed_data_json[selected_option] = url_or_text + + async with aiosqlite.connect("db/welcome.db") as db: + await db.execute("UPDATE welcome SET embed_data = ? WHERE guild_id = ?", (json.dumps(embed_data_json), ctx.guild.id)) + await db.commit() + + embed.description = f"**Response Type:** Embed\n**Embed Data:**\n```{json.dumps(embed_data_json, indent=4)}```" + await interaction.message.edit(embed=embed, view=None) + await ctx.send("Embed data has been successfully updated.") + break + except asyncio.TimeoutError: + await ctx.send("You took too long to respond.") + break + except Exception as e: + await ctx.send(f"An error occurred: {e}") + break + + select_menu.callback = select_callback + view = View() + view.add_item(select_menu) + view.add_item(VariableButton(ctx.author)) + view.add_item(cancel_button) + + await ctx.send(embed=embed, view=view) + + diff --git a/bot/cogs/commands/youtube.py b/bot/cogs/commands/youtube.py new file mode 100644 index 0000000..725195e --- /dev/null +++ b/bot/cogs/commands/youtube.py @@ -0,0 +1,35 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import urllib.parse +import urllib.request +import re + +class Youtube(commands.Cog): + def __init__(self, bot): + self.bot = bot + + + @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) + search_results = re.findall(r"watch\?v=(\S{11})", html_content.read().decode()) + if len(search_results) > 0: + result_message = f"**▶ | Here are your search results:- https://www.youtube.com/watch?v={search_results[0]} **" + await ctx.send(result_message) + else: + await ctx.send('No search results found.') \ No newline at end of file diff --git a/bot/cogs/events/Errors.py b/bot/cogs/events/Errors.py new file mode 100644 index 0000000..6faadbb --- /dev/null +++ b/bot/cogs/events/Errors.py @@ -0,0 +1,122 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +import json +import aiosqlite +from discord.ext import commands +from utils.config import serverLink +from core import zyrox, Cog, Context +from utils.Tools import get_ignore_data + +class Errors(Cog): + def __init__(self, client: zyrox): + self.client = client + + @commands.Cog.listener() + async def on_command_error(self, ctx: Context, error): + if ctx.command is None: + return + + + if isinstance(error, commands.CommandNotFound): + return + + if isinstance(error, commands.MissingRequiredArgument): + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + return + + if isinstance(error, commands.CheckFailure): + data = await get_ignore_data(ctx.guild.id) + ch = data["channel"] + iuser = data["user"] + cmd = data["command"] + buser = data["bypassuser"] + + if str(ctx.author.id) in buser: + return + + if str(ctx.channel.id) in ch: + await ctx.reply(f"{ctx.author.mention} **This channel was in ignored list try my commands on other channel**.", + delete_after=8) + return + + if str(ctx.author.id) in iuser: + await ctx.reply(f"{ctx.author.mention} **You are set as an ignored user for this guild. Please try my commands in a different guild.**", delete_after=8) + return + + if ctx.command.name in cmd or any(alias in cmd for alias in ctx.command.aliases): + await ctx.reply(f"{ctx.author.mention} **This command is ignored in this guild. Please use other commands or try this command in a different guild**", delete_after=8) + return + + if isinstance(error, commands.NoPrivateMessage): + embed = discord.Embed(color=0xFF0000, description="You can't use my commands in DMs.") + embed.set_author(name=ctx.author, icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.set_thumbnail(url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.reply(embed=embed, delete_after=20) + return + + if isinstance(error, commands.TooManyArguments): + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + return + + if isinstance(error, commands.CommandOnCooldown): + embed = discord.Embed(color=0xFF0000, description=f"**{ctx.author.mention} Couldown is here Bro Tryy commands in {error.retry_after:.2f} seconds**.") + embed.set_author(name="Cooldown", icon_url=self.client.user.avatar.url) + + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.reply(embed=embed, delete_after=10) + return + + if isinstance(error, commands.MaxConcurrencyReached): + embed = discord.Embed(color=0xFF0000, description=f"{ctx.author.mention} This command is already in progress. Please let it finish and try again afterward.") + embed.set_author(name="Command in Progress.", icon_url=self.client.user.avatar.url) + + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.reply(embed=embed, delete_after=10) + ctx.command.reset_cooldown(ctx) + return + + if isinstance(error, commands.MissingPermissions): + missing = [perm.replace("_", " ").replace("guild", "server").title() for perm in error.missing_permissions] + fmt = "{}, and {}".format(", ".join(missing[:-1]), missing[-1]) if len(missing) > 2 else " and ".join(missing) + embed = discord.Embed(color=0xFF0000, description=f"**Ops! You don't have {fmt} Permission to run the {ctx.command.name} command!**") + embed.set_author(name="Missing Permissions", icon_url=self.client.user.avatar.url) + + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.reply(embed=embed, delete_after=7) + ctx.command.reset_cooldown(ctx) + return + + if isinstance(error, commands.BadArgument): + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + return + + if isinstance(error, commands.BotMissingPermissions): + missing = ", ".join(error.missing_permissions) + await ctx.reply(f'** Huh! I need {missing} Permission to run the {ctx.command.qualified_name}command! Give me {missing} Permission**', delete_after=7) + return + + if isinstance(error, discord.HTTPException): + print(f"[ERROR] HTTPException in {ctx.command}: {error}") + return + + if isinstance(error, commands.CommandInvokeError): + print(f"[ERROR] CommandInvokeError in {ctx.command}: {error}") + print(f" Original: {error.original}") + return + diff --git a/bot/cogs/events/ai.py b/bot/cogs/events/ai.py new file mode 100644 index 0000000..8d2716e --- /dev/null +++ b/bot/cogs/events/ai.py @@ -0,0 +1,27 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands + +class AIResponses(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.Cog.listener() + async def on_ready(self): + pass + +async def setup(bot): + await bot.add_cog(AIResponses(bot)) \ No newline at end of file diff --git a/bot/cogs/events/auto.py b/bot/cogs/events/auto.py new file mode 100644 index 0000000..ee0d183 --- /dev/null +++ b/bot/cogs/events/auto.py @@ -0,0 +1,52 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ARROWRED, ZMODULE +from discord.utils import * +from core import zyrox, Cog +from utils.Tools import * +from utils.config import BotName, serverLink +from discord.ext import commands +from discord.ui import Button, View + +class Autorole(Cog): + def __init__(self, bot: zyrox): + self.bot = bot + + + @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/codexdev)**", + 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/codexdev') + 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: + await entry.user.send(embed=embed, view=view) + except Exception as e: + print(e) diff --git a/bot/cogs/events/autoblacklist.py b/bot/cogs/events/autoblacklist.py new file mode 100644 index 0000000..7d55c0a --- /dev/null +++ b/bot/cogs/events/autoblacklist.py @@ -0,0 +1,161 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ZWARNING +from core import zyrox, Cog +from discord.ext import commands +import aiosqlite +from datetime import datetime, timedelta + +class AutoBlacklist(Cog): + def __init__(self, client: zyrox): + self.client = client + self.spam_cd_mapping = commands.CooldownMapping.from_cooldown(5, 5, commands.BucketType.member) + self.spam_command_mapping = commands.CooldownMapping.from_cooldown(6, 10, commands.BucketType.member) + self.last_spam = {} + self.spam_threshold = 5 + self.spam_window = timedelta(minutes=10) + self.db_path = 'db/block.db' + self.bot_user_id = self.client.user.id if self.client.user else None + self.guild_command_tracking = {} + + async def add_to_blacklist(self, user_id=None, guild_id=None, channel=None): + try: + async with aiosqlite.connect(self.db_path) as db: + timestamp = datetime.utcnow() + if guild_id: + await db.execute(''' + INSERT OR IGNORE INTO guild_blacklist (guild_id, timestamp) VALUES (?, ?) + ''', (guild_id, timestamp)) + if channel: + embed = discord.Embed( + title=f"{ZWARNING} Guild Blacklisted", + description=( + f"This guild has been blacklisted due to spamming or automation. " + f"If you believe this is a mistake, please contact our [Support Server](https://discord.gg/codexdev) with any proof if possible." + ), + color=0xFF0000 + ) + await channel.send(embed=embed) + elif user_id: + await db.execute(''' + INSERT OR IGNORE INTO user_blacklist (user_id, timestamp) VALUES (?, ?) + ''', (user_id, timestamp)) + await db.commit() + except aiosqlite.Error as e: + print(f"Database error: {e}") + + async def check_and_blacklist_guild(self, guild_id): + async with aiosqlite.connect(self.db_path) as db: + async with db.execute( + ''' + SELECT COUNT(DISTINCT user_id) FROM user_blacklist + WHERE timestamp >= ? + ''', + (datetime.utcnow() - self.spam_window,) + ) as cursor: + count = await cursor.fetchone() + if count[0] >= self.spam_threshold: + async with db.execute('SELECT channel_id FROM guild_settings WHERE guild_id = ?', (guild_id,)) as cursor: + channel_id = await cursor.fetchone() + if channel_id: + channel = self.client.get_channel(channel_id[0]) + if channel: + await self.add_to_blacklist(None, guild_id, channel) + + @commands.Cog.listener() + async def on_message(self, message): + if message.author.bot: + return + + + guild_id = message.guild.id if message.guild else None + if guild_id: + if guild_id not in self.guild_command_tracking: + self.guild_command_tracking[guild_id] = [] + + + self.guild_command_tracking[guild_id].append(datetime.utcnow()) + + + self.guild_command_tracking[guild_id] = [ + timestamp for timestamp in self.guild_command_tracking[guild_id] if timestamp >= datetime.utcnow() - timedelta(seconds=2) + ] + + + if len(self.guild_command_tracking[guild_id]) > 8: + + await self.add_to_blacklist(guild_id=guild_id, channel=message.channel) + embed = discord.Embed( + title=f"{ZWARNING} Guild Blacklisted", + description=( + f"The guild has been blacklisted for excessive command usage. " + f"If you believe this is a mistake, please contact our [Support Server](https://discord.gg/codexdev)." + ), + color=0xFF0000 + ) + await message.channel.send(embed=embed) + return + + + bucket = self.spam_cd_mapping.get_bucket(message) + retry = bucket.update_rate_limit() + + if retry: + async with aiosqlite.connect(self.db_path) as db: + async with db.execute('SELECT user_id FROM user_blacklist WHERE user_id = ?', (message.author.id,)) as cursor: + if await cursor.fetchone(): + return + + if message.content in (f'<@{self.bot_user_id}>', f'<@!{self.bot_user_id}>'): + await self.add_to_blacklist(user_id=message.author.id) + embed = discord.Embed( + title=f"{ZWARNING} User Blacklisted", + description=f"**{message.author.mention} has been blacklisted for repeatedly mentioning me. If you believe this is a mistake, please contact our [Support Server](https://discord.gg/codexdev) with any proof if possible.**", + color=0xFF0000 + ) + await message.channel.send(embed=embed) + return + + if message.guild: + if message.author.id not in self.last_spam: + self.last_spam[message.author.id] = [] + self.last_spam[message.author.id].append(datetime.utcnow()) + recent_spam = [timestamp for timestamp in self.last_spam.get(message.author.id, []) if timestamp >= datetime.utcnow() - self.spam_window] + self.last_spam[message.author.id] = recent_spam + if len(recent_spam) >= self.spam_threshold: + await self.check_and_blacklist_guild(message.guild.id) + + @commands.Cog.listener() + async def on_command(self, ctx): + if ctx.author.bot: + return + + bucket = self.spam_command_mapping.get_bucket(ctx.message) + retry = bucket.update_rate_limit() + + if retry: + async with aiosqlite.connect(self.db_path) as db: + async with db.execute('SELECT user_id FROM user_blacklist WHERE user_id = ?', (ctx.author.id,)) as cursor: + if await cursor.fetchone(): + return + + await self.add_to_blacklist(user_id=ctx.author.id) + embed = discord.Embed( + title=f"{ZWARNING} User Blacklisted", + description=f"**{ctx.author.mention} has been blacklisted for spamming commands. If you believe this is a mistake, please contact our [Support Server](https://discord.gg/codexdev) with any proof if possible.**", + color=0xFF0000 + ) + await ctx.reply(embed=embed) diff --git a/bot/cogs/events/autoreact.py b/bot/cogs/events/autoreact.py new file mode 100644 index 0000000..c89ae1f --- /dev/null +++ b/bot/cogs/events/autoreact.py @@ -0,0 +1,72 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from discord.ext import commands +import aiosqlite +import re +import asyncio + +class AutoReactListener(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.db_path = 'db/autoreact.db' + self.rate_limited_users = set() + + async def get_triggers(self, guild_id): + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute("SELECT trigger, emojis FROM autoreact WHERE guild_id = ?", (guild_id,)) + return await cursor.fetchall() + + async def add_rate_limit(self, user_id): + self.rate_limited_users.add(user_id) + await asyncio.sleep(5) + self.rate_limited_users.remove(user_id) + + @commands.Cog.listener() + async def on_message(self, message): + if message.author.bot or not message.guild: + return + + if message.author.id in self.rate_limited_users: + return + + triggers = await self.get_triggers(message.guild.id) + if not triggers: + return + + content = message.content.strip().lower() + for trigger, emojis in triggers: + if content == trigger: + emoji_list = emojis.split() + for emoji in emoji_list: + try: + + if re.match(r"", emoji): + emoji_obj = discord.PartialEmoji.from_str(emoji) + else: + + emoji_obj = emoji + + await message.add_reaction(emoji_obj) + except discord.errors.NotFound: + continue + except discord.errors.Forbidden: + continue + except discord.errors.HTTPException: + continue + + await self.add_rate_limit(message.author.id) + break + diff --git a/bot/cogs/events/autorole.py b/bot/cogs/events/autorole.py new file mode 100644 index 0000000..9be6a66 --- /dev/null +++ b/bot/cogs/events/autorole.py @@ -0,0 +1,76 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +import aiohttp +import aiosqlite +import asyncio +import logging +from discord.ext import commands +from core import zyrox, Cog +from utils.config import * + +DATABASE_PATH = 'db/autorole.db' +logger = logging.getLogger(__name__) + +class Autorole2(Cog): + def __init__(self, bot: zyrox): + self.bot = bot + self.headers = {"Authorization": f"Bot {self.bot.http.token}"} + + async def get_autorole(self, guild_id: int): + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute("SELECT bots, humans FROM autorole WHERE guild_id = ?", (guild_id,)) as cursor: + row = await cursor.fetchone() + if row: + bots, humans = row + bots = [int(role_id) for role_id in bots.replace('[', '').replace(']', '').replace(' ', '').split(',') if role_id] + humans = [int(role_id) for role_id in humans.replace('[', '').replace(']', '').split(',') if role_id] + return {"bots": bots, "humans": humans} + else: + return {"bots": [], "humans": []} + + @commands.Cog.listener() + async def on_member_join(self, member): + data = await self.get_autorole(member.guild.id) + bot_roles = data["bots"] + human_roles = data["humans"] + + if member.bot: + roles_to_add = bot_roles + else: + roles_to_add = human_roles + + for role_id in roles_to_add: + role = member.guild.get_role(role_id) + if role: + try: + await member.add_roles(role, reason=f"{BRAND_NAME} | Autoroles") + except discord.Forbidden: + print(f"Bot lacks permissions to add role in a guild during Autorole Event .") + except discord.HTTPException as e: + if e.status == 429: + retry_after = e.response.headers.get('Retry-After') + if retry_after: + retry_after = float(retry_after) + print(f"(Autorole) Rate limit encountered. Retrying after {retry_after} seconds.") + await asyncio.sleep(retry_after) + await member.add_roles(role, reason=f"{BRAND_NAME} | Autoroles") + except discord.errors.RateLimited as e: + print(f"Rate limit encountered: {e}. Retrying in {e.retry_after} seconds.") + await asyncio.sleep(e.retry_after) + await member.add_roles(role, reason=f"{BRAND_NAME} | Autoroles") + except Exception as e: + logger.error(f"Unexpected error in Autorole: {e}") + diff --git a/bot/cogs/events/greet2.py b/bot/cogs/events/greet2.py new file mode 100644 index 0000000..c2f616b --- /dev/null +++ b/bot/cogs/events/greet2.py @@ -0,0 +1,115 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +import aiosqlite +import json +import re +import asyncio +from discord.ext import commands + +class greet(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.join_queue = {} + self.processing = set() + + async def safe_format(self, text, placeholders): + placeholders_lower = {k.lower(): v for k, v in placeholders.items()} + def replace_var(match): + var_name = match.group(1).lower() + return str(placeholders_lower.get(var_name, f"{{{var_name}}}")) + return re.sub(r"\{(\w+)\}", replace_var, text or "") + + @commands.Cog.listener() + async def on_member_join(self, member): + if member.guild.id not in self.join_queue: + self.join_queue[member.guild.id] = [] + self.join_queue[member.guild.id].append(member) + if member.guild.id not in self.processing: + self.processing.add(member.guild.id) + await self.process_queue(member.guild) + + async def process_queue(self, guild): + while self.join_queue[guild.id]: + member = self.join_queue[guild.id].pop(0) + async with aiosqlite.connect("db/welcome.db") as db: + async with db.execute("SELECT welcome_type, welcome_message, channel_id, embed_data, auto_delete_duration FROM welcome WHERE guild_id = ?", (guild.id,)) as cursor: + row = await cursor.fetchone() + if row is None: + continue + welcome_type, welcome_message, channel_id, embed_data, auto_delete_duration = row + welcome_channel = self.bot.get_channel(channel_id) + if not welcome_channel: + continue + placeholders = { + "user": member.mention, + "user_avatar": member.avatar.url if member.avatar else member.default_avatar.url, + "user_name": member.name, + "user_id": member.id, + "user_nick": member.display_name, + "user_joindate": member.joined_at.strftime("%a, %b %d, %Y"), + "user_createdate": member.created_at.strftime("%a, %b %d, %Y"), + "server_name": guild.name, + "server_id": guild.id, + "server_membercount": guild.member_count, + "server_icon": guild.icon.url if guild.icon else "https://cdn.discordapp.com/embed/avatars/0.png", + "timestamp": discord.utils.format_dt(discord.utils.utcnow()) + } + try: + if welcome_type == "simple" and welcome_message: + content = await self.safe_format(welcome_message, placeholders) + sent_message = await welcome_channel.send(content=content) + elif welcome_type == "embed" and embed_data: + embed_info = json.loads(embed_data) + color_value = embed_info.get("color", None) + embed_color = 0x2f3136 + if color_value and isinstance(color_value, str) and color_value.startswith("#"): + embed_color = discord.Color(int(color_value.lstrip("#"), 16)) + elif isinstance(color_value, int): + embed_color = discord.Color(color_value) + content = await self.safe_format(embed_info.get("message", ""), placeholders) or None + embed = discord.Embed( + title=await self.safe_format(embed_info.get("title", ""), placeholders), + description=await self.safe_format(embed_info.get("description", ""), placeholders), + color=embed_color + ) + embed.timestamp = discord.utils.utcnow() + if embed_info.get("footer_text"): + embed.set_footer( + text=await self.safe_format(embed_info["footer_text"], placeholders), + icon_url=await self.safe_format(embed_info.get("footer_icon", ""), placeholders) + ) + if embed_info.get("author_name"): + embed.set_author( + name=await self.safe_format(embed_info["author_name"], placeholders), + icon_url=await self.safe_format(embed_info.get("author_icon", ""), placeholders) + ) + if embed_info.get("thumbnail"): + embed.set_thumbnail(url=await self.safe_format(embed_info["thumbnail"], placeholders)) + if embed_info.get("image"): + embed.set_image(url=await self.safe_format(embed_info["image"], placeholders)) + sent_message = await welcome_channel.send(content=content, embed=embed) + if auto_delete_duration: + await sent_message.delete(delay=auto_delete_duration) + except discord.Forbidden: + continue + except discord.HTTPException as e: + if e.code == 50035 or e.status == 429: + await asyncio.sleep(1) + self.join_queue[guild.id].append(member) + continue + await asyncio.sleep(2) + self.processing.remove(guild.id) + diff --git a/bot/cogs/events/mention.py b/bot/cogs/events/mention.py new file mode 100644 index 0000000..8600952 --- /dev/null +++ b/bot/cogs/events/mention.py @@ -0,0 +1,155 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from utils import getConfig +from utils.config import BotName +import discord +from utils.emoji import ARROWRED, CODEBASE, HEART3, INDEX, ZYROXLINKS +from discord.ui import LayoutView, TextDisplay, Separator, Container, ActionRow, Select +from discord.ext import commands +from utils.Tools import get_ignore_data +import aiosqlite + + +class MentionSelectView(LayoutView): + def __init__(self, message, bot, prefix): + super().__init__(timeout=300) + self.message = message + self.bot = bot + self.prefix = prefix + + self.select = Select( + placeholder=f"Start With {BotName}", + options=[ + discord.SelectOption( + label="Home", + emoji=INDEX, + description="Go to the main menu", + ), + discord.SelectOption( + label="Developer Info", + emoji=CODEBASE, + description="See who created me", + ), + discord.SelectOption( + label="Links", + emoji=ZYROXLINKS, + description="Useful bot links", + ), + ], + ) + self.select.callback = self.on_select + + self.add_item( + Container( + TextDisplay(f"**{message.guild.name}**"), + Separator(visible=True), + TextDisplay( + f"> {HEART3} **Hey {message.author.mention}**\n" + f"> {ARROWRED} **Prefix For This Server: `{prefix}`**\n\n" + f"___Type `{prefix}help` for more information.___" + ), + ActionRow(self.select), + ) + ) + + async def on_select(self, interaction: discord.Interaction): + if interaction.user.id != self.message.author.id: + await interaction.response.send_message( + "This menu is not for you!", ephemeral=True + ) + return + + selected = interaction.data.get("values", ["Home"])[0] + + if selected == "Home": + content = ( + f"> {HEART3} **Hey {interaction.user.mention}**\n" + f"> {ARROWRED} **Prefix For This Server: `{self.prefix}`**\n\n" + f"___Type `{self.prefix}help` for more information.___" + ) + elif selected == "Developer Info": + content = ( + "There are only 2 Founders Who Created Me. Thanks You To Them 💞.\n\n" + "**The Founder**\n" + "**[01]. [Ray](https://discord.com/users/870179991462236170)**\n**[02]. [runxking](https://discord.com/users/767979794411028491)**" + ) + elif selected == "Links": + content = ( + f"**[Invite {BotName}](https://discord.com/oauth2/authorize?client_id=1396114795102470196)**\n" + "**[Join Support Server](https://discord.gg/codexdev)**" + ) + + new_container = Container( + TextDisplay(f"**{self.message.guild.name}**"), + Separator(visible=True), + TextDisplay(content), + ActionRow(self.select), + ) + + self.clear_items() + self.add_item(new_container) + + await interaction.response.edit_message(view=self) + + +class Mention(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.color = 0xFF0000 + self.bot_name = BotName + + async def is_blacklisted(self, message): + async with aiosqlite.connect("db/block.db") as db: + cursor = await db.execute( + "SELECT 1 FROM guild_blacklist WHERE guild_id = ?", (message.guild.id,) + ) + if await cursor.fetchone(): + return True + cursor = await db.execute( + "SELECT 1 FROM user_blacklist WHERE user_id = ?", (message.author.id,) + ) + if await cursor.fetchone(): + return True + return False + + @commands.Cog.listener() + async def on_message(self, message: discord.Message): + if message.author.bot or not message.guild: + return + + if await self.is_blacklisted(message): + return + + ignore_data = await get_ignore_data(message.guild.id) + if ( + str(message.author.id) in ignore_data["user"] + or str(message.channel.id) in ignore_data["channel"] + ): + return + + if ( + self.bot.user in message.mentions + and len(message.content.strip().split()) == 1 + ): + guild_id = message.guild.id + data = await getConfig(guild_id) + prefix = data["prefix"] + + view = MentionSelectView(message, self.bot, prefix) + await message.channel.send(view=view) + + +def setup(bot): + bot.add_cog(Mention(bot)) diff --git a/bot/cogs/events/mentionold.txt b/bot/cogs/events/mentionold.txt new file mode 100644 index 0000000..265c431 --- /dev/null +++ b/bot/cogs/events/mentionold.txt @@ -0,0 +1,67 @@ +from utils import getConfig +import discord +from utils.emoji import ARROWRED, HEART3 +from discord.ext import commands +from utils.Tools import get_ignore_data +import aiosqlite + +class Mention(commands.Cog): + + def __init__(self, bot): + self.bot = bot + self.color = 0xFF0000 + self.bot_name = "Zyrox X" + + async def is_blacklisted(self, message): + async with aiosqlite.connect("db/block.db") as db: + cursor = await db.execute("SELECT 1 FROM guild_blacklist WHERE guild_id = ?", (message.guild.id,)) + if await cursor.fetchone(): + return True + + cursor = await db.execute("SELECT 1 FROM user_blacklist WHERE user_id = ?", (message.author.id,)) + if await cursor.fetchone(): + return True + + return False + + @commands.Cog.listener() + async def on_message(self, message): + if message.author.bot or not message.guild: + return + + if await self.is_blacklisted(message): + return + + ignore_data = await get_ignore_data(message.guild.id) + if str(message.author.id) in ignore_data["user"] or str(message.channel.id) in ignore_data["channel"]: + return + + if message.reference and message.reference.resolved: + if isinstance(message.reference.resolved, discord.Message): + if message.reference.resolved.author.id == self.bot.user.id: + return + + guild_id = message.guild.id + data = await getConfig(guild_id) + prefix = data["prefix"] + + if self.bot.user in message.mentions: + if len(message.content.strip().split()) == 1: + embed = discord.Embed( + title=f"{message.guild.name}", + color=self.color, + description=f"> {HEART3} **Hey {message.author.mention}**\n> {ARROWRED} **Prefix For This Server `{prefix}`**\n\n___Type `{prefix}help` for more information.___" + ) + embed.set_thumbnail(url=self.bot.user.avatar.url) + embed.set_footer(text="Powered by Zyrox Development™", icon_url=self.bot.user.avatar.url) + + buttons = [ + discord.ui.Button(label="Invite", style=discord.ButtonStyle.link, url="https://discord.com/oauth2/authorize?client_id=1396114795102470196&permissions=8&integration_type=0&scope=bot+applications.commands"), + discord.ui.Button(label="Support", style=discord.ButtonStyle.link, url="https://discord.gg/codexdev"), + ] + + view = discord.ui.View() + for button in buttons: + view.add_item(button) + + await message.channel.send(embed=embed, view=view) diff --git a/bot/cogs/events/on_guild.py b/bot/cogs/events/on_guild.py new file mode 100644 index 0000000..60e146a --- /dev/null +++ b/bot/cogs/events/on_guild.py @@ -0,0 +1,223 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from discord.ext import commands +from core import zyrox, Cog +import discord +from utils.emoji import ARROWRED, KING, ZBOT, ZHUMAN, ZROCKET +import logging +from discord.ui import View, Button, Select +from utils.config import * + +logging.basicConfig( + level=logging.INFO, + format="\x1b[38;5;197m[\x1b[0m%(asctime)s\x1b[38;5;197m]\x1b[0m -> \x1b[38;5;197m%(message)s\x1b[0m", + datefmt="%H:%M:%S", +) + +client = zyrox() + + +class Guild(Cog): + def __init__(self, client: zyrox): + self.client = client + self.recently_removed_guilds = set() + self._removal_timestamps = {} + + @client.event + @commands.Cog.listener(name="on_guild_join") + async def on_guild_add(self, guild): + try: + rope = [ + inv + for inv in await guild.invites() + if inv.max_age == 0 and inv.max_uses == 0 + ] + ch = LOG_CHANNEL_ID + if not ch: + return + me = self.client.get_channel(ch) + if me is None: + logging.error(f"Channel with ID {ch} not found.") + return + + channels = len(set(self.client.get_all_channels())) + embed = discord.Embed(title=f"{guild.name}'s Information", color=0xFF0000) + + embed.set_author(name="Guild Joined") + embed.set_footer(text=f"Added in {guild.name}") + + embed.add_field( + name="**__About__**", + value=f"**Name : ** {guild.name}\n**ID :** {guild.id}\n**Owner {KING} :** {guild.owner} (<@{guild.owner_id}>)\n**Created At : **{guild.created_at.month}/{guild.created_at.day}/{guild.created_at.year}\n**Members :** {len(guild.members)}", + inline=False, + ) + embed.add_field( + name="**__Description__**", + value=f"""{guild.description}""", + inline=False, + ) + embed.add_field( + name="**__Members__**", + value=f"""{ZROCKET} Members : {len(guild.members)}\n {ZHUMAN} Humans : {len(list(filter(lambda m: not m.bot, guild.members)))}\n {ZBOT} Bots : {len(list(filter(lambda m: m.bot, guild.members)))} + """, + inline=False, + ) + embed.add_field( + name="**__Channels__**", + value=f""" +Categories : {len(guild.categories)} +Text Channels : {len(guild.text_channels)} +Voice Channels : {len(guild.voice_channels)} +Threads : {len(guild.threads)} + """, + inline=False, + ) + embed.add_field( + name="__Bot Stats:__", + value=f"Servers: `{len(self.client.guilds)}`\nUsers: `{len(self.client.users)}`\nChannels: `{channels}`", + inline=False, + ) + + if guild.icon is not None: + embed.set_thumbnail(url=guild.icon.url) + + embed.timestamp = discord.utils.utcnow() + await me.send( + f"{rope[0]}" if rope else "No Pre-Made Invite Found", embed=embed + ) + + if not guild.chunked: + 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/codexdev)**", + color=0xFF0000, + ) + embed.set_author( + name="Thanks for adding me!", icon_url=guild.me.display_avatar.url + ) + embed.set_footer( + text=f"Powered by {BRAND_NAME}™", + ) + if guild.icon: + embed.set_thumbnail(url=guild.icon.url) + + support = Button( + label="Support", + style=discord.ButtonStyle.link, + url=f"https://discord.gg/codexdev", + ) + + view = View() + view.add_item(support) + channel = discord.utils.get(guild.text_channels, name="general") + if not channel: + channels = [ + channel + for channel in guild.text_channels + if channel.permissions_for(guild.me).send_messages + ] + if channels: + channel = channels[0] + else: + logging.error( + f"No channel found with send permissions in guild: {guild.name}" + ) + return + + await channel.send(embed=embed, view=view) + + except Exception as e: + logging.error(f"Error in on_guild_join: {e}") + + @client.event + @commands.Cog.listener(name="on_guild_remove") + async def on_guild_remove(self, guild): + import time + + current_time = time.time() + + if guild.id in self.recently_removed_guilds: + last_removal = self._removal_timestamps.get(guild.id, 0) + if current_time - last_removal < 60: + return + + self.recently_removed_guilds.add(guild.id) + self._removal_timestamps[guild.id] = current_time + + if len(self.recently_removed_guilds) > 100: + self.recently_removed_guilds.clear() + + try: + ch = LOG_CHANNEL_ID + if not ch: + return + idk = self.client.get_channel(ch) + if idk is None: + logging.error(f"Channel with ID {ch} not found.") + return + + channels = len(set(self.client.get_all_channels())) + embed = discord.Embed(title=f"{guild.name}'s Information", color=0xFF0000) + + embed.set_author(name="Guild Removed") + embed.set_footer(text=f"{guild.name}") + + embed.add_field( + name="**__About__**", + value=f"**Name : ** {guild.name}\n**ID :** {guild.id}\n**Owner {KING} :** {guild.owner} (<@{guild.owner_id}>)\n**Created At : **{guild.created_at.month}/{guild.created_at.day}/{guild.created_at.year}\n**Members :** {len(guild.members)}", + inline=False, + ) + embed.add_field( + name="**__Description__**", + value=f"""{guild.description}""", + inline=False, + ) + + embed.add_field( + name="**__Members__**", + value=f""" +Members : {len(guild.members)} +Humans : {len(list(filter(lambda m: not m.bot, guild.members)))} +Bots : {len(list(filter(lambda m: m.bot, guild.members)))} + """, + inline=False, + ) + embed.add_field( + name="**__Channels__**", + value=f""" +Categories : {len(guild.categories)} +Text Channels : {len(guild.text_channels)} +Voice Channels : {len(guild.voice_channels)} +Threads : {len(guild.threads)} + """, + inline=False, + ) + embed.add_field( + name="__Bot Stats:__", + value=f"Servers: `{len(self.client.guilds)}`\nUsers: `{len(self.client.users)}`\nChannels: `{channels}`", + inline=False, + ) + + if guild.icon is not None: + embed.set_thumbnail(url=guild.icon.url) + + embed.timestamp = discord.utils.utcnow() + await idk.send(embed=embed) + except Exception as e: + logging.error(f"Error in on_guild_remove: {e}") + + +# client.add_cog(Guild(client)) diff --git a/bot/cogs/events/react.py b/bot/cogs/events/react.py new file mode 100644 index 0000000..fde4715 --- /dev/null +++ b/bot/cogs/events/react.py @@ -0,0 +1,57 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ACTIVE_DEVELOPER, BLACKCROWN, MINGLE, STAFF +from discord.ext import commands +from utils.config import OWNER_IDS +import asyncio + +class React(commands.Cog): + + def __init__(self, bot): + self.bot = bot + + @commands.Cog.listener() + async def on_message(self, message): + if message.author.bot: + return + for owner in self.bot.owner_ids: + if f"<@{owner}>" in message.content: + try: + # Primary owner (first in OWNER_IDS) gets an extra emoji + if owner == OWNER_IDS[0]: + emojis = [ + BLACKCROWN, + ACTIVE_DEVELOPER, + STAFF, + MINGLE, + ] + else: + emojis = [ + BLACKCROWN, + ACTIVE_DEVELOPER, + STAFF, + ] + + for emoji in emojis: + try: + await message.add_reaction(emoji) + except discord.HTTPException: + pass # ignore if emoji is invalid or not accessible + + except discord.errors.RateLimited as e: + await asyncio.sleep(e.retry_after) + except Exception as e: + print(f"An unexpected error occurred Auto react owner mention: {e}") \ No newline at end of file diff --git a/bot/cogs/events/stickymessage.py b/bot/cogs/events/stickymessage.py new file mode 100644 index 0000000..7a525e4 --- /dev/null +++ b/bot/cogs/events/stickymessage.py @@ -0,0 +1,208 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +import aiosqlite +import json +import asyncio +from discord.ext import commands +from utils.config import * + +class StickyMessageListener(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.processing_channels = set() + self.last_processed = {} + + @commands.Cog.listener() + async def on_message(self, message): + + if message.author.bot: + return + + if not hasattr(message, "guild") or message.guild is None: + return + + if message.channel.id in self.processing_channels: + return + + async with aiosqlite.connect("db/stickymessages.db") as db: + cursor = await db.execute(""" + SELECT id, message_type, message_content, embed_data, last_message_id, + enabled, delay_seconds, auto_delete_after, ignore_bots, + ignore_commands, trigger_count, current_count + FROM sticky_messages + WHERE guild_id = ? AND channel_id = ? AND enabled = 1 + """, (message.guild.id, message.channel.id)) + sticky_data = await cursor.fetchone() + + if not sticky_data: + return + + ( + sticky_id, msg_type, msg_content, embed_data, last_msg_id, + enabled, delay_seconds, auto_delete_after, ignore_bots, + ignore_commands, trigger_count, current_count + ) = sticky_data + + if ignore_bots and message.author.bot: + return + + if ignore_commands and message.content.startswith(await self.get_prefix(message)): + return + + self.processing_channels.add(message.channel.id) + + try: + new_count = current_count + 1 + + if new_count >= trigger_count: + await self.update_counter(message.guild.id, message.channel.id, 0) + + if last_msg_id: + try: + old_message = await message.channel.fetch_message(last_msg_id) + await old_message.delete() + except (discord.NotFound, discord.Forbidden, discord.HTTPException): + pass + + await asyncio.sleep(delay_seconds) + + new_sticky_msg = await self.send_sticky_message( + message.channel, msg_type, msg_content, embed_data + ) + + if new_sticky_msg: + async with aiosqlite.connect("db/stickymessages.db") as db: + await db.execute(""" + UPDATE sticky_messages + SET last_message_id = ? + WHERE guild_id = ? AND channel_id = ? + """, (new_sticky_msg.id, message.guild.id, message.channel.id)) + await db.commit() + + if auto_delete_after > 0: + asyncio.create_task( + self.auto_delete_message(new_sticky_msg, auto_delete_after) + ) + else: + await self.update_counter(message.guild.id, message.channel.id, new_count) + + except Exception: + pass + finally: + self.processing_channels.discard(message.channel.id) + + async def get_prefix(self, message): + try: + async with aiosqlite.connect("db/prefix.db") as db: + cursor = await db.execute( + "SELECT prefix FROM prefix WHERE guild_id = ?", + (message.guild.id,) + ) + result = await cursor.fetchone() + return result[0] if result else "!" + except: + return "!" + + async def update_counter(self, guild_id, channel_id, new_count): + async with aiosqlite.connect("db/stickymessages.db") as db: + await db.execute(""" + UPDATE sticky_messages + SET current_count = ? + WHERE guild_id = ? AND channel_id = ? + """, (new_count, guild_id, channel_id)) + await db.commit() + + async def send_sticky_message(self, channel, msg_type, msg_content, embed_data): + try: + if msg_type == "plain" and msg_content: + return await channel.send(content=msg_content) + + elif msg_type == "embed" and embed_data: + try: + data = json.loads(embed_data) + embed = discord.Embed(color=discord.Color.red()) + + if data.get("title"): + embed.title = data["title"] + + if data.get("description"): + embed.description = data["description"] + + if data.get("color"): + try: + color_str = data["color"] + if color_str.startswith("#"): + embed.color = discord.Color( + int(color_str.lstrip("#"), 16) + ) + except: + embed.color = discord.Color.red() + + if data.get("footer"): + embed.set_footer(text=data["footer"]) + else: + embed.set_footer(text=f"{BRAND_NAME} Development") + + embed.timestamp = discord.utils.utcnow() + return await channel.send(embed=embed) + + except json.JSONDecodeError: + return await channel.send(content="*[Embed data corrupted]*") + + except (discord.Forbidden, discord.HTTPException): + pass + except Exception: + pass + + return None + + async def auto_delete_message(self, message, delay): + try: + await asyncio.sleep(delay) + await message.delete() + except (discord.NotFound, discord.Forbidden, discord.HTTPException): + pass + + @commands.Cog.listener() + async def on_guild_channel_delete(self, channel): + try: + async with aiosqlite.connect("db/stickymessages.db") as db: + await db.execute( + "DELETE FROM sticky_messages WHERE channel_id = ?", + (channel.id,) + ) + await db.commit() + except: + pass + + @commands.Cog.listener() + async def on_guild_remove(self, guild): + try: + async with aiosqlite.connect("db/stickymessages.db") as db: + await db.execute( + "DELETE FROM sticky_messages WHERE guild_id = ?", + (guild.id,) + ) + await db.execute( + "DELETE FROM sticky_settings WHERE guild_id = ?", + (guild.id,) + ) + await db.commit() + except: + pass + +async def setup(bot): + await bot.add_cog(StickyMessageListener(bot)) \ No newline at end of file diff --git a/bot/cogs/moderation/Moderation.tar b/bot/cogs/moderation/Moderation.tar new file mode 100644 index 0000000..5813b6f Binary files /dev/null and b/bot/cogs/moderation/Moderation.tar differ diff --git a/bot/cogs/moderation/ban.py b/bot/cogs/moderation/ban.py new file mode 100644 index 0000000..4df923f --- /dev/null +++ b/bot/cogs/moderation/ban.py @@ -0,0 +1,132 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import TICK, ZWARNING +from discord.ext import commands +from discord import ui +from utils.Tools import * + +class Ban(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.color = discord.Color.from_rgb(255, 0, 0) + + def get_user_avatar(self, user): + return user.avatar.url if user.avatar else user.default_avatar.url + + @commands.hybrid_command( + name="ban", + help="Bans a user from the Server", + usage="ban ", + aliases=["fuckban", "hackban","kuttaban"]) + @blacklist_check() + @ignore_check() + @top_check() + @commands.cooldown(1, 10, commands.BucketType.member) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(ban_members=True) + @commands.bot_has_permissions(ban_members=True) + async def ban(self, ctx, user: discord.User, *, reason=None): + + member = ctx.guild.get_member(user.id) + if not member: + try: + user = await self.bot.fetch_user(user.id) + except discord.NotFound: + # User not found container + container = ui.Container() + container.add_item(ui.TextDisplay(f"❌ **User Not Found**\nUser with ID {user.id} not found.")) + view = ui.LayoutView() + view.add_item(container) + await ctx.send(view=view) + return + + bans = [entry async for entry in ctx.guild.bans()] + if any(ban_entry.user.id == user.id for ban_entry in bans): + # Already banned container + container = ui.Container() + container.add_item(ui.TextDisplay(f"⚠️ **{user.name} is Already Banned!**")) + container.add_item(ui.TextDisplay("**Requested User is already banned in this server.**")) + container.add_item(ui.TextDisplay(f"*Requested by {ctx.author}*")) + view = ui.LayoutView() + view.add_item(container) + await ctx.send(view=view) + return + + if member == ctx.guild.owner: + # Server owner error container + container = ui.Container() + container.add_item(ui.TextDisplay("❌ **Error Banning User**")) + container.add_item(ui.TextDisplay("I can't ban the Server Owner!")) + container.add_item(ui.TextDisplay(f"*Requested by {ctx.author}*")) + view = ui.LayoutView() + view.add_item(container) + return await ctx.send(view=view) + + if isinstance(member, discord.Member) and member.top_role >= ctx.guild.me.top_role: + # Role hierarchy error container + container = ui.Container() + container.add_item(ui.TextDisplay("❌ **Error Banning User**")) + container.add_item(ui.TextDisplay("I can't ban a user with a higher or equal role!")) + container.add_item(ui.TextDisplay(f"*Requested by {ctx.author}*")) + view = ui.LayoutView() + view.add_item(container) + return await ctx.send(view=view) + + if isinstance(member, discord.Member): + if ctx.author != ctx.guild.owner: + if member.top_role >= ctx.author.top_role: + # Author role hierarchy error container + container = ui.Container() + container.add_item(ui.TextDisplay("❌ **Error Banning User**")) + container.add_item(ui.TextDisplay("You can't ban a user with a higher or equal role!")) + container.add_item(ui.TextDisplay(f"*Requested by {ctx.author}*")) + view = ui.LayoutView() + view.add_item(container) + return await ctx.send(view=view) + + # Try to DM the user + try: + await user.send(f"{ZWARNING} You have been banned from **{ctx.guild.name}** by **{ctx.author}**. Reason: {reason or 'No reason provided'}") + dm_status = "Yes" + except discord.Forbidden: + dm_status = "No" + except discord.HTTPException: + dm_status = "No" + + # Ban the user + await ctx.guild.ban(user, reason=f"Ban requested by {ctx.author} for reason: {reason or 'No reason provided'}") + + # Success container with Components V2 + container = ui.Container() + container.add_item(ui.TextDisplay(f"✅ **Successfully Banned {user.name}**")) + container.add_item(ui.Separator()) + container.add_item(ui.TextDisplay( + f"**{TICK} | [{user}](https://discord.com/users/{user.id}) Has Been Banned Successfully**" + f"\n**Reason:** {reason or 'No reason provided'}" + f"\n**DM Sent:** {dm_status}" + f"\n**Moderator:** {ctx.author.mention}" + )) + container.add_item(ui.Separator()) + container.add_item(ui.TextDisplay(f"*Requested by {ctx.author} • {discord.utils.format_dt(discord.utils.utcnow(), 'R')}*")) + + view = ui.LayoutView() + view.add_item(container) + + message = await ctx.send(view=view) + +async def setup(bot): + await bot.add_cog(Ban(bot)) \ No newline at end of file diff --git a/bot/cogs/moderation/ban.txt b/bot/cogs/moderation/ban.txt new file mode 100644 index 0000000..bac33de --- /dev/null +++ b/bot/cogs/moderation/ban.txt @@ -0,0 +1,200 @@ +import discord +from utils.emoji import CODEBASE, DELETE_ALT1, MENTION_ALT1, TICK, ZHUMAN, ZWARNING +from discord.ext import commands +from discord import ui +from utils.Tools import * + +#class BanView(ui.View): + # def __init__(self, user, author): + # super().__init__(timeout=120) + # self.user = user + # self.author = author + # self.message = None + # self.color = discord.Color.from_rgb(255, 0, 0) + + # async def interaction_check(self, interaction: discord.Interaction) -> bool: + # if interaction.user != self.author: + # await interaction.response.send_message("You are not allowed to interact with this!", ephemeral=True) + # return False + # return True + + # async def on_timeout(self): + # for item in self.children: + # item.disabled = True + # if self.message: + # try: + # await self.message.edit(view=self) + # except Exception: + # pass + + #@ui.button(label="Unban", style=discord.ButtonStyle.success) + # async def unban(self, interaction: discord.Interaction, button: discord.ui.Button): + # modal = ReasonModal(user=self.user, author=self.author, view=self) + # await interaction.response.send_modal(modal) + + #@ui.button(style=discord.ButtonStyle.gray, emoji=DELETE_ALT1) + #async def delete(self, interaction: discord.Interaction, button: discord.ui.Button): + #await interaction.message.delete() + +#class AlreadyBannedView(ui.View): + # def __init__(self, user, author): + # super().__init__(timeout=120) + # self.user = user + # self.author = author + # self.message = None + + # async def interaction_check(self, interaction: discord.Interaction) -> bool: + # if interaction.user != self.author: + # await interaction.response.send_message("You are not allowed to interact with this!", ephemeral=True) + # return False + # return True + + # async def on_timeout(self): + # for item in self.children: + # item.disabled = True + # if self.message: + # await self.message.edit(view=self) + + #@ui.button(label="Unban", style=discord.ButtonStyle.success) + # async def unban(self, interaction: discord.Interaction, button: discord.ui.Button): + # modal = ReasonModal(user=self.user, author=self.author, view=self) + # await interaction.response.send_modal(modal) + +# @ui.button(style=discord.ButtonStyle.gray, emoji=DELETE_ALT1) + # async def delete(self, interaction: discord.Interaction, button: discord.ui.Button): + # await interaction.message.delete() + +#class ReasonModal(ui.Modal): + # def __init__(self, user, author, view): + # super().__init__(title="Unban Reason") + # self.user = user + # self.author = author + # self.view = view + # self.reason_input = ui.TextInput(label="Reason for Unbanning", placeholder="Provide a reason to unban or leave it blank for no reason.", required = False, max_length=2000, style=discord.TextStyle.paragraph) + # self.add_item(self.reason_input) + + # async def on_submit(self, interaction: discord.Interaction): + # reason = self.reason_input.value or "No reason provided" + # try: + # await self.user.send(f"{TICK} You have been Unbanned from **{self.author.guild.name}** by **{self.author}**. Reason: {reason or 'No reason provided'}") + # dm_status = "Yes" + # except discord.Forbidden: + # dm_status = "No" + # except discord.HTTPException: + # dm_status = "No" + + # embed = discord.Embed(description=f"**{ZHUMAN} Target User:** [{self.user}](https://discord.com/users/{self.user.id})\n{MENTION_ALT1} **User Mention:** {self.user.mention}\n**{TICK} DM Sent:** {dm_status}\n**{CODEBASE} Reason:** {reason}", color=0xFF0000) + # embed.set_author(name=f"Successfully Unbanned {self.user.name}", icon_url=self.user.avatar.url if self.user.avatar else self.user.default_avatar.url) + # embed.add_field(name=f"{ZHUMAN} Moderator:", value=interaction.user.mention, inline=False) + # embed.set_footer(text=f"Requested by {self.author}", icon_url=self.author.avatar.url if self.author.avatar else self.author.default_avatar.url) + # embed.timestamp = discord.utils.utcnow() + + # try: + # await interaction.guild.unban(self.user, reason=f"Unban requested by {self.author}") + + # except discord.NotFound: + # pass + # except discord.Forbidden: + # pass + # except discord.HTTPException: + # pass + + # try: + # await interaction.response.edit_message(embed=embed, view=self.view) + # for item in self.view.children: + # item.disabled = True + # await interaction.message.edit(view=self.view) + # except discord.NotFound: + # pass + # except discord.Forbidden: + # pass + # except discord.HTTPException: + # pass + + + +class Ban(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.color = discord.Color.from_rgb(255, 0, 0) + + def get_user_avatar(self, user): + return user.avatar.url if user.avatar else user.default_avatar.url + + @commands.hybrid_command( + name="ban", + help="Bans a user from the Server", + usage="ban ", + aliases=["fuckban", "hackban","kuttaban"]) + @blacklist_check() + @ignore_check() + @top_check() + @commands.cooldown(1, 10, commands.BucketType.member) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(ban_members=True) + @commands.bot_has_permissions(ban_members=True) + async def ban(self, ctx, user: discord.User, *, reason=None): + + member = ctx.guild.get_member(user.id) + if not member: + try: + user = await self.bot.fetch_user(user.id) + except discord.NotFound: + await ctx.send(f"User with ID {user.id} not found.") + return + + bans = [entry async for entry in ctx.guild.bans()] + if any(ban_entry.user.id == user.id for ban_entry in bans): + embed = discord.Embed(description=f"**Requested User is already banned in this server.**", color=0xFF0000) + #embed.add_field(name="__Unban__:", value="Click on the `Unban` button to unban the mentioned user.") + embed.set_author(name=f"{user.name} is Already Banned!", icon_url=self.get_user_avatar(user)) + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + #view = AlreadyBannedView(user=user, author=ctx.author) + message = await ctx.send(embed=embed) + # view.message = message + return + + if member == ctx.guild.owner: + error = discord.Embed(color=self.color, description="I can't ban the Server Owner!") + error.set_author(name="Error Banning User", icon_url="") + error.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + return await ctx.send(embed=error) + + if isinstance(member, discord.Member) and member.top_role >= ctx.guild.me.top_role: + error = discord.Embed(color=self.color, description="I can't ban a user with a higher or equal role!") + error.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + error.set_author(name="Error Banning User", icon_url="") + return await ctx.send(embed=error) + + if isinstance(member, discord.Member): + if ctx.author != ctx.guild.owner: + if member.top_role >= ctx.author.top_role: + error = discord.Embed(color=self.color, description="You can't ban a user with a higher or equal role!") + error.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + error.set_author(name="Error Banning User", icon_url="") + return await ctx.send(embed=error) + + try: + await user.send(f"{ZWARNING} You have been banned from **{ctx.guild.name}** by **{ctx.author}**. Reason: {reason or 'No reason provided'}") + dm_status = "Yes" + except discord.Forbidden: + dm_status = "No" + except discord.HTTPException: + dm_status = "No" + + await ctx.guild.ban(user, reason=f"Ban requested by {ctx.author} for reason: {reason or 'No reason provided'}") + + reasonn = reason or "No reason provided" + embed = discord.Embed(description=f"**{TICK} | [{user}](https://discord.com/users/{user.id}) Has Been Banned Successfully\nReason {reason}**", color=0xFF0000) + embed.set_author(name=f"Successfully Banned {user.name}", icon_url=self.get_user_avatar(user)) + embed.set_thumbnail(url=ctx.author.display_avatar.url) + # embed.add_field(name=f"{ZHUMAN} Moderator:", value=ctx.author.mention, inline=False) + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + embed.timestamp = discord.utils.utcnow() + + # view = BanView(user=user, author=ctx.author) + message = await ctx.send(embed=embed) + # view.message = message + + diff --git a/bot/cogs/moderation/hide.py b/bot/cogs/moderation/hide.py new file mode 100644 index 0000000..765585d --- /dev/null +++ b/bot/cogs/moderation/hide.py @@ -0,0 +1,70 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CHANNEL, TICK, ZCROSS +from discord.ext import commands + +class Hide(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.color = discord.Color.from_rgb(255, 0, 0) # Red color for embeds + + @commands.hybrid_command( + name="hide", + help="Hides a channel from the default role (@everyone).", + usage="hide [channel]", + aliases=["hidechannel"]) + @commands.has_permissions(manage_channels=True) + @commands.bot_has_permissions(manage_channels=True) + async def hide_command(self, ctx, channel: discord.TextChannel = None): + """Hides a channel from @everyone.""" + # If no channel is specified, default to the current channel + channel = channel or ctx.channel + + # Get the authorf's avatar URL, handling cases where they might have a default avatar + author_avatar_url = ctx.author.avatar.url if ctx.author.avatar else None + + # Check if the channel is already hidden + if not channel.permissions_for(ctx.guild.default_role).read_messages: + embed = discord.Embed( + description=f"**{CHANNEL} Channel**: {channel.mention}\n{ZCROSS} **Status**: Already Hidden", + color=self.color + ) + embed.set_author(name=f"{channel.name} is Already Hidden") + # Set the author's avatar as the thumbnail + if author_avatar_url: + embed.set_thumbnail(url=author_avatar_url) + await ctx.send(embed=embed) + return + + # Hide the channel by updating permissions for the @everyone role + await channel.set_permissions(ctx.guild.default_role, read_messages=False) + + # Create the success embed + embed = discord.Embed( + description=f"**{TICK} | {channel.mention} has been successfully hidden.**", + color=self.color + ) + embed.set_author(name=f"Channel Hidden") + embed.set_footer(text=f"Action by {ctx.author.name}", icon_url=author_avatar_url) + # Set the author's avatar as the thumbnail + if author_avatar_url: + embed.set_thumbnail(url=author_avatar_url) + + await ctx.send(embed=embed) + +# Function to add the cog to your bot +async def setup(bot): + await bot.add_cog(Hide(bot)) diff --git a/bot/cogs/moderation/hide.txt b/bot/cogs/moderation/hide.txt new file mode 100644 index 0000000..bbe0b51 --- /dev/null +++ b/bot/cogs/moderation/hide.txt @@ -0,0 +1,93 @@ +import discord +from utils.emoji import CHANNEL, CODEBASE, DELETE_ALT1, TICK, ZHUMAN +from discord.ext import commands +from discord import ui + +#class HideUnhideView(ui.View): + #def __init__(self, channel, author, ctx): + # super().__init__(timeout=120) + # self.channel = channel + # self.author = author + # self.ctx = ctx + # self.message = None + + # async def interaction_check(self, interaction: discord.Interaction) -> bool: + # if interaction.user != self.author: + # await interaction.response.send_message("You are not allowed to interact with this!", ephemeral=True) + # return False + # return True + + # async def on_timeout(self): + # for item in self.children: + # if item.label != "Delete": + # item.disabled = True + # if self.message: + # try: + # await self.message.edit(view=self) + # except Exception: + # pass + + # @ui.button(label="Unhide", style=discord.ButtonStyle.success) + # async def unhide(self, interaction: discord.Interaction, button: discord.ui.Button): + # await self.channel.set_permissions(interaction.guild.default_role, read_messages=True) + # await interaction.response.send_message(f"{self.channel.mention} has been unhidden.", ephemeral=True) + + # embed = discord.Embed( + # description=f"{CHANNEL} **Channel**: {self.channel.mention}\n{TICK} **Status**: Unhidden\n {CODEBASE}**Reason:** Unhide request by {self.author}", + # color=0xFF0000 + # ) + # embed.add_field(name=f"{ZHUMAN} **Moderator:**", value=self.ctx.author.mention, inline=False) + # embed.set_author(name=f"Successfully Unhidden {self.channel.name}", icon_url="") + # await self.message.edit(embed=embed, view=self) + + # for item in self.children: + # if item.label != "Delete": + # item.disabled = True + # await self.message.edit(view=self) + + # @ui.button(style=discord.ButtonStyle.gray, emoji=DELETE_ALT1) + # async def delete(self, interaction: discord.Interaction, button: discord.ui.Button): + # await interaction.message.delete() + + +class Hide(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.color = discord.Color.from_rgb(255, 0, 0) + + @commands.hybrid_command( + name="hide", + help="Hides a channel from the default role (@everyone).", + usage="hide ", + aliases=["hidechannel"]) + @commands.has_permissions(manage_roles=True) + @commands.bot_has_permissions(manage_roles=True) + async def hide_command(self, ctx, channel: discord.TextChannel = None): + channel = channel or ctx.channel + if not channel.permissions_for(ctx.guild.default_role).read_messages: + embed = discord.Embed( + description=f"**{CHANNEL} Channel**: {channel.mention}\n{TICK} **Status**: Already Hidden", + color=self.color + ) + embed.set_author(name=f"{channel.name} is Already Hidden", icon_url="") + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=ctx.author.avatar.url) + view = HideUnhideView(channel=channel, author=ctx.author, ctx=ctx) + message = await ctx.send(embed=embed, view=view) + view.message = message + return + + await channel.set_permissions(ctx.guild.default_role, read_messages=False) + + embed = discord.Embed( + description=f"**{TICK} | {channel.mention} Has Successfully Hidden", + color=self.color + ) + # embed.add_field(name=f"{ZHUMAN} **Moderator:**", value=ctx.author.mention, inline=False) + embed.set_author(name=f"Successfully Hidden {channel.name}", icon_url="") + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=ctx.author.avatar.url) + view = HideUnhideView(channel=channel, author=ctx.author, ctx=ctx) + message = await ctx.send(embed=embed, view=view) + view.message = message + + + \ No newline at end of file diff --git a/bot/cogs/moderation/kick.py b/bot/cogs/moderation/kick.py new file mode 100644 index 0000000..823bea1 --- /dev/null +++ b/bot/cogs/moderation/kick.py @@ -0,0 +1,84 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import TICK +from discord.ext import commands +from utils.Tools import * # Assuming these decorators exist as provided + +class Kick(commands.Cog): + def __init__(self, bot): + self.bot = bot + # Color is set to Red (255, 0, 0) as requested + self.color = discord.Color.from_rgb(255, 0, 0) + + @commands.hybrid_command( + name="kick", + help="Kicks a member from the server.", + usage="kick [reason]", + aliases=["kickmember"]) + @blacklist_check() + @ignore_check() + @top_check() + @commands.has_permissions(kick_members=True) + @commands.bot_has_permissions(kick_members=True) + @commands.guild_only() + async def kick_command(self, ctx, member: discord.Member, *, reason: str = None): + """Kicks a member from the server with an optional reason.""" + reason = reason or "No reason provided" + + # --- Hierarchy and permission checks --- + if member == ctx.author: + return await ctx.send("You cannot kick yourself.") + + if member == self.bot.user: + return await ctx.send("You cannot kick me.") + + if ctx.author.top_role <= member.top_role and ctx.guild.owner != ctx.author: + return await ctx.send("You cannot kick a member with a higher or equal role than you.") + + if ctx.guild.me.top_role <= member.top_role: + return await ctx.send("My role is not high enough to kick this member.") + + # --- Attempt to DM the user --- + try: + dm_message = f"You have been kicked from **{ctx.guild.name}**. Reason: {reason}" + await member.send(dm_message) + except (discord.Forbidden, discord.HTTPException): + # Fails silently if the user has DMs closed or an error occurs + pass + + # --- Kick the member --- + await member.kick(reason=f"Action by {ctx.author.name} | Reason: {reason}") + + # --- Create and send the simplified confirmation embed --- + member_avatar_url = member.avatar.url if member.avatar else None + + embed = discord.Embed( + description=( + f"**{TICK} | {member.mention} has been kicked successfully\nReason:{reason}**" + ), + color=self.color # Uses the red color 0xFF0000 + ) + embed.set_author(name=f"Successfully Kicked {member.name}") + embed.set_footer(text=f"Action by {ctx.author.name}", icon_url=ctx.author.avatar.url if ctx.author.avatar else None) + + if member_avatar_url: + embed.set_thumbnail(url=member_avatar_url) + + await ctx.send(embed=embed) + +# Function to add the cog to your bot +async def setup(bot): + await bot.add_cog(Kick(bot)) diff --git a/bot/cogs/moderation/kick.txt b/bot/cogs/moderation/kick.txt new file mode 100644 index 0000000..5732dad --- /dev/null +++ b/bot/cogs/moderation/kick.txt @@ -0,0 +1,107 @@ +import discord +from utils.emoji import CODEBASE, DELETE_ALT1, MENTION_ALT1, TICK, ZHUMAN +from discord.ext import commands +from discord import ui +from utils.Tools import * + +class KickView(ui.View): + def __init__(self, member): + super().__init__(timeout=120) + self.member = member + self.message = None + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + return True + + async def on_timeout(self): + for item in self.children: + item.disabled = True + if self.message: + try: + await self.message.edit(view=self) + except Exception: + pass + + @ui.button(style=discord.ButtonStyle.gray, emoji=DELETE_ALT1) + async def delete(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.message.delete() + +class Kick(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.color = discord.Color.from_rgb(255, 0, 0) + + @commands.hybrid_command( + name="kick", + help="Kicks a member from the server.", + usage="kick [reason]", + aliases=["kickmember"]) + @blacklist_check() + @ignore_check() + @top_check() + @commands.has_permissions(kick_members=True) + @commands.bot_has_permissions(kick_members=True) + @commands.guild_only() + async def kick_command(self, ctx, member: discord.Member, *, reason: str = None): + reason = reason or "No reason provided" + + if member == ctx.author: + return await ctx.reply("You cannot kick yourself.") + + if member == ctx.bot.user: + return await ctx.reply("You cannot kick me.") + + if not ctx.author == ctx.guild.owner: + if member == ctx.guild.owner: + return await ctx.reply("I cannot kick the server owner.") + + if ctx.author.top_role <= member.top_role: + return await ctx.reply("You cannot kick a member with a higher or equal role.") + + if ctx.guild.me.top_role <= member.top_role: + return await ctx.reply("I cannot kick a member with a higher or equal role.") + + if member not in ctx.guild.members: + embed = discord.Embed( + description=f"**Member Not Found:** The specified member does not exist in this server.", + color=self.color + ) + view = KickView(member) + message = await ctx.send(embed=embed, view=view) + view.message = message + return + + + dm_status = "Yes" + try: + await member.send(f"You have been kicked from **{ctx.guild.name}**. Reason: {reason}") + except discord.Forbidden: + dm_status = "No" + except discord.HTTPException: + dm_status = "No" + + + await member.kick(reason=f"Kicked by {ctx.author} | Reason: {reason}") + + + + embed = discord.Embed( + description=( + f"**{ZHUMAN} Target User:** [{member}](https://discord.com/users/{member.id})\n" + f"{MENTION_ALT1} **User Mention:** {member.mention}\n" + f"{CODEBASE} **Reason:** {reason}\n" + f"{TICK}**DM Sent:** {dm_status}" + ), + color=self.color + ) + embed.set_author(name=f"Successfully Kicked {member.name}", icon_url=member.avatar.url if member.avatar else member.default_avatar.url) + embed.add_field(name=f"{ZHUMAN} Moderator:", value=ctx.author.mention, inline=False) + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + embed.timestamp = discord.utils.utcnow() + + view = KickView(member) + message = await ctx.send(embed=embed, view=view) + view.message = message + + + \ No newline at end of file diff --git a/bot/cogs/moderation/lock.py b/bot/cogs/moderation/lock.py new file mode 100644 index 0000000..0df6915 --- /dev/null +++ b/bot/cogs/moderation/lock.py @@ -0,0 +1,67 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import TICK +from discord.ext import commands + +class Lock(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.color = discord.Color.red() + + @commands.hybrid_command( + name="lock", + description="Locks a channel to prevent sending messages.", + aliases=["lockchannel"] + ) + # Using manage_channels is more appropriate for locking/unlocking + @commands.has_permissions(manage_channels=True) + @commands.bot_has_permissions(manage_channels=True) + async def lock_command(self, ctx: commands.Context, channel: discord.TextChannel = None): + """Locks the specified channel or the current one if none is provided.""" + + # If no channel is specified, use the current channel + channel = channel or ctx.channel + + # Check if the channel is already locked + if not channel.permissions_for(ctx.guild.default_role).send_messages: + embed = discord.Embed( + description=f"**Channel: {channel.mention}\n{TICK} Status: Already Locked**", + color=0xFF0000 + ) + #embed.set_author(name=f"{c", icon_url="") + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=ctx.author.avatar.url) + await ctx.send(embed=embed) + return + + # Lock the channel by updating permissions for the @everyone role + await channel.set_permissions(ctx.guild.default_role, send_messages=False) + + # Create the confirmation embed + embed = discord.Embed( + title="{BRAND_NAME} | Lockdown", + description=f"{TICK} | Successfully Locked {channel.mention}", + color=0xFF0000 + ) + #embed.set_author(name=f"Successfully Locked {channel.name}", icon_url="") + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=ctx.author.avatar.url) + #embed.set_thumbnail(url=ctx.author.display_avatar.url) + + # Send the final message + await ctx.send(embed=embed) + +# Standard setup function to load the cog +async def setup(bot): + await bot.add_cog(Lock(bot)) diff --git a/bot/cogs/moderation/message.py b/bot/cogs/moderation/message.py new file mode 100644 index 0000000..1585a98 --- /dev/null +++ b/bot/cogs/moderation/message.py @@ -0,0 +1,267 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import TICK +from discord.ext import commands, tasks +import asyncio +import datetime +import re +from typing import * +from utils.Tools import * +from discord.ui import Button, View +from typing import Union, Optional +from typing import Union, Optional +from io import BytesIO +import requests +import aiohttp +import time +from datetime import datetime, timezone, timedelta + + +time_regex = re.compile(r"(?:(\d{1,5})(h|s|m|d))+?") +time_dict = {"h": 3600, "s": 1, "m": 60, "d": 86400} + + +def convert(argument): + args = argument.lower() + matches = re.findall(time_regex, args) + time = 0 + for key, value in matches: + try: + time += time_dict[value] * float(key) + except KeyError: + raise commands.BadArgument( + f"{value} is an invalid time key! h|m|s|d are valid arguments") + except ValueError: + raise commands.BadArgument(f"{key} is not a number!") + return round(time) + +async def do_removal(ctx, limit, predicate, *, before=None, after=None): + if limit > 2000: + return await ctx.error(f"Too many messages to search given ({limit}/2000)") + + if before is None: + before = ctx.message + else: + before = discord.Object(id=before) + + if after is not None: + after = discord.Object(id=after) + + try: + deleted = await ctx.channel.purge(limit=limit, before=before, after=after, check=predicate) + except discord.Forbidden as e: + return await ctx.error("I do not have permissions to delete messages.") + except discord.HTTPException as e: + return await ctx.error(f"Error: {e} (try a smaller search?)") + + spammers = Counter(m.author.display_name for m in deleted) + deleted = len(deleted) + messages = [f'{TICK} | {deleted} message{" was" if deleted == 1 else "s were"} removed.'] + if deleted: + messages.append("") + spammers = sorted(spammers.items(), key=lambda t: t[1], reverse=True) + messages.extend(f"**{name}**: {count}" for name, count in spammers) + + to_send = "\n".join(messages) + + if len(to_send) > 2000: + await ctx.send(f"{TICK} | Successfully removed {deleted} messages.", delete_after=7) + else: + await ctx.send(to_send, delete_after=7) + + +class Message(commands.Cog): + + def __init__(self, bot): + self.bot = bot + self.color = 0xFF0000 + + + @commands.group(invoke_without_command=True, aliases=["purge"], help="Clears the messages") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(manage_messages=True) + @commands.bot_has_permissions(manage_messages=True) + async def clear(self, ctx, Choice: Union[discord.Member, int], Amount: int = None): + await ctx.message.delete() + + if isinstance(Choice, discord.Member): + search = Amount or 5 + return await do_removal(ctx, search, lambda e: e.author == Choice) + + elif isinstance(Choice, int): + return await do_removal(ctx, Choice, lambda e: True) + + + + @clear.command(help="Clears the messages having embeds") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(manage_messages=True) + @commands.bot_has_permissions(manage_messages=True) + async def embeds(self, ctx, search=100): + await ctx.message.delete() + await do_removal(ctx, search, lambda e: len(e.embeds)) + + + @clear.command(help="Clears the messages having files") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(manage_messages=True) + @commands.bot_has_permissions(manage_messages=True) + async def files(self, ctx, search=100): + + await ctx.message.delete() + await do_removal(ctx, search, lambda e: len(e.attachments)) + + @clear.command(help="Clears the messages having images") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(manage_messages=True) + @commands.bot_has_permissions(manage_messages=True) + async def images(self, ctx, search=100): + + await ctx.message.delete() + await do_removal(ctx, search, lambda e: len(e.embeds) or len(e.attachments)) + + + @clear.command(name="all", help="Clears all messages") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(manage_messages=True) + @commands.bot_has_permissions(manage_messages=True) + async def _remove_all(self, ctx, search=100): + + await ctx.message.delete() + await do_removal(ctx, search, lambda e: True) + + @clear.command(help="Clears the messages of a specific user") + @commands.has_permissions(manage_messages=True) + @commands.bot_has_permissions(manage_messages=True) + async def user(self, ctx, member: discord.Member, search=100): + + await ctx.message.delete() + await do_removal(ctx, search, lambda e: e.author == member) + + + + @clear.command(help="Clears the messages containing a specifix string") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(manage_messages=True) + @commands.bot_has_permissions(manage_messages=True) + async def contains(self, ctx, *, string: str): + + await ctx.message.delete() + if len(string) < 3: + await ctx.error("The substring length must be at least 3 characters.") + else: + await do_removal(ctx, 100, lambda e: string in e.content) + + @clear.command(name="bot", aliases=["bots","b"], help="Clears the messages sent by bot") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(manage_messages=True) + @commands.bot_has_permissions(manage_messages=True) + async def _bot(self, ctx, prefix=None, search=100): + + await ctx.message.delete() + + def predicate(m): + return (m.webhook_id is None and m.author.bot) or (prefix and m.content.startswith(prefix)) + + await do_removal(ctx, search, predicate) + + @clear.command(name="emoji", aliases=["emojis"], help="Clears the messages having emojis") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(manage_messages=True) + @commands.bot_has_permissions(manage_messages=True) + + async def _emoji(self, ctx, search=100): + + await ctx.message.delete() + custom_emoji = re.compile(r"") + + def predicate(m): + return custom_emoji.search(m.content) + + await do_removal(ctx, search, predicate) + + @clear.command(name="reactions", help="Clears the reaction from the messages") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(manage_messages=True) + @commands.bot_has_permissions(manage_messages=True) + async def _reactions(self, ctx, search=100): + + await ctx.message.delete() + + if search > 2000: + return await ctx.send(f"Too many messages to search for ({search}/2000)") + + total_reactions = 0 + async for message in ctx.history(limit=search, before=ctx.message): + if len(message.reactions): + total_reactions += sum(r.count for r in message.reactions) + await message.clear_reactions() + + await ctx.success(f"{TICK} | Successfully removed {total_reactions} reactions.") + + + + + @commands.command(name="purgebots", + aliases=["cleanup", "pb", "clearbot", "clearbots"], + help="Clear recently bot messages in channel") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(manage_messages=True) + @commands.bot_has_permissions(manage_messages=True) + async def _purgebot(self, ctx, prefix=None, search=100): + + await ctx.message.delete() + + def predicate(m): + return (m.webhook_id is None and m.author.bot) or (prefix and m.content.startswith(prefix)) + + await do_removal(ctx, search, predicate) + + + @commands.command(name="purgeuser", + aliases=["pu", "cu", "clearuser"], + help="Clear recent messages of a user in channel") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(manage_messages=True) + @commands.bot_has_permissions(manage_messages=True) + async def purguser(self, ctx, member: discord.Member, search=100): + + await ctx.message.delete() + await do_removal(ctx, search, lambda e: e.author == member) + + \ No newline at end of file diff --git a/bot/cogs/moderation/moderation.py b/bot/cogs/moderation/moderation.py new file mode 100644 index 0000000..3a362f1 --- /dev/null +++ b/bot/cogs/moderation/moderation.py @@ -0,0 +1,1075 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CROSS, DENIED, TICK, ZWARNING +import asyncio +import datetime +import re +import typing +import typing as t +from typing import * +from utils.Tools import * +from core import Cog, zyrox, Context +from discord.ext.commands import Converter +from discord.ext import commands, tasks +from discord.ui import Button, View +from typing import Union, Optional +from utils import Paginator, DescriptionEmbedPaginator, FieldPagePaginator, TextPaginator +from typing import Union, Optional +from io import BytesIO +import requests +import aiohttp +import time +from datetime import datetime, timezone, timedelta +import sqlite3 +from typing import * +from discord.utils import utcnow + + + +time_regex = re.compile(r"(?:(\d{1,5})(h|s|m|d))+?") +time_dict = {"h": 3600, "s": 1, "m": 60, "d": 86400} + + +def convert(argument): + args = argument.lower() + matches = re.findall(time_regex, args) + time = 0 + for key, value in matches: + try: + time += time_dict[value] * float(key) + except KeyError: + raise commands.BadArgument( + f"{value} is an invalid time key! h|m|s|d are valid arguments") + except ValueError: + raise commands.BadArgument(f"{key} is not a number!") + return round(time) + +async def do_removal(ctx, limit, predicate, *, before=None, after=None): + if limit > 2000: + return await ctx.error(f"Too many messages to search given ({limit}/2000)") + + if before is None: + before = ctx.message + else: + before = discord.Object(id=before) + + if after is not None: + after = discord.Object(id=after) + + try: + deleted = await ctx.channel.purge(limit=limit, before=before, after=after, check=predicate) + except discord.Forbidden as e: + return await ctx.error("I do not have permissions to delete messages.") + except discord.HTTPException as e: + return await ctx.error(f"Error: {e} (try a smaller search?)") + + spammers = Counter(m.author.display_name for m in deleted) + deleted = len(deleted) + messages = [f'{TICK}> | {deleted} message{" was" if deleted == 1 else "s were"} removed.'] + if deleted: + messages.append("") + spammers = sorted(spammers.items(), key=lambda t: t[1], reverse=True) + messages.extend(f"**{name}**: {count}" for name, count in spammers) + + to_send = "\n".join(messages) + + if len(to_send) > 2000: + await ctx.send(f"{TICK}> | Successfully removed {deleted} messages.", delete_after=7) + else: + await ctx.send(to_send, delete_after=7) + + + + +class Moderation(commands.Cog): + + def __init__(self, bot): + self.bot = bot + self.color = 0xFF0000 + self.sniped = {} + + def convert(self, time): + pos = ["s", "m", "h", "d"] + + time_dict = {"s": 1, "m": 60, "h": 3600, "d": 3600 * 24} + unit = time[-1] + if unit not in pos: + return -1 + try: + val = int(time[:-1]) + except: + return -2 + return val * time_dict[unit] + + + + @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]): + url = emoji.url + await ctx.send(url) + + + + + @commands.hybrid_command(name="unlockall", + help="Unlocks all channels in the Guild.", + usage="unlockall") + @blacklist_check() + @ignore_check() + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + @commands.cooldown(1, 15, commands.BucketType.channel) + async def unlockall(self, ctx): + if ctx.author == ctx.guild.owner or ctx.author.top_role.position > ctx.guild.me.top_role.position: + button = Button(label="Confirm", + style=discord.ButtonStyle.green, + emoji=f"{TICK}>") + button1 = Button(label="Cancel", + style=discord.ButtonStyle.red, + emoji=ZWARNING) + async def button_callback(interaction: discord.Interaction): + a = 0 + if interaction.user == ctx.author: + if interaction.guild.me.guild_permissions.manage_roles: + embed1 = discord.Embed( + color=self.color, + description=f"Unlocking all channels in {ctx.guild.name} .") + await interaction.response.edit_message( + embed=embed1, view=None) + for channel in interaction.guild.channels: + try: + await channel.set_permissions( + ctx.guild.default_role, + overwrite=discord.PermissionOverwrite(send_messages=True, + read_messages=True), + reason="Unlockall Command Executed By: {}".format(ctx.author)) + a += 1 + except Exception as e: + print(e) + await interaction.channel.send( + content=f"{TICK}> | Successfully Unlocked {a} Channels") + return + else: + await interaction.response.edit_message( + content= + f"{ZWARNING} | It seems I'm missing the necessary permissions. Please grant me the `manage roles` permissions and try again.", + embed=None, + view=None) + else: + await interaction.response.send_message("Oops! It looks like that message isn't from you. You need to run the command yourself to interact with it.", + embed=None, + view=None, + ephemeral=True) + + async def button1_callback(interaction: discord.Interaction): + if interaction.user == ctx.author: + embed2 = discord.Embed( + color=self.color, + description=f"Cancelled, I won't proceed with unlocking any channel.") + await interaction.response.edit_message( + embed=embed2, view=None) + else: + await interaction.response.send_message("Oops! It looks like that message isn't from you. You need to run the command yourself to interact with it.", + embed=None, + view=None, + ephemeral=True) + embed = discord.Embed( + color=self.color, + description=f'**Do you really want to unlock all channels in {ctx.guild.name}**') + view = View() + button.callback = button_callback + button1.callback = button1_callback + view.add_item(button) + view.add_item(button1) + embed.set_footer(text="Please click either 'Confirm' or 'Cancel' to proceed. You have 30 seconds to decide!") + await ctx.reply(embed=embed, view=view, mention_author=False,delete_after=30) + + + else: + embed5 = discord.Embed(title=f"{ZWARNING} Access Denied", + description="Your role should be above my top role.", + color=0xFF0000) + embed5.set_footer(text=f"“{ctx.command.qualified_name}” Command executed by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.send(embed=embed5, mention_author=False) + + + + @commands.hybrid_command(name="lockall", + help="locks all the channels in Guild.", + usage="lockall") + @blacklist_check() + @ignore_check() + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + @commands.cooldown(1, 15, commands.BucketType.channel) + async def lockall(self, ctx): + if ctx.author == ctx.guild.owner or ctx.author.top_role.position > ctx.guild.me.top_role.position: + button = Button(label="Confirm", + style=discord.ButtonStyle.green, + emoji=f"{TICK}>") + button1 = Button(label="Cancel", + style=discord.ButtonStyle.red, + emoji=CROSS) + async def button_callback(interaction: discord.Interaction): + a = 0 + if interaction.user == ctx.author: + if interaction.guild.me.guild_permissions.manage_roles: + embed1 = discord.Embed( + color=self.color, + description=f"Locking all channels in {ctx.guild.name}...") + await interaction.response.edit_message( + embed=embed1, view=None) + for channel in interaction.guild.channels: + try: + await channel.set_permissions(ctx.guild.default_role, + overwrite=discord.PermissionOverwrite( + send_messages=False, + read_messages=True), + reason="Lockall command executed by: {}".format(ctx.author)) + a += 1 + except Exception as e: + print(e) + await interaction.channel.send( + content=f"{TICK}>| Successfully locked {a} Channels") + return + else: + await interaction.response.edit_message( + content= + f"{ZWARNING} | It seems I'm missing the necessary permissions. Please grant me the `manage roles` permissions and try again.", + embed=None, + view=None) + else: + await interaction.response.send_message("Oops! It looks like that message isn't from you. You need to run the command yourself to interact with it.", + embed=None, + view=None, + ephemeral=True) + + async def button1_callback(interaction: discord.Interaction): + if interaction.user == ctx.author: + embed2 = discord.Embed( + color=self.color, + description=f"Cancelled, I won't proceed with locking any channel.") + await interaction.response.edit_message( + embed=embed2, view=None) + else: + await interaction.response.send_message("Oops! It looks like that message isn't from you. You need to run the command yourself to interact with it.", + embed=None, + view=None, + ephemeral=True) + embed = discord.Embed( + color=self.color, + description=f'**Do you really want to lock all channels in {ctx.guild.name}**') + view = View() + button.callback = button_callback + button1.callback = button1_callback + view.add_item(button) + view.add_item(button1) + embed.set_footer(text=f"Please click either 'Confirm' or 'Cancel' to proceed. You have 30 seconds to decide!") + await ctx.reply(embed=embed, view=view, mention_author=False,delete_after=30) + + else: + denied = discord.Embed(title=f"{ZWARNING} Access Denied", + description="Your role should be above my top role.", + color=0xFF0000) + denied.set_footer(text=f"“{ctx.command.qualified_name}” Command executed by {ctx.author}", + 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="give", + help="Gives the mentioned user a role.", + usage="give ", + aliases=["addrole"]) + @blacklist_check() + @ignore_check() + @top_check() + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.has_permissions(manage_roles=True) + @commands.bot_has_permissions(manage_roles= True) + async def give(self, ctx, member: discord.Member, *, role: discord.Role): + if not ctx.guild.me.guild_permissions.manage_roles: + return await ctx.send(f"{DENIED} I don't have permission to manage roles!") + + if role >= ctx.guild.me.top_role: + error = discord.Embed( + color=self.color, + description="I can't manage roles for a user with a higher or equal role!" + ) + + error.set_author(name="Error", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + error.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + return await ctx.send(embed=error) + + if ctx.author != ctx.guild.owner and ctx.author.top_role <= member.top_role: + error = discord.Embed( + color=self.color, + description="You can't manage roles for a user with a higher or equal role than yours!" + ) + error.set_author(name="Access Denied", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + error.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + return await ctx.send(embed=error) + + try: + if role not in member.roles: + await member.add_roles(role, reason=f"Role added by {ctx.author} (ID: {ctx.author.id})") + success = discord.Embed( + color=self.color, + description=f"Successfully **added** role {role.name} to {member.mention}." + ) + success.set_author(name="Role Added", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + success.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + else: + await member.remove_roles(role, reason=f"Role removed by {ctx.author} (ID: {ctx.author.id})") + success = discord.Embed( + color=self.color, + description=f"Successfully **removed** role {role.name} from {member.mention}." + ) + success.set_author(name="Role Removed", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + success.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.send(embed=success) + except discord.Forbidden: + error = discord.Embed( + color=self.color, + description=f"{ZWARNING} I don't have permission to manage roles for this user!" + ) + await ctx.send(embed=error) + except Exception as e: + error = discord.Embed( + color=self.color, + description=f"{ZWARNING} An unexpected error occurred: {str(e)}" + ) + await ctx.send(embed=error) + + + + @commands.hybrid_command(name="hideall", help="Hides all the channels .", + usage="hideall") + @blacklist_check() + @ignore_check() + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + @commands.cooldown(1, 15, commands.BucketType.channel) + async def hideall(self, ctx): + if ctx.author == ctx.guild.owner or ctx.author.top_role.position > ctx.guild.me.top_role.position: + button = Button(label="Confirm", + style=discord.ButtonStyle.green, + emoji=f"{TICK}>") + button1 = Button(label="Cancel", + style=discord.ButtonStyle.red, + emoji=CROSS) + async def button_callback(interaction: discord.Interaction): + a = 0 + if interaction.user == ctx.author: + if interaction.guild.me.guild_permissions.manage_roles: + embed1 = discord.Embed( + color=self.color, + description=f"Hiding all channels in {ctx.guild.name} ...") + await interaction.response.edit_message( + embed=embed1, view=None) + for channel in interaction.guild.channels: + try: + await channel.set_permissions(ctx.guild.default_role, view_channel=False, + reason="Hideall Executed by: {}".format(ctx.author)) + a += 1 + except Exception as e: + print(e) + await interaction.channel.send( + content=f"{TICK}> | Successfully Hidden {a} Channel(s) .") + return + else: + await interaction.response.edit_message( + content= + f"{ZWARNING} | It seems I'm missing the necessary permissions. Please grant me the `manage channels` permissions and try again.", + embed=None, + view=None) + else: + await interaction.response.send_message("Oops! It looks like that message isn't from you. You need to run the command yourself to interact with it.", + embed=None, + view=None, + ephemeral=True) + + async def button1_callback(interaction: discord.Interaction): + if interaction.user == ctx.author: + embed2 = discord.Embed( + color=self.color, + description=f"Cancelled, I won't proceed with hiding any channel.") + await interaction.response.edit_message( + embed=embed2, view=None) + else: + await interaction.response.send_message("Oops! It looks like that message isn't from you. You need to run the command yourself to interact with it.", + embed=None, + view=None, + ephemeral=True) + embed = discord.Embed( + color=self.color, + description=f'**Do you really want to hide all channels in {ctx.guild.name}**') + view = View() + button.callback = button_callback + button1.callback = button1_callback + view.add_item(button) + view.add_item(button1) + embed.set_footer(text=f"Please click either 'Confirm' or 'Cancel' to proceed. You have 30 seconds to decide!") + await ctx.reply(embed=embed, view=view, mention_author=False,delete_after=30) + + else: + denied = discord.Embed(title=f"{ZWARNING} Access Denied", + description="Your role should be above my top role.", + color=0xFF0000) + denied.set_footer(text=f"“{ctx.command.qualified_name}” Command executed by {ctx.author}", + 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.", + usage="unhideall") + @blacklist_check() + @ignore_check() + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + @commands.cooldown(1, 15, commands.BucketType.channel) + async def unhideall(self, ctx): + if ctx.author == ctx.guild.owner or ctx.author.top_role.position > ctx.guild.me.top_role.position: + button = Button(label="Confirm", + style=discord.ButtonStyle.green, + emoji=f"{TICK}>") + button1 = Button(label="Cancel", + style=discord.ButtonStyle.red, + emoji=CROSS) + async def button_callback(interaction: discord.Interaction): + a = 0 + if interaction.user == ctx.author: + if interaction.guild.me.guild_permissions.manage_roles: + embed1 = discord.Embed( + color=self.color, + description=f"Unhiding all channels in {ctx.guild.name} .") + await interaction.response.edit_message( + embed=embed1, view=None) + for channel in interaction.guild.channels: + try: + await channel.set_permissions(ctx.guild.default_role, view_channel=True, + reason="Unhideall Command Executed By: {}".format(ctx.author)) + a += 1 + except Exception as e: + print(e) + await interaction.channel.send( + content=f"{TICK}> | Successfully Unhidden {a} Channel(s) .") + return + else: + await interaction.response.edit_message( + content= + f"{ZWARNING} | It seems I'm missing the necessary permissions. Please grant me the `manage channels` permissions and try again.", + embed=None, + view=None) + else: + await interaction.response.send_message("Oops! It looks like that message isn't from you. You need to run the command yourself to interact with it.", + embed=None, + view=None, + ephemeral=True) + + async def button1_callback(interaction: discord.Interaction): + if interaction.user == ctx.author: + embed2 = discord.Embed( + color=self.color, + description=f"Cancelled, I won't proceed with unhiding any channel.") + await interaction.response.edit_message( + embed=embed2, view=None) + else: + await interaction.response.send_message("Oops! It looks like that message isn't from you. You need to run the command yourself to interact with it.", + embed=None, + view=None, + ephemeral=True) + embed = discord.Embed( + color=self.color, + description=f'**Do you really want to unhide all channels in {ctx.guild.name}**') + view = View() + button.callback = button_callback + button1.callback = button1_callback + view.add_item(button) + view.add_item(button1) + embed.set_footer(text=f"Please click either 'Confirm' or 'Cancel' to proceed. You have 30 seconds to decide!") + await ctx.reply(embed=embed, view=view, mention_author=False,delete_after=30) + + else: + denied = discord.Embed(title=f"{ZWARNING} Access Denied", + description="Your role should be above my top role.", + color=0xFF0000) + denied.set_footer(text=f"“{ctx.command.qualified_name}” Command executed by {ctx.author}", + 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="prefix", + aliases=["setprefix", "prefixset"], + help="Allows you to change the prefix of the bot for this server" + ) + @blacklist_check() + @ignore_check() + @commands.has_permissions(administrator=True) + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + async def _prefix(self, ctx: commands.Context, prefix: str): + if not prefix: + await ctx.reply(embed=discord.Embed(title=f"{CROSS} Error", + description="Prefix cannot be empty. Please provide a valid prefix.", + color=self.color + )) + return + + data = await getConfig(ctx.guild.id) + if ctx.author == ctx.guild.owner or ctx.author.top_role.position > ctx.guild.me.top_role.position: + data["prefix"] = str(prefix) + await updateConfig(ctx.guild.id, data) + embed1=discord.Embed(title=f"{TICK}> Success", + description=f"Changed Prefix For this guild to `{prefix}`\n\nNew Prefix for **{ctx.guild.name}** is : `{prefix}`\nUse `{prefix}help` For More.", + color=self.color + ) + await ctx.reply(embed=embed1) + else: + denied = discord.Embed(title=f"{ZWARNING} Access Denied", + description="Your role should be above my top role.", + color=0xFF0000) + denied.set_footer(text=f"“{ctx.command.qualified_name}” Command executed by {ctx.author}", + 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="clone", help="Clones a channel.") + @blacklist_check() + @ignore_check() + @commands.has_permissions(manage_channels=True) + async def clone(self, ctx: commands.Context, channel: discord.TextChannel): + + if not ctx.guild.me.guild_permissions.manage_channels: + error = discord.Embed( + color=self.color, + description="I don't have permission to manage channels!" + ) + error.set_author(name="Error", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + error.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + return await ctx.send(embed=error) + + + try: + + await channel.clone() + success = discord.Embed( + color=self.color, + description=f"{channel.name} has been successfully cloned" + ) + success.set_author(name="Success", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + success.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.send(embed=success) + except discord.Forbidden: + error = discord.Embed( + color=self.color, + description="I don't have permission to clone channels!" + ) + error.set_author(name="Error", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + await ctx.send(embed=error) + except Exception as e: + error = discord.Embed( + color=self.color, + description=f"An error occurred while trying to clone the channel: {str(e)}" + ) + error.set_author(name="Error", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + await ctx.send(embed=error) + + + @commands.hybrid_command(name="nick", + aliases=['setnick'], + help="To change someone's nickname.", + usage="nick [member]") + @blacklist_check() + @ignore_check() + @commands.has_permissions(manage_nicknames=True) + @commands.bot_has_permissions(manage_nicknames=True) + async def changenickname(self, ctx: commands.Context, member: discord.Member, *, name: str = None): + + + if member == ctx.guild.owner: + error = discord.Embed( + color=self.color, + description="I can't change the nickname of the server owner!" + ) + error.set_author(name="Error", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + error.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + return await ctx.send(embed=error) + + + if member.top_role >= ctx.guild.me.top_role: + error = discord.Embed( + color=self.color, + description="I can't change the nickname of a user with a higher or equal role than mine!" + ) + error.set_author(name="Error", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + error.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + return await ctx.send(embed=error) + + + if ctx.author != ctx.guild.owner and ctx.author.top_role <= member.top_role: + error = discord.Embed( + color=self.color, + description="You can't change the nickname of a user with a higher or equal role than you!" + ) + error.set_author(name="Access Denied", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + error.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + return await ctx.send(embed=error) + + try: + await member.edit(nick=name) + if name: + success = discord.Embed( + color=self.color, + description=f"Successfully changed nickname of {member.mention} to {name}." + ) + success.set_author(name="Nickname Updated", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + success.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + else: + success = discord.Embed( + color=self.color, + description=f"Successfully cleared nickname of {member.mention}." + ) + success.set_author(name="Nickname Cleared", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + success.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.send(embed=success) + except discord.Forbidden: + error = discord.Embed( + color=self.color, + description=f"{ZWARNING} | I don't have permission to manage this user's nickname!" + ) + await ctx.send(embed=error) + except Exception as e: + error = discord.Embed( + color=self.color, + description=f"{ZWARNING} | An error occurred while trying to change the nickname: {str(e)}" + ) + await ctx.send(embed=error) + + + @commands.hybrid_command(name="nuke", help="Nukes a channel", usage="nuke") + @blacklist_check() + @ignore_check() + @top_check() + @commands.cooldown(1, 7, commands.BucketType.user) + @commands.has_permissions(manage_channels=True) + async def _nuke(self, ctx: commands.Context): + button = Button(label="Confirm", + style=discord.ButtonStyle.green, + emoji=f"{TICK}>") + button1 = Button(label="Cancel", + style=discord.ButtonStyle.red, + emoji=CROSS) + + async def button_callback(interaction: discord.Interaction): + if interaction.user == ctx.author: + if interaction.guild.me.guild_permissions.manage_channels: + channel = interaction.channel + newchannel = await channel.clone() + await newchannel.edit(position=channel.position) + + await channel.delete() + embed = discord.Embed( + description="Channel has been Successfully nuked by **`%s`**" % (ctx.author), + color=self.color) + embed.set_author(name="Channel Nuked", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + embed.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await newchannel.send(embed=embed) + else: + await interaction.response.edit_message( + content= + f"{ZWARNING} | It seems I'm missing the necessary permissions. Please grant me the `manage channel` permissions and try again.", + embed=None, + view=None) + else: + await interaction.response.send_message("Oops! It looks like that message isn't from you. You need to run the command yourself to interact with it.", + embed=None, + view=None, + ephemeral=True) + + async def button1_callback(interaction: discord.Interaction): + if interaction.user == ctx.author: + await interaction.response.edit_message( + content="Cancelled, I won't proceed with nuking channel.", embed=None, view=None) + else: + await interaction.response.send_message("Oops! It looks like that message isn't from you. You need to run the command yourself to interact with it.", + embed=None, + view=None, + ephemeral=True) + + embed = discord.Embed( + color=self.color, description='**Do you really want to nuke the channel?**') + + view = View() + button.callback = button_callback + button1.callback = button1_callback + view.add_item(button) + view.add_item(button1) + embed.set_footer(text="Please click either 'Confirm' or 'Cancel' to proceed. You have 30 seconds to decide!") + await ctx.reply(embed=embed, view=view, mention_author=False,delete_after=30) + + + + @commands.hybrid_command(name="slowmode", + help="Changes the slowmode", + usage="slowmode [seconds]", + aliases=["slow"]) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.has_permissions(manage_messages=True) + async def _slowmode(self, ctx: commands.Context, seconds: int = 0): + if seconds > 120: + embed=discord.Embed(description="Slowmode can not be over 2 minutes", + color=self.color) + embed.set_author(name="Error", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + embed.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + return await ctx.send(embed=embed) + if seconds == 0: + await ctx.channel.edit(slowmode_delay=seconds) + await ctx.send(embed=discord.Embed( + title="Slowmode", description="Slowmode is disabled", color=self.color)) + else: + await ctx.channel.edit(slowmode_delay=seconds) + embed=discord.Embed(description="Successfully Set slowmode to **`%s`**" % (seconds), + color=self.color) + embed.set_author(name="Slowmode Activated", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + embed.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.send(embed=embed) + + + @commands.hybrid_command(name="unslowmode", + help="Disables slowmode", + usage="unslowmode", + aliases=["unslow"]) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 2, commands.BucketType.user) + @commands.has_permissions(manage_messages=True) + @commands.bot_has_permissions(manage_messages=True) + async def _unslowmode(self, ctx: commands.Context): + await ctx.channel.edit(slowmode_delay=0) + embed=discord.Embed(description="Successfully Disabled slowmode", color=self.color) + embed.set_author(name="Unslowmode", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + embed.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.send(embed=embed) + + + + @commands.command(aliases=["deletesticker", "removesticker"], description="Delete the sticker from the server") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(manage_emojis=True) + @commands.bot_has_permissions(manage_emojis=True) + async def delsticker(self, ctx: commands.Context, *, name=None): + if ctx.message.reference is None: + return await ctx.reply("No replied message found") + msg = await ctx.channel.fetch_message(ctx.message.reference.message_id) + if len(msg.stickers) == 0: + return await ctx.reply("No sticker found") + try: + name = "" + for i in msg.stickers: + name = i.name + await ctx.guild.delete_sticker(i) + await ctx.reply(f"{TICK}> Sucessfully deleted sticker named `{name}`") + except: + await ctx.reply("Failed to delete the sticker") + + + @commands.command(aliases=["deleteemoji", "removeemoji"], description="Deletes the emoji from the server") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(manage_emojis=True) + async def delemoji(self, ctx, emoji: str = None): + init_message = await ctx.reply("Processing to delete emojis...", mention_author=False) + message_content = None + + + if ctx.message.reference is not None: + referenced_message = await ctx.channel.fetch_message(ctx.message.reference.message_id) + message_content = str(referenced_message.content) + else: + message_content = str(ctx.message.content) + + + if message_content: + + emoji_pattern = r"" + found_emojis = re.findall(emoji_pattern, message_content) + delete_count = 0 + + if len(found_emojis) != 0: + + if len(found_emojis) > 15: + await init_message.delete() + return await ctx.reply("Maximum 15 emojis can be deleted at a time.") + + + for emoji_id in found_emojis: + try: + emoji_to_delete = await ctx.guild.fetch_emoji(int(emoji_id)) + await emoji_to_delete.delete(reason=f"Deleted by {ctx.author}") + delete_count += 1 + except discord.NotFound: + continue + except discord.Forbidden: + continue + await init_message.delete() + return await ctx.reply(f"{TICK}> | Successfully deleted {delete_count}/{len(found_emojis)} emoji(s).") + + + await init_message.delete() + return await ctx.reply("No valid emoji found to delete.") + + + + + @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): + + if role.position >= ctx.guild.me.top_role.position: + error_embed = discord.Embed( + description=f"{role.mention} is higher than my role. Please move my role above it.", + color=self.color + ) + error_embed.set_author(name="Error", icon_url="https://cdn.discordapp.com/avatars/1396114795102470196/198b9bc616ec574f6fd2f7121a1d3abc.png?size=1024") + error_embed.set_footer( + text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url + ) + return await ctx.send(embed=error_embed) + + + if ctx.author != ctx.guild.owner and ctx.author.top_role.position <= role.position: + error_embed = discord.Embed( + description=f"{role.mention} has the same or a higher position than your top role!", + color=self.color + ) + error_embed.set_author(name="Error") + error_embed.set_footer( + text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url + ) + return await ctx.send(embed=error_embed) + + + if icon is None: + attachment_found = False + attachment_url = None + for attachment in ctx.message.attachments: + attachment_url = attachment.url + attachment_found = True + + if attachment_found: + try: + async with aiohttp.request("GET", attachment_url) as r: + image_data = await r.read() + await role.edit(display_icon=image_data) + success_embed = discord.Embed( + description=f"Successfully changed the icon of {role.mention}.", + color=self.color + ) + success_embed.set_author(name="Icon Updated") + success_embed.set_footer( + text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url + ) + return await ctx.send(embed=success_embed) + except Exception as e: + print(e) + return await ctx.reply("Failed to change the icon of the role.") + else: + await role.edit(display_icon=None) + removal_embed = discord.Embed( + description=f"Successfully removed the icon from {role.mention}.", + color=self.color + ) + removal_embed.set_author(name="Icon Removed") + removal_embed.set_footer( + text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url + ) + return await ctx.reply(embed=removal_embed, mention_author=False) + + + if isinstance(icon, discord.Emoji) or isinstance(icon, discord.PartialEmoji): + emoji_url = f"https://cdn.discordapp.com/emojis/{icon.id}.png" + try: + async with aiohttp.request("GET", emoji_url) as r: + image_data = await r.read() + await role.edit(display_icon=image_data) + success_embed = discord.Embed( + description=f"Successfully changed the icon for {role.mention} to {icon}.", + color=self.color + ) + success_embed.set_author(name="Icon Updated") + success_embed.set_footer( + text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url + ) + return await ctx.reply(embed=success_embed, mention_author=False) + except Exception as e: + print(e) + return await ctx.reply("Failed to change the icon of the role.") + + + else: + if not icon.startswith("https://"): + return await ctx.reply("Please provide a valid link.") + try: + async with aiohttp.request("GET", icon) as r: + image_data = await r.read() + await role.edit(display_icon=image_data) + success_embed = discord.Embed( + description=f"{TICK}>| Successfully changed the icon for {role.mention}.", + color=self.color + ) + return await ctx.reply(embed=success_embed, mention_author=False) + except Exception as e: + print(e) + return await ctx.reply("An error occurred while changing the icon for the role.") + + + + @commands.hybrid_command(name="unbanall", + help="Unbans Everyone In The Guild!", + aliases=['massunban'], + usage="Unbanall", + with_app_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 30, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(ban_members=True) + async def unbanall(self, ctx): + button = Button(label="Confirm", + style=discord.ButtonStyle.green, + emoji=f"{TICK}>") + button1 = Button(label="Cancel", + style=discord.ButtonStyle.red, + emoji=CROSS) + + async def button_callback(interaction: discord.Interaction): + a = 0 + if interaction.user == ctx.author: + if interaction.guild.me.guild_permissions.ban_members: + await interaction.response.edit_message( + content="Unbanning All Banned Members...", embed=None, view=None) + async for idk in interaction.guild.bans(limit=None): + await interaction.guild.unban( + user=idk.user, + reason="Unbanall Command Executed By: {}".format(ctx.author)) + a += 1 + await interaction.channel.send( + content=f"{TICK}> Successfully Unbanned {a} Members") + else: + await interaction.response.edit_message( + content= + "I am missing `ban members` in this Guil.", + embed=None, + view=None) + else: + await interaction.response.send_message("Uh oh! That message doesn't belong to you.\nYou must run this command to interact with it.", + embed=None, + view=None, + ephemeral=True) + + async def button1_callback(interaction: discord.Interaction): + if interaction.user == ctx.author: + await interaction.response.edit_message( + content="Cancelled, I will Not unban anyone.", embed=None, view=None) + else: + await interaction.response.send_message("Uh oh! That message doesn't belong to you.\nYou must run this command to interact with it.", + embed=None, + view=None, + ephemeral=True) + + embed = discord.Embed( + color=self.color, + description='**Are you sure you want to unban all members in this guild?**') + + view = View() + button.callback = button_callback + button1.callback = button1_callback + view.add_item(button) + view.add_item(button1) + await ctx.reply(embed=embed, view=view, mention_author=False) + + @commands.hybrid_command(name="audit", + help="See recents audit log action in the server .") + @blacklist_check() + @ignore_check() + @commands.has_permissions(view_audit_log=True) + @commands.bot_has_permissions(view_audit_log=True) + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + async def auditlog(self, ctx, limit: int): + if limit >= 31: + await ctx.reply( + "Action rejected, you are not allowed to fetch more than `30` entries.", + mention_author=False) + return + idk = [] + str = "" + async for entry in ctx.guild.audit_logs(limit=limit): + idk.append(f'''User: `{entry.user}` +Action: `{entry.action}` +Target: `{entry.target}` +Reason: `{entry.reason}`\n\n''') + for n in idk: + str += n + str = str.replace("AuditLogAction.", "") + embed = discord.Embed(title=f"Audit Logs Of {ctx.guild.name}", + description=f">>> {str}", + color=0xFF0000) + embed.set_footer(text=f"Audit Log Actions For {ctx.guild.name}") + await ctx.reply(embed=embed, mention_author=False) + diff --git a/bot/cogs/moderation/role.py b/bot/cogs/moderation/role.py new file mode 100644 index 0000000..78ef750 --- /dev/null +++ b/bot/cogs/moderation/role.py @@ -0,0 +1,943 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CROSS, TICK, ZWARNING +import discord +from discord.ext import commands, tasks +import asyncio +import datetime +import re +from typing import * +from utils.Tools import * +from discord.ui import Button, View +from typing import Union, Optional +from typing import Union, Optional +from io import BytesIO +import requests +import aiohttp +import time +from datetime import datetime, timezone, timedelta + + +time_regex = re.compile(r"(?:(\d{1,5})(h|s|m|d))+?") +time_dict = {"h": 3600, "s": 1, "m": 60, "d": 86400} + + +def convert(argument): + args = argument.lower() + matches = re.findall(time_regex, args) + time = 0 + for key, value in matches: + try: + time += time_dict[value] * float(key) + except KeyError: + raise commands.BadArgument( + f"{value} is an invalid time key! h|m|s|d are valid arguments") + except ValueError: + raise commands.BadArgument(f"{key} is not a number!") + return round(time) + + +class Role(commands.Cog): + + def __init__(self, bot): + self.bot = bot + self.color = 0xFF0000 + + + @commands.group(name="role",invoke_without_command=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.has_permissions(manage_roles=True) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @top_check() + @blacklist_check() + async def role(self, ctx, member: discord.Member, *, role: discord.Role): + if not ctx.guild.me.guild_permissions.manage_roles: + return await ctx.send(f"{ZWARNING} I don't have permission to manage roles!") + + if role >= ctx.guild.me.top_role: + error = discord.Embed( + color=self.color, + description="I can't manage roles for a user with a higher or equal role!" + ) + + error.set_author(name="Error") + error.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + return await ctx.send(embed=error) + + if ctx.author != ctx.guild.owner and ctx.author.top_role <= member.top_role: + error = discord.Embed( + color=self.color, + description="You can't manage roles for a user with a higher or equal role than yours!" + ) + error.set_author(name="Access Denied") + error.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + return await ctx.send(embed=error) + + try: + if role not in member.roles: + await member.add_roles(role, reason=f"Role added by {ctx.author} (ID: {ctx.author.id})") + success = discord.Embed( + color=self.color, + description=f"Successfully **added** role {role.name} to {member.mention}." + ) + success.set_author(name="Role Added") + success.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + else: + await member.remove_roles(role, reason=f"Role removed by {ctx.author} (ID: {ctx.author.id})") + success = discord.Embed( + color=self.color, + description=f"Successfully **removed** role {role.name} from {member.mention}." + ) + success.set_author(name="Role Removed") + success.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.send(embed=success) + except discord.Forbidden: + error = discord.Embed( + color=self.color, + description=f"{ZWARNING} I don't have permission to manage roles for this user!" + ) + await ctx.send(embed=error) + except Exception as e: + error = discord.Embed( + color=self.color, + description=f"{ZWARNING} An unexpected error occurred: {str(e)}" + ) + await ctx.send(embed=error) + + + @role.command(help="Give role to member for particular time") + @commands.bot_has_permissions(manage_roles=True) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 7, commands.BucketType.user) + @commands.has_permissions(manage_roles=True) + @commands.bot_has_permissions(manage_roles=True) + async def temp(self, ctx, role: discord.Role, time, *, user: discord.Member): + if ctx.author != ctx.guild.owner and role.position >= ctx.author.top_role.position: + embed = discord.Embed( + description=f"You can't manage a role that is higher or equal to your top role!", + color=self.color + ) + embed.set_author(name="Error") + embed.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + return await ctx.send(embed=embed) + + else: + if role.position >= ctx.guild.me.top_role.position: + embed1 = discord.Embed( + description= + f"{role} is higher than my top role, move my role above {role}.", + color=self.color) + embed1.set_author(name="Error") + embed1.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + return await ctx.send(embed=embed1) + seconds = convert(time) + await user.add_roles(role, reason=None) + success = discord.Embed( + description= + f"Successfully added {role.mention} to {user.mention} .", + color=self.color) + success.set_author(name="Success") + success.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.send(embed=success) + await asyncio.sleep(seconds) + await user.remove_roles(role) + + + @role.command(help="Delete a role in the guild") + @blacklist_check() + @ignore_check() + @top_check() + @commands.cooldown(1, 7, commands.BucketType.user) + @commands.has_permissions(manage_roles=True) + @commands.bot_has_permissions(manage_roles=True) + async def delete(self, ctx, *, role: discord.Role): + if ctx.author != ctx.guild.owner and role.position >= ctx.author.top_role.position: + embed = discord.Embed( + description=f"You cannot delete a role that is higher or equal to your top role!", + color=self.color + ) + embed.set_author(name="Error") + embed.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + return await ctx.send(embed=embed) + + if role.position >= ctx.guild.me.top_role.position: + embed = discord.Embed( + description=f"I cannot delete {role} because it is higher than my top role. Please move my role above {role}.", + color=self.color + ) + embed.set_author(name="Error") + embed.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + return await ctx.send(embed=embed) + + if role is None: + embed = discord.Embed( + description=f"No role named {role} found in this server.", + color=self.color + ) + embed.set_author(name="Error") + embed.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + return await ctx.send(embed=embed) + + await role.delete() + + embed = discord.Embed( + description=f"Successfully deleted {role}.", + color=self.color + ) + embed.set_author(name="Success") + embed.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.send(embed=embed) + + @role.command(help="Create a role in the guild") + @blacklist_check() + @ignore_check() + @top_check() + @commands.cooldown(1, 7, commands.BucketType.user) + @commands.has_permissions(administrator=True) + @commands.bot_has_permissions(manage_roles=True) + async def create(self, ctx, *, name): + embed = discord.Embed( + description=f"Successfully created a role named {name}.", + color=self.color + ) + embed.set_author(name="Success") + embed.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.guild.create_role(name=name, color=discord.Color.default()) + await ctx.send(embed=embed) + + + @role.command(help="Renames a role in the server.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.has_permissions(administrator=True) + @commands.bot_has_permissions(manage_roles=True) + async def rename(self, ctx, role: discord.Role, *, newname): + + if role.position >= ctx.author.top_role.position: + embed = discord.Embed( + description=f"You can't manage the role {role.mention} because it is higher or equal to your top role.", + color=self.color + ) + embed.set_author(name="Error") + embed.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + return await ctx.send(embed=embed) + + + if role.position >= ctx.guild.me.top_role.position: + embed = discord.Embed( + description=f"I can't manage the role {role.mention} because it is higher than my top role.", + color=self.color + ) + embed.set_author(name="Error") + embed.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + return await ctx.send(embed=embed) + + await role.edit(name=newname) + embed = discord.Embed( + description=f"Role {role.name} has been renamed to {newname}.", + color=self.color + ) + embed.set_author(name="Success") + embed.set_footer(text=f"Requested by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.send(embed=embed) + + @role.command(name="humans", help="Gives role to all humans in the guild") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 15, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def role_humans(self, ctx, *, role: discord.Role): + if ctx.author == ctx.guild.owner or ctx.author.top_role.position > ctx.guild.me.top_role.position: + button = Button(label="Confirm", + style=discord.ButtonStyle.green, + emoji=f"{TICK}>") + button1 = Button(label="Cancel", + style=discord.ButtonStyle.red, + emoji=CROSS) + + async def button_callback(interaction: discord.Interaction): + count = 0 + if interaction.user == ctx.author: + if interaction.guild.me.guild_permissions.manage_roles: + embed1 = discord.Embed( + color=self.color, + description=f"Assigning {role.mention} to all humans...") + await interaction.response.edit_message(embed=embed1, view=None) + for member in interaction.guild.members: + if not member.bot and role not in member.roles: + try: + await member.add_roles(role, reason=f"Role Humans Command Executed By: {ctx.author}") + count += 1 + except Exception as e: + print(e) + + await interaction.channel.send( + content=f"{TICK}> | Successfully assigned {role.mention} to {count} human(s).") + else: + await interaction.response.edit_message( + content=f"{ZWARNING} I am missing the required permissions. Please grant the necessary permissions and try again.", + embed=None, + view=None) + else: + await interaction.response.send_message( + "This action is not for you!", + embed=None, + view=None, + ephemeral=True) + + async def button1_callback(interaction: discord.Interaction): + if interaction.user == ctx.author: + embed2 = discord.Embed( + color=self.color, + description=f"Action cancelled. No humans will be assigned the role {role.mention}.") + await interaction.response.edit_message(embed=embed2, view=None) + else: + await interaction.response.send_message( + "This action is not for you!", + embed=None, + view=None, + ephemeral=True) + + members_without_role = [member for member in ctx.guild.members if not member.bot and role not in member.roles] + if len(members_without_role) == 0: + return await ctx.reply(embed=discord.Embed(description=f"{ZWARNING} | All humans already have the {role.mention} role.", color=self.color)) + else: + embed = discord.Embed( + color=self.color, + description=f"Are you sure you want to assign {role.mention} to {len(members_without_role)} members?") + view = View() + button.callback = button_callback + button1.callback = button1_callback + view.add_item(button) + view.add_item(button1) + await ctx.reply(embed=embed, view=view, mention_author=False) + + else: + denied = discord.Embed(title=f"{ZWARNING} Access Denied", + description="Your role should be above my top role.", + color=0xFF0000) + denied.set_footer(text=f"“{ctx.command.qualified_name}” Command executed by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.send(embed=denied, mention_author=False) + + + + @role.command(name="bots", help="Gives role to all the bots in the guild") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def role_bots(self, ctx, *, role: discord.Role): + if ctx.author == ctx.guild.owner or ctx.author.top_role.position > ctx.guild.me.top_role.position: + button = Button(label="Confirm", + style=discord.ButtonStyle.green, + emoji=f"{TICK}>") + button1 = Button(label="Cancel", + style=discord.ButtonStyle.red, + emoji=CROSS) + + async def button_callback(interaction: discord.Interaction): + count = 0 + if interaction.user == ctx.author: + if interaction.guild.me.guild_permissions.manage_roles: + embed1 = discord.Embed( + color=self.color, + description=f"Adding {role.mention} to all bots...") + await interaction.response.edit_message(embed=embed1, view=None) + for member in interaction.guild.members: + if member.bot and role not in member.roles: + try: + await member.add_roles(role, reason=f"Role Bots Command Executed By: {ctx.author}") + count += 1 + except Exception as e: + print(e) + + await interaction.channel.send( + content=f"{TICK}> | Successfully added {role.mention} to {count} bot(s).") + else: + await interaction.response.edit_message( + content="I am missing the required permission. Please grant the necessary permissions and try again.", + embed=None, + view=None) + else: + await interaction.response.send_message( + "This action is not for you!", + embed=None, + view=None, + ephemeral=True) + + async def button1_callback(interaction: discord.Interaction): + if interaction.user == ctx.author: + embed2 = discord.Embed( + color=self.color, + description=f"Action cancelled. No bots will be assigned the role {role.mention}.") + await interaction.response.edit_message(embed=embed2, view=None) + else: + await interaction.response.send_message( + "This action is not for you!", + embed=None, + view=None, + ephemeral=True) + + bots_without_role = [member for member in ctx.guild.members if member.bot and role not in member.roles] + if len(bots_without_role) == 0: + return await ctx.reply(embed=discord.Embed(description=f"{ZWARNING} | All bots already have the {role.mention} role.", color=self.color)) + else: + embed = discord.Embed( + color=self.color, + description=f"**Are you sure you want to give {role.mention} to {len(bots_without_role)} bots?**") + view = View() + button.callback = button_callback + button1.callback = button1_callback + view.add_item(button) + view.add_item(button1) + await ctx.reply(embed=embed, view=view, mention_author=False) + + else: + denied = discord.Embed(title=f"{CROSS} Access Denied", + description="Your role should be above my top role.", + color=0xFF0000) + denied.set_footer(text=f"“{ctx.command.qualified_name}” Command executed by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.send(embed=denied, mention_author=False) + + + + @role.command(name="unverified", help="Gives role to all the unverified members in the guild") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def role_unverified(self, ctx, *, role: discord.Role): + if ctx.author == ctx.guild.owner or ctx.author.top_role.position > ctx.guild.me.top_role.position: + button = Button(label="Confirm", + style=discord.ButtonStyle.green, + emoji=f"{TICK}>") + button1 = Button(label="Cancel", + style=discord.ButtonStyle.red, + emoji=CROSS) + + async def button_callback(interaction: discord.Interaction): + count = 0 + if interaction.user == ctx.author: + if interaction.guild.me.guild_permissions.manage_roles: + embed1 = discord.Embed( + color=self.color, + description=f"Adding {role.mention} to all unverified members.") + await interaction.response.edit_message(embed=embed1, view=None) + for member in interaction.guild.members: + if member.avatar is None and role not in member.roles: + try: + await member.add_roles(role, reason=f"Role Unverified Command Executed By: {ctx.author}") + count += 1 + except Exception as e: + print(e) + + await interaction.channel.send( + content=f"{TICK}> | Successfully added {role.mention} to {count} unverified member(s).") + else: + await interaction.response.edit_message( + content="I am missing the required permission. Please grant the necessary permissions and try again.", + embed=None, + view=None) + else: + await interaction.response.send_message( + "This action is not for you!", + embed=None, + view=None, + ephemeral=True) + + async def button1_callback(interaction: discord.Interaction): + if interaction.user == ctx.author: + embed2 = discord.Embed( + color=self.color, + description=f"Action cancelled. No unverified members will be assigned the role {role.mention}.") + await interaction.response.edit_message(embed=embed2, view=None) + else: + await interaction.response.send_message( + "This action is not for you!", + embed=None, + view=None, + ephemeral=True) + + embed = discord.Embed( + color=self.color, + description=f'**Are you sure you want to give {role.mention} to all unverified members in this guild?**') + view = View() + button.callback = button_callback + button1.callback = button1_callback + view.add_item(button) + view.add_item(button1) + await ctx.reply(embed=embed, view=view, mention_author=False) + + else: + denied = discord.Embed(title=f"{CROSS} Access Denied", + description="Your role should be above my top role.", + color=0xFF0000) + denied.set_footer(text=f"“{ctx.command.qualified_name}” Command executed by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.send(embed=denied, mention_author=False) + + + @role.command(name="all", help="Gives role to all the members in the guild") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 15, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(administrator=True) + async def role_all(self, ctx, *, role: discord.Role): + if ctx.author == ctx.guild.owner or ctx.author.top_role.position > ctx.guild.me.top_role.position: + button = Button(label="Confirm", + style=discord.ButtonStyle.green, + emoji=f"{TICK}>") + button1 = Button(label="Cancel", + style=discord.ButtonStyle.red, + emoji=CROSS) + + async def button_callback(interaction: discord.Interaction): + count = 0 + if interaction.user == ctx.author: + if interaction.guild.me.guild_permissions.manage_roles: + embed1 = discord.Embed( + color=self.color, + description=f"Adding {role.mention} to all members.") + await interaction.response.edit_message(embed=embed1, view=None) + for member in interaction.guild.members: + try: + await member.add_roles(role, reason=f"Role All Command Executed By: {ctx.author}") + count += 1 + except Exception as e: + print(e) + + await interaction.channel.send( + content=f"{TICK}> | Successfully added {role.mention} to {count} member(s).") + else: + await interaction.response.edit_message( + content="I am missing the required permission. Please grant the necessary permissions and try again.", + embed=None, + view=None) + else: + await interaction.response.send_message( + "This action is not for you!", + embed=None, + view=None, + ephemeral=True) + + async def button1_callback(interaction: discord.Interaction): + if interaction.user == ctx.author: + embed2 = discord.Embed( + color=self.color, + description=f"Action cancelled. No members will be assigned the role {role.mention}.") + await interaction.response.edit_message(embed=embed2, view=None) + else: + await interaction.response.send_message( + "This action is not for you!", + embed=None, + view=None, + ephemeral=True) + + members_without_role = [member for member in ctx.guild.members if role not in member.roles] + if len(members_without_role) == 0: + return await ctx.reply(embed=discord.Embed(description=f"{ZWARNING} | {role.mention} is already given to all the members of the server.", color=self.color)) + else: + embed = discord.Embed( + color=self.color, + description=f'**Are you sure you want to give {role.mention} to {len(members_without_role)} members?**') + view = View() + button.callback = button_callback + button1.callback = button1_callback + view.add_item(button) + view.add_item(button1) + await ctx.reply(embed=embed, view=view, mention_author=False) + else: + denied = discord.Embed(title=f"{CROSS} Access Denied", + description="Your role should be above my top role.", + color=0xFF0000) + denied.set_footer(text=f"“{ctx.command.qualified_name}” Command executed by {ctx.author}", + 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.group(name="removerole",invoke_without_command=True, + aliases=['rrole'], + help="remove a role from all members .") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 5, commands.BucketType.user) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @blacklist_check() + @commands.has_permissions(administrator=True) + async def rrole(self,ctx): + if ctx.subcommand_passed is None: + await ctx.send_help(ctx.command) + ctx.command.reset_cooldown(ctx) + + + @rrole.command(name="humans", help="Removes a role from all the humans in the server.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def rrole_humans(self, ctx, *, role: discord.Role): + if ctx.author == ctx.guild.owner or ctx.author.top_role.position > ctx.guild.me.top_role.position: + button = Button(label="Confirm", + style=discord.ButtonStyle.green, + emoji=f"{TICK}>") + button1 = Button(label="Cancel", + style=discord.ButtonStyle.red, + emoji=CROSS) + + async def button_callback(interaction: discord.Interaction): + count = 0 + if interaction.user == ctx.author: + if interaction.guild.me.guild_permissions.manage_roles: + embed1 = discord.Embed( + color=self.color, + description=f"Removing {role.mention} from all humans.") + await interaction.response.edit_message(embed=embed1, view=None) + for member in interaction.guild.members: + if not member.bot and role in member.roles: + try: + await member.remove_roles(role, reason=f"Remove Role Humans Command Executed By: {ctx.author}") + count += 1 + except Exception as e: + print(e) + + await interaction.channel.send( + content=f"{TICK}>| Successfully removed {role.mention} from {count} human(s).") + else: + await interaction.response.edit_message( + content="I am missing the required permission. Please grant the necessary permissions and try again.", + embed=None, + view=None) + else: + await interaction.response.send_message( + "This action is not for you!", + embed=None, + view=None, + ephemeral=True) + + async def button1_callback(interaction: discord.Interaction): + if interaction.user == ctx.author: + embed2 = discord.Embed( + color=self.color, + description=f"Action cancelled. {role.mention} will not be removed from any humans.") + await interaction.response.edit_message(embed=embed2, view=None) + else: + await interaction.response.send_message( + "This action is not for you!", + embed=None, + view=None, + ephemeral=True) + + humans_with_role = [member for member in ctx.guild.members if not member.bot and role in member.roles] + if len(humans_with_role) == 0: + return await ctx.reply(embed=discord.Embed(description=f"| Already no humans have {role.mention}.", color=self.color)) + else: + embed = discord.Embed( + color=self.color, + description=f'**Are you sure you want to remove {role.mention} from {len(humans_with_role)} humans in this guild?**') + view = View() + button.callback = button_callback + button1.callback = button1_callback + view.add_item(button) + view.add_item(button1) + await ctx.reply(embed=embed, view=view, mention_author=False) + else: + denied = discord.Embed(title=f"{CROSS} Access Denied", + description="Your role should be above my top role.", + color=0xFF0000) + denied.set_footer(text=f"“{ctx.command.qualified_name}” Command executed by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.send(embed=denied, mention_author=False) + + await ctx.send(embed=embed, mention_author=False) + + + + @rrole.command(name="bots", help="Removes a role from all the bots in the server.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def rrole_bots(self, ctx, *, role: discord.Role): + if ctx.author == ctx.guild.owner or ctx.author.top_role.position > ctx.guild.me.top_role.position: + button = Button(label="Confirm", + style=discord.ButtonStyle.green, + emoji=f"{TICK}>") + button1 = Button(label="Cancel", + style=discord.ButtonStyle.red, + emoji=CROSS) + + async def button_callback(interaction: discord.Interaction): + count = 0 + if interaction.user == ctx.author: + if interaction.guild.me.guild_permissions.manage_roles: + embed1 = discord.Embed( + color=self.color, + description=f"Removing {role.mention} from all bots...") + await interaction.response.edit_message(embed=embed1, view=None) + for member in interaction.guild.members: + if member.bot and role in member.roles: + try: + await member.remove_roles(role, reason=f"Remove Role Bots Command Executed By: {ctx.author}") + count += 1 + except Exception as e: + print(e) + + await interaction.channel.send( + content=f"{TICK}> | Successfully removed {role.mention} from {count} bot(s).") + else: + await interaction.response.edit_message( + content="I am missing the required permission. Please grant the necessary permissions and try again.", + embed=None, + view=None) + else: + await interaction.response.send_message( + "This action is not for you!", + embed=None, + view=None, + ephemeral=True) + + async def button1_callback(interaction: discord.Interaction): + if interaction.user == ctx.author: + embed2 = discord.Embed( + color=self.color, + description=f"Action cancelled. {role.mention} will not be removed from any bots.") + await interaction.response.edit_message(embed=embed2, view=None) + else: + await interaction.response.send_message( + "This action is not for you!", + embed=None, + view=None, + ephemeral=True) + + bots_with_role = [member for member in ctx.guild.members if member.bot and role in member.roles] + if len(bots_with_role) == 0: + return await ctx.reply(embed=discord.Embed(description=f"| Already no bots have {role.mention}.", color=self.color)) + else: + embed = discord.Embed( + color=self.color, + description=f'**Are you sure you want to remove {role.mention} from {len(bots_with_role)} bots in this guild?**') + view = View() + button.callback = button_callback + button1.callback = button1_callback + view.add_item(button) + view.add_item(button1) + await ctx.reply(embed=embed, view=view, mention_author=False) + else: + denied = discord.Embed(title=f"{CROSS} Access Denied", + description="Your role should be above my top role.", + color=0xFF0000) + denied.set_footer(text=f"“{ctx.command.qualified_name}” Command executed by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.send(embed=denied, mention_author=False) + + + + + + @rrole.command(name="all", help="Removes a role from all members in the server.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def rrole_all(self, ctx, *, role: discord.Role): + if ctx.author == ctx.guild.owner or ctx.author.top_role.position > ctx.guild.me.top_role.position: + button = Button(label="Confirm", + style=discord.ButtonStyle.green, + emoji=f"{TICK}>") + button1 = Button(label="Cancel", + style=discord.ButtonStyle.red, + emoji=CROSS) + + async def button_callback(interaction: discord.Interaction): + removed_count = 0 + if interaction.user == ctx.author: + if interaction.guild.me.guild_permissions.manage_roles: + embed1 = discord.Embed( + color=self.color, + description=f"Removing {role.mention} from all members.") + await interaction.response.edit_message(embed=embed1, view=None) + + for member in interaction.guild.members: + if role in member.roles: + try: + await member.remove_roles(role, reason=f"Remove Role All Command Executed By: {ctx.author}") + removed_count += 1 + except Exception as e: + print(e) + + await interaction.channel.send( + content=f"{TICK}>| Successfully removed {role.mention} from {removed_count} member(s).") + else: + await interaction.response.edit_message( + content="I am missing the required permission. Please grant the necessary permissions and try again.", + embed=None, + view=None) + else: + await interaction.response.send_message("This action is not for you!", + embed=None, + view=None, + ephemeral=True) + + async def button1_callback(interaction: discord.Interaction): + if interaction.user == ctx.author: + embed2 = discord.Embed( + color=self.color, + description=f"Action cancelled. {role.mention} will not be removed from anyone.") + await interaction.response.edit_message(embed=embed2, view=None) + else: + await interaction.response.send_message("This action is not for you!", + embed=None, + view=None, + ephemeral=True) + + members_with_role = [member for member in ctx.guild.members if role in member.roles] + if len(members_with_role) == 0: + return await ctx.reply(embed=discord.Embed(description=f"| No members currently have {role.mention}.", color=self.color)) + else: + embed = discord.Embed( + color=self.color, + description=f'**Are you sure you want to remove {role.mention} from {len(members_with_role)} members in this guild?**') + view = View() + button.callback = button_callback + button1.callback = button1_callback + view.add_item(button) + view.add_item(button1) + await ctx.reply(embed=embed, view=view, mention_author=False) + else: + denied = discord.Embed(title=f"{CROSS} Access Denied", + description="Your role should be above my top role.", + color=0xFF0000) + denied.set_footer(text=f"“{ctx.command.qualified_name}” Command executed by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.send(embed=denied, mention_author=False) + + + + + @rrole.command(name="unverified", help="Removes a role from all the unverified members in the server.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.user) + @commands.has_permissions(administrator=True) + async def rrole_unverified(self, ctx, *, role: discord.Role): + if ctx.author == ctx.guild.owner or ctx.author.top_role.position > ctx.guild.me.top_role.position: + button = Button(label="Yes", + style=discord.ButtonStyle.green, + emoji=f"{TICK}>") + button1 = Button(label="No", + style=discord.ButtonStyle.red, + emoji=CROSS) + + async def button_callback(interaction: discord.Interaction): + count = 0 + if interaction.user == ctx.author: + if interaction.guild.me.guild_permissions.manage_roles: + embed1 = discord.Embed( + color=self.color, + description=f"Removing {role.mention} from all unverified members.") + await interaction.response.edit_message(embed=embed1, view=None) + + for member in interaction.guild.members: + if member.avatar is None and role in member.roles: + try: + await member.remove_roles(role, reason=f"Remove Role Unverified Command Executed By: {ctx.author}") + count += 1 + except Exception as e: + print(e) + + await interaction.channel.send( + content=f"{TICK}> | Successfully removed {role.mention} from {count} unverified member(s).") + else: + await interaction.response.edit_message( + content="I am missing the required permission. Please grant the necessary permissions and try again.", + embed=None, + view=None) + else: + await interaction.response.send_message( + "This action is not for you!", + embed=None, + view=None, + ephemeral=True) + + async def button1_callback(interaction: discord.Interaction): + if interaction.user == ctx.author: + embed2 = discord.Embed( + color=self.color, + description=f"Action cancelled. {role.mention} will not be removed from any unverified members.") + await interaction.response.edit_message(embed=embed2, view=None) + else: + await interaction.response.send_message( + "This action is not for you!", + embed=None, + view=None, + ephemeral=True) + + unverified_members = [member for member in ctx.guild.members if member.avatar is None and role in member.roles] + if len(unverified_members) == 0: + return await ctx.reply(embed=discord.Embed(description=f"| Already no unverified members have {role.mention}.", color=self.color)) + else: + embed = discord.Embed( + color=self.color, + description=f'**Are you sure you want to remove {role.mention} from {len(unverified_members)} unverified members in this guild?**') + view = View() + button.callback = button_callback + button1.callback = button1_callback + view.add_item(button) + view.add_item(button1) + await ctx.reply(embed=embed, view=view, mention_author=False) + else: + denied = discord.Embed(title=f"{CROSS} Access Denied", + description="Your role should be above my top role.", + color=0xFF0000) + denied.set_footer(text=f"“{ctx.command.qualified_name}” Command executed by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url) + await ctx.send(embed=denied, mention_author=False) + + + + \ No newline at end of file diff --git a/bot/cogs/moderation/snipe.py b/bot/cogs/moderation/snipe.py new file mode 100644 index 0000000..853e64e --- /dev/null +++ b/bot/cogs/moderation/snipe.py @@ -0,0 +1,73 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ERROR +from discord.ext import commands +from datetime import datetime + +class Snipe(commands.Cog): + def __init__(self, bot): + self.bot = bot + # This dictionary will store the last deleted message for each channel. + # Format: {channel_id: {'content': '...', 'author': '...', 'deleted_at': ...}} + self.sniped_messages = {} + + @commands.Cog.listener() + async def on_message_delete(self, message): + # Ignore messages from bots or from DMs + if message.author.bot or not message.guild: + return + + # Store the message details + self.sniped_messages[message.channel.id] = { + 'content': message.content, + 'author': message.author, + 'deleted_at': datetime.utcnow() + } + + @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 + if ctx.channel.id in self.sniped_messages: + sniped_data = self.sniped_messages[ctx.channel.id] + author = sniped_data['author'] + content = sniped_data['content'] + deleted_at = sniped_data['deleted_at'] + + # If the deleted message had no text content (e.g., only an image) + if not content: + content = "No text content was found in the deleted message." + + # Create the simple embed + embed = discord.Embed( + description=content, + color=0xFF0000, + timestamp=deleted_at + ) + embed.set_author(name=f"Sniped message from {author.name}", icon_url=author.display_avatar.url) + embed.set_footer(text="Deleted at") # The timestamp is automatically formatted in the footer + + await ctx.send(embed=embed) + else: + # Send an error message if no message is stored for this channel + embed = discord.Embed( + description=f"{ERROR} | There are no deleted messages to snipe in this channel.", + color=0xFF0000 + ) + await ctx.send(embed=embed) + +async def setup(bot): + await bot.add_cog(Snipe(bot)) diff --git a/bot/cogs/moderation/snipe.txt b/bot/cogs/moderation/snipe.txt new file mode 100644 index 0000000..1b0034c --- /dev/null +++ b/bot/cogs/moderation/snipe.txt @@ -0,0 +1,152 @@ +import discord +from utils.emoji import DELETE, DELETE_ALT1, FORWARD, REWIND +from discord.ext import commands +from datetime import datetime +from utils.Tools import * + +class SnipeView(discord.ui.View): + def __init__(self, bot, snipes, user_id): + super().__init__(timeout=120) + self.bot = bot + self.snipes = snipes + self.index = 0 + self.user_id = user_id + self.update_buttons() + + # def update_buttons(self): + # self.first_button.disabled = self.index == 0 or len(self.snipes) == 1 + # self.prev_button.disabled = self.index == 0 or len(self.snipes) == 1 + # self.next_button.disabled = self.index == len(self.snipes) - 1 or len(self.snipes) == 1 + # self.last_button.disabled = self.index == len(self.snipes) - 1 or len(self.snipes) == 1 + + # async def send_snipe_embed(self, interaction: discord.Interaction): + # snipe = self.snipes[self.index] + # embed = discord.Embed(color=0xFF0000) + # embed.set_author(name=f"Deleted Message {self.index + 1}/{len(self.snipes)}", icon_url=snipe['author_avatar']) + # uid = snipe['author_id'] + # display_name = snipe['author_name'] + # embed.description = ( + # f"**Author:** **[{display_name}](https://discord.com/users/{uid})**\n" + # f" **Author ID:** `{snipe['author_id']}`\n" + # f" **Author Mention:** <@{snipe['author_id']}>\n" + # f"**Deleted:** \n" + # ) + + # if snipe['content']: + # embed.add_field(name=f"{DELETE} **Content:**", value=snipe['content']) + # if snipe['attachments']: + # attachment_links = "\n".join([f"[{attachment['name']}]({attachment['url']})" for attachment in snipe['attachments']]) + # embed.add_field(name="**Attachments:**", value=attachment_links) + + # embed.set_footer(text=f"Total Deleted Messages: {len(self.snipes)} | Requested by {interaction.user}", icon_url=interaction.user.avatar.url) + # await interaction.response.edit_message(embed=embed, view=self) + + #async def interaction_check(self, interaction: discord.Interaction) -> bool: + # return interaction.user.id == self.user_id + + # @discord.ui.button(emoji=FORWARD, style=discord.ButtonStyle.secondary, custom_id="first") + # async def first_button(self, interaction: discord.Interaction, button: discord.ui.Button): + # self.index = 0 + # self.update_buttons() + # await self.send_snipe_embed(interaction) + + #@discord.ui.button(emoji=REWIND, style=discord.ButtonStyle.secondary, custom_id="previous") + # async def prev_button(self, interaction: discord.Interaction, button: discord.ui.Button): + # if self.index > 0: + # self.index -= 1 + # self.update_buttons() + # await self.send_snipe_embed(interaction) + + # @discord.ui.button(emoji=DELETE, style=discord.ButtonStyle.danger, custom_id="delete") + # async def delete_button(self, interaction: discord.Interaction, button: discord.ui.Button): + # await interaction.message.delete() + + # @discord.ui.button(emoji=FORWARD, style=discord.ButtonStyle.secondary, custom_id="next") + # async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button): + # if self.index < len(self.snipes) - 1: + # self.index += 1 + # self.update_buttons() + # await self.send_snipe_embed(interaction) + + # @discord.ui.button(emoji=REWIND, style=discord.ButtonStyle.secondary, custom_id="last") + # async def last_button(self, interaction: discord.Interaction, button: discord.ui.Button): + # self.index = len(self.snipes) - 1 + # self.update_buttons() + # await self.send_snipe_embed(interaction) + + #async def on_timeout(self): + # for child in self.children: + # child.disabled = True + # await self.message.edit(view=self) + + +class Snipe(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.snipes = {} + + @commands.Cog.listener() + async def on_message_delete(self, message): + if not message.guild or message.author.bot: + return + if message.channel.id not in self.snipes: + self.snipes[message.channel.id] = [] + if len(self.snipes[message.channel.id]) >= 10: + self.snipes[message.channel.id].pop(0) + + attachments = [] + if message.attachments: + attachments = [{'name': attachment.filename, 'url': attachment.url} for attachment in message.attachments] + + self.snipes[message.channel.id].insert(0, { + 'author_name': message.author.name, + 'author_avatar': message.author.display_avatar.url, + 'author_id': message.author.id, + 'content': message.content or None, + 'deleted_at': int(datetime.utcnow().timestamp()), + 'attachments': attachments + }) + + @commands.hybrid_command(name='snipe', help="Shows the recently deleted messages in the channel.") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 3, commands.BucketType.user) + @commands.has_permissions(manage_messages=True) + async def snipe(self, ctx): + channel_snipes = self.snipes.get(ctx.channel.id, []) + if not channel_snipes: + await ctx.send("No recently deleted messages found in this channel.") + return + + first_snipe = channel_snipes[0] + embed = discord.Embed(color=0xFF0000) + embed.set_author(name="Last Deleted Message", icon_url=first_snipe['author_avatar']) + uid = first_snipe['author_id'] + display_name = first_snipe['author_name'] + embed.description = ( + f" **Author:** **[{display_name}](https://discord.com/users/{uid})**\n" + f"**Author ID:** `{first_snipe['author_id']}`\n" + f"**Author Mention:** <@{first_snipe['author_id']}>\n" + f" **Deleted:** \n" + ) + + if first_snipe['content']: + embed.add_field(name=f"{DELETE_ALT1} **Content:**", value=first_snipe['content']) + if first_snipe['attachments']: + attachment_links = "\n".join([f"[{attachment['name']}]({attachment['url']})" for attachment in first_snipe['attachments']]) + embed.add_field(name="**Attachments:**", value=attachment_links) + + embed.set_footer(text=f"Total Deleted Messages: {len(channel_snipes)} | Requested by {ctx.author}", icon_url=ctx.author.avatar.url) + + view = SnipeView(self.bot, channel_snipes, ctx.author.id) + + if len(channel_snipes) > 1: + message = await ctx.send(embed=embed, view=view) + view.message = message + # else: + # view.first_button.disabled = True + # view.prev_button.disabled = True + # view.next_button.disabled = True + # view.last_button.disabled = True + message = await ctx.send(embed=embed, view=view) + view.message = message diff --git a/bot/cogs/moderation/timeout.py b/bot/cogs/moderation/timeout.py new file mode 100644 index 0000000..ee237b9 --- /dev/null +++ b/bot/cogs/moderation/timeout.py @@ -0,0 +1,222 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CROSS, DELETE, TICK, ZWARNING +from discord.ext import commands +from discord import ui +from datetime import timedelta +import re +from utils.Tools import * + +#class TimeoutView(ui.View): + # def __init__(self, user, author): + # super().__init__(timeout=120) + # self.user = user + # self.author = author + # self.message = None + # self.color = discord.Color.from_rgb(255, 0, 0) + + # async def interaction_check(self, interaction: discord.Interaction) -> bool: + # if interaction.user != self.author: + # await interaction.response.send_message("You are not allowed to interact with this!", ephemeral=True) + # return False + # return True + + # async def on_timeout(self): + # for item in self.children: + # item.disabled = True + # if self.message: + # await self.message.edit(view=self) + + # @ui.button(label="Unmute", style=discord.ButtonStyle.success) + #async def unmute(self, interaction: discord.Interaction, button: discord.ui.Button): + # modal = ReasonModal(user=self.user, author=self.author, view=self) + # await interaction.response.send_modal(modal) +# + # @ui.button(style=discord.ButtonStyle.gray, emoji=DELETE) + # async def delete(self, interaction: discord.Interaction, button: discord.ui.Button): + # await interaction.message.delete() +# +#class AlreadyTimedoutView(ui.View): + # def __init__(self, user, author): + # super().__init__(timeout=60) + # self.user = user + # self.author = author + # self.message = None + + # async def interaction_check(self, interaction: discord.Interaction) -> bool: + # if interaction.user != self.author: + # await interaction.response.send_message("You are not allowed to interact with this!", ephemeral=True) + # return False + # return True + + # async def on_timeout(self): + # for item in self.children: + # item.disabled = True + # if self.message: + # try: + # await self.message.edit(view=self) + # except Exception: + # pass + + # @ui.button(label="Unmute", style=discord.ButtonStyle.success) + # async def unmute(self, interaction: discord.Interaction, button: discord.ui.Button): + # modal = ReasonModal(user=self.user, author=self.author, view=self) + # await interaction.response.send_modal(modal) + + # @ui.button(style=discord.ButtonStyle.gray, emoji=DELETE) + # async def delete(self, interaction: discord.Interaction, button: discord.ui.Button): + # await interaction.message.delete() +# +#class ReasonModal(ui.Modal): + # def __init__(self, user, author, view): + # super().__init__(title="Unmute Reason") + # self.user = user + # self.author = author + # self.view = view + # self.reason_input = ui.TextInput(label="Reason for Unmuting", placeholder="Provide a reason to unmute or leave it blank.", required = False, max_length=2000, style=discord.TextStyle.paragraph) + # self.add_item(self.reason_input) + + # async def on_submit(self, interaction: discord.Interaction): + # reason = self.reason_input.value or "No reason provided" + # try: + # await self.user.send(f"You have been Unmuted in **{self.author.guild.name}** by **{self.author}**. Reason: {reason or 'No reason provided'}") + # dm_status = "Yes" + #except discord.Forbidden: + # dm_status = "No" + # except discord.HTTPException: + # dm_status = "No" + + # embed = discord.Embed(description=f"** Target User:** [{self.user}](https://discord.com/users/{self.user.id})\n **User Mention:** {self.user.mention}\n** DM Sent:** {dm_status}\n**Reason:** {reason}", color=0xFF0000) + # embed.set_author(name=f"Successfully Unmuted {self.user.name}", icon_url=self.user.avatar.url if self.user.avatar else self.user.default_avatar.url) + # embed.add_field(name=" Moderator:", value=interaction.user.mention, inline=False) + # embed.set_footer(text=f"Requested by {self.author}", icon_url=self.author.avatar.url if self.author.avatar else self.author.default_avatar.url) + # embed.timestamp = discord.utils.utcnow() +# + # await self.user.edit(timed_out_until=None, reason=f"Unmute requested by {self.author}") + # await interaction.response.edit_message(embed=embed, view=self.view) + # for item in self.view.children: + # item.disabled = True + # await interaction.message.edit(view=self.view) + +class Mute(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.color = discord.Color.from_rgb(255, 0, 0) + + def get_user_avatar(self, user): + return user.avatar.url if user.avatar else user.default_avatar.url + + def parse_time(self, time_str): + time_pattern = r"(\d+)([mhd])" + match = re.match(time_pattern, time_str) + if match: + time_value = int(match.group(1)) + time_unit = match.group(2) + if time_unit == 'm' and 0 < time_value <= 60: + return timedelta(minutes=time_value), f"{time_value} minutes" + elif time_unit == 'h' and 0 < time_value <= 24: + return timedelta(hours=time_value), f"{time_value} hours" + elif time_unit == 'd' and 0 < time_value <= 28: + return timedelta(days=time_value), f"{time_value} days" + return None, None + + @commands.hybrid_command( + name="mute", + help="Mutes a user with optional time and reason", + usage="mute [time] [reason]", + aliases=["timeout", "stfu","chup"]) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.member) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(moderate_members=True) + @commands.bot_has_permissions(moderate_members=True) + async def mute(self, ctx, user: discord.Member, time: str = None, *, reason=None): + + if user.is_timed_out(): + embed = discord.Embed(description="**Requested User is already muted in this server.**", color=self.color) + # embed.add_field(name="__Unmute__:", value="Click on the `Unmute` button to remove the timeout from the user.") + embed.set_author(name=f"{user.name} is Already Timed Out!", icon_url=self.get_user_avatar(user)) + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + # view = AlreadyTimedoutView(user=user, author=ctx.author) + message = await ctx.send(embed=embed) + # view.message = message + return + + if user == ctx.guild.owner: + error = discord.Embed(color=self.color, description="You can't timeout the Server Owner!") + error.set_author(name="Error Timing Out User") + error.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + return await ctx.send(embed=error) + + if ctx.author != ctx.guild.owner and user.top_role >= ctx.author.top_role: + error = discord.Embed(color=self.color, description="You can't timeout users having higher or equal role than yours!") + error.set_author(name="Error Timing Out User") + error.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + return await ctx.send(embed=error) + + if user.top_role >= ctx.guild.me.top_role: + error = discord.Embed(color=self.color, description="I can't timeout users having higher or equal role than mine.") + error.set_author(name="Error Timing Out User") + error.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + return await ctx.send(embed=error) + + time_delta, duration_text = self.parse_time(time) if time else (timedelta(hours=24), "24 hours") + + if not time_delta: + error = discord.Embed(color=self.color, description="Invalid time format! Use `` where `m` is minutes (max 60), `h` is hours (max 24), and `d` is days (max 28).") + error.set_author(name="Error Timing Out User") + error.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + return await ctx.send(embed=error) + + try: + await user.send(f"{ZWARNING} You have been muted in **{ctx.guild.name}** by **{ctx.author}** for {duration_text}. Reason: {reason or 'None'}") + dm_status = "Yes" + except discord.Forbidden: + dm_status = "No" + except discord.HTTPException: + dm_status = "No" + + await user.edit(timed_out_until=discord.utils.utcnow() + time_delta, reason=f"Muted by {ctx.author} for {duration_text}. Reason: {reason or 'None'}") + + + embed = discord.Embed(description=f"**{TICK} | Successfully Muted [{user}](https://discord.com/users/{user.id}) For {duration_text}\nReason {reason}**", + color=self.color) + embed.set_thumbnail(url=ctx.author.display_avatar.url) + embed.set_author(name=f"Successfully Muted {user.name}", icon_url=self.get_user_avatar(user)) + # embed.add_field(name=" Moderator:", value=ctx.author.mention, inline=False) + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + embed.timestamp = discord.utils.utcnow() + + #view = TimeoutView(user=user, author=ctx.author) + message = await ctx.send(embed=embed) + # view.message = message + + @mute.error + async def mute_error(self, ctx, error): + + if isinstance(error, commands.BotMissingPermissions): + embed = discord.Embed(title=f"{CROSS}> Access Denied", description="I don't have permission to mute members.", color=self.color) + await ctx.send(embed=embed) + elif isinstance(error, discord.Forbidden): + embed = discord.Embed(title=f"{CROSS} Missing Permissions", description="I can't mute this user as they might have higher privileges (e.g., Admin).", color=self.color) + await ctx.send(embed=embed) + + else: + embed = discord.Embed(title=f"{CROSS} Unexpected Error", description=str(error), color=self.color) + await ctx.send(embed=embed) + diff --git a/bot/cogs/moderation/topcheck.py b/bot/cogs/moderation/topcheck.py new file mode 100644 index 0000000..e75c0f1 --- /dev/null +++ b/bot/cogs/moderation/topcheck.py @@ -0,0 +1,105 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CROSS, TICK +from discord.ext import commands +import aiosqlite +import asyncio +from utils.config import * + +class TopCheck(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.db_path = "db/topcheck.db" + self.bot.loop.create_task(self.setup()) + + async def setup(self): + async with aiosqlite.connect(self.db_path) as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS topcheck ( + guild_id INTEGER PRIMARY KEY, + enabled INTEGER + ) + """) + await db.commit() + + async def is_topcheck_enabled(self, guild_id: int): + async with aiosqlite.connect(self.db_path) as db: + async with db.execute("SELECT enabled FROM topcheck WHERE guild_id = ?", (guild_id,)) as cursor: + row = await cursor.fetchone() + if row: + return row[0] == 1 + return False + + async def enable_topcheck(self, guild_id: int): + async with aiosqlite.connect(self.db_path) as db: + await db.execute("INSERT OR REPLACE INTO topcheck (guild_id, enabled) VALUES (?, 1)", (guild_id,)) + await db.commit() + + async def disable_topcheck(self, guild_id: int): + async with aiosqlite.connect(self.db_path) as db: + await db.execute("UPDATE topcheck SET enabled = 0 WHERE guild_id = ?", (guild_id,)) + await db.commit() + + @commands.group( + name="topcheck", + help="Manage topcheck settings for the server.", + invoke_without_command=True) + @commands.guild_only() + async def topcheck(self, ctx): + embed = discord.Embed(title="Top Check System", + description=( + "This system ensures that the bot’s role is positioned higher than the user’s top role before executing specific commands.\n\n" + f"When topcheck is enabled, only users with roles above the bot's ({BRAND_NAME}) role can perform certain moderation actions. " + "If topcheck is disabled, any user with the required permissions for a command can execute it.\n\n" + "**Moderation actions affected by topcheck:**\n" + "- BAN\n" + "- KICK\n" + "- ROLE DELETE\n" + "- ROLE CREATE\n" + "- MEMBER UPDATE\n\n" + "__**Subcommands:**__\n" + f"• `{ctx.prefix}topcheck enable` - Enables top check for the server.\n" + f"• `{ctx.prefix}topcheck disable` - Disables top check for the server." + ), + color=0xFF0000) + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=ctx.author.avatar.url) + await ctx.send(embed=embed) + + @topcheck.command( + name="enable", + help="Enable topcheck for the guild") + @commands.guild_only() + async def topcheck_enable(self, ctx): + if ctx.author.id != ctx.guild.owner_id: + return await ctx.reply(f"{CROSS} Only the **Server Owner** can enable topcheck.") + if await self.is_topcheck_enabled(ctx.guild.id): + return await ctx.reply(f"{CROSS} Topcheck is already enabled for this server.") + await self.enable_topcheck(ctx.guild.id) + await ctx.reply(f"{TICK} Topcheck has been Successfully enabled for this server.") + + @topcheck.command( + name="disable", + help="Disable topcheck for the guild") + @commands.guild_only() + async def topcheck_disable(self, ctx): + if ctx.author.id != ctx.guild.owner_id: + return await ctx.reply("Only the **Server Owner** can disable topcheck.") + if not await self.is_topcheck_enabled(ctx.guild.id): + return await ctx.reply(f"{CROSS} Topcheck is not enabled for this server.") + await self.disable_topcheck(ctx.guild.id) + await ctx.reply(f"{TICK} Topcheck has been Successfully disabled for this server.") + + \ No newline at end of file diff --git a/bot/cogs/moderation/unban.py b/bot/cogs/moderation/unban.py new file mode 100644 index 0000000..767ad7b --- /dev/null +++ b/bot/cogs/moderation/unban.py @@ -0,0 +1,182 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import DELETE, TICK, ZWARNING +from discord.ext import commands +from discord import ui +from utils.Tools import * + +#class BanView(ui.View): + # def __init__(self, user, author): + # super().__init__(timeout=120) + # self.user = user + # self.author = author + # self.message = None + # self.color = discord.Color.from_rgb(255, 0, 0) + + # async def interaction_check(self, interaction: discord.Interaction) -> bool: + # if interaction.user != self.author: + # await interaction.response.send_message("You are not allowed to interact with this!", ephemeral=True) + # return False + # return True + + # async def on_timeout(self): + # for item in self.children: + # item.disabled = True + # if self.message: + # try: + # await self.message.edit(view=self) + # except Exception: + # pass + + # @ui.button(label="Ban", style=discord.ButtonStyle.danger) + # async def ban(self, interaction: discord.Interaction, button: discord.ui.Button): + # modal = ReasonModal(user=self.user, author=self.author, view=self) + # await interaction.response.send_modal(modal) + + # @ui.button(style=discord.ButtonStyle.gray, emoji=DELETE) + # async def delete(self, interaction: discord.Interaction, button: discord.ui.Button): + # await interaction.message.delete() + +#class AlreadyUnbannedView(ui.View): + # def __init__(self, user, author): + # super().__init__(timeout=60) + # self.user = user + # self.author = author + # self.message = None + + # async def interaction_check(self, interaction: discord.Interaction) -> bool: + # if interaction.user != self.author: + # await interaction.response.send_message("You are not allowed to interact with this!", ephemeral=True) + # return False + # return True + + # async def on_timeout(self): + # for item in self.children: + # item.disabled = True + # if self.message: + # await self.message.edit(view=self) + + # @ui.button(label="Ban", style=discord.ButtonStyle.danger) + #async def ban(self, interaction: discord.Interaction, button: discord.ui.Button): + # modal = ReasonModal(user=self.user, author=self.author, view=self) + # await interaction.response.send_modal(modal) + + # @ui.button(style=discord.ButtonStyle.gray, emoji=DELETE) + # async def delete(self, interaction: discord.Interaction, button: discord.ui.Button): + # await interaction.message.delete() + +#class ReasonModal(ui.Modal): + # def __init__(self, user, author, view): + # super().__init__(title="Ban Reason") + # self.user = user + # self.author = author + # self.view = view + # self.reason_input = ui.TextInput(label="Reason for Banning", placeholder="Provide a reason for banning or leave it blank for no reason.", required = False, max_length=2000, style=discord.TextStyle.paragraph) + # self.add_item(self.reason_input) + + #async def on_submit(self, interaction: discord.Interaction): + # reason = self.reason_input.value or "No reason provided" + # try: + # await self.user.send(f"{ZWARNING} You have been Banned from **{self.author.guild.name}** by **{self.author}**. Reason: {reason or 'No reason provided'}") + # dm_status = "Yes" + #except discord.Forbidden: + # dm_status = "No" + # except discord.HTTPException: + # dm_status = "No" + + # embed = discord.Embed(description=f"** Target User:** [{self.user}](https://discord.com/users/{self.user.id})\n **User Mention:** {self.user.mention}\n** DM Sent:** {dm_status}\n** Reason:** {reason}", color=0xFF0000) + # embed.set_author(name=f"Successfully Banned {self.user.name}", icon_url=self.user.avatar.url if self.user.avatar else self.user.default_avatar.url) + # embed.add_field(name=" Moderator:", value=interaction.user.mention, inline=False) + # embed.set_footer(text=f"Requested by {self.author}", icon_url=self.author.avatar.url if self.author.avatar else self.author.default_avatar.url) + # embed.timestamp = discord.utils.utcnow() + + # try: + # await interaction.guild.ban(self.user, reason=f"Ban requested by {self.author}") + # except discord.NotFound: + # pass + # except discord.Forbidden: + # pass + # except discord.HTTPException: + # pass + + # try: + # await interaction.response.edit_message(embed=embed, view=self.view) + # for item in self.view.children: + # item.disabled = True + # await interaction.message.edit(view=self.view) + # except discord.NotFound: + # pass + # except discord.Forbidden: + # pass + # except discord.HTTPException: + # pass + + +class Unban(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.color = discord.Color.from_rgb(255, 0, 0) + + def get_user_avatar(self, user): + return user.avatar.url if user.avatar else user.default_avatar.url + + @commands.hybrid_command( + name="unban", + help="Unbans a user from the Server", + usage="unban ", + aliases=["forgive", "pardon"]) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.member) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(ban_members=True) + @commands.bot_has_permissions(ban_members=True) + async def unban(self, ctx, user: discord.User, *, reason=None): + bans = [entry async for entry in ctx.guild.bans()] + if not any(ban_entry.user.id == user.id for ban_entry in bans): + embed = discord.Embed(description="**Requested User is not banned in this server.**", color=self.color) + # embed.add_field(name="__Ban__:", value="Click on the `Ban` button to ban the mentioned user.") + embed.set_author(name=f"{user.name} is Not Banned!", icon_url=self.get_user_avatar(user)) + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + #view = AlreadyUnbannedView(user=user, author=ctx.author) + message = await ctx.send(embed=embed) + #view.message = message + return + + try: + await user.send(f"{TICK} You have been unbanned from **{ctx.guild.name}** by **{ctx.author}**. Reason: {reason or 'No reason provided'}") + dm_status = "Yes" + except discord.Forbidden: + dm_status = "No" + except discord.HTTPException: + dm_status = "No" + + await ctx.guild.unban(user, reason=f"Unban requested by {ctx.author} for reason: {reason or 'No reason provided'}") + + reasonn = reason or "No reason provided" + embed = discord.Embed(description=f"**{TICK} | Successfully Unbaned [{user}](https://discord.com/users/{user.id})\nReason {reasonn}**", color=0xFF0000) + embed.set_author(name=f"Successfully Unbanned {user.name}", icon_url=self.get_user_avatar(user)) + embed.set_thumbnail(url=ctx.author.display_avatar.url) + # embed.add_field(name=" Moderator:", value=ctx.author.mention, inline=False) + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + embed.timestamp = discord.utils.utcnow() + + #view = BanView(user=user, author=ctx.author) + message = await ctx.send(embed=embed) + # view.message = message + + diff --git a/bot/cogs/moderation/unhide.py b/bot/cogs/moderation/unhide.py new file mode 100644 index 0000000..cf75f2b --- /dev/null +++ b/bot/cogs/moderation/unhide.py @@ -0,0 +1,68 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CHANNEL, TICK +from discord.ext import commands + +class Unhide(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.color = discord.Color.from_rgb(255,0, 0) # Green color for success + + @commands.hybrid_command( + name="unhide", + help="Unhides a channel for the default role (@everyone).", + usage="unhide [channel]", + aliases=["unhidechannel"]) + @commands.has_permissions(manage_channels=True) + @commands.bot_has_permissions(manage_channels=True) + async def unhide_command(self, ctx, channel: discord.TextChannel = None): + """Makes a channel visible to @everyone again.""" + # If no channel is mentioned, use the current channel + channel = channel or ctx.channel + + # Get the author's avatar URL + author_avatar_url = ctx.author.avatar.url if ctx.author.avatar else None + + # Check if the channel is already visible + if channel.permissions_for(ctx.guild.default_role).read_messages: + embed = discord.Embed( + description=f"**{CHANNEL} Channel**: {channel.mention}\n{TICK} **Status**: Already Visible", + color=self.color + ) + embed.set_author(name=f"{channel.name} is Already Visible") + if author_avatar_url: + embed.set_thumbnail(url=author_avatar_url) + await ctx.send(embed=embed) + return + + # Unhide the channel by updating permissions + await channel.set_permissions(ctx.guild.default_role, read_messages=True) + + # Create the success embed + embed = discord.Embed( + description=f"**{TICK} | {channel.mention} has been successfully unhidden.**", + color=self.color + ) + embed.set_author(name="Channel Unhidden") + embed.set_footer(text=f"Action by {ctx.author.name}", icon_url=author_avatar_url) + if author_avatar_url: + embed.set_thumbnail(url=author_avatar_url) + + await ctx.send(embed=embed) + +# Function to add the cog to your bot +async def setup(bot): + await bot.add_cog(Unhide(bot)) diff --git a/bot/cogs/moderation/unhide.txt b/bot/cogs/moderation/unhide.txt new file mode 100644 index 0000000..e2aa302 --- /dev/null +++ b/bot/cogs/moderation/unhide.txt @@ -0,0 +1,94 @@ +import discord +from utils.emoji import DELETE +from discord.ext import commands +from discord import ui + +class HideUnhideView(ui.View): + def __init__(self, channel, author, ctx): + super().__init__(timeout=120) + self.channel = channel + self.author = author + self.ctx = ctx + self.message = None + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user != self.author: + await interaction.response.send_message("You are not allowed to interact with this!", ephemeral=True) + return False + return True + + async def on_timeout(self): + for item in self.children: + if item.label != "Delete": + item.disabled = True + if self.message: + try: + await self.message.edit(view=self) + except Exception: + pass + + @ui.button(label="Hide", style=discord.ButtonStyle.danger) + async def hide(self, interaction: discord.Interaction, button: discord.ui.Button): + await self.channel.set_permissions(interaction.guild.default_role, read_messages=False) + await interaction.response.send_message(f"{self.channel.mention} has been hidden.", ephemeral=True) + + embed = discord.Embed( + description=f" **Channel**: {self.channel.mention}\n **Status**: Hidden\n**Reason:** Hide request by {self.author}", + color=0xFF0000 + ) + embed.add_field(name=" **Moderator:**", value=self.ctx.author.mention, inline=False) + embed.set_author(name=f"Successfully Hidden {self.channel.name}") + await self.message.edit(embed=embed, view=self) + + for item in self.children: + if item.label != "Delete": + item.disabled = True + await self.message.edit(view=self) + + @ui.button(style=discord.ButtonStyle.gray, emoji=DELETE) + async def delete(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.message.delete() + + +class Unhide(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.color = discord.Color.from_rgb(255, 0, 0) + + @commands.hybrid_command( + name="unhide", + help="Unhides a channel to allow the default role (@everyone) to read messages.", + usage="unhide ", + aliases=["unhidechannel"]) + @commands.has_permissions(manage_roles=True) + @commands.bot_has_permissions(manage_roles=True) + async def unhide_command(self, ctx, channel: discord.TextChannel = None): + channel = channel or ctx.channel + if channel.permissions_for(ctx.guild.default_role).read_messages: + embed = discord.Embed( + description=f"** Channel**: {channel.mention}\n **Status**: Already Unhidden", + color=self.color + ) + embed.set_author(name=f"{channel.name} is Already Unhidden") + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=ctx.author.avatar.url) + view = HideUnhideView(channel=channel, author=ctx.author, ctx=ctx) + message = await ctx.send(embed=embed, view=view) + view.message = message + return + + await channel.set_permissions(ctx.guild.default_role, read_messages=True) + + embed = discord.Embed( + description=f" **Channel**: {channel.mention}\n< **Status**: Unhidden\n **Reason:** Unhide request by {ctx.author}", + color=self.color + ) + embed.add_field(name=" **Moderator:**", value=ctx.author.mention, inline=False) + embed.set_author(name=f"Successfully Unhidden {channel.name}") + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=ctx.author.avatar.url) + view = HideUnhideView(channel=channel, author=ctx.author, ctx=ctx) + message = await ctx.send(embed=embed, view=view) + view.message = message + + + + \ No newline at end of file diff --git a/bot/cogs/moderation/unlock.py b/bot/cogs/moderation/unlock.py new file mode 100644 index 0000000..b8c0fe3 --- /dev/null +++ b/bot/cogs/moderation/unlock.py @@ -0,0 +1,108 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import DELETE, TICK +from discord.ext import commands +from discord import ui + +#class LockUnlockView(ui.View): + #def __init__(self, channel, author, ctx): + # super().__init__(timeout=120) + # self.channel = channel + # self.author = author + # self.ctx = ctx + # self.message = None + + # async def interaction_check(self, interaction: discord.Interaction) -> bool: + # if interaction.user != self.author: + # await interaction.response.send_message("You are not allowed to interact with this!", ephemeral=True) + # return False + # return True + + # async def on_timeout(self): + # for item in self.children: + # if item.label != "Delete": + # item.disabled = True + # if self.message: + # try: + # await self.message.edit(view=self) + # except Exception: + # pass + + + # @ui.button(label="Lock", style=discord.ButtonStyle.danger) + # async def lock(self, interaction: discord.Interaction, button: discord.ui.Button): + # await self.channel.set_permissions(interaction.guild.default_role, send_messages=False) + # await interaction.response.send_message(f"{self.channel.mention} has been locked.", ephemeral=True) + + # embed = discord.Embed( + # description=f" **Channel**: {self.channel.mention}\n **Status**: Locked\n **Reason:** Lock request by {self.author}", + # color=0xFF0000 + # ) + # embed.add_field(name=" **Moderator:**", value=self.ctx.author.mention, inline=False) + # embed.set_author(name=f"Successfully Locked {self.channel.name}") + # await self.message.edit(embed=embed, view=self) + + # for item in self.children: + # if item.label != "Delete": + # item.disabled = True + # await self.message.edit(view=self) + + # @ui.button(style=discord.ButtonStyle.gray, emoji=DELETE) + # async def delete(self, interaction: discord.Interaction, button: discord.ui.Button): + # await interaction.message.delete() + + +class Unlock(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.color = discord.Color.from_rgb(255, 0, 0) + + @commands.hybrid_command( + name="unlock", + help="Unlocks a channel to allow sending messages.", + usage="unlock ", + aliases=["unlockchannel"]) + @commands.has_permissions(manage_roles=True) + @commands.bot_has_permissions(manage_roles=True) + async def unlock_command(self, ctx, channel: discord.TextChannel = None): + channel = channel or ctx.channel + if channel.permissions_for(ctx.guild.default_role).send_messages is True: + embed = discord.Embed( + description=f"**{channel.mention} Already Unlocked", + color=self.color + ) + embed.set_author(name=f"{channel.name} is Already Unlocked") + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=ctx.author.avatar.url) + #view = LockUnlockView(channel=channel, author=ctx.author, ctx=ctx) + message = await ctx.send(embed=embed) + # view.message = message + return + + await channel.set_permissions(ctx.guild.default_role, send_messages=True) + + embed = discord.Embed( + description=f"**{TICK} | Successfully {channel.mention} is now unhidden", + color=self.color + ) + # embed.add_field(name=" **Moderator:**", value=ctx.author.mention, inline=False) + embed.set_thumbnail(url=ctx.author.display_avatar.url) + embed.set_author(name=f"Successfully Unlocked {channel.name}") + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=ctx.author.avatar.url) + # view = LockUnlockView(channel=channel, author=ctx.author, ctx=ctx) + message = await ctx.send(embed=embed) + # view.message = message + + diff --git a/bot/cogs/moderation/unmute.py b/bot/cogs/moderation/unmute.py new file mode 100644 index 0000000..ac406ef --- /dev/null +++ b/bot/cogs/moderation/unmute.py @@ -0,0 +1,199 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import DELETE, TICK, ZWARNING +from discord.ext import commands +from discord import ui +from utils.Tools import * +from datetime import timedelta + +#class MuteUnmuteView(ui.View): + # def __init__(self, user, author): + # super().__init__(timeout=120) + # self.user = user + #self.author = author + # self.message = None + + # async def interaction_check(self, interaction: discord.Interaction) -> bool: + # if interaction.user != self.author: + # await interaction.response.send_message("You are not allowed to interact with this!", ephemeral=True) + # return False + # return True + + # async def on_timeout(self): + # for item in self.children: + # if item.label != "Delete": + # item.disabled = True + # if self.message: + # try: + # await self.message.edit(view=self) + # except Exception: + # pass + + # @ui.button(label="Add Timeout", style=discord.ButtonStyle.danger) + # async def mute(self, interaction: discord.Interaction, button: discord.ui.Button): + # modal = MuteReasonModal(user=self.user, author=self.author, view=self) + # await interaction.response.send_modal(modal) + + + # for item in self.children: + # if item.label != "Delete": + # item.disabled = True + #await self.message.edit(view=self) + + # @ui.button(style=discord.ButtonStyle.gray, emoji=DELETE) + # async def delete(self, interaction: discord.Interaction, button: discord.ui.Button): + # await interaction.message.delete() + + +#class MuteReasonModal(ui.Modal): + # def __init__(self, user, author, view): + # super().__init__(title="Mute Information") + # self.user = user + # self.author = author + # self.view = view + # self.time_input = ui.TextInput(label="Duration (m/h/d)", placeholder="Leave blank for default 24h", required=False, max_length=5) + # self.reason_input = ui.TextInput(label="Reason", placeholder="Provide a reason or leave it blank.", required=False, max_length=2000, style=discord.TextStyle.paragraph) + # self.add_item(self.time_input) + # self.add_item(self.reason_input) + + #async def on_submit(self, interaction: discord.Interaction): + # reason = self.reason_input.value or "No reason provided" + # time_str = self.time_input.value or "24h" + #time_seconds = self.parse_duration(time_str) + + # if time_seconds is None: + # await interaction.response.send_message(f"Invalid time format! Please provide in m (minutes), h (hours), or d (days).", ephemeral=True) + # return + + # try: + # await self.user.edit(timed_out_until=discord.utils.utcnow() + timedelta(seconds=time_seconds)) + # except discord.Forbidden: + # await interaction.response.send_message(f"Failed to mute {self.user.mention}. I lack the permissions.", ephemeral=True) + # return + + + # try: + # await self.user.send(f"{ZWARNING} You have been muted in **{interaction.guild.name}** for {time_str}. Reason: {reason}") + # dm_status = "Yes" + # except discord.Forbidden: + # dm_status = "No" + #except discord.HTTPException: + # dm_status = "No" + + # success_embed = discord.Embed( + # description=f"** Target User:** [{self.user}](https://discord.com/users/{self.user.id})\n**User Mention:** {self.user.mention}\n**Reason:** {reason}\n **DM Sent:** {dm_status}", + # color=0xFF0000 + # ) + # success_embed.set_author(name=f"Muted {self.user.name}", icon_url=self.user.avatar.url if self.user.avatar else self.user.default_avatar.url) + # success_embed.add_field(name=" Moderator:", value=self.author.mention, inline=False) + # success_embed.add_field(name="Duration", value=f"{time_str}", inline=False) + # success_embed.set_footer(text=f"Requested by {self.author}", icon_url=self.author.avatar.url if self.author.avatar else self.author.default_avatar.url) + # success_embed.timestamp = discord.utils.utcnow() + + # await interaction.response.edit_message(embed=success_embed, view=self.view) + + + # for item in self.view.children: + # if item.label != "Delete": + # item.disabled = True + # await self.view.message.edit(view=self.view) + + # def parse_duration(self, duration_str: str) -> int: + # try: + # if duration_str.endswith("m"): + # duration = int(duration_str[:-1]) + # return duration * 60 + # elif duration_str.endswith("h"): + # duration = int(duration_str[:-1]) + # return duration * 3600 + # elif duration_str.endswith("d"): + #duration = int(duration_str[:-1]) + # return duration * 86400 + # else: + + # duration = int(duration_str) + # if duration > 60: + # return (duration // 60) * 3600 + # else: + # return duration * 60 + # except ValueError: + # return None + + +class Unmute(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.color = discord.Color.from_rgb(255, 0, 0) + + def get_user_avatar(self, user): + return user.avatar.url if user.avatar else user.default_avatar.url + + @commands.hybrid_command( + name="unmute", + help="Unmutes a user from the Server", + usage="unmute ", + aliases=["untimeout"]) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.member) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(moderate_members=True) + @commands.bot_has_permissions(moderate_members=True) + async def unmute(self, ctx, user: discord.Member): + if not user.timed_out_until or user.timed_out_until <= discord.utils.utcnow(): + embed = discord.Embed(description="**Requested User is not muted in this server.**", color=self.color) + #embed.add_field(name="__Mute__:", value="Click on the `Add Timeout` button to mute the mentioned user.") + embed.set_thumbnail(url=ctx.author.display_avatar.url) + embed.set_author(name=f"{user.name} is Not Muted!", icon_url=self.get_user_avatar(user)) + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + # view = MuteUnmuteView(user=user, author=ctx.author) + message = await ctx.send(embed=embed) + # view.message = message + return + + try: + await user.edit(timed_out_until=None) + + + try: + await user.send(f"{TICK} You have been unmuted in **{ctx.guild.name}**.") + dm_status = "Yes" + except discord.Forbidden: + dm_status = "No" + except discord.HTTPException: + dm_status = "No" + + except discord.Forbidden: + error = discord.Embed(color=self.color, description="I can't unmute a user with higher permissions!") + error.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + error.set_author(name="Error Unmuting User", icon_url="https://cdn.discordapp.com/emojis/1448949627712966717.png?v=1&size=48&quality=lossless") + return await ctx.send(embed=error) + + embed = discord.Embed( + description=f"**{TICK} | Successfully Unmuted [{user}](https://discord.com/users/{user.id})**", + color=self.color + ) + embed.set_thumbnail(url=ctx.author.display_avatar.url) + embed.set_author(name=f"Successfully Unmuted {user.name}", icon_url=self.get_user_avatar(user)) + # embed.add_field(name=" Moderator:", value=ctx.author.mention, inline=False) + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + embed.timestamp = discord.utils.utcnow() + + # view = MuteUnmuteView(user=user, author=ctx.author) + message = await ctx.send(embed=embed) + # view.message = message + diff --git a/bot/cogs/moderation/warn.py b/bot/cogs/moderation/warn.py new file mode 100644 index 0000000..6933947 --- /dev/null +++ b/bot/cogs/moderation/warn.py @@ -0,0 +1,182 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import DELETE, TICK +from discord.ext import commands +from discord import ui +import aiosqlite +import asyncio +from utils.Tools import * + + +#class WarnView(ui.View): + # def __init__(self, user, author): + # super().__init__(timeout=60) + # self.user = user + # self.author = author + # self.message = None + + # async def interaction_check(self, interaction: discord.Interaction) -> bool: + # if interaction.user != self.author: + # await interaction.response.send_message("You are not allowed to interact with this!", ephemeral=True) + # return False + # return True + + # async def on_timeout(self): + # for item in self.children: + # item.disabled = True + # if self.message: + # try: + # await self.message.edit(view=self) + # except Exception: + # pass + + # @ui.button(style=discord.ButtonStyle.gray, emoji=DELETE) + # async def delete(self, interaction: discord.Interaction, button: discord.ui.Button): + # await interaction.message.delete() + + +class Warn(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.color = discord.Color.from_rgb(255, 0, 0) + self.db_path = "db/warn.db" + + + asyncio.create_task(self.setup()) + + def get_user_avatar(self, user): + return user.avatar.url if user.avatar else user.default_avatar.url + + async def add_warn(self, guild_id: int, user_id: int): + async with aiosqlite.connect(self.db_path) as db: + await db.execute("INSERT OR IGNORE INTO warns (guild_id, user_id, warns) VALUES (?, ?, 0)", (guild_id, user_id)) + await db.execute("UPDATE warns SET warns = warns + 1 WHERE guild_id = ? AND user_id = ?", (guild_id, user_id)) + await db.commit() + + async def get_total_warns(self, guild_id: int, user_id: int): + async with aiosqlite.connect(self.db_path) as db: + async with db.execute("SELECT warns FROM warns WHERE guild_id = ? AND user_id = ?", (guild_id, user_id)) as cursor: + row = await cursor.fetchone() + if row: + return row[0] + return 0 + + async def reset_warns(self, guild_id: int, user_id: int): + async with aiosqlite.connect(self.db_path) as db: + await db.execute("UPDATE warns SET warns = 0 WHERE guild_id = ? AND user_id = ?", (guild_id, user_id)) + await db.commit() + + async def setup(self): + try: + async with aiosqlite.connect(self.db_path) as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS warns ( + guild_id INTEGER, + user_id INTEGER, + warns INTEGER, + PRIMARY KEY (guild_id, user_id) + ) + """) + await db.commit() + except Exception as e: + print(f"Error during database setup: {e}") + + @commands.hybrid_command( + name="warn", + help="Warn a user in the server", + usage="warn [reason]", + aliases=["warnuser"]) + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.member) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(moderate_members=True) + #@commands.bot_has_permissions(manage_messages=True) + async def warn(self, ctx, user: discord.Member, *, reason=None): + if user == ctx.author: + return await ctx.reply("You cannot warn yourself.") + + if user == ctx.bot.user: + return await ctx.reply("You cannot warn me.") + + if not ctx.author == ctx.guild.owner: + if user == ctx.guild.owner: + return await ctx.reply("I cannot warn the server owner.") + + if ctx.author.top_role <= user.top_role: + return await ctx.reply("You cannot Warn a member with a higher or equal role.") + + if ctx.guild.me.top_role <= user.top_role: + return await ctx.reply("I cannot Warn a member with a higher or equal role.") + + if user not in ctx.guild.members: + return await ctx.reply("The user is not a member of this server.") + try: + + await self.add_warn(ctx.guild.id, user.id) + total_warns = await self.get_total_warns(ctx.guild.id, user.id) + + + reason_to_send = reason or "No reason provided" + try: + await user.send(f"You have been warned in **{ctx.guild.name}** by **{ctx.author}**. Reason: {reason_to_send}") + dm_status = "Yes" + except discord.Forbidden: + dm_status = "No" + except discord.HTTPException: + dm_status = "No" + + + embed = discord.Embed(description=f"**{TICK} | Successfully Warned [{user}](https://discord.com/users/{user.id})\nReason {reason_to_send}\nNow He has {total_warns} Warns**", + color=self.color) + embed.set_thumbnail(url=ctx.author.display_avatar.url) + embed.set_author(name=f"Successfully Warned {user.name}", icon_url=self.get_user_avatar(user)) + #embed.add_field(name="Moderator:", value=ctx.author.mention, inline=False) + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + embed.timestamp = discord.utils.utcnow() + + # view = WarnView(user=user, author=ctx.author) + message = await ctx.send(embed=embed) + # view.message = message + except Exception as e: + await ctx.send(f"An error occurred: {str(e)}") + print(f"Error during warn command: {e}") + + @commands.hybrid_command( + name="clearwarns", + help="Clear all warnings for a user", + aliases=["clearwarn" , "clearwarnings"], + usage="clearwarns ") + @blacklist_check() + @ignore_check() + @commands.cooldown(1, 10, commands.BucketType.member) + @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) + @commands.guild_only() + @commands.has_permissions(moderate_members=True) + async def clearwarns(self, ctx, user: discord.Member): + try: + await self.reset_warns(ctx.guild.id, user.id) + embed = discord.Embed(description=f"{TICK} | All warnings have been cleared for **{user}** in this guild.", color=self.color) + embed.set_author(name=f"Warnings Cleared", icon_url=self.get_user_avatar(user)) + embed.set_footer(text=f"Requested by {ctx.author}", icon_url=self.get_user_avatar(ctx.author)) + embed.timestamp = discord.utils.utcnow() + + await ctx.send(embed=embed) + except Exception as e: + await ctx.send(f"An error occurred: {str(e)}") + print(f"Error during clearwarns command: {e}") + diff --git a/bot/cogs/zyrox/ai.py b/bot/cogs/zyrox/ai.py new file mode 100644 index 0000000..697b749 --- /dev/null +++ b/bot/cogs/zyrox/ai.py @@ -0,0 +1,36 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + + + +import discord +from utils.emoji import ZAI +from discord .ext import commands + +class _ai (commands .Cog ): + def __init__ (self ,bot ): + self .bot =bot + + """AI commands""" + + def help_custom (self ): + emoji =ZAI + label ="AI Commands" + description ="Show you the commands of AI" + return emoji ,label ,description + + @commands .group () + async def __AI__ (self ,ctx :commands .Context ): + """`ai activate`, `ai deactivate`, `ai analyze`, `ai analyse`, `ai code`, `ai explain`, `ai conversation-clear`, `ai mood-analyzer`, `ai personality`, `ai conversation-stats`, `ai summarize`, `ai ask`, `ai fact`, `ai database-clear`, `ai roleplay-enable`, `ai roleplay-disable`""" + pass diff --git a/bot/cogs/zyrox/antinuke.py b/bot/cogs/zyrox/antinuke.py new file mode 100644 index 0000000..b8545d9 --- /dev/null +++ b/bot/cogs/zyrox/antinuke.py @@ -0,0 +1,35 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ZSAFE +from discord.ext import commands + + +class _antinuke(commands.Cog): + def __init__(self, bot): + self.bot = bot + + """Antinuke commands""" + + def help_custom(self): + emoji = ZSAFE + label = "Security Commands" + description = "Show you Commands of Antinuke" + return emoji, label, description + + @commands.group() + async def __Antinuke__(self, ctx: commands.Context): + """`antinuke` , `antinuke enable` , `antinuke disable` , `whitelist` , `whitelist @user` , `unwhitelist` , `whitelisted` , `whitelist reset` , `extraowner` , `extraowner set` , `extraowner view` , `extraowner reset`, `nightmode` , `nightmode enable` , `nightmode disable`\n""" + diff --git a/bot/cogs/zyrox/automod.py b/bot/cogs/zyrox/automod.py new file mode 100644 index 0000000..76a256a --- /dev/null +++ b/bot/cogs/zyrox/automod.py @@ -0,0 +1,34 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ZBOT +from discord.ext import commands + +class _automod(commands.Cog): + def __init__(self, bot): + self.bot = bot + + """Automod commands""" + + def help_custom(self): + emoji = ZBOT + label = "Automod Commands" + description = "Show you Commands of automod" + return emoji, label, description + + @commands.group() + async def __Automod__(self, ctx: commands.Context): + """`automod` , `automod enable` , `automod disable` , `automod punishment` , `autmod config` , `automod logging` `automod ignore` , `automod ignore channel` , `automod ignore role` , `automod ignore show` , `automod ignore reset` , `automod unignore` , `automod unignore channel` , `automod unignore role`\n\n__**Blacklistword Commands**__\n`blacklistword` , `blacklistword add ` , `blacklistword remove ` , `blacklistword reset` , `blacklistword config` , `blacklistword bypass add ` , `blacklistword bypass remove ` , `blacklistword bypass show` +""" \ No newline at end of file diff --git a/bot/cogs/zyrox/birth.py b/bot/cogs/zyrox/birth.py new file mode 100644 index 0000000..930a3f9 --- /dev/null +++ b/bot/cogs/zyrox/birth.py @@ -0,0 +1,35 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ZCIRCLE +from discord .ext import commands + + +class _birth(commands .Cog ): + def __init__ (self ,bot ): + self .bot =bot + + """Birthday commands""" + + def help_custom (self ): + emoji =ZCIRCLE + label ="Birthday Commands" + description ="Show you the commands of Birthday" + return emoji ,label ,description + + @commands.group () + async def __Birthday__ (self ,ctx :commands .Context ): + """`birthdaysetup` , `setbirthday` , `removebirthday` , `listbirthdays` , `birthday`""" + pass diff --git a/bot/cogs/zyrox/booster.py b/bot/cogs/zyrox/booster.py new file mode 100644 index 0000000..de26432 --- /dev/null +++ b/bot/cogs/zyrox/booster.py @@ -0,0 +1,35 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import BOOST +from discord .ext import commands + + +class __boost(commands .Cog ): + def __init__ (self ,bot ): + self .bot =bot + + """Boost commands""" + + def help_custom (self ): + emoji =BOOST + label ="Boost Commands" + description ="Show you the commands of boost" + return emoji ,label ,description + + @commands .group () + async def __Boost__ (self ,ctx :commands .Context ): + """`boost setup` , `boost message` , `boost channel` , `boostrole` , `boost config`""" + pass diff --git a/bot/cogs/zyrox/counting.py b/bot/cogs/zyrox/counting.py new file mode 100644 index 0000000..b23386e --- /dev/null +++ b/bot/cogs/zyrox/counting.py @@ -0,0 +1,42 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ZCOUNTING + +from discord.ext import commands + +class _Counting(commands.Cog): + + def __init__(self, bot): + + self.bot = bot + + """Counting""" + + def help_custom(self): + + emoji = ZCOUNTING + + label = "Counting" + + description = "Show you Commands of Counting" + + return emoji, label, description + + @commands.group() + + async def __Counting__(self, ctx: commands.Context): + + """`>counting`, `>counting enable/disable`, `>counting channel #channel`, `>counting stats`, `>counting config continue/reset`""" \ No newline at end of file diff --git a/bot/cogs/zyrox/encryption.py b/bot/cogs/zyrox/encryption.py new file mode 100644 index 0000000..3094c32 --- /dev/null +++ b/bot/cogs/zyrox/encryption.py @@ -0,0 +1,79 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import LOCK +from discord.ext import commands + + +class _encrypt(commands.Cog): + def __init__(self, bot): + self.bot = bot + + def help_custom(self): + emoji = LOCK + label = "Encryption Commands" + description = "Encode & decode text in various formats" + return emoji, label, description + + @commands.group(name="encryption", aliases=["encrypt"], invoke_without_command=True) + async def _Encryption(self, ctx: commands.Context): + """Show all available encryption commands""" + embed = discord.Embed( + title="Encryption Commands", + description=f"Use `{ctx.prefix}encode ` to encode text, `{ctx.prefix}decode ` to decode.", + color=0x000000, + ) + + embed.add_field( + name="📝 Encode Commands", + value=f""" +`{ctx.prefix}encode base32` / `{ctx.prefix}encode b32` - Encode to base32 +`{ctx.prefix}encode base64` / `{ctx.prefix}encode b64` - Encode to base64 +`{ctx.prefix}encode rot13` / `{ctx.prefix}encode r13` - Encode to rot13 +`{ctx.prefix}encode hex` - Encode to hex +`{ctx.prefix}encode base85` / `{ctx.prefix}encode b85` - Encode to base85 +`{ctx.prefix}encode ascii85` / `{ctx.prefix}encode a85` - Encode to ASCII85 +""", + inline=False, + ) + + embed.add_field( + name="📄 Decode Commands", + value=f""" +`{ctx.prefix}decode base32` / `{ctx.prefix}decode b32` - Decode from base32 +`{ctx.prefix}decode base64` / `{ctx.prefix}decode b64` - Decode from base64 +`{ctx.prefix}decode rot13` / `{ctx.prefix}decode r13` - Decode from rot13 +`{ctx.prefix}decode hex` - Decode from hex +`{ctx.prefix}decode base85` / `{ctx.prefix}decode b85` - Decode from base85 +`{ctx.prefix}decode ascii85` / `{ctx.prefix}decode a85` - Decode from ASCII85 +""", + inline=False, + ) + + embed.add_field( + name="🔐 Utility", + value=f"`{ctx.prefix}password` - Generate a random secure password (sent via DM)", + inline=False, + ) + + embed.set_footer( + text="Use encode/decode commands to encrypt or decrypt your text" + ) + + await ctx.reply(embed=embed) + + +async def setup(bot): + await bot.add_cog(_encrypt(bot)) diff --git a/bot/cogs/zyrox/extra.py b/bot/cogs/zyrox/extra.py new file mode 100644 index 0000000..9d31896 --- /dev/null +++ b/bot/cogs/zyrox/extra.py @@ -0,0 +1,34 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ZWRENCH +from discord.ext import commands + + +class _extra(commands.Cog): + def __init__(self, bot): + self.bot = bot + + """Utility commands""" + + def help_custom(self): + emoji = ZWRENCH + label = "Utility Commands" + description = "Show you Commands of Utility" + return emoji, label, description + + @commands.group() + async def __Utility__(self, ctx: commands.Context): + """`botinfo` , `stats` , `invite` , `serverinfo` , `userinfo` , `roleinfo` , `boostcount` , `unbanall` , `joined-at` , `ping` , `github` , `vcinfo` , `channelinfo` , `badges` , `banner user` , `banner server` , `reminder start` , `reminder clear` , `permissions` , `timer`\n\n__**Media Commands**__\n`media` , `media setup ` , `media remove` , `media config` , `media bypass` , `media bypass add` , `media bypass remove` , `media bypass show`""" \ No newline at end of file diff --git a/bot/cogs/zyrox/fun.py b/bot/cogs/zyrox/fun.py new file mode 100644 index 0000000..1dc7ca1 --- /dev/null +++ b/bot/cogs/zyrox/fun.py @@ -0,0 +1,34 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ZROCKET +from discord.ext import commands + + +class _fun(commands.Cog): + def __init__(self, bot): + self.bot = bot + + """Fun commands""" + + def help_custom(self): + emoji = ZROCKET + label = "Fun Commands" + description = "Show you Commands of Fun" + return emoji, label, description + + @commands.group() + async def __Fun__(self, ctx: commands.Context): + """`/imagine` , `ship` , `mydog` , `chat` , `translate` , `howgay` , `lesbian` , `cute` , `intelligence`, `chutiya` , `horny` , `tharki` , `gif` , `iplookup` , `weather` , `hug` , `kiss` , `pat` , `cuddle` , `slap` , `tickle` , `spank` , `8ball` , `truth` , `dare` , `nitro`""" \ No newline at end of file diff --git a/bot/cogs/zyrox/games.py b/bot/cogs/zyrox/games.py new file mode 100644 index 0000000..ba363e3 --- /dev/null +++ b/bot/cogs/zyrox/games.py @@ -0,0 +1,34 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import GAMES +from discord.ext import commands + + +class _games(commands.Cog): + def __init__(self, bot): + self.bot = bot + + """Games commands""" + + def help_custom(self): + emoji = GAMES + label = "Games Commands" + description = "Show you Commands of Games" + return emoji, label, description + + @commands.group() + async def __Games__(self, ctx: commands.Context): + """`blackjack` , `chess` , `tic-tac-toe` , `country-guesser` , `rps` , `lights-out` , `wordle` , `2048` , `memory-game` , `number-slider` , `battleship` , `connect-four` , `slots`, `counting`""" diff --git a/bot/cogs/zyrox/general.py b/bot/cogs/zyrox/general.py new file mode 100644 index 0000000..9cfda9e --- /dev/null +++ b/bot/cogs/zyrox/general.py @@ -0,0 +1,34 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ZMODULE +from discord.ext import commands + + +class _general(commands.Cog): + def __init__(self, bot): + self.bot = bot + + """General commands""" + + def help_custom(self): + emoji = ZMODULE + label = "General Commands" + description = "Show you Commands of General" + return emoji, label, description + + @commands.group() + async def __General__(self, ctx: commands.Context): + """`status` , `afk` , `avatar` , `banner` , `servericon` , `membercount` , `poll` , `users` , `urban` , `rickroll` , `hash` , `snipe` , `users` , `list boosters` , `list inrole` , `list emojis` , `list bots` , `list admins` , `list invoice` , `list mods` , `list early` , `list activedeveloper` , `list createpos` , `list roles` , `calculator`""" \ No newline at end of file diff --git a/bot/cogs/zyrox/giveaway.py b/bot/cogs/zyrox/giveaway.py new file mode 100644 index 0000000..d2bb28f --- /dev/null +++ b/bot/cogs/zyrox/giveaway.py @@ -0,0 +1,34 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ZTADA +from discord.ext import commands + + +class _giveaway(commands.Cog): + def __init__(self, bot): + self.bot = bot + + """Giveaway commands""" + + def help_custom(self): + emoji = f'{ZTADA} ' + label = "Giveaway Commands" + description = "Show you Commands of Giveaway" + return emoji, label, description + + @commands.group() + async def __Giveaway__(self, ctx: commands.Context): + """`gstart`, `gend`, `greroll` , `glist`""" \ No newline at end of file diff --git a/bot/cogs/zyrox/ignore.py b/bot/cogs/zyrox/ignore.py new file mode 100644 index 0000000..872085b --- /dev/null +++ b/bot/cogs/zyrox/ignore.py @@ -0,0 +1,34 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ZBAN +from discord.ext import commands + + +class _ignore(commands.Cog): + def __init__(self, bot): + self.bot = bot + + """Ignore commands""" + + def help_custom(self): + emoji = ZBAN + label = "Ignore Commands" + description = "Show you Commands of Ignore" + return emoji, label, description + + @commands.group() + async def __Ignore__(self, ctx: commands.Context): + """`ignore` , `ignore command add` , `ignore command remove` , `ignore command show` , `ignore channel add` , `ignore channel remove` , `ignore channel show` , `ignore user add` , `ignore user remove` , `ignore user show` , `ignore bypass add` , `ignore bypass show` , `ignore bypass remove`""" \ No newline at end of file diff --git a/bot/cogs/zyrox/inviteTracker.py b/bot/cogs/zyrox/inviteTracker.py new file mode 100644 index 0000000..f15b088 --- /dev/null +++ b/bot/cogs/zyrox/inviteTracker.py @@ -0,0 +1,42 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import ZPEOPLE + +from discord.ext import commands + +class inviteTracker(commands.Cog): + + def __init__(self, bot): + + self.bot = bot + + """Invite Tracker""" + + def help_custom(self): + + emoji = ZPEOPLE + + label = "Invite Tracker" + + description = "Show you Commands of Invite Tracker" + + return emoji, label, description + + @commands.group() + + async def __InviteTracker__(self, ctx: commands.Context): + + """`>invites`, `>addinvites`, `>inviteleaderboard`, `>invitelogging`""" \ No newline at end of file diff --git a/bot/cogs/zyrox/j2c.py b/bot/cogs/zyrox/j2c.py new file mode 100644 index 0000000..084e7dd --- /dev/null +++ b/bot/cogs/zyrox/j2c.py @@ -0,0 +1,42 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import SYSTEM + +from discord.ext import commands + +class _J2C(commands.Cog): + + def __init__(self, bot): + + self.bot = bot + + """Join To Create""" + + def help_custom(self): + + emoji = SYSTEM + + label = "J2C" + + description = "Show you Commands of J2C" + + return emoji, label, description + + @commands.group() + + async def __J2C__(self, ctx: commands.Context): + + """`>j2csetup`, `>j2creset`""" \ No newline at end of file diff --git a/bot/cogs/zyrox/joindm.py b/bot/cogs/zyrox/joindm.py new file mode 100644 index 0000000..f6996f7 --- /dev/null +++ b/bot/cogs/zyrox/joindm.py @@ -0,0 +1,30 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import MESSAGE +from discord.ext import commands + +class _joindm(commands.Cog): + def __init__(self, bot): + self.bot = bot + """__Join Dm__""" + def help_custom(self): + emoji = MESSAGE + label = "Joindm" + description = "Show you Commands of Joindm" + return emoji, label, description + @commands.group() + async def __Joindm__(self, ctx: commands.Context): + """`joindm enable` , `joindm disable` , `joindm message` , `joindm test`""" \ No newline at end of file diff --git a/bot/cogs/zyrox/leveling.py b/bot/cogs/zyrox/leveling.py new file mode 100644 index 0000000..6ad2d41 --- /dev/null +++ b/bot/cogs/zyrox/leveling.py @@ -0,0 +1,34 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import LEVEL_UP +from discord .ext import commands + + +class _leveling (commands .Cog ): + def __init__ (self ,bot ): + self .bot =bot + + """Leveling commands""" + + def help_custom (self ): + emoji =LEVEL_UP + label ="Leveling Commands" + description ="Shows you the commands of leveling" + return emoji ,label ,description + + @commands .group () + async def __Leveling__ (self ,ctx :commands .Context ): + """`level status`, `level channel`, `level message`, `level desc`, `level color`, `level thumbnail`, `level image`, `level clearimage`, `level xprange`, `level multiplier`, `level addreward`, `level removereward`, `level rewards`, `level setxp`, `level preview`, `level xpboost`, `level xpboost add`, `level xpboost remove`, `level xpboost list`, `level blacklist`, `level blacklist channel`, `level blacklist role`, `level unblacklist`, `level unblacklist channel`, `level unblacklist role`, `level stats`, `level leaderboard`, `level reset`, `level reset user`, `level reset all`, `level placeholders`, `level rank`""" \ No newline at end of file diff --git a/bot/cogs/zyrox/logging.py b/bot/cogs/zyrox/logging.py new file mode 100644 index 0000000..dcad8f9 --- /dev/null +++ b/bot/cogs/zyrox/logging.py @@ -0,0 +1,33 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import CAST +from discord .ext import commands + +class _logging (commands .Cog ): + def __init__ (self ,bot ): + self .bot =bot + + """Logging commands""" + + def help_custom (self ): + emoji =CAST + label ="Logging Commands" + description ="Shows you the commands of logging" + return emoji ,label ,description + + @commands .group () + async def __Logging__ (self ,ctx :commands .Context ): + """`log`, `log enable`, `log disable`, `log config`, `log ignore`, `log status`, `log toggle`""" diff --git a/bot/cogs/zyrox/mc.py b/bot/cogs/zyrox/mc.py new file mode 100644 index 0000000..3b85342 --- /dev/null +++ b/bot/cogs/zyrox/mc.py @@ -0,0 +1,35 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import MINECRAFT +from discord.ext import commands + + +class _mc(commands.Cog): + def __init__(self, bot): + self.bot = bot + + """Minecraft commands""" + + def help_custom(self): + emoji = MINECRAFT + label = "Minecraft Commands" + description = "Show you Commands of Minecraft" + return emoji, label, description + + @commands.group() + async def __Minecraft__(self, ctx: commands.Context): + """`minecraft setup` , `minecraft reset` , `minecraft status`""" + pass diff --git a/bot/cogs/zyrox/moderation.py b/bot/cogs/zyrox/moderation.py new file mode 100644 index 0000000..00d150f --- /dev/null +++ b/bot/cogs/zyrox/moderation.py @@ -0,0 +1,34 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import SWORD +from discord.ext import commands + + +class _moderation(commands.Cog): + def __init__(self, bot): + self.bot = bot + + """Moderation commands""" + + def help_custom(self): + emoji = f'{SWORD} ' + label = "Moderation Commands" + description = "Show you Commands of Moderation" + return emoji, label, description + + @commands.group() + async def __Moderation__(self, ctx: commands.Context): + """`audit` , `warn` , `clearwarns` , `ban` , `clone` , `snipe` , `hide` , `hideall` , `kick` , `lock` , `mute` , `nick` , `nuke` , `role` , `roleicon` , `role all` , `role bots` , `role create` , `role delete` , `role humans` , `role rename` , `role temp` , `role unverified` , `slowmode` , `lockall` `unlockall` , `steal` , `unban` , `unhide` , `unhideall` , `unlock` , `unslowmode` , `removerole all` , `removerole bots` , `removerole humans` , `removerole unverified` , `clear` , `clear all` , `clear bots` , `clear contains` , `clear embeds` , `clear files` , `clear images` , `clear mentions` , `clear reactions` , `clear user` , `deleteemoji` , `deletesticker` , `enlarge`\n\n`topcheck` , `topcheck enable` , `topcheck disable`""" \ No newline at end of file diff --git a/bot/cogs/zyrox/music.py b/bot/cogs/zyrox/music.py new file mode 100644 index 0000000..8b6fab4 --- /dev/null +++ b/bot/cogs/zyrox/music.py @@ -0,0 +1,34 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import MUSIC +from discord.ext import commands + + +class _music(commands.Cog): + def __init__(self, bot): + self.bot = bot + + """Music commands""" + + def help_custom(self): + emoji = MUSIC + label = "Music Commands" + description = "Show you Commands of Music" + return emoji, label, description + + @commands.group() + async def __Music__(self, ctx: commands.Context): + """`play` , `search` , `loop` , `autoplay` , `nowplaying` , `shuffle` , `stop` , `skip` , `seek` , `join` , `disconnect` , `replay` , `queue` , `clearqueue` , `pause` , `resume` , `volume` , `filter` , `filter enable` , `filter disable`""" \ No newline at end of file diff --git a/bot/cogs/zyrox/server.py b/bot/cogs/zyrox/server.py new file mode 100644 index 0000000..e7ff4b7 --- /dev/null +++ b/bot/cogs/zyrox/server.py @@ -0,0 +1,34 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import WIFI +from discord.ext import commands + +class _server(commands.Cog): + def __init__(self, bot): + self.bot = bot + + """Server commands""" + + def help_custom(self): + emoji = WIFI + label = "Server Commands" + description = "Show you Commands of Server" + return emoji, label, description + + @commands.group() + async def __Setup__(self, ctx: commands.Context): + """`setup` , `setup create ` , `setup delete ` , `setup list` , `setup staff` , `setup girl` , `setup friend` , `setup vip` , `setup guest` , `setup config` , `setup reset` , `staff` , `girl` , `friend` , `vip` , `guest`\n\n__**Auto Role**__\n`autorole bots add` , `autorole bots remove` , `autorole bots` , `autorole config` , `autorole humans add` , `autorole humans remove` , `autorole humans` , `autorole reset all` , `autorole reset bots` , `autorole reset humans` , `autorole`\n\n__**Autoresponder**__\n`autoresponder` , `autoresponder create` , `autoresponder delete` , `autoresponder edit` , `autoresponder config`\n\n__**Auto React Commands**__\n`react` , `react add` , `react remove` , `react list` , `react reset`""" + diff --git a/bot/cogs/zyrox/sticky.py b/bot/cogs/zyrox/sticky.py new file mode 100644 index 0000000..e957cd7 --- /dev/null +++ b/bot/cogs/zyrox/sticky.py @@ -0,0 +1,36 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import PIN +from discord.ext import commands + + +class _sticky(commands.Cog): + def __init__(self, bot): + self.bot = bot + + """Sticky commands""" + + def help_custom(self): + emoji = PIN + label = "Sticky Commands" + description = "Show you Commands of Sticky" + return emoji, label, description + + @commands.group() + async def __Sticky__(self, ctx: commands.Context): + """`sticky setup` , `sticky edit` , `sticky list` , `sticky remove`""" + pass + diff --git a/bot/cogs/zyrox/ticket.py b/bot/cogs/zyrox/ticket.py new file mode 100644 index 0000000..a75f339 --- /dev/null +++ b/bot/cogs/zyrox/ticket.py @@ -0,0 +1,42 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import TICKET + +from discord.ext import commands + +class _ticket(commands.Cog): + + def __init__(self, bot): + + self.bot = bot + + """Ticket""" + + def help_custom(self): + + emoji = TICKET + + label = "Ticket" + + description = "Show you Commands of Ticket" + + return emoji, label, description + + @commands.group() + + async def __Ticket__(self, ctx: commands.Context): + + """`/ticket setup`, `/ticket close`, `/ticket lock`, `/ticket claim`, `/ticket unlock`, `/ticket transcript`""" \ No newline at end of file diff --git a/bot/cogs/zyrox/vanity.py b/bot/cogs/zyrox/vanity.py new file mode 100644 index 0000000..6e6c6c4 --- /dev/null +++ b/bot/cogs/zyrox/vanity.py @@ -0,0 +1,42 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import STAR + +from discord.ext import commands + +class _vanity(commands.Cog): + + def __init__(self, bot): + + self.bot = bot + + """Vanity Roles""" + + def help_custom(self): + + emoji = STAR + + label = "Vanity" + + description = "Show you Commands of Vanity Roles" + + return emoji, label, description + + @commands.group() + + async def __Vanity__(self, ctx: commands.Context): + + """`>vanityroles setup` , `>vanityroles reset `, `>vanityroles show` ,""" \ No newline at end of file diff --git a/bot/cogs/zyrox/verify.py b/bot/cogs/zyrox/verify.py new file mode 100644 index 0000000..6298bc4 --- /dev/null +++ b/bot/cogs/zyrox/verify.py @@ -0,0 +1,37 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import THUNDER +from discord .ext import commands + +class _verify(commands .Cog ): + def __init__ (self ,bot ): + self.bot=bot + + """Verification commands help""" + + def help_custom (self ): + emoji =THUNDER + label ="Verification Commands" + description ="Show you the commands of Verification" + return emoji ,label ,description + + @commands .group () + async def __Verification__ (self ,ctx :commands .Context ): + """`verification setup`, `verification status`, `verification enable`, `verification disable`, `verification logs`, `verification reset`, `verification verify`, `verification fix`""" + pass + +async def setup (bot ): + await bot.add_cog(_verify(bot)) diff --git a/bot/cogs/zyrox/voice.py b/bot/cogs/zyrox/voice.py new file mode 100644 index 0000000..5419d06 --- /dev/null +++ b/bot/cogs/zyrox/voice.py @@ -0,0 +1,41 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import MUTE +from discord.ext import commands + + +class _voice(commands.Cog): + def __init__(self, bot): + self.bot = bot + + """Voice commands""" + + def help_custom(self): + emoji = MUTE + label = "Voice Commands" + description = "Show you Command Of Voice" + return emoji, label, description + + @commands.group() + async def __Voice__(self, ctx: commands.Context): + """ + `voice` , `voice kick` , `voice kickall` , `voice mute` , `voice muteall` , `voice unmute` , `voice unmuteall` , `voice deafen` , `voice deafenall` , `voice undeafen` , `voice undeafenall` , `voice move` , `voice moveall` , `voice pull` , `voice pullall` , `voice lock` , `voice unlock` , `voice private` , `voice unprivate`\n\n**__VC Autorole__**\n`vcrole add` , `vcrole remove` , `vcrole config`""" + + + + + + diff --git a/bot/cogs/zyrox/welcome.py b/bot/cogs/zyrox/welcome.py new file mode 100644 index 0000000..24bcf20 --- /dev/null +++ b/bot/cogs/zyrox/welcome.py @@ -0,0 +1,34 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.emoji import SEED +from discord.ext import commands + + +class _welcome(commands.Cog): + def __init__(self, bot): + self.bot = bot + + """Welcome commands""" + + def help_custom(self): + emoji = SEED + label = "Welcomer Commands" + description = "Show you Command Of Welcomer" + return emoji, label, description + + @commands.group() + async def __Welcomer__(self, ctx: commands.Context): + """`greet setup` , `greet reset`, `greet channel` , `greet edit` , `greet test` , `greet config` , `greet autodeletete` , `greet`""" \ No newline at end of file diff --git a/bot/config.yml b/bot/config.yml new file mode 100644 index 0000000..d965341 --- /dev/null +++ b/bot/config.yml @@ -0,0 +1,38 @@ +API_BASE_URL: https://api.groq.com/openai/v1/ +INTERNET_ACCESS: true +MAX_SEARCH_RESULTS: 4 +ALLOW_DM: true +SMART_MENTION: true +MODEL_ID: mixtral-8x7b-32768 +MAX_HISTORY: 8 +PRESENCES_CHANGE_DELAY: 8 +AI_NSFW_CONTENT_FILTER: true +LANGUAGE: en +DEFAULT_INSTRUCTION: soniye + +Discord: https://discord.gg/codexdev + +TRIGGER: + - chatbot + - $ + - soniye + +AUTO_SHARDING: true + +DISABLE_PRESENCE: false +PRESENCES: + - How can I assist you? + - Currently in {guild_count} guilds + - Support at https://discord.gg/codexdev + + +BLACKLIST_WORDS: #changed to cogs/imagine.py + - naked + - loli + - hentai + - explicit + - pornography + - adult + - XXX + - sex + - erotic \ No newline at end of file diff --git a/bot/core/Cog.py b/bot/core/Cog.py new file mode 100644 index 0000000..7d9a276 --- /dev/null +++ b/bot/core/Cog.py @@ -0,0 +1,28 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from discord.ext import commands + +__all__ = ("Cog",) + + +class Cog(commands.Cog): + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + def __str__(self) -> str: + return "{0.__class__.__name__}".format(self) \ No newline at end of file diff --git a/bot/core/Context.py b/bot/core/Context.py new file mode 100644 index 0000000..0155820 --- /dev/null +++ b/bot/core/Context.py @@ -0,0 +1,90 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from discord.ext import commands +import discord +import functools +from typing import Optional, Any +import asyncio + +__all__ = ("Context", ) + + +class Context(commands.Context): + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + def __repr__(self): + return "" + + @property + async def session(self): + return self.bot.session + + @discord.utils.cached_property + def replied_reference(self) -> Optional[discord.Message]: + ref = self.message.reference + if ref and isinstance(ref.resolved, discord.Message): + return ref.resolved.to_reference() + return None + + def with_type(func): + + @functools.wraps(func) + async def wrapped(self, *args, **kwargs): + context = args[0] if isinstance(args[0], + commands.Context) else args[1] + try: + async with context.typing(): + await func(*args, **kwargs) + except discord.Forbidden: + await func(*args, **kwargs) + + return wrapped + + async def show_help(self, command: str = None) -> Any: + cmd = self.bot.get_command('help') + command = command or self.command.qualified_name + await self.invoke(cmd, command=command) + + async def send(self, + content: Optional[str] = None, + **kwargs) -> Optional[discord.Message]: + if not (self.channel.permissions_for(self.me)).send_messages: + try: + await self.author.send( + "bot dont have perms to send msg in that channel") + except discord.Forbidden: + pass + return + return await super().send(content, **kwargs) + + async def reply(self, + content: Optional[str] = None, + **kwargs) -> Optional[discord.Message]: + if not (self.channel.permissions_for(self.me)).send_messages: + try: + await self.author.send( + "bot dont have perms to send msg in that channel") + except discord.Forbidden: + pass + return + return await super().reply(content, **kwargs) + + async def release(self, delay: Optional[int] = None) -> None: + delay = delay or 0 + await asyncio.sleep(delay) diff --git a/bot/core/__init__.py b/bot/core/__init__.py new file mode 100644 index 0000000..6fcc598 --- /dev/null +++ b/bot/core/__init__.py @@ -0,0 +1,17 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from .zyrox import zyrox +from .Context import Context +from .Cog import Cog \ No newline at end of file diff --git a/bot/core/zyrox.py b/bot/core/zyrox.py new file mode 100644 index 0000000..78e0b08 --- /dev/null +++ b/bot/core/zyrox.py @@ -0,0 +1,143 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations +from discord.ext import commands, tasks +import discord +import aiohttp +import typing +from typing import List +import aiosqlite +from utils.config import OWNER_IDS, BotName +from utils import getConfig, updateConfig +from .Context import Context +from colorama import Fore, Style, init +import importlib +import inspect + +init(autoreset=True) + +# Corrected the extensions list +extensions: List[str] = [ + "cogs" +] + +class zyrox(commands.AutoShardedBot): + def __init__(self, *arg, **kwargs): + intents = discord.Intents.all() + intents.presences = True + intents.members = True + super().__init__(command_prefix=self.get_prefix, + case_insensitive=True, + intents=intents, + # The status is already set to Do Not Disturb here + status=discord.Status.do_not_disturb, + strip_after_prefix=True, + owner_ids=OWNER_IDS, + allowed_mentions=discord.AllowedMentions( + everyone=False, replied_user=False, roles=False), + sync_commands_debug=True, + sync_commands=True, + shard_count=1) + self.status_index = 0 + self.status_list = [] + + async def setup_hook(self): + await self.load_extensions() + self.status_task.start() + + async def load_extensions(self): + for extension in extensions: + try: + await self.load_extension(extension) + print(Fore.GREEN + Style.BRIGHT + f"Loaded extension: {extension}") + except Exception as e: + print(f"{Fore.RED}{Style.BRIGHT}Failed to load extension {extension}. {e}") + print(Fore.GREEN + Style.BRIGHT + "*" * 20) + + @tasks.loop(seconds=30) + async def status_task(self): + await self.wait_until_ready() + if not self.guilds: + return + + guild = self.guilds[0] # Use first available guild for prefix + try: + config = await getConfig(guild.id) + prefix = config.get("prefix", ">") + except: + prefix = ">" + + user_count = sum(g.member_count or 0 for g in self.guilds) + guild_count = len(self.guilds) + + self.status_list = [ + (discord.ActivityType.playing, f"{prefix}help | Security in your Server"), + (discord.ActivityType.watching, f"{user_count} users"), + (discord.ActivityType.watching, f"{guild_count} servers"), + (discord.ActivityType.listening, "Killing Nukers"), + (discord.ActivityType.playing, f"Protector {BotName}"), + ] + + current = self.status_list[self.status_index % len(self.status_list)] + # This task only changes the activity, not the online status (dnd, idle, etc.) + await self.change_presence(activity=discord.Activity(type=current[0], name=current[1])) + self.status_index += 1 + + async def send_raw(self, channel_id: int, content: str, **kwargs) -> typing.Optional[discord.Message]: + await self.http.send_message(channel_id, content, **kwargs) + + async def invoke_help_command(self, ctx: Context) -> None: + return await ctx.send_help(ctx.command) + + async def fetch_message_by_channel(self, channel: discord.TextChannel, messageID: int) -> typing.Optional[discord.Message]: + async for msg in channel.history(limit=1, before=discord.Object(messageID + 1), after=discord.Object(messageID - 1)): + return msg + + async def get_prefix(self, message: discord.Message): + if message.guild: + guild_id = message.guild.id + async with aiosqlite.connect('db/np.db') as db: + async with db.execute("SELECT id FROM np WHERE id = ?", (message.author.id,)) as cursor: + row = await cursor.fetchone() + data = await getConfig(guild_id) + prefix = data["prefix"] + if row: + return commands.when_mentioned_or(prefix, '')(self, message) + else: + return commands.when_mentioned_or(prefix)(self, message) + else: + async with aiosqlite.connect('db/np.db') as db: + async with db.execute("SELECT id FROM np WHERE id = ?", (message.author.id,)) as cursor: + row = await cursor.fetchone() + if row: + return commands.when_mentioned_or('?', '')(self, message) + else: + return commands.when_mentioned_or('')(self, message) + + async def on_message_edit(self, before, after): + ctx: Context = await self.get_context(after, cls=Context) + if before.content != after.content: + if after.guild is None or after.author.bot: + return + if ctx.command is None: + return + if type(ctx.channel) == "public_thread": + return + await self.invoke(ctx) + +def setup_bot(): + intents = discord.Intents.all() + bot = zyrox(intents=intents) + return bot diff --git a/bot/data/1147899646602121216.png b/bot/data/1147899646602121216.png new file mode 100644 index 0000000..39b57e6 Binary files /dev/null and b/bot/data/1147899646602121216.png differ diff --git a/bot/data/500293365494054932.png b/bot/data/500293365494054932.png new file mode 100644 index 0000000..80a62a8 Binary files /dev/null and b/bot/data/500293365494054932.png differ diff --git a/bot/data/952846045124120607.png b/bot/data/952846045124120607.png new file mode 100644 index 0000000..a0156be Binary files /dev/null and b/bot/data/952846045124120607.png differ diff --git a/bot/data/cards/10C.png b/bot/data/cards/10C.png new file mode 100644 index 0000000..a480340 Binary files /dev/null and b/bot/data/cards/10C.png differ diff --git a/bot/data/cards/10D.png b/bot/data/cards/10D.png new file mode 100644 index 0000000..e00abc5 Binary files /dev/null and b/bot/data/cards/10D.png differ diff --git a/bot/data/cards/10H.png b/bot/data/cards/10H.png new file mode 100644 index 0000000..3e95b05 Binary files /dev/null and b/bot/data/cards/10H.png differ diff --git a/bot/data/cards/10S.png b/bot/data/cards/10S.png new file mode 100644 index 0000000..52338a8 Binary files /dev/null and b/bot/data/cards/10S.png differ diff --git a/bot/data/cards/2C.png b/bot/data/cards/2C.png new file mode 100644 index 0000000..8a76d6e Binary files /dev/null and b/bot/data/cards/2C.png differ diff --git a/bot/data/cards/2D.png b/bot/data/cards/2D.png new file mode 100644 index 0000000..3ae35fc Binary files /dev/null and b/bot/data/cards/2D.png differ diff --git a/bot/data/cards/2H.png b/bot/data/cards/2H.png new file mode 100644 index 0000000..9f7debe Binary files /dev/null and b/bot/data/cards/2H.png differ diff --git a/bot/data/cards/2S.png b/bot/data/cards/2S.png new file mode 100644 index 0000000..353fe87 Binary files /dev/null and b/bot/data/cards/2S.png differ diff --git a/bot/data/cards/3C.png b/bot/data/cards/3C.png new file mode 100644 index 0000000..7791836 Binary files /dev/null and b/bot/data/cards/3C.png differ diff --git a/bot/data/cards/3D.png b/bot/data/cards/3D.png new file mode 100644 index 0000000..f3d7abe Binary files /dev/null and b/bot/data/cards/3D.png differ diff --git a/bot/data/cards/3H.png b/bot/data/cards/3H.png new file mode 100644 index 0000000..89aebaa Binary files /dev/null and b/bot/data/cards/3H.png differ diff --git a/bot/data/cards/3S.png b/bot/data/cards/3S.png new file mode 100644 index 0000000..3109650 Binary files /dev/null and b/bot/data/cards/3S.png differ diff --git a/bot/data/cards/4C.png b/bot/data/cards/4C.png new file mode 100644 index 0000000..1319a50 Binary files /dev/null and b/bot/data/cards/4C.png differ diff --git a/bot/data/cards/4D.png b/bot/data/cards/4D.png new file mode 100644 index 0000000..5539d75 Binary files /dev/null and b/bot/data/cards/4D.png differ diff --git a/bot/data/cards/4H.png b/bot/data/cards/4H.png new file mode 100644 index 0000000..8426b8f Binary files /dev/null and b/bot/data/cards/4H.png differ diff --git a/bot/data/cards/4S.png b/bot/data/cards/4S.png new file mode 100644 index 0000000..043afa5 Binary files /dev/null and b/bot/data/cards/4S.png differ diff --git a/bot/data/cards/5C.png b/bot/data/cards/5C.png new file mode 100644 index 0000000..301aa7b Binary files /dev/null and b/bot/data/cards/5C.png differ diff --git a/bot/data/cards/5D.png b/bot/data/cards/5D.png new file mode 100644 index 0000000..fe095b2 Binary files /dev/null and b/bot/data/cards/5D.png differ diff --git a/bot/data/cards/5H.png b/bot/data/cards/5H.png new file mode 100644 index 0000000..a94db67 Binary files /dev/null and b/bot/data/cards/5H.png differ diff --git a/bot/data/cards/5S.png b/bot/data/cards/5S.png new file mode 100644 index 0000000..c03b24b Binary files /dev/null and b/bot/data/cards/5S.png differ diff --git a/bot/data/cards/6C.png b/bot/data/cards/6C.png new file mode 100644 index 0000000..17358c6 Binary files /dev/null and b/bot/data/cards/6C.png differ diff --git a/bot/data/cards/6D.png b/bot/data/cards/6D.png new file mode 100644 index 0000000..fd457d6 Binary files /dev/null and b/bot/data/cards/6D.png differ diff --git a/bot/data/cards/6H.png b/bot/data/cards/6H.png new file mode 100644 index 0000000..efcb385 Binary files /dev/null and b/bot/data/cards/6H.png differ diff --git a/bot/data/cards/6S.png b/bot/data/cards/6S.png new file mode 100644 index 0000000..899adf5 Binary files /dev/null and b/bot/data/cards/6S.png differ diff --git a/bot/data/cards/7C.png b/bot/data/cards/7C.png new file mode 100644 index 0000000..5f30e60 Binary files /dev/null and b/bot/data/cards/7C.png differ diff --git a/bot/data/cards/7D.png b/bot/data/cards/7D.png new file mode 100644 index 0000000..b211f14 Binary files /dev/null and b/bot/data/cards/7D.png differ diff --git a/bot/data/cards/7H.png b/bot/data/cards/7H.png new file mode 100644 index 0000000..b1f51f8 Binary files /dev/null and b/bot/data/cards/7H.png differ diff --git a/bot/data/cards/7S.png b/bot/data/cards/7S.png new file mode 100644 index 0000000..66b02d0 Binary files /dev/null and b/bot/data/cards/7S.png differ diff --git a/bot/data/cards/8C.png b/bot/data/cards/8C.png new file mode 100644 index 0000000..401dd4b Binary files /dev/null and b/bot/data/cards/8C.png differ diff --git a/bot/data/cards/8D.png b/bot/data/cards/8D.png new file mode 100644 index 0000000..f1a50e4 Binary files /dev/null and b/bot/data/cards/8D.png differ diff --git a/bot/data/cards/8H.png b/bot/data/cards/8H.png new file mode 100644 index 0000000..e977bc6 Binary files /dev/null and b/bot/data/cards/8H.png differ diff --git a/bot/data/cards/8S.png b/bot/data/cards/8S.png new file mode 100644 index 0000000..0124ce1 Binary files /dev/null and b/bot/data/cards/8S.png differ diff --git a/bot/data/cards/9C.png b/bot/data/cards/9C.png new file mode 100644 index 0000000..73cb646 Binary files /dev/null and b/bot/data/cards/9C.png differ diff --git a/bot/data/cards/9D.png b/bot/data/cards/9D.png new file mode 100644 index 0000000..7e2a7ef Binary files /dev/null and b/bot/data/cards/9D.png differ diff --git a/bot/data/cards/9H.png b/bot/data/cards/9H.png new file mode 100644 index 0000000..8fca4ab Binary files /dev/null and b/bot/data/cards/9H.png differ diff --git a/bot/data/cards/9S.png b/bot/data/cards/9S.png new file mode 100644 index 0000000..5eb8b9f Binary files /dev/null and b/bot/data/cards/9S.png differ diff --git a/bot/data/cards/AC.png b/bot/data/cards/AC.png new file mode 100644 index 0000000..ec42917 Binary files /dev/null and b/bot/data/cards/AC.png differ diff --git a/bot/data/cards/AD.png b/bot/data/cards/AD.png new file mode 100644 index 0000000..c755b96 Binary files /dev/null and b/bot/data/cards/AD.png differ diff --git a/bot/data/cards/AH.png b/bot/data/cards/AH.png new file mode 100644 index 0000000..9ec396c Binary files /dev/null and b/bot/data/cards/AH.png differ diff --git a/bot/data/cards/AS.png b/bot/data/cards/AS.png new file mode 100644 index 0000000..27242e5 Binary files /dev/null and b/bot/data/cards/AS.png differ diff --git a/bot/data/cards/JC.png b/bot/data/cards/JC.png new file mode 100644 index 0000000..b74c53c Binary files /dev/null and b/bot/data/cards/JC.png differ diff --git a/bot/data/cards/JD.png b/bot/data/cards/JD.png new file mode 100644 index 0000000..953e289 Binary files /dev/null and b/bot/data/cards/JD.png differ diff --git a/bot/data/cards/JH.png b/bot/data/cards/JH.png new file mode 100644 index 0000000..14d0198 Binary files /dev/null and b/bot/data/cards/JH.png differ diff --git a/bot/data/cards/JS.png b/bot/data/cards/JS.png new file mode 100644 index 0000000..c4cae81 Binary files /dev/null and b/bot/data/cards/JS.png differ diff --git a/bot/data/cards/KC.png b/bot/data/cards/KC.png new file mode 100644 index 0000000..e884b31 Binary files /dev/null and b/bot/data/cards/KC.png differ diff --git a/bot/data/cards/KD.png b/bot/data/cards/KD.png new file mode 100644 index 0000000..fece39a Binary files /dev/null and b/bot/data/cards/KD.png differ diff --git a/bot/data/cards/KH.png b/bot/data/cards/KH.png new file mode 100644 index 0000000..0c857f8 Binary files /dev/null and b/bot/data/cards/KH.png differ diff --git a/bot/data/cards/KS.png b/bot/data/cards/KS.png new file mode 100644 index 0000000..505b3b4 Binary files /dev/null and b/bot/data/cards/KS.png differ diff --git a/bot/data/cards/QC.png b/bot/data/cards/QC.png new file mode 100644 index 0000000..00143d3 Binary files /dev/null and b/bot/data/cards/QC.png differ diff --git a/bot/data/cards/QD.png b/bot/data/cards/QD.png new file mode 100644 index 0000000..f28c02b Binary files /dev/null and b/bot/data/cards/QD.png differ diff --git a/bot/data/cards/QH.png b/bot/data/cards/QH.png new file mode 100644 index 0000000..6cafe44 Binary files /dev/null and b/bot/data/cards/QH.png differ diff --git a/bot/data/cards/QS.png b/bot/data/cards/QS.png new file mode 100644 index 0000000..3955655 Binary files /dev/null and b/bot/data/cards/QS.png differ diff --git a/bot/data/cards/aces.png b/bot/data/cards/aces.png new file mode 100644 index 0000000..58d0e46 Binary files /dev/null and b/bot/data/cards/aces.png differ diff --git a/bot/data/cards/red_back.png b/bot/data/cards/red_back.png new file mode 100644 index 0000000..1164d5c Binary files /dev/null and b/bot/data/cards/red_back.png differ diff --git a/bot/data/cards/table.png b/bot/data/cards/table.png new file mode 100644 index 0000000..72c3f47 Binary files /dev/null and b/bot/data/cards/table.png differ diff --git a/bot/data/pictures/album_image.png b/bot/data/pictures/album_image.png new file mode 100644 index 0000000..eee5e2d Binary files /dev/null and b/bot/data/pictures/album_image.png differ diff --git a/bot/data/pictures/mydog.jpg b/bot/data/pictures/mydog.jpg new file mode 100644 index 0000000..e3c8707 Binary files /dev/null and b/bot/data/pictures/mydog.jpg differ diff --git a/bot/data/pictures/player.png b/bot/data/pictures/player.png new file mode 100644 index 0000000..9ec5968 Binary files /dev/null and b/bot/data/pictures/player.png differ diff --git a/bot/data/pictures/slot-face.png b/bot/data/pictures/slot-face.png new file mode 100644 index 0000000..656d0ce Binary files /dev/null and b/bot/data/pictures/slot-face.png differ diff --git a/bot/data/pictures/slot-reel.png b/bot/data/pictures/slot-reel.png new file mode 100644 index 0000000..a5c7d58 Binary files /dev/null and b/bot/data/pictures/slot-reel.png differ diff --git a/bot/data/pictures/spotify.png b/bot/data/pictures/spotify.png new file mode 100644 index 0000000..faab2ee Binary files /dev/null and b/bot/data/pictures/spotify.png differ diff --git a/bot/data/pictures/spotify_card_output.png b/bot/data/pictures/spotify_card_output.png new file mode 100644 index 0000000..f642d07 Binary files /dev/null and b/bot/data/pictures/spotify_card_output.png differ diff --git a/bot/data/pictures/table.png b/bot/data/pictures/table.png new file mode 100644 index 0000000..72c3f47 Binary files /dev/null and b/bot/data/pictures/table.png differ diff --git a/bot/data/ship/Template.png b/bot/data/ship/Template.png new file mode 100644 index 0000000..1dffdce Binary files /dev/null and b/bot/data/ship/Template.png differ diff --git a/bot/data/ship/Tmpl_fill.png b/bot/data/ship/Tmpl_fill.png new file mode 100644 index 0000000..51c3fbb Binary files /dev/null and b/bot/data/ship/Tmpl_fill.png differ diff --git a/bot/data/ship/font.ttf b/bot/data/ship/font.ttf new file mode 100644 index 0000000..465c550 Binary files /dev/null and b/bot/data/ship/font.ttf differ diff --git a/bot/data/ship/tmp_ship.png b/bot/data/ship/tmp_ship.png new file mode 100644 index 0000000..6c3769c Binary files /dev/null and b/bot/data/ship/tmp_ship.png differ diff --git a/bot/db/_db.py b/bot/db/_db.py new file mode 100644 index 0000000..221e1ce --- /dev/null +++ b/bot/db/_db.py @@ -0,0 +1,64 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import aiosqlite +import asyncio +import random +from typing import Optional + +class Database: + _instance: Optional['Database'] = None + _lock: asyncio.Lock + db_path: str + db: Optional[aiosqlite.Connection] + + def __new__(cls, db_path='db/anti.db'): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance.db_path = db_path + cls._instance.db = None + cls._instance._lock = asyncio.Lock() + return cls._instance + + async def connect(self, timeout=30): + async with self._lock: + if self.db is None: + self.db = await aiosqlite.connect(self.db_path, timeout=timeout) + await self.db.execute('PRAGMA journal_mode=WAL;') + await self.db.commit() + return self.db + + async def ensure_connection(self): + if self.db is None or not self.db.is_open: + await self.connect() + return self.db + + async def execute_with_retries(self, func, retries=5, delay=1): + for attempt in range(retries): + try: + return await func() + except aiosqlite.OperationalError as e: + if 'database is locked' in str(e).lower(): + sleep_time = delay * (2 ** attempt) + random.uniform(0, 1) + print(f"Database is locked. Retryng in {sleep_time:.2f} seconds.....") + await asyncio.sleep(sleep_time) + else: + raise + raise Exception("Max retrys.") + + async def close(self): + async with self._lock: + if self.db is not None: + await self.db.close() + self.db = None \ No newline at end of file diff --git a/bot/db/admin_config.db b/bot/db/admin_config.db new file mode 100644 index 0000000..922b179 Binary files /dev/null and b/bot/db/admin_config.db differ diff --git a/bot/db/afk.db b/bot/db/afk.db new file mode 100644 index 0000000..0f07c08 Binary files /dev/null and b/bot/db/afk.db differ diff --git a/bot/db/ai_data.db b/bot/db/ai_data.db new file mode 100644 index 0000000..b275ce2 Binary files /dev/null and b/bot/db/ai_data.db differ diff --git a/bot/db/anti.db b/bot/db/anti.db new file mode 100644 index 0000000..cb98098 Binary files /dev/null and b/bot/db/anti.db differ diff --git a/bot/db/automod.db b/bot/db/automod.db new file mode 100644 index 0000000..b946384 Binary files /dev/null and b/bot/db/automod.db differ diff --git a/bot/db/autoreact.db b/bot/db/autoreact.db new file mode 100644 index 0000000..358e5cb Binary files /dev/null and b/bot/db/autoreact.db differ diff --git a/bot/db/autoresponder.db b/bot/db/autoresponder.db new file mode 100644 index 0000000..c2f7db7 Binary files /dev/null and b/bot/db/autoresponder.db differ diff --git a/bot/db/autorole.db b/bot/db/autorole.db new file mode 100644 index 0000000..1e2d037 Binary files /dev/null and b/bot/db/autorole.db differ diff --git a/bot/db/badges.db b/bot/db/badges.db new file mode 100644 index 0000000..8273ff8 Binary files /dev/null and b/bot/db/badges.db differ diff --git a/bot/db/block.db b/bot/db/block.db new file mode 100644 index 0000000..553a7a6 Binary files /dev/null and b/bot/db/block.db differ diff --git a/bot/db/blword.db b/bot/db/blword.db new file mode 100644 index 0000000..effc293 Binary files /dev/null and b/bot/db/blword.db differ diff --git a/bot/db/boost.db b/bot/db/boost.db new file mode 100644 index 0000000..9ba7281 Binary files /dev/null and b/bot/db/boost.db differ diff --git a/bot/db/counting.json b/bot/db/counting.json new file mode 100644 index 0000000..e982e1f --- /dev/null +++ b/bot/db/counting.json @@ -0,0 +1,8 @@ +{ + "1419729237480575070": { + "enabled": false, + "channel": 1421538951323193467, + "count": 4, + "reset_on_fail": false + } +} \ No newline at end of file diff --git a/bot/db/customrole.db b/bot/db/customrole.db new file mode 100644 index 0000000..24dff9a Binary files /dev/null and b/bot/db/customrole.db differ diff --git a/bot/db/emergency.db b/bot/db/emergency.db new file mode 100644 index 0000000..cb5e0c0 Binary files /dev/null and b/bot/db/emergency.db differ diff --git a/bot/db/fastgreet.db b/bot/db/fastgreet.db new file mode 100644 index 0000000..358491d Binary files /dev/null and b/bot/db/fastgreet.db differ diff --git a/bot/db/giveaways.db b/bot/db/giveaways.db new file mode 100644 index 0000000..42bcd90 Binary files /dev/null and b/bot/db/giveaways.db differ diff --git a/bot/db/ignore.db b/bot/db/ignore.db new file mode 100644 index 0000000..c29db71 Binary files /dev/null and b/bot/db/ignore.db differ diff --git a/bot/db/invc.db b/bot/db/invc.db new file mode 100644 index 0000000..f65d43f Binary files /dev/null and b/bot/db/invc.db differ diff --git a/bot/db/invite.db b/bot/db/invite.db new file mode 100644 index 0000000..0672c16 Binary files /dev/null and b/bot/db/invite.db differ diff --git a/bot/db/jail.db b/bot/db/jail.db new file mode 100644 index 0000000..1586e83 Binary files /dev/null and b/bot/db/jail.db differ diff --git a/bot/db/leveling.db b/bot/db/leveling.db new file mode 100644 index 0000000..ebb0fba Binary files /dev/null and b/bot/db/leveling.db differ diff --git a/bot/db/media.db b/bot/db/media.db new file mode 100644 index 0000000..70a499e Binary files /dev/null and b/bot/db/media.db differ diff --git a/bot/db/messages.db b/bot/db/messages.db new file mode 100644 index 0000000..e7eb9bf Binary files /dev/null and b/bot/db/messages.db differ diff --git a/bot/db/minecraft.db b/bot/db/minecraft.db new file mode 100644 index 0000000..3704e5c Binary files /dev/null and b/bot/db/minecraft.db differ diff --git a/bot/db/notify.db b/bot/db/notify.db new file mode 100644 index 0000000..5285e98 Binary files /dev/null and b/bot/db/notify.db differ diff --git a/bot/db/np.db b/bot/db/np.db new file mode 100644 index 0000000..20933b3 Binary files /dev/null and b/bot/db/np.db differ diff --git a/bot/db/prefix.db b/bot/db/prefix.db new file mode 100644 index 0000000..c583f73 Binary files /dev/null and b/bot/db/prefix.db differ diff --git a/bot/db/stats.db b/bot/db/stats.db new file mode 100644 index 0000000..0342a26 Binary files /dev/null and b/bot/db/stats.db differ diff --git a/bot/db/stickymessages.db b/bot/db/stickymessages.db new file mode 100644 index 0000000..a7e3ed3 Binary files /dev/null and b/bot/db/stickymessages.db differ diff --git a/bot/db/ticket.db b/bot/db/ticket.db new file mode 100644 index 0000000..e6b775d Binary files /dev/null and b/bot/db/ticket.db differ diff --git a/bot/db/ticket.db-journal b/bot/db/ticket.db-journal new file mode 100644 index 0000000..041dd2a Binary files /dev/null and b/bot/db/ticket.db-journal differ diff --git a/bot/db/topcheck.db b/bot/db/topcheck.db new file mode 100644 index 0000000..fe06a15 Binary files /dev/null and b/bot/db/topcheck.db differ diff --git a/bot/db/vanity.db b/bot/db/vanity.db new file mode 100644 index 0000000..686c9f8 Binary files /dev/null and b/bot/db/vanity.db differ diff --git a/bot/db/verification.db b/bot/db/verification.db new file mode 100644 index 0000000..9714347 Binary files /dev/null and b/bot/db/verification.db differ diff --git a/bot/db/warn.db b/bot/db/warn.db new file mode 100644 index 0000000..7844342 Binary files /dev/null and b/bot/db/warn.db differ diff --git a/bot/db/welcome.db b/bot/db/welcome.db new file mode 100644 index 0000000..4f7c530 Binary files /dev/null and b/bot/db/welcome.db differ diff --git a/bot/games/__init__.py b/bot/games/__init__.py new file mode 100644 index 0000000..38a2bf4 --- /dev/null +++ b/bot/games/__init__.py @@ -0,0 +1,74 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +"""Discord-Games + +A library designed for simple implementation of various classical games into a discord.py bot +""" +from __future__ import annotations + +from typing import NamedTuple + +#import os +#os.system("pip install chess") +#os.system("pip install english-words") +#os.system("pip install pillow") +#os.system("pip install typing_extensions") +#from .aki import Akinator +from .battleship import BattleShip +from .chess_game import Chess +from .connect_four import ConnectFour +#from .hangman import Hangman +from .tictactoe import Tictactoe +from .twenty_48 import Twenty48, create_2048_emojis +from .typeracer import TypeRacer +from .rps import RockPaperScissors +from .reaction_test import ReactionGame +from .country_guess import CountryGuesser +from .wordle import Wordle + +__all__: tuple[str, ...] = ( + "BattleShip", + "Chess", + "ConnectFour", + "Tictactoe", + "Twenty48", + "create_2048_emojis", + "TypeRacer", + "RockPaperScissors", + "ReactionGame", + "CountryGuesser", + "Wordle", +) + +__title__ = "discord_games" +__version__ = "1.10.6" +__author__ = "Tom-the-Bomb" +__license__ = "MIT" +__copyright__ = "Copyright 2021-present Tom-the-Bomb" + + +class VersionInfo(NamedTuple): + major: int + minor: int + micro: int + + +version_info: VersionInfo = VersionInfo( + major=1, + minor=10, + micro=6, +) + +del NamedTuple, VersionInfo diff --git a/bot/games/assets/2048-emoji-asset-examples/0.png b/bot/games/assets/2048-emoji-asset-examples/0.png new file mode 100644 index 0000000..84b4da7 Binary files /dev/null and b/bot/games/assets/2048-emoji-asset-examples/0.png differ diff --git a/bot/games/assets/2048-emoji-asset-examples/1024.png b/bot/games/assets/2048-emoji-asset-examples/1024.png new file mode 100644 index 0000000..de25dfe Binary files /dev/null and b/bot/games/assets/2048-emoji-asset-examples/1024.png differ diff --git a/bot/games/assets/2048-emoji-asset-examples/128.png b/bot/games/assets/2048-emoji-asset-examples/128.png new file mode 100644 index 0000000..66b79d9 Binary files /dev/null and b/bot/games/assets/2048-emoji-asset-examples/128.png differ diff --git a/bot/games/assets/2048-emoji-asset-examples/16.png b/bot/games/assets/2048-emoji-asset-examples/16.png new file mode 100644 index 0000000..a139908 Binary files /dev/null and b/bot/games/assets/2048-emoji-asset-examples/16.png differ diff --git a/bot/games/assets/2048-emoji-asset-examples/2.png b/bot/games/assets/2048-emoji-asset-examples/2.png new file mode 100644 index 0000000..81d9632 Binary files /dev/null and b/bot/games/assets/2048-emoji-asset-examples/2.png differ diff --git a/bot/games/assets/2048-emoji-asset-examples/2048.png b/bot/games/assets/2048-emoji-asset-examples/2048.png new file mode 100644 index 0000000..39aba40 Binary files /dev/null and b/bot/games/assets/2048-emoji-asset-examples/2048.png differ diff --git a/bot/games/assets/2048-emoji-asset-examples/256.png b/bot/games/assets/2048-emoji-asset-examples/256.png new file mode 100644 index 0000000..904516a Binary files /dev/null and b/bot/games/assets/2048-emoji-asset-examples/256.png differ diff --git a/bot/games/assets/2048-emoji-asset-examples/32.png b/bot/games/assets/2048-emoji-asset-examples/32.png new file mode 100644 index 0000000..b6f0f86 Binary files /dev/null and b/bot/games/assets/2048-emoji-asset-examples/32.png differ diff --git a/bot/games/assets/2048-emoji-asset-examples/4.png b/bot/games/assets/2048-emoji-asset-examples/4.png new file mode 100644 index 0000000..98f914c Binary files /dev/null and b/bot/games/assets/2048-emoji-asset-examples/4.png differ diff --git a/bot/games/assets/2048-emoji-asset-examples/4096.png b/bot/games/assets/2048-emoji-asset-examples/4096.png new file mode 100644 index 0000000..445b099 Binary files /dev/null and b/bot/games/assets/2048-emoji-asset-examples/4096.png differ diff --git a/bot/games/assets/2048-emoji-asset-examples/512.png b/bot/games/assets/2048-emoji-asset-examples/512.png new file mode 100644 index 0000000..b4e4b8f Binary files /dev/null and b/bot/games/assets/2048-emoji-asset-examples/512.png differ diff --git a/bot/games/assets/2048-emoji-asset-examples/64.png b/bot/games/assets/2048-emoji-asset-examples/64.png new file mode 100644 index 0000000..676433d Binary files /dev/null and b/bot/games/assets/2048-emoji-asset-examples/64.png differ diff --git a/bot/games/assets/2048-emoji-asset-examples/8.png b/bot/games/assets/2048-emoji-asset-examples/8.png new file mode 100644 index 0000000..e14bf84 Binary files /dev/null and b/bot/games/assets/2048-emoji-asset-examples/8.png differ diff --git a/bot/games/assets/2048-emoji-asset-examples/8192.png b/bot/games/assets/2048-emoji-asset-examples/8192.png new file mode 100644 index 0000000..105b5de Binary files /dev/null and b/bot/games/assets/2048-emoji-asset-examples/8192.png differ diff --git a/bot/games/assets/ClearSans-Bold.ttf b/bot/games/assets/ClearSans-Bold.ttf new file mode 100644 index 0000000..bb2d797 Binary files /dev/null and b/bot/games/assets/ClearSans-Bold.ttf differ diff --git a/bot/games/assets/HelveticaNeuBold.ttf b/bot/games/assets/HelveticaNeuBold.ttf new file mode 100644 index 0000000..9aa746a Binary files /dev/null and b/bot/games/assets/HelveticaNeuBold.ttf differ diff --git a/bot/games/assets/battleship.png b/bot/games/assets/battleship.png new file mode 100644 index 0000000..df80467 Binary files /dev/null and b/bot/games/assets/battleship.png differ diff --git a/bot/games/assets/country-data/Afghanistan.png b/bot/games/assets/country-data/Afghanistan.png new file mode 100644 index 0000000..00e44dc Binary files /dev/null and b/bot/games/assets/country-data/Afghanistan.png differ diff --git a/bot/games/assets/country-data/Albania.png b/bot/games/assets/country-data/Albania.png new file mode 100644 index 0000000..0b7cb26 Binary files /dev/null and b/bot/games/assets/country-data/Albania.png differ diff --git a/bot/games/assets/country-data/Algeria.png b/bot/games/assets/country-data/Algeria.png new file mode 100644 index 0000000..266bafb Binary files /dev/null and b/bot/games/assets/country-data/Algeria.png differ diff --git a/bot/games/assets/country-data/American Samoa.png b/bot/games/assets/country-data/American Samoa.png new file mode 100644 index 0000000..8ab68d6 Binary files /dev/null and b/bot/games/assets/country-data/American Samoa.png differ diff --git a/bot/games/assets/country-data/Andorra.png b/bot/games/assets/country-data/Andorra.png new file mode 100644 index 0000000..0c1410b Binary files /dev/null and b/bot/games/assets/country-data/Andorra.png differ diff --git a/bot/games/assets/country-data/Angola.png b/bot/games/assets/country-data/Angola.png new file mode 100644 index 0000000..efde9d4 Binary files /dev/null and b/bot/games/assets/country-data/Angola.png differ diff --git a/bot/games/assets/country-data/Anguilla.png b/bot/games/assets/country-data/Anguilla.png new file mode 100644 index 0000000..281a294 Binary files /dev/null and b/bot/games/assets/country-data/Anguilla.png differ diff --git a/bot/games/assets/country-data/Antarctica.png b/bot/games/assets/country-data/Antarctica.png new file mode 100644 index 0000000..c9ac47e Binary files /dev/null and b/bot/games/assets/country-data/Antarctica.png differ diff --git a/bot/games/assets/country-data/Antigua and Barbuda.png b/bot/games/assets/country-data/Antigua and Barbuda.png new file mode 100644 index 0000000..17e0e67 Binary files /dev/null and b/bot/games/assets/country-data/Antigua and Barbuda.png differ diff --git a/bot/games/assets/country-data/Argentina.png b/bot/games/assets/country-data/Argentina.png new file mode 100644 index 0000000..0724c49 Binary files /dev/null and b/bot/games/assets/country-data/Argentina.png differ diff --git a/bot/games/assets/country-data/Armenia.png b/bot/games/assets/country-data/Armenia.png new file mode 100644 index 0000000..5bc2d6b Binary files /dev/null and b/bot/games/assets/country-data/Armenia.png differ diff --git a/bot/games/assets/country-data/Aruba.png b/bot/games/assets/country-data/Aruba.png new file mode 100644 index 0000000..f357576 Binary files /dev/null and b/bot/games/assets/country-data/Aruba.png differ diff --git a/bot/games/assets/country-data/Australia.png b/bot/games/assets/country-data/Australia.png new file mode 100644 index 0000000..942b96f Binary files /dev/null and b/bot/games/assets/country-data/Australia.png differ diff --git a/bot/games/assets/country-data/Austria.png b/bot/games/assets/country-data/Austria.png new file mode 100644 index 0000000..5a716af Binary files /dev/null and b/bot/games/assets/country-data/Austria.png differ diff --git a/bot/games/assets/country-data/Azerbaijan.png b/bot/games/assets/country-data/Azerbaijan.png new file mode 100644 index 0000000..116b965 Binary files /dev/null and b/bot/games/assets/country-data/Azerbaijan.png differ diff --git a/bot/games/assets/country-data/Bahamas.png b/bot/games/assets/country-data/Bahamas.png new file mode 100644 index 0000000..1a46dbf Binary files /dev/null and b/bot/games/assets/country-data/Bahamas.png differ diff --git a/bot/games/assets/country-data/Bahrain.png b/bot/games/assets/country-data/Bahrain.png new file mode 100644 index 0000000..a8dca8d Binary files /dev/null and b/bot/games/assets/country-data/Bahrain.png differ diff --git a/bot/games/assets/country-data/Bangladesh.png b/bot/games/assets/country-data/Bangladesh.png new file mode 100644 index 0000000..6fb75f4 Binary files /dev/null and b/bot/games/assets/country-data/Bangladesh.png differ diff --git a/bot/games/assets/country-data/Barbados.png b/bot/games/assets/country-data/Barbados.png new file mode 100644 index 0000000..4c54c9f Binary files /dev/null and b/bot/games/assets/country-data/Barbados.png differ diff --git a/bot/games/assets/country-data/Belarus.png b/bot/games/assets/country-data/Belarus.png new file mode 100644 index 0000000..b0108f7 Binary files /dev/null and b/bot/games/assets/country-data/Belarus.png differ diff --git a/bot/games/assets/country-data/Belgium.png b/bot/games/assets/country-data/Belgium.png new file mode 100644 index 0000000..2e99375 Binary files /dev/null and b/bot/games/assets/country-data/Belgium.png differ diff --git a/bot/games/assets/country-data/Belize.png b/bot/games/assets/country-data/Belize.png new file mode 100644 index 0000000..8975b94 Binary files /dev/null and b/bot/games/assets/country-data/Belize.png differ diff --git a/bot/games/assets/country-data/Benin.png b/bot/games/assets/country-data/Benin.png new file mode 100644 index 0000000..504eafc Binary files /dev/null and b/bot/games/assets/country-data/Benin.png differ diff --git a/bot/games/assets/country-data/Bermuda.png b/bot/games/assets/country-data/Bermuda.png new file mode 100644 index 0000000..0321567 Binary files /dev/null and b/bot/games/assets/country-data/Bermuda.png differ diff --git a/bot/games/assets/country-data/Bhutan.png b/bot/games/assets/country-data/Bhutan.png new file mode 100644 index 0000000..1191f81 Binary files /dev/null and b/bot/games/assets/country-data/Bhutan.png differ diff --git a/bot/games/assets/country-data/Bolivia.png b/bot/games/assets/country-data/Bolivia.png new file mode 100644 index 0000000..26877e1 Binary files /dev/null and b/bot/games/assets/country-data/Bolivia.png differ diff --git a/bot/games/assets/country-data/Bosnia.png b/bot/games/assets/country-data/Bosnia.png new file mode 100644 index 0000000..010feca Binary files /dev/null and b/bot/games/assets/country-data/Bosnia.png differ diff --git a/bot/games/assets/country-data/Botswana.png b/bot/games/assets/country-data/Botswana.png new file mode 100644 index 0000000..07f7363 Binary files /dev/null and b/bot/games/assets/country-data/Botswana.png differ diff --git a/bot/games/assets/country-data/Bouvet Island.png b/bot/games/assets/country-data/Bouvet Island.png new file mode 100644 index 0000000..6a25efe Binary files /dev/null and b/bot/games/assets/country-data/Bouvet Island.png differ diff --git a/bot/games/assets/country-data/Brazil.png b/bot/games/assets/country-data/Brazil.png new file mode 100644 index 0000000..2d162a8 Binary files /dev/null and b/bot/games/assets/country-data/Brazil.png differ diff --git a/bot/games/assets/country-data/British Indian Ocean Territory.png b/bot/games/assets/country-data/British Indian Ocean Territory.png new file mode 100644 index 0000000..14aa418 Binary files /dev/null and b/bot/games/assets/country-data/British Indian Ocean Territory.png differ diff --git a/bot/games/assets/country-data/Brunei.png b/bot/games/assets/country-data/Brunei.png new file mode 100644 index 0000000..4936e62 Binary files /dev/null and b/bot/games/assets/country-data/Brunei.png differ diff --git a/bot/games/assets/country-data/Bulgaria.png b/bot/games/assets/country-data/Bulgaria.png new file mode 100644 index 0000000..1165d0a Binary files /dev/null and b/bot/games/assets/country-data/Bulgaria.png differ diff --git a/bot/games/assets/country-data/Burkina Faso.png b/bot/games/assets/country-data/Burkina Faso.png new file mode 100644 index 0000000..6a3cae0 Binary files /dev/null and b/bot/games/assets/country-data/Burkina Faso.png differ diff --git a/bot/games/assets/country-data/Burundi.png b/bot/games/assets/country-data/Burundi.png new file mode 100644 index 0000000..6e53fa9 Binary files /dev/null and b/bot/games/assets/country-data/Burundi.png differ diff --git a/bot/games/assets/country-data/Cambodia.png b/bot/games/assets/country-data/Cambodia.png new file mode 100644 index 0000000..d6dfc0d Binary files /dev/null and b/bot/games/assets/country-data/Cambodia.png differ diff --git a/bot/games/assets/country-data/Cameroon.png b/bot/games/assets/country-data/Cameroon.png new file mode 100644 index 0000000..643f726 Binary files /dev/null and b/bot/games/assets/country-data/Cameroon.png differ diff --git a/bot/games/assets/country-data/Canada.png b/bot/games/assets/country-data/Canada.png new file mode 100644 index 0000000..5762b6b Binary files /dev/null and b/bot/games/assets/country-data/Canada.png differ diff --git a/bot/games/assets/country-data/Cape Verde.png b/bot/games/assets/country-data/Cape Verde.png new file mode 100644 index 0000000..752ac91 Binary files /dev/null and b/bot/games/assets/country-data/Cape Verde.png differ diff --git a/bot/games/assets/country-data/Cayman Islands.png b/bot/games/assets/country-data/Cayman Islands.png new file mode 100644 index 0000000..7d3d083 Binary files /dev/null and b/bot/games/assets/country-data/Cayman Islands.png differ diff --git a/bot/games/assets/country-data/Central African Republic.png b/bot/games/assets/country-data/Central African Republic.png new file mode 100644 index 0000000..b13cd60 Binary files /dev/null and b/bot/games/assets/country-data/Central African Republic.png differ diff --git a/bot/games/assets/country-data/Chad.png b/bot/games/assets/country-data/Chad.png new file mode 100644 index 0000000..eb950d6 Binary files /dev/null and b/bot/games/assets/country-data/Chad.png differ diff --git a/bot/games/assets/country-data/Chile.png b/bot/games/assets/country-data/Chile.png new file mode 100644 index 0000000..238e2aa Binary files /dev/null and b/bot/games/assets/country-data/Chile.png differ diff --git a/bot/games/assets/country-data/China.png b/bot/games/assets/country-data/China.png new file mode 100644 index 0000000..fe826e6 Binary files /dev/null and b/bot/games/assets/country-data/China.png differ diff --git a/bot/games/assets/country-data/Christmas Island.png b/bot/games/assets/country-data/Christmas Island.png new file mode 100644 index 0000000..66c0293 Binary files /dev/null and b/bot/games/assets/country-data/Christmas Island.png differ diff --git a/bot/games/assets/country-data/Cocos Islands.png b/bot/games/assets/country-data/Cocos Islands.png new file mode 100644 index 0000000..bd923fb Binary files /dev/null and b/bot/games/assets/country-data/Cocos Islands.png differ diff --git a/bot/games/assets/country-data/Colombia.png b/bot/games/assets/country-data/Colombia.png new file mode 100644 index 0000000..2d76641 Binary files /dev/null and b/bot/games/assets/country-data/Colombia.png differ diff --git a/bot/games/assets/country-data/Comoros.png b/bot/games/assets/country-data/Comoros.png new file mode 100644 index 0000000..b856b93 Binary files /dev/null and b/bot/games/assets/country-data/Comoros.png differ diff --git a/bot/games/assets/country-data/Congo.png b/bot/games/assets/country-data/Congo.png new file mode 100644 index 0000000..c450bef Binary files /dev/null and b/bot/games/assets/country-data/Congo.png differ diff --git a/bot/games/assets/country-data/Cook Islands.png b/bot/games/assets/country-data/Cook Islands.png new file mode 100644 index 0000000..e59d02a Binary files /dev/null and b/bot/games/assets/country-data/Cook Islands.png differ diff --git a/bot/games/assets/country-data/Costa Rica.png b/bot/games/assets/country-data/Costa Rica.png new file mode 100644 index 0000000..fb541f0 Binary files /dev/null and b/bot/games/assets/country-data/Costa Rica.png differ diff --git a/bot/games/assets/country-data/Cote D'Ivoire.png b/bot/games/assets/country-data/Cote D'Ivoire.png new file mode 100644 index 0000000..eca6f69 Binary files /dev/null and b/bot/games/assets/country-data/Cote D'Ivoire.png differ diff --git a/bot/games/assets/country-data/Croatia.png b/bot/games/assets/country-data/Croatia.png new file mode 100644 index 0000000..dc33750 Binary files /dev/null and b/bot/games/assets/country-data/Croatia.png differ diff --git a/bot/games/assets/country-data/Cuba.png b/bot/games/assets/country-data/Cuba.png new file mode 100644 index 0000000..79ea0ab Binary files /dev/null and b/bot/games/assets/country-data/Cuba.png differ diff --git a/bot/games/assets/country-data/Cyprus.png b/bot/games/assets/country-data/Cyprus.png new file mode 100644 index 0000000..75b3a01 Binary files /dev/null and b/bot/games/assets/country-data/Cyprus.png differ diff --git a/bot/games/assets/country-data/Czech Republic.png b/bot/games/assets/country-data/Czech Republic.png new file mode 100644 index 0000000..4ed6be7 Binary files /dev/null and b/bot/games/assets/country-data/Czech Republic.png differ diff --git a/bot/games/assets/country-data/Democratic Republic of Congo.png b/bot/games/assets/country-data/Democratic Republic of Congo.png new file mode 100644 index 0000000..7226b2c Binary files /dev/null and b/bot/games/assets/country-data/Democratic Republic of Congo.png differ diff --git a/bot/games/assets/country-data/Denmark.png b/bot/games/assets/country-data/Denmark.png new file mode 100644 index 0000000..0a1ef4e Binary files /dev/null and b/bot/games/assets/country-data/Denmark.png differ diff --git a/bot/games/assets/country-data/Djibouti.png b/bot/games/assets/country-data/Djibouti.png new file mode 100644 index 0000000..38a529b Binary files /dev/null and b/bot/games/assets/country-data/Djibouti.png differ diff --git a/bot/games/assets/country-data/Dominica.png b/bot/games/assets/country-data/Dominica.png new file mode 100644 index 0000000..52b8915 Binary files /dev/null and b/bot/games/assets/country-data/Dominica.png differ diff --git a/bot/games/assets/country-data/Dominican Republic.png b/bot/games/assets/country-data/Dominican Republic.png new file mode 100644 index 0000000..460cede Binary files /dev/null and b/bot/games/assets/country-data/Dominican Republic.png differ diff --git a/bot/games/assets/country-data/Ecuador.png b/bot/games/assets/country-data/Ecuador.png new file mode 100644 index 0000000..f288967 Binary files /dev/null and b/bot/games/assets/country-data/Ecuador.png differ diff --git a/bot/games/assets/country-data/Egypt.png b/bot/games/assets/country-data/Egypt.png new file mode 100644 index 0000000..e73a447 Binary files /dev/null and b/bot/games/assets/country-data/Egypt.png differ diff --git a/bot/games/assets/country-data/El Salvador.png b/bot/games/assets/country-data/El Salvador.png new file mode 100644 index 0000000..b89890b Binary files /dev/null and b/bot/games/assets/country-data/El Salvador.png differ diff --git a/bot/games/assets/country-data/Equatorial Guinea.png b/bot/games/assets/country-data/Equatorial Guinea.png new file mode 100644 index 0000000..d3fafc7 Binary files /dev/null and b/bot/games/assets/country-data/Equatorial Guinea.png differ diff --git a/bot/games/assets/country-data/Eritrea.png b/bot/games/assets/country-data/Eritrea.png new file mode 100644 index 0000000..619164f Binary files /dev/null and b/bot/games/assets/country-data/Eritrea.png differ diff --git a/bot/games/assets/country-data/Estonia.png b/bot/games/assets/country-data/Estonia.png new file mode 100644 index 0000000..ed27fe9 Binary files /dev/null and b/bot/games/assets/country-data/Estonia.png differ diff --git a/bot/games/assets/country-data/Ethiopia.png b/bot/games/assets/country-data/Ethiopia.png new file mode 100644 index 0000000..3ab6e44 Binary files /dev/null and b/bot/games/assets/country-data/Ethiopia.png differ diff --git a/bot/games/assets/country-data/Falkland Islands.png b/bot/games/assets/country-data/Falkland Islands.png new file mode 100644 index 0000000..5856b6e Binary files /dev/null and b/bot/games/assets/country-data/Falkland Islands.png differ diff --git a/bot/games/assets/country-data/Faroe Islands.png b/bot/games/assets/country-data/Faroe Islands.png new file mode 100644 index 0000000..440227d Binary files /dev/null and b/bot/games/assets/country-data/Faroe Islands.png differ diff --git a/bot/games/assets/country-data/Fiji.png b/bot/games/assets/country-data/Fiji.png new file mode 100644 index 0000000..b68fa01 Binary files /dev/null and b/bot/games/assets/country-data/Fiji.png differ diff --git a/bot/games/assets/country-data/Finland.png b/bot/games/assets/country-data/Finland.png new file mode 100644 index 0000000..d128622 Binary files /dev/null and b/bot/games/assets/country-data/Finland.png differ diff --git a/bot/games/assets/country-data/France.png b/bot/games/assets/country-data/France.png new file mode 100644 index 0000000..3348ab9 Binary files /dev/null and b/bot/games/assets/country-data/France.png differ diff --git a/bot/games/assets/country-data/French Guiana.png b/bot/games/assets/country-data/French Guiana.png new file mode 100644 index 0000000..9638705 Binary files /dev/null and b/bot/games/assets/country-data/French Guiana.png differ diff --git a/bot/games/assets/country-data/French Polynesia.png b/bot/games/assets/country-data/French Polynesia.png new file mode 100644 index 0000000..c653bf5 Binary files /dev/null and b/bot/games/assets/country-data/French Polynesia.png differ diff --git a/bot/games/assets/country-data/French Southern Territories.png b/bot/games/assets/country-data/French Southern Territories.png new file mode 100644 index 0000000..5d2f876 Binary files /dev/null and b/bot/games/assets/country-data/French Southern Territories.png differ diff --git a/bot/games/assets/country-data/Gabon.png b/bot/games/assets/country-data/Gabon.png new file mode 100644 index 0000000..a538858 Binary files /dev/null and b/bot/games/assets/country-data/Gabon.png differ diff --git a/bot/games/assets/country-data/Gambia.png b/bot/games/assets/country-data/Gambia.png new file mode 100644 index 0000000..f4851f4 Binary files /dev/null and b/bot/games/assets/country-data/Gambia.png differ diff --git a/bot/games/assets/country-data/Georgia.png b/bot/games/assets/country-data/Georgia.png new file mode 100644 index 0000000..909ad00 Binary files /dev/null and b/bot/games/assets/country-data/Georgia.png differ diff --git a/bot/games/assets/country-data/Germany.png b/bot/games/assets/country-data/Germany.png new file mode 100644 index 0000000..674636c Binary files /dev/null and b/bot/games/assets/country-data/Germany.png differ diff --git a/bot/games/assets/country-data/Ghana.png b/bot/games/assets/country-data/Ghana.png new file mode 100644 index 0000000..4bbca5a Binary files /dev/null and b/bot/games/assets/country-data/Ghana.png differ diff --git a/bot/games/assets/country-data/Gibraltar.png b/bot/games/assets/country-data/Gibraltar.png new file mode 100644 index 0000000..2e66d8f Binary files /dev/null and b/bot/games/assets/country-data/Gibraltar.png differ diff --git a/bot/games/assets/country-data/Greece.png b/bot/games/assets/country-data/Greece.png new file mode 100644 index 0000000..6af0c96 Binary files /dev/null and b/bot/games/assets/country-data/Greece.png differ diff --git a/bot/games/assets/country-data/Greenland.png b/bot/games/assets/country-data/Greenland.png new file mode 100644 index 0000000..e269a11 Binary files /dev/null and b/bot/games/assets/country-data/Greenland.png differ diff --git a/bot/games/assets/country-data/Grenada.png b/bot/games/assets/country-data/Grenada.png new file mode 100644 index 0000000..5f9c545 Binary files /dev/null and b/bot/games/assets/country-data/Grenada.png differ diff --git a/bot/games/assets/country-data/Guadeloupe.png b/bot/games/assets/country-data/Guadeloupe.png new file mode 100644 index 0000000..5e0dd91 Binary files /dev/null and b/bot/games/assets/country-data/Guadeloupe.png differ diff --git a/bot/games/assets/country-data/Guam.png b/bot/games/assets/country-data/Guam.png new file mode 100644 index 0000000..2bf00cb Binary files /dev/null and b/bot/games/assets/country-data/Guam.png differ diff --git a/bot/games/assets/country-data/Guatemala.png b/bot/games/assets/country-data/Guatemala.png new file mode 100644 index 0000000..98a2a56 Binary files /dev/null and b/bot/games/assets/country-data/Guatemala.png differ diff --git a/bot/games/assets/country-data/Guinea-Bissau.png b/bot/games/assets/country-data/Guinea-Bissau.png new file mode 100644 index 0000000..b20d05d Binary files /dev/null and b/bot/games/assets/country-data/Guinea-Bissau.png differ diff --git a/bot/games/assets/country-data/Guinea.png b/bot/games/assets/country-data/Guinea.png new file mode 100644 index 0000000..9c37d84 Binary files /dev/null and b/bot/games/assets/country-data/Guinea.png differ diff --git a/bot/games/assets/country-data/Guyana.png b/bot/games/assets/country-data/Guyana.png new file mode 100644 index 0000000..e369dd1 Binary files /dev/null and b/bot/games/assets/country-data/Guyana.png differ diff --git a/bot/games/assets/country-data/Haiti.png b/bot/games/assets/country-data/Haiti.png new file mode 100644 index 0000000..00c98e3 Binary files /dev/null and b/bot/games/assets/country-data/Haiti.png differ diff --git a/bot/games/assets/country-data/Heard Island and Mcdonald Islands.png b/bot/games/assets/country-data/Heard Island and Mcdonald Islands.png new file mode 100644 index 0000000..9c54681 Binary files /dev/null and b/bot/games/assets/country-data/Heard Island and Mcdonald Islands.png differ diff --git a/bot/games/assets/country-data/Honduras.png b/bot/games/assets/country-data/Honduras.png new file mode 100644 index 0000000..11edb1a Binary files /dev/null and b/bot/games/assets/country-data/Honduras.png differ diff --git a/bot/games/assets/country-data/Hong Kong.png b/bot/games/assets/country-data/Hong Kong.png new file mode 100644 index 0000000..1ff2bf2 Binary files /dev/null and b/bot/games/assets/country-data/Hong Kong.png differ diff --git a/bot/games/assets/country-data/Hungary.png b/bot/games/assets/country-data/Hungary.png new file mode 100644 index 0000000..5afada2 Binary files /dev/null and b/bot/games/assets/country-data/Hungary.png differ diff --git a/bot/games/assets/country-data/Iceland.png b/bot/games/assets/country-data/Iceland.png new file mode 100644 index 0000000..8c82259 Binary files /dev/null and b/bot/games/assets/country-data/Iceland.png differ diff --git a/bot/games/assets/country-data/India.png b/bot/games/assets/country-data/India.png new file mode 100644 index 0000000..4f3528e Binary files /dev/null and b/bot/games/assets/country-data/India.png differ diff --git a/bot/games/assets/country-data/Indonesia.png b/bot/games/assets/country-data/Indonesia.png new file mode 100644 index 0000000..20b3cbd Binary files /dev/null and b/bot/games/assets/country-data/Indonesia.png differ diff --git a/bot/games/assets/country-data/Iran.png b/bot/games/assets/country-data/Iran.png new file mode 100644 index 0000000..ac31719 Binary files /dev/null and b/bot/games/assets/country-data/Iran.png differ diff --git a/bot/games/assets/country-data/Iraq.png b/bot/games/assets/country-data/Iraq.png new file mode 100644 index 0000000..158aab1 Binary files /dev/null and b/bot/games/assets/country-data/Iraq.png differ diff --git a/bot/games/assets/country-data/Ireland.png b/bot/games/assets/country-data/Ireland.png new file mode 100644 index 0000000..9eb7abd Binary files /dev/null and b/bot/games/assets/country-data/Ireland.png differ diff --git a/bot/games/assets/country-data/Israel.png b/bot/games/assets/country-data/Israel.png new file mode 100644 index 0000000..5c64a4f Binary files /dev/null and b/bot/games/assets/country-data/Israel.png differ diff --git a/bot/games/assets/country-data/Italy.png b/bot/games/assets/country-data/Italy.png new file mode 100644 index 0000000..ffeaaaa Binary files /dev/null and b/bot/games/assets/country-data/Italy.png differ diff --git a/bot/games/assets/country-data/Jamaica.png b/bot/games/assets/country-data/Jamaica.png new file mode 100644 index 0000000..0664ccc Binary files /dev/null and b/bot/games/assets/country-data/Jamaica.png differ diff --git a/bot/games/assets/country-data/Japan.png b/bot/games/assets/country-data/Japan.png new file mode 100644 index 0000000..99ad04e Binary files /dev/null and b/bot/games/assets/country-data/Japan.png differ diff --git a/bot/games/assets/country-data/Jordan.png b/bot/games/assets/country-data/Jordan.png new file mode 100644 index 0000000..42f29b2 Binary files /dev/null and b/bot/games/assets/country-data/Jordan.png differ diff --git a/bot/games/assets/country-data/Kazakhstan.png b/bot/games/assets/country-data/Kazakhstan.png new file mode 100644 index 0000000..b842c35 Binary files /dev/null and b/bot/games/assets/country-data/Kazakhstan.png differ diff --git a/bot/games/assets/country-data/Kenya.png b/bot/games/assets/country-data/Kenya.png new file mode 100644 index 0000000..af59e9c Binary files /dev/null and b/bot/games/assets/country-data/Kenya.png differ diff --git a/bot/games/assets/country-data/Kiribati.png b/bot/games/assets/country-data/Kiribati.png new file mode 100644 index 0000000..4f34ba8 Binary files /dev/null and b/bot/games/assets/country-data/Kiribati.png differ diff --git a/bot/games/assets/country-data/Kuwait.png b/bot/games/assets/country-data/Kuwait.png new file mode 100644 index 0000000..da1a307 Binary files /dev/null and b/bot/games/assets/country-data/Kuwait.png differ diff --git a/bot/games/assets/country-data/Kyrgyzstan.png b/bot/games/assets/country-data/Kyrgyzstan.png new file mode 100644 index 0000000..120e3ba Binary files /dev/null and b/bot/games/assets/country-data/Kyrgyzstan.png differ diff --git a/bot/games/assets/country-data/Laos.png b/bot/games/assets/country-data/Laos.png new file mode 100644 index 0000000..45ab8e9 Binary files /dev/null and b/bot/games/assets/country-data/Laos.png differ diff --git a/bot/games/assets/country-data/Latvia.png b/bot/games/assets/country-data/Latvia.png new file mode 100644 index 0000000..2df0d82 Binary files /dev/null and b/bot/games/assets/country-data/Latvia.png differ diff --git a/bot/games/assets/country-data/Lebanon.png b/bot/games/assets/country-data/Lebanon.png new file mode 100644 index 0000000..9c1b608 Binary files /dev/null and b/bot/games/assets/country-data/Lebanon.png differ diff --git a/bot/games/assets/country-data/Lesotho.png b/bot/games/assets/country-data/Lesotho.png new file mode 100644 index 0000000..88fe59b Binary files /dev/null and b/bot/games/assets/country-data/Lesotho.png differ diff --git a/bot/games/assets/country-data/Liberia.png b/bot/games/assets/country-data/Liberia.png new file mode 100644 index 0000000..f33a050 Binary files /dev/null and b/bot/games/assets/country-data/Liberia.png differ diff --git a/bot/games/assets/country-data/Libya.png b/bot/games/assets/country-data/Libya.png new file mode 100644 index 0000000..e7b0f30 Binary files /dev/null and b/bot/games/assets/country-data/Libya.png differ diff --git a/bot/games/assets/country-data/Liechtenstein.png b/bot/games/assets/country-data/Liechtenstein.png new file mode 100644 index 0000000..3a859e6 Binary files /dev/null and b/bot/games/assets/country-data/Liechtenstein.png differ diff --git a/bot/games/assets/country-data/Lithuania.png b/bot/games/assets/country-data/Lithuania.png new file mode 100644 index 0000000..1827eda Binary files /dev/null and b/bot/games/assets/country-data/Lithuania.png differ diff --git a/bot/games/assets/country-data/Luxembourg.png b/bot/games/assets/country-data/Luxembourg.png new file mode 100644 index 0000000..6265168 Binary files /dev/null and b/bot/games/assets/country-data/Luxembourg.png differ diff --git a/bot/games/assets/country-data/Macao.png b/bot/games/assets/country-data/Macao.png new file mode 100644 index 0000000..73732c2 Binary files /dev/null and b/bot/games/assets/country-data/Macao.png differ diff --git a/bot/games/assets/country-data/Macedonia.png b/bot/games/assets/country-data/Macedonia.png new file mode 100644 index 0000000..09670e2 Binary files /dev/null and b/bot/games/assets/country-data/Macedonia.png differ diff --git a/bot/games/assets/country-data/Madagascar.png b/bot/games/assets/country-data/Madagascar.png new file mode 100644 index 0000000..45cb2eb Binary files /dev/null and b/bot/games/assets/country-data/Madagascar.png differ diff --git a/bot/games/assets/country-data/Malawi.png b/bot/games/assets/country-data/Malawi.png new file mode 100644 index 0000000..c39f3ea Binary files /dev/null and b/bot/games/assets/country-data/Malawi.png differ diff --git a/bot/games/assets/country-data/Malaysia.png b/bot/games/assets/country-data/Malaysia.png new file mode 100644 index 0000000..dab570b Binary files /dev/null and b/bot/games/assets/country-data/Malaysia.png differ diff --git a/bot/games/assets/country-data/Maldives.png b/bot/games/assets/country-data/Maldives.png new file mode 100644 index 0000000..37b9d29 Binary files /dev/null and b/bot/games/assets/country-data/Maldives.png differ diff --git a/bot/games/assets/country-data/Mali.png b/bot/games/assets/country-data/Mali.png new file mode 100644 index 0000000..e50c49b Binary files /dev/null and b/bot/games/assets/country-data/Mali.png differ diff --git a/bot/games/assets/country-data/Malta.png b/bot/games/assets/country-data/Malta.png new file mode 100644 index 0000000..f6e7e90 Binary files /dev/null and b/bot/games/assets/country-data/Malta.png differ diff --git a/bot/games/assets/country-data/Martinique.png b/bot/games/assets/country-data/Martinique.png new file mode 100644 index 0000000..e63c2e8 Binary files /dev/null and b/bot/games/assets/country-data/Martinique.png differ diff --git a/bot/games/assets/country-data/Mauritania.png b/bot/games/assets/country-data/Mauritania.png new file mode 100644 index 0000000..7d30a75 Binary files /dev/null and b/bot/games/assets/country-data/Mauritania.png differ diff --git a/bot/games/assets/country-data/Mauritius.png b/bot/games/assets/country-data/Mauritius.png new file mode 100644 index 0000000..e0407d7 Binary files /dev/null and b/bot/games/assets/country-data/Mauritius.png differ diff --git a/bot/games/assets/country-data/Mayotte.png b/bot/games/assets/country-data/Mayotte.png new file mode 100644 index 0000000..0bf2ad3 Binary files /dev/null and b/bot/games/assets/country-data/Mayotte.png differ diff --git a/bot/games/assets/country-data/Mexico.png b/bot/games/assets/country-data/Mexico.png new file mode 100644 index 0000000..05ddbb9 Binary files /dev/null and b/bot/games/assets/country-data/Mexico.png differ diff --git a/bot/games/assets/country-data/Moldova.png b/bot/games/assets/country-data/Moldova.png new file mode 100644 index 0000000..9efe92d Binary files /dev/null and b/bot/games/assets/country-data/Moldova.png differ diff --git a/bot/games/assets/country-data/Monaco.png b/bot/games/assets/country-data/Monaco.png new file mode 100644 index 0000000..5e19b81 Binary files /dev/null and b/bot/games/assets/country-data/Monaco.png differ diff --git a/bot/games/assets/country-data/Mongolia.png b/bot/games/assets/country-data/Mongolia.png new file mode 100644 index 0000000..558ec61 Binary files /dev/null and b/bot/games/assets/country-data/Mongolia.png differ diff --git a/bot/games/assets/country-data/Montserrat.png b/bot/games/assets/country-data/Montserrat.png new file mode 100644 index 0000000..494960e Binary files /dev/null and b/bot/games/assets/country-data/Montserrat.png differ diff --git a/bot/games/assets/country-data/Morocco.png b/bot/games/assets/country-data/Morocco.png new file mode 100644 index 0000000..0d5ef5b Binary files /dev/null and b/bot/games/assets/country-data/Morocco.png differ diff --git a/bot/games/assets/country-data/Mozambique.png b/bot/games/assets/country-data/Mozambique.png new file mode 100644 index 0000000..932a4bd Binary files /dev/null and b/bot/games/assets/country-data/Mozambique.png differ diff --git a/bot/games/assets/country-data/Myanmar.png b/bot/games/assets/country-data/Myanmar.png new file mode 100644 index 0000000..be34633 Binary files /dev/null and b/bot/games/assets/country-data/Myanmar.png differ diff --git a/bot/games/assets/country-data/Namibia.png b/bot/games/assets/country-data/Namibia.png new file mode 100644 index 0000000..ea4b7e1 Binary files /dev/null and b/bot/games/assets/country-data/Namibia.png differ diff --git a/bot/games/assets/country-data/Nauru.png b/bot/games/assets/country-data/Nauru.png new file mode 100644 index 0000000..78cc107 Binary files /dev/null and b/bot/games/assets/country-data/Nauru.png differ diff --git a/bot/games/assets/country-data/Nepal.png b/bot/games/assets/country-data/Nepal.png new file mode 100644 index 0000000..9a76c55 Binary files /dev/null and b/bot/games/assets/country-data/Nepal.png differ diff --git a/bot/games/assets/country-data/Netherlands.png b/bot/games/assets/country-data/Netherlands.png new file mode 100644 index 0000000..1e695f0 Binary files /dev/null and b/bot/games/assets/country-data/Netherlands.png differ diff --git a/bot/games/assets/country-data/New Caledonia.png b/bot/games/assets/country-data/New Caledonia.png new file mode 100644 index 0000000..6cfe3cf Binary files /dev/null and b/bot/games/assets/country-data/New Caledonia.png differ diff --git a/bot/games/assets/country-data/New Zealand.png b/bot/games/assets/country-data/New Zealand.png new file mode 100644 index 0000000..a9ef4fd Binary files /dev/null and b/bot/games/assets/country-data/New Zealand.png differ diff --git a/bot/games/assets/country-data/Nicaragua.png b/bot/games/assets/country-data/Nicaragua.png new file mode 100644 index 0000000..8a3256f Binary files /dev/null and b/bot/games/assets/country-data/Nicaragua.png differ diff --git a/bot/games/assets/country-data/Niger.png b/bot/games/assets/country-data/Niger.png new file mode 100644 index 0000000..5e642ad Binary files /dev/null and b/bot/games/assets/country-data/Niger.png differ diff --git a/bot/games/assets/country-data/Nigeria.png b/bot/games/assets/country-data/Nigeria.png new file mode 100644 index 0000000..98de09d Binary files /dev/null and b/bot/games/assets/country-data/Nigeria.png differ diff --git a/bot/games/assets/country-data/Niue.png b/bot/games/assets/country-data/Niue.png new file mode 100644 index 0000000..bcccb47 Binary files /dev/null and b/bot/games/assets/country-data/Niue.png differ diff --git a/bot/games/assets/country-data/Norfolk Island.png b/bot/games/assets/country-data/Norfolk Island.png new file mode 100644 index 0000000..0cc01f2 Binary files /dev/null and b/bot/games/assets/country-data/Norfolk Island.png differ diff --git a/bot/games/assets/country-data/North Korea.png b/bot/games/assets/country-data/North Korea.png new file mode 100644 index 0000000..2118e0d Binary files /dev/null and b/bot/games/assets/country-data/North Korea.png differ diff --git a/bot/games/assets/country-data/Norway.png b/bot/games/assets/country-data/Norway.png new file mode 100644 index 0000000..8cfdf94 Binary files /dev/null and b/bot/games/assets/country-data/Norway.png differ diff --git a/bot/games/assets/country-data/Oman.png b/bot/games/assets/country-data/Oman.png new file mode 100644 index 0000000..c50f741 Binary files /dev/null and b/bot/games/assets/country-data/Oman.png differ diff --git a/bot/games/assets/country-data/Pakistan.png b/bot/games/assets/country-data/Pakistan.png new file mode 100644 index 0000000..984145f Binary files /dev/null and b/bot/games/assets/country-data/Pakistan.png differ diff --git a/bot/games/assets/country-data/Palau.png b/bot/games/assets/country-data/Palau.png new file mode 100644 index 0000000..9b56f7d Binary files /dev/null and b/bot/games/assets/country-data/Palau.png differ diff --git a/bot/games/assets/country-data/Panama.png b/bot/games/assets/country-data/Panama.png new file mode 100644 index 0000000..92a1c09 Binary files /dev/null and b/bot/games/assets/country-data/Panama.png differ diff --git a/bot/games/assets/country-data/Papua New Guinea.png b/bot/games/assets/country-data/Papua New Guinea.png new file mode 100644 index 0000000..f35c56a Binary files /dev/null and b/bot/games/assets/country-data/Papua New Guinea.png differ diff --git a/bot/games/assets/country-data/Paraguay.png b/bot/games/assets/country-data/Paraguay.png new file mode 100644 index 0000000..d450c41 Binary files /dev/null and b/bot/games/assets/country-data/Paraguay.png differ diff --git a/bot/games/assets/country-data/Peru.png b/bot/games/assets/country-data/Peru.png new file mode 100644 index 0000000..2b8795d Binary files /dev/null and b/bot/games/assets/country-data/Peru.png differ diff --git a/bot/games/assets/country-data/Philippines.png b/bot/games/assets/country-data/Philippines.png new file mode 100644 index 0000000..58a9b83 Binary files /dev/null and b/bot/games/assets/country-data/Philippines.png differ diff --git a/bot/games/assets/country-data/Pitcairn.png b/bot/games/assets/country-data/Pitcairn.png new file mode 100644 index 0000000..b766ebc Binary files /dev/null and b/bot/games/assets/country-data/Pitcairn.png differ diff --git a/bot/games/assets/country-data/Poland.png b/bot/games/assets/country-data/Poland.png new file mode 100644 index 0000000..7a502b8 Binary files /dev/null and b/bot/games/assets/country-data/Poland.png differ diff --git a/bot/games/assets/country-data/Portugal.png b/bot/games/assets/country-data/Portugal.png new file mode 100644 index 0000000..bc2f637 Binary files /dev/null and b/bot/games/assets/country-data/Portugal.png differ diff --git a/bot/games/assets/country-data/Puerto Rico.png b/bot/games/assets/country-data/Puerto Rico.png new file mode 100644 index 0000000..2e95180 Binary files /dev/null and b/bot/games/assets/country-data/Puerto Rico.png differ diff --git a/bot/games/assets/country-data/Qatar.png b/bot/games/assets/country-data/Qatar.png new file mode 100644 index 0000000..df89da0 Binary files /dev/null and b/bot/games/assets/country-data/Qatar.png differ diff --git a/bot/games/assets/country-data/Reunion.png b/bot/games/assets/country-data/Reunion.png new file mode 100644 index 0000000..3d41b87 Binary files /dev/null and b/bot/games/assets/country-data/Reunion.png differ diff --git a/bot/games/assets/country-data/Romania.png b/bot/games/assets/country-data/Romania.png new file mode 100644 index 0000000..de10fbc Binary files /dev/null and b/bot/games/assets/country-data/Romania.png differ diff --git a/bot/games/assets/country-data/Russia.png b/bot/games/assets/country-data/Russia.png new file mode 100644 index 0000000..743e281 Binary files /dev/null and b/bot/games/assets/country-data/Russia.png differ diff --git a/bot/games/assets/country-data/Rwanda.png b/bot/games/assets/country-data/Rwanda.png new file mode 100644 index 0000000..7a6418b Binary files /dev/null and b/bot/games/assets/country-data/Rwanda.png differ diff --git a/bot/games/assets/country-data/Samoa.png b/bot/games/assets/country-data/Samoa.png new file mode 100644 index 0000000..f169c1c Binary files /dev/null and b/bot/games/assets/country-data/Samoa.png differ diff --git a/bot/games/assets/country-data/San Marino.png b/bot/games/assets/country-data/San Marino.png new file mode 100644 index 0000000..7191db3 Binary files /dev/null and b/bot/games/assets/country-data/San Marino.png differ diff --git a/bot/games/assets/country-data/Sao Tome and Principe.png b/bot/games/assets/country-data/Sao Tome and Principe.png new file mode 100644 index 0000000..fcb6d66 Binary files /dev/null and b/bot/games/assets/country-data/Sao Tome and Principe.png differ diff --git a/bot/games/assets/country-data/Saudi Arabia.png b/bot/games/assets/country-data/Saudi Arabia.png new file mode 100644 index 0000000..7a6418b Binary files /dev/null and b/bot/games/assets/country-data/Saudi Arabia.png differ diff --git a/bot/games/assets/country-data/Senegal.png b/bot/games/assets/country-data/Senegal.png new file mode 100644 index 0000000..78a4692 Binary files /dev/null and b/bot/games/assets/country-data/Senegal.png differ diff --git a/bot/games/assets/country-data/Seychelles.png b/bot/games/assets/country-data/Seychelles.png new file mode 100644 index 0000000..5132df6 Binary files /dev/null and b/bot/games/assets/country-data/Seychelles.png differ diff --git a/bot/games/assets/country-data/Sierra Leone.png b/bot/games/assets/country-data/Sierra Leone.png new file mode 100644 index 0000000..ed18f7f Binary files /dev/null and b/bot/games/assets/country-data/Sierra Leone.png differ diff --git a/bot/games/assets/country-data/Singapore.png b/bot/games/assets/country-data/Singapore.png new file mode 100644 index 0000000..2529a6f Binary files /dev/null and b/bot/games/assets/country-data/Singapore.png differ diff --git a/bot/games/assets/country-data/Slovakia.png b/bot/games/assets/country-data/Slovakia.png new file mode 100644 index 0000000..6be3601 Binary files /dev/null and b/bot/games/assets/country-data/Slovakia.png differ diff --git a/bot/games/assets/country-data/Slovenia.png b/bot/games/assets/country-data/Slovenia.png new file mode 100644 index 0000000..d48aaba Binary files /dev/null and b/bot/games/assets/country-data/Slovenia.png differ diff --git a/bot/games/assets/country-data/Solomon Islands.png b/bot/games/assets/country-data/Solomon Islands.png new file mode 100644 index 0000000..275e288 Binary files /dev/null and b/bot/games/assets/country-data/Solomon Islands.png differ diff --git a/bot/games/assets/country-data/Somalia.png b/bot/games/assets/country-data/Somalia.png new file mode 100644 index 0000000..b39eed0 Binary files /dev/null and b/bot/games/assets/country-data/Somalia.png differ diff --git a/bot/games/assets/country-data/South Africa.png b/bot/games/assets/country-data/South Africa.png new file mode 100644 index 0000000..72107ff Binary files /dev/null and b/bot/games/assets/country-data/South Africa.png differ diff --git a/bot/games/assets/country-data/South Georgia.png b/bot/games/assets/country-data/South Georgia.png new file mode 100644 index 0000000..ab47e01 Binary files /dev/null and b/bot/games/assets/country-data/South Georgia.png differ diff --git a/bot/games/assets/country-data/South Korea.png b/bot/games/assets/country-data/South Korea.png new file mode 100644 index 0000000..97a4352 Binary files /dev/null and b/bot/games/assets/country-data/South Korea.png differ diff --git a/bot/games/assets/country-data/Spain.png b/bot/games/assets/country-data/Spain.png new file mode 100644 index 0000000..f2832a6 Binary files /dev/null and b/bot/games/assets/country-data/Spain.png differ diff --git a/bot/games/assets/country-data/Sri Lanka.png b/bot/games/assets/country-data/Sri Lanka.png new file mode 100644 index 0000000..87b9e0d Binary files /dev/null and b/bot/games/assets/country-data/Sri Lanka.png differ diff --git a/bot/games/assets/country-data/St Helena.png b/bot/games/assets/country-data/St Helena.png new file mode 100644 index 0000000..1908419 Binary files /dev/null and b/bot/games/assets/country-data/St Helena.png differ diff --git a/bot/games/assets/country-data/St Kitts and Nevis.png b/bot/games/assets/country-data/St Kitts and Nevis.png new file mode 100644 index 0000000..ecd9aea Binary files /dev/null and b/bot/games/assets/country-data/St Kitts and Nevis.png differ diff --git a/bot/games/assets/country-data/St Lucia.png b/bot/games/assets/country-data/St Lucia.png new file mode 100644 index 0000000..08dc3ed Binary files /dev/null and b/bot/games/assets/country-data/St Lucia.png differ diff --git a/bot/games/assets/country-data/St Pierre and Miquelon.png b/bot/games/assets/country-data/St Pierre and Miquelon.png new file mode 100644 index 0000000..9850094 Binary files /dev/null and b/bot/games/assets/country-data/St Pierre and Miquelon.png differ diff --git a/bot/games/assets/country-data/St Vincent and the Grenadines.png b/bot/games/assets/country-data/St Vincent and the Grenadines.png new file mode 100644 index 0000000..ad56fef Binary files /dev/null and b/bot/games/assets/country-data/St Vincent and the Grenadines.png differ diff --git a/bot/games/assets/country-data/Sudan.png b/bot/games/assets/country-data/Sudan.png new file mode 100644 index 0000000..527d6b1 Binary files /dev/null and b/bot/games/assets/country-data/Sudan.png differ diff --git a/bot/games/assets/country-data/Suriname.png b/bot/games/assets/country-data/Suriname.png new file mode 100644 index 0000000..9869f3d Binary files /dev/null and b/bot/games/assets/country-data/Suriname.png differ diff --git a/bot/games/assets/country-data/Svalbard and Jan Mayen.png b/bot/games/assets/country-data/Svalbard and Jan Mayen.png new file mode 100644 index 0000000..b1745b7 Binary files /dev/null and b/bot/games/assets/country-data/Svalbard and Jan Mayen.png differ diff --git a/bot/games/assets/country-data/Swaziland.png b/bot/games/assets/country-data/Swaziland.png new file mode 100644 index 0000000..8097bbb Binary files /dev/null and b/bot/games/assets/country-data/Swaziland.png differ diff --git a/bot/games/assets/country-data/Sweden.png b/bot/games/assets/country-data/Sweden.png new file mode 100644 index 0000000..b885870 Binary files /dev/null and b/bot/games/assets/country-data/Sweden.png differ diff --git a/bot/games/assets/country-data/Switzerland.png b/bot/games/assets/country-data/Switzerland.png new file mode 100644 index 0000000..ea10873 Binary files /dev/null and b/bot/games/assets/country-data/Switzerland.png differ diff --git a/bot/games/assets/country-data/Syria.png b/bot/games/assets/country-data/Syria.png new file mode 100644 index 0000000..5d7b676 Binary files /dev/null and b/bot/games/assets/country-data/Syria.png differ diff --git a/bot/games/assets/country-data/Taiwan.png b/bot/games/assets/country-data/Taiwan.png new file mode 100644 index 0000000..d5d4ce1 Binary files /dev/null and b/bot/games/assets/country-data/Taiwan.png differ diff --git a/bot/games/assets/country-data/Tajikistan.png b/bot/games/assets/country-data/Tajikistan.png new file mode 100644 index 0000000..3f72289 Binary files /dev/null and b/bot/games/assets/country-data/Tajikistan.png differ diff --git a/bot/games/assets/country-data/Thailand.png b/bot/games/assets/country-data/Thailand.png new file mode 100644 index 0000000..961aa4b Binary files /dev/null and b/bot/games/assets/country-data/Thailand.png differ diff --git a/bot/games/assets/country-data/Timor-Leste.png b/bot/games/assets/country-data/Timor-Leste.png new file mode 100644 index 0000000..2e36029 Binary files /dev/null and b/bot/games/assets/country-data/Timor-Leste.png differ diff --git a/bot/games/assets/country-data/Togo.png b/bot/games/assets/country-data/Togo.png new file mode 100644 index 0000000..bf43ba9 Binary files /dev/null and b/bot/games/assets/country-data/Togo.png differ diff --git a/bot/games/assets/country-data/Tokelau.png b/bot/games/assets/country-data/Tokelau.png new file mode 100644 index 0000000..3817af7 Binary files /dev/null and b/bot/games/assets/country-data/Tokelau.png differ diff --git a/bot/games/assets/country-data/Tonga.png b/bot/games/assets/country-data/Tonga.png new file mode 100644 index 0000000..1042493 Binary files /dev/null and b/bot/games/assets/country-data/Tonga.png differ diff --git a/bot/games/assets/country-data/Trinidad and Tobago.png b/bot/games/assets/country-data/Trinidad and Tobago.png new file mode 100644 index 0000000..0171edc Binary files /dev/null and b/bot/games/assets/country-data/Trinidad and Tobago.png differ diff --git a/bot/games/assets/country-data/Tunisia.png b/bot/games/assets/country-data/Tunisia.png new file mode 100644 index 0000000..303d322 Binary files /dev/null and b/bot/games/assets/country-data/Tunisia.png differ diff --git a/bot/games/assets/country-data/Turkey.png b/bot/games/assets/country-data/Turkey.png new file mode 100644 index 0000000..c225fcb Binary files /dev/null and b/bot/games/assets/country-data/Turkey.png differ diff --git a/bot/games/assets/country-data/Turkmenistan.png b/bot/games/assets/country-data/Turkmenistan.png new file mode 100644 index 0000000..91c45dd Binary files /dev/null and b/bot/games/assets/country-data/Turkmenistan.png differ diff --git a/bot/games/assets/country-data/Turks and Caicos Islands.png b/bot/games/assets/country-data/Turks and Caicos Islands.png new file mode 100644 index 0000000..c9fac00 Binary files /dev/null and b/bot/games/assets/country-data/Turks and Caicos Islands.png differ diff --git a/bot/games/assets/country-data/Uganda.png b/bot/games/assets/country-data/Uganda.png new file mode 100644 index 0000000..d896d2c Binary files /dev/null and b/bot/games/assets/country-data/Uganda.png differ diff --git a/bot/games/assets/country-data/Ukraine.png b/bot/games/assets/country-data/Ukraine.png new file mode 100644 index 0000000..c2cb517 Binary files /dev/null and b/bot/games/assets/country-data/Ukraine.png differ diff --git a/bot/games/assets/country-data/United Arab Emirates.png b/bot/games/assets/country-data/United Arab Emirates.png new file mode 100644 index 0000000..4265fb9 Binary files /dev/null and b/bot/games/assets/country-data/United Arab Emirates.png differ diff --git a/bot/games/assets/country-data/United Kingdom.png b/bot/games/assets/country-data/United Kingdom.png new file mode 100644 index 0000000..64d0aa3 Binary files /dev/null and b/bot/games/assets/country-data/United Kingdom.png differ diff --git a/bot/games/assets/country-data/United States.png b/bot/games/assets/country-data/United States.png new file mode 100644 index 0000000..cd8e9c3 Binary files /dev/null and b/bot/games/assets/country-data/United States.png differ diff --git a/bot/games/assets/country-data/Uruguay.png b/bot/games/assets/country-data/Uruguay.png new file mode 100644 index 0000000..eb73f88 Binary files /dev/null and b/bot/games/assets/country-data/Uruguay.png differ diff --git a/bot/games/assets/country-data/Uzbekistan.png b/bot/games/assets/country-data/Uzbekistan.png new file mode 100644 index 0000000..6ec21a9 Binary files /dev/null and b/bot/games/assets/country-data/Uzbekistan.png differ diff --git a/bot/games/assets/country-data/Vanuatu.png b/bot/games/assets/country-data/Vanuatu.png new file mode 100644 index 0000000..1959608 Binary files /dev/null and b/bot/games/assets/country-data/Vanuatu.png differ diff --git a/bot/games/assets/country-data/Vatican City.png b/bot/games/assets/country-data/Vatican City.png new file mode 100644 index 0000000..00abebb Binary files /dev/null and b/bot/games/assets/country-data/Vatican City.png differ diff --git a/bot/games/assets/country-data/Venezuela.png b/bot/games/assets/country-data/Venezuela.png new file mode 100644 index 0000000..9214dd7 Binary files /dev/null and b/bot/games/assets/country-data/Venezuela.png differ diff --git a/bot/games/assets/country-data/Vietnam.png b/bot/games/assets/country-data/Vietnam.png new file mode 100644 index 0000000..a55cf55 Binary files /dev/null and b/bot/games/assets/country-data/Vietnam.png differ diff --git a/bot/games/assets/country-data/Virgin Islands, British.png b/bot/games/assets/country-data/Virgin Islands, British.png new file mode 100644 index 0000000..455d63f Binary files /dev/null and b/bot/games/assets/country-data/Virgin Islands, British.png differ diff --git a/bot/games/assets/country-data/Virgin Islands, U.S..png b/bot/games/assets/country-data/Virgin Islands, U.S..png new file mode 100644 index 0000000..466953e Binary files /dev/null and b/bot/games/assets/country-data/Virgin Islands, U.S..png differ diff --git a/bot/games/assets/country-data/Wallis and Futuna.png b/bot/games/assets/country-data/Wallis and Futuna.png new file mode 100644 index 0000000..6c4502e Binary files /dev/null and b/bot/games/assets/country-data/Wallis and Futuna.png differ diff --git a/bot/games/assets/country-data/Western Sahara.png b/bot/games/assets/country-data/Western Sahara.png new file mode 100644 index 0000000..87bbb33 Binary files /dev/null and b/bot/games/assets/country-data/Western Sahara.png differ diff --git a/bot/games/assets/country-data/Yemen.png b/bot/games/assets/country-data/Yemen.png new file mode 100644 index 0000000..190324c Binary files /dev/null and b/bot/games/assets/country-data/Yemen.png differ diff --git a/bot/games/assets/country-data/Zambia.png b/bot/games/assets/country-data/Zambia.png new file mode 100644 index 0000000..5247f3a Binary files /dev/null and b/bot/games/assets/country-data/Zambia.png differ diff --git a/bot/games/assets/country-data/Zimbabwe.png b/bot/games/assets/country-data/Zimbabwe.png new file mode 100644 index 0000000..6f8d98b Binary files /dev/null and b/bot/games/assets/country-data/Zimbabwe.png differ diff --git a/bot/games/assets/country-data/tanzania.png b/bot/games/assets/country-data/tanzania.png new file mode 100644 index 0000000..6ae5caf Binary files /dev/null and b/bot/games/assets/country-data/tanzania.png differ diff --git a/bot/games/assets/country-flags/Afghanistan.png b/bot/games/assets/country-flags/Afghanistan.png new file mode 100644 index 0000000..104f6db Binary files /dev/null and b/bot/games/assets/country-flags/Afghanistan.png differ diff --git a/bot/games/assets/country-flags/Albania.png b/bot/games/assets/country-flags/Albania.png new file mode 100644 index 0000000..5075b86 Binary files /dev/null and b/bot/games/assets/country-flags/Albania.png differ diff --git a/bot/games/assets/country-flags/Algeria.png b/bot/games/assets/country-flags/Algeria.png new file mode 100644 index 0000000..611fd0c Binary files /dev/null and b/bot/games/assets/country-flags/Algeria.png differ diff --git a/bot/games/assets/country-flags/American Samoa.png b/bot/games/assets/country-flags/American Samoa.png new file mode 100644 index 0000000..f0f48b1 Binary files /dev/null and b/bot/games/assets/country-flags/American Samoa.png differ diff --git a/bot/games/assets/country-flags/Andorra.png b/bot/games/assets/country-flags/Andorra.png new file mode 100644 index 0000000..8e18d1b Binary files /dev/null and b/bot/games/assets/country-flags/Andorra.png differ diff --git a/bot/games/assets/country-flags/Angola.png b/bot/games/assets/country-flags/Angola.png new file mode 100644 index 0000000..5310074 Binary files /dev/null and b/bot/games/assets/country-flags/Angola.png differ diff --git a/bot/games/assets/country-flags/Anguilla.png b/bot/games/assets/country-flags/Anguilla.png new file mode 100644 index 0000000..3b4cf83 Binary files /dev/null and b/bot/games/assets/country-flags/Anguilla.png differ diff --git a/bot/games/assets/country-flags/Antarctica.png b/bot/games/assets/country-flags/Antarctica.png new file mode 100644 index 0000000..23d7a81 Binary files /dev/null and b/bot/games/assets/country-flags/Antarctica.png differ diff --git a/bot/games/assets/country-flags/Antigua and Barbuda.png b/bot/games/assets/country-flags/Antigua and Barbuda.png new file mode 100644 index 0000000..b4d6554 Binary files /dev/null and b/bot/games/assets/country-flags/Antigua and Barbuda.png differ diff --git a/bot/games/assets/country-flags/Argentina.png b/bot/games/assets/country-flags/Argentina.png new file mode 100644 index 0000000..e387653 Binary files /dev/null and b/bot/games/assets/country-flags/Argentina.png differ diff --git a/bot/games/assets/country-flags/Armenia.png b/bot/games/assets/country-flags/Armenia.png new file mode 100644 index 0000000..d0eb95c Binary files /dev/null and b/bot/games/assets/country-flags/Armenia.png differ diff --git a/bot/games/assets/country-flags/Aruba.png b/bot/games/assets/country-flags/Aruba.png new file mode 100644 index 0000000..99e95c2 Binary files /dev/null and b/bot/games/assets/country-flags/Aruba.png differ diff --git a/bot/games/assets/country-flags/Australia.png b/bot/games/assets/country-flags/Australia.png new file mode 100644 index 0000000..a725d4e Binary files /dev/null and b/bot/games/assets/country-flags/Australia.png differ diff --git a/bot/games/assets/country-flags/Austria.png b/bot/games/assets/country-flags/Austria.png new file mode 100644 index 0000000..1bb9cd3 Binary files /dev/null and b/bot/games/assets/country-flags/Austria.png differ diff --git a/bot/games/assets/country-flags/Azerbaijan.png b/bot/games/assets/country-flags/Azerbaijan.png new file mode 100644 index 0000000..20a54b6 Binary files /dev/null and b/bot/games/assets/country-flags/Azerbaijan.png differ diff --git a/bot/games/assets/country-flags/Bahamas.png b/bot/games/assets/country-flags/Bahamas.png new file mode 100644 index 0000000..2bdd4c3 Binary files /dev/null and b/bot/games/assets/country-flags/Bahamas.png differ diff --git a/bot/games/assets/country-flags/Bahrain.png b/bot/games/assets/country-flags/Bahrain.png new file mode 100644 index 0000000..5830fb8 Binary files /dev/null and b/bot/games/assets/country-flags/Bahrain.png differ diff --git a/bot/games/assets/country-flags/Bangladesh.png b/bot/games/assets/country-flags/Bangladesh.png new file mode 100644 index 0000000..57dfbab Binary files /dev/null and b/bot/games/assets/country-flags/Bangladesh.png differ diff --git a/bot/games/assets/country-flags/Barbados.png b/bot/games/assets/country-flags/Barbados.png new file mode 100644 index 0000000..dad1c1b Binary files /dev/null and b/bot/games/assets/country-flags/Barbados.png differ diff --git a/bot/games/assets/country-flags/Belarus.png b/bot/games/assets/country-flags/Belarus.png new file mode 100644 index 0000000..4754a0d Binary files /dev/null and b/bot/games/assets/country-flags/Belarus.png differ diff --git a/bot/games/assets/country-flags/Belgium.png b/bot/games/assets/country-flags/Belgium.png new file mode 100644 index 0000000..f9b63d2 Binary files /dev/null and b/bot/games/assets/country-flags/Belgium.png differ diff --git a/bot/games/assets/country-flags/Belize.png b/bot/games/assets/country-flags/Belize.png new file mode 100644 index 0000000..b870f66 Binary files /dev/null and b/bot/games/assets/country-flags/Belize.png differ diff --git a/bot/games/assets/country-flags/Benin.png b/bot/games/assets/country-flags/Benin.png new file mode 100644 index 0000000..731dcfd Binary files /dev/null and b/bot/games/assets/country-flags/Benin.png differ diff --git a/bot/games/assets/country-flags/Bermuda.png b/bot/games/assets/country-flags/Bermuda.png new file mode 100644 index 0000000..5caaa11 Binary files /dev/null and b/bot/games/assets/country-flags/Bermuda.png differ diff --git a/bot/games/assets/country-flags/Bhutan.png b/bot/games/assets/country-flags/Bhutan.png new file mode 100644 index 0000000..00d7cab Binary files /dev/null and b/bot/games/assets/country-flags/Bhutan.png differ diff --git a/bot/games/assets/country-flags/Bolivia.png b/bot/games/assets/country-flags/Bolivia.png new file mode 100644 index 0000000..0e0c82f Binary files /dev/null and b/bot/games/assets/country-flags/Bolivia.png differ diff --git a/bot/games/assets/country-flags/Bosnia.png b/bot/games/assets/country-flags/Bosnia.png new file mode 100644 index 0000000..012be01 Binary files /dev/null and b/bot/games/assets/country-flags/Bosnia.png differ diff --git a/bot/games/assets/country-flags/Botswana.png b/bot/games/assets/country-flags/Botswana.png new file mode 100644 index 0000000..2591e90 Binary files /dev/null and b/bot/games/assets/country-flags/Botswana.png differ diff --git a/bot/games/assets/country-flags/Bouvet Island.png b/bot/games/assets/country-flags/Bouvet Island.png new file mode 100644 index 0000000..4293233 Binary files /dev/null and b/bot/games/assets/country-flags/Bouvet Island.png differ diff --git a/bot/games/assets/country-flags/Brazil.png b/bot/games/assets/country-flags/Brazil.png new file mode 100644 index 0000000..62dee9a Binary files /dev/null and b/bot/games/assets/country-flags/Brazil.png differ diff --git a/bot/games/assets/country-flags/British Indian Ocean Territory.png b/bot/games/assets/country-flags/British Indian Ocean Territory.png new file mode 100644 index 0000000..fa856be Binary files /dev/null and b/bot/games/assets/country-flags/British Indian Ocean Territory.png differ diff --git a/bot/games/assets/country-flags/Brunei.png b/bot/games/assets/country-flags/Brunei.png new file mode 100644 index 0000000..34cde93 Binary files /dev/null and b/bot/games/assets/country-flags/Brunei.png differ diff --git a/bot/games/assets/country-flags/Bulgaria.png b/bot/games/assets/country-flags/Bulgaria.png new file mode 100644 index 0000000..b728a33 Binary files /dev/null and b/bot/games/assets/country-flags/Bulgaria.png differ diff --git a/bot/games/assets/country-flags/Burkina Faso.png b/bot/games/assets/country-flags/Burkina Faso.png new file mode 100644 index 0000000..42459e2 Binary files /dev/null and b/bot/games/assets/country-flags/Burkina Faso.png differ diff --git a/bot/games/assets/country-flags/Burundi.png b/bot/games/assets/country-flags/Burundi.png new file mode 100644 index 0000000..bf25719 Binary files /dev/null and b/bot/games/assets/country-flags/Burundi.png differ diff --git a/bot/games/assets/country-flags/Cambodia.png b/bot/games/assets/country-flags/Cambodia.png new file mode 100644 index 0000000..f2594a1 Binary files /dev/null and b/bot/games/assets/country-flags/Cambodia.png differ diff --git a/bot/games/assets/country-flags/Cameroon.png b/bot/games/assets/country-flags/Cameroon.png new file mode 100644 index 0000000..9ec4374 Binary files /dev/null and b/bot/games/assets/country-flags/Cameroon.png differ diff --git a/bot/games/assets/country-flags/Canada.png b/bot/games/assets/country-flags/Canada.png new file mode 100644 index 0000000..2f3f30c Binary files /dev/null and b/bot/games/assets/country-flags/Canada.png differ diff --git a/bot/games/assets/country-flags/Cape Verde.png b/bot/games/assets/country-flags/Cape Verde.png new file mode 100644 index 0000000..f15ebd5 Binary files /dev/null and b/bot/games/assets/country-flags/Cape Verde.png differ diff --git a/bot/games/assets/country-flags/Cayman Islands.png b/bot/games/assets/country-flags/Cayman Islands.png new file mode 100644 index 0000000..93c26af Binary files /dev/null and b/bot/games/assets/country-flags/Cayman Islands.png differ diff --git a/bot/games/assets/country-flags/Central African Republic.png b/bot/games/assets/country-flags/Central African Republic.png new file mode 100644 index 0000000..16b51df Binary files /dev/null and b/bot/games/assets/country-flags/Central African Republic.png differ diff --git a/bot/games/assets/country-flags/Chad.png b/bot/games/assets/country-flags/Chad.png new file mode 100644 index 0000000..ddf8b83 Binary files /dev/null and b/bot/games/assets/country-flags/Chad.png differ diff --git a/bot/games/assets/country-flags/Chile.png b/bot/games/assets/country-flags/Chile.png new file mode 100644 index 0000000..ce380b3 Binary files /dev/null and b/bot/games/assets/country-flags/Chile.png differ diff --git a/bot/games/assets/country-flags/China.png b/bot/games/assets/country-flags/China.png new file mode 100644 index 0000000..51ebde8 Binary files /dev/null and b/bot/games/assets/country-flags/China.png differ diff --git a/bot/games/assets/country-flags/Christmas Island.png b/bot/games/assets/country-flags/Christmas Island.png new file mode 100644 index 0000000..83e75cd Binary files /dev/null and b/bot/games/assets/country-flags/Christmas Island.png differ diff --git a/bot/games/assets/country-flags/Cocos.png b/bot/games/assets/country-flags/Cocos.png new file mode 100644 index 0000000..555b1a0 Binary files /dev/null and b/bot/games/assets/country-flags/Cocos.png differ diff --git a/bot/games/assets/country-flags/Colombia.png b/bot/games/assets/country-flags/Colombia.png new file mode 100644 index 0000000..39e9b64 Binary files /dev/null and b/bot/games/assets/country-flags/Colombia.png differ diff --git a/bot/games/assets/country-flags/Comoros.png b/bot/games/assets/country-flags/Comoros.png new file mode 100644 index 0000000..931b904 Binary files /dev/null and b/bot/games/assets/country-flags/Comoros.png differ diff --git a/bot/games/assets/country-flags/Congo.png b/bot/games/assets/country-flags/Congo.png new file mode 100644 index 0000000..e89f31f Binary files /dev/null and b/bot/games/assets/country-flags/Congo.png differ diff --git a/bot/games/assets/country-flags/Cook Islands.png b/bot/games/assets/country-flags/Cook Islands.png new file mode 100644 index 0000000..8b4480f Binary files /dev/null and b/bot/games/assets/country-flags/Cook Islands.png differ diff --git a/bot/games/assets/country-flags/Costa Rica.png b/bot/games/assets/country-flags/Costa Rica.png new file mode 100644 index 0000000..320aaf5 Binary files /dev/null and b/bot/games/assets/country-flags/Costa Rica.png differ diff --git a/bot/games/assets/country-flags/Cote d'Ivoire.png b/bot/games/assets/country-flags/Cote d'Ivoire.png new file mode 100644 index 0000000..125498a Binary files /dev/null and b/bot/games/assets/country-flags/Cote d'Ivoire.png differ diff --git a/bot/games/assets/country-flags/Croatia.png b/bot/games/assets/country-flags/Croatia.png new file mode 100644 index 0000000..fdfe901 Binary files /dev/null and b/bot/games/assets/country-flags/Croatia.png differ diff --git a/bot/games/assets/country-flags/Cuba.png b/bot/games/assets/country-flags/Cuba.png new file mode 100644 index 0000000..bfe8c2b Binary files /dev/null and b/bot/games/assets/country-flags/Cuba.png differ diff --git a/bot/games/assets/country-flags/Curacao.png b/bot/games/assets/country-flags/Curacao.png new file mode 100644 index 0000000..1371149 Binary files /dev/null and b/bot/games/assets/country-flags/Curacao.png differ diff --git a/bot/games/assets/country-flags/Cyprus.png b/bot/games/assets/country-flags/Cyprus.png new file mode 100644 index 0000000..5d5d1c7 Binary files /dev/null and b/bot/games/assets/country-flags/Cyprus.png differ diff --git a/bot/games/assets/country-flags/Czech Republic.png b/bot/games/assets/country-flags/Czech Republic.png new file mode 100644 index 0000000..f451023 Binary files /dev/null and b/bot/games/assets/country-flags/Czech Republic.png differ diff --git a/bot/games/assets/country-flags/Democratic Republic of Congo.png b/bot/games/assets/country-flags/Democratic Republic of Congo.png new file mode 100644 index 0000000..d643eb0 Binary files /dev/null and b/bot/games/assets/country-flags/Democratic Republic of Congo.png differ diff --git a/bot/games/assets/country-flags/Denmark.png b/bot/games/assets/country-flags/Denmark.png new file mode 100644 index 0000000..1f865ac Binary files /dev/null and b/bot/games/assets/country-flags/Denmark.png differ diff --git a/bot/games/assets/country-flags/Djibouti.png b/bot/games/assets/country-flags/Djibouti.png new file mode 100644 index 0000000..3e56e52 Binary files /dev/null and b/bot/games/assets/country-flags/Djibouti.png differ diff --git a/bot/games/assets/country-flags/Dominica.png b/bot/games/assets/country-flags/Dominica.png new file mode 100644 index 0000000..8109554 Binary files /dev/null and b/bot/games/assets/country-flags/Dominica.png differ diff --git a/bot/games/assets/country-flags/Dominican Republic.png b/bot/games/assets/country-flags/Dominican Republic.png new file mode 100644 index 0000000..3ae21fc Binary files /dev/null and b/bot/games/assets/country-flags/Dominican Republic.png differ diff --git a/bot/games/assets/country-flags/Ecuador.png b/bot/games/assets/country-flags/Ecuador.png new file mode 100644 index 0000000..7d0a0f7 Binary files /dev/null and b/bot/games/assets/country-flags/Ecuador.png differ diff --git a/bot/games/assets/country-flags/Egypt.png b/bot/games/assets/country-flags/Egypt.png new file mode 100644 index 0000000..6851930 Binary files /dev/null and b/bot/games/assets/country-flags/Egypt.png differ diff --git a/bot/games/assets/country-flags/El Salvador.png b/bot/games/assets/country-flags/El Salvador.png new file mode 100644 index 0000000..e6786e2 Binary files /dev/null and b/bot/games/assets/country-flags/El Salvador.png differ diff --git a/bot/games/assets/country-flags/England.png b/bot/games/assets/country-flags/England.png new file mode 100644 index 0000000..c8ebb01 Binary files /dev/null and b/bot/games/assets/country-flags/England.png differ diff --git a/bot/games/assets/country-flags/Equatorial Guinea.png b/bot/games/assets/country-flags/Equatorial Guinea.png new file mode 100644 index 0000000..a72b251 Binary files /dev/null and b/bot/games/assets/country-flags/Equatorial Guinea.png differ diff --git a/bot/games/assets/country-flags/Eritrea.png b/bot/games/assets/country-flags/Eritrea.png new file mode 100644 index 0000000..6ad1eaf Binary files /dev/null and b/bot/games/assets/country-flags/Eritrea.png differ diff --git a/bot/games/assets/country-flags/Estonia.png b/bot/games/assets/country-flags/Estonia.png new file mode 100644 index 0000000..9220a15 Binary files /dev/null and b/bot/games/assets/country-flags/Estonia.png differ diff --git a/bot/games/assets/country-flags/Ethiopia.png b/bot/games/assets/country-flags/Ethiopia.png new file mode 100644 index 0000000..7404e67 Binary files /dev/null and b/bot/games/assets/country-flags/Ethiopia.png differ diff --git a/bot/games/assets/country-flags/Europe.png b/bot/games/assets/country-flags/Europe.png new file mode 100644 index 0000000..5d195d2 Binary files /dev/null and b/bot/games/assets/country-flags/Europe.png differ diff --git a/bot/games/assets/country-flags/Falkland Islands.png b/bot/games/assets/country-flags/Falkland Islands.png new file mode 100644 index 0000000..fb68549 Binary files /dev/null and b/bot/games/assets/country-flags/Falkland Islands.png differ diff --git a/bot/games/assets/country-flags/Faroe Islands.png b/bot/games/assets/country-flags/Faroe Islands.png new file mode 100644 index 0000000..e18f9f1 Binary files /dev/null and b/bot/games/assets/country-flags/Faroe Islands.png differ diff --git a/bot/games/assets/country-flags/Fiji.png b/bot/games/assets/country-flags/Fiji.png new file mode 100644 index 0000000..6aa78cf Binary files /dev/null and b/bot/games/assets/country-flags/Fiji.png differ diff --git a/bot/games/assets/country-flags/Finland.png b/bot/games/assets/country-flags/Finland.png new file mode 100644 index 0000000..04f0932 Binary files /dev/null and b/bot/games/assets/country-flags/Finland.png differ diff --git a/bot/games/assets/country-flags/France.png b/bot/games/assets/country-flags/France.png new file mode 100644 index 0000000..51d21a9 Binary files /dev/null and b/bot/games/assets/country-flags/France.png differ diff --git a/bot/games/assets/country-flags/French Guiana.png b/bot/games/assets/country-flags/French Guiana.png new file mode 100644 index 0000000..51d21a9 Binary files /dev/null and b/bot/games/assets/country-flags/French Guiana.png differ diff --git a/bot/games/assets/country-flags/French Polynesia.png b/bot/games/assets/country-flags/French Polynesia.png new file mode 100644 index 0000000..355537f Binary files /dev/null and b/bot/games/assets/country-flags/French Polynesia.png differ diff --git a/bot/games/assets/country-flags/French Southern Territories.png b/bot/games/assets/country-flags/French Southern Territories.png new file mode 100644 index 0000000..4530314 Binary files /dev/null and b/bot/games/assets/country-flags/French Southern Territories.png differ diff --git a/bot/games/assets/country-flags/Gabon.png b/bot/games/assets/country-flags/Gabon.png new file mode 100644 index 0000000..41bf567 Binary files /dev/null and b/bot/games/assets/country-flags/Gabon.png differ diff --git a/bot/games/assets/country-flags/Gambia.png b/bot/games/assets/country-flags/Gambia.png new file mode 100644 index 0000000..40f7fa7 Binary files /dev/null and b/bot/games/assets/country-flags/Gambia.png differ diff --git a/bot/games/assets/country-flags/Georgia.png b/bot/games/assets/country-flags/Georgia.png new file mode 100644 index 0000000..bf2988d Binary files /dev/null and b/bot/games/assets/country-flags/Georgia.png differ diff --git a/bot/games/assets/country-flags/Germany.png b/bot/games/assets/country-flags/Germany.png new file mode 100644 index 0000000..08d67be Binary files /dev/null and b/bot/games/assets/country-flags/Germany.png differ diff --git a/bot/games/assets/country-flags/Ghana.png b/bot/games/assets/country-flags/Ghana.png new file mode 100644 index 0000000..9c27e4d Binary files /dev/null and b/bot/games/assets/country-flags/Ghana.png differ diff --git a/bot/games/assets/country-flags/Gibraltar.png b/bot/games/assets/country-flags/Gibraltar.png new file mode 100644 index 0000000..548a800 Binary files /dev/null and b/bot/games/assets/country-flags/Gibraltar.png differ diff --git a/bot/games/assets/country-flags/Greece.png b/bot/games/assets/country-flags/Greece.png new file mode 100644 index 0000000..197041d Binary files /dev/null and b/bot/games/assets/country-flags/Greece.png differ diff --git a/bot/games/assets/country-flags/Greenland.png b/bot/games/assets/country-flags/Greenland.png new file mode 100644 index 0000000..f411948 Binary files /dev/null and b/bot/games/assets/country-flags/Greenland.png differ diff --git a/bot/games/assets/country-flags/Grenada.png b/bot/games/assets/country-flags/Grenada.png new file mode 100644 index 0000000..dc2fee9 Binary files /dev/null and b/bot/games/assets/country-flags/Grenada.png differ diff --git a/bot/games/assets/country-flags/Guadeloupe.png b/bot/games/assets/country-flags/Guadeloupe.png new file mode 100644 index 0000000..51d21a9 Binary files /dev/null and b/bot/games/assets/country-flags/Guadeloupe.png differ diff --git a/bot/games/assets/country-flags/Guam.png b/bot/games/assets/country-flags/Guam.png new file mode 100644 index 0000000..bbcf2fa Binary files /dev/null and b/bot/games/assets/country-flags/Guam.png differ diff --git a/bot/games/assets/country-flags/Guatemala.png b/bot/games/assets/country-flags/Guatemala.png new file mode 100644 index 0000000..edeb4cc Binary files /dev/null and b/bot/games/assets/country-flags/Guatemala.png differ diff --git a/bot/games/assets/country-flags/Guernsey.png b/bot/games/assets/country-flags/Guernsey.png new file mode 100644 index 0000000..d01e4ca Binary files /dev/null and b/bot/games/assets/country-flags/Guernsey.png differ diff --git a/bot/games/assets/country-flags/Guinea-Bissau.png b/bot/games/assets/country-flags/Guinea-Bissau.png new file mode 100644 index 0000000..2deae26 Binary files /dev/null and b/bot/games/assets/country-flags/Guinea-Bissau.png differ diff --git a/bot/games/assets/country-flags/Guinea.png b/bot/games/assets/country-flags/Guinea.png new file mode 100644 index 0000000..ff3d2f1 Binary files /dev/null and b/bot/games/assets/country-flags/Guinea.png differ diff --git a/bot/games/assets/country-flags/Guyana.png b/bot/games/assets/country-flags/Guyana.png new file mode 100644 index 0000000..3d65ec6 Binary files /dev/null and b/bot/games/assets/country-flags/Guyana.png differ diff --git a/bot/games/assets/country-flags/Haiti.png b/bot/games/assets/country-flags/Haiti.png new file mode 100644 index 0000000..7bbbd47 Binary files /dev/null and b/bot/games/assets/country-flags/Haiti.png differ diff --git a/bot/games/assets/country-flags/Heard Island and McDonald Islands.png b/bot/games/assets/country-flags/Heard Island and McDonald Islands.png new file mode 100644 index 0000000..a725d4e Binary files /dev/null and b/bot/games/assets/country-flags/Heard Island and McDonald Islands.png differ diff --git a/bot/games/assets/country-flags/Honduras.png b/bot/games/assets/country-flags/Honduras.png new file mode 100644 index 0000000..4010211 Binary files /dev/null and b/bot/games/assets/country-flags/Honduras.png differ diff --git a/bot/games/assets/country-flags/Hong Kong.png b/bot/games/assets/country-flags/Hong Kong.png new file mode 100644 index 0000000..d919099 Binary files /dev/null and b/bot/games/assets/country-flags/Hong Kong.png differ diff --git a/bot/games/assets/country-flags/Hungary.png b/bot/games/assets/country-flags/Hungary.png new file mode 100644 index 0000000..95c4eac Binary files /dev/null and b/bot/games/assets/country-flags/Hungary.png differ diff --git a/bot/games/assets/country-flags/Iceland.png b/bot/games/assets/country-flags/Iceland.png new file mode 100644 index 0000000..2a5f318 Binary files /dev/null and b/bot/games/assets/country-flags/Iceland.png differ diff --git a/bot/games/assets/country-flags/India.png b/bot/games/assets/country-flags/India.png new file mode 100644 index 0000000..f126ce3 Binary files /dev/null and b/bot/games/assets/country-flags/India.png differ diff --git a/bot/games/assets/country-flags/Indonesia.png b/bot/games/assets/country-flags/Indonesia.png new file mode 100644 index 0000000..8790a3a Binary files /dev/null and b/bot/games/assets/country-flags/Indonesia.png differ diff --git a/bot/games/assets/country-flags/Iran.png b/bot/games/assets/country-flags/Iran.png new file mode 100644 index 0000000..d0da97f Binary files /dev/null and b/bot/games/assets/country-flags/Iran.png differ diff --git a/bot/games/assets/country-flags/Iraq.png b/bot/games/assets/country-flags/Iraq.png new file mode 100644 index 0000000..810dd13 Binary files /dev/null and b/bot/games/assets/country-flags/Iraq.png differ diff --git a/bot/games/assets/country-flags/Ireland.png b/bot/games/assets/country-flags/Ireland.png new file mode 100644 index 0000000..dd6c411 Binary files /dev/null and b/bot/games/assets/country-flags/Ireland.png differ diff --git a/bot/games/assets/country-flags/Isle of Man.png b/bot/games/assets/country-flags/Isle of Man.png new file mode 100644 index 0000000..b209580 Binary files /dev/null and b/bot/games/assets/country-flags/Isle of Man.png differ diff --git a/bot/games/assets/country-flags/Israel.png b/bot/games/assets/country-flags/Israel.png new file mode 100644 index 0000000..3e3a78c Binary files /dev/null and b/bot/games/assets/country-flags/Israel.png differ diff --git a/bot/games/assets/country-flags/Italy.png b/bot/games/assets/country-flags/Italy.png new file mode 100644 index 0000000..c2dc752 Binary files /dev/null and b/bot/games/assets/country-flags/Italy.png differ diff --git a/bot/games/assets/country-flags/Jamaica.png b/bot/games/assets/country-flags/Jamaica.png new file mode 100644 index 0000000..a5b9418 Binary files /dev/null and b/bot/games/assets/country-flags/Jamaica.png differ diff --git a/bot/games/assets/country-flags/Japan.png b/bot/games/assets/country-flags/Japan.png new file mode 100644 index 0000000..56570a0 Binary files /dev/null and b/bot/games/assets/country-flags/Japan.png differ diff --git a/bot/games/assets/country-flags/Jersey.png b/bot/games/assets/country-flags/Jersey.png new file mode 100644 index 0000000..4890a8e Binary files /dev/null and b/bot/games/assets/country-flags/Jersey.png differ diff --git a/bot/games/assets/country-flags/Jordan.png b/bot/games/assets/country-flags/Jordan.png new file mode 100644 index 0000000..4ac2168 Binary files /dev/null and b/bot/games/assets/country-flags/Jordan.png differ diff --git a/bot/games/assets/country-flags/Kazakhstan.png b/bot/games/assets/country-flags/Kazakhstan.png new file mode 100644 index 0000000..5c916af Binary files /dev/null and b/bot/games/assets/country-flags/Kazakhstan.png differ diff --git a/bot/games/assets/country-flags/Kenya.png b/bot/games/assets/country-flags/Kenya.png new file mode 100644 index 0000000..c1c074e Binary files /dev/null and b/bot/games/assets/country-flags/Kenya.png differ diff --git a/bot/games/assets/country-flags/Kiribati.png b/bot/games/assets/country-flags/Kiribati.png new file mode 100644 index 0000000..c20a847 Binary files /dev/null and b/bot/games/assets/country-flags/Kiribati.png differ diff --git a/bot/games/assets/country-flags/Kosovo.png b/bot/games/assets/country-flags/Kosovo.png new file mode 100644 index 0000000..6b036c8 Binary files /dev/null and b/bot/games/assets/country-flags/Kosovo.png differ diff --git a/bot/games/assets/country-flags/Kuwait.png b/bot/games/assets/country-flags/Kuwait.png new file mode 100644 index 0000000..3dc78f7 Binary files /dev/null and b/bot/games/assets/country-flags/Kuwait.png differ diff --git a/bot/games/assets/country-flags/Kyrgyzstan.png b/bot/games/assets/country-flags/Kyrgyzstan.png new file mode 100644 index 0000000..124d938 Binary files /dev/null and b/bot/games/assets/country-flags/Kyrgyzstan.png differ diff --git a/bot/games/assets/country-flags/Laos.png b/bot/games/assets/country-flags/Laos.png new file mode 100644 index 0000000..61001f2 Binary files /dev/null and b/bot/games/assets/country-flags/Laos.png differ diff --git a/bot/games/assets/country-flags/Latvia.png b/bot/games/assets/country-flags/Latvia.png new file mode 100644 index 0000000..d5c9df7 Binary files /dev/null and b/bot/games/assets/country-flags/Latvia.png differ diff --git a/bot/games/assets/country-flags/Lebanon.png b/bot/games/assets/country-flags/Lebanon.png new file mode 100644 index 0000000..115ec8e Binary files /dev/null and b/bot/games/assets/country-flags/Lebanon.png differ diff --git a/bot/games/assets/country-flags/Lesotho.png b/bot/games/assets/country-flags/Lesotho.png new file mode 100644 index 0000000..a5a66c9 Binary files /dev/null and b/bot/games/assets/country-flags/Lesotho.png differ diff --git a/bot/games/assets/country-flags/Liberia.png b/bot/games/assets/country-flags/Liberia.png new file mode 100644 index 0000000..65727be Binary files /dev/null and b/bot/games/assets/country-flags/Liberia.png differ diff --git a/bot/games/assets/country-flags/Libya.png b/bot/games/assets/country-flags/Libya.png new file mode 100644 index 0000000..758a3ef Binary files /dev/null and b/bot/games/assets/country-flags/Libya.png differ diff --git a/bot/games/assets/country-flags/Liechtenstein.png b/bot/games/assets/country-flags/Liechtenstein.png new file mode 100644 index 0000000..16d791f Binary files /dev/null and b/bot/games/assets/country-flags/Liechtenstein.png differ diff --git a/bot/games/assets/country-flags/Lithuania.png b/bot/games/assets/country-flags/Lithuania.png new file mode 100644 index 0000000..6a6c3e8 Binary files /dev/null and b/bot/games/assets/country-flags/Lithuania.png differ diff --git a/bot/games/assets/country-flags/Luxembourg.png b/bot/games/assets/country-flags/Luxembourg.png new file mode 100644 index 0000000..b2908b8 Binary files /dev/null and b/bot/games/assets/country-flags/Luxembourg.png differ diff --git a/bot/games/assets/country-flags/Macao.png b/bot/games/assets/country-flags/Macao.png new file mode 100644 index 0000000..88cdedc Binary files /dev/null and b/bot/games/assets/country-flags/Macao.png differ diff --git a/bot/games/assets/country-flags/Madagascar.png b/bot/games/assets/country-flags/Madagascar.png new file mode 100644 index 0000000..6779091 Binary files /dev/null and b/bot/games/assets/country-flags/Madagascar.png differ diff --git a/bot/games/assets/country-flags/Malawi.png b/bot/games/assets/country-flags/Malawi.png new file mode 100644 index 0000000..c2a8acc Binary files /dev/null and b/bot/games/assets/country-flags/Malawi.png differ diff --git a/bot/games/assets/country-flags/Malaysia.png b/bot/games/assets/country-flags/Malaysia.png new file mode 100644 index 0000000..6304f84 Binary files /dev/null and b/bot/games/assets/country-flags/Malaysia.png differ diff --git a/bot/games/assets/country-flags/Maldives.png b/bot/games/assets/country-flags/Maldives.png new file mode 100644 index 0000000..e807c93 Binary files /dev/null and b/bot/games/assets/country-flags/Maldives.png differ diff --git a/bot/games/assets/country-flags/Mali.png b/bot/games/assets/country-flags/Mali.png new file mode 100644 index 0000000..2a24ef0 Binary files /dev/null and b/bot/games/assets/country-flags/Mali.png differ diff --git a/bot/games/assets/country-flags/Malta.png b/bot/games/assets/country-flags/Malta.png new file mode 100644 index 0000000..8559a89 Binary files /dev/null and b/bot/games/assets/country-flags/Malta.png differ diff --git a/bot/games/assets/country-flags/Marshall Islands.png b/bot/games/assets/country-flags/Marshall Islands.png new file mode 100644 index 0000000..c28be35 Binary files /dev/null and b/bot/games/assets/country-flags/Marshall Islands.png differ diff --git a/bot/games/assets/country-flags/Martinique.png b/bot/games/assets/country-flags/Martinique.png new file mode 100644 index 0000000..51d21a9 Binary files /dev/null and b/bot/games/assets/country-flags/Martinique.png differ diff --git a/bot/games/assets/country-flags/Mauritania.png b/bot/games/assets/country-flags/Mauritania.png new file mode 100644 index 0000000..f8ca3e3 Binary files /dev/null and b/bot/games/assets/country-flags/Mauritania.png differ diff --git a/bot/games/assets/country-flags/Mauritius.png b/bot/games/assets/country-flags/Mauritius.png new file mode 100644 index 0000000..f169250 Binary files /dev/null and b/bot/games/assets/country-flags/Mauritius.png differ diff --git a/bot/games/assets/country-flags/Mayotte.png b/bot/games/assets/country-flags/Mayotte.png new file mode 100644 index 0000000..51d21a9 Binary files /dev/null and b/bot/games/assets/country-flags/Mayotte.png differ diff --git a/bot/games/assets/country-flags/Mexico.png b/bot/games/assets/country-flags/Mexico.png new file mode 100644 index 0000000..beb7423 Binary files /dev/null and b/bot/games/assets/country-flags/Mexico.png differ diff --git a/bot/games/assets/country-flags/Micronesia.png b/bot/games/assets/country-flags/Micronesia.png new file mode 100644 index 0000000..3f51837 Binary files /dev/null and b/bot/games/assets/country-flags/Micronesia.png differ diff --git a/bot/games/assets/country-flags/Moldova.png b/bot/games/assets/country-flags/Moldova.png new file mode 100644 index 0000000..23a822c Binary files /dev/null and b/bot/games/assets/country-flags/Moldova.png differ diff --git a/bot/games/assets/country-flags/Monaco.png b/bot/games/assets/country-flags/Monaco.png new file mode 100644 index 0000000..d519f4e Binary files /dev/null and b/bot/games/assets/country-flags/Monaco.png differ diff --git a/bot/games/assets/country-flags/Mongolia.png b/bot/games/assets/country-flags/Mongolia.png new file mode 100644 index 0000000..f9c8b89 Binary files /dev/null and b/bot/games/assets/country-flags/Mongolia.png differ diff --git a/bot/games/assets/country-flags/Montenegro.png b/bot/games/assets/country-flags/Montenegro.png new file mode 100644 index 0000000..b81742d Binary files /dev/null and b/bot/games/assets/country-flags/Montenegro.png differ diff --git a/bot/games/assets/country-flags/Montserrat.png b/bot/games/assets/country-flags/Montserrat.png new file mode 100644 index 0000000..bc4ad64 Binary files /dev/null and b/bot/games/assets/country-flags/Montserrat.png differ diff --git a/bot/games/assets/country-flags/Morocco.png b/bot/games/assets/country-flags/Morocco.png new file mode 100644 index 0000000..a5d96b3 Binary files /dev/null and b/bot/games/assets/country-flags/Morocco.png differ diff --git a/bot/games/assets/country-flags/Mozambique.png b/bot/games/assets/country-flags/Mozambique.png new file mode 100644 index 0000000..429f77d Binary files /dev/null and b/bot/games/assets/country-flags/Mozambique.png differ diff --git a/bot/games/assets/country-flags/Myanmar.png b/bot/games/assets/country-flags/Myanmar.png new file mode 100644 index 0000000..6e418dc Binary files /dev/null and b/bot/games/assets/country-flags/Myanmar.png differ diff --git a/bot/games/assets/country-flags/Namibia.png b/bot/games/assets/country-flags/Namibia.png new file mode 100644 index 0000000..84c046b Binary files /dev/null and b/bot/games/assets/country-flags/Namibia.png differ diff --git a/bot/games/assets/country-flags/Nauru.png b/bot/games/assets/country-flags/Nauru.png new file mode 100644 index 0000000..f3d77cd Binary files /dev/null and b/bot/games/assets/country-flags/Nauru.png differ diff --git a/bot/games/assets/country-flags/Nepal.png b/bot/games/assets/country-flags/Nepal.png new file mode 100644 index 0000000..db0849e Binary files /dev/null and b/bot/games/assets/country-flags/Nepal.png differ diff --git a/bot/games/assets/country-flags/Netherlands.png b/bot/games/assets/country-flags/Netherlands.png new file mode 100644 index 0000000..d835771 Binary files /dev/null and b/bot/games/assets/country-flags/Netherlands.png differ diff --git a/bot/games/assets/country-flags/New Caledonia.png b/bot/games/assets/country-flags/New Caledonia.png new file mode 100644 index 0000000..51d21a9 Binary files /dev/null and b/bot/games/assets/country-flags/New Caledonia.png differ diff --git a/bot/games/assets/country-flags/New Zealand.png b/bot/games/assets/country-flags/New Zealand.png new file mode 100644 index 0000000..098f71f Binary files /dev/null and b/bot/games/assets/country-flags/New Zealand.png differ diff --git a/bot/games/assets/country-flags/Nicaragua.png b/bot/games/assets/country-flags/Nicaragua.png new file mode 100644 index 0000000..86144b0 Binary files /dev/null and b/bot/games/assets/country-flags/Nicaragua.png differ diff --git a/bot/games/assets/country-flags/Niger.png b/bot/games/assets/country-flags/Niger.png new file mode 100644 index 0000000..0127e03 Binary files /dev/null and b/bot/games/assets/country-flags/Niger.png differ diff --git a/bot/games/assets/country-flags/Nigeria.png b/bot/games/assets/country-flags/Nigeria.png new file mode 100644 index 0000000..b29dd10 Binary files /dev/null and b/bot/games/assets/country-flags/Nigeria.png differ diff --git a/bot/games/assets/country-flags/Niue.png b/bot/games/assets/country-flags/Niue.png new file mode 100644 index 0000000..47c5544 Binary files /dev/null and b/bot/games/assets/country-flags/Niue.png differ diff --git a/bot/games/assets/country-flags/Norfolk Island.png b/bot/games/assets/country-flags/Norfolk Island.png new file mode 100644 index 0000000..f3afa90 Binary files /dev/null and b/bot/games/assets/country-flags/Norfolk Island.png differ diff --git a/bot/games/assets/country-flags/North Korea.png b/bot/games/assets/country-flags/North Korea.png new file mode 100644 index 0000000..387be12 Binary files /dev/null and b/bot/games/assets/country-flags/North Korea.png differ diff --git a/bot/games/assets/country-flags/North Macedonia.png b/bot/games/assets/country-flags/North Macedonia.png new file mode 100644 index 0000000..c036a4e Binary files /dev/null and b/bot/games/assets/country-flags/North Macedonia.png differ diff --git a/bot/games/assets/country-flags/Northern Ireland.png b/bot/games/assets/country-flags/Northern Ireland.png new file mode 100644 index 0000000..80983ae Binary files /dev/null and b/bot/games/assets/country-flags/Northern Ireland.png differ diff --git a/bot/games/assets/country-flags/Northern Mariana Islands.png b/bot/games/assets/country-flags/Northern Mariana Islands.png new file mode 100644 index 0000000..629661a Binary files /dev/null and b/bot/games/assets/country-flags/Northern Mariana Islands.png differ diff --git a/bot/games/assets/country-flags/Norway.png b/bot/games/assets/country-flags/Norway.png new file mode 100644 index 0000000..4293233 Binary files /dev/null and b/bot/games/assets/country-flags/Norway.png differ diff --git a/bot/games/assets/country-flags/Oman.png b/bot/games/assets/country-flags/Oman.png new file mode 100644 index 0000000..f46edf5 Binary files /dev/null and b/bot/games/assets/country-flags/Oman.png differ diff --git a/bot/games/assets/country-flags/Pakistan.png b/bot/games/assets/country-flags/Pakistan.png new file mode 100644 index 0000000..da2b8d3 Binary files /dev/null and b/bot/games/assets/country-flags/Pakistan.png differ diff --git a/bot/games/assets/country-flags/Palau.png b/bot/games/assets/country-flags/Palau.png new file mode 100644 index 0000000..816f21d Binary files /dev/null and b/bot/games/assets/country-flags/Palau.png differ diff --git a/bot/games/assets/country-flags/Palestine.png b/bot/games/assets/country-flags/Palestine.png new file mode 100644 index 0000000..b65b9b4 Binary files /dev/null and b/bot/games/assets/country-flags/Palestine.png differ diff --git a/bot/games/assets/country-flags/Panama.png b/bot/games/assets/country-flags/Panama.png new file mode 100644 index 0000000..4ae2361 Binary files /dev/null and b/bot/games/assets/country-flags/Panama.png differ diff --git a/bot/games/assets/country-flags/Papua New Guinea.png b/bot/games/assets/country-flags/Papua New Guinea.png new file mode 100644 index 0000000..f590675 Binary files /dev/null and b/bot/games/assets/country-flags/Papua New Guinea.png differ diff --git a/bot/games/assets/country-flags/Paraguay.png b/bot/games/assets/country-flags/Paraguay.png new file mode 100644 index 0000000..e4f0568 Binary files /dev/null and b/bot/games/assets/country-flags/Paraguay.png differ diff --git a/bot/games/assets/country-flags/Peru.png b/bot/games/assets/country-flags/Peru.png new file mode 100644 index 0000000..17abcf6 Binary files /dev/null and b/bot/games/assets/country-flags/Peru.png differ diff --git a/bot/games/assets/country-flags/Philippines.png b/bot/games/assets/country-flags/Philippines.png new file mode 100644 index 0000000..af7bf17 Binary files /dev/null and b/bot/games/assets/country-flags/Philippines.png differ diff --git a/bot/games/assets/country-flags/Pitcairn.png b/bot/games/assets/country-flags/Pitcairn.png new file mode 100644 index 0000000..df2fc2b Binary files /dev/null and b/bot/games/assets/country-flags/Pitcairn.png differ diff --git a/bot/games/assets/country-flags/Poland.png b/bot/games/assets/country-flags/Poland.png new file mode 100644 index 0000000..3b6d000 Binary files /dev/null and b/bot/games/assets/country-flags/Poland.png differ diff --git a/bot/games/assets/country-flags/Portugal.png b/bot/games/assets/country-flags/Portugal.png new file mode 100644 index 0000000..87a16ba Binary files /dev/null and b/bot/games/assets/country-flags/Portugal.png differ diff --git a/bot/games/assets/country-flags/Puerto Rico.png b/bot/games/assets/country-flags/Puerto Rico.png new file mode 100644 index 0000000..69f4a4c Binary files /dev/null and b/bot/games/assets/country-flags/Puerto Rico.png differ diff --git a/bot/games/assets/country-flags/Qatar.png b/bot/games/assets/country-flags/Qatar.png new file mode 100644 index 0000000..ebb3f0e Binary files /dev/null and b/bot/games/assets/country-flags/Qatar.png differ diff --git a/bot/games/assets/country-flags/Romania.png b/bot/games/assets/country-flags/Romania.png new file mode 100644 index 0000000..aeb287e Binary files /dev/null and b/bot/games/assets/country-flags/Romania.png differ diff --git a/bot/games/assets/country-flags/Russia.png b/bot/games/assets/country-flags/Russia.png new file mode 100644 index 0000000..93be8a2 Binary files /dev/null and b/bot/games/assets/country-flags/Russia.png differ diff --git a/bot/games/assets/country-flags/Rwanda.png b/bot/games/assets/country-flags/Rwanda.png new file mode 100644 index 0000000..52f5c78 Binary files /dev/null and b/bot/games/assets/country-flags/Rwanda.png differ diff --git a/bot/games/assets/country-flags/R‚union.png b/bot/games/assets/country-flags/R‚union.png new file mode 100644 index 0000000..51d21a9 Binary files /dev/null and b/bot/games/assets/country-flags/R‚union.png differ diff --git a/bot/games/assets/country-flags/Samoa.png b/bot/games/assets/country-flags/Samoa.png new file mode 100644 index 0000000..e2c5a3f Binary files /dev/null and b/bot/games/assets/country-flags/Samoa.png differ diff --git a/bot/games/assets/country-flags/San Marino.png b/bot/games/assets/country-flags/San Marino.png new file mode 100644 index 0000000..d9e6dbc Binary files /dev/null and b/bot/games/assets/country-flags/San Marino.png differ diff --git a/bot/games/assets/country-flags/Sao Tome and Principe.png b/bot/games/assets/country-flags/Sao Tome and Principe.png new file mode 100644 index 0000000..85458a1 Binary files /dev/null and b/bot/games/assets/country-flags/Sao Tome and Principe.png differ diff --git a/bot/games/assets/country-flags/Saudi Arabia.png b/bot/games/assets/country-flags/Saudi Arabia.png new file mode 100644 index 0000000..2fb198d Binary files /dev/null and b/bot/games/assets/country-flags/Saudi Arabia.png differ diff --git a/bot/games/assets/country-flags/Scotland.png b/bot/games/assets/country-flags/Scotland.png new file mode 100644 index 0000000..a7a5319 Binary files /dev/null and b/bot/games/assets/country-flags/Scotland.png differ diff --git a/bot/games/assets/country-flags/Senegal.png b/bot/games/assets/country-flags/Senegal.png new file mode 100644 index 0000000..7ad6408 Binary files /dev/null and b/bot/games/assets/country-flags/Senegal.png differ diff --git a/bot/games/assets/country-flags/Serbia.png b/bot/games/assets/country-flags/Serbia.png new file mode 100644 index 0000000..b2592f3 Binary files /dev/null and b/bot/games/assets/country-flags/Serbia.png differ diff --git a/bot/games/assets/country-flags/Seychelles.png b/bot/games/assets/country-flags/Seychelles.png new file mode 100644 index 0000000..cf8cc43 Binary files /dev/null and b/bot/games/assets/country-flags/Seychelles.png differ diff --git a/bot/games/assets/country-flags/Sierra Leone.png b/bot/games/assets/country-flags/Sierra Leone.png new file mode 100644 index 0000000..a40ee63 Binary files /dev/null and b/bot/games/assets/country-flags/Sierra Leone.png differ diff --git a/bot/games/assets/country-flags/Singapore.png b/bot/games/assets/country-flags/Singapore.png new file mode 100644 index 0000000..8d58936 Binary files /dev/null and b/bot/games/assets/country-flags/Singapore.png differ diff --git a/bot/games/assets/country-flags/Sint Maarten.png b/bot/games/assets/country-flags/Sint Maarten.png new file mode 100644 index 0000000..a472bd1 Binary files /dev/null and b/bot/games/assets/country-flags/Sint Maarten.png differ diff --git a/bot/games/assets/country-flags/Slovakia.png b/bot/games/assets/country-flags/Slovakia.png new file mode 100644 index 0000000..fd3285c Binary files /dev/null and b/bot/games/assets/country-flags/Slovakia.png differ diff --git a/bot/games/assets/country-flags/Slovenia.png b/bot/games/assets/country-flags/Slovenia.png new file mode 100644 index 0000000..854cb8e Binary files /dev/null and b/bot/games/assets/country-flags/Slovenia.png differ diff --git a/bot/games/assets/country-flags/Solomon Islands.png b/bot/games/assets/country-flags/Solomon Islands.png new file mode 100644 index 0000000..d8afd29 Binary files /dev/null and b/bot/games/assets/country-flags/Solomon Islands.png differ diff --git a/bot/games/assets/country-flags/Somalia.png b/bot/games/assets/country-flags/Somalia.png new file mode 100644 index 0000000..bbb842f Binary files /dev/null and b/bot/games/assets/country-flags/Somalia.png differ diff --git a/bot/games/assets/country-flags/South Africa.png b/bot/games/assets/country-flags/South Africa.png new file mode 100644 index 0000000..d87cd2d Binary files /dev/null and b/bot/games/assets/country-flags/South Africa.png differ diff --git a/bot/games/assets/country-flags/South Georgia.png b/bot/games/assets/country-flags/South Georgia.png new file mode 100644 index 0000000..eed237b Binary files /dev/null and b/bot/games/assets/country-flags/South Georgia.png differ diff --git a/bot/games/assets/country-flags/South Korea.png b/bot/games/assets/country-flags/South Korea.png new file mode 100644 index 0000000..dfd5df9 Binary files /dev/null and b/bot/games/assets/country-flags/South Korea.png differ diff --git a/bot/games/assets/country-flags/South Sudan.png b/bot/games/assets/country-flags/South Sudan.png new file mode 100644 index 0000000..47fa68f Binary files /dev/null and b/bot/games/assets/country-flags/South Sudan.png differ diff --git a/bot/games/assets/country-flags/Spain.png b/bot/games/assets/country-flags/Spain.png new file mode 100644 index 0000000..b712bb8 Binary files /dev/null and b/bot/games/assets/country-flags/Spain.png differ diff --git a/bot/games/assets/country-flags/Sri Lanka.png b/bot/games/assets/country-flags/Sri Lanka.png new file mode 100644 index 0000000..e32d029 Binary files /dev/null and b/bot/games/assets/country-flags/Sri Lanka.png differ diff --git a/bot/games/assets/country-flags/St Barth‚lemy.png b/bot/games/assets/country-flags/St Barth‚lemy.png new file mode 100644 index 0000000..51d21a9 Binary files /dev/null and b/bot/games/assets/country-flags/St Barth‚lemy.png differ diff --git a/bot/games/assets/country-flags/St Helena.png b/bot/games/assets/country-flags/St Helena.png new file mode 100644 index 0000000..80983ae Binary files /dev/null and b/bot/games/assets/country-flags/St Helena.png differ diff --git a/bot/games/assets/country-flags/St Kitts and Nevis.png b/bot/games/assets/country-flags/St Kitts and Nevis.png new file mode 100644 index 0000000..7851b86 Binary files /dev/null and b/bot/games/assets/country-flags/St Kitts and Nevis.png differ diff --git a/bot/games/assets/country-flags/St Lucia.png b/bot/games/assets/country-flags/St Lucia.png new file mode 100644 index 0000000..514081b Binary files /dev/null and b/bot/games/assets/country-flags/St Lucia.png differ diff --git a/bot/games/assets/country-flags/St Martin.png b/bot/games/assets/country-flags/St Martin.png new file mode 100644 index 0000000..51d21a9 Binary files /dev/null and b/bot/games/assets/country-flags/St Martin.png differ diff --git a/bot/games/assets/country-flags/St Pierre and Miquelon.png b/bot/games/assets/country-flags/St Pierre and Miquelon.png new file mode 100644 index 0000000..51d21a9 Binary files /dev/null and b/bot/games/assets/country-flags/St Pierre and Miquelon.png differ diff --git a/bot/games/assets/country-flags/St Vincent and the Grenadines.png b/bot/games/assets/country-flags/St Vincent and the Grenadines.png new file mode 100644 index 0000000..547b470 Binary files /dev/null and b/bot/games/assets/country-flags/St Vincent and the Grenadines.png differ diff --git a/bot/games/assets/country-flags/Sudan.png b/bot/games/assets/country-flags/Sudan.png new file mode 100644 index 0000000..189c5b9 Binary files /dev/null and b/bot/games/assets/country-flags/Sudan.png differ diff --git a/bot/games/assets/country-flags/Suriname.png b/bot/games/assets/country-flags/Suriname.png new file mode 100644 index 0000000..6a86364 Binary files /dev/null and b/bot/games/assets/country-flags/Suriname.png differ diff --git a/bot/games/assets/country-flags/Svalbard and Jan Mayen Islands.png b/bot/games/assets/country-flags/Svalbard and Jan Mayen Islands.png new file mode 100644 index 0000000..4293233 Binary files /dev/null and b/bot/games/assets/country-flags/Svalbard and Jan Mayen Islands.png differ diff --git a/bot/games/assets/country-flags/Swaziland.png b/bot/games/assets/country-flags/Swaziland.png new file mode 100644 index 0000000..99f182a Binary files /dev/null and b/bot/games/assets/country-flags/Swaziland.png differ diff --git a/bot/games/assets/country-flags/Sweden.png b/bot/games/assets/country-flags/Sweden.png new file mode 100644 index 0000000..ffd6b5f Binary files /dev/null and b/bot/games/assets/country-flags/Sweden.png differ diff --git a/bot/games/assets/country-flags/Switzerland.png b/bot/games/assets/country-flags/Switzerland.png new file mode 100644 index 0000000..e98e372 Binary files /dev/null and b/bot/games/assets/country-flags/Switzerland.png differ diff --git a/bot/games/assets/country-flags/Syria.png b/bot/games/assets/country-flags/Syria.png new file mode 100644 index 0000000..c5064cb Binary files /dev/null and b/bot/games/assets/country-flags/Syria.png differ diff --git a/bot/games/assets/country-flags/Taiwan (Republic of China).png b/bot/games/assets/country-flags/Taiwan (Republic of China).png new file mode 100644 index 0000000..319432d Binary files /dev/null and b/bot/games/assets/country-flags/Taiwan (Republic of China).png differ diff --git a/bot/games/assets/country-flags/Tajikistan.png b/bot/games/assets/country-flags/Tajikistan.png new file mode 100644 index 0000000..974d6ee Binary files /dev/null and b/bot/games/assets/country-flags/Tajikistan.png differ diff --git a/bot/games/assets/country-flags/Tanzania.png b/bot/games/assets/country-flags/Tanzania.png new file mode 100644 index 0000000..ce53bcb Binary files /dev/null and b/bot/games/assets/country-flags/Tanzania.png differ diff --git a/bot/games/assets/country-flags/Thailand.png b/bot/games/assets/country-flags/Thailand.png new file mode 100644 index 0000000..7787887 Binary files /dev/null and b/bot/games/assets/country-flags/Thailand.png differ diff --git a/bot/games/assets/country-flags/Timor-Leste.png b/bot/games/assets/country-flags/Timor-Leste.png new file mode 100644 index 0000000..8540223 Binary files /dev/null and b/bot/games/assets/country-flags/Timor-Leste.png differ diff --git a/bot/games/assets/country-flags/Togo.png b/bot/games/assets/country-flags/Togo.png new file mode 100644 index 0000000..16c0c43 Binary files /dev/null and b/bot/games/assets/country-flags/Togo.png differ diff --git a/bot/games/assets/country-flags/Tokelau.png b/bot/games/assets/country-flags/Tokelau.png new file mode 100644 index 0000000..d71597a Binary files /dev/null and b/bot/games/assets/country-flags/Tokelau.png differ diff --git a/bot/games/assets/country-flags/Tonga.png b/bot/games/assets/country-flags/Tonga.png new file mode 100644 index 0000000..f504309 Binary files /dev/null and b/bot/games/assets/country-flags/Tonga.png differ diff --git a/bot/games/assets/country-flags/Trinidad and Tobago.png b/bot/games/assets/country-flags/Trinidad and Tobago.png new file mode 100644 index 0000000..9c3cac2 Binary files /dev/null and b/bot/games/assets/country-flags/Trinidad and Tobago.png differ diff --git a/bot/games/assets/country-flags/Tunisia.png b/bot/games/assets/country-flags/Tunisia.png new file mode 100644 index 0000000..c69c5da Binary files /dev/null and b/bot/games/assets/country-flags/Tunisia.png differ diff --git a/bot/games/assets/country-flags/Turkey.png b/bot/games/assets/country-flags/Turkey.png new file mode 100644 index 0000000..6c5ea73 Binary files /dev/null and b/bot/games/assets/country-flags/Turkey.png differ diff --git a/bot/games/assets/country-flags/Turkmenistan.png b/bot/games/assets/country-flags/Turkmenistan.png new file mode 100644 index 0000000..9be260f Binary files /dev/null and b/bot/games/assets/country-flags/Turkmenistan.png differ diff --git a/bot/games/assets/country-flags/Turks and Caicos Islands.png b/bot/games/assets/country-flags/Turks and Caicos Islands.png new file mode 100644 index 0000000..0439ef1 Binary files /dev/null and b/bot/games/assets/country-flags/Turks and Caicos Islands.png differ diff --git a/bot/games/assets/country-flags/Tuvalu.png b/bot/games/assets/country-flags/Tuvalu.png new file mode 100644 index 0000000..a2454c0 Binary files /dev/null and b/bot/games/assets/country-flags/Tuvalu.png differ diff --git a/bot/games/assets/country-flags/Uganda.png b/bot/games/assets/country-flags/Uganda.png new file mode 100644 index 0000000..c58eb43 Binary files /dev/null and b/bot/games/assets/country-flags/Uganda.png differ diff --git a/bot/games/assets/country-flags/Ukraine.png b/bot/games/assets/country-flags/Ukraine.png new file mode 100644 index 0000000..2ef1a7e Binary files /dev/null and b/bot/games/assets/country-flags/Ukraine.png differ diff --git a/bot/games/assets/country-flags/United Arab Emirates.png b/bot/games/assets/country-flags/United Arab Emirates.png new file mode 100644 index 0000000..ccb32fd Binary files /dev/null and b/bot/games/assets/country-flags/United Arab Emirates.png differ diff --git a/bot/games/assets/country-flags/United Kingdom.png b/bot/games/assets/country-flags/United Kingdom.png new file mode 100644 index 0000000..80983ae Binary files /dev/null and b/bot/games/assets/country-flags/United Kingdom.png differ diff --git a/bot/games/assets/country-flags/United States.png b/bot/games/assets/country-flags/United States.png new file mode 100644 index 0000000..a12b076 Binary files /dev/null and b/bot/games/assets/country-flags/United States.png differ diff --git a/bot/games/assets/country-flags/Uruguay.png b/bot/games/assets/country-flags/Uruguay.png new file mode 100644 index 0000000..571502e Binary files /dev/null and b/bot/games/assets/country-flags/Uruguay.png differ diff --git a/bot/games/assets/country-flags/Uzbekistan.png b/bot/games/assets/country-flags/Uzbekistan.png new file mode 100644 index 0000000..f39add3 Binary files /dev/null and b/bot/games/assets/country-flags/Uzbekistan.png differ diff --git a/bot/games/assets/country-flags/Vanuatu.png b/bot/games/assets/country-flags/Vanuatu.png new file mode 100644 index 0000000..fdc0d40 Binary files /dev/null and b/bot/games/assets/country-flags/Vanuatu.png differ diff --git a/bot/games/assets/country-flags/Vatican City.png b/bot/games/assets/country-flags/Vatican City.png new file mode 100644 index 0000000..63f11d3 Binary files /dev/null and b/bot/games/assets/country-flags/Vatican City.png differ diff --git a/bot/games/assets/country-flags/Venezuela.png b/bot/games/assets/country-flags/Venezuela.png new file mode 100644 index 0000000..27a9c8f Binary files /dev/null and b/bot/games/assets/country-flags/Venezuela.png differ diff --git a/bot/games/assets/country-flags/Vietnam.png b/bot/games/assets/country-flags/Vietnam.png new file mode 100644 index 0000000..f535c89 Binary files /dev/null and b/bot/games/assets/country-flags/Vietnam.png differ diff --git a/bot/games/assets/country-flags/Virgin Islands, British.png b/bot/games/assets/country-flags/Virgin Islands, British.png new file mode 100644 index 0000000..05942c8 Binary files /dev/null and b/bot/games/assets/country-flags/Virgin Islands, British.png differ diff --git a/bot/games/assets/country-flags/Virgin Islands, U.S..png b/bot/games/assets/country-flags/Virgin Islands, U.S..png new file mode 100644 index 0000000..1c177de Binary files /dev/null and b/bot/games/assets/country-flags/Virgin Islands, U.S..png differ diff --git a/bot/games/assets/country-flags/Wales.png b/bot/games/assets/country-flags/Wales.png new file mode 100644 index 0000000..dab728d Binary files /dev/null and b/bot/games/assets/country-flags/Wales.png differ diff --git a/bot/games/assets/country-flags/Wallis and Futuna Islands.png b/bot/games/assets/country-flags/Wallis and Futuna Islands.png new file mode 100644 index 0000000..ed63402 Binary files /dev/null and b/bot/games/assets/country-flags/Wallis and Futuna Islands.png differ diff --git a/bot/games/assets/country-flags/Western Sahara.png b/bot/games/assets/country-flags/Western Sahara.png new file mode 100644 index 0000000..cf9e3e2 Binary files /dev/null and b/bot/games/assets/country-flags/Western Sahara.png differ diff --git a/bot/games/assets/country-flags/Yemen.png b/bot/games/assets/country-flags/Yemen.png new file mode 100644 index 0000000..b9f21d8 Binary files /dev/null and b/bot/games/assets/country-flags/Yemen.png differ diff --git a/bot/games/assets/country-flags/Zambia.png b/bot/games/assets/country-flags/Zambia.png new file mode 100644 index 0000000..8263be9 Binary files /dev/null and b/bot/games/assets/country-flags/Zambia.png differ diff --git a/bot/games/assets/country-flags/Zimbabwe.png b/bot/games/assets/country-flags/Zimbabwe.png new file mode 100644 index 0000000..6f37548 Binary files /dev/null and b/bot/games/assets/country-flags/Zimbabwe.png differ diff --git a/bot/games/assets/country-flags/land Islands.png b/bot/games/assets/country-flags/land Islands.png new file mode 100644 index 0000000..970d752 Binary files /dev/null and b/bot/games/assets/country-flags/land Islands.png differ diff --git a/bot/games/assets/segoe-ui-semilight-411.ttf b/bot/games/assets/segoe-ui-semilight-411.ttf new file mode 100644 index 0000000..34805d3 Binary files /dev/null and b/bot/games/assets/segoe-ui-semilight-411.ttf differ diff --git a/bot/games/assets/words.txt b/bot/games/assets/words.txt new file mode 100644 index 0000000..ff6e164 --- /dev/null +++ b/bot/games/assets/words.txt @@ -0,0 +1,12947 @@ +women +nikau +swack +feens +fyles +poled +clags +starn +bindi +woops +fanos +cabin +souct +trass +shoat +lefty +durra +hypes +junta +baisa +bises +kipps +sable +abacs +thurl +nurrs +saris +wroth +venal +texas +soman +linds +laden +nolos +pixie +calms +chert +oxbow +groma +nomen +potae +noyed +fifty +emerg +shtup +aspic +shone +junky +louns +babka +roton +abaft +hykes +nipas +inbye +kaing +pukus +muils +snowy +piled +brook +avens +baiza +edger +fawns +genii +mavis +argal +assay +cocas +shash +wrath +thins +karat +tunny +mudge +syped +chose +zupas +hants +leech +lyric +winds +mened +momus +usher +qophs +ombus +gavel +swive +slant +firns +beigy +unlid +flegs +wangs +awner +claut +ceded +manos +fuggy +bunde +shute +snoke +bulky +cents +agama +chess +ranid +flurr +dewar +night +porks +voema +cimex +samfu +query +snipy +glens +kests +peril +falls +urges +krunk +tased +folia +orgia +verve +rinks +choko +hully +fakey +durgy +polje +sects +giant +iftar +hayed +elfed +likes +sword +banty +blech +daubs +exies +tetra +agros +shier +kines +yanks +herma +bitte +spook +ribby +fazes +faqir +pluck +devos +bares +looks +sepad +blats +splay +wimpy +husos +forge +femes +irony +hurra +annoy +macas +phons +gymps +sepic +horde +redox +raise +venom +balks +houff +bivvy +farci +sodas +salvo +gumbo +monad +tidal +jammy +gurly +gapes +drere +seems +bouge +ollas +fakir +fetta +thesp +trots +sixes +parps +rewed +wakes +gades +hired +ferny +orals +faxes +surds +larns +sophs +malts +delos +vixen +hosts +drawn +indow +oddly +grume +radix +sacra +spoom +poopy +datos +salse +skean +loess +sownd +boast +tragi +noyau +yeven +blore +tawas +furor +dotes +thief +dacks +pilus +wader +ralph +dropt +illth +paged +humor +great +neves +ratio +lordy +sonic +gybes +shama +limed +salal +aorta +beach +glogg +abris +sayne +mince +dukes +sloth +laked +exeem +troys +kehua +studs +lummy +rhumb +ardeb +yeads +liney +salat +tappa +zilch +yeggs +girly +hoots +parev +gusle +awake +umiak +swang +dunts +ridgy +fakie +seils +seels +kagus +yodhs +sools +richt +runds +snark +domed +glede +urbia +laiks +keech +pinna +ebook +flips +lewis +corse +camus +swaps +delis +hamed +zowee +egers +atmas +xeric +apery +beryl +ocher +lysol +pokal +watap +metic +burns +dibbs +vares +cruor +snods +probs +undue +scaur +thole +sexed +onion +zoril +nance +deffo +prize +curds +bazar +milko +cowed +rager +corps +audio +boofy +hollo +hapax +jeune +idant +swiss +catch +gript +spewy +roble +waurs +beeps +kales +prest +geals +tater +tassa +bocce +ulans +ahull +sheik +elegy +plops +scrip +zaire +laddy +dings +punka +pacey +bilgy +chewy +hemps +jolts +greet +leans +squit +tromp +flume +rower +penne +umbra +palmy +tunas +cleek +flimp +pedal +cuppy +bundu +dweeb +pupas +prude +alter +nyaff +laxer +gerbe +anime +nieve +bwazi +brule +cider +roneo +nirls +kaugh +oases +fewer +pinny +sault +carvy +ultra +kloof +spazz +spoil +logie +orang +fices +atocs +fungi +kilos +amnia +glads +chaft +lusts +toxin +boozy +yourn +medal +maras +sowse +enmew +mains +olpae +tride +nival +loure +crook +spied +ketas +labis +tossy +yapon +tweer +peris +kudzu +odals +rosed +noser +crena +heist +pervy +amaze +neons +mirid +mured +helve +hepar +demic +besaw +molls +annex +warks +smews +warby +fayer +minks +ripes +hacek +spait +audad +clack +afros +greve +dwaal +bayts +ottar +diels +tansy +ikons +craps +phpht +glass +talar +umrah +yesks +toter +waifs +limby +alure +elint +exude +annas +sputa +ettin +oaker +yerks +noils +trier +yulan +ghast +tuile +kylin +cadet +molys +dobro +barry +disme +tifos +betid +sonar +butte +dojos +befog +noles +guyot +avels +kneel +suety +biali +perve +kieve +faves +stims +qibla +sprig +cited +tinas +profs +gamay +simis +duads +picks +sweer +sippy +mound +hault +gouts +zobos +shaws +fosse +vawte +telia +soggy +dopes +sheal +crape +jimmy +swale +loans +strop +pizes +synes +kiers +agley +matzo +bands +blahs +lymph +lysis +ruche +anion +chomp +dikas +volta +luffs +howso +quiff +bling +clomb +upper +dumps +buddy +caron +upped +shite +raxed +kamis +sweal +creep +albee +taint +bitsy +abyss +gadje +spaul +shiny +fusts +yeeds +cusks +deely +smolt +lanky +unwon +ology +anise +foram +scops +rakia +banjo +domic +hoody +shott +sooth +panim +lathi +licit +machi +vetch +boffo +belch +downa +tofus +wulls +steno +hoten +snoep +meted +halls +tryke +lovat +vardy +modal +updry +lythe +nidus +stave +kanji +poted +stabs +pargo +slews +cirri +grosz +pooks +pimps +emmys +flies +nelly +felly +nuffs +amity +vodun +stock +cacti +skips +whops +apian +acted +bawty +lande +tufts +besot +mewls +sunns +pulus +paint +gouch +scrag +quote +louis +heeds +jihad +minus +unban +ranke +licht +ishes +grees +gimpy +paren +nudzh +thawy +toras +laten +enoki +limey +biked +grans +emmas +enews +aweel +sedum +askew +arish +fusee +tolas +carve +rayle +emits +dampy +fakes +meson +gaita +fauve +slung +vowel +goffs +fogou +vinic +ratan +becke +algal +manis +gelds +erred +pekoe +flitt +dotty +tronc +loirs +firie +gonks +joles +lumen +sensa +undid +dhobi +tuner +skail +homey +alack +clear +slums +mange +weamb +papas +smoot +buats +hooka +recco +agars +plank +bolas +bolix +hashy +easel +wasps +sexto +queer +wacke +crits +moria +uncos +kibei +parly +ebbet +rusks +kibbe +buppy +zeals +glout +bigly +stool +avale +wales +fermi +unred +puppy +swayl +peage +bingy +verts +baaed +mogul +beany +debel +mifty +levis +gowfs +winks +musha +bayes +bidet +starr +cloye +ennui +hussy +cosey +coset +darcy +iodid +treks +tsars +groof +razoo +polls +clame +pilei +bunns +donko +fedex +vasal +enema +gaffe +slash +prise +mandi +solve +dames +hullo +snarl +monty +wuxia +beige +reech +solus +doggy +evohe +papal +dolce +awork +cedar +youse +mamey +icily +scoup +zonda +whamo +serry +coyed +amine +mudra +clews +proof +horny +jarul +falaj +clons +limbs +anyon +lanch +muled +kirri +kroon +skees +gothy +james +chirr +yarta +rayas +femme +kasha +milos +asyla +tanks +unbox +umber +crine +situp +singe +pyrex +flote +yogis +scrow +kypes +esses +istle +jeeps +zendo +rough +tight +stewy +scent +arett +yelts +apode +hoaed +ivies +heids +twire +sighs +coppy +jotun +chems +benes +jebel +swaly +holks +doest +fluky +wares +cusso +reist +darbs +peans +erven +peeoy +curls +sways +blaud +nowts +proso +zooid +liard +oundy +sughs +jehus +cotts +guess +teste +bizes +loipe +liart +gitch +mauds +mufti +vutty +haver +diker +score +araks +wekas +nuked +nervy +spiff +orbed +buyer +routs +impel +truly +presa +whats +naans +seams +flint +rives +matai +culpa +frere +wazir +logon +gungy +wrang +fenis +rakes +fores +duroc +ailed +clous +toyon +sawer +tikis +withy +embed +subas +dadas +bacca +epees +kamas +earst +prole +cimar +dirts +strep +mount +lacet +wrier +nites +nomas +rearm +yclad +galed +owsen +tints +sculk +culet +swamp +homie +keens +genny +split +anvil +stoln +sazes +pesky +bento +witan +besit +clubs +causa +weids +theic +fitch +deere +kelim +chare +simas +madam +jumbo +pronk +enols +jails +tohos +kagos +plues +aboil +bangs +graze +kauri +rewin +weize +hadji +misdo +parma +urent +laich +panni +deary +coses +exome +mohel +poems +axoid +chump +puked +apers +claro +slade +fetts +mutts +rural +vower +argan +stook +muids +budge +arvos +cynic +fasci +jurel +grand +gynie +garbs +cuffs +beget +abled +artel +miens +shops +piums +maxis +kwela +rigid +vends +farts +asana +viers +later +haith +motte +kokas +cooed +drent +ekkas +whata +sweir +borne +karas +heats +etude +regna +resew +bulbs +balus +order +jibba +cocoa +hauds +exuls +spuer +frena +karos +these +skier +sclim +jaggy +coost +kulfi +rhomb +rejon +jupon +awarn +bowie +spets +pipet +debug +folie +moyle +rauns +sooms +carpi +proas +siren +shyly +doers +walks +doric +smees +skeed +ricin +lassu +aboma +qaids +asper +scull +glost +chord +brant +kefir +topic +gibel +nimps +zigan +twirp +lazes +pawls +wells +coned +wembs +frats +genom +quota +brunt +spout +tache +trunk +wifed +acini +coast +manta +coopt +tabid +dauds +crank +untie +nasal +shine +sauls +yeuky +sturt +odder +gucks +lungs +dight +rawer +rykes +limns +stoit +crumb +dewed +moldy +kreep +frith +opsin +bools +kempy +kohas +slurp +nudie +ephas +email +skosh +tolly +speks +patio +congo +kanes +swile +kneed +merge +ycled +hewgh +loyal +stens +blent +alapa +gulag +medle +nagor +lulls +sawah +spier +poufs +lunch +stupe +sewen +shend +dirls +trows +iched +hello +leery +lowse +boron +aunts +trash +naiks +crows +poult +emove +inorb +local +rorid +pumie +gloom +bolus +fogie +peach +erick +guppy +lurgy +popes +veges +taken +redye +spred +sugar +reird +chubs +grail +paler +ydred +stoas +exile +child +yaars +snide +snips +album +rebit +mochs +looby +raphe +tawny +pioys +makar +cowan +slipt +tyros +saith +caved +colts +unrip +pases +saran +tykes +allod +ovals +festa +puers +pight +treif +cocky +feeds +blads +gouks +brail +skald +feted +realo +allot +delph +oumas +sklim +shlep +angle +silts +stand +jibed +frows +tayra +sculp +dicht +spags +pebas +nurse +prows +hubby +togas +helot +hangs +neele +pools +bidis +hoers +abuts +serif +scuba +copsy +lumme +numbs +yogic +sober +biped +lawks +mixen +yurts +hokey +stied +gowan +chary +pendu +rojak +punga +soree +hoods +sefer +mabes +plume +dowel +shrug +oucht +vista +fatwa +diode +kents +swoun +barps +obiit +hosta +kanga +sophy +quich +bravi +skied +spaza +faurd +karts +waide +coper +broke +gayer +pieta +pengo +lokes +amole +cruet +busty +rewon +jorum +shave +maths +jotas +sifts +kophs +nisus +pucer +sekos +nadas +punky +ameba +lupin +iambs +revel +wiles +vairy +tesla +ouped +skite +teads +cusec +citer +ratel +gleam +lends +hater +saims +strig +kalpa +foamy +drats +dowse +atimy +opepe +basho +bield +hasty +rheas +filer +moors +skirl +slogs +sayed +hoord +thaim +hetes +roast +hitch +roins +jhala +levee +woven +cites +yacht +maile +ancon +divvy +romal +gapos +bedim +roded +wagga +banda +swear +linny +welkt +nixes +yangs +slorm +splat +skelp +perdy +comby +spike +ender +sapid +muhly +agger +arose +chops +mitis +gompa +skids +nines +leges +baric +psora +bayer +disas +bribe +bower +tawse +pyxed +coven +purin +sokol +tuffs +ileus +grebo +seeps +spicy +ninon +floes +mozes +chant +tasso +dauts +wenge +cabal +mammy +yuppy +ginny +karma +softs +yirrs +morse +newsy +renga +grues +alang +ahold +chaos +pyned +gemmy +solid +conus +dryas +burks +thraw +cotta +rurus +octan +resee +poncy +balls +sybil +vesta +wonts +kilty +reedy +charr +upled +aidos +epoxy +surra +soils +barfi +guyse +aryls +convo +nepit +lakes +maize +neper +zooty +voted +murti +izzat +glaur +bohos +pelas +lomes +jambs +semes +donne +middy +barge +jazzy +moose +sicks +loggy +xylan +noons +judge +toaze +roids +doeth +palla +luvvy +farer +treyf +oculi +shuts +ureal +yummy +octas +cased +slump +argle +bigos +janns +gnaws +scags +pouks +plaps +vrils +swash +cline +fuffy +viffs +neeps +birls +quoll +duped +barde +porty +byway +siree +stria +hushy +ingot +genre +kists +deens +begin +mooli +retox +noway +camos +alary +tommy +ulnar +fetwa +oshac +tower +elute +brool +thoft +beset +rebuy +slays +amble +blady +fetor +pimas +coits +daggy +crore +metho +noyes +eejit +luter +beare +nills +relet +delve +spunk +eying +curio +kolas +wiver +apiol +meals +malls +zooms +kades +psych +harks +soars +juice +fremd +joule +laund +ovine +aggry +zitis +spite +guimp +powan +knops +spade +bully +muser +taboo +tress +fatso +thine +cardy +dorad +lepra +ulcer +lamer +huzzy +yucca +cital +aloes +dowdy +acned +danks +villi +crams +appel +heart +lakhs +talls +clans +squat +yowed +synth +grebe +telos +annal +props +hilar +metre +arled +shorn +aarti +swamy +crias +float +flesh +tinty +dedal +above +sonse +minar +tanna +tikes +salts +madid +poupt +lager +maced +guilt +reccy +sever +hoove +scyes +kafir +tangy +hillo +scant +comal +simul +liner +stele +borax +azygy +mises +tiges +roary +spiky +lived +waxed +duded +brers +cowps +grips +roons +pilar +poser +cesta +thill +huhus +waxer +testy +phage +loins +grama +flexo +pound +dobes +kaput +ratal +cacks +upjet +henry +javel +resus +weepy +fussy +onely +mirin +gests +shura +akene +bohea +haler +wames +grufe +amiga +kaama +synch +duked +raser +shwas +width +rehem +eyras +tates +preys +knave +raper +litho +solas +zuzim +batch +biggs +smirr +dryad +skeef +hanky +imply +sujee +xylyl +cheat +bords +ilial +bowne +silos +samel +cater +gleis +sicko +primy +pechs +tiros +glams +becks +wilis +rowie +goold +ligne +aures +spelk +pepla +fired +devil +goxes +corni +sicky +tutus +pians +zymes +viold +staig +roose +undos +dolma +qanat +moust +doole +soger +agios +nalla +garda +kylie +eking +fairs +snell +urped +rumly +brack +chiel +valse +frier +psalm +flays +lucks +karri +mensa +luxer +spahi +aurar +moory +ensue +cameo +dwams +cuber +urbex +walie +reggo +ankus +irids +ervil +slopy +greed +snoop +mugga +zoeal +poyse +ivory +urena +eruct +trave +ranee +flubs +holme +rhone +jutty +tanky +whirl +limit +prosy +joker +sneed +asset +czars +lefts +ouzel +moats +nicad +tawer +softy +creme +lemel +modes +dopas +sopor +cleat +lomed +unica +talks +hutch +ulnas +abbed +azure +excel +hicks +eloge +onlay +dobie +krans +shiur +idees +puhas +oxims +eched +plims +terse +molds +corby +heave +rifts +lucre +puker +heady +sabre +frogs +cuing +sabes +frets +occam +abohm +tases +baffs +quais +black +mumms +gnats +roost +lites +linin +boars +sorgo +hists +apage +sural +kobos +churl +twill +puzel +tholi +greek +humpy +enjoy +genas +cotes +cohog +snary +kavas +poyou +fairy +peeve +levas +colby +qubit +terfs +cromb +cogue +zoppa +thens +herds +hests +poney +oaths +oxids +tirls +huers +diddy +rorie +radon +syned +apeak +smeke +wises +lehrs +direr +buffo +laced +peise +hertz +mitts +zaida +bloop +kydst +boygs +amies +zayin +gifts +rebar +viols +gluts +motza +begar +nomic +mummy +poofs +besat +hajes +jetes +niffy +rifle +frust +footy +hemal +copse +foids +bison +horst +tomos +mused +joled +rimae +spill +crops +gogga +parch +fades +hyrax +ewked +garum +flame +kutis +fovea +capos +fords +harns +fives +dwarf +gross +redia +colds +hogan +vlogs +soare +dowly +looky +spalt +tined +hills +trims +fraud +sorry +fritt +horal +mamas +gummy +cangs +torcs +kerne +feral +pharm +queys +looie +ofter +pygal +sites +geste +pooed +ragis +rapid +start +besom +justs +skugs +shogi +haoma +slurb +lezzy +datto +intra +amoks +spies +sukhs +rater +broil +waive +intil +vomit +breid +syrup +axons +curch +fauld +cupel +knish +drake +faena +dogey +shily +pyets +chavs +airth +genie +salop +zebub +twite +rayed +teats +culty +jures +snafu +mosey +kvell +cigar +zonks +yelps +ulnae +swole +rotor +buaze +wootz +trail +speal +odeum +mongs +milch +draft +harls +rugae +quoad +innit +kelpy +aphis +davit +manus +bobos +point +atrip +twirl +waspy +wynds +santo +gnarl +phyla +ponks +grrls +tiler +semie +clump +sidhe +naves +dooms +swath +daynt +donee +waffs +blebs +fouer +mpret +feese +seise +napas +civil +qualm +darre +hands +decaf +ramie +trial +folic +estop +peeks +bride +mines +stunk +spars +tabis +aiery +frugs +cycad +solan +dairy +bries +emule +comus +guaco +pager +ramen +louie +gowds +zaris +duars +voile +witty +kidel +pulka +ascon +skios +jarls +finos +noirs +stale +azole +womby +short +umiaq +exeat +heare +pined +leben +malus +vizir +cycle +seism +limma +zones +dells +north +drain +pouch +timon +caums +wakfs +sowce +doeks +caids +roily +shiel +tense +hakam +timer +wends +clips +serac +fagin +tummy +clint +honor +namer +ummas +boked +lobed +gluey +pards +scend +resod +bored +fleys +jinns +agita +pened +edits +biffo +blain +yabas +radar +yucky +veale +caboc +skelm +yabba +runts +waddy +lemme +merse +celli +fones +itchy +lower +towts +bleat +mekka +chibs +crump +meads +konbu +shmoe +jubas +bhats +prion +phene +dicey +nashi +halse +apish +rafts +queue +moved +currs +oboli +souks +boxes +gorps +zocco +ergot +wafts +ruins +saser +aghas +gyral +kulan +index +sapan +poake +tavas +madre +afald +bidon +tumps +asdic +combe +nubia +scowp +aider +mutis +taber +emoji +trets +spugs +dench +fight +fatly +sires +storm +kudos +scogs +hyphy +unhat +mille +mommy +misgo +piles +cadee +poops +youth +latke +bufos +drape +budas +tunes +clime +bayle +mails +lauan +yarto +skoal +ytost +guyed +luxed +kebob +mosed +sekts +pizza +hurts +eeven +freit +bingo +micra +rouls +riffs +afoot +egest +duddy +bombs +uprun +kebab +chowk +vinew +upbow +jokes +dagga +temse +sofas +godet +fuzil +yocks +shews +ditts +spork +felon +moron +gator +sneak +licks +hajis +gigot +cerci +tubby +whizz +spims +aloha +arval +leper +dribs +bubas +blets +rabid +began +drops +petti +pinko +sixmo +sigil +grypt +rolag +logia +adorn +hoagy +appui +rudie +vlies +tepee +cramp +byded +scaud +icing +warst +fubar +apace +poral +reifs +cavel +tolan +leash +sushi +avyze +pikul +gloss +unces +aging +thank +payor +tempt +calyx +bawks +malva +goads +miaul +trooz +pasty +luaus +rotan +mowed +preon +tocos +nasty +armet +porae +boose +amass +cuddy +gyron +hooly +safes +gaitt +bemud +queen +neddy +hodad +spray +steal +lerps +vizor +muset +riems +mofos +jawed +camps +slues +feers +faced +vespa +admen +riced +lifes +teene +foxie +stown +marri +jones +cists +plays +mouch +yonic +droll +lifts +antes +third +worth +proul +boned +arums +tutty +ovary +turme +moten +rubby +nazis +aurae +eusol +adsum +gazes +usure +leggo +chino +loper +dargs +owler +eilds +sting +terga +payer +nears +hemes +eupad +naker +kilts +roque +gilet +pandy +sills +fomes +coude +hypha +mooks +lubra +fangs +sulph +frill +bails +gooby +pelau +unmix +ajwan +maims +atuas +mingy +cacao +cored +wirer +boule +tians +birrs +mensh +riels +sound +dawts +toney +syver +spode +prore +micos +miler +deros +snick +labra +weest +vasty +jobed +aisle +rugby +cobby +ingan +dines +maneh +bauds +tunds +smear +coths +pasts +daffs +quipo +renin +slake +laers +dumbo +slove +booed +leish +molts +titre +sweed +hover +leavy +visor +antra +pogos +cycas +dawah +razee +poaka +bedad +stowp +ryots +ceili +panes +molto +coats +afrit +mneme +awdls +molly +hoosh +cyano +rekes +dosai +slaws +daals +speck +neigh +press +saury +toman +lists +birsy +smote +petar +brute +lisks +glazy +rears +teres +zoned +etens +armil +aroma +sloot +bardy +hecht +mobey +garbe +drawl +facet +entry +paean +nobly +mirvs +bisks +rebid +algid +gists +radge +ticks +ariot +daint +ester +pudus +reffo +parse +spool +siler +jades +aizle +trite +livid +ouphs +drily +sages +kebar +ratas +bykes +evets +upend +skank +tryst +fiest +laufs +marra +adage +sadly +clefs +conns +dazer +shore +estoc +naive +apays +atoms +trapt +angel +decko +gryde +dates +gimel +kanzu +kranz +payee +iodic +story +mushy +canon +paled +shogs +cerne +loser +seers +alaap +nided +snore +tizzy +henna +ribes +updos +gauze +helps +vuggs +teddy +veers +shaya +alkos +ranks +plica +regos +axils +kempt +eigne +lassi +waqfs +zizel +nooky +noses +volks +beard +taggy +beedi +senvy +mieve +teels +dulia +pores +slomo +boyla +haars +claes +pongs +tongs +musth +telex +bluid +walla +haick +deevs +smout +gaols +inane +arhat +reeve +frags +marms +pavin +earls +rents +bylaw +mayed +putid +other +crock +bouks +yolks +stalk +goods +durzi +yaups +capon +ammos +arked +unrig +elain +slept +giust +shies +caned +sheds +jooks +soums +incus +shads +skart +cyton +polar +blini +mesne +fumer +gauch +hadst +ahead +croci +muzak +topes +tegua +lauch +dinic +cills +eruvs +tween +joins +gypos +trank +birle +layup +cadis +pises +cabre +grace +trust +sojas +harpy +yukes +trews +opera +keema +undee +guild +entia +swoop +chias +boyed +armer +thymy +comms +aroba +curvy +quino +jiggy +payed +lifer +mirly +neist +betes +baghs +golps +paseo +mucky +coals +mohur +cabby +bolos +fikes +sorbs +chaya +feyly +divan +reuse +pisos +dempt +bowse +damns +laugh +waulk +mason +hexad +ruder +ohone +swish +hazed +gundy +jauks +gaths +gajos +scand +neive +ummah +skyte +comes +khazi +toyer +scrog +juror +buses +fazed +stott +seame +urvas +wards +vinal +gaspy +deash +oggin +hause +tweel +touse +groks +hwyls +plump +tafia +niffs +nodal +patus +laura +yoick +germs +tozed +laids +feare +ginks +saice +camel +flisk +swops +labia +tusks +auloi +halid +masse +mercs +kooks +ryals +slops +macks +doilt +meres +moult +wairs +hauld +strae +baddy +kraut +lemes +songs +grogs +comfy +topis +cares +bowls +atmos +losed +gadjo +warbs +laces +bepat +vulns +glime +lytta +pilaw +murls +anode +meers +casco +mulse +grabs +agues +burnt +dalle +wince +murly +hempy +olios +pubco +lardy +gulas +gilas +lungi +elans +haros +belly +rojis +carte +beery +clied +budos +soppy +sowed +winos +valis +jagra +peers +sally +ablet +plast +cyclo +crabs +taluk +jucos +metif +labda +lethe +mucks +kaiks +gamer +treed +manty +wheft +spitz +orval +grece +easts +swire +grout +septs +eaved +bhaji +duply +trams +lited +smuts +nexus +mures +bubba +teeny +aggie +nymph +elsin +piler +ympes +ewest +pisco +blays +guqin +tatty +enlit +dongs +antic +hyena +spear +skegg +ether +slobs +blees +steds +brawl +ohing +warts +seare +rider +alant +gleys +urged +gaids +vigas +coomy +mbira +aloin +rubin +rabis +copes +grots +kerky +reate +cytes +gelee +hobby +flams +anile +dawns +flors +raked +filar +stull +cleve +floss +trice +salut +chapt +palsy +doobs +scena +rials +agloo +serow +cocco +leses +mixes +meows +inter +cinct +wagyu +skogs +pingo +clary +toils +atopy +suers +tubar +saver +monas +nouns +mulie +leare +slane +prior +elfin +refel +botte +bumph +rabbi +peepe +mural +blubs +foist +yuans +derny +stede +quoif +topaz +cunit +wined +sharp +kakis +mahwa +aleck +botts +saunt +snort +clipt +alods +jibbs +ayahs +eject +arses +yonis +inert +blate +skies +sooty +found +evhoe +gilly +dummy +fixes +runic +poods +swain +rosti +shame +feats +amnio +himbo +enate +eards +luged +abets +venae +kings +tuple +bubus +aspis +turrs +exode +agent +gynae +syren +write +ratus +genip +alews +glued +yourt +outre +loupe +twice +kabab +under +remex +rudes +dicta +shaul +scabs +cobbs +seton +wooly +gazoo +jirds +skill +jaups +etape +cells +gunny +rakis +alien +ulnad +dauby +tolts +pious +walds +servo +movie +livre +ooped +ergon +dosha +toque +mochy +quilt +belle +uhuru +yappy +loses +neaps +garbo +berks +javas +shtik +algae +prank +booms +embar +liars +wikis +covet +dashy +noule +cosie +kybos +trock +fisks +baccy +trape +homas +turds +voulu +pylon +yearn +kacha +amend +rutty +fluke +beath +ricey +grovy +sengi +leese +sists +purls +adred +krill +rorty +shams +mells +murky +sasse +gombo +slims +shays +phial +knags +fudge +loner +shoji +degas +hocus +loche +still +bunks +slang +hodja +breme +birks +perdu +waste +dusky +snarf +chivy +zezes +house +zoris +cheep +freer +flung +buist +iring +moops +blurs +skink +ensky +cadre +babas +felts +culls +avers +doner +cinqs +meany +koban +apism +mopey +embow +mings +decks +skyfs +chelp +potsy +pudsy +hiant +felch +sycee +enfix +ganof +muxed +roper +plied +lants +hanap +pipes +assam +rayon +dogma +visas +yesty +noels +beted +gauzy +meshy +terce +augur +moist +murri +dater +foyer +yaird +stiff +erect +unrid +seder +delfs +assot +fabby +nixed +mered +amuck +cease +oleos +boggy +quair +beady +vaded +combs +wodge +baels +reran +chain +baffy +cooks +souce +sedge +zouks +whams +phots +oulks +tasks +chich +guyle +piste +couta +palsa +japed +caman +savoy +inrun +gippo +kooky +pudic +laval +molla +crith +frump +orris +dodge +nexts +wrest +chile +woose +yente +lidos +gonef +sluff +googs +courd +jokey +hires +alans +demob +whilk +stews +spice +grows +aalii +carob +liefs +motto +smell +swads +yokes +varec +twerp +kokum +recal +roker +adown +lento +dimly +volae +tocky +thoro +rogue +sedan +shins +orfes +sammy +winch +dowle +oboes +wisps +foals +ended +voars +rance +basta +fleas +bosie +monie +favas +wasts +wilja +briar +petre +kugel +snare +suave +glode +safer +testa +ragas +chins +dozen +blues +razed +sates +basal +gleba +hiked +gofer +cymol +fisty +zatis +fests +pinup +muddy +scail +idler +hafiz +surfy +comic +chars +slope +jouks +diane +dorrs +oxter +field +ictus +rathe +kusso +decor +marks +incog +loris +paolo +tehrs +train +smeik +flues +adoze +gemma +wried +towse +plats +chape +hapus +sneck +chirm +tuktu +recce +chado +fonly +burly +anana +rumor +urial +files +grund +paste +areal +lemma +aided +preen +rondo +rosin +lyses +hejra +ledge +heths +masks +stole +using +plumy +roved +afire +laith +tumpy +belay +baler +vozhd +sithe +koura +navel +taels +flock +duxes +pommy +geist +ginch +quake +rewax +pawky +cures +negus +braxy +sanko +below +betel +copay +arrow +wield +sozin +knees +bench +ancho +reorg +sowth +spaed +absit +regal +fells +lunge +riled +stirp +hucks +siroc +hymen +crisp +rumal +erevs +hatch +haets +clows +wordy +umami +royne +hoved +genic +senas +thana +padis +luach +pwned +diazo +thews +culch +osmol +tenny +hosed +smock +leafs +thrae +ursid +beray +umpie +nonet +caged +tombs +amido +bliss +drest +margs +euros +algin +hangi +table +holes +latah +reans +upsey +sewin +welch +fordo +spate +outta +yechy +aphid +phizz +posho +snail +abele +bortz +oller +blaes +doums +huzza +sylva +oches +whirs +yexes +aptly +campi +speos +fuses +gorge +bekah +winey +finks +gurls +faker +horme +aural +akees +donut +siped +carrs +quirk +bravo +swami +crees +intis +waxes +cades +flane +cooer +balmy +cavil +solar +shark +nonis +prowl +ceria +agaze +quats +chons +raced +bunje +resin +prang +risks +cloys +snoek +uncoy +skegs +parve +eliad +skyed +yeans +brusk +crepe +burke +glean +orgic +buras +spued +clavi +smoor +ebony +cafes +helos +toshy +howes +robin +berth +volti +handy +serge +fayed +forex +surah +sibbs +reify +smith +stead +ochry +purse +scrob +ossia +missa +rises +faver +mutes +seven +undug +libri +viler +avail +knout +miked +fugue +unais +aspro +fyrds +skunk +soyas +tikas +baloo +galvo +nawab +kirns +grana +kissy +hopes +galea +manul +denim +benis +caped +folly +daych +sista +zooea +sutra +hyoid +unsod +ogler +samas +gauje +green +embus +urubu +wazoo +sawed +escar +unlaw +roods +debur +leant +wetas +fubby +mozos +butty +neral +glent +stars +nanos +hyper +emyds +ponga +picra +saids +ileac +arias +toyos +cates +roots +manes +leggy +monal +regar +prick +sowls +moves +boffs +ciggy +gopik +nutsy +bawdy +cetes +puris +liras +whits +ikats +unwit +salix +tetes +belon +nomad +chits +lithe +heaps +crapy +yowie +sudsy +board +sorns +hadal +snush +wheal +yarrs +poove +kayos +taker +sidle +pukey +babel +resty +cooky +feres +tents +almeh +senor +beads +rhody +scaup +phyle +targa +confs +burps +rived +bumpy +etnas +unlet +bialy +gypsy +cymar +mores +fumet +gamic +mizzy +telco +spoon +abbot +snaps +gaddi +orzos +waned +forum +infos +kedgy +frees +imams +kepis +spire +unarm +flash +latte +fonds +trigo +acker +dazed +roate +skeet +ephor +sacks +mezze +pareo +omens +repel +stoup +naggy +dawed +gairs +lazos +huggy +tonne +poddy +sagas +slart +skool +pedro +stoat +scalp +treys +ouija +terek +chirl +spawn +cajun +bunco +sanes +baize +bauks +warns +haugh +gybed +cysts +begum +apgar +woads +boons +bajri +hinds +whids +temes +herns +clade +slush +blist +filmy +creak +braky +privy +boing +abore +agile +coeds +casks +ramee +kilns +easer +vault +cocos +jills +dites +oppos +fetas +pinto +holon +duals +achar +cajon +lisle +sherd +maxes +biogs +scurf +cutes +growl +myopy +filly +filum +vaped +romeo +spawl +grate +knell +orbit +scars +swopt +bevvy +dalts +truss +aroha +candy +sheep +blear +lathy +peaze +daube +ginzo +sairs +lotto +sakia +specs +etuis +zizit +kukus +aunes +raged +kvass +bonks +pater +vegie +micas +belah +going +papaw +tonka +rooms +matey +zexes +parra +brios +scuft +grist +cowls +oread +palpi +minty +wreck +onned +aroid +vairs +scall +locks +ratha +sypes +touzy +roues +sexts +grimy +farms +mages +troop +sybbe +kalam +howdy +nicht +ayrie +strap +sigla +fraim +daunt +poboy +germy +roule +vouge +whisk +tippy +roums +bouts +carer +equal +hinky +hajji +topoi +picky +botas +verry +maids +prese +lusks +naunt +sents +moder +trior +quill +grime +chums +hoxed +skene +lowly +merls +debes +apart +dizen +oleic +facia +spang +borks +oribi +smeek +flory +temed +terms +vrows +boils +bogan +blins +zygon +caaed +syces +yards +elope +wenny +quite +gazed +pones +nabes +silds +pavan +havoc +halva +harms +unked +frorn +unset +tahas +plait +revie +dunny +daraf +marid +agree +fence +erhus +cunei +fleck +wages +artsy +bocci +cauls +centu +ameer +navew +gyred +abrim +goths +wains +comte +curie +kiefs +fecal +fruit +neems +yills +feuds +oidia +izars +hunky +pumps +nicks +docht +ennog +darts +walis +meter +hoyle +again +downs +rheme +tapen +feued +jimpy +cubit +arefy +fucus +shoed +opter +napoo +juicy +kandy +ravin +tushy +nanas +silen +poots +kurus +nomos +lymes +dikey +loved +aioli +bubbe +pioye +roams +pesto +timbo +carns +optic +skeer +zappy +tarsi +imari +paver +pleat +ibrik +chuse +fleam +onkus +blond +corbe +owche +feart +gulls +izard +haole +dwelt +pampa +japer +mazer +pucks +playa +orgue +debud +doomy +chays +doubt +wongi +virga +malms +limbi +suite +crura +raile +crips +porno +after +kabob +apsos +rasta +beaut +hutia +serfs +roguy +tazze +unfit +urman +arame +paras +nosed +daurs +zygal +fixed +tenor +dolts +saner +begun +azide +abaca +facta +rabic +kofta +macro +hefte +whigs +viral +waits +areae +throe +flabs +fenks +jurat +scraw +sowle +dried +stoop +agate +hable +douse +gobbi +droke +situs +burgs +tenth +thymi +doter +morph +coste +zoppo +broth +siker +dwang +quipu +oxlip +stain +botel +zooey +prigs +hoise +stipe +betta +nemas +cursi +aviso +graft +bobak +taiko +braks +aduki +serve +ketes +daily +doxed +lurry +shist +smugs +kembo +gimme +medii +wisha +enter +rynds +fonda +jesus +pains +butoh +tegus +adits +loued +ledum +oakum +roupy +flawn +drole +gleed +cyber +ledes +wries +hours +deawy +laigh +soily +ached +rocky +volet +segol +owled +avine +bodle +knelt +wroke +borde +foley +swobs +mould +rimes +punks +smaak +haint +lysin +afear +jeers +vitex +polly +jeels +firer +keros +peens +glebe +relic +cubeb +elogy +abamp +lucid +clart +shmek +wadts +gulpy +rooky +balun +onery +conia +brede +lades +tifts +kayak +scorn +bunas +skits +salto +divos +torsi +hairy +azlon +girls +marly +mutha +adder +vatic +gular +roked +vehme +arsis +leady +rigol +borak +ionic +zuppa +snibs +latus +tammy +smart +manor +llano +haggs +warty +blocs +nugae +segni +oracy +scurs +reamy +blade +mumps +brier +retem +libra +laree +begem +apted +acute +hefts +brent +wicky +primi +asura +grrrl +titer +herye +sheen +fusty +cnida +agoge +lowne +sicht +odahs +panne +gibus +gases +neese +bobas +hippo +steem +lotos +sowar +gamba +ashet +parts +final +gemot +deman +snake +urnal +shank +eaves +dancy +albas +pulks +finis +snirt +punas +paven +nohow +joust +rosts +cruds +piend +salsa +lingy +girrs +trade +stony +makos +poori +stats +tenty +faffy +jager +hoses +noise +dutch +mamma +porge +yirds +paper +raias +howls +spurs +roans +flota +ungod +tweet +fyces +krait +yamen +oxeye +fique +gadso +theft +bewet +fease +patte +tacts +craft +moses +canal +oscar +wails +allay +pyots +rhyme +await +heled +rimed +prong +manto +axone +gassy +shape +repot +trull +wipes +wedel +jerks +breem +swank +wahoo +tilak +siens +doily +sours +yomim +yowes +glugs +inion +malas +spoot +elite +coate +palps +pried +hylic +aedes +dinna +slipe +molar +paspy +reame +jafas +recti +tonga +hoars +dandy +crack +clamp +beals +hamza +imine +steen +soths +spial +talak +dryly +atria +relit +teems +elmen +prana +taata +mesic +droil +ylems +seine +wimps +front +trait +mimeo +login +borts +merer +larky +zoaea +sorra +galut +gurdy +shaps +quare +rusma +chirt +lacer +vogie +bourd +shrub +filed +hoiks +uplit +rozet +duras +koses +rawin +yucas +gully +avize +stile +calos +amirs +merks +rouse +sweat +epopt +eniac +sizel +pedis +argil +rungs +repos +soled +doffs +lycra +pyins +scapa +roomy +woons +aguti +cumin +whomp +roils +vezir +bokeh +krais +sirih +doabs +gayly +colza +boyar +wyted +lepid +emure +phang +curns +tices +glial +jeely +rupia +rimus +strim +brink +flics +thuya +mulct +potin +almud +wilga +liege +tulle +slips +campo +rusty +maser +murre +perea +jives +crame +yewen +ragus +oared +sieur +taxus +codec +troke +sager +moups +ulvas +lense +lasso +china +tulip +damme +corso +agave +pyoid +toffy +cadie +weeke +slubs +alder +thrum +kraal +maron +gloat +aimed +buffe +pitta +bolts +lassy +dowar +india +mawks +weeks +token +purrs +yippy +marah +apsis +axled +shook +heald +biers +hijab +nifes +derms +iambi +hound +jobes +byrls +idyll +skyer +odyls +idols +torta +conin +smowt +kalis +puces +eerie +uneth +kerve +chaws +writs +baulk +badge +flows +irons +shady +panto +conne +chaps +perse +tared +brank +lover +masty +vifda +plouk +gambo +scary +eikon +shewn +ealed +liger +palis +demoi +morne +caxon +gooly +heedy +paddy +newel +hedge +blimp +garni +toits +ferly +forel +kumys +twoer +mafic +weedy +demes +virtu +fujis +liter +geode +druxy +recto +puffa +paths +bobac +fared +noisy +memos +fitna +tondi +baned +rupee +noble +gadge +debit +faiks +hasks +cuzes +showy +decos +loopy +small +ymolt +gippy +blood +rhino +fugie +poulp +drips +chais +crown +booky +comer +abuse +widen +menge +odism +barra +massy +sonde +modii +gatch +berms +lushy +yarns +hones +shchi +piper +crusy +hurly +banco +staid +metol +rules +ollav +spins +hexed +gills +amain +sooey +scamp +groin +codex +estro +yelks +saheb +hissy +mikes +rotes +varus +doits +asses +sheaf +owrie +goosy +aread +tread +brith +yogin +yobby +konks +sayid +tains +carbs +knubs +ramps +ictal +skiff +rines +easle +seifs +maker +deems +begot +visto +liber +super +moues +piths +tramp +dital +lobes +caner +flops +zills +eggar +civvy +noted +calmy +colog +piccy +fouet +chads +tarts +teras +hayer +lawed +mango +jeats +newer +niter +pacos +brize +potoo +evens +among +aurum +fayre +patin +jubes +gonch +sewar +gamin +alvar +moers +frowy +risps +ravey +noria +dazes +titch +myope +shalm +oncus +morel +types +wheat +bothy +rubel +baken +bliny +tarps +sleek +mixte +courb +suede +topek +lilac +resit +nubby +eater +pomos +biota +might +boysy +angas +rozit +vises +pries +egret +weets +lurch +piton +craze +vaute +kazoo +goras +fluff +nerks +serrs +skive +rigor +chiro +dolls +imago +myall +kaons +thigh +nerds +dinos +hoser +zloty +waked +weird +wooer +getup +amnic +trays +drusy +clomp +agrin +quash +sewan +kythe +messy +fusil +dizzy +dadah +tacos +saucy +quale +gourd +venue +skran +sidas +cones +vacua +gwine +pours +apses +lurve +pawns +carle +proms +xerus +daris +intro +rukhs +dives +moble +cadge +adult +jerky +yarfa +shoot +zanja +soops +golly +wauls +khats +fadge +lated +ydrad +shaly +yikes +derat +inbox +ninny +zeins +redos +dwalm +kanae +acold +locus +faith +drank +vivda +vired +velar +cered +tiled +mecca +runny +dixie +fiber +apple +recap +judgy +sango +ileum +sitka +fuzed +ethyl +wafer +napes +hohed +boner +breds +cloak +selah +raggs +tubas +scans +monic +pilea +lenti +coves +lairs +brins +vined +mikva +dyads +goaty +capes +faked +droit +oozed +borgo +seirs +swift +milks +deity +taiga +chirp +agila +terai +greys +nerdy +moira +scuta +snubs +vague +cedes +hared +viced +stems +woods +leers +banes +talky +heeze +rello +limax +repay +durrs +kevil +cutup +chard +tolus +glees +aiver +pical +redes +spake +sease +soces +leats +fados +brims +bundt +joual +paces +drove +farro +bibes +gighe +whort +bulgy +crome +miltz +largo +hards +globi +mites +raree +crave +padle +serai +obeys +emong +ember +petty +accas +larum +haram +horns +ombre +hoxes +ourie +laxly +triol +prems +plash +ryked +frass +diked +revue +dhols +inlay +nying +dhals +tries +firry +perry +abray +shola +obols +cruft +frigs +boite +deray +merle +sered +cooch +stirk +miasm +blown +calks +gobby +zanza +choco +dayan +scaff +swags +laldy +souse +fouth +bough +elect +hooks +araba +tyred +shrew +steed +iches +rowdy +sider +blobs +tenia +lazar +clams +uhlan +sythe +brawn +bebop +orach +wryer +gecks +musty +setal +arere +fides +dowps +klang +taxis +dinks +older +highs +aloof +silty +donna +maiko +imbar +whaup +goles +pikey +binds +touts +comae +quail +nosey +routh +twals +buhrs +adyta +imshi +hakim +qursh +lycea +thees +igged +pryer +aping +snigs +pilum +haded +basen +dodgy +unity +tuber +yoker +coqui +urase +garth +abode +feint +hakes +boart +owner +binks +biros +speel +liman +yapps +plebs +expat +liane +gotta +khuds +hives +salic +brast +foule +heigh +annul +bavin +ceaze +boets +ruggy +solum +pages +cawed +fella +shall +subby +tepoy +embay +heuch +trugs +wisht +peace +brick +moyas +slaid +reddy +ponts +pilis +puree +jarps +bunny +vealy +sabin +jabot +zilas +sheas +thous +aidoi +bruin +pomes +rotal +lapis +organ +horks +refed +sayer +volte +ricks +fills +anker +hares +gaits +frost +merch +ariki +praos +rotis +devon +weeny +dufus +lined +knuts +fondu +toped +waved +rebut +baggy +pause +podgy +drows +hench +meffs +renay +agria +poupe +years +creed +bludy +cusps +niqab +indue +peart +gowns +grain +mouls +rores +zimbi +musks +fling +leads +clued +caphs +knawe +swill +hoors +lapel +osmic +sorbo +axles +kapok +gesso +tibia +scuff +grews +dingy +bedew +tondo +rizas +mungs +freed +stipa +sweys +flong +webby +murid +melba +syrah +mezzo +blank +obeli +goofy +zacks +nukes +realm +giron +gripe +picul +whish +knack +would +pyxes +penis +segue +toyed +cornu +cagot +lirot +sport +minor +brood +choke +ronts +scald +mashy +frize +jiber +poked +shiva +horah +talcy +torot +cowry +atoks +besee +tarty +doper +scrap +wacko +laths +denis +draps +shako +nanna +griff +brust +lalls +erupt +derry +tajes +animi +serks +ranis +quirt +ready +parrs +pinon +lapse +wager +heirs +turms +jacky +ouzos +ayins +cloke +snows +pease +invar +ungag +motty +arars +radii +shown +noily +splog +flump +midge +piety +nudge +piers +bound +karoo +calla +cilia +spans +netts +gowls +hokku +goris +stood +cours +kogal +tryps +locie +patly +scows +enemy +sangh +kbars +thuja +maggs +stupa +evert +plows +grize +withs +quods +yager +meets +hafts +keyed +churn +raupo +nizam +apayd +nikah +fists +brake +fluty +aecia +belts +kindy +ghaut +atoke +blitz +shoos +birse +flack +dykon +lores +spain +covin +oobit +hurls +minim +begad +shuck +basse +arena +rugal +piezo +muggy +hiems +bason +naras +galah +chair +cryer +moody +amide +polis +heame +yealm +fucks +caird +smarm +zazen +clone +yetis +crick +crass +fundi +vleis +enzym +nixie +rival +arbor +jirga +faces +ankhs +sabra +avise +tousy +beefs +lisps +opine +phase +gites +tiyin +gawsy +jomos +wryly +doggo +lyams +murra +kotos +adays +gable +gulps +worst +means +moola +thars +fezzy +roped +cawks +yirth +argon +curst +oomph +wonks +coapt +bawds +geese +bedes +dulse +signa +pilot +drugs +dault +arise +glias +grads +aflaj +coral +mimer +couch +skuas +wyled +pecky +abler +spina +ploys +sumac +sling +sagos +foxes +fiefs +lunks +skell +fiere +charm +amlas +fanga +jarks +fishy +piony +heben +saros +dited +roosa +cibol +pokey +yrapt +rappe +pacer +lipes +geres +shojo +dices +dwell +spink +hevea +lying +drunk +graip +build +scute +fames +blame +gucky +retia +dangs +fears +diffs +wadds +sited +psyop +indri +wooed +plier +fuels +slojd +magma +tapas +loups +golds +fural +sibyl +pukes +taigs +eiked +cokes +alfas +toise +chick +force +trugo +genoa +sprue +gnars +skein +aweto +modge +cross +alums +sunny +phare +surat +cutch +cower +graal +inker +donor +faros +baser +thuds +didie +muggs +ditto +khets +ribas +cowks +allow +slunk +vanda +ponzu +neals +goons +palls +rejig +tuque +ravel +cutty +towny +vials +knurl +notal +sinds +bahus +fraus +retry +tugra +crocs +halms +manas +ceres +dishy +calps +binge +cosec +hovea +tenet +gumps +phuts +keyer +fudgy +raird +whorl +offal +grump +noint +mulla +creek +lough +docus +mincy +colly +techy +blaze +sofar +dynes +tests +clegs +elpee +borms +nulla +haute +funny +satay +grade +yelms +atony +dools +gutta +paans +kills +harim +mossy +pelts +karst +raven +poxes +plods +grief +nauch +ozzie +boink +tufty +silva +vital +camis +dated +erses +kawas +foams +xenic +thegn +lacks +antsy +eclat +riots +rumps +snood +coxed +scoog +roted +dosas +barny +agons +pinky +ichor +ramis +crios +farle +trist +malis +lepta +yenta +sangs +nerol +whens +kotch +vroom +knows +gyppy +lumps +prent +orles +elude +owies +tenno +civie +fully +vivas +conic +coffs +quoin +therm +parer +gloze +marts +maces +oonts +toing +timid +oncer +hawse +miser +promo +pally +daled +tawie +cacas +early +stags +mesto +gorse +perks +trace +tails +pawer +punto +amrit +saics +jambu +emyde +elemi +hecks +delts +galas +grone +dough +moral +faint +rases +fouds +avast +gelts +gaffs +dicky +serin +witch +exits +doura +perps +adobe +inset +stopt +dials +tamis +fehme +amowt +bluey +pogge +apert +glims +nebek +meths +neifs +pohed +miter +grith +furrs +bombo +think +blude +trats +jolly +meved +since +wanze +reens +algum +pipis +champ +fresh +mebos +karns +pirns +pekin +woker +barca +letup +azine +snowk +fiscs +mangy +titan +sesey +hefty +styli +rindy +moots +crony +bubal +clean +oxime +mento +hedgy +vimen +tacan +sewer +spend +jetty +tuxes +swart +karsy +seamy +drays +empty +homme +duvet +ergos +vughy +fuddy +honds +makis +quaky +whelp +sugos +britt +groan +poley +seats +bevor +froth +drubs +boult +weber +glady +biont +botes +danio +fleer +swipe +kojis +legal +gived +eased +gibes +dorps +minds +wizen +dryer +dotal +copen +brond +sodom +logan +clops +hansa +shags +saute +wecht +scrae +tarot +hilch +laics +fiers +drack +pooch +vills +swabs +marsh +jongs +snash +chark +coaly +kiack +doucs +space +mynah +pulls +avoid +etwee +wrote +stall +scams +lunes +jomon +civet +clapt +blink +koras +irked +manic +miner +dopey +basic +pills +plasm +unite +tachs +waite +geyer +soral +crays +decry +alula +bunia +obang +close +brill +royst +duros +mache +drone +clots +nache +brigs +penny +bungs +lacey +tatou +nonas +cholo +refer +swerf +bardo +kirby +bints +layer +plumb +frack +brisk +butut +stond +alias +alike +hazer +kaiak +garre +tabla +rhyne +sudor +tusky +kench +wawls +cedis +cohen +loads +ruddy +maxed +nummy +sexes +yoops +repeg +agist +adopt +learn +lulus +meats +plong +grass +pilch +untax +chocs +speed +tamal +saist +gyoza +hallo +aredd +styte +worse +honed +weirs +roral +kyars +adrad +mamee +codes +venus +heads +zingy +baths +poler +droog +tinny +ukase +grigs +choon +hooty +ditch +emacs +audit +mungo +tings +speer +fiked +trest +dewan +pouts +uteri +pitas +faffs +goier +senes +tutor +gyved +cloff +aside +gulph +pinta +dorky +gamed +shoyu +alkie +chola +clunk +olein +flans +bawls +dowed +cards +downy +trois +pinch +kilim +janty +graph +armor +decad +timed +trine +daker +spall +bream +marka +veena +jiffs +corno +lotta +clipe +sampi +nitid +waldo +towzy +dados +linac +cauri +logic +store +draff +beths +pursy +lotsa +stern +wider +taxon +pleon +tomes +stell +texes +jenny +mirks +stulm +banak +demit +hosey +syboe +flaps +zibet +afara +suids +flank +chest +pixes +spyre +moras +raine +allel +theek +ocker +solde +villa +jasey +screw +judas +haafs +maqui +vauts +cupid +ngoma +chock +rudds +abate +wojus +croon +dread +ajuga +manky +nutso +pardi +ayres +gents +debar +ruses +lamps +nancy +haulm +darks +bilks +hythe +stage +dikes +mondo +bairn +totty +brame +swans +uraei +zests +wirra +repin +icier +overt +calid +loons +added +coyau +goory +darer +conch +dacha +musos +scowl +yodle +madly +lathe +surfs +deism +dered +skrik +yupon +wally +feoff +zoeae +fetus +niger +ensew +debby +kurre +mokis +ejido +swell +clang +carat +abysm +gamas +tires +hiker +whelm +cooly +quate +feast +choof +cushy +jacks +tilts +mamie +forms +livor +veles +prahu +sappy +sarin +wamed +fuzee +udons +lupus +dured +fibro +clads +epics +ideas +colls +cants +coxib +flirt +sleds +loofs +kelty +snogs +zoner +slebs +chizz +subha +verge +honan +pygmy +bider +acing +tolar +posed +yates +serre +rinds +compt +hoven +covey +mambo +meane +crunk +eisel +keeks +rubes +moled +floor +carls +dover +pigmy +quiet +chimp +circa +schwa +names +amuse +nadir +hents +virge +thong +zebus +tinct +petit +solah +droob +coirs +swoln +ngaio +vamps +rivel +shirt +trins +acmic +fyked +marae +cogie +rhies +zeros +ardor +slice +prism +scone +yowls +which +sayon +tymps +feods +acres +dries +teloi +lurgi +busti +pikau +cases +tinea +trump +reney +nempt +yawey +assez +bunds +minis +wites +fugle +cumec +airts +cured +beamy +naled +gleet +ticky +gomer +ahind +thowl +sculs +gride +plaid +tares +torte +dirty +xoana +cubby +verbs +leuch +hales +heyed +honks +romps +ashed +ferry +globy +radio +rants +helio +brise +briki +byres +flava +aware +hopak +dooly +boppy +relax +clung +shyer +marse +shote +recta +gliff +racon +fuffs +forme +posse +lands +genro +eevns +mirth +nidor +ruers +prays +quads +abram +puffs +bourg +idiot +axing +stive +apnea +exalt +platy +habus +pushy +flogs +ippon +typic +nicol +ruffe +fumed +xenon +alowe +sprew +dewax +tumor +astir +chawk +almas +scart +lutea +laika +sarks +lowts +heavy +tasty +ample +hairs +koppa +lures +saint +aglee +cocci +hiply +thack +kutas +souls +jugal +abrin +sired +traps +preve +rowme +stick +azurn +tyers +lysed +caple +bourn +tophi +prier +kangs +docks +zonae +almah +ecads +navar +datal +ysame +punji +jukus +gurus +nails +rowen +units +taira +porny +girth +gadid +raxes +chynd +oxide +hippy +eyrie +globs +gongs +betas +fated +kawed +pulli +auxin +bombe +birds +apoop +soddy +capot +extol +quern +feign +pagod +tying +pipas +abhor +lagan +sooky +evict +hayle +sorus +trick +elbow +lieve +skimp +begat +extra +dealt +stays +farcy +cooze +fleek +queme +drums +bumfs +lochs +expos +huias +hogen +mythi +quass +idled +edges +gates +raped +lavas +caese +porgy +immit +taunt +axmen +lippy +copra +whack +skews +trefa +jalop +filch +tripy +miffs +duomi +narky +honey +fugio +oints +yuzus +proud +chiks +aired +moner +flown +sends +buroo +sumph +taler +amber +cakes +kerry +untin +tetri +brosy +probe +amped +newed +niece +croak +rezes +skody +mixed +parvo +pseud +doona +gipon +vuggy +fizzy +tweed +krona +cowal +rekey +jelly +fares +wanle +stedd +viver +thagi +groom +humus +wrens +leany +melty +pants +knaps +odist +unkid +tribe +woful +ivied +codas +rohes +kievs +snipe +tuans +ceorl +month +halma +limpa +traik +ainee +darzi +fixit +orixa +array +vying +appay +arroz +ulama +slily +depth +broch +durst +ident +tolls +guard +abmho +goels +gynos +spays +quags +circs +abuna +peaks +smirk +bezel +vexed +wowed +guano +valid +crews +spean +leave +willy +whoso +natch +imido +bests +bluds +plane +aggri +curli +coxal +angst +speir +loran +ansae +spane +kalif +jambo +throw +dirge +bandy +toles +coifs +nitre +stere +baled +cense +bloat +expel +rinse +crime +absey +welts +ungum +glair +squaw +count +hinny +fours +xylol +voter +agene +renew +mochi +spoof +buhls +homer +cuish +gored +simba +crout +booay +poesy +ghost +veals +rings +musts +roups +rumbo +guava +cided +indie +sarky +isles +atone +aceta +valve +range +naris +deice +shuns +scums +trods +dorts +usurp +alway +chogs +syncs +hails +poind +sense +lawin +yamun +nould +wasms +polka +bwana +rudis +clept +salty +teaed +kente +adeem +alate +khaph +stomp +erose +widow +dsobo +monte +anigh +sceat +gibli +naffs +clats +devot +reata +hules +gusli +plaza +stimy +coled +swats +notch +joint +usnea +cruck +hiree +adbot +viola +onset +casts +eared +amiss +imbed +hurst +siled +hints +sepoy +kombu +humic +viner +thorn +kasme +menta +gusla +gilts +loops +palki +trads +upsee +jibes +auris +parle +kiter +elder +looed +price +synod +omovs +hided +goopy +sorts +wifey +reast +chela +armed +carom +seepy +spree +samek +nomoi +mocha +spiel +regma +houfs +feyed +belee +lolls +eskar +snaky +dream +rangs +envoy +gipsy +blart +gater +koker +glume +perce +benny +asway +cinch +balms +stean +palet +cunts +cides +filks +yacks +ceils +thing +wurst +ulzie +dower +tythe +moove +spats +loral +frays +flaks +basis +wonga +blert +wexed +sorda +heast +umphs +porer +cerge +pshaw +lakin +karks +kulas +needy +quean +barky +jewel +glaze +grisy +clout +ninth +paris +canny +bonne +jocos +vaned +netop +tichy +cooey +loped +until +boord +mooch +dying +banns +caked +loges +razes +comet +aygre +assai +aking +bewig +egads +poort +gobis +suber +gigue +weals +alone +bezzy +orant +kyack +owlet +sains +woody +whows +sewel +pinks +haply +roopy +tuath +nurdy +nazes +lynes +horsy +aback +picas +glibs +watts +virid +kauru +morts +pudor +toppy +trued +sloom +games +lazzi +macer +dwale +abase +solos +bajra +tigon +peony +scope +thebe +misch +zarfs +recut +facts +usage +waker +chank +perst +teuch +aways +buoys +sweet +coped +rolls +hance +daisy +reels +yirks +blaer +vales +cheka +sente +tools +backs +clays +vouch +unpen +lotte +soapy +glans +biled +pilao +xebec +marls +babes +gulet +stare +noggs +berme +fires +korat +unlit +corms +sumis +frape +scrod +smalm +micro +leaze +mints +virus +lairy +lunas +cymae +nduja +bromo +mires +teaks +quays +parka +slack +strow +agers +bogue +books +joist +jumby +dunno +thiol +nanua +smurs +torch +keirs +bland +grise +works +resow +meint +yoked +goyim +marle +todde +pesos +mawky +zobus +shelf +ouens +ovens +teiid +anima +minae +derns +fluor +wiggy +seron +kanas +booze +tinge +foggy +aerie +artis +psion +bafts +disco +bolds +kenos +festy +marge +natal +nirly +dense +lares +spail +vogue +redid +gigas +owned +ixora +cheth +funds +docos +zebec +chips +quist +biffs +feels +elchi +yitie +guise +toled +amour +moper +modus +simps +noxes +alifs +waded +koala +chara +arson +stake +rusts +ngati +slaty +strum +khaya +hooch +harps +wings +flask +sprad +drear +lilos +tatie +tales +rebop +taxer +ranas +skims +fiars +sizes +zupan +minge +pukas +grouf +senza +humph +derro +yawns +takis +beans +corns +riles +longs +gamma +winze +stoke +keets +marvy +wayed +byked +pansy +padma +dudes +gares +blits +taals +dared +loofa +lodes +genty +attap +lotas +podge +lours +cubes +stobs +epoch +arcos +omits +purda +drink +etyma +alaps +olive +pubis +reins +chana +besti +burrs +dooks +cable +schul +ketol +abune +frizz +gelid +sonly +gonzo +vitta +adept +tines +zaman +unfix +liens +bozos +thali +linns +tauon +toddy +canty +socko +pervo +thein +sneds +kyles +jaggs +swarf +towsy +pucan +phono +aides +heles +cines +holds +reeds +blite +piney +cento +caret +jiver +urger +syker +fiery +ethne +sures +nobby +saags +ridge +brads +meous +dhole +poovy +varan +wicca +jembe +howfs +prims +howks +bodes +awoke +holed +saman +dukka +thyme +doled +goeth +gormy +unaus +plugs +caste +flier +ludic +vaunt +mover +batta +crepy +brash +ditsy +vatus +reses +aland +cahow +sargo +craal +colin +gesse +tacky +neemb +scrat +apron +slabs +loams +myrrh +silks +twiny +ardri +scree +haled +guans +agued +cwtch +targe +peags +louse +dhuti +zerks +saves +weils +jutes +plans +bussu +berko +wents +gnarr +tolyl +acerb +minos +boomy +fubsy +jeffs +toile +acock +abyes +argue +gusts +mazed +otaku +chaco +autos +align +goral +tahrs +jasps +amino +panga +dully +indew +apaid +gynny +cyder +brows +ontic +tewel +octet +okapi +brags +stich +slate +pirog +vives +pelfs +zebra +snift +pubic +sames +lurks +tanto +mucid +derth +drier +marcs +impis +bines +ninja +bouse +myths +suete +orcin +enure +bilby +straw +grapy +hawms +tally +refly +pyros +pepsi +gaily +stuck +grant +paves +horis +imped +naevi +troat +plews +bijou +doddy +tilth +fungo +ponty +spuds +towns +hyleg +knoll +skate +toffs +abuzz +humfs +teers +bogie +blots +essay +immix +comma +diary +prexy +mills +dexie +teals +crimp +drill +axite +strag +larva +cozen +semis +sonsy +winge +chout +wised +loves +genus +fjeld +melds +yules +rices +fumes +never +ticca +ascus +stonk +remet +grope +paced +nowls +noter +tacho +gabba +wadis +gazon +dolor +chore +ament +akita +wrate +needs +meeds +jupes +sines +berry +maned +shule +cages +wanky +awash +ankle +borel +mussy +piano +chuts +savor +ticed +unapt +phlox +humid +gawky +fluid +recur +hejab +poppy +haiku +resat +plunk +usque +pawks +baste +objet +bases +wicks +unzip +zedas +steek +roofs +satai +twain +arris +polts +gaunt +gloam +ducts +ables +boors +spule +wroot +denet +sonny +discs +puddy +filos +icons +plena +maiks +crazy +gibed +pubes +sulfo +yomps +orcas +casky +conge +drouk +heard +laker +mecks +glaik +windy +glare +kiddy +rotls +sords +axiom +every +bared +gazar +gimps +veils +wrick +rared +motif +uncut +divna +amort +ousel +frise +leuds +yeses +cover +bulla +limen +dyers +motel +dahls +redub +zulus +kynds +famed +mozed +katis +gebur +scody +metes +chics +fecht +myoma +passe +fidge +shahs +diebs +ovate +gluer +fetal +peeps +offed +ocean +reink +cauld +punce +gores +echos +phish +dicts +spoke +progs +hunts +flocs +blaff +conks +gleds +flunk +mausy +urson +rubli +fiche +pures +boost +padre +pulpy +fiver +lanai +lexis +silex +share +fleme +breed +cheek +vagal +lytic +vakil +ojime +tyran +sorex +wists +upbye +metro +chefs +friar +kahal +battu +mares +quoit +heals +bleys +kaies +arils +kinds +remen +zesty +ginge +tozie +ascot +polys +tarry +spumy +pulik +clink +seans +gerne +saved +steil +brats +tways +vomer +tiddy +deans +kinks +gamey +herry +bajus +erase +piets +aesir +shakt +maund +queyn +chufa +nerve +massa +lapje +teend +seize +seyen +thanx +delly +exams +buxom +teade +sient +equip +grens +dupes +broad +sowff +panax +tabor +spark +andro +hoops +inurn +phese +diner +guest +samps +heils +tyees +crier +rarks +kopek +bizzo +yucks +wudus +bachs +neeld +pesty +sitar +tepid +braws +bolls +psoai +gobar +birch +ruled +naval +fytte +manna +typos +gambs +shoon +elide +squad +derma +quids +unman +prill +amyls +pands +breis +hunch +salle +paise +ayelp +limba +emmer +skers +sweel +soups +rouen +ferms +robes +scugs +zippy +embox +arced +knars +rorts +snead +enarm +savey +shand +tides +watch +aleph +whaps +crems +stein +gawds +jivey +sants +igloo +coyly +emote +deeps +boyos +chill +mumus +pecke +world +holly +kaury +twits +zetas +coted +haiks +appal +thete +volva +jinne +theme +hoppy +yeesh +nerts +pully +booth +gutsy +leaks +lubed +rages +taped +shirr +percs +admix +spirt +favor +hulls +lions +stour +gonia +bunts +kukri +whirr +bikie +tosas +tsade +sucky +hilus +yecch +guars +rhyta +togae +manse +baker +odium +youks +oncet +reign +juked +frati +jeons +turns +acidy +tizes +sandy +evils +yukky +pouff +uveal +tynes +hoing +inspo +forty +raits +uredo +wited +spifs +tanty +boaks +mayos +softa +shied +rones +whipt +gutty +yeast +relay +utile +coots +speld +duchy +magus +locum +jehad +whins +idles +slimy +exact +ghoul +jocky +kores +gouge +rutin +clown +smile +tauts +trema +zings +mists +alley +voice +hoggs +genal +toged +lawer +dhoti +speil +linky +ogmic +delay +kadis +bomas +sloid +grown +plotz +tutti +ducky +wrast +galop +kicky +hinge +ajiva +dorsa +sugan +tempo +sigma +hosen +seely +rebus +getas +fribs +tzars +abies +meves +hawks +fetch +yumps +bruit +spald +waugh +zorro +curry +roist +skoff +crudy +viper +ronde +uvula +malam +loden +bitou +nandu +towed +slyer +palms +ephah +trone +yauds +sores +ruana +yfere +deled +wizes +milky +raths +zoism +piker +cobza +cronk +dinge +haika +bahts +metal +suets +chunk +commy +ovoid +weels +surgy +jacal +beaty +gamps +zambo +vichy +mawns +yores +mooed +smore +miles +ogres +gouty +trove +prune +reest +break +fecit +debts +taxes +hider +rouge +daces +benty +afars +dimbo +reeks +brens +halfa +aswim +flyby +laver +sansa +narcs +goura +fifth +vinyl +frown +outgo +halts +nulls +acyls +alays +eidos +edema +bemad +rangi +tones +triff +dowry +senti +tuffe +peggy +trike +hulks +rewan +limes +gerah +zlote +breve +skort +mauri +ashen +danny +uncap +bonze +bulks +ewhow +glory +naams +patka +fesse +unfed +wonky +ulpan +scale +feaze +lazed +fools +boeps +sabha +podia +digit +subah +torii +sunks +milfs +haufs +swage +octad +tabby +humps +durum +tawts +warez +teffs +vrous +sonce +kains +cully +sware +creds +knock +peghs +ancle +faery +mumsy +ambit +hexer +koffs +nabis +defis +dobra +lengs +caves +giver +caver +abear +carex +waler +lycee +musky +homed +amins +imino +belie +nacho +eland +argol +sukuk +pends +octyl +verra +mania +visne +reman +lytes +teeth +eyots +evade +kelts +tical +nowty +gaups +proem +taper +inept +deify +sinhs +frits +dants +kaifs +rales +ozeki +smack +duces +obeah +vails +murry +sauce +frock +adore +bosky +bloke +koels +priss +unpin +haven +pines +hymns +hotty +budis +zymic +runed +fates +glaum +camas +remit +abask +sycon +butts +mamba +flaky +limos +desse +ahint +snuff +flary +sused +taupe +speat +laevo +aleft +touch +rowed +alarm +girns +oater +boxla +slain +gally +cymas +moong +clift +waltz +annat +jamon +forky +wived +arets +tiers +gluon +tucks +bagel +pudgy +voces +snits +capul +maven +rownd +goose +deign +grein +ackee +dungy +retro +birth +secco +manga +kohls +flyer +tubae +unpay +atlas +baurs +mixup +ureic +ragee +proke +ernes +setae +panko +vapes +menad +cirls +offie +grind +sulfa +orpin +aegis +colic +hinau +bunya +pulas +beano +socle +scath +biter +trope +pugil +bifid +siver +toper +ostia +spams +melon +hongs +eagre +yacca +ambos +algas +overs +focal +attar +elver +dreer +serra +stroy +duets +hells +beaks +bhoot +ahing +hates +roset +froze +lapin +mauts +obits +gyans +nopal +focus +bazoo +dozes +argus +kight +flits +rides +smerk +tratt +admit +loamy +bevue +nyssa +pyats +naric +auric +gerle +plink +track +cecum +dwine +dicks +clogs +batty +socas +phony +loast +raggy +titup +newts +duper +piked +aport +prief +modem +leafy +folio +stong +sposh +shaft +medic +tenne +vegas +snyes +shope +drool +poilu +twang +novas +nighs +edile +brees +neats +solon +arete +redux +douce +ruing +volts +ammon +sheel +sizar +herls +teams +rache +deils +scaly +chica +thang +slaps +arvee +renig +rosit +mothy +seres +treen +spasm +runes +erics +pance +canto +lipid +ixias +model +katas +tacit +daddy +toads +bezil +dents +lemur +lowes +irate +baldy +ogeed +tench +ports +buffy +tithe +veiny +frank +hooey +bovid +tonus +irade +jumar +shtum +chimb +scuds +perts +mvule +hesps +laxed +lease +bight +rayne +vicar +jirre +sunna +hasps +deers +blook +stack +tulpa +lumas +sixte +goats +nabla +patty +dzhos +pions +missy +fiqhs +lovie +dunks +agood +refry +carta +burls +janny +ledgy +puffy +doted +peels +cruse +canoe +tatus +pokes +awmry +nides +cozie +namma +owres +satem +sabir +sados +ruffs +pibal +toast +celeb +goony +femur +kitty +plack +eensy +ariel +peals +cargo +yipes +preif +knits +cloam +segos +theow +datum +galax +nolls +lyssa +roman +yawed +kyloe +knosp +zippo +oriel +kidge +gawks +coups +biner +smorg +pelon +skens +amici +olden +aquas +flirs +wools +roust +skirt +toted +emeus +kiley +hoghs +flood +daric +cripe +gools +nimbi +stude +lobos +hizen +gonof +sylph +outdo +sepia +yales +raves +bundy +mourn +unsay +quyte +lotah +viand +scoop +dreks +human +bilbo +aidas +chang +taxed +kubie +culex +grame +punch +bakes +trans +jalap +toked +couth +cribs +scuzz +hanch +thumb +touze +naira +narks +taros +wedgy +kitul +djinn +chalk +morns +oasis +rains +scrub +toner +dawks +stirs +obied +sixty +hynde +pagri +swing +boyfs +jowar +sotol +sehri +flout +pasha +asker +stela +briny +sands +barks +earns +steer +heugh +recit +cache +holms +wussy +craig +lowry +doors +dawen +sough +lopes +reges +baton +gilds +refix +luted +flews +salps +lanes +mylar +hemin +lanas +monos +axels +ordos +binal +scaws +taits +ickle +chive +liven +tweak +rifer +sield +laari +aruhe +bawns +quart +rubus +joked +lever +creel +lests +doved +bells +gemel +wraps +style +bukes +aloed +nurls +alkyl +asked +bract +zowie +socks +nards +kipes +whips +plaas +large +waney +cuppa +skint +eyres +biker +shear +earnt +cubic +swail +plesh +nones +woofs +brogs +rangy +stire +togue +saree +buffi +jedis +barbs +barro +apeek +hebes +freon +dimes +bocks +cleep +sleys +mulls +malwa +taces +cosed +thema +hilts +druid +schmo +botty +washy +vulva +siles +fitts +eosin +tenge +trogs +deads +gussy +boong +skirr +turnt +fixer +tophs +seric +ditzy +fichu +their +dulls +niefs +mixer +tubed +relie +mimic +union +winns +pulps +guide +hoist +finny +lamia +hulky +tupik +joyed +wispy +yield +staws +dildo +jujus +certy +awned +sayst +urari +tided +triac +bitty +laxes +koaps +geoid +omber +casas +being +altho +lured +linen +keeps +knife +bonny +brane +ettle +droop +built +vases +curer +wilts +luxes +timps +posts +vulgo +hades +hiver +vibex +basij +kerfs +trons +advew +roshi +ogees +cluey +flake +finca +cavas +raids +darns +yechs +khaki +okras +moths +gunge +talon +moxie +motts +jumps +biles +condo +uptak +brown +alamo +corey +borty +chyme +suses +barbe +pogey +elven +porch +scats +ethic +wears +bibbs +joeys +check +vendu +foils +milor +karzy +meris +zante +umble +lofty +stope +epris +cesse +whear +whyda +proxy +bowed +quayd +kheda +pappy +jougs +carks +plant +biome +peavy +wades +oaten +mouth +samba +kimbo +inure +dooce +upran +kayle +ferer +doses +ronte +smoky +braza +goary +vinca +burro +doseh +incle +hanks +yogee +pocky +arepa +reach +pelta +lyart +ngwee +comix +impot +furzy +swine +toned +amban +users +hings +crare +busky +soras +urine +kaids +pinot +derig +lumpy +dowie +mimsy +mapau +wushu +amply +doges +buzzy +skeen +opium +basto +meath +achoo +rummy +zonal +mavie +taste +jocko +dowls +moans +golem +aeons +misty +gramp +yages +exons +withe +pelma +creps +hoied +wacky +pents +futon +gaucy +earth +bruts +rajah +predy +legge +peare +sohur +study +rubai +dures +snars +palas +tangi +oiled +ligge +mihas +bokes +dusty +soupy +pakka +gulfy +ritzy +gummi +drome +poxed +sikas +abeam +pipit +gnawn +sweep +ngana +sowfs +totes +nonyl +emmet +ziffs +dumky +three +buiks +rishi +arras +bursa +benne +razer +dreys +ruble +capiz +educe +yokel +sirup +veery +pates +resto +tavah +bowrs +kells +stogy +keeve +mends +ludes +puler +jests +smalt +salmi +iller +curbs +arcus +azote +adios +novum +major +arbas +caper +sagum +mardy +lahar +clote +pulse +emics +tepal +ziram +psoas +bales +ghyll +hight +unjam +kraft +agami +choky +sowne +oubit +gasps +fused +ceiba +crues +franc +calls +plain +caeca +raved +gadis +basti +antis +sones +elops +perch +vices +pheer +young +glitz +doody +stums +resay +bowet +legit +pulis +knarl +loose +ylike +sabot +leary +bugle +folds +unled +elate +campy +mokos +kudus +madge +mayor +tired +blued +comas +racks +kaphs +false +bathe +value +cauda +chirk +issue +rille +vodka +fents +deave +ronin +longe +teins +hudud +towel +coign +thirl +jingo +mucus +riyal +quina +domal +stash +flora +powny +preop +bluff +basts +bivia +crane +raffs +styes +stink +squid +dumka +brays +place +wauks +mowas +beres +acrid +saiga +sysop +gonys +mongo +calve +vitro +first +egged +micky +denay +roach +vells +mitch +gammy +films +isbas +berob +choom +muton +freet +layed +stork +awing +heapy +south +brews +fient +yahoo +ratoo +vakas +zimbs +piani +fault +yacka +blest +worts +munga +oldie +leccy +kohen +olpes +sijos +peery +ahigh +meynt +neuks +knobs +retie +smoko +bants +fends +teind +sechs +toots +aitus +gauss +abcee +primp +pricy +vegos +crate +slive +roger +arnas +depot +niner +pikis +wagon +boxen +roric +rowts +naiad +brush +trend +bigae +sakes +perns +sture +wexes +prone +goner +krewe +given +bahut +crude +unary +neums +rands +necks +parge +sodic +daiko +tewed +ethal +ottos +scarp +coria +swirl +oupas +boots +diota +where +matlo +dipso +leapt +atoll +lovey +feuar +segar +unget +luger +pured +preps +pilaf +pipal +tough +teary +noops +cacky +porta +nodus +shunt +scape +stend +fiats +forks +jaunt +rumpo +chack +negro +defog +cloth +osier +worms +glade +opens +frate +maire +boeuf +wrawl +stonn +odors +vires +caneh +quena +volar +zakat +badly +goban +trigs +kiore +brass +zombi +turks +gyrus +pupus +barre +powre +retch +rumpy +melik +skins +goofs +diwan +tuyer +vents +abort +throb +crool +kiddo +pikas +nocks +stoep +maybe +fulls +duple +kiaat +dormy +rhime +avows +kondo +coles +limbo +pyxie +barmy +zurfs +uncia +bolar +ficin +lamed +cried +benga +blatt +chuck +wants +inkle +cadgy +gumma +sprod +bambi +seeld +askos +decoy +flail +error +gyals +nonce +ching +leeks +basks +hurry +doorn +ileal +slows +pluto +omlah +lucky +bosun +devas +paedo +shack +fluyt +debag +leach +sadis +savvy +alcid +butyl +litre +spume +tears +clour +cavie +opahs +scene +abord +cesti +heels +firks +imaum +nenes +kiang +refit +fidos +reaps +acari +cream +coley +dirke +moray +furth +whist +skear +dobla +muons +somas +onces +dukas +hemic +cebid +kolos +pewit +valet +turfs +dores +piped +gotch +times +bhais +opted +winna +quant +aigas +keels +farce +cuffo +vison +jeton +cruel +fined +urban +anele +cezve +huffs +gyros +fiord +milty +paten +wills +dalis +longa +soler +croze +klieg +clems +often +waken +truck +barns +lozen +sauna +bices +brung +torse +spale +doree +seeks +hoyas +doser +genua +snots +faxed +krabs +mayan +poets +stove +spaer +nevus +mooly +liang +jesse +plies +felty +fains +suits +parki +dorms +mense +knurr +yabby +capas +katti +kerma +mzees +coble +yokul +sessa +praam +pagle +soldo +lames +kawau +foins +fykes +porin +ootid +bhuna +libel +texts +retag +bossy +junto +hoast +kenaf +chows +clerk +fogle +miaow +elves +dicty +tends +oasts +reset +clave +yoghs +chime +typed +mikra +gaudy +curve +lipos +idyls +maxim +piece +owing +redly +nisei +prees +gruel +shuls +aunty +epode +stows +irone +spoor +silky +eldin +trips +pirls +pence +etics +feces +fader +avgas +panty +jatos +quark +nevel +hexes +lemon +fayne +bawrs +roofy +hobos +moggy +snook +malax +geats +bandh +hikes +jaded +busby +menus +popsy +grege +genet +baron +tract +clank +tardo +droid +outed +oxers +hylas +carol +axial +oyers +tynde +match +welsh +verso +mahua +clype +heres +yawls +doozy +rainy +duppy +veldt +velum +hayey +altos +craic +deets +ebons +unhip +jigot +mosks +lanks +wires +glute +sarod +balsa +hence +baith +showd +rally +pooka +slank +grice +lowns +shool +wetly +woold +sakis +harry +clast +junco +weros +dumbs +tsubo +combo +rimer +tanga +loppy +amate +adhan +whios +khans +power +whole +eyrir +nomes +slype +fract +doats +brugh +hoick +gyppo +welke +odour +anglo +exist +devis +unbar +turfy +jukes +ogive +cella +choli +fango +ummed +yrent +tonks +piert +banya +stang +corgi +drabs +blunt +thorp +dural +conga +offer +agism +vapid +zinky +salep +commo +archi +enows +aster +meare +mercy +moods +renne +adunc +wocks +blunk +yeves +pocks +kyats +worry +sahib +litai +shans +tabun +minny +soote +flair +russe +tempi +whose +uveas +ouphe +racer +whale +iliac +looey +lohan +foray +cloop +aloud +venin +shawn +tuart +crest +taras +sials +quits +gonif +ganef +canso +races +kaims +macaw +panda +molal +grody +avian +malmy +penks +apply +vampy +muses +tonal +dunsh +tasar +shoal +acton +mauve +churr +golfs +henny +adaws +twaes +husky +wefte +defat +cords +loach +pirai +cries +laity +losen +lidar +hoyed +hexyl +tabes +naked +senna +kippa +bodhi +maaed +nerka +matte +dusts +arpen +sauts +emery +undam +jaxie +rooks +perog +rioja +widdy +momma +otary +yodel +vades +addax +wanly +giddy +whets +juvie +pungs +named +siris +horse +fatty +tikka +spell +aggro +fogey +trabs +wheys +druse +beton +melas +alane +tyler +platt +grift +treck +welly +lofts +spyal +hokes +acids +calix +atomy +knots +wrung +mease +sabed +stoai +kyrie +maise +crons +spine +pasta +korma +pokie +sebum +foehn +beefy +mopsy +netes +coypu +rover +drive +smaze +skimo +paxes +sowms +liver +rerig +sipes +blabs +beams +happi +jagas +loord +daven +snack +nooit +coils +kiosk +repla +acmes +tawed +damps +those +satyr +genes +brans +basso +penes +suhur +grave +warms +tosed +klett +demon +woken +bally +redan +pulao +gamut +oorie +lobus +coala +bides +towie +mosso +lyted +lound +pawaw +snool +erbia +sordo +tatts +yucch +money +oohed +swink +skeps +hacks +peaky +powin +tipsy +riant +satin +feeze +mobie +tunic +safed +aheap +herse +gonna +narco +terne +taish +hyped +basan +crags +furol +sauba +pippy +semen +styme +ering +mirex +sheva +whift +bleep +sinus +gyeld +myoid +bluer +haves +gambe +preed +warre +wadge +prats +blips +unwed +skags +wrong +filth +knurs +ainga +raiks +bedye +ronne +butch +biffy +peles +lings +cause +ephod +chews +mirza +jugum +tooth +lorry +moile +woofy +gorms +vivat +bumbo +blend +bania +dozed +rayah +ovist +actin +emend +bleed +smogs +bunjy +spots +firth +drice +plate +pauls +manly +sabal +reave +costs +eorls +pears +coofs +prost +femmy +jells +roses +bongo +varix +ceric +lunar +gismo +boric +kelep +vexes +hides +niton +lamby +media +spivs +sewed +zerda +krubi +hosel +nooks +blase +fecks +inlet +snabs +ebbed +aiyee +ahuru +meiny +boxed +poles +nahal +muzzy +pervs +bated +lammy +harts +barer +pavid +grams +vanes +ohmic +cetyl +dears +spews +sored +arear +clasp +godly +toges +twyer +yurta +thelf +voips +chute +miggs +yeahs +vagus +argot +hypos +parky +dross +halon +sissy +avant +talma +cleft +hurds +ycond +rails +klick +mobby +cogon +septa +smelt +glops +tardy +ornis +esker +woosh +gears +husks +diols +ridic +sikes +aught +leaps +heros +olent +bigha +disks +inwit +packs +jiaos +chide +ybore +fever +wiper +steam +symar +ramal +dirks +strut +koori +pross +there +gauds +etats +event +barye +jinni +gaums +kamik +shawl +zincs +potch +title +scrum +nonny +sades +kotow +ablow +slime +ruler +wases +miche +setts +yuked +scrim +rivet +curfs +imshy +brits +iroko +tours +teens +navvy +chott +flued +ogled +skyrs +bapus +esile +folky +mased +saxes +serer +luser +carts +lieus +glyph +boral +oinks +fanny +shade +visit +quips +kurta +halos +inned +kicks +snaws +simar +hoped +dicer +stear +stone +ganev +dreck +fauna +sieth +prawn +gojis +hokas +breys +thick +syphs +forze +pluff +colon +belga +valor +picot +smite +ogams +jarta +threw +zines +dived +doxes +waled +oflag +nappe +thale +losel +toons +lusty +dhikr +ashes +zoeas +goals +leeps +tyned +frond +goafs +lavra +patsy +enurn +etage +occur +nunny +tsadi +kibes +meril +punny +slags +posit +gawcy +kapas +guiro +pared +whoop +ovolo +vitae +defer +combi +limas +umiac +sunup +choux +palea +infix +curia +votes +terts +hulas +wanks +caddy +caups +billy +gleek +yobbo +whiss +drail +busks +dints +talpa +forza +nihil +hames +laser +goety +crewe +nakfa +belar +gnome +topee +gulfs +fitly +quire +goest +malar +slats +desks +slier +outby +riley +react +manat +iliad +shire +cukes +frame +papes +bassi +douts +misos +aargh +lefte +gavot +cuter +aloft +lance +gloop +quest +pruta +risus +wound +furls +gilpy +gravs +scuts +rebec +broos +refis +palay +typps +bless +holts +koans +tacet +spues +fines +denes +meins +slosh +fugal +usury +cisco +bizzy +dervs +arede +ponds +phoca +mohua +tungs +vireo +pests +quoth +tiyns +domes +hithe +korus +music +bears +cores +pairs +hived +cager +kerel +praty +gives +stair +yites +ficos +doula +jowls +mesas +bluet +foxed +rotch +auger +gosse +glace +rated +ayaya +exeme +shoer +tuina +antar +umras +adzed +route +yorps +norks +woozy +dorty +pryse +revet +yogas +kelly +fancy +cylix +mechs +keeno +whiny +parks +arrah +setup +turbo +signs +ferns +troad +touks +tonic +accoy +remix +celts +seral +yapok +syens +shill +twats +taroc +coked +calif +booai +xylic +usual +woxen +stept +trayf +blogs +kytes +moped +rills +peize +label +rarer +tulsi +adobo +bimah +narre +mazey +pujah +stagy +mopus +loxes +weans +aglus +busts +synds +vauch +mynas +fraps +spaes +bogus +baals +clonk +sapor +strew +hasta +poohs +honky +novae +sided +poynt +nisse +qajaq +allee +tamin +ocrea +jokol +debut +comps +tepas +bunce +adzes +yampy +barms +sniff +anear +pacha +soaks +troll +kluge +typal +grins +sarus +roles +cives +vughs +trild +slish +japan +corks +wokka +gloms +boras +moxas +satis +mucor +natty +jaffa +pling +harem +krone +talas +zinke +wawes +adust +swies +eloin +sucre +fount +dimps +amigo +ruths +wines +artic +gulch +poofy +erode +derby +nippy +rawly +aband +letch +cohab +tazza +sneap +azons +selle +sumps +souts +dunam +sulci +clock +sixer +dingo +attic +sloop +fauns +slots +fifer +caput +rurps +xerox +rebel +coyer +forbs +fiend +scoug +riser +snuck +pheon +linty +lodge +skeds +ochre +ships +bards +molas +gecko +clove +wrapt +nertz +theca +corer +tiles +sanga +grese +noddy +sykes +bosks +talcs +anlas +sirra +kilps +shove +kivas +mythy +fowls +nelis +thaws +kinda +becap +adman +tacks +azans +cepes +logoi +penal +suent +cleik +weeps +yarco +kerns +panel +prods +oping +known +globe +glits +chace +tango +patch +booty +forts +mauls +corny +clods +inust +dales +tilly +gopak +salon +vaper +wigga +bydes +boohs +suras +regur +spard +melee +cakey +laird +nucha +crudo +stane +geeks +dwile +yrneh +stray +brogh +cutto +uptie +whelk +immew +nyala +snoot +donny +laris +suing +milpa +askoi +nikab +strak +teach +wides +solei +fuzzy +tills +premy +moste +canid +fands +rieve +scudo +glift +laded +hawed +monde +sinky +gauge +appuy +shits +talus +color +kelps +lotic +lyase +neeze +wifty +loafs +kites +emeer +prime +grego +eales +pekan +sight +miffy +lownd +reads +conto +sized +katal +spelt +fugly +chiru +dolly +diyas +midgy +furan +coach +hydro +plebe +capri +waxen +pulmo +sauch +rahed +larch +anils +weens +hyson +furry +diver +welds +saugh +vexer +cough +trona +tatar +shots +hilly +along +acers +sedes +scarf +twink +croft +tamps +buchu +aspie +dexes +yowza +slyly +fluey +terry +flamy +motet +nouls +vocab +dogie +punty +skatt +yonks +acorn +okays +locos +bedel +dilli +deshi +wifie +munis +mucro +drupe +holla +frush +tomia +gazer +hears +uncus +plush +esnes +lines +faddy +unbid +wharf +acros +dilly +mhorr +amaut +putts +pagan +quops +words +fenny +chico +snobs +howre +muted +fillo +manet +whaur +gaped +ghest +nates +azoic +lousy +chart +brods +geeps +vangs +shish +dunce +uraos +warps +chugs +lipin +swits +toeas +tapus +sprit +pluot +whiff +hauls +vraic +jaker +scuse +glint +sokah +savin +slued +sakti +geare +carbo +crowd +stoun +peyse +whups +linum +monks +ampul +skiey +hewer +about +nouny +lolly +uplay +rifte +suint +veins +lyres +gogos +ortho +blurt +edify +telic +aurei +graff +boats +poule +clepe +rudas +obese +dance +leres +shuln +eagle +edict +notes +omasa +remap +chasm +mules +swees +slams +latex +axion +podex +roops +dowts +sumos +dills +macho +party +flare +caffs +craws +vaxes +ducat +pukka +links +toros +sully +milts +meuse +duits +tumid +voddy +voxel +boney +plots +rokes +lweis +mengs +fawny +burry +sprag +janes +rowan +muley +sloyd +sards +bowat +musca +certs +poach +nacre +teles +burst +kirks +sleer +erugo +habit +oktas +hewed +video +fauts +liana +wands +pawas +maneb +twixt +rests +grove +sulus +morro +stime +cuspy +urdee +puled +cagey +weete +gusto +canes +gains +stamp +chota +kufis +poses +pombe +slick +snags +posey +geans +fowth +meith +kembs +mysid +aumil +coins +potts +torts +sulks +wanna +leugh +lurid +reefs +sheer +thans +diram +bigot +briss +jived +loave +atman +mowra +tutee +risky +ctene +urare +pujas +fanks +temps +segno +shent +crust +amias +zinco +empts +nests +lobby +group +dusks +vowed +mayst +tangs +glike +motus +rhine +dubbo +eaten +weems +khafs +primo +drags +toran +terra +isled +airns +azyme +spits +bever +pacts +seals +right +nkosi +freak +unbed +oozes +durry +fezes +pekes +doped +gawps +tarns +peins +jural +hamba +diene +sedgy +skyre +lipas +wheel +beast +yojan +rooty +arsed +tyiyn +umbos +stumm +grike +proin +recon +euked +byssi +buret +pithy +anata +rivas +terfe +areic +yarak +choil +linga +jewie +choir +pacta +rates +femal +coarb +jolls +gland +titis +nappa +jerry +pongy +howbe +ylkes +swims +varas +cliff +lords +wedge +summa +gurry +beats +chook +boxty +affix +bungy +diced +varia +redip +luges +jaaps +randy +degus +rakee +lemed +sured +crwth +ewers +geeky +tripe +ureas +newly +jemmy +hatha +munch +stint +logos +plead +cults +binit +casus +menes +didos +acred +agmas +epact +labor +clach +rajes +bents +mairs +breer +voles +susus +goyle +edged +salad +pusle +grays +conky +zanze +stubs +hokis +vests +seaze +smoke +flags +chuff +xenia +funky +loath +sleet +boles +herbs +glove +reive +raver +frons +doing +pride +parae +toxic +bunty +catty +flite +zhomo +takin +bacco +parti +maist +quaff +furze +stade +evoke +wiped +envoi +leone +lilts +azido +schav +rueda +moyls +balky +aulas +gyres +torrs +bites +hudna +sooks +flaws +vants +lingo +tophe +pynes +reoil +moons +skivy +stots +scots +decan +dobby +amids +julep +dares +borer +jeans +kinin +waver +semee +doves +eques +unsew +biggy +shout +codon +risen +bowel +quims +gorsy +kutch +gurge +sensi +podal +ladle +braze +prial +bhels +porns +hongi +weave +saola +antas +pings +dunes +krang +ousts +soots +cains +mower +nitro +paned +fungs +polyp +cheap +kiths +snout +munts +rebbe +ovule +atigi +angry +supra +infra +mudir +peats +dekes +leger +tapes +durns +print +spiny +kemps +anent +taffy +buran +ranch +lasts +butes +tofts +spore +ganch +crush +polio +mayas +kappa +golpe +degum +klong +miaou +ooses +cairn +trout +morat +orlop +milia +flaxy +butle +vivid +larks +nappy +pucka +pecks +brain +gault +quonk +gnash +wolve +yucko +prate +knive +chine +looms +bicep +piggy +eases +zamia +pupil +anted +tinks +alkyd +pomps +ficus +deedy +walls +norms +scold +deair +tinds +zooks +riper +malik +wyles +upter +rowel +alert +stoss +malty +chemo +flute +bongs +robed +moppy +buffa +lader +swore +mucin +sorta +auras +woopy +korai +aquae +soave +beses +cobra +liker +geits +womyn +gripy +amene +chivs +fryer +ovels +ganja +motes +daman +abaka +guids +rahui +salpa +swapt +purer +kheth +lowed +guffs +rehab +flype +whine +tanhs +slaes +kinas +kexes +atilt +dovie +smaik +avion +yince +tenue +otter +girts +saned +least +foots +delft +claim +slink +aline +vibes +dekko +arnut +mouse +spilt +tecta +nowed +tozes +recks +rotte +nares +noups +cleck +talaq +spaws +layin +mulch +ulmin +cress +mewed +maror +exult +jaspe +ictic +vexil +seity +ghees +rapes +mealy +vigia +repps +twilt +crims +perky +masas +eughs +resaw +apods +dicot +unmet +eathe +vints +scour +liers +yerds +wolfs +brach +escot +shiso +reked +rumba +welks +ogles +coure +fille +telae +surly +honda +divis +sties +proto +blare +luffa +muras +halal +lazzo +feebs +cocks +rerun +faine +coney +flawy +lehua +pivot +urali +abash +conte +blive +baits +holey +pouke +paisa +tuned +garms +minas +reeky +bunch +mopes +prads +sajou +chimo +scapi +duett +bores +bobol +swizz +yedes +mawed +coupe +lunts +tagma +ditty +deres +sieve +bhang +ogham +quine +mojos +lobar +louma +boabs +magot +dabba +korun +stops +yours +udals +tiars +pedes +cluck +aspen +pouty +favel +tsuba +wacks +niche +devel +skats +laves +flush +nabob +enact +homes +kopje +riven +wynns +veeps +ditas +gunky +riata +hoper +gazal +exine +kecks +glary +kapus +pissy +drony +stump +roars +foyne +fifes +jilts +sarge +lowan +twerk +galls +mokes +loots +aeros +puggy +sprug +urned +tiara +vocal +sepal +bused +redds +dinky +snath +iodin +daffy +godso +hotel +photo +sobas +lossy +happy +anger +cardi +scare +hikoi +spacy +trees +bread +curny +fasti +pilae +awato +neath +meses +sheet +masts +aboon +crept +klutz +tames +ohias +goody +brief +sorer +umbel +algor +bided +wolds +lards +burse +climb +taxol +padri +wombs +steys +ratch +kynde +coady +bloom +toady +sices +topos +braai +lezes +hunks +roads +putty +pearl +gangs +knowe +huger +kutus +sides +blurb +lavvy +rajas +award +dript +nuder +incel +march +knead +thrip +disci +bendy +bulse +samey +stilb +shred +voled +kazis +keefs +doups +gurns +sloes +dholl +haems +cruve +foody +lauds +paire +aimer +loric +shake +theed +ksars +chyle +heron +carap +jinks +rammy +shale +capex +dungs +bemas +dumas +flour +slits +bonie +leets +arene +raker +deaws +hazan +sluit +leves +whews +zoons +lills +iglus +renal +aulic +fetes +ropes +pikes +crake +azyms +scion +rodes +oaked +jocks +duffs +lezza +effed +newbs +shalt +divas +tharm +grued +machs +jagir +unsex +views +swigs +limps +azury +zaxes +fundy +lints +broom +ducks +frail +hoofs +salol +bubby +kails +tules +sexer +okehs +braid +deked +khors +frist +bosom +afore +whoot +parry +styre +gyves +blows +brock +burgh +purge +bimbo +heder +yaffs +hijra +gugas +erica +gandy +lotes +execs +gruff +lomas +cions +lusus +waged +noris +louts +runty +lears +metis +taxor +yufts +alibi +nuddy +jambe +seles +bogey +vibey +upset +momes +helix +quick +sadza +peece +filmi +flick +bouns +areas +triad +skelf +beaus +mahoe +rotos +typto +donas +exert +kesar +sakai +yoofs +bufty +wingy +blype +nitty +dress +torso +knaur +vertu +beret +kabar +sinks +hocks +sonne +fifis +resid +rybat +hotch +sadhu +techs +ilium +duing +paals +thunk +pales +afoul +ramin +paved +wilds +ugged +morra +munge +farls +stoor +umpty +redry +gudes +burbs +wheen +penna +hilum +jolty +dozer +kithe +tarre +orate +stilt +tokay +nicer +jours +eyers +unket +court +favus +lirks +huffy +twine +kiwis +maple +scads +bacha +drant +guile +sambo +kevel +lahal +gager +peppy +truth +sloan +level +toric +nebel +yagis +reiks +skyey +items +ranga +hazel +sucks +wiser +fatal +vinas +airer +tried +jodel +babus +cobia +pecan +inked +squab +ritts +amens +slide +scala +coxae +crost +allyl +titty +leaky +mater +zoist +drama +unify +rasps +bergs +pauas +gayal +mated +chevy +evite +teggs +fried +typey +soken +cohoe +larnt +enorm +baboo +deles +basin +pitch +ataxy +antre +turps +unsaw +powns +appro +lotus +antae +makes +tronk +squib +chals +supes +iotas +nixer +snugs +vegan +dixit +leear +ugali +caulk +adapt +scatt +draco +teils +mazut +fanon +saker +peres +murva +bates +sunis +wigan +desis +rased +bleak +kinky +salue +pated +varve +wavey +arret +noahs +input +thilk +vinos +ixtle +pyric +yexed +trems +amove +nutty +coded +delta +prink +bocca +mesel +sella +quasi +vigil +seeds +tiffs +polks +swelt +shock +shets +wiled +cames +bitts +squiz +finer +canns +wifes +obias +stunt +actor +nimbs +fards +hoves +brava +flaff +pyran +jawan +sowps +oxies +issei +pacas +guana +cozey +voids +bring +bagie +memes +spods +shirs +wolly +sleep +gisms +soole +ulema +bonds +boree +crans +saddo +shell +boded +laved +ceroc +crash +allis +ought +treat +kaika +mucho +poeps +muxes +eight +zincy +forby +frisk +whump +diact +blume +ither +poise +dolia +based +inner +pooja +indol +akela +slugs +frosh +cabas +beers +whity +psoae +kendo +ikans +jello +doyly +cooms +yawny +lunet +gusty +boyau +equid +exing +polos +gales +curdy +soldi +chief +yeuks +magic +runch +rumen +dorse +pells +duels +blimy +rewth +takky +hoked +mauzy +brose +gnows +pavis +dohyo +tores +bulls +amice +mirky +hakea +akkas +blams +endue +hiois +oiler +jisms +banks +pepos +mavin +novel +petri +awave +cauks +sizer +gizmo +perms +batik +vodou +admin +boche +lichi +robot +bevel +rares +pashm +amahs +cotan +hough +vigor +finds +hygge +waist +scran +pzazz +takhi +swoon +quack +ligan +zineb +sutta +pawed +ponce +elvan +ethos +spank +lurer +hyles +tupek +glues +bimas +emirs +swede +debye +laksa +norma +duans +macle +dagos +biach +mizen +hying +koine +shere +roves +silly +leirs +intel +buffs +bakra +reede +kaval +retax +hovel +shlub +wring +reefy +agast +mujik +blash +ounce +reals +tooms +kokam +toker +egmas +tasse +beaux +lased +numen +yiked +kulak +oaken +paska +cuits +tubal +igapo +brere +japes +pratt +cuifs +flims +midst +islet +lered +finch +anomy +arsey +kuzus +orlon +mousy +sneer +claps +sadhe +barfs +kuias +jewed +readd +piing +could +wanty +tenon +wight +soyle +urite +soaps +stymy +porky +swith +luces +omrah +steak +agone +isnae +nanny +dulce +inarm +ollie +cozed +maill +ricer +liken +round +ketch +gobos +shawm +scrab +ursae +foods +river +repro +apres +civic +rowth +wowee +samen +leams +razor +cissy +nurds +naeve +trios +judos +tranq +troth +flyte +visie +sales +tsked +pumas +aldol +bulge +borna +acais +unsee +ludos +ratos +unlay +locis +wakas +bones +unmew +burqa +sprog +state +virls +clash +curat +houri +cires +torsk +shris +thrid +scoff +flosh +sluse +mawrs +hahas +doyen +boobs +aglet +meant +chere +woman +slurs +purpy +honer +siege +gamme +shift +truer +ropey +alist +agape +plonk +pleas +surer +frory +nagas +filet +pudge +khoum +spiry +tirrs +dimer +croup +dules +odeon +shoes +imide +emmew +ceder +bitos +loids +ofays +dwaum +wrist +mocks +ciels +icers +voila +bible +balas +phone +nudes +vrouw +imids +juves +haunt +incur +saick +whoof +ripen +tells +dorks +shush +pyres +krays +sorel +merry +sidha +sears +braes +gobbo +melic +yarer +douma +namus +drams +shirk +tendu +cloud +wives +faugh +drown +credo +melts +pules +zabra +dippy +farad +cling +xrays +fetid +nodes +flexi +douks +pized +eaned +umped +cloot +darga +clues +chiao +bunko +sperm +kacks +gules +demur +shiai +bumps +carse +pixel +anoas +wreak +infer +helms +speak +ormer +swept +cuvee +fusel +sinew +caber +naped +bogle +touns +curly +obole +bancs +clits +ihram +heath +jakey +crypt +raita +deter +cared +velds +mutch +skull +hoons +shorl +rawns +pangs +aight +veily +fossa +lutes +spurn +horas +jerid +bousy +hazes +tilde +succi +sudds +stank +panic +giros +demos +poppa +donah +culms +musse +wuddy +tweep +mohos +lexes +ghats +abbey +colas +sojus +cutin +shaky +mezes +bonce +twist +mined +soyuz +mesal +kaneh +buteo +anura +mulga +fritz +rumes +scopa +deuce +viewy +lyard +dunch +glisk +tanti +bacon +gryke +gosht +pewee +midis +ulyie +blaws +tapir +kuris +doxie +weigh +strad +fuero +kliks +unpeg +henge +stets +octal +vasts +sokes +ezine +shard +paeon +fifed +staph +mates +merel +decay +sybow +musar +yolky +coxes +educt +sward +vilde +purty +kibla +gaumy +boogy +tanas +alcos +boody +herms +skols +varna +cheer +pappi +frore +hyens +bronc +chats +acnes +howff +bayou +aldea +forte +mulsh +mohrs +renos +stung +gooey +abaya +duroy +bezes +tanka +hammy +volve +embog +briks +trant +scoot +rewet +selva +gaged +likin +crone +urate +steps +costa +wytes +yakow +liked +carps +apter +rymme +hanse +beech +poons +forth +alloy +penie +bowes +rheum +seiza +sings +fails +spayd +hoary +figos +wared +mucic +cohos +slubb +banal +tokos +coact +buggy +kikoi +yukos +deids +vised +lawns +weeds +sprat +mafia +ingle +class +gings +glows +benet +skeos +addle +cusum +puton +douar +riads +leman +strip +jotty +basil +dined +wheep +dhows +tamer +dorba +lorel +girsh +muirs +moits +hooky +levin +exfil +paoli +gryce +holos +pyxis +sined +erned +verse +petal +lubes +dolci +taube +ramet +pints +llama +yests +burds +cello +brome +flats +styed +yummo +yauld +shive +twins +dhaks +mitre +stuff +uncle +bajan +sells +prose +light +venge +noobs +rucks +whare +pipul +spare +oleum +addio +grasp +verst +glide +feals +axman +sutor +wests +bayed +peaty +cutis +griot +kyaks +imbue +qapik +omers +fleet +spica +lenos +wrack +yrivd +egger +feria +bantu +jefes +filii +paiks +thugs +axile +brace +balds +waacs +thewy +kibbi +coved +johns +jumpy +odyle +riped +reaks +flied +scout +kokra +tewit +oners +kedge +praus +bokos +inula +walty +veney +prima +hardy +chams +bends +mauby +chode +latch +plums +yetts +loxed +hains +coder +yarks +endew +djins +kreng +fonts +brume +click +goors +bilge +kamme +drave +nidal +alefs +trues +fries +brine +staun +lenis +bikes +shivs +kadai +foyle +reais +gabby +rites +aulos +jiffy +qadis +takes +flamm +prove +scups +rusas +while +gunks +muter +moles +burfi +yerba +proyn +botch +smits +truce +vines +azoth +xysts +cutie +curet +stuns +tauld +leeze +kited +plook +pails +shero +damar +putti +aahed +cabob +carry +tocks +sdayn +dolos +culti +astun +whang +cyans +glums +areca +gants +abers +takas +tokes +booby +prams +sheol +artal +eager +matts +muntu +qorma +bucko +unwet +rakus +liths +fjord +prase +totem +reams +sulky +diets +grape +brand +calfs +emcee +awful +rodeo +micht +dogan +gasts +deist +malic +jonty +didst +kumis +doles +bobby +pisky +garis +sdein +ragde +murrs +bonza +moobs +sassy +stivy +eyass +gravy +quell +tenes +putto +aguna +kakas +ahent +houts +denar +debus +coati +boxer +wilco +doris +quins +herby +hakus +maars +jnana +mangs +awols +furca +wuses +pardy +chafe +carby +bucku +mimed +dsomo +paysd +blags +ambry +abide +drift +carny +farse +lives +folks +blush +fleur +abaci +tubes +fanum +netty +weary +sower +inapt +gated +reded +ecrus +gursh +snies +udder +today +stent +mimes +stoae +ehing +linos +noose +scudi +weald +ducal +sixth +maria +teths +fitte +bodge +notum +feyer +gaper +spado +lumbi +dowds +steme +taser +grids +parol +bucks +abbas +neume +yells +trawl +raspy +towze +bushy +deeve +rexes +yeard +rifty +spile +rushy +ungot +loony +salet +umbre +shims +forgo +matin +nabks +twank +saber +humas +azuki +quint +jeeze +waves +ceros +dawds +donga +decal +joram +chase +trill +drook +cippi +tease +dodos +wired +hakas +girds +royal +detox +desex +idiom +ploye +weise +ideal +coops +torus +talea +goers +dosed +lambs +haily +sylis +unbag +wormy +altar +wersh +hauns +deeds +oaves +merit +miros +mosts +grill +hamal +murex +dreed +regie +bundh +muist +groat +alpha +shows +daine +flobs +tuism +puses +taver +tryma +yakka +nazir +duomo +sared +fusks +klaps +gleby +coomb +proll +ragga +redon +troak +junks +perai +harsh +white +grunt +grubs +stylo +smirs +canst +toses +dinar +woald +maerl +cubed +pyral +tipis +cutey +muffs +sated +kapow +motey +fohns +coram +geest +sopra +moire +mobes +skaws +icker +skulk +bipod +aliya +avert +babul +tamed +lenes +steel +snees +sisal +bonus +bisom +haffs +spide +halfs +sasin +rocks +surge +vitas +funks +marry +scram +outro +pinas +pownd +stark +nairu +noxal +aleye +thane +mihis +dumpy +brave +jowly +pique +fugus +theta +twigs +utter +lindy +rends +inerm +kinos +bings +milds +myops +impro +yorks +saine +roven +spurt +befit +halve +cecal +micks +gelly +ratty +blind +dorbs +blowy +matza +jowed +compo +peter +potes +crogs +nalas +saggy +enrol +boson +bytes +tryer +amman +dally +masus +idola +wanes +ozone +minke +goldy +feist +linux +litas +nemns +froes +hokum +staps +canna +xysti +pongo +plyer +morae +sworn +skets +geyan +hated +jakes +hydra +gowks +gooky +bills +pitot +flees +poker +bhuts +chaff +omega +crawl +ryper +bemix +smash +cozes +fendy +dynel +musit +rolfs +neafe +jelab +gaurs +hirer +felid +aglow +alecs +almug +betty +haste +ataps +copal +newie +reply +nifty +anole +opals +blawn +incut +males +fouat +tufas +pupae +wawas +plage +arles +draws +tapet +vapor +nylon +swarm +gages +wiels +teugh +thump +stoic +eider +ayont +moted +dashi +gonad +salve +figgy +linch +scray +snebs +sharn +biddy +squeg +myxos +penni +reiki +biccy +wefts +arpas +seedy +fouls +dearn +donsy +ajies +torah +sises +smush +peons +murks +exurb +meaty +natis +rente +potty +chave +wamus +halwa +block +calpa +lowps +terns +fable +lases +solds +moony +chili +kylix +ramus +aitch +soles +fasts +lawny +almes +yugas +ripps +kyang +coden +prunt +barby +maare +celom +divot +vaire +bassy +cooee +blast +adieu +nongs +serum +gappy +crise +swots +sails +batts +aloos +waift +gushy +kisan +xylem +lurex +claws +staff +cools +total +curse +cloze +gytes +wauff +deare +macon +mazes +eches +endow +swung +beaky +pares +khadi +hotly +moils +tiger +rotas +beets +mools +goops +khoja +water +cundy +pilau +alive +steep +beins +twier +deals +thigs +mases +punts +tyres +ploat +agony +burin +giber +corky +petto +aches +merde +naifs +lamas +selfs +taths +lolog +baked +fuzes +pored +hoard +firms +kants +dregs +frabs +yawps +potto +stout +rabat +tapis +speug +pilow +koros +clies +tawai +burka +pareu +mired +benni +kerbs +loury +dhaba +image +tabus +abbes +telly +motis +ghazi +cymes +onium +zaidy +outer +rasse +deoxy +zilla +spart +ixnay +grype +riggs +stoma +leuco +tarok +balti +fanal +nitry +facer +shrow +faded +whims +doven +pozzy +drees +spect +spent +hends +ovoli +fanes +trode +teaze +motor +geums +grits +kames +death \ No newline at end of file diff --git a/bot/games/battleship.py b/bot/games/battleship.py new file mode 100644 index 0000000..3864e5a --- /dev/null +++ b/bot/games/battleship.py @@ -0,0 +1,451 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional, Union, ClassVar +from io import BytesIO +import asyncio +import pathlib +import random +import re + +import discord +from discord.ext import commands +from PIL import Image, ImageDraw + +from .utils import * + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + + Coords: TypeAlias = tuple[int, int] + +SHIPS: dict[str, tuple[int, tuple[int, int, int]]] = { + "carrier": (5, (52, 152, 219)), + "battleship": (4, (246, 246, 112)), + "destroyer": (3, (14, 146, 150)), + "submarine": (3, (95, 245, 80)), + "patrol boat": (2, (190, 190, 190)), +} + + +class Ship: + def __init__( + self, + name: str, + size: int, + start: Coords, + color: tuple[int, int, int], + vertical: bool = False, + ) -> None: + + self.name: str = name + self.size: int = size + + self.start: Coords = start + self.vertical: bool = vertical + self.color: tuple[int, int, int] = color + + self.end: Coords = ( + (self.start[0], self.start[1] + self.size - 1) + if self.vertical + else (self.start[0] + self.size - 1, self.start[1]) + ) + + self.span: list[Coords] = ( + [(self.start[0], i) for i in range(self.start[1], self.end[1] + 1)] + if self.vertical + else [(i, self.start[1]) for i in range(self.start[0], self.end[0] + 1)] + ) + + self.hits: list[bool] = [False] * self.size + + +class Board: + def __init__(self, player: discord.User, random: bool = True) -> None: + + self.player: discord.User = player + self.ships: list[Ship] = [] + + self.my_hits: list[Coords] = [] + self.my_misses: list[Coords] = [] + + self.op_hits: list[Coords] = [] + self.op_misses: list[Coords] = [] + + if random: + self._place_ships() + + @property + def moves(self) -> list[Coords]: + return self.my_hits + self.my_misses + + def _is_valid(self, ship: Ship) -> bool: + + if ship.end[0] > 10 or ship.end[1] > 10: + return False + + for existing in self.ships: + if any(c in existing.span for c in ship.span): + return False + return True + + def _place_ships(self) -> None: + def place_ship(ship: str, size: int, color: tuple[int, int, int]) -> None: + start = random.randint(1, 10), random.randint(1, 10) + vertical = bool(random.randint(0, 1)) + + new_ship = Ship( + name=ship, + size=size, + start=start, + vertical=vertical, + color=color, + ) + + if self._is_valid(new_ship): + self.ships.append(new_ship) + else: + place_ship(ship, size, color) + + for ship, (size, color) in SHIPS.items(): + place_ship(ship, size, color) + + def won(self) -> bool: + return all(all(ship.hits) for ship in self.ships) + + def draw_dot( + self, cur: ImageDraw.Draw, x: int, y: int, fill: Union[int, tuple[int, ...]] + ) -> None: + x1, y1 = x - 10, y - 10 + x2, y2 = x + 10, y + 10 + cur.ellipse((x1, y1, x2, y2), fill=fill) + + def draw_sq( + self, cur: ImageDraw.Draw, x: int, y: int, *, coord: Coords, ship: Ship + ) -> None: + vertical = ship.vertical + left_end = ship.span.index(coord) == 0 + right_end = ship.span.index(coord) == ship.size - 1 + + if vertical and left_end: + diffs = (18, 18, 25, 18) + elif vertical and right_end: + diffs = (25, 18, 18, 18) + elif not vertical and left_end: + diffs = (18, 18, 18, 25) + elif not vertical and right_end: + diffs = (18, 25, 18, 18) + elif vertical: + diffs = (25, 18, 25, 18) + else: + diffs = (18, 25, 18, 25) + + d1, d2, d3, d4 = diffs + x1, y1 = x - d1, y - d2 + x2, y2 = x + d3, y + d4 + cur.rounded_rectangle((x1, y1, x2, y2), radius=5, fill=ship.color) + + def get_ship(self, coord: Coords) -> Optional[Ship]: + if s := [ship for ship in self.ships if coord in ship.span]: + return s[0] + + @executor() + def to_image(self, hide: bool = False) -> BytesIO: + RED = (255, 0, 0) + GRAY = (128, 128, 128) + + with Image.open(pathlib.Path(__file__).parent / "assets/battleship.png") as img: + cur = ImageDraw.Draw(img) + + for i, y in zip(range(1, 11), range(75, 530, 50)): + for j, x in zip(range(1, 11), range(75, 530, 50)): + coord = (i, j) + if coord in self.op_misses: + self.draw_dot(cur, x, y, fill=GRAY) + + elif coord in self.op_hits: + if hide: + self.draw_dot(cur, x, y, fill=RED) + else: + ship = self.get_ship(coord) + self.draw_sq(cur, x, y, coord=coord, ship=ship) + self.draw_dot(cur, x, y, fill=RED) + + elif ship := self.get_ship(coord): + if not hide: + self.draw_sq(cur, x, y, coord=coord, ship=ship) + buffer = BytesIO() + img.save(buffer, "PNG") + + buffer.seek(0) + del img + return buffer + + +class BattleShip: + """ + BattleShip Game + """ + + inputpat: ClassVar[re.Pattern] = re.compile(r"([a-j])(10|[1-9])") + + def __init__( + self, + player1: discord.User, + player2: discord.User, + *, + random: bool = True, + ) -> None: + + self.embed_color: Optional[DiscordColor] = None + + self.player1: discord.User = player1 + self.player2: discord.User = player2 + + self.random: bool = random + + self.player1_board: Board = Board(player1, random=self.random) + self.player2_board: Board = Board(player2, random=self.random) + + self.turn: discord.User = self.player1 + self.timeout: Optional[int] = None + + self.message1: Optional[discord.Message] = None + self.message2: Optional[discord.Message] = None + + def get_board(self, player: discord.User, other: bool = False) -> Board: + if other: + return self.player2_board if player == self.player1 else self.player1_board + else: + return self.player1_board if player == self.player1 else self.player2_board + + def place_move(self, player: discord.User, coords: Coords) -> tuple[bool, bool]: + board = self.get_board(player) + op_board = self.get_board(player, other=True) + + for i, ship in enumerate(op_board.ships): + for j, coord in enumerate(ship.span): + if coords == coord: + op_board.ships[i].hits[j] = True + board.my_hits.append(coords) + op_board.op_hits.append(coords) + return all(op_board.ships[i].hits), True + + board.my_misses.append(coords) + op_board.op_misses.append(coords) + return False, False + + async def get_file( + self, player: discord.User, *, hide: bool = True + ) -> tuple[discord.Embed, discord.File, discord.Embed, discord.File]: + + board = self.get_board(player) + image1 = await board.to_image() + + board2 = self.get_board(player, other=True) + image2 = await board2.to_image(hide=hide) + + file1 = discord.File(image1, "board1.png") + file2 = discord.File(image2, "board2.png") + + embed1 = discord.Embed(color=self.embed_color) + embed2 = discord.Embed(color=self.embed_color) + + embed1.set_image(url="attachment://board1.png") + embed2.set_image(url="attachment://board2.png") + + return embed1, file1, embed2, file2 + + def to_num(self, alpha: str) -> int: + return ord(alpha) % 96 + + def get_coords(self, inp: str) -> tuple[str, Coords]: + inp = re.sub(r"\s+", "", inp).lower() + match = self.inputpat.match(inp) + x, y = match.group(1), match.group(2) + return (inp, (self.to_num(x), int(y))) + + def who_won(self) -> Optional[discord.User]: + if self.player1_board.won(): + return self.player2 + elif self.player2_board.won(): + return self.player1 + else: + return None + + async def get_ship_inputs( + self, ctx: commands.Context[commands.Bot], user: discord.User + ) -> bool: + + board = self.get_board(user) + + async def place_ship(ship: str, size: int, color: tuple[int, int, int]) -> bool: + embed, file, _, _ = await self.get_file(user) + await user.send( + f"Where do you want to place your `{ship}`?\nSend the start coordinate... e.g. (`a1`)", + embed=embed, + file=file, + ) + + def check(msg: discord.Message) -> bool: + if not msg.guild and msg.author == user: + content = re.sub(r"\s+", "", message.content).lower() + return bool(self.inputpat.match(content)) + + try: + message: discord.Message = await ctx.bot.wait_for( + "message", check=check, timeout=self.timeout + ) + except asyncio.TimeoutError: + await user.send( + f"The timeout of {self.timeout} seconds, has been reached. Aborting..." + ) + return False + + _, start = self.get_coords(message.content) + + await user.send("Do you want it to be vertical?\nSay `yes` or `no`") + + def check(msg: discord.Message) -> bool: + if not msg.guild and msg.author == user: + content = msg.content.replace(" ", "").lower() + return content in ("yes", "no") + + try: + message: discord.Message = await ctx.bot.wait_for( + "message", check=check, timeout=self.timeout + ) + except asyncio.TimeoutError: + await user.send( + f"The timeout of {self.timeout} seconds, has been reached. Aborting..." + ) + return False + + vertical = message.content.replace(" ", "").lower() != "yes" + + new_ship = Ship( + name=ship, + size=size, + start=start, + vertical=vertical, + color=color, + ) + + if board._is_valid(new_ship): + board.ships.append(new_ship) + else: + await user.send("That is a not a valid location, please try again") + await place_ship(ship, size, color) + + for ship, (size, color) in SHIPS.items(): + await place_ship(ship, size, color) + + await user.send("All setup! (Game will soon start after the opponent finishes)") + return True + + async def start( + self, ctx: commands.Context[commands.Bot], *, timeout: Optional[float] = None + ) -> tuple[discord.Message, discord.Message]: + """ + starts the battleship game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + timeout : Optional[float], optional + the timeout for when waiting, by default None + + Returns + ------- + tuple[discord.Message, discord.Message] + returns both player's messages respectively + """ + + await ctx.send("**Game Started!**\nI've setup the boards in your dms!") + + if not self.random: + await asyncio.gather( + self.get_ship_inputs(ctx, self.player1), + self.get_ship_inputs(ctx, self.player2), + ) + + _, f1, _, f2 = await self.get_file(self.player1) + _, f3, _, f4 = await self.get_file(self.player2) + + self.message1 = await self.player1.send("**Game starting!**", files=[f2, f1]) + self.message2 = await self.player2.send("**Game starting!**", files=[f4, f3]) + self.timeout = timeout + + while not ctx.bot.is_closed(): + + def check(msg: discord.Message) -> bool: + if not msg.guild and msg.author == self.turn: + content = msg.content.replace(" ", "").lower() + return bool(self.inputpat.match(content)) + + try: + message: discord.Message = await ctx.bot.wait_for( + "message", check=check, timeout=self.timeout + ) + except asyncio.TimeoutError: + await ctx.send( + f"The timeout of {timeout} seconds, has been reached. Aborting..." + ) + break + + raw, coords = self.get_coords(message.content) + + if coords in self.get_board(self.turn): + await self.turn.send("You've attacked this coordinate before!") + + else: + sunk, hit = self.place_move(self.turn, coords) + next_turn: discord.User = ( + self.player2 if self.turn == self.player1 else self.player1 + ) + + if hit and sunk: + await self.turn.send( + f"`{raw}` was a hit!, you also sank one of their ships! :)" + ) + await next_turn.send( + f"They went for `{raw}`, and it was a hit!\nOne of your ships also got sunk! :(" + ) + elif hit: + await self.turn.send(f"`{raw}` was a hit :)") + await next_turn.send(f"They went for `{raw}`, and it was a hit! :(") + else: + await self.turn.send(f"`{raw}` was a miss :(") + await next_turn.send( + f"They went for `{raw}`, and it was a miss! :)" + ) + + _, f1, _, f2 = await self.get_file(self.player1) + _, f3, _, f4 = await self.get_file(self.player2) + + await self.player1.send(files=[f2, f1]) + await self.player2.send(files=[f4, f3]) + self.turn = next_turn + + if winner := self.who_won(): + await winner.send("Congrats, you won! :)") + + other = self.player2 if winner == self.player1 else self.player1 + await other.send("You lost, better luck next time :(") + break + + return self.message1, self.message2 diff --git a/bot/games/button_games/__init__.py b/bot/games/button_games/__init__.py new file mode 100644 index 0000000..cd52ba1 --- /dev/null +++ b/bot/games/button_games/__init__.py @@ -0,0 +1,50 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +"""This folder contains games that require discord.py v2.0.0 + to be used +they utilize UI components such as buttons. +""" +from __future__ import annotations + +#from .aki_buttons import BetaAkinator +from .twenty_48_buttons import BetaTwenty48 +from .wordle_buttons import BetaWordle +from .tictactoe_buttons import BetaTictactoe +from .memory_game import MemoryGame +from .rps_buttons import BetaRockPaperScissors +#from .hangman_buttons import BetaHangman +from .reaction_test_buttons import BetaReactionGame +from .country_guess_buttons import BetaCountryGuesser +from .chess_buttons import BetaChess +from .battleship_buttons import BetaBattleShip +from .number_slider import NumberSlider +from .lights_out import LightsOut +#from .boggle import Boggle +from .connect_four_buttons import BetaConnectFour + + +__all__: tuple[str, ...] = ( + "BetaConnectFour", + "BetaTwenty48", + "BetaWordle", + "BetaTictactoe", + "MemoryGame", + "BetaRockPaperScissors", + "BetaReactionGame", + "BetaCountryGuesser", + "BetaChess", + "BetaBattleShip", + "NumberSlider", + "LightsOut", +) diff --git a/bot/games/button_games/battleship_buttons.py b/bot/games/button_games/battleship_buttons.py new file mode 100644 index 0000000..63a079f --- /dev/null +++ b/bot/games/button_games/battleship_buttons.py @@ -0,0 +1,517 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import Optional, Any, Coroutine, Union +import asyncio +import string + +import discord +from discord.ext import commands +from utils.emoji import TARGET +from utils.emoji import TARGET + +from ..battleship import ( + BattleShip, + SHIPS, + Ship, + Board, +) + +from .wordle_buttons import WordInputButton +from ..utils import DiscordColor, DEFAULT_COLOR, BaseView + + +class Player: + def __init__(self, player: discord.User, *, game: BetaBattleShip) -> None: + self.game = game + self.player = player + + self.embed = discord.Embed(title="Log", description="```\n\u200b\n```") + + self._logs: list[str] = [] + self.log: str = "" + + self.approves_cancel: bool = False + + def update_log(self, log: str) -> None: + self._logs.append(log) + log_str = "\n\n".join(self._logs[-self.game.max_log_size :]) + + if len(self._logs) > self.game.max_log_size: + log_str = "...\n\n" + log_str + + self.embed.description = f"```diff\n{log_str}\n```" + + def __getattribute__(self, name: str) -> Any: + try: + return super().__getattribute__(name) + except AttributeError: + return self.player.__getattribute__(name) + + +class BattleshipInput(discord.ui.Modal, title="Input a coordinate"): + def __init__(self, view: BattleshipView) -> None: + super().__init__() + self.view = view + + self.coord = discord.ui.TextInput( + label="Enter your target coordinate", + placeholder="ex: a8", + style=discord.TextStyle.short, + required=True, + min_length=2, + max_length=3, + ) + + self.add_item(self.coord) + + async def on_submit(self, interaction: discord.Interaction) -> None: + game = self.view.game + content = self.coord.value + content = content.strip().lower() + + if not game.inputpat.fullmatch(content): + return await interaction.response.send_message( + f"`{content}` is not a valid coordinate!", ephemeral=True + ) + else: + raw, coords = game.get_coords(content) + self.view.update_views() + + if coords in self.view.player_board.moves: + return await interaction.response.send_message( + "You've attacked this coordinate before!", ephemeral=True + ) + else: + await interaction.response.defer() + return await game.process_move(raw, coords) + + +class BattleshipButton(WordInputButton): + view: BattleshipView + + async def callback(self, interaction: discord.Interaction) -> None: + game = self.view.game + + if self.label == "Cancel": + player = self.view.player + other_player = ( + game.player2 + if interaction.user == game.player1.player + else game.player1 + ) + + if not player.approves_cancel: + player.approves_cancel = True + + await interaction.response.defer() + + if not other_player.approves_cancel: + await player.send("- Waiting for opponent to approve cancellation -") + await other_player.send( + "Opponent wants to cancel, press the `Cancel` button if you approve." + ) + else: + game.view1.disable_all() + game.view2.disable_all() + + await game.player1.send("**GAME OVER**, Cancelled") + await game.player2.send("**GAME OVER**, Cancelled") + + await game.message1.edit(view=game.view1) + await game.message2.edit(view=game.view2) + + game.view1.stop() + return game.view2.stop() + else: + if interaction.user != game.turn.player: + return await interaction.response.send_message( + "It is not your turn yet!", ephemeral=True + ) + else: + return await interaction.response.send_modal(BattleshipInput(self.view)) + + +class CoordButton(discord.ui.Button["BattleshipView"]): + def __init__(self, letter_or_num: Union[str, int]) -> None: + super().__init__( + label=str(letter_or_num), + style=discord.ButtonStyle.green, + ) + + async def callback(self, interaction: discord.Interaction) -> None: + game = self.view.game + + if self.label.isdigit(): + self.view.digit = int(self.label) + + raw = self.view.alpha + str(self.view.digit) + coords = (game.to_num(self.view.alpha), self.view.digit) + await interaction.response.defer() + + self.view.alpha = None + self.view.digit = None + + self.view.update_views() + return await game.process_move(raw, coords) + else: + self.view.alpha = self.label.lower() + self.view.initialize_view(clear=True) + return await interaction.response.edit_message(view=self.view) + + +class BattleshipView(BaseView): + def __init__(self, game: BetaBattleShip, user: Player, *, timeout: float) -> None: + super().__init__(timeout=timeout) + + self.game = game + self.player = user + self.player_board = self.game.get_board(self.player) + + self.initialize_view(start=True) + + self.alpha: Optional[str] = None + self.digit: Optional[int] = None + + def disable(self) -> None: + self.disable_all() + self.children[-1].disabled = False + + def update_views(self) -> None: + game = self.game + self.disable() + + other_view = game.view1 if game.turn == game.player2 else game.view2 + + other_view.clear_items() + other_view.initialize_view() + + def initialize_view(self, *, clear: bool = False, start: bool = False) -> None: + moves = self.player_board.moves + + if clear: + self.clear_items() + for num in range(1, 11): + button = CoordButton(num) + coord = (self.game.to_num(self.alpha), num) + if coord in moves: + button.disabled = True + self.add_item(button) + else: + for letter in string.ascii_uppercase[:10]: + button = CoordButton(letter) + if all( + (self.game.to_num(letter.lower()), i) in moves for i in range(1, 11) + ): + button.disabled = True + self.add_item(button) + + inpbutton = BattleshipButton() + inpbutton.label = "\u200b" + inpbutton.emoji = TARGET + + self.add_item(inpbutton) + self.add_item(BattleshipButton(cancel_button=True)) + + if start and self.player == self.game.player2: + self.disable() + + +class SetupInput(discord.ui.Modal): + def __init__(self, button: SetupButton) -> None: + self.button = button + self.ship = self.button.label + + super().__init__(title=f"{self.ship} Setup") + + self.start_coord = discord.ui.TextInput( + label=f"Enter the starting coordinate", + placeholder="ex: a8", + style=discord.TextStyle.short, + required=True, + min_length=2, + max_length=3, + ) + + self.is_vertical = discord.ui.TextInput( + label=f"Do you want it to be vertical? (y/n)", + placeholder='"y" or "n"', + style=discord.TextStyle.short, + required=True, + min_length=1, + max_length=1, + ) + + self.add_item(self.start_coord) + self.add_item(self.is_vertical) + + async def on_submit(self, interaction: discord.Interaction) -> None: + game = self.button.view.game + + start = self.start_coord.value.strip().lower() + vertical = self.is_vertical.value.strip().lower() + + board = game.get_board(interaction.user) + + if not game.inputpat.match(start): + return await interaction.response.send_message( + f"{start} is not a valid coordinate!", ephemeral=True + ) + + if vertical not in ("y", "n"): + return await interaction.response.send_message( + f"Response for `vertical` must be either `y` or `n`", ephemeral=True + ) + + vertical = vertical != "y" + + _, start = game.get_coords(start) + + new_ship = Ship( + name=self.ship, + size=self.button.ship_size, + start=start, + vertical=vertical, + color=self.button.ship_color, + ) + + if board._is_valid(new_ship): + self.button.disabled = True + board.ships.append(new_ship) + + embed, file, _, _ = await game.get_file(interaction.user, hide=False) + + await interaction.response.edit_message( + attachments=[file], embed=embed, view=self.button.view + ) + + if all( + button.disabled + for button in self.button.view.children + if isinstance(button, discord.ui.Button) + ): + await interaction.user.send( + "**All setup!** (Game will soon start after the opponent finishes)" + ) + return self.button.view.stop() + else: + return await interaction.response.send_message( + "Ship placement was detected to be invalid, please try again.", + ephemeral=True, + ) + + +class SetupButton(discord.ui.Button["SetupView"]): + def __init__( + self, label: str, ship_size: int, ship_color: tuple[int, int, int] + ) -> None: + super().__init__( + label=label, + style=discord.ButtonStyle.green, + ) + + self.ship_size = ship_size + self.ship_color = ship_color + + async def callback(self, interaction: discord.Interaction) -> None: + await interaction.response.send_modal(SetupInput(self)) + + +class SetupView(BaseView): + def __init__(self, game: BetaBattleShip, timeout: float) -> None: + super().__init__(timeout=timeout) + + self.game = game + + for ship, (size, color) in SHIPS.items(): + self.add_item(SetupButton(ship, size, color)) + + +class BetaBattleShip(BattleShip): + """ + BattleShip(buttons) Game + """ + + embed: discord.Embed + + def __init__( + self, + player1: discord.User, + player2: discord.User, + *, + random: bool = True, + ) -> None: + + super().__init__(player1, player2, random=random) + + self.player1: Player = Player(player1, game=self) + self.player2: Player = Player(player2, game=self) + + self.turn: Player = self.player1 + + def get_board(self, player: discord.User, other: bool = False) -> Board: + player = getattr(player, "player", player) + if other: + return ( + self.player2_board + if player == self.player1.player + else self.player1_board + ) + else: + return ( + self.player1_board + if player == self.player1.player + else self.player2_board + ) + + async def get_ship_inputs(self, user: Player) -> Coroutine[Any, Any, bool]: + embed, file, _, _ = await self.get_file(user) + + embed1 = discord.Embed( + description="**Press the buttons to place your ships!**", + color=self.embed_color, + ) + + view = SetupView(self, timeout=self.timeout) + await user.send(file=file, embeds=[embed, embed1], view=view) + + return view.wait() + + async def process_move(self, raw: str, coords: tuple[int, int]): + sunk, hit = self.place_move(self.turn, coords) + next_turn = self.player2 if self.turn == self.player1 else self.player1 + + if hit and sunk: + self.turn.update_log( + f"+ ({raw}) was a hit!, you also sank one of their ships! :)" + ) + next_turn.update_log( + f"- They went for ({raw}), and it was a hit!\n- One of your ships also got sunk! :(" + ) + elif hit: + self.turn.update_log(f"+ ({raw}) was a hit :)") + next_turn.update_log(f"- They went for ({raw}), and it was a hit! :(") + else: + self.turn.update_log(f"- ({raw}) was a miss :(") + next_turn.update_log(f"+ They went for ({raw}), and it was a miss! :)") + + e1, f1, e2, f2 = await self.get_file(self.player1) + e3, f3, e4, f4 = await self.get_file(self.player2) + + self.turn = next_turn + + self.player1.embed.set_field_at( + 0, name="\u200b", value=f"```yml\nturn: {self.turn.player}\n```" + ) + self.player2.embed.set_field_at( + 0, name="\u200b", value=f"```yml\nturn: {self.turn.player}\n```" + ) + + await self.message1.edit( + view=self.view1, + content="**Battleship**", + embeds=[e2, e1, self.player1.embed], + attachments=[f2, f1], + ) + await self.message2.edit( + view=self.view2, + content="**Battleship**", + embeds=[e4, e3, self.player2.embed], + attachments=[f4, f3], + ) + + if winner := self.who_won(): + await winner.send("Congrats, you won! :)") + + other = self.player2 if winner == self.player1 else self.player1 + await other.send("You lost, better luck next time :(") + + self.view1.stop() + return self.view2.stop() + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + max_log_size: int = 10, + embed_color: DiscordColor = DEFAULT_COLOR, + timeout: Optional[float] = None, + ) -> tuple[discord.Message, discord.Message]: + """ + starts the battleship(buttons) game + + Parameters + ---------- + ctx : commands.Context[commands.Bot] + the context of the invokation command + max_log_size : int, optional + indicates the length of the move log to show, by default 10 + embed_color : DiscordColor, optional + the color of the game embed, by default DEFAULT_COLOR + timeout : Optional[float], optional + the timeout for the view, by default None + + Returns + ------- + tuple[discord.Message, discord.Message] + returns the game messages respectively + """ + self.max_log_size = max_log_size + self.timeout = timeout + self.embed_color = embed_color + + await ctx.send("**Game Started!**\nI've setup the boards in your dms!") + + if not self.random: + await asyncio.gather( + await self.get_ship_inputs(self.player1), + await self.get_ship_inputs(self.player2), + ) + + self.player1.embed.color = self.embed_color + self.player2.embed.color = self.embed_color + + e1, f1, e2, f2 = await self.get_file(self.player1) + e3, f3, e4, f4 = await self.get_file(self.player2) + + self.view1 = BattleshipView(self, user=self.player1, timeout=timeout) + self.view2 = BattleshipView(self, user=self.player2, timeout=timeout) + + self.player1.embed.add_field( + name="\u200b", value=f"```yml\nturn: {self.turn.player}\n```" + ) + self.player2.embed.add_field( + name="\u200b", value=f"```yml\nturn: {self.turn.player}\n```" + ) + + self.message1 = await self.player1.send( + content="**Game starting!**", + view=self.view1, + embeds=[e2, e1, self.player1.embed], + files=[f2, f1], + ) + self.message2 = await self.player2.send( + content="**Game starting!**", + view=self.view2, + embeds=[e4, e3, self.player2.embed], + files=[f4, f3], + ) + + await asyncio.gather( + self.view1.wait(), + self.view2.wait(), + ) + return self.message1, self.message2 diff --git a/bot/games/button_games/chess_buttons.py b/bot/games/button_games/chess_buttons.py new file mode 100644 index 0000000..e3b18aa --- /dev/null +++ b/bot/games/button_games/chess_buttons.py @@ -0,0 +1,154 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import Optional +import discord +from discord.ext import commands + +from ..chess_game import Chess +from .wordle_buttons import WordInputButton +from ..utils import DiscordColor, DEFAULT_COLOR, BaseView + + +class ChessInput(discord.ui.Modal, title="Make your move"): + def __init__(self, view: ChessView) -> None: + super().__init__() + self.view = view + + self.move_from = discord.ui.TextInput( + label="from coordinate", + style=discord.TextStyle.short, + required=True, + min_length=2, + max_length=2, + ) + + self.move_to = discord.ui.TextInput( + label="to coordinate", + style=discord.TextStyle.short, + required=True, + min_length=2, + max_length=2, + ) + + self.add_item(self.move_from) + self.add_item(self.move_to) + + async def on_submit(self, interaction: discord.Interaction) -> discord.Message: + game = self.view.game + from_coord = self.move_from.value.strip().lower() + to_coord = self.move_to.value.strip().lower() + + uci = from_coord + to_coord + + try: + is_valid_uci = game.board.parse_uci(uci) + except ValueError: + is_valid_uci = False + + if not is_valid_uci: + return await interaction.response.send_message( + f"Invalid coordinates for move: `{from_coord} -> {to_coord}`", + ephemeral=True, + ) + else: + await game.place_move(uci) + + if game.board.is_game_over(): + self.view.disable_all() + embed = await game.fetch_results() + self.view.stop() + else: + embed = await game.make_embed() + + return await interaction.response.edit_message(embed=embed, view=self.view) + + +class ChessButton(WordInputButton): + view: ChessView + + async def callback(self, interaction: discord.Interaction) -> None: + game = self.view.game + if interaction.user not in (game.black, game.white): + return await interaction.response.send_message( + "You are not part of this game!", ephemeral=True + ) + else: + if self.label == "Cancel": + self.view.disable_all() + await interaction.message.edit(view=self.view) + await interaction.response.send_message(f"**Game Over!** Cancelled") + return self.view.stop() + else: + if interaction.user != game.turn: + return await interaction.response.send_message( + "It is not your turn yet!", ephemeral=True + ) + else: + return await interaction.response.send_modal(ChessInput(self.view)) + + +class ChessView(BaseView): + def __init__(self, game: BetaChess, *, timeout: float) -> None: + super().__init__(timeout=timeout) + + self.game = game + + inpbutton = ChessButton() + inpbutton.label = "Make your move!" + + self.add_item(inpbutton) + self.add_item(ChessButton(cancel_button=True)) + + +class BetaChess(Chess): + """ + Chess(buttons) Game + """ + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + embed_color: DiscordColor = DEFAULT_COLOR, + timeout: Optional[float] = None, + ) -> discord.Message: + """ + starts the Chess(buttons) Game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + embed_color : DiscordColor, optional + the color of the game embed, by default DEFAULT_COLOR + timeout : Optional[float], optional + the timeout for the view, by default None + + Returns + ------- + discord.Message + returns the game message + """ + self.embed_color = discord.Color.random() + + embed = await self.make_embed() + self.view = ChessView(self, timeout=timeout) + + self.message = await ctx.send(embed=embed, view=self.view) + + await self.view.wait() + return self.message diff --git a/bot/games/button_games/connect_four_buttons.py b/bot/games/button_games/connect_four_buttons.py new file mode 100644 index 0000000..3320605 --- /dev/null +++ b/bot/games/button_games/connect_four_buttons.py @@ -0,0 +1,127 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import Optional + +import discord +from discord.ext import commands + +from ..connect_four import ConnectFour, BLANK +from ..utils import * + + +class ConnectFourButton(discord.ui.Button["ConnectFourView"]): + def __init__(self, number: int, style: discord.ButtonStyle) -> None: + self.number = number + + super().__init__( + label=str(self.number), + style=style, + ) + + async def callback(self, interaction: discord.Interaction) -> None: + game = self.view.game + + if interaction.user not in (game.red_player, game.blue_player): + return await interaction.response.send_message( + "You are not part of this game!", ephemeral=True + ) + + if interaction.user != game.turn: + return await interaction.response.send_message( + "It is not your turn yet!", ephemeral=True + ) + + if game.board[0][self.number - 1] != BLANK: + return await interaction.response.send_message( + "Selected column is full!", ephemeral=True + ) + + game.place_move(self.number - 1, interaction.user) + + status = game.is_game_over() + + embed = game.make_embed(status=status) + + if status: + self.view.disable_all() + self.view.stop() + + return await interaction.response.edit_message( + view=self.view, + embed=embed, + content=game.board_string(), + ) + + +class ConnectFourView(BaseView): + game: ConnectFour + + def __init__(self, game: BetaConnectFour, timeout: float) -> None: + super().__init__(timeout=timeout) + + self.game = game + + for i in range(1, 8): + self.add_item(ConnectFourButton(i, self.game.button_style)) + + +class BetaConnectFour(ConnectFour): + """ + Connect-4(buttons) Game + """ + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + timeout: Optional[float] = None, + button_style: discord.ButtonStyle = discord.ButtonStyle.blurple, + embed_color: DiscordColor = DEFAULT_COLOR, + ) -> discord.Message: + """ + starts the Connect-4(buttons) game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + timeout : Optional[float], optional + the timeout for when waiting, by default None + button_style : discord.ButtonStyle, optional + the primary button style to use, by default discord.ButtonStyle.red + embed_color : DiscordColor, optional + the color of the game embed, by default DEFAULT_COLOR + + Returns + ------- + discord.Message + returns the game message + """ + self.embed_color = discord.Color.random() + self.button_style = button_style + + self.view = ConnectFourView(self, timeout=timeout) + + embed = self.make_embed(status=False) + self.message = await ctx.send( + content=self.board_string(), + view=self.view, + embed=embed, + ) + + await self.view.wait() + return self.message diff --git a/bot/games/button_games/country_guess_buttons.py b/bot/games/button_games/country_guess_buttons.py new file mode 100644 index 0000000..d7fec76 --- /dev/null +++ b/bot/games/button_games/country_guess_buttons.py @@ -0,0 +1,180 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import Optional + +import discord +from discord.ext import commands + +from ..country_guess import CountryGuesser +from ..utils import DiscordColor, DEFAULT_COLOR, BaseView + + +class CountryInput(discord.ui.Modal, title="Input your guess!"): + def __init__(self, view: CountryView) -> None: + super().__init__() + self.view = view + + self.guess = discord.ui.TextInput( + label="Input your guess", + style=discord.TextStyle.short, + required=True, + max_length=self.view.game.accepted_length, + ) + + self.add_item(self.guess) + + async def on_submit(self, interaction: discord.Interaction) -> None: + guess = self.guess.value.strip().lower() + game = self.view.game + + if guess == game.country: + game.update_guesslog("+ GAME OVER, you won! +") + await interaction.response.send_message( + f"That is correct! The country was `{game.country.title()}`" + ) + + self.view.disable_all() + game.embed.description = f"```fix\n{game.country.title()}\n```" + await interaction.message.edit(view=self.view, embed=game.embed) + return self.view.stop() + else: + game.guesses -= 1 + + if not game.guesses: + self.view.disable_all() + game.update_guesslog("- GAME OVER, you lost -") + + await interaction.message.edit(embed=game.embed, view=self.view) + await interaction.response.send_message( + f"Game Over! you lost, The country was `{game.country.title()}`" + ) + return self.view.stop() + else: + acc = game.get_accuracy(guess) + game.update_guesslog( + f"- [{guess}] was incorrect! but you are ({acc}%) of the way there!\n" + f"+ You have {game.guesses} guesses left.\n" + ) + + await interaction.response.edit_message(embed=game.embed) + + +class CountryView(BaseView): + def __init__( + self, game: BetaCountryGuesser, *, user: discord.User, timeout: float + ) -> None: + super().__init__(timeout=timeout) + + self.game = game + self.user = user + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user != self.user: + await interaction.response.send_message( + f"This is not your game!", ephemeral=True + ) + return False + else: + return True + + @discord.ui.button(label="Make a guess!", style=discord.ButtonStyle.blurple) + async def guess_button(self, interaction: discord.Interaction, _) -> None: + return await interaction.response.send_modal(CountryInput(self)) + + @discord.ui.button(label="hint", style=discord.ButtonStyle.green) + async def hint_button( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + hint = self.game.get_hint() + self.game.hints -= 1 + await interaction.response.send_message( + f"Here is your hint: `{hint}`", ephemeral=True + ) + + if not self.game.hints: + button.disabled = True + await interaction.message.edit(view=self) + + @discord.ui.button(label="Cancel", style=discord.ButtonStyle.red) + async def cancel_button(self, interaction: discord.Interaction, _) -> None: + self.disable_all() + + self.game.embed.description = f"```fix\n{self.game.country.title()}\n```" + self.game.update_guesslog("- GAME OVER, CANCELLED -") + + await interaction.response.send_message( + f"Game Over! The country was `{self.game.country.title()}`" + ) + await interaction.message.edit(view=self, embed=self.game.embed) + return self.stop() + + +class BetaCountryGuesser(CountryGuesser): + """ + Country Guesser(buttons) Game + """ + + guesslog: str = "" + + def update_guesslog(self, entry: str) -> None: + self.guesslog += entry + "\n" + self.embed.set_field_at( + 1, name="Guess Log", value=f"```diff\n{self.guesslog}\n```" + ) + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + embed_color: DiscordColor = DEFAULT_COLOR, + ignore_diff_len: bool = False, + timeout: Optional[float] = None, + ) -> discord.Message: + """ + starts the Country Guesser(buttons) Game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + embed_color : DiscordColor, optional + the color of the game embed, by default DEFAULT_COLOR + ignore_diff_len : bool, optional + specifies whether or not to ignore guesses that are not of the same length as the correct answer, by default False + timeout : Optional[float], optional + the timeout for the view, by default None + + Returns + ------- + discord.Message + returns the game message + """ + self.accepted_length = len(self.country) if ignore_diff_len else None + + file = await self.get_country() + + self.embed_color = discord.Color.random() + self.embed = self.get_embed() + self.embed.add_field( + name="Guess Log", value="```diff\n\u200b\n```", inline=False + ) + + self.view = CountryView(self, user=ctx.author, timeout=timeout) + self.message = await ctx.send(embed=self.embed, file=file, view=self.view) + + await self.view.wait() + return self.message diff --git a/bot/games/button_games/lights_out.py b/bot/games/button_games/lights_out.py new file mode 100644 index 0000000..121c178 --- /dev/null +++ b/bot/games/button_games/lights_out.py @@ -0,0 +1,182 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +import random +from typing import Any, TYPE_CHECKING, Optional, Literal, Final + +import discord +from discord.ext import commands + +from .number_slider import SlideView +from ..utils import * + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + + Board: TypeAlias = list[list[Optional[Literal["💡"]]]] + +BULB: Final[Literal["💡"]] = "💡" + + +class LightsOutButton(discord.ui.Button["LightsOutView"]): + def __init__( + self, emoji: str, *, style: discord.ButtonStyle, row: int, col: int + ) -> None: + super().__init__( + emoji=emoji, + label="\u200b", + style=style, + row=row, + ) + + self.col = col + + async def callback(self, interaction: discord.Interaction) -> None: + game = self.view.game + + if interaction.user != game.player: + return await interaction.response.send_message( + "This is not your game!", ephemeral=True + ) + else: + row, col = self.row, self.col + + beside_item = game.beside_item(row, col) + game.toggle(row, col) + + for i, j in beside_item: + game.toggle(i, j) + + self.view.update_board(clear=True) + + game.moves += 1 + game.embed.set_field_at(0, name="\u200b", value=f"Moves: `{game.moves}`") + + if game.tiles == game.completed: + self.view.disable_all() + self.view.stop() + game.embed.description = "**Congrats! You won!**" + + return await interaction.response.edit_message( + embed=game.embed, view=self.view + ) + + +class LightsOutView(SlideView): + game: LightsOut + + def __init__(self, game: LightsOut, *, timeout: float) -> None: + super().__init__(game, timeout=timeout) + + def update_board(self, *, clear: bool = False) -> None: + + if clear: + self.clear_items() + + for i, row in enumerate(self.game.tiles): + for j, tile in enumerate(row): + button = LightsOutButton( + emoji=tile, + style=self.game.button_style, + row=i, + col=j, + ) + self.add_item(button) + + +class LightsOut: + """ + Lights Out Game + """ + + def __init__(self, count: Literal[1, 2, 3, 4, 5] = 4) -> None: + + if count not in range(1, 6): + raise ValueError("Count must be an integer between 1 and 5") + + self.moves: int = 0 + self.count = count + + self.completed: Final[Board] = [[None] * self.count for _ in range(self.count)] + self.tiles: Board = [] + + self.player: Optional[discord.User] = None + self.button_style: discord.ButtonStyle = discord.ButtonStyle.green + + def toggle(self, row: int, col: int) -> None: + self.tiles[row][col] = BULB if self.tiles[row][col] is None else None + + def beside_item(self, row: int, col: int) -> list[tuple[int, int]]: + beside = [ + (row - 1, col), + (row, col - 1), + (row + 1, col), + (row, col + 1), + ] + + data = [ + (i, j) + for i, j in beside + if i in range(self.count) and j in range(self.count) + ] + return data + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + button_style: discord.ButtonStyle = discord.ButtonStyle.green, + embed_color: DiscordColor = DEFAULT_COLOR, + timeout: Optional[float] = None, + ) -> discord.Message: + """ + starts the Lights Out Game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + button_style : discord.ButtonStyle, optional + the primary button style to use, by default discord.ButtonStyle.green + embed_color : DiscordColor, optional + the color of the game embed, by default DEFAULT_COLOR + timeout : Optional[float], optional + the timeout for the view, by default None + + Returns + ------- + discord.Message + returns the game message + """ + self.button_style = button_style + self.player = ctx.author + + self.tiles = random.choices((None, BULB), k=self.count**2) + self.tiles = chunk(self.tiles, count=self.count) + + self.view = LightsOutView(self, timeout=timeout) + self.embed = discord.Embed( + description="Turn off all the tiles!", color=embed_color + ) + self.embed.add_field(name="\u200b", value="Moves: `0`") + + self.message = await ctx.send(embed=self.embed, view=self.view) + + await double_wait( + wait_for_delete(ctx, self.message), + self.view.wait(), + ) + return self.message diff --git a/bot/games/button_games/memory_game.py b/bot/games/button_games/memory_game.py new file mode 100644 index 0000000..7498f23 --- /dev/null +++ b/bot/games/button_games/memory_game.py @@ -0,0 +1,195 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import Optional, ClassVar +import random +import asyncio + +import discord +from discord.ext import commands + +from ..utils import * + + +class MemoryButton(discord.ui.Button["MemoryView"]): + def __init__(self, emoji: str, *, style: discord.ButtonStyle, row: int = 0) -> None: + self.value = emoji + + super().__init__( + label="\u200b", + style=style, + row=row, + ) + + async def callback(self, interaction: discord.Interaction) -> None: + game = self.view.game + + if opened := self.view.opened: + game.moves += 1 + game.embed.set_field_at(0, name="\u200b", value=f"Moves: `{game.moves}`") + + self.emoji = self.value + self.disabled = True + await interaction.response.edit_message(view=self.view) + + if opened.value != self.value: + await asyncio.sleep(self.view.pause_time) + + opened.emoji = None + opened.disabled = False + + self.emoji = None + self.disabled = False + self.view.opened = None + else: + self.view.opened = None + + if all( + button.disabled + for button in self.view.children + if isinstance(button, discord.ui.Button) + ): + await interaction.message.edit( + content="Game Over, Congrats!", view=self.view + ) + return self.view.stop() + + return await interaction.message.edit(view=self.view, embed=game.embed) + else: + self.emoji = self.value + self.view.opened = self + self.disabled = True + return await interaction.response.edit_message(view=self.view) + + +class MemoryView(BaseView): + board: list[list[str]] + DEFAULT_ITEMS: ClassVar[list[str]] = [ + "🥝", + "🍓", + "🍹", + "🍋", + "🥭", + "🍎", + "🍊", + "🍍", + "🍑", + "🍇", + "🍉", + "🥬", + ] + + def __init__( + self, + game: MemoryGame, + items: list[str], + *, + button_style: discord.ButtonStyle, + pause_time: float, + timeout: Optional[float] = None, + ) -> None: + + super().__init__(timeout=timeout) + + self.game = game + + self.button_style = button_style + self.pause_time = pause_time + self.opened: Optional[MemoryButton] = None + + if not items: + items = self.DEFAULT_ITEMS[:] + assert len(items) == 12 + + items *= 2 + random.shuffle(items) + random.shuffle(items) + items.insert(12, None) + + self.board = chunk(items, count=5) + + for i, row in enumerate(self.board): + for item in row: + button = MemoryButton(item, style=self.button_style, row=i) + + if not item: + button.disabled = True + self.add_item(button) + + +class MemoryGame: + """ + Memory Game + """ + + def __init__(self) -> None: + self.embed_color: Optional[DiscordColor] = None + self.embed: Optional[discord.Embed] = None + self.moves: int = 0 + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + embed_color: DiscordColor = DEFAULT_COLOR, + items: list[str] = [], + pause_time: float = 0.7, + button_style: discord.ButtonStyle = discord.ButtonStyle.red, + timeout: Optional[float] = None, + ) -> discord.Message: + """ + starts the memory game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + embed_color : DiscordColor, optional + the color of the game embed, by default DEFAULT_COLOR + items : list[str], optional + items to use for the game tiles, by default [] + pause_time : float, optional + specifies the duration to pause for before hiding the tiles again, by default 0.7 + button_style : discord.ButtonStyle, optional + the primary button style to use, by default discord.ButtonStyle.red + timeout : Optional[float], optional + the timeout for the view, by default None + + Returns + ------- + discord.Message + returns the game message + """ + self.embed_color = discord.Color.random() + self.embed = discord.Embed( + description="**Memory Game**", color=discord.Color.random() + ) + self.embed.add_field(name="\u200b", value="Moves: `0`") + + self.view = MemoryView( + game=self, + items=items, + button_style=button_style, + pause_time=pause_time, + timeout=timeout, + ) + self.message = await ctx.send(embed=self.embed, view=self.view) + + await double_wait( + wait_for_delete(ctx, self.message), + self.view.wait(), + ) + return self.message diff --git a/bot/games/button_games/number_slider.py b/bot/games/button_games/number_slider.py new file mode 100644 index 0000000..2ccafca --- /dev/null +++ b/bot/games/button_games/number_slider.py @@ -0,0 +1,210 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional, Literal +import random + +import discord +from discord.ext import commands + +from ..utils import * + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + + Board: TypeAlias = list[list[Optional[int]]] + + +class SlideButton(discord.ui.Button["SlideView"]): + def __init__(self, label: str, *, style: discord.ButtonStyle, row: int) -> None: + super().__init__( + label=label, + style=style, + row=row, + ) + + if label == "\u200b": + self.disabled = True + + async def callback(self, interaction: discord.Interaction) -> None: + game = self.view.game + + if interaction.user != game.player: + return await interaction.response.send_message( + "This is not your game!", ephemeral=True + ) + else: + num = int(self.label) + + if num not in game.beside_blank(): + return await interaction.response.defer() + else: + ix, iy = game.get_item(num) + nx, ny = game.get_item() + + game.numbers[nx][ny], game.numbers[ix][iy] = ( + game.numbers[ix][iy], + game.numbers[nx][ny], + ) + + self.view.update_board(clear=True) + + game.moves += 1 + game.embed.set_field_at( + 0, name="\u200b", value=f"Moves: `{game.moves}`" + ) + + if game.numbers == game.completed: + self.view.disable_all() + self.view.stop() + game.embed.description = "**Congrats! You won!**" + + return await interaction.response.edit_message( + embed=game.embed, view=self.view + ) + + +class SlideView(BaseView): + def __init__(self, game: NumberSlider, *, timeout: float) -> None: + super().__init__(timeout=timeout) + + self.game = game + + self.update_board() + + def update_board(self, *, clear: bool = False) -> None: + + if clear: + self.clear_items() + + for i, row in enumerate(self.game.numbers): + for j, number in enumerate(row): + if number == self.game.completed[i][j]: + style = self.game.correct_style + else: + style = self.game.wrong_style + + button = SlideButton( + label=number or "\u200b", + style=style, + row=i, + ) + self.add_item(button) + + +class NumberSlider: + """ + Number Slider Game + """ + + def __init__(self, count: Literal[1, 2, 3, 4, 5] = 4) -> None: + + if count not in range(1, 6): + raise ValueError("Count must be an integer between 1 and 5") + + self.all_numbers = list(range(1, count**2)) + + self.player: Optional[discord.User] = None + + self.moves: int = 0 + self.count = count + self.numbers: Board = [] + self.completed: Board = [] + + self.wrong_style: discord.ButtonStyle = discord.ButtonStyle.gray + self.correct_style: discord.ButtonStyle = discord.ButtonStyle.green + + def get_item(self, obj: Optional[int] = None) -> tuple[int, int]: + return next( + (x, y) + for x, row in enumerate(self.numbers) + for y, item in enumerate(row) + if item == obj + ) + + def beside_blank(self) -> list[int]: + nx, ny = self.get_item() + + beside_item = [ + (nx - 1, ny), + (nx, ny - 1), + (nx + 1, ny), + (nx, ny + 1), + ] + + data = [ + self.numbers[i][j] + for i, j in beside_item + if i in range(self.count) and j in range(self.count) + ] + return data + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + wrong_style: discord.ButtonStyle = discord.ButtonStyle.gray, + correct_style: discord.ButtonStyle = discord.ButtonStyle.green, + embed_color: DiscordColor = DEFAULT_COLOR, + timeout: Optional[float] = None, + ) -> discord.Message: + """ + starts the number slider game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + wrong_style : discord.ButtonStyle, optional + the button style to use for tiles that are in the wrong spot, by default discord.ButtonStyle.gray + correct_style : discord.ButtonStyle, optional + the button style to use for tiles that are in the right spot, by default discord.ButtonStyle.green + embed_color : DiscordColor, optional + the game embedd color, by default DEFAULT_COLOR + timeout : Optional[float], optional + the timeout for the view, by default None + + Returns + ------- + discord.Message + returns the game message + """ + self.player = ctx.author + self.wrong_style = wrong_style + self.correct_style = correct_style + + numbers = self.all_numbers[:] + random.shuffle(numbers) + random.shuffle(numbers) + + numbers.append(None) + self.numbers = chunk(numbers, count=self.count) + + self.completed = chunk(self.all_numbers + [None], count=self.count) + + self.view = SlideView(self, timeout=timeout) + self.embed = discord.Embed( + description="Slide the tiles back in ascending order!", color=discord.Color.random() + ) + self.embed.add_field(name="\u200b", value="Moves: `0`") + + self.message = await ctx.send(embed=self.embed, view=self.view) + + await double_wait( + wait_for_delete(ctx, self.message), + self.view.wait(), + ) + return self.message diff --git a/bot/games/button_games/reaction_test_buttons.py b/bot/games/button_games/reaction_test_buttons.py new file mode 100644 index 0000000..f9c5a82 --- /dev/null +++ b/bot/games/button_games/reaction_test_buttons.py @@ -0,0 +1,146 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import Optional, Union +import time +import random +import asyncio + +import discord +from discord.ext import commands + +from ..utils import DiscordColor, DEFAULT_COLOR, BaseView + + +class ReactionButton(discord.ui.Button["ReactionView"]): + def __init__(self, style: discord.ButtonStyle) -> None: + super().__init__(label="\u200b", style=style) + + self.edited: bool = False + self.clicked: bool = False + + async def callback(self, interaction: discord.Interaction) -> None: + game = self.view.game + + if game.author_only and interaction.user != game.author: + return await interaction.response.send_message( + "This game is only for the author!", ephemeral=True + ) + + if not self.edited or self.clicked: + return await interaction.response.defer() + else: + end_time = time.perf_counter() + elapsed = end_time - self.view.game.start_time + + game.embed.description = ( + f"{interaction.user.mention} reacted first in `{elapsed:.2f}s` !" + ) + await interaction.response.edit_message(embed=game.embed) + + self.clicked = True + return game.finished_event.set() + + +class ReactionView(BaseView): + game: BetaReactionGame + + def __init__( + self, + game: BetaReactionGame, + *, + button_style: discord.ButtonStyle, + timeout: float, + ) -> None: + + super().__init__(timeout=timeout) + + self.game = game + self.button_style = button_style + self.button = ReactionButton(self.button_style) + self.add_item(self.button) + + +class BetaReactionGame: + """ + Reaction(buttons) game + """ + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + author_only: bool = False, + pause_range: tuple[float, float] = (1.0, 5.0), + start_button_style: discord.ButtonStyle = discord.ButtonStyle.blurple, + end_button_style: Union[ + discord.ButtonStyle, tuple[discord.ButtonStyle, ...] + ] = (discord.ButtonStyle.green, discord.ButtonStyle.red), + embed_color: DiscordColor = DEFAULT_COLOR, + timeout: Optional[float] = None, + ) -> discord.Message: + """ + starts the Reaction(buttons) Game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + author_only : bool, optional + specifies whether or not tjhe view is only limited to the author, by default False + pause_range : tuple[float, float], optional + the time range to randomly pause for, by default (1.0, 5.0) + start_button_style : discord.ButtonStyle, optional + specifies the button style to start with, by default discord.ButtonStyle.blurple + end_button_style : Union[discord.ButtonStyle, tuple[discord.ButtonStyle, ...]], optional + specifies the button styles(s) to change to, by default (discord.ButtonStyle.green, discord.ButtonStyle.red) + embed_color : DiscordColor, optional + the color of the game embed, by default DEFAULT_COLOR + timeout : Optional[float], optional + the timeout for the view, by default None + + Returns + ------- + discord.Message + returns the game message + """ + self.finished_event = asyncio.Event() + + self.author_only = author_only + self.author = ctx.author + + self.embed = discord.Embed( + title="Reaction Game", + description=f"Click the button below, when the button changes color!", + color=discord.Color.random(), + ) + self.view = ReactionView(self, button_style=start_button_style, timeout=timeout) + self.message = await ctx.send(embed=self.embed, view=self.view) + + pause = random.uniform(*pause_range) + await asyncio.sleep(pause) + + if isinstance(end_button_style, tuple): + self.view.button.style = random.choice(end_button_style) + else: + self.view.button.style = end_button_style + + await self.message.edit(view=self.view) + self.start_time = time.perf_counter() + self.view.button.edited = True + + await self.finished_event.wait() + return self.message diff --git a/bot/games/button_games/rps_buttons.py b/bot/games/button_games/rps_buttons.py new file mode 100644 index 0000000..e6bf046 --- /dev/null +++ b/bot/games/button_games/rps_buttons.py @@ -0,0 +1,188 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import Optional +import random + +import discord +from utils.emoji import WARNING_ALT +from discord.ext import commands + +from ..rps import RockPaperScissors +from ..utils import DiscordColor, DEFAULT_COLOR, BaseView + + +class RPSButton(discord.ui.Button["RPSView"]): + def __init__(self, emoji: str, *, style: discord.ButtonStyle) -> None: + super().__init__( + label="\u200b", + emoji=emoji, + style=style, + ) + + def get_choice(self, user: discord.User, other: bool = False) -> Optional[str]: + game = self.view.game + if other: + return game.player2_choice if user == game.player2 else game.player1_choice + else: + return game.player1_choice if user == game.player1 else game.player2_choice + + async def callback(self, interaction: discord.Interaction) -> None: + game = self.view.game + players = (game.player1, game.player2) if game.player2 else (game.player1,) + + if interaction.user not in players: + return await interaction.response.send_message( + "This is not your game!", ephemeral=True + ) + else: + if not game.player2: + bot_choice = random.choice(game.OPTIONS) + user_choice = self.emoji.name + + if user_choice == bot_choice: + game.embed.description = f"**Tie!**\nWe both picked {user_choice}" + else: + if game.check_win(bot_choice, user_choice): + game.embed.description = f"**You Won!**\nYou picked {user_choice} and I picked {bot_choice}." + else: + game.embed.description = f"**You Lost!**\nI picked {bot_choice} and you picked {user_choice}." + + self.view.disable_all() + self.view.stop() + + else: + if self.get_choice(interaction.user): + return await interaction.response.send_message( + "You have already chosen!", ephemeral=True + ) + + other_player_choice = self.get_choice(interaction.user, other=True) + + if interaction.user == game.player1: + game.player1_choice = self.emoji.name + + if not other_player_choice: + game.embed.description += f"\n\n{game.player1.mention} has chosen...\n*Waiting for {game.player2.mention} to choose...*" + else: + game.player2_choice = self.emoji.name + + if not other_player_choice: + game.embed.description += f"\n\n{game.player2.mention} has chosen...\n*Waiting for {game.player1.mention} to choose...*" + + if game.player1_choice and game.player2_choice: + if game.player1_choice == game.player2_choice: + game.embed.description = f"**Tie!**\nBoth {game.player1.mention} and {game.player2.mention} picked {game.player1_choice}." + else: + who_won = ( + game.player1 + if game.check_win(game.player2_choice, game.player1_choice) + else game.player2 + ) + + game.embed.description = ( + f"**{who_won.mention} Won!**" + f"\n\n{game.player1.mention} chose {game.player1_choice}." + f"\n{game.player2.mention} chose {game.player2_choice}." + ) + + self.view.disable_all() + self.view.stop() + + return await interaction.response.edit_message( + embed=game.embed, view=self.view + ) + + +class RPSView(BaseView): + game: BetaRockPaperScissors + + def __init__( + self, + game: BetaRockPaperScissors, + *, + button_style: discord.ButtonStyle, + timeout: float, + ) -> None: + + super().__init__(timeout=timeout) + + self.button_style = button_style + self.game = game + + for option in self.game.OPTIONS: + self.add_item(RPSButton(option, style=self.button_style)) + + +class BetaRockPaperScissors(RockPaperScissors): + """ + RockPaperScissors(buttons) game + """ + + player1: discord.User + embed: discord.Embed + + def __init__(self, other_player: Optional[discord.User] = None) -> None: + self.player2 = other_player + + if self.player2: + self.player1_choice: Optional[str] = None + self.player2_choice: Optional[str] = None + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + button_style: discord.ButtonStyle = discord.ButtonStyle.blurple, + embed_color: DiscordColor = DEFAULT_COLOR, + timeout: Optional[float] = None, + ) -> discord.Message: + if ctx.author == self.player2: + embed = discord.Embed(title=f"{WARNING_ALT} Access Denied", description="You cannot play against yourself!", color=0x000000) + return await ctx.reply(embed=embed) + + """ + Starts the Rock Paper Scissors (buttons) game. + + Parameters + ---------- + ctx : commands.Context + The context of the invoking command. + button_style : discord.ButtonStyle, optional + The primary button style to use, by default discord.ButtonStyle.blurple. + embed_color : DiscordColor, optional + The color of the game embed, by default DEFAULT_COLOR. + timeout : Optional[float], optional + The timeout for the view, by default None. + + Returns + ------- + discord.Message + Returns the game message. + """ + self.player1 = ctx.author + + self.embed = discord.Embed( + title="Rock Paper Scissors", + description="Select a button to play!", + color=discord.Color.random(), + ) + + self.view = RPSView(self, button_style=button_style, timeout=timeout) + self.message = await ctx.send(embed=self.embed, view=self.view) + + await self.view.wait() + return self.message diff --git a/bot/games/button_games/tictactoe_buttons.py b/bot/games/button_games/tictactoe_buttons.py new file mode 100644 index 0000000..09ada97 --- /dev/null +++ b/bot/games/button_games/tictactoe_buttons.py @@ -0,0 +1,142 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import ClassVar, Optional + +import discord +from discord.ext import commands + +from ..tictactoe import Tictactoe +from ..utils import * + + +class TTTButton(discord.ui.Button["TTTView"]): + def __init__(self, label: str, style: discord.ButtonStyle, *, row: int, col: int): + super().__init__( + label=label, + style=style, + row=row, + ) + + self.col = col + + async def callback(self, interaction: discord.Interaction) -> None: + user = interaction.user + game = self.view.game + + if user not in (game.cross, game.circle): + return await interaction.response.send_message( + "You are not part of this game!", ephemeral=True + ) + + if user != game.turn: + return await interaction.response.send_message( + "it is not your turn!", ephemeral=True + ) + + self.label = game.player_to_emoji[user] + self.disabled = True + + game.board[self.row][self.col] = self.label + game.turn = game.circle if user == game.cross else game.cross + + tie = all(button.disabled for button in self.view.children) + + if game_over := game.is_game_over(tie=tie): + if game.winning_indexes: + self.view.disable_all() + game.create_streak() + self.view.stop() + + embed = game.make_embed(game_over=game_over or tie) + await interaction.response.edit_message(embed=embed, view=self.view) + + +class TTTView(BaseView): + def __init__(self, game: BetaTictactoe, *, timeout: float) -> None: + super().__init__(timeout=timeout) + + self.game = game + + for x, row in enumerate(game.board): + for y, square in enumerate(row): + button = TTTButton( + label=square, + style=self.game.button_style, + row=x, + col=y, + ) + self.add_item(button) + + +class BetaTictactoe(Tictactoe): + """ + Tictactoe(buttons) game + """ + + BLANK: ClassVar[str] = "\u200b" + CIRCLE: ClassVar[str] = "O" + CROSS: ClassVar[str] = "X" + + def create_streak(self) -> None: + chunked = chunk(self.view.children, count=3) + for row, col in self.winning_indexes: + button: TTTButton = chunked[row][col] + button.style = self.win_button_style + + async def start( + self, + ctx: commands.Context[commands.Bot], + button_style: discord.ButtonStyle = discord.ButtonStyle.green, + *, + embed_color: DiscordColor = DEFAULT_COLOR, + win_button_style: discord.ButtonStyle = discord.ButtonStyle.red, + timeout: Optional[float] = None, + ) -> discord.Message: + """ + starts the tictactoe(buttons) game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + button_style : discord.ButtonStyle, optional + the primary button style to use, by default discord.ButtonStyle.green + embed_color : DiscordColor, optional + the color of the game embed, by default DEFAULT_COLOR + win_button_style : discord.ButtonStyle, optional + the button style to use to show the winning line, by default discord.ButtonStyle.red + timeout : Optional[float], optional + the timeout for the view, by default None + + Returns + ------- + discord.Message + returns the game message + """ + self.embed_color = discord.Color.random() + self.button_style = button_style + self.win_button_style = win_button_style + + self.view = TTTView(self, timeout=timeout) + self.message = await ctx.send(embed=self.make_embed(), view=self.view) + + await double_wait( + wait_for_delete(ctx, self.message, user=(self.cross, self.circle)), + self.view.wait(), + ) + + return self.message diff --git a/bot/games/button_games/twenty_48_buttons.py b/bot/games/button_games/twenty_48_buttons.py new file mode 100644 index 0000000..feb0039 --- /dev/null +++ b/bot/games/button_games/twenty_48_buttons.py @@ -0,0 +1,151 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import Optional, Literal +import random + +import discord +from discord.ext import commands + +from ..twenty_48 import Twenty48 +from ..utils import DiscordColor, DEFAULT_COLOR, BaseView + + +class Twenty48_Button(discord.ui.Button["BaseView"]): + def __init__(self, game: BetaTwenty48, emoji: str) -> None: + self.game = game + + style = ( + discord.ButtonStyle.red if emoji == "⏹️" else discord.ButtonStyle.blurple + ) + + super().__init__( + style=style, emoji=discord.PartialEmoji(name=emoji), label="\u200b" + ) + + async def callback(self, interaction: discord.Interaction) -> None: + + if interaction.user != self.game.player: + return await interaction.response.send_message( + "This isn't your game!", ephemeral=True + ) + + emoji = str(self.emoji) + + if emoji == "⏹️": + self.view.stop() + return await interaction.message.delete() + + elif emoji == "➡️": + self.game.move_right() + + elif emoji == "⬅️": + self.game.move_left() + + elif emoji == "⬇️": + self.game.move_down() + + elif emoji == "⬆️": + self.game.move_up() + + lost = self.game.spawn_new() + won = self.game.check_win() + + if won or lost: + self.view.disable_all() + self.view.stop() + + if lost: + self.game.embed = discord.Embed( + description="Game Over! You lost.", + color=self.game.embed_color, + ) + + if self.game._render_image: + image = await self.game.render_image() + await interaction.response.edit_message( + attachments=[image], embed=self.game.embed + ) + else: + board_string = self.game.number_to_emoji() + await interaction.response.edit_message( + content=board_string, embed=self.game.embed + ) + + +class BetaTwenty48(Twenty48): + view: discord.ui.View + """ + Twenty48(buttons) game + """ + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + win_at: Literal[2048, 4096, 8192] = 8192, + timeout: Optional[float] = None, + delete_button: bool = False, + embed_color: DiscordColor = DEFAULT_COLOR, + **kwargs, + ) -> discord.Message: + """ + starts the 2048(buttons) game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + win_at : Literal[2048, 4096, 8192], optional + the tile to stop the game / win at, by default 8192 + timeout : Optional[float], optional + the timeout for the view, by default None + delete_button : bool, optional + specifies whether or not to add a stop button, by default False + embed_color : DiscordColor, optional + the color of the game embed, by default DEFAULT_COLOR + + Returns + ------- + discord.Message + returns the game message + """ + self.win_at = win_at + self.embed_color = discord.Color.random() + + self.player = ctx.author + self.view = BaseView(timeout=timeout) + + self.board[random.randrange(4)][random.randrange(4)] = 2 + self.board[random.randrange(4)][random.randrange(4)] = 2 + + if delete_button: + self._controls.append("⏹️") + + for button in self._controls: + self.view.add_item(Twenty48_Button(self, button)) + + if self._render_image: + image = await self.render_image() + self.message = await ctx.send(file=image, view=self.view, **kwargs) + else: + board_string = self.number_to_emoji() + self.message = await ctx.send( + content=board_string, view=self.view, **kwargs + ) + + await self.view.wait() + return self.message diff --git a/bot/games/button_games/wordle_buttons.py b/bot/games/button_games/wordle_buttons.py new file mode 100644 index 0000000..7119309 --- /dev/null +++ b/bot/games/button_games/wordle_buttons.py @@ -0,0 +1,153 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import Optional + +import discord +from discord.ext import commands + +from ..wordle import Wordle +from ..utils import DiscordColor, DEFAULT_COLOR, BaseView + + +class WordInput(discord.ui.Modal, title="Word Input"): + word = discord.ui.TextInput( + label=f"Input your guess", + style=discord.TextStyle.short, + required=True, + min_length=5, + max_length=5, + ) + + def __init__(self, view: WordleView) -> None: + super().__init__() + self.view = view + + async def on_submit(self, interaction: discord.Interaction) -> None: + content = self.word.value.lower() + game = self.view.game + + if content not in game._valid_words: + return await interaction.response.send_message( + "That is not a valid word!", ephemeral=True + ) + else: + won = game.parse_guess(content) + buf = await game.render_image() + + embed = discord.Embed(title="Wordle!", color=self.view.game.embed_color) + embed.set_image(url="attachment://wordle.png") + file = discord.File(buf, "wordle.png") + + if won: + await interaction.message.reply( + "Game Over! You won!", mention_author=True + ) + elif lost := len(game.guesses) >= 6: + await interaction.message.reply( + f"Game Over! You lose, the word was: **{game.word}**", + mention_author=True, + ) + + if won or lost: + self.view.disable_all() + self.view.stop() + + return await interaction.response.edit_message( + embed=embed, attachments=[file], view=self.view + ) + + +class WordInputButton(discord.ui.Button["WordleView"]): + def __init__(self, *, cancel_button: bool = False): + super().__init__( + label="Cancel" if cancel_button else "Make a guess!", + style=discord.ButtonStyle.red + if cancel_button + else discord.ButtonStyle.blurple, + ) + + async def callback(self, interaction: discord.Interaction) -> None: + game = self.view.game + if interaction.user != game.player: + return await interaction.response.send_message( + "This isn't your game!", ephemeral=True + ) + else: + if self.label == "Cancel": + await interaction.response.send_message( + f"Game Over! the word was: **{game.word}**" + ) + await interaction.message.delete() + return self.view.stop() + else: + return await interaction.response.send_modal(WordInput(self.view)) + + +class WordleView(BaseView): + def __init__(self, game: BetaWordle, *, timeout: float): + super().__init__(timeout=timeout) + + self.game = game + self.add_item(WordInputButton()) + self.add_item(WordInputButton(cancel_button=True)) + + +class BetaWordle(Wordle): + player: discord.User + """ + Wordle(buttons) game + """ + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + embed_color: DiscordColor = DEFAULT_COLOR, + timeout: Optional[float] = None, + ) -> discord.Message: + """ + starts the wordle(buttons) game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + embed_color : DiscordColor, optional + the color of the game embed, by default DEFAULT_COLOR + timeout : Optional[float], optional + the timeout for the view, by default None + + Returns + ------- + discord.Message + returns the game message + """ + self.embed_color = discord.Color.random() + self.player = ctx.author + + buf = await self.render_image() + embed = discord.Embed(title="Wordle!", color=self.embed_color) + embed.set_image(url="attachment://wordle.png") + + self.view = WordleView(self, timeout=timeout) + self.message = await ctx.send( + embed=embed, + file=discord.File(buf, "wordle.png"), + view=self.view, + ) + await self.view.wait() + return self.message diff --git a/bot/games/chess_game.py b/bot/games/chess_game.py new file mode 100644 index 0000000..1892c7d --- /dev/null +++ b/bot/games/chess_game.py @@ -0,0 +1,152 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import Optional, ClassVar, Literal +import asyncio + +import discord +from discord.ext import commands +import chess +from utils.emoji import SUCCESS + +from .utils import DiscordColor, DEFAULT_COLOR + + +class Chess: + BASE_URL: ClassVar[str] = "http://www.fen-to-image.com/image/64/double/coords/" + + def __init__(self, *, white: discord.User, black: discord.User) -> None: + self.white = white + self.black = black + self.turn = self.white + + self.winner: Optional[discord.User] = None + self.message: Optional[discord.Message] = None + + self.board: chess.Board = chess.Board() + + self.last_move: dict[str, str] = {} + + def get_color(self) -> Literal["white", "black"]: + return "white" if self.turn == self.white else "black" + + async def make_embed(self) -> discord.Embed: + embed = discord.Embed(title="Chess Game", color=discord.Color.random()) + embed.description = f"**Turn:** `{self.turn}`\n**Color:** `{self.get_color()}`\n**Check:** `{self.board.is_check()}`" + embed.set_image(url=f"{self.BASE_URL}{self.board.board_fen()}") + + embed.add_field( + name="Last Move", + value=f"```yml\n{self.last_move.get('color', '-')}: {self.last_move.get('move', '-')}\n```", + ) + return embed + + async def place_move(self, uci: str) -> chess.Board: + self.last_move = {"color": self.get_color(), "move": f"{uci[:2]} -> {uci[2:]}"} + + self.board.push_uci(uci) + self.turn = self.white if self.turn == self.black else self.black + return self.board + + async def fetch_results(self) -> discord.Embed: + results = self.board.result() + embed = discord.Embed(title="Chess Game") + + if self.board.is_checkmate(): + embed.description = f"Game over\nCheckmate | Score: `{results}`" + elif self.board.is_stalemate(): + embed.description = f"Game over\nStalemate | Score: `{results}`" + elif self.board.is_insufficient_material(): + embed.description = f"Game over\nInsufficient material left to continue the game | Score: `{results}`" + elif self.board.is_seventyfive_moves(): + embed.description = f"Game over\n75-moves rule | Score: `{results}`" + elif self.board.is_fivefold_repetition(): + embed.description = f"Game over\nFive-fold repitition. | Score: `{results}`" + else: + embed.description = ( + f"Game over\nVariant end condition. | Score: `{results}`" + ) + + embed.set_image(url=f"{self.BASE_URL}{self.board.board_fen()}") + return embed + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + timeout: Optional[float] = None, + embed_color: DiscordColor = 0x000000, + add_reaction_after_move: bool = False, + **kwargs, + ) -> discord.Message: + """ + starts the chess game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + timeout : Optional[float], optional + the timeout for when waiting, by default None + embed_color : DiscordColor, optional + the color of the game embed, by default DEFAULT_COLOR + add_reaction_after_move : bool, optional + specifies whether or not to add a reaction to the user's move validating it, by default False + + Returns + ------- + Optional[discord.Message] + returns the game message + """ + self.embed_color = discord.Color.random() + + embed = await self.make_embed() + self.message = await ctx.send(embed=embed, **kwargs) + + while not ctx.bot.is_closed(): + + def check(m: discord.Message) -> bool: + try: + if self.board.parse_uci(m.content.lower()): + return m.author == self.turn and m.channel == ctx.channel + else: + return False + except ValueError: + return False + + try: + message: discord.Message = await ctx.bot.wait_for( + "message", timeout=timeout, check=check + ) + except asyncio.TimeoutError: + return + + await self.place_move(message.content.lower()) + embed = await self.make_embed() + + if add_reaction_after_move: + await message.add_reaction(SUCCESS) + + if self.board.is_game_over(): + break + + await self.message.edit(embed=embed) + + embed = await self.fetch_results() + await self.message.edit(embed=embed) + await ctx.send("~ Game Over ~") + + return self.message diff --git a/bot/games/connect_four.py b/bot/games/connect_four.py new file mode 100644 index 0000000..e51e8cc --- /dev/null +++ b/bot/games/connect_four.py @@ -0,0 +1,219 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import Optional, Union +import asyncio + +import discord +from discord.ext import commands + +from .utils import DiscordColor, DEFAULT_COLOR + +RED = "🔴" +BLUE = "🔵" +BLANK = "⬛" + + +class ConnectFour: + """ + Connect-4 Game + """ + + def __init__(self, *, red: discord.User, blue: discord.User) -> None: + self.red_player = red + self.blue_player = blue + + self.board: list[list[str]] = [[BLANK for _ in range(7)] for _ in range(6)] + self._controls: tuple[str, ...] = ( + "1️⃣", + "2️⃣", + "3️⃣", + "4️⃣", + "5️⃣", + "6️⃣", + "7️⃣", + ) + + self.turn = self.red_player + self.message: Optional[discord.Message] = None + self.winner: Optional[discord.User] = None + + self._conversion: dict[str, int] = { + emoji: i for i, emoji in enumerate(self._controls) + } + self.player_to_emoji: dict[discord.User, str] = { + self.red_player: RED, + self.blue_player: BLUE, + } + self.emoji_to_player: dict[str, discord.User] = { + v: k for k, v in self.player_to_emoji.items() + } + + def board_string(self) -> str: + board = "1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣\n" + for row in self.board: + board += "".join(row) + "\n" + return board + + def make_embed(self, *, status: bool) -> discord.Embed: + embed = discord.Embed(color=discord.Color.random()) + if not status: + embed.description = f"**Turn:** {self.turn.name}\n**Piece:** `{self.player_to_emoji[self.turn]}`" + else: + status_ = f"{self.winner} won!" if self.winner else "Tie" + embed.description = f"**Game over**\n{status_}" + return embed + + def place_move(self, column: Union[str, int], user) -> list[list[str]]: + + if isinstance(column, str): + if column not in self._controls: + raise KeyError("Provided emoji is not one of the valid controls") + + column = self._conversion[column] + + for x in range(5, -1, -1): + if self.board[x][column] == BLANK: + self.board[x][column] = self.player_to_emoji[user] + break + + self.turn = self.red_player if user == self.blue_player else self.blue_player + return self.board + + def is_game_over(self) -> bool: + + if all(i != BLANK for i in self.board[0]): + return True + + for x in range(6): + for i in range(4): + if ( + self.board[x][i] + == self.board[x][i + 1] + == self.board[x][i + 2] + == self.board[x][i + 3] + and self.board[x][i] != BLANK + ): + self.winner = self.emoji_to_player[self.board[x][i]] + return True + + for x in range(3): + for i in range(7): + if ( + self.board[x][i] + == self.board[x + 1][i] + == self.board[x + 2][i] + == self.board[x + 3][i] + and self.board[x][i] != BLANK + ): + self.winner = self.emoji_to_player[self.board[x][i]] + return True + + for x in range(3): + for i in range(4): + if ( + self.board[x][i] + == self.board[x + 1][i + 1] + == self.board[x + 2][i + 2] + == self.board[x + 3][i + 3] + and self.board[x][i] != BLANK + ): + self.winner = self.emoji_to_player[self.board[x][i]] + return True + + for x in range(5, 2, -1): + for i in range(4): + if ( + self.board[x][i] + == self.board[x - 1][i + 1] + == self.board[x - 2][i + 2] + == self.board[x - 3][i + 3] + and self.board[x][i] != BLANK + ): + self.winner = self.emoji_to_player[self.board[x][i]] + return True + + return False + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + timeout: Optional[float] = None, + embed_color: DiscordColor = discord.Color.random(), + remove_reaction_after: bool = False, + **kwargs, + ) -> discord.Message: + """ + starts the Connect-4 game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + timeout : Optional[float], optional + the timeout for when waiting, by default None + embed_color : DiscordColor, optional + the color of the game embed, by default DEFAULT_COLOR + remove_reaction_after : bool, optional + specifies whether or not to remove the user's move reaction, by default False + + Returns + ------- + discord.Message + returns the game message + """ + self.embed_color = discord.Color.random() + + embed = self.make_embed(status=False) + self.message = await ctx.send(self.board_string(), embed=embed, **kwargs) + + for button in self._controls: + await self.message.add_reaction(button) + + while not ctx.bot.is_closed(): + + def check(reaction: discord.Reaction, user: discord.User) -> bool: + return ( + str(reaction.emoji) in self._controls + and user == self.turn + and reaction.message == self.message + and self.board[0][self._conversion[str(reaction.emoji)]] == BLANK + ) + + try: + reaction, user = await ctx.bot.wait_for( + "reaction_add", timeout=timeout, check=check + ) + except asyncio.TimeoutError: + break + + emoji = str(reaction.emoji) + self.place_move(emoji, user) + + if status := self.is_game_over(): + break + + if remove_reaction_after: + await self.message.remove_reaction(emoji, user) + + embed = self.make_embed(status=False) + await self.message.edit(content=self.board_string(), embed=embed) + + embed = self.make_embed(status=status) + await self.message.edit(content=self.board_string(), embed=embed) + + return self.message diff --git a/bot/games/country_guess.py b/bot/games/country_guess.py new file mode 100644 index 0000000..0f45c25 --- /dev/null +++ b/bot/games/country_guess.py @@ -0,0 +1,237 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import asyncio +import difflib +import os +import pathlib +import random +from typing import Union, Optional +from io import BytesIO + +import discord +from discord.ext import commands +from PIL import Image, ImageFilter, ImageOps + +from .utils import * + + +class CountryGuesser: + """ + CountryGuesser Game + """ + + embed: discord.Embed + accepted_length: Optional[int] + country: str + + def __init__( + self, + *, + is_flags: bool = False, + light_mode: bool = False, + hard_mode: bool = False, + hints: int = 1, + ) -> None: + self.embed_color: Optional[DiscordColor] = None + self.hints = hints + self.is_flags = is_flags + self.hard_mode = hard_mode + + if self.is_flags: + self.light_mode = False + else: + self.light_mode = light_mode + + folder = "assets/country-flags" if self.is_flags else "assets/country-data" + self._countries_path = pathlib.Path(__file__).parent / folder + self.all_countries = os.listdir(self._countries_path) + self.responses_count = 0 + + @executor() + def invert_image(self, image_path: Union[BytesIO, os.PathLike, str]) -> BytesIO: + with Image.open(image_path) as img: + img = img.convert("RGBA") + r, g, b, a = img.split() + rgb = Image.merge("RGB", (r, g, b)) + rgb = ImageOps.invert(rgb) + rgb = rgb.split() + img = Image.merge("RGBA", rgb + (a,)) + + buf = BytesIO() + img.save(buf, "PNG") + buf.seek(0) + return buf + + @executor() + def blur_image(self, image_path: Union[BytesIO, os.PathLike, str]) -> BytesIO: + with Image.open(image_path) as img: + img = img.convert("RGBA") + img = img.filter(ImageFilter.GaussianBlur(10)) + + buf = BytesIO() + img.save(buf, "PNG") + buf.seek(0) + return buf + + async def get_country(self) -> discord.File: + country_file = random.choice(self.all_countries) + self.country = country_file.strip()[:-4].lower() + + file = os.path.join(self._countries_path, country_file) + + if self.hard_mode: + file = await self.blur_image(file) + + if self.light_mode: + file = await self.invert_image(file) + + return discord.File(file, "country.png") + + def get_blanks(self) -> str: + return " ".join("_" if char != " " else " " for char in self.country) + + def get_hint(self) -> str: + blanks = ["_" if char != " " else " " for char in self.country] + times = round(len(blanks) / 3) + + for _ in range(times): + idx = random.choice(range(len(self.country))) + blanks[idx] = self.country[idx] + return " ".join(blanks) + + def get_accuracy(self, guess: str) -> int: + return round(difflib.SequenceMatcher(None, guess, self.country).ratio() * 100) + + def get_embed(self) -> discord.Embed: + embed = discord.Embed( + title="Guess that country!", + description=f"```fix\n{self.get_blanks()}\n```", + color=discord.Color.random(), + ) + embed.add_field( + name="\u200b", + value=f"```yml\nblurred: {str(self.hard_mode).lower()}\nflag-mode: {str(self.is_flags).lower()}\n```", + inline=False, + ) + embed.set_image(url="attachment://country.png") + return embed + + async def wait_for_response( + self, + ctx: commands.Context[commands.Bot], + *, + options: tuple[str, ...] = (), + length: Optional[int] = None, + ) -> Optional[tuple[discord.Message, str]]: + def check(m: discord.Message) -> bool: + if length: + return ( + m.channel == ctx.channel + and m.author != ctx.bot.user + and len(m.content) == length + ) + else: + return m.channel == ctx.channel and m.author != ctx.bot.user + + message: discord.Message = await ctx.bot.wait_for( + "message", timeout=self.timeout, check=check + ) + content = message.content.strip().lower() + + if options: + if not content in options: + return + + return message, content + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + timeout: Optional[float] = 100.0, + embed_color: DiscordColor = discord.Color.random(), + ) -> discord.Message: + file = await self.get_country() + + self.timeout = timeout + self.embed_color = discord.Color.random() + self.embed = self.get_embed() + self.embed.set_footer(text="send your guess within 100 seconds into the chat now!") + + self.message = await ctx.send(embed=self.embed, file=file) + + self.accepted_length = None + start_time = asyncio.get_event_loop().time() + + while not ctx.bot.is_closed() and asyncio.get_event_loop().time() - start_time < self.timeout: + try: + msg, response = await self.wait_for_response(ctx) + except asyncio.TimeoutError: + break + + self.responses_count += 1 + + if response == self.country: + elapsed_time = round(asyncio.get_event_loop().time() - start_time, 2) + await msg.reply( + f"That is correct! The country was `{self.country.title()}`" + ) + return await self.end_game(ctx, msg.author, elapsed_time) + else: + acc = self.get_accuracy(response) + + if self.responses_count % 10 == 0 and self.hints: + hint = self.get_hint() + await ctx.send(f"Hint: `{hint}`") + + await msg.reply( + f"That was incorrect! but you are `{acc}%` of the way there!", + mention_author=False, + ) + + # Check if the time has exceeded the timeout + #if asyncio.get_event_loop().time() - start_time > timeout: + #return await self.end_game(ctx) # Call end_game when timeout occurs + + + return await self.end_game(ctx) + + async def end_game( + self, + ctx: commands.Context[commands.Bot], + winner: Optional[discord.User] = None, + time_taken: Optional[float] = None, manual_end: bool = False + ) -> discord.Message: + embed = discord.Embed(title="Game Over", color=self.embed_color) + if winner and time_taken: + embed.add_field( + name="Winner", + value=f"{winner.mention} ({winner.name})", + inline=False, + ) + embed.add_field(name="Time Taken", value=f"{time_taken} seconds", inline=False) + elif manual_end: + embed.description = "The game was manually ended." + else: + embed.description = f"Time's up! No one guessed the country. The correct answer was `{self.country.title()}`." + + return await ctx.send(embed=embed) + + + async def end_game_manually(self, ctx: commands.Context): + await self.end_game(ctx, manual_end=True) + + + \ No newline at end of file diff --git a/bot/games/reaction_test.py b/bot/games/reaction_test.py new file mode 100644 index 0000000..a9c2090 --- /dev/null +++ b/bot/games/reaction_test.py @@ -0,0 +1,98 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import Optional +import time +import random +import asyncio + +import discord +from discord.ext import commands + +from .utils import DiscordColor, DEFAULT_COLOR + + +class ReactionGame: + """ + Reaction Game + """ + + def __init__(self, emoji: str = "🖱️") -> None: + self.emoji = emoji + + async def wait_for_reaction( + self, ctx: commands.Context[commands.Bot], *, timeout: float + ) -> tuple[discord.User, float]: + start = time.perf_counter() + + def check(reaction: discord.Reaction, _: discord.User) -> bool: + return ( + str(reaction.emoji) == self.emoji and reaction.message == self.message + ) + + _, user = await ctx.bot.wait_for("reaction_add", timeout=timeout, check=check) + end = time.perf_counter() + + return user, (end - start) + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + timeout: Optional[float] = None, + embed_color: DiscordColor = DEFAULT_COLOR, + ) -> discord.Message: + """ + starts the reaction game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + timeout : Optional[float], optional + the timeout for when waiting, by default None + embed_color : DiscordColor, optional + the color of the game embed, by default DEFAULT_COLOR + + Returns + ------- + discord.Message + returns the game message + """ + embed = discord.Embed( + title="Reaction Game", + description=f"React with {self.emoji} when the embed is edited!", + color=discord.Color.random(), + ) + + self.message = await ctx.send(embed=embed) + await self.message.add_reaction(self.emoji) + + pause = random.uniform(1.0, 5.0) + await asyncio.sleep(pause) + + embed.description = f"React with {self.emoji} now!" + await self.message.edit(embed=embed) + + try: + user, elapsed = await self.wait_for_reaction(ctx, timeout=timeout) + except asyncio.TimeoutError: + return self.message + + embed.description = f"{user.mention} reacted first in `{elapsed:.2f}s` !" + await self.message.edit(embed=embed) + + return self.message diff --git a/bot/games/rps.py b/bot/games/rps.py new file mode 100644 index 0000000..48cd63a --- /dev/null +++ b/bot/games/rps.py @@ -0,0 +1,108 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +import asyncio +import random +from typing import ClassVar, Optional + +import discord +from discord.ext import commands + +from .utils import DiscordColor, DEFAULT_COLOR + + +class RockPaperScissors: + message: discord.Message + + OPTIONS: ClassVar[tuple[str, str, str]] = ("\U0001faa8", "\U00002702", "\U0001f4f0") + BEATS: ClassVar[dict[str, str]] = { + OPTIONS[0]: OPTIONS[1], + OPTIONS[1]: OPTIONS[2], + OPTIONS[2]: OPTIONS[0], + } + + def check_win(self, bot_choice: str, user_choice: str) -> bool: + return self.BEATS[user_choice] == bot_choice + + async def wait_for_choice( + self, ctx: commands.Context[commands.Bot], *, timeout: float + ) -> str: + def check(reaction: discord.Reaction, user: discord.User) -> bool: + return ( + str(reaction.emoji) in self.OPTIONS + and user == ctx.author + and reaction.message == self.message + ) + + reaction, _ = await ctx.bot.wait_for( + "reaction_add", timeout=timeout, check=check + ) + return str(reaction.emoji) + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + timeout: Optional[float] = None, + embed_color: DiscordColor = DEFAULT_COLOR, + ) -> discord.Message: + """ + Starts the Rock Paper Scissors game. + + Parameters + ---------- + ctx : commands.Context + The context of the invoking command. + timeout : Optional[float], optional + The timeout for waiting, by default None. + embed_color : DiscordColor, optional + The color of the game embed, by default DEFAULT_COLOR. + + Returns + ------- + discord.Message + Returns the game message. + """ + embed = discord.Embed( + title="Rock Paper Scissors", + description="React to play!", + color=discord.Color.random(), + ) + self.message = await ctx.send(embed=embed) + + for option in self.OPTIONS: + await self.message.add_reaction(option) + + bot_choice = random.choice(self.OPTIONS) + + try: + user_choice = await self.wait_for_choice(ctx, timeout=timeout) + except asyncio.TimeoutError: + embed.description = "You took too long to respond!" + await self.message.edit(embed=embed) + return self.message + + if user_choice == bot_choice: + embed.description = f"**Tie!**\nWe both picked {user_choice}" + elif self.check_win(bot_choice, user_choice): + embed.description = ( + f"**You Won!**\nYou picked {user_choice} and I picked {bot_choice}." + ) + else: + embed.description = f"**You Lost!**\nI picked {bot_choice} and you picked {user_choice}." + + await self.message.edit(embed=embed) + return self.message diff --git a/bot/games/tictactoe.py b/bot/games/tictactoe.py new file mode 100644 index 0000000..0e465d5 --- /dev/null +++ b/bot/games/tictactoe.py @@ -0,0 +1,200 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import Optional, ClassVar +import asyncio + +import discord +from discord.ext import commands +from utils.emoji import ERROR + +from .utils import DiscordColor, DEFAULT_COLOR + + +class Tictactoe: + """ + TicTacToe Game + """ + + BLANK: ClassVar[str] = "⬛" + CIRCLE: ClassVar[str] = "⭕" + CROSS: ClassVar[str] = ERROR + _conversion: ClassVar[dict[str, tuple[int, int]]] = { + "1️⃣": (0, 0), + "2️⃣": (0, 1), + "3️⃣": (0, 2), + "4️⃣": (1, 0), + "5️⃣": (1, 1), + "6️⃣": (1, 2), + "7️⃣": (2, 0), + "8️⃣": (2, 1), + "9️⃣": (2, 2), + } + + _WINNERS: ClassVar[tuple[tuple[tuple[int, int], ...], ...]] = ( + ((0, 0), (0, 1), (0, 2)), + ((1, 0), (1, 1), (1, 2)), + ((2, 0), (2, 1), (2, 2)), + ((0, 0), (1, 0), (2, 0)), + ((0, 1), (1, 1), (2, 1)), + ((0, 2), (1, 2), (2, 2)), + ((0, 0), (1, 1), (2, 2)), + ((0, 2), (1, 1), (2, 0)), + ) + + def __init__(self, cross: discord.User, circle: discord.User) -> None: + self.cross = cross + self.circle = circle + + self.board: list[list[str]] = [[self.BLANK for _ in range(3)] for _ in range(3)] + self.turn: discord.User = self.cross + + self.winner: Optional[discord.User] = None + self.winning_indexes: list[tuple[int, int]] = [] + self.message: Optional[discord.Message] = None + + self._controls: list[str] = [ + "1️⃣", + "2️⃣", + "3️⃣", + "4️⃣", + "5️⃣", + "6️⃣", + "7️⃣", + "8️⃣", + "9️⃣", + ] + + self.emoji_to_player: dict[discord.User, str] = { + self.CIRCLE: self.circle, + self.CROSS: self.cross, + } + self.player_to_emoji: dict[str, discord.User] = { + v: k for k, v in self.emoji_to_player.items() + } + + def board_string(self) -> str: + board = "" + for row in self.board: + board += "".join(row) + "\n" + return board + + def make_embed(self, *, game_over: bool = False) -> discord.Embed: + embed = discord.Embed(color=discord.Color.random()) + if game_over: + status = f"{self.winner.mention} won!" if self.winner else "Tie" + embed.description = f"**Game over**\n{status}" + else: + embed.description = f"**Turn:** {self.turn.mention}\n**Piece:** `{self.player_to_emoji[self.turn]}`" + return embed + + def make_move(self, emoji: str, user: discord.User) -> list: + + if emoji not in self._controls: + raise KeyError("Provided emoji is not one of the valid controls") + else: + x, y = self._conversion[emoji] + piece = self.player_to_emoji[user] + self.board[x][y] = piece + + self.turn = self.circle if user == self.cross else self.cross + self._conversion.pop(emoji) + self._controls.remove(emoji) + return self.board + + def is_game_over(self, *, tie: bool = False) -> bool: + + for possibility in self._WINNERS: + row = [self.board[r][c] for r, c in possibility] + + if len(set(row)) == 1 and row[0] != self.BLANK: + self.winner = self.emoji_to_player[row[0]] + self.winning_indexes = possibility + return True + + if not self._controls or tie: + return True + + return False + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + timeout: Optional[float] = None, + embed_color: DiscordColor = discord.Color.random(), + remove_reaction_after: bool = False, + **kwargs, + ) -> discord.Message: + """ + starts the tictactoe game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + timeout : Optional[float], optional + the timeout for when waiting, by default None + embed_color : DiscordColor, optional + the color of the game embed, by default DEFAULT_COLOR + remove_reaction_after : bool, optional + specifies whether or not to remove the move reaction each time, by default False + + Returns + ------- + discord.Message + returns the game emssage + """ + self.embed_color = embed_color + + embed = self.make_embed() + self.message = await ctx.send(self.board_string(), embed=embed, **kwargs) + + for button in self._controls: + await self.message.add_reaction(button) + + while not ctx.bot.is_closed(): + + def check(reaction: discord.Reaction, user: discord.User) -> bool: + return ( + str(reaction.emoji) in self._controls + and user == self.turn + and reaction.message == self.message + ) + + try: + reaction, user = await ctx.bot.wait_for( + "reaction_add", timeout=timeout, check=check + ) + except asyncio.TimeoutError: + break + + if self.is_game_over(): + break + + emoji = str(reaction.emoji) + self.make_move(emoji, user) + embed = self.make_embed() + + if remove_reaction_after: + await self.message.remove_reaction(emoji, user) + + await self.message.edit(content=self.board_string(), embed=embed) + + embed = self.make_embed(game_over=True) + await self.message.edit(content=self.board_string(), embed=embed) + + return self.message diff --git a/bot/games/twenty_48.py b/bot/games/twenty_48.py new file mode 100644 index 0000000..4286aba --- /dev/null +++ b/bot/games/twenty_48.py @@ -0,0 +1,389 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal, Optional +from io import BytesIO +import os +import asyncio +import random +import pathlib +import itertools + +import discord +from discord.ext import commands +from PIL import Image, ImageDraw, ImageFont +from utils.emoji import ARROW_LEFT, ARROW_RIGHT, ARROW_UP, ARROW_DOWN, STOP_BUTTON + +from .utils import * + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + + Board: TypeAlias = list[list[int]] + + +async def create_2048_emojis( + guild: discord.Guild, names: Optional[list[str]] = None +) -> list[discord.Emoji]: + """ + creates 2048 emojis in the specified Guild + intended to be ran once initially manually. + + Parameters + ---------- + guild : discord.Guild + the guild to create the emojis in + names : Optional[list[str]], optional + names to use for the emojis + if not specified, _ will be used, by default None + + Returns + ------- + list[Emoji] + returns the list of emojis created + """ + result: list[discord.Emoji] = [] + + directory = pathlib.Path(__file__).parent / "assets/2048-emoji-asset-examples" + files = os.listdir(directory) + names = map(lambda n: f"_{n[:-4]}", files) if not names else names + + for name, file in zip(names, files): + with open(os.path.join(directory, file), "rb") as fp: + result.append( + await guild.create_custom_emoji( + name=name, image=fp.read(), reason="2048 emojis" + ) + ) + return result + + +class Twenty48: + """ + Twenty48 Game + """ + + player: discord.User + + def __init__( + self, + number_to_display_mapping: dict[str, str] = {}, + *, + render_image: bool = False, + ) -> None: + + self.embed_color: Optional[DiscordColor] = None + self.embed: Optional[discord.Embed] = None + + self.board: Board = [[0 for _ in range(4)] for _ in range(4)] + self.message: Optional[discord.Message] = None + + self._controls = [ARROW_LEFT, ARROW_RIGHT, ARROW_UP, ARROW_DOWN] + self._conversion = number_to_display_mapping + self._render_image = render_image + + if self._render_image and discord.version_info.major < 2: + raise ValueError( + "discord.py versions under v2.0.0 do not support rendering images since editing files is new in 2.0" + ) + + if self._render_image: + self._color_mapping: dict[str, tuple[tuple[int, int, int], int]] = { + "0": ((204, 192, 179), 50), + "2": ((237, 227, 217), 50), + "4": ((237, 224, 200), 50), + "8": ((242, 177, 121), 50), + "16": ((245, 149, 100), 50), + "32": ((246, 124, 95), 50), + "64": ((246, 94, 59), 50), + "128": ((236, 206, 113), 40), + "256": ((236, 203, 96), 40), + "512": ((236, 199, 80), 40), + "1024": ((236, 196, 62), 30), + "2048": ((236, 193, 46), 30), + "4096": ((59, 57, 49), 30), + "8192": ((59, 57, 49), 30), + } + + self.LIGHT_CLR = (249, 246, 242) + self.DARK_CLR = (119, 110, 101) + self.BG_CLR = (187, 173, 160) + + self.BORDER_W = 20 + self.SQ_S = 100 + self.SPACE_W = 15 + + self.IMG_LENGTH = self.BORDER_W * 2 + self.SQ_S * 4 + self.SPACE_W * 3 + + self._font = ImageFont.truetype( + str(pathlib.Path(__file__).parent / "assets/ClearSans-Bold.ttf"), 50 + ) + + def _reverse(self, board: Board) -> Board: + return [row[::-1] for row in board] + + def _transp(self, board: Board) -> Board: + return [[board[i][j] for i in range(4)] for j in range(4)] + + def _merge(self, board: Board) -> Board: + for i in range(4): + for j in range(3): + tile = board[i][j] + if tile == board[i][j + 1] and tile != 0: + board[i][j] *= 2 + board[i][j + 1] = 0 + return board + + def _compress(self, board: Board) -> Board: + new_board = [[0 for _ in range(4)] for _ in range(4)] + for i in range(4): + pos = 0 + for j in range(4): + if board[i][j] != 0: + new_board[i][pos] = board[i][j] + pos += 1 + return new_board + + def move_left(self) -> None: + stage = self._compress(self.board) + stage = self._merge(stage) + stage = self._compress(stage) + self.board = stage + + def move_right(self) -> None: + stage = self._reverse(self.board) + stage = self._compress(stage) + stage = self._merge(stage) + stage = self._compress(stage) + stage = self._reverse(stage) + self.board = stage + + def move_up(self) -> None: + stage = self._transp(self.board) + stage = self._compress(stage) + stage = self._merge(stage) + stage = self._compress(stage) + stage = self._transp(stage) + self.board = stage + + def move_down(self) -> None: + stage = self._transp(self.board) + stage = self._reverse(stage) + stage = self._compress(stage) + stage = self._merge(stage) + stage = self._compress(stage) + stage = self._reverse(stage) + stage = self._transp(stage) + self.board = stage + + def spawn_new(self) -> bool: + """ + spawns a new `2` + + Returns + ------- + bool + returns whether or not the game is lost + """ + board = self.board + zeroes = [ + (j, i) for j, sub in enumerate(board) for i, el in enumerate(sub) if el == 0 + ] + + if not zeroes: + return True + else: + i, j = random.choice(zeroes) + board[i][j] = 2 + return False + + def number_to_emoji(self) -> str: + board = self.board + game_string = "" + + emoji_array = [ + [self._conversion.get(str(l), f"`{l}` ") for l in row] for row in board + ] + + for row in emoji_array: + game_string += "".join(row) + "\n" + return game_string + + def check_win(self) -> bool: + flattened = itertools.chain(*self.board) + + for num in (2048, 4096, 8192): + if num in flattened: + if num == 2048: + self.embed = discord.Embed(description="", color=discord.Color.random()) + self.embed.description += f"⭐: Congrats! You hit **{num}**!\n" + + if num == self.win_at: + self.embed.description += "**Game Over! You Won**\n" + return True + return False + + @executor() + def render_image(self) -> discord.File: + SQ = self.SQ_S + with Image.new("RGB", (self.IMG_LENGTH, self.IMG_LENGTH), self.BG_CLR) as img: + cursor = ImageDraw.Draw(img) + + x = y = self.BORDER_W + for row in self.board: + for tile in row: + tile = str(tile) + color, fsize = self._color_mapping.get(tile) + font = self._font.font_variant(size=fsize) + cursor.rounded_rectangle( + (x, y, x + SQ, y + SQ), radius=5, width=0, fill=color + ) + + if tile != "0": + text_fill = ( + self.DARK_CLR if tile in ("2", "4") else self.LIGHT_CLR + ) + cursor.text( + (x + SQ / 2, y + SQ / 2), + tile, + font=font, + anchor="mm", + fill=text_fill, + ) + + x += SQ + self.SPACE_W + x = self.BORDER_W + y += SQ + self.SPACE_W + + buf = BytesIO() + img.save(buf, "PNG") + buf.seek(0) + return discord.File(buf, "2048.png") + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + win_at: Literal[2048, 4096, 8192] = 8192, + timeout: Optional[float] = None, + remove_reaction_after: bool = False, + delete_button: bool = False, + embed_color: DiscordColor = DEFAULT_COLOR, + **kwargs, + ) -> discord.Message: + """ + starts the 2048 game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + win_at : Literal[2048, 4096, 8192], optional + the tile to stop the game / win at, by default 8192 + timeout : Optional[float], optional + the timeout when waiting, by default None + remove_reaction_after : bool, optional + specifies whether or not to remove the move reaction, by default False + delete_button : bool, optional + specifies whether or not to include a stop button or not, by default False + embed_color : DiscordColor, optional + the color of the game embed, by default DEFAULT_COLOR + + Returns + ------- + discord.Message + returns the game message + """ + self.win_at = win_at + self.embed_color = embed_color + self.player = ctx.author + + self.board[random.randrange(4)][random.randrange(4)] = 2 + self.board[random.randrange(4)][random.randrange(4)] = 2 + + if self._render_image: + image = await self.render_image() + self.message = await ctx.send(file=image, **kwargs) + else: + board_string = self.number_to_emoji() + self.message = await ctx.send(board_string, **kwargs) + + if delete_button: + self._controls.append(STOP_BUTTON) + + for button in self._controls: + await self.message.add_reaction(button) + + while not ctx.bot.is_closed(): + + def check(reaction: discord.Reaction, user: discord.User) -> bool: + return ( + str(reaction.emoji) in self._controls + and user == self.player + and reaction.message == self.message + ) + + try: + reaction, user = await ctx.bot.wait_for( + "reaction_add", timeout=timeout, check=check + ) + except asyncio.TimeoutError: + break + + emoji = str(reaction.emoji) + + if delete_button and emoji == STOP_BUTTON: + await self.message.delete() + break + + if emoji == ARROW_RIGHT: + self.move_right() + + elif emoji == ARROW_LEFT: + self.move_left() + + elif emoji == ARROW_DOWN: + self.move_down() + + elif emoji == ARROW_UP: + self.move_up() + + if remove_reaction_after: + try: + await self.message.remove_reaction(emoji, user) + except discord.DiscordException: + pass + + lost = self.spawn_new() + won = self.check_win() + + if lost: + self.embed = discord.Embed( + description="Game Over! You lost.", + color=discord.Color.random(), + ) + + if self._render_image: + image = await self.render_image() + await self.message.edit(attachments=[image], embed=self.embed) + else: + board_string = self.number_to_emoji() + await self.message.edit(content=board_string, embed=self.embed) + + if won or lost: + break + + return self.message diff --git a/bot/games/typeracer.py b/bot/games/typeracer.py new file mode 100644 index 0000000..3e15623 --- /dev/null +++ b/bot/games/typeracer.py @@ -0,0 +1,244 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import Optional, ClassVar, TypedDict, Any +from datetime import datetime as dt +from io import BytesIO + +import textwrap +import time +import random +import asyncio +import aiohttp +import difflib +import pathlib + +from PIL import Image, ImageDraw, ImageFont +import discord +from discord.ext import commands + +from .utils import * + + +class UserData(TypedDict): + user: discord.User + time: float + wpm: float + acc: float + + +class TypeRacer: + """ + TypeRace Game + """ + + SENTENCE_URL: ClassVar[str] = "https://api.quotable.io/random" + EMOJI_MAP: ClassVar[dict[int, str]] = { + 1: "🥇", + 2: "🥈", + 3: "🥉", + } + + @executor() + def _tr_img(self, text: str, font: str) -> BytesIO: + + text = "\n".join(textwrap.wrap(text, width=25)) + + font = ImageFont.truetype(font, 30) + x, y = font.getsize_multiline(text) + + with Image.new("RGB", (x + 20, y + 30), (0, 0, 30)) as image: + cursor = ImageDraw.Draw(image) + cursor.multiline_text((10, 10), text, font=font, fill=(220, 200, 220)) + + buffer = BytesIO() + image.save(buffer, "PNG") + buffer.seek(0) + return buffer + + def format_line(self, i: int, data: UserData) -> str: + return f" • {self.EMOJI_MAP[i]} | {data['user'].mention} in {data['time']:.2f}s | **WPM:** {data['wpm']:.2f} | **ACC:** {data['acc']:.2f}%" + + async def wait_for_tr_response( + self, + ctx: commands.Context[commands.Bot], + text: str, + *, + timeout: float, + min_accuracy: float, + ) -> discord.Message: + + self.embed.description = "" + + text = text.lower().replace("\n", " ") + winners = [] + start = time.perf_counter() + + while not ctx.bot.is_closed(): + + def check(m: discord.Message) -> bool: + content = m.content.lower().replace("\n", " ") + if ( + m.channel == ctx.channel + and not m.author.bot + and m.author not in map(lambda m: m["user"], winners) + ): + sim = difflib.SequenceMatcher(None, content, text).ratio() + return sim >= min_accuracy + + try: + message: discord.Message = await ctx.bot.wait_for( + "message", timeout=timeout, check=check + ) + except asyncio.TimeoutError: + if winners: + break + else: + return await ctx.reply( + "Looks like no one responded", + allowed_mentions=discord.AllowedMentions.none(), + ) + + end = time.perf_counter() + content = message.content.lower().replace("\n", " ") + timeout -= round(end - start) + + winners.append( + { + "user": message.author, + "time": end - start, + "wpm": len(text.split()) / ((end - start) / 60), + "acc": difflib.SequenceMatcher(None, content, text).ratio() * 100, + } + ) + + self.embed.description += ( + self.format_line(len(winners), winners[len(winners) - 1]) + "\n" + ) + await self.message.edit(embed=self.embed) + + await message.add_reaction(self.EMOJI_MAP[len(winners)]) + + if len(winners) >= 3: + break + + desc = [self.format_line(i, x) for i, x in enumerate(winners, 1)] + embed = discord.Embed( + title="Typerace results", color=discord.Color.random(), timestamp=dt.utcnow() + ) + embed.add_field(name="Winners", value="\n".join(desc)) + + return await ctx.reply( + embed=embed, allowed_mentions=discord.AllowedMentions.none() + ) + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + embed_title: str = "Type the following sentence in the chat now!", + embed_color: DiscordColor = DEFAULT_COLOR, + path_to_text_font: Optional[str] = None, + timeout: float = 40, + words_mode: bool = False, + show_author: bool = True, + max_quote_length: Optional[int] = None, + min_accuracy: float = 0.9, + ) -> discord.Message: + """ + starts the typerace game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + embed_title : str, optional + the title of the game embed, by default 'Type the following sentence in the chat now!' + embed_color : DiscordColor, optional + the color of the game embed, by default DEFAULT_COLOR + path_to_text_font : Optional[str], optional + path to the font you want to use for the image + fallbacks to SegoeUI if not specified, by default None + timeout : float, optional + the game timeout, by default 40 + words_mode : bool, optional + specifies whether or not to just use random words instead of a quote, by default False + show_author : bool, optional + specifies whether or not to show the command author in the embed, by default True + max_quote_length : int, optional + specifies the maximum length of the quote to truncate to if necessary, by default None + min_accuracy : float, optional + specifies the minimum accuracy an attempt needs to be for it to be accepted by the bot + + Returns + ------- + discord.Message + the game message + + Raises + ------ + RuntimeError + requesting the quote failed + """ + self.embed_color = embed_color + parent = pathlib.Path(__file__).parent + + if not words_mode: + async with aiohttp.ClientSession() as session: + async with session.get(self.SENTENCE_URL) as r: + if r.ok: + text: dict[str, Any] = await r.json() + text = text.get("content", "") + else: + raise RuntimeError( + f"HTTP request raised an error: {r.status}; {r.reason}" + ) + + else: + with open(parent / "assets/words.txt", "r") as wordsfp: + words = wordsfp.read().splitlines() + text = " ".join(random.choice(words).lower() for _ in range(8)) + + if max_quote_length is not None: + if len(text) > max_quote_length: + text = textwrap.shorten(text, width=max_quote_length, placeholder="") + + if not path_to_text_font: + path_to_text_font = str( + parent / "assets/segoe-ui-semilight-411.ttf" + ) + + buffer = await self._tr_img(text, path_to_text_font) + + embed = discord.Embed( + title=embed_title, color=discord.Color.random(), timestamp=dt.utcnow() + ) + embed.set_image(url="attachment://tr.png") + + if show_author: + if discord.version_info.major >= 2: + av = ctx.author.avatar.url + else: + av = ctx.author.avatar_url + embed.set_author(name=ctx.author.name, icon_url=av) + + self.embed = embed + self.message = await ctx.send(embed=embed, file=discord.File(buffer, "tr.png")) + + await self.wait_for_tr_response( + ctx, text, timeout=timeout, min_accuracy=min_accuracy + ) + return self.message diff --git a/bot/games/utils.py b/bot/games/utils.py new file mode 100644 index 0000000..276d5b4 --- /dev/null +++ b/bot/games/utils.py @@ -0,0 +1,138 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +from typing import ( + Optional, + Coroutine, + Callable, + Final, + Union, + TypeVar, + TYPE_CHECKING, + Any, +) + +import functools +import asyncio + +import discord +from discord.ext import commands +from utils.emoji import STOP_BUTTON + +if TYPE_CHECKING: + from typing_extensions import ParamSpec, TypeAlias + + P = ParamSpec("P") + T = TypeVar("T") + + A = TypeVar("A", bool) + B = TypeVar("B", bool) + +__all__: tuple[str, ...] = ( + "DiscordColor", + "DEFAULT_COLOR", + "executor", + "chunk", + "BaseView", + "double_wait", + "wait_for_delete", +) + +DiscordColor: TypeAlias = Union[discord.Color, int] + +DEFAULT_COLOR: Final[discord.Color] = discord.Color(0x2F3136) + + +def chunk(iterable: list[T], *, count: int) -> list[list[T]]: + return [iterable[i : i + count] for i in range(0, len(iterable), count)] + + +def executor() -> Callable[[Callable[P, T]], Callable[P, Coroutine[Any, Any, T]]]: + def decorator(func: Callable[P, T]) -> Callable[P, Coroutine[Any, Any, T]]: + @functools.wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs): + partial = functools.partial(func, *args, **kwargs) + loop = asyncio.get_event_loop() + return loop.run_in_executor(None, partial) + + return wrapper + + return decorator + + +async def wait_for_delete( + ctx: commands.Context[commands.Bot], + message: discord.Message, + *, + emoji: str = STOP_BUTTON, + bot: Optional[discord.Client] = None, + user: Optional[Union[discord.User, tuple[discord.User, ...]]] = None, + timeout: Optional[float] = None, +) -> bool: + + if not user: + user = ctx.author + try: + await message.add_reaction(emoji) + except discord.DiscordException: + pass + + def check(reaction: discord.Reaction, _user: discord.User) -> bool: + if reaction.emoji == emoji and reaction.message == message: + if isinstance(user, tuple): + return _user in user + else: + return _user == user + + bot: discord.Client = bot or ctx.bot + try: + await bot.wait_for("reaction_add", timeout=timeout, check=check) + except asyncio.TimeoutError: + return False + else: + await message.delete() + return True + + +async def double_wait( + task1: Coroutine[Any, Any, A], + task2: Coroutine[Any, Any, B], + *, + loop: Optional[asyncio.AbstractEventLoop] = None, +) -> tuple[set[asyncio.Task[Union[A, B]]], set[asyncio.Task[Union[A, B]]],]: + + if not loop: + loop = asyncio.get_event_loop() + + return await asyncio.wait( + [ + loop.create_task(task1), + loop.create_task(task2), + ], + return_when=asyncio.FIRST_COMPLETED, + ) + + +if hasattr(discord, "ui"): + + class BaseView(discord.ui.View): + def disable_all(self) -> None: + for button in self.children: + if isinstance(button, discord.ui.Button): + button.disabled = True + + async def on_timeout(self) -> None: + return self.stop() diff --git a/bot/games/wordle.py b/bot/games/wordle.py new file mode 100644 index 0000000..6a59770 --- /dev/null +++ b/bot/games/wordle.py @@ -0,0 +1,196 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +import pathlib +import random +import asyncio +from typing import Optional, Final +from io import BytesIO + +import discord +from discord.ext import commands +from PIL import Image, ImageDraw, ImageFont + +from .utils import * + +BORDER: Final[int] = 40 +SQ: Final[int] = 100 +SPACE: Final[int] = 10 + +WIDTH: Final[int] = BORDER * 2 + SQ * 5 + SPACE * 4 +HEIGHT: Final[int] = BORDER * 2 + SQ * 6 + SPACE * 5 + +GRAY: Final[tuple[int, int, int]] = (119, 123, 125) +ORANGE: Final[tuple[int, int, int]] = (200, 179, 87) +GREEN: Final[tuple[int, int, int]] = (105, 169, 99) +LGRAY: Final[tuple[int, int, int]] = (198, 201, 205) + + +class Wordle: + """ + Wordle Game + """ + + def __init__(self, word: Optional[str] = None, *, text_size: int = 55) -> None: + self.embed_color: Optional[DiscordColor] = None + + parent = pathlib.Path(__file__).parent + self._valid_words = tuple( + open(parent / "assets/words.txt", "r").read().splitlines() + ) + self._text_size = text_size + self._font = ImageFont.truetype( + str(parent / "assets/HelveticaNeuBold.ttf"), self._text_size + ) + + self.guesses: list[list[dict[str, str]]] = [] + + if word: + if len(word) != 5: + raise ValueError("Word must be of length 5") + + if not word.isalpha(): + raise ValueError("Word must be an alphabetical string") + + self.word = word + else: + self.word: str = random.choice(self._valid_words) + + def parse_guess(self, guess: str) -> bool: + self.guesses.append([]) + for ind, l in enumerate(guess): + if l in self.word: + color = GREEN if self.word[ind] == l else ORANGE + else: + color = GRAY + self.guesses[-1].append({"letter": l, "color": color}) + + return guess == self.word + + @executor() + def render_image(self) -> BytesIO: + with Image.new("RGB", (WIDTH, HEIGHT), (255, 255, 255)) as img: + cursor = ImageDraw.Draw(img) + + x = y = BORDER + for i in range(6): + for j in range(5): + try: + letter = self.guesses[i][j] + color = letter["color"] + act_letter = letter["letter"] + except (IndexError, KeyError): + cursor.rectangle((x, y, x + SQ, y + SQ), outline=LGRAY, width=4) + else: + cursor.rectangle((x, y, x + SQ, y + SQ), width=0, fill=color) + cursor.text( + (x + SQ / 2, y + SQ / 2), + act_letter.upper(), + font=self._font, + anchor="mm", + fill=(255, 255, 255), + ) + + x += SQ + SPACE + x = BORDER + y += SQ + SPACE + + buf = BytesIO() + img.save(buf, "PNG") + buf.seek(0) + return buf + + async def start( + self, + ctx: commands.Context[commands.Bot], + *, + timeout: Optional[float] = None, + embed_color: DiscordColor = DEFAULT_COLOR, + ) -> discord.Message: + """ + starts the wordle game + + Parameters + ---------- + ctx : commands.Context + the context of the invokation command + timeout : Optional[float], optional + the timeout for when waiting, by default None + embed_color : DiscordColor, optional + the color of the game embed, by default DEFAULT_COLOR + + Returns + ------- + discord.Message + returns the game message + """ + self.embed_color = discord.Color.random() + + buf = await self.render_image() + + embed = discord.Embed(title="Wordle!", color=discord.Color.random()) + embed.set_image(url="attachment://wordle.png") + embed.set_footer(text='Say "stop" to cancel the game!') + + self.message = await ctx.send(embed=embed, file=discord.File(buf, "wordle.png")) + + while not ctx.bot.is_closed(): + + def check(m: discord.Message) -> bool: + return ( + (len(m.content) == 5 or m.content.lower() == "stop") + and m.author == ctx.author + and m.channel == ctx.channel + ) + + try: + guess: discord.Message = await ctx.bot.wait_for( + "message", timeout=timeout, check=check + ) + except asyncio.TimeoutError: + break + + content = guess.content.lower() + + if content == "stop": + await ctx.send(f"Game Over! cancelled, the word was: **{self.word}**") + break + + if content not in self._valid_words: + await ctx.send("That is not a valid word!") + else: + won = self.parse_guess(content) + buf = await self.render_image() + + await self.message.delete() + + embed = discord.Embed(title="Wordle!", color=discord.Color.random()) + embed.set_image(url="attachment://wordle.png") + + self.message = await ctx.send( + embed=embed, file=discord.File(buf, "wordle.png") + ) + + if won: + await ctx.send("Game Over! You won!") + break + elif len(self.guesses) >= 6: + await ctx.send( + f"Game Over! You lose, the word was: **{self.word}**" + ) + break + + return self.message diff --git a/bot/j2c_data.db b/bot/j2c_data.db new file mode 100644 index 0000000..60ce975 Binary files /dev/null and b/bot/j2c_data.db differ diff --git a/bot/jsondb/birthdays.json b/bot/jsondb/birthdays.json new file mode 100644 index 0000000..49cc8ef Binary files /dev/null and b/bot/jsondb/birthdays.json differ diff --git a/bot/jsondb/logging_config.json b/bot/jsondb/logging_config.json new file mode 100644 index 0000000..0dcece7 --- /dev/null +++ b/bot/jsondb/logging_config.json @@ -0,0 +1,24 @@ +{ + "1419729237480575070": { + "guild_id": "1419729237480575070", + "log_channels": { + "message_events": 1485250610935889921 + }, + "log_enabled": { + "message_events": false, + "join_leave_events": false, + "member_moderation": false, + "voice_events": false, + "channel_events": false, + "role_events": false, + "emoji_events": true, + "reaction_events": true, + "system_events": true + }, + "ignore_channels": [], + "ignore_roles": [], + "ignore_users": [], + "auto_delete_duration": 3600, + "last_updated": "2026-05-04T07:52:41.305608" + } +} \ No newline at end of file diff --git a/bot/lang/lang.en.json b/bot/lang/lang.en.json new file mode 100644 index 0000000..03f3683 --- /dev/null +++ b/bot/lang/lang.en.json @@ -0,0 +1,26 @@ +{ + "language_name": "English", + "instruc_image_caption": "Image-to-text models may take time to load, causing timeout errors. Fallback or functional models should be used instead. Captions for the image are categorized as OCR (1st), which is good for images containing signs or symbols, and general image detection (2nd), which will be very inaccurate for OCR. Image captions:", + "pfp": "Change bot's pfp using an image URL", + "pfp_change_msg_1": "Please provide an Image URL or attach an Image for this command", + "pfp_change_msg_2": "Profile picture changed successfully", + "ping": "PONG! Provide bot latency", + "ping_msg": "Pong! Latency: ", + "changeusr": "Change bot's actual username", + "changeusr_msg_1": "Trying to change username....", + "changeusr_msg_2_part_1": "Sorry, the username '", + "changeusr_msg_2_part_2": "' is already taken.", + "changeusr_msg_3": "Username changed to ", + "toggledm": "Toggle DM for chatting", + "toggleactive": "Toggle active channels", + "toggleactive_msg_1": "has been removed from the list of active channels.", + "toggleactive_msg_2": "has been added to the list of active channels!", + "help": "Get all other commands!", + "help_footer": "https://discord.gg/codexdev", + "nekos": "Display a random image or GIF of a neko, waifu, husbando, kitsune, or other actions", + "nekos_msg": "Invalid category provided. Valid categories are: ", + "imagine": "Generate an image", + "imagine_msg": "Finished Image Generation!", + "bonk": "Clear message", + "bonk_msg": "Message history has been cleared!" +} \ No newline at end of file diff --git a/bot/requirements.txt b/bot/requirements.txt new file mode 100644 index 0000000..b0d0242 --- /dev/null +++ b/bot/requirements.txt @@ -0,0 +1,42 @@ +Quart +discord +discord.py +discord.ui +jishaku +mcstatus +asyncio +discord-ext-menus +aiohttp +datetime +flask +typing +psutil +collection +requests +pathlib +pymongo +wavelink>=2.0.0 +httpx +discord.ext-context +tasksio +motor +Augmentor +numpy +aiosqlite +gtts +langdetect +python-dotenv +pyttsx3 +PyYAML +Requests +openai +duckduckgo-search==6.3.6 +# pillow==11.0.0 +colorama +chess +python-dateutil +deep-translator==1.11.4 +uvicorn==0.46.0 +fastapi +slowapi +pycloudflared \ No newline at end of file diff --git a/bot/rr.db b/bot/rr.db new file mode 100644 index 0000000..4ba75b2 Binary files /dev/null and b/bot/rr.db differ diff --git a/bot/utils/Tools.py b/bot/utils/Tools.py new file mode 100644 index 0000000..3a67920 --- /dev/null +++ b/bot/utils/Tools.py @@ -0,0 +1,214 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import json, sys, os +import discord +from discord.ext import commands +from core import Context +from utils.emoji import DENIED +import aiosqlite +import asyncio + +async def setup_db(): + async with aiosqlite.connect('db/prefix.db') as db: + await db.execute(''' + CREATE TABLE IF NOT EXISTS prefixes ( + guild_id INTEGER PRIMARY KEY, + prefix TEXT NOT NULL + ) + ''') + await db.commit() + + +asyncio.run(setup_db()) + +async def is_topcheck_enabled(guild_id: int): + async with aiosqlite.connect('db/topcheck.db') as db: + async with db.execute("SELECT enabled FROM topcheck WHERE guild_id = ?", (guild_id,)) as cursor: + row = await cursor.fetchone() + return row is not None and row[0] == 1 + + + +def read_json(file_path): + try: + with open(file_path, "r") as file: + return json.load(file) + except (FileNotFoundError, json.JSONDecodeError): + return {"guilds": {}} + +def write_json(file_path, data): + with open(file_path, "w") as file: + json.dump(data, file, indent=4, ensure_ascii=False) + +def get_or_create_guild_config(file_path, guild_id, default_config): + data = read_json(file_path) + if "guilds" not in data: + data["guilds"] = {} + + guild_id_str = str(guild_id) + if guild_id_str not in data["guilds"]: + data["guilds"][guild_id_str] = default_config + write_json(file_path, data) + return data["guilds"][guild_id_str] + +def update_guild_config(file_path, guild_id, new_data): + data = read_json(file_path) + if "guilds" not in data: + data["guilds"] = {} + + data["guilds"][str(guild_id)] = new_data + write_json(file_path, data) + +def getIgnore(guild_id): + default_config = { + "channel": [], + "role": None, + "user": [], + "bypassrole": None, + "bypassuser": [], + "commands": [] + } + return get_or_create_guild_config("ignore.json", guild_id, default_config) + +def updateignore(guild_id, data): + update_guild_config("ignore.json", guild_id, data) + + + + + +async def getConfig(guildID): + async with aiosqlite.connect('db/prefix.db') as db: + async with db.execute("SELECT prefix FROM prefixes WHERE guild_id = ?", (guildID,)) as cursor: + row = await cursor.fetchone() + if row: + return {"prefix": row[0]} + else: + defaultConfig = {"prefix": ">"} + await updateConfig(guildID, defaultConfig) + return defaultConfig + +async def updateConfig(guildID, data): + async with aiosqlite.connect('db/prefix.db') as db: + await db.execute( + "INSERT OR REPLACE INTO prefixes (guild_id, prefix) VALUES (?, ?)", + (guildID, data["prefix"]) + ) + await db.commit() + + + +def restart_program(): + python = sys.executable + os.execl(python, python, *sys.argv) + + +def blacklist_check(): + + async def predicate(ctx): + async with aiosqlite.connect('db/block.db') as db: + cursor = await db.execute("SELECT 1 FROM user_blacklist WHERE user_id = ?", (str(ctx.author.id),)) + user_blacklisted = await cursor.fetchone() + if user_blacklisted: + return False + + cursor = await db.execute("SELECT 1 FROM guild_blacklist WHERE guild_id = ?", (str(ctx.guild.id),)) + guild_blacklisted = await cursor.fetchone() + if guild_blacklisted: + return False + + return True + + return commands.check(predicate) + + +async def get_ignore_data(guild_id: int) -> dict: + async with aiosqlite.connect("db/ignore.db") as db: + data = { + "channel": set(), + "user": set(), + "command": set(), + "bypassuser": set(), + } + + async with db.execute("SELECT channel_id FROM ignored_channels WHERE guild_id = ?", (guild_id,)) as cursor: + channels = await cursor.fetchall() + data["channel"] = {str(channel_id) for (channel_id,) in channels} + + async with db.execute("SELECT user_id FROM ignored_users WHERE guild_id = ?", (guild_id,)) as cursor: + users = await cursor.fetchall() + data["user"] = {str(user_id) for (user_id,) in users} + + async with db.execute("SELECT command_name FROM ignored_commands WHERE guild_id = ?", (guild_id,)) as cursor: + commands = await cursor.fetchall() + data["command"] = {command_name.strip().lower() for (command_name,) in commands} + + async with db.execute("SELECT user_id FROM bypassed_users WHERE guild_id = ?", (guild_id,)) as cursor: + bypass_users = await cursor.fetchall() + data["bypassuser"] = {str(user_id) for (user_id,) in bypass_users} + + return data + +def ignore_check(): + async def predicate(ctx): + data = await get_ignore_data(ctx.guild.id) + ch = data["channel"] + iuser = data["user"] + cmd = data["command"] + buser = data["bypassuser"] + + if str(ctx.author.id) in buser: + return True + if str(ctx.channel.id) in ch or str(ctx.author.id) in iuser: + return False + + command_name = ctx.command.name.strip().lower() + aliases = [alias.strip().lower() for alias in ctx.command.aliases] + if command_name in cmd or any(alias in cmd for alias in aliases): + return False + + return True + + return commands.check(predicate) + +def top_check(): + async def predicate(ctx): + if not ctx.guild: + return True + + if getattr(ctx, "invoked_with", None) in ["help", "h"]: + return True + + topcheck_enabled = await is_topcheck_enabled(ctx.guild.id) + + if not topcheck_enabled: + return True + + if ctx.author != ctx.guild.owner and ctx.author.top_role.position <= ctx.guild.me.top_role.position: + embed = discord.Embed( + title=f"{DENIED} Access Denied", + description="Your top role must be at a **higher** position than my top role.", + color=0x000000 + ) + embed.set_footer( + text=f"“{ctx.command.qualified_name}” command executed by {ctx.author}", + icon_url=ctx.author.avatar.url if ctx.author.avatar else ctx.author.default_avatar.url + ) + await ctx.send(embed=embed) + return False + + return True + + return commands.check(predicate) \ No newline at end of file diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py new file mode 100644 index 0000000..99405a0 --- /dev/null +++ b/bot/utils/__init__.py @@ -0,0 +1,20 @@ + + +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from .config import * +from .Tools import * +from .paginators import * +from .paginator import * \ No newline at end of file diff --git a/bot/utils/ai_utils.py b/bot/utils/ai_utils.py new file mode 100644 index 0000000..ade1a95 --- /dev/null +++ b/bot/utils/ai_utils.py @@ -0,0 +1,167 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import aiohttp +import io +import time +import os +import random +import json +from langdetect import detect +from gtts import gTTS +from urllib.parse import quote +from utils.config_loader import load_current_language, config +from openai import AsyncOpenAI +from duckduckgo_search import AsyncDDGS +from dotenv import load_dotenv + +load_dotenv() + +current_language = load_current_language() +internet_access = config['INTERNET_ACCESS'] + +client = AsyncOpenAI( + base_url=config['API_BASE_URL'], + api_key="nah-ha", +) + +async def generate_response(instructions, history): + messages = [ + {"role": "system", "name": "instructions", "content": instructions}, + *history, + ] + + tools = [ + { + "type": "function", + "function": { + "name": "searchtool", + "description": "Searches the internet.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The query for search engine", + } + }, + "required": ["query"], + }, + }, + } + ] + response = await client.chat.completions.create( + model=config['MODEL_ID'], + messages=messages, + tools=tools, + tool_choice="auto", + ) + response_message = response.choices[0].message + tool_calls = response_message.tool_calls + + if tool_calls: + available_functions = { + "searchtool": duckduckgotool, + } + messages.append(response_message) + + for tool_call in tool_calls: + function_name = tool_call.function.name + function_to_call = available_functions[function_name] + function_args = json.loads(tool_call.function.arguments) + function_response = await function_to_call( + query=function_args.get("query") + ) + messages.append( + { + "tool_call_id": tool_call.id, + "role": "tool", + "name": function_name, + "content": function_response, + } + ) + second_response = await client.chat.completions.create( + model=config['MODEL_ID'], + messages=messages + ) + return second_response.choices[0].message.content + return response_message.content + +async def duckduckgotool(query) -> str: + if config['INTERNET_ACCESS']: + return "internet access has been disabled by user" + blob = '' + results = await AsyncDDGS(proxy=None).text(query, max_results=6) + try: + for index, result in enumerate(results[:6]): # Limiting to 6 results + blob += f'[{index}] Title : {result["title"]}\nSnippet : {result["body"]}\n\n\n Provide a cohesive response base on provided Search results' + except Exception as e: + blob += f"Search error: {e}\n" + return blob + + +async def poly_image_gen(session, prompt): + seed = random.randint(1, 100000) + image_url = f"https://image.pollinations.ai/prompt/{prompt}?seed={seed}" + async with session.get(image_url) as response: + image_data = await response.read() + return io.BytesIO(image_data) + +async def generate_image_prodia(prompt, model, sampler, seed, neg): + print("\033[1;32m(Prodia) Creating image for :\033[0m", prompt) + start_time = time.time() + async def create_job(prompt, model, sampler, seed, neg): + url = 'https://api.prodia.com/generate' + params = { + 'new': 'true', + 'prompt': f'{quote(prompt)}', + 'model': model, + 'steps': '100', + 'cfg': '9.5', + 'seed': f'{seed}', + 'sampler': sampler, + 'upscale': 'True', + 'aspect_ratio': 'square' + } + async with aiohttp.ClientSession() as session: + async with session.get(url, params=params) as response: + data = await response.json() + return data['job'] + + job_id = await create_job(prompt, model, sampler, seed, neg) + url = f'https://api.prodia.com/job/{job_id}' + headers = { + 'authority': 'api.prodia.com', + 'accept': '*/*', + } + + async with aiohttp.ClientSession() as session: + while True: + async with session.get(url, headers=headers) as response: + json = await response.json() + if json['status'] == 'succeeded': + async with session.get(f'https://images.prodia.xyz/{job_id}.png?download=1', headers=headers) as response: + content = await response.content.read() + img_file_obj = io.BytesIO(content) + duration = time.time() - start_time + print(f"\033[1;34m(Prodia) Finished image creation\n\033[0mJob id : {job_id} Prompt : ", prompt, "in", duration, "seconds.") + return img_file_obj + +async def text_to_speech(text): + bytes_obj = io.BytesIO() + detected_language = detect(text) + tts = gTTS(text=text, lang=detected_language) + tts.write_to_fp(bytes_obj) + bytes_obj.seek(0) + return bytes_obj \ No newline at end of file diff --git a/bot/utils/arial.ttf b/bot/utils/arial.ttf new file mode 100644 index 0000000..8682d94 Binary files /dev/null and b/bot/utils/arial.ttf differ diff --git a/bot/utils/config.py b/bot/utils/config.py new file mode 100644 index 0000000..d93faff --- /dev/null +++ b/bot/utils/config.py @@ -0,0 +1,86 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import os +from dotenv import load_dotenv + +load_dotenv() + +TOKEN = os.environ.get("TOKEN") +BRAND_NAME = os.environ.get("brand_name", "Zyrox X") +NAME = BRAND_NAME +BotName = BRAND_NAME + +server = "https://discord.gg/codexdev" +serverLink = "https://discord.gg/codexdev" +ch = "https://discord.com/channels/699587669059174461/1271825678710476911" + +# ── Security / feature toggles ──────────────────────────────────────────────── + +def _env_bool(key: str, default: bool = False) -> bool: + raw = os.getenv(key, str(default)).strip().lower() + return raw in ("1", "true", "yes", "on") + + +JISHAKU_ENABLED = _env_bool("JISHAKU_ENABLED", False) +API_ENABLED = _env_bool("API_ENABLED", False) +TUNNEL_ENABLED = _env_bool("TUNNEL_ENABLED", False) +TUNNEL_ALLOW_BINARY_DOWNLOAD = _env_bool("TUNNEL_ALLOW_BINARY_DOWNLOAD", False) + +API_HOST = os.getenv("API_HOST", "127.0.0.1").strip() or "127.0.0.1" +API_PORT = int(os.getenv("API_PORT", "8000")) + +# ── Webhooks (only active when explicitly configured) ───────────────────────── + +def _valid_webhook_url(url: str | None) -> str | None: + if not url: + return None + url = url.strip() + if not url or url == "https://discord.com/api/webhooks/": + return None + if "discord.com/api/webhooks/" not in url and "discordapp.com/api/webhooks/" not in url: + return None + return url + +CMD_WEBHOOK_URL = _valid_webhook_url(os.getenv("CMD_WEBHOOK_URL")) + +# ── Optional Discord log / stats channels (unset = disabled) ────────────────── + +def _parse_optional_id(env_key: str) -> int | None: + raw = os.getenv(env_key, "").strip() + if not raw or not raw.isdigit(): + return None + return int(raw) + +LOG_CHANNEL_ID = _parse_optional_id("LOG_CHANNEL_ID") +SERVER_COUNT_CHANNEL_ID = _parse_optional_id("SERVER_COUNT_CHANNEL_ID") +USER_COUNT_CHANNEL_ID = _parse_optional_id("USER_COUNT_CHANNEL_ID") + +# ── Owner / Staff IDs ───────────────────────────────────────────────────────── +# REQUIRED: set OWNER_IDS in .env — no hardcoded fallback IDs. + +def _parse_ids(env_key: str) -> list[int]: + raw = os.getenv(env_key, "").strip() + if not raw: + return [] + return [int(p.strip()) for p in raw.split(",") if p.strip().isdigit()] + +OWNER_IDS: list[int] = _parse_ids("OWNER_IDS") +OWNER_IDS_STR: list[str] = [str(i) for i in OWNER_IDS] + +# Aliases kept for backwards compatibility with files that import these names +BOT_OWNER_IDS = OWNER_IDS +BOT_OWNER_IDS_STR = OWNER_IDS_STR +STAFF_IDS = OWNER_IDS +STAFF_IDS_STR = OWNER_IDS_STR diff --git a/bot/utils/config_loader.py b/bot/utils/config_loader.py new file mode 100644 index 0000000..0348fba --- /dev/null +++ b/bot/utils/config_loader.py @@ -0,0 +1,59 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import yaml +import json +import os + +# Config load +with open('config.yml', 'r', encoding='utf-8') as config_file: + config = yaml.safe_load(config_file) + +## Language settings ## +valid_language_codes = [] +lang_directory = "lang" + +current_language_code = config['LANGUAGE'] + +for filename in os.listdir(lang_directory): + if filename.startswith("lang.") and filename.endswith(".json") and os.path.isfile( + os.path.join(lang_directory, filename)): + language_code = filename.split(".")[1] + valid_language_codes.append(language_code) + +def load_current_language() -> dict: + lang_file_path = os.path.join( + lang_directory, f"lang.{current_language_code}.json") + with open(lang_file_path, encoding="utf-8") as lang_file: + current_language = json.load(lang_file) + return current_language + +# Instructions loader +def load_instructions() -> dict: + instructions = {} + for file_name in os.listdir("instructions"): + if file_name.endswith('.txt'): + file_path = os.path.join("instructions", file_name) + with open(file_path, 'r', encoding='utf-8') as file: + file_content = file.read() + # Use the file name without extension as the variable name + variable_name = file_name.split('.')[0] + instructions[variable_name] = file_content + return instructions + +def load_active_channels() -> dict: + if os.path.exists("channels.json"): + with open("channels.json", "r", encoding='utf-8') as f: + active_channels = json.load(f) + return active_channels \ No newline at end of file diff --git a/bot/utils/cv2.py b/bot/utils/cv2.py new file mode 100644 index 0000000..0e259eb --- /dev/null +++ b/bot/utils/cv2.py @@ -0,0 +1,108 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +""" +Shared CV2 (Component V2) utilities for the bot. +Provides a helper to build Container objects since Container() +does NOT accept positional children arguments — items must be +added via .add_item(). +""" + +import discord +from discord.ui import LayoutView, TextDisplay, Separator, Container + + +def build_container(*items, accent_color=None): + """Build a Container and add items to it via .add_item().""" + container = Container(accent_color=accent_color) + for item in items: + container.add_item(item) + return container + + +class CV2(LayoutView): + """Quick helper: CV2("Title", "section1", "section2", ...)""" + def __init__(self, title, *sections): + super().__init__(timeout=None) + container = build_container( + TextDisplay(f"**{title}**"), + *[item for s in sections for item in (Separator(visible=True), TextDisplay(str(s)))] + ) + self.add_item(container) + +def add_action_rows(container, components): + """Safely adds components to a CV2 container across multiple ActionRows""" + from discord.ui import ActionRow + current_row = [] + for item in components: + if getattr(item, 'type', None) and getattr(item.type, 'value', 0) != 2: + if current_row: + container.add_item(ActionRow(*current_row)) + current_row = [] + container.add_item(ActionRow(item)) + else: + current_row.append(item) + if len(current_row) == 5: + container.add_item(ActionRow(*current_row)) + current_row = [] + if current_row: + container.add_item(ActionRow(*current_row)) + +class CV2Embed(CV2): + """A CV2 container that behaves like a discord.Embed.""" + def __init__(self, title="", description="", **kwargs): + self._title = title + self._description = description or "" + self._fields = [] + self._footer = None + super().__init__(title, self._description) + self.color = kwargs.get("color", 0xFF0000) + + def _rebuild(self): + self.clear_items() + sections = [self._description] if self._description else [] + for name, value in self._fields: + sections.append(f"**{name}**\n{value}") + + if self._footer: + sections.append(f"*{self._footer}*") + + container = build_container( + TextDisplay(f"**{self._title}**"), + *[item for s in sections for item in (Separator(visible=True), TextDisplay(str(s)))] + ) + self.add_item(container) + + def add_field(self, name, value, inline=False, **kwargs): + self._fields.append((name, value)) + self._rebuild() + return self + + def set_footer(self, text=None, icon_url=None, **kwargs): + if text: + self._footer = text + self._rebuild() + return self + + def set_thumbnail(self, url=None, **kwargs): + return self + + def set_author(self, name=None, url=None, icon_url=None, **kwargs): + return self + + def set_image(self, url=None, **kwargs): + return self + + def to_dict(self): + return {"title": self._title, "description": self._description} diff --git a/bot/utils/emoji.py b/bot/utils/emoji.py new file mode 100644 index 0000000..9719fdd --- /dev/null +++ b/bot/utils/emoji.py @@ -0,0 +1,379 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +""" +Centralized emoji module for the Zyrox bot. +All emoji definitions are stored here for easy management and consistency. +""" + +# ============================================================================ +# DISCORD CUSTOM EMOJIS (Static) +# ============================================================================ +BOOST = "<:boost:1448966463586041906>" +BUG_HUNTER = "<:BugHunterLevel1:1448949674898620518>" +BUG_HUNTER_LVL2 = "<:BugHunterLvl2:1122549925237375086>" +CAST = "<:zcast:1448951414301655175>" +CERTIFIED_MODERATOR = "<:CertifiedDiscordModerator:1448949742792085516>" +CHANNEL = "<:channel:1448951734096625727>" +CODEBASE = "<:codebase:1448951697853386826>" +CODED = "<:coded:1448966435622752389>" +CROSS = "<:zcross:1448951756372443296>" +CROSS_ALT = "<:CrossIcon:1327829124894429235>" +CUTE_CUTE_CUTE = "<:Cute_Cute_Cute:1384578375221248010>" +DELETE = "<:delete:1327842168693461022>" +DELETE_ALT1 = "<:delete:1448966413242073088>" +DENIED = "<:Denied:1294218790082711553>" +DISABLE = "<:Disable:1448949515787964417>" +DND = "<:dnd:1448951614172954664>" +EARLY_SUPPORTER = "<:EarlySupporter:1448949719752773703>" +ENABLE = "<:Enable:1448949527846322296>" +ERROR = "<:error:1397218903389044776>" +FORWARD = "<:forward:1329361532999569439>" +GAMES = "<:games:1448951285498777641>" +HANDSHAKE = "<:handshake:1448949571811282984>" +HAPPY_PANDA = "<:happy_panda:1384576519904559206>" +HEADMOD = "<:headmod:1274781954482376857>" +HEART3 = "<:heart3:1448966390353756200>" +HEART_EM = "<:heart_em:1274781856406962250>" +HEERIYE = "<:Heeriye:1274769360560328846>" +HOME = "<:icons_home:1337295807430393958>" +HYPESQUAD_BRILLIANCE = "<:Hypesquad_Brilliance:1448949708381749370>" +ICONLOAD = "<:iconLoad:1327829324518391824>" +ICONS_CHANNEL = "<:icons_channel:1327829380935843941>" +ICONS_MUSIC = "<:icons_music:1327829459729911900>" +ICONS_PAUSE = "<:icons_pause:1327829480835780609>" +ICONS_PLUS = "<:icons_plus:1328966531140288524>" +ICONS_WARNING = "<:icons_warning:1448949538944716922>" +ICONS_WARNING_ALT1 = "<:icons_warning:1327829522573430864>" +ICON_BROWSER = "<:icon_browser:1448951659521773598>" +IDLE = "<:idle:1448951603028693042>" +INDEX = "<:index:1448951296370544790>" +INFO = "<:info:1374723970376405113>" +KING = "<:king:1448951721479901334>" +LEVEL_UP = "<:zlevelup:1448964376504696943>" +LOCK = "<:lock:1448949549455511685>" +MANAGER = "<:manager:1394348646803439709>" +MENTION = "<:zyrox_mention:1448949481776222218>" +MESSAGE = "<:zmsg:1448964399166394483>" +MINECRAFT = "<:zmc:1448964387426537474>" +ML_CROSS = "<:ml_cross:1204106928675102770>" +MUSIC = "<:zmusic:1448951372707008533>" +MUSICSTOP_ICONS = "<:musicstop_icons:1327829536053923934>" +MUTE = "<:zmute:1448951435478700072>" +NEW = "<:New:1448949337395695616>" +NEXT = "<:icons_next:1327829470027055184>" +NEXT_ALT1 = "<:next:1448949316109733920>" +OFFLINE = "<:offline:1448951625506099261>" +PARTNER_BADGE = "<:PartneredServerOwner:1122549945532297246>" +PC = "<:pc:1448951637266665542>" +PIN = "<:zpin:1448949810462855249>" +PREVIOUS = "<:next:1327829548426854522>" +RED_BUTTON = "<:red_button:1401444612874305671>" +RED_PIN = "<:red_pin:1448949326846889994>" +REWIND = "<:rewind1:1329360839874056225>" +REWIND_ALT1 = "<:rewind:1500793294865563658>" +SEED = "<:zseed:1448951477640101929>" +SHUFFLE = "<:shuffle:1329360518367936564>" +SKIP = "<:skip:1329359900563996754>" +STAR = "<:starr:1448951307707748395>" +SWORD = "<:zsowrd:1448951362238021682>" +SYSTEM = "<:zyrox_system:1448949359159939143>" +THUNDER = "<:zyroxthunder:1448949415200034907>" +TICK = "<:ztick:1448951767990796298>" +TICKET = "<:zticket:1448951318713470987>" +TICK_ALT = "<:tick:1327829594954530896>" +TIME = "<:zyrox_time:1448949493012889610>" +TIMER = "<:ztimer:1448949799528173621>" +UNLOCK = "<:unlock:1448949560457171070>" +UPTIME = "<:uptime:1398280366501920829>" +U_ADMIN = "<:U_admin:1327829252120510567>" +WARNING = "<:warning:1448951779353038949>" +WARNING_ALT = "<:warning:1396796622347833378>" +WIFI = "<:zwifi:1448951466931912715>" +ZAI = "<:zai:1448949821611446302>" +ZARROW = "<:zArrow:1448951532837015643>" +ZBACK = "<:zback:1448949305229443124>" +ZBAN = "<:zban:1448951424665784373>" +ZBOT = "<:zbot:1448951393216888905>" +ZCIRCLE = "<:zcircle:1448964410155470848>" +ZCIRCLE_ALT1 = "<:zcircle:1448951351601270814>" +ZCLOUD = "<:zCloud:1448951498213032036>" +ZCOUNTING = "<:zcounting:1448949348103749713>" +ZCROSS = "<:zcross:1448951767990796298>" +ZDIL = "<:zdil:1448949605676093540>" +ZHUMAN = "<:zHuman:1448951509235531869>" +ZMODULE = "<:zmodule:1448951340716785744>" +ZMUSICPAUSE = "<:zmusicpause:1448951801931108413>" +ZPAUSE = "<:zpause:1448949283423522928>" +ZPEOPLE = "<:zpeople:1448951456861519962>" +ZPLAY = "<:zplay:1448949294412337222>" +ZPLUS = "<:zplus:1448951790463615038>" +ZROCKET = "<:zrocket:1448951445989888010>" +ZSAFE = "<:zSafe:1448951403434479626>" +ZSETTINGS = "<:zsettings:1448951745706459206>" +ZTADA = "<:ztada:1448951329664925717>" +ZTICK = "<:Ztick:1222750301233090600>" +ZUNMUTE = "<:zunmute:1448951487970414694>" +ZWARNING = "<:zwarning:1448949627712966717>" +ZWRENCH = "<:zwrench:1448951382597177495>" +ZYROXCONNECTION = "<:zyroxconnection:1448949425828528230>" +ZYROXHAMMER = "<:zyroxhammer:1448949447617806458>" +ZYROXLINKS = "<:zyroxlinks:1448949436939239495>" +ZYROXSYS = "<:zyroxsys:1448949469650620426>" +ZYROX_CODE = "<:zyrox_code:1448949381436014662>" +ZYROX_COMMAND = "<:zyrox_command:1448949381436014662>" +ZYROX_GLOBAL = "<:zyrox_global:1448949370539217026>" +ZYROX_OWNER = "<:zyrox_owner:1448949381436014662>" +ZYROX_SEARCH = "<:zyrox_search:1448949381436014662>" + +# ============================================================================ +# DISCORD CUSTOM EMOJIS (Animated) +# ============================================================================ +ACTIVE_DEVELOPER = "" +ARROWRED = "" +BLACKCROWN = "" +BLOBPART = "" +BOOSTS = "" +EARLY_VERIFIED_BOT_DEV = "" +EMOTE = "" +GIFD = "" +GIFN = "" +HYPESQUAD_BALANCE = "" +HYPESQUAD_BRAVERY = "" +HYPESQUAD_EVENTS = "" +KING_ALT1 = "" +LOADING = "" +LOADINGRED = "" +LOADING_ALT1 = "" +MAX__A = "" +MENTION_ALT1 = "" +MINGLE = "" +MOBILE = "" +MUSIC_ALT1 = "" +NITRO_BOOST = "" +ONLINE = "" +PREMIUM = "" +RACECAR64 = "" +REDDOT = "" +REDHEART = "" +REDRULESBOOK = "" +SG_RD = "" +STAFF = "" +STAR_ALT1 = "" +STAR_ALT2 = "" +TADAA = "" +TIMER_ALT1 = "" +_37496ALERT = "" + +# ============================================================================ +# DISCORD BADGE EMOJIS MAPPING (Dictionary) +# ============================================================================ +DISCORD_BADGE_EMOJIS = { + "staff": STAFF, + "partner": PARTNER_BADGE, + "hypesquad": HYPESQUAD_BRILLIANCE, + "hypesquad_bravery": HYPESQUAD_BRAVERY, + "hypesquad_brilliance": HYPESQUAD_BRILLIANCE, + "hypesquad_balance": HYPESQUAD_BALANCE, + "bug_hunter": BUG_HUNTER, + "bug_hunter_level_2": BUG_HUNTER_LVL2, + "early_supporter": EARLY_SUPPORTER, + "early_verified_bot_developer": EARLY_VERIFIED_BOT_DEV, + "certified_moderator": CERTIFIED_MODERATOR, + "active_developer": ACTIVE_DEVELOPER, + "discord_mod": CERTIFIED_MODERATOR, +} + +# ============================================================================ +# UNICODE EMOJIS +# ============================================================================ +ARROW_DOWN = "⬇️" +ARROW_LEFT = "⬅️" +ARROW_RIGHT = "➡️" +ARROW_UP = "⬆️" +BLOCK = "🚫" +BUBBLE_TEA = "🧋" +CELEBRATE = "🎉" +CHERRIES = "🍒" +CLOCK = "⏱️" +COOKIE = "🍪" +CURSOR = "🖱️" +DIZZY = "🥴" +ERROR_UNICODE = "❌" +GAME_CONTROLLER = "🎮" +HEARTS = "💕" +JAVA_COFFEE = "☕" +LAUGH1 = "😂" +LAUGH2 = "🤣" +LAUGH3 = "😆" +LOCK_UNICODE = "🔒" +MONEY = "💸" +MOON = "🌙" +NOTE = "📝" +PAPER = "\U0001f4f0" +PEACH = "🍑" +REFRESH = "🔄" +ROCK = "\U0001faa8" +SCISSORS = "\U00002702" +SHOCKED = "😳" +SPARKLE = "✨" +STAR_UNICODE = "⭐" +STOP_BUTTON = "⏹️" +SUCCESS = "✅" +TARGET = "🎯" +TONGUE_OUT = "😜" +UPSIDE_DOWN = "🙃" +WARNING_UNICODE = "⚠️" + +# ============================================================================ +# EMOJI COLLECTIONS BY CATEGORY (Dictionaries) +# ============================================================================ +GAME_BUTTONS = { + "up": ARROW_UP, + "down": ARROW_DOWN, + "left": ARROW_LEFT, + "right": ARROW_RIGHT, + "stop": STOP_BUTTON, + "target": "🎯", +} + +ACTION_EMOJIS = { + "success": SUCCESS, + "error": ERROR_UNICODE, + "warning": WARNING_UNICODE, + "clock": CLOCK, + "refresh": REFRESH, +} + +RPS_CHOICES = { + "rock": ROCK, + "scissors": SCISSORS, + "paper": PAPER, +} + +BUTTON_EMOJIS = { + "note": NOTE, + "privacy": LOCK_UNICODE, + "claim": STAR_UNICODE, + "untrust": ERROR_UNICODE, + "block": BLOCK, + "target": "🎯", + "edit": "✏️", +} + +REACTION_TEST_EMOJIS = [ + COOKIE, CELEBRATE, BUBBLE_TEA, CHERRIES, PEACH, MONEY, MOON, HEARTS +] + +FUN_EMOJIS = [ + LAUGH1, LAUGH2, LAUGH3, SHOCKED, DIZZY, UPSIDE_DOWN, TONGUE_OUT +] + +MINECRAFT_EMOJIS = { + "success": SUCCESS, + "error": ERROR_UNICODE, + "warning": WARNING_UNICODE, + "clock": CLOCK, + "refresh": REFRESH, + "java": JAVA_COFFEE, +} + +# ============================================================================ +# FEATURE EMOJIS (Dictionaries) +# ============================================================================ +MODERATION_EMOJIS = { + "warn": WARNING, + "mute": MUTE, + "ban": SWORD, + "kick": SWORD, + "lock": LOCK, +} + +TICKET_EMOJIS = { + "ticket": TICKET, + "close": ERROR, + "open": SUCCESS, + "pin": PIN, +} + +LEVEL_EMOJIS = { + "level_up": LEVEL_UP, + "sparkle": SPARKLE, + "achievement": STAR, +} + +UTILITY_EMOJIS = { + "music": MUSIC, + "system": SYSTEM, + "new": NEW, + "message": MESSAGE, + "wifi": WIFI, + "cast": CAST, +} + +# ============================================================================ +# HELPER FUNCTIONS +# ============================================================================ + +def get_badge_emoji(badge_name: str) -> str: + """ + Get Discord badge emoji by name. + + Args: + badge_name: The name of the badge (e.g., 'staff', 'partner', 'bug_hunter') + + Returns: + The emoji string for the badge, or None if not found + """ + return DISCORD_BADGE_EMOJIS.get(badge_name.lower()) + + +def get_action_emoji(action: str) -> str: + """ + Get emoji for a common action. + + Args: + action: The action name (e.g., 'success', 'error', 'warning') + + Returns: + The emoji string for the action + """ + return ACTION_EMOJIS.get(action.lower()) + + +def get_button_emoji(button_type: str) -> str: + """ + Get emoji for a button type. + + Args: + button_type: The button type (e.g., 'note', 'privacy', 'claim') + + Returns: + The emoji string for the button + """ + return BUTTON_EMOJIS.get(button_type.lower()) + + +# ============================================================================ +# COMPATIBILITY ALIASES +# ============================================================================ + +# Common aliases for frequently used emojis +CHECKMARK = TICK +CROSS_MARK = CROSS +CHECK = SUCCESS +FAIL = ERROR +OK = SUCCESS +NOT_OK = ERROR + diff --git a/bot/utils/help.py b/bot/utils/help.py new file mode 100644 index 0000000..b9d64ae --- /dev/null +++ b/bot/utils/help.py @@ -0,0 +1,227 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +import discord +from utils.Tools import * +from utils.cv2 import build_container +from utils.emoji import REWIND, PREVIOUS, NEXT, FORWARD, DELETE, HOME +from discord.ui import LayoutView, TextDisplay, Separator, ActionRow + + +class Dropdown(discord.ui.Select): + + def __init__(self, ctx, options, placeholder="Choose a Category for Help"): + super().__init__(placeholder=placeholder, + min_values=1, + max_values=1, + options=options) + self.invoker = ctx.author + + async def callback(self, interaction: discord.Interaction): + if self.invoker == interaction.user: + index = self.view.find_index_from_select(self.values[0]) + if not index: + index = 0 + await self.view.set_page(index, interaction) + else: + await interaction.response.send_message( + "You must run this command to interact with it.", ephemeral=True) + + +class View(LayoutView): + + def __init__(self, mapping: dict, ctx, homeembed, ui: int): + super().__init__(timeout=None) + self.mapping = mapping + self.ctx = ctx + self.index = 0 + self.current_page = 0 + self.ui = ui + + self.options, self.pages, self.total_pages = self.gen_pages(homeembed) + self.pages[0]['footer'] = f"• Help page 1/{self.total_pages} | Requested by: {self.ctx.author.display_name}" + self._rebuild() + + def _rebuild(self): + self.clear_items() + page = self.pages[self.index] + page['footer'] = f"• Help page {self.index + 1}/{self.total_pages} | Requested by: {self.ctx.author.display_name}" + + # Build container items (text content) + items = [] + if page.get('title'): + items.append(TextDisplay(f"**{page['title']}**")) + if page.get('description'): + if items: + items.append(Separator(visible=True)) + items.append(TextDisplay(page['description'])) + for name, value in page.get('fields', []): + items.append(Separator(visible=True)) + items.append(TextDisplay(f"**{name}**\n{value}")) + + # Build buttons + is_first = self.index == 0 + is_last = self.index >= len(self.pages) - 1 + + homeB = discord.ui.Button(label="", emoji=REWIND, style=discord.ButtonStyle.secondary, disabled=is_first) + backB = discord.ui.Button(label="", emoji=PREVIOUS, style=discord.ButtonStyle.secondary, disabled=is_first) + quitB = discord.ui.Button(label="", emoji=DELETE, style=discord.ButtonStyle.danger) + nextB = discord.ui.Button(label="", emoji=NEXT, style=discord.ButtonStyle.secondary, disabled=is_last) + lastB = discord.ui.Button(label="", emoji=FORWARD, style=discord.ButtonStyle.secondary, disabled=is_last) + + homeB.callback = self._home_cb + backB.callback = self._back_cb + quitB.callback = self._quit_cb + nextB.callback = self._next_cb + lastB.callback = self._last_cb + + # Add buttons ActionRow inside the container + items.append(ActionRow(homeB, backB, quitB, nextB, lastB)) + + # Add dropdowns inside the container + if self.ui == 0: + items.append(ActionRow(Dropdown(ctx=self.ctx, options=self.options))) + elif self.ui == 2: + mid = len(self.options) // 2 + o1, o2 = self.options[:mid], self.options[mid:] + if o1: + items.append(ActionRow(Dropdown(ctx=self.ctx, options=o1, placeholder="Main Commands"))) + if o2: + items.append(ActionRow(Dropdown(ctx=self.ctx, options=o2, placeholder="Extra Commands"))) + elif self.ui == 3: + items.append(ActionRow(Dropdown(ctx=self.ctx, options=self.options))) + + # Add footer after controls + if page.get('footer'): + items.append(Separator(visible=True)) + items.append(TextDisplay(f"*{page['footer']}*")) + + # Build the single container with everything inside + self.add_item(build_container(*items)) + + async def _check(self, interaction): + if interaction.user != self.ctx.author: + await interaction.response.send_message("You must run this command to interact with it.", ephemeral=True) + return False + return True + + async def _home_cb(self, interaction): + if await self._check(interaction): + await self.set_page(0, interaction) + + async def _back_cb(self, interaction): + if await self._check(interaction): + await self.set_page(self.index - 1 if self.index > 0 else len(self.pages) - 1, interaction) + + async def _quit_cb(self, interaction): + if await self._check(interaction): + await interaction.response.defer() + await interaction.delete_original_response() + + async def _next_cb(self, interaction): + if await self._check(interaction): + await self.set_page(self.index + 1 if self.index < len(self.pages) - 1 else 0, interaction) + + async def _last_cb(self, interaction): + if await self._check(interaction): + await self.set_page(len(self.pages) - 1, interaction) + + def find_index_from_select(self, value): + i = 0 + used_labels = set() + for cog in self.get_cogs(): + if cog.__class__.__name__ == "Roleplay": + continue + if "help_custom" in dir(cog): + _, label, _ = cog.help_custom() + original_label = label + counter = 1 + while label in used_labels: + label = f"{original_label} {counter}" + counter += 1 + used_labels.add(label) + if label == value or value.startswith(original_label + " "): + return i + 1 + i += 1 + return 0 + + def get_cogs(self): + return list(self.mapping.keys()) + + def gen_pages(self, homeembed): + options, pages = [], [] + total_pages = 0 + used_labels = set() + + options.append(discord.SelectOption(label="Home", emoji=HOME, description="")) + + # Convert homeembed (CV2Embed) to page data + if hasattr(homeembed, '_title'): + home_page = { + 'title': homeembed._title or '', + 'description': homeembed._description or '', + 'fields': list(homeembed._fields) if hasattr(homeembed, '_fields') else [], + 'footer': None + } + else: + home_page = { + 'title': getattr(homeembed, 'title', '') or '', + 'description': getattr(homeembed, 'description', '') or '', + 'fields': [(f.name, f.value) for f in homeembed.fields] if hasattr(homeembed, 'fields') and homeembed.fields else [], + 'footer': homeembed.footer.text if hasattr(homeembed, 'footer') and homeembed.footer else None + } + + pages.append(home_page) + total_pages += 1 + used_labels.add("Home") + + for cog in self.get_cogs(): + if cog.__class__.__name__ == "Roleplay": + continue + if "help_custom" in dir(cog): + emoji, label, description = cog.help_custom() + original_label = label + counter = 1 + while label in used_labels: + label = f"{original_label} {counter}" + counter += 1 + used_labels.add(label) + options.append(discord.SelectOption(label=label, emoji=emoji, description=description)) + + fields = [] + for command in cog.get_commands(): + params = "" + for param in command.clean_params: + if param not in ["self", "ctx"]: + params += f" <{param}>" + help_text = command.help or "No description available" + if len(help_text) > 1020: + help_text = help_text[:1017] + "..." + fields.append((f"{command.name}{params}", f"{help_text}\n•")) + + pages.append({ + 'title': f"{emoji} {original_label}", + 'description': '', + 'fields': fields, + 'footer': None + }) + total_pages += 1 + + return options, pages, total_pages + + async def set_page(self, page, interaction): + self.index = page + self.current_page = page + self._rebuild() + await interaction.response.edit_message(view=self) \ No newline at end of file diff --git a/bot/utils/paginator.py b/bot/utils/paginator.py new file mode 100644 index 0000000..a66729a --- /dev/null +++ b/bot/utils/paginator.py @@ -0,0 +1,226 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations +import asyncio +from typing import TYPE_CHECKING, Any, Dict, Optional +import discord +from discord.ext import commands +from discord.ext import menus +from discord.ext.commands import Context +from discord import Interaction, ButtonStyle +from utils.emoji import REWIND, PREVIOUS, NEXT, FORWARD, DELETE + + +class Paginator(discord.ui.View): + + def __init__( + self, + source: menus.PageSource, + *, + ctx: Context | Interaction, + check_embeds: bool = True, + ): + super().__init__() + self.source: menus.PageSource = source + self.check_embeds: bool = check_embeds + self.ctx = ctx + self.message: Optional[discord.Message] = None + self.current_page: int = 0 + self.clear_items() + self.fill_items() + + def fill_items(self) -> None: + + if self.source.is_paginating(): + max_pages = self.source.get_max_pages() + use_last_and_first = max_pages is not None and max_pages >= 2 + if use_last_and_first: + self.add_item(self.first_page_button) + self.add_item(self.previous_page_button) + self.add_item(self.stop_button) + self.add_item(self.next_page_button) + if use_last_and_first: + self.add_item(self.last_page_button) + #self.add_item(self.stop_button) + + async def _get_kwargs_from_page(self, page: int) -> Dict[str, Any]: + value = await discord.utils.maybe_coroutine(self.source.format_page, + self, page) + if isinstance(value, dict): + return value + elif isinstance(value, str): + return {'content': value, 'embed': None} + elif isinstance(value, discord.Embed): + return {'embed': value, 'content': None} + else: + return {} + + async def show_page(self, interaction: discord.Interaction, + page_number: int) -> None: + page = await self.source.get_page(page_number) + self.current_page = page_number + kwargs = await self._get_kwargs_from_page(page) + self._update_labels(page_number) + if kwargs: + if interaction.response.is_done(): + if self.message: + await self.message.edit(**kwargs, view=self) + else: + await interaction.response.edit_message(**kwargs, view=self) + + def _update_labels(self, page_number: int) -> None: + self.first_page_button.disabled = page_number == 0 + self.next_page_button.disabled = False + self.previous_page_button.disabled = False + + max_pages = self.source.get_max_pages() + if max_pages is not None: + self.last_page_button.disabled = (page_number + 1) >= max_pages + if (page_number + 1) >= max_pages: + self.next_page_button.disabled = True + if page_number == 0: + self.previous_page_button.disabled = True + + async def show_checked_page(self, interaction: discord.Interaction, + page_number: int) -> None: + max_pages = self.source.get_max_pages() + try: + if max_pages is None: + await self.show_page(interaction, page_number) + elif max_pages > page_number >= 0: + await self.show_page(interaction, page_number) + except IndexError: + pass + + async def interaction_check(self, + interaction: discord.Interaction) -> bool: + if isinstance(self.ctx, Interaction): + if interaction.user and interaction.user.id in ( + self.ctx.client.owner_id, self.ctx.user.id): + return True + await interaction.response.send_message("Uh oh! That message doesn't belong to you. You must run this command to interact with it.", + ephemeral=True) + return False + if interaction.user and interaction.user.id in (self.ctx.bot.owner_id, + self.ctx.author.id): + return True + await interaction.response.send_message("Uh oh! That message doesn't belong to you. You must run this command to interact with it.", + ephemeral=True) + return False + + async def on_timeout(self) -> None: + if self.message: + for child in self.children: + if isinstance(child, discord.ui.Button): + child.disabled = True + try: + await self.message.edit(view=self) + except Exception as e: + print(e) + + + async def on_error(self, interaction: discord.Interaction, + error: Exception, item: discord.ui.Item) -> None: + if interaction.response.is_done(): + await interaction.followup.send('An unknown error occurred, sorry', + ephemeral=True) + else: + await interaction.response.send_message( + 'An unknown error occurred, sorry', ephemeral=True) + + def update_styles(self, **kwargs): + """ + Update the button styles and emojis + """ + + # Update the button emojis + self.first_page_button.emoji = kwargs.get('first_button_emoji') or REWIND + self.previous_page_button.emoji = kwargs.get( + 'previous_button_emoji') or PREVIOUS + self.next_page_button.emoji = kwargs.get('next_button_emoji') or NEXT + self.last_page_button.emoji = kwargs.get('last_button_emoji') or FORWARD + self.stop_button.emoji = kwargs.get( + 'stop_button_emoji') or DELETE + + # Update the Button Styles + self.first_page_button.style = kwargs.get( + 'first_button_style') or ButtonStyle.secondary + self.previous_page_button.style = kwargs.get( + 'previous_button_style') or ButtonStyle.secondary + self.stop_button.style = kwargs.get( + 'stop_button_style') or ButtonStyle.red + self.next_page_button.style = kwargs.get( + 'next_button_style') or ButtonStyle.secondary + self.last_page_button.style = kwargs.get( + 'last_button_style') or ButtonStyle.secondary + + async def paginate(self, + *, + content: Optional[str] = None, + ephemeral: bool = False, + **kwargs) -> None: + self.update_styles(**kwargs) + if isinstance(self.ctx, Interaction): + await self.source._prepare_once() + page = await self.source.get_page(0) + kwargs = await self._get_kwargs_from_page(page) + if content: + kwargs.setdefault('content', content) + self._update_labels(0) + self.message = await self.ctx.response.send_message( + **kwargs, view=self, ephemeral=ephemeral) + return + + await self.source._prepare_once() + page = await self.source.get_page(0) + kwargs = await self._get_kwargs_from_page(page) + if content: + kwargs.setdefault('content', content) + self._update_labels(0) + self.message = await self.ctx.send(**kwargs, + view=self, + ephemeral=ephemeral) + + @discord.ui.button() + async def first_page_button(self, interaction: discord.Interaction, + button: discord.ui.Button): + """Go to the first page""" + await self.show_page(interaction, 0) + + @discord.ui.button() + async def previous_page_button(self, interaction: discord.Interaction, + button: discord.ui.Button): + """Go to the previous page""" + await self.show_checked_page(interaction, self.current_page - 1) + + @discord.ui.button() + async def stop_button(self, interaction: discord.Interaction, + button: discord.ui.Button): + """Stops the pagination session.""" + await interaction.response.defer() + await interaction.delete_original_response() + self.stop() + + @discord.ui.button() + async def next_page_button(self, interaction: discord.Interaction, + button: discord.ui.Button): + """Go to the next page""" + await self.show_checked_page(interaction, self.current_page + 1) + + @discord.ui.button() + async def last_page_button(self, interaction: discord.Interaction, + button: discord.ui.Button): + """Go to the last page""" + await self.show_page(interaction, self.source.get_max_pages() - 1) diff --git a/bot/utils/paginators.py b/bot/utils/paginators.py new file mode 100644 index 0000000..a461310 --- /dev/null +++ b/bot/utils/paginators.py @@ -0,0 +1,111 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations +import discord +from utils.config import BotName +try: + from discord.ext import menus + from discord.ext import commands +except ModuleNotFoundError as exc: + raise ImportError( + "discord-ext-menus is required. Install it with: pip install git+https://github.com/Rapptz/discord-ext-menus" + ) from exc + +from .paginator import Paginator as EmbedPaginator +from discord.ext.commands import Context, Paginator as CmdPaginator +from typing import Any, List + + +class FieldPagePaginator(menus.ListPageSource): + + def __init__(self, + entries: list[tuple[Any, Any]], + *, + per_page: int = 10, + inline: bool = False, + **kwargs) -> None: + super().__init__(entries, per_page=per_page) + self.embed: discord.Embed = discord.Embed( + title=kwargs.get('title'), + description=kwargs.get('description'), + color=0xFF0000) + self.inline: bool = inline + + async def format_page(self, menu: EmbedPaginator, + entries: list[tuple[Any, Any]]) -> discord.Embed: + self.embed.clear_fields() + + for key, value in entries: + self.embed.add_field(name=key, value=value, inline=self.inline) + + maximum = self.get_max_pages() + if maximum > 1: + text = f'• Page {menu.current_page + 1}/{maximum} | Requested by {menu.ctx.author.display_name}' + self.embed.set_footer( + text=text, + icon_url= + "https://cdn.discordapp.com/icons/699587669059174461/f689b4366447d5a23eda8d0ec749c1ba.png?width=115&height=115" + ) + #self.embed.timestamp = discord.utils.utcnow() + return self.embed + + +class TextPaginator(menus.ListPageSource): + + def __init__(self, text, *, prefix='```', suffix='```', max_size=2000): + pages = CmdPaginator(prefix=prefix, + suffix=suffix, + max_size=max_size - 200) + for line in text.split('\n'): + pages.add_line(line) + + super().__init__(entries=pages.pages, per_page=1) + + async def format_page(self, menu, content): + maximum = self.get_max_pages() + if maximum > 1: + return f'{content} {BotName} • Page {menu.current_page + 1}/{maximum}' + return content + + +class DescriptionEmbedPaginator(menus.ListPageSource): + + def __init__(self, + entries: list[Any], + *, + per_page: int = 10, + **kwargs) -> None: + super().__init__(entries, per_page=per_page) + self.embed: discord.Embed = discord.Embed( + title=kwargs.get('title'), + color=0xFF0000, + ) + + async def format_page(self, menu: EmbedPaginator, + entries: list[tuple[Any, Any]]) -> discord.Embed: + self.embed.clear_fields() + + self.embed.description = '\n'.join(entries) + #self.embed.timestamp = discord.utils.utcnow() + maximum = self.get_max_pages() + if maximum > 1: + text = f'• Page {menu.current_page + 1}/{maximum} | Requested by {menu.ctx.author.display_name}' + self.embed.set_footer( + text=text, + icon_url= + "https://cdn.discordapp.com/icons/699587669059174461/f689b4366447d5a23eda8d0ec749c1ba.png?width=115&height=115" + ) + + return self.embed diff --git a/bot/utils/safe_parse.py b/bot/utils/safe_parse.py new file mode 100644 index 0000000..df60ad0 --- /dev/null +++ b/bot/utils/safe_parse.py @@ -0,0 +1,66 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ Safe parsing helpers — no eval() on untrusted data ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +from __future__ import annotations + +import ast +import json +import operator +import re +from typing import Any + + +def parse_role_id_list(raw: str | None) -> list[int]: + """Parse a stored role-ID list (JSON or legacy Python repr).""" + if not raw: + return [] + try: + parsed = json.loads(raw) + except json.JSONDecodeError: + try: + parsed = ast.literal_eval(raw) + except (ValueError, SyntaxError): + return [] + if not isinstance(parsed, list): + return [] + return [int(x) for x in parsed if str(x).isdigit()] + + +def dump_role_id_list(ids: list[int]) -> str: + return json.dumps(ids) + + +_MATH_ALLOWED = re.compile(r"^[\d+\-*/().\s]+$") + + +def _eval_math_node(node: ast.AST) -> float: + if isinstance(node, ast.Expression): + return _eval_math_node(node.body) + if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)): + return float(node.value) + if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)): + value = _eval_math_node(node.operand) + return value if isinstance(node.op, ast.UAdd) else -value + if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Sub, ast.Mult, ast.Div)): + left = _eval_math_node(node.left) + right = _eval_math_node(node.right) + ops = { + ast.Add: operator.add, + ast.Sub: operator.sub, + ast.Mult: operator.mul, + ast.Div: operator.truediv, + } + if isinstance(node.op, ast.Div) and right == 0: + raise ZeroDivisionError("division by zero") + return ops[type(node.op)](left, right) + raise ValueError("unsupported expression") + + +def safe_math_eval(expression: str) -> str: + """Evaluate basic arithmetic safely (digits, +, -, *, /, parentheses).""" + expr = expression.strip() + if not expr or not _MATH_ALLOWED.match(expr): + raise ValueError("invalid characters in expression") + tree = ast.parse(expr, mode="eval") + return str(_eval_math_node(tree)) diff --git a/bot/utils/sync_emojis.py b/bot/utils/sync_emojis.py new file mode 100644 index 0000000..a41f842 --- /dev/null +++ b/bot/utils/sync_emojis.py @@ -0,0 +1,215 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +""" +Application Emoji Sync Utility +Reads all custom Discord emojis from utils/emoji.py, checks them against +the bot's application emojis, uploads any that are missing, and patches +emoji.py in-place with corrected IDs. + +Controlled by EMOJI_SYNC in .env: + EMOJI_SYNC="true" → runs on every startup + EMOJI_SYNC="false" → skipped entirely + +If emoji.py is patched (new uploads or ID fixes), the bot automatically +restarts so the fresh IDs are loaded into memory. + +Call `run_sync(token)` once inside on_ready. +""" + +import os +import re +import sys +import base64 +import asyncio +import aiohttp +from colorama import Fore, Style, init + +init(autoreset=True) + +EMOJI_PY_PATH = os.path.join(os.path.dirname(__file__), "emoji.py") + + +def _log(level: str, color: str, symbol: str, msg: str) -> None: + print(f"{color}{symbol} {level}:{Style.RESET_ALL} {msg}") + +def info(msg): _log("EmojiSync", Fore.CYAN, "◈", msg) +def success(msg): _log("EmojiSync", Fore.GREEN, "✔", msg) +def warning(msg): _log("EmojiSync", Fore.YELLOW, "↻", msg) +def error(msg): _log("EmojiSync", Fore.RED, "✖", msg) +def system(msg): _log("EmojiSync", Fore.MAGENTA, "★", msg) + + +def _restart() -> None: + """Replace the current process with a fresh copy of itself.""" + system(f"Restarting bot to load updated emoji IDs...") + # Flush stdout so the message is visible before the process is replaced + sys.stdout.flush() + os.execv(sys.executable, [sys.executable] + sys.argv) + + +async def _fetch_emoji_image(session: aiohttp.ClientSession, emoji_id: str, animated: bool): + ext = "gif" if animated else "webp" + url = f"https://cdn.discordapp.com/emojis/{emoji_id}.{ext}" + try: + async with session.get(url, allow_redirects=True) as r: + if r.status == 200: + return await r.read() + except Exception: + pass + return None + + +async def run_sync(token: str) -> None: + """ + Async emoji sync. Pass the bot token directly. + Respects the EMOJI_SYNC env var — set to "false" to disable. + Triggers an automatic restart when emoji.py is patched. + """ + # ── Toggle check ────────────────────────────────────────────────────────── + enabled = os.getenv("EMOJI_SYNC", "true").strip().lower() + if enabled != "true": + info(f"Disabled via EMOJI_SYNC={enabled!r} — skipping.") + return + + if not token: + warning("No token provided — skipping EmojiSync.") + return + + # ── Read emoji.py ───────────────────────────────────────────────────────── + try: + with open(EMOJI_PY_PATH, "r", encoding="utf-8") as f: + content = f.read() + except Exception as err: + error(f"Could not read emoji.py ({err})") + return + + matches = set(re.findall(r"<(a?):(\w+):(\d+)>", content)) + if not matches: + info("No custom emojis found in emoji.py — nothing to sync.") + return + + system(f"Starting Application Emoji Sync — {len(matches)} unique emojis found in emoji.py") + + headers = { + "Authorization": f"Bot {token}", + "Content-Type": "application/json", + } + + async with aiohttp.ClientSession(headers=headers) as session: + # Fetch bot application ID + async with session.get("https://discord.com/api/v10/users/@me") as r: + if r.status != 200: + error(f"Failed to fetch bot info [HTTP {r.status}]") + return + app_id = (await r.json()).get("id") + + # Fetch existing application emojis + async with session.get(f"https://discord.com/api/v10/applications/{app_id}/emojis") as r: + if r.status != 200: + error(f"Failed to fetch application emojis [HTTP {r.status}]") + return + data = await r.json() + app_emojis: list = data.get("items", []) if isinstance(data, dict) else data + + info( + f"Found {Fore.YELLOW}{len(matches)}{Style.RESET_ALL} templates " + f"{Fore.LIGHTBLACK_EX}|{Style.RESET_ALL} " + f"Application hosts {Fore.GREEN}{len(app_emojis)}{Style.RESET_ALL} emojis" + ) + + updated = False + skipped = uploaded = fixed = failed = 0 + + for animated_str, name, old_id in matches: + animated = animated_str == "a" + + existing = ( + next((e for e in app_emojis if e["id"] == old_id), None) + or next((e for e in app_emojis if e["name"] == name), None) + ) + + if existing: + new_id = existing["id"] + if old_id != new_id: + old_str = f"<{animated_str}:{name}:{old_id}>" + new_str = f"<{animated_str}:{existing['name']}:{new_id}>" + content = content.replace(old_str, new_str) + updated = True + fixed += 1 + warning(f"Auto-fixing ID: {name} {Fore.LIGHTBLACK_EX}-> {new_id}") + else: + skipped += 1 + continue + + # Not found — upload it + info(f"Uploading: {name} {Fore.LIGHTBLACK_EX}(not in application emojis)") + + image_data = await _fetch_emoji_image(session, old_id, animated) + if not image_data: + error(f"Could not download image for {name} [ID: {old_id}]") + failed += 1 + continue + + mime = "image/gif" if animated else "image/webp" + b64 = base64.b64encode(image_data).decode("utf-8") + image_uri = f"data:{mime};base64,{b64}" + + async with session.post( + f"https://discord.com/api/v10/applications/{app_id}/emojis", + json={"name": name, "image": image_uri}, + ) as r2: + if r2.status in (200, 201): + new_emoji = await r2.json() + new_id = new_emoji["id"] + old_str = f"<{animated_str}:{name}:{old_id}>" + new_str = f"<{animated_str}:{new_emoji['name']}:{new_id}>" + content = content.replace(old_str, new_str) + app_emojis.append(new_emoji) + updated = True + uploaded += 1 + success(f"Uploaded: {name} {Fore.LIGHTBLACK_EX}[saved as ID: {new_id}]") + else: + resp_text = await r2.text() + error(f"Discord rejected {name} -> {resp_text}") + failed += 1 + + # Small delay to respect Discord rate limits + await asyncio.sleep(0.5) + + # ── Write patched emoji.py ──────────────────────────────────────────────── + if updated: + try: + with open(EMOJI_PY_PATH, "w", encoding="utf-8") as f: + f.write(content) + success("emoji.py patched in-place to reflect current API state.") + except Exception as err: + error(f"Could not write patched emoji.py ({err})") + updated = False # don't restart if we couldn't save + + # ── Summary ─────────────────────────────────────────────────────────────── + parts = [] + if skipped: parts.append(f"{Fore.GREEN}{skipped} already matching{Style.RESET_ALL}") + if fixed: parts.append(f"{Fore.YELLOW}{fixed} ID mismatches fixed{Style.RESET_ALL}") + if uploaded: parts.append(f"{Fore.CYAN}{uploaded} newly uploaded{Style.RESET_ALL}") + if failed: parts.append(f"{Fore.RED}{failed} failures{Style.RESET_ALL}") + + if parts: + system("Sync complete: " + f" {Fore.LIGHTBLACK_EX}|{Style.RESET_ALL} ".join(parts)) + else: + system("Sync complete: nothing to do.") + + # ── Auto-restart if emoji.py was changed ───────────────────────────────── + if updated: + _restart() diff --git a/bot/utils/tunnel.py b/bot/utils/tunnel.py new file mode 100644 index 0000000..a521b4d --- /dev/null +++ b/bot/utils/tunnel.py @@ -0,0 +1,331 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ +# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ +# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ +# ║ ║ +# ║ © 2026 CodeX Devs — All Rights Reserved ║ +# ║ ║ +# ║ discord ── https://discord.gg/codexdev ║ +# ║ youtube ── https://youtube.com/@CodeXDevs ║ +# ║ github ── https://github.com/RayExo ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +""" +HTTPS Tunnel for the ZyroX API — Cloudflare Tunnel via pycloudflared. + +Zero manual installs. Just: + 1. pip install pycloudflared (already in requirements.txt) + 2. Get your tunnel token from the Cloudflare dashboard (browser only, no CLI) + 3. Set CF_TUNNEL_TOKEN in .env + +That's it. The cloudflared binary is downloaded automatically on first run. + +────────────────────────────────────────────────────────────────────────────── +How to get your CF_TUNNEL_TOKEN (browser only, no CLI needed) +────────────────────────────────────────────────────────────────────────────── +1. Go to https://one.dash.cloudflare.com +2. Networks → Tunnels → Create a tunnel +3. Choose "Cloudflared" as connector type +4. Give it a name (e.g. zyrox-api) and click Save +5. On the "Install connector" step, find the token in the command shown: + cloudflared tunnel run --token + Copy just the token string. +6. Go to "Published Application Routes" tab → Add a Published Application Routes: + Subdomain: zyrox-api Domain: yourdomain.com Service: http://localhost:8000 + (Or use any domain you have on Cloudflare) +7. Paste the token into your .env: + CF_TUNNEL_TOKEN = "eyJhIjoiX..." + CF_TUNNEL_URL = "https://zyrox-api.yourdomain.com" + +That's the permanent URL — never changes between restarts. +Unlimited bandwidth, unlimited requests, free. +────────────────────────────────────────────────────────────────────────────── +""" + +import os +import time +import threading +import subprocess + +# ── env vars ────────────────────────────────────────────────────────────────── +from utils.config import TUNNEL_ENABLED, TUNNEL_ALLOW_BINARY_DOWNLOAD, API_PORT + +CF_TUNNEL_TOKEN = os.getenv("CF_TUNNEL_TOKEN", "").strip() # token from Cloudflare dashboard +CF_TUNNEL_URL = os.getenv("CF_TUNNEL_URL", "").strip() # e.g. https://api.yourdomain.com + +# ── colours ─────────────────────────────────────────────────────────────────── +_CYAN = "\033[36m" +_GREEN = "\033[32m" +_YELLOW = "\033[33m" +_RED = "\033[31m" +_RESET = "\033[0m" + + +def _ensure_pycloudflared() -> bool: + """Return True if pycloudflared is installed (no automatic pip install).""" + try: + import pycloudflared # noqa: F401 + return True + except ImportError: + print( + f"{_RED}◈ Tunnel: pycloudflared is not installed.\n" + f" Install manually: pip install pycloudflared{_RESET}" + ) + return False + + +def _download_cloudflared_direct() -> str | None: + """ + Last-resort: download the cloudflared binary directly from GitHub + into a local ./bin/ directory. Works even if pycloudflared fails. + """ + import platform + import urllib.request + import stat + + system = platform.system().lower() # linux / darwin / windows + machine = platform.machine().lower() # x86_64 / aarch64 / arm64 + + # Map to Cloudflare's release naming + if system == "linux": + if machine in ("aarch64", "arm64"): + asset = "cloudflared-linux-arm64" + elif machine == "arm": + asset = "cloudflared-linux-arm" + else: + asset = "cloudflared-linux-amd64" + elif system == "darwin": + asset = "cloudflared-darwin-amd64" + elif system == "windows": + asset = "cloudflared-windows-amd64.exe" + else: + print(f"{_RED}◈ Tunnel: unsupported OS '{system}' for direct download.{_RESET}") + return None + + url = f"https://github.com/cloudflare/cloudflared/releases/latest/download/{asset}" + bin_dir = os.path.join(os.path.dirname(__file__), "..", "bin") + os.makedirs(bin_dir, exist_ok=True) + dest = os.path.join(bin_dir, asset) + + if os.path.isfile(dest): + # Already downloaded — just ensure it's executable + pass + else: + print(f"{_YELLOW}◈ Tunnel: downloading cloudflared binary from GitHub…{_RESET}") + try: + urllib.request.urlretrieve(url, dest) + print(f"{_GREEN}◈ Tunnel: downloaded to {dest}{_RESET}") + except Exception as exc: + print(f"{_RED}◈ Tunnel: direct download failed — {exc}{_RESET}") + return None + + # Fix execute bit + try: + current = os.stat(dest).st_mode + os.chmod(dest, current | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + except OSError: + pass + + return dest + + +def _get_binary() -> str | None: + """ + Return a working path to the cloudflared binary. + Strategy (in order): + 1. pycloudflared package (auto-installs package if missing, triggers binary download) + 2. System PATH + 3. Direct GitHub download into ./bin/ + """ + import shutil + import stat + + # ── Step 0: check pycloudflared is installed ───────────────────────────── + if not _ensure_pycloudflared(): + return None + + candidates: list[str] = [] + + # ── Step 1: ask pycloudflared for the binary ─────────────────────────────── + try: + import pycloudflared as _pcf + + # cloudflared_path attribute (set after first download) + path = getattr(_pcf, "cloudflared_path", None) + if path: + candidates.append(str(path)) + + # Walk the package directory for any cloudflared file + pkg_dir = os.path.dirname(_pcf.__file__) + for fname in os.listdir(pkg_dir): + full = os.path.join(pkg_dir, fname) + if "cloudflared" in fname.lower() and os.path.isfile(full): + candidates.append(full) + + # pycloudflared ≥ 0.2 exposes a download() helper + if not candidates or not any(os.path.isfile(c) for c in candidates): + download_fn = getattr(_pcf, "download", None) + if callable(download_fn): + print(f"{_YELLOW}◈ Tunnel: triggering pycloudflared binary download…{_RESET}") + try: + downloaded = download_fn() + if downloaded: + candidates.append(str(downloaded)) + except Exception: + pass + + except ImportError: + pass + + # ── Step 2: system PATH ──────────────────────────────────────────────────── + sys_path = shutil.which("cloudflared") + if sys_path: + candidates.append(sys_path) + + # ── Step 3: validate each candidate ─────────────────────────────────────── + for candidate in candidates: + if not os.path.isfile(candidate): + continue + # Fix execute bit (common issue in Pterodactyl containers) + try: + current = os.stat(candidate).st_mode + if not (current & stat.S_IXUSR): + os.chmod(candidate, current | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + except OSError: + pass + # Smoke-test + try: + result = subprocess.run( + [candidate, "--version"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + timeout=5, + ) + if result.returncode == 0: + return candidate + except (OSError, subprocess.TimeoutExpired): + continue + + # ── Step 4: direct GitHub download (opt-in only) ───────────────────────── + if not TUNNEL_ALLOW_BINARY_DOWNLOAD: + print( + f"{_YELLOW}◈ Tunnel: no working binary found.\n" + f" Install pycloudflared manually, or set TUNNEL_ALLOW_BINARY_DOWNLOAD=true " + f"to allow downloading cloudflared from GitHub.{_RESET}" + ) + return None + + print(f"{_YELLOW}◈ Tunnel: no working binary found — attempting direct download…{_RESET}") + direct = _download_cloudflared_direct() + if direct and os.path.isfile(direct): + try: + result = subprocess.run( + [direct, "--version"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + timeout=5, + ) + if result.returncode == 0: + return direct + except (OSError, subprocess.TimeoutExpired): + pass + + return None + + +def _run_tunnel(binary: str, token: str, port: int, public_url: str) -> None: + """ + Blocking loop — runs: + cloudflared tunnel --no-autoupdate run --token + Restarts automatically if the process exits. + """ + cmd = [ + binary, + "tunnel", + "--no-autoupdate", + "--url", f"http://localhost:{port}", + "run", + "--token", token, + ] + + first_run = True + while True: + if first_run: + print(f"{_CYAN}◈ Tunnel: connecting to Cloudflare…{_RESET}") + first_run = False + else: + print(f"{_YELLOW}◈ Tunnel: reconnecting…{_RESET}") + + try: + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + + announced = False + for line in proc.stdout: + line = line.rstrip() + if not line: + continue + + # Only surface important lines — suppress the noisy info spam + low = line.lower() + if any(k in low for k in ("error", "warn", "registered", "connected", "failed", "unable")): + print(f"{_CYAN} [cloudflared] {line}{_RESET}") + + # Announce the live URL once the tunnel is up + if not announced and ("registered tunnel connection" in low or "connection registered" in low): + announced = True + if public_url: + print(f"{_GREEN}◈ Tunnel: API is live at {public_url}{_RESET}") + print(f"{_CYAN} ↳ DASHBOARD_API_URL = {public_url}/api/v1{_RESET}") + else: + print(f"{_GREEN}◈ Tunnel: connected — check CF_TUNNEL_URL in .env for your public URL{_RESET}") + + proc.wait() + code = proc.returncode + + except (FileNotFoundError, PermissionError) as exc: + print(f"{_RED}◈ Tunnel: failed to execute binary at '{binary}' — {exc}{_RESET}") + print(f"{_RED} Check: file exists, has execute permission, and matches the server OS/arch.{_RESET}") + return + except Exception as exc: + print(f"{_RED}◈ Tunnel: unexpected error — {exc}{_RESET}") + code = -1 + + print(f"{_YELLOW}◈ Tunnel: exited (code {code}), restarting in 5 s…{_RESET}") + time.sleep(5) + + +def start_tunnel() -> None: + """ + Start the Cloudflare Tunnel in a background daemon thread. + Called from CodeX.py after keep_alive(). + """ + if not TUNNEL_ENABLED: + print(f"{_YELLOW}◈ Tunnel: disabled via TUNNEL_ENABLED=false{_RESET}") + return + + if not CF_TUNNEL_TOKEN: + print( + f"{_YELLOW}◈ Tunnel: CF_TUNNEL_TOKEN is not set — tunnel skipped.\n" + f" Get your token from https://one.dash.cloudflare.com → Networks → Tunnels{_RESET}" + ) + return + + binary = _get_binary() + if not binary: + print( + f"{_RED}◈ Tunnel: could not obtain a working cloudflared binary after all attempts.\n" + f" Tunnel will not start. Check your network or install manually:\n" + f" pip install pycloudflared{_RESET}" + ) + return + + print(f"{_CYAN}◈ Tunnel: cloudflared binary ready — starting tunnel on port {API_PORT}…{_RESET}") + t = threading.Thread(target=_run_tunnel, args=(binary, CF_TUNNEL_TOKEN, API_PORT, CF_TUNNEL_URL), daemon=True) + t.start() diff --git a/dashboard/.env.example b/dashboard/.env.example new file mode 100644 index 0000000..91b6d78 --- /dev/null +++ b/dashboard/.env.example @@ -0,0 +1,24 @@ +# Bot API URL (server-side — used by Next.js proxy and SSR) +DASHBOARD_API_URL=http://127.0.0.1:8000/api/v1 + +# Bot API key (SERVER ONLY — never use NEXT_PUBLIC_ prefix) +DASHBOARD_API_KEY=generate_a_long_random_secret_here + +# Legacy/public API URL (optional fallback for DASHBOARD_API_URL) +NEXT_PUBLIC_API_URL=http://127.0.0.1:8000/api/v1 + +# NextAuth Configuration +NEXTAUTH_URL=http://localhost:3000/ +NEXTAUTH_SECRET=generate_a_long_random_secret_here + +# Discord OAuth configuration +DISCORD_CLIENT_ID= +DISCORD_CLIENT_SECRET= + +# Dashboard admin user IDs (comma separated — your Discord user ID) +ADMIN_IDS=YOUR_DISCORD_USER_ID_HERE +NEXT_PUBLIC_ADMIN_IDS=YOUR_DISCORD_USER_ID_HERE + +# Customizable Brand / Bot name shown across the dashboard +NEXT_PUBLIC_BRAND_NAME="ZyroX" +NEXT_PUBLIC_BRAND_NAME_WORD="ZX" diff --git a/dashboard/.eslintrc.json b/dashboard/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/dashboard/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/dashboard/LICENSE b/dashboard/LICENSE new file mode 100644 index 0000000..5173ffd --- /dev/null +++ b/dashboard/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 CodeX Devs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 0000000..6832bde --- /dev/null +++ b/dashboard/README.md @@ -0,0 +1,133 @@ +
+ +# ZyroX Dashboard + +Next.js web dashboard for configuring the ZyroX Discord bot. + +
+ +--- + +## Overview + +- **Discord OAuth login** — users only see servers they can manage +- **Server-side API proxy** — `DASHBOARD_API_KEY` never exposed to the browser +- **Guild permission checks** — bot API verifies Discord Manage Server / Admin rights +- **Fully branded** — name and colours via environment variables +- **Vercel-ready** + +--- + +## Prerequisites + +| Requirement | Notes | +|---|---| +| Node.js 18+ | — | +| ZyroX bot running | with `API_ENABLED=true` on `127.0.0.1` (or via Cloudflare Tunnel) | +| Discord OAuth app | from [Discord Developer Portal](https://discord.com/developers/applications) | + +--- + +## Setup + +### 1 — Install dependencies + +```bash +npm install +``` + +### 2 — Configure environment + +Create `.env.local` in this folder (see `.env.example`): + +```env +# ── Bot API (server-side only — never use NEXT_PUBLIC_ for the key) ── +DASHBOARD_API_URL = http://127.0.0.1:8000/api/v1 +DASHBOARD_API_KEY = same_secret_as_bot_DASHBOARD_API_KEY + +# ── NextAuth ────────────────────────────────────────────────────── +NEXTAUTH_URL = http://localhost:3000 +NEXTAUTH_SECRET = a_long_random_string + +# ── Discord OAuth ───────────────────────────────────────────────── +DISCORD_CLIENT_ID = your_discord_oauth_client_id +DISCORD_CLIENT_SECRET = your_discord_oauth_client_secret + +# ── Admin & Branding ────────────────────────────────────────────── +ADMIN_IDS = your_discord_user_id +NEXT_PUBLIC_ADMIN_IDS = your_discord_user_id +NEXT_PUBLIC_BRAND_NAME = "ZyroX" +NEXT_PUBLIC_BRAND_NAME_WORD = "ZX" +``` + +### 3 — Run locally + +```bash +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) + +--- + +## Environment Reference + +| Variable | Description | +|---|---| +| `DASHBOARD_API_URL` | Bot API URL (server-side). Local: `http://127.0.0.1:8000/api/v1` | +| `DASHBOARD_API_KEY` | Must match bot `DASHBOARD_API_KEY` — **server-side only** | +| `NEXTAUTH_URL` | Dashboard public URL | +| `NEXTAUTH_SECRET` | Random secret for NextAuth session signing | +| `DISCORD_CLIENT_ID` | Discord OAuth2 client ID | +| `DISCORD_CLIENT_SECRET` | Discord OAuth2 client secret | +| `ADMIN_IDS` | Server-side admin user IDs (for admin API routes) | +| `NEXT_PUBLIC_ADMIN_IDS` | Same IDs for admin panel UI visibility | +| `NEXT_PUBLIC_BRAND_NAME` | Bot name in the dashboard UI | +| `NEXT_PUBLIC_BRAND_NAME_WORD` | Short abbreviation (e.g. `ZX`) | + +> Do **not** set `NEXT_PUBLIC_DASHBOARD_API_KEY`. The browser uses `/api/proxy`, which adds the secret server-side. + +--- + +## How API auth works + +``` +Browser → /api/proxy/guilds/... → Next.js (session check) + → Bot API with: + Authorization: Bearer DASHBOARD_API_KEY + X-Discord-Access-Token: + X-Discord-User-Id: + → Bot verifies Discord Manage Server permission per guild +``` + +SSR (server components) call the bot API directly with the same headers from the active session. + +--- + +## Deployment (Vercel) + +1. Connect repo → set root directory to `dashboard/` +2. Add all environment variables from the table above +3. Set `NEXTAUTH_URL` to your Vercel domain +4. Add OAuth redirect: `https://your-app.vercel.app/api/auth/callback/discord` +5. Deploy + +For production with Cloudflare Tunnel, set: + +```env +DASHBOARD_API_URL=https://api.yourdomain.com/api/v1 +``` + +--- + +## Troubleshooting + +| Problem | Fix | +|---|---| +| Dashboard can't load data | Bot running with `API_ENABLED=true`, `DASHBOARD_API_URL` correct | +| 401 from bot API | `DASHBOARD_API_KEY` must match bot `.env` exactly | +| 403 on guild routes | User must have Manage Server or Admin on that Discord server | +| Admin panel 403 | Add your Discord ID to `ADMIN_IDS` and bot `DASHBOARD_ADMIN_IDS` | +| Auth error on login | Check Discord OAuth client ID/secret and redirect URI | + +See also `SECURITY_FIXES.md` in the project root. diff --git a/dashboard/app/api/auth/[...nextauth]/route.ts b/dashboard/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..bf90f3e --- /dev/null +++ b/dashboard/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,22 @@ +/** + * ╔══════════════════════════════════════════════════════════════════╗ + * ║ ║ + * ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ + * ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ + * ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ + * ║ ║ + * ║ © 2026 CodeX Devs — All Rights Reserved ║ + * ║ ║ + * ║ discord ── https://discord.gg/codexdev ║ + * ║ youtube ── https://youtube.com/@CodeXDevs ║ + * ║ github ── https://github.com/RayExo ║ + * ║ ║ + * ╚══════════════════════════════════════════════════════════════════╝ + */ + +import NextAuth from "next-auth"; +import { authOptions } from "@/lib/auth"; + +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST }; diff --git a/dashboard/app/api/proxy/[...path]/route.ts b/dashboard/app/api/proxy/[...path]/route.ts new file mode 100644 index 0000000..667971c --- /dev/null +++ b/dashboard/app/api/proxy/[...path]/route.ts @@ -0,0 +1,146 @@ +/** + * Authenticated server-side proxy to the bot FastAPI backend. + * - Requires Discord OAuth session + * - Guild routes: user must have Manage Server / Admin on that guild + * - Admin routes: user must be in ADMIN_IDS + */ + +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/lib/auth"; +import { isAdmin } from "@/lib/utils"; +import { userCanManageGuild } from "@/lib/guild-auth"; + +const BOT_API_URL = + process.env.DASHBOARD_API_URL || + process.env.NEXT_PUBLIC_API_URL || + "http://127.0.0.1:8000/api/v1"; + +const API_KEY = process.env.DASHBOARD_API_KEY; + +type RouteContext = { params: { path: string[] } }; + +async function authorizeProxyRequest( + path: string, + session: { user?: { id?: string }; accessToken?: string } +): Promise { + if (!session.user) { + return NextResponse.json({ detail: "Unauthorized — sign in required." }, { status: 401 }); + } + + const accessToken = session.accessToken; + if (!accessToken) { + return NextResponse.json({ detail: "Missing Discord access token." }, { status: 401 }); + } + + const normalized = path.replace(/\/$/, ""); + + if (normalized.startsWith("admin")) { + if (!isAdmin(session.user.id)) { + return NextResponse.json({ detail: "Forbidden — admin access required." }, { status: 403 }); + } + return null; + } + + const guildMatch = normalized.match(/^guilds\/(\d+)/); + if (guildMatch) { + const guildId = guildMatch[1]; + const allowed = await userCanManageGuild(accessToken, guildId); + if (!allowed) { + return NextResponse.json( + { detail: "Forbidden — you cannot manage this server." }, + { status: 403 } + ); + } + } + + return null; +} + +async function proxyRequest(request: NextRequest, { params }: RouteContext) { + if (!API_KEY) { + return NextResponse.json( + { detail: "DASHBOARD_API_KEY is not configured on the dashboard server." }, + { status: 500 } + ); + } + + const session = await getServerSession(authOptions); + const path = params.path.join("/"); + + const authError = await authorizeProxyRequest(path, session ?? {}); + if (authError) return authError; + + const targetUrl = `${BOT_API_URL.replace(/\/$/, "")}/${path}${request.nextUrl.search}`; + + const headers = new Headers(); + headers.set("Authorization", `Bearer ${API_KEY}`); + + if (session?.accessToken) { + headers.set("X-Discord-Access-Token", session.accessToken); + } + if (session?.user?.id) { + headers.set("X-Discord-User-Id", session.user.id); + } + + const contentType = request.headers.get("content-type"); + if (contentType) { + headers.set("Content-Type", contentType); + } + + const init: RequestInit = { + method: request.method, + headers, + cache: "no-store", + }; + + if (request.method !== "GET" && request.method !== "HEAD") { + init.body = await request.text(); + } + + try { + const response = await fetch(targetUrl, init); + let body = await response.text(); + + // Filter guild list to servers the user can manage (bot guild list ∩ user admin guilds) + const normalized = path.replace(/\/$/, ""); + if ( + request.method === "GET" && + normalized === "guilds" && + response.ok && + session?.accessToken + ) { + try { + const botGuilds = JSON.parse(body) as Array<{ id: string }>; + const { fetchUserGuilds, canManageGuild } = await import("@/lib/guild-auth"); + const userGuilds = await fetchUserGuilds(session.accessToken); + const manageableIds = new Set( + userGuilds.filter(canManageGuild).map((g) => String(g.id)) + ); + const filtered = botGuilds.filter((g) => manageableIds.has(String(g.id))); + body = JSON.stringify(filtered); + } catch { + // If filtering fails, return original response rather than breaking the dashboard + } + } + + return new NextResponse(body, { + status: response.status, + headers: { + "Content-Type": response.headers.get("Content-Type") || "application/json", + }, + }); + } catch (error) { + console.error(`[API Proxy] Failed to reach bot API at ${targetUrl}:`, error); + return NextResponse.json( + { detail: "Could not connect to the bot API backend." }, + { status: 502 } + ); + } +} + +export const GET = proxyRequest; +export const POST = proxyRequest; +export const PATCH = proxyRequest; +export const DELETE = proxyRequest; +export const PUT = proxyRequest; diff --git a/dashboard/app/dashboard/admin/page.tsx b/dashboard/app/dashboard/admin/page.tsx new file mode 100644 index 0000000..11475c0 --- /dev/null +++ b/dashboard/app/dashboard/admin/page.tsx @@ -0,0 +1,37 @@ +/** + * ╔══════════════════════════════════════════════════════════════════╗ + * ║ ║ + * ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ + * ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ + * ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ + * ║ ║ + * ║ © 2026 CodeX Devs — All Rights Reserved ║ + * ║ ║ + * ║ discord ── https://discord.gg/codexdev ║ + * ║ youtube ── https://youtube.com/@CodeXDevs ║ + * ║ github ── https://github.com/RayExo ║ + * ║ ║ + * ╚══════════════════════════════════════════════════════════════════╝ + */ + +import React from "react"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/lib/auth"; +import { redirect } from "next/navigation"; +import { isAdmin, cn } from "@/lib/utils"; +import { Shield, Users, Server, Activity, Database, Cpu, Globe, Lock, Settings } from "lucide-react"; + +import { AdminContent } from "@/components/dashboard/admin-content"; + +export default async function AdminPage() { + const session = await getServerSession(authOptions); + + // Server-side protection + if (!session || !isAdmin(session.user?.id)) { + redirect("/dashboard"); + } + + return ; +} + + diff --git a/dashboard/app/dashboard/error.tsx b/dashboard/app/dashboard/error.tsx new file mode 100644 index 0000000..0729b84 --- /dev/null +++ b/dashboard/app/dashboard/error.tsx @@ -0,0 +1,72 @@ +/** + * ╔══════════════════════════════════════════════════════════════════╗ + * ║ ║ + * ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ + * ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ + * ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ + * ║ ║ + * ║ © 2026 CodeX Devs — All Rights Reserved ║ + * ║ ║ + * ║ discord ── https://discord.gg/codexdev ║ + * ║ youtube ── https://youtube.com/@CodeXDevs ║ + * ║ github ── https://github.com/RayExo ║ + * ║ ║ + * ╚══════════════════════════════════════════════════════════════════╝ + */ + +"use client"; + +import React, { useEffect } from "react"; +import { AlertTriangle, RefreshCw, Home } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; + +export default function DashboardError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + console.error("Dashboard Error:", error); + }, [error]); + + return ( +
+
+ +
+ +

System Fault Detected

+

+ The neural link experienced an unexpected interruption. This could be due to a connection timeout or an internal API failure. +

+ +
+ + + + +
+ + {process.env.NODE_ENV === 'development' && ( +
+          {error.message}
+        
+ )} +
+ ); +} diff --git a/dashboard/app/dashboard/guild/[guildId]/antinuke/page.tsx b/dashboard/app/dashboard/guild/[guildId]/antinuke/page.tsx new file mode 100644 index 0000000..17383d1 --- /dev/null +++ b/dashboard/app/dashboard/guild/[guildId]/antinuke/page.tsx @@ -0,0 +1,46 @@ +/** + * ╔══════════════════════════════════════════════════════════════════╗ + * ║ ║ + * ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ + * ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ + * ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ + * ║ ║ + * ║ © 2026 CodeX Devs — All Rights Reserved ║ + * ║ ║ + * ║ discord ── https://discord.gg/codexdev ║ + * ║ youtube ── https://youtube.com/@CodeXDevs ║ + * ║ github ── https://github.com/RayExo ║ + * ║ ║ + * ╚══════════════════════════════════════════════════════════════════╝ + */ + +import React from "react"; +import { ShieldAlert } from "lucide-react"; +import dynamic from "next/dynamic"; +import { api } from "@/lib/api"; + +const AntiNukeForm = dynamic(() => import("@/components/dashboard/antinuke-form").then(mod => mod.AntiNukeForm), { + loading: () =>
+}); + +export default async function AntiNukePage({ params }: { params: { guildId: string } }) { + const config = await api.getAntiNuke(params.guildId); + + if (!config) return null; + + return ( +
+
+
+

+ + Anti-Nuke Protection +

+

Protect your server from malicious mass-deletion, mass-banning, and other destructive actions.

+
+
+ + +
+ ); +} diff --git a/dashboard/app/dashboard/guild/[guildId]/automod/page.tsx b/dashboard/app/dashboard/guild/[guildId]/automod/page.tsx new file mode 100644 index 0000000..9a63229 --- /dev/null +++ b/dashboard/app/dashboard/guild/[guildId]/automod/page.tsx @@ -0,0 +1,46 @@ +/** + * ╔══════════════════════════════════════════════════════════════════╗ + * ║ ║ + * ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ + * ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ + * ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ + * ║ ║ + * ║ © 2026 CodeX Devs — All Rights Reserved ║ + * ║ ║ + * ║ discord ── https://discord.gg/codexdev ║ + * ║ youtube ── https://youtube.com/@CodeXDevs ║ + * ║ github ── https://github.com/RayExo ║ + * ║ ║ + * ╚══════════════════════════════════════════════════════════════════╝ + */ + +import React from "react"; +import { ShieldCheck } from "lucide-react"; +import dynamic from "next/dynamic"; +import { api } from "@/lib/api"; + +const AutomodForm = dynamic(() => import("@/components/dashboard/automod-form").then(mod => mod.AutomodForm), { + loading: () =>
+}); + +export default async function AutomodPage({ params }: { params: { guildId: string } }) { + const config = await api.getAutomod(params.guildId); + + if (!config) return null; + + return ( +
+
+
+

+ + Auto Moderation +

+

Protect your community with automated filter systems.

+
+
+ + +
+ ); +} diff --git a/dashboard/app/dashboard/guild/[guildId]/autoreact/page.tsx b/dashboard/app/dashboard/guild/[guildId]/autoreact/page.tsx new file mode 100644 index 0000000..2705ce5 --- /dev/null +++ b/dashboard/app/dashboard/guild/[guildId]/autoreact/page.tsx @@ -0,0 +1,182 @@ +/** + * ╔══════════════════════════════════════════════════════════════════╗ + * ║ ║ + * ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ + * ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ + * ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ + * ║ ║ + * ║ © 2026 CodeX Devs — All Rights Reserved ║ + * ║ ║ + * ║ discord ── https://discord.gg/codexdev ║ + * ║ youtube ── https://youtube.com/@CodeXDevs ║ + * ║ github ── https://github.com/RayExo ║ + * ║ ║ + * ╚══════════════════════════════════════════════════════════════════╝ + */ + +"use client"; + +import React, { useState, useEffect } from "react"; +import { Zap, Save, RefreshCcw, Plus, Trash2, Smile, Info } from "lucide-react"; +import { toast } from "sonner"; +import { api } from "@/lib/api"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; + +export default function AutoReactPage({ params }: { params: { guildId: string } }) { + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [config, setConfig] = useState({ triggers: [] }); + + const fetchData = async () => { + try { + setLoading(true); + const configData = await api.getAutoReact(params.guildId); + setConfig(configData); + } catch (error) { + console.error("Failed to fetch auto react data:", error); + toast.error("Failed to load auto react configuration"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetchData(); }, [params.guildId]); + + const handleSave = async () => { + setSaving(true); + const promise = api.updateAutoReact(params.guildId, { triggers: config.triggers }); + toast.promise(promise, { + loading: 'Saving auto react configuration...', + success: 'Auto react settings saved!', + error: 'Failed to save auto react config', + }); + try { await promise; } catch {} finally { setSaving(false); } + }; + + const addTrigger = () => { + if (config.triggers.length >= 10) { + toast.error("Maximum 10 triggers allowed"); + return; + } + setConfig({ ...config, triggers: [...config.triggers, { trigger: "", emojis: "" }] }); + }; + + const removeTrigger = (index: number) => { + const newTriggers = [...config.triggers]; + newTriggers.splice(index, 1); + setConfig({ ...config, triggers: newTriggers }); + }; + + const updateTrigger = (index: number, field: string, value: string) => { + const newTriggers = [...config.triggers]; + newTriggers[index] = { ...newTriggers[index], [field]: value }; + setConfig({ ...config, triggers: newTriggers }); + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+

+ + Auto React +

+

Automatically react with emojis when specific trigger words are sent.

+
+ +
+ +
+
+
+ + {config.triggers.length === 0 ? ( +
+ +

No triggers configured

+

Start by adding your first auto-reaction trigger.

+ +
+ ) : ( + <> + {config.triggers.map((item: any, index: number) => ( +
+
+
+
+ +
+

Trigger #{index + 1}

+
+ +
+
+
+ + updateTrigger(index, "trigger", e.target.value)} + className="bg-slate-900/50 border-slate-800 h-12" + /> +
+
+ + updateTrigger(index, "emojis", e.target.value)} + className="bg-slate-900/50 border-slate-800 h-12" + /> +
+
+
+ ))} + + + + )} +
+
+ + {/* Sidebar */} +
+
+
+ +
+
+ +

How It Works

+
+
    +
  • • Triggers are single words that the bot watches for.
  • +
  • • When a message contains a trigger, the bot reacts with the configured emojis.
  • +
  • • Up to 10 emojis per trigger, and 10 triggers max per guild.
  • +
  • • Custom emojis must be from this server.
  • +
+
+
+
+
+ ); +} diff --git a/dashboard/app/dashboard/guild/[guildId]/autorole/page.tsx b/dashboard/app/dashboard/guild/[guildId]/autorole/page.tsx new file mode 100644 index 0000000..39d291d --- /dev/null +++ b/dashboard/app/dashboard/guild/[guildId]/autorole/page.tsx @@ -0,0 +1,54 @@ +/** + * ╔══════════════════════════════════════════════════════════════════╗ + * ║ ║ + * ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ + * ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ + * ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ + * ║ ║ + * ║ © 2026 CodeX Devs — All Rights Reserved ║ + * ║ ║ + * ║ discord ── https://discord.gg/codexdev ║ + * ║ youtube ── https://youtube.com/@CodeXDevs ║ + * ║ github ── https://github.com/RayExo ║ + * ║ ║ + * ╚══════════════════════════════════════════════════════════════════╝ + */ + +import React from "react"; +import { UserPlus } from "lucide-react"; +import dynamic from "next/dynamic"; +import { api } from "@/lib/api"; + +export const revalidate = 0; // Never cache this page + + +const AutoRoleForm = dynamic(() => import("@/components/dashboard/autorole-form").then(mod => mod.AutoRoleForm), { + loading: () =>
+}); + +export default async function AutoRolePage({ params }: { params: { guildId: string } }) { + const [config, roles] = await Promise.all([ + api.getAutoRole(params.guildId), + api.getRoles(params.guildId) + ]); + + return ( +
+
+
+

+ + Auto Role +

+

Automatically assign roles to new members and bots.

+
+
+ + +
+ ); +} diff --git a/dashboard/app/dashboard/guild/[guildId]/customroles/page.tsx b/dashboard/app/dashboard/guild/[guildId]/customroles/page.tsx new file mode 100644 index 0000000..49f057f --- /dev/null +++ b/dashboard/app/dashboard/guild/[guildId]/customroles/page.tsx @@ -0,0 +1,49 @@ +/** + * ╔══════════════════════════════════════════════════════════════════╗ + * ║ ║ + * ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ + * ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ + * ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ + * ║ ║ + * ║ © 2026 CodeX Devs — All Rights Reserved ║ + * ║ ║ + * ║ discord ── https://discord.gg/codexdev ║ + * ║ youtube ── https://youtube.com/@CodeXDevs ║ + * ║ github ── https://github.com/RayExo ║ + * ║ ║ + * ╚══════════════════════════════════════════════════════════════════╝ + */ + +import React from "react"; +import { Ghost } from "lucide-react"; +import dynamic from "next/dynamic"; +import { api } from "@/lib/api"; + +const CustomRolesForm = dynamic(() => import("@/components/dashboard/customroles-form").then(mod => mod.CustomRolesForm), { + loading: () =>
+}); + +export default async function CustomRolesPage({ params }: { params: { guildId: string } }) { + const [config, roles] = await Promise.all([ + api.getCustomRoles(params.guildId), + api.getRoles(params.guildId), + ]); + + if (!config) return null; + + return ( +
+
+
+

+ + Custom Roles +

+

Configure predefined roles that can be easily assigned using commands.

+
+
+ + +
+ ); +} diff --git a/dashboard/app/dashboard/guild/[guildId]/invcrole/page.tsx b/dashboard/app/dashboard/guild/[guildId]/invcrole/page.tsx new file mode 100644 index 0000000..257383b --- /dev/null +++ b/dashboard/app/dashboard/guild/[guildId]/invcrole/page.tsx @@ -0,0 +1,194 @@ +/** + * ╔══════════════════════════════════════════════════════════════════╗ + * ║ ║ + * ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ + * ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ + * ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ + * ║ ║ + * ║ © 2026 CodeX Devs — All Rights Reserved ║ + * ║ ║ + * ║ discord ── https://discord.gg/codexdev ║ + * ║ youtube ── https://youtube.com/@CodeXDevs ║ + * ║ github ── https://github.com/RayExo ║ + * ║ ║ + * ╚══════════════════════════════════════════════════════════════════╝ + */ + +"use client"; + +import React, { useState, useEffect } from "react"; +import { Volume2, Save, RefreshCcw, ShieldCheck, Info, Power } from "lucide-react"; +import { toast } from "sonner"; +import { api } from "@/lib/api"; +import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { cn } from "@/lib/utils"; + +export default function InvcRolePage({ params }: { params: { guildId: string } }) { + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [roles, setRoles] = useState([]); + const [config, setConfig] = useState({ role_id: null, enabled: false }); + + const fetchData = async () => { + try { + setLoading(true); + const [configData, rolesData] = await Promise.all([ + api.getInvcRole(params.guildId), + api.getRoles(params.guildId), + ]); + setConfig(configData); + setRoles(rolesData); + } catch (error) { + console.error("Failed to fetch InvcRole data:", error); + toast.error("Failed to load Voice Role configuration"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetchData(); }, [params.guildId]); + + const handleSave = async () => { + setSaving(true); + const promise = api.updateInvcRole(params.guildId, { + role_id: config.role_id, + enabled: config.enabled + }); + toast.promise(promise, { + loading: 'Saving Voice Role configuration...', + success: 'Voice Role settings saved!', + error: 'Failed to save Voice Role config', + }); + try { await promise; } catch {} finally { setSaving(false); } + }; + + const filteredRoles = roles.filter(r => r.name !== "@everyone"); + + if (loading) { + return ( +
+ +
+ ); + } + + const formatColor = (decimal: number) => { + if (!decimal || decimal === 0) return "#94a3b8"; + return `#${decimal.toString(16).padStart(6, '0')}`; + }; + + return ( +
+
+
+

+ + Voice Role +

+

Automatically assign a role when members join a voice channel.

+
+
+ +
+
+
+ + {/* Status & Toggle */} +
+
+
+ +
+
+

Voice Role System

+

{config.enabled ? "The system is active and monitoring channels." : "The system is currently disabled."}

+
+
+
+
+
+ + {config.enabled ? 'Live' : 'Off'} + +
+ setConfig({ ...config, enabled: checked })} + className="data-[state=checked]:bg-emerald-500" + /> +
+
+ + {/* Role Selector */} +
+
+
+ +
+
+

Voice State Role

+

Choose the role to be assigned automatically to voice participants.

+
+
+ +
+ + +
+
+ + {/* Sidebar */} +
+
+
+ +
+
+ +

How It Works

+
+
    +
  • + 01 + Role is added when a user joins any voice channel. +
  • +
  • + 02 + Role is removed when they disconnect from all channels. +
  • +
  • + 03 + Make sure the bot role is higher than the selected role. +
  • +
+
+
+
+
+ ); +} diff --git a/dashboard/app/dashboard/guild/[guildId]/invites/page.tsx b/dashboard/app/dashboard/guild/[guildId]/invites/page.tsx new file mode 100644 index 0000000..00d6a08 --- /dev/null +++ b/dashboard/app/dashboard/guild/[guildId]/invites/page.tsx @@ -0,0 +1,167 @@ +/** + * ╔══════════════════════════════════════════════════════════════════╗ + * ║ ║ + * ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ + * ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ + * ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ + * ║ ║ + * ║ © 2026 CodeX Devs — All Rights Reserved ║ + * ║ ║ + * ║ discord ── https://discord.gg/codexdev ║ + * ║ youtube ── https://youtube.com/@CodeXDevs ║ + * ║ github ── https://github.com/RayExo ║ + * ║ ║ + * ╚══════════════════════════════════════════════════════════════════╝ + */ + +"use client"; + +import React, { useState, useEffect } from "react"; +import { TrendingUp, RefreshCcw, Medal, User, LogOut, UserMinus, UserPlus, Info } from "lucide-react"; +import { toast } from "sonner"; +import { api } from "@/lib/api"; +import { cn } from "@/lib/utils"; + +export default function InvitesPage({ params }: { params: { guildId: string } }) { + const [loading, setLoading] = useState(true); + const [data, setData] = useState([]); + + const fetchLeaderboard = async () => { + try { + setLoading(true); + const res = await api.getInvites(params.guildId); + setData(res.data || []); + } catch (error) { + console.error("Failed to fetch invites leaderboard:", error); + toast.error("Failed to load invites leaderboard"); + setData([]); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetchLeaderboard(); }, [params.guildId]); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+

+ + Invite Leaderboard +

+

Top inviters in the server based on tracked join events.

+
+ +
+ +
+
+
+ + {/* Stats Cards */} +
+
+
Total
+ {data.reduce((acc, curr) => acc + (curr.total || 0), 0)} +
+
+
Left
+ {data.reduce((acc, curr) => acc + (curr.left || 0), 0)} +
+
+
Fake
+ {data.reduce((acc, curr) => acc + (curr.fake || 0), 0)} +
+
+
Top
+ {data.length > 0 ? data[0].total : 0} +
+
+ + {/* Leaderboard */} +
+

+ Rankings +

+ + {data.length === 0 ? ( +
+ +

No invite data found yet.

+
+ ) : ( + data.map((row, index) => ( +
+
+
+ {index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : #{index + 1}} +
+
+
+ +
+ {row.user_id} +
+
+
+
+
Total
+
{row.total}
+
+
+
Real
+
{row.total - row.left - row.fake - row.rejoin}
+
+
+
Left
+
{row.left}
+
+
+
Fake
+
{row.fake}
+
+
+
Rejoin
+
{row.rejoin}
+
+
+
+ )) + )} +
+
+
+ + {/* Sidebar */} +
+
+
+ +
+
+ +

About Tracking

+
+
    +
  • • Invite tracking is automatic for all members.
  • +
  • • "Real" = Total minus Left, Fake, and Rejoins.
  • +
  • • Use ,invitelogging #channel to enable live logs.
  • +
  • • Admins can manually adjust invite counts.
  • +
+
+
+
+
+ ); +} diff --git a/dashboard/app/dashboard/guild/[guildId]/j2c/page.tsx b/dashboard/app/dashboard/guild/[guildId]/j2c/page.tsx new file mode 100644 index 0000000..6a0c958 --- /dev/null +++ b/dashboard/app/dashboard/guild/[guildId]/j2c/page.tsx @@ -0,0 +1,52 @@ +/** + * ╔══════════════════════════════════════════════════════════════════╗ + * ║ ║ + * ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ + * ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ + * ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ + * ║ ║ + * ║ © 2026 CodeX Devs — All Rights Reserved ║ + * ║ ║ + * ║ discord ── https://discord.gg/codexdev ║ + * ║ youtube ── https://youtube.com/@CodeXDevs ║ + * ║ github ── https://github.com/RayExo ║ + * ║ ║ + * ╚══════════════════════════════════════════════════════════════════╝ + */ + +import React from "react"; +import { Mic } from "lucide-react"; +import dynamic from "next/dynamic"; +import { api } from "@/lib/api"; + +export const revalidate = 0; // Never cache this page + + +const J2CForm = dynamic(() => import("@/components/dashboard/j2c-form").then(mod => mod.J2CForm), { + loading: () =>
+}); + +export default async function J2CPage({ params }: { params: { guildId: string } }) { + const [config, channels] = await Promise.all([ + api.getJ2C(params.guildId), + api.getChannels(params.guildId), + ]); + + if (!config) return null; + + return ( +
+
+
+

+ + Join to Create +

+

Set up temporary voice channels that are created automatically when a member joins a specific channel.

+
+
+ + +
+ ); +} diff --git a/dashboard/app/dashboard/guild/[guildId]/joindm/page.tsx b/dashboard/app/dashboard/guild/[guildId]/joindm/page.tsx new file mode 100644 index 0000000..e7f84b2 --- /dev/null +++ b/dashboard/app/dashboard/guild/[guildId]/joindm/page.tsx @@ -0,0 +1,128 @@ +/** + * ╔══════════════════════════════════════════════════════════════════╗ + * ║ ║ + * ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║ + * ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║ + * ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║ + * ║ ║ + * ║ © 2026 CodeX Devs — All Rights Reserved ║ + * ║ ║ + * ║ discord ── https://discord.gg/codexdev ║ + * ║ youtube ── https://youtube.com/@CodeXDevs ║ + * ║ github ── https://github.com/RayExo ║ + * ║ ║ + * ╚══════════════════════════════════════════════════════════════════╝ + */ + +"use client"; + +import React, { useState, useEffect } from "react"; +import { MessageSquare, Save, RefreshCcw, Send } from "lucide-react"; +import { toast } from "sonner"; +import { api } from "@/lib/api"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label"; + +export default function JoinDMPage({ params }: { params: { guildId: string } }) { + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [config, setConfig] = useState({ + message: "", + }); + + const fetchData = async () => { + try { + setLoading(true); + const configData = await api.getJoinDM(params.guildId); + setConfig(configData); + } catch (error) { + console.error("Failed to fetch JoinDM data:", error); + toast.error("Failed to load Join DM configuration"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, [params.guildId]); + + const handleSave = async () => { + try { + setSaving(true); + await api.updateJoinDM(params.guildId, config); + toast.success("Join DM configuration saved successfully"); + } catch (error) { + console.error("Failed to save JoinDM config:", error); + toast.error("Failed to save Join DM configuration"); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+
+

Join DM

+

+ Send a private message to new members when they join your server. +

+
+ + + +
+ + Welcome Message +
+ + This message will be sent to the user's DMs. You can use text to welcome them. + +
+ +
+ +