Easter eggs

pull/133/head
stijndcl 2022-09-20 00:31:33 +02:00
parent 2638c5a3c4
commit 181118aa1d
7 changed files with 151 additions and 13 deletions

View File

@ -0,0 +1,36 @@
"""Easter eggs
Revision ID: b84bb10fb8de
Revises: 515dc3f52c6d
Create Date: 2022-09-20 00:23:53.160168
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "b84bb10fb8de"
down_revision = "515dc3f52c6d"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"easter_eggs",
sa.Column("easter_egg_id", sa.Integer(), nullable=False),
sa.Column("match", sa.Text(), nullable=False),
sa.Column("response", sa.Text(), nullable=False),
sa.Column("exact", sa.Boolean(), server_default="1", nullable=False),
sa.Column("startswith", sa.Boolean(), server_default="1", nullable=False),
sa.PrimaryKeyConstraint("easter_egg_id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("easter_eggs")
# ### end Alembic commands ###

View File

@ -0,0 +1,12 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database.schemas import EasterEgg
__all__ = ["get_all_easter_eggs"]
async def get_all_easter_eggs(session: AsyncSession) -> list[EasterEgg]:
"""Return a list of all easter eggs"""
statement = select(EasterEgg)
return (await session.execute(statement)).scalars().all()

View File

@ -31,6 +31,7 @@ __all__ = [
"CustomCommandAlias", "CustomCommandAlias",
"DadJoke", "DadJoke",
"Deadline", "Deadline",
"EasterEgg",
"Link", "Link",
"MemeTemplate", "MemeTemplate",
"NightlyData", "NightlyData",
@ -144,6 +145,18 @@ class Deadline(Base):
course: UforaCourse = relationship("UforaCourse", back_populates="deadlines", uselist=False, lazy="selectin") course: UforaCourse = relationship("UforaCourse", back_populates="deadlines", uselist=False, lazy="selectin")
class EasterEgg(Base):
"""An easter egg response"""
__tablename__ = "easter_eggs"
easter_egg_id: int = Column(Integer, primary_key=True)
match: str = Column(Text, nullable=False)
response: str = Column(Text, nullable=False)
exact: bool = Column(Boolean, nullable=False, server_default="1")
startswith: bool = Column(Boolean, nullable=False, server_default="1")
class Link(Base): class Link(Base):
"""Useful links that go useful places""" """Useful links that go useful places"""

View File

@ -4,11 +4,10 @@ from discord import app_commands
from overrides import overrides from overrides import overrides
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from database.crud import links, memes, ufora_courses, wordle from database.crud import easter_eggs, links, memes, ufora_courses, wordle
from database.schemas import EasterEgg, WordleWord
__all__ = ["CacheManager", "LinkCache", "UforaCourseCache"] __all__ = ["CacheManager", "EasterEggCache", "LinkCache", "UforaCourseCache"]
from database.schemas import WordleWord
class DatabaseCache(ABC): class DatabaseCache(ABC):
@ -46,6 +45,22 @@ class DatabaseCache(ABC):
return [app_commands.Choice(name=suggestion, value=suggestion.lower()) for suggestion in suggestions] return [app_commands.Choice(name=suggestion, value=suggestion.lower()) for suggestion in suggestions]
class EasterEggCache(DatabaseCache):
"""Cache to store easter eggs invoked by messages"""
easter_eggs: list[EasterEgg] = []
@overrides
async def clear(self):
self.easter_eggs.clear()
@overrides
async def invalidate(self, database_session: AsyncSession):
"""Invalidate the data stored in this cache"""
await self.clear()
self.easter_eggs = await easter_eggs.get_all_easter_eggs(database_session)
class LinkCache(DatabaseCache): class LinkCache(DatabaseCache):
"""Cache to store the names of links""" """Cache to store the names of links"""
@ -131,12 +146,14 @@ class WordleCache(DatabaseCache):
class CacheManager: class CacheManager:
"""Class that keeps track of all caches""" """Class that keeps track of all caches"""
easter_eggs: EasterEggCache
links: LinkCache links: LinkCache
memes: MemeCache memes: MemeCache
ufora_courses: UforaCourseCache ufora_courses: UforaCourseCache
wordle_word: WordleCache wordle_word: WordleCache
def __init__(self): def __init__(self):
self.easter_eggs = EasterEggCache()
self.links = LinkCache() self.links = LinkCache()
self.memes = MemeCache() self.memes = MemeCache()
self.ufora_courses = UforaCourseCache() self.ufora_courses = UforaCourseCache()
@ -144,6 +161,7 @@ class CacheManager:
async def initialize_caches(self, postgres_session: AsyncSession): async def initialize_caches(self, postgres_session: AsyncSession):
"""Initialize the contents of all caches""" """Initialize the contents of all caches"""
await self.easter_eggs.invalidate(postgres_session)
await self.links.invalidate(postgres_session) await self.links.invalidate(postgres_session)
await self.memes.invalidate(postgres_session) await self.memes.invalidate(postgres_session)
await self.ufora_courses.invalidate(postgres_session) await self.ufora_courses.invalidate(postgres_session)

View File

@ -17,6 +17,7 @@ from didier.data.embeds.error_embed import create_error_embed
from didier.data.embeds.schedules import Schedule, parse_schedule from didier.data.embeds.schedules import Schedule, parse_schedule
from didier.exceptions import HTTPException, NoMatch from didier.exceptions import HTTPException, NoMatch
from didier.utils.discord.prefix import get_prefix from didier.utils.discord.prefix import get_prefix
from didier.utils.easter_eggs import detect_easter_egg
__all__ = ["Didier"] __all__ = ["Didier"]
@ -213,8 +214,9 @@ class Didier(commands.Bot):
await self.process_commands(message) await self.process_commands(message)
# TODO easter eggs easter_egg = await detect_easter_egg(self, message, self.database_caches.easter_eggs)
# TODO stats if easter_egg is not None:
await message.reply(easter_egg, mention_author=False)
async def _try_invoke_custom_command(self, message: discord.Message) -> bool: async def _try_invoke_custom_command(self, message: discord.Message) -> bool:
"""Check if the message tries to invoke a custom command """Check if the message tries to invoke a custom command

View File

@ -1,18 +1,19 @@
import re import re
from typing import Optional
from discord import Message from discord import Message
from discord.ext import commands from discord.ext import commands
from didier.data import constants from didier.data import constants
__all__ = ["get_prefix"] __all__ = ["get_prefix", "match_prefix"]
def get_prefix(client: commands.Bot, message: Message) -> str: def match_prefix(client: commands.Bot, message: Message) -> Optional[str]:
"""Match a prefix against a message """Try to match a prefix against a message, returning None instead of a default value
This is done dynamically to allow variable amounts of whitespace, This is done dynamically through regexes to allow case-insensitivity
and through regexes to allow case-insensitivity among other things. and variable amounts of whitespace among other things.
""" """
mention = f"<@!?{client.user.id}>" mention = f"<@!?{client.user.id}>"
regex = r"^({})\s*" regex = r"^({})\s*"
@ -26,5 +27,13 @@ def get_prefix(client: commands.Bot, message: Message) -> str:
# .group() is inconsistent with whitespace, so that can't be used # .group() is inconsistent with whitespace, so that can't be used
return message.content[: match.end()] return message.content[: match.end()]
# Matched nothing return None
return "didier"
def get_prefix(client: commands.Bot, message: Message) -> str:
"""Match a prefix against a message, with a fallback
This is the main prefix function that is used by the bot
"""
# If nothing was matched, return "didier" as a fallback
return match_prefix(client, message) or "didier"

View File

@ -0,0 +1,48 @@
import random
from typing import Optional
import discord
from discord.ext import commands
from database.utils.caches import EasterEggCache
from didier.utils.discord.prefix import match_prefix
__all__ = ["detect_easter_egg"]
def _roll_easter_egg(response: str) -> Optional[str]:
"""Roll a random chance for an easter egg to be responded with
The chance for an easter egg to be used is 33%
"""
rolled = random.randint(0, 100) < 33
return response if rolled else None
async def detect_easter_egg(client: commands.Bot, message: discord.Message, cache: EasterEggCache) -> Optional[str]:
"""Try to detect an easter egg in a message"""
prefix = match_prefix(client, message)
content = message.content.strip().lower()
# Message calls Didier
if prefix is not None:
prefix = prefix.strip().lower()
# Message is only "Didier"
if content == prefix:
return "Hmm?"
else:
# Message invokes a command: do nothing
return None
for easter_egg in cache.easter_eggs:
# Exact matches
if easter_egg.exact and easter_egg.match == content:
return _roll_easter_egg(easter_egg.response)
# Matches that start with a certain term
if easter_egg.startswith and content.startswith(easter_egg.match):
return _roll_easter_egg(easter_egg.response)
return None