Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion slackbot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from functools import lru_cache

__version__ = "0.1.6"
__version__ = "0.1.7"


@lru_cache
Expand Down
19 changes: 19 additions & 0 deletions slackbot/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@
kwargs["as_user"] = kwargs.get("as_user", 1)
return self.web.chat_postEphemeral(**kwargs)

def process_reaction(self, reaction_event: dict) -> Optional[Union[int, tuple[int, int]]]:
"""
Handles reactions added or removed in Slack messages.

:param reaction_event: A dictionary containing reaction event data.
:return: None, self.STOP, self.PROCESSED, or tuple (self.PROCESSED, self.STOP)

PROCESSED if anything was done with input
STOP if no other processor should be called after this one
"""
if "reaction" not in reaction_event:
return # Ignore events that are not reactions

Check warning on line 41 in slackbot/base.py

View check run for this annotation

Codecov / codecov/patch

slackbot/base.py#L40-L41

Added lines #L40 - L41 were not covered by tests

return self.handle_reaction(reaction_event)

Check warning on line 43 in slackbot/base.py

View check run for this annotation

Codecov / codecov/patch

slackbot/base.py#L43

Added line #L43 was not covered by tests

def handle_reaction(self, reaction_event) -> Optional[Union[int, tuple[int, int]]]:
# This method is currently a placeholder.
pass

Check warning on line 47 in slackbot/base.py

View check run for this annotation

Codecov / codecov/patch

slackbot/base.py#L47

Added line #L47 was not covered by tests

def process(self, message, **kw) -> Optional[Union[int, tuple[int, int]]]:
"""
:return: None, self.STOP, self.PROCESSED, or tuple PROCESSED,STOP
Expand Down
34 changes: 31 additions & 3 deletions slackbot/management/commands/run_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@
close_old_connections()
self.handle_message_really(**payload)

def handle_reaction(self, **payload):
event = payload.get("event", {})
processed_at_least_one = False
for p in self.processors:
try:
r = p.process_reaction(event)
if r:
if not isinstance(r, tuple):
r = (r,)
if MessageProcessor.PROCESSED in r:
processed_at_least_one = True
if MessageProcessor.STOP in r:
break
except Exception as e:
self.log_exception("Processor failed for reaction event: %s %s", event, str(e))

Check warning on line 57 in slackbot/management/commands/run_bot.py

View check run for this annotation

Codecov / codecov/patch

slackbot/management/commands/run_bot.py#L47-L57

Added lines #L47 - L57 were not covered by tests
return processed_at_least_one

def handle_message_really(self, **payload):
event = payload.get("event")

Expand Down Expand Up @@ -80,8 +97,8 @@
processed_at_least_one = True
if MessageProcessor.STOP in r:
break
except Exception as exc:
self.log_exception(f"Processor {str(p)} failed with {str(exc)} for message {message}")
except Exception as e:
self.log_exception("Processor failed for message: %s %s", message, str(e))

Check warning on line 101 in slackbot/management/commands/run_bot.py

View check run for this annotation

Codecov / codecov/patch

slackbot/management/commands/run_bot.py#L100-L101

Added lines #L100 - L101 were not covered by tests

# If private DM
if channel[0] == "D":
Expand Down Expand Up @@ -118,9 +135,19 @@
response = SocketModeResponse(envelope_id=req.envelope_id)
client.send_socket_mode_response(response)

if req.payload["event"]["type"] == "message" and req.payload["event"].get("subtype") is None:
event = req.payload["event"]

if event["type"] == "message" and event.get("subtype") is None:
return self.handle_message(**req.payload)

def process_reaction(self, client: SocketModeClient, req: SocketModeRequest):
if req.type == "events_api":
response = SocketModeResponse(envelope_id=req.envelope_id)
client.send_socket_mode_response(response)
event = req.payload["event"]
if event["type"] in ("reaction_added", "reaction_removed"):
return self.handle_reaction(**req.payload)

Check warning on line 149 in slackbot/management/commands/run_bot.py

View check run for this annotation

Codecov / codecov/patch

slackbot/management/commands/run_bot.py#L144-L149

Added lines #L144 - L149 were not covered by tests

def handle(self, *args, **options):
# faster cold boot
from slack_sdk.web.client import WebClient
Expand All @@ -133,6 +160,7 @@
)
self.set_up()
self.client.socket_mode_request_listeners.append(self.process)
self.client.socket_mode_request_listeners.append(self.process_reaction)
self.stdout.write("Connecting...\n")
self.client.connect()

Expand Down
105 changes: 105 additions & 0 deletions testapp/tests/test_run_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import unittest
from unittest.mock import patch, MagicMock
from slackbot.management.commands.run_bot import Command
from slack_sdk.socket_mode.request import SocketModeRequest


class TestSlackBotCommand(unittest.TestCase):
def setUp(self):
self.command = Command()
self.command.web = MagicMock()
self.command.client = MagicMock()
self.command.my_id = "U123456"
self.command.my_id_match = "<@U123456>"
self.command.processors = []

@patch("slackbot.management.commands.run_bot.close_old_connections")
def test_handle_message(self, mock_close_old_connections):
payload = {
"event": {
"channel": "C12345",
"user": "U67890",
"text": "Hello, bot!",
"ts": "123456.789",
}
}
result = self.command.handle_message(**payload)
self.assertFalse(result) # No processors, so should return False

@patch("threading.Event.wait", return_value=None)
@patch("slackbot.management.commands.run_bot.SocketModeClient")
@patch("slackbot.management.commands.run_bot.settings")
def test_handle(self, mock_settings, mock_socket_client, mock_event_wait):
mock_settings.SLACKBOT_BOT_TOKEN = "xoxb-123"
mock_settings.SLACKBOT_APP_TOKEN = "xapp-123"

mock_client_instance = mock_socket_client.return_value
self.command.set_up = MagicMock()
self.command.handle()

self.command.set_up.assert_called_once()
mock_client_instance.connect.assert_called_once()

def test_handle_reaction(self):
payload = {
"event": {
"type": "reaction_added",
"item": {"channel": "C12345"},
"user": "U67890",
"event_ts": "123456.789",
}
}
result = self.command.handle_reaction(**payload)
self.assertFalse(result) # No processors, so should return False

def test_process_event_message(self):
request = SocketModeRequest(
type="events_api",
envelope_id="envelope123",
payload={"event": {"type": "message", "subtype": None, "text": "Hello!"}},
)

self.command.handle_message = MagicMock(return_value=True)
result = self.command.process(self.command.client, request)

self.command.handle_message.assert_called_once()
self.assertTrue(result)

def test_post_message(self):
self.command.post_message(channel="C12345", text="Hello!")
self.command.web.chat_postMessage.assert_called_once_with(channel="C12345", text="Hello!", as_user=1)

def test_post_ephemeral(self):
self.command.post_ephemeral(channel="C12345", text="Hello!", user="U67890")
self.command.web.chat_postEphemeral.assert_called_once_with(
channel="C12345", text="Hello!", user="U67890", as_user=True
)

@patch(
"slackbot.management.commands.run_bot.unicodedata.normalize",
return_value="Hello bot!",
)
def test_handle_message_really_reacts_when_no_processors(self, mock_normalize):
self.command.web.reactions_add = MagicMock()
self.command.post_ephemeral = MagicMock()

payload = {
"event": {
"channel": "D12345",
"user": "U67890",
"text": "Hello!",
"ts": "123456.789",
"team": "T123",
}
}

self.command.handle_message_really(**payload)

self.command.web.reactions_add.assert_called_once_with(
name="surface_not_found", channel="D12345", timestamp="123456.789"
)
self.command.post_ephemeral.assert_called_once()


if __name__ == "__main__":
unittest.main()