mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 21:46:35 +03:00
Merge pull request #3835 from deltachat/adb/rpc-client-better-hooks
improve deltachat-rpc-client lib (part #2)
This commit is contained in:
@@ -17,8 +17,9 @@ async def log_event(event):
|
||||
|
||||
|
||||
@hooks.on(events.NewMessage)
|
||||
async def echo(msg):
|
||||
await msg.chat.send_text(msg.text)
|
||||
async def echo(event):
|
||||
snapshot = event.message_snapshot
|
||||
await snapshot.chat.send_text(snapshot.text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -25,14 +25,34 @@ async def log_error(event):
|
||||
logging.error(event.msg)
|
||||
|
||||
|
||||
@hooks.on(events.NewMessage(r".+", func=lambda msg: not msg.text.startswith("/")))
|
||||
async def echo(msg):
|
||||
await msg.chat.send_text(msg.text)
|
||||
@hooks.on(events.MemberListChanged)
|
||||
async def on_memberlist_changed(event):
|
||||
logging.info(
|
||||
"member %s was %s", event.member, "added" if event.member_added else "removed"
|
||||
)
|
||||
|
||||
|
||||
@hooks.on(events.NewMessage(r"/help"))
|
||||
async def help_command(msg):
|
||||
await msg.chat.send_text("Send me any text message and I will echo it back")
|
||||
@hooks.on(events.GroupImageChanged)
|
||||
async def on_group_image_changed(event):
|
||||
logging.info("group image %s", "deleted" if event.image_deleted else "changed")
|
||||
|
||||
|
||||
@hooks.on(events.GroupNameChanged)
|
||||
async def on_group_name_changed(event):
|
||||
logging.info("group name changed, old name: %s", event.old_name)
|
||||
|
||||
|
||||
@hooks.on(events.NewMessage(func=lambda e: not e.command))
|
||||
async def echo(event):
|
||||
snapshot = event.message_snapshot
|
||||
if snapshot.text or snapshot.file:
|
||||
await snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
|
||||
|
||||
|
||||
@hooks.on(events.NewMessage(command="/help"))
|
||||
async def help_command(event):
|
||||
snapshot = event.message_snapshot
|
||||
await snapshot.chat.send_text("Send me any message and I will echo it back")
|
||||
|
||||
|
||||
async def main():
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Delta Chat asynchronous high-level API"""
|
||||
from ._utils import AttrDict, run_bot_cli, run_client_cli
|
||||
from .account import Account
|
||||
from .chat import Chat
|
||||
from .client import Bot, Client
|
||||
@@ -7,4 +8,3 @@ from .contact import Contact
|
||||
from .deltachat import DeltaChat
|
||||
from .message import Message
|
||||
from .rpc import Rpc
|
||||
from .utils import AttrDict, run_bot_cli, run_client_cli
|
||||
|
||||
@@ -17,6 +17,8 @@ def _camel_to_snake(name: str) -> str:
|
||||
|
||||
|
||||
def _to_attrdict(obj):
|
||||
if isinstance(obj, AttrDict):
|
||||
return obj
|
||||
if isinstance(obj, dict):
|
||||
return AttrDict(obj)
|
||||
if isinstance(obj, list):
|
||||
@@ -112,3 +114,64 @@ async def _run_cli(
|
||||
client.configure(email=args.email, password=args.password)
|
||||
)
|
||||
await client.run_forever()
|
||||
|
||||
|
||||
def extract_addr(text: str) -> str:
|
||||
"""extract email address from the given text."""
|
||||
match = re.match(r".*\((.+@.+)\)", text)
|
||||
if match:
|
||||
text = match.group(1)
|
||||
text = text.rstrip(".")
|
||||
return text.strip()
|
||||
|
||||
|
||||
def parse_system_image_changed(text: str) -> Optional[Tuple[str, bool]]:
|
||||
"""return image changed/deleted info from parsing the given system message text."""
|
||||
text = text.lower()
|
||||
match = re.match(r"group image (changed|deleted) by (.+).", text)
|
||||
if match:
|
||||
action, actor = match.groups()
|
||||
return (extract_addr(actor), action == "deleted")
|
||||
return None
|
||||
|
||||
|
||||
def parse_system_title_changed(text: str) -> Optional[Tuple[str, str]]:
|
||||
text = text.lower()
|
||||
match = re.match(r'group name changed from "(.+)" to ".+" by (.+).', text)
|
||||
if match:
|
||||
old_title, actor = match.groups()
|
||||
return (extract_addr(actor), old_title)
|
||||
return None
|
||||
|
||||
|
||||
def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
|
||||
"""return add/remove info from parsing the given system message text.
|
||||
|
||||
returns a (action, affected, actor) tuple.
|
||||
"""
|
||||
# You removed member a@b.
|
||||
# You added member a@b.
|
||||
# Member Me (x@y) removed by a@b.
|
||||
# Member x@y added by a@b
|
||||
# Member With space (tmp1@x.org) removed by tmp2@x.org.
|
||||
# Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
|
||||
# Group left by some one (tmp1@x.org).
|
||||
# Group left by tmp1@x.org.
|
||||
text = text.lower()
|
||||
|
||||
match = re.match(r"member (.+) (removed|added) by (.+)", text)
|
||||
if match:
|
||||
affected, action, actor = match.groups()
|
||||
return action, extract_addr(affected), extract_addr(actor)
|
||||
|
||||
match = re.match(r"you (removed|added) member (.+)", text)
|
||||
if match:
|
||||
action, affected = match.groups()
|
||||
return action, extract_addr(affected), "me"
|
||||
|
||||
if text.startswith("group left by "):
|
||||
addr = extract_addr(text[13:])
|
||||
if addr:
|
||||
return "removed", addr, addr
|
||||
|
||||
return None
|
||||
@@ -1,11 +1,11 @@
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .chat import Chat
|
||||
from .const import ChatlistFlag, ContactFlag, SpecialContactId
|
||||
from .contact import Contact
|
||||
from .message import Message
|
||||
from .rpc import Rpc
|
||||
from .utils import AttrDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .deltachat import DeltaChat
|
||||
|
||||
@@ -2,11 +2,11 @@ import calendar
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .const import ChatVisibility
|
||||
from .contact import Contact
|
||||
from .message import Message
|
||||
from .rpc import Rpc
|
||||
from .utils import AttrDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .account import Account
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
"""Event loop implementations offering high level event handling/hooking."""
|
||||
import inspect
|
||||
import logging
|
||||
from typing import Callable, Dict, Iterable, Optional, Set, Tuple, Type, Union
|
||||
from typing import (
|
||||
Callable,
|
||||
Coroutine,
|
||||
Dict,
|
||||
Iterable,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
from deltachat_rpc_client.account import Account
|
||||
|
||||
from .const import EventType
|
||||
from .events import EventFilter, NewInfoMessage, NewMessage, RawEvent
|
||||
from .utils import AttrDict
|
||||
from ._utils import (
|
||||
AttrDict,
|
||||
parse_system_add_remove,
|
||||
parse_system_image_changed,
|
||||
parse_system_title_changed,
|
||||
)
|
||||
from .const import COMMAND_PREFIX, EventType, SystemMessageType
|
||||
from .events import (
|
||||
EventFilter,
|
||||
GroupImageChanged,
|
||||
GroupNameChanged,
|
||||
MemberListChanged,
|
||||
NewMessage,
|
||||
RawEvent,
|
||||
)
|
||||
|
||||
|
||||
class Client:
|
||||
@@ -21,6 +44,7 @@ class Client:
|
||||
self.account = account
|
||||
self.logger = logger or logging
|
||||
self._hooks: Dict[type, Set[tuple]] = {}
|
||||
self._should_process_messages = 0
|
||||
self.add_hooks(hooks or [])
|
||||
|
||||
def add_hooks(
|
||||
@@ -36,12 +60,24 @@ class Client:
|
||||
if isinstance(event, type):
|
||||
event = event()
|
||||
assert isinstance(event, EventFilter)
|
||||
self._should_process_messages += int(
|
||||
isinstance(
|
||||
event,
|
||||
(NewMessage, MemberListChanged, GroupImageChanged, GroupNameChanged),
|
||||
)
|
||||
)
|
||||
self._hooks.setdefault(type(event), set()).add((hook, event))
|
||||
|
||||
def remove_hook(self, hook: Callable, event: Union[type, EventFilter]) -> None:
|
||||
"""Unregister hook from the given event filter."""
|
||||
if isinstance(event, type):
|
||||
event = event()
|
||||
self._should_process_messages -= int(
|
||||
isinstance(
|
||||
event,
|
||||
(NewMessage, MemberListChanged, GroupImageChanged, GroupNameChanged),
|
||||
)
|
||||
)
|
||||
self._hooks.get(type(event), set()).remove((hook, event))
|
||||
|
||||
async def is_configured(self) -> bool:
|
||||
@@ -56,6 +92,18 @@ class Client:
|
||||
self.logger.debug("Account configured")
|
||||
|
||||
async def run_forever(self) -> None:
|
||||
"""Process events forever."""
|
||||
await self.run_until(lambda _: False)
|
||||
|
||||
async def run_until(
|
||||
self, func: Callable[[AttrDict], Union[bool, Coroutine]]
|
||||
) -> AttrDict:
|
||||
"""Process events until the given callable evaluates to True.
|
||||
|
||||
The callable should accept an AttrDict object representing the
|
||||
last processed event. The event is returned when the callable
|
||||
evaluates to True.
|
||||
"""
|
||||
self.logger.debug("Listening to incoming events...")
|
||||
if await self.is_configured():
|
||||
await self.account.start_io()
|
||||
@@ -68,6 +116,12 @@ class Client:
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
await self._process_messages()
|
||||
|
||||
stop = func(event)
|
||||
if inspect.isawaitable(stop):
|
||||
stop = await stop
|
||||
if stop:
|
||||
return event
|
||||
|
||||
async def _on_event(
|
||||
self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent
|
||||
) -> None:
|
||||
@@ -78,17 +132,82 @@ class Client:
|
||||
except Exception as ex:
|
||||
self.logger.exception(ex)
|
||||
|
||||
def _should_process_messages(self) -> bool:
|
||||
return any(issubclass(filter_type, NewMessage) for filter_type in self._hooks)
|
||||
async def _parse_command(self, event: AttrDict) -> None:
|
||||
cmds = [
|
||||
hook[1].command
|
||||
for hook in self._hooks.get(NewMessage, [])
|
||||
if hook[1].command
|
||||
]
|
||||
parts = event.message_snapshot.text.split(maxsplit=1)
|
||||
payload = parts[1] if len(parts) > 1 else ""
|
||||
cmd = parts.pop(0)
|
||||
|
||||
if "@" in cmd:
|
||||
suffix = "@" + (await self.account.self_contact.get_snapshot()).address
|
||||
if cmd.endswith(suffix):
|
||||
cmd = cmd[: -len(suffix)]
|
||||
else:
|
||||
return
|
||||
|
||||
parts = cmd.split("_")
|
||||
_payload = payload
|
||||
while parts:
|
||||
_cmd = "_".join(parts)
|
||||
if _cmd in cmds:
|
||||
break
|
||||
_payload = (parts.pop() + " " + _payload).rstrip()
|
||||
|
||||
if parts:
|
||||
cmd = _cmd
|
||||
payload = _payload
|
||||
|
||||
event["command"], event["payload"] = cmd, payload
|
||||
|
||||
async def _on_new_msg(self, snapshot: AttrDict) -> None:
|
||||
event = AttrDict(command="", payload="", message_snapshot=snapshot)
|
||||
if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX):
|
||||
await self._parse_command(event)
|
||||
await self._on_event(event, NewMessage)
|
||||
|
||||
async def _handle_info_msg(self, snapshot: AttrDict) -> None:
|
||||
event = AttrDict(message_snapshot=snapshot)
|
||||
|
||||
img_changed = parse_system_image_changed(snapshot.text)
|
||||
if img_changed:
|
||||
_, event["image_deleted"] = img_changed
|
||||
await self._on_event(event, GroupImageChanged)
|
||||
return
|
||||
|
||||
title_changed = parse_system_title_changed(snapshot.text)
|
||||
if title_changed:
|
||||
_, event["old_name"] = title_changed
|
||||
await self._on_event(event, GroupNameChanged)
|
||||
return
|
||||
|
||||
members_changed = parse_system_add_remove(snapshot.text)
|
||||
if members_changed:
|
||||
action, event["member"], _ = members_changed
|
||||
event["member_added"] = action == "added"
|
||||
await self._on_event(event, MemberListChanged)
|
||||
return
|
||||
|
||||
self.logger.warning(
|
||||
"ignoring unsupported system message id=%s text=%s",
|
||||
snapshot.id,
|
||||
snapshot.text,
|
||||
)
|
||||
|
||||
async def _process_messages(self) -> None:
|
||||
if self._should_process_messages():
|
||||
if self._should_process_messages:
|
||||
for message in await self.account.get_fresh_messages_in_arrival_order():
|
||||
snapshot = await message.get_snapshot()
|
||||
if snapshot.is_info:
|
||||
await self._on_event(snapshot, NewInfoMessage)
|
||||
else:
|
||||
await self._on_event(snapshot, NewMessage)
|
||||
await self._on_new_msg(snapshot)
|
||||
if (
|
||||
snapshot.is_info
|
||||
and snapshot.system_message_type
|
||||
!= SystemMessageType.WEBXDC_INFO_MESSAGE
|
||||
):
|
||||
await self._handle_info_msg(snapshot)
|
||||
await snapshot.message.mark_seen()
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from enum import Enum, IntEnum
|
||||
|
||||
COMMAND_PREFIX = "/"
|
||||
|
||||
|
||||
class ContactFlag(IntEnum):
|
||||
VERIFIED_ONLY = 0x01
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .rpc import Rpc
|
||||
from .utils import AttrDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .account import Account
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from typing import Dict, List
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .account import Account
|
||||
from .rpc import Rpc
|
||||
from .utils import AttrDict
|
||||
|
||||
|
||||
class DeltaChat:
|
||||
|
||||
@@ -4,8 +4,8 @@ import re
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Callable, Iterable, Iterator, Optional, Set, Tuple, Union
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .const import EventType
|
||||
from .utils import AttrDict
|
||||
|
||||
|
||||
def _tuple_of(obj, type_: type) -> tuple:
|
||||
@@ -91,8 +91,19 @@ class RawEvent(EventFilter):
|
||||
class NewMessage(EventFilter):
|
||||
"""Matches whenever a new message arrives.
|
||||
|
||||
Warning: registering a handler for this event or any subclass will cause the messages
|
||||
Warning: registering a handler for this event will cause the messages
|
||||
to be marked as read. Its usage is mainly intended for bots.
|
||||
|
||||
:param pattern: if set, this Pattern will be used to filter the message by its text
|
||||
content.
|
||||
:param command: If set, only match messages with the given command (ex. /help).
|
||||
Setting this property implies `is_info==False`.
|
||||
: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.
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -103,9 +114,17 @@ class NewMessage(EventFilter):
|
||||
Callable[[str], bool],
|
||||
re.Pattern,
|
||||
] = None,
|
||||
command: Optional[str] = None,
|
||||
is_info: Optional[bool] = None,
|
||||
func: Optional[Callable[[AttrDict], bool]] = None,
|
||||
) -> None:
|
||||
super().__init__(func=func)
|
||||
self.is_info = is_info
|
||||
if command is not None and not isinstance(command, str):
|
||||
raise TypeError("Invalid command")
|
||||
self.command = command
|
||||
if self.is_info and self.command:
|
||||
raise AttributeError("Can not use command and is_info at the same time.")
|
||||
if isinstance(pattern, str):
|
||||
pattern = re.compile(pattern)
|
||||
if isinstance(pattern, re.Pattern):
|
||||
@@ -119,13 +138,22 @@ class NewMessage(EventFilter):
|
||||
return hash((self.pattern, self.func))
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if type(other) is self.__class__: # noqa
|
||||
return (self.pattern, self.func) == (other.pattern, other.func)
|
||||
if isinstance(other, NewMessage):
|
||||
return (self.pattern, self.command, self.is_info, self.func) == (
|
||||
other.pattern,
|
||||
other.command,
|
||||
other.is_info,
|
||||
other.func,
|
||||
)
|
||||
return False
|
||||
|
||||
async def filter(self, event: AttrDict) -> bool:
|
||||
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:
|
||||
return False
|
||||
if self.pattern:
|
||||
match = self.pattern(event.text)
|
||||
match = self.pattern(event.message_snapshot.text)
|
||||
if inspect.isawaitable(match):
|
||||
match = await match
|
||||
if not match:
|
||||
@@ -133,8 +161,91 @@ class NewMessage(EventFilter):
|
||||
return await super()._call_func(event)
|
||||
|
||||
|
||||
class NewInfoMessage(NewMessage):
|
||||
"""Matches whenever a new info/system message arrives."""
|
||||
class MemberListChanged(EventFilter):
|
||||
"""Matches when a group member is added or removed.
|
||||
|
||||
Warning: registering a handler for this event will cause the messages
|
||||
to be marked as read. Its usage is mainly intended for bots.
|
||||
|
||||
:param added: If set to True only match if a member was added, if set to False
|
||||
only match if a member was removed. If omitted both, member additions
|
||||
and removals, will be matched.
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
|
||||
def __init__(self, added: Optional[bool] = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.added = added
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.added, self.func))
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if isinstance(other, MemberListChanged):
|
||||
return (self.added, self.func) == (other.added, other.func)
|
||||
return False
|
||||
|
||||
async def filter(self, event: AttrDict) -> bool:
|
||||
if self.added is not None and self.added != event.member_added:
|
||||
return False
|
||||
return await self._call_func(event)
|
||||
|
||||
|
||||
class GroupImageChanged(EventFilter):
|
||||
"""Matches when the group image is changed.
|
||||
|
||||
Warning: registering a handler for this event will cause the messages
|
||||
to be marked as read. Its usage is mainly intended for bots.
|
||||
|
||||
:param deleted: If set to True only match if the image was deleted, if set to False
|
||||
only match if a new image was set. If omitted both, image changes and
|
||||
removals, will be matched.
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
|
||||
def __init__(self, deleted: Optional[bool] = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.deleted = deleted
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.deleted, self.func))
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if isinstance(other, GroupImageChanged):
|
||||
return (self.deleted, self.func) == (other.deleted, other.func)
|
||||
return False
|
||||
|
||||
async def filter(self, event: AttrDict) -> bool:
|
||||
if self.deleted is not None and self.deleted != event.image_deleted:
|
||||
return False
|
||||
return await self._call_func(event)
|
||||
|
||||
|
||||
class GroupNameChanged(EventFilter):
|
||||
"""Matches when the group name is changed.
|
||||
|
||||
Warning: registering a handler for this event will cause the messages
|
||||
to be marked as read. Its usage is mainly intended for bots.
|
||||
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((GroupNameChanged, self.func))
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if isinstance(other, GroupNameChanged):
|
||||
return self.func == other.func
|
||||
return False
|
||||
|
||||
async def filter(self, event: AttrDict) -> bool:
|
||||
return await self._call_func(event)
|
||||
|
||||
|
||||
class HookCollection:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .contact import Contact
|
||||
from .rpc import Rpc
|
||||
from .utils import AttrDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .account import Account
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import json
|
||||
import os
|
||||
from typing import AsyncGenerator, List
|
||||
from typing import AsyncGenerator, List, Optional
|
||||
|
||||
import aiohttp
|
||||
import pytest_asyncio
|
||||
|
||||
from .account import Account
|
||||
from .client import Bot
|
||||
from .deltachat import DeltaChat
|
||||
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
|
||||
from .rpc import Rpc
|
||||
|
||||
|
||||
@@ -51,6 +49,44 @@ class ACFactory:
|
||||
await account.start_io()
|
||||
return accounts
|
||||
|
||||
async def send_message(
|
||||
self,
|
||||
to_account: Account,
|
||||
from_account: Optional[Account] = None,
|
||||
text: Optional[str] = None,
|
||||
file: Optional[str] = None,
|
||||
group: Optional[str] = None,
|
||||
) -> Message:
|
||||
if not from_account:
|
||||
from_account = (await self.get_online_accounts(1))[0]
|
||||
to_contact = await from_account.create_contact(
|
||||
await to_account.get_config("addr")
|
||||
)
|
||||
if group:
|
||||
to_chat = await from_account.create_group(group)
|
||||
await to_chat.add_contact(to_contact)
|
||||
else:
|
||||
to_chat = await to_contact.create_chat()
|
||||
return await to_chat.send_message(text=text, file=file)
|
||||
|
||||
async def process_message(
|
||||
self,
|
||||
to_client: Client,
|
||||
from_account: Optional[Account] = None,
|
||||
text: Optional[str] = None,
|
||||
file: Optional[str] = None,
|
||||
group: Optional[str] = None,
|
||||
) -> AttrDict:
|
||||
await self.send_message(
|
||||
to_account=to_client.account,
|
||||
from_account=from_account,
|
||||
text=text,
|
||||
file=file,
|
||||
group=group,
|
||||
)
|
||||
|
||||
return await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def rpc(tmp_path) -> AsyncGenerator:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import AttrDict, EventType, events
|
||||
from deltachat_rpc_client import EventType, events
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@@ -216,31 +218,42 @@ async def test_message(acfactory) -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bot(acfactory) -> None:
|
||||
async def callback(e):
|
||||
res.append(e)
|
||||
|
||||
res = []
|
||||
mock = MagicMock()
|
||||
user = (await acfactory.get_online_accounts(1))[0]
|
||||
bot = await acfactory.new_configured_bot()
|
||||
|
||||
assert await bot.is_configured()
|
||||
assert await bot.account.get_config("bot") == "1"
|
||||
|
||||
bot.add_hook(callback, events.RawEvent(EventType.INFO))
|
||||
info_event = AttrDict(account=bot.account, type=EventType.INFO, msg="info")
|
||||
warn_event = AttrDict(account=bot.account, type=EventType.WARNING, msg="warning")
|
||||
await bot._on_event(info_event)
|
||||
await bot._on_event(warn_event)
|
||||
assert info_event in res
|
||||
assert warn_event not in res
|
||||
assert len(res) == 1
|
||||
hook = lambda e: mock.hook(e.msg_id), events.RawEvent(EventType.INCOMING_MSG)
|
||||
bot.add_hook(*hook)
|
||||
event = await acfactory.process_message(
|
||||
from_account=user, to_client=bot, text="Hello!"
|
||||
)
|
||||
mock.hook.assert_called_once_with(event.msg_id)
|
||||
bot.remove_hook(*hook)
|
||||
|
||||
res = []
|
||||
bot.add_hook(callback, events.NewMessage(r"hello"))
|
||||
snapshot1 = AttrDict(text="hello")
|
||||
snapshot2 = AttrDict(text="hello, world")
|
||||
snapshot3 = AttrDict(text="hey!")
|
||||
for snapshot in [snapshot1, snapshot2, snapshot3]:
|
||||
await bot._on_event(snapshot, events.NewMessage)
|
||||
assert len(res) == 2
|
||||
assert snapshot1 in res
|
||||
assert snapshot2 in res
|
||||
assert snapshot3 not in res
|
||||
track = lambda e: mock.hook(e.message_snapshot.id)
|
||||
|
||||
mock.hook.reset_mock()
|
||||
hook = track, events.NewMessage(r"hello")
|
||||
bot.add_hook(*hook)
|
||||
bot.add_hook(track, events.NewMessage(command="/help"))
|
||||
event = await acfactory.process_message(
|
||||
from_account=user, to_client=bot, text="hello"
|
||||
)
|
||||
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=user, to_client=bot, text="hey!")
|
||||
assert len(mock.hook.mock_calls) == 2
|
||||
bot.remove_hook(*hook)
|
||||
|
||||
mock.hook.reset_mock()
|
||||
await acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
event = await acfactory.process_message(
|
||||
from_account=user, to_client=bot, text="/help"
|
||||
)
|
||||
mock.hook.assert_called_once_with(event.msg_id)
|
||||
|
||||
Reference in New Issue
Block a user