Compare commits
26 Commits
f546eaa633
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| acc7eaa861 | |||
| 5a31115458 | |||
| b35ecb52b7 | |||
| 7ea6a52a6c | |||
| 65ddb244fe | |||
| 500c7daaae | |||
| 07b436ed8c | |||
| 01a484ddb6 | |||
| acb71337fa | |||
| 81f2eee409 | |||
| c2367369f1 | |||
| 0e5afcb999 | |||
| b27a734e3c | |||
| 9ad286b671 | |||
| b186a9c119 | |||
| d3ee48112a | |||
| 48c3793768 | |||
| 141efc4253 | |||
| 61b81068ba | |||
| a0df694243 | |||
| ca7dba5af6 | |||
| d65197e552 | |||
| 0909717fb6 | |||
| 84a2b41a79 | |||
| 82e48c2383 | |||
| 2d199d9247 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -179,3 +179,5 @@ cython_debug/
|
|||||||
.vs/
|
.vs/
|
||||||
|
|
||||||
pyproject.toml
|
pyproject.toml
|
||||||
|
|
||||||
|
treegen.py
|
||||||
|
|||||||
4
datamanager/__init__.py
Normal file
4
datamanager/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .datamanager import DataManager
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(DataManager(bot))
|
||||||
128
datamanager/datamanager.py
Normal file
128
datamanager/datamanager.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import discord
|
||||||
|
from redbot.core import commands, Config
|
||||||
|
import datetime
|
||||||
|
from typing import Dict, Optional, Literal
|
||||||
|
from discord.ext import tasks
|
||||||
|
|
||||||
|
SUPPORTED_COGS = Literal["ModMail", "Hiring", "ServiceReview", "StaffMsg", "Logging"]
|
||||||
|
|
||||||
|
class DataManager(commands.Cog):
|
||||||
|
"""
|
||||||
|
A cog to automatically manage and purge old data from other cogs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.config = Config.get_conf(self, identifier=1234567894, force_registration=True)
|
||||||
|
default_guild = {
|
||||||
|
"policies": {} # {cog_name: days}
|
||||||
|
}
|
||||||
|
self.config.register_guild(**default_guild)
|
||||||
|
self.data_purge_loop.start()
|
||||||
|
|
||||||
|
def cog_unload(self):
|
||||||
|
self.data_purge_loop.cancel()
|
||||||
|
|
||||||
|
@tasks.loop(hours=24)
|
||||||
|
async def data_purge_loop(self):
|
||||||
|
"""Periodically purges old data based on configured policies."""
|
||||||
|
await self.bot.wait_until_ready()
|
||||||
|
all_guilds = self.bot.guilds
|
||||||
|
for guild in all_guilds:
|
||||||
|
policies = await self.config.guild(guild).policies()
|
||||||
|
if not policies:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for cog_name, days in policies.items():
|
||||||
|
cog = self.bot.get_cog(cog_name)
|
||||||
|
if not cog or not hasattr(cog, "config"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
retention_delta = datetime.timedelta(days=days)
|
||||||
|
|
||||||
|
# Determine the correct config group to purge
|
||||||
|
# This needs to match what we defined in the other cogs
|
||||||
|
data_group_name = ""
|
||||||
|
if cog_name == "ModMail":
|
||||||
|
data_group_name = "closed_threads"
|
||||||
|
elif cog_name == "Hiring":
|
||||||
|
data_group_name = "closed_applications"
|
||||||
|
elif cog_name == "ServiceReview":
|
||||||
|
data_group_name = "reviews"
|
||||||
|
elif cog_name in ["StaffMsg", "Logging"]:
|
||||||
|
data_group_name = "logged_events"
|
||||||
|
|
||||||
|
if not data_group_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
async with cog.config.guild(guild).get_attr(data_group_name)() as data_log:
|
||||||
|
to_delete = []
|
||||||
|
for entry_id, timestamp_str in data_log.items():
|
||||||
|
try:
|
||||||
|
entry_timestamp = datetime.datetime.fromisoformat(timestamp_str)
|
||||||
|
if (datetime.datetime.now(datetime.timezone.utc) - entry_timestamp) > retention_delta:
|
||||||
|
to_delete.append(entry_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue # Skip invalid timestamps
|
||||||
|
|
||||||
|
for entry_id in to_delete:
|
||||||
|
del data_log[entry_id]
|
||||||
|
|
||||||
|
# --- SETTINGS COMMANDS ---
|
||||||
|
@commands.group(aliases=["dmset"]) # type: ignore
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
|
async def datamanagerset(self, ctx: commands.Context):
|
||||||
|
"""Configure DataManager settings."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@datamanagerset.command(name="policy")
|
||||||
|
async def set_purge_policy(self, ctx: commands.Context, cog_name: SUPPORTED_COGS, days: int):
|
||||||
|
"""
|
||||||
|
Set the data retention policy for a cog.
|
||||||
|
|
||||||
|
Use 0 days to disable purging for a cog.
|
||||||
|
"""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
if days < 0:
|
||||||
|
await ctx.send("Please provide a non-negative number of days.")
|
||||||
|
return
|
||||||
|
|
||||||
|
async with self.config.guild(ctx.guild).policies() as policies:
|
||||||
|
if days == 0:
|
||||||
|
if cog_name in policies:
|
||||||
|
del policies[cog_name]
|
||||||
|
await ctx.send(f"Purge policy for `{cog_name}` has been removed.")
|
||||||
|
else:
|
||||||
|
await ctx.send(f"No purge policy was set for `{cog_name}`.")
|
||||||
|
else:
|
||||||
|
policies[cog_name] = days
|
||||||
|
await ctx.send(f"Data from `{cog_name}` will now be purged after {days} days.")
|
||||||
|
|
||||||
|
@datamanagerset.command(name="view")
|
||||||
|
async def view_policies(self, ctx: commands.Context):
|
||||||
|
"""View the current data retention policies."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
policies = await self.config.guild(ctx.guild).policies()
|
||||||
|
if not policies:
|
||||||
|
await ctx.send("No data retention policies have been set for this server.")
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Data Retention Policies",
|
||||||
|
color=await ctx.embed_color()
|
||||||
|
)
|
||||||
|
description = "Data from the following cogs will be automatically purged after the specified duration:"
|
||||||
|
for cog_name, days in policies.items():
|
||||||
|
description += f"\n- **{cog_name}**: {days} days"
|
||||||
|
|
||||||
|
embed.description = description
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(DataManager(bot))
|
||||||
|
|
||||||
17
datamanager/info.json
Normal file
17
datamanager/info.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"author": [
|
||||||
|
"unstableCogs"
|
||||||
|
],
|
||||||
|
"install_msg": "Thank you for installing the Data Manager cog! Please use the setup commands to configure your data retention policies.",
|
||||||
|
"name": "DataManager",
|
||||||
|
"short": "A cog to manage long-term data retention.",
|
||||||
|
"description": "Provides tools to automatically purge or archive old data from other cogs to prevent database bloat.",
|
||||||
|
"tags": [
|
||||||
|
"data",
|
||||||
|
"database",
|
||||||
|
"utility",
|
||||||
|
"admin"
|
||||||
|
],
|
||||||
|
"requirements": [],
|
||||||
|
"end_user_data_statement": "This cog reads and deletes data from other cogs based on administrator configuration. It does not store any unique user data itself."
|
||||||
|
}
|
||||||
4
hiring/__init__.py
Normal file
4
hiring/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .hiring import Hiring
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(Hiring(bot))
|
||||||
268
hiring/hiring.py
Normal file
268
hiring/hiring.py
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
import discord
|
||||||
|
from redbot.core import commands, Config, app_commands
|
||||||
|
from typing import Literal, Optional, TYPE_CHECKING, Type
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
|
# --- Modals for the Application Forms ---
|
||||||
|
|
||||||
|
class StaffApplicationModal(discord.ui.Modal, title="Staff Application"):
|
||||||
|
chosen_plan = discord.ui.TextInput(label="What plan are you interested in?")
|
||||||
|
tox_level = discord.ui.TextInput(label="What is your toxicity level tolerance?")
|
||||||
|
describe_server = discord.ui.TextInput(label="Please describe your server.", style=discord.TextStyle.paragraph)
|
||||||
|
server_link = discord.ui.TextInput(label="Server Invite Link")
|
||||||
|
|
||||||
|
def __init__(self, ticket_channel: discord.TextChannel):
|
||||||
|
super().__init__()
|
||||||
|
self.ticket_channel = ticket_channel
|
||||||
|
|
||||||
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
|
cog: Optional["Hiring"] = interaction.client.get_cog("Hiring") # type: ignore
|
||||||
|
if not cog:
|
||||||
|
await interaction.response.send_message("Hiring cog not loaded.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = discord.Embed(title="New Staff Application", color=discord.Color.blue())
|
||||||
|
embed.set_author(name=interaction.user.name, icon_url=interaction.user.display_avatar.url)
|
||||||
|
embed.add_field(name="Chosen Plan", value=self.chosen_plan.value, inline=False)
|
||||||
|
embed.add_field(name="Toxicity Level", value=self.tox_level.value, inline=False)
|
||||||
|
embed.add_field(name="Server Description", value=self.describe_server.value, inline=False)
|
||||||
|
embed.add_field(name="Server Link", value=self.server_link.value, inline=False)
|
||||||
|
await self.ticket_channel.send(embed=embed)
|
||||||
|
await interaction.response.send_message("Your staff application has been submitted.", ephemeral=True)
|
||||||
|
|
||||||
|
await interaction.response.send_message("Your application has been submitted!", ephemeral=True)
|
||||||
|
if isinstance(interaction.channel, discord.TextChannel):
|
||||||
|
await interaction.channel.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
|
class PMApplicationModal(discord.ui.Modal, title="PM Application"):
|
||||||
|
ad = discord.ui.TextInput(label="Your Ad", style=discord.TextStyle.paragraph)
|
||||||
|
reqs = discord.ui.TextInput(label="Your Requirements", style=discord.TextStyle.paragraph)
|
||||||
|
tox_level = discord.ui.TextInput(label="What is your toxicity level tolerance?")
|
||||||
|
chosen_plan = discord.ui.TextInput(label="What plan are you interested in?")
|
||||||
|
|
||||||
|
def __init__(self, ticket_channel: discord.TextChannel):
|
||||||
|
super().__init__()
|
||||||
|
self.ticket_channel = ticket_channel
|
||||||
|
|
||||||
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
|
cog: Optional["Hiring"] = interaction.client.get_cog("Hiring") # type: ignore
|
||||||
|
if not cog:
|
||||||
|
await interaction.response.send_message("Hiring cog not loaded.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = discord.Embed(title="New PM Application", color=discord.Color.green())
|
||||||
|
embed.set_author(name=interaction.user.name, icon_url=interaction.user.display_avatar.url)
|
||||||
|
embed.add_field(name="Ad", value=f"```\n{self.ad.value}\n```", inline=False)
|
||||||
|
embed.add_field(name="Requirements", value=f"```\n{self.reqs.value}\n```", inline=False)
|
||||||
|
embed.add_field(name="Toxicity Level", value=self.tox_level.value, inline=False)
|
||||||
|
embed.add_field(name="Chosen Plan", value=self.chosen_plan.value, inline=False)
|
||||||
|
await self.ticket_channel.send(embed=embed)
|
||||||
|
await interaction.response.send_message("Your PM application has been submitted.", ephemeral=True)
|
||||||
|
|
||||||
|
class HPMApplicationModal(discord.ui.Modal, title="HPM Application"):
|
||||||
|
ad = discord.ui.TextInput(label="Your Ad", style=discord.TextStyle.paragraph)
|
||||||
|
reqs = discord.ui.TextInput(label="Your Requirements", style=discord.TextStyle.paragraph)
|
||||||
|
|
||||||
|
def __init__(self, ticket_channel: discord.TextChannel):
|
||||||
|
super().__init__()
|
||||||
|
self.ticket_channel = ticket_channel
|
||||||
|
|
||||||
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
|
cog: Optional["Hiring"] = interaction.client.get_cog("Hiring") # type: ignore
|
||||||
|
if not cog:
|
||||||
|
await interaction.response.send_message("Hiring cog not loaded.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = discord.Embed(title="New HPM Application", color=discord.Color.purple())
|
||||||
|
embed.set_author(name=interaction.user.name, icon_url=interaction.user.display_avatar.url)
|
||||||
|
embed.add_field(name="Ad", value=f"```\n{self.ad.value}\n```", inline=False)
|
||||||
|
embed.add_field(name="Requirements", value=f"```\n{self.reqs.value}\n```", inline=False)
|
||||||
|
await self.ticket_channel.send(embed=embed)
|
||||||
|
await interaction.response.send_message("Your HPM application has been submitted.", ephemeral=True)
|
||||||
|
|
||||||
|
# --- Views with Buttons ---
|
||||||
|
|
||||||
|
async def create_ticket(interaction: discord.Interaction, ticket_type: str, modal_class: Type[discord.ui.Modal]):
|
||||||
|
"""Helper function to create a ticket, used by multiple views."""
|
||||||
|
cog: Optional["Hiring"] = interaction.client.get_cog("Hiring") # type: ignore
|
||||||
|
if not cog:
|
||||||
|
await interaction.response.send_message("The Hiring cog is not loaded.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
guild = interaction.guild
|
||||||
|
if not guild:
|
||||||
|
await interaction.response.send_message("This action can only be performed in a server.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
category_id_raw = await cog.config.guild(guild).get_raw(f"{ticket_type}_category")
|
||||||
|
if not isinstance(category_id_raw, int):
|
||||||
|
await interaction.response.send_message(f"The category for '{ticket_type}' applications has not been set correctly.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
category = guild.get_channel(category_id_raw)
|
||||||
|
if not isinstance(category, discord.CategoryChannel):
|
||||||
|
await interaction.response.send_message(f"The configured category for '{ticket_type}' is invalid.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
assert isinstance(interaction.user, discord.Member)
|
||||||
|
|
||||||
|
try:
|
||||||
|
thread_name = f"{ticket_type}-application-{interaction.user.name}"
|
||||||
|
overwrites = {
|
||||||
|
guild.default_role: discord.PermissionOverwrite(read_messages=False),
|
||||||
|
interaction.user: discord.PermissionOverwrite(read_messages=True, send_messages=True)
|
||||||
|
}
|
||||||
|
ticket_channel = await category.create_text_channel(name=thread_name, overwrites=overwrites)
|
||||||
|
|
||||||
|
modal_instance = modal_class(ticket_channel) # type: ignore
|
||||||
|
await interaction.response.send_modal(modal_instance)
|
||||||
|
|
||||||
|
async with cog.config.guild(guild).closed_applications() as closed_apps:
|
||||||
|
closed_apps[str(ticket_channel.id)] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||||
|
|
||||||
|
except discord.Forbidden:
|
||||||
|
if not interaction.response.is_done():
|
||||||
|
await interaction.response.send_message("I don't have permission to create channels in that category.", ephemeral=True)
|
||||||
|
except Exception as e:
|
||||||
|
if not interaction.response.is_done():
|
||||||
|
await interaction.response.send_message(f"An unexpected error occurred: {e}", ephemeral=True)
|
||||||
|
|
||||||
|
class HireView(discord.ui.View):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(timeout=None)
|
||||||
|
|
||||||
|
@discord.ui.button(label="Staff", style=discord.ButtonStyle.primary, custom_id="staff_apply_button_persistent")
|
||||||
|
async def staff_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
await create_ticket(interaction, "staff", StaffApplicationModal)
|
||||||
|
|
||||||
|
@discord.ui.button(label="PM", style=discord.ButtonStyle.secondary, custom_id="pm_apply_button_persistent")
|
||||||
|
async def pm_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
await create_ticket(interaction, "pm", PMApplicationModal)
|
||||||
|
|
||||||
|
@discord.ui.button(label="HPM", style=discord.ButtonStyle.success, custom_id="hpm_apply_button_persistent")
|
||||||
|
async def hpm_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
await create_ticket(interaction, "hpm", HPMApplicationModal)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkView(discord.ui.View):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(timeout=None)
|
||||||
|
|
||||||
|
@discord.ui.button(label="Apply for PM Position", style=discord.ButtonStyle.green, custom_id="work_apply_pm_persistent")
|
||||||
|
async def work_apply_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
await create_ticket(interaction, "pm", PMApplicationModal)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Main Cog Class ---
|
||||||
|
|
||||||
|
class Hiring(commands.Cog):
|
||||||
|
"""
|
||||||
|
A cog for handling staff hiring applications.
|
||||||
|
"""
|
||||||
|
def __init__(self, bot: "Red"):
|
||||||
|
self.bot = bot
|
||||||
|
self.config = Config.get_conf(self, identifier=1234567891, force_registration=True)
|
||||||
|
default_guild = {
|
||||||
|
"staff_category": None,
|
||||||
|
"pm_category": None,
|
||||||
|
"hpm_category": None,
|
||||||
|
"work_channel": None,
|
||||||
|
"closed_applications": {}
|
||||||
|
}
|
||||||
|
self.config.register_guild(**default_guild)
|
||||||
|
|
||||||
|
async def cog_load(self):
|
||||||
|
self.bot.add_view(HireView())
|
||||||
|
self.bot.add_view(WorkView())
|
||||||
|
|
||||||
|
@commands.hybrid_command() # type: ignore
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
|
async def hire(self, ctx: commands.Context):
|
||||||
|
"""Sends the hiring application view."""
|
||||||
|
view = HireView()
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Hiring Applications",
|
||||||
|
description="Please select the position you are applying for below.",
|
||||||
|
color=await ctx.embed_color()
|
||||||
|
)
|
||||||
|
await ctx.send(embed=embed, view=view)
|
||||||
|
|
||||||
|
@commands.hybrid_command() # type: ignore
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
|
async def work(self, ctx: commands.Context):
|
||||||
|
"""Posts the persistent PM application button."""
|
||||||
|
guild = ctx.guild
|
||||||
|
if not guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
work_channel_id = await self.config.guild(guild).work_channel()
|
||||||
|
if not work_channel_id:
|
||||||
|
await ctx.send("The work channel has not been set. Please use `/hiringset workchannel`.")
|
||||||
|
return
|
||||||
|
|
||||||
|
channel = guild.get_channel(work_channel_id)
|
||||||
|
if not isinstance(channel, discord.TextChannel):
|
||||||
|
await ctx.send("The configured work channel is invalid.")
|
||||||
|
return
|
||||||
|
|
||||||
|
view = WorkView()
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Partnership Manager Applications",
|
||||||
|
description="Click the button below to apply for a Partnership Manager (PM) position.",
|
||||||
|
color=discord.Color.green()
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await channel.send(embed=embed, view=view)
|
||||||
|
await ctx.send(f"Application message posted in {channel.mention}.", ephemeral=True)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await ctx.send("I don't have permission to send messages in that channel.", ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
@commands.group(aliases=["hset"]) # type: ignore
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
|
async def hiringset(self, ctx: commands.Context):
|
||||||
|
"""Configure Hiring settings."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@hiringset.command(name="staffcategory")
|
||||||
|
async def set_staff_category(self, ctx: commands.Context, category: discord.CategoryChannel):
|
||||||
|
"""Set the category for Staff applications."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).staff_category.set(category.id)
|
||||||
|
await ctx.send(f"Staff application category set to **{category.name}**.")
|
||||||
|
|
||||||
|
@hiringset.command(name="pmcategory")
|
||||||
|
async def set_pm_category(self, ctx: commands.Context, category: discord.CategoryChannel):
|
||||||
|
"""Set the category for PM applications."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).pm_category.set(category.id)
|
||||||
|
await ctx.send(f"PM application category set to **{category.name}**.")
|
||||||
|
|
||||||
|
@hiringset.command(name="hpmcategory")
|
||||||
|
async def set_hpm_category(self, ctx: commands.Context, category: discord.CategoryChannel):
|
||||||
|
"""Set the category for HPM applications."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).hpm_category.set(category.id)
|
||||||
|
await ctx.send(f"HPM application category set to **{category.name}**.")
|
||||||
|
|
||||||
|
@hiringset.command(name="workchannel")
|
||||||
|
async def set_work_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
|
"""Set the channel for the /work command announcements."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).work_channel.set(channel.id)
|
||||||
|
await ctx.send(f"Work announcement channel set to {channel.mention}.")
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(Hiring(bot))
|
||||||
|
|
||||||
15
hiring/info.json
Normal file
15
hiring/info.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"author": [ "unstableCogs" ],
|
||||||
|
"install_msg": "Thank you for installing the Hiring cog! Use `[p]help Hiring` for a list of commands.",
|
||||||
|
"name": "Hiring",
|
||||||
|
"short": "A ticket-based system for staff and PM applications.",
|
||||||
|
"description": "Provides /hire and /work commands to manage the staff and PM hiring process through a ticket system with forms and buttons.",
|
||||||
|
"tags": [
|
||||||
|
"hiring",
|
||||||
|
"tickets",
|
||||||
|
"utility",
|
||||||
|
"modmail"
|
||||||
|
],
|
||||||
|
"requirements": [],
|
||||||
|
"end_user_data_statement": "This cog stores user IDs and the content of their applications for the duration of the hiring process."
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"author" : ["KWUK"],
|
"author" : ["KWUK"],
|
||||||
"install_msg" : "Thank you for installing my repo! If you need support, create an issue on Gitea or ping me in KWDS.",
|
"install_msg" : "Thank you for installing my repo! If you need support, create an issue on Gitea or ping me in KWDS. Cogs: hiring, kbump, kofishop, modmail, mors, pp, rpg, iservice, welcomer. ",
|
||||||
"name" : "unstable-cogs",
|
"name" : "unstable-cogs",
|
||||||
"short" : "Cogs for Red-DiscordBot!",
|
"short" : "Cogs for Red-DiscordBot!",
|
||||||
"description" : "Cogs for Red-DiscordBot!",
|
"description" : "Cogs for Red-DiscordBot!",
|
||||||
|
|||||||
4
kofishop/__init__.py
Normal file
4
kofishop/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .kofishop import KofiShop
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(KofiShop(bot))
|
||||||
178
kofishop/kofishop.py
Normal file
178
kofishop/kofishop.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import discord
|
||||||
|
from redbot.core import commands, Config, app_commands
|
||||||
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
|
# --- Modals for the Forms ---
|
||||||
|
|
||||||
|
class OrderModal(discord.ui.Modal, title="Commission Order Form"):
|
||||||
|
commission_type = discord.ui.TextInput(label="What type of commission?")
|
||||||
|
payment_status = discord.ui.TextInput(label="Is this a Free or Paid commission?")
|
||||||
|
description = discord.ui.TextInput(label="Description", style=discord.TextStyle.paragraph)
|
||||||
|
questions = discord.ui.TextInput(label="Any questions?", style=discord.TextStyle.paragraph, required=False)
|
||||||
|
|
||||||
|
def __init__(self, cog: "KofiShop"):
|
||||||
|
super().__init__()
|
||||||
|
self.cog = cog
|
||||||
|
|
||||||
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
|
guild = interaction.guild
|
||||||
|
if not guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
channel_id = await self.cog.config.guild(guild).order_channel()
|
||||||
|
if not channel_id:
|
||||||
|
await interaction.response.send_message("The order channel has not been set by an admin.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
channel = guild.get_channel(channel_id)
|
||||||
|
if not isinstance(channel, discord.TextChannel):
|
||||||
|
await interaction.response.send_message("The configured order channel is invalid.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = discord.Embed(title=f"New Order from {interaction.user.name}", color=discord.Color.blurple())
|
||||||
|
embed.set_author(name=interaction.user.name, icon_url=interaction.user.display_avatar.url)
|
||||||
|
embed.add_field(name="Commission Type", value=self.commission_type.value, inline=False)
|
||||||
|
embed.add_field(name="Payment Status", value=self.payment_status.value, inline=False)
|
||||||
|
embed.add_field(name="Description", value=self.description.value, inline=False)
|
||||||
|
if self.questions.value:
|
||||||
|
embed.add_field(name="Questions", value=self.questions.value, inline=False)
|
||||||
|
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
await interaction.response.send_message("Your order has been submitted!", ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewModal(discord.ui.Modal, title="Shop Review"):
|
||||||
|
item_name = discord.ui.TextInput(label="What item/commission are you reviewing?")
|
||||||
|
rating = discord.ui.TextInput(label="Rating (out of 10)", max_length=2)
|
||||||
|
review_text = discord.ui.TextInput(label="Your Review", style=discord.TextStyle.paragraph, max_length=1000)
|
||||||
|
|
||||||
|
def __init__(self, cog: "KofiShop"):
|
||||||
|
super().__init__()
|
||||||
|
self.cog = cog
|
||||||
|
|
||||||
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
|
guild = interaction.guild
|
||||||
|
if not guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
channel_id = await self.cog.config.guild(guild).review_channel()
|
||||||
|
if not channel_id:
|
||||||
|
await interaction.response.send_message("The review channel has not been set by an admin.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
channel = guild.get_channel(channel_id)
|
||||||
|
if not isinstance(channel, discord.TextChannel):
|
||||||
|
await interaction.response.send_message("The configured review channel is invalid.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = discord.Embed(title=f"New Review for {self.item_name.value}", color=discord.Color.gold())
|
||||||
|
embed.set_author(name=interaction.user.name, icon_url=interaction.user.display_avatar.url)
|
||||||
|
embed.add_field(name="Rating", value=f"{self.rating.value}/10")
|
||||||
|
embed.add_field(name="Review", value=self.review_text.value, inline=False)
|
||||||
|
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
await interaction.response.send_message("Thank you for your review!", ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Main Cog Class ---
|
||||||
|
|
||||||
|
class KofiShop(commands.Cog):
|
||||||
|
"""
|
||||||
|
An interactive front-end for a Ko-fi store.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot: "Red"):
|
||||||
|
self.bot = bot
|
||||||
|
self.config = Config.get_conf(self, identifier=1234567894, force_registration=True)
|
||||||
|
default_guild = {
|
||||||
|
"order_channel": None,
|
||||||
|
"review_channel": None,
|
||||||
|
"waitlist_channel": None,
|
||||||
|
"waitlist_entries": {} # For DataManager
|
||||||
|
}
|
||||||
|
self.config.register_guild(**default_guild)
|
||||||
|
|
||||||
|
@app_commands.command()
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def order(self, interaction: discord.Interaction):
|
||||||
|
"""Place an order for a commission."""
|
||||||
|
await interaction.response.send_modal(OrderModal(self))
|
||||||
|
|
||||||
|
@app_commands.command(name="rev")
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def review(self, interaction: discord.Interaction):
|
||||||
|
"""Leave a review for a completed commission."""
|
||||||
|
await interaction.response.send_modal(ReviewModal(self))
|
||||||
|
|
||||||
|
@commands.hybrid_command() # type: ignore
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def waitlist(self, ctx: commands.Context, user: Optional[discord.Member], *, item: str):
|
||||||
|
"""Add a user and their requested item to the waitlist."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
target_user = user or ctx.author
|
||||||
|
|
||||||
|
waitlist_channel_id = await self.config.guild(ctx.guild).waitlist_channel()
|
||||||
|
if not waitlist_channel_id:
|
||||||
|
return await ctx.send("The waitlist channel has not been set by an admin.", ephemeral=True)
|
||||||
|
|
||||||
|
waitlist_channel = ctx.guild.get_channel(waitlist_channel_id)
|
||||||
|
if not isinstance(waitlist_channel, discord.TextChannel):
|
||||||
|
return await ctx.send("The configured waitlist channel is invalid.", ephemeral=True)
|
||||||
|
|
||||||
|
message_to_send = f"**{item}** ིྀ {target_user.mention} ✧ in {ctx.channel.mention if isinstance(ctx.channel, discord.TextChannel) else 'this ticket'}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
sent_message = await waitlist_channel.send(message_to_send)
|
||||||
|
# For DataManager
|
||||||
|
async with self.config.guild(ctx.guild).waitlist_entries() as entries:
|
||||||
|
entries[str(sent_message.id)] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||||
|
|
||||||
|
await ctx.send(f"{target_user.mention} has been added to the waitlist for '{item}'.", ephemeral=True)
|
||||||
|
# Also send confirmation to user's ticket if it's a ticket channel
|
||||||
|
if isinstance(ctx.channel, discord.TextChannel) and "ticket" in ctx.channel.name.lower():
|
||||||
|
await ctx.channel.send(f"You have been added to the waitlist for **{item}**.")
|
||||||
|
|
||||||
|
except discord.Forbidden:
|
||||||
|
await ctx.send("I do not have permission to send messages in the waitlist channel.", ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
@commands.group(aliases=["kset"]) # type: ignore
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
|
async def kofiset(self, ctx: commands.Context):
|
||||||
|
"""Configure KofiShop settings."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@kofiset.command(name="orderchannel")
|
||||||
|
async def set_order_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
|
"""Set the channel for new orders."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).order_channel.set(channel.id)
|
||||||
|
await ctx.send(f"Order channel set to {channel.mention}")
|
||||||
|
|
||||||
|
@kofiset.command(name="reviewchannel")
|
||||||
|
async def set_review_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
|
"""Set the channel for new reviews."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).review_channel.set(channel.id)
|
||||||
|
await ctx.send(f"Review channel set to {channel.mention}")
|
||||||
|
|
||||||
|
@kofiset.command(name="waitlistchannel")
|
||||||
|
async def set_waitlist_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
|
"""Set the channel for waitlist notifications."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).waitlist_channel.set(channel.id)
|
||||||
|
await ctx.send(f"Waitlist channel set to {channel.mention}")
|
||||||
|
|
||||||
|
async def setup(bot: "Red"):
|
||||||
|
await bot.add_cog(KofiShop(bot))
|
||||||
|
|
||||||
4
logging/__init__.py
Normal file
4
logging/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .logging import Logging
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(Logging(bot))
|
||||||
17
logging/info.json
Normal file
17
logging/info.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"author": [
|
||||||
|
"unstableCogs"
|
||||||
|
],
|
||||||
|
"install_msg": "Thank you for installing the Logging cog! Please configure a log channel to begin.",
|
||||||
|
"name": "Logging",
|
||||||
|
"short": "A cog for comprehensive server event logging.",
|
||||||
|
"description": "Logs various server events (joins, leaves, message edits/deletes, etc.) to a designated channel for moderation and auditing purposes.",
|
||||||
|
"tags": [
|
||||||
|
"logging",
|
||||||
|
"moderation",
|
||||||
|
"utility",
|
||||||
|
"events"
|
||||||
|
],
|
||||||
|
"requirements": [],
|
||||||
|
"end_user_data_statement": "This cog does not persistently store any user data."
|
||||||
|
}
|
||||||
276
logging/logging.py
Normal file
276
logging/logging.py
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import discord
|
||||||
|
from redbot.core import commands, Config
|
||||||
|
import datetime
|
||||||
|
from typing import Dict, Optional, Union
|
||||||
|
|
||||||
|
class Logging(commands.Cog):
|
||||||
|
"""
|
||||||
|
A cog for comprehensive server event logging.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.config = Config.get_conf(self, identifier=1234567893, force_registration=True)
|
||||||
|
default_guild = {
|
||||||
|
"log_channel": None,
|
||||||
|
"logged_events": {}, # For DataManager
|
||||||
|
"enabled_events": {
|
||||||
|
"on_message_delete": True,
|
||||||
|
"on_message_edit": True,
|
||||||
|
"on_member_join": True,
|
||||||
|
"on_member_remove": True,
|
||||||
|
"on_member_update": True,
|
||||||
|
"on_voice_state_update": True,
|
||||||
|
"on_invite_create": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
self.config.register_guild(**default_guild)
|
||||||
|
|
||||||
|
async def _send_log(self, guild: discord.Guild, embed: discord.Embed, event_name: str):
|
||||||
|
"""Helper function to send logs."""
|
||||||
|
enabled_events = await self.config.guild(guild).enabled_events()
|
||||||
|
if not enabled_events.get(event_name, False):
|
||||||
|
return
|
||||||
|
|
||||||
|
log_channel_id = await self.config.guild(guild).log_channel()
|
||||||
|
if not log_channel_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
log_channel = guild.get_channel(log_channel_id)
|
||||||
|
if isinstance(log_channel, discord.TextChannel):
|
||||||
|
try:
|
||||||
|
log_message = await log_channel.send(embed=embed)
|
||||||
|
if event_name in ["on_message_delete", "on_message_edit"]:
|
||||||
|
async with self.config.guild(guild).logged_events() as events:
|
||||||
|
events[str(log_message.id)] = datetime.datetime.now(
|
||||||
|
datetime.timezone.utc
|
||||||
|
).isoformat()
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass # Bot lacks permissions
|
||||||
|
|
||||||
|
# --- SETTINGS COMMANDS ---
|
||||||
|
@commands.group(aliases=["logset"]) # type: ignore
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
|
async def loggingset(self, ctx: commands.Context):
|
||||||
|
"""Configure logging settings."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@loggingset.command(name="channel")
|
||||||
|
async def set_log_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
|
"""Set the channel where logs will be sent."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).log_channel.set(channel.id)
|
||||||
|
await ctx.send(f"Log channel has been set to {channel.mention}")
|
||||||
|
|
||||||
|
@loggingset.command(name="toggle")
|
||||||
|
async def toggle_log_event(self, ctx: commands.Context, event: str):
|
||||||
|
"""Toggle logging for a specific event."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
valid_events = list((await self.config.guild(ctx.guild).enabled_events()).keys())
|
||||||
|
if event not in valid_events:
|
||||||
|
await ctx.send(f"Invalid event. Valid events are: `{'`, `'.join(valid_events)}`")
|
||||||
|
return
|
||||||
|
|
||||||
|
async with self.config.guild(ctx.guild).enabled_events() as events:
|
||||||
|
current_status = events.get(event, False)
|
||||||
|
events[event] = not current_status
|
||||||
|
new_status = "enabled" if not current_status else "disabled"
|
||||||
|
await ctx.send(f"Logging for `{event}` has been {new_status}.")
|
||||||
|
|
||||||
|
# --- EVENT LISTENERS ---
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_message_delete(self, message: discord.Message):
|
||||||
|
"""Log when a message is deleted."""
|
||||||
|
if message.author.bot or not message.guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
location = ""
|
||||||
|
if isinstance(message.channel, (discord.TextChannel, discord.Thread)):
|
||||||
|
location = message.channel.mention
|
||||||
|
elif isinstance(message.channel, discord.DMChannel):
|
||||||
|
location = f"DM with {message.channel.recipient}"
|
||||||
|
elif isinstance(message.channel, discord.GroupChannel):
|
||||||
|
location = f"Group DM ({message.channel.id})"
|
||||||
|
else:
|
||||||
|
location = f"Channel ID: {message.channel.id}"
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
description=f"**Message deleted in {location}**\n{message.content or 'No text content.'}",
|
||||||
|
color=discord.Color.red(),
|
||||||
|
timestamp=datetime.datetime.now(datetime.timezone.utc),
|
||||||
|
)
|
||||||
|
embed.set_author(
|
||||||
|
name=f"{message.author.name} ({message.author.id})",
|
||||||
|
icon_url=message.author.display_avatar.url,
|
||||||
|
)
|
||||||
|
await self._send_log(message.guild, embed, "on_message_delete")
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_message_edit(self, before: discord.Message, after: discord.Message):
|
||||||
|
"""Log when a message is edited."""
|
||||||
|
if before.author.bot or not before.guild or before.content == after.content:
|
||||||
|
return
|
||||||
|
|
||||||
|
location = ""
|
||||||
|
if isinstance(before.channel, (discord.TextChannel, discord.Thread)):
|
||||||
|
location = before.channel.mention
|
||||||
|
elif isinstance(before.channel, discord.DMChannel):
|
||||||
|
location = f"DM with {before.channel.recipient}"
|
||||||
|
elif isinstance(before.channel, discord.GroupChannel):
|
||||||
|
location = f"Group DM ({before.channel.id})"
|
||||||
|
else:
|
||||||
|
location = f"Channel ID: {before.channel.id}"
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
description=f"**Message edited in {location}** [Jump to Message]({after.jump_url})",
|
||||||
|
color=discord.Color.orange(),
|
||||||
|
timestamp=datetime.datetime.now(datetime.timezone.utc),
|
||||||
|
)
|
||||||
|
embed.set_author(
|
||||||
|
name=f"{before.author.name} ({before.author.id})",
|
||||||
|
icon_url=before.author.display_avatar.url,
|
||||||
|
)
|
||||||
|
embed.add_field(name="Before", value=before.content or "Empty", inline=False)
|
||||||
|
embed.add_field(name="After", value=after.content or "Empty", inline=False)
|
||||||
|
await self._send_log(before.guild, embed, "on_message_edit")
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_member_join(self, member: discord.Member):
|
||||||
|
"""Log when a user joins the server."""
|
||||||
|
embed = discord.Embed(
|
||||||
|
description=f"**{member.mention} has joined the server.**",
|
||||||
|
color=discord.Color.green(),
|
||||||
|
timestamp=datetime.datetime.now(datetime.timezone.utc),
|
||||||
|
)
|
||||||
|
embed.set_author(
|
||||||
|
name=f"{member.name} ({member.id})", icon_url=member.display_avatar.url
|
||||||
|
)
|
||||||
|
await self._send_log(member.guild, embed, "on_member_join")
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_member_remove(self, member: discord.Member):
|
||||||
|
"""Log when a user leaves the server."""
|
||||||
|
embed = discord.Embed(
|
||||||
|
description=f"**{member.mention} has left the server.**",
|
||||||
|
color=discord.Color.dark_red(),
|
||||||
|
timestamp=datetime.datetime.now(datetime.timezone.utc),
|
||||||
|
)
|
||||||
|
embed.set_author(
|
||||||
|
name=f"{member.name} ({member.id})", icon_url=member.display_avatar.url
|
||||||
|
)
|
||||||
|
await self._send_log(member.guild, embed, "on_member_remove")
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
||||||
|
"""Log when a member's roles or nickname changes."""
|
||||||
|
guild = after.guild
|
||||||
|
if before.nick != after.nick:
|
||||||
|
embed = discord.Embed(
|
||||||
|
description=f"**{after.mention} changed their nickname.**",
|
||||||
|
color=discord.Color.blue(),
|
||||||
|
timestamp=datetime.datetime.now(datetime.timezone.utc),
|
||||||
|
)
|
||||||
|
embed.set_author(
|
||||||
|
name=f"{after.name} ({after.id})", icon_url=after.display_avatar.url
|
||||||
|
)
|
||||||
|
embed.add_field(name="Before", value=before.nick or "None", inline=False)
|
||||||
|
embed.add_field(name="After", value=after.nick or "None", inline=False)
|
||||||
|
await self._send_log(guild, embed, "on_member_update")
|
||||||
|
|
||||||
|
if before.roles != after.roles:
|
||||||
|
added_roles = [r.mention for r in after.roles if r not in before.roles]
|
||||||
|
removed_roles = [r.mention for r in before.roles if r not in after.roles]
|
||||||
|
if added_roles or removed_roles:
|
||||||
|
embed = discord.Embed(
|
||||||
|
description=f"**{after.mention}'s roles were updated.**",
|
||||||
|
color=discord.Color.blue(),
|
||||||
|
timestamp=datetime.datetime.now(datetime.timezone.utc),
|
||||||
|
)
|
||||||
|
embed.set_author(
|
||||||
|
name=f"{after.name} ({after.id})",
|
||||||
|
icon_url=after.display_avatar.url,
|
||||||
|
)
|
||||||
|
if added_roles:
|
||||||
|
embed.add_field(
|
||||||
|
name="Added Roles", value=", ".join(added_roles), inline=False
|
||||||
|
)
|
||||||
|
if removed_roles:
|
||||||
|
embed.add_field(
|
||||||
|
name="Removed Roles",
|
||||||
|
value=", ".join(removed_roles),
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
await self._send_log(guild, embed, "on_member_update")
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_voice_state_update(
|
||||||
|
self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState
|
||||||
|
):
|
||||||
|
"""Log when a user's voice state changes."""
|
||||||
|
if before.channel == after.channel:
|
||||||
|
return # Ignore mutes, deafens, etc. for now
|
||||||
|
|
||||||
|
guild = member.guild
|
||||||
|
action = ""
|
||||||
|
if before.channel and not after.channel:
|
||||||
|
action = f"left voice channel {before.channel.mention}."
|
||||||
|
elif not before.channel and after.channel:
|
||||||
|
action = f"joined voice channel {after.channel.mention}."
|
||||||
|
elif before.channel and after.channel:
|
||||||
|
action = f"moved from {before.channel.mention} to {after.channel.mention}."
|
||||||
|
|
||||||
|
if action:
|
||||||
|
embed = discord.Embed(
|
||||||
|
description=f"**{member.mention} {action}**",
|
||||||
|
color=discord.Color.light_grey(),
|
||||||
|
timestamp=datetime.datetime.now(datetime.timezone.utc),
|
||||||
|
)
|
||||||
|
embed.set_author(
|
||||||
|
name=f"{member.name} ({member.id})", icon_url=member.display_avatar.url
|
||||||
|
)
|
||||||
|
await self._send_log(guild, embed, "on_voice_state_update")
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_invite_create(self, invite: discord.Invite):
|
||||||
|
"""Log when an invite is created."""
|
||||||
|
guild = invite.guild
|
||||||
|
if not guild or not isinstance(guild, discord.Guild):
|
||||||
|
return
|
||||||
|
|
||||||
|
inviter_str = "Unknown User"
|
||||||
|
if invite.inviter and isinstance(invite.inviter, (discord.User, discord.Member)):
|
||||||
|
inviter_str = invite.inviter.mention
|
||||||
|
elif invite.inviter:
|
||||||
|
inviter_str = f"User ({invite.inviter.id})"
|
||||||
|
|
||||||
|
channel_str = "Unknown Channel"
|
||||||
|
if invite.channel and isinstance(invite.channel, (discord.abc.GuildChannel)):
|
||||||
|
if hasattr(invite.channel, 'mention'):
|
||||||
|
channel_str = invite.channel.mention
|
||||||
|
else:
|
||||||
|
channel_str = f"`{invite.channel.name}`"
|
||||||
|
elif invite.channel:
|
||||||
|
channel_str = f"Channel ID: {invite.channel.id}"
|
||||||
|
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Invite Created",
|
||||||
|
description=f"Invite `{invite.code}` to {channel_str} created by {inviter_str}.",
|
||||||
|
color=discord.Color.teal(),
|
||||||
|
timestamp=datetime.datetime.now(datetime.timezone.utc),
|
||||||
|
)
|
||||||
|
embed.add_field(name="Max Uses", value=invite.max_uses or "Unlimited")
|
||||||
|
embed.add_field(
|
||||||
|
name="Max Age",
|
||||||
|
value=f"{invite.max_age // 3600} hours" if invite.max_age else "Permanent",
|
||||||
|
)
|
||||||
|
await self._send_log(guild, embed, "on_invite_create")
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(Logging(bot))
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from .modmail import Modmail
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(Modmail(bot))
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import discord
|
||||||
|
import datetime
|
||||||
|
from redbot.core import commands, Config, app_commands
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class Modmail(commands.Cog):
|
||||||
|
"""
|
||||||
|
A private, forum-based ModMail system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.config = Config.get_conf(self, identifier=1234567890, force_registration=True)
|
||||||
|
default_guild = {
|
||||||
|
"forum_channel": None,
|
||||||
|
"enabled": False,
|
||||||
|
"active_threads": {},
|
||||||
|
"closed_threads": {} # NEW: To log closed tickets for purging
|
||||||
|
}
|
||||||
|
self.config.register_guild(**default_guild)
|
||||||
|
|
||||||
|
# ... existing on_message listener ...
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_message(self, message: discord.Message):
|
||||||
|
if message.author.bot:
|
||||||
|
return
|
||||||
|
|
||||||
|
# --- User to Staff DM Logic ---
|
||||||
|
if isinstance(message.channel, discord.DMChannel):
|
||||||
|
# ... existing user DM logic ...
|
||||||
|
pass
|
||||||
|
|
||||||
|
# --- Staff to User Reply Logic ---
|
||||||
|
elif isinstance(message.channel, discord.Thread):
|
||||||
|
# ... existing staff reply logic ...
|
||||||
|
pass
|
||||||
|
|
||||||
|
@app_commands.command(name="close")
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def modmail_close(self, interaction: discord.Interaction, *, reason: Optional[str] = "No reason provided."):
|
||||||
|
"""Close the current ModMail ticket."""
|
||||||
|
guild = interaction.guild
|
||||||
|
if not guild:
|
||||||
|
await interaction.response.send_message("This command can only be used in a server.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# NEW: Safely check if the interaction has a channel
|
||||||
|
if not interaction.channel:
|
||||||
|
await interaction.response.send_message("This command cannot be used in this context.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# ... existing logic to check if it's a modmail thread ...
|
||||||
|
|
||||||
|
thread_id_str = str(interaction.channel.id)
|
||||||
|
async with self.config.guild(guild).active_threads() as active_threads:
|
||||||
|
user_id = active_threads.pop(thread_id_str, None)
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
# NEW: Log the closed thread with a timestamp
|
||||||
|
async with self.config.guild(guild).closed_threads() as closed_threads:
|
||||||
|
closed_threads[thread_id_str] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||||
|
|
||||||
|
# ... existing logic to send final message and archive thread ...
|
||||||
|
user = self.bot.get_user(int(user_id))
|
||||||
|
if user:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Ticket Closed",
|
||||||
|
description=f"Your ModMail ticket has been closed.\n**Reason:** {reason}",
|
||||||
|
color=0x8b9ed7 # Pastel Blue
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await user.send(embed=embed)
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await interaction.response.send_message("Ticket has been closed and archived.", ephemeral=True)
|
||||||
|
if isinstance(interaction.channel, discord.Thread):
|
||||||
|
await interaction.channel.edit(archived=True, locked=True)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message("This does not appear to be an active ModMail ticket.", ephemeral=True)
|
||||||
|
|
||||||
|
@commands.group(aliases=["mmset"]) # type: ignore
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
|
async def modmailset(self, ctx: commands.Context):
|
||||||
|
"""Configure ModMail settings."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ... existing modmailset subcommands ...
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from .mors import MORS
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(MORS(bot))
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
"author": [ "unstableCogs" ],
|
"author": [
|
||||||
"install_msg": "Thank you for installing the Mass Outreach & Review System! For a full command list, please see the wiki.",
|
"unstableCogs"
|
||||||
"name": "MassOutreach",
|
],
|
||||||
|
"install_msg": "Thank you for installing the Mass Outreach & Review System cog!",
|
||||||
|
"name": "MORS",
|
||||||
"short": "A multi-step system for mass outreach and reviews.",
|
"short": "A multi-step system for mass outreach and reviews.",
|
||||||
"description": "Manages a complete workflow for user outreach. The process begins with /game, uses /over for time selection, and concludes with a /pudding-head command for the user to submit a mass review.",
|
"description": "Manages a complete workflow for user outreach, including ad submission, time selection, and final reviews.",
|
||||||
"tags": [
|
"tags": [
|
||||||
"mass",
|
"mass",
|
||||||
"outreach",
|
"outreach",
|
||||||
@@ -11,6 +13,6 @@
|
|||||||
"tickets",
|
"tickets",
|
||||||
"utility"
|
"utility"
|
||||||
],
|
],
|
||||||
"requirements": [ "rich" ],
|
"requirements": [],
|
||||||
"end_user_data_statement": "This cog stores user IDs and ticket information temporarily for the duration of the outreach process. Submitted reviews are stored persistently."
|
"end_user_data_statement": "This cog stores user IDs, ticket information, and submitted reviews persistently for record-keeping purposes."
|
||||||
}
|
}
|
||||||
444
mors/mors.py
444
mors/mors.py
@@ -0,0 +1,444 @@
|
|||||||
|
import discord
|
||||||
|
import asyncio
|
||||||
|
from redbot.core import commands, Config, app_commands
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
|
# --- UI Components ---
|
||||||
|
|
||||||
|
class AdSubmissionModal(discord.ui.Modal, title="MORS Ad Submission"):
|
||||||
|
ad_text = discord.ui.TextInput(
|
||||||
|
label="Your Server Advertisement",
|
||||||
|
style=discord.TextStyle.paragraph,
|
||||||
|
placeholder="Please enter the full text of your ad here.",
|
||||||
|
required=True,
|
||||||
|
max_length=2000,
|
||||||
|
)
|
||||||
|
ad_link = discord.ui.TextInput(
|
||||||
|
label="Your Server Invite Link",
|
||||||
|
placeholder="https://discord.gg/your-invite",
|
||||||
|
required=True,
|
||||||
|
max_length=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, cog: "MORS"):
|
||||||
|
super().__init__()
|
||||||
|
self.cog = cog
|
||||||
|
|
||||||
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
|
guild = interaction.guild
|
||||||
|
if not guild:
|
||||||
|
await interaction.response.send_message("Something went wrong.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
conf = self.cog.config.guild(guild)
|
||||||
|
ad_channel_id = await conf.ad_channel()
|
||||||
|
|
||||||
|
if not ad_channel_id:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"The MORS system has not been fully configured by an administrator.",
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
ad_channel = guild.get_channel(ad_channel_id)
|
||||||
|
|
||||||
|
if not isinstance(ad_channel, discord.TextChannel):
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"The configured ad channel is invalid. Please contact an administrator.",
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
thread = await ad_channel.create_thread(
|
||||||
|
name=f"mors-ticket-{interaction.user.name}",
|
||||||
|
type=discord.ChannelType.private_thread
|
||||||
|
)
|
||||||
|
await thread.add_user(interaction.user)
|
||||||
|
except (discord.Forbidden, discord.HTTPException):
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"I don't have permissions to create private threads in the ad channel.",
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await thread.send(f"Welcome {interaction.user.mention}! Your MORS ticket has been created and is awaiting admin approval.")
|
||||||
|
await thread.send(f"For easy access on mobile, your thread ID is: `{thread.id}`")
|
||||||
|
|
||||||
|
async with conf.active_tickets() as active_tickets:
|
||||||
|
active_tickets[str(interaction.user.id)] = {
|
||||||
|
"thread_id": thread.id,
|
||||||
|
"ad_text": self.ad_text.value,
|
||||||
|
"ad_link": self.ad_link.value,
|
||||||
|
"status": "pending_approval"
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.response.send_message("Your submission has been received and is awaiting approval!", ephemeral=True)
|
||||||
|
|
||||||
|
class TimeSelect(discord.ui.Select):
|
||||||
|
def __init__(self, cog: "MORS"):
|
||||||
|
self.cog = cog
|
||||||
|
options = [
|
||||||
|
discord.SelectOption(label="30 Minutes", value="30m"),
|
||||||
|
discord.SelectOption(label="1 Hour", value="1h"),
|
||||||
|
discord.SelectOption(label="2 Hours", value="2h"),
|
||||||
|
discord.SelectOption(label="3 Hours", value="3h"),
|
||||||
|
discord.SelectOption(label="4 Hours (Bypass Only)", value="4h"),
|
||||||
|
discord.SelectOption(label="Once a Week (Bypass Only)", value="ovn"),
|
||||||
|
]
|
||||||
|
super().__init__(placeholder="Select the advertisement duration...", min_values=1, max_values=1, options=options)
|
||||||
|
|
||||||
|
async def callback(self, interaction: discord.Interaction):
|
||||||
|
guild = interaction.guild
|
||||||
|
if not guild or not isinstance(interaction.user, discord.Member):
|
||||||
|
await interaction.response.send_message("An error occurred.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
member = interaction.user
|
||||||
|
selected_time = self.values[0]
|
||||||
|
conf = self.cog.config.guild(guild)
|
||||||
|
|
||||||
|
# Logic for bypass role and cooldowns will be added here
|
||||||
|
bypass_role_id = await conf.bypass_role()
|
||||||
|
has_bypass = False
|
||||||
|
if bypass_role_id:
|
||||||
|
bypass_role = guild.get_role(bypass_role_id)
|
||||||
|
if bypass_role and bypass_role in member.roles:
|
||||||
|
has_bypass = True
|
||||||
|
|
||||||
|
if selected_time in ["4h", "ovn"] and not has_bypass:
|
||||||
|
await interaction.response.send_message("You do not have the required role for this duration.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
async with conf.active_tickets() as active_tickets:
|
||||||
|
user_id_str = str(interaction.user.id)
|
||||||
|
if user_id_str not in active_tickets:
|
||||||
|
await interaction.response.send_message("You don't have an active MORS ticket.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
thread_id = active_tickets[user_id_str]["thread_id"]
|
||||||
|
thread = guild.get_thread(thread_id)
|
||||||
|
|
||||||
|
if not thread:
|
||||||
|
await interaction.response.send_message("Could not find your ticket thread.", ephemeral=True)
|
||||||
|
del active_tickets[user_id_str]
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await thread.edit(name=f"n2p-{selected_time}-{interaction.user.name}")
|
||||||
|
# Logic to add the role will go here once the role ID is configured
|
||||||
|
except discord.Forbidden:
|
||||||
|
await interaction.response.send_message("I don't have permission to rename your ticket thread.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Send confirmation messages
|
||||||
|
queue_channel_id = await conf.queue_channel()
|
||||||
|
if queue_channel_id:
|
||||||
|
queue_channel = guild.get_channel(queue_channel_id)
|
||||||
|
if isinstance(queue_channel, discord.TextChannel):
|
||||||
|
queue_link = f"https://discord.com/channels/{guild.id}/{queue_channel.id}"
|
||||||
|
await queue_channel.send(f"{thread.mention} :mooncatblue: {interaction.user.mention} queued for __{selected_time}__ *!*")
|
||||||
|
await thread.send(f":81407babybluebutterfly: you're in the [queue]({queue_link}) *!* \n-# **p.s using invites command is __required__ for sep to state invites. no reply to pings within 3 days or inv cmnd done will get you suspended from massing.**")
|
||||||
|
|
||||||
|
await interaction.response.send_message(f"You have selected: {selected_time}", ephemeral=True)
|
||||||
|
if self.view:
|
||||||
|
self.view.stop()
|
||||||
|
|
||||||
|
|
||||||
|
class TimeSelectionView(discord.ui.View):
|
||||||
|
def __init__(self, cog: "MORS"):
|
||||||
|
super().__init__(timeout=180)
|
||||||
|
self.add_item(TimeSelect(cog))
|
||||||
|
|
||||||
|
class ReviewModal(discord.ui.Modal, title="MORS Review Submission"):
|
||||||
|
service_type = discord.ui.TextInput(label="What service type did you use?")
|
||||||
|
rating = discord.ui.TextInput(label="Rating (1-10)", max_length=2)
|
||||||
|
review_text = discord.ui.TextInput(label="Your Review", style=discord.TextStyle.paragraph, max_length=1500)
|
||||||
|
toxicity = discord.ui.TextInput(label="Toxicity Level (ntox/stox)")
|
||||||
|
|
||||||
|
def __init__(self, cog: "MORS"):
|
||||||
|
super().__init__()
|
||||||
|
self.cog = cog
|
||||||
|
|
||||||
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
|
guild = interaction.guild
|
||||||
|
if not guild:
|
||||||
|
await interaction.response.send_message("An error occurred.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
review_channel_id = await self.cog.config.guild(guild).review_channel()
|
||||||
|
if not review_channel_id:
|
||||||
|
await interaction.response.send_message("The review channel has not been configured.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
review_channel = guild.get_channel(review_channel_id)
|
||||||
|
if not isinstance(review_channel, discord.TextChannel):
|
||||||
|
await interaction.response.send_message("The configured review channel is invalid.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="New MORS Review",
|
||||||
|
color=discord.Color.green()
|
||||||
|
)
|
||||||
|
embed.set_author(name=interaction.user.name, icon_url=interaction.user.display_avatar.url)
|
||||||
|
embed.add_field(name="Service Type", value=self.service_type.value, inline=False)
|
||||||
|
embed.add_field(name="Rating", value=f"{self.rating.value}/10", inline=True)
|
||||||
|
embed.add_field(name="Toxicity", value=self.toxicity.value, inline=True)
|
||||||
|
embed.add_field(name="Review", value=self.review_text.value, inline=False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await review_channel.send(embed=embed)
|
||||||
|
except discord.Forbidden:
|
||||||
|
# This would only happen if permissions changed mid-process
|
||||||
|
pass
|
||||||
|
|
||||||
|
await interaction.response.send_message("Your review has been submitted! This ticket will close shortly.", ephemeral=True)
|
||||||
|
|
||||||
|
class MORS(commands.Cog):
|
||||||
|
"""
|
||||||
|
Mass Outreach & Review System
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot: "Red"):
|
||||||
|
self.bot = bot
|
||||||
|
self.config = Config.get_conf(self, identifier=1234567894, force_registration=True)
|
||||||
|
default_guild = {
|
||||||
|
"ad_channel": None,
|
||||||
|
"review_channel": None,
|
||||||
|
"done_channel": None,
|
||||||
|
"queue_channel": None, # Added for queue messages
|
||||||
|
"access_role": None,
|
||||||
|
"bypass_role": None,
|
||||||
|
"active_tickets": {}
|
||||||
|
}
|
||||||
|
self.config.register_guild(**default_guild)
|
||||||
|
|
||||||
|
# --- User Commands ---
|
||||||
|
@app_commands.command()
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def game(self, interaction: discord.Interaction):
|
||||||
|
"""Start the mass outreach process by submitting an ad."""
|
||||||
|
guild = interaction.guild
|
||||||
|
if not guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not isinstance(interaction.user, discord.Member):
|
||||||
|
await interaction.response.send_message("This command can only be used by server members.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
member = interaction.user
|
||||||
|
access_role_id = await self.config.guild(guild).access_role()
|
||||||
|
if access_role_id:
|
||||||
|
access_role = guild.get_role(access_role_id)
|
||||||
|
if access_role and access_role not in member.roles:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"You need the {access_role.name} role to use this command.",
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
modal = AdSubmissionModal(cog=self)
|
||||||
|
await interaction.response.send_modal(modal)
|
||||||
|
|
||||||
|
@app_commands.command()
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def over(self, interaction: discord.Interaction):
|
||||||
|
"""Select your advertisement duration."""
|
||||||
|
guild = interaction.guild
|
||||||
|
if not guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
active_tickets = await self.config.guild(guild).active_tickets()
|
||||||
|
if str(interaction.user.id) not in active_tickets:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"You need to start the process with `/game` before you can use this command.",
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
view = TimeSelectionView(cog=self)
|
||||||
|
await interaction.response.send_message("Please select your desired ad duration from the dropdown below:", view=view, ephemeral=True)
|
||||||
|
|
||||||
|
@app_commands.command(name="pudding-head")
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def pudding_head(self, interaction: discord.Interaction):
|
||||||
|
"""Submit your final review for the MORS process."""
|
||||||
|
guild = interaction.guild
|
||||||
|
if not guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
active_tickets = await self.config.guild(guild).active_tickets()
|
||||||
|
if str(interaction.user.id) not in active_tickets:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"You do not have an active MORS ticket to review.",
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
modal = ReviewModal(cog=self)
|
||||||
|
await interaction.response.send_modal(modal)
|
||||||
|
|
||||||
|
# --- Admin Commands ---
|
||||||
|
@app_commands.command()
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(member="The user whose ad you want to post.")
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def posted(self, interaction: discord.Interaction, member: discord.Member):
|
||||||
|
"""Approves and posts a user's submitted ad."""
|
||||||
|
guild = interaction.guild
|
||||||
|
if not guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
conf = self.config.guild(guild)
|
||||||
|
ad_channel_id = await conf.ad_channel()
|
||||||
|
if not ad_channel_id:
|
||||||
|
await interaction.response.send_message("Ad channel not configured.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
ad_channel = guild.get_channel(ad_channel_id)
|
||||||
|
if not isinstance(ad_channel, discord.TextChannel):
|
||||||
|
await interaction.response.send_message("Configured ad channel is invalid.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
async with conf.active_tickets() as active_tickets:
|
||||||
|
user_id_str = str(member.id)
|
||||||
|
if user_id_str not in active_tickets:
|
||||||
|
await interaction.response.send_message(f"{member.name} has no pending ad.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
ticket_data = active_tickets[user_id_str]
|
||||||
|
ad_embed = discord.Embed(
|
||||||
|
title="New Server Advertisement",
|
||||||
|
description=ticket_data.get("ad_text", "No ad text provided."),
|
||||||
|
color=discord.Color.blue()
|
||||||
|
)
|
||||||
|
ad_embed.add_field(name="Invite Link", value=ticket_data.get("ad_link", "No link provided."))
|
||||||
|
ad_embed.set_author(name=member.name, icon_url=member.display_avatar.url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ad_message = await ad_channel.send(embed=ad_embed)
|
||||||
|
await ad_message.pin()
|
||||||
|
active_tickets[user_id_str]["status"] = "posted"
|
||||||
|
except discord.Forbidden:
|
||||||
|
await interaction.response.send_message("I lack permissions to post or pin in the ad channel.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
await interaction.response.send_message(f"Ad for {member.name} has been posted and pinned. Remember to set a reminder.", ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
@app_commands.command()
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(member="The user whose ticket you want to finalize.")
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def done(self, interaction: discord.Interaction, member: discord.Member):
|
||||||
|
"""Admin command to finalize and close a MORS ticket."""
|
||||||
|
guild = interaction.guild
|
||||||
|
if not guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
conf = self.config.guild(guild)
|
||||||
|
done_channel_id = await conf.done_channel()
|
||||||
|
if not done_channel_id:
|
||||||
|
await interaction.response.send_message("The 'done' channel is not configured.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
done_channel = guild.get_channel(done_channel_id)
|
||||||
|
if not isinstance(done_channel, discord.TextChannel):
|
||||||
|
await interaction.response.send_message("The configured 'done' channel is invalid.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
async with conf.active_tickets() as active_tickets:
|
||||||
|
user_id_str = str(member.id)
|
||||||
|
if user_id_str not in active_tickets:
|
||||||
|
await interaction.response.send_message(f"{member.name} does not have an active MORS ticket.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
ticket_data = active_tickets.pop(user_id_str) # Remove ticket from active list
|
||||||
|
thread_id = ticket_data.get("thread_id")
|
||||||
|
ad_link = ticket_data.get("ad_link", "Not available")
|
||||||
|
|
||||||
|
thread = guild.get_thread(thread_id) if thread_id else None
|
||||||
|
|
||||||
|
# Send embed to done channel
|
||||||
|
done_embed = discord.Embed(
|
||||||
|
description=f"Advertisement period is complete for {member.mention}.",
|
||||||
|
color=discord.Color.teal()
|
||||||
|
)
|
||||||
|
done_embed.add_field(name="Server Ad Link", value=ad_link, inline=False)
|
||||||
|
# Client mentioned "image", will need clarification on what image to include.
|
||||||
|
|
||||||
|
await done_channel.send(embed=done_embed)
|
||||||
|
|
||||||
|
if thread:
|
||||||
|
await thread.send(f"{member.mention}, your MORS process is complete! Please submit your review with `/pudding-head`.")
|
||||||
|
await thread.send("This ticket will be archived in 60 seconds.")
|
||||||
|
await interaction.response.send_message(f"Finalizing ticket for {member.name}. The thread will be archived in 60 seconds.", ephemeral=True)
|
||||||
|
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
try:
|
||||||
|
await thread.edit(archived=True, locked=True)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await thread.send("I don't have permission to archive this thread.")
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(f"Finalizing ticket for {member.name}. Could not find original thread to archive.", ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Settings Commands ---
|
||||||
|
@commands.group() # type: ignore
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
|
async def morsset(self, ctx: commands.Context):
|
||||||
|
"""Configure MORS settings."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@morsset.command(name="adchannel")
|
||||||
|
async def set_ad_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
|
"""Set the channel where ads are posted."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).ad_channel.set(channel.id)
|
||||||
|
await ctx.send(f"Ad channel has been set to {channel.mention}")
|
||||||
|
|
||||||
|
@morsset.command(name="reviewchannel")
|
||||||
|
async def set_review_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
|
"""Set the channel where final reviews are sent."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).review_channel.set(channel.id)
|
||||||
|
await ctx.send(f"Review channel has been set to {channel.mention}")
|
||||||
|
|
||||||
|
@morsset.command(name="donechannel")
|
||||||
|
async def set_done_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
|
"""Set the channel for the /done command embeds."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).done_channel.set(channel.id)
|
||||||
|
await ctx.send(f"Done channel has been set to {channel.mention}")
|
||||||
|
|
||||||
|
@morsset.command(name="queuechannel")
|
||||||
|
async def set_queue_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
|
"""Set the channel for queue confirmation messages."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).queue_channel.set(channel.id)
|
||||||
|
await ctx.send(f"Queue channel has been set to {channel.mention}")
|
||||||
|
|
||||||
|
@morsset.command(name="accessrole")
|
||||||
|
async def set_access_role(self, ctx: commands.Context, role: discord.Role):
|
||||||
|
"""Set the role required to use the /game command."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).access_role.set(role.id)
|
||||||
|
await ctx.send(f"Access role has been set to {role.name}")
|
||||||
|
|
||||||
|
@morsset.command(name="bypassrole")
|
||||||
|
async def set_bypass_role(self, ctx: commands.Context, role: discord.Role):
|
||||||
|
"""Set the bypass role for MORS cooldowns."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).bypass_role.set(role.id)
|
||||||
|
await ctx.send(f"Bypass role has been set to {role.name}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"author": ["kitsunic"],
|
"author": [ "kitsunic" ],
|
||||||
|
"name": "PP",
|
||||||
"description": "A cog for password protected channels",
|
"description": "A cog for password protected channels",
|
||||||
"end_user_data_statement": "This cog stores user, avatar, and guild data. A simple delete request to bot to remove your data or guild data",
|
"end_user_data_statement": "This cog stores user, avatar, and guild data. A simple delete request to bot to remove your data or guild data",
|
||||||
"install_msg": "Thanks for installing & testing.",
|
"install_msg": "Thanks for installing & testing.",
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
{
|
{
|
||||||
"author" : ["UnstableKitsune (unstablekitsune)"],
|
"author": [
|
||||||
"install _msg" : "Oh you installed me! How dare you obtain me! return me right now!",
|
"UnstableKitsune (unstablekitsune)"
|
||||||
"name" : "RPG",
|
],
|
||||||
"short" : "Its a TTRPG",
|
"install_msg": "Thank you for installing the RPG cog! Get ready for an adventure. Use the help command to see available actions.",
|
||||||
"requirements" : [""],
|
"name": "RPG",
|
||||||
"permissions" : [""],
|
"short": "A text-based tabletop role-playing game cog.",
|
||||||
"tags" : [""],
|
"description": "A comprehensive TTRPG system allowing users to create characters, manage inventory, go on adventures, and interact with a game world, all within Discord.",
|
||||||
"min_python_version" : [3, 1, 1],
|
"requirements": [],
|
||||||
"end_user_data_statement" : ""
|
"permissions": [],
|
||||||
|
"tags": [
|
||||||
|
"rpg",
|
||||||
|
"game",
|
||||||
|
"ttrpg",
|
||||||
|
"fun",
|
||||||
|
"adventure"
|
||||||
|
],
|
||||||
|
"min_python_version": [
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"end_user_data_statement": "This cog stores user data persistently. This includes character sheets (stats, inventory, etc.) and game progress tied to your Discord User ID."
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from .servicereview import ServiceReview
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(ServiceReview(bot))
|
||||||
116
servicereview/servicereview.py
Normal file
116
servicereview/servicereview.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import discord
|
||||||
|
from redbot.core import commands, Config, app_commands
|
||||||
|
from typing import Optional
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
# --- Modal for the Review Form ---
|
||||||
|
|
||||||
|
class ReviewModal(discord.ui.Modal, title="Service Review"):
|
||||||
|
service_type = discord.ui.TextInput(
|
||||||
|
label="Service Type",
|
||||||
|
placeholder="e.g., HPM, Staff, PM, Commission, etc."
|
||||||
|
)
|
||||||
|
rating = discord.ui.TextInput(
|
||||||
|
label="Rating (1-10)",
|
||||||
|
placeholder="Please enter a number from 1 to 10.",
|
||||||
|
max_length=2
|
||||||
|
)
|
||||||
|
review_text = discord.ui.TextInput(
|
||||||
|
label="Your Review",
|
||||||
|
style=discord.TextStyle.paragraph,
|
||||||
|
placeholder="Please provide details about your experience.",
|
||||||
|
max_length=1500
|
||||||
|
)
|
||||||
|
toxicity = discord.ui.TextInput(
|
||||||
|
label="Toxicity Level (ntox/stox)",
|
||||||
|
placeholder="ntox (non-toxic) or stox (semi-toxic)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, cog: "ServiceReview"):
|
||||||
|
super().__init__()
|
||||||
|
self.cog = cog
|
||||||
|
|
||||||
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
|
guild = interaction.guild
|
||||||
|
if not guild:
|
||||||
|
# This should not happen due to guild_only decorator
|
||||||
|
return
|
||||||
|
|
||||||
|
channel_id = await self.cog.config.guild(guild).review_channel()
|
||||||
|
if not channel_id:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"The review channel has not been configured by an administrator.",
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
channel = guild.get_channel(channel_id)
|
||||||
|
if not isinstance(channel, discord.TextChannel):
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"The configured review channel is invalid. Please contact an administrator.",
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = discord.Embed(title="New Service Review", color=discord.Color.purple())
|
||||||
|
embed.set_author(name=f"{interaction.user.name} ({interaction.user.id})", icon_url=interaction.user.display_avatar.url)
|
||||||
|
embed.add_field(name="Service Type", value=self.service_type.value, inline=False)
|
||||||
|
embed.add_field(name="Rating", value=f"{self.rating.value}/10", inline=True)
|
||||||
|
embed.add_field(name="Toxicity", value=self.toxicity.value, inline=True)
|
||||||
|
embed.add_field(name="Review", value=self.review_text.value, inline=False)
|
||||||
|
embed.set_footer(text=f"User ID: {interaction.user.id}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
review_message = await channel.send(embed=embed)
|
||||||
|
# **FIX:** Log the timestamp for the DataManager
|
||||||
|
async with self.cog.config.guild(guild).submitted_reviews() as reviews:
|
||||||
|
reviews[str(review_message.id)] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||||
|
|
||||||
|
await interaction.response.send_message("Thank you! Your review has been submitted successfully.", ephemeral=True)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"I don't have permission to send messages in the review channel. Please contact an administrator.",
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
await interaction.response.send_message(f"An error occurred while trying to send your review: {e}", ephemeral=True)
|
||||||
|
|
||||||
|
# --- Main Cog Class ---
|
||||||
|
|
||||||
|
class ServiceReview(commands.Cog):
|
||||||
|
"""A cog for users to leave service reviews for staff."""
|
||||||
|
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.config = Config.get_conf(self, identifier=1234567891, force_registration=True)
|
||||||
|
default_guild = {
|
||||||
|
"review_channel": None,
|
||||||
|
"submitted_reviews": {} # **FIX:** Added for DataManager logging
|
||||||
|
}
|
||||||
|
self.config.register_guild(**default_guild)
|
||||||
|
|
||||||
|
@app_commands.command(name="srev")
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def service_review(self, interaction: discord.Interaction):
|
||||||
|
"""Leave a review for a staff member or service."""
|
||||||
|
modal = ReviewModal(self)
|
||||||
|
await interaction.response.send_modal(modal)
|
||||||
|
|
||||||
|
@commands.group(aliases=["srset"]) # type: ignore
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
|
async def srevset(self, ctx: commands.Context):
|
||||||
|
"""Configure ServiceReview settings."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@srevset.command(name="channel")
|
||||||
|
async def set_review_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
|
"""Set the channel where service reviews will be sent."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).review_channel.set(channel.id)
|
||||||
|
await ctx.send(f"Service review channel has been set to {channel.mention}")
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(ServiceReview(bot))
|
||||||
|
|
||||||
4
staffmsg/__init__.py
Normal file
4
staffmsg/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .staffmsg import StaffMsg
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(StaffMsg(bot))
|
||||||
17
staffmsg/info.json
Normal file
17
staffmsg/info.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"author": [
|
||||||
|
"unstableCogs"
|
||||||
|
],
|
||||||
|
"install_msg": "Thank you for installing the Staff Messaging cog!",
|
||||||
|
"name": "StaffMsg",
|
||||||
|
"short": "A command for staff to send official DMs.",
|
||||||
|
"description": "Provides a permission-controlled command for staff to send direct messages to server members, with logging for accountability.",
|
||||||
|
"tags": [
|
||||||
|
"dm",
|
||||||
|
"messaging",
|
||||||
|
"staff",
|
||||||
|
"utility"
|
||||||
|
],
|
||||||
|
"requirements": [],
|
||||||
|
"end_user_data_statement": "This cog may store message content and user IDs in a log file or channel for moderation purposes."
|
||||||
|
}
|
||||||
188
staffmsg/staffmsg.py
Normal file
188
staffmsg/staffmsg.py
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import discord
|
||||||
|
from redbot.core import commands, Config, app_commands
|
||||||
|
from typing import Optional
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
# --- Modal for the Message Form ---
|
||||||
|
|
||||||
|
class MessageModal(discord.ui.Modal, title="Staff Message"):
|
||||||
|
message_content = discord.ui.TextInput(
|
||||||
|
label="Message to Send",
|
||||||
|
style=discord.TextStyle.paragraph,
|
||||||
|
placeholder="Type the official message you want to send to the user here.",
|
||||||
|
max_length=1800
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, cog: "StaffMsg", target_user: discord.Member):
|
||||||
|
super().__init__()
|
||||||
|
self.cog = cog
|
||||||
|
self.target_user = target_user
|
||||||
|
|
||||||
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
|
guild = interaction.guild
|
||||||
|
if not guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
# --- Send DM to User ---
|
||||||
|
embed_to_user = discord.Embed(
|
||||||
|
title=f"A Message from the Staff of {guild.name}",
|
||||||
|
description=self.message_content.value,
|
||||||
|
color=await self.cog.bot.get_embed_color(interaction.channel) if interaction.channel else discord.Color.blurple()
|
||||||
|
)
|
||||||
|
embed_to_user.set_footer(text="This is an official communication. Please do not reply to the bot.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.target_user.send(embed=embed_to_user)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"I could not send a DM to {self.target_user.mention}. They may have DMs disabled.",
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
await interaction.response.send_message(f"An error occurred while sending the DM: {e}", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# --- Log the Message ---
|
||||||
|
log_channel_id = await self.cog.config.guild(guild).log_channel()
|
||||||
|
if log_channel_id:
|
||||||
|
log_channel = guild.get_channel(log_channel_id)
|
||||||
|
if isinstance(log_channel, discord.TextChannel):
|
||||||
|
log_embed = discord.Embed(
|
||||||
|
title="Staff DM Sent",
|
||||||
|
description=self.message_content.value,
|
||||||
|
color=discord.Color.blue()
|
||||||
|
)
|
||||||
|
log_embed.set_author(name=f"From: {interaction.user.name} ({interaction.user.id})", icon_url=interaction.user.display_avatar.url)
|
||||||
|
log_embed.add_field(name="To", value=f"{self.target_user.mention} ({self.target_user.id})")
|
||||||
|
|
||||||
|
try:
|
||||||
|
log_message = await log_channel.send(embed=log_embed)
|
||||||
|
async with self.cog.config.guild(guild).sent_dms() as dms:
|
||||||
|
dms[str(log_message.id)] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await interaction.response.send_message(f"Your message has been successfully sent to {self.target_user.mention}.", ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Main Cog Class ---
|
||||||
|
|
||||||
|
class StaffMsg(commands.Cog):
|
||||||
|
"""A cog for staff to send official DMs to users."""
|
||||||
|
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.config = Config.get_conf(self, identifier=1234567892, force_registration=True)
|
||||||
|
default_guild = {
|
||||||
|
"log_channel": None,
|
||||||
|
"authorized_role": None,
|
||||||
|
"sent_dms": {} # For DataManager
|
||||||
|
}
|
||||||
|
self.config.register_guild(**default_guild)
|
||||||
|
|
||||||
|
async def cog_check(self, ctx: commands.Context) -> bool:
|
||||||
|
if not ctx.guild:
|
||||||
|
await ctx.send("This command can only be used in a server.", ephemeral=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
auth_role_id = await self.config.guild(ctx.guild).authorized_role()
|
||||||
|
if not auth_role_id:
|
||||||
|
await ctx.send("The authorized role for this command has not been set.", ephemeral=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
author = ctx.author
|
||||||
|
if not isinstance(author, discord.Member):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if author.guild_permissions.administrator:
|
||||||
|
return True
|
||||||
|
|
||||||
|
auth_role = ctx.guild.get_role(auth_role_id)
|
||||||
|
if not auth_role or auth_role not in author.roles:
|
||||||
|
await ctx.send("You are not authorized to use this command.", ephemeral=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@commands.hybrid_command(name="smsg", aliases=["staff", "staffmsg"])
|
||||||
|
@app_commands.describe(user="The user to send a DM to.", message="(Optional) The message to send. Opens a form if left blank.")
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def staff_message(self, ctx: commands.Context, user: discord.Member, *, message: Optional[str] = None):
|
||||||
|
"""Send an official DM to a user. Opens a form if no message is provided."""
|
||||||
|
|
||||||
|
# This is a clever way to handle both slash and prefix commands.
|
||||||
|
# If the 'message' is None, it means it was likely a slash command
|
||||||
|
# or a prefix command with no text, so we open the modal.
|
||||||
|
if message is None:
|
||||||
|
if not ctx.interaction:
|
||||||
|
await ctx.send("Please provide a message to send.", ephemeral=True)
|
||||||
|
return
|
||||||
|
modal = MessageModal(self, user)
|
||||||
|
await ctx.interaction.response.send_modal(modal)
|
||||||
|
return
|
||||||
|
|
||||||
|
guild = ctx.guild
|
||||||
|
if not guild: # Should be caught by cog_check but for type safety
|
||||||
|
return
|
||||||
|
|
||||||
|
embed_to_user = discord.Embed(
|
||||||
|
title=f"A Message from the Staff of {guild.name}",
|
||||||
|
description=message,
|
||||||
|
color=await ctx.embed_color()
|
||||||
|
)
|
||||||
|
embed_to_user.set_footer(text="This is an official communication. Please do not reply to the bot.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await user.send(embed=embed_to_user)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await ctx.send(f"I could not send a DM to {user.mention}. They may have DMs disabled.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
log_channel_id = await self.config.guild(guild).log_channel()
|
||||||
|
if log_channel_id:
|
||||||
|
log_channel = guild.get_channel(log_channel_id)
|
||||||
|
if isinstance(log_channel, discord.TextChannel):
|
||||||
|
log_embed = discord.Embed(
|
||||||
|
title="Staff DM Sent",
|
||||||
|
description=message,
|
||||||
|
color=discord.Color.blue()
|
||||||
|
)
|
||||||
|
log_embed.set_author(name=f"From: {ctx.author.name} ({ctx.author.id})", icon_url=ctx.author.display_avatar.url)
|
||||||
|
log_embed.add_field(name="To", value=f"{user.mention} ({user.id})")
|
||||||
|
try:
|
||||||
|
log_message = await log_channel.send(embed=log_embed)
|
||||||
|
async with self.config.guild(guild).sent_dms() as dms:
|
||||||
|
dms[str(log_message.id)] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await ctx.send(f"Your message has been sent to {user.mention}.", ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
@commands.group(aliases=["smset"]) # type: ignore
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
|
async def staffmsgset(self, ctx: commands.Context):
|
||||||
|
"""Configure StaffMsg settings."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staffmsgset.command(name="role")
|
||||||
|
async def set_auth_role(self, ctx: commands.Context, role: discord.Role):
|
||||||
|
"""Set the role authorized to use the staff message commands."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).authorized_role.set(role.id)
|
||||||
|
await ctx.send(f"Authorized role has been set to {role.mention}")
|
||||||
|
|
||||||
|
@staffmsgset.command(name="logchannel")
|
||||||
|
async def set_log_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
|
"""Set the channel where sent DMs will be logged."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).log_channel.set(channel.id)
|
||||||
|
await ctx.send(f"Log channel has been set to {channel.mention}")
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(StaffMsg(bot))
|
||||||
|
|
||||||
4
translator/__init__.py
Normal file
4
translator/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .translator import Translator
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(Translator(bot))
|
||||||
286
translator/commands.py
Normal file
286
translator/commands.py
Normal 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))
|
||||||
|
|
||||||
17
translator/info.json
Normal file
17
translator/info.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"author": [
|
||||||
|
"unstableCogs"
|
||||||
|
],
|
||||||
|
"install_msg": "Thank you for installing the Translator cog. Use the `/translate` command to get started.",
|
||||||
|
"name": "Translator",
|
||||||
|
"short": "Translates text into various fantasy and fun languages.",
|
||||||
|
"description": "A feature-rich translator cog ported from a web application. Supports numerous languages from Common to Valspiren, Sinary, and more. Includes a command to list all available languages.",
|
||||||
|
"tags": [
|
||||||
|
"translate",
|
||||||
|
"fun",
|
||||||
|
"roleplay",
|
||||||
|
"language"
|
||||||
|
],
|
||||||
|
"requirements": [],
|
||||||
|
"end_user_data_statement": "This cog does not store any end user data."
|
||||||
|
}
|
||||||
1
translator/languages/__init__.py
Normal file
1
translator/languages/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
8
translator/languages/abyssal.py
Normal file
8
translator/languages/abyssal.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/angelic.py
Normal file
8
translator/languages/angelic.py
Normal 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
|
||||||
|
}
|
||||||
9
translator/languages/aquan.py
Normal file
9
translator/languages/aquan.py
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
8
translator/languages/aquatic.py
Normal file
8
translator/languages/aquatic.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/arachnid.py
Normal file
8
translator/languages/arachnid.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/avain.py
Normal file
8
translator/languages/avain.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/celestial.py
Normal file
8
translator/languages/celestial.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/common.py
Normal file
8
translator/languages/common.py
Normal 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'
|
||||||
|
}
|
||||||
8
translator/languages/construct.py
Normal file
8
translator/languages/construct.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/devilish.py
Normal file
8
translator/languages/devilish.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/draconic.py
Normal file
8
translator/languages/draconic.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/dwarvish.py
Normal file
8
translator/languages/dwarvish.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/elemental.py
Normal file
8
translator/languages/elemental.py
Normal 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
|
||||||
|
}
|
||||||
11
translator/languages/elvish.py
Normal file
11
translator/languages/elvish.py
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
8
translator/languages/feline.py
Normal file
8
translator/languages/feline.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/fiendish.py
Normal file
8
translator/languages/fiendish.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/gnomish.py
Normal file
8
translator/languages/gnomish.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/goblin.py
Normal file
8
translator/languages/goblin.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/halftongue.py
Normal file
8
translator/languages/halftongue.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/herbilore.py
Normal file
8
translator/languages/herbilore.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/infernal.py
Normal file
8
translator/languages/infernal.py
Normal 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
|
||||||
|
}
|
||||||
9
translator/languages/kitsune.py
Normal file
9
translator/languages/kitsune.py
Normal 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': ''
|
||||||
|
}
|
||||||
9
translator/languages/leet.py
Normal file
9
translator/languages/leet.py
Normal 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
|
||||||
|
|
||||||
|
}
|
||||||
8
translator/languages/lizardfolk.py
Normal file
8
translator/languages/lizardfolk.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/morsecode.py
Normal file
8
translator/languages/morsecode.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/myconid.py
Normal file
8
translator/languages/myconid.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/ogrish.py
Normal file
8
translator/languages/ogrish.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/orcish.py
Normal file
8
translator/languages/orcish.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/piglatin.py
Normal file
8
translator/languages/piglatin.py
Normal 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'
|
||||||
|
}
|
||||||
8
translator/languages/ratfolk.py
Normal file
8
translator/languages/ratfolk.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/scorpion.py
Normal file
8
translator/languages/scorpion.py
Normal 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
|
||||||
|
}
|
||||||
10
translator/languages/sinary.py
Normal file
10
translator/languages/sinary.py
Normal 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': ''
|
||||||
|
}
|
||||||
8
translator/languages/spiritual.py
Normal file
8
translator/languages/spiritual.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/succubus.py
Normal file
8
translator/languages/succubus.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/sylvan.py
Normal file
8
translator/languages/sylvan.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/undercommon.py
Normal file
8
translator/languages/undercommon.py
Normal 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
|
||||||
|
}
|
||||||
8
translator/languages/uwu.py
Normal file
8
translator/languages/uwu.py
Normal 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'
|
||||||
|
}
|
||||||
10
translator/languages/valspiren.py
Normal file
10
translator/languages/valspiren.py
Normal 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': ''
|
||||||
|
}
|
||||||
8
translator/languages/voidtouched.py
Normal file
8
translator/languages/voidtouched.py
Normal 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
196
translator/logic.py
Normal 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
249
translator/translator.py
Normal 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
28
translator/views.py
Normal 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()
|
||||||
|
|
||||||
@@ -1 +1,4 @@
|
|||||||
from .welcomer import setup
|
from .welcomer import Welcomer
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(Welcomer(bot))
|
||||||
@@ -1,195 +1,121 @@
|
|||||||
# welcomer.py
|
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from redbot.core import commands, Config
|
from redbot.core import commands, Config
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
class Welcomer(commands.Cog):
|
class Welcomer(commands.Cog):
|
||||||
"""
|
"""
|
||||||
A configurable cog to welcome new users to a server.
|
A configurable cog to automatically welcome new users with multiple modes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot: Red):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
# Initialize Red's Config system. This will store our settings.
|
|
||||||
# The "GUILD" identifier means settings will be saved on a per-server basis.
|
|
||||||
self.config = Config.get_conf(self, identifier=1234567890, force_registration=True)
|
self.config = Config.get_conf(self, identifier=1234567890, force_registration=True)
|
||||||
|
|
||||||
# Define the default settings for each server.
|
|
||||||
default_guild = {
|
default_guild = {
|
||||||
"welcome_channel": None, # The ID of the channel to send welcomes to
|
"welcome_channel": None,
|
||||||
"welcome_message": "Welcome to the server, {user.mention}!", # Default message
|
"welcome_message": "Welcome to the server, {user.mention}!",
|
||||||
"enabled": False # Whether the system is on or off
|
"enabled": False,
|
||||||
|
"welcome_mode": "normal" # 'normal' or 'solstira'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Register the default settings.
|
|
||||||
self.config.register_guild(**default_guild)
|
self.config.register_guild(**default_guild)
|
||||||
|
|
||||||
@commands.Cog.listener()
|
@commands.Cog.listener()
|
||||||
async def on_member_join(self, member: discord.Member):
|
async def on_member_join(self, member: discord.Member):
|
||||||
"""
|
|
||||||
The event listener that runs when a new member joins.
|
|
||||||
"""
|
|
||||||
guild = member.guild
|
guild = member.guild
|
||||||
|
|
||||||
# Check if the welcomer is enabled for this server.
|
|
||||||
if not await self.config.guild(guild).enabled():
|
if not await self.config.guild(guild).enabled():
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get the welcome channel ID from our saved settings.
|
mode = await self.config.guild(guild).welcome_mode()
|
||||||
channel_id = await self.config.guild(guild).welcome_channel()
|
|
||||||
|
if mode == "solstira":
|
||||||
|
# Hardcoded Solstira welcome event
|
||||||
|
channel_id = 1409965732669948024
|
||||||
|
else: # Normal mode
|
||||||
|
channel_id = await self.config.guild(guild).welcome_channel()
|
||||||
|
|
||||||
if not channel_id:
|
if not channel_id:
|
||||||
return # If no channel is set, do nothing.
|
return
|
||||||
|
|
||||||
# Try to find the channel object in the server.
|
|
||||||
channel = guild.get_channel(channel_id)
|
channel = guild.get_channel(channel_id)
|
||||||
if not channel:
|
|
||||||
# The channel might have been deleted.
|
|
||||||
return
|
|
||||||
|
|
||||||
# Explicitly check if the channel is a TextChannel before trying to send a message.
|
|
||||||
if not isinstance(channel, discord.TextChannel):
|
if not isinstance(channel, discord.TextChannel):
|
||||||
print(f"Welcomer: Configured channel '{channel.name}' in {guild.name} is not a text channel.")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get the custom welcome message from our settings.
|
|
||||||
message_template = await self.config.guild(guild).welcome_message()
|
|
||||||
|
|
||||||
# Format the message with the new member's info.
|
|
||||||
# .format() is a safe way to replace placeholders.
|
|
||||||
formatted_message = message_template.format(
|
|
||||||
user=member,
|
|
||||||
user_mention=member.mention,
|
|
||||||
user_name=member.name,
|
|
||||||
server_name=guild.name
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if we have permission to send messages in the channel.
|
|
||||||
if not channel.permissions_for(guild.me).send_messages:
|
if not channel.permissions_for(guild.me).send_messages:
|
||||||
# We can't send a message, so we'll just log this internally.
|
|
||||||
print(f"Welcomer: No permission to send messages in {channel.name} in {guild.name}.")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
if mode == "solstira":
|
||||||
await channel.send(formatted_message)
|
content = (
|
||||||
except discord.Forbidden:
|
f"{member.mention}\n"
|
||||||
# This is a final safety check in case permissions change suddenly.
|
f"- `guide` <https://discord.com/channels/1409939326382651/140993936542124073>\n"
|
||||||
pass
|
f"- <a:f_purpleflower:1409967049561524859> `roles` <@&1409937150>\n"
|
||||||
except discord.HTTPException as e:
|
f"(<https://discord.com/channels/1409939326382651/140996735493797150>)\n"
|
||||||
# This can happen if the message is too long or there's a Discord API error.
|
f"- `psa` (<https://discord.com/channels/1409939326382651/140996779492440732>)"
|
||||||
print(f"Welcomer: Failed to send welcome message in {guild.name}: {e}")
|
)
|
||||||
|
embed = discord.Embed(description="--", color=0x8b9ed7)
|
||||||
|
try:
|
||||||
|
await channel.send(content, embed=embed)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
else: # Normal mode
|
||||||
|
message_template = await self.config.guild(guild).welcome_message()
|
||||||
|
formatted_message = message_template.format(
|
||||||
|
user=member,
|
||||||
|
user_mention=member.mention,
|
||||||
|
user_name=member.name,
|
||||||
|
server_name=guild.name
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await channel.send(formatted_message)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
|
||||||
# Create a command group for all our settings commands.
|
|
||||||
@commands.group(aliases=["wset"]) # type: ignore
|
@commands.group(aliases=["wset"]) # type: ignore
|
||||||
@commands.guild_only() # Ensures this command and its subcommands can only be run in a server
|
@commands.guild_only()
|
||||||
@commands.admin_or_permissions(manage_guild=True)
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
async def welcomeset(self, ctx: commands.Context):
|
async def welcomeset(self, ctx: commands.Context):
|
||||||
"""
|
"""Commands for configuring the welcome system."""
|
||||||
Configure the welcomer settings for this server.
|
|
||||||
"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@welcomeset.command(name="channel", aliases=["chnl"])
|
@welcomeset.command(name="mode")
|
||||||
async def welcomeset_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
async def set_welcome_mode(self, ctx: commands.Context, mode: Literal["normal", "solstira"]):
|
||||||
"""
|
"""Set the welcome mode for this server.
|
||||||
Set the channel where welcome messages will be sent.
|
|
||||||
|
|
||||||
Example:
|
Modes:
|
||||||
[p]welcomeset channel #welcome
|
- `normal`: A simple, configurable welcome message.
|
||||||
|
- `solstira`: The special, complex welcome event.
|
||||||
"""
|
"""
|
||||||
if not ctx.guild:
|
if not ctx.guild:
|
||||||
return # This check satisfies the type checker
|
return
|
||||||
|
await self.config.guild(ctx.guild).welcome_mode.set(mode.lower())
|
||||||
|
await ctx.send(f"Welcome mode has been set to **{mode.lower()}**.")
|
||||||
|
|
||||||
|
@welcomeset.command(name="channel")
|
||||||
|
async def set_welcome_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
|
"""(Normal Mode) Set the channel for welcome messages."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
await self.config.guild(ctx.guild).welcome_channel.set(channel.id)
|
await self.config.guild(ctx.guild).welcome_channel.set(channel.id)
|
||||||
await ctx.send(f"The welcome channel has been set to {channel.mention}.")
|
await ctx.send(f"Welcome channel set to {channel.mention}")
|
||||||
|
|
||||||
@welcomeset.command(name="message", aliases=["msg"])
|
@welcomeset.command(name="message")
|
||||||
async def welcomeset_message(self, ctx: commands.Context, *, message: str):
|
async def set_welcome_message(self, ctx: commands.Context, *, message: str):
|
||||||
"""
|
"""(Normal Mode) Set the welcome message. Use placeholders like {user.mention}."""
|
||||||
Set the custom welcome message.
|
|
||||||
|
|
||||||
You can use these placeholders:
|
|
||||||
{user} - The user object.
|
|
||||||
{user_mention} - Pings the user.
|
|
||||||
{user_name} - The user's name.
|
|
||||||
{server_name} - The name of this server.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
[p]welcomeset message Hello {user_mention}, welcome to {server_name}!
|
|
||||||
"""
|
|
||||||
if not ctx.guild:
|
if not ctx.guild:
|
||||||
return
|
return
|
||||||
await self.config.guild(ctx.guild).welcome_message.set(message)
|
await self.config.guild(ctx.guild).welcome_message.set(message)
|
||||||
await ctx.send(f"The welcome message has been updated.")
|
await ctx.send("Welcome message updated.")
|
||||||
# Send a preview of the new message.
|
|
||||||
preview = message.format(
|
|
||||||
user=ctx.author,
|
|
||||||
user_mention=ctx.author.mention,
|
|
||||||
user_name=ctx.author.name,
|
|
||||||
server_name=ctx.guild.name
|
|
||||||
)
|
|
||||||
await ctx.send(f"**Preview:**\n{preview}")
|
|
||||||
|
|
||||||
@welcomeset.command(name="toggle", aliases=["on", "off"])
|
@welcomeset.command(name="toggle")
|
||||||
async def welcomeset_toggle(self, ctx: commands.Context):
|
async def toggle_welcome(self, ctx: commands.Context):
|
||||||
"""
|
"""Toggle the welcome system on or off."""
|
||||||
Enable or disable the welcomer system on this server.
|
|
||||||
"""
|
|
||||||
if not ctx.guild:
|
if not ctx.guild:
|
||||||
return
|
return
|
||||||
current_status = await self.config.guild(ctx.guild).enabled()
|
current_status = await self.config.guild(ctx.guild).enabled()
|
||||||
new_status = not current_status
|
new_status = not current_status
|
||||||
await self.config.guild(ctx.guild).enabled.set(new_status)
|
await self.config.guild(ctx.guild).enabled.set(new_status)
|
||||||
status_text = "enabled" if new_status else "disabled"
|
status_text = "enabled" if new_status else "disabled"
|
||||||
await ctx.send(f"The welcomer system has been {status_text}.")
|
await ctx.send(f"Welcome system has been {status_text}.")
|
||||||
|
|
||||||
@welcomeset.command(name="settings", aliases=["show", "status"])
|
|
||||||
async def welcomeset_settings(self, ctx: commands.Context):
|
|
||||||
"""
|
|
||||||
Show the current welcomer settings for this server.
|
|
||||||
"""
|
|
||||||
if not ctx.guild:
|
|
||||||
return
|
|
||||||
settings = await self.config.guild(ctx.guild).all()
|
|
||||||
channel_id = settings['welcome_channel']
|
|
||||||
channel = ctx.guild.get_channel(channel_id) if channel_id else None
|
|
||||||
message = settings['welcome_message']
|
|
||||||
enabled = "Enabled" if settings['enabled'] else "Disabled"
|
|
||||||
|
|
||||||
embed = discord.Embed(title="Welcomer Settings", color=await ctx.embed_color())
|
|
||||||
embed.add_field(name="Status", value=enabled, inline=False)
|
|
||||||
embed.add_field(name="Channel", value=channel.mention if isinstance(channel, discord.TextChannel) else "Not Set", inline=False)
|
|
||||||
embed.add_field(name="Message", value=f"```{message}```", inline=False)
|
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
|
|
||||||
@welcomeset.command(name="test")
|
|
||||||
async def welcomeset_test(self, ctx: commands.Context):
|
|
||||||
"""
|
|
||||||
Test the welcome message by sending a preview to this channel.
|
|
||||||
"""
|
|
||||||
if not ctx.guild:
|
|
||||||
return
|
|
||||||
message_template = await self.config.guild(ctx.guild).welcome_message()
|
|
||||||
preview = message_template.format(
|
|
||||||
user=ctx.author,
|
|
||||||
user_mention=ctx.author.mention,
|
|
||||||
user_name=ctx.author.name,
|
|
||||||
server_name=ctx.guild.name
|
|
||||||
)
|
|
||||||
await ctx.send(f"**Welcome Message Preview**:\n{preview}")
|
|
||||||
|
|
||||||
@welcomeset.command(name="reset")
|
|
||||||
async def welcomeset_reset(self, ctx: commands.Context):
|
|
||||||
"""
|
|
||||||
Reset all welcomer settings to their defaults.
|
|
||||||
"""
|
|
||||||
if not ctx.guild:
|
|
||||||
return
|
|
||||||
await self.config.guild(ctx.guild).clear()
|
|
||||||
await ctx.send("The welcomer settings have been reset to their defaults.")
|
|
||||||
|
|
||||||
|
|
||||||
# This function allows Red to load the cog.
|
|
||||||
# It is required in every cog file.
|
|
||||||
async def setup(bot: Red):
|
async def setup(bot: Red):
|
||||||
await bot.add_cog(Welcomer(bot))
|
await bot.add_cog(Welcomer(bot))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user