maxkit 2.13.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.
- aiomax/__init__.py +9 -0
- aiomax/bot.py +1166 -0
- aiomax/buttons.py +303 -0
- aiomax/cache.py +31 -0
- aiomax/exceptions.py +85 -0
- aiomax/filters.py +179 -0
- aiomax/fsm.py +107 -0
- aiomax/router.py +383 -0
- aiomax/russian_trusted_root_ca.cer +33 -0
- aiomax/types.py +1512 -0
- aiomax/utils.py +125 -0
- maxkit-2.13.0.dist-info/METADATA +45 -0
- maxkit-2.13.0.dist-info/RECORD +16 -0
- maxkit-2.13.0.dist-info/WHEEL +5 -0
- maxkit-2.13.0.dist-info/licenses/LICENSE.md +22 -0
- maxkit-2.13.0.dist-info/top_level.txt +1 -0
aiomax/buttons.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
button_logger = logging.getLogger("aiomax.buttons")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Button:
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
type: Literal[
|
|
11
|
+
"callback",
|
|
12
|
+
"link",
|
|
13
|
+
"request_geo_location",
|
|
14
|
+
"request_contact",
|
|
15
|
+
"chat",
|
|
16
|
+
],
|
|
17
|
+
text: str,
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
Base button class
|
|
21
|
+
"""
|
|
22
|
+
self.type: Literal[
|
|
23
|
+
"callback",
|
|
24
|
+
"link",
|
|
25
|
+
"request_geo_location",
|
|
26
|
+
"request_contact",
|
|
27
|
+
"chat",
|
|
28
|
+
] = type
|
|
29
|
+
self.text: str = text
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def from_json(data: dict) -> "Button":
|
|
33
|
+
if data["type"] == "callback":
|
|
34
|
+
return CallbackButton.from_json(data)
|
|
35
|
+
elif data["type"] == "link":
|
|
36
|
+
return LinkButton.from_json(data)
|
|
37
|
+
elif data["type"] == "request_geo_location":
|
|
38
|
+
return GeolocationButton.from_json(data)
|
|
39
|
+
elif data["type"] == "request_contact":
|
|
40
|
+
return ContactButton.from_json(data)
|
|
41
|
+
elif data["type"] == "chat":
|
|
42
|
+
return ChatButton.from_json(data)
|
|
43
|
+
elif data["type"] == "message":
|
|
44
|
+
return MessageButton.from_json(data)
|
|
45
|
+
elif data["type"] == "open_app":
|
|
46
|
+
return WebAppButton.from_json(data)
|
|
47
|
+
else:
|
|
48
|
+
# Unknown/newly-added button type: keep parsing the keyboard
|
|
49
|
+
# instead of crashing the whole update.
|
|
50
|
+
button_logger.warning("Unknown button type: %s", data.get("type"))
|
|
51
|
+
return Button(data["type"], data.get("text", ""))
|
|
52
|
+
|
|
53
|
+
def to_json(self) -> dict:
|
|
54
|
+
return {"type": self.type, "text": self.text}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CallbackButton(Button):
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
text: str,
|
|
61
|
+
payload: str,
|
|
62
|
+
intent: Literal["default", "positive", "negative"] = "default",
|
|
63
|
+
):
|
|
64
|
+
"""
|
|
65
|
+
Callback button
|
|
66
|
+
|
|
67
|
+
:param text: Button text
|
|
68
|
+
:param payload: Payload that will be sent to the bot when the button is
|
|
69
|
+
pressed
|
|
70
|
+
:param intent: Intent of the button (changes appearance on client)
|
|
71
|
+
"""
|
|
72
|
+
super().__init__("callback", text)
|
|
73
|
+
self.payload: str = payload
|
|
74
|
+
self.intent: Literal["default", "positive", "negative"] = intent
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def from_json(data: dict) -> "CallbackButton":
|
|
78
|
+
return CallbackButton(
|
|
79
|
+
data.get("text", ""),
|
|
80
|
+
data.get("payload", ""),
|
|
81
|
+
data.get("intent", "default"),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def to_json(self) -> dict:
|
|
85
|
+
return {
|
|
86
|
+
"type": "callback",
|
|
87
|
+
"text": self.text,
|
|
88
|
+
"payload": self.payload,
|
|
89
|
+
"intent": self.intent,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class LinkButton(Button):
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
text: str,
|
|
97
|
+
url: str,
|
|
98
|
+
):
|
|
99
|
+
"""
|
|
100
|
+
URL button on a message
|
|
101
|
+
|
|
102
|
+
:param text: Button text
|
|
103
|
+
:param url: Link that the button redirects to
|
|
104
|
+
"""
|
|
105
|
+
super().__init__("link", text)
|
|
106
|
+
self.url: str = url
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def from_json(data: dict) -> "LinkButton":
|
|
110
|
+
return LinkButton(data.get("text", ""), data.get("url", ""))
|
|
111
|
+
|
|
112
|
+
def to_json(self) -> dict:
|
|
113
|
+
return {"type": "link", "text": self.text, "url": self.url}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class GeolocationButton(Button):
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
text: str,
|
|
120
|
+
quick: bool = False,
|
|
121
|
+
):
|
|
122
|
+
"""
|
|
123
|
+
Request geolocation button on a message
|
|
124
|
+
|
|
125
|
+
:param text: Button text
|
|
126
|
+
:param quick: Whether to show a confirmational message to a user when
|
|
127
|
+
pressing the button
|
|
128
|
+
"""
|
|
129
|
+
super().__init__("request_geo_location", text)
|
|
130
|
+
self.quick: bool = quick
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def from_json(data: dict) -> "GeolocationButton":
|
|
134
|
+
return GeolocationButton(
|
|
135
|
+
data.get("text", ""), data.get("quick", False)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def to_json(self) -> dict:
|
|
139
|
+
return {
|
|
140
|
+
"type": "request_geo_location",
|
|
141
|
+
"text": self.text,
|
|
142
|
+
"quick": self.quick,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ContactButton(Button):
|
|
147
|
+
def __init__(self, text: str):
|
|
148
|
+
"""
|
|
149
|
+
Request contact button on a message
|
|
150
|
+
|
|
151
|
+
:param text: Button text
|
|
152
|
+
"""
|
|
153
|
+
super().__init__("request_contact", text)
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def from_json(data: dict) -> "ContactButton":
|
|
157
|
+
return ContactButton(data.get("text", ""))
|
|
158
|
+
|
|
159
|
+
def to_json(self) -> dict:
|
|
160
|
+
return {"type": "request_contact", "text": self.text}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class ChatButton(Button):
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
text: str,
|
|
167
|
+
title: str,
|
|
168
|
+
description: "str | None" = None,
|
|
169
|
+
payload: "str | None" = None,
|
|
170
|
+
uuid: "int | None" = None,
|
|
171
|
+
):
|
|
172
|
+
"""
|
|
173
|
+
Chat creation button on a message
|
|
174
|
+
|
|
175
|
+
:param text: Button text
|
|
176
|
+
:param title: Name of the new chat
|
|
177
|
+
:param description: Description of the new chat
|
|
178
|
+
:param payload: Payload that will be sent to the bot when the chat is
|
|
179
|
+
created
|
|
180
|
+
:param uuid: Chat UUID, assigned when new message is sent.
|
|
181
|
+
Provide when editing message
|
|
182
|
+
"""
|
|
183
|
+
super().__init__("chat", text)
|
|
184
|
+
self.title: str = title
|
|
185
|
+
self.description: str | None = description
|
|
186
|
+
self.payload: str | None = payload
|
|
187
|
+
self.uuid: int | None = uuid
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def from_json(data: dict) -> "ChatButton":
|
|
191
|
+
return ChatButton(
|
|
192
|
+
data.get("text", ""),
|
|
193
|
+
data.get("chat_title"),
|
|
194
|
+
data.get("chat_description"),
|
|
195
|
+
data.get("start_payload"),
|
|
196
|
+
data.get("uuid"),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def to_json(self) -> dict:
|
|
200
|
+
return {
|
|
201
|
+
"type": "chat",
|
|
202
|
+
"text": self.text,
|
|
203
|
+
"chat_title": self.title,
|
|
204
|
+
"chat_description": self.description,
|
|
205
|
+
"start_payload": self.payload,
|
|
206
|
+
"uuid": self.uuid,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class WebAppButton(Button):
|
|
211
|
+
def __init__(self, text: str, bot: "str | int"):
|
|
212
|
+
"""
|
|
213
|
+
Open web app button
|
|
214
|
+
|
|
215
|
+
:param text: Button text
|
|
216
|
+
:param bot: Bot ID, username or link of which to open
|
|
217
|
+
the web app
|
|
218
|
+
"""
|
|
219
|
+
super().__init__("open_app", text)
|
|
220
|
+
self.bot: "str | int" = bot
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def from_json(data: dict) -> "WebAppButton":
|
|
224
|
+
bot = data.get("contact_id", data.get("web_app"))
|
|
225
|
+
return WebAppButton(data.get("text", ""), bot)
|
|
226
|
+
|
|
227
|
+
def to_json(self) -> dict:
|
|
228
|
+
data = {"type": "open_app", "text": self.text}
|
|
229
|
+
if isinstance(self.bot, int):
|
|
230
|
+
data["contact_id"] = self.bot
|
|
231
|
+
else:
|
|
232
|
+
data["web_app"] = self.bot
|
|
233
|
+
return data
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class MessageButton(Button):
|
|
237
|
+
def __init__(self, text: str):
|
|
238
|
+
"""
|
|
239
|
+
Send text to chat button
|
|
240
|
+
|
|
241
|
+
:param text: Button text. Will be sent to the chat when a user
|
|
242
|
+
presses the button
|
|
243
|
+
"""
|
|
244
|
+
super().__init__("message", text)
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
def from_json(data: dict) -> "MessageButton":
|
|
248
|
+
return MessageButton(data.get("text", ""))
|
|
249
|
+
|
|
250
|
+
def to_json(self) -> dict:
|
|
251
|
+
return {"type": "message", "text": self.text}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# builder
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class KeyboardBuilder:
|
|
258
|
+
def __init__(self):
|
|
259
|
+
"""
|
|
260
|
+
Keyboard builder
|
|
261
|
+
"""
|
|
262
|
+
self.buttons: list[list[Button]] = []
|
|
263
|
+
|
|
264
|
+
def to_list(self) -> list[list[dict]]:
|
|
265
|
+
"""
|
|
266
|
+
Returns a serialised interpretation of the keyboard to put in a message
|
|
267
|
+
"""
|
|
268
|
+
return [[button.to_json() for button in row] for row in self.buttons]
|
|
269
|
+
|
|
270
|
+
def add(self, *buttons: Button):
|
|
271
|
+
"""
|
|
272
|
+
Add buttons to the last row of the keyboard
|
|
273
|
+
"""
|
|
274
|
+
if len(self.buttons) == 0:
|
|
275
|
+
self.buttons.append([])
|
|
276
|
+
|
|
277
|
+
self.buttons[-1].extend(buttons)
|
|
278
|
+
return self
|
|
279
|
+
|
|
280
|
+
def row(self, *buttons: Button):
|
|
281
|
+
"""
|
|
282
|
+
Add a row of buttons
|
|
283
|
+
"""
|
|
284
|
+
self.buttons.append(list(buttons))
|
|
285
|
+
return self
|
|
286
|
+
|
|
287
|
+
def table(self, in_row: int, *buttons: Button):
|
|
288
|
+
"""
|
|
289
|
+
Adds multiple rows of buttons so that there are `in_row` buttons
|
|
290
|
+
in each row
|
|
291
|
+
|
|
292
|
+
:param in_row: How many buttons to put in each row
|
|
293
|
+
"""
|
|
294
|
+
counter = 0
|
|
295
|
+
|
|
296
|
+
for button in buttons:
|
|
297
|
+
if counter == 0:
|
|
298
|
+
self.row()
|
|
299
|
+
counter = in_row
|
|
300
|
+
|
|
301
|
+
self.add(button)
|
|
302
|
+
counter -= 1
|
|
303
|
+
return self
|
aiomax/cache.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .types import Message
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MessageCache:
|
|
5
|
+
def __init__(self, max_size: int = 10000):
|
|
6
|
+
"""
|
|
7
|
+
New message cache.
|
|
8
|
+
|
|
9
|
+
:param max_size: Maximum number of messages to store
|
|
10
|
+
"""
|
|
11
|
+
self.max_size: int = max_size
|
|
12
|
+
self.messages: dict[str, Message] = {}
|
|
13
|
+
|
|
14
|
+
def get_message(self, id: str) -> "Message | None":
|
|
15
|
+
"""
|
|
16
|
+
Returns a message by ID. None if message wasnt cached
|
|
17
|
+
|
|
18
|
+
:param id: Message ID
|
|
19
|
+
"""
|
|
20
|
+
return self.messages.get(id, None)
|
|
21
|
+
|
|
22
|
+
def add_message(self, message: Message):
|
|
23
|
+
"""
|
|
24
|
+
Caches a message.
|
|
25
|
+
|
|
26
|
+
:param message: Message
|
|
27
|
+
"""
|
|
28
|
+
self.messages[message.id] = message
|
|
29
|
+
|
|
30
|
+
while len(self.messages) > self.max_size:
|
|
31
|
+
(k := next(iter(self.messages)), self.messages.pop(k))
|
aiomax/exceptions.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
class AiomaxException(Exception):
|
|
2
|
+
"""
|
|
3
|
+
Default class for aiomax Exceptions
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class InvalidToken(AiomaxException):
|
|
8
|
+
"""
|
|
9
|
+
Invalid token Exception
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AttachmentNotReady(AiomaxException):
|
|
14
|
+
"""
|
|
15
|
+
Attachment not ready Exception
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ChatNotFound(AiomaxException):
|
|
20
|
+
"""
|
|
21
|
+
Chat not found Exception
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class IncorrectTextLength(AiomaxException):
|
|
26
|
+
"""
|
|
27
|
+
Incorrect text length Exception
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class InternalError(AiomaxException):
|
|
32
|
+
"""
|
|
33
|
+
Internal error Exception
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, id: "str | None" = None):
|
|
37
|
+
self.id: str = id
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class UnknownErrorException(AiomaxException):
|
|
41
|
+
"""
|
|
42
|
+
Unknown error Exception
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, text: str, description: "str | None" = None):
|
|
46
|
+
self.text: str = text
|
|
47
|
+
self.description: "str | None" = description
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AccessDeniedException(AiomaxException):
|
|
51
|
+
"""
|
|
52
|
+
Access Denied Exception
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, description: "str | None" = None):
|
|
56
|
+
self.description: "str | None" = description
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class NotFoundException(AiomaxException):
|
|
60
|
+
"""
|
|
61
|
+
Something not found Exception
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, description: "str | None" = None):
|
|
65
|
+
self.description: "str | None" = description
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class MessageNotFoundException(NotFoundException):
|
|
69
|
+
"""
|
|
70
|
+
Child `NotFoundException` exception class that is raised
|
|
71
|
+
in `Bot.get_message` function
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class FilenameNotProvided(AiomaxException):
|
|
76
|
+
"""
|
|
77
|
+
Filename not provided exception
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class InvalidSSLException(AiomaxException):
|
|
82
|
+
"""
|
|
83
|
+
Invalid SSL certificate. Might mean that
|
|
84
|
+
Mintsifra certificate is not installed
|
|
85
|
+
"""
|
aiomax/filters.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def normalize_filter(filter_):
|
|
6
|
+
if isinstance(filter_, str):
|
|
7
|
+
return equals(filter_)
|
|
8
|
+
|
|
9
|
+
elif isinstance(filter_, bool):
|
|
10
|
+
return lambda _: filter_
|
|
11
|
+
|
|
12
|
+
elif callable(filter_):
|
|
13
|
+
return filter_
|
|
14
|
+
|
|
15
|
+
raise ValueError(f"Unsupported filter type: {type(filter_)}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _filter:
|
|
19
|
+
"""
|
|
20
|
+
Superclass of other filters for support of bit-wise or and bit-wise and
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __or__(self, other):
|
|
24
|
+
return _OrFilter(self, other)
|
|
25
|
+
|
|
26
|
+
def __and__(self, other):
|
|
27
|
+
return _AndFilter(self, other)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _OrFilter(_filter):
|
|
31
|
+
"""
|
|
32
|
+
Class for using bit-wise or on filters
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, filter1, filter2):
|
|
36
|
+
self.filter1 = normalize_filter(filter1)
|
|
37
|
+
self.filter2 = normalize_filter(filter2)
|
|
38
|
+
|
|
39
|
+
def __call__(self, obj: any):
|
|
40
|
+
return self.filter1(obj) or self.filter2(obj)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _AndFilter(_filter):
|
|
44
|
+
"""
|
|
45
|
+
Class for using bit-wise and on filters
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, filter1, filter2):
|
|
49
|
+
self.filter1 = normalize_filter(filter1)
|
|
50
|
+
self.filter2 = normalize_filter(filter2)
|
|
51
|
+
|
|
52
|
+
def __call__(self, obj: any):
|
|
53
|
+
return self.filter1(obj) and self.filter2(obj)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class equals(_filter):
|
|
57
|
+
def __init__(self, content: str):
|
|
58
|
+
"""
|
|
59
|
+
:param content: Content to check
|
|
60
|
+
|
|
61
|
+
Checks if the content equals to the given string
|
|
62
|
+
"""
|
|
63
|
+
self.content = content
|
|
64
|
+
|
|
65
|
+
def __call__(self, obj: any):
|
|
66
|
+
if hasattr(obj, "content"):
|
|
67
|
+
return obj.content == self.content
|
|
68
|
+
else:
|
|
69
|
+
raise Exception(f"Class {type(obj).__name__} has no content")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class has(_filter):
|
|
73
|
+
def __init__(self, content: str):
|
|
74
|
+
"""
|
|
75
|
+
:param content: Content to check
|
|
76
|
+
|
|
77
|
+
Checks if the content has the given string
|
|
78
|
+
"""
|
|
79
|
+
self.content = content
|
|
80
|
+
|
|
81
|
+
def __call__(self, obj: any):
|
|
82
|
+
if not hasattr(obj, "content"):
|
|
83
|
+
raise Exception(f"Class {type(obj).__name__} has no content")
|
|
84
|
+
if obj.content is None:
|
|
85
|
+
return False
|
|
86
|
+
return self.content in obj.content
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class startswith(_filter):
|
|
90
|
+
def __init__(self, prefix: str):
|
|
91
|
+
"""
|
|
92
|
+
:param prefix: Prefix to check
|
|
93
|
+
|
|
94
|
+
Checks if the content starts with the given prefix
|
|
95
|
+
"""
|
|
96
|
+
self.prefix = prefix
|
|
97
|
+
|
|
98
|
+
def __call__(self, obj: any):
|
|
99
|
+
if not hasattr(obj, "content"):
|
|
100
|
+
raise Exception(f"Class {type(obj).__name__} has no content")
|
|
101
|
+
if obj.content is None:
|
|
102
|
+
return False
|
|
103
|
+
return obj.content.startswith(self.prefix)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class endswith(_filter):
|
|
107
|
+
def __init__(self, suffix: str):
|
|
108
|
+
"""
|
|
109
|
+
:param suffix: Suffix to check
|
|
110
|
+
|
|
111
|
+
Checks if the content ends with the given suffix
|
|
112
|
+
"""
|
|
113
|
+
self.suffix = suffix
|
|
114
|
+
|
|
115
|
+
def __call__(self, obj: any):
|
|
116
|
+
if not hasattr(obj, "content"):
|
|
117
|
+
raise Exception(f"Class {type(obj).__name__} has no content")
|
|
118
|
+
if obj.content is None:
|
|
119
|
+
return False
|
|
120
|
+
return obj.content.endswith(self.suffix)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class regex(_filter):
|
|
124
|
+
def __init__(self, pattern: str):
|
|
125
|
+
"""
|
|
126
|
+
:param pattern: Regex pattern to check
|
|
127
|
+
|
|
128
|
+
Checks if the content matches the given pattern
|
|
129
|
+
"""
|
|
130
|
+
self.pattern = pattern
|
|
131
|
+
|
|
132
|
+
def __call__(self, obj: any):
|
|
133
|
+
if not hasattr(obj, "content"):
|
|
134
|
+
raise Exception(f"Class {type(obj).__name__} has no content")
|
|
135
|
+
if obj.content is None:
|
|
136
|
+
return None
|
|
137
|
+
return re.fullmatch(self.pattern, obj.content)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def papaya(obj: any):
|
|
141
|
+
"""
|
|
142
|
+
Checks if the content's second-to-last word of the content is "папайя".
|
|
143
|
+
|
|
144
|
+
You do not need to call this.
|
|
145
|
+
"""
|
|
146
|
+
if not hasattr(obj, "content"):
|
|
147
|
+
raise Exception(f"Class {type(obj).__name__} has no content")
|
|
148
|
+
if obj.content is None:
|
|
149
|
+
return False
|
|
150
|
+
words = obj.content.split()
|
|
151
|
+
if len(words) < 2:
|
|
152
|
+
return False
|
|
153
|
+
return words[-2].lower() == "папайя"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class state(_filter):
|
|
157
|
+
def __init__(self, state: Any):
|
|
158
|
+
"""
|
|
159
|
+
:param state: State to check
|
|
160
|
+
|
|
161
|
+
Checks if the content matches the given pattern
|
|
162
|
+
"""
|
|
163
|
+
self.state = state
|
|
164
|
+
|
|
165
|
+
def __call__(self, obj: Any):
|
|
166
|
+
if not hasattr(obj, "user_id"):
|
|
167
|
+
raise Exception(f"Class {type(obj).__name__} has no user id")
|
|
168
|
+
|
|
169
|
+
user_id = obj.user_id
|
|
170
|
+
|
|
171
|
+
if not user_id:
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
if not hasattr(obj, "bot") or not obj.bot:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
storage = obj.bot.storage
|
|
178
|
+
|
|
179
|
+
return storage.get_state(user_id) == self.state
|
aiomax/fsm.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FSMStorage:
|
|
5
|
+
"""
|
|
6
|
+
In-memory storage of FSM state and data.
|
|
7
|
+
|
|
8
|
+
State is keyed by ``user_id`` only, so a user shares the same state and
|
|
9
|
+
data across every chat they talk to the bot in. In multi-chat/group bots
|
|
10
|
+
a single user's flows in different chats will therefore collide; scope
|
|
11
|
+
your own keys by chat if you need per-chat isolation.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.states: dict[int, Any] = {}
|
|
16
|
+
self.data: dict[int, Any] = {}
|
|
17
|
+
|
|
18
|
+
def get_state(self, user_id: int) -> Any:
|
|
19
|
+
"""
|
|
20
|
+
Gets user's state
|
|
21
|
+
"""
|
|
22
|
+
return self.states.get(user_id)
|
|
23
|
+
|
|
24
|
+
def get_data(self, user_id: int) -> Any:
|
|
25
|
+
"""
|
|
26
|
+
Gets user's data
|
|
27
|
+
"""
|
|
28
|
+
return self.data.get(user_id)
|
|
29
|
+
|
|
30
|
+
def change_state(self, user_id: int, new: Any):
|
|
31
|
+
"""
|
|
32
|
+
Changes user's state
|
|
33
|
+
"""
|
|
34
|
+
self.states[user_id] = new
|
|
35
|
+
|
|
36
|
+
def change_data(self, user_id: int, new: Any):
|
|
37
|
+
"""
|
|
38
|
+
Changes user's data
|
|
39
|
+
"""
|
|
40
|
+
self.data[user_id] = new
|
|
41
|
+
|
|
42
|
+
def clear_state(self, user_id: int) -> Any:
|
|
43
|
+
"""
|
|
44
|
+
Clears user's state and returns it
|
|
45
|
+
"""
|
|
46
|
+
return self.states.pop(user_id, None)
|
|
47
|
+
|
|
48
|
+
def clear_data(self, user_id: int) -> Any:
|
|
49
|
+
"""
|
|
50
|
+
Clears user's data and returns it
|
|
51
|
+
"""
|
|
52
|
+
return self.data.pop(user_id, None)
|
|
53
|
+
|
|
54
|
+
def clear(self, user_id: int):
|
|
55
|
+
"""
|
|
56
|
+
Clears user's state and data
|
|
57
|
+
"""
|
|
58
|
+
self.states.pop(user_id, None)
|
|
59
|
+
self.data.pop(user_id, None)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FSMCursor:
|
|
63
|
+
def __init__(self, storage: FSMStorage, user_id: int):
|
|
64
|
+
self.storage: FSMStorage = storage
|
|
65
|
+
self.user_id: int = user_id
|
|
66
|
+
|
|
67
|
+
def get_state(self) -> Any:
|
|
68
|
+
"""
|
|
69
|
+
Gets user's state
|
|
70
|
+
"""
|
|
71
|
+
return self.storage.get_state(self.user_id)
|
|
72
|
+
|
|
73
|
+
def get_data(self) -> Any:
|
|
74
|
+
"""
|
|
75
|
+
Gets user's data
|
|
76
|
+
"""
|
|
77
|
+
return self.storage.get_data(self.user_id)
|
|
78
|
+
|
|
79
|
+
def change_state(self, new: Any):
|
|
80
|
+
"""
|
|
81
|
+
Changes user's state
|
|
82
|
+
"""
|
|
83
|
+
self.storage.change_state(self.user_id, new)
|
|
84
|
+
|
|
85
|
+
def change_data(self, new: Any):
|
|
86
|
+
"""
|
|
87
|
+
Changes user's data
|
|
88
|
+
"""
|
|
89
|
+
self.storage.change_data(self.user_id, new)
|
|
90
|
+
|
|
91
|
+
def clear_state(self) -> Any:
|
|
92
|
+
"""
|
|
93
|
+
Deletes user's state and returns it
|
|
94
|
+
"""
|
|
95
|
+
return self.storage.clear_state(self.user_id)
|
|
96
|
+
|
|
97
|
+
def clear_data(self) -> Any:
|
|
98
|
+
"""
|
|
99
|
+
Deletes user's data and returns it
|
|
100
|
+
"""
|
|
101
|
+
return self.storage.clear_data(self.user_id)
|
|
102
|
+
|
|
103
|
+
def clear(self):
|
|
104
|
+
"""
|
|
105
|
+
Clears user's state and data
|
|
106
|
+
"""
|
|
107
|
+
self.storage.clear(self.user_id)
|