hidmart 0.2.0__tar.gz
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.
- hidmart-0.2.0/PKG-INFO +36 -0
- hidmart-0.2.0/README.md +24 -0
- hidmart-0.2.0/hidmart/__init__.py +29 -0
- hidmart-0.2.0/hidmart/bot.py +425 -0
- hidmart-0.2.0/hidmart/client.py +357 -0
- hidmart-0.2.0/hidmart/exceptions.py +25 -0
- hidmart-0.2.0/hidmart/handlers.py +58 -0
- hidmart-0.2.0/hidmart/types.py +139 -0
- hidmart-0.2.0/hidmart.egg-info/PKG-INFO +36 -0
- hidmart-0.2.0/hidmart.egg-info/SOURCES.txt +14 -0
- hidmart-0.2.0/hidmart.egg-info/dependency_links.txt +1 -0
- hidmart-0.2.0/hidmart.egg-info/requires.txt +1 -0
- hidmart-0.2.0/hidmart.egg-info/top_level.txt +1 -0
- hidmart-0.2.0/pyproject.toml +26 -0
- hidmart-0.2.0/setup.cfg +4 -0
- hidmart-0.2.0/tests/test_basic.py +69 -0
hidmart-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hidmart
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Async Python framework for building token-based bots for Bale Messenger
|
|
5
|
+
Author: HidMart Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/programmersatlantis-hash/HidMart
|
|
8
|
+
Project-URL: Repository, https://github.com/programmersatlantis-hash/HidMart
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
12
|
+
|
|
13
|
+
# HidMart
|
|
14
|
+
|
|
15
|
+
<div align="center">
|
|
16
|
+
|
|
17
|
+
# HidMart
|
|
18
|
+
|
|
19
|
+
### Async Python Framework for Bale Messenger Bots
|
|
20
|
+
|
|
21
|
+
[](https://www.python.org/)
|
|
22
|
+
[](https://github.com/programmersatlantis-hash/HidMart)
|
|
23
|
+
[](https://github.com/programmersatlantis-hash/HidMart)
|
|
24
|
+
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
Install the latest version of **HidMart** directly from GitHub.
|
|
32
|
+
|
|
33
|
+
### Install with pip
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install https://github.com/programmersatlantis-hash/HidMart/archive/main.zip --force-reinstall
|
hidmart-0.2.0/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# HidMart
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
# HidMart
|
|
6
|
+
|
|
7
|
+
### Async Python Framework for Bale Messenger Bots
|
|
8
|
+
|
|
9
|
+
[](https://www.python.org/)
|
|
10
|
+
[](https://github.com/programmersatlantis-hash/HidMart)
|
|
11
|
+
[](https://github.com/programmersatlantis-hash/HidMart)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Install the latest version of **HidMart** directly from GitHub.
|
|
20
|
+
|
|
21
|
+
### Install with pip
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install https://github.com/programmersatlantis-hash/HidMart/archive/main.zip --force-reinstall
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from .bot import Bot
|
|
2
|
+
|
|
3
|
+
from .types import (
|
|
4
|
+
Message,
|
|
5
|
+
User,
|
|
6
|
+
Chat,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
HidMartError,
|
|
11
|
+
APIError,
|
|
12
|
+
NetworkError,
|
|
13
|
+
InvalidTokenError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__version__ = "0.2.0"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Bot",
|
|
22
|
+
"Message",
|
|
23
|
+
"User",
|
|
24
|
+
"Chat",
|
|
25
|
+
"HidMartError",
|
|
26
|
+
"APIError",
|
|
27
|
+
"NetworkError",
|
|
28
|
+
"InvalidTokenError",
|
|
29
|
+
]
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from .client import BaleClient
|
|
5
|
+
from .types import Message
|
|
6
|
+
from .handlers import (
|
|
7
|
+
CommandHandler,
|
|
8
|
+
MessageHandler,
|
|
9
|
+
TextHandler,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("hidmart")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Bot:
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
token: str,
|
|
21
|
+
poll_interval: float = 1.0,
|
|
22
|
+
timeout: int = 25,
|
|
23
|
+
):
|
|
24
|
+
|
|
25
|
+
if not token:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
"Bot token is required"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
self.token = token
|
|
31
|
+
|
|
32
|
+
self.poll_interval = poll_interval
|
|
33
|
+
self.timeout = timeout
|
|
34
|
+
|
|
35
|
+
self.client = BaleClient(
|
|
36
|
+
token
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
self.handlers = []
|
|
40
|
+
|
|
41
|
+
self.running = False
|
|
42
|
+
|
|
43
|
+
self.offset = None
|
|
44
|
+
|
|
45
|
+
self.me = None
|
|
46
|
+
|
|
47
|
+
# -------------------------
|
|
48
|
+
# Handlers
|
|
49
|
+
# -------------------------
|
|
50
|
+
|
|
51
|
+
def on_command(self, *commands):
|
|
52
|
+
|
|
53
|
+
if not commands:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
"At least one command is required"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def decorator(callback):
|
|
59
|
+
|
|
60
|
+
for command in commands:
|
|
61
|
+
|
|
62
|
+
self.handlers.append(
|
|
63
|
+
CommandHandler(
|
|
64
|
+
command,
|
|
65
|
+
callback,
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return callback
|
|
70
|
+
|
|
71
|
+
return decorator
|
|
72
|
+
|
|
73
|
+
def on_message(self):
|
|
74
|
+
|
|
75
|
+
def decorator(callback):
|
|
76
|
+
|
|
77
|
+
self.handlers.append(
|
|
78
|
+
MessageHandler(
|
|
79
|
+
callback
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return callback
|
|
84
|
+
|
|
85
|
+
return decorator
|
|
86
|
+
|
|
87
|
+
def on_text(self, text):
|
|
88
|
+
|
|
89
|
+
def decorator(callback):
|
|
90
|
+
|
|
91
|
+
self.handlers.append(
|
|
92
|
+
TextHandler(
|
|
93
|
+
text,
|
|
94
|
+
callback,
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return callback
|
|
99
|
+
|
|
100
|
+
return decorator
|
|
101
|
+
|
|
102
|
+
# -------------------------
|
|
103
|
+
# API methods
|
|
104
|
+
# -------------------------
|
|
105
|
+
|
|
106
|
+
async def send_message(
|
|
107
|
+
self,
|
|
108
|
+
chat_id,
|
|
109
|
+
text,
|
|
110
|
+
**kwargs,
|
|
111
|
+
):
|
|
112
|
+
|
|
113
|
+
return await self.client.send_message(
|
|
114
|
+
chat_id,
|
|
115
|
+
text,
|
|
116
|
+
**kwargs,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
async def send_photo(
|
|
120
|
+
self,
|
|
121
|
+
chat_id,
|
|
122
|
+
photo,
|
|
123
|
+
caption=None,
|
|
124
|
+
**kwargs,
|
|
125
|
+
):
|
|
126
|
+
|
|
127
|
+
return await self.client.send_photo(
|
|
128
|
+
chat_id,
|
|
129
|
+
photo,
|
|
130
|
+
caption,
|
|
131
|
+
**kwargs,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
async def send_video(
|
|
135
|
+
self,
|
|
136
|
+
chat_id,
|
|
137
|
+
video,
|
|
138
|
+
caption=None,
|
|
139
|
+
**kwargs,
|
|
140
|
+
):
|
|
141
|
+
|
|
142
|
+
return await self.client.send_video(
|
|
143
|
+
chat_id,
|
|
144
|
+
video,
|
|
145
|
+
caption,
|
|
146
|
+
**kwargs,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
async def send_audio(
|
|
150
|
+
self,
|
|
151
|
+
chat_id,
|
|
152
|
+
audio,
|
|
153
|
+
caption=None,
|
|
154
|
+
**kwargs,
|
|
155
|
+
):
|
|
156
|
+
|
|
157
|
+
return await self.client.send_audio(
|
|
158
|
+
chat_id,
|
|
159
|
+
audio,
|
|
160
|
+
caption,
|
|
161
|
+
**kwargs,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
async def send_document(
|
|
165
|
+
self,
|
|
166
|
+
chat_id,
|
|
167
|
+
document,
|
|
168
|
+
caption=None,
|
|
169
|
+
**kwargs,
|
|
170
|
+
):
|
|
171
|
+
|
|
172
|
+
return await self.client.send_document(
|
|
173
|
+
chat_id,
|
|
174
|
+
document,
|
|
175
|
+
caption,
|
|
176
|
+
**kwargs,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
async def send_voice(
|
|
180
|
+
self,
|
|
181
|
+
chat_id,
|
|
182
|
+
voice,
|
|
183
|
+
caption=None,
|
|
184
|
+
**kwargs,
|
|
185
|
+
):
|
|
186
|
+
|
|
187
|
+
return await self.client.send_voice(
|
|
188
|
+
chat_id,
|
|
189
|
+
voice,
|
|
190
|
+
caption,
|
|
191
|
+
**kwargs,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
async def send_location(
|
|
195
|
+
self,
|
|
196
|
+
chat_id,
|
|
197
|
+
latitude,
|
|
198
|
+
longitude,
|
|
199
|
+
**kwargs,
|
|
200
|
+
):
|
|
201
|
+
|
|
202
|
+
return await self.client.send_location(
|
|
203
|
+
chat_id,
|
|
204
|
+
latitude,
|
|
205
|
+
longitude,
|
|
206
|
+
**kwargs,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
async def edit_message_text(
|
|
210
|
+
self,
|
|
211
|
+
chat_id,
|
|
212
|
+
message_id,
|
|
213
|
+
text,
|
|
214
|
+
**kwargs,
|
|
215
|
+
):
|
|
216
|
+
|
|
217
|
+
return await self.client.edit_message_text(
|
|
218
|
+
chat_id,
|
|
219
|
+
message_id,
|
|
220
|
+
text,
|
|
221
|
+
**kwargs,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
async def delete_message(
|
|
225
|
+
self,
|
|
226
|
+
chat_id,
|
|
227
|
+
message_id,
|
|
228
|
+
):
|
|
229
|
+
|
|
230
|
+
return await self.client.delete_message(
|
|
231
|
+
chat_id,
|
|
232
|
+
message_id,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
async def get_me(self):
|
|
236
|
+
|
|
237
|
+
self.me = await self.client.get_me()
|
|
238
|
+
|
|
239
|
+
return self.me
|
|
240
|
+
|
|
241
|
+
async def get_chat(self, chat_id):
|
|
242
|
+
|
|
243
|
+
return await self.client.get_chat(
|
|
244
|
+
chat_id
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
async def get_chat_member(
|
|
248
|
+
self,
|
|
249
|
+
chat_id,
|
|
250
|
+
user_id,
|
|
251
|
+
):
|
|
252
|
+
|
|
253
|
+
return await self.client.get_chat_member(
|
|
254
|
+
chat_id,
|
|
255
|
+
user_id,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
async def get_updates(
|
|
259
|
+
self,
|
|
260
|
+
offset=None,
|
|
261
|
+
timeout=None,
|
|
262
|
+
limit=None,
|
|
263
|
+
):
|
|
264
|
+
|
|
265
|
+
if timeout is None:
|
|
266
|
+
timeout = self.timeout
|
|
267
|
+
|
|
268
|
+
return await self.client.get_updates(
|
|
269
|
+
offset=offset,
|
|
270
|
+
timeout=timeout,
|
|
271
|
+
limit=limit,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# -------------------------
|
|
275
|
+
# Update processing
|
|
276
|
+
# -------------------------
|
|
277
|
+
|
|
278
|
+
async def process_update(
|
|
279
|
+
self,
|
|
280
|
+
update,
|
|
281
|
+
):
|
|
282
|
+
|
|
283
|
+
if not isinstance(update, dict):
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
message_data = update.get(
|
|
287
|
+
"message"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
if not message_data:
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
message = Message.from_dict(
|
|
294
|
+
message_data,
|
|
295
|
+
bot=self,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
for handler in self.handlers:
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
|
|
302
|
+
if hasattr(
|
|
303
|
+
handler,
|
|
304
|
+
"matches",
|
|
305
|
+
):
|
|
306
|
+
|
|
307
|
+
if not handler.matches(
|
|
308
|
+
message
|
|
309
|
+
):
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
await handler.run(
|
|
313
|
+
message
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
except Exception:
|
|
317
|
+
|
|
318
|
+
logger.exception(
|
|
319
|
+
"Handler error"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# -------------------------
|
|
323
|
+
# Polling
|
|
324
|
+
# -------------------------
|
|
325
|
+
|
|
326
|
+
async def polling(self):
|
|
327
|
+
|
|
328
|
+
self.running = True
|
|
329
|
+
|
|
330
|
+
logger.info(
|
|
331
|
+
"HidMart polling started"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
while self.running:
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
|
|
338
|
+
updates = await self.get_updates(
|
|
339
|
+
offset=self.offset
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if not updates:
|
|
343
|
+
|
|
344
|
+
await asyncio.sleep(
|
|
345
|
+
self.poll_interval
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
for update in updates:
|
|
351
|
+
|
|
352
|
+
update_id = update.get(
|
|
353
|
+
"update_id"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
if update_id is not None:
|
|
357
|
+
|
|
358
|
+
self.offset = (
|
|
359
|
+
update_id + 1
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
await self.process_update(
|
|
363
|
+
update
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
except asyncio.CancelledError:
|
|
367
|
+
|
|
368
|
+
break
|
|
369
|
+
|
|
370
|
+
except Exception:
|
|
371
|
+
|
|
372
|
+
logger.exception(
|
|
373
|
+
"Polling error"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
await asyncio.sleep(
|
|
377
|
+
3
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
logger.info(
|
|
381
|
+
"HidMart polling stopped"
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
async def start(self):
|
|
385
|
+
|
|
386
|
+
self.running = True
|
|
387
|
+
|
|
388
|
+
self.me = await self.get_me()
|
|
389
|
+
|
|
390
|
+
logger.info(
|
|
391
|
+
"Bot started: %s",
|
|
392
|
+
self.me,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
await self.polling()
|
|
396
|
+
|
|
397
|
+
async def stop(self):
|
|
398
|
+
|
|
399
|
+
self.running = False
|
|
400
|
+
|
|
401
|
+
await self.client.close()
|
|
402
|
+
|
|
403
|
+
async def _run(self):
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
|
|
407
|
+
await self.start()
|
|
408
|
+
|
|
409
|
+
finally:
|
|
410
|
+
|
|
411
|
+
await self.client.close()
|
|
412
|
+
|
|
413
|
+
def run(self):
|
|
414
|
+
|
|
415
|
+
try:
|
|
416
|
+
|
|
417
|
+
asyncio.run(
|
|
418
|
+
self._run()
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
except KeyboardInterrupt:
|
|
422
|
+
|
|
423
|
+
logger.info(
|
|
424
|
+
"HidMart stopped by user"
|
|
425
|
+
)
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
APIError,
|
|
8
|
+
NetworkError,
|
|
9
|
+
InvalidTokenError,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaleClient:
|
|
14
|
+
"""
|
|
15
|
+
Low-level asynchronous client for Bale Bot API.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
token: str,
|
|
21
|
+
base_url: str = "https://tapi.bale.ai",
|
|
22
|
+
timeout: float = 30.0,
|
|
23
|
+
max_retries: int = 3,
|
|
24
|
+
):
|
|
25
|
+
if not token:
|
|
26
|
+
raise ValueError("Bot token is required")
|
|
27
|
+
|
|
28
|
+
self.token = token
|
|
29
|
+
self.base_url = base_url.rstrip("/")
|
|
30
|
+
self.timeout = timeout
|
|
31
|
+
self.max_retries = max_retries
|
|
32
|
+
|
|
33
|
+
self.http = httpx.AsyncClient(
|
|
34
|
+
timeout=httpx.Timeout(timeout)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def _url(self, method: str) -> str:
|
|
38
|
+
return f"{self.base_url}/bot{self.token}/{method}"
|
|
39
|
+
|
|
40
|
+
async def call(
|
|
41
|
+
self,
|
|
42
|
+
method: str,
|
|
43
|
+
data: Optional[Dict[str, Any]] = None,
|
|
44
|
+
) -> Any:
|
|
45
|
+
"""
|
|
46
|
+
Call a Bale Bot API method.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
payload = data or {}
|
|
50
|
+
|
|
51
|
+
last_error = None
|
|
52
|
+
|
|
53
|
+
for attempt in range(self.max_retries + 1):
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
response = await self.http.post(
|
|
57
|
+
self._url(method),
|
|
58
|
+
json=payload,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
response.raise_for_status()
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
result = response.json()
|
|
65
|
+
except ValueError as exc:
|
|
66
|
+
raise APIError(
|
|
67
|
+
"Invalid JSON response from Bale API"
|
|
68
|
+
) from exc
|
|
69
|
+
|
|
70
|
+
if not result.get("ok", False):
|
|
71
|
+
|
|
72
|
+
error_code = result.get("error_code")
|
|
73
|
+
description = result.get(
|
|
74
|
+
"description",
|
|
75
|
+
"Bale API request failed",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if error_code == 401:
|
|
79
|
+
raise InvalidTokenError(
|
|
80
|
+
description
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
raise APIError(
|
|
84
|
+
description=description,
|
|
85
|
+
error_code=error_code,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return result.get("result")
|
|
89
|
+
|
|
90
|
+
except InvalidTokenError:
|
|
91
|
+
raise
|
|
92
|
+
|
|
93
|
+
except APIError:
|
|
94
|
+
raise
|
|
95
|
+
|
|
96
|
+
except httpx.HTTPStatusError as exc:
|
|
97
|
+
last_error = exc
|
|
98
|
+
|
|
99
|
+
if attempt >= self.max_retries:
|
|
100
|
+
raise NetworkError(
|
|
101
|
+
f"HTTP error: {exc.response.status_code}"
|
|
102
|
+
) from exc
|
|
103
|
+
|
|
104
|
+
except httpx.RequestError as exc:
|
|
105
|
+
last_error = exc
|
|
106
|
+
|
|
107
|
+
if attempt >= self.max_retries:
|
|
108
|
+
raise NetworkError(
|
|
109
|
+
f"Network error: {exc}"
|
|
110
|
+
) from exc
|
|
111
|
+
|
|
112
|
+
except httpx.HTTPError as exc:
|
|
113
|
+
last_error = exc
|
|
114
|
+
|
|
115
|
+
if attempt >= self.max_retries:
|
|
116
|
+
raise NetworkError(
|
|
117
|
+
f"HTTP error: {exc}"
|
|
118
|
+
) from exc
|
|
119
|
+
|
|
120
|
+
if attempt < self.max_retries:
|
|
121
|
+
await asyncio.sleep(2 ** attempt)
|
|
122
|
+
|
|
123
|
+
raise NetworkError(
|
|
124
|
+
f"Request failed: {last_error}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
async def get_me(self):
|
|
128
|
+
return await self.call("getMe")
|
|
129
|
+
|
|
130
|
+
async def get_updates(
|
|
131
|
+
self,
|
|
132
|
+
offset: Optional[int] = None,
|
|
133
|
+
timeout: int = 25,
|
|
134
|
+
limit: Optional[int] = None,
|
|
135
|
+
):
|
|
136
|
+
data = {
|
|
137
|
+
"timeout": timeout,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if offset is not None:
|
|
141
|
+
data["offset"] = offset
|
|
142
|
+
|
|
143
|
+
if limit is not None:
|
|
144
|
+
data["limit"] = limit
|
|
145
|
+
|
|
146
|
+
return await self.call(
|
|
147
|
+
"getUpdates",
|
|
148
|
+
data,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
async def send_message(
|
|
152
|
+
self,
|
|
153
|
+
chat_id,
|
|
154
|
+
text: str,
|
|
155
|
+
**kwargs,
|
|
156
|
+
):
|
|
157
|
+
data = {
|
|
158
|
+
"chat_id": chat_id,
|
|
159
|
+
"text": text,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
data.update(kwargs)
|
|
163
|
+
|
|
164
|
+
return await self.call(
|
|
165
|
+
"sendMessage",
|
|
166
|
+
data,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
async def send_photo(
|
|
170
|
+
self,
|
|
171
|
+
chat_id,
|
|
172
|
+
photo,
|
|
173
|
+
caption: Optional[str] = None,
|
|
174
|
+
**kwargs,
|
|
175
|
+
):
|
|
176
|
+
data = {
|
|
177
|
+
"chat_id": chat_id,
|
|
178
|
+
"photo": photo,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if caption is not None:
|
|
182
|
+
data["caption"] = caption
|
|
183
|
+
|
|
184
|
+
data.update(kwargs)
|
|
185
|
+
|
|
186
|
+
return await self.call(
|
|
187
|
+
"sendPhoto",
|
|
188
|
+
data,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
async def send_video(
|
|
192
|
+
self,
|
|
193
|
+
chat_id,
|
|
194
|
+
video,
|
|
195
|
+
caption: Optional[str] = None,
|
|
196
|
+
**kwargs,
|
|
197
|
+
):
|
|
198
|
+
data = {
|
|
199
|
+
"chat_id": chat_id,
|
|
200
|
+
"video": video,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if caption is not None:
|
|
204
|
+
data["caption"] = caption
|
|
205
|
+
|
|
206
|
+
data.update(kwargs)
|
|
207
|
+
|
|
208
|
+
return await self.call(
|
|
209
|
+
"sendVideo",
|
|
210
|
+
data,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
async def send_audio(
|
|
214
|
+
self,
|
|
215
|
+
chat_id,
|
|
216
|
+
audio,
|
|
217
|
+
caption: Optional[str] = None,
|
|
218
|
+
**kwargs,
|
|
219
|
+
):
|
|
220
|
+
data = {
|
|
221
|
+
"chat_id": chat_id,
|
|
222
|
+
"audio": audio,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if caption is not None:
|
|
226
|
+
data["caption"] = caption
|
|
227
|
+
|
|
228
|
+
data.update(kwargs)
|
|
229
|
+
|
|
230
|
+
return await self.call(
|
|
231
|
+
"sendAudio",
|
|
232
|
+
data,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
async def send_document(
|
|
236
|
+
self,
|
|
237
|
+
chat_id,
|
|
238
|
+
document,
|
|
239
|
+
caption: Optional[str] = None,
|
|
240
|
+
**kwargs,
|
|
241
|
+
):
|
|
242
|
+
data = {
|
|
243
|
+
"chat_id": chat_id,
|
|
244
|
+
"document": document,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if caption is not None:
|
|
248
|
+
data["caption"] = caption
|
|
249
|
+
|
|
250
|
+
data.update(kwargs)
|
|
251
|
+
|
|
252
|
+
return await self.call(
|
|
253
|
+
"sendDocument",
|
|
254
|
+
data,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
async def send_voice(
|
|
258
|
+
self,
|
|
259
|
+
chat_id,
|
|
260
|
+
voice,
|
|
261
|
+
caption: Optional[str] = None,
|
|
262
|
+
**kwargs,
|
|
263
|
+
):
|
|
264
|
+
data = {
|
|
265
|
+
"chat_id": chat_id,
|
|
266
|
+
"voice": voice,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if caption is not None:
|
|
270
|
+
data["caption"] = caption
|
|
271
|
+
|
|
272
|
+
data.update(kwargs)
|
|
273
|
+
|
|
274
|
+
return await self.call(
|
|
275
|
+
"sendVoice",
|
|
276
|
+
data,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
async def send_location(
|
|
280
|
+
self,
|
|
281
|
+
chat_id,
|
|
282
|
+
latitude: float,
|
|
283
|
+
longitude: float,
|
|
284
|
+
**kwargs,
|
|
285
|
+
):
|
|
286
|
+
data = {
|
|
287
|
+
"chat_id": chat_id,
|
|
288
|
+
"latitude": latitude,
|
|
289
|
+
"longitude": longitude,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
data.update(kwargs)
|
|
293
|
+
|
|
294
|
+
return await self.call(
|
|
295
|
+
"sendLocation",
|
|
296
|
+
data,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
async def edit_message_text(
|
|
300
|
+
self,
|
|
301
|
+
chat_id,
|
|
302
|
+
message_id,
|
|
303
|
+
text: str,
|
|
304
|
+
**kwargs,
|
|
305
|
+
):
|
|
306
|
+
data = {
|
|
307
|
+
"chat_id": chat_id,
|
|
308
|
+
"message_id": message_id,
|
|
309
|
+
"text": text,
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
data.update(kwargs)
|
|
313
|
+
|
|
314
|
+
return await self.call(
|
|
315
|
+
"editMessageText",
|
|
316
|
+
data,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
async def delete_message(
|
|
320
|
+
self,
|
|
321
|
+
chat_id,
|
|
322
|
+
message_id,
|
|
323
|
+
):
|
|
324
|
+
return await self.call(
|
|
325
|
+
"deleteMessage",
|
|
326
|
+
{
|
|
327
|
+
"chat_id": chat_id,
|
|
328
|
+
"message_id": message_id,
|
|
329
|
+
},
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
async def get_chat(
|
|
333
|
+
self,
|
|
334
|
+
chat_id,
|
|
335
|
+
):
|
|
336
|
+
return await self.call(
|
|
337
|
+
"getChat",
|
|
338
|
+
{
|
|
339
|
+
"chat_id": chat_id,
|
|
340
|
+
},
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
async def get_chat_member(
|
|
344
|
+
self,
|
|
345
|
+
chat_id,
|
|
346
|
+
user_id,
|
|
347
|
+
):
|
|
348
|
+
return await self.call(
|
|
349
|
+
"getChatMember",
|
|
350
|
+
{
|
|
351
|
+
"chat_id": chat_id,
|
|
352
|
+
"user_id": user_id,
|
|
353
|
+
},
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
async def close(self):
|
|
357
|
+
await self.http.aclose()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class HidMartError(Exception):
|
|
2
|
+
"""Base exception for HidMart."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class APIError(HidMartError):
|
|
6
|
+
"""Raised when Bale API returns an error."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, description=None, error_code=None):
|
|
9
|
+
self.description = description or "Unknown API error"
|
|
10
|
+
self.error_code = error_code
|
|
11
|
+
|
|
12
|
+
if error_code is not None:
|
|
13
|
+
message = f"[{error_code}] {self.description}"
|
|
14
|
+
else:
|
|
15
|
+
message = self.description
|
|
16
|
+
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NetworkError(HidMartError):
|
|
21
|
+
"""Raised when a network request fails."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class InvalidTokenError(HidMartError):
|
|
25
|
+
"""Raised when the bot token is invalid."""
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Handler:
|
|
5
|
+
|
|
6
|
+
def __init__(self, callback):
|
|
7
|
+
self.callback = callback
|
|
8
|
+
|
|
9
|
+
async def run(self, message):
|
|
10
|
+
result = self.callback(message)
|
|
11
|
+
|
|
12
|
+
if inspect.isawaitable(result):
|
|
13
|
+
await result
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CommandHandler(Handler):
|
|
17
|
+
|
|
18
|
+
def __init__(self, command, callback):
|
|
19
|
+
super().__init__(callback)
|
|
20
|
+
|
|
21
|
+
self.commands = {
|
|
22
|
+
command.lstrip("/").lower()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def matches(self, message):
|
|
26
|
+
|
|
27
|
+
if not message.text:
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
text = message.text.strip()
|
|
31
|
+
|
|
32
|
+
if not text.startswith("/"):
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
command = text[1:].split()[0].lower()
|
|
36
|
+
|
|
37
|
+
return command in self.commands
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MessageHandler(Handler):
|
|
41
|
+
|
|
42
|
+
def matches(self, message):
|
|
43
|
+
return bool(message.text)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TextHandler(Handler):
|
|
47
|
+
|
|
48
|
+
def __init__(self, text, callback):
|
|
49
|
+
super().__init__(callback)
|
|
50
|
+
|
|
51
|
+
self.text = text
|
|
52
|
+
|
|
53
|
+
def matches(self, message):
|
|
54
|
+
|
|
55
|
+
if not message.text:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
return message.text == self.text
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional, Any, Dict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class User:
|
|
7
|
+
|
|
8
|
+
id: int
|
|
9
|
+
|
|
10
|
+
first_name: Optional[str] = None
|
|
11
|
+
last_name: Optional[str] = None
|
|
12
|
+
username: Optional[str] = None
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def from_dict(cls, data: Dict[str, Any]):
|
|
16
|
+
|
|
17
|
+
return cls(
|
|
18
|
+
id=data.get("id"),
|
|
19
|
+
first_name=data.get("first_name"),
|
|
20
|
+
last_name=data.get("last_name"),
|
|
21
|
+
username=data.get("username"),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def full_name(self):
|
|
26
|
+
|
|
27
|
+
parts = []
|
|
28
|
+
|
|
29
|
+
if self.first_name:
|
|
30
|
+
parts.append(self.first_name)
|
|
31
|
+
|
|
32
|
+
if self.last_name:
|
|
33
|
+
parts.append(self.last_name)
|
|
34
|
+
|
|
35
|
+
return " ".join(parts)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Chat:
|
|
40
|
+
|
|
41
|
+
id: int
|
|
42
|
+
|
|
43
|
+
type: Optional[str] = None
|
|
44
|
+
title: Optional[str] = None
|
|
45
|
+
username: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_dict(cls, data: Dict[str, Any]):
|
|
49
|
+
|
|
50
|
+
return cls(
|
|
51
|
+
id=data.get("id"),
|
|
52
|
+
type=data.get("type"),
|
|
53
|
+
title=data.get("title"),
|
|
54
|
+
username=data.get("username"),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def is_private(self):
|
|
59
|
+
|
|
60
|
+
return self.type == "private"
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_group(self):
|
|
64
|
+
|
|
65
|
+
return self.type in (
|
|
66
|
+
"group",
|
|
67
|
+
"supergroup",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class Message:
|
|
73
|
+
|
|
74
|
+
message_id: int
|
|
75
|
+
|
|
76
|
+
chat: Chat
|
|
77
|
+
|
|
78
|
+
from_user: Optional[User] = None
|
|
79
|
+
|
|
80
|
+
text: Optional[str] = None
|
|
81
|
+
|
|
82
|
+
raw: Optional[Dict[str, Any]] = None
|
|
83
|
+
|
|
84
|
+
bot: Any = None
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def from_dict(
|
|
88
|
+
cls,
|
|
89
|
+
data: Dict[str, Any],
|
|
90
|
+
bot=None,
|
|
91
|
+
):
|
|
92
|
+
|
|
93
|
+
user_data = data.get("from")
|
|
94
|
+
chat_data = data.get("chat", {})
|
|
95
|
+
|
|
96
|
+
return cls(
|
|
97
|
+
message_id=data.get("message_id"),
|
|
98
|
+
chat=Chat.from_dict(chat_data),
|
|
99
|
+
from_user=(
|
|
100
|
+
User.from_dict(user_data)
|
|
101
|
+
if user_data
|
|
102
|
+
else None
|
|
103
|
+
),
|
|
104
|
+
text=data.get("text"),
|
|
105
|
+
raw=data,
|
|
106
|
+
bot=bot,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def id(self):
|
|
111
|
+
|
|
112
|
+
return self.message_id
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def sender(self):
|
|
116
|
+
|
|
117
|
+
return self.from_user
|
|
118
|
+
|
|
119
|
+
async def reply(self, text: str, **kwargs):
|
|
120
|
+
|
|
121
|
+
return await self.bot.send_message(
|
|
122
|
+
chat_id=self.chat.id,
|
|
123
|
+
text=text,
|
|
124
|
+
**kwargs,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
async def answer(self, text: str, **kwargs):
|
|
128
|
+
|
|
129
|
+
return await self.reply(
|
|
130
|
+
text,
|
|
131
|
+
**kwargs,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
async def delete(self):
|
|
135
|
+
|
|
136
|
+
return await self.bot.delete_message(
|
|
137
|
+
chat_id=self.chat.id,
|
|
138
|
+
message_id=self.message_id,
|
|
139
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hidmart
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Async Python framework for building token-based bots for Bale Messenger
|
|
5
|
+
Author: HidMart Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/programmersatlantis-hash/HidMart
|
|
8
|
+
Project-URL: Repository, https://github.com/programmersatlantis-hash/HidMart
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
12
|
+
|
|
13
|
+
# HidMart
|
|
14
|
+
|
|
15
|
+
<div align="center">
|
|
16
|
+
|
|
17
|
+
# HidMart
|
|
18
|
+
|
|
19
|
+
### Async Python Framework for Bale Messenger Bots
|
|
20
|
+
|
|
21
|
+
[](https://www.python.org/)
|
|
22
|
+
[](https://github.com/programmersatlantis-hash/HidMart)
|
|
23
|
+
[](https://github.com/programmersatlantis-hash/HidMart)
|
|
24
|
+
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
Install the latest version of **HidMart** directly from GitHub.
|
|
32
|
+
|
|
33
|
+
### Install with pip
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install https://github.com/programmersatlantis-hash/HidMart/archive/main.zip --force-reinstall
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
hidmart/__init__.py
|
|
4
|
+
hidmart/bot.py
|
|
5
|
+
hidmart/client.py
|
|
6
|
+
hidmart/exceptions.py
|
|
7
|
+
hidmart/handlers.py
|
|
8
|
+
hidmart/types.py
|
|
9
|
+
hidmart.egg-info/PKG-INFO
|
|
10
|
+
hidmart.egg-info/SOURCES.txt
|
|
11
|
+
hidmart.egg-info/dependency_links.txt
|
|
12
|
+
hidmart.egg-info/requires.txt
|
|
13
|
+
hidmart.egg-info/top_level.txt
|
|
14
|
+
tests/test_basic.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
httpx<1.0,>=0.27
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hidmart
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hidmart"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Async Python framework for building token-based bots for Bale Messenger"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "HidMart Team"}
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
dependencies = [
|
|
18
|
+
"httpx>=0.27,<1.0"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/programmersatlantis-hash/HidMart"
|
|
23
|
+
Repository = "https://github.com/programmersatlantis-hash/HidMart"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
include = ["hidmart*"]
|
hidmart-0.2.0/setup.cfg
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from hidmart import (
|
|
2
|
+
Bot,
|
|
3
|
+
Message,
|
|
4
|
+
User,
|
|
5
|
+
Chat,
|
|
6
|
+
APIError,
|
|
7
|
+
NetworkError,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_bot_creation():
|
|
12
|
+
|
|
13
|
+
bot = Bot(
|
|
14
|
+
"TEST_TOKEN"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
assert bot.token == "TEST_TOKEN"
|
|
18
|
+
|
|
19
|
+
assert bot.handlers == []
|
|
20
|
+
|
|
21
|
+
assert bot.offset is None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_user():
|
|
25
|
+
|
|
26
|
+
user = User(
|
|
27
|
+
id=123,
|
|
28
|
+
first_name="Ali",
|
|
29
|
+
last_name="Test",
|
|
30
|
+
username="ali",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
assert user.id == 123
|
|
34
|
+
|
|
35
|
+
assert user.username == "ali"
|
|
36
|
+
|
|
37
|
+
assert user.full_name == "Ali Test"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_chat():
|
|
41
|
+
|
|
42
|
+
chat = Chat(
|
|
43
|
+
id=123,
|
|
44
|
+
type="private",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
assert chat.id == 123
|
|
48
|
+
|
|
49
|
+
assert chat.is_private is True
|
|
50
|
+
|
|
51
|
+
assert chat.is_group is False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_message():
|
|
55
|
+
|
|
56
|
+
chat = Chat(
|
|
57
|
+
id=123,
|
|
58
|
+
type="private",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
message = Message(
|
|
62
|
+
message_id=1,
|
|
63
|
+
chat=chat,
|
|
64
|
+
text="Hello",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assert message.id == 1
|
|
68
|
+
|
|
69
|
+
assert message.text == "Hello"
|