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/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)