From d7341b3ec204e7f780909d3b2d6e1b0dd3ec6830 Mon Sep 17 00:00:00 2001 From: Slipstream Date: Thu, 29 May 2025 11:23:20 -0600 Subject: [PATCH] feat: Enhance emoji and sticker update listeners to process changes more efficiently --- gurt/cog.py | 117 +++++++++++++++++++++++++++------------------- gurt/listeners.py | 79 +++++++++++++++++++++++++++---- 2 files changed, 139 insertions(+), 57 deletions(-) diff --git a/gurt/cog.py b/gurt/cog.py index dff2676..5da8286 100644 --- a/gurt/cog.py +++ b/gurt/cog.py @@ -268,57 +268,76 @@ class GurtCog(commands.Cog, name="Gurt"): # Added explicit Cog name self.user_relationships[user_id_1][user_id_2] = new_score # print(f"Updated relationship {user_id_1}-{user_id_2}: {current_score:.1f} -> {new_score:.1f} ({change:+.1f})") # Debug log + async def _process_single_emoji(self, emoji: discord.Emoji): + """Processes a single emoji: generates description if needed and updates EmojiManager.""" + try: + name_key = f":{emoji.name}:" + emoji_url = str(emoji.url) + guild_id = emoji.guild.id # Get guild_id from the emoji object + + existing_emoji = await self.emoji_manager.get_emoji(name_key) + if existing_emoji and \ + existing_emoji.get("id") == str(emoji.id) and \ + existing_emoji.get("url") == emoji_url and \ + existing_emoji.get("description") and \ + existing_emoji.get("description") != "No description generated.": + # print(f"Skipping already processed emoji: {name_key} in guild {emoji.guild.name}") + return + + print(f"Generating description for emoji: {name_key} in guild {emoji.guild.name}") + mime_type = "image/gif" if emoji.animated else "image/png" + description = await api.generate_image_description(self, emoji_url, emoji.name, "emoji", mime_type) + await self.emoji_manager.add_emoji(name_key, str(emoji.id), emoji.animated, guild_id, emoji_url, description or "No description generated.") + await asyncio.sleep(1) # Rate limiting + except Exception as e: + print(f"Error processing single emoji {emoji.name} (ID: {emoji.id}) in guild {emoji.guild.name}: {e}") + + async def _process_single_sticker(self, sticker: discord.StickerItem): + """Processes a single sticker: generates description if needed and updates EmojiManager.""" + try: + name_key = f":{sticker.name}:" + sticker_url = str(sticker.url) + guild_id = sticker.guild_id # Stickers have guild_id directly + + existing_sticker = await self.emoji_manager.get_sticker(name_key) + if existing_sticker and \ + existing_sticker.get("id") == str(sticker.id) and \ + existing_sticker.get("url") == sticker_url and \ + existing_sticker.get("description") and \ + existing_sticker.get("description") not in ["No description generated.", "Lottie animation, visual description not applicable."]: + # print(f"Skipping already processed sticker: {name_key} in guild ID {guild_id}") + return + + print(f"Generating description for sticker: {sticker.name} (ID: {sticker.id}) in guild ID {guild_id}") + description_to_add = "No description generated." + if sticker.format == discord.StickerFormatType.png or sticker.format == discord.StickerFormatType.apng: + mime_type = "image/png" + description = await api.generate_image_description(self, sticker_url, sticker.name, "sticker", mime_type) + description_to_add = description or "No description generated." + elif sticker.format == discord.StickerFormatType.lottie: + description_to_add = "Lottie animation, visual description not applicable." + else: + print(f"Skipping sticker {sticker.name} due to unsupported format: {sticker.format}") + description_to_add = f"Unsupported format: {sticker.format}, visual description not applicable." + + await self.emoji_manager.add_sticker(name_key, str(sticker.id), guild_id, sticker_url, description_to_add) + await asyncio.sleep(1) # Rate limiting + except Exception as e: + print(f"Error processing single sticker {sticker.name} (ID: {sticker.id}) in guild ID {sticker.guild_id}: {e}") + async def _fetch_and_process_guild_assets(self, guild: discord.Guild): - """Iterates through a guild's emojis and stickers, generates descriptions, and updates EmojiManager.""" - print(f"Processing assets for guild: {guild.name} ({guild.id})") - processed_count = 0 - # Emojis - for emoji in guild.emojis: - try: - name_key = f":{emoji.name}:" - emoji_url = str(emoji.url) - mime_type = "image/gif" if emoji.animated else "image/png" - - # Check if already processed with a description to avoid re-processing unless necessary - existing_emoji = await self.emoji_manager.get_emoji(name_key) - if existing_emoji and existing_emoji.get("url") == emoji_url and existing_emoji.get("description") and existing_emoji.get("description") != "No description generated.": - # print(f"Skipping already processed emoji: {name_key} in guild {guild.name}") - continue + """Iterates through a guild's emojis and stickers, and processes each one concurrently.""" + print(f"Queueing asset processing for guild: {guild.name} ({guild.id})") + emoji_tasks = [asyncio.create_task(self._process_single_emoji(emoji)) for emoji in guild.emojis] + sticker_tasks = [asyncio.create_task(self._process_single_sticker(sticker)) for sticker in guild.stickers] + + all_tasks = emoji_tasks + sticker_tasks + if all_tasks: + await asyncio.gather(*all_tasks, return_exceptions=True) # Wait for all tasks for this guild to complete + print(f"Finished concurrent asset processing for guild: {guild.name} ({guild.id}). Processed {len(all_tasks)} potential items.") + else: + print(f"No emojis or stickers to process for guild: {guild.name} ({guild.id})") - print(f"Generating description for emoji: {name_key} in guild {guild.name}") - description = await api.generate_image_description(self, emoji_url, emoji.name, "emoji", mime_type) - await self.emoji_manager.add_emoji(name_key, str(emoji.id), emoji.animated, guild.id, emoji_url, description or "No description generated.") - processed_count +=1 - await asyncio.sleep(1) # Rate limiting - except Exception as e: - print(f"Error processing emoji {emoji.name} in guild {guild.name}: {e}") - - # Stickers - for sticker in guild.stickers: - try: - name_key = f":{sticker.name}:" - sticker_url = str(sticker.url) - - existing_sticker = await self.emoji_manager.get_sticker(name_key) - if existing_sticker and existing_sticker.get("url") == sticker_url and existing_sticker.get("description") and existing_sticker.get("description") not in ["No description generated.", "Lottie animation, visual description not applicable."]: - # print(f"Skipping already processed sticker: {name_key} in guild {guild.name}") - continue - - print(f"Generating description for sticker: {sticker.name} in guild {guild.name}") - if sticker.format == discord.StickerFormatType.png or sticker.format == discord.StickerFormatType.apng: - mime_type = "image/png" # APNG is also fine as image/png for Gemini - description = await api.generate_image_description(self, sticker_url, sticker.name, "sticker", mime_type) - await self.emoji_manager.add_sticker(name_key, str(sticker.id), guild.id, sticker_url, description or "No description generated.") - elif sticker.format == discord.StickerFormatType.lottie: - await self.emoji_manager.add_sticker(name_key, str(sticker.id), guild.id, sticker_url, "Lottie animation, visual description not applicable.") - else: - print(f"Skipping sticker {sticker.name} due to unsupported format: {sticker.format}") - await self.emoji_manager.add_sticker(name_key, str(sticker.id), guild.id, sticker_url, f"Unsupported format: {sticker.format}, visual description not applicable.") - processed_count += 1 - await asyncio.sleep(1) # Rate limiting - except Exception as e: - print(f"Error processing sticker {sticker.name} in guild {guild.name}: {e}") - print(f"Finished processing {processed_count} new/updated assets for guild: {guild.name} ({guild.id})") async def initial_emoji_sticker_scan(self): """Scans all guilds GURT is in on startup for emojis and stickers.""" diff --git a/gurt/listeners.py b/gurt/listeners.py index cbda241..ec406ad 100644 --- a/gurt/listeners.py +++ b/gurt/listeners.py @@ -650,15 +650,78 @@ async def on_guild_join_listener(cog: 'GurtCog', guild: discord.Guild): async def on_guild_emojis_update_listener(cog: 'GurtCog', guild: discord.Guild, before: List[discord.Emoji], after: List[discord.Emoji]): """Listener function for on_guild_emojis_update.""" print(f"Emojis updated in guild: {guild.name} ({guild.id}). Before: {len(before)}, After: {len(after)}") - # For simplicity and to ensure all changes (add, remove, name change) are caught, - # re-process all emojis for the guild. - # A more optimized approach could diff 'before' and 'after' lists. - print(f"Re-processing all emojis for guild: {guild.name}") - asyncio.create_task(cog._fetch_and_process_guild_assets(guild)) # This will re-process stickers too, which is fine. + + before_map = {emoji.id: emoji for emoji in before} + after_map = {emoji.id: emoji for emoji in after} + + tasks = [] + + # Process added emojis + for emoji_id, emoji_obj in after_map.items(): + if emoji_id not in before_map: + print(f"New emoji added: {emoji_obj.name} ({emoji_id}) in guild {guild.name}") + tasks.append(asyncio.create_task(cog._process_single_emoji(emoji_obj))) + else: + # Check for changes in existing emojis (e.g., name change) + # The _process_single_emoji method already checks if a description exists and is valid. + # If the name changes, the old key won't match, so it will be treated as new by the manager if name is key. + # If ID is the primary key for checking existence, then a name change might need explicit handling. + # Current EmojiManager uses name as key, so a name change means old is gone, new is added. + # If an emoji's URL or other relevant properties change, _process_single_emoji will handle it. + before_emoji = before_map[emoji_id] + if before_emoji.name != emoji_obj.name or str(before_emoji.url) != str(emoji_obj.url): + print(f"Emoji changed: {before_emoji.name} -> {emoji_obj.name} or URL changed in guild {guild.name}") + # Remove old entry if name changed, as EmojiManager uses name as key + if before_emoji.name != emoji_obj.name: + await cog.emoji_manager.remove_emoji(f":{before_emoji.name}:") + tasks.append(asyncio.create_task(cog._process_single_emoji(emoji_obj))) + + + # Process removed emojis + for emoji_id, emoji_obj in before_map.items(): + if emoji_id not in after_map: + print(f"Emoji removed: {emoji_obj.name} ({emoji_id}) from guild {guild.name}") + await cog.emoji_manager.remove_emoji(f":{emoji_obj.name}:") # Remove by name key + + if tasks: + print(f"Queued {len(tasks)} tasks for emoji updates in guild {guild.name}") + await asyncio.gather(*tasks, return_exceptions=True) + else: + print(f"No new or significantly changed emojis to process in guild {guild.name}") + async def on_guild_stickers_update_listener(cog: 'GurtCog', guild: discord.Guild, before: List[discord.StickerItem], after: List[discord.StickerItem]): """Listener function for on_guild_stickers_update.""" print(f"Stickers updated in guild: {guild.name} ({guild.id}). Before: {len(before)}, After: {len(after)}") - # Similar to emojis, re-process all assets for simplicity. - print(f"Re-processing all stickers (and emojis) for guild: {guild.name}") - asyncio.create_task(cog._fetch_and_process_guild_assets(guild)) + + before_map = {sticker.id: sticker for sticker in before} + after_map = {sticker.id: sticker for sticker in after} + tasks = [] + + # Process added or changed stickers + for sticker_id, sticker_obj in after_map.items(): + if sticker_id not in before_map: + print(f"New sticker added: {sticker_obj.name} ({sticker_id}) in guild {guild.name}") + tasks.append(asyncio.create_task(cog._process_single_sticker(sticker_obj))) + else: + before_sticker = before_map[sticker_id] + # Check for relevant changes (name, URL, format) + if before_sticker.name != sticker_obj.name or \ + str(before_sticker.url) != str(sticker_obj.url) or \ + before_sticker.format != sticker_obj.format: + print(f"Sticker changed: {before_sticker.name} -> {sticker_obj.name} or URL/format changed in guild {guild.name}") + if before_sticker.name != sticker_obj.name: + await cog.emoji_manager.remove_sticker(f":{before_sticker.name}:") + tasks.append(asyncio.create_task(cog._process_single_sticker(sticker_obj))) + + # Process removed stickers + for sticker_id, sticker_obj in before_map.items(): + if sticker_id not in after_map: + print(f"Sticker removed: {sticker_obj.name} ({sticker_id}) from guild {guild.name}") + await cog.emoji_manager.remove_sticker(f":{sticker_obj.name}:") + + if tasks: + print(f"Queued {len(tasks)} tasks for sticker updates in guild {guild.name}") + await asyncio.gather(*tasks, return_exceptions=True) + else: + print(f"No new or significantly changed stickers to process in guild {guild.name}")