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/asynco.py
ADDED
|
@@ -0,0 +1,2541 @@
|
|
|
1
|
+
import asyncio,aiohttp,aiofiles,time,datetime,json,tempfile,os,sys,subprocess,mimetypes,time, hashlib,sqlite3,re
|
|
2
|
+
from typing import List, Optional, Dict, Any, Literal, Callable, Union,Set
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from .exceptions import APIRequestError,raise_for_status,InvalidAccessError,InvalidInputError,TooRequestError,InvalidTokenError
|
|
5
|
+
from .adaptorrubka import Client as Client_get
|
|
6
|
+
from .logger import logger
|
|
7
|
+
from .metadata import Track_parsed as GlyphWeaver
|
|
8
|
+
from .rubino import Bot as Rubino
|
|
9
|
+
from . import filters
|
|
10
|
+
try:from .context import Message, InlineMessage
|
|
11
|
+
except (ImportError, ModuleNotFoundError):from context import Message, InlineMessage
|
|
12
|
+
try:from .button import ChatKeypadBuilder, InlineBuilder
|
|
13
|
+
except (ImportError, ModuleNotFoundError):from button import ChatKeypadBuilder, InlineBuilder
|
|
14
|
+
class FeatureNotAvailableError(Exception):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
from tqdm.asyncio import tqdm
|
|
18
|
+
from urllib.parse import urlparse, parse_qs
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from tqdm import tqdm
|
|
22
|
+
API_URL = "https://botapi.rubika.ir/v3"
|
|
23
|
+
|
|
24
|
+
def install_package(package_name: str) -> bool:
|
|
25
|
+
try:
|
|
26
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", package_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
27
|
+
return True
|
|
28
|
+
except Exception:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def metadata_CallBack(text: str, mode: str) -> Dict[str, Any]:
|
|
32
|
+
meta_data_parts = []
|
|
33
|
+
if mode == "Markdown":
|
|
34
|
+
for match in re.finditer(r"\*\*(.*?)\*\*", text):
|
|
35
|
+
meta_data_parts.append({
|
|
36
|
+
"type": "Bold",
|
|
37
|
+
"from_index": match.start(1),
|
|
38
|
+
"length": len(match.group(1))
|
|
39
|
+
})
|
|
40
|
+
for match in re.finditer(r"(?<!\*)\*(?!\*)(.*?)\*(?!\*)", text):
|
|
41
|
+
meta_data_parts.append({
|
|
42
|
+
"type": "Italic",
|
|
43
|
+
"from_index": match.start(1),
|
|
44
|
+
"length": len(match.group(1))
|
|
45
|
+
})
|
|
46
|
+
for match in re.finditer(r"~~(.*?)~~", text):
|
|
47
|
+
meta_data_parts.append({
|
|
48
|
+
"type": "Strike",
|
|
49
|
+
"from_index": match.start(1),
|
|
50
|
+
"length": len(match.group(1))
|
|
51
|
+
})
|
|
52
|
+
for match in re.finditer(r"__(.*?)__", text):
|
|
53
|
+
meta_data_parts.append({
|
|
54
|
+
"type": "Underline",
|
|
55
|
+
"from_index": match.start(1),
|
|
56
|
+
"length": len(match.group(1))
|
|
57
|
+
})
|
|
58
|
+
for match in re.finditer(r"^> (.*)$", text, re.MULTILINE):
|
|
59
|
+
meta_data_parts.append({
|
|
60
|
+
"type": "Quote",
|
|
61
|
+
"from_index": match.start(1),
|
|
62
|
+
"length": len(match.group(1))
|
|
63
|
+
})
|
|
64
|
+
for match in re.finditer(r"\|\|(.*?)\|\|", text):
|
|
65
|
+
meta_data_parts.append({
|
|
66
|
+
"type": "Spoiler",
|
|
67
|
+
"from_index": match.start(1),
|
|
68
|
+
"length": len(match.group(1))
|
|
69
|
+
})
|
|
70
|
+
for match in re.finditer(r"`(.*?)`", text):
|
|
71
|
+
meta_data_parts.append({
|
|
72
|
+
"type": "Mono",
|
|
73
|
+
"from_index": match.start(1),
|
|
74
|
+
"length": len(match.group(1))
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
for match in re.finditer(r"```(.*?)```", text, re.DOTALL):
|
|
78
|
+
meta_data_parts.append({
|
|
79
|
+
"type": "Pre",
|
|
80
|
+
"from_index": match.start(1),
|
|
81
|
+
"length": len(match.group(1))
|
|
82
|
+
})
|
|
83
|
+
for match in re.finditer(r"\[(.*?)\]\((.*?)\)", text):
|
|
84
|
+
meta_data_parts.append({
|
|
85
|
+
"type": "Link",
|
|
86
|
+
"from_index": match.start(1),
|
|
87
|
+
"length": len(match.group(1)),
|
|
88
|
+
"link_url": match.group(2)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
elif mode == "HTML":
|
|
92
|
+
for match in re.finditer(r"<b>(.*?)</b>", text):
|
|
93
|
+
meta_data_parts.append({
|
|
94
|
+
"type": "Bold",
|
|
95
|
+
"from_index": match.start(1),
|
|
96
|
+
"length": len(match.group(1))
|
|
97
|
+
})
|
|
98
|
+
for match in re.finditer(r"<i>(.*?)</i>", text):
|
|
99
|
+
meta_data_parts.append({
|
|
100
|
+
"type": "Italic",
|
|
101
|
+
"from_index": match.start(1),
|
|
102
|
+
"length": len(match.group(1))
|
|
103
|
+
})
|
|
104
|
+
for match in re.finditer(r"<u>(.*?)</u>", text):
|
|
105
|
+
meta_data_parts.append({
|
|
106
|
+
"type": "Underline",
|
|
107
|
+
"from_index": match.start(1),
|
|
108
|
+
"length": len(match.group(1))
|
|
109
|
+
})
|
|
110
|
+
for match in re.finditer(r"<s>(.*?)</s>|<strike>(.*?)</strike>", text):
|
|
111
|
+
inner = match.group(1) or match.group(2)
|
|
112
|
+
meta_data_parts.append({
|
|
113
|
+
"type": "Strike",
|
|
114
|
+
"from_index": match.start(),
|
|
115
|
+
"length": len(inner)
|
|
116
|
+
})
|
|
117
|
+
for match in re.finditer(r"<blockquote>(.*?)</blockquote>", text, re.DOTALL):
|
|
118
|
+
meta_data_parts.append({
|
|
119
|
+
"type": "Quote",
|
|
120
|
+
"from_index": match.start(1),
|
|
121
|
+
"length": len(match.group(1))
|
|
122
|
+
})
|
|
123
|
+
for match in re.finditer(r'<span class="spoiler">(.*?)</span>', text):
|
|
124
|
+
meta_data_parts.append({
|
|
125
|
+
"type": "Spoiler",
|
|
126
|
+
"from_index": match.start(1),
|
|
127
|
+
"length": len(match.group(1))
|
|
128
|
+
})
|
|
129
|
+
for match in re.finditer(r"<code>(.*?)</code>", text):
|
|
130
|
+
meta_data_parts.append({
|
|
131
|
+
"type": "Pre",
|
|
132
|
+
"from_index": match.start(1),
|
|
133
|
+
"length": len(match.group(1))
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
for match in re.finditer(r"<pre>(.*?)</pre>", text, re.DOTALL):
|
|
137
|
+
meta_data_parts.append({
|
|
138
|
+
"type": "Pre",
|
|
139
|
+
"from_index": match.start(1),
|
|
140
|
+
"length": len(match.group(1))
|
|
141
|
+
})
|
|
142
|
+
for match in re.finditer(r'<a href="(.*?)">(.*?)</a>', text):
|
|
143
|
+
meta_data_parts.append({
|
|
144
|
+
"type": "Link",
|
|
145
|
+
"from_index": match.start(2),
|
|
146
|
+
"length": len(match.group(2)),
|
|
147
|
+
"link_url": match.group(1)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
return {"meta_data_parts": meta_data_parts} if meta_data_parts else {}
|
|
151
|
+
|
|
152
|
+
def get_importlib_metadata():
|
|
153
|
+
try:
|
|
154
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
155
|
+
return version, PackageNotFoundError
|
|
156
|
+
except ImportError:
|
|
157
|
+
if install_package("importlib-metadata"):
|
|
158
|
+
try:
|
|
159
|
+
from importlib_metadata import version, PackageNotFoundError
|
|
160
|
+
return version, PackageNotFoundError
|
|
161
|
+
except ImportError:
|
|
162
|
+
return None, None
|
|
163
|
+
return None, None
|
|
164
|
+
|
|
165
|
+
version, PackageNotFoundError = get_importlib_metadata()
|
|
166
|
+
|
|
167
|
+
def get_installed_version(package_name: str) -> Optional[str]:
|
|
168
|
+
if version is None:
|
|
169
|
+
return "unknown"
|
|
170
|
+
try:
|
|
171
|
+
return version(package_name)
|
|
172
|
+
except PackageNotFoundError:
|
|
173
|
+
return None
|
|
174
|
+
BASE_URLS = {
|
|
175
|
+
"botapi": "https://botapi.rubika.ir/v3",
|
|
176
|
+
"messenger": "https://messengerg2b1.iranlms.ir/v3"
|
|
177
|
+
}
|
|
178
|
+
async def get_latest_version(package_name: str) -> Optional[str]:
|
|
179
|
+
url = f"https://pypi.org/pypi/{package_name}/json"
|
|
180
|
+
try:
|
|
181
|
+
async with aiohttp.ClientSession() as session:
|
|
182
|
+
async with session.get(url, timeout=5) as resp:
|
|
183
|
+
resp.raise_for_status()
|
|
184
|
+
data = await resp.json()
|
|
185
|
+
return data.get("info", {}).get("version")
|
|
186
|
+
except Exception:
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
async def check_rubka_version():
|
|
190
|
+
package_name = "rubka"
|
|
191
|
+
installed_version = get_installed_version(package_name)
|
|
192
|
+
if installed_version is None:
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
latest_version = await get_latest_version(package_name)
|
|
196
|
+
if latest_version is None:
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
if installed_version != latest_version:
|
|
200
|
+
print("This poses a serious risk to stability, security, and compatibility with current features.")
|
|
201
|
+
print(f"- Installed version : {installed_version}")
|
|
202
|
+
print(f"- Latest version : {latest_version}")
|
|
203
|
+
print("\nImmediate action is required.")
|
|
204
|
+
print(f"Run the following command to update safely:")
|
|
205
|
+
print(f"\npip install {package_name}=={latest_version}\n")
|
|
206
|
+
print("New Channel Rubka : @rubka_official\n")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def show_last_six_words(text: str) -> str:
|
|
211
|
+
text = text.strip()
|
|
212
|
+
return text[-6:]
|
|
213
|
+
class AttrDict(dict):
|
|
214
|
+
def __getattr__(self, item):
|
|
215
|
+
value = self.get(item)
|
|
216
|
+
if isinstance(value, dict):
|
|
217
|
+
return AttrDict(value)
|
|
218
|
+
return value
|
|
219
|
+
|
|
220
|
+
class Robot:
|
|
221
|
+
"""
|
|
222
|
+
Main asynchronous class to interact with the Rubika Bot API.
|
|
223
|
+
|
|
224
|
+
This class handles sending and receiving messages, inline queries, callbacks,
|
|
225
|
+
and manages sessions and API interactions. It is initialized with a bot token
|
|
226
|
+
and provides multiple optional parameters for configuration.
|
|
227
|
+
|
|
228
|
+
Attributes:
|
|
229
|
+
token (str): Bot token used for authentication with Rubika Bot API.
|
|
230
|
+
session_name (str | None): Optional session name for storing session data.
|
|
231
|
+
auth (str | None): Optional authentication string for advanced features related to account key.
|
|
232
|
+
Key (str | None): Optional account key for additional authorization if required.
|
|
233
|
+
platform (str): Platform type, default is 'web'.
|
|
234
|
+
web_hook (str | None): Optional webhook URL for receiving updates.
|
|
235
|
+
timeout (int): Timeout for API requests in seconds (default 10).
|
|
236
|
+
show_progress (bool): Whether to show progress for long operations (default False).
|
|
237
|
+
raise_errors (bool): Whether to raise exceptions on API errors (default True).
|
|
238
|
+
proxy (str | None): Optional proxy URL to route requests through.
|
|
239
|
+
retries (int): Number of times to retry a failed API request (default 2).
|
|
240
|
+
retry_delay (float): Delay between retries in seconds (default 0.5).
|
|
241
|
+
user_agent (str | None): Custom User-Agent header for requests.
|
|
242
|
+
safeSendMode (bool): If True, messages are sent safely. If reply fails using message_id, sends without message_id (default False).
|
|
243
|
+
max_cache_size (int): Maximum number of processed messages stored to prevent duplicates (default 1000).
|
|
244
|
+
max_msg_age (int): Maximum age of messages in seconds to consider for processing (default 20).
|
|
245
|
+
|
|
246
|
+
Example:
|
|
247
|
+
```python
|
|
248
|
+
import asyncio
|
|
249
|
+
from rubka.asynco import Robot, filters, Message
|
|
250
|
+
|
|
251
|
+
bot = Robot(token="YOUR_BOT_TOKEN", safeSendMode=False, max_cache_size=1000)
|
|
252
|
+
|
|
253
|
+
@bot.on_message(filters.is_command.start)
|
|
254
|
+
async def start_command(bot: Robot, message: Message):
|
|
255
|
+
await message.reply("Hello!")
|
|
256
|
+
|
|
257
|
+
asyncio.run(bot.run())
|
|
258
|
+
```
|
|
259
|
+
Notes:
|
|
260
|
+
|
|
261
|
+
token is mandatory, all other parameters are optional.
|
|
262
|
+
|
|
263
|
+
safeSendMode ensures reliable message sending even if replying by message_id fails.
|
|
264
|
+
|
|
265
|
+
max_cache_size and max_msg_age help manage duplicate message processing efficiently.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
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, raise_errors: bool = True,proxy: str = None,retries: int = 2,retry_delay: float = 0.5,user_agent: str = None,safeSendMode = False,max_cache_size: int = 2000,max_msg_age : int = 60,chunk_size : int = 64 * 1024,parse_mode: Optional[Literal["HTML", "Markdown"]] = "Markdown",api_endpoint: Optional[Literal["botapi", "messenger"]] = "botapi"):
|
|
270
|
+
self.token = token
|
|
271
|
+
self._inline_query_handlers: List[dict] = []
|
|
272
|
+
self.timeout = timeout
|
|
273
|
+
self.auth = auth
|
|
274
|
+
self.chunk_size = chunk_size
|
|
275
|
+
self.safeSendMode = safeSendMode
|
|
276
|
+
self.user_agent = user_agent
|
|
277
|
+
self.proxy = proxy
|
|
278
|
+
self.max_msg_age = max_msg_age
|
|
279
|
+
self.retries = retries
|
|
280
|
+
self.middleware_data = []
|
|
281
|
+
|
|
282
|
+
self.retry_delay = retry_delay
|
|
283
|
+
self.raise_errors = raise_errors
|
|
284
|
+
self.show_progress = show_progress
|
|
285
|
+
self.session_name = session_name
|
|
286
|
+
self.Key = Key
|
|
287
|
+
self.platform = platform
|
|
288
|
+
self.web_hook = web_hook
|
|
289
|
+
self.parse_mode = parse_mode
|
|
290
|
+
self._offset_id: Optional[str] = None
|
|
291
|
+
self._aiohttp_session: aiohttp.ClientSession = None
|
|
292
|
+
self.sessions: Dict[str, Dict[str, Any]] = {}
|
|
293
|
+
self._callback_handler = None
|
|
294
|
+
self._processed_message_ids = OrderedDict()
|
|
295
|
+
self._max_cache_size = max_cache_size
|
|
296
|
+
self._callback_handlers: List[dict] = []
|
|
297
|
+
self._edited_message_handlers = []
|
|
298
|
+
self._message_saver_enabled = False
|
|
299
|
+
self._max_messages = None
|
|
300
|
+
self._db_path = os.path.join(os.getcwd(), "RubkaSaveMessage.db")
|
|
301
|
+
self._ensure_db()
|
|
302
|
+
self._message_handlers: List[dict] = []
|
|
303
|
+
if api_endpoint not in BASE_URLS:raise ValueError(f"api_endpoint must be one of {list(BASE_URLS.keys())}")
|
|
304
|
+
self.api_endpoint = api_endpoint
|
|
305
|
+
|
|
306
|
+
logger.info(f"Initialized RubikaBot with token: {token[:8]}***")
|
|
307
|
+
async def _get_session(self) -> aiohttp.ClientSession:
|
|
308
|
+
if self._aiohttp_session is None or self._aiohttp_session.closed:
|
|
309
|
+
connector = aiohttp.TCPConnector(limit=100, ssl=False)
|
|
310
|
+
timeout = aiohttp.ClientTimeout(total=self.timeout)
|
|
311
|
+
self._aiohttp_session = aiohttp.ClientSession(connector=connector, timeout=timeout)
|
|
312
|
+
return self._aiohttp_session
|
|
313
|
+
async def close(self):
|
|
314
|
+
if self._aiohttp_session and not self._aiohttp_session.closed:
|
|
315
|
+
await self._aiohttp_session.close()
|
|
316
|
+
logger.debug("aiohttp session closed successfully.")
|
|
317
|
+
|
|
318
|
+
async def _initialize_webhook(self):
|
|
319
|
+
"""Initializes and sets the webhook endpoint if provided."""
|
|
320
|
+
if not self.web_hook:
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
session = await self._get_session()
|
|
324
|
+
try:
|
|
325
|
+
async with session.get(self.web_hook, timeout=self.timeout) as response:
|
|
326
|
+
response.raise_for_status()
|
|
327
|
+
data = await response.json()
|
|
328
|
+
if data:print(f"[INFO] Retrieving WebHook URL information...")
|
|
329
|
+
json_url = data.get('url', self.web_hook)
|
|
330
|
+
for endpoint_type in [
|
|
331
|
+
"ReceiveUpdate",
|
|
332
|
+
"ReceiveInlineMessage",
|
|
333
|
+
"ReceiveQuery",
|
|
334
|
+
"GetSelectionItem",
|
|
335
|
+
"SearchSelectionItems"
|
|
336
|
+
]:
|
|
337
|
+
result = await self.update_bot_endpoint(self.web_hook, endpoint_type)
|
|
338
|
+
if result['status'] =="OK":print(f"✔ Set endpoint type to '{endpoint_type}' — Operation succeeded with status: {result['status']}")
|
|
339
|
+
else:print(f"[ERROR] Failed to set endpoint type '{endpoint_type}': Status code {result['status']}")
|
|
340
|
+
self.web_hook = json_url
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logger.error(f"Failed to set webhook from {self.web_hook}: {e}")
|
|
343
|
+
self.web_hook = None
|
|
344
|
+
async def _post(self, method: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
345
|
+
base_url = BASE_URLS[self.api_endpoint]
|
|
346
|
+
url = f"{base_url}/{self.token}/{method}"
|
|
347
|
+
session = await self._get_session()
|
|
348
|
+
for attempt in range(1, self.retries + 1):
|
|
349
|
+
try:
|
|
350
|
+
headers = {}
|
|
351
|
+
if self.user_agent:headers["User-Agent"] = self.user_agent
|
|
352
|
+
async with session.post(url, json=data, proxy=self.proxy,headers=headers) as response:
|
|
353
|
+
if response.status in (429, 500, 502, 503, 504):
|
|
354
|
+
logger.warning(f"[{method}] Got status {response.status}, retry {attempt}/{self.retries}...")
|
|
355
|
+
if attempt < self.retries:
|
|
356
|
+
await asyncio.sleep(self.retry_delay)
|
|
357
|
+
continue
|
|
358
|
+
response.raise_for_status()
|
|
359
|
+
|
|
360
|
+
response.raise_for_status()
|
|
361
|
+
try:
|
|
362
|
+
json_resp = await response.json(content_type=None)
|
|
363
|
+
except Exception:
|
|
364
|
+
text_resp = await response.text()
|
|
365
|
+
logger.error(f"[{method}] Invalid JSON response: {text_resp}")
|
|
366
|
+
raise APIRequestError(f"Invalid JSON response: {text_resp}")
|
|
367
|
+
|
|
368
|
+
status = json_resp.get("status")
|
|
369
|
+
if status in {"INVALID_ACCESS", "INVALID_INPUT", "TOO_REQUESTS"}:
|
|
370
|
+
if self.raise_errors:
|
|
371
|
+
raise_for_status(json_resp)
|
|
372
|
+
return AttrDict(json_resp)
|
|
373
|
+
return AttrDict({**json_resp, **data,"message_id":json_resp.get("data").get("message_id")})
|
|
374
|
+
|
|
375
|
+
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
|
376
|
+
logger.warning(f"[{method}] Attempt {attempt}/{self.retries} failed: {e}")
|
|
377
|
+
if attempt < self.retries:
|
|
378
|
+
await asyncio.sleep(self.retry_delay)
|
|
379
|
+
continue
|
|
380
|
+
logger.error(f"[{method}] API request failed after {self.retries} retries: {e}")
|
|
381
|
+
raise APIRequestError(f"API request failed: {e}") from e
|
|
382
|
+
def _make_dup_key(self, message_id: str, update_type: str, msg_data: dict) -> str:
|
|
383
|
+
raw = f"{message_id}:{update_type}:{msg_data.get('text','')}:{msg_data.get('author_guid','')}"
|
|
384
|
+
return hashlib.sha1(raw.encode()).hexdigest()
|
|
385
|
+
async def get_me(self) -> Dict[str, Any]:
|
|
386
|
+
return await self._post("getMe", {})
|
|
387
|
+
async def geteToken(self):
|
|
388
|
+
try:
|
|
389
|
+
if (await self.get_me())['status'] != "OK":
|
|
390
|
+
raise InvalidTokenError("The provided bot token is invalid or expired.")
|
|
391
|
+
except Exception as e:
|
|
392
|
+
print(e)
|
|
393
|
+
from typing import Callable, Any, Optional, List
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
#save message database __________________________
|
|
397
|
+
|
|
398
|
+
def _ensure_db(self):
|
|
399
|
+
conn = sqlite3.connect(self._db_path)
|
|
400
|
+
cur = conn.cursor()
|
|
401
|
+
cur.execute("""
|
|
402
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
403
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
404
|
+
chat_id TEXT NOT NULL,
|
|
405
|
+
message_id TEXT NOT NULL,
|
|
406
|
+
sender_id TEXT,
|
|
407
|
+
text TEXT,
|
|
408
|
+
raw_data TEXT,
|
|
409
|
+
time TEXT,
|
|
410
|
+
saved_at INTEGER
|
|
411
|
+
);
|
|
412
|
+
""")
|
|
413
|
+
cur.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_message ON messages(chat_id, message_id);")
|
|
414
|
+
conn.commit()
|
|
415
|
+
conn.close()
|
|
416
|
+
|
|
417
|
+
def _insert_message(self, record: dict):
|
|
418
|
+
conn = sqlite3.connect(self._db_path)
|
|
419
|
+
cur = conn.cursor()
|
|
420
|
+
cur.execute("""
|
|
421
|
+
INSERT OR IGNORE INTO messages
|
|
422
|
+
(chat_id, message_id, sender_id, text, raw_data, time, saved_at)
|
|
423
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
424
|
+
""", (
|
|
425
|
+
record.get("chat_id"),
|
|
426
|
+
record.get("message_id"),
|
|
427
|
+
record.get("sender_id"),
|
|
428
|
+
record.get("text"),
|
|
429
|
+
json.dumps(record.get("raw_data") or {}, ensure_ascii=False),
|
|
430
|
+
record.get("time"),
|
|
431
|
+
int(time.time())
|
|
432
|
+
))
|
|
433
|
+
conn.commit()
|
|
434
|
+
if getattr(self, "_max_messages", None) is not None:
|
|
435
|
+
cur.execute("SELECT COUNT(*) FROM messages")
|
|
436
|
+
total = cur.fetchone()[0]
|
|
437
|
+
if total > self._max_messages:
|
|
438
|
+
remove_count = total - self._max_messages
|
|
439
|
+
cur.execute(
|
|
440
|
+
"DELETE FROM messages WHERE id IN (SELECT id FROM messages ORDER BY saved_at ASC LIMIT ?)",
|
|
441
|
+
(remove_count,)
|
|
442
|
+
)
|
|
443
|
+
conn.commit()
|
|
444
|
+
|
|
445
|
+
conn.close()
|
|
446
|
+
|
|
447
|
+
def _fetch_message(self, chat_id: str, message_id: str):
|
|
448
|
+
conn = sqlite3.connect(self._db_path)
|
|
449
|
+
cur = conn.cursor()
|
|
450
|
+
cur.execute(
|
|
451
|
+
"SELECT chat_id, message_id, sender_id, text, raw_data, time, saved_at FROM messages WHERE chat_id=? AND message_id=?",
|
|
452
|
+
(chat_id, message_id)
|
|
453
|
+
)
|
|
454
|
+
row = cur.fetchone()
|
|
455
|
+
conn.close()
|
|
456
|
+
if not row:
|
|
457
|
+
return None
|
|
458
|
+
chat_id, message_id, sender_id, text, raw_data_json, time_val, saved_at = row
|
|
459
|
+
try:
|
|
460
|
+
raw = json.loads(raw_data_json)
|
|
461
|
+
except:
|
|
462
|
+
raw = {}
|
|
463
|
+
return {
|
|
464
|
+
"chat_id": chat_id,
|
|
465
|
+
"message_id": message_id,
|
|
466
|
+
"sender_id": sender_id,
|
|
467
|
+
"text": text,
|
|
468
|
+
"raw_data": raw,
|
|
469
|
+
"time": time_val,
|
|
470
|
+
"saved_at": saved_at
|
|
471
|
+
}
|
|
472
|
+
async def save_message(self, message: Message):
|
|
473
|
+
try:
|
|
474
|
+
record = {
|
|
475
|
+
"chat_id": getattr(message, "chat_id", None),
|
|
476
|
+
"message_id": getattr(message, "message_id", None),
|
|
477
|
+
"sender_id": getattr(message, "author_guid", None),
|
|
478
|
+
"text": getattr(message, "text", None),
|
|
479
|
+
"raw_data": getattr(message, "raw_data", {}),
|
|
480
|
+
"time": getattr(message, "time", None),
|
|
481
|
+
}
|
|
482
|
+
await asyncio.to_thread(self._insert_message, record)
|
|
483
|
+
except Exception as e:
|
|
484
|
+
print(f"[DB] Error saving message: {e}")
|
|
485
|
+
|
|
486
|
+
async def get_message(self, chat_id: str, message_id: str):
|
|
487
|
+
return await asyncio.to_thread(self._fetch_message, chat_id, message_id)
|
|
488
|
+
|
|
489
|
+
def start_save_message(self, max_messages: int = 1000):
|
|
490
|
+
if self._message_saver_enabled:
|
|
491
|
+
return
|
|
492
|
+
self._message_saver_enabled = True
|
|
493
|
+
self._max_messages = max_messages
|
|
494
|
+
decorators = [
|
|
495
|
+
"on_message", "on_edited_message", "on_message_file", "on_message_forwarded",
|
|
496
|
+
"on_message_reply", "on_message_text", "on_update", "on_callback",
|
|
497
|
+
"on_callback_query", "callback_query_handler", "callback_query",
|
|
498
|
+
"on_inline_query", "on_inline_query_prefix", "on_message_private", "on_message_group"
|
|
499
|
+
]
|
|
500
|
+
|
|
501
|
+
for decorator_name in decorators:
|
|
502
|
+
if hasattr(self, decorator_name):
|
|
503
|
+
original_decorator = getattr(self, decorator_name)
|
|
504
|
+
|
|
505
|
+
def make_wrapper(orig_decorator):
|
|
506
|
+
def wrapper(*args, **kwargs):
|
|
507
|
+
decorator = orig_decorator(*args, **kwargs)
|
|
508
|
+
def inner_wrapper(func):
|
|
509
|
+
async def inner(bot, message, *a, **kw):
|
|
510
|
+
try:
|
|
511
|
+
await bot.save_message(message)
|
|
512
|
+
if getattr(self, "_max_messages", None) is not None:
|
|
513
|
+
await asyncio.to_thread(self._prune_old_messages)
|
|
514
|
+
except Exception as e:
|
|
515
|
+
print(f"[DB] Save error: {e}")
|
|
516
|
+
return await func(bot, message, *a, **kw)
|
|
517
|
+
return decorator(inner)
|
|
518
|
+
return inner_wrapper
|
|
519
|
+
return wrapper
|
|
520
|
+
|
|
521
|
+
setattr(self, decorator_name, make_wrapper(original_decorator))
|
|
522
|
+
def _prune_old_messages(self):
|
|
523
|
+
if not hasattr(self, "_max_messages") or self._max_messages is None:
|
|
524
|
+
return
|
|
525
|
+
conn = sqlite3.connect(self._db_path)
|
|
526
|
+
cur = conn.cursor()
|
|
527
|
+
cur.execute("SELECT COUNT(*) FROM messages")
|
|
528
|
+
total = cur.fetchone()[0]
|
|
529
|
+
if total > self._max_messages:
|
|
530
|
+
remove_count = total - self._max_messages
|
|
531
|
+
cur.execute(
|
|
532
|
+
"DELETE FROM messages WHERE id IN (SELECT id FROM messages ORDER BY saved_at ASC LIMIT ?)",
|
|
533
|
+
(remove_count,)
|
|
534
|
+
)
|
|
535
|
+
conn.commit()
|
|
536
|
+
conn.close()
|
|
537
|
+
|
|
538
|
+
#save message database __________________________ end
|
|
539
|
+
|
|
540
|
+
#decorator#
|
|
541
|
+
|
|
542
|
+
def middleware(self):
|
|
543
|
+
def decorator(func: Callable[[Any, Union[Message, InlineMessage]], None]):
|
|
544
|
+
self.middleware_data.append(func)
|
|
545
|
+
return func
|
|
546
|
+
return decorator
|
|
547
|
+
def on_message_private(
|
|
548
|
+
self,
|
|
549
|
+
chat_id: Optional[Union[str, List[str]]] = None,
|
|
550
|
+
commands: Optional[List[str]] = None,
|
|
551
|
+
filters: Optional[Callable[[Message], bool]] = None,
|
|
552
|
+
sender_id: Optional[Union[str, List[str]]] = None,
|
|
553
|
+
sender_type: Optional[str] = None,
|
|
554
|
+
allow_forwarded: bool = True,
|
|
555
|
+
allow_files: bool = True,
|
|
556
|
+
allow_stickers: bool = True,
|
|
557
|
+
allow_polls: bool = True,
|
|
558
|
+
allow_contacts: bool = True,
|
|
559
|
+
allow_locations: bool = True,
|
|
560
|
+
min_text_length: Optional[int] = None,
|
|
561
|
+
max_text_length: Optional[int] = None,
|
|
562
|
+
contains: Optional[str] = None,
|
|
563
|
+
startswith: Optional[str] = None,
|
|
564
|
+
endswith: Optional[str] = None,
|
|
565
|
+
case_sensitive: bool = False
|
|
566
|
+
):
|
|
567
|
+
"""
|
|
568
|
+
Advanced decorator for handling only private messages with extended filters.
|
|
569
|
+
"""
|
|
570
|
+
|
|
571
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
572
|
+
async def wrapper(bot, message: Message):
|
|
573
|
+
|
|
574
|
+
if not message.is_private:
|
|
575
|
+
return
|
|
576
|
+
if chat_id:
|
|
577
|
+
if isinstance(chat_id, str) and message.chat_id != chat_id:
|
|
578
|
+
return
|
|
579
|
+
if isinstance(chat_id, list) and message.chat_id not in chat_id:
|
|
580
|
+
return
|
|
581
|
+
if sender_id:
|
|
582
|
+
if isinstance(sender_id, str) and message.sender_id != sender_id:
|
|
583
|
+
return
|
|
584
|
+
if isinstance(sender_id, list) and message.sender_id not in sender_id:
|
|
585
|
+
return
|
|
586
|
+
if sender_type and message.sender_type != sender_type:
|
|
587
|
+
return
|
|
588
|
+
if not allow_forwarded and message.forwarded_from:
|
|
589
|
+
return
|
|
590
|
+
if not allow_files and message.file:
|
|
591
|
+
return
|
|
592
|
+
if not allow_stickers and message.sticker:
|
|
593
|
+
return
|
|
594
|
+
if not allow_polls and message.poll:
|
|
595
|
+
return
|
|
596
|
+
if not allow_contacts and message.contact_message:
|
|
597
|
+
return
|
|
598
|
+
if not allow_locations and (message.location or message.live_location):
|
|
599
|
+
return
|
|
600
|
+
if message.text:
|
|
601
|
+
text = message.text if case_sensitive else message.text.lower()
|
|
602
|
+
if min_text_length and len(message.text) < min_text_length:
|
|
603
|
+
return
|
|
604
|
+
if max_text_length and len(message.text) > max_text_length:
|
|
605
|
+
return
|
|
606
|
+
if contains and (contains if case_sensitive else contains.lower()) not in text:
|
|
607
|
+
return
|
|
608
|
+
if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
|
|
609
|
+
return
|
|
610
|
+
if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
|
|
611
|
+
return
|
|
612
|
+
if commands:
|
|
613
|
+
if not message.text:
|
|
614
|
+
return
|
|
615
|
+
parts = message.text.strip().split()
|
|
616
|
+
cmd = parts[0].lstrip("/")
|
|
617
|
+
if cmd not in commands:
|
|
618
|
+
return
|
|
619
|
+
message.args = parts[1:]
|
|
620
|
+
if filters and not filters(message):
|
|
621
|
+
return
|
|
622
|
+
return await func(bot, message)
|
|
623
|
+
self._message_handlers.append({
|
|
624
|
+
"func": wrapper,
|
|
625
|
+
"filters": filters,
|
|
626
|
+
"commands": commands,
|
|
627
|
+
"chat_id": chat_id,
|
|
628
|
+
"private_only": True,
|
|
629
|
+
"sender_id": sender_id,
|
|
630
|
+
"sender_type": sender_type
|
|
631
|
+
})
|
|
632
|
+
return wrapper
|
|
633
|
+
return decorator
|
|
634
|
+
def on_message_channel(
|
|
635
|
+
self,
|
|
636
|
+
chat_id: Optional[Union[str, List[str]]] = None,
|
|
637
|
+
commands: Optional[List[str]] = None,
|
|
638
|
+
filters: Optional[Callable[[Message], bool]] = None,
|
|
639
|
+
sender_id: Optional[Union[str, List[str]]] = None,
|
|
640
|
+
sender_type: Optional[str] = None,
|
|
641
|
+
allow_forwarded: bool = True,
|
|
642
|
+
allow_files: bool = True,
|
|
643
|
+
allow_stickers: bool = True,
|
|
644
|
+
allow_polls: bool = True,
|
|
645
|
+
allow_contacts: bool = True,
|
|
646
|
+
allow_locations: bool = True,
|
|
647
|
+
min_text_length: Optional[int] = None,
|
|
648
|
+
max_text_length: Optional[int] = None,
|
|
649
|
+
contains: Optional[str] = None,
|
|
650
|
+
startswith: Optional[str] = None,
|
|
651
|
+
endswith: Optional[str] = None,
|
|
652
|
+
case_sensitive: bool = False
|
|
653
|
+
):
|
|
654
|
+
"""
|
|
655
|
+
Advanced decorator for handling only channel messages with extended filters.
|
|
656
|
+
"""
|
|
657
|
+
|
|
658
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
659
|
+
async def wrapper(bot, message: Message):
|
|
660
|
+
|
|
661
|
+
if not message.is_channel:
|
|
662
|
+
return
|
|
663
|
+
if chat_id:
|
|
664
|
+
if isinstance(chat_id, str) and message.chat_id != chat_id:
|
|
665
|
+
return
|
|
666
|
+
if isinstance(chat_id, list) and message.chat_id not in chat_id:
|
|
667
|
+
return
|
|
668
|
+
if sender_id:
|
|
669
|
+
if isinstance(sender_id, str) and message.sender_id != sender_id:
|
|
670
|
+
return
|
|
671
|
+
if isinstance(sender_id, list) and message.sender_id not in sender_id:
|
|
672
|
+
return
|
|
673
|
+
if sender_type and message.sender_type != sender_type:
|
|
674
|
+
return
|
|
675
|
+
if not allow_forwarded and message.forwarded_from:
|
|
676
|
+
return
|
|
677
|
+
if not allow_files and message.file:
|
|
678
|
+
return
|
|
679
|
+
if not allow_stickers and message.sticker:
|
|
680
|
+
return
|
|
681
|
+
if not allow_polls and message.poll:
|
|
682
|
+
return
|
|
683
|
+
if not allow_contacts and message.contact_message:
|
|
684
|
+
return
|
|
685
|
+
if not allow_locations and (message.location or message.live_location):
|
|
686
|
+
return
|
|
687
|
+
if message.text:
|
|
688
|
+
text = message.text if case_sensitive else message.text.lower()
|
|
689
|
+
if min_text_length and len(message.text) < min_text_length:
|
|
690
|
+
return
|
|
691
|
+
if max_text_length and len(message.text) > max_text_length:
|
|
692
|
+
return
|
|
693
|
+
if contains and (contains if case_sensitive else contains.lower()) not in text:
|
|
694
|
+
return
|
|
695
|
+
if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
|
|
696
|
+
return
|
|
697
|
+
if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
|
|
698
|
+
return
|
|
699
|
+
if commands:
|
|
700
|
+
if not message.text:
|
|
701
|
+
return
|
|
702
|
+
parts = message.text.strip().split()
|
|
703
|
+
cmd = parts[0].lstrip("/")
|
|
704
|
+
if cmd not in commands:
|
|
705
|
+
return
|
|
706
|
+
message.args = parts[1:]
|
|
707
|
+
if filters and not filters(message):
|
|
708
|
+
return
|
|
709
|
+
return await func(bot, message)
|
|
710
|
+
self._message_handlers.append({
|
|
711
|
+
"func": wrapper,
|
|
712
|
+
"filters": filters,
|
|
713
|
+
"commands": commands,
|
|
714
|
+
"chat_id": chat_id,
|
|
715
|
+
"group_only": True,
|
|
716
|
+
"sender_id": sender_id,
|
|
717
|
+
"sender_type": sender_type
|
|
718
|
+
})
|
|
719
|
+
return wrapper
|
|
720
|
+
return decorator
|
|
721
|
+
def on_message_group(
|
|
722
|
+
self,
|
|
723
|
+
chat_id: Optional[Union[str, List[str]]] = None,
|
|
724
|
+
commands: Optional[List[str]] = None,
|
|
725
|
+
filters: Optional[Callable[[Message], bool]] = None,
|
|
726
|
+
sender_id: Optional[Union[str, List[str]]] = None,
|
|
727
|
+
sender_type: Optional[str] = None,
|
|
728
|
+
allow_forwarded: bool = True,
|
|
729
|
+
allow_files: bool = True,
|
|
730
|
+
allow_stickers: bool = True,
|
|
731
|
+
allow_polls: bool = True,
|
|
732
|
+
allow_contacts: bool = True,
|
|
733
|
+
allow_locations: bool = True,
|
|
734
|
+
min_text_length: Optional[int] = None,
|
|
735
|
+
max_text_length: Optional[int] = None,
|
|
736
|
+
contains: Optional[str] = None,
|
|
737
|
+
startswith: Optional[str] = None,
|
|
738
|
+
endswith: Optional[str] = None,
|
|
739
|
+
case_sensitive: bool = False
|
|
740
|
+
):
|
|
741
|
+
"""
|
|
742
|
+
Advanced decorator for handling only group messages with extended filters.
|
|
743
|
+
"""
|
|
744
|
+
|
|
745
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
746
|
+
async def wrapper(bot, message: Message):
|
|
747
|
+
|
|
748
|
+
if not message.is_group:
|
|
749
|
+
return
|
|
750
|
+
if chat_id:
|
|
751
|
+
if isinstance(chat_id, str) and message.chat_id != chat_id:
|
|
752
|
+
return
|
|
753
|
+
if isinstance(chat_id, list) and message.chat_id not in chat_id:
|
|
754
|
+
return
|
|
755
|
+
if sender_id:
|
|
756
|
+
if isinstance(sender_id, str) and message.sender_id != sender_id:
|
|
757
|
+
return
|
|
758
|
+
if isinstance(sender_id, list) and message.sender_id not in sender_id:
|
|
759
|
+
return
|
|
760
|
+
if sender_type and message.sender_type != sender_type:
|
|
761
|
+
return
|
|
762
|
+
if not allow_forwarded and message.forwarded_from:
|
|
763
|
+
return
|
|
764
|
+
if not allow_files and message.file:
|
|
765
|
+
return
|
|
766
|
+
if not allow_stickers and message.sticker:
|
|
767
|
+
return
|
|
768
|
+
if not allow_polls and message.poll:
|
|
769
|
+
return
|
|
770
|
+
if not allow_contacts and message.contact_message:
|
|
771
|
+
return
|
|
772
|
+
if not allow_locations and (message.location or message.live_location):
|
|
773
|
+
return
|
|
774
|
+
if message.text:
|
|
775
|
+
text = message.text if case_sensitive else message.text.lower()
|
|
776
|
+
if min_text_length and len(message.text) < min_text_length:
|
|
777
|
+
return
|
|
778
|
+
if max_text_length and len(message.text) > max_text_length:
|
|
779
|
+
return
|
|
780
|
+
if contains and (contains if case_sensitive else contains.lower()) not in text:
|
|
781
|
+
return
|
|
782
|
+
if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
|
|
783
|
+
return
|
|
784
|
+
if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
|
|
785
|
+
return
|
|
786
|
+
if commands:
|
|
787
|
+
if not message.text:
|
|
788
|
+
return
|
|
789
|
+
parts = message.text.strip().split()
|
|
790
|
+
cmd = parts[0].lstrip("/")
|
|
791
|
+
if cmd not in commands:
|
|
792
|
+
return
|
|
793
|
+
message.args = parts[1:]
|
|
794
|
+
if filters and not filters(message):
|
|
795
|
+
return
|
|
796
|
+
return await func(bot, message)
|
|
797
|
+
self._message_handlers.append({
|
|
798
|
+
"func": wrapper,
|
|
799
|
+
"filters": filters,
|
|
800
|
+
"commands": commands,
|
|
801
|
+
"chat_id": chat_id,
|
|
802
|
+
"group_only": True,
|
|
803
|
+
"sender_id": sender_id,
|
|
804
|
+
"sender_type": sender_type
|
|
805
|
+
})
|
|
806
|
+
return wrapper
|
|
807
|
+
return decorator
|
|
808
|
+
def remove_handler(self, func: Callable):
|
|
809
|
+
"""
|
|
810
|
+
Remove a message handler by its original function reference.
|
|
811
|
+
"""
|
|
812
|
+
self._message_handlers = [
|
|
813
|
+
h for h in self._message_handlers if h["func"].__wrapped__ != func
|
|
814
|
+
]
|
|
815
|
+
def on_edited_message(
|
|
816
|
+
self,
|
|
817
|
+
filters: Optional[Callable[[Message], bool]] = None,
|
|
818
|
+
commands: Optional[List[str]] = None
|
|
819
|
+
):
|
|
820
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
821
|
+
async def wrapper(bot, message: Message):
|
|
822
|
+
if filters and not filters(message):
|
|
823
|
+
return
|
|
824
|
+
if commands:
|
|
825
|
+
if not message.is_command:
|
|
826
|
+
return
|
|
827
|
+
cmd = message.text.split()[0].lstrip("/")
|
|
828
|
+
if cmd not in commands:
|
|
829
|
+
return
|
|
830
|
+
return await func(bot, message)
|
|
831
|
+
|
|
832
|
+
self._edited_message_handlers.append({
|
|
833
|
+
"func": wrapper,
|
|
834
|
+
"filters": filters,
|
|
835
|
+
"commands": commands
|
|
836
|
+
})
|
|
837
|
+
return wrapper
|
|
838
|
+
return decorator
|
|
839
|
+
def on_message(
|
|
840
|
+
self,
|
|
841
|
+
filters: Optional[Callable[[Message], bool]] = None,
|
|
842
|
+
commands: Optional[List[str]] = None):
|
|
843
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
844
|
+
async def wrapper(bot, message: Message):
|
|
845
|
+
if filters and not filters(message):
|
|
846
|
+
return
|
|
847
|
+
if commands:
|
|
848
|
+
if not message.is_command:
|
|
849
|
+
return
|
|
850
|
+
cmd = message.text.split()[0].lstrip("/")
|
|
851
|
+
if cmd not in commands:
|
|
852
|
+
return
|
|
853
|
+
|
|
854
|
+
return await func(bot, message)
|
|
855
|
+
self._message_handlers.append({
|
|
856
|
+
"func": wrapper,
|
|
857
|
+
"filters": filters,
|
|
858
|
+
"commands": commands
|
|
859
|
+
})
|
|
860
|
+
self._edited_message_handlers.append({
|
|
861
|
+
"func": wrapper,
|
|
862
|
+
"filters": filters,
|
|
863
|
+
"commands": commands
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
return wrapper
|
|
867
|
+
return decorator
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def on_message_file(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
871
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
872
|
+
async def wrapper(bot, message: Message):
|
|
873
|
+
if not message.file:return
|
|
874
|
+
if filters and not filters(message):return
|
|
875
|
+
return await func(bot, message)
|
|
876
|
+
|
|
877
|
+
self._message_handlers.append({
|
|
878
|
+
"func": wrapper,
|
|
879
|
+
"filters": filters,
|
|
880
|
+
"file_only": True,
|
|
881
|
+
"commands": commands
|
|
882
|
+
})
|
|
883
|
+
return wrapper
|
|
884
|
+
return decorator
|
|
885
|
+
def on_message_forwarded(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
886
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
887
|
+
async def wrapper(bot, message: Message):
|
|
888
|
+
if not message.is_forwarded:return
|
|
889
|
+
if filters and not filters(message):return
|
|
890
|
+
return await func(bot, message)
|
|
891
|
+
|
|
892
|
+
self._message_handlers.append({
|
|
893
|
+
"func": wrapper,
|
|
894
|
+
"filters": filters,
|
|
895
|
+
"forwarded_only": True,
|
|
896
|
+
"commands": commands
|
|
897
|
+
})
|
|
898
|
+
return wrapper
|
|
899
|
+
return decorator
|
|
900
|
+
def on_message_reply(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
901
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
902
|
+
async def wrapper(bot, message: Message):
|
|
903
|
+
if not message.is_reply:return
|
|
904
|
+
if filters and not filters(message):return
|
|
905
|
+
return await func(bot, message)
|
|
906
|
+
|
|
907
|
+
self._message_handlers.append({
|
|
908
|
+
"func": wrapper,
|
|
909
|
+
"filters": filters,
|
|
910
|
+
"reply_only": True,
|
|
911
|
+
"commands": commands
|
|
912
|
+
})
|
|
913
|
+
return wrapper
|
|
914
|
+
return decorator
|
|
915
|
+
def on_message_text(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
916
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
917
|
+
async def wrapper(bot, message: Message):
|
|
918
|
+
if not message.text:return
|
|
919
|
+
if filters and not filters(message):return
|
|
920
|
+
return await func(bot, message)
|
|
921
|
+
|
|
922
|
+
self._message_handlers.append({
|
|
923
|
+
"func": wrapper,
|
|
924
|
+
"filters": filters,
|
|
925
|
+
"text_only": True,
|
|
926
|
+
"commands": commands
|
|
927
|
+
})
|
|
928
|
+
return wrapper
|
|
929
|
+
return decorator
|
|
930
|
+
def on_message_media(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
931
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
932
|
+
async def wrapper(bot, message: Message):
|
|
933
|
+
if not message.is_media:return
|
|
934
|
+
if filters and not filters(message):return
|
|
935
|
+
return await func(bot, message)
|
|
936
|
+
|
|
937
|
+
self._message_handlers.append({
|
|
938
|
+
"func": wrapper,
|
|
939
|
+
"filters": filters,
|
|
940
|
+
"media_only": True,
|
|
941
|
+
"commands": commands
|
|
942
|
+
})
|
|
943
|
+
return wrapper
|
|
944
|
+
return decorator
|
|
945
|
+
def on_message_sticker(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
946
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
947
|
+
async def wrapper(bot, message: Message):
|
|
948
|
+
if not message.sticker:return
|
|
949
|
+
if filters and not filters(message):return
|
|
950
|
+
return await func(bot, message)
|
|
951
|
+
|
|
952
|
+
self._message_handlers.append({
|
|
953
|
+
"func": wrapper,
|
|
954
|
+
"filters": filters,
|
|
955
|
+
"sticker_only": True,
|
|
956
|
+
"commands": commands
|
|
957
|
+
})
|
|
958
|
+
return wrapper
|
|
959
|
+
return decorator
|
|
960
|
+
def on_message_contact(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
961
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
962
|
+
async def wrapper(bot, message: Message):
|
|
963
|
+
if not message.is_contact:return
|
|
964
|
+
if filters and not filters(message):return
|
|
965
|
+
return await func(bot, message)
|
|
966
|
+
|
|
967
|
+
self._message_handlers.append({
|
|
968
|
+
"func": wrapper,
|
|
969
|
+
"filters": filters,
|
|
970
|
+
"contact_only": True,
|
|
971
|
+
"commands": commands
|
|
972
|
+
})
|
|
973
|
+
return wrapper
|
|
974
|
+
return decorator
|
|
975
|
+
def on_message_location(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
976
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
977
|
+
async def wrapper(bot, message: Message):
|
|
978
|
+
if not message.is_location:return
|
|
979
|
+
if filters and not filters(message):return
|
|
980
|
+
return await func(bot, message)
|
|
981
|
+
|
|
982
|
+
self._message_handlers.append({
|
|
983
|
+
"func": wrapper,
|
|
984
|
+
"filters": filters,
|
|
985
|
+
"location_only": True,
|
|
986
|
+
"commands": commands
|
|
987
|
+
})
|
|
988
|
+
return wrapper
|
|
989
|
+
return decorator
|
|
990
|
+
def on_message_poll(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
991
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
992
|
+
async def wrapper(bot, message: Message):
|
|
993
|
+
if not message.is_poll:return
|
|
994
|
+
if filters and not filters(message):return
|
|
995
|
+
return await func(bot, message)
|
|
996
|
+
|
|
997
|
+
self._message_handlers.append({
|
|
998
|
+
"func": wrapper,
|
|
999
|
+
"filters": filters,
|
|
1000
|
+
"poll_only": True,
|
|
1001
|
+
"commands": commands
|
|
1002
|
+
})
|
|
1003
|
+
return wrapper
|
|
1004
|
+
return decorator
|
|
1005
|
+
def on_update(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
|
|
1006
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
1007
|
+
self._message_handlers.append({
|
|
1008
|
+
"func": func,
|
|
1009
|
+
"filters": filters,
|
|
1010
|
+
"commands": commands
|
|
1011
|
+
})
|
|
1012
|
+
return func
|
|
1013
|
+
return decorator
|
|
1014
|
+
|
|
1015
|
+
def on_callback(self, button_id: Optional[str] = None):
|
|
1016
|
+
def decorator(func: Callable[[Any, Union[Message, InlineMessage]], None]):
|
|
1017
|
+
if not hasattr(self, "_callback_handlers"):
|
|
1018
|
+
self._callback_handlers = []
|
|
1019
|
+
self._callback_handlers.append({
|
|
1020
|
+
"func": func,
|
|
1021
|
+
"button_id": button_id
|
|
1022
|
+
})
|
|
1023
|
+
return func
|
|
1024
|
+
return decorator
|
|
1025
|
+
def on_callback_query(self, button_id: Optional[str] = None):
|
|
1026
|
+
def decorator(func: Callable[[Any, Union[Message, InlineMessage]], None]):
|
|
1027
|
+
if not hasattr(self, "_callback_handlers"):
|
|
1028
|
+
self._callback_handlers = []
|
|
1029
|
+
self._callback_handlers.append({
|
|
1030
|
+
"func": func,
|
|
1031
|
+
"button_id": button_id
|
|
1032
|
+
})
|
|
1033
|
+
return func
|
|
1034
|
+
return decorator
|
|
1035
|
+
def callback_query_handler(self, button_id: Optional[str] = None):
|
|
1036
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
1037
|
+
if not hasattr(self, "_callback_handlers"):
|
|
1038
|
+
self._callback_handlers = []
|
|
1039
|
+
self._callback_handlers.append({
|
|
1040
|
+
"func": func,
|
|
1041
|
+
"button_id": button_id
|
|
1042
|
+
})
|
|
1043
|
+
return func
|
|
1044
|
+
return decorator
|
|
1045
|
+
def callback_query(self, button_id: Optional[str] = None):
|
|
1046
|
+
def decorator(func: Callable[[Any, Message], None]):
|
|
1047
|
+
if not hasattr(self, "_callback_handlers"):
|
|
1048
|
+
self._callback_handlers = []
|
|
1049
|
+
self._callback_handlers.append({
|
|
1050
|
+
"func": func,
|
|
1051
|
+
"button_id": button_id
|
|
1052
|
+
})
|
|
1053
|
+
return func
|
|
1054
|
+
return decorator
|
|
1055
|
+
|
|
1056
|
+
async def _handle_inline_query(self, inline_message: InlineMessage):
|
|
1057
|
+
aux_button_id = inline_message.aux_data.button_id if inline_message.aux_data else None
|
|
1058
|
+
for handler in self._inline_query_handlers:
|
|
1059
|
+
if handler["button_id"] is None or handler["button_id"] == aux_button_id:
|
|
1060
|
+
try:
|
|
1061
|
+
await handler["func"](self, inline_message)
|
|
1062
|
+
except Exception as e:
|
|
1063
|
+
raise Exception(f"Error in inline query handler: {e}")
|
|
1064
|
+
|
|
1065
|
+
def on_inline_query(self, button_id: Optional[str] = None):
|
|
1066
|
+
def decorator(func: Callable[[Any, InlineMessage], None]):
|
|
1067
|
+
self._inline_query_handlers.append({
|
|
1068
|
+
"func": func,
|
|
1069
|
+
"button_id": button_id
|
|
1070
|
+
})
|
|
1071
|
+
return func
|
|
1072
|
+
return decorator
|
|
1073
|
+
def on_inline_query_prefix(self, prefix: str, button_id: Optional[str] = None):
|
|
1074
|
+
if not prefix.startswith('/'):
|
|
1075
|
+
prefix = '/' + prefix
|
|
1076
|
+
def decorator(func: Callable[[Any, InlineMessage], None]):
|
|
1077
|
+
async def handler_wrapper(bot_instance, inline_message: InlineMessage):
|
|
1078
|
+
if not inline_message.raw_data or 'text' not in inline_message.raw_data:
|
|
1079
|
+
return
|
|
1080
|
+
query_text = inline_message.raw_data['text']
|
|
1081
|
+
if query_text.startswith(prefix):
|
|
1082
|
+
try:
|
|
1083
|
+
await func(bot_instance, inline_message)
|
|
1084
|
+
except Exception as e:
|
|
1085
|
+
raise Exception(f"Error in inline query prefix handler '{prefix}': {e}")
|
|
1086
|
+
self._inline_query_handlers.append({
|
|
1087
|
+
"func": handler_wrapper,
|
|
1088
|
+
"button_id": button_id
|
|
1089
|
+
})
|
|
1090
|
+
return func
|
|
1091
|
+
return decorator
|
|
1092
|
+
async def _process_update(self, update: dict):
|
|
1093
|
+
if update.get("type") == "ReceiveQuery":
|
|
1094
|
+
msg = update.get("inline_message", {})
|
|
1095
|
+
context = InlineMessage(bot=self, raw_data=msg)
|
|
1096
|
+
if hasattr(self, "_callback_handlers"):
|
|
1097
|
+
for handler in self._callback_handlers:
|
|
1098
|
+
if not handler["button_id"] or getattr(context.aux_data, "button_id", None) == handler["button_id"]:
|
|
1099
|
+
asyncio.create_task(handler["func"](self, context))
|
|
1100
|
+
asyncio.create_task(self._handle_inline_query(context))
|
|
1101
|
+
return
|
|
1102
|
+
|
|
1103
|
+
if update.get("type") == "NewMessage":
|
|
1104
|
+
msg = update.get("new_message", {})
|
|
1105
|
+
try:
|
|
1106
|
+
if msg.get("time") and (time.time() - float(msg["time"])) > 20:return
|
|
1107
|
+
except (ValueError, TypeError):return
|
|
1108
|
+
context = Message(bot=self,
|
|
1109
|
+
chat_id=update.get("chat_id"),
|
|
1110
|
+
message_id=msg.get("message_id"),
|
|
1111
|
+
sender_id=msg.get("sender_id"),
|
|
1112
|
+
text=msg.get("text"),
|
|
1113
|
+
raw_data=msg)
|
|
1114
|
+
if context.aux_data and self._callback_handlers:
|
|
1115
|
+
for handler in self._callback_handlers:
|
|
1116
|
+
if not handler["button_id"] or context.aux_data.button_id == handler["button_id"]:
|
|
1117
|
+
asyncio.create_task(handler["func"](self, context))
|
|
1118
|
+
return
|
|
1119
|
+
if self._message_handlers:
|
|
1120
|
+
for handler_info in self._message_handlers:
|
|
1121
|
+
|
|
1122
|
+
if handler_info["commands"]:
|
|
1123
|
+
if not context.text or not context.text.startswith("/"):
|
|
1124
|
+
continue
|
|
1125
|
+
parts = context.text.split()
|
|
1126
|
+
cmd = parts[0][1:]
|
|
1127
|
+
if cmd not in handler_info["commands"]:
|
|
1128
|
+
continue
|
|
1129
|
+
context.args = parts[1:]
|
|
1130
|
+
if handler_info["filters"]:
|
|
1131
|
+
if not handler_info["filters"](context):
|
|
1132
|
+
continue
|
|
1133
|
+
if not handler_info["commands"] and not handler_info["filters"]:
|
|
1134
|
+
asyncio.create_task(handler_info["func"](self, context))
|
|
1135
|
+
continue
|
|
1136
|
+
if handler_info["commands"] or handler_info["filters"]:
|
|
1137
|
+
asyncio.create_task(handler_info["func"](self, context))#kir baba kir
|
|
1138
|
+
continue
|
|
1139
|
+
elif update.get("type") == "UpdatedMessage":
|
|
1140
|
+
msg = update.get("updated_message", {})
|
|
1141
|
+
if not msg:
|
|
1142
|
+
return
|
|
1143
|
+
|
|
1144
|
+
context = Message(
|
|
1145
|
+
bot=self,
|
|
1146
|
+
chat_id=update.get("chat_id"),
|
|
1147
|
+
message_id=msg.get("message_id"),
|
|
1148
|
+
text=msg.get("text"),
|
|
1149
|
+
sender_id=msg.get("sender_id"),
|
|
1150
|
+
raw_data=msg
|
|
1151
|
+
)
|
|
1152
|
+
if self._edited_message_handlers:
|
|
1153
|
+
for handler_info in self._edited_message_handlers:
|
|
1154
|
+
if handler_info["commands"]:
|
|
1155
|
+
if not context.text or not context.text.startswith("/"):
|
|
1156
|
+
continue
|
|
1157
|
+
parts = context.text.split()
|
|
1158
|
+
cmd = parts[0][1:]
|
|
1159
|
+
if cmd not in handler_info["commands"]:
|
|
1160
|
+
continue
|
|
1161
|
+
context.args = parts[1:]
|
|
1162
|
+
if handler_info["filters"]:
|
|
1163
|
+
if not handler_info["filters"](context):
|
|
1164
|
+
continue
|
|
1165
|
+
asyncio.create_task(handler_info["func"](self, context))
|
|
1166
|
+
|
|
1167
|
+
async def get_updates(self, offset_id: Optional[str] = None, limit: Optional[int] = None) -> Dict[str, Any]:
|
|
1168
|
+
data = {}
|
|
1169
|
+
if offset_id: data["offset_id"] = offset_id
|
|
1170
|
+
if limit: data["limit"] = limit
|
|
1171
|
+
return await self._post("getUpdates", data)
|
|
1172
|
+
|
|
1173
|
+
async def update_webhook(self, offset_id: Optional[str] = None, limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
1174
|
+
session = await self._get_session()
|
|
1175
|
+
params = {}
|
|
1176
|
+
if offset_id: params['offset_id'] = offset_id
|
|
1177
|
+
if limit: params['limit'] = limit
|
|
1178
|
+
async with session.get(self.web_hook, params=params) as response:
|
|
1179
|
+
response.raise_for_status()
|
|
1180
|
+
return await response.json()
|
|
1181
|
+
|
|
1182
|
+
def _is_duplicate(self, key: str, max_age_sec: int = 300) -> bool:
|
|
1183
|
+
now = time.time()
|
|
1184
|
+
expired = [mid for mid, ts in self._processed_message_ids.items() if now - ts > max_age_sec]
|
|
1185
|
+
for mid in expired:
|
|
1186
|
+
del self._processed_message_ids[mid]
|
|
1187
|
+
if key in self._processed_message_ids:
|
|
1188
|
+
return True
|
|
1189
|
+
self._processed_message_ids[key] = now
|
|
1190
|
+
if len(self._processed_message_ids) > self._max_cache_size:
|
|
1191
|
+
self._processed_message_ids.popitem(last=False)
|
|
1192
|
+
return False
|
|
1193
|
+
|
|
1194
|
+
async def run_progelry(
|
|
1195
|
+
self,
|
|
1196
|
+
debug: bool = False,
|
|
1197
|
+
sleep_time: float = 0.1,
|
|
1198
|
+
webhook_timeout: int = 20,
|
|
1199
|
+
update_limit: int = 100,
|
|
1200
|
+
retry_delay: float = 5.0,
|
|
1201
|
+
stop_on_error: bool = False,
|
|
1202
|
+
max_errors: int = 0,
|
|
1203
|
+
auto_restart: bool = False,
|
|
1204
|
+
max_runtime: Optional[float] = None,
|
|
1205
|
+
loop_forever: bool = True,
|
|
1206
|
+
allowed_update_types: Optional[List[str]] = None,
|
|
1207
|
+
ignore_duplicate_messages: bool = True,
|
|
1208
|
+
skip_inline_queries: bool = False,
|
|
1209
|
+
skip_channel_posts: bool = False,
|
|
1210
|
+
skip_service_messages: bool = False,
|
|
1211
|
+
skip_edited_messages: bool = False,
|
|
1212
|
+
skip_bot_messages: bool = False,
|
|
1213
|
+
log_file: Optional[str] = None,
|
|
1214
|
+
log_level: str = "info",
|
|
1215
|
+
print_exceptions: bool = True,
|
|
1216
|
+
error_handler: Optional[Callable[[Exception], Any]] = None,
|
|
1217
|
+
shutdown_hook: Optional[Callable[[], Any]] = None,
|
|
1218
|
+
save_unprocessed_updates: bool = False,
|
|
1219
|
+
log_to_console: bool = True,
|
|
1220
|
+
rate_limit: Optional[float] = None,
|
|
1221
|
+
max_message_size: Optional[int] = None,
|
|
1222
|
+
ignore_users: Optional[Set[str]] = None,
|
|
1223
|
+
ignore_groups: Optional[Set[str]] = None,
|
|
1224
|
+
require_auth_token: bool = False,
|
|
1225
|
+
only_private_chats: bool = False,
|
|
1226
|
+
only_groups: bool = False,
|
|
1227
|
+
require_admin_rights: bool = False,
|
|
1228
|
+
custom_update_fetcher: Optional[Callable[[], Any]] = None,
|
|
1229
|
+
custom_update_processor: Optional[Callable[[Any], Any]] = None,
|
|
1230
|
+
process_in_background: bool = False,
|
|
1231
|
+
max_queue_size: int = 1000,
|
|
1232
|
+
thread_workers: int = 3,
|
|
1233
|
+
message_filter: Optional[Callable[[Any], bool]] = None,
|
|
1234
|
+
pause_on_idle: bool = False,
|
|
1235
|
+
max_concurrent_tasks: Optional[int] = None,
|
|
1236
|
+
metrics_enabled: bool = False,
|
|
1237
|
+
metrics_handler: Optional[Callable[[dict], Any]] = None,
|
|
1238
|
+
notify_on_error: bool = False,
|
|
1239
|
+
notification_handler: Optional[Callable[[str], Any]] = None,
|
|
1240
|
+
watchdog_timeout: Optional[float] = None,
|
|
1241
|
+
):
|
|
1242
|
+
"""
|
|
1243
|
+
Starts the bot's main execution loop with extensive configuration options.
|
|
1244
|
+
|
|
1245
|
+
This function handles:
|
|
1246
|
+
- Update fetching and processing with optional filters for types and sources.
|
|
1247
|
+
- Error handling, retry mechanisms, and automatic restart options.
|
|
1248
|
+
- Logging to console and/or files with configurable log levels.
|
|
1249
|
+
- Message filtering based on users, groups, chat types, admin rights, and more.
|
|
1250
|
+
- Custom update fetchers and processors for advanced use cases.
|
|
1251
|
+
- Background processing with threading and task concurrency controls.
|
|
1252
|
+
- Metrics collection and error notifications.
|
|
1253
|
+
- Optional runtime limits, sleep delays, rate limiting, and watchdog monitoring.
|
|
1254
|
+
|
|
1255
|
+
Parameters
|
|
1256
|
+
----------
|
|
1257
|
+
debug : bool
|
|
1258
|
+
Enable debug mode for detailed logging and runtime checks.
|
|
1259
|
+
sleep_time : float
|
|
1260
|
+
Delay between update fetch cycles (seconds).
|
|
1261
|
+
webhook_timeout : int
|
|
1262
|
+
Timeout for webhook requests (seconds).
|
|
1263
|
+
update_limit : int
|
|
1264
|
+
Maximum updates to fetch per request.
|
|
1265
|
+
retry_delay : float
|
|
1266
|
+
Delay before retrying after failure (seconds).
|
|
1267
|
+
stop_on_error : bool
|
|
1268
|
+
Stop bot on unhandled errors.
|
|
1269
|
+
max_errors : int
|
|
1270
|
+
Maximum consecutive errors before stopping (0 = unlimited).
|
|
1271
|
+
auto_restart : bool
|
|
1272
|
+
Automatically restart the bot if it stops unexpectedly.
|
|
1273
|
+
max_runtime : float | None
|
|
1274
|
+
Maximum runtime in seconds before stopping.
|
|
1275
|
+
loop_forever : bool
|
|
1276
|
+
Keep the bot running continuously.
|
|
1277
|
+
|
|
1278
|
+
allowed_update_types : list[str] | None
|
|
1279
|
+
Limit processing to specific update types.
|
|
1280
|
+
ignore_duplicate_messages : bool
|
|
1281
|
+
Skip identical messages.
|
|
1282
|
+
skip_inline_queries : bool
|
|
1283
|
+
Ignore inline query updates.
|
|
1284
|
+
skip_channel_posts : bool
|
|
1285
|
+
Ignore channel post updates.
|
|
1286
|
+
skip_service_messages : bool
|
|
1287
|
+
Ignore service messages.
|
|
1288
|
+
skip_edited_messages : bool
|
|
1289
|
+
Ignore edited messages.
|
|
1290
|
+
skip_bot_messages : bool
|
|
1291
|
+
Ignore messages from other bots.
|
|
1292
|
+
|
|
1293
|
+
log_file : str | None
|
|
1294
|
+
File path for logging.
|
|
1295
|
+
log_level : str
|
|
1296
|
+
Logging level (debug, info, warning, error).
|
|
1297
|
+
print_exceptions : bool
|
|
1298
|
+
Print exceptions to console.
|
|
1299
|
+
error_handler : callable | None
|
|
1300
|
+
Custom function to handle errors.
|
|
1301
|
+
shutdown_hook : callable | None
|
|
1302
|
+
Function to execute on shutdown.
|
|
1303
|
+
save_unprocessed_updates : bool
|
|
1304
|
+
Save updates that failed processing.
|
|
1305
|
+
log_to_console : bool
|
|
1306
|
+
Enable/disable console logging.
|
|
1307
|
+
|
|
1308
|
+
rate_limit : float | None
|
|
1309
|
+
Minimum delay between processing updates from the same user/group.
|
|
1310
|
+
max_message_size : int | None
|
|
1311
|
+
Maximum allowed message size.
|
|
1312
|
+
ignore_users : set[str] | None
|
|
1313
|
+
User IDs to ignore.
|
|
1314
|
+
ignore_groups : set[str] | None
|
|
1315
|
+
Group IDs to ignore.
|
|
1316
|
+
require_auth_token : bool
|
|
1317
|
+
Require users to provide authentication token.
|
|
1318
|
+
only_private_chats : bool
|
|
1319
|
+
Process only private chats.
|
|
1320
|
+
only_groups : bool
|
|
1321
|
+
Process only group chats.
|
|
1322
|
+
require_admin_rights : bool
|
|
1323
|
+
Process only if sender is admin.
|
|
1324
|
+
|
|
1325
|
+
custom_update_fetcher : callable | None
|
|
1326
|
+
Custom update fetching function.
|
|
1327
|
+
custom_update_processor : callable | None
|
|
1328
|
+
Custom update processing function.
|
|
1329
|
+
process_in_background : bool
|
|
1330
|
+
Run processing in background threads.
|
|
1331
|
+
max_queue_size : int
|
|
1332
|
+
Maximum updates in processing queue.
|
|
1333
|
+
thread_workers : int
|
|
1334
|
+
Number of background worker threads.
|
|
1335
|
+
message_filter : callable | None
|
|
1336
|
+
Function to filter messages.
|
|
1337
|
+
pause_on_idle : bool
|
|
1338
|
+
Pause processing if idle.
|
|
1339
|
+
max_concurrent_tasks : int | None
|
|
1340
|
+
Maximum concurrent processing tasks.
|
|
1341
|
+
|
|
1342
|
+
metrics_enabled : bool
|
|
1343
|
+
Enable metrics collection.
|
|
1344
|
+
metrics_handler : callable | None
|
|
1345
|
+
Function to handle metrics.
|
|
1346
|
+
notify_on_error : bool
|
|
1347
|
+
Send notifications on errors.
|
|
1348
|
+
notification_handler : callable | None
|
|
1349
|
+
Function to send error notifications.
|
|
1350
|
+
watchdog_timeout : float | None
|
|
1351
|
+
Maximum idle time before triggering watchdog restart.
|
|
1352
|
+
"""
|
|
1353
|
+
import asyncio, time, datetime, traceback
|
|
1354
|
+
from collections import deque
|
|
1355
|
+
def _log(msg: str, level: str = "info"):
|
|
1356
|
+
level_order = {"debug": 10, "info": 20, "warning": 30, "error": 40}
|
|
1357
|
+
if level not in level_order:
|
|
1358
|
+
level = "info"
|
|
1359
|
+
if level_order[level] < level_order.get(log_level, 20):
|
|
1360
|
+
return
|
|
1361
|
+
line = f"[{level.upper()}] {datetime.datetime.now().isoformat()} - {msg}"
|
|
1362
|
+
if log_to_console:
|
|
1363
|
+
print(msg)
|
|
1364
|
+
if log_file:
|
|
1365
|
+
try:
|
|
1366
|
+
with open(log_file, "a", encoding="utf-8") as f:
|
|
1367
|
+
f.write(line + "\n")
|
|
1368
|
+
except Exception:
|
|
1369
|
+
pass
|
|
1370
|
+
|
|
1371
|
+
def _get_sender_and_chat(update: dict):
|
|
1372
|
+
|
|
1373
|
+
sender = None
|
|
1374
|
+
chat = None
|
|
1375
|
+
t = update.get("type")
|
|
1376
|
+
if t == "NewMessage":
|
|
1377
|
+
nm = update.get("new_message", {})
|
|
1378
|
+
sender = nm.get("author_object_guid") or nm.get("author_guid") or nm.get("from_id")
|
|
1379
|
+
chat = nm.get("object_guid") or nm.get("chat_id")
|
|
1380
|
+
elif t == "ReceiveQuery":
|
|
1381
|
+
im = update.get("inline_message", {})
|
|
1382
|
+
sender = im.get("author_object_guid") or im.get("author_guid")
|
|
1383
|
+
chat = im.get("object_guid") or im.get("chat_id")
|
|
1384
|
+
elif t == "UpdatedMessage":
|
|
1385
|
+
im = update.get("updated_message", {})
|
|
1386
|
+
sender = im.get("author_object_guid") or im.get("author_guid")
|
|
1387
|
+
chat = im.get("object_guid") or im.get("chat_id")
|
|
1388
|
+
else:
|
|
1389
|
+
sender = update.get("author_guid") or update.get("from_id")
|
|
1390
|
+
chat = update.get("object_guid") or update.get("chat_id")
|
|
1391
|
+
return str(sender) if sender is not None else None, str(chat) if chat is not None else None
|
|
1392
|
+
|
|
1393
|
+
def _is_group_chat(chat_guid: Optional[str]) -> Optional[bool]:
|
|
1394
|
+
|
|
1395
|
+
if chat_guid is None:
|
|
1396
|
+
return None
|
|
1397
|
+
if hasattr(self, "_is_group_chat") and callable(getattr(self, "_is_group_chat")):
|
|
1398
|
+
try:
|
|
1399
|
+
return bool(self._is_group_chat(chat_guid))
|
|
1400
|
+
except Exception:
|
|
1401
|
+
return None
|
|
1402
|
+
return None
|
|
1403
|
+
|
|
1404
|
+
async def _maybe_notify(err: Exception, context: dict):
|
|
1405
|
+
if notify_on_error and notification_handler:
|
|
1406
|
+
try:
|
|
1407
|
+
if asyncio.iscoroutinefunction(notification_handler):
|
|
1408
|
+
await notification_handler(err, context)
|
|
1409
|
+
else:
|
|
1410
|
+
|
|
1411
|
+
notification_handler(err, context)
|
|
1412
|
+
except Exception:
|
|
1413
|
+
pass
|
|
1414
|
+
|
|
1415
|
+
async def _handle_error(err: Exception, context: dict):
|
|
1416
|
+
if print_exceptions:
|
|
1417
|
+
_log("Exception occurred:\n" + "".join(traceback.format_exception(type(err), err, err.__traceback__)), "error")
|
|
1418
|
+
else:
|
|
1419
|
+
_log(f"Exception occurred: {err}", "error")
|
|
1420
|
+
await _maybe_notify(err, context)
|
|
1421
|
+
if error_handler:
|
|
1422
|
+
try:
|
|
1423
|
+
if asyncio.iscoroutinefunction(error_handler):
|
|
1424
|
+
await error_handler(err, context)
|
|
1425
|
+
else:
|
|
1426
|
+
error_handler(err, context)
|
|
1427
|
+
except Exception as e2:
|
|
1428
|
+
_log(f"Error in error_handler: {e2}", "error")
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
rate_window = deque()
|
|
1432
|
+
def _rate_ok():
|
|
1433
|
+
if rate_limit is None or rate_limit <= 0:
|
|
1434
|
+
return True
|
|
1435
|
+
now = time.time()
|
|
1436
|
+
|
|
1437
|
+
while rate_window and now - rate_window[0] > 1.0:
|
|
1438
|
+
rate_window.popleft()
|
|
1439
|
+
if len(rate_window) < int(rate_limit):
|
|
1440
|
+
rate_window.append(now)
|
|
1441
|
+
return True
|
|
1442
|
+
return False
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
queue = asyncio.Queue(maxsize=max_queue_size) if process_in_background else None
|
|
1446
|
+
active_workers = []
|
|
1447
|
+
|
|
1448
|
+
sem = asyncio.Semaphore(max_concurrent_tasks) if max_concurrent_tasks and max_concurrent_tasks > 0 else None
|
|
1449
|
+
|
|
1450
|
+
async def _process(update: dict):
|
|
1451
|
+
|
|
1452
|
+
if allowed_update_types and update.get("type") not in allowed_update_types:
|
|
1453
|
+
return False
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
t = update.get("type")
|
|
1457
|
+
if skip_inline_queries and t == "ReceiveQuery":
|
|
1458
|
+
return False
|
|
1459
|
+
if skip_service_messages and t == "ServiceMessage":
|
|
1460
|
+
return False
|
|
1461
|
+
if skip_channel_posts and t == "ChannelPost":
|
|
1462
|
+
return False
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
sender, chat = _get_sender_and_chat(update)
|
|
1466
|
+
if ignore_users and sender and sender in ignore_users:
|
|
1467
|
+
return False
|
|
1468
|
+
if ignore_groups and chat and chat in ignore_groups:
|
|
1469
|
+
return False
|
|
1470
|
+
if require_auth_token and not getattr(self, "_has_auth_token", False):
|
|
1471
|
+
return False
|
|
1472
|
+
if only_private_chats:
|
|
1473
|
+
is_group = _is_group_chat(chat)
|
|
1474
|
+
if is_group is True:
|
|
1475
|
+
return False
|
|
1476
|
+
if only_groups:
|
|
1477
|
+
is_group = _is_group_chat(chat)
|
|
1478
|
+
if is_group is False:
|
|
1479
|
+
return False
|
|
1480
|
+
if skip_bot_messages and getattr(self, "_is_bot_guid", None) and sender == self._is_bot_guid:
|
|
1481
|
+
return False
|
|
1482
|
+
|
|
1483
|
+
if max_message_size is not None and max_message_size > 0:
|
|
1484
|
+
|
|
1485
|
+
content = None
|
|
1486
|
+
if t == "NewMessage":
|
|
1487
|
+
content = (update.get("new_message") or {}).get("text")
|
|
1488
|
+
elif t == "ReceiveQuery":
|
|
1489
|
+
content = (update.get("inline_message") or {}).get("text")
|
|
1490
|
+
elif t == "UpdatedMessage":
|
|
1491
|
+
content = (update.get("updated_message") or {}).get("text")
|
|
1492
|
+
elif "text" in update:
|
|
1493
|
+
content = update.get("text")
|
|
1494
|
+
if content and isinstance(content, str) and len(content) > max_message_size:
|
|
1495
|
+
return False
|
|
1496
|
+
|
|
1497
|
+
if message_filter:
|
|
1498
|
+
try:
|
|
1499
|
+
if not message_filter(update):
|
|
1500
|
+
return False
|
|
1501
|
+
except Exception:
|
|
1502
|
+
|
|
1503
|
+
pass
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
if not _rate_ok():
|
|
1507
|
+
return False
|
|
1508
|
+
|
|
1509
|
+
|
|
1510
|
+
if custom_update_processor:
|
|
1511
|
+
if asyncio.iscoroutinefunction(custom_update_processor):
|
|
1512
|
+
await custom_update_processor(update)
|
|
1513
|
+
else:
|
|
1514
|
+
|
|
1515
|
+
await asyncio.get_running_loop().run_in_executor(None, custom_update_processor, update)
|
|
1516
|
+
else:
|
|
1517
|
+
|
|
1518
|
+
await self._process_update(update)
|
|
1519
|
+
return True
|
|
1520
|
+
|
|
1521
|
+
async def _worker():
|
|
1522
|
+
while True:
|
|
1523
|
+
update = await queue.get()
|
|
1524
|
+
try:
|
|
1525
|
+
if sem:
|
|
1526
|
+
async with sem:
|
|
1527
|
+
await _process(update)
|
|
1528
|
+
else:
|
|
1529
|
+
await _process(update)
|
|
1530
|
+
except Exception as e:
|
|
1531
|
+
await _handle_error(e, {"stage": "worker_process", "update": update})
|
|
1532
|
+
finally:
|
|
1533
|
+
queue.task_done()
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
start_ts = time.time()
|
|
1537
|
+
error_count = 0
|
|
1538
|
+
last_loop_tick = time.time()
|
|
1539
|
+
processed_count = 0
|
|
1540
|
+
skipped_count = 0
|
|
1541
|
+
enqueued_count = 0
|
|
1542
|
+
unprocessed_storage = []
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
if process_in_background:
|
|
1546
|
+
n_workers = max(1, int(thread_workers))
|
|
1547
|
+
for _ in range(n_workers):
|
|
1548
|
+
active_workers.append(asyncio.create_task(_worker()))
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
await check_rubka_version()
|
|
1552
|
+
await self._initialize_webhook()
|
|
1553
|
+
await self.geteToken()
|
|
1554
|
+
_log("Bot is up and running...", "info")
|
|
1555
|
+
|
|
1556
|
+
try:
|
|
1557
|
+
while True:
|
|
1558
|
+
try:
|
|
1559
|
+
|
|
1560
|
+
if max_runtime is not None and (time.time() - start_ts) >= max_runtime:
|
|
1561
|
+
_log("Max runtime reached. Stopping loop.", "warning")
|
|
1562
|
+
break
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
now = time.time()
|
|
1566
|
+
if watchdog_timeout and (now - last_loop_tick) > watchdog_timeout:
|
|
1567
|
+
_log(f"Watchdog triggered (> {watchdog_timeout}s)", "warning")
|
|
1568
|
+
if auto_restart:
|
|
1569
|
+
break
|
|
1570
|
+
last_loop_tick = now
|
|
1571
|
+
|
|
1572
|
+
|
|
1573
|
+
received_updates = None
|
|
1574
|
+
if custom_update_fetcher:
|
|
1575
|
+
received_updates = await custom_update_fetcher()
|
|
1576
|
+
elif self.web_hook:
|
|
1577
|
+
webhook_data = await self.update_webhook()
|
|
1578
|
+
received_updates = []
|
|
1579
|
+
if isinstance(webhook_data, list):
|
|
1580
|
+
for item in webhook_data:
|
|
1581
|
+
data = item.get("data", {})
|
|
1582
|
+
|
|
1583
|
+
received_at_str = item.get("received_at")
|
|
1584
|
+
if received_at_str:
|
|
1585
|
+
try:
|
|
1586
|
+
received_at_ts = datetime.datetime.strptime(received_at_str, "%Y-%m-%d %H:%M:%S").timestamp()
|
|
1587
|
+
if time.time() - received_at_ts > webhook_timeout:
|
|
1588
|
+
if debug:
|
|
1589
|
+
_log(f"Skipped old webhook update ({received_at_str})", "debug")
|
|
1590
|
+
continue
|
|
1591
|
+
except (ValueError, TypeError):
|
|
1592
|
+
pass
|
|
1593
|
+
|
|
1594
|
+
update = None
|
|
1595
|
+
if "update" in data:
|
|
1596
|
+
update = data["update"]
|
|
1597
|
+
elif "inline_message" in data:
|
|
1598
|
+
update = {"type": "ReceiveQuery", "inline_message": data["inline_message"]}
|
|
1599
|
+
else:
|
|
1600
|
+
continue
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
message_id = None
|
|
1604
|
+
if update.get("type") == "NewMessage":
|
|
1605
|
+
message_id = update.get("new_message", {}).get("message_id")
|
|
1606
|
+
elif update.get("type") == "ReceiveQuery":
|
|
1607
|
+
message_id = update.get("inline_message", {}).get("message_id")
|
|
1608
|
+
elif update.get("type") == "UpdatedMessage":
|
|
1609
|
+
message_id = update.get("updated_message", {}).get("message_id")
|
|
1610
|
+
elif "message_id" in update:
|
|
1611
|
+
message_id = update.get("message_id")
|
|
1612
|
+
|
|
1613
|
+
|
|
1614
|
+
dup_ok = True
|
|
1615
|
+
if ignore_duplicate_messages:
|
|
1616
|
+
key = str(received_at_str) if received_at_str else str(message_id)
|
|
1617
|
+
dup_ok = (not self._is_duplicate(str(key))) if key else True
|
|
1618
|
+
|
|
1619
|
+
if message_id and dup_ok:
|
|
1620
|
+
received_updates.append(update)
|
|
1621
|
+
else:
|
|
1622
|
+
get_updates_response = await self.get_updates(offset_id=self._offset_id, limit=update_limit)
|
|
1623
|
+
received_updates = []
|
|
1624
|
+
if get_updates_response and get_updates_response.get("data"):
|
|
1625
|
+
updates = get_updates_response["data"].get("updates", [])
|
|
1626
|
+
self._offset_id = get_updates_response["data"].get("next_offset_id", self._offset_id)
|
|
1627
|
+
for update in updates:
|
|
1628
|
+
message_id = None
|
|
1629
|
+
if update.get("type") == "NewMessage":
|
|
1630
|
+
msg_data = update.get("new_message", {})
|
|
1631
|
+
message_id = msg_data.get("message_id")
|
|
1632
|
+
text_content = msg_data.get("text", "")
|
|
1633
|
+
msg_time = int(msg_data.get("time", 0))
|
|
1634
|
+
elif update.get("type") == "ReceiveQuery":
|
|
1635
|
+
msg_data = update.get("inline_message", {})
|
|
1636
|
+
message_id = msg_data.get("message_id")
|
|
1637
|
+
text_content = msg_data.get("text", "")
|
|
1638
|
+
msg_time = int(msg_data.get("time", 0))
|
|
1639
|
+
elif update.get("type") == "UpdatedMessage":
|
|
1640
|
+
msg_data = update.get("updated_message", {})
|
|
1641
|
+
message_id = msg_data.get("message_id")
|
|
1642
|
+
text_content = msg_data.get("text", "")
|
|
1643
|
+
msg_time = int(msg_data.get("time", 0))
|
|
1644
|
+
elif "message_id" in update:
|
|
1645
|
+
message_id = update.get("message_id")
|
|
1646
|
+
else:
|
|
1647
|
+
msg_time = time.time()
|
|
1648
|
+
msg_data = update.get("updated_message", {})
|
|
1649
|
+
message_id = msg_data.get("message_id")
|
|
1650
|
+
text_content = msg_data.get("text", "")
|
|
1651
|
+
now = int(time.time())
|
|
1652
|
+
|
|
1653
|
+
if msg_time and (now - msg_time > self.max_msg_age):
|
|
1654
|
+
continue
|
|
1655
|
+
dup_ok = True
|
|
1656
|
+
if ignore_duplicate_messages and message_id:
|
|
1657
|
+
dup_key = self._make_dup_key(message_id, update.get("type", ""), msg_data)
|
|
1658
|
+
dup_ok = not self._is_duplicate(dup_key)
|
|
1659
|
+
if message_id and dup_ok:
|
|
1660
|
+
received_updates.append(update)
|
|
1661
|
+
if not received_updates:
|
|
1662
|
+
if pause_on_idle and sleep_time == 0:await asyncio.sleep(0.005)
|
|
1663
|
+
else:await asyncio.sleep(sleep_time)
|
|
1664
|
+
if not loop_forever and max_runtime is None:break
|
|
1665
|
+
continue
|
|
1666
|
+
|
|
1667
|
+
|
|
1668
|
+
for update in received_updates:
|
|
1669
|
+
if require_admin_rights:
|
|
1670
|
+
|
|
1671
|
+
sender, _ = _get_sender_and_chat(update)
|
|
1672
|
+
if hasattr(self, "is_admin") and callable(getattr(self, "is_admin")):
|
|
1673
|
+
try:
|
|
1674
|
+
if not await self.is_admin(sender) if asyncio.iscoroutinefunction(self.is_admin) else not self.is_admin(sender):
|
|
1675
|
+
skipped_count += 1
|
|
1676
|
+
continue
|
|
1677
|
+
except Exception:
|
|
1678
|
+
pass
|
|
1679
|
+
|
|
1680
|
+
if process_in_background:
|
|
1681
|
+
try:
|
|
1682
|
+
queue.put_nowait(update)
|
|
1683
|
+
enqueued_count += 1
|
|
1684
|
+
except asyncio.QueueFull:
|
|
1685
|
+
|
|
1686
|
+
skipped_count += 1
|
|
1687
|
+
if save_unprocessed_updates:
|
|
1688
|
+
unprocessed_storage.append(update)
|
|
1689
|
+
else:
|
|
1690
|
+
try:
|
|
1691
|
+
if sem:
|
|
1692
|
+
async with sem:
|
|
1693
|
+
ok = await _process(update)
|
|
1694
|
+
else:
|
|
1695
|
+
ok = await _process(update)
|
|
1696
|
+
processed_count += 1 if ok else 0
|
|
1697
|
+
skipped_count += 0 if ok else 1
|
|
1698
|
+
except Exception as e:
|
|
1699
|
+
await _handle_error(e, {"stage": "inline_process", "update": update})
|
|
1700
|
+
error_count += 1
|
|
1701
|
+
if save_unprocessed_updates:
|
|
1702
|
+
unprocessed_storage.append(update)
|
|
1703
|
+
if stop_on_error or (max_errors and error_count >= max_errors):
|
|
1704
|
+
raise
|
|
1705
|
+
|
|
1706
|
+
|
|
1707
|
+
if process_in_background and queue.qsize() > 0:
|
|
1708
|
+
await asyncio.sleep(0)
|
|
1709
|
+
|
|
1710
|
+
|
|
1711
|
+
if debug:
|
|
1712
|
+
_log(f"Loop stats — processed: {processed_count}, enqueued: {enqueued_count}, skipped: {skipped_count}, queue: {queue.qsize() if queue else 0}", "debug")
|
|
1713
|
+
|
|
1714
|
+
|
|
1715
|
+
await asyncio.sleep(sleep_time)
|
|
1716
|
+
|
|
1717
|
+
except Exception as e:
|
|
1718
|
+
await _handle_error(e, {"stage": "run_loop"})
|
|
1719
|
+
error_count += 1
|
|
1720
|
+
if stop_on_error or (max_errors and error_count >= max_errors):
|
|
1721
|
+
break
|
|
1722
|
+
await asyncio.sleep(retry_delay)
|
|
1723
|
+
|
|
1724
|
+
|
|
1725
|
+
if not loop_forever and max_runtime is None:
|
|
1726
|
+
break
|
|
1727
|
+
|
|
1728
|
+
finally:
|
|
1729
|
+
|
|
1730
|
+
if process_in_background and queue:
|
|
1731
|
+
try:
|
|
1732
|
+
await queue.join()
|
|
1733
|
+
except Exception:
|
|
1734
|
+
pass
|
|
1735
|
+
for w in active_workers:
|
|
1736
|
+
w.cancel()
|
|
1737
|
+
|
|
1738
|
+
for w in active_workers:
|
|
1739
|
+
try:
|
|
1740
|
+
await w
|
|
1741
|
+
except Exception:
|
|
1742
|
+
pass
|
|
1743
|
+
|
|
1744
|
+
|
|
1745
|
+
if self._aiohttp_session:
|
|
1746
|
+
await self._aiohttp_session.close()
|
|
1747
|
+
|
|
1748
|
+
|
|
1749
|
+
stats = {
|
|
1750
|
+
"processed": processed_count,
|
|
1751
|
+
"skipped": skipped_count,
|
|
1752
|
+
"enqueued": enqueued_count,
|
|
1753
|
+
"errors": error_count,
|
|
1754
|
+
"uptime_sec": round(time.time() - start_ts, 3),
|
|
1755
|
+
}
|
|
1756
|
+
if metrics_enabled and metrics_handler:
|
|
1757
|
+
try:
|
|
1758
|
+
if asyncio.iscoroutinefunction(metrics_handler):
|
|
1759
|
+
await metrics_handler(stats)
|
|
1760
|
+
else:
|
|
1761
|
+
metrics_handler(stats)
|
|
1762
|
+
except Exception:
|
|
1763
|
+
pass
|
|
1764
|
+
|
|
1765
|
+
if shutdown_hook:
|
|
1766
|
+
try:
|
|
1767
|
+
if asyncio.iscoroutinefunction(shutdown_hook):
|
|
1768
|
+
await shutdown_hook(stats)
|
|
1769
|
+
else:
|
|
1770
|
+
shutdown_hook(stats)
|
|
1771
|
+
except Exception:
|
|
1772
|
+
pass
|
|
1773
|
+
|
|
1774
|
+
print("Bot stopped and session closed.")
|
|
1775
|
+
|
|
1776
|
+
|
|
1777
|
+
if auto_restart:
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
_log("Auto-restart requested. You can call run(...) again as needed.", "warning")
|
|
1781
|
+
def run(self, sleep_time: float = 0.1, *args, **kwargs):
|
|
1782
|
+
print("Connecting to the server...")
|
|
1783
|
+
try:
|
|
1784
|
+
loop = asyncio.get_running_loop()
|
|
1785
|
+
return loop.create_task(self.run_progelry(sleep_time=sleep_time, *args, **kwargs))
|
|
1786
|
+
except RuntimeError:return asyncio.run(self.run_progelry(sleep_time=sleep_time, *args, **kwargs))
|
|
1787
|
+
async def _delete_after_task(self, chat_id: str, message_id: str, delay: int):
|
|
1788
|
+
try:
|
|
1789
|
+
await asyncio.sleep(delay)
|
|
1790
|
+
await self.delete_message(chat_id=chat_id, message_id=message_id)
|
|
1791
|
+
except Exception:
|
|
1792
|
+
return False
|
|
1793
|
+
async def _edit_after_task(self, chat_id: str, message_id: str, text:str, delay: int):
|
|
1794
|
+
try:
|
|
1795
|
+
await asyncio.sleep(delay)
|
|
1796
|
+
await self.edit_message_text(chat_id=chat_id, message_id=message_id,text=text)
|
|
1797
|
+
except Exception:
|
|
1798
|
+
return False
|
|
1799
|
+
|
|
1800
|
+
async def delete_after(self, chat_id: str, message_id: str, delay: int = 30) -> asyncio.Task:
|
|
1801
|
+
async def _task():
|
|
1802
|
+
await asyncio.sleep(delay)
|
|
1803
|
+
try:
|
|
1804
|
+
await self.delete_message(chat_id, message_id)
|
|
1805
|
+
except Exception:
|
|
1806
|
+
pass
|
|
1807
|
+
|
|
1808
|
+
try:
|
|
1809
|
+
loop = asyncio.get_running_loop()
|
|
1810
|
+
except RuntimeError:
|
|
1811
|
+
loop = asyncio.new_event_loop()
|
|
1812
|
+
asyncio.set_event_loop(loop)
|
|
1813
|
+
|
|
1814
|
+
task = loop.create_task(_task())
|
|
1815
|
+
return task
|
|
1816
|
+
|
|
1817
|
+
async def edit_after(self, chat_id: str, message_id: str, text : str, delay: int = 30) -> asyncio.Task:
|
|
1818
|
+
async def _task():
|
|
1819
|
+
await asyncio.sleep(delay)
|
|
1820
|
+
try:
|
|
1821
|
+
await self.edit_message_text(chat_id, message_id,text)
|
|
1822
|
+
except Exception:
|
|
1823
|
+
pass
|
|
1824
|
+
|
|
1825
|
+
try:
|
|
1826
|
+
loop = asyncio.get_running_loop()
|
|
1827
|
+
except RuntimeError:
|
|
1828
|
+
loop = asyncio.new_event_loop()
|
|
1829
|
+
asyncio.set_event_loop(loop)
|
|
1830
|
+
|
|
1831
|
+
task = loop.create_task(_task())
|
|
1832
|
+
return task
|
|
1833
|
+
def _parse_text_metadata(self, text: str, parse_mode: str):
|
|
1834
|
+
formatter = GlyphWeaver()
|
|
1835
|
+
parsed = formatter.parse(text, parse_mode)
|
|
1836
|
+
return parsed.get("text"), parsed.get("metadata")
|
|
1837
|
+
|
|
1838
|
+
async def send_message(
|
|
1839
|
+
self,
|
|
1840
|
+
chat_id: str,
|
|
1841
|
+
text: str,
|
|
1842
|
+
chat_keypad: Optional[Dict[str, Any]] = None,
|
|
1843
|
+
inline_keypad: Optional[Dict[str, Any]] = None,
|
|
1844
|
+
disable_notification: bool = False,
|
|
1845
|
+
reply_to_message_id: Optional[str] = None,
|
|
1846
|
+
chat_keypad_type: Optional[Literal["New", "Remove"]] = None,
|
|
1847
|
+
delete_after: Optional[int] = None,
|
|
1848
|
+
parse_mode: Optional[Literal["HTML", "Markdown"]] = None,
|
|
1849
|
+
meta_data:Optional[dict] = None
|
|
1850
|
+
) -> Dict[str, Any]:
|
|
1851
|
+
payload = {
|
|
1852
|
+
"chat_id": chat_id,
|
|
1853
|
+
"text": text,
|
|
1854
|
+
"disable_notification": disable_notification,
|
|
1855
|
+
}
|
|
1856
|
+
if not meta_data:
|
|
1857
|
+
parse_mode_to_use = parse_mode or self.parse_mode
|
|
1858
|
+
if text:
|
|
1859
|
+
text, metadata = self._parse_text_metadata(text, parse_mode_to_use)
|
|
1860
|
+
payload["text"] = text
|
|
1861
|
+
if metadata:
|
|
1862
|
+
payload["metadata"] = metadata
|
|
1863
|
+
else :
|
|
1864
|
+
payload["metadata"] = meta_data
|
|
1865
|
+
if chat_keypad:
|
|
1866
|
+
payload["chat_keypad"] = chat_keypad
|
|
1867
|
+
payload["chat_keypad_type"] = chat_keypad_type or "New"
|
|
1868
|
+
if inline_keypad:
|
|
1869
|
+
payload["inline_keypad"] = inline_keypad
|
|
1870
|
+
if reply_to_message_id:
|
|
1871
|
+
payload["reply_to_message_id"] = reply_to_message_id
|
|
1872
|
+
try:
|
|
1873
|
+
state = await self._post("sendMessage", payload)
|
|
1874
|
+
except Exception:
|
|
1875
|
+
if self.safeSendMode and reply_to_message_id:
|
|
1876
|
+
payload.pop("reply_to_message_id", None)
|
|
1877
|
+
state = await self._post("sendMessage", payload)
|
|
1878
|
+
else:
|
|
1879
|
+
raise
|
|
1880
|
+
if delete_after:
|
|
1881
|
+
await self.delete_after(chat_id, state.message_id, delete_after)
|
|
1882
|
+
return state
|
|
1883
|
+
|
|
1884
|
+
|
|
1885
|
+
async def send_sticker(
|
|
1886
|
+
self,
|
|
1887
|
+
chat_id: str,
|
|
1888
|
+
sticker_id: str,
|
|
1889
|
+
chat_keypad: Optional[Dict[str, Any]] = None,
|
|
1890
|
+
disable_notification: bool = False,
|
|
1891
|
+
inline_keypad: Optional[Dict[str, Any]] = None,
|
|
1892
|
+
reply_to_message_id: Optional[str] = None,
|
|
1893
|
+
chat_keypad_type: Optional[Literal['New', 'Remove']] = None,
|
|
1894
|
+
) -> str:
|
|
1895
|
+
"""
|
|
1896
|
+
Send a sticker to a chat.
|
|
1897
|
+
|
|
1898
|
+
Args:
|
|
1899
|
+
token: Bot token.
|
|
1900
|
+
chat_id: Target chat ID.
|
|
1901
|
+
sticker_id: ID of the sticker to send.
|
|
1902
|
+
chat_keypad: Optional chat keypad data.
|
|
1903
|
+
disable_notification: If True, disables notification.
|
|
1904
|
+
inline_keypad: Optional inline keyboard data.
|
|
1905
|
+
reply_to_message_id: Optional message ID to reply to.
|
|
1906
|
+
chat_keypad_type: Type of chat keypad change ('New' or 'Remove').
|
|
1907
|
+
|
|
1908
|
+
Returns:
|
|
1909
|
+
API response as a string.
|
|
1910
|
+
"""
|
|
1911
|
+
data = {
|
|
1912
|
+
'chat_id': chat_id,
|
|
1913
|
+
'sticker_id': sticker_id,
|
|
1914
|
+
'chat_keypad': chat_keypad,
|
|
1915
|
+
'disable_notification': disable_notification,
|
|
1916
|
+
'inline_keypad': inline_keypad,
|
|
1917
|
+
'reply_to_message_id': reply_to_message_id,
|
|
1918
|
+
'chat_keypad_type': chat_keypad_type,
|
|
1919
|
+
}
|
|
1920
|
+
return await self._post("sendSticker", data)
|
|
1921
|
+
|
|
1922
|
+
|
|
1923
|
+
async def get_url_file(self,file_id):
|
|
1924
|
+
data = await self._post("getFile", {'file_id': file_id})
|
|
1925
|
+
return data.get("data").get("download_url")
|
|
1926
|
+
|
|
1927
|
+
def _get_client(self) -> Client_get:
|
|
1928
|
+
if self.session_name:
|
|
1929
|
+
return Client_get(self.session_name, self.auth, self.Key, self.platform)
|
|
1930
|
+
else:
|
|
1931
|
+
return Client_get(show_last_six_words(self.token), self.auth, self.Key, self.platform)
|
|
1932
|
+
async def send_button_join(
|
|
1933
|
+
self,
|
|
1934
|
+
chat_id,
|
|
1935
|
+
title_button : Union[str, list],
|
|
1936
|
+
username : Union[str, list],
|
|
1937
|
+
text,
|
|
1938
|
+
reply_to_message_id=None,
|
|
1939
|
+
id="None"):
|
|
1940
|
+
from .button import InlineBuilder
|
|
1941
|
+
builder = InlineBuilder()
|
|
1942
|
+
if isinstance(username, (list, tuple)) and isinstance(title_button, (list, tuple)):
|
|
1943
|
+
for t, u in zip(title_button, username):
|
|
1944
|
+
builder = builder.row(
|
|
1945
|
+
InlineBuilder().button_join_channel(
|
|
1946
|
+
text=t,
|
|
1947
|
+
id=id,
|
|
1948
|
+
username=u
|
|
1949
|
+
)
|
|
1950
|
+
)
|
|
1951
|
+
elif isinstance(username, (list, tuple)) and isinstance(title_button, str):
|
|
1952
|
+
for u in username:
|
|
1953
|
+
builder = builder.row(
|
|
1954
|
+
InlineBuilder().button_join_channel(
|
|
1955
|
+
text=title_button,
|
|
1956
|
+
id=id,
|
|
1957
|
+
username=u
|
|
1958
|
+
)
|
|
1959
|
+
)
|
|
1960
|
+
else:
|
|
1961
|
+
builder = builder.row(
|
|
1962
|
+
InlineBuilder().button_join_channel(
|
|
1963
|
+
text=title_button,
|
|
1964
|
+
id=id,
|
|
1965
|
+
username=username
|
|
1966
|
+
)
|
|
1967
|
+
)
|
|
1968
|
+
return await self.send_message(
|
|
1969
|
+
chat_id=chat_id,
|
|
1970
|
+
text=text,
|
|
1971
|
+
inline_keypad=builder.build(),
|
|
1972
|
+
reply_to_message_id=reply_to_message_id
|
|
1973
|
+
)
|
|
1974
|
+
async def send_button_link(
|
|
1975
|
+
self,
|
|
1976
|
+
chat_id,
|
|
1977
|
+
title_button: Union[str, list],
|
|
1978
|
+
url: Union[str, list],
|
|
1979
|
+
text,
|
|
1980
|
+
reply_to_message_id=None,
|
|
1981
|
+
id="None"
|
|
1982
|
+
):
|
|
1983
|
+
from .button import InlineBuilder
|
|
1984
|
+
builder = InlineBuilder()
|
|
1985
|
+
if isinstance(url, (list, tuple)) and isinstance(title_button, (list, tuple)):
|
|
1986
|
+
for t, u in zip(title_button, url):
|
|
1987
|
+
builder = builder.row(
|
|
1988
|
+
InlineBuilder().button_url_link(
|
|
1989
|
+
text=t,
|
|
1990
|
+
id=id,
|
|
1991
|
+
url=u
|
|
1992
|
+
)
|
|
1993
|
+
)
|
|
1994
|
+
elif isinstance(url, (list, tuple)) and isinstance(title_button, str):
|
|
1995
|
+
for u in url:
|
|
1996
|
+
builder = builder.row(
|
|
1997
|
+
InlineBuilder().button_url_link(
|
|
1998
|
+
text=title_button,
|
|
1999
|
+
id=id,
|
|
2000
|
+
url=u
|
|
2001
|
+
)
|
|
2002
|
+
)
|
|
2003
|
+
else:
|
|
2004
|
+
builder = builder.row(
|
|
2005
|
+
InlineBuilder().button_url_link(
|
|
2006
|
+
text=title_button,
|
|
2007
|
+
id=id,
|
|
2008
|
+
url=url
|
|
2009
|
+
)
|
|
2010
|
+
)
|
|
2011
|
+
return await self.send_message(
|
|
2012
|
+
chat_id=chat_id,
|
|
2013
|
+
text=text,
|
|
2014
|
+
inline_keypad=builder.build(),
|
|
2015
|
+
reply_to_message_id=reply_to_message_id
|
|
2016
|
+
)
|
|
2017
|
+
|
|
2018
|
+
async def close_poll(self, chat_id: str, message_id: str) -> Dict[str, Any]:
|
|
2019
|
+
return await self._post("closePoll", {"chat_id": chat_id, "message_id": message_id})
|
|
2020
|
+
async def send_location(self, chat_id: str, latitude: str, longitude: str, disable_notification: bool = False, inline_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, chat_keypad_type: Optional[Literal["New", "Remove"]] = None) -> Dict[str, Any]:
|
|
2021
|
+
payload = {"chat_id": chat_id, "latitude": latitude, "longitude": longitude, "disable_notification": disable_notification}
|
|
2022
|
+
if inline_keypad: payload["inline_keypad"] = inline_keypad
|
|
2023
|
+
if reply_to_message_id: payload["reply_to_message_id"] = reply_to_message_id
|
|
2024
|
+
if chat_keypad_type: payload["chat_keypad_type"] = chat_keypad_type
|
|
2025
|
+
return await self._post("sendLocation", {k: v for k, v in payload.items() if v is not None})
|
|
2026
|
+
async def upload_media_file(self, upload_url: str, name: str, path: Union[str, Path, bytes]) -> str:
|
|
2027
|
+
session = await self._get_session()
|
|
2028
|
+
is_temp_file = False
|
|
2029
|
+
if isinstance(path, str) and path.startswith("http"):
|
|
2030
|
+
async with session.get(path) as response:
|
|
2031
|
+
if response.status != 200:
|
|
2032
|
+
raise Exception(f"Failed to download file from URL ({response.status})")
|
|
2033
|
+
content = await response.read()
|
|
2034
|
+
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
|
2035
|
+
tmp.write(content)
|
|
2036
|
+
path = tmp.name
|
|
2037
|
+
is_temp_file = True
|
|
2038
|
+
elif isinstance(path, bytes):
|
|
2039
|
+
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
|
2040
|
+
tmp.write(path)
|
|
2041
|
+
path = tmp.name
|
|
2042
|
+
is_temp_file = True
|
|
2043
|
+
|
|
2044
|
+
file_size = os.path.getsize(path)
|
|
2045
|
+
chunk_size = self.chunk_size
|
|
2046
|
+
|
|
2047
|
+
progress_bar = tqdm(total=file_size, unit='B', unit_scale=True, unit_divisor=1024,
|
|
2048
|
+
desc=f'Upload : {name}', colour='blue', disable=not getattr(self, 'show_progress', True))
|
|
2049
|
+
|
|
2050
|
+
async def file_generator(file_path):
|
|
2051
|
+
async with aiofiles.open(file_path, 'rb') as f:
|
|
2052
|
+
while chunk := await f.read(chunk_size):
|
|
2053
|
+
progress_bar.update(len(chunk))
|
|
2054
|
+
yield chunk
|
|
2055
|
+
|
|
2056
|
+
form = aiohttp.FormData()
|
|
2057
|
+
form.add_field('file', file_generator(path), filename=name, content_type='application/octet-stream')
|
|
2058
|
+
try:
|
|
2059
|
+
async with session.post(upload_url, data=form, timeout=aiohttp.ClientTimeout(total=None)) as response:
|
|
2060
|
+
progress_bar.close()
|
|
2061
|
+
if response.status != 200:
|
|
2062
|
+
text = await response.text()
|
|
2063
|
+
raise Exception(f"Upload failed ({response.status}): {text}")
|
|
2064
|
+
return (await response.json()).get('data', {}).get('file_id')
|
|
2065
|
+
except Exception as e:
|
|
2066
|
+
raise FeatureNotAvailableError(f"File upload not supported : {e}")
|
|
2067
|
+
finally:
|
|
2068
|
+
if is_temp_file:
|
|
2069
|
+
os.remove(path)
|
|
2070
|
+
def get_extension(content_type: str) -> str:
|
|
2071
|
+
ext = mimetypes.guess_extension(content_type)
|
|
2072
|
+
return ext if ext else ''
|
|
2073
|
+
async def download(self, file_id: str, save_as: str = None, chunk_size: int = 1024 * 512,timeout_sec: int = 60, verbose: bool = False):
|
|
2074
|
+
"""
|
|
2075
|
+
Download a file from server using its file_id with chunked transfer,
|
|
2076
|
+
progress bar, file extension detection, custom filename, and timeout.
|
|
2077
|
+
|
|
2078
|
+
If save_as is not provided, filename will be extracted from
|
|
2079
|
+
Content-Disposition header or Content-Type header extension.
|
|
2080
|
+
|
|
2081
|
+
Parameters:
|
|
2082
|
+
file_id (str): The file ID to fetch the download URL.
|
|
2083
|
+
save_as (str, optional): Custom filename to save. If None, automatically detected.
|
|
2084
|
+
chunk_size (int, optional): Size of each chunk in bytes. Default 512KB.
|
|
2085
|
+
timeout_sec (int, optional): HTTP timeout in seconds. Default 60.
|
|
2086
|
+
verbose (bool, optional): Show progress messages. Default True.
|
|
2087
|
+
|
|
2088
|
+
Returns:
|
|
2089
|
+
bool: True if success, raises exceptions otherwise.
|
|
2090
|
+
"""
|
|
2091
|
+
try:
|
|
2092
|
+
url = await self.get_url_file(file_id)
|
|
2093
|
+
if not url:raise ValueError("Download URL not found in response.")
|
|
2094
|
+
except Exception as e:raise ValueError(f"Failed to get download URL: {e}")
|
|
2095
|
+
timeout = aiohttp.ClientTimeout(total=timeout_sec)
|
|
2096
|
+
try:
|
|
2097
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
2098
|
+
async with session.get(url) as resp:
|
|
2099
|
+
if resp.status != 200:
|
|
2100
|
+
raise aiohttp.ClientResponseError(
|
|
2101
|
+
request_info=resp.request_info,
|
|
2102
|
+
history=resp.history,
|
|
2103
|
+
status=resp.status,
|
|
2104
|
+
message="Failed to download file.",
|
|
2105
|
+
headers=resp.headers
|
|
2106
|
+
)
|
|
2107
|
+
if not save_as:
|
|
2108
|
+
content_disp = resp.headers.get("Content-Disposition", "")
|
|
2109
|
+
import re
|
|
2110
|
+
match = re.search(r'filename="?([^\";]+)"?', content_disp)
|
|
2111
|
+
if match:save_as = match.group(1)
|
|
2112
|
+
else:
|
|
2113
|
+
content_type = resp.headers.get("Content-Type", "").split(";")[0]
|
|
2114
|
+
extension = mimetypes.guess_extension(content_type) or ".bin"
|
|
2115
|
+
save_as = f"{file_id}{extension}"
|
|
2116
|
+
total_size = int(resp.headers.get("Content-Length", 0))
|
|
2117
|
+
progress = tqdm(total=total_size, unit="B", unit_scale=True, disable=not verbose)
|
|
2118
|
+
async with aiofiles.open(save_as, "wb") as f:
|
|
2119
|
+
async for chunk in resp.content.iter_chunked(chunk_size):
|
|
2120
|
+
await f.write(chunk)
|
|
2121
|
+
progress.update(len(chunk))
|
|
2122
|
+
|
|
2123
|
+
progress.close()
|
|
2124
|
+
if verbose:
|
|
2125
|
+
print(f"✅ File saved as: {save_as}")
|
|
2126
|
+
|
|
2127
|
+
return True
|
|
2128
|
+
|
|
2129
|
+
except aiohttp.ClientError as e:
|
|
2130
|
+
raise aiohttp.ClientError(f"HTTP error occurred: {e}")
|
|
2131
|
+
except asyncio.TimeoutError:
|
|
2132
|
+
raise asyncio.TimeoutError("Download timed out.")
|
|
2133
|
+
except Exception as e:
|
|
2134
|
+
raise Exception(f"Error downloading file: {e}")
|
|
2135
|
+
|
|
2136
|
+
except aiohttp.ClientError as e:
|
|
2137
|
+
raise aiohttp.ClientError(f"HTTP error occurred: {e}")
|
|
2138
|
+
except asyncio.TimeoutError:
|
|
2139
|
+
raise asyncio.TimeoutError("The download operation timed out.")
|
|
2140
|
+
except Exception as e:
|
|
2141
|
+
raise Exception(f"An error occurred while downloading the file: {e}")
|
|
2142
|
+
|
|
2143
|
+
except aiohttp.ClientError as e:
|
|
2144
|
+
raise aiohttp.ClientError(f"HTTP error occurred: {e}")
|
|
2145
|
+
except asyncio.TimeoutError:
|
|
2146
|
+
raise asyncio.TimeoutError("The download operation timed out.")
|
|
2147
|
+
except Exception as e:
|
|
2148
|
+
raise Exception(f"An error occurred while downloading the file: {e}")
|
|
2149
|
+
async def get_upload_url(self, media_type: Literal['File', 'Image', 'voice', 'Music', 'Gif', 'Video']) -> str:
|
|
2150
|
+
allowed = ['File', 'Image', 'voice', 'Music', 'Gif', 'Video']
|
|
2151
|
+
if media_type not in allowed:
|
|
2152
|
+
raise ValueError(f"Invalid media type. Must be one of {allowed}")
|
|
2153
|
+
result = await self._post("requestSendFile", {"type": media_type})
|
|
2154
|
+
return result.get("data", {}).get("upload_url")
|
|
2155
|
+
async 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", "Remove", "None"]] = "None",parse_mode: Optional[Literal["HTML", "Markdown"]] = None,meta_data:Optional[dict] = None) -> Dict[str, Any]:
|
|
2156
|
+
payload = {"chat_id": chat_id, "file_id": file_id, "text": text, "disable_notification": disable_notification, "chat_keypad_type": chat_keypad_type}
|
|
2157
|
+
if chat_keypad: payload["chat_keypad"] = chat_keypad
|
|
2158
|
+
if inline_keypad: payload["inline_keypad"] = inline_keypad
|
|
2159
|
+
if reply_to_message_id: payload["reply_to_message_id"] = str(reply_to_message_id)
|
|
2160
|
+
if not meta_data:
|
|
2161
|
+
parse_mode_to_use = parse_mode or self.parse_mode
|
|
2162
|
+
if text:
|
|
2163
|
+
text, metadata = self._parse_text_metadata(text, parse_mode_to_use)
|
|
2164
|
+
payload["text"] = text
|
|
2165
|
+
if metadata:
|
|
2166
|
+
payload["metadata"] = metadata
|
|
2167
|
+
else :
|
|
2168
|
+
payload["metadata"] = meta_data
|
|
2169
|
+
payload["time"] = "10"
|
|
2170
|
+
resp = await self._post("sendFile", payload)
|
|
2171
|
+
message_id_put = resp["data"]["message_id"]
|
|
2172
|
+
result = {
|
|
2173
|
+
"status": resp.get("status"),
|
|
2174
|
+
"status_det": resp.get("status_det"),
|
|
2175
|
+
"file_id": file_id,
|
|
2176
|
+
"text":text,
|
|
2177
|
+
"message_id": message_id_put,
|
|
2178
|
+
"send_to_chat_id": chat_id,
|
|
2179
|
+
"reply_to_message_id": reply_to_message_id,
|
|
2180
|
+
"disable_notification": disable_notification,
|
|
2181
|
+
"type_file": type_file,
|
|
2182
|
+
"raw_response": resp,
|
|
2183
|
+
"chat_keypad":chat_keypad,
|
|
2184
|
+
"inline_keypad":inline_keypad,
|
|
2185
|
+
"chat_keypad_type":chat_keypad_type
|
|
2186
|
+
}
|
|
2187
|
+
return AttrDict(result)
|
|
2188
|
+
async def _send_file_generic(self, media_type, chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type,parse_mode: Optional[Literal["HTML", "Markdown"]] = None,meta_data:Optional[dict] = None):
|
|
2189
|
+
if path:
|
|
2190
|
+
file_name = file_name or Path(path).name
|
|
2191
|
+
upload_url = await self.get_upload_url(media_type)
|
|
2192
|
+
file_id = await self.upload_media_file(upload_url, file_name, path)
|
|
2193
|
+
if not file_id:
|
|
2194
|
+
raise ValueError("Either path or file_id must be provided.")
|
|
2195
|
+
return await self._send_uploaded_file(chat_id=chat_id, file_id=file_id, text=text, inline_keypad=inline_keypad, chat_keypad=chat_keypad, reply_to_message_id=reply_to_message_id, disable_notification=disable_notification, chat_keypad_type=chat_keypad_type,type_file=media_type,parse_mode=parse_mode,meta_data=meta_data)
|
|
2196
|
+
async def send_document(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",parse_mode: Optional[Literal["HTML", "Markdown"]] = None,meta_data:Optional[dict] = None) -> Dict[str, Any]:
|
|
2197
|
+
return await self._send_file_generic("File", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type,parse_mode=parse_mode,meta_data=meta_data)
|
|
2198
|
+
async def send_file(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, caption: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",parse_mode: Optional[Literal["HTML", "Markdown"]] = None,meta_data:Optional[dict] = None) -> Dict[str, Any]:
|
|
2199
|
+
return await self._send_file_generic("File", chat_id, path, file_id, caption, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type,parse_mode=parse_mode,meta_data=meta_data)
|
|
2200
|
+
async def re_send(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, caption: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",parse_mode: Optional[Literal["HTML", "Markdown"]] = None,meta_data:Optional[dict] = None) -> Dict[str, Any]:
|
|
2201
|
+
return await self._send_file_generic("File", chat_id, path, file_id, caption, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type,parse_mode=parse_mode,meta_data=meta_data)
|
|
2202
|
+
async def send_video(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",parse_mode: Optional[Literal["HTML", "Markdown"]] = None,meta_data:Optional[dict] = None) -> Dict[str, Any]:
|
|
2203
|
+
return await self._send_file_generic("Video", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type,parse_mode=parse_mode,meta_data=meta_data)
|
|
2204
|
+
async def send_voice(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",parse_mode: Optional[Literal["HTML", "Markdown"]] = None,meta_data:Optional[dict] = None) -> Dict[str, Any]:
|
|
2205
|
+
return await self._send_file_generic("voice", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type,parse_mode=parse_mode,meta_data=meta_data)
|
|
2206
|
+
async def send_image(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",parse_mode: Optional[Literal["HTML", "Markdown"]] = None,meta_data:Optional[dict] = None) -> Dict[str, Any]:
|
|
2207
|
+
return await self._send_file_generic("Image", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type,parse_mode=parse_mode,meta_data=meta_data)
|
|
2208
|
+
async def send_music(
|
|
2209
|
+
self,
|
|
2210
|
+
chat_id: str,
|
|
2211
|
+
path: Optional[Union[str, Path]] = None,
|
|
2212
|
+
file_id: Optional[str] = None,
|
|
2213
|
+
text: Optional[str] = None,
|
|
2214
|
+
file_name: Optional[str] = None,
|
|
2215
|
+
inline_keypad: Optional[Dict[str, Any]] = None,
|
|
2216
|
+
chat_keypad: Optional[Dict[str, Any]] = None,
|
|
2217
|
+
reply_to_message_id: Optional[str] = None,
|
|
2218
|
+
disable_notification: bool = False,
|
|
2219
|
+
chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",
|
|
2220
|
+
parse_mode: Optional[Literal["HTML", "Markdown"]] = None,
|
|
2221
|
+
meta_data:Optional[dict] = None
|
|
2222
|
+
) -> Dict[str, Any]:
|
|
2223
|
+
valid_extensions = {"ogg", "oga", "opus", "flac"}
|
|
2224
|
+
extension = "flac"
|
|
2225
|
+
if path:
|
|
2226
|
+
path_str = str(path)
|
|
2227
|
+
if path_str.startswith("http://") or path_str.startswith("https://"):
|
|
2228
|
+
parsed = urlparse(path_str)
|
|
2229
|
+
base_name = os.path.basename(parsed.path)
|
|
2230
|
+
else:
|
|
2231
|
+
base_name = os.path.basename(path_str)
|
|
2232
|
+
name, ext = os.path.splitext(base_name)
|
|
2233
|
+
|
|
2234
|
+
if file_name is None or not file_name.strip():
|
|
2235
|
+
file_name = name or "music"
|
|
2236
|
+
ext = ext.lower().replace(".", "")
|
|
2237
|
+
if ext in valid_extensions:
|
|
2238
|
+
extension = ext
|
|
2239
|
+
else:
|
|
2240
|
+
if file_name is None:
|
|
2241
|
+
file_name = "music"
|
|
2242
|
+
return await self._send_file_generic(
|
|
2243
|
+
"File",
|
|
2244
|
+
chat_id,
|
|
2245
|
+
path,
|
|
2246
|
+
file_id,
|
|
2247
|
+
text,
|
|
2248
|
+
f"{file_name}.{extension}",
|
|
2249
|
+
inline_keypad,
|
|
2250
|
+
chat_keypad,
|
|
2251
|
+
reply_to_message_id,
|
|
2252
|
+
disable_notification,
|
|
2253
|
+
chat_keypad_type,
|
|
2254
|
+
parse_mode=parse_mode,
|
|
2255
|
+
meta_data=meta_data
|
|
2256
|
+
)
|
|
2257
|
+
async def send_gif(
|
|
2258
|
+
self,
|
|
2259
|
+
chat_id: str,
|
|
2260
|
+
path: Optional[Union[str, Path]] = None,
|
|
2261
|
+
file_id: Optional[str] = None,
|
|
2262
|
+
text: Optional[str] = None,
|
|
2263
|
+
file_name: Optional[str] = None,
|
|
2264
|
+
inline_keypad: Optional[Dict[str, Any]] = None,
|
|
2265
|
+
chat_keypad: Optional[Dict[str, Any]] = None,
|
|
2266
|
+
reply_to_message_id: Optional[str] = None,
|
|
2267
|
+
disable_notification: bool = False,
|
|
2268
|
+
chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",
|
|
2269
|
+
parse_mode: Optional[Literal["HTML", "Markdown"]] = None,
|
|
2270
|
+
meta_data:Optional[dict] = None
|
|
2271
|
+
) -> Dict[str, Any]:
|
|
2272
|
+
valid_extensions = {"gif"}
|
|
2273
|
+
extension = "gif"
|
|
2274
|
+
if path:
|
|
2275
|
+
path_str = str(path)
|
|
2276
|
+
if path_str.startswith("http://") or path_str.startswith("https://"):
|
|
2277
|
+
parsed = urlparse(path_str)
|
|
2278
|
+
base_name = os.path.basename(parsed.path)
|
|
2279
|
+
else:
|
|
2280
|
+
base_name = os.path.basename(path_str)
|
|
2281
|
+
name, ext = os.path.splitext(base_name)
|
|
2282
|
+
|
|
2283
|
+
if file_name is None or not file_name.strip():
|
|
2284
|
+
file_name = name or "gif"
|
|
2285
|
+
ext = ext.lower().replace(".", "")
|
|
2286
|
+
if ext in valid_extensions:
|
|
2287
|
+
extension = ext
|
|
2288
|
+
else:
|
|
2289
|
+
if file_name is None:
|
|
2290
|
+
file_name = "gif"
|
|
2291
|
+
return await self._send_file_generic(
|
|
2292
|
+
"File",
|
|
2293
|
+
chat_id,
|
|
2294
|
+
path,
|
|
2295
|
+
file_id,
|
|
2296
|
+
text,
|
|
2297
|
+
f"{file_name}.{extension}",
|
|
2298
|
+
inline_keypad,
|
|
2299
|
+
chat_keypad,
|
|
2300
|
+
reply_to_message_id,
|
|
2301
|
+
disable_notification,
|
|
2302
|
+
chat_keypad_type,
|
|
2303
|
+
parse_mode=parse_mode,meta_data=meta_data
|
|
2304
|
+
)
|
|
2305
|
+
|
|
2306
|
+
async def get_avatar_me(self, save_as: str = None) -> str:
|
|
2307
|
+
session = None
|
|
2308
|
+
try:
|
|
2309
|
+
me_info = await self.get_me()
|
|
2310
|
+
avatar = me_info.get('data', {}).get('bot', {}).get('avatar', {})
|
|
2311
|
+
file_id = avatar.get('file_id')
|
|
2312
|
+
if not file_id:
|
|
2313
|
+
return "null"
|
|
2314
|
+
|
|
2315
|
+
file_info = await self.get_url_file(file_id)
|
|
2316
|
+
url = file_info.get("download_url") if isinstance(file_info, dict) else file_info
|
|
2317
|
+
|
|
2318
|
+
if save_as:
|
|
2319
|
+
session = aiohttp.ClientSession()
|
|
2320
|
+
async with session.get(url) as resp:
|
|
2321
|
+
if resp.status == 200:
|
|
2322
|
+
content = await resp.read()
|
|
2323
|
+
with open(save_as, "wb") as f:
|
|
2324
|
+
f.write(content)
|
|
2325
|
+
|
|
2326
|
+
return url
|
|
2327
|
+
except Exception as e:
|
|
2328
|
+
print(f"[get_avatar_me] Error: {e}")
|
|
2329
|
+
return "null"
|
|
2330
|
+
finally:
|
|
2331
|
+
if session and not session.closed:
|
|
2332
|
+
await session.close()
|
|
2333
|
+
|
|
2334
|
+
async def get_name(self, chat_id: str) -> str:
|
|
2335
|
+
try:
|
|
2336
|
+
chat = await self.get_chat(chat_id)
|
|
2337
|
+
chat_info = chat.get("data", {}).get("chat", {})
|
|
2338
|
+
chat_type = chat_info.get("chat_type", "").lower()
|
|
2339
|
+
if chat_type == "user":
|
|
2340
|
+
first_name = chat_info.get("first_name", "")
|
|
2341
|
+
last_name = chat_info.get("last_name", "")
|
|
2342
|
+
full_name = f"{first_name} {last_name}".strip()
|
|
2343
|
+
return full_name if full_name else "null"
|
|
2344
|
+
elif chat_type in ["group", "channel"]:
|
|
2345
|
+
title = chat_info.get("title", "")
|
|
2346
|
+
return title if title else "null"
|
|
2347
|
+
else:return "null"
|
|
2348
|
+
except Exception:return "null"
|
|
2349
|
+
async def get_username(self, chat_id: str) -> str:
|
|
2350
|
+
chat_info = await self.get_chat(chat_id)
|
|
2351
|
+
return chat_info.get("data", {}).get("chat", {}).get("username", "None")
|
|
2352
|
+
async def send_bulk_message(
|
|
2353
|
+
self,
|
|
2354
|
+
chat_ids: List[str],
|
|
2355
|
+
text: str,
|
|
2356
|
+
concurrency: int = 5,
|
|
2357
|
+
delay_between: float = 0.0,
|
|
2358
|
+
log_errors: bool = True,
|
|
2359
|
+
**kwargs
|
|
2360
|
+
) -> Dict[str, Optional[Dict]]:
|
|
2361
|
+
if not chat_ids:return {}
|
|
2362
|
+
semaphore = asyncio.Semaphore(concurrency)
|
|
2363
|
+
results: Dict[str, Optional[Dict]] = {}
|
|
2364
|
+
async def _send(chat_id: str):
|
|
2365
|
+
async with semaphore:
|
|
2366
|
+
try:
|
|
2367
|
+
res = await self.send_message(chat_id, text, **kwargs)
|
|
2368
|
+
results[chat_id] = res
|
|
2369
|
+
except Exception as e:
|
|
2370
|
+
results[chat_id] = None
|
|
2371
|
+
if log_errors:print(f"[send_bulk_message] Error {chat_id} : {e}")
|
|
2372
|
+
if delay_between > 0:await asyncio.sleep(delay_between)
|
|
2373
|
+
await asyncio.gather(*[_send(cid) for cid in chat_ids])
|
|
2374
|
+
return results
|
|
2375
|
+
async def delete_bulk_message(self, chat_id: str, message_ids: list[str]):
|
|
2376
|
+
tasks = [self.delete_message(chat_id, mid) for mid in message_ids]
|
|
2377
|
+
return await asyncio.gather(*tasks, return_exceptions=True)
|
|
2378
|
+
async def edit_bulk_message(self, chat_id: str, messages: dict[str, str]):
|
|
2379
|
+
tasks = [self.edit_message_text(chat_id, mid, new_text) for mid, new_text in messages.items()]
|
|
2380
|
+
return await asyncio.gather(*tasks, return_exceptions=True)
|
|
2381
|
+
async def send_scheduled_message(self, chat_id: str, text: str, delay: int, **kwargs):
|
|
2382
|
+
await asyncio.sleep(delay)
|
|
2383
|
+
return await self.send_message(chat_id, text, **kwargs)
|
|
2384
|
+
async def disable_inline_keyboard(
|
|
2385
|
+
self,
|
|
2386
|
+
chat_id: str,
|
|
2387
|
+
message_id: str,
|
|
2388
|
+
text: Optional[str] = "~",
|
|
2389
|
+
delay: float = 5.0,
|
|
2390
|
+
) -> Dict[str, any]:
|
|
2391
|
+
if text is not None:await self.edit_inline_keypad(chat_id, message_id, inline_keypad={}, text=text)
|
|
2392
|
+
if delay > 0:
|
|
2393
|
+
await asyncio.sleep(delay)
|
|
2394
|
+
response = await self.edit_inline_keypad(chat_id, message_id, inline_keypad={})
|
|
2395
|
+
return response
|
|
2396
|
+
else:return await self.edit_inline_keypad(chat_id, message_id, inline_keypad={})
|
|
2397
|
+
async def get_chat_admins(self, chat_id: str) -> Dict[str, Any]:
|
|
2398
|
+
return await self._post("getChatAdmins", {"chat_id": chat_id})
|
|
2399
|
+
async def get_chat_members(self, chat_id: str, start_id: str = "") -> Dict[str, Any]:
|
|
2400
|
+
return await self._post("getChatMembers", {"chat_id": chat_id, "start_id": start_id})
|
|
2401
|
+
async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
|
|
2402
|
+
return await self._post("getChatInfo", {"chat_id": chat_id})
|
|
2403
|
+
async def set_chat_title(self, chat_id: str, title: str) -> Dict[str, Any]:
|
|
2404
|
+
return await self._post("editChatTitle", {"chat_id": chat_id, "title": title})
|
|
2405
|
+
async def set_chat_description(self, chat_id: str, description: str) -> Dict[str, Any]:
|
|
2406
|
+
return await self._post("editChatDescription", {"chat_id": chat_id, "description": description})
|
|
2407
|
+
async def set_chat_photo(self, chat_id: str, file_id: str) -> Dict[str, Any]:
|
|
2408
|
+
return await self._post("editChatPhoto", {"chat_id": chat_id, "file_id": file_id})
|
|
2409
|
+
async def remove_chat_photo(self, chat_id: str) -> Dict[str, Any]:
|
|
2410
|
+
return await self._post("editChatPhoto", {"chat_id": chat_id, "file_id": "Remove"})
|
|
2411
|
+
async def add_member_chat(self, chat_id: str, user_ids: list[str]) -> Dict[str, Any]:
|
|
2412
|
+
return await self._post("addChatMembers", {"chat_id": chat_id, "member_ids": user_ids})
|
|
2413
|
+
async def ban_member_chat(self, chat_id: str, user_id: str) -> Dict[str, Any]:
|
|
2414
|
+
return await self._post("banChatMember", {"chat_id": chat_id, "member_id": user_id})
|
|
2415
|
+
async def unban_chat_member(self, chat_id: str, user_id: str) -> Dict[str, Any]:
|
|
2416
|
+
return await self._post("unbanChatMember", {"chat_id": chat_id, "member_id": user_id})
|
|
2417
|
+
async def restrict_chat_member(self, chat_id: str, user_id: str, until: int = 0) -> Dict[str, Any]:
|
|
2418
|
+
return await self._post("restrictChatMember", {"chat_id": chat_id, "member_id": user_id, "until_date": until})
|
|
2419
|
+
async def get_chat_member(self, chat_id: str, user_id: str):
|
|
2420
|
+
return await self._post("getChatMember", {"chat_id": chat_id, "user_id": user_id})
|
|
2421
|
+
async def get_admin_chat(self, chat_id: str):
|
|
2422
|
+
return await self._post("getChatAdministrators", {"chat_id": chat_id})
|
|
2423
|
+
async def get_chat_member_count(self, chat_id: str):
|
|
2424
|
+
return await self._post("getChatMemberCount", {"chat_id": chat_id})
|
|
2425
|
+
async def ban_chat_member(self, chat_id: str, user_id: str):
|
|
2426
|
+
return await self._post("banChatMember", {"chat_id": chat_id, "user_id": user_id})
|
|
2427
|
+
async def promote_chat_member(self, chat_id: str, user_id: str, rights: dict) -> Dict[str, Any]:
|
|
2428
|
+
return await self._post("promoteChatMember", {"chat_id": chat_id, "member_id": user_id, "rights": rights})
|
|
2429
|
+
async def demote_chat_member(self, chat_id: str, user_id: str) -> Dict[str, Any]:
|
|
2430
|
+
return await self._post("promoteChatMember", {"chat_id": chat_id, "member_id": user_id, "rights": {}})
|
|
2431
|
+
async def pin_chat_message(self, chat_id: str, message_id: str) -> Dict[str, Any]:
|
|
2432
|
+
return await self._post("pinChatMessage", {"chat_id": chat_id, "message_id": message_id})
|
|
2433
|
+
async def unpin_chat_message(self, chat_id: str, message_id: str = "") -> Dict[str, Any]:
|
|
2434
|
+
return await self._post("unpinChatMessage", {"chat_id": chat_id, "message_id": message_id})
|
|
2435
|
+
async def export_chat_invite_link(self, chat_id: str) -> Dict[str, Any]:
|
|
2436
|
+
return await self._post("exportChatInviteLink", {"chat_id": chat_id})
|
|
2437
|
+
async def revoke_chat_invite_link(self, chat_id: str, link: str) -> Dict[str, Any]:
|
|
2438
|
+
return await self._post("revokeChatInviteLink", {"chat_id": chat_id, "invite_link": link})
|
|
2439
|
+
async def create_group(self, title: str, user_ids: list[str]) -> Dict[str, Any]:
|
|
2440
|
+
return await self._post("createGroup", {"title": title, "user_ids": user_ids})
|
|
2441
|
+
async def create_channel(self, title: str, description: str = "") -> Dict[str, Any]:
|
|
2442
|
+
return await self._post("createChannel", {"title": title, "description": description})
|
|
2443
|
+
async def leave_chat(self, chat_id: str) -> Dict[str, Any]:
|
|
2444
|
+
return await self._post("leaveChat", {"chat_id": chat_id})
|
|
2445
|
+
async def forward_message(self, from_chat_id: str, message_id: str, to_chat_id: str, disable_notification: bool = False) -> Dict[str, Any]:
|
|
2446
|
+
return await self._post("forwardMessage", {"from_chat_id": from_chat_id, "message_id": message_id, "to_chat_id": to_chat_id, "disable_notification": disable_notification})
|
|
2447
|
+
async def edit_message_text(self, chat_id: str, message_id: str, text: str, parse_mode: Optional[Literal["HTML", "Markdown"]] = None,meta_data:Optional[dict] = None) -> Dict[str, Any]:
|
|
2448
|
+
payload = {
|
|
2449
|
+
"chat_id": chat_id,
|
|
2450
|
+
"message_id": message_id,
|
|
2451
|
+
"text": text,
|
|
2452
|
+
}
|
|
2453
|
+
if not meta_data:
|
|
2454
|
+
parse_mode_to_use = parse_mode or self.parse_mode
|
|
2455
|
+
if text:
|
|
2456
|
+
text, metadata = self._parse_text_metadata(text, parse_mode_to_use)
|
|
2457
|
+
payload["text"] = text
|
|
2458
|
+
if metadata:
|
|
2459
|
+
payload["metadata"] = metadata
|
|
2460
|
+
else :
|
|
2461
|
+
payload["metadata"] = meta_data
|
|
2462
|
+
return await self._post("editMessageText", payload)
|
|
2463
|
+
async def edit_inline_keypad(self,chat_id: str,message_id: str,inline_keypad: Dict[str, Any],text: str = None) -> Dict[str, Any]:
|
|
2464
|
+
if text is not None:await self._post("editMessageText", {"chat_id": chat_id,"message_id": message_id,"text": text})
|
|
2465
|
+
return await self._post("editMessageKeypad", {"chat_id": chat_id,"message_id": message_id,"inline_keypad": inline_keypad})
|
|
2466
|
+
async def delete_message(self, chat_id: str, message_id: str) -> Dict[str, Any]:
|
|
2467
|
+
return await self._post("deleteMessage", {"chat_id": chat_id, "message_id": message_id})
|
|
2468
|
+
async def set_commands(self, bot_commands: List[Dict[str, str]]) -> Dict[str, Any]:
|
|
2469
|
+
return await self._post("setCommands", {"bot_commands": bot_commands})
|
|
2470
|
+
async def update_bot_endpoint(self, url: str, type: str) -> Dict[str, Any]:
|
|
2471
|
+
return await self._post("updateBotEndpoints", {"url": url, "type": type})
|
|
2472
|
+
async def remove_keypad(self, chat_id: str) -> Dict[str, Any]:
|
|
2473
|
+
return await self._post("editChatKeypad", {"chat_id": chat_id, "chat_keypad_type": "Remove"})
|
|
2474
|
+
async def edit_chat_keypad(self, chat_id: str, chat_keypad: Dict[str, Any]) -> Dict[str, Any]:
|
|
2475
|
+
return await self._post("editChatKeypad", {"chat_id": chat_id, "chat_keypad_type": "New", "chat_keypad": chat_keypad})
|
|
2476
|
+
async def send_contact(self, chat_id: str, first_name: str, last_name: str, phone_number: str,inline_keypad: Optional[Dict[str, Any]] = None,chat_keypad: Optional[Dict[str, Any]] = None,chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = None,) -> Dict[str, Any]:
|
|
2477
|
+
return await self._post("sendContact", {"chat_id": chat_id, "first_name": first_name, "last_name": last_name, "phone_number": phone_number,"inline_keypad": inline_keypad,"chat_keypad": chat_keypad,"chat_keypad_type": chat_keypad_type})
|
|
2478
|
+
async def get_chat(self, chat_id: str) -> Dict[str, Any]:
|
|
2479
|
+
return await self._post("getChat", {"chat_id": chat_id})
|
|
2480
|
+
|
|
2481
|
+
def get_all_member(self, channel_guid: str, search_text: str = None, start_id: str = None, just_get_guids: bool = False):
|
|
2482
|
+
client = self._get_client()
|
|
2483
|
+
return client.get_all_members(channel_guid, search_text, start_id, just_get_guids)
|
|
2484
|
+
async def send_poll(
|
|
2485
|
+
self,
|
|
2486
|
+
chat_id: str,
|
|
2487
|
+
question: str,
|
|
2488
|
+
options: List[str],
|
|
2489
|
+
type: Literal["Regular", "Quiz"] = "Regular",
|
|
2490
|
+
allows_multiple_answers: bool = False,
|
|
2491
|
+
is_anonymous: bool = True,
|
|
2492
|
+
correct_option_index: Optional[int] = None,
|
|
2493
|
+
hint: Optional[str] = None,
|
|
2494
|
+
reply_to_message_id: Optional[str] = None,
|
|
2495
|
+
disable_notification: bool = False,
|
|
2496
|
+
inline_keypad: Optional[Dict[str, Any]] = None,
|
|
2497
|
+
chat_keypad: Optional[Dict[str, Any]] = None,
|
|
2498
|
+
chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = None,
|
|
2499
|
+
) -> AttrDict:
|
|
2500
|
+
|
|
2501
|
+
payload = {
|
|
2502
|
+
"chat_id": chat_id,
|
|
2503
|
+
"question": question,
|
|
2504
|
+
"options": options,
|
|
2505
|
+
"type": type,
|
|
2506
|
+
"allows_multiple_answers": allows_multiple_answers,
|
|
2507
|
+
"is_anonymous": is_anonymous,
|
|
2508
|
+
"correct_option_index": correct_option_index,
|
|
2509
|
+
"explanation": hint,
|
|
2510
|
+
"reply_to_message_id": reply_to_message_id,
|
|
2511
|
+
"disable_notification": disable_notification,
|
|
2512
|
+
"inline_keypad": inline_keypad,
|
|
2513
|
+
"chat_keypad": chat_keypad,
|
|
2514
|
+
"chat_keypad_type": chat_keypad_type,
|
|
2515
|
+
}
|
|
2516
|
+
payload = {k: v for k, v in payload.items() if v is not None or (k in ["is_anonymous", "disable_notification"] and v is False)}
|
|
2517
|
+
return await self._post("sendPoll", payload)
|
|
2518
|
+
|
|
2519
|
+
async def check_join(self, channel_guid: str, chat_id: str = None) -> Union[bool, list[str]]:
|
|
2520
|
+
client = self._get_client()
|
|
2521
|
+
if chat_id:
|
|
2522
|
+
chat_info_data = await self.get_chat(chat_id)
|
|
2523
|
+
chat_info = chat_info_data.get('data', {}).get('chat', {})
|
|
2524
|
+
username = chat_info.get('username')
|
|
2525
|
+
first_name = chat_info.get("first_name", "")
|
|
2526
|
+
if username:
|
|
2527
|
+
result = await asyncio.to_thread(self.get_all_member, channel_guid, search_text=username)
|
|
2528
|
+
members = result.get('in_chat_members', [])
|
|
2529
|
+
return any(m.get('username') == username for m in members)
|
|
2530
|
+
elif first_name:
|
|
2531
|
+
result = await asyncio.to_thread(self.get_all_member, channel_guid, search_text=first_name)
|
|
2532
|
+
members = result.get('in_chat_members', [])
|
|
2533
|
+
return any(m.get('first_name') == first_name for m in members)
|
|
2534
|
+
return False
|
|
2535
|
+
|
|
2536
|
+
class Bot(Robot):
|
|
2537
|
+
pass
|
|
2538
|
+
class bot(Robot):
|
|
2539
|
+
pass
|
|
2540
|
+
class robot(Robot):
|
|
2541
|
+
pass
|