diff --git a/db/migrations/v2_5_9_reactions.sql b/db/migrations/v2_5_9_reactions.sql index 40dc357..d5b2440 100644 --- a/db/migrations/v2_5_9_reactions.sql +++ b/db/migrations/v2_5_9_reactions.sql @@ -1,7 +1,7 @@ -- Create a table to store custom reactions CREATE TABLE custom_reactions ( id SERIAL PRIMARY KEY, -- Unique identifier for each custom reaction - trigger TEXT NOT NULL, -- The text that triggers the custom reaction + trigger_text TEXT NOT NULL, -- The text that triggers the custom reaction response TEXT, -- The response text for the custom reaction (nullable for emoji reactions) emoji TEXT, -- The emoji for the custom reaction (nullable for text responses) is_emoji BOOLEAN DEFAULT FALSE, -- Indicates if the reaction is a discord emoji reaction @@ -12,10 +12,10 @@ CREATE TABLE custom_reactions ( usage_count INT DEFAULT 0, -- The number of times a custom reaction has been used created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the custom reaction was created updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the custom reaction was last updated - CONSTRAINT unique_trigger_guild UNIQUE (trigger, guild_id) -- Ensure that the combination of trigger, guild_id, and is_full_match is unique + CONSTRAINT unique_trigger_guild UNIQUE (trigger_text, guild_id) -- Ensure that the combination of trigger_text, guild_id, and is_full_match is unique ); -- Create indexes to speed up lookups CREATE INDEX idx_custom_reactions_guild_id ON custom_reactions(guild_id); CREATE INDEX idx_custom_reactions_creator_id ON custom_reactions(creator_id); -CREATE INDEX idx_custom_reactions_trigger ON custom_reactions(trigger); +CREATE INDEX idx_custom_reactions_trigger_text ON custom_reactions(trigger_text); diff --git a/handlers/reaction_handler.py b/handlers/reaction_handler.py index 897b1b6..b8218b0 100644 --- a/handlers/reaction_handler.py +++ b/handlers/reaction_handler.py @@ -1,11 +1,8 @@ -import random -import asyncio -from typing import List, Dict, Any - -from discord.ext.commands import Cog from discord import Message +from discord.ext.commands import Cog +from loguru import logger -from config.parser import JsonCache +from services.reactions_service import CustomReactionsService from services.blacklist_service import BlacklistUserService @@ -13,68 +10,53 @@ class ReactionHandler: """ Handles reactions to messages based on predefined triggers and responses. """ - + def __init__(self, client, message: Message) -> None: - self.reactions: Dict[str, Any] = JsonCache.read_json("reactions") - self.eightball: List[str] = self.reactions["eightball"] - self.full_response: Dict[str, str] = self.reactions["full_content_responses"] - self.partial_react: Dict[str, str] = self.reactions["partial_content_reactions"] + self.client = client self.message: Message = message self.content: str = self.message.content.lower() - self.client = client - + self.reaction_service = CustomReactionsService() + async def run_all_checks(self) -> None: """ Runs all checks for reactions and responses. """ - await asyncio.gather( - self.check_eightball(self.eightball), - self.check_full_response(), - self.react() - ) - - async def check_eightball(self, choices: List[str]) -> None: - """ - Checks if the message is a question directed at Lumi and responds with a random choice. - - :param choices: List of possible responses. - """ - if (self.content.startswith("lumi ") or self.content.startswith("lumi, ")) and self.content.endswith("?"): - response: str = random.choice(choices) - await self.message.reply(content=response) - - async def check_full_response(self) -> None: - """ - Checks if the message content matches any full content triggers and responds accordingly. - """ - for trigger, response in self.full_response.items(): - if trigger.lower() == self.content: - await self.message.reply(response) + guild_id = self.message.guild.id if self.message.guild else None - async def react(self) -> None: - """ - Adds reactions to the message based on partial content triggers. - """ - ctx = await self.client.get_context(self.message) - for trigger, emoji in self.partial_react.items(): - if trigger.lower() in self.content: - emoji = await self.client.convert_to_emoji(ctx, emoji) - if emoji: - await self.message.add_reaction(emoji) + if guild_id: + reaction = await self.reaction_service.find_trigger(guild_id, self.content) + if reaction: + processed = False + try: + if reaction["type"] == "text": + await self.message.reply(reaction["response"]) + processed = True + elif reaction["type"] == "emoji": + await self.message.add_reaction(reaction["response"]) + processed = True + except Exception as e: + logger.warning(f"Failed to process reaction: {e}") + + if processed: + await self.reaction_service.increment_reaction_usage( + int(reaction["id"]) + ) class ReactionListener(Cog): def __init__(self, client) -> None: self.client = client - @Cog.listener('on_message') + @Cog.listener("on_message") async def reaction_listener(self, message: Message) -> None: """ Listens for new messages and processes them if the author is not a bot and not blacklisted. - + :param message: The message to process. """ - if not message.author.bot and not BlacklistUserService.is_user_blacklisted(message.author.id): + if not message.author.bot and not BlacklistUserService.is_user_blacklisted( + message.author.id + ): await ReactionHandler(self.client, message).run_all_checks() diff --git a/services/reactions_service.py b/services/reactions_service.py index 50a36a1..091df00 100644 --- a/services/reactions_service.py +++ b/services/reactions_service.py @@ -1,20 +1,3 @@ -""" -TABLE custom_reactions: - id SERIAL PRIMARY KEY - trigger TEXT NOT NULL - response TEXT - emoji TEXT - is_emoji BOOLEAN DEFAULT FALSE - is_full_match BOOLEAN DEFAULT FALSE - is_global BOOLEAN DEFAULT TRUE - guild_id BIGINT - creator_id BIGINT NOT NULL - usage_count INT DEFAULT 0 - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - CONSTRAINT unique_trigger_guild UNIQUE (trigger, guild_id) -""" - from typing import Optional, Dict, Any from datetime import datetime from db import database @@ -31,18 +14,29 @@ class CustomReactionsService: query = """ SELECT * FROM custom_reactions WHERE (guild_id = ? OR is_global = TRUE) AND ( - (is_full_match = TRUE AND trigger = ?) OR - (is_full_match = FALSE AND ? LIKE CONCAT('%', trigger, '%')) + (is_full_match = TRUE AND trigger_text = ?) OR + (is_full_match = FALSE AND ? LIKE CONCAT('%', trigger_text, '%')) ) ORDER BY guild_id = ? DESC, is_global ASC LIMIT 1 """ - result = database.select_query_one(query, (guild_id, message_content, message_content, guild_id)) + result = database.select_query(query, (guild_id, message_content, message_content, guild_id)) if result: - reaction = dict(result) + reaction = result[0] # Get the first result from the list return { - "response": reaction.get("response"), - "type": "emoji" if reaction.get("is_emoji") else "text" + "id": reaction[0], + "trigger_text": reaction[1], + "response": reaction[2], + "emoji": reaction[3], + "is_emoji": reaction[4], + "is_full_match": reaction[5], + "is_global": reaction[6], + "guild_id": reaction[7], + "creator_id": reaction[8], + "usage_count": reaction[9], + "created_at": reaction[10], + "updated_at": reaction[11], + "type": "emoji" if reaction[4] else "text" } return None @@ -50,7 +44,7 @@ class CustomReactionsService: self, guild_id: int, creator_id: int, - trigger: str, + trigger_text: str, response: Optional[str] = None, emoji: Optional[str] = None, is_emoji: bool = False, @@ -61,14 +55,14 @@ class CustomReactionsService: return False query = """ - INSERT INTO custom_reactions (trigger, response, emoji, is_emoji, is_full_match, is_global, guild_id, creator_id) + INSERT INTO custom_reactions (trigger_text, response, emoji, is_emoji, is_full_match, is_global, guild_id, creator_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE trigger=trigger + ON DUPLICATE KEY UPDATE trigger_text=trigger_text """ database.execute_query( query, ( - trigger, + trigger_text, response, emoji, is_emoji, @@ -83,7 +77,7 @@ class CustomReactionsService: async def edit_custom_reaction( self, guild_id: int, - trigger: str, + trigger_text: str, new_response: Optional[str] = None, new_emoji: Optional[str] = None, is_emoji: Optional[bool] = None, @@ -98,7 +92,7 @@ class CustomReactionsService: is_full_match = COALESCE(?, is_full_match), is_global = COALESCE(?, is_global), updated_at = ? - WHERE guild_id = ? AND trigger = ? + WHERE guild_id = ? AND trigger_text = ? """ database.execute_query( query, @@ -110,17 +104,17 @@ class CustomReactionsService: is_global, datetime.utcnow(), guild_id, - trigger, + trigger_text, ), ) return True - async def delete_custom_reaction(self, guild_id: int, trigger: str) -> bool: + async def delete_custom_reaction(self, guild_id: int, trigger_text: str) -> bool: query = """ DELETE FROM custom_reactions - WHERE guild_id = ? AND trigger = ? + WHERE guild_id = ? AND trigger_text = ? """ - database.execute_query(query, (guild_id, trigger)) + database.execute_query(query, (guild_id, trigger_text)) return True async def count_custom_reactions(self, guild_id: int) -> int: @@ -131,19 +125,16 @@ class CustomReactionsService: count = database.select_query_one(query, (guild_id,)) return count if count else 0 - async def increment_reaction_usage(self, guild_id: int, trigger: str) -> bool: + async def increment_reaction_usage(self, reaction_id: int) -> bool: query = """ UPDATE custom_reactions - SET usage_count = usage_count + 1, - updated_at = ? - WHERE guild_id = ? AND trigger = ? + SET usage_count = usage_count + 1 + WHERE id = ? """ database.execute_query( query, ( - datetime.utcnow(), - guild_id, - trigger, + reaction_id, ), ) return True