diff --git a/README.md b/README.md index 07770d9..617c3ab 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ | sidestore | `help`, `refresh`, `code`, `crash`, `pairing`, `server`, `half`, `sparse`, `afc`, `udid` | | idevice | `help`, `noapps`, `errorcode`, `developermode`, `mountddi` | | melonx | `help`, `transfer`, `mods`, `gamecrash`, `requirements`, `error`, `26`, `legal` | +| events | `baitbot` | | miscellaneous | `keanu`, `labubu`, `piracy`, `tryitandsee`, `rickroll`, `dontasktoask`, `support`, `depart`, `docs` `sigma`, `duck`, `silly`, `color` | | utilities | `translate`, `codepreview`, `dictionary` | | media | `download`, `mcquote`, `img2gif`, `tweety`, `tts` | diff --git a/cogs/events/__init__.py b/cogs/events/__init__.py new file mode 100644 index 0000000..3e4921e --- /dev/null +++ b/cogs/events/__init__.py @@ -0,0 +1,67 @@ +import discord +from discord import app_commands +from discord.ext import commands +from discord.ext.commands import Context + +from .baitbot import baitbot_command, BaitBotListener, has_protected_role + + +def _require_group_prefix(context: Context) -> bool: + if getattr(context, "interaction", None): + return True + group = getattr(getattr(context, "cog", None), "qualified_name", "").lower() + if not group: + return True + prefix = context.prefix or "" + content = context.message.content.strip().lower() + return content.startswith(f"{prefix}{group} ") + + +@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) +@app_commands.allowed_installs(guilds=True, users=True) +class Events(commands.GroupCog, name="events"): + def __init__(self, bot) -> None: + self.bot = bot + super().__init__() + + @commands.group(name="events", invoke_without_command=True) + async def events_group(self, context: Context): + embed = discord.Embed( + title="Events Commands", + description="Use `.events ` or `/events `.", + color=0x7289DA + ) + embed.set_author(name="Events", icon_url="https://yes.nighty.works/raw/eW5lLm.webp") + embed.add_field(name="Available", value="baitbot", inline=False) + await context.send(embed=embed) + + async def _invoke_hybrid(self, context: Context, name: str, **kwargs): + command = self.bot.get_command(name) + if command is not None: + await context.invoke(command, **kwargs) + else: + await context.send(f"Unknown events command: {name}") + + @events_group.command(name="baitbot") + @has_protected_role() + async def events_group_baitbot(self, context: Context): + await self._invoke_hybrid(context, "baitbot") + + @commands.check(_require_group_prefix) + @has_protected_role() + @commands.hybrid_command( + name="baitbot", + description="View bait bot configuration and status." + ) + async def baitbot(self, context): + return await baitbot_command()(self, context) + + +async def setup(bot) -> None: + cog = Events(bot) + await bot.add_cog(cog) + + listener = BaitBotListener(bot) + await bot.add_cog(listener) + + bot.logger.info("Loaded extension 'events.baitbot'") diff --git a/cogs/events/baitbot.py b/cogs/events/baitbot.py new file mode 100644 index 0000000..c8fec79 --- /dev/null +++ b/cogs/events/baitbot.py @@ -0,0 +1,290 @@ +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 +BAIT_CONFIGS = { + "SideStore": { + "guild_id": 949183273383395328, + "channel_ids": [ + 1432134748062482586, + 1432204211009097819, + ], + "protected_role_id": 959598279685963776, + "log_channel_id": 1433532504647667823, + }, + "neotest": { + "guild_id": 1069946178659160076, + "channel_ids": [ + 1432175690270118012, + 1433987189670281278, + 1433988339031080991, + ], + "protected_role_id": 1432165329483857940, + "log_channel_id": 1433987853184139365, + }, + "idevice": { + "guild_id": 1329314147434758175, + "channel_ids": [ + 1434317669695492177, + ], + "protected_role_id": 1333666918548111373, + "log_channel_id": 1333673259446571022, + }, + "melonx": { + "guild_id": 1300369899704680479, + "channel_ids": [ + 1434327970679492830, + ], + "protected_role_id": 1300372178138697758, + "log_channel_id": 1300374786471366696, + }, +} + +BAN_REASON = 'Detected bot/scammer in bait channel' + +def has_protected_role(): + async def predicate(context: Context): + if not context.guild: + context.bot.logger.warning(f'[BAITBOT] Unauthorized baitbot 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'[BAITBOT] Unauthorized baitbot 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 BAIT_CONFIGS.values(): + protected_role_id = config.get("protected_role_id") + if protected_role_id: + protected_role = context.guild.get_role(protected_role_id) + if protected_role: + for role in context.author.roles: + if role.position >= protected_role.position and role.id != context.guild.default_role.id: + return True + + context.bot.logger.warning(f'[BAITBOT] Unauthorized baitbot 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 baitbot_command(): + async def wrapper(self, context: Context): + embed = discord.Embed( + title="Bait Bot", + description="Bans people who post in a specific channel.", + color=0x7289DA + ) + embed.set_author(name="Events", icon_url="https://yes.nighty.works/raw/C8Hh6o.png") + + found_config = False + if BAIT_CONFIGS: + for name, config in BAIT_CONFIGS.items(): + guild_id = config.get("guild_id") + if context.guild and guild_id == context.guild.id: + channel_ids = config.get("channel_ids", []) + if not channel_ids: + channel_id = config.get("channel_id") + if channel_id: + channel_ids = [channel_id] + role_id = config.get("protected_role_id", "Not set") + + 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" + + role = context.guild.get_role(role_id) + role_display = f"<@&{role_id}> (`{role_id}`)" if role else f"`{role_id}`" + + log_channel_id = config.get("log_channel_id") + log_channel = None + if log_channel_id: + log_channel = context.guild.get_channel(log_channel_id) + log_display = f"<#{log_channel_id}> (`{log_channel_id}`)" if log_channel else (f"`{log_channel_id}`" if log_channel_id else "Not set") + + embed.add_field( + name="\u200b", + value=f"Channels:\n{channels_text}\n\nProtected Role:\n{role_display}\n\nLog Channel:\n{log_display}", + inline=False + ) + found_config = True + + if not found_config: + embed.add_field( + name="No Configurations", + value="No bait channels configured 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 BaitBotListener(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.Cog.listener() + async def on_message(self, message: discord.Message): + if message.guild is None: + return + + if message.author.bot: + return + + bait_config = None + config_name = None + for name, config in BAIT_CONFIGS.items(): + if message.guild.id != config.get("guild_id"): + continue + + channel_ids = config.get("channel_ids", []) + if not channel_ids: + channel_id = config.get("channel_id") + if channel_id: + channel_ids = [channel_id] + + if message.channel.id in channel_ids: + bait_config = config + config_name = name + break + + if not bait_config: + return + + protected_role_id = bait_config.get("protected_role_id") + is_protected = False + if protected_role_id and hasattr(message.author, 'roles'): + protected_role = message.guild.get_role(protected_role_id) + if protected_role: + for role in message.author.roles: + if role.position >= protected_role.position and role.id != message.guild.default_role.id: + self.bot.logger.info(f'[BAITBOT] Skipped banning {message.author} ({message.author.id}) in #{message.channel.name}: protected role ({role.name})') + is_protected = True + break + + message_content = message.content if message.content else "*No text content*" + message_attachments = message.attachments + message_embeds = message.embeds + + try: + await message.delete() + self.bot.logger.info(f'[BAITBOT] Deleted message from {message.author} in #{message.channel.name}') + except Exception as e: + self.bot.logger.warning(f'[BAITBOT] Could not delete message from {message.author}: {e}') + banned = False + if not is_protected: + try: + self.bot.logger.warning(f'[BAITBOT] Detected user in bait channel [{config_name}]: {message.author.name} ({message.author.id}) in #{message.channel.name}') + + if not message.guild.me.guild_permissions.ban_members: + self.bot.logger.error(f'[BAITBOT] No permission to ban members in {message.guild.name}') + else: + try: + await message.author.ban(reason=BAN_REASON, delete_message_days=7) + self.bot.logger.info(f'[BAITBOT] Banned {message.author.name} - deleted messages from last 7 days') + banned = True + except discord.Forbidden: + self.bot.logger.error(f'[BAITBOT] Could not ban {message.author.name}: missing permissions') + except Exception as e: + self.bot.logger.error(f'[BAITBOT] Error banning {message.author.name}: {e}') + + if banned: + await asyncio.sleep(2) + try: + await message.guild.unban(message.author, reason="Auto-unban after cleanup") + self.bot.logger.info(f'[BAITBOT] Unbanned {message.author.name} - cleanup complete') + except Exception as e: + self.bot.logger.error(f'[BAITBOT] Error unbanning {message.author.name}: {e}') + except Exception as e: + self.bot.logger.error(f'[BAITBOT] Error handling bait message: {e}') + + log_channel_id = bait_config.get("log_channel_id") + if log_channel_id: + try: + log_channel = self.bot.get_channel(log_channel_id) + if log_channel: + action_text = "Message deleted (user banned and unbanned)" if banned else "Message deleted (protected user)" if is_protected else "Message deleted" + log_embed = discord.Embed( + title="Bait Bot", + description=action_text, + color=0xE02B2B, + timestamp=message.created_at + ) + log_embed.set_author(name=str(message.author), icon_url=message.author.display_avatar.url) + log_embed.add_field(name="User", value=message.author.mention, inline=True) + log_embed.add_field(name="Channel", value=message.channel.mention, inline=True) + + combined_content = [] + if message_content and message_content != "*No text content*": + combined_content.append(message_content) + + image_url = None + if message_attachments: + for attachment in message_attachments: + if attachment.content_type and attachment.content_type.startswith("image/"): + if not image_url: + image_url = attachment.url + combined_content.append(attachment.filename) + + content_text = "\n".join(combined_content) if combined_content else "*No content*" + if len(content_text) > 1000: + content_text = content_text[:997] + "..." + + log_embed.add_field(name="Content", value=f"```\n{content_text}\n```", inline=False) + + if image_url: + log_embed.set_image(url=image_url) + + if message_embeds: + embed_info = [] + for embed in message_embeds[:3]: + embed_desc = f"**Embed:** {embed.title or 'Untitled'}\n" + if embed.description: + desc_text = embed.description[:200] + "..." if len(embed.description) > 200 else embed.description + embed_desc += f"{desc_text}\n" + if embed.url: + embed_desc += f"[Link]({embed.url})" + embed_info.append(embed_desc) + if embed_info: + log_embed.add_field(name="Embeds", value="\n\n".join(embed_info), inline=False) + + log_embed.set_footer(text=f"Message ID: {message.id}") + + try: + await log_channel.send(embed=log_embed) + self.bot.logger.info(f'[BAITBOT] Sent log to #{log_channel.name}') + except discord.Forbidden: + self.bot.logger.error(f'[BAITBOT] No permission to send log to #{log_channel.name}') + except Exception as e: + self.bot.logger.error(f'[BAITBOT] Error sending log: {e}') + else: + self.bot.logger.warning(f'[BAITBOT] Log channel {log_channel_id} not found') + except Exception as e: + self.bot.logger.error(f'[BAITBOT] Error handling log channel: {e}') diff --git a/cogs/help.py b/cogs/help.py index bc0bdd6..dec0279 100644 --- a/cogs/help.py +++ b/cogs/help.py @@ -13,7 +13,7 @@ class Help(commands.Cog, name="help"): interaction: discord.Interaction, current: str, ) -> list[app_commands.Choice[str]]: - categories = ["general", "fun", "moderation", "owner", "sidestore", "idevice", "melonx", "media", "miscellaneous", "utilities"] + categories = ["general", "fun", "moderation", "owner", "sidestore", "idevice", "melonx", "media", "miscellaneous", "utilities", "events"] suggestions = [] for category in categories: @@ -48,6 +48,7 @@ class Help(commands.Cog, name="help"): "sidestore": "sidestore", "utils": "utilities", "utilities": "utilities", + "events": "events", "sync": "owner", "logs": "owner", @@ -69,7 +70,8 @@ class Help(commands.Cog, name="help"): "melonx": "MeloNX troubleshooting commands", "media": "Media commands", "utilities": "Utility commands", - "miscellaneous": "Miscellaneous commands" + "miscellaneous": "Miscellaneous commands", + "events": "Events commands" } if category is None: