python-trueconf-bot 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_trueconf_bot-1.0.0.dist-info/METADATA +115 -0
- python_trueconf_bot-1.0.0.dist-info/RECORD +120 -0
- python_trueconf_bot-1.0.0.dist-info/WHEEL +5 -0
- python_trueconf_bot-1.0.0.dist-info/licenses/LICENSE +32 -0
- python_trueconf_bot-1.0.0.dist-info/top_level.txt +1 -0
- trueconf/__init__.py +18 -0
- trueconf/_version.py +34 -0
- trueconf/client/__init__.py +0 -0
- trueconf/client/bot.py +1269 -0
- trueconf/client/context_controller.py +27 -0
- trueconf/client/session.py +60 -0
- trueconf/dispatcher/__init__.py +0 -0
- trueconf/dispatcher/dispatcher.py +56 -0
- trueconf/dispatcher/router.py +207 -0
- trueconf/enums/__init__.py +23 -0
- trueconf/enums/aouth_error.py +15 -0
- trueconf/enums/chat_participant_role.py +18 -0
- trueconf/enums/chat_type.py +17 -0
- trueconf/enums/envelope_author_type.py +13 -0
- trueconf/enums/file_ready_state.py +14 -0
- trueconf/enums/incoming_update_method.py +14 -0
- trueconf/enums/message_type.py +27 -0
- trueconf/enums/parse_mode.py +14 -0
- trueconf/enums/survey_type.py +6 -0
- trueconf/enums/update_type.py +14 -0
- trueconf/exceptions.py +17 -0
- trueconf/filters/__init__.py +11 -0
- trueconf/filters/base.py +8 -0
- trueconf/filters/command.py +32 -0
- trueconf/filters/instance_of.py +11 -0
- trueconf/filters/message.py +15 -0
- trueconf/filters/method.py +12 -0
- trueconf/loggers.py +4 -0
- trueconf/methods/__init__.py +59 -0
- trueconf/methods/add_participant_to_chat.py +22 -0
- trueconf/methods/auth.py +25 -0
- trueconf/methods/base.py +107 -0
- trueconf/methods/create_channel.py +20 -0
- trueconf/methods/create_group_chat.py +20 -0
- trueconf/methods/create_p2p_chat.py +20 -0
- trueconf/methods/edit_message.py +25 -0
- trueconf/methods/edit_survey.py +32 -0
- trueconf/methods/forward_message.py +22 -0
- trueconf/methods/get_chat_by_id.py +20 -0
- trueconf/methods/get_chat_history.py +24 -0
- trueconf/methods/get_chat_participants.py +24 -0
- trueconf/methods/get_chats.py +22 -0
- trueconf/methods/get_file_info.py +20 -0
- trueconf/methods/get_message_by_id.py +19 -0
- trueconf/methods/get_user_display_name.py +20 -0
- trueconf/methods/has_chat_participant.py +22 -0
- trueconf/methods/remove_chat.py +20 -0
- trueconf/methods/remove_message.py +23 -0
- trueconf/methods/remove_participant_from_chat.py +23 -0
- trueconf/methods/send_file.py +25 -0
- trueconf/methods/send_message.py +29 -0
- trueconf/methods/send_survey.py +40 -0
- trueconf/methods/subscribe_file_progress.py +21 -0
- trueconf/methods/unsubscribe_file_progress.py +21 -0
- trueconf/methods/upload_file.py +21 -0
- trueconf/py.typed +0 -0
- trueconf/types/__init__.py +25 -0
- trueconf/types/author_box.py +17 -0
- trueconf/types/chat_participant.py +11 -0
- trueconf/types/content/__init__.py +25 -0
- trueconf/types/content/attachment.py +14 -0
- trueconf/types/content/base.py +8 -0
- trueconf/types/content/chat_created.py +9 -0
- trueconf/types/content/document.py +71 -0
- trueconf/types/content/forward_message.py +21 -0
- trueconf/types/content/photo.py +70 -0
- trueconf/types/content/remove_participant.py +9 -0
- trueconf/types/content/sticker.py +70 -0
- trueconf/types/content/survey.py +19 -0
- trueconf/types/content/text.py +9 -0
- trueconf/types/content/video.py +71 -0
- trueconf/types/last_message.py +33 -0
- trueconf/types/message.py +411 -0
- trueconf/types/parser.py +90 -0
- trueconf/types/requests/__init__.py +21 -0
- trueconf/types/requests/added_chat_participant.py +40 -0
- trueconf/types/requests/created_channel.py +42 -0
- trueconf/types/requests/created_group_chat.py +42 -0
- trueconf/types/requests/created_personal_chat.py +42 -0
- trueconf/types/requests/edited_message.py +37 -0
- trueconf/types/requests/removed_chat.py +32 -0
- trueconf/types/requests/removed_chat_participant.py +39 -0
- trueconf/types/requests/removed_message.py +37 -0
- trueconf/types/requests/uploading_progress.py +34 -0
- trueconf/types/responses/__init__.py +57 -0
- trueconf/types/responses/add_chat_participant_response.py +8 -0
- trueconf/types/responses/api_error.py +38 -0
- trueconf/types/responses/auth_response_payload.py +9 -0
- trueconf/types/responses/create_channel_response.py +8 -0
- trueconf/types/responses/create_group_chat_response.py +8 -0
- trueconf/types/responses/create_p2p_chat_response.py +8 -0
- trueconf/types/responses/edit_message_response.py +9 -0
- trueconf/types/responses/edit_survey_response.py +9 -0
- trueconf/types/responses/forward_message_response.py +10 -0
- trueconf/types/responses/get_chat_by_id_response.py +13 -0
- trueconf/types/responses/get_chat_history_response.py +13 -0
- trueconf/types/responses/get_chat_participants_response.py +12 -0
- trueconf/types/responses/get_chats_response.py +12 -0
- trueconf/types/responses/get_file_info_response.py +24 -0
- trueconf/types/responses/get_message_by_id_response.py +26 -0
- trueconf/types/responses/get_user_display_name_response.py +8 -0
- trueconf/types/responses/has_chat_participant_response.py +8 -0
- trueconf/types/responses/remove_chat_participant_response.py +8 -0
- trueconf/types/responses/remove_chat_response.py +8 -0
- trueconf/types/responses/remove_message_response.py +8 -0
- trueconf/types/responses/send_file_response.py +10 -0
- trueconf/types/responses/send_message_response.py +10 -0
- trueconf/types/responses/send_survey_response.py +10 -0
- trueconf/types/responses/subscribe_file_progress_response.py +8 -0
- trueconf/types/responses/unsubscribe_file_progress_response.py +8 -0
- trueconf/types/responses/upload_file_response.py +8 -0
- trueconf/types/update.py +12 -0
- trueconf/utils/__init__.py +3 -0
- trueconf/utils/generate_secret_for_survey.py +10 -0
- trueconf/utils/token.py +78 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .bot import Bot
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class BoundToBot:
|
|
10
|
+
__slots__ = ('_bot',)
|
|
11
|
+
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
self._bot = None
|
|
14
|
+
|
|
15
|
+
def bind(self, bot: "Bot"):
|
|
16
|
+
self._bot = bot
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
@property
|
|
21
|
+
def bot(self) -> "Bot": ...
|
|
22
|
+
else:
|
|
23
|
+
@property
|
|
24
|
+
def bot(self) :
|
|
25
|
+
if not self._bot or self._bot is None:
|
|
26
|
+
raise RuntimeError("Object isn’t bound to ChatBot. Bind via bind(bot) before using shortcuts.")
|
|
27
|
+
return self._bot
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import asyncio
|
|
3
|
+
import contextlib
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Awaitable, Callable, Optional
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("chat_bot")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WebSocketSession:
|
|
12
|
+
"""
|
|
13
|
+
Владеет объектом ws, поднимает фоновый listener и
|
|
14
|
+
пробрасывает сырые сообщения в callback on_message(raw: str).
|
|
15
|
+
"""
|
|
16
|
+
def __init__(self, on_message: Optional[Callable[[str], Awaitable[None]]] = None):
|
|
17
|
+
self.ws = None
|
|
18
|
+
self._listener_task: Optional[asyncio.Task] = None
|
|
19
|
+
self._on_message = on_message
|
|
20
|
+
|
|
21
|
+
def attach(self, ws) -> None:
|
|
22
|
+
"""Подключить актуальный ws и запустить фонового listener-а."""
|
|
23
|
+
self.ws = ws
|
|
24
|
+
if self._listener_task and not self._listener_task.done():
|
|
25
|
+
self._listener_task.cancel()
|
|
26
|
+
self._listener_task = asyncio.create_task(self._listener())
|
|
27
|
+
|
|
28
|
+
async def detach(self) -> None:
|
|
29
|
+
"""Остановить listener (ws остаётся под управлением ChatBot/connect loop)."""
|
|
30
|
+
if self._listener_task:
|
|
31
|
+
try:
|
|
32
|
+
self._listener_task.cancel()
|
|
33
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
34
|
+
await self._listener_task
|
|
35
|
+
finally:
|
|
36
|
+
self._listener_task = None
|
|
37
|
+
self.ws = None
|
|
38
|
+
|
|
39
|
+
async def close(self) -> None:
|
|
40
|
+
"""Закрыть ws и listener."""
|
|
41
|
+
if self.ws:
|
|
42
|
+
await self.ws.close()
|
|
43
|
+
await self.detach()
|
|
44
|
+
|
|
45
|
+
async def send_json(self, message: dict) -> None:
|
|
46
|
+
if not self.ws:
|
|
47
|
+
raise RuntimeError("WS is not attached")
|
|
48
|
+
await self.ws.send(json.dumps(message))
|
|
49
|
+
|
|
50
|
+
async def _listener(self):
|
|
51
|
+
try:
|
|
52
|
+
async for raw in self.ws:
|
|
53
|
+
if self._on_message:
|
|
54
|
+
try:
|
|
55
|
+
await self._on_message(raw)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error(f"on_message callback error: {e}")
|
|
58
|
+
except Exception as e:
|
|
59
|
+
# Закрытие/обрыв — это нормально, верхний цикл переподключит
|
|
60
|
+
logger.debug(f"Session listener stopped: {type(e).__name__}: {e}")
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import List
|
|
3
|
+
from trueconf.filters.base import Event
|
|
4
|
+
from trueconf.dispatcher.router import Router
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Dispatcher:
|
|
8
|
+
"""
|
|
9
|
+
Central event dispatcher for processing and routing incoming events.
|
|
10
|
+
|
|
11
|
+
The `Dispatcher` aggregates one or more `Router` instances and feeds each
|
|
12
|
+
incoming event through them. The routers are traversed recursively via their
|
|
13
|
+
`subrouters` (using `_iter_all()`), and each event is passed to `_feed()` of
|
|
14
|
+
each router in order until it is handled.
|
|
15
|
+
|
|
16
|
+
Typical usage includes registering routers with handlers and then calling
|
|
17
|
+
`feed_update()` with incoming events.
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
>>> dispatcher = Dispatcher()
|
|
21
|
+
>>> dispatcher.include_router(my_router)
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
routers (List[Router]): List of root routers included in the dispatcher.
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
"""Initializes an empty dispatcher with no routers."""
|
|
30
|
+
self.routers: List[Router] = []
|
|
31
|
+
|
|
32
|
+
def include_router(self, router: Router):
|
|
33
|
+
"""
|
|
34
|
+
Includes a router to be used by the dispatcher.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
router (Router): A `Router` instance to include.
|
|
38
|
+
"""
|
|
39
|
+
self.routers.append(router)
|
|
40
|
+
|
|
41
|
+
async def _feed_update(self, event: Event):
|
|
42
|
+
"""
|
|
43
|
+
Feeds an event to all routers and subrouters in order,
|
|
44
|
+
stopping at the first one that handles it.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
event (Event): The event to be processed.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
None
|
|
51
|
+
"""
|
|
52
|
+
for root in self.routers:
|
|
53
|
+
for r in root._iter_all():
|
|
54
|
+
handled = await r._feed(event)
|
|
55
|
+
if handled:
|
|
56
|
+
return
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import Callable, Awaitable, List, Tuple, Any, Union
|
|
6
|
+
from magic_filter import MagicFilter
|
|
7
|
+
from trueconf.filters.base import Event
|
|
8
|
+
from trueconf.filters.base import Filter
|
|
9
|
+
from trueconf.filters.instance_of import InstanceOfFilter
|
|
10
|
+
from trueconf.filters.method import MethodFilter
|
|
11
|
+
from trueconf.types.message import Message
|
|
12
|
+
from trueconf.types.requests.added_chat_participant import AddedChatParticipant
|
|
13
|
+
from trueconf.types.requests.created_channel import CreatedChannel
|
|
14
|
+
from trueconf.types.requests.created_group_chat import CreatedGroupChat
|
|
15
|
+
from trueconf.types.requests.created_personal_chat import CreatedPersonalChat
|
|
16
|
+
from trueconf.types.requests.edited_message import EditedMessage
|
|
17
|
+
from trueconf.types.requests.removed_chat import RemovedChat
|
|
18
|
+
from trueconf.types.requests.removed_chat_participant import RemovedChatParticipant
|
|
19
|
+
from trueconf.types.requests.removed_message import RemovedMessage
|
|
20
|
+
from trueconf.types.requests.uploading_progress import UploadingProgress
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("chat_bot")
|
|
23
|
+
|
|
24
|
+
Handler = Callable[[Event], Awaitable[None]]
|
|
25
|
+
FilterLike = Union[Filter, MagicFilter, Callable[[Event], bool], Callable[[Event], Awaitable[bool]], Any]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Router:
|
|
29
|
+
"""
|
|
30
|
+
Event router for handling incoming events in a structured and extensible way.
|
|
31
|
+
|
|
32
|
+
A `Router` allows you to register event handlers with specific filters,
|
|
33
|
+
such as message types, chat events, or custom logic.
|
|
34
|
+
|
|
35
|
+
You can also include nested routers using `include_router()` to build modular and reusable event structures.
|
|
36
|
+
|
|
37
|
+
Handlers can be registered for:
|
|
38
|
+
|
|
39
|
+
- Messages (`@<router>.message(...)`)
|
|
40
|
+
- Chat creation events (`@<router>.created_personal_chat()`, `@<router>.created_group_chat()`, `@<router>.created_channel()`)
|
|
41
|
+
- Participant events (`@<router>.added_chat_participant()`, `@<router>.removed_chat_participant()`)
|
|
42
|
+
- Message lifecycle events (`@<router>.edited_message()`, `@<router>.removed_message()`)
|
|
43
|
+
- File upload events (`@<router>.uploading_progress()`)
|
|
44
|
+
- Removed chats (`@<router>.removed_chat()`)
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
router = Router()
|
|
50
|
+
|
|
51
|
+
@router.message(F.text == "hello")
|
|
52
|
+
async def handle_hello(msg: Message):
|
|
53
|
+
await msg.answer("Hi there!")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If you have multiple routers, use `.include_router()` to add them to a parent router.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, name: str | None = None, stop_on_first: bool = True):
|
|
60
|
+
|
|
61
|
+
self.name = name or hex(id(self))
|
|
62
|
+
self.stop_on_first = stop_on_first
|
|
63
|
+
self._handlers: List[Tuple[Tuple[FilterLike, ...], Handler]] = []
|
|
64
|
+
self._subrouters: List["Router"] = []
|
|
65
|
+
|
|
66
|
+
def include_router(self, router: "Router") -> None:
|
|
67
|
+
"""Include a child router for hierarchical event routing."""
|
|
68
|
+
self._subrouters.append(router)
|
|
69
|
+
|
|
70
|
+
def _iter_all(self) -> List["Router"]:
|
|
71
|
+
"""Return a list of this router and all nested subrouters recursively."""
|
|
72
|
+
out = [self]
|
|
73
|
+
for child in self._subrouters:
|
|
74
|
+
out.extend(child._iter_all())
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
def event(self, method: str, *filters: FilterLike):
|
|
78
|
+
"""
|
|
79
|
+
Register a handler for a generic event type, filtered by method name.
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
>>> @r.event(F.method == "SendMessage")
|
|
83
|
+
>>> async def handle_message(msg: Message): ...
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
mf = MethodFilter(method)
|
|
87
|
+
return self._register((mf, *filters))
|
|
88
|
+
|
|
89
|
+
def message(self, *filters: FilterLike):
|
|
90
|
+
"""Register a handler for incoming `Message` events."""
|
|
91
|
+
return self._register((InstanceOfFilter(Message), *filters))
|
|
92
|
+
|
|
93
|
+
def uploading_progress(self, *filters: FilterLike):
|
|
94
|
+
"""Register a handler for file uploading progress events."""
|
|
95
|
+
return self._register((InstanceOfFilter(UploadingProgress), *filters))
|
|
96
|
+
|
|
97
|
+
def created_personal_chat(self, *filters: FilterLike):
|
|
98
|
+
"""Register a handler for personal chat creation events."""
|
|
99
|
+
return self._register((InstanceOfFilter(CreatedPersonalChat), *filters))
|
|
100
|
+
|
|
101
|
+
def created_group_chat(self, *filters: FilterLike):
|
|
102
|
+
"""Register a handler for group chat creation events."""
|
|
103
|
+
return self._register((InstanceOfFilter(CreatedGroupChat), *filters))
|
|
104
|
+
|
|
105
|
+
def created_channel(self, *filters: FilterLike):
|
|
106
|
+
"""Register a handler for channel creation events."""
|
|
107
|
+
return self._register((InstanceOfFilter(CreatedChannel), *filters))
|
|
108
|
+
|
|
109
|
+
def added_chat_participant(self, *filters: FilterLike):
|
|
110
|
+
"""Register a handler when a participant is added to a chat."""
|
|
111
|
+
return self._register((InstanceOfFilter(AddedChatParticipant), *filters))
|
|
112
|
+
|
|
113
|
+
def removed_chat_participant(self, *filters: FilterLike):
|
|
114
|
+
"""Register a handler when a participant is removed from a chat."""
|
|
115
|
+
return self._register((InstanceOfFilter(RemovedChatParticipant), *filters))
|
|
116
|
+
|
|
117
|
+
def removed_chat(self, *filters: FilterLike):
|
|
118
|
+
"""Register a handler when a chat is removed."""
|
|
119
|
+
return self._register((InstanceOfFilter(RemovedChat), *filters))
|
|
120
|
+
|
|
121
|
+
def edited_message(self, *filters: FilterLike):
|
|
122
|
+
"""Register a handler for message edit events."""
|
|
123
|
+
return self._register((InstanceOfFilter(EditedMessage), *filters))
|
|
124
|
+
|
|
125
|
+
def removed_message(self, *filters: FilterLike):
|
|
126
|
+
"""Register a handler for message deletion events."""
|
|
127
|
+
return self._register((InstanceOfFilter(RemovedMessage), *filters))
|
|
128
|
+
|
|
129
|
+
def _register(self, filters: Tuple[FilterLike, ...]):
|
|
130
|
+
"""Internal decorator for registering handlers with filters."""
|
|
131
|
+
|
|
132
|
+
def decorator(func: Handler):
|
|
133
|
+
if not asyncio.iscoroutinefunction(func):
|
|
134
|
+
async def async_wrapper(evt: Event):
|
|
135
|
+
return func(evt)
|
|
136
|
+
|
|
137
|
+
self._handlers.append((filters, async_wrapper))
|
|
138
|
+
return func
|
|
139
|
+
self._handlers.append((filters, func))
|
|
140
|
+
return func
|
|
141
|
+
|
|
142
|
+
return decorator
|
|
143
|
+
|
|
144
|
+
async def _feed(self, event: Event) -> bool:
|
|
145
|
+
"""Feed an incoming event to the router and invoke the first matching handler."""
|
|
146
|
+
logger.info(f"📥 Incoming event: {event}")
|
|
147
|
+
for flts, handler in self._handlers:
|
|
148
|
+
|
|
149
|
+
if not flts:
|
|
150
|
+
self._spawn(handler, event, "<none>")
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
matched = True
|
|
154
|
+
for f in flts:
|
|
155
|
+
try:
|
|
156
|
+
if not await self._apply_filter(f, event):
|
|
157
|
+
matched = False
|
|
158
|
+
break
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.exception(f"Filter {type(f).__name__} error: {e}")
|
|
161
|
+
matched = False
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
if matched:
|
|
165
|
+
filters_str = ", ".join(
|
|
166
|
+
getattr(f, "__name__", type(f).__name__) if callable(f) else type(f).__name__
|
|
167
|
+
for f in flts
|
|
168
|
+
)
|
|
169
|
+
self._spawn(handler, event, filters_str)
|
|
170
|
+
return True
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
def _spawn(self, handler: Handler, event: Event, filters_str: str):
|
|
174
|
+
"""Internal method to spawn a task for executing the matched handler."""
|
|
175
|
+
name = getattr(handler, "__name__", "<handler>")
|
|
176
|
+
logger.info(f"[router:{self.name}] matched handler={name} filters=[{filters_str}]")
|
|
177
|
+
|
|
178
|
+
async def _run():
|
|
179
|
+
try:
|
|
180
|
+
await handler(event)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.exception(f"Handler {name} failed: {e}")
|
|
183
|
+
|
|
184
|
+
asyncio.create_task(_run())
|
|
185
|
+
|
|
186
|
+
async def _apply_filter(self, f: Filter | Any, event: Event) -> bool:
|
|
187
|
+
"""Evaluate a filter (sync or async) against the event."""
|
|
188
|
+
if isinstance(f, MagicFilter):
|
|
189
|
+
try:
|
|
190
|
+
return bool(f.resolve(event))
|
|
191
|
+
except Exception:
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
res = f(event)
|
|
196
|
+
except Exception:
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
if inspect.isawaitable(res):
|
|
200
|
+
try:
|
|
201
|
+
res = await res
|
|
202
|
+
except Exception:
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
if isinstance(res, bool):
|
|
206
|
+
return res
|
|
207
|
+
return bool(res)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .aouth_error import OAuthError
|
|
2
|
+
from .chat_participant_role import ChatParticipantRole
|
|
3
|
+
from .chat_type import ChatType
|
|
4
|
+
from .envelope_author_type import EnvelopeAuthorType
|
|
5
|
+
from .message_type import MessageType
|
|
6
|
+
from .file_ready_state import FileReadyState
|
|
7
|
+
from .incoming_update_method import IncomingUpdateMethod
|
|
8
|
+
from .update_type import UpdateType
|
|
9
|
+
from .parse_mode import ParseMode
|
|
10
|
+
from .survey_type import SurveyType
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'UpdateType',
|
|
14
|
+
'FileReadyState',
|
|
15
|
+
'ParseMode',
|
|
16
|
+
'ChatType',
|
|
17
|
+
'ChatParticipantRole',
|
|
18
|
+
'OAuthError',
|
|
19
|
+
'MessageType',
|
|
20
|
+
'EnvelopeAuthorType',
|
|
21
|
+
'SurveyType',
|
|
22
|
+
'IncomingUpdateMethod',
|
|
23
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class OAuthError(str, Enum):
|
|
5
|
+
"""
|
|
6
|
+
Error codes, according to the OAuth 2.0 specification, are presented as ASCII strings from the list specified in the specification.
|
|
7
|
+
|
|
8
|
+
Source:
|
|
9
|
+
https://trueconf.com/docs/chatbot-connector/en/objects/#oauth-error
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
INVALID_REQUEST = "invalid_request"
|
|
13
|
+
INVALID_CLIENT = "invalid_client"
|
|
14
|
+
INVALID_GRANT = "invalid_grant"
|
|
15
|
+
UNSUPORTED_GRANT_TYPE = "unsupported_grant_type"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ChatParticipantRole(str, Enum):
|
|
5
|
+
"""
|
|
6
|
+
This object represents a possible participant role in a chat.
|
|
7
|
+
|
|
8
|
+
Source:
|
|
9
|
+
https://trueconf.com/docs/chatbot-connector/en/objects/#chatparticipantroleenum
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
OWNER = "owner"
|
|
13
|
+
ADMIN = "admin"
|
|
14
|
+
USER = "user"
|
|
15
|
+
CONF_OWNER = "conf_owner"
|
|
16
|
+
CONF_MODERATOR = "conf_moderator"
|
|
17
|
+
FAVORITES_OWNER = "favorites_owner"
|
|
18
|
+
WRITER = "writer"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ChatType(int, Enum):
|
|
5
|
+
"""
|
|
6
|
+
The enumeration contains possible chat types.
|
|
7
|
+
|
|
8
|
+
Source:
|
|
9
|
+
https://trueconf.com/docs/chatbot-connector/en/objects/#chattypeenum
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
UNDEF = 0
|
|
13
|
+
P2P = 1
|
|
14
|
+
GROUP = 2
|
|
15
|
+
SYSTEM = 3
|
|
16
|
+
FAVORITES = 5
|
|
17
|
+
CHANNEL = 6
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FileReadyState(int, Enum):
|
|
5
|
+
"""
|
|
6
|
+
This enumeration is used to indicate the status of a file on the server.
|
|
7
|
+
|
|
8
|
+
Source:
|
|
9
|
+
https://trueconf.com/docs/chatbot-connector/en/objects/#filereadystateenum
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
NOT_AVAILABLE = 0
|
|
13
|
+
UPLOADING = 1
|
|
14
|
+
READY = 2
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class IncomingUpdateMethod(str, Enum):
|
|
5
|
+
ADDED_CHAT_PARTICIPANT = "addChatParticipant"
|
|
6
|
+
CREATED_CHANNEL = "createChannel"
|
|
7
|
+
CREATED_GROUP_CHAT = "createGroupChat"
|
|
8
|
+
CREATED_PERSONAL_CHAT = "createP2PChat"
|
|
9
|
+
EDITED_MESSAGE = "editMessage"
|
|
10
|
+
REMOVED_CHAT_PARTICIPANT = "removeChatParticipant"
|
|
11
|
+
REMOVED_MESSAGE = "removeMessage"
|
|
12
|
+
MESSAGE = "sendMessage"
|
|
13
|
+
UPLOADING_PROGRESS = "uploadFileProgress"
|
|
14
|
+
REMOVED_CHAT = "removeChat"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MessageType(int, Enum):
|
|
6
|
+
"""
|
|
7
|
+
The enumeration contains the message type.
|
|
8
|
+
|
|
9
|
+
Source:
|
|
10
|
+
https://trueconf.com/docs/chatbot-connector/en/objects/#envelopetypeenum
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
ADD_PARTICIPANT = 1
|
|
14
|
+
REMOVE_PARTICIPANT = 2
|
|
15
|
+
PARTICIPANT_ROLE = 110
|
|
16
|
+
PLAIN_MESSAGE = 200
|
|
17
|
+
FORWARDED_MESSAGE = 201
|
|
18
|
+
ATTACHMENT = 202
|
|
19
|
+
SURVEY = 204
|
|
20
|
+
|
|
21
|
+
async def __call__(self, event: Any) -> bool:
|
|
22
|
+
if getattr(event, "type", None) is None:
|
|
23
|
+
return False
|
|
24
|
+
try:
|
|
25
|
+
return int(getattr(event, "type")) == int(self)
|
|
26
|
+
except Exception:
|
|
27
|
+
return False
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UpdateType(int, Enum):
|
|
5
|
+
"""
|
|
6
|
+
There are three types of messages. Only REQUEST and RESPONSE are applicable.
|
|
7
|
+
|
|
8
|
+
Source:
|
|
9
|
+
https://trueconf.com/docs/chatbot-connector/en/objects/#message-type
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
RESERVED = 0
|
|
13
|
+
REQUEST = 1
|
|
14
|
+
RESPONSE = 2
|
trueconf/exceptions.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TrueConfChatBotError(Exception):
|
|
5
|
+
"""
|
|
6
|
+
Base exception for all TrueConf ChatBot Connector errors.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
class TokenValidationError(TrueConfChatBotError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
class InvalidGrantError(TrueConfChatBotError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ApiError(TrueConfChatBotError):
|
|
17
|
+
pass
|
trueconf/filters/base.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any
|
|
3
|
+
from trueconf.enums.message_type import MessageType
|
|
4
|
+
from trueconf.types.message import Message
|
|
5
|
+
|
|
6
|
+
Event = Any # позже можно заменить на типизированные модели
|
|
7
|
+
|
|
8
|
+
class Command:
|
|
9
|
+
"""
|
|
10
|
+
/start, /help, /echo hi
|
|
11
|
+
Command("start") == Command("/start") # оба варианта валидны
|
|
12
|
+
Command(("start", "join")) — строго '/start join'
|
|
13
|
+
Command("echo", with_param=True) — '/echo <любой текст>'
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, cmd: str, with_param: bool = False):
|
|
17
|
+
self.cmd = cmd[1:] if cmd.startswith("/") else cmd
|
|
18
|
+
self.with_param = with_param
|
|
19
|
+
|
|
20
|
+
async def __call__(self, event: Event) -> bool:
|
|
21
|
+
if not isinstance(event, Message):
|
|
22
|
+
return False
|
|
23
|
+
if event.type != MessageType.PLAIN_MESSAGE:
|
|
24
|
+
return False
|
|
25
|
+
text = (event.content.text or "")
|
|
26
|
+
if not text.startswith("/"):
|
|
27
|
+
return False
|
|
28
|
+
no_slash = text[1:]
|
|
29
|
+
parts = no_slash.split(maxsplit=1)
|
|
30
|
+
if not parts or parts[0] != self.cmd:
|
|
31
|
+
return False
|
|
32
|
+
return (len(parts) == 2 and bool(parts[1].strip())) if self.with_param else True
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Type
|
|
3
|
+
from trueconf.filters.base import Event
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class InstanceOfFilter:
|
|
7
|
+
def __init__(self, cls: Type[object]):
|
|
8
|
+
self.cls = cls
|
|
9
|
+
|
|
10
|
+
async def __call__(self, event: Event) -> bool:
|
|
11
|
+
return isinstance(event, self.cls)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from trueconf.enums.message_type import MessageType
|
|
3
|
+
from trueconf.types.message import Message
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MessageFilter:
|
|
7
|
+
"""Фильтр по payload.type (= EnvelopeType)."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, *types: MessageType):
|
|
10
|
+
self.types = frozenset(int(t) for t in types)
|
|
11
|
+
|
|
12
|
+
async def __call__(self, event: Message) -> bool:
|
|
13
|
+
if not isinstance(event, Message):
|
|
14
|
+
return False
|
|
15
|
+
return int(event.type) in self.types
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from trueconf.filters.base import Event
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MethodFilter:
|
|
6
|
+
def __init__(self, method: str):
|
|
7
|
+
self.method = method
|
|
8
|
+
|
|
9
|
+
async def __call__(self, event: Event) -> bool:
|
|
10
|
+
if isinstance(event, dict):
|
|
11
|
+
return event.get("method") == self.method
|
|
12
|
+
return getattr(event, "method", None) == self.method
|
trueconf/loggers.py
ADDED