def load_from_dict(bot, user_data): """ Loads a new user obj from a dict Parameters ---------- bot : commands.Bot The bot user_data : dict The data to load state from Returns ------- User """ user = User( bot=bot, id=user_data["id"], guild_id=user_data["guild_id"], options=deepcopy(user_data["options"]), ) user.in_guild = user_data["is_in_guild"] user.warn_count = user_data["warn_count"] user.kick_count = user_data["kick_count"] user.duplicate_counter = user_data["duplicate_count"] user.duplicate_channel_counter_dict = deepcopy( user_data["duplicate_channel_counter_dict"] ) for message_data in user_data["messages"]: # Do this to save overhead in the message object # and keep it as small as possible message = Message( id=message_data["id"], content=message_data["content"], guild_id=message_data["guild_id"], author_id=message_data["author_id"], channel_id=message_data["channel_id"], ) message.is_duplicate = message_data["is_duplicate"] message._creation_time = datetime.datetime.strptime( message_data["creation_time"], "%f:%S:%M:%H:%d:%Y" ) user.messages = message log.debug(f"Created Message ({message.id}) from saved state") log.debug(f"Created User ({user.id}) from saved state") return user
async def propagate(self, value: discord.Message): """ This method handles a message object and then adds it to the relevant member Parameters ========== value : discord.Message The message that needs to be propagated out """ if not isinstance(value, discord.Message) and not isinstance( value, MagicMock): raise ValueError( "Expected message of ignore_type: discord.Message") # Setup our return values for the end user to use return_data = { "was_punished_this_message": False, "was_warned": False, "was_kicked": False, "was_banned": False, "status": "Unknown", } # Here we just check if the user is still in the guild by checking if the in_guild attribute is False. # Because if its False then we don't need to process the message. if not self.in_guild: return self.clean_up(datetime.datetime.now(datetime.timezone.utc)) # No point saving empty messages, although discord shouldn't allow them anyway if not bool(value.content and value.content.strip()): if not value.embeds: return embed = value.embeds[0] if not isinstance(embed, discord.Embed): return if embed.type.lower() != "rich": return content = embed_to_string(embed) else: content = value.clean_content message = Message( value.id, content, value.author.id, value.channel.id, value.guild.id, ) for message_obj in self.messages: # This calculates the relation to each other if message == message_obj: raise DuplicateObject elif (fuzz.token_sort_ratio(message.content, message_obj.content) >= self.options["message_duplicate_accuracy"]): """ The handler works off an internal message duplicate counter so just increment that and then let our logic process it """ self.duplicate_counter += 1 message.is_duplicate = True if self.duplicate_counter >= self.options[ "message_duplicate_count"]: break # We check this again, because theoretically the above can take awhile to process etc if not self.in_guild: return self.messages = message logging.info(f"Created Message: {message.id}") if self.duplicate_counter >= self.options["message_duplicate_count"]: logging.debug( f"Message: ({message.id}) requires some form of punishment") # We need to punish the member with something return_data["was_punished_this_message"] = True punish_bypass = False if self.options["warn_only"]: punish_bypass = True if (self.duplicate_counter >= self.options["warn_threshold"] and self.warn_count < self.options["kick_threshold"] and self.kick_count < self.options["ban_threshold"] or punish_bypass): logging.debug(f"Attempting to warn: {message.author_id}") """ The member has yet to reach the warn threshold, after the warn threshold is reached this will then become a kick and so on """ # We are still in the warning area self.warn_count += 1 channel = value.channel guild_message = transform_message( self.options["guild_warn_message"], value, { "warn_count": self.warn_count, "kick_count": self.kick_count }, ) try: await send_to_obj(channel, guild_message) except Exception as e: self.warn_count -= 1 raise e return_data["was_warned"] = True return_data["status"] = "User was warned." elif (self.warn_count >= self.options["kick_threshold"] and self.kick_count < self.options["ban_threshold"]): # Set this to False here to stop processing other messages, we can revert on failure self.in_guild = False self.kick_count += 1 logging.debug(f"Attempting to kick: {message.author_id}") # We should kick the member guild_message = transform_message( self.options["guild_kick_message"], value, { "warn_count": self.warn_count, "kick_count": self.kick_count }, ) user_message = transform_message( self.options["user_kick_message"], value, { "warn_count": self.warn_count, "kick_count": self.kick_count }, ) await self._punish_user( value, user_message, guild_message, Static.KICK, ) return_data["was_kicked"] = True return_data["status"] = "User was kicked." elif self.kick_count >= self.options["ban_threshold"]: # Set this to False here to stop processing other messages, we can revert on failure self.in_guild = False self.kick_count += 1 logging.debug(f"Attempting to ban: {message.author_id}") # We should ban the member guild_message = transform_message( self.options["guild_ban_message"], value, { "warn_count": self.warn_count, "kick_count": self.kick_count }, ) user_message = transform_message( self.options["user_ban_message"], value, { "warn_count": self.warn_count, "kick_count": self.kick_count }, ) await self._punish_user( value, user_message, guild_message, Static.BAN, ) return_data["was_banned"] = True return_data["status"] = "User was banned." else: raise LogicError return_data["warn_count"] = self.warn_count return_data["kick_count"] = self.kick_count return_data["duplicate_counter"] = self.get_correct_duplicate_count() return return_data
async def propagate(self, value: discord.Message): """ This method handles a message object and then adds it to the relevant member Parameters ========== value : discord.Message The message that needs to be propagated out Warnings ======== Calling this method yourself will bypass all checks """ if not isinstance(value, (discord.Message, AsyncMock)): raise ValueError("Expected message of ignore_type: discord.Message") # Setup our return values for the end user to use return_data = { "should_be_punished_this_message": False, "was_warned": False, "was_kicked": False, "was_banned": False, "status": "Unknown", } # Here we just check if the user is still in the guild by checking if the in_guild attribute is False. # Because if its False then we don't need to process the message. if not self.in_guild: return {"status": "Bypassing message check since the user isnt in a guild"} self.clean_up(datetime.datetime.now(datetime.timezone.utc)) # No point saving empty messages, although discord shouldn't allow them anyway if not bool(value.content and value.content.strip()): if not value.embeds: return {"status": "No point saving empty messages"} embed = value.embeds[0] if not isinstance(embed, discord.Embed): return {"status": "No point saving empty messages"} if embed.type.lower() != "rich": return {"status": "No point saving empty messages"} content = embed_to_string(embed) else: content = value.clean_content message = Message( value.id, content, value.author.id, value.channel.id, value.guild.id, ) for message_obj in self.messages: # This calculates the relation to each other if message == message_obj: raise DuplicateObject elif ( self.options.get("per_channel_spam") and message.channel_id != message_obj.channel_id ): # This user's spam should only be counted per channel # and these messages are in different channel continue elif ( fuzz.token_sort_ratio(message.content, message_obj.content) >= self.options["message_duplicate_accuracy"] ): """ The handler works off an internal message duplicate counter so just increment that and then let our logic process it """ self._increment_duplicate_count(message) message.is_duplicate = True if ( self._get_duplicate_count(message) >= self.options["message_duplicate_count"] ): break # We check this again, because theoretically the above can take awhile to process etc if not self.in_guild: return {"status": "Bypassing message check since the user isnt in a guild"} self.messages = message log.info(f"Created Message: {message.id}") if ( self._get_duplicate_count(message) >= self.options["message_duplicate_count"] ): log.debug(f"Message: ({message.id}) requires some form of punishment") # We need to punish the member with something return_data["should_be_punished_this_message"] = True only_warn = False if ( self.options.get("delete_spam") is True and self.options.get("no_punish") is False ): try: await value.delete() log.debug(f"Deleted message: {value.id}") except discord.HTTPException: # Failed to delete message log.warning( f"Failed to delete message {value.id} in guild {value.guild.id}" ) if self.options["warn_only"]: only_warn = True if self.options["no_punish"]: # no_punish, just return saying they should be punished return_data[ "status" ] = "User should be punished, however, was not due to no_punish being True" elif ( self._get_duplicate_count(message) >= self.options["warn_threshold"] and self.warn_count < self.options["kick_threshold"] and self.kick_count < self.options["ban_threshold"] or only_warn ): log.debug(f"Attempting to warn: {message.author_id}") """ The member has yet to reach the warn threshold, after the warn threshold is reached this will then become a kick and so on """ # We are still in the warning area self.warn_count += 1 channel = value.channel guild_message = transform_message( self.options["guild_warn_message"], value, {"warn_count": self.warn_count, "kick_count": self.kick_count}, ) try: if isinstance(guild_message, discord.Embed): await channel.send( embed=guild_message, delete_after=self.options.get( "guild_warn_message_delete_after" ), ) else: await channel.send( guild_message, delete_after=self.options.get( "guild_warn_message_delete_after" ), ) except Exception as e: self.warn_count -= 1 raise e return_data["was_warned"] = True return_data["status"] = "User was warned." elif ( self.warn_count >= self.options["kick_threshold"] and self.kick_count < self.options["ban_threshold"] ): # Set this to False here to stop processing other messages, we can revert on failure self.in_guild = False self.kick_count += 1 log.debug(f"Attempting to kick: {message.author_id}") # We should kick the member guild_message = transform_message( self.options["guild_kick_message"], value, {"warn_count": self.warn_count, "kick_count": self.kick_count}, ) user_message = transform_message( self.options["user_kick_message"], value, {"warn_count": self.warn_count, "kick_count": self.kick_count}, ) await self._punish_user( value, user_message, guild_message, Static.KICK, self.options.get("user_kick_message_delete_after"), self.options.get("guild_kick_message_delete_after"), ) return_data["was_kicked"] = True return_data["status"] = "User was kicked." elif self.kick_count >= self.options["ban_threshold"]: # Set this to False here to stop processing other messages, we can revert on failure self.in_guild = False self.kick_count += 1 log.debug(f"Attempting to ban: {message.author_id}") # We should ban the member guild_message = transform_message( self.options["guild_ban_message"], value, {"warn_count": self.warn_count, "kick_count": self.kick_count}, ) user_message = transform_message( self.options["user_ban_message"], value, {"warn_count": self.warn_count, "kick_count": self.kick_count}, ) await self._punish_user( value, user_message, guild_message, Static.BAN, self.options.get("user_ban_message_delete_after"), self.options.get("guild_ban_message_delete_after"), ) return_data["was_banned"] = True return_data["status"] = "User was banned." else: raise LogicError return_data["warn_count"] = self.warn_count return_data["kick_count"] = self.kick_count return_data["duplicate_counter"] = self.get_correct_duplicate_count( message.channel_id ) return return_data