diff --git a/CHANGELOG.md b/CHANGELOG.md index de1399d9a..29bd7b02a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ C interface is not changed. Rust and JSON-RPC API have `flags` integer argument replaced with two boolean flags `info_only` and `add_daymarker`. +- jsonrpc: add API to check if the message is sent by a bot #3877 ## 1.107.1 diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index 0f35f0c6f..9030467f6 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -48,6 +48,10 @@ pub struct MessageObject { is_setupmessage: bool, is_info: bool, is_forwarded: bool, + + /// True if the message was sent by a bot. + is_bot: bool, + /// when is_info is true this describes what type of system message it is system_message_type: SystemMessageType, @@ -182,6 +186,7 @@ impl MessageObject { is_setupmessage: message.is_setupmessage(), is_info: message.is_info(), is_forwarded: message.is_forwarded(), + is_bot: message.is_bot(), system_message_type: message.get_info_type().into(), duration: message.get_duration(), diff --git a/deltachat-rpc-client/examples/echobot_no_hooks.py b/deltachat-rpc-client/examples/echobot_no_hooks.py index bf4f6ce9e..fadf2e560 100644 --- a/deltachat-rpc-client/examples/echobot_no_hooks.py +++ b/deltachat-rpc-client/examples/echobot_no_hooks.py @@ -32,7 +32,7 @@ async def main(): async def process_messages(): for message in await account.get_fresh_messages_in_arrival_order(): snapshot = await message.get_snapshot() - if not snapshot.is_info: + if not snapshot.is_bot and not snapshot.is_info: await snapshot.chat.send_text(snapshot.text) await snapshot.message.mark_seen() diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/events.py b/deltachat-rpc-client/src/deltachat_rpc_client/events.py index 3801dcc72..0e160445c 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/events.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/events.py @@ -96,6 +96,9 @@ class NewMessage(EventFilter): content. :param command: If set, only match messages with the given command (ex. /help). Setting this property implies `is_info==False`. + :param is_bot: If set to True only match messages sent by bots, if set to None + match messages from bots and users. If omitted or set to False + only messages from users will be matched. :param is_info: If set to True only match info/system messages, if set to False only match messages that are not info/system messages. If omitted info/system messages as well as normal messages will be matched. @@ -113,10 +116,12 @@ class NewMessage(EventFilter): re.Pattern, ] = None, command: Optional[str] = None, + is_bot: Optional[bool] = False, is_info: Optional[bool] = None, func: Optional[Callable[[AttrDict], bool]] = None, ) -> None: super().__init__(func=func) + self.is_bot = is_bot self.is_info = is_info if command is not None and not isinstance(command, str): raise TypeError("Invalid command") @@ -133,19 +138,28 @@ class NewMessage(EventFilter): raise TypeError("Invalid pattern type") def __hash__(self) -> int: - return hash((self.pattern, self.func)) + return hash((self.pattern, self.command, self.is_bot, self.is_info, self.func)) def __eq__(self, other) -> bool: if isinstance(other, NewMessage): - return (self.pattern, self.command, self.is_info, self.func) == ( + return ( + self.pattern, + self.command, + self.is_bot, + self.is_info, + self.func, + ) == ( other.pattern, other.command, + other.is_bot, other.is_info, other.func, ) return False async def filter(self, event: AttrDict) -> bool: + if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot: + return False if self.is_info is not None and self.is_info != event.message_snapshot.is_info: return False if self.command and self.command != event.command: diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index fdf9b3c17..83a783f67 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -216,6 +216,7 @@ async def test_message(acfactory) -> None: snapshot = await message.get_snapshot() assert snapshot.chat_id == chat_id assert snapshot.text == "Hello!" + assert not snapshot.is_bot assert repr(message) with pytest.raises(JsonRpcError): # chat is not accepted @@ -227,18 +228,46 @@ async def test_message(acfactory) -> None: await message.send_reaction("😎") +@pytest.mark.asyncio() +async def test_is_bot(acfactory) -> None: + """Test that we can recognize messages submitted by bots.""" + alice, bob = await acfactory.get_online_accounts(2) + + bob_addr = await bob.get_config("addr") + alice_contact_bob = await alice.create_contact(bob_addr, "Bob") + alice_chat_bob = await alice_contact_bob.create_chat() + + # Alice becomes a bot. + await alice.set_config("bot", "1") + await alice_chat_bob.send_text("Hello!") + + while True: + event = await bob.wait_for_event() + if event.type == EventType.INCOMING_MSG: + msg_id = event.msg_id + message = bob.get_message_by_id(msg_id) + snapshot = await message.get_snapshot() + assert snapshot.chat_id == event.chat_id + assert snapshot.text == "Hello!" + assert snapshot.is_bot + break + + @pytest.mark.asyncio() async def test_bot(acfactory) -> None: mock = MagicMock() user = (await acfactory.get_online_accounts(1))[0] bot = await acfactory.new_configured_bot() + bot2 = await acfactory.new_configured_bot() assert await bot.is_configured() assert await bot.account.get_config("bot") == "1" - hook = lambda e: mock.hook(e.msg_id), events.RawEvent(EventType.INCOMING_MSG) + hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG) bot.add_hook(*hook) event = await acfactory.process_message(from_account=user, to_client=bot, text="Hello!") + snapshot = await bot.account.get_message_by_id(event.msg_id).get_snapshot() + assert not snapshot.is_bot mock.hook.assert_called_once_with(event.msg_id) bot.remove_hook(*hook) @@ -253,6 +282,8 @@ async def test_bot(acfactory) -> None: mock.hook.assert_called_with(event.msg_id) event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!") mock.hook.assert_called_with(event.msg_id) + await acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello") + assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots await acfactory.process_message(from_account=user, to_client=bot, text="hey!") assert len(mock.hook.mock_calls) == 2 bot.remove_hook(*hook)