from discord import Guild, Member, Message from discord.ext import commands import re from datetime import timedelta from cogs.basecog import BaseCog, BotMessage, BotMessageReaction from config import CONFIG from storage import Storage class URLSpamContext: def __init__(self, spam_message: Message): self.spam_message = spam_message self.is_deleted = False self.is_kicked = False self.is_banned = False class URLSpamCog(BaseCog): CONFIG_KEY_EARLY_URL_TIMEOUT = "urlspam_early_url_timeout" CONFIG_KEY_EARLY_URL_ACTION = "urlspam_early_url_action" def __init__(self, bot): super().__init__(bot) def __early_url_timeout(self, guild: Guild) -> int: return Storage.get_config_value(guild, self.CONFIG_KEY_EARLY_URL_TIMEOUT) or \ self.get_cog_default('early_url_timeout') def __early_url_action(self, guild: Guild) -> str: return Storage.get_config_value(guild, self.CONFIG_KEY_EARLY_URL_ACTION) or \ self.get_cog_default('early_url_action') @commands.Cog.listener() async def on_message(self, message: Message): if message.author is None or \ message.author.bot or \ message.guild is None or \ message.channel is None or \ message.content is None: return action = self.__early_url_action(message.guild) if action == 'nothing': return if not self.__contains_url(message.content): return join_age = message.created_at - message.author.joined_at join_age_str = self.__format_timedelta(join_age) if join_age.total_seconds() < self.__early_url_timeout(message.guild): if action == 'modwarn': bm = BotMessage( message.guild, f'User {message.author.mention} posted a URL ' + \ f'{join_age_str} after joining.', type = BotMessage.TYPE_MOD_WARNING, context = URLSpamContext(message)) bm.quote = message.content await bm.add_reaction(BotMessageReaction(CONFIG['trash_emoji'], True, 'Delete message')) await bm.add_reaction(BotMessageReaction(CONFIG['kick_emoji'], True, 'Kick user')) await bm.add_reaction(BotMessageReaction(CONFIG['ban_emoji'], True, 'Ban user')) await self.post_message(bm) self.log(message.guild, f'New user {message.author.name} ' + \ f'({message.author.id}) posted URL. Mods alerted.') elif action == 'delete': await message.delete() bm = BotMessage( message.guild, f'User {message.author.mention} posted a URL ' + \ f'{join_age_str} after joining. Message deleted.', type = BotMessage.TYPE_INFO, context = URLSpamContext(message)) bm.quote = message.content await bm.add_reaction(BotMessageReaction(CONFIG['kick_emoji'], True, 'Kick user')) await bm.add_reaction(BotMessageReaction(CONFIG['ban_emoji'], True, 'Ban user')) await self.post_message(bm) self.log(message.guild, f'New user {message.author.name} ' + \ f'({message.author.id}) posted URL. Message deleted.') elif action == 'kick': await message.author.kick(reason='User posted a link ' + \ f'{join_age_str} after joining') bm = BotMessage( message.guild, f'User {message.author.mention} posted a URL ' + \ f'{join_age_str} after joining. Kicked by bot.', type = BotMessage.TYPE_INFO, context = URLSpamContext(message)) bm.quote = message.content await bm.add_reaction(BotMessageReaction(CONFIG['ban_emoji'], True, 'Ban user')) await self.post_message(bm) self.log(message.guild, f'New user {message.author.name} ' + \ f'({message.author.id}) posted URL. User kicked.') elif action == 'ban': await message.author.ban(reason='User posted a link ' + \ f'{join_age_str} after joining', delete_message_days=1) bm = BotMessage( message.guild, f'User {message.author.mention} posted a URL ' + \ f'{join_age_str} after joining. Banned by bot.', type = BotMessage.TYPE_INFO, context = URLSpamContext(message)) bm.quote = message.content await self.post_message(bm) self.log(message.guild, f'New user {message.author.name} ' + \ f'({message.author.id}) posted URL. User banned.') async def on_mod_react(self, bot_message: BotMessage, reaction: BotMessageReaction, reacted_by: Member) -> None: context: URLSpamContext = bot_message.context if context is None: return sm: Message = context.spam_message if reaction.emoji == CONFIG['trash_emoji']: if not context.is_deleted: await sm.delete() context.is_deleted = True self.log(sm.guild, f'URL spam by {sm.author.name} deleted by {reacted_by.name}') elif reaction.emoji == CONFIG['kick_emoji']: if not context.is_deleted: await sm.delete() context.is_deleted = True if not context.is_kicked: await sm.author.kick(reason=f'Rocketbot: Kicked for URL spam by {reacted_by.name}') context.is_kicked = True self.log(sm.guild, f'URL spammer {sm.author.name} kicked by {reacted_by.name}') elif reaction.emoji == CONFIG['ban_emoji']: if not context.is_banned: await sm.author.ban(reason=f'Rocketbot: Banned for URL spam by {reacted_by.name}', delete_message_days=1) context.is_deleted = True context.is_kicked = True context.is_banned = True self.log(sm.guild, f'URL spammer {sm.author.name} banned by {reacted_by.name}') else: return await bot_message.set_reactions(BotMessageReaction.standard_set( did_delete=context.is_deleted, did_kick=context.is_kicked, did_ban=context.is_banned)) def __contains_url(self, text: str) -> bool: p = re.compile(r'http(?:s)?://[^\s]+') return p.search(text) is not None def __format_timedelta(self, timespan: timedelta) -> str: parts = [] d = timespan.days h = timespan.seconds // 3600 m = (timespan.seconds // 60) % 60 s = timespan.seconds % 60 if d > 0: parts.append(f'{d} days') if d > 0 or h > 0: parts.append(f'{h} hours') if d > 0 or h > 0 or m > 0: parts.append(f'{m} minutes') parts.append(f'{s} seconds') # Limit the precision to the two most significant elements while len(parts) > 2: parts.pop(-1) return ' '.join(parts)