aboutsummaryrefslogtreecommitdiffstats
path: root/bot/exts/holidays/holidayreact.py
diff options
context:
space:
mode:
authorGravatar wookie184 <[email protected]>2023-01-29 16:11:50 +0000
committerGravatar GitHub <[email protected]>2023-01-29 16:11:50 +0000
commitaa0536b7f46c7f735d2c33821b25899ad1aab0ad (patch)
treee95453c5517ed5e41f5453220f09feed8b9bbfac /bot/exts/holidays/holidayreact.py
parentBump sentry-sdk from 1.13.0 to 1.14.0 (#1193) (diff)
parentMerge branch 'main' into holiday-react (diff)
Merge pull request #1187 from thurisatic/holiday-react
Add non-Halloween holiday reaction capabilities
Diffstat (limited to 'bot/exts/holidays/holidayreact.py')
-rw-r--r--bot/exts/holidays/holidayreact.py140
1 files changed, 140 insertions, 0 deletions
diff --git a/bot/exts/holidays/holidayreact.py b/bot/exts/holidays/holidayreact.py
new file mode 100644
index 00000000..2a1347e5
--- /dev/null
+++ b/bot/exts/holidays/holidayreact.py
@@ -0,0 +1,140 @@
+import logging
+import random
+import re
+from typing import NamedTuple
+
+import discord
+from discord.ext.commands import Cog
+
+from bot.bot import Bot
+from bot.constants import Month
+from bot.utils import resolve_current_month
+from bot.utils.decorators import in_month
+
+log = logging.getLogger(__name__)
+
+
+class Trigger(NamedTuple):
+ """Representation of regex trigger and corresponding reactions."""
+
+ regex: str
+ reaction: list[str]
+
+
+class Holiday(NamedTuple):
+ """Representation of reaction holidays."""
+
+ months: list[Month]
+ triggers: dict[str, Trigger]
+
+
+Valentines = Holiday([Month.FEBRUARY], {
+ "heart": Trigger(r"\b(love|heart)\b", ["\u2764\uFE0F"]),
+ }
+)
+Easter = Holiday([Month.APRIL], {
+ "bunny": Trigger(r"\b(easter|bunny|rabbit)\b", ["\U0001F430", "\U0001F407"]),
+ "egg": Trigger(r"\begg\b", ["\U0001F95A"]),
+ }
+)
+EarthDay = Holiday([Month.FEBRUARY], {
+ "earth": Trigger(r"\b(earth|planet)\b", ["\U0001F30E", "\U0001F30D", "\U0001F30F"]),
+ }
+)
+Pride = Holiday([Month.JUNE], {
+ "pride": Trigger(r"\bpride\b", ["\U0001F3F3\uFE0F\u200D\U0001F308"]),
+ }
+)
+Halloween = Holiday([Month.OCTOBER], {
+ "bat": Trigger(r"\bbat((wo)?m[ae]n|persons?|people|s)?\b", ["\U0001F987"]),
+ "danger": Trigger(r"\bdanger\b", ["\U00002620"]),
+ "doot": Trigger(r"\bdo{2,}t\b", ["\U0001F480"]),
+ "halloween": Trigger(r"\bhalloween\b", ["\U0001F383"]),
+ "jack-o-lantern": Trigger(r"\bjack-o-lantern\b", ["\U0001F383"]),
+ "pumpkin": Trigger(r"\bpumpkin\b", ["\U0001F383"]),
+ "skeleton": Trigger(r"\bskeleton\b", ["\U0001F480"]),
+ "spooky": Trigger(r"\bspo{2,}[k|p][i|y](er|est)?\b", ["\U0001F47B"]),
+ }
+)
+Hanukkah = Holiday([Month.NOVEMBER, Month.DECEMBER], {
+ "menorah": Trigger(r"\b(c?hanukkah|menorah)\b", ["\U0001F54E"]),
+ }
+)
+Christmas = Holiday([Month.DECEMBER], {
+ "christmas tree": Trigger(r"\b((christ|x)mas|tree)\b", ["\U0001F384"]),
+ "reindeer": Trigger(r"\b(reindeer|caribou|buck|stag)\b", ["\U0001F98C"]),
+ "santa": Trigger(r"\bsanta\b", ["\U0001F385"]),
+ "snowflake": Trigger(r"\b(snow ?)?flake(?! ?8)\b", ["\u2744\uFE0F"]),
+ "snowman": Trigger(r"\bsnow(man|angel)\b", ["\u2603\uFE0F", "\u26C4"]),
+ }
+)
+HOLIDAYS_TO_REACT = [
+ Valentines, Easter, EarthDay, Pride, Halloween, Hanukkah, Christmas
+]
+# Type (or order) doesn't matter here - set is for de-duplication
+MONTHS_TO_REACT = set(
+ month for holiday in HOLIDAYS_TO_REACT for month in holiday.months
+)
+
+
+class HolidayReact(Cog):
+ """A cog that makes the bot react to message triggers."""
+
+ def __init__(self, bot: Bot):
+ self.bot = bot
+
+ @in_month(*MONTHS_TO_REACT)
+ @Cog.listener()
+ async def on_message(self, message: discord.Message) -> None:
+ """Triggered when the bot sees a message in a holiday month."""
+ # Check message for bot replies and/or command invocations
+ # Short circuit if they're found, logging is handled in _short_circuit_check
+ if await self._short_circuit_check(message):
+ return
+
+ for holiday in HOLIDAYS_TO_REACT:
+ await self._check_message(message, holiday)
+
+ async def _check_message(self, message: discord.Message, holiday: Holiday) -> None:
+ """
+ Check if message is reactable.
+
+ React to message if:
+ * month is valid
+ * message contains reaction regex triggers
+ """
+ if resolve_current_month() not in holiday.months:
+ return
+
+ for name, trigger in holiday.triggers.items():
+ trigger_test = re.search(trigger.regex, message.content, flags=re.IGNORECASE)
+ if trigger_test:
+ await message.add_reaction(random.choice(trigger.reaction))
+ log.info(f"Added {name!r} reaction to message ID: {message.id}")
+
+ async def _short_circuit_check(self, message: discord.Message) -> bool:
+ """
+ Short-circuit helper check.
+
+ Return True if:
+ * author is a bot
+ * prefix is not None
+ """
+ # Check if message author is a bot
+ if message.author.bot:
+ log.debug(f"Ignoring reactions on bot message. Message ID: {message.id}")
+ return True
+
+ # Check for command invocation
+ # Because on_message doesn't give a full Context object, generate one first
+ ctx = await self.bot.get_context(message)
+ if ctx.prefix:
+ log.debug(f"Ignoring reactions on command invocation. Message ID: {message.id}")
+ return True
+
+ return False
+
+
+async def setup(bot: Bot) -> None:
+ """Load the Holiday Reaction Cog."""
+ await bot.add_cog(HolidayReact(bot))