async def asyncSetUp(self): """ Simply setup our Ash obj before usage """ self.ash = AntiSpamHandler(get_mocked_bot(name="bot", id=98987)) self.ash.guilds = Guild(None, 12, Static.DEFAULTS) self.ash.guilds = Guild(None, 15, Static.DEFAULTS)
def remove_custom_guild_options(self, guild_id: int) -> None: """ Reset a guilds options to the ASH options Parameters ---------- guild_id : int The guild to reset Notes ----- This method will silently ignore guilds that do not exist, as it is considered to have 'removed' custom options due to how Guild's are created """ guild = Guild(self.bot, guild_id, self.options, logger=logging) try: guild = next(iter(g for g in self.guilds if g == guild)) except StopIteration: pass else: guild.options = self.options guild.has_custom_options = False
def remove_custom_guild_options(self, guild_id: int) -> None: """ Reset a guilds options to the ASH options Parameters ---------- guild_id : int The guild to reset Warnings -------- If using ``AntiSpamTracker``, please call this method on that class instance. Not this one. Notes ----- This method will silently ignore guilds that do not exist, as it is considered to have 'removed' custom options due to how Guild's are created This is also a somewhat expensive operation at ``O(n)`` where ``n`` is the total number of users cached for the guild """ guild = Guild(self.bot, guild_id, self.options) try: guild = next(iter(g for g in self.guilds if g == guild)) except StopIteration: pass else: guild.options = self.options guild.has_custom_options = False log.debug(f"Reset guild options for {guild_id}")
def remove_custom_guild_options(self, guild_id: int) -> None: """ Reset a guilds options to the ASH options Parameters ---------- guild_id : int The guild to reset Warnings -------- If using ``AntiSpamTracker``, please call this method on that class instance. Not this one. Notes ----- This method will silently ignore guilds that do not exist, as it is considered to have 'removed' custom options due to how Guild's are created """ guild = Guild(self.bot, guild_id, self.options) try: guild = next(iter(g for g in self.guilds if g == guild)) except StopIteration: pass else: guild.options = self.options guild.has_custom_options = False
async def asyncSetUp(self): """ Simply setup our Ash obj before usage """ self.ash = AntiSpamHandler( MockedMember(name="bot", member_id=98987, mock_type="bot").to_mock()) self.ash.guilds = Guild(None, 12, Static.DEFAULTS) self.ash.guilds = Guild(None, 15, Static.DEFAULTS)
def setUp(self): """ Simply setup our Guild obj before usage """ self.guild = Guild(None, 15, Static.DEFAULTS) self.guild.users = User(None, 20, 15, Static.DEFAULTS) self.guild.users = User(None, 21, 15, Static.DEFAULTS)
async def test_properties(self): with self.assertRaises(ValueError): self.ash.guilds = "1" with self.assertRaises(DuplicateObject): self.ash.guilds = Guild( None, 15, Static.DEFAULTS, logger=logging.getLogger(__name__) )
def reset_user_count(self, user_id: int, guild_id: int, counter: str) -> None: """ Reset an internal counter attached to a User object Parameters ---------- user_id : int The user to reset guild_id : int The guild they are attached to counter : str A str denoting which count to reset, Options are:\n ``warn_counter`` -> Reset the warn count\n ``kick_counter`` -> Reset the kick count Raises ====== LogicError Invalid count to reset Notes ===== Silently ignores if the User or Guild does not exist. This is because in the packages mind, the counts are 'reset' since the default value is the reset value. """ guild = Guild(self.bot, guild_id, self.options) try: guild = next(iter(g for g in self.guilds if g == guild)) except StopIteration: return user = User(self.bot, user_id, guild_id=guild_id, options=guild.options) try: user = next(iter(u for u in guild.users if u == user)) except StopIteration: return if counter.lower() == Static.WARNCOUNTER: user.warn_count = 0 log.debug(f"Reset the warn count for user: {user_id}") elif counter.lower() == Static.KICKCOUNTER: user.kick_count = 0 log.debug(f"Reset the kick count for user: {user_id}") else: raise LogicError( "Invalid counter argument, please select a valid counter.")
def get_guild_options(self, guild_id: int) -> tuple: """ Get the options dictionary for a given guild, if the guild doesnt exist raise an exception Parameters ---------- guild_id : int The guild to get custom options for Returns ------- tuple The options for this guild as tuple[0] and tuple[1] is a bool which is used to say if the guild has custom options or not Be wary of the return value. It is in the format, (dict, boolean), where dict is the options and boolean is whether or not these options are custom Raises ------ BaseASHException This guild does not exist Notes ----- The value for tuple[1] is not checked/ensured at runtime. Be wary of this if you access guild and manually change options rather then using this libraries methods. Another thing to note is this returns a deepcopy of the options dictionary. This is to encourage usage of this libraries methods for changing options, rather then playing around with them yourself and potentially doing damage. """ guild = Guild(self.bot, guild_id, self.options) try: guild = next(iter(g for g in self.guilds if g == guild)) except StopIteration: raise BaseASHException("This guild does not exist") else: log.debug(f"Returned guild options for {guild_id}") return deepcopy(guild.options), guild.has_custom_options
def setUp(self): """ Simply setup our Guild obj before usage """ self.guild = Guild(None, 15, Static.DEFAULTS, logger=logging.getLogger(__name__)) self.guild.users = User(None, 20, 15, Static.DEFAULTS, logger=logging.getLogger(__name__)) self.guild.users = User(None, 21, 15, Static.DEFAULTS, logger=logging.getLogger(__name__))
async def test_properties(self): with self.assertRaises(ValueError): self.ash.guilds = "1" with self.assertRaises(DuplicateObject): self.ash.guilds = Guild(None, 15, Static.DEFAULTS)
def load_from_dict( bot, data: dict, *, raise_on_exception: bool = True): # TODO typehint this correct """ Can be used as an entry point when starting your bot to reload a previous state so you don't lose all of the previous punishment records, etc, etc Parameters ---------- bot : commands.Bot The bot instance data : dict The data to load AntiSpamHandler from raise_on_exception : bool Whether or not to raise if an issue is encountered while trying to rebuild ``AntiSpamHandler`` from a saved state If you set this to False, and an exception occurs during the build process. This will return an ``AntiSpamhandler`` instance **without** any of the saved state and is equivalent to simply doing ``AntiSpamHandler(bot)`` Returns ------- AntiSpamHandler A new AntiSpamHandler instance where the state is equal to the provided dict Warnings -------- Don't provide data that was not given to you outside of the ``save_to_dict`` method unless you are maintaining the correct format. Notes ----- This method does not check for data conformity. Any invalid input will error unless you set ``raise_on_exception`` to ``False`` in which case the following occurs If you set ``raise_on_exception`` to ``False``, and an exception occurs during the build process. This method will return an ``AntiSpamhandler`` instance **without** any of the saved state and is equivalent to simply doing ``AntiSpamHandler(bot)`` ----- This is fairly computationally expensive. It deepcopies nearly everything lol. """ try: ash = AntiSpamHandler(bot=bot, **data["options"]) for guild in data["guilds"]: ash.guilds = Guild.load_from_dict(bot, guild) log.info("Loaded AntiSpamHandler from state") except Exception as e: if raise_on_exception: log.debug( "Raising exception when attempting to load from state") raise e ash = AntiSpamHandler(bot=bot) log.info( "Failed to load AntiSpamHandler from state, returning a generic instance" ) return ash
def add_custom_guild_options(self, guild_id: int, **kwargs): """ Set a guild's options to a custom set, rather then the base level set used and defined in ASH initialization Parameters ---------- guild_id : int The id of the guild to create warn_threshold : int, optional This is the amount of messages in a row that result in a warning within the message_interval kick_threshold : int, optional The amount of 'warns' before a kick occurs ban_threshold : int, optional The amount of 'kicks' that occur before a ban occurs message_interval : int, optional Amount of time a message is kept before being discarded. Essentially the amount of time (In milliseconds) a message can count towards spam guild_warn_message : Union[str, dict], optional The message to be sent in the guild upon warn_threshold being reached guild_kick_message : Union[str, dict], optional The message to be sent in the guild upon kick_threshold being reached guild_ban_message : Union[str, dict], optional The message to be sent in the guild upon ban_threshold being reached user_kick_message : Union[str, dict], optional The message to be sent to the user who is being warned user_ban_message : Union[str, dict], optional The message to be sent to the user who is being banned user_failed_kick_message : Union[str, dict], optional The message to be sent to the user if the bot fails to kick them user_failed_ban_message : Union[str, dict], optional The message to be sent to the user if the bot fails to ban them message_duplicate_count : int, optional Amount of duplicate messages needed to trip a punishment message_duplicate_accuracy : float, optional How 'close' messages need to be to be registered as duplicates (Out of 100) delete_spam : bool, optional Whether or not to delete any messages marked as spam ignore_perms : list, optional The perms (ID Form), that bypass anti-spam ignore_channels : list, optional The channels (ID Form) that are ignored ignore_roles : list, optional The roles (ID, Name) that are ignored ignore_guilds : list, optional The guilds (ID) that are ignored ignore_users : list, optional The users (ID Form), that bypass anti-spam ignore_bots : bool, optional Should bots bypass anti-spam? warn_only : bool, optional Only warn users? no_punish : bool, optional Dont punish users? Return if they should be punished or not without actually punishing them per_channel_spam : bool, optional Track spam as per channel, rather then per guild guild_warn_message_delete_after : int, optional The time to delete the ``guild_warn_message`` message user_kick_message_delete_after : int, optional The time to delete the ``user_kick_message`` message guild_kick_message_delete_after : int, optional The time to delete the ``guild_kick_message`` message user_ban_message_delete_after : int, optional The time to delete the ``user_ban_message`` message guild_ban_message_delete_after : int, optional The time to delete the ``guild_ban_message`` message Warnings -------- If using ``AntiSpamTracker``, please call this method on that class instance. Not this one. Notes ===== This will override any current settings, if you wish to continue using existing settings and merely change some I suggest using the get_options method first and then giving those values back to this method with the changed arguments This is also a somewhat expensive operation at ``O(n)`` where ``n`` is the total number of users cached for the guild """ options = self._ensure_options(**kwargs) guild = Guild(self.bot, guild_id, options, custom_options=True) try: guild = next(iter(g for g in self.guilds if g == guild)) except StopIteration: log.warning( f"I cannot ensure I have permissions to kick/ban ban people in guild: {guild_id}" ) self.guilds = guild log.info(f"Created Guild: {guild.id}") else: guild.options = options guild.has_custom_options = True log.info(f"Set custom options for guild: {guild_id}")
async def propagate(self, message: discord.Message) -> Optional[dict]: """ This method is the base level intake for messages, then propagating it out to the relevant guild or creating one if that is required For what this returns please see the top of this page. Parameters ========== message : discord.Message The message that needs to be propagated out Returns ======= dict A dictionary of useful information about the user in question """ if not isinstance(message, discord.Message) and not isinstance( message, AsyncMock): log.debug("Invalid value given to propagate") raise ValueError( "Expected message of ignore_type: discord.Message") # Ensure we only moderate actual guild messages if not message.guild: log.debug("Message was not in a guild") return {"status": "Ignoring messages from dm's"} # The bot is immune to spam if message.author.id == self.bot.user.id: log.debug("Message was from myself") return {"status": "Ignoring messages from myself (the bot)"} if isinstance(message.author, discord.User): log.warning(f"Given message with an author of type User") # Return if ignored bot if self.options["ignore_bots"] and message.author.bot: log.debug( f"I ignore bots, and this is a bot message: {message.author.id}" ) return {"status": "Ignoring messages from bots"} # Return if ignored member if message.author.id in self.options["ignore_users"]: log.debug( f"The user who sent this message is ignored: {message.author.id}" ) return {"status": f"Ignoring this user: {message.author.id}"} # Return if ignored channel if (message.channel.id in self.options["ignore_channels"] or message.channel.name in self.options["ignore_channels"]): log.debug(f"{message.channel} is ignored") return {"status": f"Ignoring this channel: {message.channel.id}"} # Return if member has an ignored role try: user_roles = [role.id for role in message.author.roles] user_roles.extend([role.name for role in message.author.roles]) for item in user_roles: if item in self.options.get("ignore_roles"): log.debug(f"{item} is a part of ignored roles") return {"status": f"Ignoring this role: {item}"} except AttributeError: log.warning( f"Could not compute ignore_roles for {message.author.name}({message.author.id})" ) # Return if ignored guild if message.guild.id in self.options.get("ignore_guilds"): log.debug(f"{message.guild.id} is an ignored guild") return {"status": f"Ignoring this guild: {message.guild.id}"} log.debug( f"Propagating message for: {message.author.name}({message.author.id})" ) guild = Guild(self.bot, message.guild.id, self.options) try: guild = next(iter(g for g in self.guilds if g == guild)) except StopIteration: # Check we have perms to actually create this guild object # and punish based upon our guild wide permissions perms = message.guild.me.guild_permissions if not perms.kick_members or not perms.ban_members: raise MissingGuildPermissions self.guilds = guild log.info(f"Created Guild: {guild.id}") data = {"pre_invoke_extensions": [], "after_invoke_extensions": []} for pre_invoke_ext in self.pre_invoke_extensions.values(): pre_invoke_return = await pre_invoke_ext.propagate(message) data["pre_invoke_extensions"].append(pre_invoke_return) main_return = await guild.propagate(message) for after_invoke_ext in self.after_invoke_extensions.values(): after_invoke_return = await after_invoke_ext.propagate( message, main_return) data["after_invoke_extensions"].append(after_invoke_return) return {**main_return, **data}