From 816da05102780c3932841959cbfd37efaac19308 Mon Sep 17 00:00:00 2001 From: mysistersbrother <70792267+mysistersbrother@users.noreply.github.com> Date: Mon, 5 Jan 2026 19:20:13 +0100 Subject: [PATCH 1/4] add cooldown to flags and implement cooldown map --- plugins/factoids.py | 47 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/plugins/factoids.py b/plugins/factoids.py index ba8d229..1fd72d7 100644 --- a/plugins/factoids.py +++ b/plugins/factoids.py @@ -1,9 +1,10 @@ +import asyncio from datetime import datetime import json -from typing import TYPE_CHECKING, Literal, Mapping, Optional, TypedDict, Union, cast, overload +from typing import Any, TYPE_CHECKING, Literal, Mapping, Optional, TypedDict, Union, cast, overload from typing_extensions import NotRequired -from discord import AllowedMentions, Embed, Message, MessageReference, Thread +from discord import AllowedMentions, Embed, Message, MessageReference, NotFound, Thread from discord.abc import GuildChannel from sqlalchemy import TEXT, TIMESTAMP, BigInteger, Computed, ForeignKey, Integer, delete, func, select from sqlalchemy.dialects.postgresql import JSONB @@ -41,9 +42,14 @@ class GlobalConfig: def __init__(self, *, id: int = ..., prefix: Optional[str] = ...) -> None: ... +class CooldownInfo(TypedDict): + length: int + applies_for: str + class Flags(TypedDict): mentions: NotRequired[bool] acl: NotRequired[str] + cooldown: NotRequired[CooldownInfo] @registry.mapped @@ -150,9 +156,29 @@ async def init() -> None: prefix = conf.prefix +class CooldownMap: + def __init__(self): + self.cooldown_buckets = {} + + def _purge_expired_buckets(self, current: int) -> None: + to_delete = [key for key, expires_at in self.cooldown_buckets.items() if expires_at <= current] + for key in to_delete: + del self.cooldown_buckets[key] + + def update_cooldown(self, key: Any, length: int, current: int) -> bool: + self._purge_expired_buckets(current) + + if key not in self.cooldown_buckets: + self.cooldown_buckets[key] = current + length + return False + return True + @cog class Factoids(Cog): """Manage factoids.""" + def __init__(self): + super().__init__() + self.cd_mapping = CooldownMap() @Cog.listener() async def on_message(self, msg: Message) -> None: @@ -184,6 +210,20 @@ async def on_message(self, msg: Message) -> None: if "mentions" in flags and flags["mentions"]: mentions = AllowedMentions(roles=True, users=True) + cooldown = flags.get("cooldown", None) + # has a cooldown, acl evalutes to true and currently on cooldown + if cooldown and \ + evaluate_acl(cooldown["applies_for"], msg.author, msg.channel) == EvalResult.TRUE and \ + self.cd_mapping.update_cooldown((msg.channel.id, alias.factoid.id), cooldown["length"], int(msg.created_at.timestamp())): + await msg.add_reaction("\u231B") + await asyncio.sleep(5) + try: + await msg.delete() + except NotFound: + # the message was already deleted + pass + return + embed = Embed.from_dict(alias.factoid.embed_data) if alias.factoid.embed_data is not None else None if msg.reference is not None and msg.reference.message_id is not None: reference = MessageReference( @@ -392,6 +432,9 @@ async def tag_flags(self, ctx: Context, name: str, flags: Optional[Union[CodeBlo Configure admin-only flags for a factoid. The flags are a JSON dictionary with the following keys: - "mentions": a boolean, if true, makes the factoid invocation ping the roles and users it involves - "acl": a string referring to an ACL (configurable with `acl`) required to use the factoid + - "cooldown": a dictionary that if present, specifies a cooldown for the factoid + - "length": how long the cooldown lasts for + - "applies_for": a string referring to an ACL that specifies when the cooldown applies """ assert prefix is not None name = validate_name(name) From ffe9597860e4fe7a2d9328464257ee91d33472e9 Mon Sep 17 00:00:00 2001 From: mysistersbrother <70792267+mysistersbrother@users.noreply.github.com> Date: Mon, 5 Jan 2026 19:28:12 +0100 Subject: [PATCH 2/4] Remove message deletion --- plugins/factoids.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/plugins/factoids.py b/plugins/factoids.py index 1fd72d7..e0e3559 100644 --- a/plugins/factoids.py +++ b/plugins/factoids.py @@ -1,10 +1,9 @@ -import asyncio from datetime import datetime import json from typing import Any, TYPE_CHECKING, Literal, Mapping, Optional, TypedDict, Union, cast, overload from typing_extensions import NotRequired -from discord import AllowedMentions, Embed, Message, MessageReference, NotFound, Thread +from discord import AllowedMentions, Embed, Message, MessageReference, Thread from discord.abc import GuildChannel from sqlalchemy import TEXT, TIMESTAMP, BigInteger, Computed, ForeignKey, Integer, delete, func, select from sqlalchemy.dialects.postgresql import JSONB @@ -216,13 +215,6 @@ async def on_message(self, msg: Message) -> None: evaluate_acl(cooldown["applies_for"], msg.author, msg.channel) == EvalResult.TRUE and \ self.cd_mapping.update_cooldown((msg.channel.id, alias.factoid.id), cooldown["length"], int(msg.created_at.timestamp())): await msg.add_reaction("\u231B") - await asyncio.sleep(5) - try: - await msg.delete() - except NotFound: - # the message was already deleted - pass - return embed = Embed.from_dict(alias.factoid.embed_data) if alias.factoid.embed_data is not None else None if msg.reference is not None and msg.reference.message_id is not None: From 5ca694a7c46bc5695d4b06ab040b9b93bd311f51 Mon Sep 17 00:00:00 2001 From: mysistersbrother <70792267+mysistersbrother@users.noreply.github.com> Date: Mon, 5 Jan 2026 19:28:51 +0100 Subject: [PATCH 3/4] formatting --- plugins/factoids.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/plugins/factoids.py b/plugins/factoids.py index e0e3559..7e5720d 100644 --- a/plugins/factoids.py +++ b/plugins/factoids.py @@ -1,6 +1,6 @@ from datetime import datetime import json -from typing import Any, TYPE_CHECKING, Literal, Mapping, Optional, TypedDict, Union, cast, overload +from typing import TYPE_CHECKING, Any, Literal, Mapping, Optional, TypedDict, Union, cast, overload from typing_extensions import NotRequired from discord import AllowedMentions, Embed, Message, MessageReference, Thread @@ -45,6 +45,7 @@ class CooldownInfo(TypedDict): length: int applies_for: str + class Flags(TypedDict): mentions: NotRequired[bool] acl: NotRequired[str] @@ -172,9 +173,11 @@ def update_cooldown(self, key: Any, length: int, current: int) -> bool: return False return True + @cog class Factoids(Cog): """Manage factoids.""" + def __init__(self): super().__init__() self.cd_mapping = CooldownMap() @@ -211,9 +214,13 @@ async def on_message(self, msg: Message) -> None: cooldown = flags.get("cooldown", None) # has a cooldown, acl evalutes to true and currently on cooldown - if cooldown and \ - evaluate_acl(cooldown["applies_for"], msg.author, msg.channel) == EvalResult.TRUE and \ - self.cd_mapping.update_cooldown((msg.channel.id, alias.factoid.id), cooldown["length"], int(msg.created_at.timestamp())): + if ( + cooldown + and evaluate_acl(cooldown["applies_for"], msg.author, msg.channel) == EvalResult.TRUE + and self.cd_mapping.update_cooldown( + (msg.channel.id, alias.factoid.id), cooldown["length"], int(msg.created_at.timestamp()) + ) + ): await msg.add_reaction("\u231B") embed = Embed.from_dict(alias.factoid.embed_data) if alias.factoid.embed_data is not None else None From e553c55b701cb44540d5255c90364ee4140ec4c3 Mon Sep 17 00:00:00 2001 From: mysistersbrother <70792267+mysistersbrother@users.noreply.github.com> Date: Mon, 5 Jan 2026 19:30:59 +0100 Subject: [PATCH 4/4] add back early return --- plugins/factoids.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/factoids.py b/plugins/factoids.py index 7e5720d..509e772 100644 --- a/plugins/factoids.py +++ b/plugins/factoids.py @@ -222,6 +222,7 @@ async def on_message(self, msg: Message) -> None: ) ): await msg.add_reaction("\u231B") + return embed = Embed.from_dict(alias.factoid.embed_data) if alias.factoid.embed_data is not None else None if msg.reference is not None and msg.reference.message_id is not None: