Compare commits
22 Commits
backup-ref
...
01a484ddb6
| Author | SHA1 | Date | |
|---|---|---|---|
| 01a484ddb6 | |||
| acb71337fa | |||
| 0e5afcb999 | |||
| b27a734e3c | |||
| 9ad286b671 | |||
| b186a9c119 | |||
| d3ee48112a | |||
| 48c3793768 | |||
| 141efc4253 | |||
| 61b81068ba | |||
| a0df694243 | |||
| ca7dba5af6 | |||
| d65197e552 | |||
| 0909717fb6 | |||
| 84a2b41a79 | |||
| 82e48c2383 | |||
| 2d199d9247 | |||
| f546eaa633 | |||
| e0330148c2 | |||
| 3955e61a62 | |||
| a3e210a7ce | |||
| 5fd4e08d90 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -179,5 +179,3 @@ cython_debug/
|
|||||||
.vs/
|
.vs/
|
||||||
|
|
||||||
pyproject.toml
|
pyproject.toml
|
||||||
|
|
||||||
treegen.py
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
from .datamanager import DataManager
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(DataManager(bot))
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
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))
|
|
||||||
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"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 setup
|
||||||
|
|
||||||
|
# This function is required for the cog to be loaded by Red.
|
||||||
|
# It simply imports the setup function from the main cog file.
|
||||||
274
hiring/hiring.py
274
hiring/hiring.py
@@ -1,10 +1,10 @@
|
|||||||
import discord
|
import discord
|
||||||
from redbot.core import commands, Config, app_commands
|
from redbot.core import commands, Config
|
||||||
from typing import Literal, Optional, TYPE_CHECKING, Type
|
from redbot.core.bot import Red
|
||||||
import datetime
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from redbot.core.bot import Red
|
from hiring.hiring import Hiring
|
||||||
|
|
||||||
# --- Modals for the Application Forms ---
|
# --- Modals for the Application Forms ---
|
||||||
|
|
||||||
@@ -12,252 +12,242 @@ class StaffApplicationModal(discord.ui.Modal, title="Staff Application"):
|
|||||||
chosen_plan = discord.ui.TextInput(label="What plan are you interested in?")
|
chosen_plan = discord.ui.TextInput(label="What plan are you interested in?")
|
||||||
tox_level = discord.ui.TextInput(label="What is your toxicity level tolerance?")
|
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)
|
describe_server = discord.ui.TextInput(label="Please describe your server.", style=discord.TextStyle.paragraph)
|
||||||
server_link = discord.ui.TextInput(label="Server Invite Link")
|
server_link = discord.ui.TextInput(label="Server Link")
|
||||||
|
|
||||||
def __init__(self, ticket_channel: discord.TextChannel):
|
|
||||||
super().__init__()
|
|
||||||
self.ticket_channel = ticket_channel
|
|
||||||
|
|
||||||
async def on_submit(self, interaction: discord.Interaction):
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
cog: Optional["Hiring"] = interaction.client.get_cog("Hiring") # type: ignore
|
embed = discord.Embed(
|
||||||
if not cog:
|
title="New Staff Application",
|
||||||
await interaction.response.send_message("Hiring cog not loaded.", ephemeral=True)
|
description=f"Submitted by {interaction.user.mention}",
|
||||||
return
|
color=0xadd8e6
|
||||||
|
)
|
||||||
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="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="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 Description", value=self.describe_server.value, inline=False)
|
||||||
embed.add_field(name="Server Link", value=self.server_link.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"):
|
class PMApplicationModal(discord.ui.Modal, title="PM Application"):
|
||||||
ad = discord.ui.TextInput(label="Your Ad", style=discord.TextStyle.paragraph)
|
ad = discord.ui.TextInput(label="Please provide your server ad.", style=discord.TextStyle.paragraph)
|
||||||
reqs = discord.ui.TextInput(label="Your Requirements", style=discord.TextStyle.paragraph)
|
reqs = discord.ui.TextInput(label="What are your requirements?")
|
||||||
tox_level = discord.ui.TextInput(label="What is your toxicity level tolerance?")
|
tox_level = discord.ui.TextInput(label="What is your toxicity level tolerance?")
|
||||||
chosen_plan = discord.ui.TextInput(label="What plan are you interested in?")
|
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):
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
cog: Optional["Hiring"] = interaction.client.get_cog("Hiring") # type: ignore
|
embed = discord.Embed(
|
||||||
if not cog:
|
title="New PM Application",
|
||||||
await interaction.response.send_message("Hiring cog not loaded.", ephemeral=True)
|
description=f"Submitted by {interaction.user.mention}",
|
||||||
return
|
color=0xadd8e6
|
||||||
|
)
|
||||||
embed = discord.Embed(title="New PM Application", color=discord.Color.green())
|
embed.add_field(name="Server Ad", value=f"```\n{self.ad.value}\n```", inline=False)
|
||||||
embed.set_author(name=interaction.user.name, icon_url=interaction.user.display_avatar.url)
|
embed.add_field(name="Requirements", value=self.reqs.value, inline=False)
|
||||||
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="Toxicity Level", value=self.tox_level.value, inline=False)
|
||||||
embed.add_field(name="Chosen Plan", value=self.chosen_plan.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 application has been submitted!", ephemeral=True)
|
||||||
await interaction.response.send_message("Your PM application has been submitted.", ephemeral=True)
|
if isinstance(interaction.channel, discord.TextChannel):
|
||||||
|
await interaction.channel.send(embed=embed)
|
||||||
|
|
||||||
class HPMApplicationModal(discord.ui.Modal, title="HPM Application"):
|
class HPMApplicationModal(discord.ui.Modal, title="HPM Application"):
|
||||||
ad = discord.ui.TextInput(label="Your Ad", style=discord.TextStyle.paragraph)
|
ad = discord.ui.TextInput(label="Please provide your server ad.", style=discord.TextStyle.paragraph)
|
||||||
reqs = discord.ui.TextInput(label="Your Requirements", style=discord.TextStyle.paragraph)
|
reqs = discord.ui.TextInput(label="What are your requirements?")
|
||||||
|
|
||||||
def __init__(self, ticket_channel: discord.TextChannel):
|
|
||||||
super().__init__()
|
|
||||||
self.ticket_channel = ticket_channel
|
|
||||||
|
|
||||||
async def on_submit(self, interaction: discord.Interaction):
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
cog: Optional["Hiring"] = interaction.client.get_cog("Hiring") # type: ignore
|
embed = discord.Embed(
|
||||||
if not cog:
|
title="New HPM Application",
|
||||||
await interaction.response.send_message("Hiring cog not loaded.", ephemeral=True)
|
description=f"Submitted by {interaction.user.mention}",
|
||||||
|
color=0xadd8e6
|
||||||
|
)
|
||||||
|
embed.add_field(name="Server Ad", value=f"```\n{self.ad.value}\n```", inline=False)
|
||||||
|
embed.add_field(name="Requirements", value=self.reqs.value, inline=False)
|
||||||
|
await interaction.response.send_message("Your application has been submitted!", ephemeral=True)
|
||||||
|
if isinstance(interaction.channel, discord.TextChannel):
|
||||||
|
await interaction.channel.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Reusable Ticket Creation Logic ---
|
||||||
|
async def create_ticket(interaction: discord.Interaction, role_type: str, category_id: Optional[int], modal: discord.ui.Modal):
|
||||||
|
if not interaction.guild:
|
||||||
|
await interaction.response.send_message("This interaction must be used in a server.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
embed = discord.Embed(title="New HPM Application", color=discord.Color.purple())
|
if not category_id:
|
||||||
embed.set_author(name=interaction.user.name, icon_url=interaction.user.display_avatar.url)
|
await interaction.response.send_message(f"The category for '{role_type}' applications has not been set by an admin.", ephemeral=True)
|
||||||
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
|
return
|
||||||
|
|
||||||
guild = interaction.guild
|
category = interaction.guild.get_channel(category_id)
|
||||||
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):
|
if not isinstance(category, discord.CategoryChannel):
|
||||||
await interaction.response.send_message(f"The configured category for '{ticket_type}' is invalid.", ephemeral=True)
|
await interaction.response.send_message(f"The category for '{role_type}' applications is invalid or has been deleted.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
assert isinstance(interaction.user, discord.Member)
|
ticket_name = f"{role_type}-app-{interaction.user.name}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
thread_name = f"{ticket_type}-application-{interaction.user.name}"
|
|
||||||
overwrites = {
|
overwrites = {
|
||||||
guild.default_role: discord.PermissionOverwrite(read_messages=False),
|
interaction.guild.default_role: discord.PermissionOverwrite(read_messages=False),
|
||||||
interaction.user: discord.PermissionOverwrite(read_messages=True, send_messages=True)
|
interaction.user: discord.PermissionOverwrite(read_messages=True, send_messages=True),
|
||||||
|
interaction.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True)
|
||||||
}
|
}
|
||||||
ticket_channel = await category.create_text_channel(name=thread_name, overwrites=overwrites)
|
ticket_channel = await interaction.guild.create_text_channel(
|
||||||
|
name=ticket_name,
|
||||||
modal_instance = modal_class(ticket_channel) # type: ignore
|
category=category,
|
||||||
await interaction.response.send_modal(modal_instance)
|
overwrites=overwrites
|
||||||
|
)
|
||||||
async with cog.config.guild(guild).closed_applications() as closed_apps:
|
await ticket_channel.send(f"Welcome {interaction.user.mention}! Please fill out the form to complete your application.")
|
||||||
closed_apps[str(ticket_channel.id)] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
await interaction.response.send_modal(modal)
|
||||||
|
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
if not interaction.response.is_done():
|
if interaction.response.is_done():
|
||||||
|
await interaction.followup.send("I don't have permission to create channels in that category.", ephemeral=True)
|
||||||
|
else:
|
||||||
await interaction.response.send_message("I don't have permission to create channels in that category.", ephemeral=True)
|
await interaction.response.send_message("I don't have permission to create channels in that category.", ephemeral=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if not interaction.response.is_done():
|
if interaction.response.is_done():
|
||||||
|
await interaction.followup.send(f"An unexpected error occurred: {e}", ephemeral=True)
|
||||||
|
else:
|
||||||
await interaction.response.send_message(f"An unexpected error occurred: {e}", ephemeral=True)
|
await interaction.response.send_message(f"An unexpected error occurred: {e}", ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Button Views for Commands ---
|
||||||
|
|
||||||
class HireView(discord.ui.View):
|
class HireView(discord.ui.View):
|
||||||
def __init__(self):
|
def __init__(self, cog: "Hiring"):
|
||||||
super().__init__(timeout=None)
|
super().__init__(timeout=None)
|
||||||
|
self.cog = cog
|
||||||
|
|
||||||
@discord.ui.button(label="Staff", style=discord.ButtonStyle.primary, custom_id="staff_apply_button_persistent")
|
@discord.ui.button(label="Staff", style=discord.ButtonStyle.primary, custom_id="staff_apply_button")
|
||||||
async def staff_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
async def staff_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
await create_ticket(interaction, "staff", StaffApplicationModal)
|
if not interaction.guild: return
|
||||||
|
category_id = await self.cog.config.guild(interaction.guild).staff_category()
|
||||||
|
await create_ticket(interaction, "staff", category_id, StaffApplicationModal())
|
||||||
|
|
||||||
@discord.ui.button(label="PM", style=discord.ButtonStyle.secondary, custom_id="pm_apply_button_persistent")
|
@discord.ui.button(label="PM", style=discord.ButtonStyle.secondary, custom_id="pm_apply_button")
|
||||||
async def pm_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
async def pm_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
await create_ticket(interaction, "pm", PMApplicationModal)
|
if not interaction.guild: return
|
||||||
|
category_id = await self.cog.config.guild(interaction.guild).pm_category()
|
||||||
|
await create_ticket(interaction, "pm", category_id, PMApplicationModal())
|
||||||
|
|
||||||
@discord.ui.button(label="HPM", style=discord.ButtonStyle.success, custom_id="hpm_apply_button_persistent")
|
@discord.ui.button(label="HPM", style=discord.ButtonStyle.success, custom_id="hpm_apply_button")
|
||||||
async def hpm_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
async def hpm_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
await create_ticket(interaction, "hpm", HPMApplicationModal)
|
if not interaction.guild: return
|
||||||
|
category_id = await self.cog.config.guild(interaction.guild).hpm_category()
|
||||||
|
await create_ticket(interaction, "hpm", category_id, HPMApplicationModal())
|
||||||
|
|
||||||
|
|
||||||
class WorkView(discord.ui.View):
|
class WorkView(discord.ui.View):
|
||||||
def __init__(self):
|
def __init__(self, cog: "Hiring"):
|
||||||
super().__init__(timeout=None)
|
super().__init__(timeout=None)
|
||||||
|
self.cog = cog
|
||||||
|
|
||||||
@discord.ui.button(label="Apply for PM Position", style=discord.ButtonStyle.green, custom_id="work_apply_pm_persistent")
|
@discord.ui.button(label="Apply for PM Position", style=discord.ButtonStyle.blurple, custom_id="work_apply_button")
|
||||||
async def work_apply_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
async def work_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
await create_ticket(interaction, "pm", PMApplicationModal)
|
if not interaction.guild: return
|
||||||
|
category_id = await self.cog.config.guild(interaction.guild).pm_category()
|
||||||
|
await create_ticket(interaction, "pm", category_id, PMApplicationModal())
|
||||||
|
|
||||||
|
|
||||||
# --- Main Cog Class ---
|
|
||||||
|
|
||||||
class Hiring(commands.Cog):
|
class Hiring(commands.Cog):
|
||||||
"""
|
"""
|
||||||
A cog for handling staff hiring applications.
|
A cog for managing staff and PM applications.
|
||||||
"""
|
"""
|
||||||
def __init__(self, bot: "Red"):
|
|
||||||
|
def __init__(self, bot: Red):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.config = Config.get_conf(self, identifier=1234567891, force_registration=True)
|
self.config = Config.get_conf(self, identifier=1122334455, force_registration=True)
|
||||||
|
|
||||||
default_guild = {
|
default_guild = {
|
||||||
"staff_category": None,
|
|
||||||
"pm_category": None,
|
|
||||||
"hpm_category": None,
|
"hpm_category": None,
|
||||||
"work_channel": None,
|
"pm_category": None,
|
||||||
"closed_applications": {}
|
"staff_category": None,
|
||||||
|
"work_channel": None # New setting for the /work command channel
|
||||||
}
|
}
|
||||||
self.config.register_guild(**default_guild)
|
self.config.register_guild(**default_guild)
|
||||||
|
# We need to make sure the views are persistent so buttons work after a restart
|
||||||
|
self.bot.add_view(HireView(self))
|
||||||
|
self.bot.add_view(WorkView(self))
|
||||||
|
|
||||||
async def cog_load(self):
|
|
||||||
self.bot.add_view(HireView())
|
|
||||||
self.bot.add_view(WorkView())
|
|
||||||
|
|
||||||
|
# --- Commands ---
|
||||||
@commands.hybrid_command() # type: ignore
|
@commands.hybrid_command() # type: ignore
|
||||||
@app_commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.admin_or_permissions(manage_guild=True)
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
async def hire(self, ctx: commands.Context):
|
async def hire(self, ctx: commands.Context):
|
||||||
"""Sends the hiring application view."""
|
"""Post the application message with buttons in the current channel."""
|
||||||
view = HireView()
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title="Hiring Applications",
|
title="Start an Application",
|
||||||
description="Please select the position you are applying for below.",
|
description="Click a button below to open a ticket for the role you are interested in.",
|
||||||
color=await ctx.embed_color()
|
color=0xadd8e6
|
||||||
)
|
)
|
||||||
await ctx.send(embed=embed, view=view)
|
await ctx.send(embed=embed, view=HireView(self))
|
||||||
|
|
||||||
@commands.hybrid_command() # type: ignore
|
@commands.hybrid_command() # type: ignore
|
||||||
@app_commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.admin_or_permissions(manage_guild=True)
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
async def work(self, ctx: commands.Context):
|
async def work(self, ctx: commands.Context):
|
||||||
"""Posts the persistent PM application button."""
|
"""Post the PM hiring message in the configured work channel."""
|
||||||
guild = ctx.guild
|
if not ctx.guild:
|
||||||
if not guild:
|
|
||||||
return
|
return
|
||||||
|
work_channel_id = await self.config.guild(ctx.guild).work_channel()
|
||||||
work_channel_id = await self.config.guild(guild).work_channel()
|
|
||||||
if not work_channel_id:
|
if not work_channel_id:
|
||||||
await ctx.send("The work channel has not been set. Please use `/hiringset workchannel`.")
|
await ctx.send("The work channel has not been set. Please use `[p]hiringset workchannel` to set it.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
channel = guild.get_channel(work_channel_id)
|
work_channel = ctx.guild.get_channel(work_channel_id)
|
||||||
if not isinstance(channel, discord.TextChannel):
|
if not isinstance(work_channel, discord.TextChannel):
|
||||||
await ctx.send("The configured work channel is invalid.")
|
await ctx.send("The configured work channel is invalid or has been deleted.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
view = WorkView()
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title="Partnership Manager Applications",
|
title="Now Hiring: Partnership Managers",
|
||||||
description="Click the button below to apply for a Partnership Manager (PM) position.",
|
description="We are for talented Partnership Managers (PMs) to join our team. If you are interested in applying, please click the button below to begin the application process.",
|
||||||
color=discord.Color.green()
|
color=0xadd8e6
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await channel.send(embed=embed, view=view)
|
await work_channel.send(embed=embed, view=WorkView(self))
|
||||||
await ctx.send(f"Application message posted in {channel.mention}.", ephemeral=True)
|
await ctx.send(f"Hiring message posted in {work_channel.mention}.", ephemeral=True)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await ctx.send("I don't have permission to send messages in that channel.", ephemeral=True)
|
await ctx.send(f"I don't have permission to send messages in {work_channel.mention}.", ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Settings Commands ---
|
||||||
@commands.group(aliases=["hset"]) # type: ignore
|
@commands.group(aliases=["hset"]) # type: ignore
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.admin_or_permissions(manage_guild=True)
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
async def hiringset(self, ctx: commands.Context):
|
async def hiringset(self, ctx: commands.Context):
|
||||||
"""Configure Hiring settings."""
|
"""Configure the Hiring cog settings."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@hiringset.command(name="staffcategory")
|
@hiringset.command(name="staffcategory")
|
||||||
async def set_staff_category(self, ctx: commands.Context, category: discord.CategoryChannel):
|
async def set_staff_category(self, ctx: commands.Context, category: discord.CategoryChannel):
|
||||||
"""Set the category for Staff applications."""
|
"""Set the category for Staff applications."""
|
||||||
if not ctx.guild:
|
if not ctx.guild: return
|
||||||
return
|
|
||||||
await self.config.guild(ctx.guild).staff_category.set(category.id)
|
await self.config.guild(ctx.guild).staff_category.set(category.id)
|
||||||
await ctx.send(f"Staff application category set to **{category.name}**.")
|
await ctx.send(f"Staff application category set to **{category.name}**.")
|
||||||
|
|
||||||
@hiringset.command(name="pmcategory")
|
@hiringset.command(name="pmcategory")
|
||||||
async def set_pm_category(self, ctx: commands.Context, category: discord.CategoryChannel):
|
async def set_pm_category(self, ctx: commands.Context, category: discord.CategoryChannel):
|
||||||
"""Set the category for PM applications."""
|
"""Set the category for PM applications."""
|
||||||
if not ctx.guild:
|
if not ctx.guild: return
|
||||||
return
|
|
||||||
await self.config.guild(ctx.guild).pm_category.set(category.id)
|
await self.config.guild(ctx.guild).pm_category.set(category.id)
|
||||||
await ctx.send(f"PM application category set to **{category.name}**.")
|
await ctx.send(f"PM application category set to **{category.name}**.")
|
||||||
|
|
||||||
@hiringset.command(name="hpmcategory")
|
@hiringset.command(name="hpmcategory")
|
||||||
async def set_hpm_category(self, ctx: commands.Context, category: discord.CategoryChannel):
|
async def set_hpm_category(self, ctx: commands.Context, category: discord.CategoryChannel):
|
||||||
"""Set the category for HPM applications."""
|
"""Set the category for HPM applications."""
|
||||||
if not ctx.guild:
|
if not ctx.guild: return
|
||||||
return
|
|
||||||
await self.config.guild(ctx.guild).hpm_category.set(category.id)
|
await self.config.guild(ctx.guild).hpm_category.set(category.id)
|
||||||
await ctx.send(f"HPM application category set to **{category.name}**.")
|
await ctx.send(f"HPM application category set to **{category.name}**.")
|
||||||
|
|
||||||
@hiringset.command(name="workchannel")
|
@hiringset.command(name="workchannel")
|
||||||
async def set_work_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
async def set_work_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
"""Set the channel for the /work command announcements."""
|
"""Set the channel where the /work hiring message will be posted."""
|
||||||
if not ctx.guild:
|
if not ctx.guild: return
|
||||||
return
|
|
||||||
await self.config.guild(ctx.guild).work_channel.set(channel.id)
|
await self.config.guild(ctx.guild).work_channel.set(channel.id)
|
||||||
await ctx.send(f"Work announcement channel set to {channel.mention}.")
|
await ctx.send(f"The work channel has been set to {channel.mention}.")
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
|
async def setup(bot: Red):
|
||||||
await bot.add_cog(Hiring(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!",
|
||||||
|
|||||||
1
kofishop/__init__.py
Normal file
1
kofishop/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .kofishop import setup
|
||||||
@@ -1,85 +1,94 @@
|
|||||||
import discord
|
import discord
|
||||||
from redbot.core import commands, Config, app_commands
|
from redbot.core import commands, Config
|
||||||
from typing import Optional, TYPE_CHECKING
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
class OrderModal(discord.ui.Modal, title="Commission Order Form"):
|
# --- Modals for the Commands ---
|
||||||
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)
|
|
||||||
|
|
||||||
|
class OrderModal(discord.ui.Modal, title="Commission/Shop Order"):
|
||||||
def __init__(self, cog: "KofiShop"):
|
def __init__(self, cog: "KofiShop"):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.cog = cog
|
self.cog = cog
|
||||||
|
|
||||||
|
comm_type = discord.ui.TextInput(label="What type of commission/item is this?")
|
||||||
|
payment_status = discord.ui.TextInput(label="Is this free or paid?")
|
||||||
|
description = discord.ui.TextInput(label="Please describe your request.", style=discord.TextStyle.paragraph)
|
||||||
|
questions = discord.ui.TextInput(label="Any questions for the artist?", style=discord.TextStyle.paragraph, required=False)
|
||||||
|
|
||||||
async def on_submit(self, interaction: discord.Interaction):
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
guild = interaction.guild
|
if not interaction.guild:
|
||||||
if not guild:
|
return await interaction.response.send_message("This must be used in a server.", ephemeral=True)
|
||||||
return
|
|
||||||
|
|
||||||
channel_id = await self.cog.config.guild(guild).order_channel()
|
order_channel_id = await self.cog.config.guild(interaction.guild).order_channel()
|
||||||
if not channel_id:
|
if not order_channel_id:
|
||||||
await interaction.response.send_message("Order channel not set.", ephemeral=True)
|
return await interaction.response.send_message("The order channel has not been set by an admin.", ephemeral=True)
|
||||||
return
|
|
||||||
|
|
||||||
channel = guild.get_channel(channel_id)
|
order_channel = interaction.guild.get_channel(order_channel_id)
|
||||||
if not isinstance(channel, discord.TextChannel):
|
if not isinstance(order_channel, discord.TextChannel):
|
||||||
await interaction.response.send_message("Invalid order channel.", ephemeral=True)
|
return 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 = discord.Embed(
|
||||||
embed.add_field(name="Commission Type", value=self.commission_type.value, inline=False)
|
title="New Order Placed",
|
||||||
|
description=f"Submitted by {interaction.user.mention}",
|
||||||
|
color=0x00ff00 # Green for new orders
|
||||||
|
)
|
||||||
|
embed.add_field(name="Item/Commission Type", value=self.comm_type.value, inline=False)
|
||||||
embed.add_field(name="Payment Status", value=self.payment_status.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)
|
embed.add_field(name="Description", value=self.description.value, inline=False)
|
||||||
if self.questions.value:
|
if self.questions.value:
|
||||||
embed.add_field(name="Questions", value=self.questions.value, inline=False)
|
embed.add_field(name="Questions", value=self.questions.value, inline=False)
|
||||||
|
|
||||||
await channel.send(embed=embed)
|
try:
|
||||||
await interaction.response.send_message("Your order has been submitted!", ephemeral=True)
|
await order_channel.send(embed=embed)
|
||||||
|
await interaction.response.send_message("Your order has been successfully submitted!", ephemeral=True)
|
||||||
class ReviewModal(discord.ui.Modal, title="Shop Review"):
|
except discord.Forbidden:
|
||||||
item_name = discord.ui.TextInput(label="What item/commission are you reviewing?")
|
await interaction.response.send_message("I don't have permission to send messages in the order channel.", ephemeral=True)
|
||||||
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)
|
|
||||||
|
|
||||||
|
class ReviewModal(discord.ui.Modal, title="Leave a Review"):
|
||||||
def __init__(self, cog: "KofiShop"):
|
def __init__(self, cog: "KofiShop"):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.cog = cog
|
self.cog = cog
|
||||||
|
|
||||||
|
item_name = discord.ui.TextInput(label="What item/commission are you reviewing?")
|
||||||
|
rating = discord.ui.TextInput(label="Rating (e.g., 10/10)")
|
||||||
|
review_text = discord.ui.TextInput(label="Your Review", style=discord.TextStyle.paragraph)
|
||||||
|
|
||||||
async def on_submit(self, interaction: discord.Interaction):
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
guild = interaction.guild
|
if not interaction.guild:
|
||||||
if not guild:
|
return await interaction.response.send_message("This must be used in a server.", ephemeral=True)
|
||||||
return
|
|
||||||
|
|
||||||
channel_id = await self.cog.config.guild(guild).review_channel()
|
review_channel_id = await self.cog.config.guild(interaction.guild).review_channel()
|
||||||
if not channel_id:
|
if not review_channel_id:
|
||||||
await interaction.response.send_message("Review channel not set.", ephemeral=True)
|
return await interaction.response.send_message("The review channel has not been set by an admin.", ephemeral=True)
|
||||||
return
|
|
||||||
|
|
||||||
channel = guild.get_channel(channel_id)
|
review_channel = interaction.guild.get_channel(review_channel_id)
|
||||||
if not isinstance(channel, discord.TextChannel):
|
if not isinstance(review_channel, discord.TextChannel):
|
||||||
await interaction.response.send_message("Invalid review channel.", ephemeral=True)
|
return 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 = discord.Embed(
|
||||||
embed.set_author(name=interaction.user.name, icon_url=interaction.user.display_avatar.url)
|
title=f"New Review for: {self.item_name.value}",
|
||||||
embed.add_field(name="Rating", value=f"{self.rating.value}/10")
|
description=f"Submitted by {interaction.user.mention}",
|
||||||
|
color=0xadd8e6 # Pastel Blue
|
||||||
|
)
|
||||||
|
embed.add_field(name="Rating", value=self.rating.value, inline=False)
|
||||||
embed.add_field(name="Review", value=self.review_text.value, inline=False)
|
embed.add_field(name="Review", value=self.review_text.value, inline=False)
|
||||||
|
|
||||||
await channel.send(embed=embed)
|
try:
|
||||||
await interaction.response.send_message("Thank you for your review!", ephemeral=True)
|
await review_channel.send(embed=embed)
|
||||||
|
await interaction.response.send_message("Thank you! Your review has been submitted.", ephemeral=True)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await interaction.response.send_message("I don't have permission to send messages in the review channel.", ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
class KofiShop(commands.Cog):
|
class KofiShop(commands.Cog):
|
||||||
"""
|
"""
|
||||||
An interactive front-end for a Ko-fi store.
|
A cog to manage Ko-fi shop orders and reviews.
|
||||||
"""
|
"""
|
||||||
def __init__(self, bot: "Red"):
|
|
||||||
|
def __init__(self, bot: Red):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.config = Config.get_conf(self, identifier=1234567894, force_registration=True)
|
self.config = Config.get_conf(self, identifier=5566778899, force_registration=True)
|
||||||
|
|
||||||
default_guild = {
|
default_guild = {
|
||||||
"order_channel": None,
|
"order_channel": None,
|
||||||
"review_channel": None,
|
"review_channel": None,
|
||||||
@@ -87,70 +96,80 @@ class KofiShop(commands.Cog):
|
|||||||
}
|
}
|
||||||
self.config.register_guild(**default_guild)
|
self.config.register_guild(**default_guild)
|
||||||
|
|
||||||
@app_commands.command()
|
# --- Commands ---
|
||||||
@app_commands.guild_only()
|
@commands.hybrid_command()
|
||||||
async def order(self, interaction: discord.Interaction):
|
@commands.guild_only()
|
||||||
"""Place an order for a commission."""
|
async def order(self, ctx: commands.Context):
|
||||||
await interaction.response.send_modal(OrderModal(self))
|
"""Place an order for a shop or commission item."""
|
||||||
|
if not ctx.interaction:
|
||||||
@app_commands.command()
|
|
||||||
@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))
|
|
||||||
|
|
||||||
@app_commands.command()
|
|
||||||
@app_commands.guild_only()
|
|
||||||
async def waitlist(self, interaction: discord.Interaction, *, item: str):
|
|
||||||
"""Add an item to the waitlist."""
|
|
||||||
guild = interaction.guild
|
|
||||||
if not guild:
|
|
||||||
return
|
return
|
||||||
|
# We pass `self` (the cog instance) to the modal
|
||||||
|
await ctx.interaction.response.send_modal(OrderModal(self))
|
||||||
|
|
||||||
channel_id = await self.config.guild(guild).waitlist_channel()
|
@commands.hybrid_command()
|
||||||
if not channel_id:
|
@commands.guild_only()
|
||||||
await interaction.response.send_message("Waitlist channel not set.", ephemeral=True)
|
async def review(self, ctx: commands.Context):
|
||||||
|
"""Leave a review for a completed shop or commission item."""
|
||||||
|
if not ctx.interaction:
|
||||||
return
|
return
|
||||||
|
await ctx.interaction.response.send_modal(ReviewModal(self))
|
||||||
|
|
||||||
channel = guild.get_channel(channel_id)
|
@commands.hybrid_command() # type: ignore
|
||||||
if not isinstance(channel, discord.TextChannel):
|
@commands.guild_only()
|
||||||
await interaction.response.send_message("Invalid waitlist channel.", ephemeral=True)
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
return
|
async def waitlist(self, ctx: commands.Context, user: discord.Member, *, item: str):
|
||||||
|
"""Add a user and their requested item to the waitlist."""
|
||||||
|
if not ctx.guild or not isinstance(ctx.channel, discord.TextChannel):
|
||||||
|
return await ctx.send("This command must be used in a server's text channel.", ephemeral=True)
|
||||||
|
|
||||||
embed = discord.Embed(description=f"{item} ིྀ {interaction.user.mention}✧ {interaction.channel.mention if isinstance(interaction.channel, discord.TextChannel) else ''}")
|
waitlist_channel_id = await self.config.guild(ctx.guild).waitlist_channel()
|
||||||
await channel.send(embed=embed)
|
if not waitlist_channel_id:
|
||||||
await interaction.response.send_message("You have been added to the waitlist!", ephemeral=True)
|
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 = f"**{item}** ིྀ {user.mention} ✧ in {ctx.channel.mention}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
await waitlist_channel.send(message)
|
||||||
|
await ctx.send(f"{user.mention} has been added to the waitlist for '{item}'.", ephemeral=True)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await ctx.send(f"I don't have permission to send messages in the waitlist channel.", ephemeral=True)
|
||||||
|
|
||||||
|
# --- Settings Commands ---
|
||||||
@commands.group(aliases=["kset"]) # type: ignore
|
@commands.group(aliases=["kset"]) # type: ignore
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.admin_or_permissions(manage_guild=True)
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
async def kofiset(self, ctx: commands.Context):
|
async def kofiset(self, ctx: commands.Context):
|
||||||
"""Configure KofiShop settings."""
|
"""
|
||||||
|
Configure the KofiShop settings.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@kofiset.command(name="orderchannel")
|
@kofiset.command(name="orderchannel")
|
||||||
async def set_order_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
async def kofiset_order(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
"""Set the channel for new orders."""
|
"""Set the channel where new orders will be sent."""
|
||||||
if not ctx.guild:
|
if not ctx.guild: return
|
||||||
return
|
|
||||||
await self.config.guild(ctx.guild).order_channel.set(channel.id)
|
await self.config.guild(ctx.guild).order_channel.set(channel.id)
|
||||||
await ctx.send(f"Order channel set to {channel.mention}")
|
await ctx.send(f"Order channel has been set to {channel.mention}.")
|
||||||
|
|
||||||
@kofiset.command(name="reviewchannel")
|
@kofiset.command(name="reviewchannel")
|
||||||
async def set_review_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
async def kofiset_review(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
"""Set the channel for new reviews."""
|
"""Set the channel where new reviews will be sent."""
|
||||||
if not ctx.guild:
|
if not ctx.guild: return
|
||||||
return
|
|
||||||
await self.config.guild(ctx.guild).review_channel.set(channel.id)
|
await self.config.guild(ctx.guild).review_channel.set(channel.id)
|
||||||
await ctx.send(f"Review channel set to {channel.mention}")
|
await ctx.send(f"Review channel has been set to {channel.mention}.")
|
||||||
|
|
||||||
@kofiset.command(name="waitlistchannel")
|
@kofiset.command(name="waitlistchannel")
|
||||||
async def set_waitlist_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
async def kofiset_waitlist(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
"""Set the channel for waitlist notifications."""
|
"""Set the channel where waitlist notifications will be sent."""
|
||||||
if not ctx.guild:
|
if not ctx.guild: return
|
||||||
return
|
|
||||||
await self.config.guild(ctx.guild).waitlist_channel.set(channel.id)
|
await self.config.guild(ctx.guild).waitlist_channel.set(channel.id)
|
||||||
await ctx.send(f"Waitlist channel set to {channel.mention}")
|
await ctx.send(f"Waitlist channel has been set to {channel.mention}.")
|
||||||
|
|
||||||
async def setup(bot: "Red"):
|
|
||||||
|
async def setup(bot: Red):
|
||||||
await bot.add_cog(KofiShop(bot))
|
await bot.add_cog(KofiShop(bot))
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
from .logging import Logging
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(Logging(bot))
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
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 @@
|
|||||||
|
from .modmail import setup
|
||||||
@@ -1,90 +1,218 @@
|
|||||||
import discord
|
import discord
|
||||||
import datetime
|
from redbot.core import commands, Config
|
||||||
from redbot.core import commands, Config, app_commands
|
from redbot.core.bot import Red
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
class Modmail(commands.Cog):
|
class ModMail(commands.Cog):
|
||||||
"""
|
"""
|
||||||
A private, forum-based ModMail system.
|
A configurable, forum-based ModMail system.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot: Red):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.config = Config.get_conf(self, identifier=1234567890, force_registration=True)
|
# Initialize Red's Config system for storing settings per-server.
|
||||||
|
self.config = Config.get_conf(self, identifier=9876543210, force_registration=True)
|
||||||
|
|
||||||
|
# Define the default settings for each server.
|
||||||
default_guild = {
|
default_guild = {
|
||||||
"forum_channel": None,
|
"modmail_forum": None, # The ID of the forum channel for tickets
|
||||||
"enabled": False,
|
"enabled": False, # Whether the system is on or off
|
||||||
"active_threads": {},
|
"active_threads": {} # A dictionary to track {user_id: thread_id}
|
||||||
"closed_threads": {} # NEW: To log closed tickets for purging
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Register the default settings.
|
||||||
self.config.register_guild(**default_guild)
|
self.config.register_guild(**default_guild)
|
||||||
|
|
||||||
# ... existing on_message listener ...
|
|
||||||
@commands.Cog.listener()
|
@commands.Cog.listener()
|
||||||
async def on_message(self, message: discord.Message):
|
async def on_message(self, message: discord.Message):
|
||||||
|
"""
|
||||||
|
This is the core event listener. It handles both incoming DMs from users
|
||||||
|
and outgoing replies from staff.
|
||||||
|
"""
|
||||||
|
# Ignore messages from bots to prevent loops.
|
||||||
if message.author.bot:
|
if message.author.bot:
|
||||||
return
|
return
|
||||||
|
|
||||||
# --- User to Staff DM Logic ---
|
# --- Part 1: Handling DMs from Users ---
|
||||||
if isinstance(message.channel, discord.DMChannel):
|
if isinstance(message.channel, discord.DMChannel):
|
||||||
# ... existing user DM logic ...
|
await self.handle_dm(message)
|
||||||
pass
|
|
||||||
|
|
||||||
# --- Staff to User Reply Logic ---
|
# --- Part 2: Handling Replies from Staff ---
|
||||||
elif isinstance(message.channel, discord.Thread):
|
elif isinstance(message.channel, discord.Thread):
|
||||||
# ... existing staff reply logic ...
|
await self.handle_staff_reply(message)
|
||||||
pass
|
|
||||||
|
|
||||||
@app_commands.command(name="close")
|
async def handle_dm(self, message: discord.Message):
|
||||||
@app_commands.guild_only()
|
"""Handles messages sent directly to the bot."""
|
||||||
async def modmail_close(self, interaction: discord.Interaction, *, reason: Optional[str] = "No reason provided."):
|
# Find a mutual server with the user.
|
||||||
"""Close the current ModMail ticket."""
|
guild = next((g for g in self.bot.guilds if g.get_member(message.author.id)), None)
|
||||||
guild = interaction.guild
|
|
||||||
if not guild:
|
if not guild:
|
||||||
await interaction.response.send_message("This command can only be used in a server.", ephemeral=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# NEW: Safely check if the interaction has a channel
|
settings = await self.config.guild(guild).all()
|
||||||
if not interaction.channel:
|
if not settings["enabled"] or not settings["modmail_forum"]:
|
||||||
await interaction.response.send_message("This command cannot be used in this context.", ephemeral=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# ... existing logic to check if it's a modmail thread ...
|
forum_channel = guild.get_channel(settings["modmail_forum"])
|
||||||
|
if not isinstance(forum_channel, discord.ForumChannel):
|
||||||
|
return
|
||||||
|
|
||||||
thread_id_str = str(interaction.channel.id)
|
active_threads = settings["active_threads"]
|
||||||
async with self.config.guild(guild).active_threads() as active_threads:
|
user_id_str = str(message.author.id)
|
||||||
user_id = active_threads.pop(thread_id_str, None)
|
|
||||||
|
|
||||||
if user_id:
|
# Check if the user already has an active thread.
|
||||||
# NEW: Log the closed thread with a timestamp
|
if user_id_str in active_threads:
|
||||||
async with self.config.guild(guild).closed_threads() as closed_threads:
|
thread_id = active_threads[user_id_str]
|
||||||
closed_threads[thread_id_str] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
thread = guild.get_thread(thread_id)
|
||||||
|
if thread:
|
||||||
# ... existing logic to send final message and archive thread ...
|
# Relay the message to the existing thread.
|
||||||
user = self.bot.get_user(int(user_id))
|
await thread.send(f"**{message.author.display_name}:** {message.content}")
|
||||||
if user:
|
await message.add_reaction("✅")
|
||||||
embed = discord.Embed(
|
return
|
||||||
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:
|
else:
|
||||||
await interaction.response.send_message("This does not appear to be an active ModMail ticket.", ephemeral=True)
|
# The thread was deleted, so we clean up our records.
|
||||||
|
async with self.config.guild(guild).active_threads() as threads:
|
||||||
|
del threads[user_id_str]
|
||||||
|
|
||||||
|
# Create a new thread for the user.
|
||||||
|
try:
|
||||||
|
thread_name = f"ModMail | {message.author.name}"
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"New ModMail Thread",
|
||||||
|
description=f"**User:** {message.author.mention} (`{message.author.id}`)",
|
||||||
|
color=0xadd8e6 # Light grey pastel blue
|
||||||
|
)
|
||||||
|
embed.add_field(name="Initial Message", value=message.content, inline=False)
|
||||||
|
embed.set_footer(text="Staff can reply in this thread to send a message.")
|
||||||
|
|
||||||
|
thread_with_message = await forum_channel.create_thread(name=thread_name, embed=embed)
|
||||||
|
thread = thread_with_message.thread
|
||||||
|
|
||||||
|
async with self.config.guild(guild).active_threads() as threads:
|
||||||
|
threads[user_id_str] = thread.id
|
||||||
|
|
||||||
|
await message.channel.send("Your message has been received, and a ModMail ticket has been opened. Staff will be with you shortly.")
|
||||||
|
await message.add_reaction("✅")
|
||||||
|
except discord.Forbidden:
|
||||||
|
print(f"ModMail: I don't have permission to create threads in {forum_channel.name}.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ModMail: An unexpected error occurred: {e}")
|
||||||
|
|
||||||
|
async def handle_staff_reply(self, message: discord.Message):
|
||||||
|
"""Handles messages sent by staff inside a ModMail thread."""
|
||||||
|
guild = message.guild
|
||||||
|
if not guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
active_threads = await self.config.guild(guild).active_threads()
|
||||||
|
|
||||||
|
# Find which user this thread belongs to by checking our records.
|
||||||
|
thread_id_str = str(message.channel.id)
|
||||||
|
user_id = None
|
||||||
|
for uid, tid in active_threads.items():
|
||||||
|
if str(tid) == thread_id_str:
|
||||||
|
user_id = int(uid)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
# This is a regular thread, not a ModMail thread we're tracking.
|
||||||
|
return
|
||||||
|
|
||||||
|
user = guild.get_member(user_id)
|
||||||
|
if not user:
|
||||||
|
# User might have left the server.
|
||||||
|
await message.channel.send("⚠️ **Error:** Could not find the user. They may have left the server.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Send the staff's message to the user's DMs.
|
||||||
|
try:
|
||||||
|
embed = discord.Embed(
|
||||||
|
description=message.content,
|
||||||
|
color=0xadd8e6 # Light grey pastel blue
|
||||||
|
)
|
||||||
|
embed.set_author(name="Staff Response") # Anonymize the staff member
|
||||||
|
|
||||||
|
await user.send(embed=embed)
|
||||||
|
await message.add_reaction("📨") # Add a mail icon to show it was sent
|
||||||
|
except discord.Forbidden:
|
||||||
|
await message.channel.send("⚠️ **Error:** I could not send a DM to this user. They may have DMs disabled.")
|
||||||
|
except Exception as e:
|
||||||
|
await message.channel.send(f"⚠️ **Error:** An unexpected error occurred: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Settings and Management Commands ---
|
||||||
@commands.group(aliases=["mmset"]) # type: ignore
|
@commands.group(aliases=["mmset"]) # type: ignore
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.admin_or_permissions(manage_guild=True)
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
async def modmailset(self, ctx: commands.Context):
|
async def modmailset(self, ctx: commands.Context):
|
||||||
"""Configure ModMail settings."""
|
"""
|
||||||
|
Configure the ModMail settings for this server.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# ... existing modmailset subcommands ...
|
@modmailset.command(name="forum")
|
||||||
|
async def modmailset_forum(self, ctx: commands.Context, channel: discord.ForumChannel):
|
||||||
|
"""Set the forum channel where ModMail tickets will be created."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
await self.config.guild(ctx.guild).modmail_forum.set(channel.id)
|
||||||
|
await ctx.send(f"The ModMail forum has been set to {channel.mention}.")
|
||||||
|
|
||||||
|
@modmailset.command(name="toggle")
|
||||||
|
async def modmailset_toggle(self, ctx: commands.Context):
|
||||||
|
"""Enable or disable the ModMail system on this server."""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
current_status = await self.config.guild(ctx.guild).enabled()
|
||||||
|
new_status = not current_status
|
||||||
|
await self.config.guild(ctx.guild).enabled.set(new_status)
|
||||||
|
status_text = "enabled" if new_status else "disabled"
|
||||||
|
await ctx.send(f"The ModMail system has been {status_text}.")
|
||||||
|
|
||||||
|
@modmailset.command(name="close")
|
||||||
|
async def modmailset_close(self, ctx: commands.Context, *, reason: Optional[str] = "No reason provided."):
|
||||||
|
"""
|
||||||
|
Close the current ModMail thread.
|
||||||
|
|
||||||
|
You must run this command inside the thread you wish to close.
|
||||||
|
"""
|
||||||
|
if not ctx.guild or not isinstance(ctx.channel, discord.Thread):
|
||||||
|
await ctx.send("This command can only be run inside a ModMail thread.")
|
||||||
|
return
|
||||||
|
|
||||||
|
active_threads = await self.config.guild(ctx.guild).active_threads()
|
||||||
|
thread_id_str = str(ctx.channel.id)
|
||||||
|
user_id = None
|
||||||
|
for uid, tid in active_threads.items():
|
||||||
|
if str(tid) == thread_id_str:
|
||||||
|
user_id = int(uid)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
await ctx.send("This does not appear to be an active ModMail thread.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Clean up our records.
|
||||||
|
async with self.config.guild(ctx.guild).active_threads() as threads:
|
||||||
|
del threads[str(user_id)]
|
||||||
|
|
||||||
|
# Notify the user.
|
||||||
|
user = self.bot.get_user(user_id)
|
||||||
|
if user:
|
||||||
|
try:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="ModMail Ticket Closed",
|
||||||
|
description=f"Your ticket has been closed by staff.\n\n**Reason:** {reason}",
|
||||||
|
color=0xadd8e6
|
||||||
|
)
|
||||||
|
await user.send(embed=embed)
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass # Can't notify user if DMs are closed
|
||||||
|
|
||||||
|
# Archive the thread.
|
||||||
|
await ctx.send(f"Ticket closed by {ctx.author.mention}. Archiving thread...")
|
||||||
|
await ctx.channel.edit(archived=True, locked=True)
|
||||||
|
|
||||||
|
# This required function allows Red to load the cog.
|
||||||
|
async def setup(bot: Red):
|
||||||
|
await bot.add_cog(ModMail(bot))
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
from .mors import MORS
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(MORS(bot))
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
{
|
{
|
||||||
"author": [
|
"author": [ "unstableCogs" ],
|
||||||
"unstableCogs"
|
"install_msg": "Thank you for installing the Mass Outreach & Review System! For a full command list, please see the wiki.",
|
||||||
],
|
"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, including ad submission, time selection, and final 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.",
|
||||||
"tags": [
|
"tags": [
|
||||||
"mass",
|
"mass",
|
||||||
"outreach",
|
"outreach",
|
||||||
@@ -13,6 +11,6 @@
|
|||||||
"tickets",
|
"tickets",
|
||||||
"utility"
|
"utility"
|
||||||
],
|
],
|
||||||
"requirements": [],
|
"requirements": [ "rich" ],
|
||||||
"end_user_data_statement": "This cog stores user IDs, ticket information, and submitted reviews persistently for record-keeping purposes."
|
"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."
|
||||||
}
|
}
|
||||||
444
mors/mors.py
444
mors/mors.py
@@ -1,444 +0,0 @@
|
|||||||
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,4 +0,0 @@
|
|||||||
from .servicereview import ServiceReview
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(ServiceReview(bot))
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
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))
|
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
from .staffmsg import StaffMsg
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(StaffMsg(bot))
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
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))
|
||||||
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."
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user