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