Rubka 7.2.8__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.
- rubka/__init__.py +79 -0
- rubka/adaptorrubka/__init__.py +4 -0
- rubka/adaptorrubka/client/__init__.py +1 -0
- rubka/adaptorrubka/client/client.py +60 -0
- rubka/adaptorrubka/crypto/__init__.py +1 -0
- rubka/adaptorrubka/crypto/crypto.py +82 -0
- rubka/adaptorrubka/enums.py +36 -0
- rubka/adaptorrubka/exceptions.py +22 -0
- rubka/adaptorrubka/methods/__init__.py +1 -0
- rubka/adaptorrubka/methods/methods.py +90 -0
- rubka/adaptorrubka/network/__init__.py +3 -0
- rubka/adaptorrubka/network/helper.py +22 -0
- rubka/adaptorrubka/network/network.py +221 -0
- rubka/adaptorrubka/network/socket.py +31 -0
- rubka/adaptorrubka/sessions/__init__.py +1 -0
- rubka/adaptorrubka/sessions/sessions.py +72 -0
- rubka/adaptorrubka/types/__init__.py +1 -0
- rubka/adaptorrubka/types/socket/__init__.py +1 -0
- rubka/adaptorrubka/types/socket/message.py +187 -0
- rubka/adaptorrubka/utils/__init__.py +2 -0
- rubka/adaptorrubka/utils/configs.py +18 -0
- rubka/adaptorrubka/utils/utils.py +251 -0
- rubka/api.py +1723 -0
- rubka/asynco.py +2541 -0
- rubka/button.py +404 -0
- rubka/config.py +3 -0
- rubka/context.py +1077 -0
- rubka/decorators.py +30 -0
- rubka/exceptions.py +37 -0
- rubka/filters.py +330 -0
- rubka/helpers.py +1461 -0
- rubka/jobs.py +15 -0
- rubka/keyboards.py +16 -0
- rubka/keypad.py +298 -0
- rubka/logger.py +12 -0
- rubka/metadata.py +114 -0
- rubka/rubino.py +1271 -0
- rubka/tv.py +145 -0
- rubka/update.py +1038 -0
- rubka/utils.py +3 -0
- rubka-7.2.8.dist-info/METADATA +1047 -0
- rubka-7.2.8.dist-info/RECORD +45 -0
- rubka-7.2.8.dist-info/WHEEL +5 -0
- rubka-7.2.8.dist-info/entry_points.txt +2 -0
- rubka-7.2.8.dist-info/top_level.txt +1 -0
rubka/api.py
ADDED
|
@@ -0,0 +1,1723 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from typing import List, Optional, Dict, Any, Literal
|
|
3
|
+
from .exceptions import APIRequestError
|
|
4
|
+
from .adaptorrubka import Client as Client_get
|
|
5
|
+
from .logger import logger
|
|
6
|
+
from . import filters
|
|
7
|
+
from . import helpers
|
|
8
|
+
from typing import Callable
|
|
9
|
+
from .context import Message,InlineMessage
|
|
10
|
+
from typing import Optional, Union, Literal, Dict, Any
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import time
|
|
13
|
+
import datetime
|
|
14
|
+
import tempfile
|
|
15
|
+
from tqdm import tqdm
|
|
16
|
+
import os
|
|
17
|
+
API_URL = "https://botapi.rubika.ir/v3"
|
|
18
|
+
import mimetypes
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
import subprocess
|
|
22
|
+
class InvalidTokenError(Exception):pass
|
|
23
|
+
def install_package(package_name):
|
|
24
|
+
try:
|
|
25
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", package_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
26
|
+
return True
|
|
27
|
+
except Exception:return False
|
|
28
|
+
|
|
29
|
+
def get_importlib_metadata():
|
|
30
|
+
try:
|
|
31
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
32
|
+
return version, PackageNotFoundError
|
|
33
|
+
except ImportError:
|
|
34
|
+
if install_package("importlib-metadata"):
|
|
35
|
+
try:
|
|
36
|
+
from importlib_metadata import version, PackageNotFoundError
|
|
37
|
+
return version, PackageNotFoundError
|
|
38
|
+
except ImportError:
|
|
39
|
+
return None, None
|
|
40
|
+
return None, None
|
|
41
|
+
|
|
42
|
+
version, PackageNotFoundError = get_importlib_metadata()
|
|
43
|
+
def get_installed_version(package_name: str) -> str:
|
|
44
|
+
if version is None:return "unknown"
|
|
45
|
+
try:
|
|
46
|
+
return version(package_name)
|
|
47
|
+
except PackageNotFoundError:
|
|
48
|
+
return None
|
|
49
|
+
def get_latest_version(package_name: str) -> str:
|
|
50
|
+
url = f"https://pypi.org/pypi/{package_name}/json"
|
|
51
|
+
try:
|
|
52
|
+
resp = requests.get(url, timeout=5)
|
|
53
|
+
resp.raise_for_status()
|
|
54
|
+
data = resp.json()
|
|
55
|
+
return data["info"]["version"]
|
|
56
|
+
except Exception:return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def show_last_six_words(text):
|
|
60
|
+
text = text.strip()
|
|
61
|
+
return text[-6:]
|
|
62
|
+
import requests
|
|
63
|
+
from pathlib import Path
|
|
64
|
+
from typing import Union, Optional, Dict, Any, Literal
|
|
65
|
+
import tempfile
|
|
66
|
+
import os
|
|
67
|
+
class Robot:
|
|
68
|
+
"""
|
|
69
|
+
Main class to interact with Rubika Bot API.
|
|
70
|
+
Initialized with bot token.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, token: str,session_name : str = None,auth : str = None , Key : str = None,platform : str ="web",web_hook:str=None,timeout : int =10,show_progress:bool=False):
|
|
74
|
+
"""
|
|
75
|
+
راهاندازی اولیه ربات روبیکا و پیکربندی پارامترهای پایه.
|
|
76
|
+
|
|
77
|
+
Parameters:
|
|
78
|
+
token (str): توکن اصلی ربات برای احراز هویت با Rubika Bot API. این مقدار الزامی است.
|
|
79
|
+
session_name (str, optional): نام نشست دلخواه برای شناسایی یا جداسازی کلاینتها. پیشفرض None.
|
|
80
|
+
auth (str, optional): مقدار احراز هویت اضافی برای اتصال به کلاینت سفارشی. پیشفرض None.
|
|
81
|
+
Key (str, optional): کلید اضافی برای رمزنگاری یا تأیید. در صورت نیاز استفاده میشود. پیشفرض None.
|
|
82
|
+
platform (str, optional): پلتفرم اجرایی مورد نظر (مثل "web" یا "android"). پیشفرض "web".
|
|
83
|
+
timeout (int, optional): مدتزمان تایماوت برای درخواستهای HTTP بر حسب ثانیه. پیشفرض 10 ثانیه.
|
|
84
|
+
|
|
85
|
+
توضیحات:
|
|
86
|
+
- این تابع یک شیء Session از requests میسازد برای استفاده در تمام درخواستها.
|
|
87
|
+
- هندلرهای مختلف مانند پیام، دکمه، کوئری اینلاین و ... در این مرحله مقداردهی اولیه میشوند.
|
|
88
|
+
- لیست `self._callback_handlers` برای مدیریت چندین callback مختلف الزامی است.
|
|
89
|
+
|
|
90
|
+
مثال:
|
|
91
|
+
>>> bot = Robot(token="BOT_TOKEN", platform="android", timeout=15)
|
|
92
|
+
"""
|
|
93
|
+
self.token = token
|
|
94
|
+
self._inline_query_handlers = []
|
|
95
|
+
self._inline_query_handlers: List[dict] = []
|
|
96
|
+
self.timeout = timeout
|
|
97
|
+
self.auth = auth
|
|
98
|
+
self.show_progress = show_progress
|
|
99
|
+
self.session_name = session_name
|
|
100
|
+
self.Key = Key
|
|
101
|
+
|
|
102
|
+
self.platform = platform
|
|
103
|
+
self.web_hook = web_hook
|
|
104
|
+
self.hook = web_hook
|
|
105
|
+
self._offset_id = None
|
|
106
|
+
self.session = requests.Session()
|
|
107
|
+
self.sessions: Dict[str, Dict[str, Any]] = {}
|
|
108
|
+
self._callback_handler = None
|
|
109
|
+
self._message_handler = None
|
|
110
|
+
self._inline_query_handler = None
|
|
111
|
+
self._message_handlers: List[dict] = []
|
|
112
|
+
self._callback_handlers = None
|
|
113
|
+
self._callback_handlers = []
|
|
114
|
+
self.geteToken()
|
|
115
|
+
if web_hook:
|
|
116
|
+
try:
|
|
117
|
+
json_url = requests.get(web_hook, timeout=self.timeout).json().get('url', web_hook)
|
|
118
|
+
for endpoint_type in [
|
|
119
|
+
"ReceiveUpdate",
|
|
120
|
+
"ReceiveInlineMessage",
|
|
121
|
+
"ReceiveQuery",
|
|
122
|
+
"GetSelectionItem",
|
|
123
|
+
"SearchSelectionItems"
|
|
124
|
+
]:
|
|
125
|
+
print(self.update_bot_endpoint(self.web_hook, endpoint_type))
|
|
126
|
+
self.web_hook = json_url
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"Failed to set webhook from {web_hook}: {e}")
|
|
129
|
+
else:
|
|
130
|
+
self.web_hook = self.hook
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
logger.info(f"Initialized RubikaBot with token: {token[:8]}***")
|
|
135
|
+
def _post(self, method: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
136
|
+
url = f"{API_URL}/{self.token}/{method}"
|
|
137
|
+
try:
|
|
138
|
+
response = self.session.post(url, json=data, timeout=self.timeout)
|
|
139
|
+
response.raise_for_status()
|
|
140
|
+
try:
|
|
141
|
+
json_resp = response.json()
|
|
142
|
+
except ValueError:
|
|
143
|
+
logger.error(f"Invalid JSON response from {method}: {response.text}")
|
|
144
|
+
raise APIRequestError(f"Invalid JSON response: {response.text}")
|
|
145
|
+
if method != "getUpdates":logger.debug(f"API Response from {method}: {json_resp}")
|
|
146
|
+
|
|
147
|
+
return json_resp
|
|
148
|
+
except requests.RequestException as e:
|
|
149
|
+
logger.error(f"API request failed: {e}")
|
|
150
|
+
raise APIRequestError(f"API request failed: {e}") from e
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_me(self) -> Dict[str, Any]:
|
|
154
|
+
"""Get info about the bot itself."""
|
|
155
|
+
return self._post("getMe", {})
|
|
156
|
+
def geteToken(self):
|
|
157
|
+
"""b"""
|
|
158
|
+
if self.get_me()['status'] != "OK":
|
|
159
|
+
raise InvalidTokenError("The provided bot token is invalid or expired.")
|
|
160
|
+
def on_message_private(
|
|
161
|
+
self,
|
|
162
|
+
chat_id: Optional[Union[str, List[str]]] = None,
|
|
163
|
+
commands: Optional[List[str]] = None,
|
|
164
|
+
filters: Optional[Callable[[Message], bool]] = None,
|
|
165
|
+
sender_id: Optional[Union[str, List[str]]] = None,
|
|
166
|
+
sender_type: Optional[str] = None,
|
|
167
|
+
allow_forwarded: bool = True,
|
|
168
|
+
allow_files: bool = True,
|
|
169
|
+
allow_stickers: bool = True,
|
|
170
|
+
allow_polls: bool = True,
|
|
171
|
+
allow_contacts: bool = True,
|
|
172
|
+
allow_locations: bool = True,
|
|
173
|
+
min_text_length: Optional[int] = None,
|
|
174
|
+
max_text_length: Optional[int] = None,
|
|
175
|
+
contains: Optional[str] = None,
|
|
176
|
+
startswith: Optional[str] = None,
|
|
177
|
+
endswith: Optional[str] = None,
|
|
178
|
+
case_sensitive: bool = False
|
|
179
|
+
):
|
|
180
|
+
"""
|
|
181
|
+
Sync decorator for handling only private messages with extended filters.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
185
|
+
def wrapper(bot, message: Message):
|
|
186
|
+
|
|
187
|
+
if not message.is_private:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if chat_id:
|
|
192
|
+
if isinstance(chat_id, str) and message.chat_id != chat_id:
|
|
193
|
+
return
|
|
194
|
+
if isinstance(chat_id, list) and message.chat_id not in chat_id:
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if sender_id:
|
|
199
|
+
if isinstance(sender_id, str) and message.sender_id != sender_id:
|
|
200
|
+
return
|
|
201
|
+
if isinstance(sender_id, list) and message.sender_id not in sender_id:
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
if sender_type and message.sender_type != sender_type:
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if not allow_forwarded and message.forwarded_from:
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if not allow_files and message.file:
|
|
214
|
+
return
|
|
215
|
+
if not allow_stickers and message.sticker:
|
|
216
|
+
return
|
|
217
|
+
if not allow_polls and message.poll:
|
|
218
|
+
return
|
|
219
|
+
if not allow_contacts and message.contact_message:
|
|
220
|
+
return
|
|
221
|
+
if not allow_locations and (message.location or message.live_location):
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if message.text:
|
|
226
|
+
text = message.text if case_sensitive else message.text.lower()
|
|
227
|
+
if min_text_length and len(message.text) < min_text_length:
|
|
228
|
+
return
|
|
229
|
+
if max_text_length and len(message.text) > max_text_length:
|
|
230
|
+
return
|
|
231
|
+
if contains and (contains if case_sensitive else contains.lower()) not in text:
|
|
232
|
+
return
|
|
233
|
+
if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
|
|
234
|
+
return
|
|
235
|
+
if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
if commands:
|
|
240
|
+
if not message.text:
|
|
241
|
+
return
|
|
242
|
+
parts = message.text.strip().split()
|
|
243
|
+
cmd = parts[0].lstrip("/")
|
|
244
|
+
if cmd not in commands:
|
|
245
|
+
return
|
|
246
|
+
message.args = parts[1:]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if filters and not filters(message):
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
return func(bot, message)
|
|
253
|
+
|
|
254
|
+
self._message_handlers.append({
|
|
255
|
+
"func": wrapper,
|
|
256
|
+
"filters": filters,
|
|
257
|
+
"commands": commands,
|
|
258
|
+
"chat_id": chat_id,
|
|
259
|
+
"private_only": True,
|
|
260
|
+
"sender_id": sender_id,
|
|
261
|
+
"sender_type": sender_type
|
|
262
|
+
})
|
|
263
|
+
return wrapper
|
|
264
|
+
|
|
265
|
+
return decorator
|
|
266
|
+
def on_message_channel(
|
|
267
|
+
self,
|
|
268
|
+
chat_id: Optional[Union[str, List[str]]] = None,
|
|
269
|
+
commands: Optional[List[str]] = None,
|
|
270
|
+
filters: Optional[Callable[[Message], bool]] = None,
|
|
271
|
+
sender_id: Optional[Union[str, List[str]]] = None,
|
|
272
|
+
sender_type: Optional[str] = None,
|
|
273
|
+
allow_forwarded: bool = True,
|
|
274
|
+
allow_files: bool = True,
|
|
275
|
+
allow_stickers: bool = True,
|
|
276
|
+
allow_polls: bool = True,
|
|
277
|
+
allow_contacts: bool = True,
|
|
278
|
+
allow_locations: bool = True,
|
|
279
|
+
min_text_length: Optional[int] = None,
|
|
280
|
+
max_text_length: Optional[int] = None,
|
|
281
|
+
contains: Optional[str] = None,
|
|
282
|
+
startswith: Optional[str] = None,
|
|
283
|
+
endswith: Optional[str] = None,
|
|
284
|
+
case_sensitive: bool = False
|
|
285
|
+
):
|
|
286
|
+
"""
|
|
287
|
+
Sync decorator for handling only private messages with extended filters.
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
291
|
+
def wrapper(bot, message: Message):
|
|
292
|
+
|
|
293
|
+
if not message.is_channel:
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
if chat_id:
|
|
298
|
+
if isinstance(chat_id, str) and message.chat_id != chat_id:
|
|
299
|
+
return
|
|
300
|
+
if isinstance(chat_id, list) and message.chat_id not in chat_id:
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
if sender_id:
|
|
305
|
+
if isinstance(sender_id, str) and message.sender_id != sender_id:
|
|
306
|
+
return
|
|
307
|
+
if isinstance(sender_id, list) and message.sender_id not in sender_id:
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
if sender_type and message.sender_type != sender_type:
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
if not allow_forwarded and message.forwarded_from:
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if not allow_files and message.file:
|
|
320
|
+
return
|
|
321
|
+
if not allow_stickers and message.sticker:
|
|
322
|
+
return
|
|
323
|
+
if not allow_polls and message.poll:
|
|
324
|
+
return
|
|
325
|
+
if not allow_contacts and message.contact_message:
|
|
326
|
+
return
|
|
327
|
+
if not allow_locations and (message.location or message.live_location):
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
if message.text:
|
|
332
|
+
text = message.text if case_sensitive else message.text.lower()
|
|
333
|
+
if min_text_length and len(message.text) < min_text_length:
|
|
334
|
+
return
|
|
335
|
+
if max_text_length and len(message.text) > max_text_length:
|
|
336
|
+
return
|
|
337
|
+
if contains and (contains if case_sensitive else contains.lower()) not in text:
|
|
338
|
+
return
|
|
339
|
+
if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
|
|
340
|
+
return
|
|
341
|
+
if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
if commands:
|
|
346
|
+
if not message.text:
|
|
347
|
+
return
|
|
348
|
+
parts = message.text.strip().split()
|
|
349
|
+
cmd = parts[0].lstrip("/")
|
|
350
|
+
if cmd not in commands:
|
|
351
|
+
return
|
|
352
|
+
message.args = parts[1:]
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
if filters and not filters(message):
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
return func(bot, message)
|
|
359
|
+
|
|
360
|
+
self._message_handlers.append({
|
|
361
|
+
"func": wrapper,
|
|
362
|
+
"filters": filters,
|
|
363
|
+
"commands": commands,
|
|
364
|
+
"chat_id": chat_id,
|
|
365
|
+
"private_only": True,
|
|
366
|
+
"sender_id": sender_id,
|
|
367
|
+
"sender_type": sender_type
|
|
368
|
+
})
|
|
369
|
+
return wrapper
|
|
370
|
+
|
|
371
|
+
return decorator
|
|
372
|
+
|
|
373
|
+
def on_message_group(
|
|
374
|
+
self,
|
|
375
|
+
chat_id: Optional[Union[str, List[str]]] = None,
|
|
376
|
+
commands: Optional[List[str]] = None,
|
|
377
|
+
filters: Optional[Callable[[Message], bool]] = None,
|
|
378
|
+
sender_id: Optional[Union[str, List[str]]] = None,
|
|
379
|
+
sender_type: Optional[str] = None,
|
|
380
|
+
allow_forwarded: bool = True,
|
|
381
|
+
allow_files: bool = True,
|
|
382
|
+
allow_stickers: bool = True,
|
|
383
|
+
allow_polls: bool = True,
|
|
384
|
+
allow_contacts: bool = True,
|
|
385
|
+
allow_locations: bool = True,
|
|
386
|
+
min_text_length: Optional[int] = None,
|
|
387
|
+
max_text_length: Optional[int] = None,
|
|
388
|
+
contains: Optional[str] = None,
|
|
389
|
+
startswith: Optional[str] = None,
|
|
390
|
+
endswith: Optional[str] = None,
|
|
391
|
+
case_sensitive: bool = False
|
|
392
|
+
):
|
|
393
|
+
"""
|
|
394
|
+
Sync decorator for handling only group messages with extended filters.
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
398
|
+
def wrapper(bot, message: Message):
|
|
399
|
+
|
|
400
|
+
if not message.is_group:
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
if chat_id:
|
|
405
|
+
if isinstance(chat_id, str) and message.chat_id != chat_id:
|
|
406
|
+
return
|
|
407
|
+
if isinstance(chat_id, list) and message.chat_id not in chat_id:
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
if sender_id:
|
|
412
|
+
if isinstance(sender_id, str) and message.sender_id != sender_id:
|
|
413
|
+
return
|
|
414
|
+
if isinstance(sender_id, list) and message.sender_id not in sender_id:
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
if sender_type and message.sender_type != sender_type:
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
if not allow_forwarded and message.forwarded_from:
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
if not allow_files and message.file:
|
|
427
|
+
return
|
|
428
|
+
if not allow_stickers and message.sticker:
|
|
429
|
+
return
|
|
430
|
+
if not allow_polls and message.poll:
|
|
431
|
+
return
|
|
432
|
+
if not allow_contacts and message.contact_message:
|
|
433
|
+
return
|
|
434
|
+
if not allow_locations and (message.location or message.live_location):
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
if message.text:
|
|
439
|
+
text = message.text if case_sensitive else message.text.lower()
|
|
440
|
+
if min_text_length and len(message.text) < min_text_length:
|
|
441
|
+
return
|
|
442
|
+
if max_text_length and len(message.text) > max_text_length:
|
|
443
|
+
return
|
|
444
|
+
if contains and (contains if case_sensitive else contains.lower()) not in text:
|
|
445
|
+
return
|
|
446
|
+
if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
|
|
447
|
+
return
|
|
448
|
+
if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
if commands:
|
|
453
|
+
if not message.text:
|
|
454
|
+
return
|
|
455
|
+
parts = message.text.strip().split()
|
|
456
|
+
cmd = parts[0].lstrip("/")
|
|
457
|
+
if cmd not in commands:
|
|
458
|
+
return
|
|
459
|
+
message.args = parts[1:]
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
if filters and not filters(message):
|
|
463
|
+
return
|
|
464
|
+
|
|
465
|
+
return func(bot, message)
|
|
466
|
+
|
|
467
|
+
self._message_handlers.append({
|
|
468
|
+
"func": wrapper,
|
|
469
|
+
"filters": filters,
|
|
470
|
+
"commands": commands,
|
|
471
|
+
"chat_id": chat_id,
|
|
472
|
+
"group_only": True,
|
|
473
|
+
"sender_id": sender_id,
|
|
474
|
+
"sender_type": sender_type
|
|
475
|
+
})
|
|
476
|
+
return wrapper
|
|
477
|
+
|
|
478
|
+
return decorator
|
|
479
|
+
|
|
480
|
+
def on_message(
|
|
481
|
+
self,
|
|
482
|
+
filters: Optional[Callable[[Message], bool]] = None,
|
|
483
|
+
commands: Optional[List[str]] = None
|
|
484
|
+
):
|
|
485
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
486
|
+
def wrapper(bot, message: Message):
|
|
487
|
+
if filters and not filters(message):
|
|
488
|
+
return
|
|
489
|
+
if commands:
|
|
490
|
+
if not getattr(message, "is_command", False):
|
|
491
|
+
return
|
|
492
|
+
cmd = message.text.split()[0].lstrip("/") if message.text else ""
|
|
493
|
+
if cmd not in commands:
|
|
494
|
+
return
|
|
495
|
+
return func(bot, message)
|
|
496
|
+
self._message_handlers.append({
|
|
497
|
+
"func": wrapper,
|
|
498
|
+
"filters": filters,
|
|
499
|
+
"commands": commands
|
|
500
|
+
})
|
|
501
|
+
return wrapper
|
|
502
|
+
return decorator
|
|
503
|
+
def on_message_file(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
504
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
505
|
+
def wrapper(bot, message: Message):
|
|
506
|
+
if not message.file:
|
|
507
|
+
return
|
|
508
|
+
if filters and not filters(message):
|
|
509
|
+
return
|
|
510
|
+
return func(bot, message)
|
|
511
|
+
|
|
512
|
+
self._message_handlers.append({
|
|
513
|
+
"func": wrapper,
|
|
514
|
+
"filters": filters,
|
|
515
|
+
"file_only": True,
|
|
516
|
+
"commands":commands
|
|
517
|
+
})
|
|
518
|
+
return wrapper
|
|
519
|
+
return decorator
|
|
520
|
+
|
|
521
|
+
def on_message_forwarded(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
522
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
523
|
+
def wrapper(bot, message: Message):
|
|
524
|
+
if not message.is_forwarded:
|
|
525
|
+
return
|
|
526
|
+
if filters and not filters(message):
|
|
527
|
+
return
|
|
528
|
+
return func(bot, message)
|
|
529
|
+
|
|
530
|
+
self._message_handlers.append({
|
|
531
|
+
"func": wrapper,
|
|
532
|
+
"filters": filters,
|
|
533
|
+
"forwarded_only": True,
|
|
534
|
+
"commands":commands
|
|
535
|
+
})
|
|
536
|
+
return wrapper
|
|
537
|
+
return decorator
|
|
538
|
+
|
|
539
|
+
def on_message_reply(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
540
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
541
|
+
def wrapper(bot, message: Message):
|
|
542
|
+
if not message.is_reply:
|
|
543
|
+
return
|
|
544
|
+
if filters and not filters(message):
|
|
545
|
+
return
|
|
546
|
+
return func(bot, message)
|
|
547
|
+
|
|
548
|
+
self._message_handlers.append({
|
|
549
|
+
"func": wrapper,
|
|
550
|
+
"filters": filters,
|
|
551
|
+
"reply_only": True,
|
|
552
|
+
"commands":commands
|
|
553
|
+
})
|
|
554
|
+
return wrapper
|
|
555
|
+
return decorator
|
|
556
|
+
|
|
557
|
+
def on_message_text(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
558
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
559
|
+
def wrapper(bot, message: Message):
|
|
560
|
+
if not message.text:
|
|
561
|
+
return
|
|
562
|
+
if filters and not filters(message):
|
|
563
|
+
return
|
|
564
|
+
return func(bot, message)
|
|
565
|
+
|
|
566
|
+
self._message_handlers.append({
|
|
567
|
+
"func": wrapper,
|
|
568
|
+
"filters": filters,
|
|
569
|
+
"text_only": True,
|
|
570
|
+
"commands":commands
|
|
571
|
+
})
|
|
572
|
+
return wrapper
|
|
573
|
+
return decorator
|
|
574
|
+
|
|
575
|
+
def on_message_media(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
576
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
577
|
+
def wrapper(bot, message: Message):
|
|
578
|
+
if not message.is_media:
|
|
579
|
+
return
|
|
580
|
+
if filters and not filters(message):
|
|
581
|
+
return
|
|
582
|
+
return func(bot, message)
|
|
583
|
+
|
|
584
|
+
self._message_handlers.append({
|
|
585
|
+
"func": wrapper,
|
|
586
|
+
"filters": filters,
|
|
587
|
+
"media_only": True,
|
|
588
|
+
"commands":commands
|
|
589
|
+
})
|
|
590
|
+
return wrapper
|
|
591
|
+
return decorator
|
|
592
|
+
|
|
593
|
+
def on_message_sticker(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
594
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
595
|
+
def wrapper(bot, message: Message):
|
|
596
|
+
if not message.sticker:
|
|
597
|
+
return
|
|
598
|
+
if filters and not filters(message):
|
|
599
|
+
return
|
|
600
|
+
return func(bot, message)
|
|
601
|
+
|
|
602
|
+
self._message_handlers.append({
|
|
603
|
+
"func": wrapper,
|
|
604
|
+
"filters": filters,
|
|
605
|
+
"sticker_only": True,
|
|
606
|
+
"commands":commands
|
|
607
|
+
})
|
|
608
|
+
return wrapper
|
|
609
|
+
return decorator
|
|
610
|
+
|
|
611
|
+
def on_message_contact(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
612
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
613
|
+
def wrapper(bot, message: Message):
|
|
614
|
+
if not message.is_contact:
|
|
615
|
+
return
|
|
616
|
+
if filters and not filters(message):
|
|
617
|
+
return
|
|
618
|
+
return func(bot, message)
|
|
619
|
+
|
|
620
|
+
self._message_handlers.append({
|
|
621
|
+
"func": wrapper,
|
|
622
|
+
"filters": filters,
|
|
623
|
+
"contact_only": True,
|
|
624
|
+
"commands":commands
|
|
625
|
+
})
|
|
626
|
+
return wrapper
|
|
627
|
+
return decorator
|
|
628
|
+
|
|
629
|
+
def on_message_location(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
630
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
631
|
+
def wrapper(bot, message: Message):
|
|
632
|
+
if not message.is_location:
|
|
633
|
+
return
|
|
634
|
+
if filters and not filters(message):
|
|
635
|
+
return
|
|
636
|
+
return func(bot, message)
|
|
637
|
+
|
|
638
|
+
self._message_handlers.append({
|
|
639
|
+
"func": wrapper,
|
|
640
|
+
"filters": filters,
|
|
641
|
+
"location_only": True,
|
|
642
|
+
"commands":commands
|
|
643
|
+
})
|
|
644
|
+
return wrapper
|
|
645
|
+
return decorator
|
|
646
|
+
|
|
647
|
+
def on_message_poll(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
648
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
649
|
+
def wrapper(bot, message: Message):
|
|
650
|
+
if not message.is_poll:
|
|
651
|
+
return
|
|
652
|
+
if filters and not filters(message):
|
|
653
|
+
return
|
|
654
|
+
return func(bot, message)
|
|
655
|
+
|
|
656
|
+
self._message_handlers.append({
|
|
657
|
+
"func": wrapper,
|
|
658
|
+
"filters": filters,
|
|
659
|
+
"poll_only": True,
|
|
660
|
+
"commands":commands
|
|
661
|
+
})
|
|
662
|
+
return wrapper
|
|
663
|
+
return decorator
|
|
664
|
+
|
|
665
|
+
def message_handler(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
666
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
667
|
+
self._message_handlers.append({
|
|
668
|
+
"func": func,
|
|
669
|
+
"filters": filters,
|
|
670
|
+
"commands": commands
|
|
671
|
+
})
|
|
672
|
+
return func
|
|
673
|
+
return decorator
|
|
674
|
+
def on_update(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
675
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
676
|
+
self._message_handlers.append({
|
|
677
|
+
"func": func,
|
|
678
|
+
"filters": filters,
|
|
679
|
+
"commands": commands
|
|
680
|
+
})
|
|
681
|
+
return func
|
|
682
|
+
return decorator
|
|
683
|
+
|
|
684
|
+
def on_callback(self, button_id: Optional[str] = None):
|
|
685
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
686
|
+
if not hasattr(self, "_callback_handlers"):
|
|
687
|
+
self._callback_handlers = []
|
|
688
|
+
self._callback_handlers.append({
|
|
689
|
+
"func": func,
|
|
690
|
+
"button_id": button_id
|
|
691
|
+
})
|
|
692
|
+
return func
|
|
693
|
+
return decorator
|
|
694
|
+
def callback_query(self, button_id: Optional[str] = None):
|
|
695
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
696
|
+
if not hasattr(self, "_callback_handlers"):
|
|
697
|
+
self._callback_handlers = []
|
|
698
|
+
self._callback_handlers.append({
|
|
699
|
+
"func": func,
|
|
700
|
+
"button_id": button_id
|
|
701
|
+
})
|
|
702
|
+
return func
|
|
703
|
+
return decorator
|
|
704
|
+
def callback_query_handler(self, button_id: Optional[str] = None):
|
|
705
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
706
|
+
if not hasattr(self, "_callback_handlers"):
|
|
707
|
+
self._callback_handlers = []
|
|
708
|
+
self._callback_handlers.append({
|
|
709
|
+
"func": func,
|
|
710
|
+
"button_id": button_id
|
|
711
|
+
})
|
|
712
|
+
return func
|
|
713
|
+
return decorator
|
|
714
|
+
def _handle_inline_query(self, inline_message: InlineMessage):
|
|
715
|
+
aux_button_id = inline_message.aux_data.button_id if inline_message.aux_data else None
|
|
716
|
+
|
|
717
|
+
for handler in self._inline_query_handlers:
|
|
718
|
+
if handler["button_id"] is None or handler["button_id"] == aux_button_id:
|
|
719
|
+
try:
|
|
720
|
+
handler["func"](self, inline_message)
|
|
721
|
+
except Exception as e:
|
|
722
|
+
print(f"Error in inline query handler: {e}")
|
|
723
|
+
|
|
724
|
+
def on_inline_query(self, button_id: Optional[str] = None):
|
|
725
|
+
def decorator(func: Callable[[Any, InlineMessage], None]):
|
|
726
|
+
self._inline_query_handlers.append({
|
|
727
|
+
"func": func,
|
|
728
|
+
"button_id": button_id
|
|
729
|
+
})
|
|
730
|
+
return func
|
|
731
|
+
return decorator
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _process_update(self, update: dict):
|
|
735
|
+
import threading
|
|
736
|
+
|
|
737
|
+
if update.get("type") == "ReceiveQuery":
|
|
738
|
+
msg = update.get("inline_message", {})
|
|
739
|
+
context = InlineMessage(bot=self, raw_data=msg)
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
if hasattr(self, "_callback_handlers"):
|
|
743
|
+
for handler in self._callback_handlers:
|
|
744
|
+
cb_id = getattr(context.aux_data, "button_id", None)
|
|
745
|
+
if not handler["button_id"] or handler["button_id"] == cb_id:
|
|
746
|
+
threading.Thread(target=handler["func"], args=(self, context), daemon=True).start()
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
threading.Thread(target=self._handle_inline_query, args=(context,), daemon=True).start()
|
|
750
|
+
return
|
|
751
|
+
|
|
752
|
+
if update.get("type") == "NewMessage":
|
|
753
|
+
msg = update.get("new_message", {})
|
|
754
|
+
try:
|
|
755
|
+
if msg.get("time") and (time.time() - float(msg["time"])) > 20:
|
|
756
|
+
return
|
|
757
|
+
except Exception:
|
|
758
|
+
return
|
|
759
|
+
|
|
760
|
+
context = Message(bot=self,
|
|
761
|
+
chat_id=update.get("chat_id"),
|
|
762
|
+
message_id=msg.get("message_id"),
|
|
763
|
+
sender_id=msg.get("sender_id"),
|
|
764
|
+
text=msg.get("text"),
|
|
765
|
+
raw_data=msg)
|
|
766
|
+
|
|
767
|
+
if context.aux_data and self._callback_handlers:
|
|
768
|
+
for handler in self._callback_handlers:
|
|
769
|
+
if not handler["button_id"] or context.aux_data.button_id == handler["button_id"]:
|
|
770
|
+
threading.Thread(target=handler["func"], args=(self, context), daemon=True).start()
|
|
771
|
+
return
|
|
772
|
+
|
|
773
|
+
if self._message_handlers:
|
|
774
|
+
for handler in self._message_handlers:
|
|
775
|
+
if handler["commands"]:
|
|
776
|
+
if not context.text or not context.text.startswith("/"):
|
|
777
|
+
continue
|
|
778
|
+
parts = context.text.split()
|
|
779
|
+
cmd = parts[0][1:]
|
|
780
|
+
if cmd not in handler["commands"]:
|
|
781
|
+
continue
|
|
782
|
+
context.args = parts[1:]
|
|
783
|
+
|
|
784
|
+
if handler["filters"] and not handler["filters"](context):
|
|
785
|
+
continue
|
|
786
|
+
|
|
787
|
+
threading.Thread(target=handler["func"], args=(self, context), daemon=True).start()
|
|
788
|
+
continue
|
|
789
|
+
|
|
790
|
+
def get_updates(
|
|
791
|
+
self,
|
|
792
|
+
offset_id: Optional[str] = None,
|
|
793
|
+
limit: Optional[int] = None
|
|
794
|
+
) -> Dict[str, Any]:
|
|
795
|
+
"""Get updates."""
|
|
796
|
+
data = {}
|
|
797
|
+
if offset_id:
|
|
798
|
+
data["offset_id"] = offset_id
|
|
799
|
+
if limit:
|
|
800
|
+
data["limit"] = limit
|
|
801
|
+
return self._post("getUpdates", data)
|
|
802
|
+
def update_webhook(
|
|
803
|
+
self,
|
|
804
|
+
offset_id: Optional[str] = None,
|
|
805
|
+
limit: Optional[int] = None
|
|
806
|
+
) -> Dict[str, Any]:
|
|
807
|
+
data = {}
|
|
808
|
+
if offset_id:
|
|
809
|
+
data["offset_id"] = offset_id
|
|
810
|
+
if limit:
|
|
811
|
+
data["limit"] = limit
|
|
812
|
+
return list(requests.get(self.web_hook).json())
|
|
813
|
+
def _is_duplicate(self, message_id: str, max_age_sec: int = 300) -> bool:
|
|
814
|
+
now = time.time()
|
|
815
|
+
|
|
816
|
+
expired = [mid for mid, ts in self._processed_message_ids.items() if now - ts > max_age_sec]
|
|
817
|
+
for mid in expired:
|
|
818
|
+
del self._processed_message_ids[mid]
|
|
819
|
+
|
|
820
|
+
if message_id in self._processed_message_ids:
|
|
821
|
+
return True
|
|
822
|
+
|
|
823
|
+
self._processed_message_ids[message_id] = now
|
|
824
|
+
return False
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
import time
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
import datetime
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def run(
|
|
834
|
+
self,
|
|
835
|
+
debug=False,
|
|
836
|
+
sleep_time=0.1,
|
|
837
|
+
webhook_timeout=20,
|
|
838
|
+
update_limit=100,
|
|
839
|
+
retry_delay=5,
|
|
840
|
+
stop_on_error=False,
|
|
841
|
+
max_errors=None,
|
|
842
|
+
max_runtime=None,
|
|
843
|
+
allowed_update_types=None,
|
|
844
|
+
ignore_duplicate_messages=True,
|
|
845
|
+
skip_inline_queries=False,
|
|
846
|
+
skip_channel_posts=False,
|
|
847
|
+
skip_service_messages=False,
|
|
848
|
+
skip_edited_messages=False,
|
|
849
|
+
skip_bot_messages=False,
|
|
850
|
+
log_file=None,
|
|
851
|
+
print_exceptions=True,
|
|
852
|
+
error_handler=None,
|
|
853
|
+
shutdown_hook=None,
|
|
854
|
+
log_to_console=True,
|
|
855
|
+
custom_update_fetcher=None,
|
|
856
|
+
custom_update_processor=None,
|
|
857
|
+
message_filter=None,
|
|
858
|
+
notify_on_error=False,
|
|
859
|
+
notification_handler=None,
|
|
860
|
+
):
|
|
861
|
+
import time
|
|
862
|
+
from typing import Dict
|
|
863
|
+
if debug:
|
|
864
|
+
print("[DEBUG] Bot started running server...")
|
|
865
|
+
|
|
866
|
+
self._processed_message_ids: Dict[str, float] = {}
|
|
867
|
+
error_count = 0
|
|
868
|
+
start_time = time.time()
|
|
869
|
+
|
|
870
|
+
try:
|
|
871
|
+
while True:
|
|
872
|
+
try:
|
|
873
|
+
|
|
874
|
+
if max_runtime and (time.time() - start_time > max_runtime):
|
|
875
|
+
if debug:
|
|
876
|
+
print("[DEBUG] Max runtime reached, stopping...")
|
|
877
|
+
break
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
if self.web_hook:
|
|
881
|
+
updates = custom_update_fetcher() if custom_update_fetcher else self.update_webhook()
|
|
882
|
+
if isinstance(updates, list):
|
|
883
|
+
for item in updates:
|
|
884
|
+
data = item.get("data", {})
|
|
885
|
+
received_at_str = item.get("received_at")
|
|
886
|
+
|
|
887
|
+
if received_at_str:
|
|
888
|
+
try:
|
|
889
|
+
received_at_ts = datetime.datetime.strptime(received_at_str, "%Y-%m-%d %H:%M:%S").timestamp()
|
|
890
|
+
if time.time() - received_at_ts > webhook_timeout:
|
|
891
|
+
continue
|
|
892
|
+
except (ValueError, TypeError):
|
|
893
|
+
pass
|
|
894
|
+
|
|
895
|
+
update = data.get("update") or (
|
|
896
|
+
{"type": "ReceiveQuery", "inline_message": data.get("inline_message")}
|
|
897
|
+
if "inline_message" in data else None
|
|
898
|
+
)
|
|
899
|
+
if not update:
|
|
900
|
+
continue
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
if skip_inline_queries and update.get("type") == "ReceiveQuery":
|
|
904
|
+
continue
|
|
905
|
+
if skip_channel_posts and update.get("type") == "ChannelPost":
|
|
906
|
+
continue
|
|
907
|
+
if skip_service_messages and update.get("type") == "ServiceMessage":
|
|
908
|
+
continue
|
|
909
|
+
if skip_edited_messages and update.get("type") == "EditedMessage":
|
|
910
|
+
continue
|
|
911
|
+
if skip_bot_messages and update.get("from", {}).get("is_bot"):
|
|
912
|
+
continue
|
|
913
|
+
if allowed_update_types and update.get("type") not in allowed_update_types:
|
|
914
|
+
continue
|
|
915
|
+
|
|
916
|
+
message_id = (
|
|
917
|
+
update.get("new_message", {}).get("message_id")
|
|
918
|
+
if update.get("type") == "NewMessage"
|
|
919
|
+
else update.get("inline_message", {}).get("message_id")
|
|
920
|
+
if update.get("type") == "ReceiveQuery"
|
|
921
|
+
else update.get("message_id")
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
if message_id is not None:
|
|
925
|
+
message_id = str(message_id)
|
|
926
|
+
|
|
927
|
+
if message_id and (not ignore_duplicate_messages or not self._is_duplicate(received_at_str)):
|
|
928
|
+
if message_filter and not message_filter(update):
|
|
929
|
+
continue
|
|
930
|
+
if custom_update_processor:
|
|
931
|
+
custom_update_processor(update)
|
|
932
|
+
else:
|
|
933
|
+
self._process_update(update)
|
|
934
|
+
if message_id:
|
|
935
|
+
self._processed_message_ids[message_id] = time.time()
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
else:
|
|
939
|
+
updates = custom_update_fetcher() if custom_update_fetcher else self.get_updates(offset_id=self._offset_id, limit=update_limit)
|
|
940
|
+
if updates and updates.get("data"):
|
|
941
|
+
for update in updates["data"].get("updates", []):
|
|
942
|
+
if allowed_update_types and update.get("type") not in allowed_update_types:
|
|
943
|
+
continue
|
|
944
|
+
|
|
945
|
+
message_id = (
|
|
946
|
+
update.get("new_message", {}).get("message_id")
|
|
947
|
+
if update.get("type") == "NewMessage"
|
|
948
|
+
else update.get("inline_message", {}).get("message_id")
|
|
949
|
+
if update.get("type") == "ReceiveQuery"
|
|
950
|
+
else update.get("message_id")
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
if message_id is not None:
|
|
954
|
+
message_id = str(message_id)
|
|
955
|
+
|
|
956
|
+
if message_id and (not ignore_duplicate_messages or not self._is_duplicate(message_id)):
|
|
957
|
+
if message_filter and not message_filter(update):
|
|
958
|
+
continue
|
|
959
|
+
if custom_update_processor:
|
|
960
|
+
custom_update_processor(update)
|
|
961
|
+
else:
|
|
962
|
+
self._process_update(update)
|
|
963
|
+
if message_id:
|
|
964
|
+
self._processed_message_ids[message_id] = time.time()
|
|
965
|
+
|
|
966
|
+
self._offset_id = updates["data"].get("next_offset_id", self._offset_id)
|
|
967
|
+
|
|
968
|
+
if sleep_time:
|
|
969
|
+
time.sleep(sleep_time)
|
|
970
|
+
|
|
971
|
+
except Exception as e:
|
|
972
|
+
error_count += 1
|
|
973
|
+
if log_to_console:
|
|
974
|
+
print(f"Error in run loop: {e}")
|
|
975
|
+
if log_file:
|
|
976
|
+
with open(log_file, "a", encoding="utf-8") as f:
|
|
977
|
+
f.write(f"{datetime.datetime.now()} - ERROR: {e}\n")
|
|
978
|
+
if print_exceptions:
|
|
979
|
+
import traceback
|
|
980
|
+
traceback.print_exc()
|
|
981
|
+
if error_handler:
|
|
982
|
+
error_handler(e)
|
|
983
|
+
if notify_on_error and notification_handler:
|
|
984
|
+
notification_handler(e)
|
|
985
|
+
|
|
986
|
+
if max_errors and error_count >= max_errors and stop_on_error:
|
|
987
|
+
break
|
|
988
|
+
|
|
989
|
+
time.sleep(retry_delay)
|
|
990
|
+
|
|
991
|
+
finally:
|
|
992
|
+
if shutdown_hook:
|
|
993
|
+
shutdown_hook()
|
|
994
|
+
if debug:
|
|
995
|
+
print("Bot stopped and session closed.")
|
|
996
|
+
|
|
997
|
+
def send_message(
|
|
998
|
+
self,
|
|
999
|
+
chat_id: str,
|
|
1000
|
+
text: str,
|
|
1001
|
+
chat_keypad: Optional[Dict[str, Any]] = None,
|
|
1002
|
+
inline_keypad: Optional[Dict[str, Any]] = None,
|
|
1003
|
+
disable_notification: bool = False,
|
|
1004
|
+
reply_to_message_id: Optional[str] = None,
|
|
1005
|
+
chat_keypad_type: Optional[Literal["New", "Removed"]] = None,
|
|
1006
|
+
delete_after = None,
|
|
1007
|
+
parse_mode = None
|
|
1008
|
+
) -> Dict[str, Any]:
|
|
1009
|
+
"""
|
|
1010
|
+
Send a text message to a chat.
|
|
1011
|
+
"""
|
|
1012
|
+
payload = {
|
|
1013
|
+
"chat_id": chat_id,
|
|
1014
|
+
"text": text,
|
|
1015
|
+
"disable_notification": disable_notification
|
|
1016
|
+
}
|
|
1017
|
+
if chat_keypad:
|
|
1018
|
+
payload["chat_keypad"] = chat_keypad
|
|
1019
|
+
if inline_keypad:
|
|
1020
|
+
payload["inline_keypad"] = inline_keypad
|
|
1021
|
+
if reply_to_message_id:
|
|
1022
|
+
payload["reply_to_message_id"] = reply_to_message_id
|
|
1023
|
+
if chat_keypad_type:
|
|
1024
|
+
payload["chat_keypad_type"] = chat_keypad_type
|
|
1025
|
+
|
|
1026
|
+
return self._post("sendMessage", payload)
|
|
1027
|
+
|
|
1028
|
+
def _get_client(self):
|
|
1029
|
+
if self.session_name:
|
|
1030
|
+
return Client_get(self.session_name,self.auth,self.Key,self.platform)
|
|
1031
|
+
else :
|
|
1032
|
+
return Client_get(show_last_six_words(self.token),self.auth,self.Key,self.platform)
|
|
1033
|
+
from typing import Union
|
|
1034
|
+
|
|
1035
|
+
def check_join(self, channel_guid: str, chat_id: str = None) -> Union[bool, list[str]]:
|
|
1036
|
+
client = self._get_client()
|
|
1037
|
+
|
|
1038
|
+
if chat_id:
|
|
1039
|
+
chat_info = self.get_chat(chat_id).get('data', {}).get('chat', {})
|
|
1040
|
+
username = chat_info.get('username')
|
|
1041
|
+
user_id = chat_info.get('user_id')
|
|
1042
|
+
|
|
1043
|
+
if username:
|
|
1044
|
+
members = self.get_all_member(channel_guid, search_text=username).get('in_chat_members', [])
|
|
1045
|
+
return any(m.get('username') == username for m in members)
|
|
1046
|
+
|
|
1047
|
+
elif user_id:
|
|
1048
|
+
member_guids = client.get_all_members(channel_guid, just_get_guids=True)
|
|
1049
|
+
return user_id in member_guids
|
|
1050
|
+
|
|
1051
|
+
return False
|
|
1052
|
+
|
|
1053
|
+
return False
|
|
1054
|
+
|
|
1055
|
+
def get_url_file(self,file_id):
|
|
1056
|
+
data = self._post("getFile", {'file_id': file_id})
|
|
1057
|
+
return data.get("data").get("download_url")
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def get_all_member(
|
|
1061
|
+
self,
|
|
1062
|
+
channel_guid: str,
|
|
1063
|
+
search_text: str = None,
|
|
1064
|
+
start_id: str = None,
|
|
1065
|
+
just_get_guids: bool = False
|
|
1066
|
+
):
|
|
1067
|
+
client = self._get_client()
|
|
1068
|
+
return client.get_all_members(channel_guid, search_text, start_id, just_get_guids)
|
|
1069
|
+
|
|
1070
|
+
def send_poll(
|
|
1071
|
+
self,
|
|
1072
|
+
chat_id: str,
|
|
1073
|
+
question: str,
|
|
1074
|
+
options: List[str]
|
|
1075
|
+
) -> Dict[str, Any]:
|
|
1076
|
+
"""
|
|
1077
|
+
Send a poll to a chat.
|
|
1078
|
+
"""
|
|
1079
|
+
return self._post("sendPoll", {
|
|
1080
|
+
"chat_id": chat_id,
|
|
1081
|
+
"question": question,
|
|
1082
|
+
"options": options
|
|
1083
|
+
})
|
|
1084
|
+
|
|
1085
|
+
def send_location(
|
|
1086
|
+
self,
|
|
1087
|
+
chat_id: str,
|
|
1088
|
+
latitude: str,
|
|
1089
|
+
longitude: str,
|
|
1090
|
+
disable_notification: bool = False,
|
|
1091
|
+
inline_keypad: Optional[Dict[str, Any]] = None,
|
|
1092
|
+
reply_to_message_id: Optional[str] = None,
|
|
1093
|
+
chat_keypad_type: Optional[Literal["New", "Removed"]] = None
|
|
1094
|
+
) -> Dict[str, Any]:
|
|
1095
|
+
"""
|
|
1096
|
+
Send a location to a chat.
|
|
1097
|
+
"""
|
|
1098
|
+
payload = {
|
|
1099
|
+
"chat_id": chat_id,
|
|
1100
|
+
"latitude": latitude,
|
|
1101
|
+
"longitude": longitude,
|
|
1102
|
+
"disable_notification": disable_notification,
|
|
1103
|
+
"inline_keypad": inline_keypad,
|
|
1104
|
+
"reply_to_message_id": reply_to_message_id,
|
|
1105
|
+
"chat_keypad_type": chat_keypad_type
|
|
1106
|
+
}
|
|
1107
|
+
payload = {k: v for k, v in payload.items() if v is not None}
|
|
1108
|
+
return self._post("sendLocation", payload)
|
|
1109
|
+
|
|
1110
|
+
def send_contact(
|
|
1111
|
+
self,
|
|
1112
|
+
chat_id: str,
|
|
1113
|
+
first_name: str,
|
|
1114
|
+
last_name: str,
|
|
1115
|
+
phone_number: str
|
|
1116
|
+
) -> Dict[str, Any]:
|
|
1117
|
+
"""
|
|
1118
|
+
Send a contact to a chat.
|
|
1119
|
+
"""
|
|
1120
|
+
return self._post("sendContact", {
|
|
1121
|
+
"chat_id": chat_id,
|
|
1122
|
+
"first_name": first_name,
|
|
1123
|
+
"last_name": last_name,
|
|
1124
|
+
"phone_number": phone_number
|
|
1125
|
+
})
|
|
1126
|
+
def download(self,file_id: str, save_as: str = None, chunk_size: int = 1024 * 512, timeout_sec: int = 60, verbose: bool = False):
|
|
1127
|
+
"""
|
|
1128
|
+
Download a file from server using its file_id with chunked transfer,
|
|
1129
|
+
progress bar, file extension detection, custom filename, and timeout.
|
|
1130
|
+
|
|
1131
|
+
If save_as is not provided, filename will be extracted from
|
|
1132
|
+
Content-Disposition header or Content-Type header extension.
|
|
1133
|
+
|
|
1134
|
+
Parameters:
|
|
1135
|
+
file_id (str): The file ID to fetch the download URL.
|
|
1136
|
+
save_as (str, optional): Custom filename to save. If None, automatically detected.
|
|
1137
|
+
chunk_size (int, optional): Size of each chunk in bytes. Default 512KB.
|
|
1138
|
+
timeout_sec (int, optional): HTTP timeout in seconds. Default 60.
|
|
1139
|
+
verbose (bool, optional): Show progress messages. Default True.
|
|
1140
|
+
|
|
1141
|
+
Returns:
|
|
1142
|
+
bool: True if success, raises exceptions otherwise.
|
|
1143
|
+
"""
|
|
1144
|
+
try:
|
|
1145
|
+
url = self.get_url_file(file_id)
|
|
1146
|
+
if not url:
|
|
1147
|
+
raise ValueError("Download URL not found in response.")
|
|
1148
|
+
except Exception as e:
|
|
1149
|
+
raise ValueError(f"Failed to get download URL: {e}")
|
|
1150
|
+
|
|
1151
|
+
try:
|
|
1152
|
+
with requests.get(url, stream=True, timeout=timeout_sec) as resp:
|
|
1153
|
+
if resp.status_code != 200:
|
|
1154
|
+
raise requests.HTTPError(f"Failed to download file. Status code: {resp.status_code}")
|
|
1155
|
+
|
|
1156
|
+
if not save_as:
|
|
1157
|
+
content_disp = resp.headers.get("Content-Disposition", "")
|
|
1158
|
+
match = re.search(r'filename="?([^\";]+)"?', content_disp)
|
|
1159
|
+
if match:
|
|
1160
|
+
save_as = match.group(1)
|
|
1161
|
+
else:
|
|
1162
|
+
content_type = resp.headers.get("Content-Type", "").split(";")[0]
|
|
1163
|
+
extension = mimetypes.guess_extension(content_type) or ".bin"
|
|
1164
|
+
save_as = f"{file_id}{extension}"
|
|
1165
|
+
|
|
1166
|
+
total_size = int(resp.headers.get("Content-Length", 0))
|
|
1167
|
+
progress = tqdm(total=total_size, unit="B", unit_scale=True)
|
|
1168
|
+
|
|
1169
|
+
with open(save_as, "wb") as f:
|
|
1170
|
+
for chunk in resp.iter_content(chunk_size=chunk_size):
|
|
1171
|
+
if chunk:
|
|
1172
|
+
f.write(chunk)
|
|
1173
|
+
progress.update(len(chunk))
|
|
1174
|
+
|
|
1175
|
+
progress.close()
|
|
1176
|
+
if verbose:
|
|
1177
|
+
print(f"File saved as: {save_as}")
|
|
1178
|
+
|
|
1179
|
+
return True
|
|
1180
|
+
|
|
1181
|
+
except Exception as e:
|
|
1182
|
+
raise RuntimeError(f"Download failed: {e}")
|
|
1183
|
+
def get_chat(self, chat_id: str) -> Dict[str, Any]:
|
|
1184
|
+
"""Get chat info."""
|
|
1185
|
+
return self._post("getChat", {"chat_id": chat_id})
|
|
1186
|
+
|
|
1187
|
+
def upload_media_file(self, upload_url: str, name: str, path: Union[str, Path]) -> str:
|
|
1188
|
+
is_temp_file = False
|
|
1189
|
+
|
|
1190
|
+
if isinstance(path, str) and path.startswith("http"):
|
|
1191
|
+
response = requests.get(path)
|
|
1192
|
+
if response.status_code != 200:
|
|
1193
|
+
raise Exception(f"Failed to download file from URL ({response.status_code})")
|
|
1194
|
+
temp_file = tempfile.NamedTemporaryFile(delete=False)
|
|
1195
|
+
temp_file.write(response.content)
|
|
1196
|
+
temp_file.close()
|
|
1197
|
+
path = temp_file.name
|
|
1198
|
+
is_temp_file = True
|
|
1199
|
+
|
|
1200
|
+
file_size = os.path.getsize(path)
|
|
1201
|
+
|
|
1202
|
+
with open(path, 'rb') as f:
|
|
1203
|
+
progress_bar = None
|
|
1204
|
+
|
|
1205
|
+
if self.show_progress:
|
|
1206
|
+
progress_bar = tqdm(
|
|
1207
|
+
total=file_size,
|
|
1208
|
+
unit='B',
|
|
1209
|
+
unit_scale=True,
|
|
1210
|
+
unit_divisor=1024,
|
|
1211
|
+
desc=f'Uploading : {name}',
|
|
1212
|
+
bar_format='{l_bar}{bar:100}{r_bar}',
|
|
1213
|
+
colour='cyan'
|
|
1214
|
+
)
|
|
1215
|
+
|
|
1216
|
+
class FileWithProgress:
|
|
1217
|
+
def __init__(self, file, progress):
|
|
1218
|
+
self.file = file
|
|
1219
|
+
self.progress = progress
|
|
1220
|
+
|
|
1221
|
+
def read(self, size=-1):
|
|
1222
|
+
data = self.file.read(size)
|
|
1223
|
+
if self.progress:
|
|
1224
|
+
self.progress.update(len(data))
|
|
1225
|
+
return data
|
|
1226
|
+
|
|
1227
|
+
def __getattr__(self, attr):
|
|
1228
|
+
return getattr(self.file, attr)
|
|
1229
|
+
|
|
1230
|
+
file_with_progress = FileWithProgress(f, progress_bar)
|
|
1231
|
+
|
|
1232
|
+
files = {
|
|
1233
|
+
'file': (name, file_with_progress, 'application/octet-stream')
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
response = requests.post(upload_url, files=files)
|
|
1237
|
+
|
|
1238
|
+
if progress_bar:
|
|
1239
|
+
progress_bar.close()
|
|
1240
|
+
|
|
1241
|
+
if is_temp_file:
|
|
1242
|
+
os.remove(path)
|
|
1243
|
+
|
|
1244
|
+
if response.status_code != 200:
|
|
1245
|
+
raise Exception(f"Upload failed ({response.status_code}): {response.text}")
|
|
1246
|
+
|
|
1247
|
+
data = response.json()
|
|
1248
|
+
return data.get('data', {}).get('file_id')
|
|
1249
|
+
|
|
1250
|
+
def send_button_join(
|
|
1251
|
+
self,
|
|
1252
|
+
chat_id,
|
|
1253
|
+
title_button : Union[str, list],
|
|
1254
|
+
username : Union[str, list],
|
|
1255
|
+
text,
|
|
1256
|
+
reply_to_message_id=None,
|
|
1257
|
+
id="None"
|
|
1258
|
+
):
|
|
1259
|
+
from .button import InlineBuilder
|
|
1260
|
+
builder = InlineBuilder()
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
if isinstance(username, (list, tuple)) and isinstance(title_button, (list, tuple)):
|
|
1264
|
+
for t, u in zip(title_button, username):
|
|
1265
|
+
builder = builder.row(
|
|
1266
|
+
InlineBuilder().button_join_channel(
|
|
1267
|
+
text=t,
|
|
1268
|
+
id=id,
|
|
1269
|
+
username=u
|
|
1270
|
+
)
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
elif isinstance(username, (list, tuple)) and isinstance(title_button, str):
|
|
1275
|
+
for u in username:
|
|
1276
|
+
builder = builder.row(
|
|
1277
|
+
InlineBuilder().button_join_channel(
|
|
1278
|
+
text=title_button,
|
|
1279
|
+
id=id,
|
|
1280
|
+
username=u
|
|
1281
|
+
)
|
|
1282
|
+
)
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
else:
|
|
1286
|
+
builder = builder.row(
|
|
1287
|
+
InlineBuilder().button_join_channel(
|
|
1288
|
+
text=title_button,
|
|
1289
|
+
id=id,
|
|
1290
|
+
username=username
|
|
1291
|
+
)
|
|
1292
|
+
)
|
|
1293
|
+
|
|
1294
|
+
return self.send_message(
|
|
1295
|
+
chat_id=chat_id,
|
|
1296
|
+
text=text,
|
|
1297
|
+
inline_keypad=builder.build(),
|
|
1298
|
+
reply_to_message_id=reply_to_message_id
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
def send_button_url(
|
|
1303
|
+
self,
|
|
1304
|
+
chat_id,
|
|
1305
|
+
title_button : Union[str, list],
|
|
1306
|
+
url : Union[str, list],
|
|
1307
|
+
text,
|
|
1308
|
+
reply_to_message_id=None,
|
|
1309
|
+
id="None"
|
|
1310
|
+
):
|
|
1311
|
+
from .button import InlineBuilder
|
|
1312
|
+
builder = InlineBuilder()
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
if isinstance(url, (list, tuple)) and isinstance(title_button, (list, tuple)):
|
|
1316
|
+
for t, u in zip(title_button, url):
|
|
1317
|
+
builder = builder.row(
|
|
1318
|
+
InlineBuilder().button_url_link(
|
|
1319
|
+
text=t,
|
|
1320
|
+
id=id,
|
|
1321
|
+
url=u
|
|
1322
|
+
)
|
|
1323
|
+
)
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
elif isinstance(url, (list, tuple)) and isinstance(title_button, str):
|
|
1327
|
+
for u in url:
|
|
1328
|
+
builder = builder.row(
|
|
1329
|
+
InlineBuilder().button_url_link(
|
|
1330
|
+
text=title_button,
|
|
1331
|
+
id=id,
|
|
1332
|
+
url=u
|
|
1333
|
+
)
|
|
1334
|
+
)
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
else:
|
|
1338
|
+
builder = builder.row(
|
|
1339
|
+
InlineBuilder().button_url_link(
|
|
1340
|
+
text=title_button,
|
|
1341
|
+
id=id,
|
|
1342
|
+
url=url
|
|
1343
|
+
)
|
|
1344
|
+
)
|
|
1345
|
+
|
|
1346
|
+
return self.send_message(
|
|
1347
|
+
chat_id=chat_id,
|
|
1348
|
+
text=text,
|
|
1349
|
+
inline_keypad=builder.build(),
|
|
1350
|
+
reply_to_message_id=reply_to_message_id
|
|
1351
|
+
)
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
def get_upload_url(self, media_type: Literal['File', 'Image', 'Voice', 'Music', 'Gif','Video']) -> str:
|
|
1355
|
+
allowed = ['File', 'Image', 'Voice', 'Music', 'Gif','Video']
|
|
1356
|
+
if media_type not in allowed:
|
|
1357
|
+
raise ValueError(f"Invalid media type. Must be one of {allowed}")
|
|
1358
|
+
result = self._post("requestSendFile", {"type": media_type})
|
|
1359
|
+
return result.get("data", {}).get("upload_url")
|
|
1360
|
+
def _send_uploaded_file(self, chat_id: str, file_id: str,type_file : str = "file",text: Optional[str] = None, chat_keypad: Optional[Dict[str, Any]] = None, inline_keypad: Optional[Dict[str, Any]] = None, disable_notification: bool = False, reply_to_message_id: Optional[str] = None, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None") -> Dict[str, Any]:
|
|
1361
|
+
payload = {
|
|
1362
|
+
"chat_id": chat_id,
|
|
1363
|
+
"file_id": file_id,
|
|
1364
|
+
"text": text,
|
|
1365
|
+
"disable_notification": disable_notification,
|
|
1366
|
+
"chat_keypad_type": chat_keypad_type,
|
|
1367
|
+
}
|
|
1368
|
+
if chat_keypad:
|
|
1369
|
+
payload["chat_keypad"] = chat_keypad
|
|
1370
|
+
if inline_keypad:
|
|
1371
|
+
payload["inline_keypad"] = inline_keypad
|
|
1372
|
+
if reply_to_message_id:
|
|
1373
|
+
payload["reply_to_message_id"] = str(reply_to_message_id)
|
|
1374
|
+
|
|
1375
|
+
resp = self._post("sendFile", payload)
|
|
1376
|
+
message_id_put = resp["data"]["message_id"]
|
|
1377
|
+
result = {
|
|
1378
|
+
"status": resp.get("status"),
|
|
1379
|
+
"status_det": resp.get("status_det"),
|
|
1380
|
+
"file_id": file_id,
|
|
1381
|
+
"text":text,
|
|
1382
|
+
"message_id": message_id_put,
|
|
1383
|
+
"send_to_chat_id": chat_id,
|
|
1384
|
+
"reply_to_message_id": reply_to_message_id,
|
|
1385
|
+
"disable_notification": disable_notification,
|
|
1386
|
+
"type_file": type_file,
|
|
1387
|
+
"raw_response": resp,
|
|
1388
|
+
"chat_keypad":chat_keypad,
|
|
1389
|
+
"inline_keypad":inline_keypad,
|
|
1390
|
+
"chat_keypad_type":chat_keypad_type
|
|
1391
|
+
}
|
|
1392
|
+
import json
|
|
1393
|
+
return json.dumps(result, ensure_ascii=False, indent=4)
|
|
1394
|
+
def send_file(
|
|
1395
|
+
self,
|
|
1396
|
+
chat_id: str,
|
|
1397
|
+
path: Optional[Union[str, Path]] = None,
|
|
1398
|
+
file_id: Optional[str] = None,
|
|
1399
|
+
caption: Optional[str] = None,
|
|
1400
|
+
file_name: Optional[str] = None,
|
|
1401
|
+
inline_keypad: Optional[Dict[str, Any]] = None,
|
|
1402
|
+
chat_keypad: Optional[Dict[str, Any]] = None,
|
|
1403
|
+
reply_to_message_id: Optional[str] = None,
|
|
1404
|
+
disable_notification: bool = False,
|
|
1405
|
+
chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
|
|
1406
|
+
) -> Dict[str, Any]:
|
|
1407
|
+
if path:
|
|
1408
|
+
file_name = file_name or Path(path).name
|
|
1409
|
+
upload_url = self.get_upload_url("File")
|
|
1410
|
+
file_id = self.upload_media_file(upload_url, file_name, path)
|
|
1411
|
+
if not file_id:
|
|
1412
|
+
raise ValueError("Either path or file_id must be provided.")
|
|
1413
|
+
return self._send_uploaded_file(
|
|
1414
|
+
chat_id=chat_id,
|
|
1415
|
+
file_id=file_id,
|
|
1416
|
+
text=caption,
|
|
1417
|
+
inline_keypad=inline_keypad,
|
|
1418
|
+
chat_keypad=chat_keypad,
|
|
1419
|
+
reply_to_message_id=reply_to_message_id,
|
|
1420
|
+
disable_notification=disable_notification,
|
|
1421
|
+
chat_keypad_type=chat_keypad_type
|
|
1422
|
+
)
|
|
1423
|
+
def re_send_file(
|
|
1424
|
+
self,
|
|
1425
|
+
chat_id: str,
|
|
1426
|
+
path: Optional[Union[str, Path]] = None,
|
|
1427
|
+
file_id: Optional[str] = None,
|
|
1428
|
+
caption: Optional[str] = None,
|
|
1429
|
+
file_name: Optional[str] = None,
|
|
1430
|
+
inline_keypad: Optional[Dict[str, Any]] = None,
|
|
1431
|
+
chat_keypad: Optional[Dict[str, Any]] = None,
|
|
1432
|
+
reply_to_message_id: Optional[str] = None,
|
|
1433
|
+
disable_notification: bool = False,
|
|
1434
|
+
chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
|
|
1435
|
+
) -> Dict[str, Any]:
|
|
1436
|
+
if path:
|
|
1437
|
+
file_name = file_name or Path(path).name
|
|
1438
|
+
upload_url = self.get_upload_url("File")
|
|
1439
|
+
file_id = self.upload_media_file(upload_url, file_name, path)
|
|
1440
|
+
if not file_id:
|
|
1441
|
+
raise ValueError("Either path or file_id must be provided.")
|
|
1442
|
+
return self._send_uploaded_file(
|
|
1443
|
+
chat_id=chat_id,
|
|
1444
|
+
file_id=file_id,
|
|
1445
|
+
text=caption,
|
|
1446
|
+
inline_keypad=inline_keypad,
|
|
1447
|
+
chat_keypad=chat_keypad,
|
|
1448
|
+
reply_to_message_id=reply_to_message_id,
|
|
1449
|
+
disable_notification=disable_notification,
|
|
1450
|
+
chat_keypad_type=chat_keypad_type
|
|
1451
|
+
)
|
|
1452
|
+
def send_document(
|
|
1453
|
+
self,
|
|
1454
|
+
chat_id: str,
|
|
1455
|
+
path: Optional[Union[str, Path]] = None,
|
|
1456
|
+
file_id: Optional[str] = None,
|
|
1457
|
+
text: Optional[str] = None,
|
|
1458
|
+
file_name: Optional[str] = None,
|
|
1459
|
+
inline_keypad: Optional[Dict[str, Any]] = None,
|
|
1460
|
+
chat_keypad: Optional[Dict[str, Any]] = None,
|
|
1461
|
+
reply_to_message_id: Optional[str] = None,
|
|
1462
|
+
disable_notification: bool = False,
|
|
1463
|
+
chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
|
|
1464
|
+
) -> Dict[str, Any]:
|
|
1465
|
+
if path:
|
|
1466
|
+
file_name = file_name or Path(path).name
|
|
1467
|
+
upload_url = self.get_upload_url("File")
|
|
1468
|
+
file_id = self.upload_media_file(upload_url, file_name, path)
|
|
1469
|
+
if not file_id:
|
|
1470
|
+
raise ValueError("Either path or file_id must be provided.")
|
|
1471
|
+
return self._send_uploaded_file(
|
|
1472
|
+
chat_id=chat_id,
|
|
1473
|
+
file_id=file_id,
|
|
1474
|
+
text=text,
|
|
1475
|
+
inline_keypad=inline_keypad,
|
|
1476
|
+
chat_keypad=chat_keypad,
|
|
1477
|
+
reply_to_message_id=reply_to_message_id,
|
|
1478
|
+
disable_notification=disable_notification,
|
|
1479
|
+
chat_keypad_type=chat_keypad_type
|
|
1480
|
+
)
|
|
1481
|
+
def send_music(
|
|
1482
|
+
self,
|
|
1483
|
+
chat_id: str,
|
|
1484
|
+
path: Optional[Union[str, Path]] = None,
|
|
1485
|
+
file_id: Optional[str] = None,
|
|
1486
|
+
text: Optional[str] = None,
|
|
1487
|
+
file_name: Optional[str] = None,
|
|
1488
|
+
inline_keypad: Optional[Dict[str, Any]] = None,
|
|
1489
|
+
chat_keypad: Optional[Dict[str, Any]] = None,
|
|
1490
|
+
reply_to_message_id: Optional[str] = None,
|
|
1491
|
+
disable_notification: bool = False,
|
|
1492
|
+
chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
|
|
1493
|
+
) -> Dict[str, Any]:
|
|
1494
|
+
if path:
|
|
1495
|
+
file_name = file_name or Path(path).name
|
|
1496
|
+
upload_url = self.get_upload_url("Music")
|
|
1497
|
+
file_id = self.upload_media_file(upload_url, file_name, path)
|
|
1498
|
+
if not file_id:
|
|
1499
|
+
raise ValueError("Either path or file_id must be provided.")
|
|
1500
|
+
return self._send_uploaded_file(
|
|
1501
|
+
chat_id=chat_id,
|
|
1502
|
+
file_id=file_id,
|
|
1503
|
+
text=text,
|
|
1504
|
+
inline_keypad=inline_keypad,
|
|
1505
|
+
chat_keypad=chat_keypad,
|
|
1506
|
+
reply_to_message_id=reply_to_message_id,
|
|
1507
|
+
disable_notification=disable_notification,
|
|
1508
|
+
chat_keypad_type=chat_keypad_type
|
|
1509
|
+
)
|
|
1510
|
+
def send_video(
|
|
1511
|
+
self,
|
|
1512
|
+
chat_id: str,
|
|
1513
|
+
path: Optional[Union[str, Path]] = None,
|
|
1514
|
+
file_id: Optional[str] = None,
|
|
1515
|
+
text: Optional[str] = None,
|
|
1516
|
+
file_name: Optional[str] = None,
|
|
1517
|
+
inline_keypad: Optional[Dict[str, Any]] = None,
|
|
1518
|
+
chat_keypad: Optional[Dict[str, Any]] = None,
|
|
1519
|
+
reply_to_message_id: Optional[str] = None,
|
|
1520
|
+
disable_notification: bool = False,
|
|
1521
|
+
chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
|
|
1522
|
+
) -> Dict[str, Any]:
|
|
1523
|
+
if path:
|
|
1524
|
+
file_name = file_name or Path(path).name
|
|
1525
|
+
upload_url = self.get_upload_url("Video")
|
|
1526
|
+
file_id = self.upload_media_file(upload_url, file_name, path)
|
|
1527
|
+
if not file_id:
|
|
1528
|
+
raise ValueError("Either path or file_id must be provided.")
|
|
1529
|
+
return self._send_uploaded_file(
|
|
1530
|
+
chat_id=chat_id,
|
|
1531
|
+
file_id=file_id,
|
|
1532
|
+
text=text,
|
|
1533
|
+
inline_keypad=inline_keypad,
|
|
1534
|
+
chat_keypad=chat_keypad,
|
|
1535
|
+
reply_to_message_id=reply_to_message_id,
|
|
1536
|
+
disable_notification=disable_notification,
|
|
1537
|
+
chat_keypad_type=chat_keypad_type
|
|
1538
|
+
)
|
|
1539
|
+
def send_voice(
|
|
1540
|
+
self,
|
|
1541
|
+
chat_id: str,
|
|
1542
|
+
path: Optional[Union[str, Path]] = None,
|
|
1543
|
+
file_id: Optional[str] = None,
|
|
1544
|
+
text: Optional[str] = None,
|
|
1545
|
+
file_name: Optional[str] = None,
|
|
1546
|
+
inline_keypad: Optional[Dict[str, Any]] = None,
|
|
1547
|
+
chat_keypad: Optional[Dict[str, Any]] = None,
|
|
1548
|
+
reply_to_message_id: Optional[str] = None,
|
|
1549
|
+
disable_notification: bool = False,
|
|
1550
|
+
chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
|
|
1551
|
+
) -> Dict[str, Any]:
|
|
1552
|
+
if path:
|
|
1553
|
+
file_name = file_name or Path(path).name
|
|
1554
|
+
upload_url = self.get_upload_url("Voice")
|
|
1555
|
+
file_id = self.upload_media_file(upload_url, file_name, path)
|
|
1556
|
+
if not file_id:
|
|
1557
|
+
raise ValueError("Either path or file_id must be provided.")
|
|
1558
|
+
return self._send_uploaded_file(
|
|
1559
|
+
chat_id=chat_id,
|
|
1560
|
+
file_id=file_id,
|
|
1561
|
+
text=text,
|
|
1562
|
+
inline_keypad=inline_keypad,
|
|
1563
|
+
chat_keypad=chat_keypad,
|
|
1564
|
+
reply_to_message_id=reply_to_message_id,
|
|
1565
|
+
disable_notification=disable_notification,
|
|
1566
|
+
chat_keypad_type=chat_keypad_type
|
|
1567
|
+
)
|
|
1568
|
+
def send_image(
|
|
1569
|
+
self,
|
|
1570
|
+
chat_id: str,
|
|
1571
|
+
path: Optional[Union[str, Path]] = None,
|
|
1572
|
+
file_id: Optional[str] = None,
|
|
1573
|
+
text: Optional[str] = None,
|
|
1574
|
+
file_name: Optional[str] = None,
|
|
1575
|
+
inline_keypad: Optional[Dict[str, Any]] = None,
|
|
1576
|
+
chat_keypad: Optional[Dict[str, Any]] = None,
|
|
1577
|
+
reply_to_message_id: Optional[str] = None,
|
|
1578
|
+
disable_notification: bool = False,
|
|
1579
|
+
chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
|
|
1580
|
+
) -> Dict[str, Any]:
|
|
1581
|
+
if path:
|
|
1582
|
+
file_name = file_name or Path(path).name
|
|
1583
|
+
upload_url = self.get_upload_url("Image")
|
|
1584
|
+
file_id = self.upload_media_file(upload_url, file_name, path)
|
|
1585
|
+
if not file_id:
|
|
1586
|
+
raise ValueError("Either path or file_id must be provided.")
|
|
1587
|
+
return self._send_uploaded_file(
|
|
1588
|
+
chat_id=chat_id,
|
|
1589
|
+
file_id=file_id,
|
|
1590
|
+
text=text,
|
|
1591
|
+
inline_keypad=inline_keypad,
|
|
1592
|
+
chat_keypad=chat_keypad,
|
|
1593
|
+
reply_to_message_id=reply_to_message_id,
|
|
1594
|
+
disable_notification=disable_notification,
|
|
1595
|
+
chat_keypad_type=chat_keypad_type
|
|
1596
|
+
)
|
|
1597
|
+
def send_gif(
|
|
1598
|
+
self,
|
|
1599
|
+
chat_id: str,
|
|
1600
|
+
path: Optional[Union[str, Path]] = None,
|
|
1601
|
+
file_id: Optional[str] = None,
|
|
1602
|
+
text: Optional[str] = None,
|
|
1603
|
+
file_name: Optional[str] = None,
|
|
1604
|
+
inline_keypad: Optional[Dict[str, Any]] = None,
|
|
1605
|
+
chat_keypad: Optional[Dict[str, Any]] = None,
|
|
1606
|
+
reply_to_message_id: Optional[str] = None,
|
|
1607
|
+
disable_notification: bool = False,
|
|
1608
|
+
chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
|
|
1609
|
+
) -> Dict[str, Any]:
|
|
1610
|
+
if path:
|
|
1611
|
+
file_name = file_name or Path(path).name
|
|
1612
|
+
upload_url = self.get_upload_url("Gif")
|
|
1613
|
+
file_id = self.upload_media_file(upload_url, file_name, path)
|
|
1614
|
+
if not file_id:
|
|
1615
|
+
raise ValueError("Either path or file_id must be provided.")
|
|
1616
|
+
return self._send_uploaded_file(
|
|
1617
|
+
chat_id=chat_id,
|
|
1618
|
+
file_id=file_id,
|
|
1619
|
+
text=text,
|
|
1620
|
+
inline_keypad=inline_keypad,
|
|
1621
|
+
chat_keypad=chat_keypad,
|
|
1622
|
+
reply_to_message_id=reply_to_message_id,
|
|
1623
|
+
disable_notification=disable_notification,
|
|
1624
|
+
chat_keypad_type=chat_keypad_type
|
|
1625
|
+
)
|
|
1626
|
+
|
|
1627
|
+
|
|
1628
|
+
|
|
1629
|
+
def forward_message(
|
|
1630
|
+
self,
|
|
1631
|
+
from_chat_id: str,
|
|
1632
|
+
message_id: str,
|
|
1633
|
+
to_chat_id: str,
|
|
1634
|
+
disable_notification: bool = False
|
|
1635
|
+
) -> Dict[str, Any]:
|
|
1636
|
+
"""Forward a message from one chat to another."""
|
|
1637
|
+
return self._post("forwardMessage", {
|
|
1638
|
+
"from_chat_id": from_chat_id,
|
|
1639
|
+
"message_id": message_id,
|
|
1640
|
+
"to_chat_id": to_chat_id,
|
|
1641
|
+
"disable_notification": disable_notification
|
|
1642
|
+
})
|
|
1643
|
+
|
|
1644
|
+
def edit_message_text(
|
|
1645
|
+
self,
|
|
1646
|
+
chat_id: str,
|
|
1647
|
+
message_id: str,
|
|
1648
|
+
text: str
|
|
1649
|
+
) -> Dict[str, Any]:
|
|
1650
|
+
"""Edit text of an existing message."""
|
|
1651
|
+
return self._post("editMessageText", {
|
|
1652
|
+
"chat_id": chat_id,
|
|
1653
|
+
"message_id": message_id,
|
|
1654
|
+
"text": text
|
|
1655
|
+
})
|
|
1656
|
+
|
|
1657
|
+
def edit_inline_keypad(
|
|
1658
|
+
self,
|
|
1659
|
+
chat_id: str,
|
|
1660
|
+
message_id: str,
|
|
1661
|
+
inline_keypad: Dict[str, Any],
|
|
1662
|
+
text : str = None
|
|
1663
|
+
) -> Dict[str, Any]:
|
|
1664
|
+
"""Edit inline keypad of a message."""
|
|
1665
|
+
if text is not None:self._post("editMessageText", {"chat_id": chat_id,"message_id": message_id,"text": text})
|
|
1666
|
+
return self._post("editMessageKeypad", {
|
|
1667
|
+
"chat_id": chat_id,
|
|
1668
|
+
"message_id": message_id,
|
|
1669
|
+
"inline_keypad": inline_keypad
|
|
1670
|
+
})
|
|
1671
|
+
|
|
1672
|
+
def delete_message(self, chat_id: str, message_id: str) -> Dict[str, Any]:
|
|
1673
|
+
"""Delete a message from chat."""
|
|
1674
|
+
return self._post("deleteMessage", {
|
|
1675
|
+
"chat_id": chat_id,
|
|
1676
|
+
"message_id": message_id
|
|
1677
|
+
})
|
|
1678
|
+
|
|
1679
|
+
def set_commands(self, bot_commands: List[Dict[str, str]]) -> Dict[str, Any]:
|
|
1680
|
+
"""Set bot commands."""
|
|
1681
|
+
return self._post("setCommands", {"bot_commands": bot_commands})
|
|
1682
|
+
|
|
1683
|
+
def update_bot_endpoint(self, url: str, type: str) -> Dict[str, Any]:
|
|
1684
|
+
"""Update bot endpoint (Webhook or Polling)."""
|
|
1685
|
+
return self._post("updateBotEndpoints", {
|
|
1686
|
+
"url": url,
|
|
1687
|
+
"type": type
|
|
1688
|
+
})
|
|
1689
|
+
|
|
1690
|
+
def remove_keypad(self, chat_id: str) -> Dict[str, Any]:
|
|
1691
|
+
"""Remove chat keypad."""
|
|
1692
|
+
return self._post("editChatKeypad", {
|
|
1693
|
+
"chat_id": chat_id,
|
|
1694
|
+
"chat_keypad_type": "Removed"
|
|
1695
|
+
})
|
|
1696
|
+
|
|
1697
|
+
def edit_chat_keypad(self, chat_id: str, chat_keypad: Dict[str, Any]) -> Dict[str, Any]:
|
|
1698
|
+
"""Edit or add new chat keypad."""
|
|
1699
|
+
return self._post("editChatKeypad", {
|
|
1700
|
+
"chat_id": chat_id,
|
|
1701
|
+
"chat_keypad_type": "New",
|
|
1702
|
+
"chat_keypad": chat_keypad
|
|
1703
|
+
})
|
|
1704
|
+
def get_name(self, chat_id: str) -> str:
|
|
1705
|
+
try:
|
|
1706
|
+
chat = self.get_chat(chat_id)
|
|
1707
|
+
chat_info = chat.get("data", {}).get("chat", {})
|
|
1708
|
+
first_name = chat_info.get("first_name", "")
|
|
1709
|
+
last_name = chat_info.get("last_name", "")
|
|
1710
|
+
|
|
1711
|
+
if first_name and last_name:
|
|
1712
|
+
return f"{first_name} {last_name}"
|
|
1713
|
+
elif first_name:
|
|
1714
|
+
return first_name
|
|
1715
|
+
elif last_name:
|
|
1716
|
+
return last_name
|
|
1717
|
+
else:
|
|
1718
|
+
return "Unknown"
|
|
1719
|
+
except Exception:
|
|
1720
|
+
return "Unknown"
|
|
1721
|
+
def get_username(self, chat_id: str) -> str:
|
|
1722
|
+
chat_info = self.get_chat(chat_id).get("data", {}).get("chat", {})
|
|
1723
|
+
return chat_info.get("username", "None")
|