first commit
This commit is contained in:
200
bot/utils/Tools.py
Normal file
200
bot/utils/Tools.py
Normal 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
4
bot/utils/__init__.py
Normal 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
153
bot/utils/ai_utils.py
Normal 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
BIN
bot/utils/arial.ttf
Normal file
Binary file not shown.
14
bot/utils/config.py
Normal file
14
bot/utils/config.py
Normal 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")
|
||||
45
bot/utils/config_loader.py
Normal file
45
bot/utils/config_loader.py
Normal 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
94
bot/utils/cv2.py
Normal 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
365
bot/utils/emoji.py
Normal 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
213
bot/utils/help.py
Normal 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
212
bot/utils/paginator.py
Normal 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
96
bot/utils/paginators.py
Normal 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
|
||||
Reference in New Issue
Block a user