async def test_propagate_warn_only(self, create_core): member = Member(1, 1) await create_core.cache.set_member(member) create_core._increment_duplicate_count(member, Guild(1), 1, 15) guild = await create_core.cache.get_guild(1) create_core.options.warn_only = True return_data = await create_core.propagate_user( MockedMessage(guild_id=1, author_id=1).to_mock(), guild) assert return_data == CorePayload( member_should_be_punished_this_message=True, member_status="Member was warned", member_was_warned=True, member_warn_count=1, member_duplicate_count=15, ) # Test embed coverage create_core.options.guild_log_warn_message = {"title": "test"} return_data = await create_core.propagate_user( MockedMessage(guild_id=1, author_id=1, message_id=2).to_mock(), guild) assert return_data == CorePayload( member_should_be_punished_this_message=True, member_status="Member was warned", member_was_warned=True, member_warn_count=2, member_duplicate_count=16, )
async def test_update_cache_raises(self, create_anti_spam_tracker): with pytest.raises(TypeError): await create_anti_spam_tracker.update_cache(None, None) with pytest.raises(TypeError): await create_anti_spam_tracker.update_cache( MockedMessage().to_mock(), list()) with pytest.raises(TypeError): await create_anti_spam_tracker.update_cache( MockedMessage().to_mock(), set()) await create_anti_spam_tracker.update_cache( MockedMessage(is_in_guild=False).to_mock(), CorePayload()) await create_anti_spam_tracker.update_cache(MockedMessage().to_mock(), CorePayload())
async def test_remove_punishment(self, create_anti_spam_tracker): # TODO Uncomment #68 # with pytest.raises(TypeError): # await create_anti_spam_tracker.remove_punishments(MockClass) # Skip non guild messages await create_anti_spam_tracker.remove_punishments( MockedMessage(is_in_guild=False).to_mock()) # Return since guild wasnt found await create_anti_spam_tracker.remove_punishments( MockedMessage(author_id=1, guild_id=1).to_mock()) await create_anti_spam_tracker.anti_spam_handler.cache.set_member( Member(1, 1)) await create_anti_spam_tracker.update_cache( MockedMessage(author_id=1, guild_id=1).to_mock(), CorePayload(member_should_be_punished_this_message=True), ) result = await create_anti_spam_tracker.member_tracking.get_member_data( 1, 1) assert len(result) == 1 await create_anti_spam_tracker.remove_punishments( MockedMessage(author_id=1, guild_id=1).to_mock()) result = await create_anti_spam_tracker.member_tracking.get_member_data( 1, 1) assert len(result) == 0
async def test_is_spamming(self, create_anti_spam_tracker): # TODO Fix on #68 # with pytest.raises(TypeError): # await create_anti_spam_tracker.is_spamming(MockClass) # Test not in guild returns await create_anti_spam_tracker.is_spamming( MockedMessage(is_in_guild=False).to_mock()) result_one = await create_anti_spam_tracker.is_spamming( MockedMessage().to_mock()) assert result_one is False data = CorePayload(member_should_be_punished_this_message=True) await create_anti_spam_tracker.update_cache( MockedMessage(message_id=1).to_mock(), data) await create_anti_spam_tracker.update_cache( MockedMessage(message_id=2).to_mock(), data) result_two = await create_anti_spam_tracker.is_spamming( MockedMessage().to_mock()) assert result_two is False await create_anti_spam_tracker.update_cache( MockedMessage(message_id=3).to_mock(), data) result_three = await create_anti_spam_tracker.is_spamming( MockedMessage().to_mock()) assert result_three is True
async def test_get_user_count(self, create_anti_spam_tracker): data = CorePayload(member_should_be_punished_this_message=True) await create_anti_spam_tracker.update_cache( MockedMessage(author_id=1, guild_id=1).to_mock(), data) values = await create_anti_spam_tracker.member_tracking.get_member_data( 1, 1) assert len(values) == 1 result = await create_anti_spam_tracker.get_member_count( MockedMessage(author_id=1, guild_id=1).to_mock()) assert result == 1
async def test_user_propagate_skip_guild(self, create_core): """Tests if the member gets skipped if not 'in guild'""" msg = MockedMessage(author_id=1, guild_id=1).to_mock() guild = Guild(1, Options) guild.members[1] = Member(1, 1, internal_is_in_guild=False) payload = await create_core.propagate(msg, guild) assert payload == CorePayload( member_status= "Bypassing message check since the member doesn't seem to be in a guild" )
async def test_update_cache_addon_not_found(self, create_anti_spam_tracker): """For coverage""" await create_anti_spam_tracker.anti_spam_handler.cache.set_member( Member(1, 1)) data = CorePayload(member_should_be_punished_this_message=True) await create_anti_spam_tracker.update_cache( MockedMessage(author_id=1, guild_id=1).to_mock(), data) values = await create_anti_spam_tracker.member_tracking.get_member_data( 1, 1) assert len(values) == 1
async def test_propagate_no_punish(self, create_core): g = Guild(1, Options(no_punish=True)) await create_core.cache.set_guild(g) member = Member(1, 1) await create_core.cache.set_member(member) create_core._increment_duplicate_count(member, g, 1, 15) guild = await create_core.cache.get_guild(1) return_data = await create_core.propagate_user( MockedMessage(guild_id=1, author_id=1).to_mock(), guild) assert return_data == CorePayload( member_should_be_punished_this_message=True, member_status= "Member should be punished, however, was not due to no_punish being True", )
async def test_propagate_kicks(self, create_core): member = Member(1, 1) member.warn_count = 3 create_core._increment_duplicate_count(member, Guild(1), 1, 7) await create_core.cache.set_member(member) guild = await create_core.cache.get_guild(1) return_data = await create_core.propagate_user( MockedMessage(guild_id=1, author_id=1).to_mock(), guild) assert return_data == CorePayload( member_should_be_punished_this_message=True, member_status="Member was kicked", member_was_kicked=True, member_warn_count=3, member_kick_count=1, member_duplicate_count=7, )
async def test_update_cache(self, create_anti_spam_tracker): message = MockedMessage(guild_id=1, author_id=1).to_mock() # All we need to mock for this data = CorePayload(member_should_be_punished_this_message=True) await create_anti_spam_tracker.update_cache(message, data) values = await create_anti_spam_tracker.member_tracking.get_member_data( 1, 1) assert len(values) == 1 message = MockedMessage(message_id=2, author_id=1, guild_id=1).to_mock() await create_anti_spam_tracker.update_cache(message, data) values = await create_anti_spam_tracker.member_tracking.get_member_data( 1, 1) assert len(values) == 2
async def test_stats_creates(self, create_stats): """Tests the plugin creates stats entries where required""" create_stats.handler.pre_invoke_plugins["before"] = True create_stats.handler.after_invoke_plugins["after"] = True assert create_stats.data == { "pre_invoke_calls": {}, "after_invoke_calls": {}, "propagate_calls": 0, "guilds": {}, "members": {}, } await create_stats.propagate( MockedMessage().to_mock(), CorePayload(member_should_be_punished_this_message=True), ) assert len(create_stats.data["pre_invoke_calls"]) == 1 assert len(create_stats.data["after_invoke_calls"]) == 1 assert len(create_stats.data["members"]) == 1 assert len(create_stats.data["guilds"]) == 1 assert create_stats.data["propagate_calls"] == 1
async def test_stats_adds(self, create_stats): """Tests the plugin adds to existing data""" create_stats.handler.pre_invoke_plugins["before"] = True create_stats.handler.after_invoke_plugins["after"] = True create_stats.data = { "pre_invoke_calls": { "before": { "calls": 1 } }, "after_invoke_calls": { "after": { "calls": 1 } }, "propagate_calls": 1, "guilds": { 123456789: { "calls": 1, "total_messages_punished": 1 } }, "members": { 12345: { "calls": 1, "times_punished": 1 } }, } await create_stats.propagate( MockedMessage().to_mock(), CorePayload(), ) await create_stats.propagate( MockedMessage().to_mock(), CorePayload(member_should_be_punished_this_message=True), ) assert create_stats.data == { "pre_invoke_calls": { "before": { "calls": 3 } }, "after_invoke_calls": { "after": { "calls": 3 } }, "propagate_calls": 3, "guilds": { 123456789: { "calls": 3, "total_messages_punished": 2 } }, "members": { 12345: { "calls": 3, "times_punished": 2 } }, }
async def propagate_user(self, original_message, guild: Guild) -> CorePayload: """ The internal representation of core functionality. Please see and use :meth:`discord.ext.antispam.AntiSpamHandler.propagate` """ try: if original_message.author.id in guild.members: member = guild.members[original_message.author.id] else: member: Member = await self.cache.get_member( member_id=original_message.author.id, guild_id=await self.handler.lib_handler.get_guild_id(original_message), ) if not member.internal_is_in_guild: return CorePayload( member_status= "Bypassing message check since the member doesn't seem to be in a guild" ) except MemberNotFound: # Create a use-able member member = Member( id=original_message.author.id, guild_id=await self.handler.lib_handler.get_guild_id(original_message), ) guild.members[member.id] = member await self.cache.set_guild(guild=guild) await self.clean_up( member=member, current_time=get_aware_time(), channel_id=await self.handler.lib_handler.get_channel_id(original_message), guild=guild, ) message: Message = await self.handler.lib_handler.create_message( original_message) self._calculate_ratios(message, member, guild) await self.cache.add_message(message) log.info( "Created Message(%s) on Member(id=%s) in Guild(id=%s)", message.id, member.id, member.guild_id, ) if (self._get_duplicate_count( member, guild, channel_id=message.channel_id) < self.options(guild).message_duplicate_count): return CorePayload() # Check again since in theory the above could take awhile # Not sure how to hit this in tests, but I've seen it happen so is required if not member.internal_is_in_guild: # pragma: no cover return CorePayload( member_status= "Bypassing message check since the member doesn't seem to be in a guild" ) # We need to punish the member with something log.debug( "Message(%s) on Member(id=%s) in Guild(id=%s) requires some form of punishment", message.id, member.id, member.guild_id, ) # We need to punish the member with something return_payload = CorePayload( member_should_be_punished_this_message=True) if self.options(guild).no_punish: # User will handle punishments themselves return CorePayload( member_should_be_punished_this_message=True, member_status= "Member should be punished, however, was not due to no_punish being True", ) if self.options(guild).use_timeouts: log.debug( "Attempting to timeout Member(id=%s) in Guild(id=%s)", message.author_id, message.guild_id, ) _author = await self.handler.lib_handler.get_member_from_message( original_message) if await self.handler.lib_handler.is_member_currently_timed_out( _author): return CorePayload( member_should_be_punished_this_message=None, member_status= "Attempted to timeout this member, however, they are already timed out.", ) member.internal_is_in_guild = False times_timed_out: int = member.times_timed_out + 1 guild_message = await self.handler.lib_handler.transform_message( self.options(guild).guild_log_timeout_message, original_message, member.warn_count, member.kick_count, ) user_message = await self.handler.lib_handler.transform_message( self.options(guild).member_timeout_message, original_message, member.warn_count, member.kick_count, ) try: await self.handler.lib_handler.send_message_to_( original_message.author, user_message, original_message.author.mention, self.options(guild).member_timeout_message_delete_after, ) except: await self.handler.lib_handler.send_guild_log( guild=guild, message= f"Sending a message to {original_message.author.mention} about their timeout failed.", delete_after_time=self.options( guild).member_timeout_message_delete_after, original_channel=original_message.channel, ) log.warning( f"Failed to message Member(id=%s) about being timed out.", original_message.author.id, ) # Timeouts # 5 # 20 # 45 # ... timeout_until: datetime.timedelta = datetime.timedelta( minutes=(times_timed_out * times_timed_out) * 5) try: await self.handler.lib_handler.timeout_member( original_message.author, original_message, timeout_until) except UnsupportedAction: member.internal_is_in_guild = True raise except Exception as e: member.internal_is_in_guild = True user_failed_message = await self.handler.lib_handler.transform_message( self.handler.options.member_failed_timeout_message, original_message, member.warn_count, member.kick_count, ) guild_failed_message = await self.handler.lib_handler.transform_message( "I failed to timeout $MEMBERNAME ($MEMBERID) as I lack permissions.", original_message, member.warn_count, member.kick_count, ) await self.handler.lib_handler.send_guild_log( guild, guild_failed_message, self.options(guild).member_timeout_message_delete_after, original_message.channel, ) await self.handler.lib_handler.send_message_to_( original_message.author, user_failed_message, original_message.author.mention, self.options(guild).member_timeout_message_delete_after, ) raise e from None else: await self.handler.lib_handler.send_guild_log( guild, guild_message, self.options(guild).guild_log_timeout_message_delete_after, original_channel=await self.handler.lib_handler.get_channel_from_message( original_message), ) member.times_timed_out += 1 member.internal_is_in_guild = True await self.handler.cache.set_member(member) return_payload.member_was_timed_out = True return_payload.member_status = "Member was timed out" elif (self.options(guild).warn_only or self._get_duplicate_count(member, guild, message.channel_id) >= self.options(guild).warn_threshold and member.warn_count < self.options(guild).kick_threshold and member.kick_count < self.options(guild).ban_threshold): """ WARN The member has yet to reach the warn threshold, after the warn threshold is reached this will then become a kick and so on """ log.debug( "Attempting to warn Member(id=%s) in Guild(id=%s)", message.author_id, message.guild_id, ) member.warn_count += 1 channel = await self.handler.lib_handler.get_channel_from_message( original_message) member_message = await self.handler.lib_handler.transform_message( self.options(guild).member_warn_message, original_message, member.warn_count, member.kick_count, ) guild_message = await self.handler.lib_handler.transform_message( self.options(guild).guild_log_warn_message, original_message, member.warn_count, member.kick_count, ) try: await self.handler.lib_handler.send_message_to_( channel, member_message, original_message.author.mention, self.options(guild).member_warn_message_delete_after, ) except Exception as e: # pragma: no cover # This is a general sos, haven't figured out a way # to raise this late but it could in theory happen member.warn_count -= 1 raise e # Log this within guild log channels await self.handler.lib_handler.send_guild_log( guild=guild, message=guild_message, original_channel=await self.handler.lib_handler.get_channel_from_message( original_message), delete_after_time=self.options( guild).guild_log_warn_message_delete_after, ) return_payload.member_was_warned = True return_payload.member_status = "Member was warned" elif (member.warn_count >= self.options(guild).kick_threshold and member.kick_count < self.options(guild).ban_threshold): # KICK # Set this to False here to stop processing other messages, we can revert on failure member.internal_is_in_guild = False member.kick_count += 1 log.debug( "Attempting to kick Member(id=%s) from Guild(id=%s)", message.author_id, message.guild_id, ) guild_message = await self.handler.lib_handler.transform_message( self.options(guild).guild_log_kick_message, original_message, member.warn_count, member.kick_count, ) user_message = await self.handler.lib_handler.transform_message( self.options(guild).member_kick_message, original_message, member.warn_count, member.kick_count, ) # This variable is for future usage to phase out # the requirement to set member.internal_is_... # within a lib impl _success: bool = await self.handler.lib_handler.punish_member( original_message, member, guild, user_message, guild_message, True, self.options(guild).member_kick_message_delete_after, self.options(guild).guild_log_kick_message_delete_after, ) return_payload.member_was_kicked = True return_payload.member_status = "Member was kicked" elif member.kick_count >= self.options(guild).ban_threshold: # BAN # Set this to False here to stop processing other messages, we can revert on failure member.internal_is_in_guild = False member.kick_count += 1 log.debug( "Attempting to ban Member(id=%s) from Guild(id=%s)", message.author_id, message.guild_id, ) guild_message = await self.handler.lib_handler.transform_message( self.options(guild).guild_log_ban_message, original_message, member.warn_count, member.kick_count, ) user_message = await self.handler.lib_handler.transform_message( self.options(guild).member_ban_message, original_message, member.warn_count, member.kick_count, ) _success: bool = await self.handler.lib_handler.punish_member( original_message, member, guild, user_message, guild_message, False, self.options(guild).member_ban_message_delete_after, self.options(guild).guild_log_ban_message_delete_after, ) return_payload.member_was_banned = True return_payload.member_status = "Member was banned" else: # pragma: no cover # We shouldn't hit this, but for punishments # i'd rather be explicit then implicit raise LogicError # Store the updated values await self.cache.set_member(member) # Delete the message if wanted if (self.options(guild).delete_spam is True and self.options(guild).no_punish is False): await self.handler.lib_handler.delete_message(original_message) await self.handler.lib_handler.delete_member_messages(member) # Finish payload and return return_payload.member_warn_count = member.warn_count return_payload.member_kick_count = member.kick_count return_payload.member_duplicate_count = (self._get_duplicate_count( member=member, channel_id=message.channel_id, guild=guild) - 1) return return_payload
async def propagate_user(self, original_message, guild: Guild) -> CorePayload: """ The internal representation of core functionality. Please see and use :meth:`discord.ext.antispam.AntiSpamHandler.propagate` """ try: if original_message.author.id in guild.members: member = guild.members[original_message.author.id] else: member: Member = await self.cache.get_member( member_id=original_message.author.id, guild_id=await self.handler.lib_handler.get_guild_id( original_message ), ) if not member._in_guild: return CorePayload( member_status="Bypassing message check since the member isn't seen to be in a guild" ) except MemberNotFound: # Create a use-able member member = Member( id=original_message.author.id, guild_id=await self.handler.lib_handler.get_guild_id(original_message), ) guild.members[member.id] = member await self.cache.set_guild(guild=guild) await self.clean_up( member=member, current_time=get_aware_time(), channel_id=await self.handler.lib_handler.get_channel_id(original_message), ) message: Message = await self.handler.lib_handler.create_message( original_message ) self._calculate_ratios(message, member) # Check again since in theory the above could take awhile if not member._in_guild: return CorePayload( member_status="Bypassing message check since the member isn't seen to be in a guild" ) member.messages.append(message) log.info( "Created Message(%s) on Member(id=%s) in Guild(id=%s)", message.id, member.id, member.guild_id, ) if ( self._get_duplicate_count(member, channel_id=message.channel_id) < self.options.message_duplicate_count ): return CorePayload() # We need to punish the member with something log.debug( "Message(%s) on Member(id=%s) in Guild(id=%s) requires some form of punishment", message.id, member.id, member.guild_id, ) # We need to punish the member with something return_payload = CorePayload(member_should_be_punished_this_message=True) if self.options.no_punish: # User will handle punishments themselves return CorePayload( member_should_be_punished_this_message=True, member_status="Member should be punished, however, was not due to no_punish being True", ) if ( self.options.warn_only or self._get_duplicate_count(member, message.channel_id) >= self.options.warn_threshold and member.warn_count < self.options.kick_threshold and member.kick_count < self.options.ban_threshold ): """ WARN The member has yet to reach the warn threshold, after the warn threshold is reached this will then become a kick and so on """ log.debug( "Attempting to warn Member(id=%s) in Guild(id=%s)", message.author_id, message.guild_id, ) member.warn_count += 1 channel = await self.handler.lib_handler.get_channel_from_message( original_message ) member_message = await self.handler.lib_handler.transform_message( self.options.member_warn_message, original_message, member.warn_count, member.kick_count, ) guild_message = await self.handler.lib_handler.transform_message( self.options.guild_log_warn_message, original_message, member.warn_count, member.kick_count, ) try: await self.handler.lib_handler.send_message_to_( channel, member_message, original_message.author.mention, self.options.member_warn_message_delete_after, ) except Exception as e: member.warn_count -= 1 raise e # Log this within guild log channels await self.handler.lib_handler.send_guild_log( guild=guild, message=guild_message, original_channel=await self.handler.lib_handler.get_channel_from_message( original_message ), delete_after_time=self.options.guild_log_warn_message_delete_after, ) return_payload.member_was_warned = True return_payload.member_status = "Member was warned" elif ( member.warn_count >= self.options.kick_threshold and member.kick_count < self.options.ban_threshold ): # KICK # Set this to False here to stop processing other messages, we can revert on failure member._in_guild = False member.kick_count += 1 log.debug( "Attempting to kick Member(id=%s) from Guild(id=%s)", message.author_id, message.guild_id, ) guild_message = await self.handler.lib_handler.transform_message( self.options.guild_log_kick_message, original_message, member.warn_count, member.kick_count, ) user_message = await self.handler.lib_handler.transform_message( self.options.member_kick_message, original_message, member.warn_count, member.kick_count, ) await self.handler.lib_handler.punish_member( original_message, member, guild, user_message, guild_message, True, self.options.member_kick_message_delete_after, self.options.guild_log_kick_message_delete_after, ) return_payload.member_was_kicked = True return_payload.member_status = "Member was kicked" elif member.kick_count >= self.options.ban_threshold: # BAN # Set this to False here to stop processing other messages, we can revert on failure member._in_guild = False member.kick_count += 1 log.debug( "Attempting to ban Member(id=%s) from Guild(id=%s)", message.author_id, message.guild_id, ) guild_message = await self.handler.lib_handler.transform_message( self.options.guild_log_ban_message, original_message, member.warn_count, member.kick_count, ) user_message = await self.handler.lib_handler.transform_message( self.options.member_ban_message, original_message, member.warn_count, member.kick_count, ) await self.handler.lib_handler.punish_member( original_message, member, guild, user_message, guild_message, False, self.options.member_ban_message_delete_after, self.options.guild_log_ban_message_delete_after, ) return_payload.member_was_banned = True return_payload.member_status = "Member was banned" else: # Supports backwards compat? # Not sure if this is required tbh raise LogicError # Store the updated values await self.cache.set_member(member) # Delete the message if wanted if self.options.delete_spam is True and self.options.no_punish is False: await self.handler.lib_handler.delete_message(original_message) await self.handler.lib_handler.delete_member_messages(member) # Finish payload and return return_payload.member_warn_count = member.warn_count return_payload.member_kick_count = member.kick_count return_payload.member_duplicate_count = ( self._get_duplicate_count( member=member, channel_id=message.channel_id, ) - 1 ) return return_payload
async def test_propagate(self, create_anti_spam_tracker): """Tests this propagates the call to update_cache""" return_value = await create_anti_spam_tracker.propagate( MockedMessage().to_mock(), CorePayload()) assert return_value == {"status": "Cache updated"}