Rubka 6.6.2__py3-none-any.whl → 7.1.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rubka/asynco.py CHANGED
@@ -1,31 +1,155 @@
1
- import asyncio,aiohttp,aiofiles,time,datetime,json,tempfile,os,sys,subprocess,mimetypes
2
- from typing import List, Optional, Dict, Any, Literal, Callable, Union
3
- from .exceptions import APIRequestError
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
4
5
  from .adaptorrubka import Client as Client_get
5
6
  from .logger import logger
7
+ from .metadata import Track_parsed as GlyphWeaver
8
+ from .rubino import Bot as Rubino
6
9
  from . import filters
7
10
  try:from .context import Message, InlineMessage
8
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
9
14
  class FeatureNotAvailableError(Exception):
10
15
  pass
11
16
 
12
17
  from tqdm.asyncio import tqdm
13
18
  from urllib.parse import urlparse, parse_qs
14
- class InvalidTokenError(Exception):pass
19
+
15
20
  from pathlib import Path
16
21
  from tqdm import tqdm
17
22
  API_URL = "https://botapi.rubika.ir/v3"
18
23
 
19
24
  def install_package(package_name: str) -> bool:
20
- """Installs a package using pip."""
21
25
  try:
22
26
  subprocess.check_call([sys.executable, "-m", "pip", "install", package_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
23
27
  return True
24
28
  except Exception:
25
29
  return False
26
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
+
27
152
  def get_importlib_metadata():
28
- """Dynamically imports and returns metadata functions from importlib."""
29
153
  try:
30
154
  from importlib.metadata import version, PackageNotFoundError
31
155
  return version, PackageNotFoundError
@@ -41,16 +165,17 @@ def get_importlib_metadata():
41
165
  version, PackageNotFoundError = get_importlib_metadata()
42
166
 
43
167
  def get_installed_version(package_name: str) -> Optional[str]:
44
- """Gets the installed version of a package."""
45
168
  if version is None:
46
169
  return "unknown"
47
170
  try:
48
171
  return version(package_name)
49
172
  except PackageNotFoundError:
50
173
  return None
51
-
174
+ BASE_URLS = {
175
+ "botapi": "https://botapi.rubika.ir/v3",
176
+ "messenger": "https://messengerg2b1.iranlms.ir/v3"
177
+ }
52
178
  async def get_latest_version(package_name: str) -> Optional[str]:
53
- """Fetches the latest version of a package from PyPI asynchronously."""
54
179
  url = f"https://pypi.org/pypi/{package_name}/json"
55
180
  try:
56
181
  async with aiohttp.ClientSession() as session:
@@ -62,7 +187,6 @@ async def get_latest_version(package_name: str) -> Optional[str]:
62
187
  return None
63
188
 
64
189
  async def check_rubka_version():
65
- """Checks for outdated 'rubka' package and warns the user."""
66
190
  package_name = "rubka"
67
191
  installed_version = get_installed_version(package_name)
68
192
  if installed_version is None:
@@ -73,19 +197,20 @@ async def check_rubka_version():
73
197
  return
74
198
 
75
199
  if installed_version != latest_version:
76
- print(f"\n\nWARNING: Your installed version of '{package_name}' is OUTDATED and may cause errors or security risks!")
77
- print(f"Installed version : {installed_version}")
78
- print(f"Latest available version : {latest_version}")
79
- print(f"Please update IMMEDIATELY by running:")
200
+ print(f"CRITICAL WARNING: Your installed version of '{package_name}' is outdated.")
201
+ print("This poses a serious risk to stability, security, and compatibility with current features.")
202
+ print(f"- Installed version : {installed_version}")
203
+ print(f"- Latest version : {latest_version}")
204
+ print("\nImmediate action is required.")
205
+ print(f"Run the following command to update safely:")
80
206
  print(f"\npip install {package_name}=={latest_version}\n")
81
- print("Not updating may lead to malfunctions or incompatibility.")
82
- print("To see new methods : @rubka_library\n\n")
83
-
207
+ print("Delaying this update may result in unexpected crashes, data loss, or broken functionality.")
208
+ print("Stay up-to-date to ensure full support and access to the latest improvements.")
209
+ print("For new methods and updates, visit: @rubka_library\n")
84
210
 
85
211
 
86
212
 
87
213
  def show_last_six_words(text: str) -> str:
88
- """Returns the last 6 characters of a stripped string."""
89
214
  text = text.strip()
90
215
  return text[-6:]
91
216
  class AttrDict(dict):
@@ -97,60 +222,100 @@ class AttrDict(dict):
97
222
 
98
223
  class Robot:
99
224
  """
100
- Main asynchronous class to interact with the Rubika Bot API.
101
-
102
- This class handles sending and receiving messages, inline queries, callbacks,
103
- and manages sessions and API interactions. It is initialized with a bot token
104
- and provides multiple optional parameters for configuration.
105
-
106
- Attributes:
107
- token (str): Bot token used for authentication with Rubika Bot API.
108
- session_name (str | None): Optional session name for storing session data.
109
- auth (str | None): Optional authentication string for advanced features.
110
- Key (str | None): Optional key for additional authorization if required.
111
- platform (str): Platform type, default is 'web'.
112
- web_hook (str | None): Optional webhook URL for receiving updates.
113
- timeout (int): Timeout for API requests in seconds (default 10).
114
- show_progress (bool): Whether to show progress for long operations (default False).
115
- Example:
116
- ```python
117
- import asyncio
118
- from rubka.asynco import Robot,filters,Message
119
- bot = Robot(token="YOUR_BOT_TOKEN")
120
- @bot.on_message(filters.is_command.start)
121
- async def start_command(bot: Robot, message: Message):
122
- await message.reply("Hello!")
123
- asyncio.run(bot.run())
124
-
125
- """
126
-
127
- 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):
225
+ Main asynchronous class to interact with the Rubika Bot API.
226
+
227
+ This class handles sending and receiving messages, inline queries, callbacks,
228
+ and manages sessions and API interactions. It is initialized with a bot token
229
+ and provides multiple optional parameters for configuration.
230
+
231
+ Attributes:
232
+ token (str): Bot token used for authentication with Rubika Bot API.
233
+ session_name (str | None): Optional session name for storing session data.
234
+ auth (str | None): Optional authentication string for advanced features related to account key.
235
+ Key (str | None): Optional account key for additional authorization if required.
236
+ platform (str): Platform type, default is 'web'.
237
+ web_hook (str | None): Optional webhook URL for receiving updates.
238
+ timeout (int): Timeout for API requests in seconds (default 10).
239
+ show_progress (bool): Whether to show progress for long operations (default False).
240
+ raise_errors (bool): Whether to raise exceptions on API errors (default True).
241
+ proxy (str | None): Optional proxy URL to route requests through.
242
+ retries (int): Number of times to retry a failed API request (default 2).
243
+ retry_delay (float): Delay between retries in seconds (default 0.5).
244
+ user_agent (str | None): Custom User-Agent header for requests.
245
+ safeSendMode (bool): If True, messages are sent safely. If reply fails using message_id, sends without message_id (default False).
246
+ max_cache_size (int): Maximum number of processed messages stored to prevent duplicates (default 1000).
247
+ max_msg_age (int): Maximum age of messages in seconds to consider for processing (default 20).
248
+
249
+ Example:
250
+ ```python
251
+ import asyncio
252
+ from rubka.asynco import Robot, filters, Message
253
+
254
+ bot = Robot(token="YOUR_BOT_TOKEN", safeSendMode=False, max_cache_size=1000)
255
+
256
+ @bot.on_message(filters.is_command.start)
257
+ async def start_command(bot: Robot, message: Message):
258
+ await message.reply("Hello!")
259
+
260
+ asyncio.run(bot.run())
261
+ ```
262
+ Notes:
263
+
264
+ token is mandatory, all other parameters are optional.
265
+
266
+ safeSendMode ensures reliable message sending even if replying by message_id fails.
267
+
268
+ max_cache_size and max_msg_age help manage duplicate message processing efficiently.
269
+ """
270
+
271
+
272
+ 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"):
128
273
  self.token = token
129
274
  self._inline_query_handlers: List[dict] = []
130
275
  self.timeout = timeout
131
276
  self.auth = auth
277
+ self.chunk_size = chunk_size
278
+ self.safeSendMode = safeSendMode
279
+ self.user_agent = user_agent
280
+ self.proxy = proxy
281
+ self.max_msg_age = max_msg_age
282
+ self.retries = retries
283
+
284
+ self.retry_delay = retry_delay
285
+ self.raise_errors = raise_errors
132
286
  self.show_progress = show_progress
133
287
  self.session_name = session_name
134
288
  self.Key = Key
135
289
  self.platform = platform
136
290
  self.web_hook = web_hook
291
+ self.parse_mode = parse_mode
137
292
  self._offset_id: Optional[str] = None
138
293
  self._aiohttp_session: aiohttp.ClientSession = None
139
294
  self.sessions: Dict[str, Dict[str, Any]] = {}
140
295
  self._callback_handler = None
141
- self._message_handler = None
142
- self._inline_query_handler = None
143
-
296
+ self._processed_message_ids = OrderedDict()
297
+ self._max_cache_size = max_cache_size
144
298
  self._callback_handlers: List[dict] = []
145
- self._processed_message_ids: Dict[str, float] = {}
299
+ self._edited_message_handlers = []
300
+ self._message_saver_enabled = False
301
+ self._max_messages = None
302
+ self._db_path = os.path.join(os.getcwd(), "RubkaSaveMessage.db")
303
+ self._ensure_db()
146
304
  self._message_handlers: List[dict] = []
305
+ if api_endpoint not in BASE_URLS:raise ValueError(f"api_endpoint must be one of {list(BASE_URLS.keys())}")
306
+ self.api_endpoint = api_endpoint
147
307
 
148
308
  logger.info(f"Initialized RubikaBot with token: {token[:8]}***")
149
309
  async def _get_session(self) -> aiohttp.ClientSession:
150
- """Lazily creates and returns the aiohttp session."""
151
310
  if self._aiohttp_session is None or self._aiohttp_session.closed:
152
- self._aiohttp_session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.timeout))
311
+ connector = aiohttp.TCPConnector(limit=100, ssl=False)
312
+ timeout = aiohttp.ClientTimeout(total=self.timeout)
313
+ self._aiohttp_session = aiohttp.ClientSession(connector=connector, timeout=timeout)
153
314
  return self._aiohttp_session
315
+ async def close(self):
316
+ if self._aiohttp_session and not self._aiohttp_session.closed:
317
+ await self._aiohttp_session.close()
318
+ logger.debug("aiohttp session closed successfully.")
154
319
 
155
320
  async def _initialize_webhook(self):
156
321
  """Initializes and sets the webhook endpoint if provided."""
@@ -162,9 +327,8 @@ class Robot:
162
327
  async with session.get(self.web_hook, timeout=self.timeout) as response:
163
328
  response.raise_for_status()
164
329
  data = await response.json()
165
- print(data)
330
+ if data:print(f"[INFO] Retrieving WebHook URL information...")
166
331
  json_url = data.get('url', self.web_hook)
167
- print(self.web_hook)
168
332
  for endpoint_type in [
169
333
  "ReceiveUpdate",
170
334
  "ReceiveInlineMessage",
@@ -173,40 +337,209 @@ class Robot:
173
337
  "SearchSelectionItems"
174
338
  ]:
175
339
  result = await self.update_bot_endpoint(self.web_hook, endpoint_type)
176
- print(result)
340
+ if result['status'] =="OK":print(f"✔ Set endpoint type to '{endpoint_type}' — Operation succeeded with status: {result['status']}")
341
+ else:print(f"[ERROR] Failed to set endpoint type '{endpoint_type}': Status code {result['status']}")
177
342
  self.web_hook = json_url
178
343
  except Exception as e:
179
344
  logger.error(f"Failed to set webhook from {self.web_hook}: {e}")
180
345
  self.web_hook = None
181
346
  async def _post(self, method: str, data: Dict[str, Any]) -> Dict[str, Any]:
182
- url = f"{API_URL}/{self.token}/{method}"
347
+ base_url = BASE_URLS[self.api_endpoint]
348
+ url = f"{base_url}/{self.token}/{method}"
349
+ print(url)
183
350
  session = await self._get_session()
184
- try:
185
- async with session.post(url, json=data) as response:
186
- response.raise_for_status()
187
- try:
188
- json_resp = await response.json()
189
- except aiohttp.ContentTypeError:
190
- text_resp = await response.text()
191
- logger.error(f"Invalid JSON response from {method}: {text_resp}")
192
- raise APIRequestError(f"Invalid JSON response: {text_resp}")
193
- if method != "getUpdates":
194
- logger.debug(f"API Response from {method}: {json_resp}")
195
- return AttrDict(json_resp)
196
- except aiohttp.ClientError as e:
197
- logger.error(f"API request failed: {e}")
198
- raise APIRequestError(f"API request failed: {e}") from e
199
-
351
+ for attempt in range(1, self.retries + 1):
352
+ try:
353
+ headers = {}
354
+ if self.user_agent:headers["User-Agent"] = self.user_agent
355
+ async with session.post(url, json=data, proxy=self.proxy,headers=headers) as response:
356
+ if response.status in (429, 500, 502, 503, 504):
357
+ logger.warning(f"[{method}] Got status {response.status}, retry {attempt}/{self.retries}...")
358
+ if attempt < self.retries:
359
+ await asyncio.sleep(self.retry_delay)
360
+ continue
361
+ response.raise_for_status()
362
+
363
+ response.raise_for_status()
364
+ try:
365
+ json_resp = await response.json(content_type=None)
366
+ except Exception:
367
+ text_resp = await response.text()
368
+ logger.error(f"[{method}] Invalid JSON response: {text_resp}")
369
+ raise APIRequestError(f"Invalid JSON response: {text_resp}")
370
+
371
+ status = json_resp.get("status")
372
+ if status in {"INVALID_ACCESS", "INVALID_INPUT", "TOO_REQUESTS"}:
373
+ if self.raise_errors:
374
+ raise_for_status(json_resp)
375
+ return AttrDict(json_resp)
376
+ return AttrDict({**json_resp, **data,"message_id":json_resp.get("data").get("message_id")})
377
+
378
+ except (aiohttp.ClientError, asyncio.TimeoutError) as e:
379
+ logger.warning(f"[{method}] Attempt {attempt}/{self.retries} failed: {e}")
380
+ if attempt < self.retries:
381
+ await asyncio.sleep(self.retry_delay)
382
+ continue
383
+ logger.error(f"[{method}] API request failed after {self.retries} retries: {e}")
384
+ raise APIRequestError(f"API request failed: {e}") from e
385
+ def _make_dup_key(self, message_id: str, update_type: str, msg_data: dict) -> str:
386
+ raw = f"{message_id}:{update_type}:{msg_data.get('text','')}:{msg_data.get('author_guid','')}"
387
+ return hashlib.sha1(raw.encode()).hexdigest()
200
388
  async def get_me(self) -> Dict[str, Any]:
201
- """Get info about the bot itself."""
202
389
  return await self._post("getMe", {})
203
390
  async def geteToken(self):
204
- """Check if the bot token is valid."""
205
- if (await self.get_me())['status'] != "OK":
206
- raise InvalidTokenError("The provided bot token is invalid or expired.")
391
+ try:
392
+ if (await self.get_me())['status'] != "OK":
393
+ raise InvalidTokenError("The provided bot token is invalid or expired.")
394
+ except Exception as e:
395
+ print(e)
207
396
  from typing import Callable, Any, Optional, List
208
397
 
209
398
 
399
+ #save message database __________________________
400
+
401
+ def _ensure_db(self):
402
+ conn = sqlite3.connect(self._db_path)
403
+ cur = conn.cursor()
404
+ cur.execute("""
405
+ CREATE TABLE IF NOT EXISTS messages (
406
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
407
+ chat_id TEXT NOT NULL,
408
+ message_id TEXT NOT NULL,
409
+ sender_id TEXT,
410
+ text TEXT,
411
+ raw_data TEXT,
412
+ time TEXT,
413
+ saved_at INTEGER
414
+ );
415
+ """)
416
+ cur.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_message ON messages(chat_id, message_id);")
417
+ conn.commit()
418
+ conn.close()
419
+
420
+ def _insert_message(self, record: dict):
421
+ conn = sqlite3.connect(self._db_path)
422
+ cur = conn.cursor()
423
+ cur.execute("""
424
+ INSERT OR IGNORE INTO messages
425
+ (chat_id, message_id, sender_id, text, raw_data, time, saved_at)
426
+ VALUES (?, ?, ?, ?, ?, ?, ?)
427
+ """, (
428
+ record.get("chat_id"),
429
+ record.get("message_id"),
430
+ record.get("sender_id"),
431
+ record.get("text"),
432
+ json.dumps(record.get("raw_data") or {}, ensure_ascii=False),
433
+ record.get("time"),
434
+ int(time.time())
435
+ ))
436
+ conn.commit()
437
+ if getattr(self, "_max_messages", None) is not None:
438
+ cur.execute("SELECT COUNT(*) FROM messages")
439
+ total = cur.fetchone()[0]
440
+ if total > self._max_messages:
441
+ remove_count = total - self._max_messages
442
+ cur.execute(
443
+ "DELETE FROM messages WHERE id IN (SELECT id FROM messages ORDER BY saved_at ASC LIMIT ?)",
444
+ (remove_count,)
445
+ )
446
+ conn.commit()
447
+
448
+ conn.close()
449
+
450
+ def _fetch_message(self, chat_id: str, message_id: str):
451
+ conn = sqlite3.connect(self._db_path)
452
+ cur = conn.cursor()
453
+ cur.execute(
454
+ "SELECT chat_id, message_id, sender_id, text, raw_data, time, saved_at FROM messages WHERE chat_id=? AND message_id=?",
455
+ (chat_id, message_id)
456
+ )
457
+ row = cur.fetchone()
458
+ conn.close()
459
+ if not row:
460
+ return None
461
+ chat_id, message_id, sender_id, text, raw_data_json, time_val, saved_at = row
462
+ try:
463
+ raw = json.loads(raw_data_json)
464
+ except:
465
+ raw = {}
466
+ return {
467
+ "chat_id": chat_id,
468
+ "message_id": message_id,
469
+ "sender_id": sender_id,
470
+ "text": text,
471
+ "raw_data": raw,
472
+ "time": time_val,
473
+ "saved_at": saved_at
474
+ }
475
+ async def save_message(self, message: Message):
476
+ try:
477
+ record = {
478
+ "chat_id": getattr(message, "chat_id", None),
479
+ "message_id": getattr(message, "message_id", None),
480
+ "sender_id": getattr(message, "author_guid", None),
481
+ "text": getattr(message, "text", None),
482
+ "raw_data": getattr(message, "raw_data", {}),
483
+ "time": getattr(message, "time", None),
484
+ }
485
+ await asyncio.to_thread(self._insert_message, record)
486
+ except Exception as e:
487
+ print(f"[DB] Error saving message: {e}")
488
+
489
+ async def get_message(self, chat_id: str, message_id: str):
490
+ return await asyncio.to_thread(self._fetch_message, chat_id, message_id)
491
+
492
+ def start_save_message(self, max_messages: int = 1000):
493
+ if self._message_saver_enabled:
494
+ return
495
+ self._message_saver_enabled = True
496
+ self._max_messages = max_messages
497
+ decorators = [
498
+ "on_message", "on_edited_message", "on_message_file", "on_message_forwarded",
499
+ "on_message_reply", "on_message_text", "on_update", "on_callback",
500
+ "on_callback_query", "callback_query_handler", "callback_query",
501
+ "on_inline_query", "on_inline_query_prefix", "on_message_private", "on_message_group"
502
+ ]
503
+
504
+ for decorator_name in decorators:
505
+ if hasattr(self, decorator_name):
506
+ original_decorator = getattr(self, decorator_name)
507
+
508
+ def make_wrapper(orig_decorator):
509
+ def wrapper(*args, **kwargs):
510
+ decorator = orig_decorator(*args, **kwargs)
511
+ def inner_wrapper(func):
512
+ async def inner(bot, message, *a, **kw):
513
+ try:
514
+ await bot.save_message(message)
515
+ if getattr(self, "_max_messages", None) is not None:
516
+ await asyncio.to_thread(self._prune_old_messages)
517
+ except Exception as e:
518
+ print(f"[DB] Save error: {e}")
519
+ return await func(bot, message, *a, **kw)
520
+ return decorator(inner)
521
+ return inner_wrapper
522
+ return wrapper
523
+
524
+ setattr(self, decorator_name, make_wrapper(original_decorator))
525
+ def _prune_old_messages(self):
526
+ if not hasattr(self, "_max_messages") or self._max_messages is None:
527
+ return
528
+ conn = sqlite3.connect(self._db_path)
529
+ cur = conn.cursor()
530
+ cur.execute("SELECT COUNT(*) FROM messages")
531
+ total = cur.fetchone()[0]
532
+ if total > self._max_messages:
533
+ remove_count = total - self._max_messages
534
+ cur.execute(
535
+ "DELETE FROM messages WHERE id IN (SELECT id FROM messages ORDER BY saved_at ASC LIMIT ?)",
536
+ (remove_count,)
537
+ )
538
+ conn.commit()
539
+ conn.close()
540
+
541
+ #save message database __________________________ end
542
+
210
543
  #decorator#
211
544
 
212
545
  def on_message_private(
@@ -222,7 +555,7 @@ class Robot:
222
555
  allow_polls: bool = True,
223
556
  allow_contacts: bool = True,
224
557
  allow_locations: bool = True,
225
- min_text_length: Optional[int] = None,
558
+ min_text_length: Optional[int] = None,
226
559
  max_text_length: Optional[int] = None,
227
560
  contains: Optional[str] = None,
228
561
  startswith: Optional[str] = None,
@@ -409,6 +742,7 @@ class Robot:
409
742
 
410
743
  def decorator(func: Callable[[Any, Message], None]):
411
744
  async def wrapper(bot, message: Message):
745
+
412
746
  if not message.is_group:
413
747
  return
414
748
  if chat_id:
@@ -469,27 +803,66 @@ class Robot:
469
803
  })
470
804
  return wrapper
471
805
  return decorator
472
-
473
- def on_message(
474
- self,
475
- filters: Optional[Callable[[Message], bool]] = None,
806
+ def remove_handler(self, func: Callable):
807
+ """
808
+ Remove a message handler by its original function reference.
809
+ """
810
+ self._message_handlers = [
811
+ h for h in self._message_handlers if h["func"].__wrapped__ != func
812
+ ]
813
+ def on_edited_message(
814
+ self,
815
+ filters: Optional[Callable[[Message], bool]] = None,
476
816
  commands: Optional[List[str]] = None
477
- ):
817
+ ):
478
818
  def decorator(func: Callable[[Any, Message], None]):
479
819
  async def wrapper(bot, message: Message):
480
- if filters and not filters(message):return
820
+ if filters and not filters(message):
821
+ return
481
822
  if commands:
482
- if not message.is_command:return
823
+ if not message.is_command:
824
+ return
483
825
  cmd = message.text.split()[0].lstrip("/")
484
- if cmd not in commands:return
826
+ if cmd not in commands:
827
+ return
485
828
  return await func(bot, message)
486
- self._message_handlers.append({
829
+
830
+ self._edited_message_handlers.append({
487
831
  "func": wrapper,
488
832
  "filters": filters,
489
833
  "commands": commands
490
834
  })
491
835
  return wrapper
492
836
  return decorator
837
+ def on_message(
838
+ self,
839
+ filters: Optional[Callable[[Message], bool]] = None,
840
+ commands: Optional[List[str]] = None):
841
+ def decorator(func: Callable[[Any, Message], None]):
842
+ async def wrapper(bot, message: Message):
843
+ if filters and not filters(message):
844
+ return
845
+ if commands:
846
+ if not message.is_command:
847
+ return
848
+ cmd = message.text.split()[0].lstrip("/")
849
+ if cmd not in commands:
850
+ return
851
+
852
+ return await func(bot, message)
853
+ self._message_handlers.append({
854
+ "func": wrapper,
855
+ "filters": filters,
856
+ "commands": commands
857
+ })
858
+ self._edited_message_handlers.append({
859
+ "func": wrapper,
860
+ "filters": filters,
861
+ "commands": commands
862
+ })
863
+
864
+ return wrapper
865
+ return decorator
493
866
 
494
867
 
495
868
  def on_message_file(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
@@ -759,9 +1132,36 @@ class Robot:
759
1132
  asyncio.create_task(handler_info["func"](self, context))
760
1133
  continue
761
1134
  if handler_info["commands"] or handler_info["filters"]:
762
- asyncio.create_task(handler_info["func"](self, context))#jaq
1135
+ asyncio.create_task(handler_info["func"](self, context))#kir baba kir
763
1136
  continue
1137
+ elif update.get("type") == "UpdatedMessage":
1138
+ msg = update.get("updated_message", {})
1139
+ if not msg:
1140
+ return
764
1141
 
1142
+ context = Message(
1143
+ bot=self,
1144
+ chat_id=update.get("chat_id"),
1145
+ message_id=msg.get("message_id"),
1146
+ text=msg.get("text"),
1147
+ sender_id=msg.get("sender_id"),
1148
+ raw_data=msg
1149
+ )
1150
+ if self._edited_message_handlers:
1151
+ for handler_info in self._edited_message_handlers:
1152
+ if handler_info["commands"]:
1153
+ if not context.text or not context.text.startswith("/"):
1154
+ continue
1155
+ parts = context.text.split()
1156
+ cmd = parts[0][1:]
1157
+ if cmd not in handler_info["commands"]:
1158
+ continue
1159
+ context.args = parts[1:]
1160
+ if handler_info["filters"]:
1161
+ if not handler_info["filters"](context):
1162
+ continue
1163
+ asyncio.create_task(handler_info["func"](self, context))
1164
+
765
1165
  async def get_updates(self, offset_id: Optional[str] = None, limit: Optional[int] = None) -> Dict[str, Any]:
766
1166
  data = {}
767
1167
  if offset_id: data["offset_id"] = offset_id
@@ -777,63 +1177,66 @@ class Robot:
777
1177
  response.raise_for_status()
778
1178
  return await response.json()
779
1179
 
780
- def _is_duplicate(self, message_id: str, max_age_sec: int = 300) -> bool:
1180
+ def _is_duplicate(self, key: str, max_age_sec: int = 300) -> bool:
781
1181
  now = time.time()
782
1182
  expired = [mid for mid, ts in self._processed_message_ids.items() if now - ts > max_age_sec]
783
- for mid in expired:del self._processed_message_ids[mid]
784
- if message_id in self._processed_message_ids:return True
785
- self._processed_message_ids[message_id] = now
1183
+ for mid in expired:
1184
+ del self._processed_message_ids[mid]
1185
+ if key in self._processed_message_ids:
1186
+ return True
1187
+ self._processed_message_ids[key] = now
1188
+ if len(self._processed_message_ids) > self._max_cache_size:
1189
+ self._processed_message_ids.popitem(last=False)
786
1190
  return False
787
-
788
1191
 
789
- async def run(
790
- self,
791
- debug: bool = False,
792
- sleep_time: float = 0.2,
793
- webhook_timeout: int = 20,
794
- update_limit: int = 100,
795
- retry_delay: float = 5.0,
796
- stop_on_error: bool = False,
797
- max_errors: int = 0,
798
- auto_restart: bool = False,
799
- max_runtime: float | None = None,
800
- loop_forever: bool = True,
801
- allowed_update_types: list[str] | None = None,
802
- ignore_duplicate_messages: bool = True,
803
- skip_inline_queries: bool = False,
804
- skip_channel_posts: bool = False,
805
- skip_service_messages: bool = False,
806
- skip_edited_messages: bool = False,
807
- skip_bot_messages: bool = False,
808
- log_file: str | None = None,
809
- log_level: str = "info",
810
- print_exceptions: bool = True,
811
- error_handler=None,
812
- shutdown_hook=None,
813
- save_unprocessed_updates: bool = False,
814
- log_to_console: bool = True,
815
- rate_limit: float | None = None,
816
- max_message_size: int | None = None,
817
- ignore_users: set[str] | None = None,
818
- ignore_groups: set[str] | None = None,
819
- require_auth_token: bool = False,
820
- only_private_chats: bool = False,
821
- only_groups: bool = False,
822
- require_admin_rights: bool = False,
823
- custom_update_fetcher=None,
824
- custom_update_processor=None,
825
- process_in_background: bool = False,
826
- max_queue_size: int = 1000,
827
- thread_workers: int = 3,
828
- message_filter=None,
829
- pause_on_idle: bool = False,
830
- max_concurrent_tasks: int | None = None,
831
- metrics_enabled: bool = False,
832
- metrics_handler=None,
833
- notify_on_error: bool = False,
834
- notification_handler=None,
835
- watchdog_timeout: float | None = None,
836
- ):
1192
+ async def run_progelry(
1193
+ self,
1194
+ debug: bool = False,
1195
+ sleep_time: float = 0.1,
1196
+ webhook_timeout: int = 20,
1197
+ update_limit: int = 100,
1198
+ retry_delay: float = 5.0,
1199
+ stop_on_error: bool = False,
1200
+ max_errors: int = 0,
1201
+ auto_restart: bool = False,
1202
+ max_runtime: Optional[float] = None,
1203
+ loop_forever: bool = True,
1204
+ allowed_update_types: Optional[List[str]] = None,
1205
+ ignore_duplicate_messages: bool = True,
1206
+ skip_inline_queries: bool = False,
1207
+ skip_channel_posts: bool = False,
1208
+ skip_service_messages: bool = False,
1209
+ skip_edited_messages: bool = False,
1210
+ skip_bot_messages: bool = False,
1211
+ log_file: Optional[str] = None,
1212
+ log_level: str = "info",
1213
+ print_exceptions: bool = True,
1214
+ error_handler: Optional[Callable[[Exception], Any]] = None,
1215
+ shutdown_hook: Optional[Callable[[], Any]] = None,
1216
+ save_unprocessed_updates: bool = False,
1217
+ log_to_console: bool = True,
1218
+ rate_limit: Optional[float] = None,
1219
+ max_message_size: Optional[int] = None,
1220
+ ignore_users: Optional[Set[str]] = None,
1221
+ ignore_groups: Optional[Set[str]] = None,
1222
+ require_auth_token: bool = False,
1223
+ only_private_chats: bool = False,
1224
+ only_groups: bool = False,
1225
+ require_admin_rights: bool = False,
1226
+ custom_update_fetcher: Optional[Callable[[], Any]] = None,
1227
+ custom_update_processor: Optional[Callable[[Any], Any]] = None,
1228
+ process_in_background: bool = False,
1229
+ max_queue_size: int = 1000,
1230
+ thread_workers: int = 3,
1231
+ message_filter: Optional[Callable[[Any], bool]] = None,
1232
+ pause_on_idle: bool = False,
1233
+ max_concurrent_tasks: Optional[int] = None,
1234
+ metrics_enabled: bool = False,
1235
+ metrics_handler: Optional[Callable[[dict], Any]] = None,
1236
+ notify_on_error: bool = False,
1237
+ notification_handler: Optional[Callable[[str], Any]] = None,
1238
+ watchdog_timeout: Optional[float] = None,
1239
+ ):
837
1240
  """
838
1241
  Starts the bot's main execution loop with extensive configuration options.
839
1242
 
@@ -976,12 +1379,16 @@ class Robot:
976
1379
  im = update.get("inline_message", {})
977
1380
  sender = im.get("author_object_guid") or im.get("author_guid")
978
1381
  chat = im.get("object_guid") or im.get("chat_id")
1382
+ elif t == "UpdatedMessage":
1383
+ im = update.get("updated_message", {})
1384
+ sender = im.get("author_object_guid") or im.get("author_guid")
1385
+ chat = im.get("object_guid") or im.get("chat_id")
979
1386
  else:
980
1387
  sender = update.get("author_guid") or update.get("from_id")
981
1388
  chat = update.get("object_guid") or update.get("chat_id")
982
1389
  return str(sender) if sender is not None else None, str(chat) if chat is not None else None
983
1390
 
984
- def _is_group_chat(chat_guid: str | None) -> bool | None:
1391
+ def _is_group_chat(chat_guid: Optional[str]) -> Optional[bool]:
985
1392
 
986
1393
  if chat_guid is None:
987
1394
  return None
@@ -1049,8 +1456,6 @@ class Robot:
1049
1456
  return False
1050
1457
  if skip_service_messages and t == "ServiceMessage":
1051
1458
  return False
1052
- if skip_edited_messages and t == "EditMessage":
1053
- return False
1054
1459
  if skip_channel_posts and t == "ChannelPost":
1055
1460
  return False
1056
1461
 
@@ -1080,6 +1485,8 @@ class Robot:
1080
1485
  content = (update.get("new_message") or {}).get("text")
1081
1486
  elif t == "ReceiveQuery":
1082
1487
  content = (update.get("inline_message") or {}).get("text")
1488
+ elif t == "UpdatedMessage":
1489
+ content = (update.get("updated_message") or {}).get("text")
1083
1490
  elif "text" in update:
1084
1491
  content = update.get("text")
1085
1492
  if content and isinstance(content, str) and len(content) > max_message_size:
@@ -1142,7 +1549,7 @@ class Robot:
1142
1549
  await check_rubka_version()
1143
1550
  await self._initialize_webhook()
1144
1551
  await self.geteToken()
1145
- _log("Bot started running...", "info")
1552
+ _log("Bot is up and running...", "info")
1146
1553
 
1147
1554
  try:
1148
1555
  while True:
@@ -1196,6 +1603,8 @@ class Robot:
1196
1603
  message_id = update.get("new_message", {}).get("message_id")
1197
1604
  elif update.get("type") == "ReceiveQuery":
1198
1605
  message_id = update.get("inline_message", {}).get("message_id")
1606
+ elif update.get("type") == "UpdatedMessage":
1607
+ message_id = update.get("updated_message", {}).get("message_id")
1199
1608
  elif "message_id" in update:
1200
1609
  message_id = update.get("message_id")
1201
1610
 
@@ -1216,26 +1625,41 @@ class Robot:
1216
1625
  for update in updates:
1217
1626
  message_id = None
1218
1627
  if update.get("type") == "NewMessage":
1219
- message_id = update.get("new_message", {}).get("message_id")
1628
+ msg_data = update.get("new_message", {})
1629
+ message_id = msg_data.get("message_id")
1630
+ text_content = msg_data.get("text", "")
1631
+ msg_time = int(msg_data.get("time", 0))
1220
1632
  elif update.get("type") == "ReceiveQuery":
1221
- message_id = update.get("inline_message", {}).get("message_id")
1633
+ msg_data = update.get("inline_message", {})
1634
+ message_id = msg_data.get("message_id")
1635
+ text_content = msg_data.get("text", "")
1636
+ msg_time = int(msg_data.get("time", 0))
1637
+ elif update.get("type") == "UpdatedMessage":
1638
+ msg_data = update.get("updated_message", {})
1639
+ message_id = msg_data.get("message_id")
1640
+ text_content = msg_data.get("text", "")
1641
+ msg_time = int(msg_data.get("time", 0))
1222
1642
  elif "message_id" in update:
1223
1643
  message_id = update.get("message_id")
1224
-
1644
+ else:
1645
+ msg_time = time.time()
1646
+ msg_data = update.get("updated_message", {})
1647
+ message_id = msg_data.get("message_id")
1648
+ text_content = msg_data.get("text", "")
1649
+ now = int(time.time())
1650
+
1651
+ if msg_time and (now - msg_time > self.max_msg_age):
1652
+ continue
1225
1653
  dup_ok = True
1226
- if ignore_duplicate_messages:
1227
- dup_ok = (not self._is_duplicate(str(message_id))) if message_id else True
1654
+ if ignore_duplicate_messages and message_id:
1655
+ dup_key = self._make_dup_key(message_id, update.get("type", ""), msg_data)
1656
+ dup_ok = not self._is_duplicate(dup_key)
1228
1657
  if message_id and dup_ok:
1229
1658
  received_updates.append(update)
1230
-
1231
-
1232
1659
  if not received_updates:
1233
- if pause_on_idle and sleep_time == 0:
1234
- await asyncio.sleep(0.05)
1235
- else:
1236
- await asyncio.sleep(sleep_time)
1237
- if not loop_forever and max_runtime is None:
1238
- break
1660
+ if pause_on_idle and sleep_time == 0:await asyncio.sleep(0.005)
1661
+ else:await asyncio.sleep(sleep_time)
1662
+ if not loop_forever and max_runtime is None:break
1239
1663
  continue
1240
1664
 
1241
1665
 
@@ -1352,22 +1776,87 @@ class Robot:
1352
1776
 
1353
1777
 
1354
1778
  _log("Auto-restart requested. You can call run(...) again as needed.", "warning")
1355
-
1779
+ def run(self, sleep_time: float = 0.1, *args, **kwargs):
1780
+ print("Connecting to the server...")
1781
+ try:
1782
+ loop = asyncio.get_running_loop()
1783
+ return loop.create_task(self.run_progelry(sleep_time=sleep_time, *args, **kwargs))
1784
+ except RuntimeError:return asyncio.run(self.run_progelry(sleep_time=sleep_time, *args, **kwargs))
1785
+ async def _delete_after_task(self, chat_id: str, message_id: str, delay: int):
1786
+ try:
1787
+ await asyncio.sleep(delay)
1788
+ await self.delete_message(chat_id=chat_id, message_id=message_id)
1789
+ except Exception:
1790
+ return False
1791
+ async def _edit_after_task(self, chat_id: str, message_id: str, text:str, delay: int):
1792
+ try:
1793
+ await asyncio.sleep(delay)
1794
+ await self.edit_message_text(chat_id=chat_id, message_id=message_id,text=text)
1795
+ except Exception:
1796
+ return False
1797
+
1798
+ async def delete_after(self, chat_id: str, message_id: str, delay: int = 30) -> asyncio.Task:
1799
+ async def _task():
1800
+ await asyncio.sleep(delay)
1801
+ try:
1802
+ await self.delete_message(chat_id, message_id)
1803
+ except Exception:
1804
+ pass
1805
+
1806
+ try:
1807
+ loop = asyncio.get_running_loop()
1808
+ except RuntimeError:
1809
+ loop = asyncio.new_event_loop()
1810
+ asyncio.set_event_loop(loop)
1811
+
1812
+ task = loop.create_task(_task())
1813
+ return task
1814
+
1815
+ async def edit_after(self, chat_id: str, message_id: str, text : str, delay: int = 30) -> asyncio.Task:
1816
+ async def _task():
1817
+ await asyncio.sleep(delay)
1818
+ try:
1819
+ await self.edit_message_text(chat_id, message_id,text)
1820
+ except Exception:
1821
+ pass
1822
+
1823
+ try:
1824
+ loop = asyncio.get_running_loop()
1825
+ except RuntimeError:
1826
+ loop = asyncio.new_event_loop()
1827
+ asyncio.set_event_loop(loop)
1828
+
1829
+ task = loop.create_task(_task())
1830
+ return task
1831
+ def _parse_text_metadata(self, text: str, parse_mode: str):
1832
+ formatter = GlyphWeaver()
1833
+ parsed = formatter.parse(text, parse_mode)
1834
+ return parsed.get("text"), parsed.get("metadata")
1835
+
1356
1836
  async def send_message(
1357
- self,
1358
- chat_id: str,
1359
- text: str,
1360
- chat_keypad: Optional[Dict[str, Any]] = None,
1361
- inline_keypad: Optional[Dict[str, Any]] = None,
1362
- disable_notification: bool = False,
1363
- reply_to_message_id: Optional[str] = None,
1364
- chat_keypad_type: Optional[Literal["New", "Removed"]] = None
1837
+ self,
1838
+ chat_id: str,
1839
+ text: str,
1840
+ chat_keypad: Optional[Dict[str, Any]] = None,
1841
+ inline_keypad: Optional[Dict[str, Any]] = None,
1842
+ disable_notification: bool = False,
1843
+ reply_to_message_id: Optional[str] = None,
1844
+ chat_keypad_type: Optional[Literal["New", "Remove"]] = None,
1845
+ delete_after: Optional[int] = None,
1846
+ parse_mode: Optional[Literal["HTML", "Markdown"]] = None
1365
1847
  ) -> Dict[str, Any]:
1848
+
1366
1849
  payload = {
1367
1850
  "chat_id": chat_id,
1368
1851
  "text": text,
1369
1852
  "disable_notification": disable_notification,
1370
1853
  }
1854
+ parse_mode_to_use = parse_mode or self.parse_mode
1855
+ if text:
1856
+ text, metadata = self._parse_text_metadata(text, parse_mode_to_use)
1857
+ payload["text"] = text
1858
+ if metadata:
1859
+ payload["metadata"] = metadata
1371
1860
  if chat_keypad:
1372
1861
  payload["chat_keypad"] = chat_keypad
1373
1862
  payload["chat_keypad_type"] = chat_keypad_type or "New"
@@ -1375,7 +1864,56 @@ class Robot:
1375
1864
  payload["inline_keypad"] = inline_keypad
1376
1865
  if reply_to_message_id:
1377
1866
  payload["reply_to_message_id"] = reply_to_message_id
1378
- return await self._post("sendMessage", payload)
1867
+ try:
1868
+ state = await self._post("sendMessage", payload)
1869
+ except Exception:
1870
+ if self.safeSendMode and reply_to_message_id:
1871
+ payload.pop("reply_to_message_id", None)
1872
+ state = await self._post("sendMessage", payload)
1873
+ else:
1874
+ raise
1875
+ if delete_after:
1876
+ await self.delete_after(chat_id, state.message_id, delete_after)
1877
+ return state
1878
+
1879
+
1880
+ async def send_sticker(
1881
+ self,
1882
+ chat_id: str,
1883
+ sticker_id: str,
1884
+ chat_keypad: Optional[Dict[str, Any]] = None,
1885
+ disable_notification: bool = False,
1886
+ inline_keypad: Optional[Dict[str, Any]] = None,
1887
+ reply_to_message_id: Optional[str] = None,
1888
+ chat_keypad_type: Optional[Literal['New', 'Remove']] = None,
1889
+ ) -> str:
1890
+ """
1891
+ Send a sticker to a chat.
1892
+
1893
+ Args:
1894
+ token: Bot token.
1895
+ chat_id: Target chat ID.
1896
+ sticker_id: ID of the sticker to send.
1897
+ chat_keypad: Optional chat keypad data.
1898
+ disable_notification: If True, disables notification.
1899
+ inline_keypad: Optional inline keyboard data.
1900
+ reply_to_message_id: Optional message ID to reply to.
1901
+ chat_keypad_type: Type of chat keypad change ('New' or 'Remove').
1902
+
1903
+ Returns:
1904
+ API response as a string.
1905
+ """
1906
+ data = {
1907
+ 'chat_id': chat_id,
1908
+ 'sticker_id': sticker_id,
1909
+ 'chat_keypad': chat_keypad,
1910
+ 'disable_notification': disable_notification,
1911
+ 'inline_keypad': inline_keypad,
1912
+ 'reply_to_message_id': reply_to_message_id,
1913
+ 'chat_keypad_type': chat_keypad_type,
1914
+ }
1915
+ return await self._post("sendSticker", data)
1916
+
1379
1917
 
1380
1918
  async def get_url_file(self,file_id):
1381
1919
  data = await self._post("getFile", {'file_id': file_id})
@@ -1386,22 +1924,6 @@ class Robot:
1386
1924
  return Client_get(self.session_name, self.auth, self.Key, self.platform)
1387
1925
  else:
1388
1926
  return Client_get(show_last_six_words(self.token), self.auth, self.Key, self.platform)
1389
-
1390
- async def check_join(self, channel_guid: str, chat_id: str = None) -> Union[bool, list[str]]:
1391
- client = self._get_client()
1392
- if chat_id:
1393
- chat_info_data = await self.get_chat(chat_id)
1394
- chat_info = chat_info_data.get('data', {}).get('chat', {})
1395
- username = chat_info.get('username')
1396
- user_id = chat_info.get('user_id')
1397
- if username:
1398
- result = await asyncio.to_thread(self.get_all_member, channel_guid, search_text=username)
1399
- members = result.get('in_chat_members', [])
1400
- return any(m.get('username') == username for m in members)
1401
- elif user_id:
1402
- member_guids = await asyncio.to_thread(client.get_all_members, channel_guid, just_get_guids=True)
1403
- return user_id in member_guids
1404
- return False
1405
1927
  async def send_button_join(
1406
1928
  self,
1407
1929
  chat_id,
@@ -1487,57 +2009,55 @@ class Robot:
1487
2009
  inline_keypad=builder.build(),
1488
2010
  reply_to_message_id=reply_to_message_id
1489
2011
  )
1490
- def get_all_member(self, channel_guid: str, search_text: str = None, start_id: str = None, just_get_guids: bool = False):
1491
- client = self._get_client()
1492
- return client.get_all_members(channel_guid, search_text, start_id, just_get_guids)
1493
- async def send_poll(self, chat_id: str, question: str, options: List[str]) -> Dict[str, Any]:
1494
- return await self._post("sendPoll", {"chat_id": chat_id, "question": question, "options": options})
1495
- 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", "Removed"]] = None) -> Dict[str, Any]:
2012
+
2013
+ async def close_poll(self, chat_id: str, message_id: str) -> Dict[str, Any]:
2014
+ return await self._post("closePoll", {"chat_id": chat_id, "message_id": message_id})
2015
+ 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]:
1496
2016
  payload = {"chat_id": chat_id, "latitude": latitude, "longitude": longitude, "disable_notification": disable_notification}
1497
2017
  if inline_keypad: payload["inline_keypad"] = inline_keypad
1498
2018
  if reply_to_message_id: payload["reply_to_message_id"] = reply_to_message_id
1499
2019
  if chat_keypad_type: payload["chat_keypad_type"] = chat_keypad_type
1500
2020
  return await self._post("sendLocation", {k: v for k, v in payload.items() if v is not None})
1501
- async def send_contact(self, chat_id: str, first_name: str, last_name: str, phone_number: str) -> Dict[str, Any]:
1502
- return await self._post("sendContact", {"chat_id": chat_id, "first_name": first_name, "last_name": last_name, "phone_number": phone_number})
1503
- async def get_chat(self, chat_id: str) -> Dict[str, Any]:
1504
- return await self._post("getChat", {"chat_id": chat_id})
1505
2021
  async def upload_media_file(self, upload_url: str, name: str, path: Union[str, Path]) -> str:
1506
- is_temp_file = False
1507
2022
  session = await self._get_session()
2023
+ is_temp_file = False
1508
2024
  if isinstance(path, str) and path.startswith("http"):
1509
2025
  async with session.get(path) as response:
1510
2026
  if response.status != 200:
1511
2027
  raise Exception(f"Failed to download file from URL ({response.status})")
1512
2028
  content = await response.read()
1513
- with tempfile.NamedTemporaryFile(delete=False) as temp_file:
1514
- temp_file.write(content)
1515
- path = temp_file.name
2029
+ with tempfile.NamedTemporaryFile(delete=False) as tmp:
2030
+ tmp.write(content)
2031
+ path = tmp.name
1516
2032
  is_temp_file = True
2033
+
1517
2034
  file_size = os.path.getsize(path)
1518
- progress_bar = tqdm(total=file_size, unit='B', unit_scale=True, unit_divisor=1024, desc=f'Uploading : {name}', bar_format='{l_bar}{bar:100}{r_bar}', colour='cyan', disable=not self.show_progress)
1519
- async def file_progress_generator(file_path, chunk_size=8192):
2035
+ chunk_size = self.chunk_size
2036
+
2037
+ progress_bar = tqdm(total=file_size, unit='B', unit_scale=True, unit_divisor=1024,
2038
+ desc=f'Uploading: {name}', colour='cyan', disable=not getattr(self, 'show_progress', True))
2039
+
2040
+ async def file_generator(file_path):
1520
2041
  async with aiofiles.open(file_path, 'rb') as f:
1521
- while True:
1522
- chunk = await f.read(chunk_size)
1523
- if not chunk:
1524
- break
2042
+ while chunk := await f.read(chunk_size):
1525
2043
  progress_bar.update(len(chunk))
1526
2044
  yield chunk
1527
- data = aiohttp.FormData()
1528
- data.add_field('file', file_progress_generator(path), filename=name, content_type='application/octet-stream')
2045
+
2046
+ form = aiohttp.FormData()
2047
+ form.add_field('file', file_generator(path), filename=name, content_type='application/octet-stream')
2048
+
1529
2049
  try:
1530
- async with session.post(upload_url, data=data) as response:
2050
+ async with session.post(upload_url, data=form, timeout=aiohttp.ClientTimeout(total=None)) as response:
1531
2051
  progress_bar.close()
1532
2052
  if response.status != 200:
1533
- raise Exception(f"Upload failed ({response.status}): {await response.text()}")
1534
-
1535
- json_data = await response.json()
1536
- if is_temp_file:
1537
- os.remove(path)
1538
- return json_data.get('data', {}).get('file_id')
1539
- except :
1540
- raise FeatureNotAvailableError(f"files is not currently supported by the server.")
2053
+ text = await response.text()
2054
+ raise Exception(f"Upload failed ({response.status}): {text}")
2055
+ return (await response.json()).get('data', {}).get('file_id')
2056
+ except Exception as e:
2057
+ raise FeatureNotAvailableError(f"File upload not supported: {e}")
2058
+ finally:
2059
+ if is_temp_file:
2060
+ os.remove(path)
1541
2061
  def get_extension(content_type: str) -> str:
1542
2062
  ext = mimetypes.guess_extension(content_type)
1543
2063
  return ext if ext else ''
@@ -1617,19 +2137,22 @@ class Robot:
1617
2137
  raise asyncio.TimeoutError("The download operation timed out.")
1618
2138
  except Exception as e:
1619
2139
  raise Exception(f"An error occurred while downloading the file: {e}")
1620
-
1621
2140
  async def get_upload_url(self, media_type: Literal['File', 'Image', 'voice', 'Music', 'Gif', 'Video']) -> str:
1622
2141
  allowed = ['File', 'Image', 'voice', 'Music', 'Gif', 'Video']
1623
2142
  if media_type not in allowed:
1624
2143
  raise ValueError(f"Invalid media type. Must be one of {allowed}")
1625
2144
  result = await self._post("requestSendFile", {"type": media_type})
1626
2145
  return result.get("data", {}).get("upload_url")
1627
-
1628
- 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", "Removed", "None"]] = "None") -> Dict[str, Any]:
2146
+ 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) -> Dict[str, Any]:
1629
2147
  payload = {"chat_id": chat_id, "file_id": file_id, "text": text, "disable_notification": disable_notification, "chat_keypad_type": chat_keypad_type}
1630
2148
  if chat_keypad: payload["chat_keypad"] = chat_keypad
1631
2149
  if inline_keypad: payload["inline_keypad"] = inline_keypad
1632
2150
  if reply_to_message_id: payload["reply_to_message_id"] = str(reply_to_message_id)
2151
+ parse_mode_to_use = parse_mode or self.parse_mode
2152
+ if text:
2153
+ text, metadata = self._parse_text_metadata(text, parse_mode_to_use)
2154
+ payload["text"] = text
2155
+ if metadata:payload["metadata"] = metadata
1633
2156
  payload["time"] = "10"
1634
2157
  resp = await self._post("sendFile", payload)
1635
2158
  message_id_put = resp["data"]["message_id"]
@@ -1649,49 +2172,149 @@ class Robot:
1649
2172
  "chat_keypad_type":chat_keypad_type
1650
2173
  }
1651
2174
  return AttrDict(result)
1652
-
1653
- 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):
2175
+ 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):
1654
2176
  if path:
1655
2177
  file_name = file_name or Path(path).name
1656
2178
  upload_url = await self.get_upload_url(media_type)
1657
2179
  file_id = await self.upload_media_file(upload_url, file_name, path)
1658
2180
  if not file_id:
1659
2181
  raise ValueError("Either path or file_id must be provided.")
1660
- 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)
1661
-
1662
- 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", "Removed", "None"]] = "None") -> Dict[str, Any]:
1663
- 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)
1664
- 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", "Removed", "None"]] = "None") -> Dict[str, Any]:
1665
- 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)
1666
- 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", "Removed", "None"]] = "None") -> Dict[str, Any]:
1667
- 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)
1668
- async def send_music(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = "music", 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", "Removed", "None"]] = "None") -> Dict[str, Any]:
1669
- return await self._send_file_generic("File", chat_id, path, file_id, text, f"{file_name}.ogg", inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
1670
- 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", "Removed", "None"]] = "None") -> Dict[str, Any]:
1671
- 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)
1672
- 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", "Removed", "None"]] = "None") -> Dict[str, Any]:
1673
- 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)
1674
- 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", "Removed", "None"]] = "None") -> Dict[str, Any]:
1675
- 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)
1676
- async def send_gif(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", "Removed", "None"]] = "None") -> Dict[str, Any]:
1677
- return await self._send_file_generic("Gif", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
1678
- async def forward_message(self, from_chat_id: str, message_id: str, to_chat_id: str, disable_notification: bool = False) -> Dict[str, Any]:
1679
- 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})
1680
- async def edit_message_text(self, chat_id: str, message_id: str, text: str) -> Dict[str, Any]:
1681
- return await self._post("editMessageText", {"chat_id": chat_id, "message_id": message_id, "text": text})
1682
- async def edit_inline_keypad(self,chat_id: str,message_id: str,inline_keypad: Dict[str, Any],text: str = None) -> Dict[str, Any]:
1683
- if text is not None:await self._post("editMessageText", {"chat_id": chat_id,"message_id": message_id,"text": text})
1684
- return await self._post("editMessageKeypad", {"chat_id": chat_id,"message_id": message_id,"inline_keypad": inline_keypad})
1685
- async def delete_message(self, chat_id: str, message_id: str) -> Dict[str, Any]:
1686
- return await self._post("deleteMessage", {"chat_id": chat_id, "message_id": message_id})
1687
- async def set_commands(self, bot_commands: List[Dict[str, str]]) -> Dict[str, Any]:
1688
- return await self._post("setCommands", {"bot_commands": bot_commands})
1689
- async def update_bot_endpoint(self, url: str, type: str) -> Dict[str, Any]:
1690
- return await self._post("updateBotEndpoints", {"url": url, "type": type})
1691
- async def remove_keypad(self, chat_id: str) -> Dict[str, Any]:
1692
- return await self._post("editChatKeypad", {"chat_id": chat_id, "chat_keypad_type": "Removed"})
1693
- async def edit_chat_keypad(self, chat_id: str, chat_keypad: Dict[str, Any]) -> Dict[str, Any]:
1694
- return await self._post("editChatKeypad", {"chat_id": chat_id, "chat_keypad_type": "New", "chat_keypad": chat_keypad})
2182
+ 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)
2183
+ 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) -> Dict[str, Any]:
2184
+ 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)
2185
+ 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) -> Dict[str, Any]:
2186
+ 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)
2187
+ 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) -> Dict[str, Any]:
2188
+ 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)
2189
+ 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) -> Dict[str, Any]:
2190
+ 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)
2191
+ 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) -> Dict[str, Any]:
2192
+ 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)
2193
+ 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) -> Dict[str, Any]:
2194
+ 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)
2195
+ async def send_music(
2196
+ self,
2197
+ chat_id: str,
2198
+ path: Optional[Union[str, Path]] = None,
2199
+ file_id: Optional[str] = None,
2200
+ text: Optional[str] = None,
2201
+ file_name: Optional[str] = None,
2202
+ inline_keypad: Optional[Dict[str, Any]] = None,
2203
+ chat_keypad: Optional[Dict[str, Any]] = None,
2204
+ reply_to_message_id: Optional[str] = None,
2205
+ disable_notification: bool = False,
2206
+ chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",
2207
+ parse_mode: Optional[Literal["HTML", "Markdown"]] = None
2208
+ ) -> Dict[str, Any]:
2209
+ valid_extensions = {"ogg", "oga", "opus", "flac"}
2210
+ extension = "flac"
2211
+ if path:
2212
+ path_str = str(path)
2213
+ if path_str.startswith("http://") or path_str.startswith("https://"):
2214
+ parsed = urlparse(path_str)
2215
+ base_name = os.path.basename(parsed.path)
2216
+ else:
2217
+ base_name = os.path.basename(path_str)
2218
+ name, ext = os.path.splitext(base_name)
2219
+
2220
+ if file_name is None or not file_name.strip():
2221
+ file_name = name or "music"
2222
+ ext = ext.lower().replace(".", "")
2223
+ if ext in valid_extensions:
2224
+ extension = ext
2225
+ else:
2226
+ if file_name is None:
2227
+ file_name = "music"
2228
+ return await self._send_file_generic(
2229
+ "File",
2230
+ chat_id,
2231
+ path,
2232
+ file_id,
2233
+ text,
2234
+ f"{file_name}.{extension}",
2235
+ inline_keypad,
2236
+ chat_keypad,
2237
+ reply_to_message_id,
2238
+ disable_notification,
2239
+ chat_keypad_type,
2240
+ parse_mode=parse_mode
2241
+ )
2242
+ async def send_gif(
2243
+ self,
2244
+ chat_id: str,
2245
+ path: Optional[Union[str, Path]] = None,
2246
+ file_id: Optional[str] = None,
2247
+ text: Optional[str] = None,
2248
+ file_name: Optional[str] = None,
2249
+ inline_keypad: Optional[Dict[str, Any]] = None,
2250
+ chat_keypad: Optional[Dict[str, Any]] = None,
2251
+ reply_to_message_id: Optional[str] = None,
2252
+ disable_notification: bool = False,
2253
+ chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = "None",
2254
+ parse_mode: Optional[Literal["HTML", "Markdown"]] = None
2255
+ ) -> Dict[str, Any]:
2256
+ valid_extensions = {"gif"}
2257
+ extension = "gif"
2258
+ if path:
2259
+ path_str = str(path)
2260
+ if path_str.startswith("http://") or path_str.startswith("https://"):
2261
+ parsed = urlparse(path_str)
2262
+ base_name = os.path.basename(parsed.path)
2263
+ else:
2264
+ base_name = os.path.basename(path_str)
2265
+ name, ext = os.path.splitext(base_name)
2266
+
2267
+ if file_name is None or not file_name.strip():
2268
+ file_name = name or "gif"
2269
+ ext = ext.lower().replace(".", "")
2270
+ if ext in valid_extensions:
2271
+ extension = ext
2272
+ else:
2273
+ if file_name is None:
2274
+ file_name = "gif"
2275
+ return await self._send_file_generic(
2276
+ "File",
2277
+ chat_id,
2278
+ path,
2279
+ file_id,
2280
+ text,
2281
+ f"{file_name}.{extension}",
2282
+ inline_keypad,
2283
+ chat_keypad,
2284
+ reply_to_message_id,
2285
+ disable_notification,
2286
+ chat_keypad_type,
2287
+ parse_mode=parse_mode
2288
+ )
2289
+
2290
+ async def get_avatar_me(self, save_as: str = None) -> str:
2291
+ session = None
2292
+ try:
2293
+ me_info = await self.get_me()
2294
+ avatar = me_info.get('data', {}).get('bot', {}).get('avatar', {})
2295
+ file_id = avatar.get('file_id')
2296
+ if not file_id:
2297
+ return "null"
2298
+
2299
+ file_info = await self.get_url_file(file_id)
2300
+ url = file_info.get("download_url") if isinstance(file_info, dict) else file_info
2301
+
2302
+ if save_as:
2303
+ session = aiohttp.ClientSession()
2304
+ async with session.get(url) as resp:
2305
+ if resp.status == 200:
2306
+ content = await resp.read()
2307
+ with open(save_as, "wb") as f:
2308
+ f.write(content)
2309
+
2310
+ return url
2311
+ except Exception as e:
2312
+ print(f"[get_avatar_me] Error: {e}")
2313
+ return "null"
2314
+ finally:
2315
+ if session and not session.closed:
2316
+ await session.close()
2317
+
1695
2318
  async def get_name(self, chat_id: str) -> str:
1696
2319
  try:
1697
2320
  chat = await self.get_chat(chat_id)
@@ -1709,4 +2332,184 @@ class Robot:
1709
2332
  except Exception:return "null"
1710
2333
  async def get_username(self, chat_id: str) -> str:
1711
2334
  chat_info = await self.get_chat(chat_id)
1712
- return chat_info.get("data", {}).get("chat", {}).get("username", "None")
2335
+ return chat_info.get("data", {}).get("chat", {}).get("username", "None")
2336
+ async def send_bulk_message(
2337
+ self,
2338
+ chat_ids: List[str],
2339
+ text: str,
2340
+ concurrency: int = 5,
2341
+ delay_between: float = 0.0,
2342
+ log_errors: bool = True,
2343
+ **kwargs
2344
+ ) -> Dict[str, Optional[Dict]]:
2345
+ if not chat_ids:return {}
2346
+ semaphore = asyncio.Semaphore(concurrency)
2347
+ results: Dict[str, Optional[Dict]] = {}
2348
+ async def _send(chat_id: str):
2349
+ async with semaphore:
2350
+ try:
2351
+ res = await self.send_message(chat_id, text, **kwargs)
2352
+ results[chat_id] = res
2353
+ except Exception as e:
2354
+ results[chat_id] = None
2355
+ if log_errors:print(f"[send_bulk_message] Error {chat_id} : {e}")
2356
+ if delay_between > 0:await asyncio.sleep(delay_between)
2357
+ await asyncio.gather(*[_send(cid) for cid in chat_ids])
2358
+ return results
2359
+ async def delete_bulk_message(self, chat_id: str, message_ids: list[str]):
2360
+ tasks = [self.delete_message(chat_id, mid) for mid in message_ids]
2361
+ return await asyncio.gather(*tasks, return_exceptions=True)
2362
+ async def edit_bulk_message(self, chat_id: str, messages: dict[str, str]):
2363
+ tasks = [self.edit_message_text(chat_id, mid, new_text) for mid, new_text in messages.items()]
2364
+ return await asyncio.gather(*tasks, return_exceptions=True)
2365
+ async def send_scheduled_message(self, chat_id: str, text: str, delay: int, **kwargs):
2366
+ await asyncio.sleep(delay)
2367
+ return await self.send_message(chat_id, text, **kwargs)
2368
+ async def disable_inline_keyboard(
2369
+ self,
2370
+ chat_id: str,
2371
+ message_id: str,
2372
+ text: Optional[str] = "~",
2373
+ delay: float = 5.0,
2374
+ ) -> Dict[str, any]:
2375
+ if text is not None:await self.edit_inline_keypad(chat_id, message_id, inline_keypad={}, text=text)
2376
+ if delay > 0:
2377
+ await asyncio.sleep(delay)
2378
+ response = await self.edit_inline_keypad(chat_id, message_id, inline_keypad={})
2379
+ return response
2380
+ else:return await self.edit_inline_keypad(chat_id, message_id, inline_keypad={})
2381
+ async def get_chat_admins(self, chat_id: str) -> Dict[str, Any]:
2382
+ return await self._post("getChatAdmins", {"chat_id": chat_id})
2383
+ async def get_chat_members(self, chat_id: str, start_id: str = "") -> Dict[str, Any]:
2384
+ return await self._post("getChatMembers", {"chat_id": chat_id, "start_id": start_id})
2385
+ async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
2386
+ return await self._post("getChatInfo", {"chat_id": chat_id})
2387
+ async def set_chat_title(self, chat_id: str, title: str) -> Dict[str, Any]:
2388
+ return await self._post("editChatTitle", {"chat_id": chat_id, "title": title})
2389
+ async def set_chat_description(self, chat_id: str, description: str) -> Dict[str, Any]:
2390
+ return await self._post("editChatDescription", {"chat_id": chat_id, "description": description})
2391
+ async def set_chat_photo(self, chat_id: str, file_id: str) -> Dict[str, Any]:
2392
+ return await self._post("editChatPhoto", {"chat_id": chat_id, "file_id": file_id})
2393
+ async def remove_chat_photo(self, chat_id: str) -> Dict[str, Any]:
2394
+ return await self._post("editChatPhoto", {"chat_id": chat_id, "file_id": "Remove"})
2395
+ async def add_member_chat(self, chat_id: str, user_ids: list[str]) -> Dict[str, Any]:
2396
+ return await self._post("addChatMembers", {"chat_id": chat_id, "member_ids": user_ids})
2397
+ async def ban_member_chat(self, chat_id: str, user_id: str) -> Dict[str, Any]:
2398
+ return await self._post("banChatMember", {"chat_id": chat_id, "member_id": user_id})
2399
+ async def unban_chat_member(self, chat_id: str, user_id: str) -> Dict[str, Any]:
2400
+ return await self._post("unbanChatMember", {"chat_id": chat_id, "member_id": user_id})
2401
+ async def restrict_chat_member(self, chat_id: str, user_id: str, until: int = 0) -> Dict[str, Any]:
2402
+ return await self._post("restrictChatMember", {"chat_id": chat_id, "member_id": user_id, "until_date": until})
2403
+ async def get_chat_member(self, chat_id: str, user_id: str):
2404
+ return await self._post("getChatMember", {"chat_id": chat_id, "user_id": user_id})
2405
+ async def get_admin_chat(self, chat_id: str):
2406
+ return await self._post("getChatAdministrators", {"chat_id": chat_id})
2407
+ async def get_chat_member_count(self, chat_id: str):
2408
+ return await self._post("getChatMemberCount", {"chat_id": chat_id})
2409
+ async def ban_chat_member(self, chat_id: str, user_id: str):
2410
+ return await self._post("banChatMember", {"chat_id": chat_id, "user_id": user_id})
2411
+ async def promote_chat_member(self, chat_id: str, user_id: str, rights: dict) -> Dict[str, Any]:
2412
+ return await self._post("promoteChatMember", {"chat_id": chat_id, "member_id": user_id, "rights": rights})
2413
+ async def demote_chat_member(self, chat_id: str, user_id: str) -> Dict[str, Any]:
2414
+ return await self._post("promoteChatMember", {"chat_id": chat_id, "member_id": user_id, "rights": {}})
2415
+ async def pin_chat_message(self, chat_id: str, message_id: str) -> Dict[str, Any]:
2416
+ return await self._post("pinChatMessage", {"chat_id": chat_id, "message_id": message_id})
2417
+ async def unpin_chat_message(self, chat_id: str, message_id: str = "") -> Dict[str, Any]:
2418
+ return await self._post("unpinChatMessage", {"chat_id": chat_id, "message_id": message_id})
2419
+ async def export_chat_invite_link(self, chat_id: str) -> Dict[str, Any]:
2420
+ return await self._post("exportChatInviteLink", {"chat_id": chat_id})
2421
+ async def revoke_chat_invite_link(self, chat_id: str, link: str) -> Dict[str, Any]:
2422
+ return await self._post("revokeChatInviteLink", {"chat_id": chat_id, "invite_link": link})
2423
+ async def create_group(self, title: str, user_ids: list[str]) -> Dict[str, Any]:
2424
+ return await self._post("createGroup", {"title": title, "user_ids": user_ids})
2425
+ async def create_channel(self, title: str, description: str = "") -> Dict[str, Any]:
2426
+ return await self._post("createChannel", {"title": title, "description": description})
2427
+ async def leave_chat(self, chat_id: str) -> Dict[str, Any]:
2428
+ return await self._post("leaveChat", {"chat_id": chat_id})
2429
+ async def forward_message(self, from_chat_id: str, message_id: str, to_chat_id: str, disable_notification: bool = False) -> Dict[str, Any]:
2430
+ 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})
2431
+ async def edit_message_text(self, chat_id: str, message_id: str, text: str, parse_mode: Optional[Literal["HTML", "Markdown"]] = None) -> Dict[str, Any]:
2432
+ payload = {
2433
+ "chat_id": chat_id,
2434
+ "message_id": message_id,
2435
+ "text": text,
2436
+ }
2437
+ parse_mode_to_use = parse_mode or self.parse_mode
2438
+ if text:
2439
+ text, metadata = self._parse_text_metadata(text, parse_mode_to_use)
2440
+ payload["text"] = text
2441
+ if metadata:
2442
+ payload["metadata"] = metadata
2443
+ return await self._post("editMessageText", payload)
2444
+ async def edit_inline_keypad(self,chat_id: str,message_id: str,inline_keypad: Dict[str, Any],text: str = None) -> Dict[str, Any]:
2445
+ if text is not None:await self._post("editMessageText", {"chat_id": chat_id,"message_id": message_id,"text": text})
2446
+ return await self._post("editMessageKeypad", {"chat_id": chat_id,"message_id": message_id,"inline_keypad": inline_keypad})
2447
+ async def delete_message(self, chat_id: str, message_id: str) -> Dict[str, Any]:
2448
+ return await self._post("deleteMessage", {"chat_id": chat_id, "message_id": message_id})
2449
+ async def set_commands(self, bot_commands: List[Dict[str, str]]) -> Dict[str, Any]:
2450
+ return await self._post("setCommands", {"bot_commands": bot_commands})
2451
+ async def update_bot_endpoint(self, url: str, type: str) -> Dict[str, Any]:
2452
+ return await self._post("updateBotEndpoints", {"url": url, "type": type})
2453
+ async def remove_keypad(self, chat_id: str) -> Dict[str, Any]:
2454
+ return await self._post("editChatKeypad", {"chat_id": chat_id, "chat_keypad_type": "Remove"})
2455
+ async def edit_chat_keypad(self, chat_id: str, chat_keypad: Dict[str, Any]) -> Dict[str, Any]:
2456
+ return await self._post("editChatKeypad", {"chat_id": chat_id, "chat_keypad_type": "New", "chat_keypad": chat_keypad})
2457
+ 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]:
2458
+ 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})
2459
+ async def get_chat(self, chat_id: str) -> Dict[str, Any]:
2460
+ return await self._post("getChat", {"chat_id": chat_id})
2461
+
2462
+ def get_all_member(self, channel_guid: str, search_text: str = None, start_id: str = None, just_get_guids: bool = False):
2463
+ client = self._get_client()
2464
+ return client.get_all_members(channel_guid, search_text, start_id, just_get_guids)
2465
+ async def send_poll(
2466
+ self,
2467
+ chat_id: str,
2468
+ question: str,
2469
+ options: List[str],
2470
+ type: Literal["Regular", "Quiz"] = "Regular",
2471
+ allows_multiple_answers: bool = False,
2472
+ is_anonymous: bool = True,
2473
+ correct_option_index: Optional[int] = None,
2474
+ hint: Optional[str] = None,
2475
+ reply_to_message_id: Optional[str] = None,
2476
+ disable_notification: bool = False,
2477
+ inline_keypad: Optional[Dict[str, Any]] = None,
2478
+ chat_keypad: Optional[Dict[str, Any]] = None,
2479
+ chat_keypad_type: Optional[Literal["New", "Remove", "None"]] = None,
2480
+ ) -> AttrDict:
2481
+
2482
+ payload = {
2483
+ "chat_id": chat_id,
2484
+ "question": question,
2485
+ "options": options,
2486
+ "type": type,
2487
+ "allows_multiple_answers": allows_multiple_answers,
2488
+ "is_anonymous": is_anonymous,
2489
+ "correct_option_index": correct_option_index,
2490
+ "explanation": hint,
2491
+ "reply_to_message_id": reply_to_message_id,
2492
+ "disable_notification": disable_notification,
2493
+ "inline_keypad": inline_keypad,
2494
+ "chat_keypad": chat_keypad,
2495
+ "chat_keypad_type": chat_keypad_type,
2496
+ }
2497
+ 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)}
2498
+ return await self._post("sendPoll", payload)
2499
+
2500
+ async def check_join(self, channel_guid: str, chat_id: str = None) -> Union[bool, list[str]]:
2501
+ client = self._get_client()
2502
+ if chat_id:
2503
+ chat_info_data = await self.get_chat(chat_id)
2504
+ chat_info = chat_info_data.get('data', {}).get('chat', {})
2505
+ username = chat_info.get('username')
2506
+ first_name = chat_info.get("first_name", "")
2507
+ if username:
2508
+ result = await asyncio.to_thread(self.get_all_member, channel_guid, search_text=username)
2509
+ members = result.get('in_chat_members', [])
2510
+ return any(m.get('username') == username for m in members)
2511
+ elif first_name:
2512
+ result = await asyncio.to_thread(self.get_all_member, channel_guid, search_text=first_name)
2513
+ members = result.get('in_chat_members', [])
2514
+ return any(m.get('first_name') == first_name for m in members)
2515
+ return False