Skip to content

Commit feee71a

Browse files
authored
Merge pull request #34 from surface-security/change_the_process
Reaction process implementation
2 parents 854f980 + 36d2c47 commit feee71a

4 files changed

Lines changed: 156 additions & 4 deletions

File tree

slackbot/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from functools import lru_cache
22

3-
__version__ = "0.1.6"
3+
__version__ = "0.1.7"
44

55

66
@lru_cache

slackbot/base.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,25 @@ def post_ephemeral(self, **kwargs):
2727
kwargs["as_user"] = kwargs.get("as_user", 1)
2828
return self.web.chat_postEphemeral(**kwargs)
2929

30+
def process_reaction(self, reaction_event: dict) -> Optional[Union[int, tuple[int, int]]]:
31+
"""
32+
Handles reactions added or removed in Slack messages.
33+
34+
:param reaction_event: A dictionary containing reaction event data.
35+
:return: None, self.STOP, self.PROCESSED, or tuple (self.PROCESSED, self.STOP)
36+
37+
PROCESSED if anything was done with input
38+
STOP if no other processor should be called after this one
39+
"""
40+
if "reaction" not in reaction_event:
41+
return # Ignore events that are not reactions
42+
43+
return self.handle_reaction(reaction_event)
44+
45+
def handle_reaction(self, reaction_event) -> Optional[Union[int, tuple[int, int]]]:
46+
# This method is currently a placeholder.
47+
pass
48+
3049
def process(self, message, **kw) -> Optional[Union[int, tuple[int, int]]]:
3150
"""
3251
:return: None, self.STOP, self.PROCESSED, or tuple PROCESSED,STOP

slackbot/management/commands/run_bot.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@ def handle_message(self, **payload):
4040
close_old_connections()
4141
self.handle_message_really(**payload)
4242

43+
def handle_reaction(self, **payload):
44+
event = payload.get("event", {})
45+
processed_at_least_one = False
46+
for p in self.processors:
47+
try:
48+
r = p.process_reaction(event)
49+
if r:
50+
if not isinstance(r, tuple):
51+
r = (r,)
52+
if MessageProcessor.PROCESSED in r:
53+
processed_at_least_one = True
54+
if MessageProcessor.STOP in r:
55+
break
56+
except Exception as e:
57+
self.log_exception("Processor failed for reaction event: %s %s", event, str(e))
58+
return processed_at_least_one
59+
4360
def handle_message_really(self, **payload):
4461
event = payload.get("event")
4562

@@ -80,8 +97,8 @@ def handle_message_really(self, **payload):
8097
processed_at_least_one = True
8198
if MessageProcessor.STOP in r:
8299
break
83-
except Exception as exc:
84-
self.log_exception(f"Processor {str(p)} failed with {str(exc)} for message {message}")
100+
except Exception as e:
101+
self.log_exception("Processor failed for message: %s %s", message, str(e))
85102

86103
# If private DM
87104
if channel[0] == "D":
@@ -118,9 +135,19 @@ def process(self, client: SocketModeClient, req: SocketModeRequest):
118135
response = SocketModeResponse(envelope_id=req.envelope_id)
119136
client.send_socket_mode_response(response)
120137

121-
if req.payload["event"]["type"] == "message" and req.payload["event"].get("subtype") is None:
138+
event = req.payload["event"]
139+
140+
if event["type"] == "message" and event.get("subtype") is None:
122141
return self.handle_message(**req.payload)
123142

143+
def process_reaction(self, client: SocketModeClient, req: SocketModeRequest):
144+
if req.type == "events_api":
145+
response = SocketModeResponse(envelope_id=req.envelope_id)
146+
client.send_socket_mode_response(response)
147+
event = req.payload["event"]
148+
if event["type"] in ("reaction_added", "reaction_removed"):
149+
return self.handle_reaction(**req.payload)
150+
124151
def handle(self, *args, **options):
125152
# faster cold boot
126153
from slack_sdk.web.client import WebClient
@@ -133,6 +160,7 @@ def handle(self, *args, **options):
133160
)
134161
self.set_up()
135162
self.client.socket_mode_request_listeners.append(self.process)
163+
self.client.socket_mode_request_listeners.append(self.process_reaction)
136164
self.stdout.write("Connecting...\n")
137165
self.client.connect()
138166

testapp/tests/test_run_bot.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import unittest
2+
from unittest.mock import patch, MagicMock
3+
from slackbot.management.commands.run_bot import Command
4+
from slack_sdk.socket_mode.request import SocketModeRequest
5+
6+
7+
class TestSlackBotCommand(unittest.TestCase):
8+
def setUp(self):
9+
self.command = Command()
10+
self.command.web = MagicMock()
11+
self.command.client = MagicMock()
12+
self.command.my_id = "U123456"
13+
self.command.my_id_match = "<@U123456>"
14+
self.command.processors = []
15+
16+
@patch("slackbot.management.commands.run_bot.close_old_connections")
17+
def test_handle_message(self, mock_close_old_connections):
18+
payload = {
19+
"event": {
20+
"channel": "C12345",
21+
"user": "U67890",
22+
"text": "Hello, bot!",
23+
"ts": "123456.789",
24+
}
25+
}
26+
result = self.command.handle_message(**payload)
27+
self.assertFalse(result) # No processors, so should return False
28+
29+
@patch("threading.Event.wait", return_value=None)
30+
@patch("slackbot.management.commands.run_bot.SocketModeClient")
31+
@patch("slackbot.management.commands.run_bot.settings")
32+
def test_handle(self, mock_settings, mock_socket_client, mock_event_wait):
33+
mock_settings.SLACKBOT_BOT_TOKEN = "xoxb-123"
34+
mock_settings.SLACKBOT_APP_TOKEN = "xapp-123"
35+
36+
mock_client_instance = mock_socket_client.return_value
37+
self.command.set_up = MagicMock()
38+
self.command.handle()
39+
40+
self.command.set_up.assert_called_once()
41+
mock_client_instance.connect.assert_called_once()
42+
43+
def test_handle_reaction(self):
44+
payload = {
45+
"event": {
46+
"type": "reaction_added",
47+
"item": {"channel": "C12345"},
48+
"user": "U67890",
49+
"event_ts": "123456.789",
50+
}
51+
}
52+
result = self.command.handle_reaction(**payload)
53+
self.assertFalse(result) # No processors, so should return False
54+
55+
def test_process_event_message(self):
56+
request = SocketModeRequest(
57+
type="events_api",
58+
envelope_id="envelope123",
59+
payload={"event": {"type": "message", "subtype": None, "text": "Hello!"}},
60+
)
61+
62+
self.command.handle_message = MagicMock(return_value=True)
63+
result = self.command.process(self.command.client, request)
64+
65+
self.command.handle_message.assert_called_once()
66+
self.assertTrue(result)
67+
68+
def test_post_message(self):
69+
self.command.post_message(channel="C12345", text="Hello!")
70+
self.command.web.chat_postMessage.assert_called_once_with(channel="C12345", text="Hello!", as_user=1)
71+
72+
def test_post_ephemeral(self):
73+
self.command.post_ephemeral(channel="C12345", text="Hello!", user="U67890")
74+
self.command.web.chat_postEphemeral.assert_called_once_with(
75+
channel="C12345", text="Hello!", user="U67890", as_user=True
76+
)
77+
78+
@patch(
79+
"slackbot.management.commands.run_bot.unicodedata.normalize",
80+
return_value="Hello bot!",
81+
)
82+
def test_handle_message_really_reacts_when_no_processors(self, mock_normalize):
83+
self.command.web.reactions_add = MagicMock()
84+
self.command.post_ephemeral = MagicMock()
85+
86+
payload = {
87+
"event": {
88+
"channel": "D12345",
89+
"user": "U67890",
90+
"text": "Hello!",
91+
"ts": "123456.789",
92+
"team": "T123",
93+
}
94+
}
95+
96+
self.command.handle_message_really(**payload)
97+
98+
self.command.web.reactions_add.assert_called_once_with(
99+
name="surface_not_found", channel="D12345", timestamp="123456.789"
100+
)
101+
self.command.post_ephemeral.assert_called_once()
102+
103+
104+
if __name__ == "__main__":
105+
unittest.main()

0 commit comments

Comments
 (0)