From 5d8342b0d50f11002f52b9788461e0cbbbaf789e Mon Sep 17 00:00:00 2001 From: wizzdom Date: Sat, 23 Nov 2024 05:20:16 +0000 Subject: [PATCH 1/6] action_items: add reactions --- src/extensions/action_items.py | 47 +++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/extensions/action_items.py b/src/extensions/action_items.py index 55d75be..f5c041e 100644 --- a/src/extensions/action_items.py +++ b/src/extensions/action_items.py @@ -95,7 +95,7 @@ async def get_action_items( # send each bullet point separately for item in formatted_bullet_points: - await action_items.client.rest.create_message( + item = await action_items.client.rest.create_message( CHANNEL_IDS["action-items"], mentions_everyone=False, user_mentions=True, @@ -103,6 +103,12 @@ async def get_action_items( content=item, ) + await action_items.client.rest.add_reaction( + channel=item.channel_id, + message=item.id, + emoji="✅", + ) + # respond with success if it executes successfully await ctx.respond( "✅ Action Items sent successfully!", @@ -111,6 +117,45 @@ async def get_action_items( return +@action_items.listen() +async def action_item_reaction(event: hikari.ReactionAddEvent) -> None: + bot_user = await action_items.client.rest.fetch_my_user() + bot_user_id = bot_user.id + + # ignore if not ✅ + if event.emoji_name != "✅": + return + # ignore bot reactions + if event.user_id == bot_user_id: + return + + # fetch the message that was reacted to + message = await action_items.client.rest.fetch_message( + event.channel_id, event.message_id + ) + + if not message.author.is_bot: + return + + # extract user mentions from the message content + mention_regex = r"<@!?(\d+)>" + mentions = re.findall(mention_regex, message.content) + + if not mentions: + return + + mentioned_user_ids = [int(user_id) for user_id in mentions] + + # only respond to reactions from mentioned user + if event.user_id in mentioned_user_ids: + # add strikethrough and checkmark + updated_content = f"- ✅ ~~{message.content[1:]}~~" + await action_items.client.rest.edit_message( + event.channel_id, event.message_id, content=updated_content + ) + return + + @arc.loader def loader(client: arc.GatewayClient) -> None: client.add_plugin(action_items) From 3843720598c870b277abb961496ab55b0d347335 Mon Sep 17 00:00:00 2001 From: wizzdom Date: Sat, 23 Nov 2024 05:53:40 +0000 Subject: [PATCH 2/6] regex go brrrrr --- src/extensions/action_items.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/extensions/action_items.py b/src/extensions/action_items.py index f5c041e..9db635e 100644 --- a/src/extensions/action_items.py +++ b/src/extensions/action_items.py @@ -13,8 +13,8 @@ @action_items.include -@arc.with_hook(restrict_to_channels(channel_ids=[CHANNEL_IDS["action-items"]])) -@arc.with_hook(restrict_to_roles(role_ids=[ROLE_IDS["committee"]])) +# @arc.with_hook(restrict_to_channels(channel_ids=[CHANNEL_IDS["action-items"]])) +# @arc.with_hook(restrict_to_roles(role_ids=[ROLE_IDS["committee"]])) @arc.slash_command( "action_items", "Display the action items from the MD", @@ -137,17 +137,22 @@ async def action_item_reaction(event: hikari.ReactionAddEvent) -> None: if not message.author.is_bot: return - # extract user mentions from the message content - mention_regex = r"<@!?(\d+)>" + # extract user and role mentions from the message content + mention_regex = r"<@!?(\d+)>|<@&(\d+)>" mentions = re.findall(mention_regex, message.content) - if not mentions: + # make a single list of both user and role mentions + mentioned_ids = [int(user_id or role_id) for user_id, role_id in mentions] + + if not mentioned_ids: return - mentioned_user_ids = [int(user_id) for user_id in mentions] + member = await action_items.client.rest.fetch_member(event.guild_id, event.user_id) + + is_mentioned_user = event.user_id in mentioned_ids + has_mentioned_role = any(role_id in mentioned_ids for role_id in member.role_ids) - # only respond to reactions from mentioned user - if event.user_id in mentioned_user_ids: + if is_mentioned_user or has_mentioned_role: # add strikethrough and checkmark updated_content = f"- ✅ ~~{message.content[1:]}~~" await action_items.client.rest.edit_message( From c432ad0ad379e1730070dcc76ec8b34022945355 Mon Sep 17 00:00:00 2001 From: nova Date: Tue, 3 Dec 2024 21:39:04 +0000 Subject: [PATCH 3/6] minor improvements --- src/extensions/action_items.py | 43 ++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/extensions/action_items.py b/src/extensions/action_items.py index 9db635e..5f5dda8 100644 --- a/src/extensions/action_items.py +++ b/src/extensions/action_items.py @@ -13,8 +13,8 @@ @action_items.include -# @arc.with_hook(restrict_to_channels(channel_ids=[CHANNEL_IDS["action-items"]])) -# @arc.with_hook(restrict_to_roles(role_ids=[ROLE_IDS["committee"]])) +@arc.with_hook(restrict_to_channels(channel_ids=[CHANNEL_IDS["action-items"]])) +@arc.with_hook(restrict_to_roles(role_ids=[ROLE_IDS["committee"]])) @arc.slash_command( "action_items", "Display the action items from the MD", @@ -118,36 +118,44 @@ async def get_action_items( @action_items.listen() -async def action_item_reaction(event: hikari.ReactionAddEvent) -> None: - bot_user = await action_items.client.rest.fetch_my_user() - bot_user_id = bot_user.id - - # ignore if not ✅ - if event.emoji_name != "✅": +async def action_item_reaction(event: hikari.GuildReactionAddEvent) -> None: + bot_user = action_items.client.app.get_me() + if not bot_user: # bot_user will always be available after the bot has started return - # ignore bot reactions - if event.user_id == bot_user_id: + + # ignore reactions by the bot, reactions that are not ✅ + # and reactions not created in the #action-items channel + if ( + event.user_id == bot_user.id + or event.emoji_name != "✅" + or event.channel_id != CHANNEL_IDS["action-items"] + ): return - # fetch the message that was reacted to - message = await action_items.client.rest.fetch_message( + # retrieve the message that was reacted to + message = action_items.client.cache.get_message( + event.message_id + ) or await action_items.client.rest.fetch_message( event.channel_id, event.message_id ) - if not message.author.is_bot: + # ignore messages not sent by the bot and messages with no content + if message.author.id != bot_user.id or not message.content: return # extract user and role mentions from the message content - mention_regex = r"<@!?(\d+)>|<@&(\d+)>" + mention_regex = r"<@[!&]?(\d+)>" mentions = re.findall(mention_regex, message.content) - # make a single list of both user and role mentions - mentioned_ids = [int(user_id or role_id) for user_id, role_id in mentions] + # make a list of all mentions + mentioned_ids = [int(id_) for id_ in mentions] if not mentioned_ids: return - member = await action_items.client.rest.fetch_member(event.guild_id, event.user_id) + member = action_items.client.cache.get_member( + event.guild_id, event.user_id + ) or await action_items.client.rest.fetch_member(event.guild_id, event.user_id) is_mentioned_user = event.user_id in mentioned_ids has_mentioned_role = any(role_id in mentioned_ids for role_id in member.role_ids) @@ -158,7 +166,6 @@ async def action_item_reaction(event: hikari.ReactionAddEvent) -> None: await action_items.client.rest.edit_message( event.channel_id, event.message_id, content=updated_content ) - return @arc.loader From 9930630c69f90a5bc245fd952be22b3877b7870c Mon Sep 17 00:00:00 2001 From: nova Date: Mon, 13 Jan 2025 11:45:12 +0000 Subject: [PATCH 4/6] Fix recursive reactions --- src/extensions/action_items.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/extensions/action_items.py b/src/extensions/action_items.py index 5f5dda8..2779fda 100644 --- a/src/extensions/action_items.py +++ b/src/extensions/action_items.py @@ -160,9 +160,10 @@ async def action_item_reaction(event: hikari.GuildReactionAddEvent) -> None: is_mentioned_user = event.user_id in mentioned_ids has_mentioned_role = any(role_id in mentioned_ids for role_id in member.role_ids) - if is_mentioned_user or has_mentioned_role: + # cross out the action item, if it was not crossed out already + if (is_mentioned_user or has_mentioned_role) and not message.content.startswith("- ✅ ~~"): # add strikethrough and checkmark - updated_content = f"- ✅ ~~{message.content[1:]}~~" + updated_content = f"- ✅ ~~{message.content[2:]}~~" await action_items.client.rest.edit_message( event.channel_id, event.message_id, content=updated_content ) From c3b0df9c708ca2a5220b9290a0d900f1c26d0e3c Mon Sep 17 00:00:00 2001 From: nova Date: Mon, 13 Jan 2025 12:04:06 +0000 Subject: [PATCH 5/6] Format code --- src/extensions/action_items.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/extensions/action_items.py b/src/extensions/action_items.py index 2779fda..6228b0c 100644 --- a/src/extensions/action_items.py +++ b/src/extensions/action_items.py @@ -161,7 +161,9 @@ async def action_item_reaction(event: hikari.GuildReactionAddEvent) -> None: has_mentioned_role = any(role_id in mentioned_ids for role_id in member.role_ids) # cross out the action item, if it was not crossed out already - if (is_mentioned_user or has_mentioned_role) and not message.content.startswith("- ✅ ~~"): + if (is_mentioned_user or has_mentioned_role) and not message.content.startswith( + "- ✅ ~~" + ): # add strikethrough and checkmark updated_content = f"- ✅ ~~{message.content[2:]}~~" await action_items.client.rest.edit_message( From d5c3a510d295c2726a52398ec7213e1b885e8d69 Mon Sep 17 00:00:00 2001 From: nova Date: Tue, 14 Jan 2025 10:24:07 +0000 Subject: [PATCH 6/6] Handle reaction removals --- src/extensions/action_items.py | 111 ++++++++++++++++++++++++++------- 1 file changed, 90 insertions(+), 21 deletions(-) diff --git a/src/extensions/action_items.py b/src/extensions/action_items.py index 6228b0c..a972ca5 100644 --- a/src/extensions/action_items.py +++ b/src/extensions/action_items.py @@ -117,11 +117,13 @@ async def get_action_items( return -@action_items.listen() -async def action_item_reaction(event: hikari.GuildReactionAddEvent) -> None: +async def check_valid_reaction( + event: hikari.GuildReactionAddEvent | hikari.GuildReactionDeleteEvent, + message: hikari.PartialMessage, +) -> bool: bot_user = action_items.client.app.get_me() if not bot_user: # bot_user will always be available after the bot has started - return + return False # ignore reactions by the bot, reactions that are not ✅ # and reactions not created in the #action-items channel @@ -130,40 +132,63 @@ async def action_item_reaction(event: hikari.GuildReactionAddEvent) -> None: or event.emoji_name != "✅" or event.channel_id != CHANNEL_IDS["action-items"] ): - return + return False - # retrieve the message that was reacted to - message = action_items.client.cache.get_message( - event.message_id - ) or await action_items.client.rest.fetch_message( - event.channel_id, event.message_id - ) + assert message.author # it will always be available # ignore messages not sent by the bot and messages with no content if message.author.id != bot_user.id or not message.content: - return + return False + return True + + +async def validate_user_reaction( + user_id: int, message_content: str, guild_id: int +) -> bool: # extract user and role mentions from the message content mention_regex = r"<@[!&]?(\d+)>" - mentions = re.findall(mention_regex, message.content) + mentions = re.findall(mention_regex, message_content) # make a list of all mentions mentioned_ids = [int(id_) for id_ in mentions] - if not mentioned_ids: - return + if user_id in mentioned_ids: + return True member = action_items.client.cache.get_member( - event.guild_id, event.user_id - ) or await action_items.client.rest.fetch_member(event.guild_id, event.user_id) + guild_id, user_id + ) or await action_items.client.rest.fetch_member(guild_id, user_id) + + if any(role_id in mentioned_ids for role_id in member.role_ids): + return True + + return False + + +@action_items.listen() +async def reaction_add(event: hikari.GuildReactionAddEvent) -> None: + # retrieve the message that was reacted to + message = action_items.client.cache.get_message( + event.message_id + ) or await action_items.client.rest.fetch_message( + event.channel_id, event.message_id + ) + + is_valid_reaction = await check_valid_reaction(event, message) + if not is_valid_reaction: + return + + assert message.content # check_valid_reaction verifies the message content exists - is_mentioned_user = event.user_id in mentioned_ids - has_mentioned_role = any(role_id in mentioned_ids for role_id in member.role_ids) + is_valid_reaction = await validate_user_reaction( + event.user_id, message.content, event.guild_id + ) + if not is_valid_reaction: + return # cross out the action item, if it was not crossed out already - if (is_mentioned_user or has_mentioned_role) and not message.content.startswith( - "- ✅ ~~" - ): + if not message.content.startswith("- ✅ ~~"): # add strikethrough and checkmark updated_content = f"- ✅ ~~{message.content[2:]}~~" await action_items.client.rest.edit_message( @@ -171,6 +196,50 @@ async def action_item_reaction(event: hikari.GuildReactionAddEvent) -> None: ) +@action_items.listen() +async def reaction_remove(event: hikari.GuildReactionDeleteEvent) -> None: + # retrieve the message that was un-reacted to + # NOTE: cannot use cached message as the reaction count will be outdated + message = await action_items.client.rest.fetch_message( + event.channel_id, event.message_id + ) + + is_valid_reaction = await check_valid_reaction(event, message) + if not is_valid_reaction: + return + + assert message.content # check_valid_reaction verifies the message content exists + + checkmark_reactions = await event.app.rest.fetch_reactions_for_emoji( + event.channel_id, + event.message_id, + "✅", + ) + + reactions = [ + await validate_user_reaction(user.id, message.content, event.guild_id) + for user in checkmark_reactions + ] + valid_reaction_count = len( + list( + filter( + lambda r: r is True, + reactions, + ) + ) + ) + + assert message.content # check_valid_reaction verifies the message content exists + # remove the strikethrough on the item, provided all mentioned users/roles + # are not currently reacted to the message + if message.content.startswith("- ✅ ~~") and valid_reaction_count == 0: + # add strikethrough and checkmark + updated_content = f"- {message.content[6:-2]}" + await action_items.client.rest.edit_message( + event.channel_id, event.message_id, content=updated_content + ) + + @arc.loader def loader(client: arc.GatewayClient) -> None: client.add_plugin(action_items)