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 @@ def post_ephemeral(self, **kwargs):
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

return self.handle_reaction(reaction_event)

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

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 @@ def handle_message(self, **payload):
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))
return processed_at_least_one

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

Expand Down Expand Up @@ -80,8 +97,8 @@ def handle_message_really(self, **payload):
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))

# If private DM
if channel[0] == "D":
Expand Down Expand Up @@ -118,9 +135,19 @@ def process(self, client: SocketModeClient, req: SocketModeRequest):
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)

def handle(self, *args, **options):
# faster cold boot
from slack_sdk.web.client import WebClient
Expand All @@ -133,6 +160,7 @@ def handle(self, *args, **options):
)
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()