From 96ea7a5c8924972ce7b89445c3e70733306520f4 Mon Sep 17 00:00:00 2001 From: Paul Zagieboylo Date: Tue, 16 Jan 2024 17:36:25 -0500 Subject: [PATCH 1/5] more forgiving handling of highfive reaction, add reaction handling to solve messages; closes !66 and !87 --- herring/puzzles/discordbot.py | 55 +++++++++++++++++++++++++++-------- herring/puzzles/tasks.py | 27 ++++++----------- 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/herring/puzzles/discordbot.py b/herring/puzzles/discordbot.py index c400ea7..a7689d6 100644 --- a/herring/puzzles/discordbot.py +++ b/herring/puzzles/discordbot.py @@ -56,7 +56,10 @@ # making this less than 25 leaves a little elbow room for cleanup_channels if necessary PUZZLES_PER_CATEGORY = 20 +# special emojis used for on_raw_reaction_add SIGNUP_EMOJI = "\N{RAISED HAND}" +LEAVE_EMOJI = "\N{LEAF FLUTTERING IN WIND}" +TRIUMPH_EMOJI = "\N{PARTY POPPER}" # this is the most users we'll try to mention during an hb!who; more than this and you just get the number MAX_USER_LIST = 10 @@ -223,12 +226,34 @@ async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): # ignore myself if payload.user_id == self.bot.user.id: return - if payload.channel_id == self.announce_channel.id and payload.emoji.name == SIGNUP_EMOJI: + channel = self.guild.get_channel(payload.channel_id) + message: discord.Message = await channel.fetch_message(payload.message_id) + # we're ok with this working anywhere for any reason if anyone ever mentions a puzzle channel + if payload.emoji.name == SIGNUP_EMOJI: # add someone to the puzzle - message: discord.Message = await self.announce_channel.fetch_message(payload.message_id) - await message.remove_reaction(SIGNUP_EMOJI, payload.member) - target_channel = message.channel_mentions[0] - await self.add_user_to_puzzle(payload.member, target_channel.name) + if len(message.channel_mentions) > 0: + target_channel = message.channel_mentions[0] + try: + puzzle = await _get_puzzle_by_slug(target_channel.name) + except Puzzle.DoesNotExist: + # probably don't do it then + return + + await message.remove_reaction(SIGNUP_EMOJI, payload.member) + await self.add_user_to_puzzle(payload.member, puzzle.name) + elif payload.emoji.name == LEAVE_EMOJI: + # this should only work on the appropriate message in the puzzle channel or in puzzle-announcements + if message.author.id == self.bot.user.id: + await message.remove_reaction(LEAVE_EMOJI, payload.member) + if payload.channel_id == self.announce_channel.id: + target_channel = message.channel_mentions[0] + elif TRIUMPH_EMOJI in message.content: + # hopefully this is the "puzzle solved!" message + target_channel = channel + else: + target_channel = None + if target_channel: + await self.remove_user_from_puzzle(payload.member, target_channel.name) @commands.Cog.listener() async def on_message(self, message: discord.Message): @@ -863,6 +888,7 @@ def get_channel_pair(self, puzzle_name): return text_channel, voice_channel async def remove_user_from_puzzle(self, member: discord.Member, puzzle_name: str): + if member.bot: return text_channel, voice_channel = self.get_channel_pair(puzzle_name) await text_channel.set_permissions(member, overwrite=None) @@ -1069,8 +1095,8 @@ def __init__(self, *args, **kwargs): # we don't want this bot to respond to messages; just don't ask for the intent intents.message_content = False super(HerringAnnouncerBot, self).__init__(*args, intents=intents, **kwargs) - self.guild = None - self.announce_channel = None + self.guild : Optional[discord.Guild] = None + self.announce_channel : Optional[discord.TextChannel] = None self._really_ready = asyncio.Event() async def on_ready(self): @@ -1143,7 +1169,7 @@ async def make_category(): round, category = await ensure_category_ready() text_channel, voice_channel = await _make_puzzle_channels_inner(category, puzzle) - announcement = await self.announce_channel.send(f"New puzzle {puzzle.name} opened! {SIGNUP_EMOJI} this message to join, then click here to jump to the channel: {text_channel.mention}.") + announcement = await self.announce_channel.send(f"New puzzle {puzzle.name} opened in round {round.name}! {SIGNUP_EMOJI} this message to join, then click here to jump to the channel: {text_channel.mention}.") await announcement.add_reaction(SIGNUP_EMOJI) async def post_message(self, channel_name, message, **kwargs): @@ -1154,16 +1180,20 @@ async def post_message(self, channel_name, message, **kwargs): return await channel.send(message, **kwargs) - async def post_local_and_global(self, puzzle_name, local_message, global_message:str): + async def post_local_and_global(self, puzzle_name, local_content, global_content:str, local_reaction=None, global_reaction=None): await self._really_ready.wait() channel: discord.TextChannel = get(self.guild.text_channels, name=puzzle_name) if channel is None: logging.error(f"Couldn't get Discord channel {puzzle_name} in post_local_and_global!") return - await channel.send(local_message) - global_message = global_message.replace(f"#{puzzle_name}", channel.mention) - await self.announce_channel.send(global_message) + local_message = await channel.send(local_content) + global_content = global_content.replace(f"#{puzzle_name}", channel.mention) + global_message = await self.announce_channel.send(global_content) + if local_reaction: + await local_message.add_reaction(local_reaction) + if global_reaction: + await global_message.add_reaction(global_reaction) async def add_user_to_puzzle(self, user_profile: UserProfile, puzzle_name): await self._really_ready.wait() @@ -1289,6 +1319,7 @@ def _get_puzzle_by_slug(slug): return Puzzle.objects.get(slug=slug, hunt_id=settings.HERRING_HUNT_ID) async def _add_user_to_channels(member, text_channel:discord.TextChannel, voice_channel): + if member.bot: return False current_perms = text_channel.overwrites if member not in current_perms: await text_channel.set_permissions(member, read_messages=True) diff --git a/herring/puzzles/tasks.py b/herring/puzzles/tasks.py index 2f54198..4bc9875 100644 --- a/herring/puzzles/tasks.py +++ b/herring/puzzles/tasks.py @@ -11,7 +11,7 @@ import json import kombu.exceptions from lazy_object_proxy import Proxy as lazy_object -from puzzles.discordbot import run_listener_bot, DISCORD_ANNOUNCER, do_in_discord +from puzzles.discordbot import run_listener_bot, DISCORD_ANNOUNCER, do_in_discord, LEAVE_EMOJI, TRIUMPH_EMOJI from puzzles.models import Puzzle, Round, UserProfile from puzzles.spreadsheets import check_spreadsheet_service, iterate_changes, make_sheet from redis import Redis @@ -72,10 +72,10 @@ def apply_async(*args, **kwargs): return t -def post_local_and_global(local_channel, local_message, global_message): - logging.warning("tasks: post_local_and_global(%s, %s, %s)", local_channel, local_message, global_message) +def post_local_and_global(local_channel, local_message, global_message, local_reaction=None, global_reaction=None): + logging.warning("tasks: post_local_and_global(%s, %s, %s, %s)", local_channel, local_message, global_message, local_reaction, global_reaction) if settings.HERRING_ACTIVATE_DISCORD: - do_in_discord(DISCORD_ANNOUNCER.post_local_and_global(local_channel, local_message, global_message)) + do_in_discord(DISCORD_ANNOUNCER.post_local_and_global(local_channel, local_message, global_message, local_reaction, global_reaction)) @optional_task @shared_task(rate_limit=0.5) @@ -84,13 +84,9 @@ def post_answer(slug, answer): puzzle = Puzzle.objects.get(slug=slug) answer = answer.upper() - local_message = "\N{PARTY POPPER} Confirmed answer: {}".format(answer) - global_message = '\N{PARTY POPPER} Puzzle "{name}" (#{slug}) was solved! The answer is: {answer}'.format( - answer=answer, - slug=slug, - name=puzzle.name - ) - post_local_and_global(slug, local_message, global_message) + local_message = f"{TRIUMPH_EMOJI} Confirmed answer: {answer}\nReact with {LEAVE_EMOJI} to leave the puzzle!" + global_message = f'{TRIUMPH_EMOJI} Puzzle "{puzzle.name}" (#{slug}) from round {puzzle.parent.name} was solved! The answer is: {answer}\nReact with {LEAVE_EMOJI} to leave the puzzle!' + post_local_and_global(slug, local_message, global_message, LEAVE_EMOJI, LEAVE_EMOJI) @optional_task @@ -102,13 +98,8 @@ def post_update(slug, updated_field, value): puzzle = Puzzle.objects.get(slug=slug) except Puzzle.DoesNotExist: return - local_message = '{} set to: {}'.format(updated_field, value) - global_message = '"{name}" (#{slug}) now has these {field}: {value}'.format( - field=updated_field, - value=value, - slug=slug, - name=puzzle.name - ) + local_message = f'{updated_field} set to: {value}' + global_message = f'"{puzzle.name}" (#{slug}) from round {puzzle.parent.name} now has these {updated_field}: {value}' post_local_and_global(slug, local_message, global_message) From 09321abd7fa78cf4154104a2489177ab793663d7 Mon Sep 17 00:00:00 2001 From: Paul Zagieboylo Date: Tue, 16 Jan 2024 17:55:41 -0500 Subject: [PATCH 2/5] update to work better with keyword subscription --- herring/puzzles/discordbot.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/herring/puzzles/discordbot.py b/herring/puzzles/discordbot.py index a7689d6..1897dd9 100644 --- a/herring/puzzles/discordbot.py +++ b/herring/puzzles/discordbot.py @@ -226,17 +226,19 @@ async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): # ignore myself if payload.user_id == self.bot.user.id: return - channel = self.guild.get_channel(payload.channel_id) + channel = self.bot.get_channel(payload.channel_id) or await self.bot.fetch_channel(payload.channel_id) message: discord.Message = await channel.fetch_message(payload.message_id) - # we're ok with this working anywhere for any reason if anyone ever mentions a puzzle channel if payload.emoji.name == SIGNUP_EMOJI: # add someone to the puzzle - if len(message.channel_mentions) > 0: - target_channel = message.channel_mentions[0] + # we're ok with this working anywhere for any reason if anyone ever mentions a puzzle channel + if len(message.raw_channel_mentions) > 0: + target_channel_id = message.raw_channel_mentions[0] + target_channel = self.guild.get_channel(target_channel_id) + if not target_channel: + return try: puzzle = await _get_puzzle_by_slug(target_channel.name) except Puzzle.DoesNotExist: - # probably don't do it then return await message.remove_reaction(SIGNUP_EMOJI, payload.member) From 8c6e6447814cb974db01a7d1e274bf8a36d48886 Mon Sep 17 00:00:00 2001 From: Paul Zagieboylo Date: Tue, 16 Jan 2024 18:06:05 -0500 Subject: [PATCH 3/5] don't try to remove reactions from DMs, it doesn't work --- herring/puzzles/discordbot.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/herring/puzzles/discordbot.py b/herring/puzzles/discordbot.py index 1897dd9..4474f6e 100644 --- a/herring/puzzles/discordbot.py +++ b/herring/puzzles/discordbot.py @@ -241,12 +241,14 @@ async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): except Puzzle.DoesNotExist: return - await message.remove_reaction(SIGNUP_EMOJI, payload.member) await self.add_user_to_puzzle(payload.member, puzzle.name) + if channel.type != discord.ChannelType.private: + await message.remove_reaction(SIGNUP_EMOJI, payload.member) elif payload.emoji.name == LEAVE_EMOJI: # this should only work on the appropriate message in the puzzle channel or in puzzle-announcements if message.author.id == self.bot.user.id: - await message.remove_reaction(LEAVE_EMOJI, payload.member) + if channel.type != discord.ChannelType.private: + await message.remove_reaction(LEAVE_EMOJI, payload.member) if payload.channel_id == self.announce_channel.id: target_channel = message.channel_mentions[0] elif TRIUMPH_EMOJI in message.content: @@ -257,7 +259,7 @@ async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): if target_channel: await self.remove_user_from_puzzle(payload.member, target_channel.name) - @commands.Cog.listener() +@commands.Cog.listener() async def on_message(self, message: discord.Message): logging.info("on_message: %s", message) if message.author.id == self.bot.user.id: From 78df3f1b0ddd0205c95c9e567652b48db5f64582 Mon Sep 17 00:00:00 2001 From: Paul Zagieboylo Date: Tue, 16 Jan 2024 18:11:55 -0500 Subject: [PATCH 4/5] messed up formatting somehow? --- herring/puzzles/discordbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/herring/puzzles/discordbot.py b/herring/puzzles/discordbot.py index 4474f6e..31d8845 100644 --- a/herring/puzzles/discordbot.py +++ b/herring/puzzles/discordbot.py @@ -259,7 +259,7 @@ async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): if target_channel: await self.remove_user_from_puzzle(payload.member, target_channel.name) -@commands.Cog.listener() + @commands.Cog.listener() async def on_message(self, message: discord.Message): logging.info("on_message: %s", message) if message.author.id == self.bot.user.id: From 79bd6a1f1b6a94ccb57d5d175523cd4e98204a49 Mon Sep 17 00:00:00 2001 From: Paul Zagieboylo Date: Tue, 16 Jan 2024 21:55:54 -0500 Subject: [PATCH 5/5] fixed a couple of bugs --- herring/puzzles/discordbot.py | 9 +++++---- herring/puzzles/tasks.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/herring/puzzles/discordbot.py b/herring/puzzles/discordbot.py index 31d8845..71c540e 100644 --- a/herring/puzzles/discordbot.py +++ b/herring/puzzles/discordbot.py @@ -241,14 +241,13 @@ async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): except Puzzle.DoesNotExist: return - await self.add_user_to_puzzle(payload.member, puzzle.name) - if channel.type != discord.ChannelType.private: + logging.info(f"adding {payload.member.name} to {puzzle.slug}") + _, changed = await self.add_user_to_puzzle(payload.member, target_channel.name) + if changed and channel.type != discord.ChannelType.private: await message.remove_reaction(SIGNUP_EMOJI, payload.member) elif payload.emoji.name == LEAVE_EMOJI: # this should only work on the appropriate message in the puzzle channel or in puzzle-announcements if message.author.id == self.bot.user.id: - if channel.type != discord.ChannelType.private: - await message.remove_reaction(LEAVE_EMOJI, payload.member) if payload.channel_id == self.announce_channel.id: target_channel = message.channel_mentions[0] elif TRIUMPH_EMOJI in message.content: @@ -258,6 +257,8 @@ async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): target_channel = None if target_channel: await self.remove_user_from_puzzle(payload.member, target_channel.name) + if channel.type != discord.ChannelType.private: + await message.remove_reaction(LEAVE_EMOJI, payload.member) @commands.Cog.listener() async def on_message(self, message: discord.Message): diff --git a/herring/puzzles/tasks.py b/herring/puzzles/tasks.py index 4bc9875..d9b5bfd 100644 --- a/herring/puzzles/tasks.py +++ b/herring/puzzles/tasks.py @@ -73,7 +73,7 @@ def apply_async(*args, **kwargs): def post_local_and_global(local_channel, local_message, global_message, local_reaction=None, global_reaction=None): - logging.warning("tasks: post_local_and_global(%s, %s, %s, %s)", local_channel, local_message, global_message, local_reaction, global_reaction) + logging.warning("tasks: post_local_and_global(%s, %s, %s, %s, %s)", local_channel, local_message, global_message, local_reaction, global_reaction) if settings.HERRING_ACTIVATE_DISCORD: do_in_discord(DISCORD_ANNOUNCER.post_local_and_global(local_channel, local_message, global_message, local_reaction, global_reaction))