Reaction roles working. Management of message and reactions is messy and needs reinforcement.
This commit is contained in:
132
cogs/react_roles.py
Normal file
132
cogs/react_roles.py
Normal 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
4
data/init.py
Normal 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
144
data/react_roles.py
Normal 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
25
main.py
@@ -6,17 +6,14 @@ from dotenv import load_dotenv
|
|||||||
import signal
|
import signal
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from pipenv.patched.safety.safety import session
|
from util.console import console, panel, track_iterable as track
|
||||||
from rich.console import Console
|
from data.init import init_databses
|
||||||
from rich.progress import track
|
|
||||||
from rich.panel import Panel
|
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
TOKEN = os.getenv("BOT_TOKEN")
|
TOKEN = os.getenv("BOT_TOKEN")
|
||||||
DEV_GUILD_ID = int(os.getenv("DEV_GUILD_ID"))
|
DEV_GUILD_ID = int(os.getenv("DEV_GUILD_ID"))
|
||||||
|
REACT_ROLE_CHANNEL_ID = int(os.getenv("REACT_ROLE_CHANNEL_ID"))
|
||||||
# Initialize the console for rich output
|
|
||||||
console = Console()
|
|
||||||
|
|
||||||
# Set up intents for the bot
|
# Set up intents for the bot
|
||||||
intents = discord.Intents.default()
|
intents = discord.Intents.default()
|
||||||
@@ -30,6 +27,14 @@ COG_PATH = Path(__file__).resolve().parent / "cogs"
|
|||||||
cogs_available = {}
|
cogs_available = {}
|
||||||
loaded_cog_modules = {}
|
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():
|
for file in COG_PATH.iterdir():
|
||||||
if file.suffix == ".py" and not file.name.startswith("__"):
|
if file.suffix == ".py" and not file.name.startswith("__"):
|
||||||
cog_name = file.stem
|
cog_name = file.stem
|
||||||
@@ -44,11 +49,13 @@ async def load_cogs():
|
|||||||
console.log(f"[red]❌ Cogs directory not found at {COG_PATH}[/red]")
|
console.log(f"[red]❌ Cogs directory not found at {COG_PATH}[/red]")
|
||||||
return
|
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..."):
|
for cog in track(cogs_available, description="Loading Cogs..."):
|
||||||
module_path = f"cogs.{cog}"
|
module_path = f"cogs.{cog}"
|
||||||
|
|
||||||
|
config = COG_CONFIG.get(cog, {})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bot.load_extension(module_path)
|
bot.load_extension(module_path)
|
||||||
loaded_cog_modules[cog] = module_path
|
loaded_cog_modules[cog] = module_path
|
||||||
@@ -74,6 +81,8 @@ async def on_ready():
|
|||||||
await load_cogs()
|
await load_cogs()
|
||||||
await bot.sync_commands()
|
await bot.sync_commands()
|
||||||
console.log("[blue]🔁 Synced slash commands[/blue]")
|
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.rule(f"[bold green]✅ Bot Ready — Logged in as {bot.user}[/]")
|
||||||
console.print(f"ID: {bot.user.id}")
|
console.print(f"ID: {bot.user.id}")
|
||||||
handle_signals()
|
handle_signals()
|
||||||
|
|||||||
17
react_roles/react_roles.py
Normal file
17
react_roles/react_roles.py
Normal 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
17
util/console.py
Normal 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)
|
||||||
Reference in New Issue
Block a user