Rubka 2.11.13__py3-none-any.whl → 7.1.17__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 +72 -3
- 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 +1450 -95
- rubka/asynco.py +2515 -0
- rubka/button.py +404 -0
- rubka/context.py +744 -34
- rubka/exceptions.py +35 -1
- rubka/filters.py +321 -0
- rubka/helpers.py +1461 -0
- rubka/keypad.py +255 -5
- rubka/metadata.py +117 -0
- rubka/rubino.py +1231 -0
- rubka/tv.py +145 -0
- rubka/update.py +1038 -0
- rubka-7.1.17.dist-info/METADATA +1048 -0
- rubka-7.1.17.dist-info/RECORD +45 -0
- rubka-7.1.17.dist-info/entry_points.txt +2 -0
- rubka-2.11.13.dist-info/METADATA +0 -315
- rubka-2.11.13.dist-info/RECORD +0 -15
- {rubka-2.11.13.dist-info → rubka-7.1.17.dist-info}/WHEEL +0 -0
- {rubka-2.11.13.dist-info → rubka-7.1.17.dist-info}/top_level.txt +0 -0
rubka/api.py
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import requests
|
|
2
2
|
from typing import List, Optional, Dict, Any, Literal
|
|
3
3
|
from .exceptions import APIRequestError
|
|
4
|
+
from .adaptorrubka import Client as Client_get
|
|
4
5
|
from .logger import logger
|
|
6
|
+
from . import filters
|
|
7
|
+
from . import helpers
|
|
5
8
|
from typing import Callable
|
|
6
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
|
|
7
17
|
API_URL = "https://botapi.rubika.ir/v3"
|
|
18
|
+
import mimetypes
|
|
19
|
+
import re
|
|
8
20
|
import sys
|
|
9
21
|
import subprocess
|
|
10
|
-
|
|
22
|
+
class InvalidTokenError(Exception):pass
|
|
11
23
|
def install_package(package_name):
|
|
12
24
|
try:
|
|
13
25
|
subprocess.check_call([sys.executable, "-m", "pip", "install", package_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
@@ -42,41 +54,88 @@ def get_latest_version(package_name: str) -> str:
|
|
|
42
54
|
data = resp.json()
|
|
43
55
|
return data["info"]["version"]
|
|
44
56
|
except Exception:return None
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
print(f"Please update it using:\n\npip install --upgrade {package_name}\n")
|
|
56
|
-
|
|
57
|
-
check_rubka_version()
|
|
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
|
|
58
67
|
class Robot:
|
|
59
68
|
"""
|
|
60
69
|
Main class to interact with Rubika Bot API.
|
|
61
70
|
Initialized with bot token.
|
|
62
71
|
"""
|
|
63
72
|
|
|
64
|
-
def __init__(self, token: str):
|
|
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
|
+
"""
|
|
65
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
|
|
66
105
|
self._offset_id = None
|
|
67
106
|
self.session = requests.Session()
|
|
68
107
|
self.sessions: Dict[str, Dict[str, Any]] = {}
|
|
69
108
|
self._callback_handler = None
|
|
70
109
|
self._message_handler = None
|
|
71
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
|
|
72
131
|
|
|
73
132
|
|
|
74
|
-
logger.info(f"Initialized RubikaBot with token: {token[:8]}***")
|
|
75
133
|
|
|
134
|
+
logger.info(f"Initialized RubikaBot with token: {token[:8]}***")
|
|
76
135
|
def _post(self, method: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
77
136
|
url = f"{API_URL}/{self.token}/{method}"
|
|
78
137
|
try:
|
|
79
|
-
response = self.session.post(url, json=data, timeout=
|
|
138
|
+
response = self.session.post(url, json=data, timeout=self.timeout)
|
|
80
139
|
response.raise_for_status()
|
|
81
140
|
try:
|
|
82
141
|
json_resp = response.json()
|
|
@@ -94,100 +153,846 @@ class Robot:
|
|
|
94
153
|
def get_me(self) -> Dict[str, Any]:
|
|
95
154
|
"""Get info about the bot itself."""
|
|
96
155
|
return self._post("getMe", {})
|
|
97
|
-
def
|
|
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):
|
|
98
666
|
def decorator(func: Callable[[Any, Message], None]):
|
|
99
|
-
self.
|
|
667
|
+
self._message_handlers.append({
|
|
100
668
|
"func": func,
|
|
101
669
|
"filters": filters,
|
|
102
670
|
"commands": commands
|
|
103
|
-
}
|
|
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
|
+
})
|
|
104
681
|
return func
|
|
105
682
|
return decorator
|
|
106
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}")
|
|
107
723
|
|
|
108
|
-
def on_inline_query(self):
|
|
724
|
+
def on_inline_query(self, button_id: Optional[str] = None):
|
|
109
725
|
def decorator(func: Callable[[Any, InlineMessage], None]):
|
|
110
|
-
self.
|
|
726
|
+
self._inline_query_handlers.append({
|
|
727
|
+
"func": func,
|
|
728
|
+
"button_id": button_id
|
|
729
|
+
})
|
|
111
730
|
return func
|
|
112
731
|
return decorator
|
|
732
|
+
|
|
113
733
|
|
|
114
|
-
def _process_update(self, update:
|
|
734
|
+
def _process_update(self, update: dict):
|
|
115
735
|
import threading
|
|
116
|
-
|
|
736
|
+
|
|
737
|
+
if update.get("type") == "ReceiveQuery":
|
|
117
738
|
msg = update.get("inline_message", {})
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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", {})
|
|
128
754
|
try:
|
|
129
|
-
import time
|
|
130
755
|
if msg.get("time") and (time.time() - float(msg["time"])) > 20:
|
|
131
756
|
return
|
|
132
757
|
except Exception:
|
|
133
758
|
return
|
|
134
759
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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)
|
|
138
766
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
cmd = parts[0][1:]
|
|
144
|
-
if cmd not in handler["commands"]:
|
|
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()
|
|
145
771
|
return
|
|
146
|
-
context.args = parts[1:]
|
|
147
772
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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:]
|
|
151
783
|
|
|
152
|
-
|
|
784
|
+
if handler["filters"] and not handler["filters"](context):
|
|
785
|
+
continue
|
|
153
786
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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()
|
|
160
815
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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]
|
|
164
819
|
|
|
820
|
+
if message_id in self._processed_message_ids:
|
|
821
|
+
return True
|
|
165
822
|
|
|
823
|
+
self._processed_message_ids[message_id] = now
|
|
824
|
+
return False
|
|
166
825
|
|
|
167
|
-
def run(self):
|
|
168
|
-
print("Bot started running...")
|
|
169
|
-
if self._offset_id is None:
|
|
170
|
-
try:
|
|
171
|
-
latest = self.get_updates(limit=100)
|
|
172
|
-
if latest and latest.get("data") and latest["data"].get("updates"):
|
|
173
|
-
updates = latest["data"]["updates"]
|
|
174
|
-
last_update = updates[-1]
|
|
175
|
-
self._offset_id = latest["data"].get("next_offset_id")
|
|
176
|
-
print(f"Offset initialized to: {self._offset_id}")
|
|
177
|
-
else:
|
|
178
|
-
print("No updates found.")
|
|
179
|
-
except Exception as e:
|
|
180
|
-
print(f"Failed to fetch latest message: {e}")
|
|
181
826
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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.")
|
|
191
996
|
|
|
192
997
|
def send_message(
|
|
193
998
|
self,
|
|
@@ -197,7 +1002,9 @@ class Robot:
|
|
|
197
1002
|
inline_keypad: Optional[Dict[str, Any]] = None,
|
|
198
1003
|
disable_notification: bool = False,
|
|
199
1004
|
reply_to_message_id: Optional[str] = None,
|
|
200
|
-
chat_keypad_type: Optional[Literal["New", "Removed"]] = None
|
|
1005
|
+
chat_keypad_type: Optional[Literal["New", "Removed"]] = None,
|
|
1006
|
+
delete_after = None,
|
|
1007
|
+
parse_mode = None
|
|
201
1008
|
) -> Dict[str, Any]:
|
|
202
1009
|
"""
|
|
203
1010
|
Send a text message to a chat.
|
|
@@ -218,6 +1025,48 @@ class Robot:
|
|
|
218
1025
|
|
|
219
1026
|
return self._post("sendMessage", payload)
|
|
220
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
|
+
|
|
221
1070
|
def send_poll(
|
|
222
1071
|
self,
|
|
223
1072
|
chat_id: str,
|
|
@@ -255,7 +1104,6 @@ class Robot:
|
|
|
255
1104
|
"reply_to_message_id": reply_to_message_id,
|
|
256
1105
|
"chat_keypad_type": chat_keypad_type
|
|
257
1106
|
}
|
|
258
|
-
# Remove None values
|
|
259
1107
|
payload = {k: v for k, v in payload.items() if v is not None}
|
|
260
1108
|
return self._post("sendLocation", payload)
|
|
261
1109
|
|
|
@@ -275,23 +1123,508 @@ class Robot:
|
|
|
275
1123
|
"last_name": last_name,
|
|
276
1124
|
"phone_number": phone_number
|
|
277
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))
|
|
278
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}")
|
|
279
1183
|
def get_chat(self, chat_id: str) -> Dict[str, Any]:
|
|
280
1184
|
"""Get chat info."""
|
|
281
1185
|
return self._post("getChat", {"chat_id": chat_id})
|
|
282
1186
|
|
|
283
|
-
def
|
|
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(
|
|
284
1395
|
self,
|
|
285
|
-
|
|
286
|
-
|
|
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"
|
|
287
1406
|
) -> Dict[str, Any]:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if
|
|
293
|
-
|
|
294
|
-
return self.
|
|
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
|
+
|
|
295
1628
|
|
|
296
1629
|
def forward_message(
|
|
297
1630
|
self,
|
|
@@ -325,10 +1658,12 @@ class Robot:
|
|
|
325
1658
|
self,
|
|
326
1659
|
chat_id: str,
|
|
327
1660
|
message_id: str,
|
|
328
|
-
inline_keypad: Dict[str, Any]
|
|
1661
|
+
inline_keypad: Dict[str, Any],
|
|
1662
|
+
text : str = None
|
|
329
1663
|
) -> Dict[str, Any]:
|
|
330
1664
|
"""Edit inline keypad of a message."""
|
|
331
|
-
|
|
1665
|
+
if text is not None:self._post("editMessageText", {"chat_id": chat_id,"message_id": message_id,"text": text})
|
|
1666
|
+
return self._post("editMessageKeypad", {
|
|
332
1667
|
"chat_id": chat_id,
|
|
333
1668
|
"message_id": message_id,
|
|
334
1669
|
"inline_keypad": inline_keypad
|
|
@@ -366,3 +1701,23 @@ class Robot:
|
|
|
366
1701
|
"chat_keypad_type": "New",
|
|
367
1702
|
"chat_keypad": chat_keypad
|
|
368
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")
|