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