feat: add multiple discord bot cogs

Adds new cogs including DataManager, Hiring, KofiShop, Logging, ModMail, MORS, ServiceReview, StaffMsg, and Translator to enhance bot functionality for data management, hiring processes, logging events, and more.
This commit is contained in:
2025-09-23 00:28:29 -04:00
parent c2367369f1
commit 81f2eee409
63 changed files with 2824 additions and 6 deletions

286
translator/commands.py Normal file
View File

@@ -0,0 +1,286 @@
# -*- coding: utf-8 -*-
import discord
import json
from typing import Optional
from redbot.core import commands, app_commands
from redbot.core.utils.chat_formatting import box, pagify
class CommandsMixin:
"""This class holds all the commands for the cog."""
@commands.hybrid_command(aliases=["ts"])
@app_commands.describe(
to_language="The language to translate to.",
text="The text to translate. For prefix commands, wrap multi-word text in quotes.",
from_language="[Optional] The language to translate from. Defaults to Common."
)
async def translate(self, ctx: commands.Context, to_language: str, text: str, from_language: Optional[str] = None):
"""Translates text from one language to another."""
from_lang_name = from_language if from_language else "common"
from_matches = await self._find_language(from_lang_name)
if not from_matches:
return await ctx.send(f"Could not find the 'from' language: `{from_lang_name}`")
if len(from_matches) > 1:
possible = [f"`{self.all_languages[k]['name']}` (`{k}`)" for k in from_matches]
return await ctx.send(f"Multiple 'from' languages found for `{from_lang_name}`. Please be more specific:\n" + ", ".join(possible))
from_lang_key = from_matches[0]
to_matches = await self._find_language(to_language)
if not to_matches:
return await ctx.send(f"Could not find the 'to' language: `{to_language}`")
if len(to_matches) > 1:
possible = [f"`{self.all_languages[k]['name']}` (`{k}`)" for k in to_matches]
return await ctx.send(f"Multiple 'to' languages found for `{to_language}`. Please be more specific:\n" + ", ".join(possible))
to_lang_key = to_matches[0]
from_lang_obj = self.all_languages[from_lang_key]
to_lang_obj = self.all_languages[to_lang_key]
try:
common_text = from_lang_obj['from_func'](text)
translated_text = to_lang_obj['to_func'](common_text)
except Exception as e:
await ctx.send(f"An error occurred during translation: `{e}`")
return
webhook = None
if ctx.guild and ctx.guild.me.guild_permissions.manage_webhooks:
for wh in await ctx.channel.webhooks():
if wh.name == "Translator Cog Webhook":
webhook = wh
break
if webhook is None:
webhook = await ctx.channel.create_webhook(name="Translator Cog Webhook")
if webhook:
if ctx.interaction:
await ctx.interaction.response.defer(ephemeral=True)
await webhook.send(content=translated_text, username=ctx.author.display_name, avatar_url=ctx.author.display_avatar.url)
await ctx.interaction.followup.send("Translation sent.", ephemeral=True)
else:
if ctx.channel.permissions_for(ctx.guild.me).manage_messages:
try:
await ctx.message.delete()
except discord.Forbidden:
pass
await webhook.send(content=translated_text, username=ctx.author.display_name, avatar_url=ctx.author.display_avatar.url)
else:
embed = discord.Embed(title=f"Translation to {to_lang_obj['name']}", color=await ctx.embed_color())
embed.add_field(name="Original Text", value=box(text), inline=False)
embed.add_field(name="Translated Text", value=box(translated_text), inline=False)
await ctx.send(embed=embed)
@commands.hybrid_command()
async def languages(self, ctx: commands.Context):
"""Lists all available languages."""
sorted_langs = sorted(self.all_languages.values(), key=lambda x: x['name'])
lang_list = [f"* `{lang['name']}`" for lang in sorted_langs]
output = "Available Languages:\n" + "\n".join(lang_list)
pages = [box(page) for page in pagify(output, page_length=1000)]
await ctx.send_interactive(pages, box_lang="md")
@commands.group(aliases=["px"], invoke_without_command=True)
async def proxy(self, ctx: commands.Context):
"""Toggles your translation proxy on or off."""
current_setting = await self.config.user(ctx.author).proxy_enabled()
new_setting = not current_setting
await self.config.user(ctx.author).proxy_enabled.set(new_setting)
status = "enabled" if new_setting else "disabled"
await ctx.send(f"Proxying is now `{status}`.")
@proxy.command(name="list")
async def proxy_list(self, ctx: commands.Context):
"""Shows your currently registered sonas."""
sonas = await self.config.user(ctx.author).sonas()
if not sonas:
return await ctx.send("You have no sonas registered.")
msg = "Your registered sonas:\n"
for name, data in sonas.items():
display_name = data.get("display_name", name)
lang_name = self.all_languages.get(data['language'], {}).get('name', 'Unknown Language')
if 'claws' in data:
start_claw, end_claw = data['claws']
elif 'brackets' in data: # Fallback for old data
start_claw, end_claw = data['brackets']
else:
continue
claw_info = f"Starts with `{start_claw}`" if not end_claw else f"`{start_claw}` and `{end_claw}`"
msg += f" - **{display_name}** (Internal Name: `{name}`): Translates to `{lang_name}`. Claws: {claw_info}\n"
for page in pagify(msg):
await ctx.send(page)
@proxy.command(name="add", aliases=["+"])
async def proxy_add(self, ctx: commands.Context, name: str, language: str, display_name: str, start_claw: str, end_claw: str = "", avatar: Optional[str] = None):
"""Registers a new sona with a name and avatar."""
matches = await self._find_language(language)
if not matches:
return await ctx.send(f"Language `{language}` not found.")
if len(matches) > 1:
possible = [f"`{self.all_languages[k]['name']}` (`{k}`)" for k in matches]
return await ctx.send(f"Multiple languages found for `{language}`. Please be more specific:\n" + ", ".join(possible))
lang_key = matches[0]
sona_key = name.lower()
avatar_url = avatar
if not avatar_url and ctx.message.attachments:
avatar_url = ctx.message.attachments[0].url
async with self.config.user(ctx.author).sonas() as sonas:
if sona_key in sonas:
return await ctx.send(f"A sona named `{name}` already exists.")
sonas[sona_key] = {
"display_name": display_name,
"avatar_url": avatar_url,
"language": lang_key,
"claws": [start_claw, end_claw]
}
await ctx.send(f"Sona `{name}` registered as `{display_name}` to translate to `{self.all_languages[lang_key]['name']}`.")
@proxy.command(name="remove", aliases=["-"])
async def proxy_remove(self, ctx: commands.Context, *, name: str):
"""Removes a sona."""
sona_key = name.lower()
async with self.config.user(ctx.author).sonas() as sonas:
if sona_key not in sonas:
return await ctx.send(f"No sona named `{name}` found.")
del sonas[sona_key]
await ctx.send(f"Sona `{name}` has been removed.")
@proxy.group()
async def sona(self, ctx: commands.Context):
"""Commands for managing a sona's appearance."""
pass
@proxy.group(name="auto")
async def proxy_auto(self, ctx: commands.Context):
"""Manage automatic translation in a channel."""
pass
@proxy_auto.command(name="set")
async def proxy_auto_set(self, ctx: commands.Context, sona_name: str):
"""Sets a sona to automatically translate your messages in this channel."""
sona_key = sona_name.lower()
sonas = await self.config.user(ctx.author).sonas()
if sona_key not in sonas:
return await ctx.send(f"No sona named `{sona_name}` found. Please register it first.")
async with self.config.channel(ctx.channel).autotranslate_users() as autotranslate_users:
autotranslate_users[str(ctx.author.id)] = sona_key
await ctx.send(f"Autotranslation enabled for you in this channel as **{sonas[sona_key]['display_name']}**.")
@proxy_auto.command(name="off")
async def proxy_auto_off(self, ctx: commands.Context):
"""Disables autotranslation for you in this channel."""
async with self.config.channel(ctx.channel).autotranslate_users() as autotranslate_users:
if str(ctx.author.id) in autotranslate_users:
del autotranslate_users[str(ctx.author.id)]
await ctx.send("Autotranslation has been disabled for you in this channel.")
else:
await ctx.send("You do not have autotranslation enabled in this channel.")
@sona.command(name="name")
async def sona_name(self, ctx: commands.Context, name: str, *, display_name: str):
"""Changes the display name of a sona."""
sona_key = name.lower()
async with self.config.user(ctx.author).sonas() as sonas:
if sona_key not in sonas:
return await ctx.send(f"No sona named `{name}` found.")
sonas[sona_key]["display_name"] = display_name
await ctx.send(f"Sona `{name}`'s display name has been changed to `{display_name}`.")
@sona.command(name="avatar")
async def sona_avatar(self, ctx: commands.Context, name: str, url: Optional[str] = None):
"""Changes the avatar of a sona.
You can either provide a direct image URL or upload an image with the command.
"""
sona_key = name.lower()
if not url and not ctx.message.attachments:
return await ctx.send("You must provide an image URL or upload an image.")
avatar_url = url
if ctx.message.attachments:
avatar_url = ctx.message.attachments[0].url
async with self.config.user(ctx.author).sonas() as sonas:
if sona_key not in sonas:
return await ctx.send(f"No sona named `{name}` found.")
sonas[sona_key]["avatar_url"] = avatar_url
await ctx.send(f"Sona `{name}`'s avatar has been updated.")
@commands.group(aliases=["tset"])
@commands.has_permissions(manage_guild=True)
async def translatorset(self, ctx: commands.Context):
"""Admin commands for the Translator cog."""
pass
@translatorset.group(name="language", aliases=["lang"])
async def translatorset_language(self, ctx: commands.Context):
"""Manage custom languages."""
pass
@translatorset_language.command(name="add", aliases=["+"])
async def translatorset_language_add(self, ctx: commands.Context, name: str, *, json_map: str):
"""Adds a new custom language."""
lang_key = name.lower()
if lang_key in self.all_languages:
return await ctx.send(f"A language with the key `{lang_key}` already exists.")
try:
lang_map = json.loads(json_map)
if not isinstance(lang_map, dict):
raise ValueError("JSON map must be an object/dictionary.")
except (json.JSONDecodeError, ValueError) as e:
return await ctx.send(f"Invalid JSON map provided: `{e}`")
new_lang_data = {'name': name.capitalize(), 'type': 'greedy', 'map': lang_map, 'is_custom': True}
async with self.config.languages() as languages:
languages[lang_key] = new_lang_data
await self._initialize_languages()
await ctx.send(f"Custom language `{name}` added successfully.")
@translatorset_language.command(name="remove", aliases=["-"])
async def translatorset_language_remove(self, ctx: commands.Context, name: str):
"""Removes a custom language."""
lang_key = name.lower()
current_data = await self.config.languages()
lang_obj = current_data.get(lang_key)
if not lang_obj or not lang_obj.get('is_custom', False):
return await ctx.send(f"No custom language named `{name}` found.")
async with self.config.languages() as languages:
if lang_key in languages:
del languages[lang_key]
await self._initialize_languages()
await ctx.send(f"Custom language `{name}` removed.")
@translatorset_language.command(name="listcustom")
async def translatorset_language_listcustom(self, ctx: commands.Context):
"""Lists all custom-added languages."""
custom_langs = [f"`{lang['name']}`" for lang in self.all_languages.values() if lang.get('is_custom')]
if not custom_langs:
return await ctx.send("There are no custom languages.")
await ctx.send("Custom Languages:\n" + ", ".join(custom_langs))
@translatorset_language.command(name="listbase")
async def translatorset_language_listbase(self, ctx: commands.Context):
"""Lists all base (built-in) languages."""
base_langs = [f"`{lang['name']}`" for lang in self.all_languages.values() if not lang.get('is_custom')]
await ctx.send("Base Languages:\n" + ", ".join(base_langs))

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,8 @@
MAP = {'a':'azg','b':'braz','c':'kraz','d':'dorg','e':'ezg','f':'fraz','g':'gor','h':'hath','i':'ix','j':'jraz','k':'kral','l':'laz','m':'maz','n':'naz','o':'oz','p':'praz','q':'qor','r':'raz','s':'saz','t':'taz','u':'uzg','v':'vraz','w':'waz','x':'xul','y':'yaz','z':'zaz'}
def get_language():
return {
'name': 'Abyssal',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'adriel','b':'baraqiel','c':'camael','d':'divinus','e':'elohim','f':'fanuel','g':'gloria','h':'hesed','i':'israfel','j':'jophiel','k':'kyrie','l':'lux','m':'michael','n':'netzach','o':'ophaniel','p':'peniel','q':'qadish','r':'raphael','s':'seraph','t':'tiferet','u':'uriel','v':'virtues','w':'wele\'el','x':'xathanael','y':'yesod','z':'zadkiel'}
def get_language():
return {
'name': 'Angelic',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,9 @@
MAP = {'a':'aqua','b':'blub','c':'\'cress','d':'drop','e':'eelee','f':'flow','g':'glur','h':'hydro','i':'ishi','j':'\'jyr','k':'\'kyr','l':'luu','m':'\'myr','n':'\'nyr','o':'oro','p':'ploop','q':'\'qyr','r':'\'ryp','s':'sh\'l','t':'tide','u':'\'urn','v':'\'vyr','w':'wash','x':'\'xyr','y':'\'yyr','z':'\'zyr'}
def get_language():
return {
'name': 'Aquan',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'abyss','b':'brine','c':'coral','d':'depth','e':'eel','f':'fin','g':'gurgle','h':'hydro','i':'ink','j':'jelly','k':'krill','l':'lagoon','m':'murk','n':'naut','o':'ocean','p':'pearl','q':'quatic','r':'reef','s':'salt','t':'tide','u':'urchin','v':'void','w':'wave','x':'xiph','y':'yacht','z':'zone'}
def get_language():
return {
'name': 'Aquatic',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'\'arr','b':'\'brach','c':'ch\'t','d':'\'drach','e':'\'err','f':'\'fune','g':'\'goss','h':'\'harr','i':'\'itt','j':'\'jarr','k':'klik\'','l':'\'lar','m':'\'marr','n':'\'narr','o':'\'orr','p':'\'parr','q':'\'qarr','r':'\'rarr','s':'skitter\'','t':'th\'k','u':'\'urr','v':'\'varr','w':'web\'','x':'\'xarr','y':'\'yarr','z':'\'zarr'}
def get_language():
return {
'name': 'Arachnid',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'\'aara','b':'\'bree','c':'chir\'','d':'\'dree','e':'eek','f':'\'flutter','g':'gree\'','h':'hrooa','i':'ii\'','j':'\'jakk','k':'kree\'','l':'\'liri','m':'\'meeka','n':'\'neer','o':'\'oroo','p':'pip\'','q':'\'qree','r':'\'reea','s':'\'skraw','t':'tweet\'','u':'\'urr','v':'\'vree','w':'\'warble','x':'\'xee','y':'\'yari','z':'\'zeer'}
def get_language():
return {
'name': 'Avian',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'\'ana','b':'\'bara','c':'\'cera','d':'\'dona','e':'\'elara','f':'\'fana','g':'\'gala','h':'\'hylia','i':'\'iana','j':'\'jana','k':'\'kana','l':'\'lora','m':'\'mara','n':'\'nara','o':'\'ora','p':'\'pera','q':'\'qana','r':'\'ria','s':'\'sera','t':'\'tara','u':'\'ura','v':'\'vara','w':'\'wana','x':'\'xara','y':'\'yana','z':'\'zara'}
def get_language():
return {
'name': 'Celestial',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
# This language is handled by its 'rule' type in the main cog.
# It doesn't need a map.
def get_language():
return {
'name': 'Common',
'type': 'rule'
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'auto','b':'bolt','c':'clank','d':'diode','e':'engine','f':'forge','g':'gear','h':'hydro','i':'iron','j':'joint','k':'kinetic','l':'link','m':'motor','n':'node','o':'optic','p':'piston','q':'quantum','r':'rivet','s':'servo','t':'titan','u':'unit','v':'volt','w':'whirr','x':'xenon','y':'yoke','z':'zinc'}
def get_language():
return {
'name': 'Construct',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'\'ayl','b':'\'baal','c':'\'cress','d':'\'drev','e':'\'eyl','f':'\'fane','g':'\'gyl','h':'hysh\'','i':'\'iyl','j':'\'jex','k':'\'krys','l':'\'lyl','m':'\'mal','n':'\'nyl','o':'\'oyl','p':'\'prax','q':'\'qyl','r':'\'ryl','s':'\'shayd','t':'\'trys','u':'\'uyl','v':'\'vyl','w':'\'wryl','x':'\'xyl','y':'\'yyl','z':'\'zyll'}
def get_language():
return {
'name': 'Devilish',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'ax','b':'baxis','c':'caex','d':'drak','e':'ess','f':'faex','g':'gix','h':'heth','i':'ir','j':'jyss','k':'kex','l':'lix','m':'maex','n':'nex','o':'oth','p':'pex','q':'qexis','r':'rax','s':'syth','t':'thrax','u':'ur','v':'vyx','w':'wess','x':'xis','y':'yth','z':'zix'}
def get_language():
return {
'name': 'Draconic',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'az','b':'bar','c':'krag','d':'dur','e':'ek','f':'fol','g':'grum','h':'hur','i':'in','j':'jor','k':'kaz','l':'lur','m':'mor','n':'nur','o':'ok','p':'por','q':'qur','r':'ruk','s':'son','t':'thor','u':'um','v':'val','w':'wor','x':'xor','z':'zul'}
def get_language():
return {
'name': 'Dwarvish',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'aer','b':'breeze','c':'cinder','d':'dust','e':'ember','f':'flow','g':'gust','h':'hail','i':'ignis','j':'jet','k':'kinetic','l':'lava','m':'mist','n':'nova','o':'ozone','p':'pyre','q':'quake','r':'rain','s':'stone','t':'terra','u':'umbra','v':'vapor','w':'wave','x':'xenon','y':'yon','z':'zephyr'}
def get_language():
return {
'name': 'Elemental',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
MAP = {'a':'ael','b':'\'eth','c':'cal','d':'dor','e':'elen','f':'fae','g':'\'gan','h':'h<EFBFBD>r','i':'ia','j':'yel','k':'\'ken','l':'lael','m':'mel','n':'n<EFBFBD>n','o':'oia','p':'\'pes','q':'qen','r':'rae','s':'sil','t':'t<EFBFBD>','u':'ui','v':'vae','w':'win','x':'\'xal','y':'yl','z':'z<EFBFBD>r'}
def get_language():
return {
'name': 'Elvish',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'meow','b':'brrt','c':'chrr','d':'drrt','e':'eek','f':'frrt','g':'grrowl','h':'hiss','i':'mii','j':'jrr','k':'krr','l':'lrr','m':'mrow','n':'nyah','o':'oww','p':'purr','q':'qrr','r':'rrr','s':'sss','t':'trill','u':'urr','v':'vrr','w':'wrr','x':'xrr','y':'yowl','z':'zzz'}
def get_language():
return {
'name': 'Feline',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'az\'ael','b':'ba\'al','c':'cre\'z','d':'dre\'th','e':'esh\'','f':'fiir\'','g':'gre\'th','h':'ha\'el','i':'i\'z','j':'je\'th','k':'krez\'','l':'le\'th','m':'morn\'','n':'ne\'th','o':'o\'z','p':'pre\'th','q':'qe\'th','r':'re\'th','s':'se\'th','t':'te\'th','u':'u\'z','v':'ve\'th','w':'we\'th','x':'xith\'','y':'ye\'th','z':'zael\''}
def get_language():
return {
'name': 'Fiendish',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'akk','b':'bink','c':'clank','d':'dink','e':'enk','f':'fizz','g':'giz','h':'hink','i':'ink','j':'jink','k':'kink','l':'link','m':'mink','n':'nink','o':'onk','p':'sprok','q':'qink','r':'rink','s':'sprock','t':'tink','u':'unk','v':'vink','w':'whirr','x':'xink','y':'yink','z':'zink'}
def get_language():
return {
'name': 'Gnomish',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'az','b':'bik','c':'clik','d':'dik','e':'ek','f':'fiz','g':'gib','h':'hik','i':'ik','j':'jik','k':'krik','l':'lik','m':'mik','n':'nik','o':'ok','p':'pik','q':'qik','r':'rik','s':'snik','t':'tik','u':'uk','v':'vik','w':'wik','x':'xik','y':'yik','z':'zik'}
def get_language():
return {
'name': 'Goblin',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'apple','b':'bramble','c':'crumpet','d':'dale','e':'elderberry','f':'fiddle','g':'garden','h':'hearth','i':'iris','j':'jam','k':'kettle','l':'lazy','m':'meadow','n':'nimble','o':'oats','p':'pudding','q':'quaint','r':'river','s':'sunny','t':'tater','u':'underhill','v':'vine','w':'willow','x':'extra','y':'yarn','z':'zesty'}
def get_language():
return {
'name': 'Half-Tongue',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'aloe','b':'bark','c':'clover','d':'dand','e':'elder','f':'fern','g':'groot','h':'herb','i':'ivy','j':'juni','k':'kelp','l':'leaf','m':'moss','n':'nettle','o':'oak','p':'petal','q':'quin','r':'root','s':'sprout','t':'thyme','u':'ursi','v':'vine','w':'willow','x':'xylem','y':'yarrow','z':'zinni'}
def get_language():
return {
'name': 'Herbilore',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'az\'','b':'\'baal','c':'\'krez','d':'\'drak','e':'ez\'','f':'\'fel','g':'\'gor','h':'\'hath','i':'iz\'','j':'\'jaz','k':'\'kraz','l':'\'laz','m':'\'mor','n':'\'naz','o':'oz\'','p':'\'paz','q':'\'qaz','r':'\'raz','s':'\'saz','t':'\'taz','u':'uz\'','v':'\'vaz','w':'\'waz','x':'\'xaz','y':'\'yaz','z':'\'zaz'}
def get_language():
return {
'name': 'Infernal',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,9 @@
MAP = {'a':'ka','b':'be','c':'chi','d':'de','e':'e','f':'fu','g':'ga','h':'hi','i':'i','j':'ji','k':'ki','l':'ru','m':'ma','n':'na','o':'o','p':'pe','q':'kyu','r':'re','s':'sa','t':'to','u':'u','v':'ve','w':'wa','x':'za','y':'ya','z':'ze'}
def get_language():
return {
'name': 'Kitsune',
'type': 'greedy',
'map': MAP,
'separator': ''
}

View File

@@ -0,0 +1,9 @@
MAP = {'a':'4','b':'8','c':'(','d':')','e':'3','f':'|=','g':'6','h':'#','i':'1','j':']','k':'|<','l':'1','m':'/\\/\\','n':'/\\/','o':'0','p':'|D','q':'(,)','r':'|2','s':'5','t':'7','u':'|_|','v':'\\/','w':'\\/\\/','x':'><','y':'`/','z':'2'}
def get_language():
return {
'name': 'Leet Speak',
'type': 'special',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'\'ax','b':'\'bax','c':'\'caz','d':'\'daz','e':'\'ex','f':'\'fax','g':'\'gaz','h':'h\'ss','i':'\'ix','j':'\'jax','k':'sk\'ex','l':'\'lax','m':'\'max','n':'\'nax','o':'\'ox','p':'\'pax','q':'\'qax','r':'\'rax','s':'s\'lith','t':'\'char','u':'\'ux','v':'\'vax','w':'\'wax','x':'\'xax','y':'\'yax','z':'\'zax'}
def get_language():
return {
'name': 'Lizardfolk',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'.-','b':'-...','c':'-.-.','d':'-..','e':'.','f':'..-.','g':'--.','h':'....','i':'..','j':'.---','k':'-.-','l':'.-..','m':'--','n':'-.','o':'---','p':'.--.','q':'--.-','r':'.-.','s':'...','t':'-','u':'..-','v':'...-','w':'.--','x':'-..-','y':'-.--','z':'--..','1':'.----','2':'..---','3':'...--','4':'....-','5':'.....','6':'-....','7':'--...','8':'---..','9':'----.','0':'-----',' ':'/'}
def get_language():
return {
'name': 'Morse Code',
'type': 'special',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'agaric','b':'bolete','c':'cap','d':'decay','e':'enoki','f':'fungi','g':'gill','h':'hyphae','i':'indigo','j':'jelly','k':'kombu','l':'lichen','m':'myco\'','n':'nidur','o':'oyster','p':'puff','q':'quorn','r':'rhizo','s':'spore\'','t':'thallus','u':'umbra','v':'velvet','w':'wart','x':'xero','y':'yeast','z':'zoospore'}
def get_language():
return {
'name': 'Myconid',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'ug','b':'blud','c':'crag','d':'dug','e':'eeg','f':'fug','g':'gron','h':'hug','i':'ig','j':'jug','k':'krug','l':'lug','m':'mush','n':'nug','o':'og','p':'pug','q':'qug','r':'rug','s':'slog','t':'thok','u':'urk','v':'vog','w':'wug','x':'xug','y':'yug','z':'zug'}
def get_language():
return {
'name': 'Ogrish',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'agh','b':'bug','c':'karg','d':'dur','e':'egh','f':'fug','g':'grol','h':'hosh','i':'izg','j':'jug','k':'krunk','l':'lug','m':'mog','n':'nog','o':'ogg','p':'pug','q':'qug','r':'ruk','s':'snaga','t':'tusk','u':'uruk','v':'vug','w':'warg','x':'xug','y':'yag','z':'zug'}
def get_language():
return {
'name': 'Orcish',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
# This language is handled by its 'rule' type in the main cog.
# It doesn't need a map.
def get_language():
return {
'name': 'Pig Latin',
'type': 'rule'
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'skree','b':'bite','c':'claw','d':'dark-thing','e':'eek','f':'filth','g':'gnaw','h':'hiss','i':'itch','j':'junk','k':'kill','l':'long-tail','m':'muck','n':'nest-thing','o':'rot-stink','p':'plague','q':'quick-quick','r':'rust','s':'skitter','t':'twitch','u':'under-thing','v':'vermin','w':'waste','x':'pox','y':'yes-yes','z':'zap-tail'}
def get_language():
return {
'name': 'Ratfolk',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'sk\'','b':'t\'k','c':'k\'ss','d':'d\'th','e':'e\'sk','f':'f\'t','g':'g\'th','h':'h\'k','i':'i\'s','j':'j\'t','k':'k\'t','l':'l\'k','m':'m\'k','n':'n\'t','o':'o\'s','p':'p\'k','q':'q\'t','r':'r\'k','s':'s\'k','t':'t\'s','u':'u\'s','v':'v\'t','w':'w\'k','x':'x\'s','y':'y\'k','z':'z\'t'}
def get_language():
return {
'name': 'Scorpion',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,10 @@
MAP = {'a':'a!','b':'7b','c':'c#','d':'d4','e':'3e','f':'f^','g':'g&','h':'h8','i':'(i','j':'j0','k':'_k','l':'l2','m':'=m','n':'n+','o':'5o','p':'-p','q':'q{','r':'}r','s':'[s','t':'t]','u':'|u','v':':v','w':'"w','x':'x<','y':'>y','z':'?z'}
def get_language():
return {
'name': 'Sinary',
'type': 'generic',
'map': MAP,
'chunk_size': 2,
'separator': ''
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'\'aura','b':'\'breth','c':'\'ciel','d':'\'dion','e':'\'ethys','f':'\'fey','g':'\'glyn','h':'\'hymn','i':'\'ia','j':'\'jora','k':'\'kye','l':'\'lume','m':'\'mana','n':'\'nima','o':'\'omni','p':'\'pria','q':'\'qia','r':'\'reth','s':'\'seren','t':'\'thyme','u':'\'umbra','v':'\'vym','w':'\'wisp','x':'\'xia','y':'\'yara','z':'\'zion'}
def get_language():
return {
'name': 'Spiritual',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'ah\'','b':'\'bel','c':'\'chae','d':'\'des','e':'\'esh','f':'\'fey','g':'\'gis','h':'hah\'','i':'\'ish','j':'\'jo','k':'\'ka','l':'\'lis','m':'\'mah','n':'\'nah','o':'oh\'','p':'\'pah','q':'\'qia','r':'\'rah','s':'\'sha','t':'\'thae','u':'uh\'','v':'\'vi','w':'\'wah','x':'\'xi','y':'\'yah','z':'\'zah'}
def get_language():
return {
'name': 'Succubus',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'ani','b':'bri','c':'cae','d':'dae','e':'eni','f':'fae','g':'gra','h':'hae','i':'ia','j':'jae','k':'kae','l':'lor','m':'mae','n':'nem','o':'olo','p':'pae','q':'qae','r':'rae','s':'sae','t':'tae','u':'uni','v':'vae','w':'wae','x':'xae','y':'yae','z':'zae'}
def get_language():
return {
'name': 'Sylvan',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'velk','b':'xund','c':'k\'yorl','d':'d\'ruth','e':'e\'trorn','f':'faer','g':'gol','h':'h\'chak','i':'i\'lith','j':'j\'lar','k':'k\'lar','l':'lil','m':'m\'lar','n':'nind','o':'olath','p':'p\'lar','q':'qu\'ellar','r':'ril','s':'sorn','t':'\'lar','u':'uss','v':'v\'lar','w':'wyl','x':'x\'lar','y':'y\'lar','z':'z\'ress'}
def get_language():
return {
'name': 'Undercommon',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
# This language is handled by its 'rule' type in the main cog.
# It doesn't need a map.
def get_language():
return {
'name': 'UwU',
'type': 'rule'
}

View File

@@ -0,0 +1,10 @@
MAP = {'a':'ak','b':'ba','c':'ce','d':'di','e':'ek','f':'fo','g':'gu','h':'ha','i':'ik','j':'je','k':'ki','l':'lo','m':'mu','n':'na','o':'ok','p':'pe','q':'qi','r':'ro','s':'su','t':'ta','u':'uk','v':'ve','w':'wi','x':'xo','y':'yk','z':'zu'}
def get_language():
return {
'name': 'Valspiren',
'type': 'generic',
'map': MAP,
'chunk_size': 2,
'separator': ''
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'a\'th','b':'b\'zoth','c':'c\'thun','d':'d\'gol','e':'e\'th','f':'f\'thagn','g':'g\'noth','h':'h\'zoth','i':'i\'th','j':'j\'th','k':'k\'th','l':'l\'th','m':'m\'th','n':'n\'th','o':'o\'th','p':'p\'th','q':'qor\'','r':'r\'lyeh','s':'s\'th','t':'t\'th','u':'u\'th','v':'v\'lath','w':'w\'th','x':'x\'thul','y':'y\'th','z':'zy\'th'}
def get_language():
return {
'name': 'Voidtouched',
'type': 'greedy',
'map': MAP
}

196
translator/logic.py Normal file
View File

@@ -0,0 +1,196 @@
# -*- coding: utf-8 -*-
import re
import random
from redbot.core.utils.chat_formatting import box
def reverse_map(m): return {v: k for k, v in m.items()}
class TranslationLogicMixin:
"""This class holds all the translation logic for the cog."""
def _escape_regex(self, s):
return re.escape(s)
def _generic_translator(self, text, lang_map, char_separator):
# Regex to find custom emojis
emoji_regex = re.compile(r"<a?:\w+:\d+>")
# Find all emojis and store them
emojis = emoji_regex.findall(text)
# Replace emojis with a unique placeholder
placeholder = "||EMOJI||"
text_with_placeholders = emoji_regex.sub(placeholder, text)
# Translate the text with placeholders
word_separator = ' ' if char_separator == '' else ' '
words = text_with_placeholders.split(' ')
translated_words = []
emoji_counter = 0
for word in words:
if placeholder in word:
# If a word contains a placeholder, it might be part of the emoji code that got split.
# We simply re-insert the emoji from our list.
translated_words.append(emojis[emoji_counter])
emoji_counter += 1
else:
translated_word = char_separator.join([lang_map.get(char.lower(), char) for char in word])
translated_words.append(translated_word)
return word_separator.join(translated_words)
def _generic_decoder(self, text, reverse_map, chunk_size):
result = ""
text_no_space = text.replace(' ','')
for i in range(0, len(text_no_space), chunk_size):
chunk = text_no_space[i:i+chunk_size]
result += reverse_map.get(chunk, '?')
return result
def _greedy_decoder(self, text, reverse_map):
syllables = sorted(reverse_map.keys(), key=len, reverse=True)
regex = re.compile('|'.join(map(self._escape_regex, syllables)))
matches = regex.findall(text.replace(' ', ''))
return "".join([reverse_map.get(m, '?') for m in matches])
def _leet_decoder(self, text, reverse_map):
decoded_text = text
sorted_keys = sorted(reverse_map.keys(), key=len, reverse=True)
for key in sorted_keys:
decoded_text = decoded_text.replace(key, reverse_map[key])
return decoded_text
def _morse_decoder(self, text, reverse_map):
words = text.split(' ')
decoded_words = []
for word in words:
chars = word.split(' ')
decoded_words.append("".join([reverse_map.get(c, '?') for c in chars]))
return " ".join(decoded_words)
def _pig_latin_translator(self, text):
vowels = "aeiou"
translated_words = []
for word in text.split(' '):
if not word: continue
match = re.match(r"^([^a-zA-Z0-9]*)(.*?)([^a-zA-Z0-9]*)$", word)
leading_punct, clean_word, trailing_punct = match.groups()
if not clean_word:
translated_words.append(word)
continue
if clean_word and clean_word[0].lower() in vowels:
translated_word = clean_word + "way"
else:
first_vowel_index = -1
for i, char in enumerate(clean_word):
if char.lower() in vowels:
first_vowel_index = i
break
if first_vowel_index == -1:
translated_word = clean_word + "ay"
else:
translated_word = clean_word[first_vowel_index:] + clean_word[:first_vowel_index] + "ay"
translated_words.append(leading_punct + translated_word + trailing_punct)
return " ".join(translated_words)
def _pig_latin_decoder(self, text):
decoded_words = []
for word in text.split(' '):
if not word: continue
match = re.match(r"^([^a-zA-Z0-9]*)(.*?)([^a-zA-Z0-9]*)$", word)
leading_punct, clean_word, trailing_punct = match.groups()
if not clean_word:
decoded_words.append(word)
continue
original_word = ""
if clean_word.lower().endswith("way"):
original_word = clean_word[:-3]
elif clean_word.lower().endswith("ay"):
base_word = clean_word[:-2]
last_consonant_block_index = -1
for i in range(len(base_word) - 1, -1, -1):
if base_word[i].lower() not in "aeiou":
last_consonant_block_index = i
else:
break
if last_consonant_block_index != -1:
while last_consonant_block_index > 0 and base_word[last_consonant_block_index-1].lower() not in "aeiou":
last_consonant_block_index -= 1
consonants = base_word[last_consonant_block_index:]
stem = base_word[:last_consonant_block_index]
original_word = consonants + stem
else:
original_word = base_word
else:
original_word = clean_word
decoded_words.append(leading_punct + original_word + trailing_punct)
return " ".join(decoded_words)
def _uwu_translator(self, text):
text = text.lower()
text = text.replace('l', 'ww')
text = text.replace('r', 'w')
text = text.replace('na', 'nya').replace('ne', 'nye').replace('ni', 'nyi').replace('no', 'nyo').replace('nu', 'nyu')
text = text.replace('ove', 'uv')
words = text.split(' ')
uwu_words = []
for word in words:
if len(word) > 3 and random.random() < 0.3:
uwu_words.append(f"{word[0]}-{word}")
else:
uwu_words.append(word)
text = " ".join(uwu_words)
if random.random() < 0.5:
emoticons = [' uwu', ' owo', ' >w<', ' ^-^', ' ;;']
text += random.choice(emoticons)
return text
def _uwu_decoder(self, text):
emoticons = [' uwu', ' owo', ' >w<', ' ^-^', ' ;;']
for emo in emoticons:
if text.endswith(emo):
text = text[:-len(emo)]
text = re.sub(r'(\w)-(\w+)', r'\2', text)
text = text.replace('uv', 'ove')
text = text.replace('nyu', 'nu').replace('nyo', 'no').replace('nyi', 'ni').replace('nye', 'ne').replace('nya', 'na')
text = text.replace('ww', 'l')
text = text.replace('w', 'r')
return text
async def _find_language(self, query: str):
"""Finds languages by key or name, case-insensitively."""
query = query.lower()
exact_key_match = [key for key in self.all_languages if key.lower() == query]
if exact_key_match:
return exact_key_match
exact_name_match = [key for key, lang in self.all_languages.items() if lang['name'].lower() == query]
if exact_name_match:
return exact_name_match
partial_matches = [
key for key, lang in self.all_languages.items()
if query in key.lower() or query in lang['name'].lower()
]
return partial_matches
async def _auto_translate_to_common(self, text: str) -> list:
"""Helper to find all possible translations for a given text."""
possible_translations = []
for lang_key, lang_obj in self.all_languages.items():
if lang_key == 'common':
continue
try:
decoded_text = lang_obj['from_func'](text)
re_encoded_text = lang_obj['to_func'](decoded_text)
if re_encoded_text == text:
possible_translations.append(f"**As {lang_obj['name']}:**\n{box(decoded_text)}")
except Exception:
continue
return possible_translations

249
translator/translator.py Normal file
View File

@@ -0,0 +1,249 @@
# -*- coding: utf-8 -*-
import discord
import pkgutil
from redbot.core import commands, app_commands, Config
from redbot.core.utils.chat_formatting import box
from . import languages as lang_pkg
from .views import DismissView
from .logic import TranslationLogicMixin
from .commands import CommandsMixin
def reverse_map(m): return {v: k for k, v in m.items()}
class Translator(CommandsMixin, TranslationLogicMixin, commands.Cog):
"""A cog for translating text into various languages."""
def __init__(self, bot):
self.bot = bot
self.config = Config.get_conf(self, identifier=8675309, force_registration=True)
self.config.register_global(languages={})
self.config.register_user(sonas={}, proxy_enabled=False)
self.config.register_channel(autotranslate_users={})
self.all_languages = {}
self.translate_to_common_context_menu = app_commands.ContextMenu(
name="Translate to Common",
callback=self.translate_to_common_context,
)
async def cog_load(self):
"""Load all languages from config and build the runtime objects."""
self.bot.tree.add_command(self.translate_to_common_context_menu)
await self._initialize_languages()
async def cog_unload(self):
"""Remove context menu on cog unload."""
self.bot.tree.remove_command(self.translate_to_common_context_menu.name, type=self.translate_to_common_context_menu.type)
def _load_base_languages_from_files(self):
"""Dynamically loads all base languages from the languages/ subfolder."""
base_languages_data = {}
for importer, modname, ispkg in pkgutil.iter_modules(lang_pkg.__path__, f"{lang_pkg.__name__}."):
if not ispkg:
try:
module = __import__(modname, fromlist=["get_language"])
if hasattr(module, "get_language"):
lang_data = module.get_language()
lang_key = modname.split('.')[-1]
base_languages_data[lang_key] = lang_data
except Exception as e:
print(f"Error loading language module {modname}: {e}")
return base_languages_data
def _build_runtime_languages(self, language_data):
"""Builds the final language dict with functions from stored data."""
runtime_langs = {}
for key, data in language_data.items():
lang_type = data.get("type", "greedy") # Default to greedy for custom
runtime_langs[key] = {"name": data["name"], "is_custom": data.get("is_custom", False), "type": lang_type}
if lang_type == "rule":
if key == "common":
runtime_langs[key]['to_func'] = lambda text: text
runtime_langs[key]['from_func'] = lambda text: text
elif key == "piglatin":
runtime_langs[key]['to_func'] = self._pig_latin_translator
runtime_langs[key]['from_func'] = self._pig_latin_decoder
elif key == "uwu":
runtime_langs[key]['to_func'] = self._uwu_translator
runtime_langs[key]['from_func'] = self._uwu_decoder
elif lang_type == "generic":
lang_map = data.get("map", {})
chunk_size = data.get("chunk_size", 2)
char_separator = data.get("separator", "")
word_separator = ' ' if char_separator == '' else ' '
runtime_langs[key]['to_func'] = lambda text, m=lang_map, s=char_separator: self._generic_translator(text, m, s)
runtime_langs[key]['from_func'] = lambda text, rm=reverse_map(lang_map), cs=chunk_size, ws=word_separator: " ".join([self._generic_decoder(word, rm, cs) for word in text.split(ws)])
elif lang_type == "special":
if key == "leet":
lang_map = data.get("map", {})
runtime_langs[key]['to_func'] = lambda text, m=lang_map: self._generic_translator(text, m, "")
runtime_langs[key]['from_func'] = lambda text, rm=reverse_map(lang_map): self._leet_decoder(text, rm)
elif key == "morse":
lang_map = data.get("map", {})
runtime_langs[key]['to_func'] = lambda text, m=lang_map: self._generic_translator(text, m, " ")
runtime_langs[key]['from_func'] = lambda text, rm=reverse_map(lang_map): self._morse_decoder(text, rm)
else: # Greedy is the default
lang_map = data.get("map", {})
char_separator = data.get("separator", " ")
word_separator = ' ' if char_separator == '' else ' '
runtime_langs[key]['to_func'] = lambda text, m=lang_map, s=char_separator: self._generic_translator(text, m, s)
runtime_langs[key]['from_func'] = lambda text, rm=reverse_map(lang_map), ws=word_separator: " ".join([self._greedy_decoder(word, rm) for word in text.split(ws)])
return runtime_langs
async def _initialize_languages(self):
"""Merge default and custom languages from config."""
base_languages_data = self._load_base_languages_from_files()
stored_languages_data = await self.config.languages()
if not stored_languages_data:
await self.config.languages.set(base_languages_data)
final_data = base_languages_data
else:
final_data = base_languages_data.copy()
for key, data in stored_languages_data.items():
if data.get("is_custom"):
final_data[key] = data
await self.config.languages.set(final_data)
self.all_languages = self._build_runtime_languages(final_data)
async def translate_to_common_context(self, interaction: discord.Interaction, message: discord.Message):
"""Translate a message to Common."""
await interaction.response.defer(ephemeral=True)
possible_translations = await self._auto_translate_to_common(message.content)
if not possible_translations:
await interaction.followup.send("Could not find a valid translation for this message.", ephemeral=True)
else:
await interaction.followup.send("\n\n".join(possible_translations), ephemeral=True)
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
if message.author.bot or not message.guild:
return
prefix = (await self.bot.get_prefix(message))[0]
if message.content.startswith(prefix):
return
if message.reference and message.content.lower() == 'translate':
try:
replied_message = await message.channel.fetch_message(message.reference.message_id)
text_to_translate = replied_message.content
possible_translations = await self._auto_translate_to_common(text_to_translate)
if not possible_translations:
response_msg = "Could not find a valid translation for the replied message."
else:
response_msg = "\n\n".join(possible_translations)
view = DismissView(author=message.author)
sent_message = await message.reply(
response_msg,
view=view,
allowed_mentions=discord.AllowedMentions.none()
)
view.message = sent_message
if message.channel.permissions_for(message.guild.me).manage_messages:
await message.delete()
return
except Exception:
return
user_settings = await self.config.user(message.author).all()
if not user_settings['sonas']:
return
content = message.content
matched_sona = None
for sona_data in user_settings['sonas'].values():
if 'claws' in sona_data:
start_claw, end_claw = sona_data['claws']
elif 'brackets' in sona_data:
start_claw, end_claw = sona_data['brackets']
else:
continue
if not content.startswith(start_claw): continue
if end_claw and end_claw != "":
if not content.endswith(end_claw) or len(content) < len(start_claw) + len(end_claw):
continue
matched_sona = sona_data
break
if matched_sona:
if not user_settings['proxy_enabled']:
msg = (
f"{message.author.mention}, it looks like you tried to proxy as **{matched_sona['display_name']}**, "
f"but your proxy is turned off. You can re-enable it with the `{prefix}proxy` command."
)
try:
view = DismissView(author=message.author)
sent_message = await message.channel.send(msg, view=view, allowed_mentions=discord.AllowedMentions(users=True))
view.message = sent_message
except discord.Forbidden:
pass
return
if 'claws' in matched_sona:
start_claw, end_claw = matched_sona['claws']
else:
start_claw, end_claw = matched_sona['brackets']
text_to_translate = content[len(start_claw):-len(end_claw)] if end_claw else content[len(start_claw):]
lang_key = matched_sona['language']
else:
autotranslate_users = await self.config.channel(message.channel).autotranslate_users()
user_id_str = str(message.author.id)
if user_id_str not in autotranslate_users:
return
sona_key = autotranslate_users[user_id_str]
sonas = user_settings.get('sonas', {})
if sona_key not in sonas:
return
matched_sona = sonas[sona_key]
text_to_translate = content
lang_key = matched_sona['language']
to_lang_obj = self.all_languages.get(lang_key)
if not to_lang_obj: return
try:
translated_text = to_lang_obj['to_func'](text_to_translate)
except Exception:
return
webhook = None
if message.channel.permissions_for(message.guild.me).manage_webhooks:
webhooks = await message.channel.webhooks()
webhook = next((wh for wh in webhooks if wh.name == "Translator Cog Webhook"), None)
if webhook is None:
webhook = await message.channel.create_webhook(name="Translator Cog Webhook")
if webhook:
if message.channel.permissions_for(message.guild.me).manage_messages:
try:
await message.delete()
except discord.Forbidden:
pass
display_name = matched_sona.get("display_name", message.author.display_name)
avatar_url = matched_sona.get("avatar_url") or message.author.display_avatar.url
await webhook.send(
content=translated_text,
username=display_name,
avatar_url=avatar_url
)

28
translator/views.py Normal file
View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
import discord
class DismissView(discord.ui.View):
def __init__(self, author: discord.Member):
super().__init__(timeout=1800) # 30 minute timeout
self.author = author
self.message: discord.Message = None
async def interaction_check(self, interaction: discord.Interaction) -> bool:
if interaction.user.id != self.author.id:
await interaction.response.send_message("You are not authorized to dismiss this message.", ephemeral=True)
return False
return True
@discord.ui.button(label="Dismiss", style=discord.ButtonStyle.grey, emoji="??")
async def dismiss_button(self, interaction: discord.Interaction, button: discord.ui.Button):
await self.message.delete()
self.stop()
async def on_timeout(self):
if self.message:
try:
await self.message.delete()
except (discord.NotFound, discord.Forbidden):
pass # Message was already deleted or permissions are missing
self.stop()