def create_guild_from_dict(guild_data: dict) -> Guild: guild: Guild = Guild(id=guild_data["id"], options=Options(**guild_data["options"])) for member in guild_data["members"]: guild.members[ member["id"]] = FactoryBuilder.create_member_from_dict(member) log.info("Created Guild(id=%s) from dict", guild.id) return guild
async def get_guild(self, guild_id: int) -> Guild: log.debug("Attempting to return cached Guild(id=%s)", guild_id) resp = await self.redis.get(f"GUILD:{guild_id}") if not resp: raise GuildNotFound as_json = json.loads(resp.decode("utf-8")) guild: Guild = Guild(**as_json) # This is actually a dict here guild.options = Options(**guild.options) # type: ignore guild_members: Dict[int, Member] = {} for member_id in guild.members: # type: ignore member: Member = await self.get_member(member_id, guild_id) guild_members[member.id] = member guild.members = guild_members return guild
async def clean_cache(self, strict=False) -> None: """ Cleans the internal cache, pruning any old/un-needed entries. Non Strict mode: - Member deletion criteria: - warn_count == default - kick_count == default - duplicate_counter == default - duplicate_channel_counter_dict == default - addons dict == default - Also must have no active messages after cleaning. - Guild deletion criteria: - options are not custom - log_channel_id is not set - addons dict == default - Also must have no members stored Strict mode: - Member deletion criteria - Has no active messages - Guild deletion criteria - Does not have custom options - log_channel_id is not set - Has no active members Parameters ---------- strict : bool Toggles the above Notes ----- This is expensive, and likely only required to be run every so often depending on how high traffic your bot is. """ # In a nutshell, # Get entire cache and loop over # Build a list of 'still valid' cache entries # Drop the previous cache # Insert the new list of 'still valid' entries, this is now the cache # Ideally I don't want to load this cache into memory cache = [] async for guild in self.cache.get_all_guilds(): new_guild = Guild(guild.id) for member in guild.members.values(): FactoryBuilder.clean_old_messages(member, get_aware_time(), self.options) if strict and len(member.messages) != 0: new_guild.members[member.id] = member elif (len(member.messages) != 0 or member.kick_count != 0 or member.warn_count != 0 or member.duplicate_counter != 1 or bool(member.duplicate_channel_counter_dict) or bool(member.addons)): new_guild.members[member.id] = member # Clean guild predicate = (guild.options != Options() or guild.log_channel_id is not None or bool(new_guild.members)) if strict and predicate: new_guild.options = guild.options new_guild.log_channel_id = guild.log_channel_id cache.append(new_guild) elif predicate or bool(guild.addons): new_guild.addons = guild.addons new_guild.options = guild.options new_guild.log_channel_id = guild.log_channel_id cache.append(new_guild) await self.cache.drop() for guild in cache: await self.cache.set_guild(guild) log.info("Cleaned the internal cache")
def __init__( self, bot, *, library: Library = Library.DPY, options: Options = None, cache: Cache = None, ): """ AntiSpamHandler entry point. Parameters ---------- bot A reference to your discord bot object. library : Library, Optional An enum denoting the library this AntiSpamHandler. See :py:class:`antispam.enums.library.Library` for more Defaults to DPY support options : Options, Optional An instance of your custom Options the handler should use cache : Cache, Optional Your choice of backend caching """ # TODO Implement an async cache initialization somehow options = options or Options() if not isinstance(options, Options): raise ValueError("Expected `option`s of type `Option`") self.options = options if self.options.no_punish and (self.options.delete_spam or self.options.warn_only or self.options.per_channel_spam): log.warning( "You are attempting to create an AntiSpamHandler with options that are mutually exclusive. " "Please note `no_punish` will over rule any other options you attempt to set which are " "mutually exclusive.") cache = cache or MemoryCache(self) if not issubclass(type(cache), Cache): raise ValueError( "Expected `cache` that inherits from the `Cache` Protocol") self.bot = bot self.cache = cache self.core = Core(self) self.needs_init = True self.pre_invoke_plugins: Dict[str, BasePlugin] = {} self.after_invoke_extensions: Dict[str, BasePlugin] = {} # Import these here to avoid errors when not having the # other lib installed, I think if library == Library.HIKARI: from antispam.libs.hikari import Hikari self.lib_handler = Hikari(self) elif library == Library.PINCER: from antispam.libs.pincer import Pincer self.lib_handler = Pincer(self) else: from antispam.libs.dpy import DPY self.lib_handler = DPY(self) log.info("Package instance created")
async def load_from_dict( bot, data: dict, *, raise_on_exception: bool = True, plugins: Set[Type[BasePlugin]] = None, ): """ 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 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)`` plugins : Set[Type[BasePlugin]] A set for plugin lookups if you want to initialise plugins from an initial saved state. This should follow the format. .. code-block:: python {ClassReference} So for example: .. code-block:: python :linenos: class Plugin(BasePlugin): pass # Where you load ASH await AntiSpamHandler.load_from_dict(..., ..., plugins={Plugin} 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 will simply ignore the saved state of plugins that don't have a ``plugins`` mapping. """ # TODO Add redis cache here plugins = plugins or {} plugins: Dict[str, Type[BasePlugin]] = { class_ref.__name__: class_ref for class_ref in plugins } caches = {"MemoryCache": MemoryCache} try: ash = AntiSpamHandler(bot=bot, options=Options(**data["options"])) cache_type = data["cache"] ash.cache = caches[cache_type](ash) for guild in data["guilds"]: await ash.cache.set_guild( FactoryBuilder.create_guild_from_dict(guild)) if pre_invoke_plugins := data.get("pre_invoke_plugins"): for plugin, plugin_data in pre_invoke_plugins.items(): if plugin_class_ref := plugins.get(plugin): ash.register_plugin(await plugin_class_ref.load_from_dict( ash, plugin_data)) else: log.debug("Skipping state loading for %s", plugin)
def __init__( self, bot, library: Library, *, options: Options = None, cache: Cache = None, ): """ AntiSpamHandler entry point. Parameters ---------- bot A reference to your discord bot object. library : Library, Optional An enum denoting the library this AntiSpamHandler. See :py:class:`antispam.enums.library.Library` for more options : Options, Optional An instance of your custom Options the handler should use cache : Cache, Optional Your choice of backend caching """ options = options or Options() if not isinstance(options, Options): raise ValueError("Expected `option`s of type `Option`") self.options = options if self.options.no_punish and ( self.options.delete_spam or self.options.warn_only or self.options.per_channel_spam ): log.warning( "You are attempting to create an AntiSpamHandler with options that are mutually exclusive. " "Please note `no_punish` will over rule any other options you attempt to set which are " "mutually exclusive." ) if self.options.use_timeouts and self.options.warn_only: log.warning( "You are attempting to create an AntiSpamHandler with options that are mutually exclusive. " "Please note `use_timeouts` will over rule any other options you attempt to set which are " "mutually exclusive." ) cache = cache or MemoryCache(self) if not issubclass(type(cache), Cache): raise ValueError("Expected `cache` that inherits from the `Cache` Protocol") self.bot = bot self.cache = cache self.core = Core(self) self.needs_init = True self.pre_invoke_plugins: Dict[str, BasePlugin] = {} self.after_invoke_plugins: Dict[str, BasePlugin] = {} # Import these here to avoid errors when not # having the other lib installed self.lib_handler = None if library == Library.HIKARI: from antispam.libs.lib_hikari import Hikari self.lib_handler = Hikari(self) elif library == Library.PINCER: from antispam.libs.lib_pincer import Pincer if self.options.use_timeouts: raise UnsupportedAction( "Pincer does not currently support timeouts. Please disable this option." ) self.lib_handler = Pincer(self) elif library == Library.DISNAKE: from antispam.libs.dpy_forks.lib_disnake import Disnake self.lib_handler = Disnake(self) elif library == Library.ENHANCED_DPY: from antispam.libs.dpy_forks.lib_enhanced_dpy import EnhancedDPY self.lib_handler = EnhancedDPY(self) elif library == Library.NEXTCORD: from antispam.libs.dpy_forks.lib_nextcord import Nextcord self.lib_handler = Nextcord(self) elif library == Library.PYCORD: raise UnsupportedAction( "Py-cord is no longer officially supported, please see the following url for support:\n" "https://gist.github.com/Skelmis/b15a64f11c2ef89a7c6083ff455774a2" ) elif library == Library.CUSTOM: pass elif library == Library.DPY: from antispam.libs.dpy import DPY self.lib_handler = DPY(self) else: raise UnsupportedAction( "You must set a library for usage. See here for choices: " "https://dpy-anti-spam.readthedocs.io/en/latest/modules/interactions/enums.html#antispam.enums.Library" ) log.info("Package instance created")