Skip to content

Commit

Permalink
Add regex_triggered decorator for matching regular expressions (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
grantbacon authored Jan 16, 2025
1 parent 7b10bb7 commit 1a58853
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 2 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ To implement your own commands, you need to inherent `Command` and overwrite fol

- `setup(self)`: Start any task that requires to send messages already, optional
- `describe(self)`: String to describe your command, optional
- `handle(self, c: Context)`: Handle an incoming message. By default, any command will read any incoming message. `Context` can be used to easily send (`c.send(text)`), reply (`c.reply(text)`), react (`c.react(emoji)`) and to type in a group (`c.start_typing()` and `c.stop_typing()`). You can use the `@triggered` decorator to listen for specific commands or you can inspect `c.message.text`.
- `handle(self, c: Context)`: Handle an incoming message. By default, any command will read any incoming message. `Context` can be used to easily send (`c.send(text)`), reply (`c.reply(text)`), react (`c.react(emoji)`) and to type in a group (`c.start_typing()` and `c.stop_typing()`). You can use the `@triggered` decorator to listen for specific commands, the `@regex_triggered` decorator to listen for regular expressions, or you can inspect `c.message.text`.

### Unit Testing

Expand Down
3 changes: 3 additions & 0 deletions example/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
TypingCommand,
TriggeredCommand,
ReplyCommand,
RegexTriggeredCommand,
)
import logging

Expand Down Expand Up @@ -37,6 +38,8 @@ def main():
# chat command is enabled for all groups and one specific contact
bot.register(TriggeredCommand(), contacts=["+490123456789"], groups=True)

bot.register(RegexTriggeredCommand())

bot.start()


Expand Down
10 changes: 10 additions & 0 deletions example/commands/regex_triggered.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from signalbot import Command, Context, regex_triggered


class RegexTriggeredCommand(Command):
def describe(self) -> str:
return "😤 RegexTriggered Command: Regular Expression Decorator Example"

@regex_triggered(r"^[a-f0-9]{40}$", r"^[a-f0-9]{32}$")
async def handle(self, c: Context):
await c.send("I am triggered by regular expressions")
3 changes: 2 additions & 1 deletion signalbot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .bot import SignalBot
from .command import Command, CommandError, triggered
from .command import Command, CommandError, triggered, regex_triggered
from .message import Message, MessageType, UnknownMessageFormatError
from .api import SignalAPI, ReceiveMessagesError, SendMessageError
from .context import Context
Expand All @@ -8,6 +8,7 @@
"SignalBot",
"Command",
"CommandError",
"regex_triggered",
"triggered",
"Message",
"MessageType",
Expand Down
19 changes: 19 additions & 0 deletions signalbot/command.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import functools
import re
from abc import ABC, abstractmethod

from .message import Message
from .context import Context


def regex_triggered(*by: str | re.Pattern[str]):
def decorator_regex_triggered(func):
@functools.wraps(func)
async def wrapper_regex_triggered(*args, **kwargs):
c = args[1]
text = c.message.text
if not isinstance(text, str):
return
matches = [bool(re.search(pattern, text)) for pattern in by]
if True not in matches:
return
return await func(*args, **kwargs)

return wrapper_regex_triggered

return decorator_regex_triggered


def triggered(*by, case_sensitive=False):
def decorator_triggered(func):
@functools.wraps(func)
Expand Down
39 changes: 39 additions & 0 deletions tests/test_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from unittest.mock import patch
import logging
from signalbot import Command, Context, triggered
from signalbot.command import regex_triggered
from signalbot.utils import ChatTestCase, SendMessagesMock, ReceiveMessagesMock


Expand All @@ -23,6 +24,15 @@ async def handle(self, c: Context):
await c.send("I am triggered")


class RegexTriggeredCommand(Command):
def describe(self) -> str:
return "😤 Triggered Command: Regular Expression Decorator Example"

@regex_triggered(r"\w+@\w+\.\w+", r"\d{3}-\d{3}-\d{4}")
async def handle(self, c: Context):
await c.send("I am triggered by regular expressions")


class TriggeredTest(ChatTestCase):
def setUp(self):
super().setUp()
Expand Down Expand Up @@ -74,6 +84,35 @@ async def test_not_triggered(self, receive_mock, send_mock):
self.assertEqual(send_mock.call_count, 0)


class RegexTriggeredTest(ChatTestCase):
def setUp(self):
super().setUp()
group = {"id": "asdf", "name": "Test"}
self.signal_bot._groups_by_internal_id = {"group_id1=": group}
self.signal_bot.register(RegexTriggeredCommand())

@patch("signalbot.SignalAPI.send", new_callable=SendMessagesMock)
@patch("signalbot.SignalAPI.receive", new_callable=ReceiveMessagesMock)
async def test_regex_triggered(self, receive_mock, send_mock):
receive_mock.define(["[email protected]"])
await self.run_bot()
self.assertEqual(send_mock.call_count, 1)

@patch("signalbot.SignalAPI.send", new_callable=SendMessagesMock)
@patch("signalbot.SignalAPI.receive", new_callable=ReceiveMessagesMock)
async def test_regex_triggered(self, receive_mock, send_mock):
receive_mock.define(["123-555-1234"])
await self.run_bot()
self.assertEqual(send_mock.call_count, 1)

@patch("signalbot.SignalAPI.send", new_callable=SendMessagesMock)
@patch("signalbot.SignalAPI.receive", new_callable=ReceiveMessagesMock)
async def test_not_regex_triggered(self, receive_mock, send_mock):
receive_mock.define(["11-222"])
await self.run_bot()
self.assertEqual(send_mock.call_count, 0)


if __name__ == "__main__":
logging.basicConfig(level="INFO")
unittest.main()

0 comments on commit 1a58853

Please sign in to comment.