diff --git a/antipolls/__init__.py b/antipolls/__init__.py new file mode 100644 index 0000000..8863332 --- /dev/null +++ b/antipolls/__init__.py @@ -0,0 +1,5 @@ +from .antipolls import AntiPolls + + +async def setup(bot): + await bot.add_cog(AntiPolls(bot)) diff --git a/antipolls/antipolls.py b/antipolls/antipolls.py new file mode 100644 index 0000000..2c4d508 --- /dev/null +++ b/antipolls/antipolls.py @@ -0,0 +1,176 @@ +# _____ _ +# / ____| (_) +# | (___ ___ __ _ _____ ___ _ __ ___ _ __ ___ ___ _ __ +# \___ \ / _ \/ _` / __\ \ /\ / / | '_ ` _ \| '_ ` _ \ / _ \ '__| +# ____) | __/ (_| \__ \\ V V /| | | | | | | | | | | | __/ | +# |_____/ \___|\__,_|___/ \_/\_/ |_|_| |_| |_|_| |_| |_|\___|_| + +import discord +from red_commons.logging import getLogger +from redbot.core import commands +from redbot.core.bot import Config, Red +from redbot.core.utils.chat_formatting import humanize_list + + +class AntiPolls(commands.Cog): + """AntiPolls deletes messages that contain polls, with a configurable per-guild role and channel whitelist and support for default Discord permissions (Manage Messages).""" + + __author__ = ["SeaswimmerTheFsh"] + __version__ = "1.0.0" + __documentation__ = "https://seacogs.coastalcommits.com/antipolls/" + + def __init__(self, bot: Red): + super().__init__() + self.bot = bot + self.logger = getLogger("red.SeaCogs.AntiPolls") + self.config = Config.get_conf(self, identifier=23517395243, force_registration=True) + self.config.register_guild( + role_whitelist=[], + channel_whitelist=[], + manage_messages=True, + ) + + def format_help_for_context(self, ctx: commands.Context) -> str: + pre_processed = super().format_help_for_context(ctx) or "" + n = "\n" if "\n\n" not in pre_processed else "" + text = [ + f"{pre_processed}{n}", + f"Cog Version: **{self.__version__}**", + f"Author: {humanize_list(self.__author__)}", + f"Documentation: {self.__documentation__}", + ] + return "\n".join(text) + + async def red_delete_data_for_user(self, **kwargs): + """Nothing to delete.""" + return + + @commands.Cog.listener('on_message') + async def listener(self, message: discord.Message) -> None: + if message.guild is None: + return self.logger.verbose("Message in direct messages ignored") + + if message.author.bot: + return self.logger.verbose("Message from bot ignored") + + if self.bot.cog_disabled_in_guild(self, message.guild): + return self.logger.verbose("Cog disabled in guild") + + guild_config = await self.config.guild(message.guild).all() + + if guild_config['manage_messages'] is True and message.author.guild_permissions.manage_messages: + return self.logger.verbose("Message from user with Manage Messages permission ignored") + + if message.channel.id in guild_config['channel_whitelist']: + return self.logger.verbose(f"Message in whitelisted channel {message.channel.id} ignored") + + if any(role.id in guild_config['role_whitelist'] for role in message.author.roles): + return self.logger.verbose(f"Message from whitelisted role {message.author.roles} ignored") + + if not message.content and not message.embeds and not message.attachments and not message.stickers: + self.logger.trace(f"Message {message.id} is a poll, attempting to delete") + + try: + await message.delete() + except discord.HTTPException as e: + return self.logger.error(f"Failed to delete message: {e}") + + self.logger.trace(f"Deleted poll message {message.id}") + + @commands.group(name="antipolls", aliases=["ap"]) + @commands.guild_only() + @commands.admin_or_permissions(manage_guild=True) + async def antipolls(self, ctx: commands.Context) -> None: + """Manage AntiPolls settings.""" + + @antipolls.group(name="roles") + async def antipolls_roles(self, ctx: commands.Context) -> None: + """Manage role whitelist.""" + + @antipolls_roles.command(name="add") + async def antipolls_roles_add(self, ctx: commands.Context, *roles: discord.Role) -> None: + """Add roles to the whitelist.""" + async with self.config.guild(ctx.guild).role_whitelist() as role_whitelist: + role_whitelist: list + failed: list[discord.Role] = [] + for role in roles: + if role.id in role_whitelist: + failed.extend(role) + continue + role_whitelist.extend(role.id) + await ctx.tick() + if failed: + await ctx.send(f"The following roles were already in the whitelist: {humanize_list(role.mention for role in failed)}", delete_after=10, allowed_mentions=discord.AllowedMentions.none) + + @antipolls_roles.command(name="remove") + async def antipolls_roles_remove(self, ctx: commands.Context, *roles: discord.Role) -> None: + """Remove roles from the whitelist.""" + async with self.config.guild(ctx.guild).role_whitelist() as role_whitelist: + role_whitelist: list + failed: list[discord.Role] = [] + for role in roles: + if role.id not in role_whitelist: + failed.extend(role) + continue + role_whitelist.remove(role.id) + await ctx.tick() + if failed: + await ctx.send(f"The following roles were not in the whitelist: {humanize_list(role.mention for role in failed)}", delete_after=10, allowed_mentions=discord.AllowedMentions.none) + + @antipolls_roles.command(name="list") + async def antipolls_roles_list(self, ctx: commands.Context) -> None: + """List roles in the whitelist.""" + role_whitelist = await self.config.guild(ctx.guild).role_whitelist() + if not role_whitelist: + return await ctx.send("No roles in the whitelist.") + roles = [ctx.guild.get_role(role) for role in role_whitelist] + await ctx.send(humanize_list(role.mention for role in roles)) + + @antipolls.group(name="channels") + async def antipolls_channels(self, ctx: commands.Context) -> None: + """Manage channel whitelist.""" + + @antipolls_channels.command(name="add") + async def antipolls_channels_add(self, ctx: commands.Context, *channels: discord.TextChannel) -> None: + """Add channels to the whitelist.""" + async with self.config.guild(ctx.guild).channel_whitelist() as channel_whitelist: + channel_whitelist: list + failed: list[discord.TextChannel] = [] + for channel in channels: + if channel.id in channel_whitelist: + failed.extend(channel) + continue + channel_whitelist.extend(channel.id) + await ctx.tick() + if failed: + await ctx.send(f"The following channels were already in the whitelist: {humanize_list(channel.mention for channel in failed)}", delete_after=10, allowed_mentions=discord.AllowedMentions.none) + + @antipolls_channels.command(name="remove") + async def antipolls_channels_remove(self, ctx: commands.Context, *channels: discord.TextChannel) -> None: + """Remove channels from the whitelist.""" + async with self.config.guild(ctx.guild).channel_whitelist() as channel_whitelist: + channel_whitelist: list + failed: list[discord.TextChannel] = [] + for channel in channels: + if channel.id not in channel_whitelist: + failed.extend(channel) + continue + channel_whitelist.remove(channel.id) + await ctx.tick() + if failed: + await ctx.send(f"The following channels were not in the whitelist: {humanize_list(channel.mention for channel in failed)}", delete_after=10, allowed_mentions=discord.AllowedMentions.none) + + @antipolls_channels.command(name="list") + async def antipolls_channels_list(self, ctx: commands.Context) -> None: + """List channels in the whitelist.""" + channel_whitelist = await self.config.guild(ctx.guild).channel_whitelist() + if not channel_whitelist: + return await ctx.send("No channels in the whitelist.") + channels = [ctx.guild.get_channel(channel) for channel in channel_whitelist] + await ctx.send(humanize_list(channel.mention for channel in channels)) + + @antipolls.command(name="managemessages") + async def antipolls_managemessages(self, ctx: commands.Context, enabled: bool) -> None: + """Toggle Manage Messages permission check.""" + await self.config.guild(ctx.guild).manage_messages.set(enabled) + await ctx.tick() diff --git a/antipolls/info.json b/antipolls/info.json new file mode 100644 index 0000000..2af0457 --- /dev/null +++ b/antipolls/info.json @@ -0,0 +1,17 @@ +{ + "author" : ["SeaswimmerTheFsh (seasw.)"], + "install_msg" : "Thank you for installing AntiPolls!\nYou can find the source code of this cog [here](https://coastalcommits.com/SeaswimmerTheFsh/SeaCogs).", + "name" : "AntiPolls", + "short" : "AntiPolls deletes messages that contain polls.", + "description" : "AntiPolls deletes messages that contain polls, with a configurable per-guild role and channel whitelist and support for default Discord permissions (Manage Messages).", + "end_user_data_statement" : "This cog does not store any user data.", + "hidden": true, + "disabled": false, + "min_bot_version": "3.5.0", + "min_python_version": [3, 10, 0], + "tags": [ + "automod", + "automoderation", + "polls" + ] +} diff --git a/aurora/aurora.py b/aurora/aurora.py index 8a602eb..5d8ead0 100644 --- a/aurora/aurora.py +++ b/aurora/aurora.py @@ -18,8 +18,7 @@ from redbot.core import app_commands, commands, data_manager from redbot.core.app_commands import Choice from redbot.core.bot import Red from redbot.core.commands.converter import parse_relativedelta, parse_timedelta -from redbot.core.utils.chat_formatting import (box, error, humanize_list, - humanize_timedelta, warning) +from redbot.core.utils.chat_formatting import box, error, humanize_list, humanize_timedelta, warning from aurora.importers.aurora import ImportAuroraView from aurora.importers.galacticbot import ImportGalacticBotView @@ -28,18 +27,10 @@ from aurora.menus.guild import Guild from aurora.menus.immune import Immune from aurora.menus.overrides import Overrides from aurora.utilities.config import config, register_config -from aurora.utilities.database import (connect, create_guild_table, fetch_case, - mysql_log) -from aurora.utilities.factory import (addrole_embed, case_factory, - changes_factory, evidenceformat_factory, - guild_embed, immune_embed, - message_factory, overrides_embed) +from aurora.utilities.database import connect, create_guild_table, fetch_case, mysql_log +from aurora.utilities.factory import addrole_embed, case_factory, changes_factory, evidenceformat_factory, guild_embed, immune_embed, message_factory, overrides_embed from aurora.utilities.logger import logger -from aurora.utilities.utils import (check_moddable, check_permissions, - convert_timedelta_to_str, - fetch_channel_dict, fetch_user_dict, - generate_dict, log, send_evidenceformat, - timedelta_from_relativedelta) +from aurora.utilities.utils import check_moddable, check_permissions, convert_timedelta_to_str, fetch_channel_dict, fetch_user_dict, generate_dict, log, send_evidenceformat, timedelta_from_relativedelta class Aurora(commands.Cog):