415 lines
20 KiB
Python
415 lines
20 KiB
Python
# ╔══════════════════════════════════════════════════════════════════╗
|
|
# ║ ║
|
|
# ║ +-+-+-+-+-+-+-+-+ ║
|
|
# ║ |H|e|x|a|H|o|s|t| ║
|
|
# ║ +-+-+-+-+-+-+-+-+ ║
|
|
# ║ ║
|
|
# ║ © 2026 HexaHost — All Rights Reserved ║
|
|
# ║ ║
|
|
# ║ discord ── https://discord.gg/hexahost ║
|
|
# ║ github ── https://github.com/theoneandonlymace ║
|
|
# ║ ║
|
|
# ╚══════════════════════════════════════════════════════════════════╝
|
|
from utils.db_paths import db_path
|
|
|
|
import discord
|
|
import aiosqlite
|
|
import json
|
|
import asyncio
|
|
import os
|
|
from discord.ext import commands
|
|
from typing import Optional
|
|
|
|
RED_THEME_COLOR = 0xFF0000
|
|
|
|
class StickyMessage(commands.Cog):
|
|
def __init__(self, bot: commands.Bot):
|
|
self.bot = bot
|
|
asyncio.create_task(self.setup_database())
|
|
|
|
async def setup_database(self):
|
|
if not os.path.exists("db"):
|
|
os.makedirs("db")
|
|
|
|
async with aiosqlite.connect(db_path("stickymessages.db")) as db:
|
|
await db.execute("""
|
|
CREATE TABLE IF NOT EXISTS sticky_messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
guild_id INTEGER NOT NULL,
|
|
channel_id INTEGER NOT NULL,
|
|
message_type TEXT DEFAULT 'plain',
|
|
message_content TEXT,
|
|
embed_data TEXT,
|
|
last_message_id INTEGER,
|
|
enabled BOOLEAN DEFAULT 1,
|
|
delay_seconds INTEGER DEFAULT 2,
|
|
auto_delete_after INTEGER DEFAULT 0,
|
|
ignore_bots BOOLEAN DEFAULT 1,
|
|
ignore_commands BOOLEAN DEFAULT 1,
|
|
trigger_count INTEGER DEFAULT 1,
|
|
current_count INTEGER DEFAULT 0,
|
|
UNIQUE(guild_id, channel_id)
|
|
)
|
|
""")
|
|
await db.commit()
|
|
|
|
@commands.Cog.listener()
|
|
async def on_message(self, message: discord.Message):
|
|
if not message.guild or (message.author.bot and message.author.id != self.bot.user.id):
|
|
return
|
|
|
|
async with aiosqlite.connect(db_path("stickymessages.db")) as db:
|
|
db.row_factory = aiosqlite.Row
|
|
cursor = await db.execute(
|
|
"SELECT * FROM sticky_messages WHERE channel_id = ? AND enabled = 1",
|
|
(message.channel.id,)
|
|
)
|
|
sticky_data = await cursor.fetchone()
|
|
|
|
if not sticky_data:
|
|
return
|
|
|
|
if sticky_data['ignore_bots'] and message.author.bot:
|
|
return
|
|
|
|
if sticky_data['ignore_commands']:
|
|
prefixes = await self.bot.get_prefix(message)
|
|
if isinstance(prefixes, str):
|
|
prefixes = [prefixes]
|
|
if any(message.content.startswith(p) for p in prefixes):
|
|
return
|
|
|
|
new_count = sticky_data['current_count'] + 1
|
|
if new_count < sticky_data['trigger_count']:
|
|
async with aiosqlite.connect(db_path("stickymessages.db")) as db:
|
|
await db.execute(
|
|
"UPDATE sticky_messages SET current_count = ? WHERE id = ?",
|
|
(new_count, sticky_data['id'])
|
|
)
|
|
await db.commit()
|
|
return
|
|
|
|
async with aiosqlite.connect(db_path("stickymessages.db")) as db:
|
|
await db.execute("UPDATE sticky_messages SET current_count = 0 WHERE id = ?", (sticky_data['id'],))
|
|
await db.commit()
|
|
|
|
await asyncio.sleep(sticky_data['delay_seconds'])
|
|
|
|
if sticky_data['last_message_id']:
|
|
try:
|
|
last_msg = await message.channel.fetch_message(sticky_data['last_message_id'])
|
|
await last_msg.delete()
|
|
except discord.NotFound:
|
|
pass
|
|
|
|
content = None
|
|
embed = None
|
|
delete_after = sticky_data['auto_delete_after'] if sticky_data['auto_delete_after'] > 0 else None
|
|
|
|
if sticky_data['message_type'] == 'plain':
|
|
content = sticky_data['message_content']
|
|
else:
|
|
try:
|
|
embed_data = json.loads(sticky_data['embed_data'])
|
|
embed = discord.Embed(
|
|
title=embed_data.get('title'),
|
|
description=embed_data.get('description'),
|
|
color=RED_THEME_COLOR
|
|
)
|
|
if footer := embed_data.get('footer'):
|
|
embed.set_footer(text=footer)
|
|
except (json.JSONDecodeError, ValueError):
|
|
embed = discord.Embed(description="Error: Could not decode embed data.", color=RED_THEME_COLOR)
|
|
|
|
new_sticky = await message.channel.send(content=content, embed=embed, delete_after=delete_after)
|
|
|
|
async with aiosqlite.connect(db_path("stickymessages.db")) as db:
|
|
await db.execute(
|
|
"UPDATE sticky_messages SET last_message_id = ? WHERE id = ?",
|
|
(new_sticky.id, sticky_data['id'])
|
|
)
|
|
await db.commit()
|
|
|
|
@commands.hybrid_group(aliases=['sticky', 'sm'], invoke_without_command=True)
|
|
@commands.has_permissions(manage_messages=True)
|
|
async def stickymessage(self, ctx: commands.Context):
|
|
if ctx.invoked_subcommand is None:
|
|
await ctx.send_help(ctx.command)
|
|
|
|
@stickymessage.command(name='setup')
|
|
@commands.has_permissions(manage_messages=True)
|
|
async def sticky_setup(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None):
|
|
channel = channel or ctx.channel
|
|
async with aiosqlite.connect(db_path("stickymessages.db")) as db:
|
|
cursor = await db.execute("SELECT 1 FROM sticky_messages WHERE channel_id = ?", (channel.id,))
|
|
if await cursor.fetchone():
|
|
embed = discord.Embed(title="Setup Error", description=f"A sticky message already exists in {channel.mention}.", color=RED_THEME_COLOR)
|
|
return await ctx.send(embed=embed)
|
|
|
|
embed = discord.Embed(title="Sticky Message Setup", description=f"Choose the message type for {channel.mention}:", color=RED_THEME_COLOR)
|
|
await ctx.send(embed=embed, view=StickySetupView(ctx, channel))
|
|
|
|
@stickymessage.command(name='remove', aliases=['delete', 'del'])
|
|
@commands.has_permissions(manage_messages=True)
|
|
async def sticky_remove(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None):
|
|
channel = channel or ctx.channel
|
|
async with aiosqlite.connect(db_path("stickymessages.db")) as db:
|
|
cursor = await db.execute("SELECT last_message_id FROM sticky_messages WHERE channel_id = ?", (channel.id,))
|
|
data = await cursor.fetchone()
|
|
if not data:
|
|
return await ctx.send(embed=discord.Embed(title="Not Found", description=f"No sticky message found in {channel.mention}.", color=RED_THEME_COLOR))
|
|
|
|
if data[0]:
|
|
try:
|
|
msg = await channel.fetch_message(data[0])
|
|
await msg.delete()
|
|
except discord.NotFound:
|
|
pass
|
|
|
|
await db.execute("DELETE FROM sticky_messages WHERE channel_id = ?", (channel.id,))
|
|
await db.commit()
|
|
|
|
embed = discord.Embed(title="Success", description=f"Sticky message in {channel.mention} has been removed.", color=RED_THEME_COLOR)
|
|
await ctx.send(embed=embed)
|
|
|
|
@stickymessage.command(name='list')
|
|
@commands.has_permissions(manage_messages=True)
|
|
async def sticky_list(self, ctx: commands.Context):
|
|
async with aiosqlite.connect(db_path("stickymessages.db")) as db:
|
|
db.row_factory = aiosqlite.Row
|
|
cursor = await db.execute("SELECT * FROM sticky_messages WHERE guild_id = ?", (ctx.guild.id,))
|
|
stickies = await cursor.fetchall()
|
|
|
|
if not stickies:
|
|
return await ctx.send(embed=discord.Embed(title="No Stickies Found", description="There are no active sticky messages on this server.", color=RED_THEME_COLOR))
|
|
|
|
embed = discord.Embed(title=f"Sticky Messages in {ctx.guild.name}", color=RED_THEME_COLOR)
|
|
for sticky in stickies:
|
|
channel = self.bot.get_channel(sticky['channel_id'])
|
|
status = "✅ Enabled" if sticky['enabled'] else "❌ Disabled"
|
|
embed.add_field(
|
|
name=f"#{channel.name if channel else 'Unknown Channel'}",
|
|
value=f"**Type**: {sticky['message_type'].title()}\n**Status**: {status}\n**Delay**: {sticky['delay_seconds']}s",
|
|
inline=True
|
|
)
|
|
await ctx.send(embed=embed)
|
|
|
|
@stickymessage.command(name='edit')
|
|
@commands.has_permissions(manage_messages=True)
|
|
async def sticky_edit(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None):
|
|
channel = channel or ctx.channel
|
|
async with aiosqlite.connect(db_path("stickymessages.db")) as db:
|
|
db.row_factory = aiosqlite.Row
|
|
cursor = await db.execute("SELECT * FROM sticky_messages WHERE channel_id = ?", (channel.id,))
|
|
sticky_data = await cursor.fetchone()
|
|
|
|
if not sticky_data:
|
|
return await ctx.send(embed=discord.Embed(title="Not Found", description=f"No sticky message found in {channel.mention}.", color=RED_THEME_COLOR))
|
|
|
|
embed = discord.Embed(title="Edit Sticky Message", description=f"Editing sticky for {channel.mention}. Choose an option:", color=RED_THEME_COLOR)
|
|
await ctx.send(embed=embed, view=StickyEditView(ctx, channel, sticky_data))
|
|
|
|
class AuthorOnlyView(discord.ui.View):
|
|
def __init__(self, ctx: commands.Context, timeout: float = 300.0):
|
|
super().__init__(timeout=timeout)
|
|
self.ctx = ctx
|
|
|
|
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
|
if interaction.user.id != self.ctx.author.id:
|
|
await interaction.response.send_message("You are not authorized to use this.", ephemeral=True)
|
|
return False
|
|
return True
|
|
|
|
async def on_timeout(self):
|
|
try:
|
|
for item in self.children:
|
|
item.disabled = True
|
|
if hasattr(self, 'message'):
|
|
await self.message.edit(view=self)
|
|
except discord.NotFound:
|
|
pass
|
|
|
|
class StickySetupView(AuthorOnlyView):
|
|
def __init__(self, ctx: commands.Context, channel: discord.TextChannel):
|
|
super().__init__(ctx)
|
|
self.channel = channel
|
|
|
|
@discord.ui.button(label='Plain Text', style=discord.ButtonStyle.primary, emoji='📝')
|
|
async def plain_text(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
await interaction.response.send_modal(PlainTextModal(self.ctx, self.channel))
|
|
|
|
@discord.ui.button(label='Embed', style=discord.ButtonStyle.secondary, emoji='📋')
|
|
async def embed_message(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
await interaction.response.send_modal(EmbedModal(self.ctx, self.channel))
|
|
|
|
@discord.ui.button(label='Cancel', style=discord.ButtonStyle.danger)
|
|
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
embed = discord.Embed(title="Cancelled", description="Sticky message setup has been cancelled.", color=RED_THEME_COLOR)
|
|
await interaction.response.edit_message(embed=embed, view=None)
|
|
|
|
class PlainTextModal(discord.ui.Modal, title='Plain Text Sticky Message'):
|
|
def __init__(self, ctx: commands.Context, channel: discord.TextChannel):
|
|
super().__init__()
|
|
self.ctx = ctx
|
|
self.channel = channel
|
|
|
|
message_content = discord.ui.TextInput(label='Message Content', style=discord.TextStyle.long, required=True, max_length=2000)
|
|
delay_seconds = discord.ui.TextInput(label='Delay (seconds)', default='2', required=False, max_length=3)
|
|
|
|
async def on_submit(self, interaction: discord.Interaction):
|
|
try:
|
|
delay = int(self.delay_seconds.value or "2")
|
|
except ValueError:
|
|
delay = 2
|
|
|
|
async with aiosqlite.connect(db_path("stickymessages.db")) as db:
|
|
await db.execute(
|
|
"INSERT INTO sticky_messages (guild_id, channel_id, message_type, message_content, delay_seconds) VALUES (?, ?, ?, ?, ?)",
|
|
(self.ctx.guild.id, self.channel.id, 'plain', self.message_content.value, delay)
|
|
)
|
|
await db.commit()
|
|
|
|
embed = discord.Embed(title="Sticky Created", description=f"Successfully created a plain text sticky in {self.channel.mention}.", color=RED_THEME_COLOR)
|
|
await interaction.response.edit_message(embed=embed, view=None)
|
|
|
|
class EmbedModal(discord.ui.Modal, title='Embed Sticky Message'):
|
|
def __init__(self, ctx: commands.Context, channel: discord.TextChannel):
|
|
super().__init__()
|
|
self.ctx = ctx
|
|
self.channel = channel
|
|
|
|
title = discord.ui.TextInput(label='Embed Title', required=False, max_length=256)
|
|
description = discord.ui.TextInput(label='Embed Description', style=discord.TextStyle.long, required=True, max_length=4000)
|
|
footer = discord.ui.TextInput(label='Embed Footer', required=False, max_length=2048)
|
|
delay_seconds = discord.ui.TextInput(label='Delay (seconds)', default='2', required=False, max_length=3)
|
|
|
|
async def on_submit(self, interaction: discord.Interaction):
|
|
try:
|
|
delay = int(self.delay_seconds.value or "2")
|
|
except ValueError:
|
|
delay = 2
|
|
|
|
embed_data = {
|
|
"title": self.title.value,
|
|
"description": self.description.value,
|
|
"color": f"#{RED_THEME_COLOR:06x}",
|
|
"footer": self.footer.value
|
|
}
|
|
|
|
async with aiosqlite.connect(db_path("stickymessages.db")) as db:
|
|
await db.execute(
|
|
"INSERT INTO sticky_messages (guild_id, channel_id, message_type, embed_data, delay_seconds) VALUES (?, ?, ?, ?, ?)",
|
|
(self.ctx.guild.id, self.channel.id, 'embed', json.dumps(embed_data), delay)
|
|
)
|
|
await db.commit()
|
|
|
|
embed = discord.Embed(title="Sticky Embed Created", description=f"Successfully created an embed sticky in {self.channel.mention}.", color=RED_THEME_COLOR)
|
|
await interaction.response.edit_message(embed=embed, view=None)
|
|
|
|
class StickyEditView(AuthorOnlyView):
|
|
def __init__(self, ctx: commands.Context, channel: discord.TextChannel, sticky_data):
|
|
super().__init__(ctx)
|
|
self.channel = channel
|
|
self.sticky_data = sticky_data
|
|
|
|
@discord.ui.button(label='Edit Content', style=discord.ButtonStyle.primary)
|
|
async def edit_content(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
if self.sticky_data['message_type'] == 'plain':
|
|
await interaction.response.send_modal(EditPlainTextModal(self.ctx, self.channel, self.sticky_data))
|
|
else:
|
|
await interaction.response.send_modal(EditEmbedModal(self.ctx, self.channel, self.sticky_data))
|
|
|
|
@discord.ui.button(label='Edit Settings', style=discord.ButtonStyle.secondary)
|
|
async def edit_settings(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
await interaction.response.send_modal(EditSettingsModal(self.ctx, self.channel, self.sticky_data))
|
|
|
|
@discord.ui.button(label='Cancel', style=discord.ButtonStyle.danger)
|
|
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
embed = discord.Embed(title="Cancelled", description="Editing has been cancelled.", color=RED_THEME_COLOR)
|
|
await interaction.response.edit_message(embed=embed, view=None)
|
|
|
|
class EditPlainTextModal(discord.ui.Modal, title='Edit Plain Text Content'):
|
|
def __init__(self, ctx: commands.Context, channel: discord.TextChannel, sticky_data):
|
|
super().__init__()
|
|
self.ctx = ctx
|
|
self.channel = channel
|
|
self.message_content = discord.ui.TextInput(
|
|
label='Message Content',
|
|
style=discord.TextStyle.long,
|
|
default=sticky_data['message_content'],
|
|
max_length=2000
|
|
)
|
|
self.add_item(self.message_content)
|
|
|
|
async def on_submit(self, interaction: discord.Interaction):
|
|
async with aiosqlite.connect(db_path("stickymessages.db")) as db:
|
|
await db.execute(
|
|
"UPDATE sticky_messages SET message_content = ? WHERE channel_id = ?",
|
|
(self.message_content.value, self.channel.id)
|
|
)
|
|
await db.commit()
|
|
embed = discord.Embed(title="Content Updated", description="The sticky message content has been updated.", color=RED_THEME_COLOR)
|
|
await interaction.response.edit_message(embed=embed, view=None)
|
|
|
|
class EditEmbedModal(discord.ui.Modal, title='Edit Embed Content'):
|
|
def __init__(self, ctx: commands.Context, channel: discord.TextChannel, sticky_data):
|
|
super().__init__()
|
|
self.ctx = ctx
|
|
self.channel = channel
|
|
embed_data = json.loads(sticky_data['embed_data'])
|
|
|
|
self.title = discord.ui.TextInput(label='Embed Title', default=embed_data.get('title', ''), required=False, max_length=256)
|
|
self.description = discord.ui.TextInput(label='Embed Description', style=discord.TextStyle.long, default=embed_data.get('description', ''), required=True, max_length=4000)
|
|
self.footer = discord.ui.TextInput(label='Embed Footer', default=embed_data.get('footer', ''), required=False, max_length=2048)
|
|
|
|
self.add_item(self.title)
|
|
self.add_item(self.description)
|
|
self.add_item(self.footer)
|
|
|
|
async def on_submit(self, interaction: discord.Interaction):
|
|
embed_data = {
|
|
"title": self.title.value, "description": self.description.value,
|
|
"color": f"#{RED_THEME_COLOR:06x}", "footer": self.footer.value
|
|
}
|
|
async with aiosqlite.connect(db_path("stickymessages.db")) as db:
|
|
await db.execute(
|
|
"UPDATE sticky_messages SET embed_data = ? WHERE channel_id = ?",
|
|
(json.dumps(embed_data), self.channel.id)
|
|
)
|
|
await db.commit()
|
|
embed = discord.Embed(title="Embed Updated", description="The sticky embed content has been updated.", color=RED_THEME_COLOR)
|
|
await interaction.response.edit_message(embed=embed, view=None)
|
|
|
|
class EditSettingsModal(discord.ui.Modal, title='Edit Sticky Settings'):
|
|
def __init__(self, ctx: commands.Context, channel: discord.TextChannel, sticky_data):
|
|
super().__init__()
|
|
self.ctx = ctx
|
|
self.channel = channel
|
|
|
|
self.delay_seconds = discord.ui.TextInput(label='Delay (seconds)', default=str(sticky_data['delay_seconds']), max_length=3)
|
|
self.auto_delete_after = discord.ui.TextInput(label='Auto-delete (seconds, 0=off)', default=str(sticky_data['auto_delete_after']), max_length=4)
|
|
self.trigger_count = discord.ui.TextInput(label='Trigger After X Msgs', default=str(sticky_data['trigger_count']), max_length=2)
|
|
|
|
self.add_item(self.delay_seconds)
|
|
self.add_item(self.auto_delete_after)
|
|
self.add_item(self.trigger_count)
|
|
|
|
async def on_submit(self, interaction: discord.Interaction):
|
|
try:
|
|
delay = int(self.delay_seconds.value)
|
|
auto_del = int(self.auto_delete_after.value)
|
|
trigger = int(self.trigger_count.value)
|
|
except ValueError:
|
|
return await interaction.response.send_message("Please enter valid numbers.", ephemeral=True)
|
|
|
|
async with aiosqlite.connect(db_path("stickymessages.db")) as db:
|
|
await db.execute(
|
|
"UPDATE sticky_messages SET delay_seconds = ?, auto_delete_after = ?, trigger_count = ? WHERE channel_id = ?",
|
|
(delay, auto_del, trigger, self.channel.id)
|
|
)
|
|
await db.commit()
|
|
embed = discord.Embed(title="Settings Updated", description="The sticky message settings have been updated.", color=RED_THEME_COLOR)
|
|
await interaction.response.edit_message(embed=embed, view=None)
|
|
|
|
async def setup(bot: commands.Bot):
|
|
await bot.add_cog(StickyMessage(bot)) |