From ef0e0bf7ebf6b791281cea11bdc61a3a355a3b01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 08:15:24 +0000 Subject: [PATCH 1/8] Initial plan From c5b76d34b811a2aea35c751b6c1ca93a7265f399 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 08:18:41 +0000 Subject: [PATCH 2/8] Add REPOST_TO_THREAD_AND_DELETE action handler with tests Co-authored-by: alexeygrigorev <875246+alexeygrigorev@users.noreply.github.com> --- automator/config.yaml | 20 ++++ automator/lambda_function.py | 60 +++++++++++ integration_tests/test.py | 16 +++ tests/test_automator_lambda.py | 191 +++++++++++++++++++++++++++++++++ 4 files changed, 287 insertions(+) diff --git a/automator/config.yaml b/automator/config.yaml index 0f2e218..f8778d0 100644 --- a/automator/config.yaml +++ b/automator/config.yaml @@ -64,6 +64,26 @@ reactions: Follow <{link}|these recommendations> to make it easier to help you. + - reaction: error-log-to-thread-and-delete + type: REPOST_TO_THREAD_AND_DELETE + placeholders: + link: + course-ml-zoomcamp: https://github.com/DataTalksClub/machine-learning-zoomcamp/blob/master/asking-questions.md + course-data-engineering: https://github.com/DataTalksClub/data-engineering-zoomcamp/blob/main/asking-questions.md + course-mlops-zoomcamp: https://github.com/DataTalksClub/mlops-zoomcamp/blob/main/asking-questions.md + course-llm-zoomcamp: https://github.com/DataTalksClub/llm-zoomcamp/blob/main/asking-questions.md + default: https://datatalks.club/slack/guidelines.html#code-problems-and-errors + message: | + Removing the message from the channel to save space. Please move the error log to the thread. + + Use code block for formatting the log: https://slack.com/help/articles/202288908-Format-your-messages + + Follow <{link}|these recommendations> to make it easier to help you. + + Here's the original message: + + > {user_message} + - reaction: no-screenshot type: SLACK_POST placeholders: diff --git a/automator/lambda_function.py b/automator/lambda_function.py index c50cef7..2569168 100644 --- a/automator/lambda_function.py +++ b/automator/lambda_function.py @@ -172,11 +172,71 @@ def handle_ask_ai(event, reaction_config): slack.post_message_thread(event, message) +def handle_repost_to_thread_and_delete(event, reaction_config): + """Repost the original message to the thread with a custom message, then delete from channel""" + item = event['item'] + channel = item['channel'] + ts = item['ts'] + channel_name = get_channel_name(channel) + + # Get the original message content + message_details = slack.get_message_content(channel, ts) + if not message_details: + logger.info(f"Message not found for {channel} {ts}") + return + + user = message_details.get('user') + original_message = message_details.get('text', '') + + # Skip if there's no user (e.g., bot message or deleted message) + if not user: + logger.info(f"Message has no user for {channel} {ts}") + return + + # Escape curly braces in user message to avoid format string issues + original_message_escaped = original_message.replace('{', '{{').replace('}', '}}') + + # Format the thread message with placeholders + message_pattern = reaction_config['message'] + + values = { + 'user': user, + 'user_message': original_message_escaped, + 'channel': channel, + } + + if 'placeholders' in reaction_config: + thread_message = util.format_message( + message_pattern, + reaction_config['placeholders'], + channel_name + ) + if thread_message is None: + logger.info(f"No placeholder matched for channel {channel_name}") + return + # Now format with user and user_message + thread_message = thread_message.format(**values) + else: + thread_message = message_pattern.format(**values) + + # Post the message to the thread + slack.post_message_thread(event, thread_message) + logger.info(f"Reposted message to thread for {channel} {ts}") + + # Delete the original message from the channel + if FAKE_DELETE: + logger.info(f"FAKE_DELETE for {channel} {ts}") + else: + slack.remove_message(channel, ts) + logger.info(f"Deleted original message from channel {channel} (kept in thread)") + + action_handlers = { 'SLACK_POST': handle_slack_post, 'DELETE_MESSAGE': handle_delete_message, 'ASK_AI': handle_ask_ai, 'REMOVE_BROADCAST': handle_remove_broadcast, + 'REPOST_TO_THREAD_AND_DELETE': handle_repost_to_thread_and_delete, } diff --git a/integration_tests/test.py b/integration_tests/test.py index 8aedc97..47e6904 100644 --- a/integration_tests/test.py +++ b/integration_tests/test.py @@ -65,6 +65,19 @@ def test_error_log_to_thread_please_other_channel(): # should send a default message +def test_error_log_to_thread_and_delete_de_zoomcamp(): + trigger_reaction_for_channel('error-log-to-thread-and-delete', 'course-data-engineering') + + +def test_error_log_to_thread_and_delete_ml_zoomcamp(): + trigger_reaction_for_channel('error-log-to-thread-and-delete', 'course-ml-zoomcamp') + + +def test_error_log_to_thread_and_delete_other_channel(): + trigger_reaction_for_channel('error-log-to-thread-and-delete', 'general') + # should send a default message and delete + + def test_no_screenshot_de_zoomcamp(): trigger_reaction_for_channel('no-screenshot', 'course-data-engineering') @@ -111,6 +124,9 @@ def run(): # test_error_log_to_thread_please_de_zoomcamp() # test_error_log_to_thread_please_ml_zoomcamp() # test_error_log_to_thread_please_other_channel() + # test_error_log_to_thread_and_delete_de_zoomcamp() + # test_error_log_to_thread_and_delete_ml_zoomcamp() + # test_error_log_to_thread_and_delete_other_channel() # test_no_screenshot_de_zoomcamp() # test_no_screenshot_other_channel() # test_shameless_rules() diff --git a/tests/test_automator_lambda.py b/tests/test_automator_lambda.py index 913d488..c334768 100644 --- a/tests/test_automator_lambda.py +++ b/tests/test_automator_lambda.py @@ -312,6 +312,27 @@ def test_thread_reaction_uses_single_handler(self): self.assertEqual(reaction_config['type'], 'SLACK_POST') # Should not have 'types' (multiple handlers) self.assertNotIn('types', reaction_config) + + def test_error_log_to_thread_and_delete_reaction_exists(self): + """Verify that 'error-log-to-thread-and-delete' reaction is configured""" + reaction_config = lambda_function.reaction_configs.get('error-log-to-thread-and-delete') + self.assertIsNotNone(reaction_config) + self.assertEqual(reaction_config['type'], 'REPOST_TO_THREAD_AND_DELETE') + self.assertIn('message', reaction_config) + self.assertIn('placeholders', reaction_config) + # Verify the message contains expected text + message = reaction_config['message'] + self.assertIn('Removing the message', message) + self.assertIn('original message', message.lower()) + self.assertIn('{user_message}', message) + + def test_action_handlers_has_repost_to_thread_and_delete(self): + """Verify that REPOST_TO_THREAD_AND_DELETE handler is registered""" + self.assertIn('REPOST_TO_THREAD_AND_DELETE', lambda_function.action_handlers) + self.assertEqual( + lambda_function.action_handlers['REPOST_TO_THREAD_AND_DELETE'], + lambda_function.handle_repost_to_thread_and_delete + ) class TestRemoveBroadcast(unittest.TestCase): @@ -544,5 +565,175 @@ def test_ask_ai_with_curly_braces_in_user_message(self, mock_slack, mock_groqu): mock_slack.post_message_thread.assert_called_once() +class TestRepostToThreadAndDelete(unittest.TestCase): + """Test REPOST_TO_THREAD_AND_DELETE handler""" + + @patch('automator_lambda_function.slack') + @patch('automator_lambda_function.util') + def test_repost_with_placeholders(self, mock_util, mock_slack): + """Test REPOST_TO_THREAD_AND_DELETE with channel-specific placeholders""" + # Setup mocks + mock_slack.get_message_content.return_value = { + 'user': 'U123456', + 'text': 'Here is my error log with lots of text' + } + mock_util.format_message.return_value = 'Removing the message. Follow . Here\'s the original message:\n\n> {user_message}' + + # Create event + event = { + 'item': { + 'channel': 'C123456', + 'ts': '1234567890.123456' + }, + 'reaction': 'error-log-to-thread-and-delete' + } + + # Create reaction config with placeholders + reaction_config = { + 'message': 'Removing the message. Follow <{link}|these recommendations>. Here\'s the original message:\n\n> {user_message}', + 'type': 'REPOST_TO_THREAD_AND_DELETE', + 'placeholders': { + 'link': { + 'course-ml-zoomcamp': 'https://github.com/DataTalksClub/machine-learning-zoomcamp/blob/master/asking-questions.md', + 'default': 'https://datatalks.club/slack/guidelines.html' + } + } + } + + # Execute + lambda_function.handle_repost_to_thread_and_delete(event, reaction_config) + + # Verify + mock_slack.get_message_content.assert_called_once_with('C123456', '1234567890.123456') + mock_slack.post_message_thread.assert_called_once() + # Verify the posted message contains the original message + posted_message = mock_slack.post_message_thread.call_args[0][1] + self.assertIn('Here is my error log with lots of text', posted_message) + + @patch('automator_lambda_function.slack') + def test_repost_without_placeholders(self, mock_slack): + """Test REPOST_TO_THREAD_AND_DELETE without placeholders""" + # Setup mocks + mock_slack.get_message_content.return_value = { + 'user': 'U123456', + 'text': 'Error message here' + } + + # Create event + event = { + 'item': { + 'channel': 'C123456', + 'ts': '1234567890.123456' + }, + 'reaction': 'error-log-to-thread-and-delete' + } + + # Create reaction config without placeholders + reaction_config = { + 'message': 'Removing from channel. Original message:\n\n> {user_message}', + 'type': 'REPOST_TO_THREAD_AND_DELETE' + } + + # Execute + lambda_function.handle_repost_to_thread_and_delete(event, reaction_config) + + # Verify + mock_slack.post_message_thread.assert_called_once() + posted_message = mock_slack.post_message_thread.call_args[0][1] + self.assertIn('Error message here', posted_message) + + @patch('automator_lambda_function.slack') + def test_repost_message_not_found(self, mock_slack): + """Test REPOST_TO_THREAD_AND_DELETE handles missing message gracefully""" + # Setup mocks - message not found + mock_slack.get_message_content.return_value = None + + # Create event + event = { + 'item': { + 'channel': 'C123456', + 'ts': '1234567890.123456' + }, + 'reaction': 'error-log-to-thread-and-delete' + } + + # Create reaction config + reaction_config = { + 'message': 'Message: {user_message}', + 'type': 'REPOST_TO_THREAD_AND_DELETE' + } + + # Execute + lambda_function.handle_repost_to_thread_and_delete(event, reaction_config) + + # Verify - should not post or delete when message not found + mock_slack.post_message_thread.assert_not_called() + mock_slack.remove_message.assert_not_called() + + @patch('automator_lambda_function.slack') + def test_repost_no_user(self, mock_slack): + """Test REPOST_TO_THREAD_AND_DELETE handles missing user gracefully""" + # Setup mocks - message without user + mock_slack.get_message_content.return_value = { + 'text': 'Bot message without user' + } + + # Create event + event = { + 'item': { + 'channel': 'C123456', + 'ts': '1234567890.123456' + }, + 'reaction': 'error-log-to-thread-and-delete' + } + + # Create reaction config + reaction_config = { + 'message': 'Message: {user_message}', + 'type': 'REPOST_TO_THREAD_AND_DELETE' + } + + # Execute + lambda_function.handle_repost_to_thread_and_delete(event, reaction_config) + + # Verify - should not post or delete when no user + mock_slack.post_message_thread.assert_not_called() + mock_slack.remove_message.assert_not_called() + + @patch('automator_lambda_function.slack') + def test_repost_with_curly_braces_in_message(self, mock_slack): + """Test REPOST_TO_THREAD_AND_DELETE handles curly braces in user message""" + # Setup mocks - message with curly braces + mock_slack.get_message_content.return_value = { + 'user': 'U123456', + 'text': 'How to configure {POSTGRES_DB} and {POSTGRES_USER}?' + } + + # Create event + event = { + 'item': { + 'channel': 'C123456', + 'ts': '1234567890.123456' + }, + 'reaction': 'error-log-to-thread-and-delete' + } + + # Create reaction config + reaction_config = { + 'message': 'Original: {user_message}', + 'type': 'REPOST_TO_THREAD_AND_DELETE' + } + + # Execute - should not raise KeyError + lambda_function.handle_repost_to_thread_and_delete(event, reaction_config) + + # Verify + mock_slack.post_message_thread.assert_called_once() + posted_message = mock_slack.post_message_thread.call_args[0][1] + # Should contain the escaped braces + self.assertIn('POSTGRES_DB', posted_message) + self.assertIn('POSTGRES_USER', posted_message) + + if __name__ == '__main__': unittest.main() From 9fa220e88310830b7fd19b100c7943cc1d4b705f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 08:19:06 +0000 Subject: [PATCH 3/8] Update documentation for new REPOST_TO_THREAD_AND_DELETE action Co-authored-by: alexeygrigorev <875246+alexeygrigorev@users.noreply.github.com> --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index b3c9f65..43ef857 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ This section defines automated responses to specific reactions or triggers in th 2. `DELETE_MESSAGE`: Removes a message and sends a notification to the user 3. `ASK_AI`: Utilizes an AI model to generate a response 4. `REMOVE_BROADCAST`: Removes a broadcasted thread reply from the channel (keeps it in the thread) +5. `REPOST_TO_THREAD_AND_DELETE`: Reposts the original message to the thread with a custom message, then deletes it from the channel #### Multiple Handlers: @@ -140,6 +141,15 @@ For reactions with channel-specific placeholders (like `{link}` in the `faq` rea - `{link}`: Channel-specific guidelines for sharing error logs - Default behavior: Uses a default link if the channel doesn't match +4a. `error-log-to-thread-and-delete`: + - Type: `REPOST_TO_THREAD_AND_DELETE` + - Action: Reposts the original message to the thread with instructions, then deletes it from the channel to save space + - Placeholders: + - `{link}`: Channel-specific guidelines for sharing error logs + - `{user_message}`: The original message content + - Default behavior: Uses a default link if the channel doesn't match + - Note: This is an enhanced version of `error-log-to-thread-please` that also removes the message from the channel + 5. `no-screenshot`: - Type: `SLACK_POST` - Action: Advises against posting screenshots of code From 1ff15e1c0e856acdfb9e76a54d7c20c27866a54a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 08:20:29 +0000 Subject: [PATCH 4/8] Address code review feedback - fix escaped quotes and add test coverage Co-authored-by: alexeygrigorev <875246+alexeygrigorev@users.noreply.github.com> --- tests/test_automator_lambda.py | 44 ++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/tests/test_automator_lambda.py b/tests/test_automator_lambda.py index c334768..a06d19d 100644 --- a/tests/test_automator_lambda.py +++ b/tests/test_automator_lambda.py @@ -577,7 +577,7 @@ def test_repost_with_placeholders(self, mock_util, mock_slack): 'user': 'U123456', 'text': 'Here is my error log with lots of text' } - mock_util.format_message.return_value = 'Removing the message. Follow . Here\'s the original message:\n\n> {user_message}' + mock_util.format_message.return_value = "Removing the message. Follow . Here's the original message:\n\n> {user_message}" # Create event event = { @@ -590,7 +590,7 @@ def test_repost_with_placeholders(self, mock_util, mock_slack): # Create reaction config with placeholders reaction_config = { - 'message': 'Removing the message. Follow <{link}|these recommendations>. Here\'s the original message:\n\n> {user_message}', + 'message': "Removing the message. Follow <{link}|these recommendations>. Here's the original message:\n\n> {user_message}", 'type': 'REPOST_TO_THREAD_AND_DELETE', 'placeholders': { 'link': { @@ -733,6 +733,46 @@ def test_repost_with_curly_braces_in_message(self, mock_slack): # Should contain the escaped braces self.assertIn('POSTGRES_DB', posted_message) self.assertIn('POSTGRES_USER', posted_message) + + @patch('automator_lambda_function.slack') + @patch('automator_lambda_function.util') + def test_repost_placeholder_returns_none(self, mock_util, mock_slack): + """Test REPOST_TO_THREAD_AND_DELETE handles placeholder matching failure gracefully""" + # Setup mocks + mock_slack.get_message_content.return_value = { + 'user': 'U123456', + 'text': 'Error message' + } + # util.format_message returns None when no placeholder matches and no default + mock_util.format_message.return_value = None + + # Create event + event = { + 'item': { + 'channel': 'C123456', + 'ts': '1234567890.123456' + }, + 'reaction': 'error-log-to-thread-and-delete' + } + + # Create reaction config with placeholders but no default + reaction_config = { + 'message': 'Message with {link}', + 'type': 'REPOST_TO_THREAD_AND_DELETE', + 'placeholders': { + 'link': { + 'course-ml-zoomcamp': 'https://example.com' + # No default value + } + } + } + + # Execute + lambda_function.handle_repost_to_thread_and_delete(event, reaction_config) + + # Verify - should not post or delete when placeholder matching fails + mock_slack.post_message_thread.assert_not_called() + mock_slack.remove_message.assert_not_called() if __name__ == '__main__': From f05f14f7a122d35f584398fdbbfceb4d365a29d1 Mon Sep 17 00:00:00 2001 From: Alexey Grigorev Date: Fri, 6 Feb 2026 09:47:43 +0100 Subject: [PATCH 5/8] Remove SLACK_POST reaction from config.yaml Removed SLACK_POST reaction configuration and its placeholders. --- automator/config.yaml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/automator/config.yaml b/automator/config.yaml index f8778d0..3677b38 100644 --- a/automator/config.yaml +++ b/automator/config.yaml @@ -49,22 +49,6 @@ reactions: Please check the <{link}|FAQ> - reaction: error-log-to-thread-please - type: SLACK_POST - placeholders: - link: - course-ml-zoomcamp: https://github.com/DataTalksClub/machine-learning-zoomcamp/blob/master/asking-questions.md - course-data-engineering: https://github.com/DataTalksClub/data-engineering-zoomcamp/blob/main/asking-questions.md - course-mlops-zoomcamp: https://github.com/DataTalksClub/mlops-zoomcamp/blob/main/asking-questions.md - course-llm-zoomcamp: https://github.com/DataTalksClub/llm-zoomcamp/blob/main/asking-questions.md - default: https://datatalks.club/slack/guidelines.html#code-problems-and-errors - message: | - Please move the error log from the main message to the thread. - - Use code block for formatting the log: https://slack.com/help/articles/202288908-Format-your-messages - - Follow <{link}|these recommendations> to make it easier to help you. - - - reaction: error-log-to-thread-and-delete type: REPOST_TO_THREAD_AND_DELETE placeholders: link: From e58441a21146c1690c1deb4fb4147f0df4a73626 Mon Sep 17 00:00:00 2001 From: Alexey Grigorev Date: Fri, 6 Feb 2026 09:50:07 +0100 Subject: [PATCH 6/8] Fix reaction config key in error log test --- tests/test_automator_lambda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_automator_lambda.py b/tests/test_automator_lambda.py index a06d19d..9b476a0 100644 --- a/tests/test_automator_lambda.py +++ b/tests/test_automator_lambda.py @@ -315,7 +315,7 @@ def test_thread_reaction_uses_single_handler(self): def test_error_log_to_thread_and_delete_reaction_exists(self): """Verify that 'error-log-to-thread-and-delete' reaction is configured""" - reaction_config = lambda_function.reaction_configs.get('error-log-to-thread-and-delete') + reaction_config = lambda_function.reaction_configs.get('error-log-to-thread-please') self.assertIsNotNone(reaction_config) self.assertEqual(reaction_config['type'], 'REPOST_TO_THREAD_AND_DELETE') self.assertIn('message', reaction_config) From 0f8a5ec0f6da1d673cd65a666134df126614fd19 Mon Sep 17 00:00:00 2001 From: Alexey Grigorev Date: Fri, 6 Feb 2026 09:51:48 +0100 Subject: [PATCH 7/8] Update reaction string in test case --- tests/test_automator_lambda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_automator_lambda.py b/tests/test_automator_lambda.py index 9b476a0..25d6a31 100644 --- a/tests/test_automator_lambda.py +++ b/tests/test_automator_lambda.py @@ -585,7 +585,7 @@ def test_repost_with_placeholders(self, mock_util, mock_slack): 'channel': 'C123456', 'ts': '1234567890.123456' }, - 'reaction': 'error-log-to-thread-and-delete' + 'reaction': 'error-log-to-thread-please' } # Create reaction config with placeholders From 5a14806531b6f87c3addd212d76318f5f42d07f6 Mon Sep 17 00:00:00 2001 From: Alexey Grigorev Date: Fri, 6 Feb 2026 09:53:25 +0100 Subject: [PATCH 8/8] Change reaction text in test cases --- tests/test_automator_lambda.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_automator_lambda.py b/tests/test_automator_lambda.py index 25d6a31..aa011f3 100644 --- a/tests/test_automator_lambda.py +++ b/tests/test_automator_lambda.py @@ -625,7 +625,7 @@ def test_repost_without_placeholders(self, mock_slack): 'channel': 'C123456', 'ts': '1234567890.123456' }, - 'reaction': 'error-log-to-thread-and-delete' + 'reaction': 'error-log-to-thread-please' } # Create reaction config without placeholders @@ -654,7 +654,7 @@ def test_repost_message_not_found(self, mock_slack): 'channel': 'C123456', 'ts': '1234567890.123456' }, - 'reaction': 'error-log-to-thread-and-delete' + 'reaction': 'error-log-to-thread-please' } # Create reaction config @@ -684,7 +684,7 @@ def test_repost_no_user(self, mock_slack): 'channel': 'C123456', 'ts': '1234567890.123456' }, - 'reaction': 'error-log-to-thread-and-delete' + 'reaction': 'error-log-to-thread-please' } # Create reaction config @@ -715,7 +715,7 @@ def test_repost_with_curly_braces_in_message(self, mock_slack): 'channel': 'C123456', 'ts': '1234567890.123456' }, - 'reaction': 'error-log-to-thread-and-delete' + 'reaction': 'error-log-to-thread-please' } # Create reaction config @@ -752,7 +752,7 @@ def test_repost_placeholder_returns_none(self, mock_util, mock_slack): 'channel': 'C123456', 'ts': '1234567890.123456' }, - 'reaction': 'error-log-to-thread-and-delete' + 'reaction': 'error-log-to-thread-please' } # Create reaction config with placeholders but no default