first commit

This commit is contained in:
RayExo
2026-07-12 15:16:07 +05:30
commit 2289301f26
915 changed files with 69400 additions and 0 deletions

200
bot/utils/Tools.py Normal file
View File

@@ -0,0 +1,200 @@
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)

4
bot/utils/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
from .config import *
from .Tools import *
from .paginators import *
from .paginator import *

153
bot/utils/ai_utils.py Normal file
View File

@@ -0,0 +1,153 @@
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

BIN
bot/utils/arial.ttf Normal file

Binary file not shown.

14
bot/utils/config.py Normal file
View File

@@ -0,0 +1,14 @@
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
server = "https://discord.gg/codexdev"
ch = "https://discord.com/channels/699587669059174461/1271825678710476911"
OWNER_IDS = [767979794411028491]
BotName = BRAND_NAME
serverLink = "https://discord.gg/codexdev"
CMD_WEBHOOK_URL = os.getenv("CMD_WEBHOOK_URL")

View File

@@ -0,0 +1,45 @@
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

94
bot/utils/cv2.py Normal file
View File

@@ -0,0 +1,94 @@
"""
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}

365
bot/utils/emoji.py Normal file
View File

@@ -0,0 +1,365 @@
"""
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 = "<a:Active_Developer:1448949755181793280>"
ARROWRED = "<a:ArrowRed:1448951520077811806>"
BLACKCROWN = "<a:BlackCrown:1448949787842973697>"
BLOBPART = "<a:blobpart:1435923345748004969>"
BOOSTS = "<a:boosts:1448949652547436654>"
EARLY_VERIFIED_BOT_DEV = "<a:EarlyVerifiedBotDeveloper:1448949731777839184>"
EMOTE = "<a:emote:1448966401887961149>"
GIFD = "<a:GIFD:1275850452323401789>"
GIFN = "<a:GIFN:1275850451212042391>"
HYPESQUAD_BALANCE = "<a:a_Hypesquad_Balance:1448949777067806720>"
HYPESQUAD_BRAVERY = "<a:a_Hypesquad_Bravery:1448949697577353307>"
HYPESQUAD_EVENTS = "<a:HypesquadEvents:1448949663821598812>"
KING_ALT1 = "<a:king:1234399917792034846>"
LOADING = "<a:loading:1448949404185657354>"
LOADINGRED = "<a:loadingred:1448966488865247232>"
LOADING_ALT1 = "<a:loading:1448951579603505154>"
MAX__A = "<a:max__A:1295014945641201685>"
MENTION_ALT1 = "<a:mention:1448966424495390801>"
MINGLE = "<a:mingle:1367773396745846895>"
MOBILE = "<a:mobile:1448951648490885132>"
MUSIC_ALT1 = "<a:music:1448966355935301643>"
NITRO_BOOST = "<a:nitroboost:1448949639540899921>"
ONLINE = "<a:online:1448951591305744427>"
PREMIUM = "<a:premium:1204110058124873889>"
RACECAR64 = "<a:racecar64:1448966449535127623>"
REDDOT = "<a:reddot:1398280090198081578>"
REDHEART = "<a:RedHeart:1272229548280512547>"
REDRULESBOOK = "<a:RedRulesBook:1448966523258404955>"
SG_RD = "<a:sg_rd:1273974278433280122>"
STAFF = "<a:staff:1448949765931925504>"
STAR_ALT1 = "<a:Star:1273588820373147803>"
STAR_ALT2 = "<a:star:1251876754516349059>"
TADAA = "<a:TADAA:1448966368044126249>"
TIMER_ALT1 = "<a:timer:1329404677820911697>"
_37496ALERT = "<a:37496alert:1273959128490049556>"
# ============================================================================
# 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

213
bot/utils/help.py Normal file
View File

@@ -0,0 +1,213 @@
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)

212
bot/utils/paginator.py Normal file
View File

@@ -0,0 +1,212 @@
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)

96
bot/utils/paginators.py Normal file
View File

@@ -0,0 +1,96 @@
from __future__ import annotations
import os
import discord
from utils.config import BotName
try:
from discord.ext import menus
from discord.ext import commands
except ModuleNotFoundError:
os.system("pip install git+https://github.com/Rapptz/discord-ext-menus")
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