Reaction roles working. Management of message and reactions is messy and needs reinforcement.

This commit is contained in:
DefsNotQuack
2025-03-29 15:59:18 +10:00
parent c9f6a4c55d
commit e00d1fdce3
6 changed files with 331 additions and 8 deletions

132
cogs/react_roles.py Normal file
View File

@@ -0,0 +1,132 @@
import os
import discord
from discord.ext import commands
from data import react_roles
from util.console import console, panel, track_iterable as track
GUILD_IDS = [int(os.getenv("DEV_GUILD_ID"))]
REACTION_ROLE_CHANNEL_ID = int(os.getenv("REACT_ROLE_CHANNEL_ID"))
def update_react_role_embed(category_name):
# Generate role list
roles = react_roles.get_react_roles_by_category(category_name)
role_list = []
for role in roles:
emoji = role[2]
description = role[3]
role_list.append(f"{emoji} - {description}")
# Create an embed for the react-role category
embed = discord.Embed(title=f"**{category_name.capitalize()} Roles**",
description="React to this message to gain access to the relevant text and voice channels.\n\n" + "\n".join(role_list),
color=discord.Color.dark_orange())
return embed
class ReactRoles(commands.Cog):
def __init__(self, bot):
self.bot = bot
# Slash command to add a new reaction role Category / Message
@commands.slash_command(name="new_react_role_category",
description="Create a new category of react roles",
guild_ids=GUILD_IDS)
@commands.has_permissions(manage_roles=True)
async def new_react_role_category(self, ctx: discord.ApplicationContext, category_name: str):
# Verify the category is unique
existing_categories = react_roles.get_react_roles_categories()
if category_name.lower() in [cat[1] for cat in existing_categories]:
await ctx.respond(f"Category '{category_name}' already exists.", ephemeral=True)
return
# Send a new Embed message
embed = discord.Embed(title=f"**{category_name.capitalize()} Roles**",
description="React to this message to gain access to the relevant text and voice channels.",
color=discord.Color.dark_orange())
message = await ctx.guild.get_channel(REACTION_ROLE_CHANNEL_ID).send(embed=embed)
# Create the category in the database
react_roles.add_react_role_category_to_db(message.id, category_name)
# Send a confirmation message
await ctx.respond(f"React role category '{category_name}' created successfully!", ephemeral=True)
#Slash command to add a new reaction role to an existing category
@commands.slash_command(name="new_react_role",
description="Create a new react role",
guild_ids=GUILD_IDS)
@commands.has_permissions(manage_roles=True)
async def new_react_role(self, ctx: discord.ApplicationContext, category_name: str, role: discord.Role, emoji: str, description: str):
# Verify the category exists
existing_categories = react_roles.get_react_roles_categories()
if category_name.lower() not in [cat[1] for cat in existing_categories]:
await ctx.respond(f"Category '{category_name}' does not exist.", ephemeral=True)
return
# Get the message ID of the category
message_id = next(cat[0] for cat in existing_categories if cat[1] == category_name.lower())
# Add the react-role to the database
react_roles.add_react_role_to_db(message_id, role.id, emoji, description)
message = await self.bot.get_channel(REACTION_ROLE_CHANNEL_ID).fetch_message(message_id)
await message.edit(embed=update_react_role_embed(category_name))
await message.add_reaction(emoji)
# Send a confirmation message
await ctx.respond(f"React role '{emoji}' - '{role.name}' added to category '{category_name}'.", ephemeral=True)
# Event listener for reaction add
@commands.Cog.listener()
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
# Check if the reaction is in the correct channel and not from the bot itself
if payload.channel_id != REACTION_ROLE_CHANNEL_ID or payload.user_id == self.bot.user.id:
return
# Ge all react roles for the message_id that was reacted to
message_id = payload.message_id
emoji = str(payload.emoji)
react_roles_list = react_roles.get_react_roles_by_message_id(message_id)
if react_roles_list:
for role in react_roles_list:
if role[2] == emoji:
guild = self.bot.get_guild(payload.guild_id)
member = payload.member
role_obj = guild.get_role(role[1])
# Add the role to the member
if role_obj not in member.roles:
await member.add_roles(role_obj)
console.log (f"[green]✔ Added role:[/] {emoji} {role_obj.name} to {member.name}")
# Event Listener for reaction remove
@commands.Cog.listener()
async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent):
# Check if the reaction is in the correct channel and not from the bot itself
if payload.channel_id != REACTION_ROLE_CHANNEL_ID or payload.user_id == self.bot.user.id:
return
# Ge all react roles for the message_id that was reacted to
message_id = payload.message_id
emoji = str(payload.emoji)
react_roles_list = react_roles.get_react_roles_by_message_id(message_id)
if react_roles_list:
for role in react_roles_list:
if role[2] == emoji:
guild = self.bot.get_guild(payload.guild_id)
# Member object is not available in the payload, so we need to fetch it...
member = await guild.fetch_member(payload.user_id)
role_obj = guild.get_role(role[1])
# Add the role to the member
if role_obj in member.roles:
await member.remove_roles(role_obj)
console.log (f"[red]✖ Removed role:[/] {emoji} {role_obj.name} from {member.name}")
def setup(bot):
bot.add_cog(ReactRoles(bot))

4
data/init.py Normal file
View File

@@ -0,0 +1,4 @@
import data.react_roles as data_react_roles
def init_databses():
data_react_roles.init_db()

144
data/react_roles.py Normal file
View File

@@ -0,0 +1,144 @@
import sqlite3
DB_PATH = 'data.db'
def init_db():
"""
Initialize the database and create necessary tables.
"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Create the react_roles table
cursor.execute('''
CREATE TABLE IF NOT EXISTS react_roles (
message_id INTEGER,
role_id INTEGER,
emoji TEXT,
description TEXT
)
''')
# Create the react_role_categories table
cursor.execute('''
CREATE TABLE IF NOT EXISTS react_role_categories (
message_id INTEGER,
category_name TEXT
)
''')
conn.commit()
conn.close()
def add_react_role_category_to_db(message_id: int, category_name: str):
"""
Add a new react role category to the database.
:param message_id: The ID of the message to which the category is associated.
:param category_name: The name of the category (e.g., "Category1").
"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Create the table if it doesn't exist
cursor.execute('''
CREATE TABLE IF NOT EXISTS react_role_categories (
message_id INTEGER,
category_name TEXT
)
''')
# Insert the new react-role category into the database
cursor.execute('''
INSERT INTO react_role_categories (message_id, category_name)
VALUES (?, ?)
''', (message_id, category_name.lower()))
conn.commit()
conn.close()
def add_react_role_to_db(message_id: int, role_id: int, emoji: str, description: str):
"""
Add a new react role to the database.
:param message_id: The message ID of the react-role message.
:param role_id: The ID of the role to assign.
:param emoji: The emoji to react with.
:param description: The description of the role.
"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Create the table if it doesn't exist
cursor.execute('''
CREATE TABLE IF NOT EXISTS react_roles (
message_id INTEGER,
role_id INTEGER,
emoji TEXT,
description TEXT
)
''')
# Insert the new react-role into the database
cursor.execute('''
INSERT INTO react_roles (message_id, role_id, emoji, description)
VALUES (?, ?, ?, ?)
''', (message_id, role_id, emoji, description))
conn.commit()
conn.close()
def get_react_roles_categories():
"""
Get all react role categories from the database.
:return: A list of tuples containing message_id and category_name.
"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Fetch all react-role categories from the database
cursor.execute('''
SELECT * FROM react_role_categories
''')
categories = cursor.fetchall()
conn.close()
return categories
def get_react_roles_by_category(category_name: str):
"""
Get all react roles for a specific category from the database.
:param category_name: The name of the category (e.g., "Category1").
:return: A list of tuples containing message_id, role_id, emoji, and description.
"""
# Get the message id of the category
categories = get_react_roles_categories()
message_id = next((cat[0] for cat in categories if cat[1] == category_name.lower()), None)
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Fetch all react-roles for the specified category from the database
cursor.execute('''
SELECT * FROM react_roles WHERE message_id = ?
''', (message_id,))
roles = cursor.fetchall()
conn.close()
return roles
def get_react_roles_by_message_id(message_id: int):
"""
Get all react roles for a specific message ID from the database.
:param message_id: The ID of the message to which the roles are associated.
:return: A list of tuples containing message_id, role_id, emoji, and description.
"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Fetch all react-roles for the specified message ID from the database
cursor.execute('''
SELECT * FROM react_roles WHERE message_id = ?
''', (message_id,))
roles = cursor.fetchall()
conn.close()
return roles

25
main.py
View File

@@ -6,17 +6,14 @@ from dotenv import load_dotenv
import signal
from pathlib import Path
from pipenv.patched.safety.safety import session
from rich.console import Console
from rich.progress import track
from rich.panel import Panel
from util.console import console, panel, track_iterable as track
from data.init import init_databses
# Load environment variables from .env file
load_dotenv()
TOKEN = os.getenv("BOT_TOKEN")
DEV_GUILD_ID = int(os.getenv("DEV_GUILD_ID"))
# Initialize the console for rich output
console = Console()
REACT_ROLE_CHANNEL_ID = int(os.getenv("REACT_ROLE_CHANNEL_ID"))
# Set up intents for the bot
intents = discord.Intents.default()
@@ -30,6 +27,14 @@ COG_PATH = Path(__file__).resolve().parent / "cogs"
cogs_available = {}
loaded_cog_modules = {}
# Arguments used in cog loading
COG_CONFIG = {
"react_roles": {
"guild_ids": [DEV_GUILD_ID],
"react_channel_id": REACT_ROLE_CHANNEL_ID
}
}
for file in COG_PATH.iterdir():
if file.suffix == ".py" and not file.name.startswith("__"):
cog_name = file.stem
@@ -44,11 +49,13 @@ async def load_cogs():
console.log(f"[red]❌ Cogs directory not found at {COG_PATH}[/red]")
return
console.print(Panel.fit("🔌 [bold]Loading Cogs[/bold]", style="cyan"))
console.print(panel(content="🔌 [bold]Loading Cogs[/bold]", style="cyan"))
for cog in track(cogs_available, description="Loading Cogs..."):
module_path = f"cogs.{cog}"
config = COG_CONFIG.get(cog, {})
try:
bot.load_extension(module_path)
loaded_cog_modules[cog] = module_path
@@ -74,6 +81,8 @@ async def on_ready():
await load_cogs()
await bot.sync_commands()
console.log("[blue]🔁 Synced slash commands[/blue]")
init_databses()
console.log("[blue]💽 Initialized databases[/blue]")
console.rule(f"[bold green]✅ Bot Ready — Logged in as {bot.user}[/]")
console.print(f"ID: {bot.user.id}")
handle_signals()

View File

@@ -0,0 +1,17 @@
import discord
from data import react_roles
def update_react_role_embed(category_name):
# Generate role list
roles = react_roles.get_react_roles_by_category(category_name)
role_list = []
for role in roles:
emoji = role[2]
description = role[3]
role_list.append(f"{emoji} - {description}")
# Create an embed for the react-role category
embed = discord.Embed(title=f"**{category_name.capitalize()} Roles**",
description="React to this message to gain access to the relevant text and voice channels.\n\n" + "\n".join(role_list),
color=discord.Color.dark_orange())
return embed

17
util/console.py Normal file
View File

@@ -0,0 +1,17 @@
from rich.console import Console
from rich.progress import track
from rich.panel import Panel
console = Console()
def panel(title: str = '', content: str = '', style: str = "cyan") -> Panel:
"""
Create a panel with the given title and content.
"""
return Panel.fit(content, title=title, style=style)
def track_iterable(iterable, description: str):
"""
Create a progress bar for the given iterable.
"""
return track(iterable, description=description)