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