pyrobale 0.2.9.4__py3-none-any.whl → 0.3.5__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.
Files changed (47) hide show
  1. pyrobale/__init__.py +3 -0
  2. pyrobale/client/__init__.py +574 -0
  3. pyrobale/exceptions/__init__.py +0 -0
  4. pyrobale/exceptions/common.py +10 -0
  5. pyrobale/objects/__init__.py +86 -0
  6. pyrobale/objects/animation.py +33 -0
  7. pyrobale/objects/audio.py +24 -0
  8. pyrobale/objects/callbackquery.py +42 -0
  9. pyrobale/objects/chat.py +463 -0
  10. pyrobale/objects/chatmember.py +162 -0
  11. pyrobale/objects/chatphoto.py +18 -0
  12. pyrobale/objects/contact.py +16 -0
  13. pyrobale/objects/copytextbutton.py +5 -0
  14. pyrobale/objects/document.py +26 -0
  15. pyrobale/objects/enums.py +28 -0
  16. pyrobale/objects/file.py +15 -0
  17. pyrobale/objects/inlinekeyboardbutton.py +24 -0
  18. pyrobale/objects/inlinekeyboardmarkup.py +84 -0
  19. pyrobale/objects/inputfile.py +17 -0
  20. pyrobale/objects/inputmedias.py +166 -0
  21. pyrobale/objects/invoice.py +14 -0
  22. pyrobale/objects/keyboardbutton.py +19 -0
  23. pyrobale/objects/labeledprice.py +8 -0
  24. pyrobale/objects/location.py +4 -0
  25. pyrobale/objects/message.py +368 -0
  26. pyrobale/objects/messageid.py +3 -0
  27. pyrobale/objects/photosize.py +9 -0
  28. pyrobale/objects/precheckoutquery.py +21 -0
  29. pyrobale/objects/replykeyboardmarkup.py +54 -0
  30. pyrobale/objects/sticker.py +16 -0
  31. pyrobale/objects/stickerset.py +12 -0
  32. pyrobale/objects/successfulpayment.py +15 -0
  33. pyrobale/objects/update.py +23 -0
  34. pyrobale/objects/user.py +20 -0
  35. pyrobale/objects/utils.py +28 -0
  36. pyrobale/objects/video.py +20 -0
  37. pyrobale/objects/voice.py +4 -0
  38. pyrobale/objects/webappdata.py +3 -0
  39. pyrobale/objects/webappinfo.py +3 -0
  40. pyrobale-0.3.5.dist-info/METADATA +176 -0
  41. pyrobale-0.3.5.dist-info/RECORD +43 -0
  42. pyrobale-0.3.5.dist-info/licenses/LICENSE +21 -0
  43. pyrobale-0.2.9.4.dist-info/METADATA +0 -833
  44. pyrobale-0.2.9.4.dist-info/RECORD +0 -5
  45. pyrobale-0.2.9.4.dist-info/licenses/LICENSE +0 -678
  46. pyrobale.py +0 -2545
  47. {pyrobale-0.2.9.4.dist-info → pyrobale-0.3.5.dist-info}/WHEEL +0 -0
pyrobale.py DELETED
@@ -1,2545 +0,0 @@
1
- """
2
- PyRobale - A Python library for developing Bale messenger bots.
3
-
4
- Features:
5
- - Simple and fast implementation
6
- - Highly customizable and feature-rich
7
- - Modern and regularly updated
8
- - Built-in database management
9
- - User-friendly API
10
- - Gentle learning curve
11
- - Thread-safe operations
12
- - Robust error handling
13
- """
14
- from typing import Optional, Dict, Any, List, Union
15
- import requests
16
- from typing import Union, Optional, Dict, Any, List, Tuple, Callable
17
- import threading
18
- import traceback
19
- import sqlite3
20
- import time
21
- import sys
22
- import collections
23
- import inspect
24
- import os
25
- from typing import Union, Optional, Dict, Any, List, Tuple, Callable
26
- import sqlite3
27
- import json
28
- import traceback
29
- import asyncio
30
- import queue
31
-
32
- __version__ = '0.2.9.4'
33
-
34
-
35
- class BaleException(Exception):
36
- """Base exception for Bale API errors"""
37
-
38
- def __init__(self, message=None, error_code=None, response=None):
39
- self.message = message
40
- self.error_code = error_code
41
- self.response = response
42
-
43
- error_text = f"Error {error_code}: {message}" if error_code and message else message or str(
44
- error_code)
45
- super().__init__(error_text)
46
-
47
- def __str__(self):
48
- error_details = []
49
- if self.error_code:
50
- error_details.append(f"code={self.error_code}")
51
- if self.message:
52
- error_details.append(f"message='{self.message}'")
53
- details = ", ".join(error_details)
54
- return f"{self.__class__.__name__}({details})"
55
-
56
-
57
- class BaleAPIError(BaleException):
58
- """Exception raised when Bale API returns an error response"""
59
- pass
60
-
61
-
62
- class BaleNetworkError(BaleException):
63
- """Exception raised when network-related issues occur during API calls"""
64
- pass
65
-
66
-
67
- class BaleAuthError(BaleException):
68
- """Exception raised when authentication fails or token is invalid"""
69
- pass
70
-
71
-
72
- class BaleValidationError(BaleException):
73
- """Exception raised when request data fails validation"""
74
- pass
75
-
76
-
77
- class BaleTimeoutError(BaleException):
78
- """Exception raised when API request times out"""
79
- pass
80
-
81
-
82
- class BaleNotFoundError(BaleException):
83
- """Exception raised when requested resource is not found (404)"""
84
- pass
85
-
86
-
87
- class BaleForbiddenError(BaleException):
88
- """Exception raised when access to resource is forbidden (403)"""
89
- pass
90
-
91
-
92
- class BaleServerError(BaleException):
93
- """Exception raised when server encounters an error (5xx)"""
94
- pass
95
-
96
-
97
- class BaleRateLimitError(BaleException):
98
- """Exception raised when API rate limit is exceeded (429)"""
99
- pass
100
-
101
-
102
- class BaleTokenNotFoundError(BaleException):
103
- """Exception raised when required API token is missing"""
104
- pass
105
-
106
-
107
- class BaleUnknownError(BaleException):
108
- """Exception raised for unexpected or unknown errors"""
109
- pass
110
-
111
-
112
- class ChatActions:
113
- """Represents different chat action states that can be sent to Bale"""
114
- TYPING: str = 'typing'
115
- PHOTO: str = 'upload_photo'
116
- VIDEO: str = 'record_video'
117
- CHOOSE_STICKER: str = 'choose_sticker'
118
-
119
-
120
- class Sticker:
121
- def __init__(self, data: dict):
122
- self.file_id = data.get('file_id')
123
- self.file_unique_id = data.get('file_unique_id')
124
- self.type = data.get('type')
125
- self.width = data.get('width')
126
- self.height = data.get('height')
127
- self.file_size = data.get('file_size')
128
-
129
-
130
- class StickerSet:
131
- def __init__(self, data: dict):
132
- self.name = data.get('name')
133
- self.title = data.get('title')
134
- self.stickers = [Sticker(sticker)
135
- for sticker in data.get('stickers', [])]
136
- self.thumbnail = data.get('thumbnail')
137
-
138
-
139
- class DataBase:
140
-
141
- """
142
- Database class for managing key-value pairs in a SQLite database.
143
- """
144
-
145
- def __init__(self, name):
146
- self.name = name
147
- self.conn = None
148
- self.cursor = None
149
- self._initialize_db()
150
-
151
- def _initialize_db(self):
152
- self.conn = sqlite3.connect(self.name)
153
- self.cursor = self.conn.cursor()
154
- self.cursor.execute('''CREATE TABLE IF NOT EXISTS key_value_store
155
- (key TEXT PRIMARY KEY, value TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
156
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
157
- self.conn.commit()
158
-
159
- def __enter__(self):
160
- return self
161
-
162
- def __exit__(self, exc_type, exc_val, exc_tb):
163
- self.close()
164
-
165
- def close(self):
166
- if self.conn:
167
- self.conn.close()
168
- self.conn = None
169
- self.cursor = None
170
-
171
- def read_database(self, include_timestamps=False):
172
- if not self.conn:
173
- self._initialize_db()
174
- if include_timestamps:
175
- self.cursor.execute(
176
- "SELECT key, value, created_at, updated_at FROM key_value_store")
177
- rows = self.cursor.fetchall()
178
- return {
179
- key: {
180
- 'value': json.loads(value),
181
- 'created_at': created,
182
- 'updated_at': updated} for key,
183
- value,
184
- created,
185
- updated in rows}
186
- else:
187
- self.cursor.execute("SELECT key, value FROM key_value_store")
188
- rows = self.cursor.fetchall()
189
- return {key: json.loads(value) for key, value in rows}
190
-
191
- def write_database(self, data_dict):
192
- if not self.conn:
193
- self._initialize_db()
194
- for key, value in data_dict.items():
195
- self.cursor.execute("""
196
- INSERT INTO key_value_store (key, value, updated_at)
197
- VALUES (?, ?, CURRENT_TIMESTAMP)
198
- ON CONFLICT(key) DO UPDATE SET
199
- value=excluded.value, updated_at=CURRENT_TIMESTAMP""",
200
- (key, json.dumps(value, default=str)))
201
- self.conn.commit()
202
-
203
- def read_key(self, key: str, default=None):
204
- if not self.conn:
205
- self._initialize_db()
206
- self.cursor.execute(
207
- "SELECT value FROM key_value_store WHERE key = ?", (key,))
208
- result = self.cursor.fetchone()
209
- return json.loads(result[0]) if result else default
210
-
211
- def write_key(self, key: str, value):
212
- if not self.conn:
213
- self._initialize_db()
214
- self.cursor.execute("""
215
- INSERT INTO key_value_store (key, value, updated_at)
216
- VALUES (?, ?, CURRENT_TIMESTAMP)
217
- ON CONFLICT(key) DO UPDATE SET
218
- value=excluded.value, updated_at=CURRENT_TIMESTAMP""",
219
- (key, json.dumps(value, default=str)))
220
- self.conn.commit()
221
-
222
- def delete_key(self, key: str):
223
- if not self.conn:
224
- self._initialize_db()
225
- self.cursor.execute(
226
- "DELETE FROM key_value_store WHERE key = ?", (key,))
227
- self.conn.commit()
228
-
229
- def keys(self):
230
- if not self.conn:
231
- self._initialize_db()
232
- self.cursor.execute("SELECT key FROM key_value_store")
233
- return [row[0] for row in self.cursor.fetchall()]
234
-
235
- def clear(self):
236
- if not self.conn:
237
- self._initialize_db()
238
- self.cursor.execute("DELETE FROM key_value_store")
239
- self.conn.commit()
240
-
241
- def get_metadata(self, key: str):
242
- if not self.conn:
243
- self._initialize_db()
244
- self.cursor.execute("""
245
- SELECT created_at, updated_at
246
- FROM key_value_store
247
- WHERE key = ?""", (key,))
248
- result = self.cursor.fetchone()
249
- return {
250
- 'created_at': result[0],
251
- 'updated_at': result[1]} if result else None
252
-
253
- def exists(self, key: str) -> bool:
254
- if not self.conn:
255
- self._initialize_db()
256
- self.cursor.execute(
257
- "SELECT 1 FROM key_value_store WHERE key = ?", (key,))
258
- return bool(self.cursor.fetchone())
259
-
260
-
261
- class ChatMember:
262
- def __init__(self, client: 'Client', data: Dict[str, Any]):
263
- if data:
264
- self.status = data.get('status')
265
- self.user = User(
266
- client, {
267
- 'ok': True, 'result': data.get(
268
- 'user', {})})
269
- self.is_anonymous = data.get('is_anonymous')
270
- self.can_be_edited = data.get('can_be_edited')
271
- self.can_manage_chat = data.get('can_manage_chat')
272
- self.can_delete_messages = data.get('can_delete_messages')
273
- self.can_manage_video_chats = data.get('can_manage_video_chats')
274
- self.can_restrict_members = data.get('can_restrict_members')
275
- self.can_promote_members = data.get('can_promote_members')
276
- self.can_change_info = data.get('can_change_info')
277
- self.can_invite_users = data.get('can_invite_users')
278
- self.can_pin_messages = data.get('can_pin_messages')
279
- self.can_manage_topics = data.get('can_manage_topics')
280
- self.is_creator = data.get('status') == 'creator'
281
-
282
-
283
- class Chat:
284
- """Represents a chat conversation"""
285
-
286
- def __init__(self, client: 'Client', data: Dict[str, Any]):
287
- if not data.get('ok'):
288
- raise BaleException(
289
- f"API request failed: {traceback.format_exc()}")
290
- self.client = client
291
- result = data.get('result', {})
292
- self.data = data
293
- self.id = result.get('id')
294
- self.type = result.get('type')
295
- self.title = result.get('title')
296
- self.username = result.get('username')
297
- self.description = result.get('description')
298
- self.invite_link = result.get('invite_link')
299
- self.photo = result.get('photo')
300
- self.is_channel_chat = self.CHANNEL = self.type == "channel"
301
- self.is_group_chat = self.GROUP = self.type == "group"
302
- self.is_private_chat = self.PRIVATE = self.type == "private"
303
-
304
- def send_photo(self,
305
- photo: Union[str,
306
- bytes,
307
- 'InputFile'],
308
- caption: Optional[str] = None,
309
- parse_mode: Optional[str] = None,
310
- reply_markup: Union['MenuKeyboardMarkup',
311
- 'InlineKeyboardMarkup'] = None) -> 'Message':
312
- """Send a photo to a chat"""
313
- files = None
314
- data = {
315
- 'chat_id': self.id,
316
- 'caption': caption,
317
- 'parse_mode': parse_mode,
318
- 'reply_markup': reply_markup.keyboard if isinstance(
319
- reply_markup,
320
- MenuKeyboardMarkup) else reply_markup.keyboard if isinstance(
321
- reply_markup,
322
- InlineKeyboardMarkup) else None}
323
-
324
- if isinstance(photo, (bytes, InputFile)) or hasattr(photo, 'read'):
325
- files = {
326
- 'photo': photo if not isinstance(
327
- photo, InputFile) else photo.file}
328
- else:
329
- data['photo'] = photo
330
-
331
- response = self.client._make_request(
332
- 'POST', 'sendPhoto', data=data, files=files)
333
- return Message(self.client, response)
334
-
335
- def send_message(self,
336
- text: str,
337
- parse_mode: Optional[str] = None,
338
- reply_markup: Union['MenuKeyboardMarkup',
339
- 'InlineKeyboardMarkup'] = None) -> 'Message':
340
- return self.client.send_message(
341
- self.id, text, parse_mode, reply_markup)
342
-
343
- def forward_message(
344
- self, from_chat_id: Union[int, str], message_id: int) -> 'Message':
345
- """Forward a message from another chat"""
346
- return self.client.forward_message(self.id, from_chat_id, message_id)
347
-
348
- def copy_message(self,
349
- from_chat_id: Union[int,
350
- str],
351
- message_id: int,
352
- caption: Optional[str] = None,
353
- parse_mode: Optional[str] = None,
354
- reply_markup: Union['MenuKeyboardMarkup',
355
- 'InlineKeyboardMarkup'] = None) -> 'Message':
356
- """Copy a message from another chat"""
357
- return self.client.copy_message(
358
- self.id,
359
- from_chat_id,
360
- message_id,
361
- caption,
362
- parse_mode,
363
- reply_markup)
364
-
365
- def send_audio(self,
366
- audio: Union[str,
367
- bytes,
368
- 'InputFile'],
369
- caption: Optional[str] = None,
370
- parse_mode: Optional[str] = None,
371
- duration: Optional[int] = None,
372
- performer: Optional[str] = None,
373
- title: Optional[str] = None,
374
- reply_markup: Union['MenuKeyboardMarkup',
375
- 'InlineKeyboardMarkup'] = None) -> 'Message':
376
- """Send an audio file"""
377
- return self.client.send_audio(
378
- self.id,
379
- audio,
380
- caption,
381
- parse_mode,
382
- duration,
383
- performer,
384
- title,
385
- reply_markup)
386
-
387
- def send_document(self,
388
- document: Union[str,
389
- bytes,
390
- 'InputFile'],
391
- caption: Optional[str] = None,
392
- parse_mode: Optional[str] = None,
393
- reply_markup: Union['MenuKeyboardMarkup',
394
- 'InlineKeyboardMarkup'] = None) -> 'Message':
395
- """Send a document"""
396
- return self.client.send_document(
397
- self.id, document, caption, parse_mode, reply_markup)
398
-
399
- def send_video(self,
400
- video: Union[str,
401
- bytes,
402
- 'InputFile'],
403
- caption: Optional[str] = None,
404
- parse_mode: Optional[str] = None,
405
- duration: Optional[int] = None,
406
- width: Optional[int] = None,
407
- height: Optional[int] = None,
408
- reply_markup: Union['MenuKeyboardMarkup',
409
- 'InlineKeyboardMarkup'] = None) -> 'Message':
410
- """Send a video"""
411
- return self.client.send_video(
412
- self.id,
413
- video,
414
- caption,
415
- parse_mode,
416
- duration,
417
- width,
418
- height,
419
- reply_markup)
420
-
421
- def send_animation(self,
422
- animation: Union[str,
423
- bytes,
424
- 'InputFile'],
425
- caption: Optional[str] = None,
426
- parse_mode: Optional[str] = None,
427
- duration: Optional[int] = None,
428
- width: Optional[int] = None,
429
- height: Optional[int] = None,
430
- reply_markup: Union['MenuKeyboardMarkup',
431
- 'InlineKeyboardMarkup'] = None) -> 'Message':
432
- """Send an animation"""
433
- return self.client.send_animation(
434
- self.id,
435
- animation,
436
- caption,
437
- parse_mode,
438
- duration,
439
- width,
440
- height,
441
- reply_markup)
442
-
443
- def send_voice(self,
444
- voice: Union[str,
445
- bytes,
446
- 'InputFile'],
447
- caption: Optional[str] = None,
448
- parse_mode: Optional[str] = None,
449
- duration: Optional[int] = None,
450
- reply_markup: Union['MenuKeyboardMarkup',
451
- 'InlineKeyboardMarkup'] = None) -> 'Message':
452
- """Send a voice message"""
453
- return self.client.send_voice(
454
- self.id,
455
- voice,
456
- caption,
457
- parse_mode,
458
- duration,
459
- reply_markup)
460
-
461
- def send_media_group(self,
462
- chat_id: Union[int,
463
- str],
464
- media: List[Dict],
465
- reply_to_message: Union['Message',
466
- int,
467
- str] = None) -> List['Message']:
468
- """Send a group of photos, videos, documents or audios as an album"""
469
- data = {
470
- 'chat_id': chat_id,
471
- 'media': media,
472
- 'reply_to_message_id': reply_to_message.message_id if isinstance(
473
- reply_to_message,
474
- Message) else reply_to_message}
475
- response = self._make_request('POST', 'sendMediaGroup', json=data)
476
- return [Message(self, msg) for msg in response]
477
-
478
- def send_location(self,
479
- latitude: float,
480
- longitude: float,
481
- live_period: Optional[int] = None,
482
- reply_markup: Union['MenuKeyboardMarkup',
483
- 'InlineKeyboardMarkup'] = None) -> 'Message':
484
- """Send a location"""
485
- return self.client.send_location(
486
- self.id, latitude, longitude, live_period, reply_markup)
487
-
488
- def send_contact(self,
489
- phone_number: str,
490
- first_name: str,
491
- last_name: Optional[str] = None,
492
- reply_markup: Union['MenuKeyboardMarkup',
493
- 'InlineKeyboardMarkup'] = None) -> 'Message':
494
- """Send a contact"""
495
- return self.client.send_contact(
496
- self.id,
497
- phone_number,
498
- first_name,
499
- last_name,
500
- reply_markup)
501
-
502
- def send_invoice(self,
503
- title: str,
504
- description: str,
505
- payload: str,
506
- provider_token: str,
507
- prices: list,
508
- photo_url: Optional[str] = None,
509
- reply_to_message: Union[int,
510
- str,
511
- 'Message'] = None,
512
- reply_markup: Union['MenuKeyboardMarkup', 'InlineKeyboardMarkup'] = None):
513
- return self.client.send_invoice(
514
- self.id,
515
- title,
516
- description,
517
- payload,
518
- provider_token,
519
- prices,
520
- photo_url,
521
- reply_to_message,
522
- reply_markup)
523
-
524
- def send_action(self, action: str, how_many_times=1) -> bool:
525
- """Send a chat action"""
526
- return self.client.send_chat_action(self.id, action, how_many_times)
527
-
528
- def ban_chat_member(
529
- self,
530
- user_id: int,
531
- until_date: Optional[int] = None) -> bool:
532
- """Ban a user from the chat"""
533
- data = {
534
- 'chat_id': self.id,
535
- 'user_id': user_id,
536
- 'until_date': until_date
537
- }
538
- response = self.client._make_request(
539
- 'POST', 'banChatMember', data=data)
540
- return response.get('ok', False)
541
-
542
- def unban_chat_member(
543
- self,
544
- user_id: int,
545
- only_if_banned: bool = False) -> bool:
546
- """Unban a previously banned user from the chat"""
547
- data = {
548
- 'chat_id': self.id,
549
- 'user_id': user_id,
550
- 'only_if_banned': only_if_banned
551
- }
552
- response = self.client._make_request(
553
- 'POST', 'unbanChatMember', data=data)
554
- return response.get('ok', False)
555
-
556
- def promotee_chat_member(
557
- self,
558
- user_id: int,
559
- can_change_info: bool = None,
560
- can_post_messages: bool = None,
561
- can_edit_messages: bool = None,
562
- can_delete_messages: bool = None,
563
- can_manage_video_chats: bool = None,
564
- can_invite_users: bool = None,
565
- can_restrict_members: bool = None) -> bool:
566
- """Promote or demote a chat member"""
567
- data = {
568
- 'chat_id': self.id,
569
- 'user_id': user_id,
570
- 'can_change_info': can_change_info,
571
- 'can_post_messages': can_post_messages,
572
- 'can_edit_messages': can_edit_messages,
573
- 'can_delete_messages': can_delete_messages,
574
- 'can_manage_video_chats': can_manage_video_chats,
575
- 'can_invite_users': can_invite_users,
576
- 'can_restrict_members': can_restrict_members
577
- }
578
- response = self.client._make_request(
579
- 'POST', 'promoteChatMember', data=data)
580
- return response.get('ok', False)
581
-
582
- def set_chat_photo(self, photo: Union[str, bytes, 'InputFile']) -> bool:
583
- """Set a new chat photo"""
584
- files = None
585
- data = {'chat_id': self.id}
586
-
587
- if isinstance(photo, (bytes, InputFile)) or hasattr(photo, 'read'):
588
- files = {
589
- 'photo': photo if not isinstance(
590
- photo, InputFile) else photo.file}
591
- else:
592
- data['photo'] = photo
593
-
594
- response = self.client._make_request(
595
- 'POST', 'setChatPhoto', data=data, files=files)
596
- return response.get('ok', False)
597
-
598
- def leave_chat(self) -> bool:
599
- """Leave the chat"""
600
- data = {'chat_id': self.id}
601
- response = self.client._make_request('POST', 'leaveChat', data=data)
602
- return response.get('ok', False)
603
-
604
- def get_chat(self) -> 'Chat':
605
- """Get up to date information about the chat"""
606
- data = {'chat_id': self.id}
607
- response = self.client._make_request('GET', 'getChat', data=data)
608
- return Chat(self.client, response)
609
-
610
- def get_members_count(self) -> int:
611
- """Get the number of members in the chat"""
612
- data = {'chat_id': self.id}
613
- response = self.client._make_request(
614
- 'POST', 'getChatMembersCount', data=data)
615
- return response.get('result', 0)
616
-
617
- def pin_message(
618
- self,
619
- message_id: int,
620
- disable_notification: bool = False) -> bool:
621
- """Pin a message in the chat"""
622
- data = {
623
- 'chat_id': self.id,
624
- 'message_id': message_id,
625
- 'disable_notification': disable_notification
626
- }
627
- response = self.client._make_request(
628
- 'POST', 'pinChatMessage', data=data)
629
- return response.get('ok', False)
630
-
631
- def unpin_message(self, message_id: int) -> bool:
632
- """Unpin a message in the chat"""
633
- data = {
634
- 'chat_id': self.id,
635
- 'message_id': message_id
636
- }
637
- response = self.client._make_request(
638
- 'POST', 'unpinChatMessage', data=data)
639
- return response.get('ok', False)
640
-
641
- def unpin_all_messages(self) -> bool:
642
- """Unpin all messages in the chat"""
643
- data = {'chat_id': self.id}
644
- response = self.client._make_request(
645
- 'POST', 'unpinAllChatMessages', data=data)
646
- return response.get('ok', False)
647
-
648
- def set_chat_title(self, title: str) -> bool:
649
- """Change the title of the chat"""
650
- data = {
651
- 'chat_id': self.id,
652
- 'title': title
653
- }
654
- response = self.client._make_request('POST', 'setChatTitle', data=data)
655
- return response.get('ok', False)
656
-
657
- def set_chat_description(self, description: str) -> bool:
658
- """Change the description of the chat"""
659
- data = {
660
- 'chat_id': self.id,
661
- 'description': description
662
- }
663
- response = self.client._make_request(
664
- 'POST', 'setChatDescription', data=data)
665
- return response.get('ok', False)
666
-
667
- def delete_chat_photo(self) -> bool:
668
- """Delete the chat photo"""
669
- data = {'chat_id': self.id}
670
- response = self.client._make_request(
671
- 'POST', 'deleteChatPhoto', data=data)
672
- return response.get('ok', False)
673
-
674
- def create_invite_link(self) -> str:
675
- """Create an invite link for the chat"""
676
- data = {'chat_id': self.id}
677
- response = self.client._make_request(
678
- 'POST', 'createChatInviteLink', data=data)
679
- return response.get('result', {}).get('invite_link')
680
-
681
- def revoke_invite_link(self, invite_link: str) -> bool:
682
- """Revoke an invite link for the chat"""
683
- data = {
684
- 'chat_id': self.id,
685
- 'invite_link': invite_link
686
- }
687
- response = self.client._make_request(
688
- 'POST', 'revokeChatInviteLink', data=data)
689
- return response.get('ok', False)
690
-
691
- def export_invite_link(self) -> str:
692
- """Generate a new invite link for the chat"""
693
- data = {'chat_id': self.id}
694
- response = self.client._make_request(
695
- 'POST', 'exportChatInviteLink', data=data)
696
- return response.get('result')
697
-
698
-
699
- class User:
700
- """Represents a Bale user"""
701
-
702
- def __init__(self, client: 'Client', data: Dict[str, Any]):
703
- if not data.get('ok'):
704
- raise BaleException(
705
- f"API request failed: {traceback.format_exc()}")
706
- self.client = client
707
- result = data.get('result', {})
708
- self.data = data
709
- self.ok = data.get('ok')
710
- self.id = result.get('id')
711
- self.is_bot = result.get('is_bot')
712
- self.first_name = result.get('first_name')
713
- self.last_name = result.get('last_name')
714
- self.username = result.get('username')
715
-
716
- def set_state(self, state: str) -> None:
717
- """Set the state for a chat or user"""
718
- self.client.states[str(self.id)] = state
719
-
720
- def get_state(self) -> str | None:
721
- """Get the state for a chat or user"""
722
- return self.client.states.get(str(self.id))
723
-
724
- @property
725
- def state(self):
726
- return self.get_state()
727
-
728
- def del_state(self) -> None:
729
- """Delete the state for a chat or user"""
730
- self.client.states.pop(str(self.id), None)
731
-
732
- def __str__(self):
733
- return self.first_name
734
-
735
- def send_message(self,
736
- text: str,
737
- parse_mode: Optional[str] = None,
738
- reply_markup: Union['MenuKeyboardMarkup',
739
- 'InlineKeyboardMarkup'] = None) -> 'Message':
740
- """Send a message to this user"""
741
- return self.client.send_message(
742
- self.id, text, parse_mode, reply_markup)
743
-
744
- def send_photo(self,
745
- photo: Union[str,
746
- bytes,
747
- 'InputFile'],
748
- caption: Optional[str] = None,
749
- parse_mode: Optional[str] = None,
750
- reply_markup: Union['MenuKeyboardMarkup',
751
- 'InlineKeyboardMarkup'] = None) -> 'Message':
752
- """Send a photo to a chat"""
753
- return self.client.send_photo(
754
- self.id, photo, caption, parse_mode, reply_markup)
755
-
756
- def forward_message(
757
- self, from_chat_id: Union[int, str], message_id: int) -> 'Message':
758
- """Forward a message to this user"""
759
- self.client.forward_message(self.id, from_chat_id, message_id)
760
-
761
- def copy_message(self,
762
- from_chat_id: Union[int,
763
- str],
764
- message_id: int) -> 'Message':
765
- """Copy a message to this user"""
766
- self.client.copy_message(self.id, from_chat_id, message_id)
767
-
768
- def send_audio(self,
769
- audio: Union[str,
770
- bytes,
771
- 'InputFile'],
772
- caption: Optional[str] = None,
773
- parse_mode: Optional[str] = None,
774
- reply_markup: Union['MenuKeyboardMarkup',
775
- 'InlineKeyboardMarkup'] = None,
776
- reply_to_message: Union[str,
777
- int,
778
- 'Message'] = None) -> 'Message':
779
- """Send an audio file to this user"""
780
- self.client.send_audio(
781
- self.id,
782
- audio,
783
- caption,
784
- parse_mode,
785
- reply_markup,
786
- reply_to_message)
787
-
788
- def send_document(self,
789
- document: Union[str,
790
- bytes,
791
- 'InputFile'],
792
- caption: Optional[str] = None,
793
- parse_mode: Optional[str] = None,
794
- reply_markup: Union['MenuKeyboardMarkup',
795
- 'InlineKeyboardMarkup'] = None,
796
- reply_to_message: Union[str,
797
- int,
798
- 'Message'] = None) -> 'Message':
799
- """Send a document to this user"""
800
- self.client.send_document(
801
- self.id,
802
- document,
803
- caption,
804
- parse_mode,
805
- reply_markup,
806
- reply_markup)
807
-
808
- def send_video(self,
809
- video: Union[str,
810
- bytes,
811
- 'InputFile'],
812
- caption: Optional[str] = None,
813
- parse_mode: Optional[str] = None,
814
- reply_markup: Union['MenuKeyboardMarkup',
815
- 'InlineKeyboardMarkup'] = None,
816
- reply_to_message: Union[str,
817
- int,
818
- 'Message'] = None) -> 'Message':
819
- """Send a video to this user"""
820
- self.client.send_video(
821
- self.id,
822
- video,
823
- caption,
824
- parse_mode,
825
- reply_markup,
826
- reply_to_message)
827
-
828
- def send_animation(self,
829
- animation: Union[str,
830
- bytes,
831
- 'InputFile'],
832
- caption: Optional[str] = None,
833
- parse_mode: Optional[str] = None,
834
- reply_markup: Union['MenuKeyboardMarkup',
835
- 'InlineKeyboardMarkup'] = None,
836
- reply_to_message: Union[int,
837
- str,
838
- 'Message'] = None) -> 'Message':
839
- """Send an animation to this user"""
840
- return self.client.send_animation(
841
- self.id,
842
- animation,
843
- caption,
844
- parse_mode,
845
- reply_markup,
846
- reply_to_message)
847
-
848
- def send_voice(self,
849
- voice: Union[str,
850
- bytes,
851
- 'InputFile'],
852
- caption: Optional[str] = None,
853
- parse_mode: Optional[str] = None,
854
- reply_markup: Union['MenuKeyboardMarkup',
855
- 'InlineKeyboardMarkup'] = None,
856
- reply_to_message: Union[int,
857
- str,
858
- 'Message'] = None) -> 'Message':
859
- """Send a voice message to this user"""
860
- return self.client.send_voice(
861
- self.id,
862
- voice,
863
- caption,
864
- parse_mode,
865
- reply_markup,
866
- reply_to_message)
867
-
868
- def send_media_group(self,
869
- chat_id: Union[int,
870
- str],
871
- media: List[Dict],
872
- reply_to_message: Union['Message',
873
- int,
874
- str] = None) -> List['Message']:
875
- """Send a group of photos, videos, documents or audios as an album"""
876
- return self.client.send_media_group(chat_id, media, reply_to_message)
877
-
878
- def send_location(self,
879
- latitude: float,
880
- longitude: float,
881
- reply_markup: Union['MenuKeyboardMarkup',
882
- 'InlineKeyboardMarkup'] = None,
883
- reply_to_message: Union[str,
884
- int,
885
- 'Message'] = None) -> 'Message':
886
- """Send a location to this user"""
887
- return self.send_location(
888
- latitude,
889
- longitude,
890
- reply_markup,
891
- reply_to_message)
892
-
893
- def send_contact(self,
894
- phone_number: str,
895
- first_name: str,
896
- last_name: Optional[str] = None,
897
- reply_markup: Union['MenuKeyboardMarkup',
898
- 'InlineKeyboardMarkup'] = None,
899
- reply_to_message: Union[str,
900
- int,
901
- 'Message'] = None) -> 'Message':
902
- """Send a contact to this user"""
903
- return self.client.send_contact(
904
- self.id,
905
- phone_number,
906
- first_name,
907
- last_name,
908
- reply_markup,
909
- reply_to_message)
910
-
911
- def send_invoice(self,
912
- title: str,
913
- description: str,
914
- payload: str,
915
- provider_token: str,
916
- prices: list,
917
- photo_url: Optional[str] = None,
918
- reply_to_message: Union[int,
919
- str,
920
- 'Message'] = None,
921
- reply_markup: Union['MenuKeyboardMarkup', 'InlineKeyboardMarkup'] = None):
922
- return self.client.send_invoice(
923
- self.id,
924
- title,
925
- description,
926
- payload,
927
- provider_token,
928
- prices,
929
- photo_url,
930
- reply_to_message,
931
- reply_markup)
932
-
933
- def send_action(self, action: str, how_many_times: int = 1):
934
- """Send a chat action to this user"""
935
- return self.client.send_chat_action(self.id, action, how_many_times)
936
-
937
-
938
- class Message:
939
- """Represents a message in Bale"""
940
-
941
- def __init__(self, client: 'Client', data: Dict[str, Any]):
942
- if not data.get('ok'):
943
- raise BaleException(
944
- f"API request failed: {traceback.format_exc()}")
945
- self.client = client
946
- self.ok = data.get('ok')
947
- result = data.get('result', {})
948
- self.json_result = result
949
-
950
- self.message_id = self.id = result.get('message_id')
951
- self.from_user = self.author = User(
952
- client, {'ok': True, 'result': result.get('from', {})})
953
- self.date = result.get('date')
954
- self.chat = Chat(
955
- client, {
956
- 'ok': True, 'result': result.get(
957
- 'chat', {})})
958
- self.text = result.get('text')
959
- self.caption = result.get('caption')
960
-
961
- self.document = Document(
962
- result.get(
963
- 'document',
964
- {})) if result.get('document') else None
965
-
966
- photos = result.get('photo', [])
967
- self.photo = [Document(photo) for photo in photos] if photos else None
968
- self.largest_photo = Document(photos[-1]) if photos else None
969
-
970
- self.video = Document(
971
- result.get('video')) if result.get('video') else None
972
- self.audio = Document(
973
- result.get('audio')) if result.get('audio') else None
974
- self.voice = Voice(
975
- result.get('voice')) if result.get('voice') else None
976
- self.animation = Document(
977
- result.get('animation')) if result.get('animation') else None
978
- self.sticker = Document(
979
- result.get('sticker')) if result.get('sticker') else None
980
- self.video_note = Document(
981
- result.get('video_note')) if result.get('video_note') else None
982
-
983
- self.media_group_id = result.get('media_group_id')
984
- self.has_media = any([self.document,
985
- self.photo,
986
- self.video,
987
- self.audio,
988
- self.voice,
989
- self.animation,
990
- self.sticker,
991
- self.video_note])
992
-
993
- self.contact = Contact(
994
- result.get('contact')) if result.get('contact') else None
995
- self.location = Location(
996
- result.get('location')) if result.get('location') else None
997
- self.forward_from = User(client, {'ok': True, 'result': result.get(
998
- 'forward_from', {})}) if result.get('forward_from') else None
999
- self.forward_from_message_id = result.get('forward_from_message_id')
1000
- self.invoice = Invoice(
1001
- result.get('invoice')) if result.get('invoice') else None
1002
- self.reply_to_message = Message(client, {'ok': True, 'result': result.get(
1003
- 'reply_to_message', {})}) if result.get('reply_to_message') else None
1004
- self.reply = self.reply_message
1005
- self.send = lambda text, parse_mode=None, reply_markup=None: self.client.send_message(
1006
- self.chat.id, text, parse_mode, reply_markup, reply_to_message=self)
1007
-
1008
- self.command = None
1009
- self.args = None
1010
- txt = self.text.split(' ') if self.text else []
1011
-
1012
- self.command = txt[0] if txt else None
1013
- self.has_slash_command = self.command.startswith(
1014
- '/') if self.text else None
1015
- self.args = txt[1:] if self.text else None
1016
-
1017
- self.start = self.command == '/start' if self.text else None
1018
-
1019
- def edit(self,
1020
- text: str,
1021
- parse_mode: Optional[str] = None,
1022
- reply_markup: Union['MenuKeyboardMarkup',
1023
- 'InlineKeyboardMarkup'] = None) -> 'Message':
1024
- """Edit this message"""
1025
- return self.client.edit_message(
1026
- self.chat.id,
1027
- self.message_id,
1028
- text,
1029
- parse_mode,
1030
- reply_markup)
1031
-
1032
- def delete(self) -> bool:
1033
- """Delete this message"""
1034
- return self.client.delete_message(self.chat.id, self.message_id)
1035
-
1036
- def reply_message(self,
1037
- text: str,
1038
- parse_mode: Optional[str] = None,
1039
- reply_markup: Union['MenuKeyboardMarkup',
1040
- 'InlineKeyboardMarkup'] = None) -> 'Message':
1041
- """Send a message to this user"""
1042
- return self.client.send_message(
1043
- self.chat.id,
1044
- text,
1045
- parse_mode,
1046
- reply_markup,
1047
- reply_to_message=self)
1048
-
1049
- def reply_photo(self,
1050
- photo: Union[str,
1051
- bytes,
1052
- 'InputFile'],
1053
- caption: Optional[str] = None,
1054
- parse_mode: Optional[str] = None,
1055
- reply_markup: Union['MenuKeyboardMarkup',
1056
- 'InlineKeyboardMarkup'] = None) -> 'Message':
1057
- """Send a photo to a chat"""
1058
- return self.client.send_photo(
1059
- self.chat.id,
1060
- photo,
1061
- caption,
1062
- parse_mode,
1063
- reply_markup,
1064
- reply_to_message=self)
1065
-
1066
- def reply_audio(self,
1067
- audio: Union[str,
1068
- bytes,
1069
- 'InputFile'],
1070
- caption: Optional[str] = None,
1071
- parse_mode: Optional[str] = None,
1072
- reply_markup: Union['MenuKeyboardMarkup',
1073
- 'InlineKeyboardMarkup'] = None) -> 'Message':
1074
- """Send an audio file to this user"""
1075
- self.client.send_audio(
1076
- self.chat.id,
1077
- audio,
1078
- caption,
1079
- parse_mode,
1080
- reply_markup,
1081
- reply_to_message=self)
1082
-
1083
- def reply_document(self,
1084
- document: Union[str,
1085
- bytes,
1086
- 'InputFile'],
1087
- caption: Optional[str] = None,
1088
- parse_mode: Optional[str] = None,
1089
- reply_markup: Union['MenuKeyboardMarkup',
1090
- 'InlineKeyboardMarkup'] = None) -> 'Message':
1091
- """Send a document to this user"""
1092
- self.client.send_document(
1093
- self.chat.id,
1094
- document,
1095
- caption,
1096
- parse_mode,
1097
- reply_markup,
1098
- reply_markup,
1099
- reply_to_message=self)
1100
-
1101
- def reply_video(self,
1102
- video: Union[str,
1103
- bytes,
1104
- 'InputFile'],
1105
- caption: Optional[str] = None,
1106
- parse_mode: Optional[str] = None,
1107
- reply_markup: Union['MenuKeyboardMarkup',
1108
- 'InlineKeyboardMarkup'] = None) -> 'Message':
1109
- """Send a video to this user"""
1110
- self.client.send_video(
1111
- self.chat.id,
1112
- video,
1113
- caption,
1114
- parse_mode,
1115
- reply_markup,
1116
- reply_to_message=self)
1117
-
1118
- def reply_animation(self,
1119
- animation: Union[str,
1120
- bytes,
1121
- 'InputFile'],
1122
- caption: Optional[str] = None,
1123
- parse_mode: Optional[str] = None,
1124
- reply_markup: Union['MenuKeyboardMarkup',
1125
- 'InlineKeyboardMarkup'] = None) -> 'Message':
1126
- """Send an animation to this user"""
1127
- return self.client.send_animation(
1128
- self.chat.id,
1129
- animation,
1130
- caption,
1131
- parse_mode,
1132
- reply_markup,
1133
- reply_to_message=self)
1134
-
1135
- def reply_voice(self,
1136
- voice: Union[str,
1137
- bytes,
1138
- 'InputFile'],
1139
- caption: Optional[str] = None,
1140
- parse_mode: Optional[str] = None,
1141
- reply_markup: Union['MenuKeyboardMarkup',
1142
- 'InlineKeyboardMarkup'] = None) -> 'Message':
1143
- """Send a voice message to this user"""
1144
- return self.client.send_voice(
1145
- self.chat.id,
1146
- voice,
1147
- caption,
1148
- parse_mode,
1149
- reply_markup,
1150
- reply_to_message=self)
1151
-
1152
- def reply_media_group(self,
1153
- media: List[Dict],
1154
- reply_to_message: Union['Message',
1155
- int,
1156
- str] = None) -> List['Message']:
1157
- """Send a group of photos, videos, documents or audios as an album"""
1158
- return self.client.send_media_group(
1159
- self.chat.id, media, reply_to_message=self)
1160
-
1161
- def reply_location(self,
1162
- latitude: float,
1163
- longitude: float,
1164
- reply_markup: Union['MenuKeyboardMarkup',
1165
- 'InlineKeyboardMarkup'] = None) -> 'Message':
1166
- """Send a location to this user"""
1167
- return self.client.send_location(
1168
- self.chat.id,
1169
- latitude,
1170
- longitude,
1171
- reply_markup,
1172
- reply_to_message=self)
1173
-
1174
- def reply_contact(self,
1175
- phone_number: str,
1176
- first_name: str,
1177
- last_name: Optional[str] = None,
1178
- reply_markup: Union['MenuKeyboardMarkup',
1179
- 'InlineKeyboardMarkup'] = None,
1180
- reply_to_message: Union[str,
1181
- int,
1182
- 'Message'] = None) -> 'Message':
1183
- """Send a contact to this user"""
1184
- return self.client.send_contact(
1185
- self.chat.id,
1186
- phone_number,
1187
- first_name,
1188
- last_name,
1189
- reply_markup,
1190
- reply_to_message)
1191
-
1192
- def reply_invoice(self,
1193
- title: str,
1194
- description: str,
1195
- payload: str,
1196
- provider_token: str,
1197
- prices: list,
1198
- photo_url: Optional[str] = None,
1199
- reply_markup: Union['MenuKeyboardMarkup', 'InlineKeyboardMarkup'] = None):
1200
- return self.client.send_invoice(
1201
- self.chat.id,
1202
- title,
1203
- description,
1204
- payload,
1205
- provider_token,
1206
- prices,
1207
- photo_url,
1208
- self,
1209
- reply_markup)
1210
-
1211
- def forward(self,
1212
- chat_id: Union[str, int]) -> 'Message':
1213
- """Forward a message to this user"""
1214
- return self.client.forward_message(
1215
- chat_id,
1216
- self.chat.id,
1217
- self.message_id)
1218
-
1219
-
1220
- class LabeledPrice:
1221
- def __init__(self, label: str, amount: int):
1222
- self.label = label
1223
- self.amount = amount
1224
- self.json = {
1225
- "label": self.label,
1226
- "amount": self.amount
1227
- }
1228
-
1229
-
1230
- class Document:
1231
- def __init__(self, data: dict):
1232
- print(data)
1233
- if data:
1234
- self.file_id = data.get('file_id')
1235
- self.file_unique_id = data.get('file_unique_id')
1236
- self.file_name = data.get('file_name')
1237
- self.mime_type = data.get('mime_type')
1238
- self.file_size = data.get('file_size')
1239
- self.input_file = InputFile(self.file_id)
1240
- else:
1241
- self.file_id = None
1242
- self.file_unique_id = None
1243
- self.file_name = None
1244
- self.mime_type = None
1245
- self.file_size = None
1246
- self.input_file = None
1247
-
1248
- def __bool__(self):
1249
- return bool(self.file_id)
1250
-
1251
-
1252
- class Invoice:
1253
- def __init__(self, data: dict):
1254
- if data:
1255
- self.title = data.get('title')
1256
- self.description = data.get('description')
1257
- self.start_parameter = data.get('start_parameter')
1258
- self.currency = data.get('currency')
1259
- self.total_amount = data.get('total_amount')
1260
- else:
1261
- self.title = None
1262
- self.description = None
1263
- self.start_parameter = None
1264
- self.currency = None
1265
- self.total_amount = None
1266
-
1267
-
1268
- class Photo:
1269
- def __init__(self, data):
1270
- if data:
1271
- self.file_id = data.get('file_id')
1272
- self.file_unique_id = data.get('file_unique_id')
1273
- self.width = data.get('width')
1274
- self.height = data.get('height')
1275
- self.file_size = data.get('file_size')
1276
- else:
1277
- self.file_id = None
1278
- self.file_unique_id = None
1279
- self.width = None
1280
- self.height = None
1281
- self.file_size = None
1282
-
1283
-
1284
- class Voice:
1285
- def __init__(self, data: dict):
1286
- if data:
1287
- self.file_id = data.get('file_id')
1288
- self.file_unique_id = data.get('file_unique_id')
1289
- self.duration = data.get('duration')
1290
- self.mime_type = data.get('mime_type')
1291
- self.file_size = data.get('file_size')
1292
- else:
1293
- self.file_id = None
1294
- self.file_unique_id = None
1295
- self.duration = None
1296
- self.mime_type = None
1297
- self.file_size = None
1298
-
1299
-
1300
- class Location:
1301
- def __init__(self, data: dict):
1302
- if data:
1303
- self.long = self.longitude = data.get('longitude')
1304
- self.lat = self.latitude = data.get('latitude')
1305
- else:
1306
- self.longitude = self.long = None
1307
- self.latitude = self.lat = None
1308
-
1309
-
1310
- class Contact:
1311
- def __init__(self, data: dict):
1312
- if data:
1313
- self.phone_number = data.get('phone_number')
1314
- self.first_name = data.get('first_name')
1315
- self.last_name = data.get('last_name')
1316
- self.user_id = data.get('user_id')
1317
- else:
1318
- self.phone_number = None
1319
- self.first_name = None
1320
- self.last_name = None
1321
- self.user_id = None
1322
-
1323
-
1324
- class MenuKeyboardButton:
1325
- def __init__(
1326
- self,
1327
- text: str,
1328
- request_contact: bool = False,
1329
- request_location: bool = False):
1330
- if not text:
1331
- raise ValueError("Text cannot be empty")
1332
- if request_contact and request_location:
1333
- raise ValueError("Cannot request both contact and location")
1334
-
1335
- self.button = {"text": text}
1336
- if request_contact:
1337
- self.button["request_contact"] = True
1338
- if request_location:
1339
- self.button["request_location"] = True
1340
-
1341
-
1342
- class InlineKeyboardButton:
1343
- def __init__(
1344
- self,
1345
- text: str,
1346
- callback_data: Optional[str] = None,
1347
- url: Optional[str] = None,
1348
- web_app: Optional[str] = None):
1349
- self.button = {"text": text}
1350
- if sum(bool(x) for x in [callback_data, url, web_app]) != 1:
1351
- raise ValueError(
1352
- "Exactly one of callback_data, url, or web_app must be provided")
1353
- if callback_data:
1354
- self.button["callback_data"] = callback_data
1355
- elif url:
1356
- self.button["url"] = url
1357
- elif web_app:
1358
- self.button["web_app"] = {"url": web_app}
1359
-
1360
-
1361
- class MenuKeyboardMarkup:
1362
- def __init__(self, menu_keyboard: Optional[list] = None):
1363
- self.menu_keyboard = []
1364
- if menu_keyboard:
1365
- for row_idx, row in enumerate(menu_keyboard):
1366
- if isinstance(row, tuple) or isinstance(row, str):
1367
- buttons = [row] if isinstance(row, str) else row
1368
- for button in buttons:
1369
- if not isinstance(button, str):
1370
- raise ValueError("Button must be string")
1371
- self.add(MenuKeyboardButton(button), row_idx)
1372
-
1373
- def add(self, button: MenuKeyboardButton,
1374
- row: int = 0) -> 'MenuKeyboardMarkup':
1375
- if row < 0:
1376
- raise ValueError("Row index cannot be negative")
1377
- while len(self.menu_keyboard) <= row:
1378
- self.menu_keyboard.append([])
1379
- self.menu_keyboard[row].append(button.button)
1380
- self.cleanup_empty_rows()
1381
- return self
1382
-
1383
- def cleanup_empty_rows(self) -> None:
1384
- self.menu_keyboard = [row for row in self.menu_keyboard if row]
1385
-
1386
- def clear(self) -> None:
1387
- self.menu_keyboard = []
1388
-
1389
- def remove_button(self, text: str) -> bool:
1390
- found = False
1391
- for row in self.menu_keyboard:
1392
- for button in row[:]:
1393
- if button.get('text') == text:
1394
- row.remove(button)
1395
- found = True
1396
- self.cleanup_empty_rows()
1397
- return found
1398
-
1399
- @property
1400
- def keyboard(self):
1401
- return {"keyboard": self.menu_keyboard}
1402
-
1403
-
1404
- class InlineKeyboardMarkup:
1405
- def __init__(self, inline_keyboard: Optional[list] = None):
1406
- self.inline_keyboard = []
1407
- if inline_keyboard:
1408
- for row_idx, row in enumerate(inline_keyboard):
1409
- for button in row:
1410
- if not isinstance(button, (tuple, list)):
1411
- raise ValueError("Button must be a tuple or list")
1412
- if len(button) != 2:
1413
- raise ValueError(
1414
- "Button must contain exactly text and callback_data/url/web_app")
1415
- if not isinstance(button[0], str) or not isinstance(
1416
- button[1], str):
1417
- raise ValueError(
1418
- "Button text and data must be strings")
1419
- self.add(
1420
- InlineKeyboardButton(
1421
- button[0],
1422
- callback_data=button[1]),
1423
- row_idx)
1424
-
1425
- def add(self, button: InlineKeyboardButton,
1426
- row: int = 0) -> 'InlineKeyboardMarkup':
1427
- if row < 0:
1428
- raise ValueError("Row index cannot be negative")
1429
- while len(self.inline_keyboard) <= row:
1430
- self.inline_keyboard.append([])
1431
- self.inline_keyboard[row].append(button.button)
1432
- self.cleanup_empty_rows()
1433
- return self
1434
-
1435
- def cleanup_empty_rows(self) -> None:
1436
- self.inline_keyboard = [row for row in self.inline_keyboard if row]
1437
-
1438
- def clear(self) -> None:
1439
- self.inline_keyboard = []
1440
-
1441
- def remove_button(self, text: str) -> bool:
1442
- found = False
1443
- for row in self.inline_keyboard:
1444
- for button in row[:]:
1445
- if button.get('text') == text:
1446
- row.remove(button)
1447
- found = True
1448
- self.cleanup_empty_rows()
1449
- return found
1450
-
1451
- @property
1452
- def keyboard(self) -> dict:
1453
- return {"inline_keyboard": self.inline_keyboard}
1454
-
1455
-
1456
- class InputMedia:
1457
- """Base class for input media types"""
1458
-
1459
- def __init__(self, media: str, caption: str = None):
1460
- self.media = media
1461
- self.caption = caption
1462
-
1463
- @property
1464
- def media_dict(self) -> dict:
1465
- media_dict = {
1466
- 'media': self.media,
1467
- 'type': self.type
1468
- }
1469
- if self.caption:
1470
- media_dict['caption'] = self.caption
1471
- return media_dict
1472
-
1473
-
1474
- class InputMediaPhoto(InputMedia):
1475
- """Represents a photo to be sent"""
1476
- type = 'photo'
1477
-
1478
-
1479
- class InputMediaVideo(InputMedia):
1480
- """Represents a video to be sent"""
1481
- type = 'video'
1482
-
1483
- def __init__(self, media: str, caption: str = None, width: int = None,
1484
- height: int = None, duration: int = None):
1485
- super().__init__(media, caption)
1486
- self.width = width
1487
- self.height = height
1488
- self.duration = duration
1489
-
1490
- @property
1491
- def media_dict(self) -> dict:
1492
- media_dict = super().media_dict
1493
- if self.width:
1494
- media_dict['width'] = self.width
1495
- if self.height:
1496
- media_dict['height'] = self.height
1497
- if self.duration:
1498
- media_dict['duration'] = self.duration
1499
- return media_dict
1500
-
1501
-
1502
- class InputMediaAnimation(InputMedia):
1503
- """Represents an animation to be sent"""
1504
- type = 'animation'
1505
-
1506
- def __init__(self, media: str, caption: str = None, width: int = None,
1507
- height: int = None, duration: int = None):
1508
- super().__init__(media, caption)
1509
- self.width = width
1510
- self.height = height
1511
- self.duration = duration
1512
-
1513
- @property
1514
- def media_dict(self) -> dict:
1515
- media_dict = super().media_dict
1516
- if self.width:
1517
- media_dict['width'] = self.width
1518
- if self.height:
1519
- media_dict['height'] = self.height
1520
- if self.duration:
1521
- media_dict['duration'] = self.duration
1522
- return media_dict
1523
-
1524
-
1525
- class InputFile:
1526
- """Represents a file to be sent"""
1527
-
1528
- def __init__(self, file: Union[str, bytes] = None, file_id: str = None):
1529
- if file and file_id:
1530
- raise ValueError(
1531
- "Either file or file_id should be provided, not both")
1532
- elif not file and not file_id:
1533
- raise ValueError("Either file or file_id must be provided")
1534
-
1535
- self.file = file
1536
- self.file_id = file_id
1537
-
1538
- @property
1539
- def file_type(self) -> str:
1540
- if self.file_id:
1541
- return "id"
1542
- if isinstance(self.file, bytes):
1543
- return "bytes"
1544
- if self.file.startswith(('http://', 'https://')):
1545
- return "url"
1546
- return "path"
1547
-
1548
- def __str__(self) -> str:
1549
- if self.file_id:
1550
- return self.file_id
1551
- return str(self.file)
1552
-
1553
-
1554
- class InputMediaAudio(InputMedia):
1555
- """Represents an audio file to be sent"""
1556
- type = 'audio'
1557
-
1558
- def __init__(self, media: str, caption: str = None, duration: int = None,
1559
- performer: str = None, title: str = None):
1560
- super().__init__(media, caption)
1561
- self.duration = duration
1562
- self.performer = performer
1563
- self.title = title
1564
-
1565
- @property
1566
- def media_dict(self) -> dict:
1567
- media_dict = super().media_dict
1568
- if self.duration:
1569
- media_dict['duration'] = self.duration
1570
- if self.performer:
1571
- media_dict['performer'] = self.performer
1572
- if self.title:
1573
- media_dict['title'] = self.title
1574
- return media_dict
1575
-
1576
-
1577
- class InputMediaDocument(InputMedia):
1578
- """Represents a document to be sent"""
1579
- type = 'document'
1580
-
1581
-
1582
- class CallbackQuery:
1583
-
1584
- """Represents a callback query from a callback button"""
1585
-
1586
- def __init__(self, client: 'Client', data: Dict[str, Any]):
1587
- if not data.get('ok'):
1588
- raise BaleException(
1589
- f"API request failed: {traceback.format_exc()}")
1590
- self.client = client
1591
- result = data.get('result', {})
1592
- self.id = result.get('id')
1593
- self.from_user = self.user = self.author = User(
1594
- client, {'ok': True, 'result': result.get('from', {})})
1595
- self.message = Message(
1596
- client, {
1597
- 'ok': True, 'result': result.get(
1598
- 'message', {})})
1599
- self.inline_message_id = result.get('inline_message_id')
1600
- self.chat_instance = result.get('chat_instance')
1601
- self.data = result.get('data')
1602
- self.chat = self.message.chat
1603
-
1604
- def answer(self,
1605
- text: str,
1606
- reply_markup: Optional[Union['MenuKeyboardMarkup',
1607
- 'InlineKeyboardMarkup']] = None) -> 'Message':
1608
- return self.client.send_message(
1609
- chat_id=self.message.chat.id,
1610
- text=text,
1611
- reply_markup=reply_markup)
1612
-
1613
- def reply(self,
1614
- text: str,
1615
- reply_markup: Optional[Union['MenuKeyboardMarkup',
1616
- 'InlineKeyboardMarkup']] = None) -> 'Message':
1617
- return self.client.send_message(
1618
- chat_id=self.message.chat.id,
1619
- text=text,
1620
- reply_markup=reply_markup,
1621
- reply_to_message=self.message.id)
1622
-
1623
-
1624
- class Client:
1625
- """Main client class for interacting with Bale API"""
1626
-
1627
- def __init__(
1628
- self,
1629
- token: str,
1630
- session: str = 'https://tapi.bale.ai',
1631
- database_name='database.db',
1632
- auto_log_start_message: bool = True,
1633
- ):
1634
- self.token = token
1635
- self.session = session
1636
- self.states = {}
1637
- self.database_name = database_name
1638
- self.auto_log_start_message = auto_log_start_message
1639
- self._base_url = f"{session}/bot{token}"
1640
- self._file_url = f"{session}/file/bot{token}"
1641
- self._session = requests.Session()
1642
- self._message_handler = None
1643
- self._message_edit_handler = None
1644
- self._callback_handler = None
1645
- self._member_leave_handler = None
1646
- self._member_join_handler = None
1647
- self._threads = []
1648
- self._polling = False
1649
- self.user = None
1650
- self.event_handlers = []
1651
- self.message_queue = queue.Queue()
1652
- self.lock = threading.Lock()
1653
- self.waiting_events = {}
1654
-
1655
- def set_state(self,
1656
- chat_or_user_id: Union[Chat,
1657
- User,
1658
- int,
1659
- str],
1660
- state: str) -> None:
1661
- """Set the state for a chat or user"""
1662
- if isinstance(chat_or_user_id, (Chat, User)):
1663
- chat_or_user_id = chat_or_user_id.id
1664
- self.states[str(chat_or_user_id)] = state
1665
-
1666
- def get_state(self,
1667
- chat_or_user_id: Union[Chat,
1668
- User,
1669
- int,
1670
- str]) -> str | None:
1671
- """Get the state for a chat or user"""
1672
- if isinstance(chat_or_user_id, (Chat, User)):
1673
- chat_or_user_id = chat_or_user_id.id
1674
- return self.states.get(str(chat_or_user_id))
1675
-
1676
- def del_state(self, chat_or_user_id: Union[Chat, User, int, str]) -> None:
1677
- """Delete the state for a chat or user"""
1678
- if isinstance(chat_or_user_id, (Chat, User)):
1679
- chat_or_user_id = chat_or_user_id.id
1680
- self.states.pop(str(chat_or_user_id), None)
1681
-
1682
- @property
1683
- def database(self) -> DataBase:
1684
- """Get the database name"""
1685
- db = DataBase(self.database_name)
1686
- return db
1687
-
1688
- def get_chat(self, chat_id: int) -> Optional[Dict]:
1689
- """Get chat information from database"""
1690
- conn = sqlite3.connect(self.database)
1691
- cursor = conn.cursor()
1692
- cursor.execute('SELECT * FROM chats WHERE chat_id = ?', (chat_id,))
1693
- chat = cursor.fetchone()
1694
- conn.close()
1695
- if chat:
1696
- return {
1697
- 'chat_id': chat[0],
1698
- 'type': chat[1],
1699
- 'title': chat[2],
1700
- 'created_at': chat[3]
1701
- }
1702
- return None
1703
-
1704
- def _make_request(self, method: str, endpoint: str,
1705
- **kwargs) -> Dict[str, Any]:
1706
- """Make an HTTP request to Bale API"""
1707
- url = f"{self._base_url}/{endpoint}"
1708
- response = self._session.request(method, url, **kwargs)
1709
- response_data = response.json()
1710
- if not response_data.get('ok'):
1711
- raise BaleException(
1712
- response_data['error_code'],
1713
- response_data['description'])
1714
- return response_data
1715
-
1716
- def __del__(self):
1717
- if hasattr(self, '_session'):
1718
- self._session.close()
1719
-
1720
- def get_me(self) -> User:
1721
- """Get information about the bot"""
1722
- data = self._make_request('GET', 'getMe')
1723
- return User(self, data)
1724
-
1725
- def get_file(self, file_id: str) -> bytes:
1726
- """Get file information from Bale API"""
1727
- data = {
1728
- 'file_id': file_id
1729
- }
1730
- response = self._make_request('POST', 'getFile', json=data)
1731
- file_path = response['result']['file_path']
1732
- url = f"{self._file_url}/{file_path}"
1733
- file_response = self._session.get(url)
1734
- return file_response.content
1735
-
1736
- def set_webhook(self, url: str, certificate: Optional[str] = None,
1737
- max_connections: Optional[int] = None) -> bool:
1738
- """Set webhook for getting updates"""
1739
- data = {
1740
- 'url': url,
1741
- 'certificate': certificate,
1742
- 'max_connections': max_connections
1743
- }
1744
- return self._make_request('POST', 'setWebhook', json=data)
1745
-
1746
- def get_webhook_info(self) -> Dict[str, Any]:
1747
- """Get current webhook status"""
1748
- return self._make_request('GET', 'getWebhookInfo')
1749
-
1750
- def send_message(self,
1751
- chat_id: Union[int,
1752
- str],
1753
- text: str,
1754
- parse_mode: Optional[str] = None,
1755
- reply_markup: Union[MenuKeyboardMarkup,
1756
- InlineKeyboardMarkup] = None,
1757
- reply_to_message: Union[Message,
1758
- int,
1759
- str] = None) -> Message:
1760
- """Send a message to a chat"""
1761
-
1762
- text = str(text)
1763
-
1764
- data = {
1765
- 'chat_id': chat_id,
1766
- 'text': text,
1767
- 'parse_mode': parse_mode,
1768
- 'reply_markup': reply_markup.keyboard if reply_markup else None,
1769
- 'reply_to_message_id': reply_to_message.message_id if isinstance(
1770
- reply_to_message,
1771
- Message) else reply_to_message}
1772
- response = self._make_request('POST', 'sendMessage', json=data)
1773
- return Message(self, response)
1774
-
1775
- def forward_message(self, chat_id: Union[int, str],
1776
- from_chat_id: Union[int, str],
1777
- message_id: int) -> Message:
1778
- """Forward a message from one chat to another"""
1779
- data = {
1780
- 'chat_id': chat_id,
1781
- 'from_chat_id': from_chat_id,
1782
- 'message_id': message_id
1783
- }
1784
- response = self._make_request('POST', 'forwardMessage', json=data)
1785
- return Message(self, response)
1786
-
1787
- def send_photo(self,
1788
- chat_id: Union[int,
1789
- str],
1790
- photo: Union[str,
1791
- bytes,
1792
- InputFile],
1793
- caption: Optional[str] = None,
1794
- parse_mode: Optional[str] = None,
1795
- reply_markup: Union[MenuKeyboardMarkup,
1796
- InlineKeyboardMarkup] = None,
1797
- reply_to_message: Union[Message,
1798
- int,
1799
- str] = None) -> Message:
1800
- """Send a photo to a chat"""
1801
- files = None
1802
- data = {
1803
- 'chat_id': chat_id,
1804
- 'caption': caption,
1805
- 'parse_mode': parse_mode,
1806
- 'reply_markup': reply_markup.keyboard if reply_markup else None,
1807
- 'reply_to_message_id': reply_to_message.message_id if isinstance(
1808
- reply_to_message,
1809
- Message) else reply_to_message}
1810
-
1811
- if isinstance(photo, (bytes, InputFile)) or hasattr(photo, 'read'):
1812
- files = {
1813
- 'photo': photo if not isinstance(
1814
- photo, InputFile) else photo.file}
1815
- else:
1816
- data['photo'] = photo
1817
-
1818
- response = self._make_request(
1819
- 'POST', 'sendPhoto', data=data, files=files)
1820
- return Message(self, response)
1821
-
1822
- def delete_message(self, chat_id: Union[int, str],
1823
- message_id: int) -> bool:
1824
- """Delete a message from a chat"""
1825
- data = {
1826
- 'chat_id': chat_id,
1827
- 'message_id': message_id
1828
- }
1829
- return self._make_request('POST', 'deleteMessage', json=data)
1830
-
1831
- def get_user(self, user_id: Union[int, str]) -> User:
1832
- """Get information about a user"""
1833
- data = self._make_request('POST', 'getChat', json={'chat_id': user_id})
1834
- return User(self, data)
1835
-
1836
- def edit_message(self,
1837
- chat_id: Union[int,
1838
- str],
1839
- message_id: int,
1840
- text: str,
1841
- parse_mode: Optional[str] = None,
1842
- reply_markup: Union[MenuKeyboardMarkup,
1843
- InlineKeyboardMarkup] = None) -> Message:
1844
- """Edit a message in a chat"""
1845
- data = {
1846
- 'chat_id': chat_id,
1847
- 'message_id': message_id,
1848
- 'text': text,
1849
- 'parse_mode': parse_mode,
1850
- 'reply_markup': reply_markup.keyboard if reply_markup else None
1851
- }
1852
- response = self._make_request('POST', 'editMessageText', json=data)
1853
- return Message(self, response)
1854
-
1855
- def get_chat(self, chat_id: Union[int, str]) -> Chat:
1856
- """Get information about a chat"""
1857
- data = self._make_request('POST', 'getChat', json={'chat_id': chat_id})
1858
- return Chat(self, data)
1859
-
1860
- def send_audio(self,
1861
- chat_id: Union[int,
1862
- str],
1863
- audio,
1864
- caption: Optional[str] = None,
1865
- parse_mode: Optional[str] = None,
1866
- reply_markup: Union[MenuKeyboardMarkup,
1867
- InlineKeyboardMarkup] = None,
1868
- reply_to_message: Union[Message,
1869
- int,
1870
- str] = None) -> Message:
1871
- """Send an audio file"""
1872
- files = None
1873
- data = {
1874
- 'chat_id': chat_id,
1875
- 'caption': caption,
1876
- 'parse_mode': parse_mode,
1877
- 'reply_markup': reply_markup.keyboard if reply_markup else None,
1878
- 'reply_to_message_id': reply_to_message.message_id if isinstance(
1879
- reply_to_message,
1880
- Message) else reply_to_message}
1881
- if isinstance(audio, (bytes, InputFile)) or hasattr(audio, 'read'):
1882
- files = {
1883
- 'audio': audio if not isinstance(
1884
- audio, InputFile) else audio.file}
1885
- else:
1886
- data['audio'] = audio
1887
-
1888
- response = self._make_request(
1889
- 'POST', 'sendAudio', data=data, files=files)
1890
- return Message(self, response)
1891
-
1892
- def send_document(self,
1893
- chat_id: Union[int,
1894
- str],
1895
- document,
1896
- caption: Optional[str] = None,
1897
- parse_mode: Optional[str] = None,
1898
- reply_markup: Union[MenuKeyboardMarkup,
1899
- InlineKeyboardMarkup] = None,
1900
- reply_to_message: Union[Message,
1901
- int,
1902
- str] = None) -> Message:
1903
- """Send a document"""
1904
- files = None
1905
- data = {
1906
- 'chat_id': chat_id,
1907
- 'caption': caption,
1908
- 'parse_mode': parse_mode,
1909
- 'reply_markup': reply_markup.keyboard if reply_markup else None,
1910
- 'reply_to_message_id': reply_to_message.message_id if isinstance(
1911
- reply_to_message,
1912
- Message) else reply_to_message}
1913
-
1914
- if isinstance(
1915
- document, (bytes, InputFile)) or hasattr(
1916
- document, 'read'):
1917
- files = {'document': document if not isinstance(
1918
- document, InputFile) else document.file}
1919
- else:
1920
- data['document'] = document
1921
-
1922
- response = self._make_request(
1923
- 'POST', 'sendDocument', data=data, files=files)
1924
- return Message(self, response)
1925
-
1926
- def send_video(self,
1927
- chat_id: Union[int,
1928
- str],
1929
- video,
1930
- caption: Optional[str] = None,
1931
- parse_mode: Optional[str] = None,
1932
- reply_markup: Union[MenuKeyboardMarkup,
1933
- InlineKeyboardMarkup] = None,
1934
- reply_to_message: Union[Message,
1935
- int,
1936
- str] = None) -> Message:
1937
- """Send a video"""
1938
- files = None
1939
- data = {
1940
- 'chat_id': chat_id,
1941
- 'caption': caption,
1942
- 'parse_mode': parse_mode,
1943
- 'reply_markup': reply_markup.keyboard if reply_markup else None,
1944
- 'reply_to_message_id': reply_to_message.message_id if isinstance(
1945
- reply_to_message,
1946
- Message) else reply_to_message}
1947
-
1948
- if isinstance(video, (bytes, InputFile)) or hasattr(video, 'read'):
1949
- files = {
1950
- 'video': video if not isinstance(
1951
- video, InputFile) else video.file}
1952
- else:
1953
- data['video'] = video
1954
-
1955
- response = self._make_request(
1956
- 'POST', 'sendVideo', data=data, files=files)
1957
- return Message(self, response)
1958
-
1959
- def send_animation(self,
1960
- chat_id: Union[int,
1961
- str],
1962
- animation,
1963
- caption: Optional[str] = None,
1964
- parse_mode: Optional[str] = None,
1965
- reply_markup: Union[MenuKeyboardMarkup,
1966
- InlineKeyboardMarkup] = None,
1967
- reply_to_message: Union[Message,
1968
- int,
1969
- str] = None) -> Message:
1970
- """Send an animation (GIF or H.264/MPEG-4 AVC video without sound)"""
1971
- files = None
1972
- data = {
1973
- 'chat_id': chat_id,
1974
- 'caption': caption,
1975
- 'parse_mode': parse_mode,
1976
- 'reply_markup': reply_markup.keyboard if reply_markup else None,
1977
- 'reply_to_message_id': reply_to_message.message_id if isinstance(
1978
- reply_to_message,
1979
- Message) else reply_to_message}
1980
-
1981
- if isinstance(
1982
- animation, (bytes, InputFile)) or hasattr(
1983
- animation, 'read'):
1984
- files = {'animation': animation if not isinstance(
1985
- animation, InputFile) else animation.file}
1986
- else:
1987
- data['animation'] = animation
1988
-
1989
- response = self._make_request(
1990
- 'POST', 'sendAnimation', data=data, files=files)
1991
- return Message(self, response)
1992
-
1993
- def send_voice(self,
1994
- chat_id: Union[int,
1995
- str],
1996
- voice,
1997
- caption: Optional[str] = None,
1998
- parse_mode: Optional[str] = None,
1999
- reply_markup: Union[MenuKeyboardMarkup,
2000
- InlineKeyboardMarkup] = None,
2001
- reply_to_message: Union[Message,
2002
- int,
2003
- str] = None) -> Message:
2004
- """Send a voice message"""
2005
- files = None
2006
- data = {
2007
- 'chat_id': chat_id,
2008
- 'caption': caption,
2009
- 'parse_mode': parse_mode,
2010
- 'reply_markup': reply_markup.keyboard if reply_markup else None,
2011
- 'reply_to_message_id': reply_to_message.message_id if isinstance(
2012
- reply_to_message,
2013
- Message) else reply_to_message}
2014
-
2015
- if isinstance(voice, (bytes, InputFile)) or hasattr(voice, 'read'):
2016
- files = {
2017
- 'voice': voice if not isinstance(
2018
- voice, InputFile) else voice.file}
2019
- else:
2020
- data['voice'] = voice
2021
-
2022
- response = self._make_request(
2023
- 'POST', 'sendVoice', data=data, files=files)
2024
- return Message(self, response)
2025
-
2026
- def send_media_group(self,
2027
- chat_id: Union[int,
2028
- str],
2029
- media: List[Dict],
2030
- reply_to_message: Union[Message,
2031
- int,
2032
- str] = None) -> List[Message]:
2033
- """Send a group of photos, videos, documents or audios as an album"""
2034
- data = {
2035
- 'chat_id': chat_id,
2036
- 'media': media,
2037
- 'reply_to_message_id': reply_to_message.message_id if isinstance(
2038
- reply_to_message,
2039
- Message) else reply_to_message}
2040
- response = self._make_request('POST', 'sendMediaGroup', json=data)
2041
- return [Message(self, msg) for msg in response]
2042
-
2043
- def send_location(self,
2044
- chat_id: Union[int,
2045
- str],
2046
- latitude: float,
2047
- longitude: float,
2048
- reply_markup: Union[MenuKeyboardMarkup,
2049
- InlineKeyboardMarkup] = None,
2050
- reply_to_message: Union[Message,
2051
- int,
2052
- str] = None) -> Message:
2053
- """Send a point on the map"""
2054
- data = {
2055
- 'chat_id': chat_id,
2056
- 'latitude': latitude,
2057
- 'longitude': longitude,
2058
- 'reply_markup': reply_markup.keyboard if reply_markup else None,
2059
- 'reply_to_message_id': reply_to_message.message_id if isinstance(
2060
- reply_to_message,
2061
- Message) else reply_to_message}
2062
- response = self._make_request('POST', 'sendLocation', json=data)
2063
- return Message(self, response)
2064
-
2065
- def send_contact(self,
2066
- chat_id: Union[int,
2067
- str],
2068
- phone_number: str,
2069
- first_name: str,
2070
- last_name: Optional[str] = None,
2071
- reply_markup: Union[MenuKeyboardMarkup,
2072
- InlineKeyboardMarkup] = None,
2073
- reply_to_message: Union[Message,
2074
- int,
2075
- str] = None) -> Message:
2076
- """Send a phone contact"""
2077
- data = {
2078
- 'chat_id': chat_id,
2079
- 'phone_number': phone_number,
2080
- 'first_name': first_name,
2081
- 'last_name': last_name,
2082
- 'reply_markup': reply_markup.keyboard if reply_markup else None,
2083
- 'reply_to_message_id': reply_to_message.message_id if isinstance(
2084
- reply_to_message,
2085
- Message) else reply_to_message}
2086
- response = self._make_request('POST', 'sendContact', json=data)
2087
- return Message(self, response)
2088
-
2089
- def send_invoice(self,
2090
- chat_id: Union[int,
2091
- str],
2092
- title: str,
2093
- description: str,
2094
- payload: str,
2095
- provider_token: str,
2096
- prices: list,
2097
- photo_url: Optional[str] = None,
2098
- reply_to_message: Union[int | str | Message] = None,
2099
- reply_markup: Union[MenuKeyboardMarkup | InlineKeyboardMarkup] = None) -> Message:
2100
- """Send a invoice"""
2101
- r = []
2102
- for x in prices:
2103
- r.append(x.json)
2104
- prices = r
2105
- data = {
2106
- 'chat_id': chat_id,
2107
- 'title': title,
2108
- 'description': description,
2109
- 'payload': payload,
2110
- 'provider_token': provider_token,
2111
- 'prices': prices,
2112
- 'photo_url': photo_url,
2113
- 'reply_to_message_id': reply_to_message.message_id if isinstance(
2114
- reply_to_message,
2115
- Message) else reply_to_message,
2116
- 'reply_markup': reply_markup.keyboard if reply_markup else None}
2117
- response = self._make_request('POST', 'sendInvoice', json=data)
2118
- return Message(self, response)
2119
-
2120
- def send_chat_action(self,
2121
- chat: Union[int,
2122
- str,
2123
- 'Chat'],
2124
- action: str,
2125
- how_many_times: int = 1) -> bool:
2126
- """Send a chat action"""
2127
- if not chat:
2128
- raise ValueError("Chat ID cannot be empty")
2129
-
2130
- data = {
2131
- 'chat_id': str(chat) if isinstance(
2132
- chat, (int, str)) else str(
2133
- chat.id), 'action': action}
2134
- res = []
2135
- for _ in range(how_many_times):
2136
- response = self._make_request('POST', 'sendChatAction', json=data)
2137
- res.append(response.get('ok', False))
2138
- return all(res)
2139
-
2140
- def copy_message(self,
2141
- chat_id: Union[int,
2142
- str,
2143
- 'Chat'],
2144
- from_chat_id: Union[int,
2145
- str,
2146
- 'Chat'],
2147
- message_id: Union[int,
2148
- str,
2149
- 'Chat']):
2150
- data = {
2151
- 'chat_id': chat_id if isinstance(
2152
- chat_id, (int, str)) else chat_id.id, 'from_chat_id': from_chat_id if isinstance(
2153
- from_chat_id, (int, str)) else from_chat_id.id, 'message_id': message_id if isinstance(
2154
- message_id, (int, str)) else message_id.id}
2155
- response = self._make_request('POST', 'copyMessage', json=data)
2156
- return Message(self, response)
2157
-
2158
- def get_chat_member(self,
2159
- chat: Union[int,
2160
- str,
2161
- 'Chat'],
2162
- user: Union[int,
2163
- str,
2164
- 'User']) -> ChatMember:
2165
- """Get information about a member of a chat including their permissions"""
2166
- data = {
2167
- 'chat_id': chat if isinstance(chat, (int, str)) else chat.id,
2168
- 'user_id': user if isinstance(user, (int, str)) else user.id
2169
- }
2170
- response = self._make_request('POST', 'getChatMember', json=data)
2171
- return ChatMember(self, response['result'])
2172
-
2173
- def get_chat_administrators(
2174
- self, chat: Union[int, str, 'Chat']) -> List[ChatMember]:
2175
- """Get a list of administrators in a chat"""
2176
- data = {'chat_id': getattr(chat, 'id', chat)}
2177
- response = self._make_request(
2178
- 'POST', 'getChatAdministrators', json=data)
2179
- return [ChatMember(self, member)
2180
- for member in response.get('result', [])]
2181
-
2182
- def get_chat_members_count(self, chat: Union[int, str, 'Chat']) -> int:
2183
- """Get the number of members in a chat"""
2184
- data = {
2185
- 'chat_id': chat if isinstance(chat, (int, str)) else chat.id
2186
- }
2187
- response = self._make_request('GET', 'getChatMembersCount', json=data)
2188
- return response['result']
2189
-
2190
- def upload_sticker_file(self, user_id: int, sticker: 'InputFile'):
2191
- """Upload a file for future use in sticker sets"""
2192
- data = {
2193
- 'user_id': user_id
2194
- }
2195
- files = {
2196
- 'sticker': sticker
2197
- }
2198
- response = self._make_request('POST', 'uploadStickerFile', data=data, files=files)
2199
- return response['result']
2200
-
2201
- def create_new_sticker_set(
2202
- self, user_id: int, name: str, title: str, stickers: List['InputFile']) -> bool:
2203
- """Create a new sticker set owned by a user"""
2204
- data = {
2205
- 'user_id': user_id,
2206
- 'name': name,
2207
- 'title': title
2208
- }
2209
- files = {}
2210
- for i, sticker in enumerate(stickers):
2211
- files[f'stickers[{i}]'] = sticker
2212
- response = self._make_request('POST', 'createNewStickerSet', data=data, files=files)
2213
- return response['result']
2214
-
2215
- def add_sticker_to_set(self, user_id: int, name: str,
2216
- sticker: 'InputFile') -> bool:
2217
- """Add a new sticker to a set created by the bot"""
2218
- data = {
2219
- 'user_id': user_id,
2220
- 'name': name
2221
- }
2222
- files = {
2223
- 'sticker': sticker
2224
- }
2225
- response = self._make_request('POST', 'addStickerToSet', data=data, files=files)
2226
- return response['result']
2227
-
2228
- def is_joined(self, user: Union[User, int, str],
2229
- chat: Union[Chat, int, str]) -> bool:
2230
- """Check if user is a member of the chat"""
2231
- data = {
2232
- 'chat_id': chat if isinstance(chat, (int, str)) else chat.id,
2233
- 'user_id': user if isinstance(user, (int, str)) else user.id
2234
- }
2235
- response = self._make_request('GET', 'getChatMember', json=data)
2236
- return response.get('status') not in ['left', 'kicked']
2237
-
2238
- def on_message(self, func):
2239
- """Decorator for handling new messages"""
2240
- self._message_handler = func
2241
- return func
2242
-
2243
- def on_callback_query(self, func):
2244
- """Decorator for handling callback queries"""
2245
- self._callback_handler = func
2246
- return func
2247
-
2248
- def on_tick(self, seconds: int):
2249
- """Decorator for handling periodic events"""
2250
- def decorator(func):
2251
- if not hasattr(self, '_tick_handlers'):
2252
- self._tick_handlers = {}
2253
- self._tick_handlers[func] = {
2254
- 'interval': seconds, 'last_run': time.time()}
2255
- return func
2256
- return decorator
2257
-
2258
- def on_close(self, func):
2259
- """Decorator for handling close event"""
2260
- self._close_handler = func
2261
- return func
2262
-
2263
- def on_ready(self, func):
2264
- """Decorator for handling ready event"""
2265
- self._ready_handler = func
2266
- return func
2267
-
2268
- def on_update(self, func):
2269
- """Decorator for handling raw updates"""
2270
- self._update_handler = func
2271
- return func
2272
-
2273
- def on_member_chat_join(self, func):
2274
- """Decorator for handling new chat members"""
2275
- self._member_join_handler = func
2276
- return func
2277
-
2278
- def on_member_chat_leave(self, func):
2279
- """Decorator for handling members leaving chat"""
2280
- self._member_leave_handler = func
2281
- return func
2282
-
2283
- def on_message_edit(self, func):
2284
- """Decorator for handling edited messages"""
2285
- self._message_edit_handler = func
2286
- return func
2287
-
2288
- def on_command(self, command: str = None, case_sensitive: bool = False):
2289
- """Decorator for handling specific text commands"""
2290
- def decorator(func):
2291
- if not hasattr(self, '_text_handlers'):
2292
- self._text_handlers = {}
2293
- cmd = f"/{command.lstrip('/')}" if command else f"/{func.__name__}"
2294
- self._text_handlers[cmd] = {
2295
- 'handler': func, 'case_sensitive': case_sensitive}
2296
- return func
2297
- return decorator
2298
-
2299
- def _create_thread(self, handler, *args, **kwargs):
2300
- """Helper method to create and start a thread"""
2301
- if handler:
2302
- thread = threading.Thread(
2303
- target=handler, args=args, kwargs=kwargs, daemon=True)
2304
- thread.start()
2305
- self._threads.append(thread)
2306
- return thread
2307
- return None
2308
-
2309
- def _handle_message(self, message, update):
2310
- """Handle different types of messages"""
2311
- msg_data = update.get('message', {})
2312
-
2313
- if 'message' in update:
2314
- message = Message(self, {'ok': True, 'result': update['message']})
2315
- self.process_event(message)
2316
-
2317
- if 'new_chat_members' in msg_data and hasattr(
2318
- self, '_member_join_handler'):
2319
- chat, user = msg_data['chat'], msg_data['new_chat_members'][0]
2320
- self._create_thread(
2321
- self._member_join_handler,
2322
- message,
2323
- Chat(self, {"ok": True, "result": chat}),
2324
- User(self, {"ok": True, "result": user})
2325
- )
2326
- return
2327
-
2328
- if 'left_chat_member' in msg_data and hasattr(
2329
- self, '_member_leave_handler'):
2330
- chat, user = msg_data['chat'], msg_data['left_chat_member']
2331
- self._create_thread(
2332
- self._member_leave_handler,
2333
- message,
2334
- Chat(self, {"ok": True, "result": chat}),
2335
- User(self, {"ok": True, "result": user})
2336
- )
2337
- return
2338
-
2339
- if 'text' in msg_data and hasattr(self, '_text_handlers'):
2340
- text = msg_data['text']
2341
- for command, handler_info in self._text_handlers.items():
2342
- handler = handler_info['handler']
2343
- case_sensitive = handler_info['case_sensitive']
2344
-
2345
- if case_sensitive:
2346
- matches = text.startswith(command)
2347
- else:
2348
- matches = text.lower().startswith(command.lower())
2349
-
2350
- if matches:
2351
- params = inspect.signature(handler).parameters
2352
- args = [message]
2353
- if len(params) > 1:
2354
- command_args = text[len(command):].strip().split()
2355
- args.extend(command_args)
2356
- self._create_thread(handler, *args)
2357
- return
2358
-
2359
- if hasattr(self, '_message_handler'):
2360
- params = inspect.signature(self._message_handler).parameters
2361
- args = ((message, update) if len(params) > 1 else (message,))
2362
- result = self._message_handler(*args)
2363
- if isinstance(result, str):
2364
- message.chat.send_message(result)
2365
-
2366
- def _handle_update(self, update):
2367
- try:
2368
- if hasattr(self, '_update_handler'):
2369
- self._create_thread(self._update_handler, update)
2370
-
2371
- message_types = {
2372
- 'message': (Message, self._handle_message),
2373
- 'edited_message': (Message, lambda m, u: self._create_thread(
2374
- self._message_edit_handler, m) if hasattr(
2375
- self, '_message_edit_handler') else None)
2376
- }
2377
-
2378
- for update_type, (cls, handler) in message_types.items():
2379
- if update_type in update:
2380
- message = cls(
2381
- self, {
2382
- 'ok': True, 'result': update[update_type]})
2383
- handler(message, update)
2384
-
2385
- if 'callback_query' in update and hasattr(
2386
- self, '_callback_handler'):
2387
- callback_data = update['callback_query']
2388
- obj = CallbackQuery(
2389
- self, {'ok': True, 'result': callback_data})
2390
- message = Message(
2391
- self, {
2392
- 'ok': True, 'result': callback_data['message']}) if 'message' in callback_data else None
2393
- chat = Chat(
2394
- self, {
2395
- 'ok': True, 'result': callback_data['message']['chat']}) if message else None
2396
- user = User(
2397
- self, {
2398
- 'ok': True, 'result': callback_data['from']})
2399
-
2400
- params = inspect.signature(self._callback_handler).parameters
2401
- args = (
2402
- obj,
2403
- message,
2404
- chat,
2405
- user) if len(params) > 1 else (
2406
- obj,
2407
- )
2408
- self._create_thread(self._callback_handler, *args)
2409
- except Exception as e:
2410
- print(f"Error handling update: {e}")
2411
- traceback.print_exc()
2412
-
2413
- def _handle_tick_events(self, current_time):
2414
- """Handle periodic tick events"""
2415
- if hasattr(self, '_tick_handlers'):
2416
- for handler, info in self._tick_handlers.items():
2417
- if current_time - info['last_run'] >= info['interval']:
2418
- self._create_thread(handler)
2419
- info['last_run'] = current_time
2420
-
2421
- def run(self, debug=False):
2422
- """Start polling for new messages"""
2423
- try:
2424
- self.user = self.get_me()
2425
- except Exception as e:
2426
- raise BaleTokenNotFoundError(f"Token not found: {str(e)}")
2427
-
2428
- self._polling = True
2429
- self._threads = []
2430
- self.message_queue = queue.Queue()
2431
- self.event_handlers = []
2432
- self.lock = threading.Lock()
2433
- offset = 0
2434
- past_updates = collections.deque(maxlen=100)
2435
- source_file = inspect.getfile(self.__class__)
2436
- last_modified = os.path.getmtime(source_file)
2437
-
2438
- if self.auto_log_start_message:
2439
- print(f"-+-+-+ [logged in as @{self.get_me().username}] +-+-+-")
2440
- if hasattr(self, '_ready_handler'):
2441
- self._ready_handler()
2442
-
2443
- while self._polling:
2444
- try:
2445
- if debug and self._check_source_file_changed(
2446
- source_file, last_modified):
2447
- last_modified = os.path.getmtime(source_file)
2448
- print("Source file changed, restarting...")
2449
- python = sys.executable
2450
- os.execl(python, python, *sys.argv)
2451
-
2452
- updates = self.get_updates(offset=offset, timeout=30)
2453
- for update in updates:
2454
- update_id = update['update_id']
2455
- if update_id not in past_updates:
2456
- past_updates.append(update_id)
2457
- if 'message' in update:
2458
- message = Message(
2459
- self, {'ok': True, 'result': update['message']})
2460
- self.process_event(message)
2461
- self._handle_update(update)
2462
- offset = update_id + 1
2463
-
2464
- current_time = time.time()
2465
- self._handle_tick_events(current_time)
2466
- self._threads = [t for t in self._threads if t.is_alive()]
2467
- time.sleep(0.1)
2468
- except Exception as e:
2469
- print(f"Error in polling: {e}")
2470
- traceback.print_exc()
2471
- time.sleep(1)
2472
-
2473
- def _check_source_file_changed(self, source_file, last_modified):
2474
- """Check if source file has been modified"""
2475
- try:
2476
- return os.path.getmtime(source_file) > last_modified
2477
- except (FileNotFoundError, OSError) as e:
2478
- print(f"Error checking file modification time: {e}")
2479
- return False
2480
-
2481
- def get_updates(self, offset=None, timeout=30) -> List[Dict[str, Any]]:
2482
- """Get updates from Bale API"""
2483
- params = {'timeout': timeout}
2484
- if offset is not None:
2485
- params['offset'] = offset
2486
- response = self._make_request('GET', 'getUpdates', params=params)
2487
- return response.get('result', [])
2488
-
2489
- def safe_close(self):
2490
- """Close the client and stop polling gracefully"""
2491
- self._polling = False
2492
- for thread in self._threads:
2493
- try:
2494
- thread.join(timeout=1.0)
2495
- except Exception as e:
2496
- print(f"Error joining thread: {e}")
2497
- self._threads.clear()
2498
- if hasattr(self, '_close_handler'):
2499
- try:
2500
- self._close_handler()
2501
- except Exception as e:
2502
- print(f"Error in close handler: {e}")
2503
-
2504
- def wait_for_message(self, checker=Any):
2505
- self.waiting_events[checker]
2506
-
2507
- def create_ref_link(self, data: str) -> str:
2508
- """Create a reference link for the bot"""
2509
- return f"https://ble.ir/{self.get_me().username}?start={data}"
2510
-
2511
-
2512
- def run_multiple_bots(bots: List[Client]) -> List[Client]:
2513
- """
2514
- Run multiple bots concurrently in separate threads.
2515
-
2516
- Args:
2517
- bots: List of Client instances to run
2518
-
2519
- Returns:
2520
- List of running bot instances
2521
- """
2522
- threads = []
2523
- for bot in bots:
2524
- thread = threading.Thread(target=bot.run, daemon=True)
2525
- thread.start()
2526
- threads.append(thread)
2527
-
2528
- for thread in threads:
2529
- thread.join()
2530
-
2531
- return bots
2532
-
2533
-
2534
- def stop_bots(bots: List[Client]) -> None:
2535
- """
2536
- Stop multiple bots gracefully.
2537
-
2538
- Args:
2539
- bots: List of Client instances to stop
2540
- """
2541
- for bot in bots:
2542
- try:
2543
- bot.safe_close()
2544
- except Exception as e:
2545
- continue