import discord from discord.ext import commands from discord.ext.commands import Context import asyncio # Make a pr to add your own server config here, you shouldn't need to touch the rest of the file, please fill in all the values for your own server STICKY_CONFIGS = { "neotest": { "guild_id": 1069946178659160076, "channel_ids": [ 1455338488546459789, ], "allowed_role_id": 1432165329483857940, "message": "# Example sticky message", # You can add your own markdown here "footer": "This is an automated sticky message.", # This will be appended to the message and uses "-#" to format the footer "delay": 10, # in seconds }, "SideStore": { "guild_id": 949183273383395328, "channel_ids": [ 1279548738586673202, ], "allowed_role_id": 949207813815697479, "message": "## Please read the README in https://discord.com/channels/949183273383395328/1155736594679083089 and the documentation at before asking your question.", "footer": "This is an automated sticky message.", "delay": 10, }, } def has_allowed_role(): async def predicate(context: Context): if not context.guild: context.bot.logger.warning( f"[STICKYBOT] Unauthorized stickybot command attempt by {context.author} ({context.author.id}) in DMs" ) embed = discord.Embed( title="Permission Denied", description="You don't have permission to use this command.", color=0xE02B2B, ) embed.set_author( name="Events", icon_url="https://yes.nighty.works/raw/C8Hh6o.png" ) await context.send(embed=embed, ephemeral=True) return False if not hasattr(context.author, "roles"): context.bot.logger.warning( f"[STICKYBOT] Unauthorized stickybot command attempt by {context.author} ({context.author.id}) in {context.guild.name} - no roles" ) embed = discord.Embed( title="Permission Denied", description="You don't have permission to use this command.", color=0xE02B2B, ) embed.set_author( name="Events", icon_url="https://yes.nighty.works/raw/C8Hh6o.png" ) await context.send(embed=embed, ephemeral=True) return False for config in STICKY_CONFIGS.values(): if context.guild.id != config.get("guild_id"): continue allowed_role_id = config.get("allowed_role_id") if allowed_role_id: allowed_role = context.guild.get_role(allowed_role_id) if allowed_role: for role in context.author.roles: if ( role.position >= allowed_role.position and role.id != context.guild.default_role.id ): return True context.bot.logger.warning( f"[STICKYBOT] Unauthorized stickybot command attempt by {context.author} ({context.author.id}) in {context.guild.name} - insufficient role permissions" ) embed = discord.Embed( title="Permission Denied", description="You don't have permission to use this command.", color=0xE02B2B, ) embed.set_author( name="Events", icon_url="https://yes.nighty.works/raw/C8Hh6o.png" ) await context.send(embed=embed, ephemeral=True) return False return commands.check(predicate) def stickybot_command(): async def wrapper(self, context: Context): embed = discord.Embed( title="Sticky Bot", description="Sends sticky messages in configured channels.", color=0x7289DA, ) embed.set_author( name="Events", icon_url="https://yes.nighty.works/raw/C8Hh6o.png" ) found_config = False if STICKY_CONFIGS: for name, config in STICKY_CONFIGS.items(): guild_id = config.get("guild_id") if context.guild and guild_id == context.guild.id: channel_ids = config.get("channel_ids", []) channel_displays = [] for channel_id in channel_ids: channel = context.guild.get_channel(channel_id) channel_display = ( f"<#{channel_id}> (`{channel_id}`)" if channel else f"`{channel_id}`" ) channel_displays.append(channel_display) channels_text = ( "\n".join(channel_displays) if channel_displays else "Not set" ) allowed_role_id = config.get("allowed_role_id", "Not set") role = context.guild.get_role(allowed_role_id) role_display = ( f"<@&{allowed_role_id}> (`{allowed_role_id}`)" if role else f"`{allowed_role_id}`" ) message_content = config.get("message", "*No message set*") footer_text = config.get( "footer", "This is an automated sticky message." ) full_content = f"{message_content}\n-# {footer_text}" embed.add_field( name="\u200b", value=f"**Channels:**\n{channels_text}\n\n**Allowed Role:**\n{role_display}\n\n**Message Preview:**\n```\n{full_content}\n```", inline=False, ) found_config = True if not found_config: embed.add_field( name="No Configurations", value="No sticky configurations found for this server", inline=False, ) if context.guild and context.guild.icon: embed.set_thumbnail(url=context.guild.icon.url) await context.send(embed=embed) return wrapper class StickyBotListener(commands.Cog): def __init__(self, bot): self.bot = bot self.last_sticky_messages = {} self.debounce_tasks = {} async def delete_last_sticky(self, channel): try: active_config = None for config in STICKY_CONFIGS.values(): if channel.guild.id == config.get( "guild_id" ) and channel.id in config.get("channel_ids", []): active_config = config break if not active_config: return target_footer = active_config.get( "footer", "This is an automated sticky message." ) async for message in channel.history(limit=20): if ( message.author.id == self.bot.user.id and target_footer in message.content ): await message.delete() except Exception as e: self.bot.logger.warning( f"[STICKYBOT] Error cleaning up sticky in #{channel.name}: {e}" ) async def send_sticky_message(self, channel, config): if not channel: return last_msg_id = self.last_sticky_messages.get(channel.id) deleted = False if last_msg_id: try: old_msg = await channel.fetch_message(last_msg_id) await old_msg.delete() deleted = True except discord.NotFound: deleted = True except discord.Forbidden: self.bot.logger.warning( f"[STICKYBOT] Missing delete permissions in #{channel.name}" ) except Exception as e: self.bot.logger.warning( f"[STICKYBOT] Error deleting info in #{channel.name}: {e}" ) if not deleted: await self.delete_last_sticky(channel) message_content = config.get("message") if not message_content: return footer_text = config.get("footer", "This is an automated sticky message.") footer = f"\n-# {footer_text}" full_content = f"{message_content}{footer}" try: new_msg = await channel.send( full_content, allowed_mentions=discord.AllowedMentions.none() ) self.last_sticky_messages[channel.id] = new_msg.id except discord.Forbidden: self.bot.logger.warning( f"[STICKYBOT] Missing send permissions in #{channel.name}" ) except Exception as e: self.bot.logger.error( f"[STICKYBOT] Error sending sticky in #{channel.name}: {e}" ) @commands.Cog.listener() async def on_ready(self): await self.bot.wait_until_ready() await self.initialize_stickies() async def initialize_stickies(self): for name, config in STICKY_CONFIGS.items(): guild_id = config.get("guild_id") guild = self.bot.get_guild(guild_id) if not guild: continue channel_ids = config.get("channel_ids", []) for channel_id in channel_ids: channel = guild.get_channel(channel_id) if channel: await self.send_sticky_message(channel, config) async def trigger_sticky(self, channel, guild): if not guild or not channel: return active_config = None for config in STICKY_CONFIGS.values(): if guild.id == config.get("guild_id"): if channel.id in config.get("channel_ids", []): active_config = config break if not active_config: return channel_id = channel.id if channel_id in self.debounce_tasks: self.debounce_tasks[channel_id].cancel() async def debounce_wrapper(): try: delay = active_config.get("delay", 5) await asyncio.sleep(delay) await self.send_sticky_message(channel, active_config) except asyncio.CancelledError: pass except Exception as e: self.bot.logger.error(f"[STICKYBOT] Error in debounce task: {e}") finally: if self.debounce_tasks.get(channel_id) == asyncio.current_task(): del self.debounce_tasks[channel_id] self.debounce_tasks[channel_id] = self.bot.loop.create_task(debounce_wrapper()) @commands.Cog.listener() async def on_message(self, message: discord.Message): if message.guild is None or message.author.bot: return if message.id == self.last_sticky_messages.get(message.channel.id): return await self.trigger_sticky(message.channel, message.guild) @commands.Cog.listener() async def on_interaction(self, interaction: discord.Interaction): if interaction.guild is None or interaction.user.bot: return if interaction.type == discord.InteractionType.application_command: await self.trigger_sticky(interaction.channel, interaction.guild)