Rubka 4.5.0__py3-none-any.whl → 4.5.11__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/api.py CHANGED
@@ -13,6 +13,8 @@ import tempfile
13
13
  from tqdm import tqdm
14
14
  import os
15
15
  API_URL = "https://botapi.rubika.ir/v3"
16
+ import mimetypes
17
+ import re
16
18
  import sys
17
19
  import subprocess
18
20
  def install_package(package_name):
@@ -109,14 +111,16 @@ class Robot:
109
111
  self.Key = Key
110
112
  self.platform = platform
111
113
  self.web_hook = web_hook
114
+ self.hook = web_hook
112
115
  self._offset_id = None
113
116
  self.session = requests.Session()
114
117
  self.sessions: Dict[str, Dict[str, Any]] = {}
115
118
  self._callback_handler = None
116
119
  self._message_handler = None
117
120
  self._inline_query_handler = None
121
+ self._message_handlers: List[dict] = []
118
122
  self._callback_handlers = None
119
- self._callback_handlers = [] # ✅ این خط مهمه
123
+ self._callback_handlers = []
120
124
  if web_hook:
121
125
  try:
122
126
  json_url = requests.get(web_hook, timeout=self.timeout).json().get('url', web_hook)
@@ -132,7 +136,7 @@ class Robot:
132
136
  except Exception as e:
133
137
  logger.error(f"Failed to set webhook from {web_hook}: {e}")
134
138
  else:
135
- self.web_hook = None
139
+ self.web_hook = self.hook
136
140
 
137
141
 
138
142
 
@@ -161,11 +165,11 @@ class Robot:
161
165
  return self._post("getMe", {})
162
166
  def on_message(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
163
167
  def decorator(func: Callable[[Any, Message], None]):
164
- self._message_handler = {
168
+ self._message_handlers.append({
165
169
  "func": func,
166
170
  "filters": filters,
167
171
  "commands": commands
168
- }
172
+ })
169
173
  return func
170
174
  return decorator
171
175
 
@@ -200,64 +204,52 @@ class Robot:
200
204
 
201
205
 
202
206
  def _process_update(self, update: dict):
203
- import threading, time
207
+ import threading
204
208
 
205
- # هندل پیام inline (معمولاً type = ReceiveQuery)
206
209
  if update.get("type") == "ReceiveQuery":
207
210
  msg = update.get("inline_message", {})
208
- if update.get("type") == "ReceiveQuery":
209
- msg = update.get("inline_message", {})
210
- context = InlineMessage(bot=self, raw_data=msg)
211
- threading.Thread(target=self._handle_inline_query, args=(context,), daemon=True).start()
212
- return
211
+ context = InlineMessage(bot=self, raw_data=msg)
212
+ threading.Thread(target=self._handle_inline_query, args=(context,), daemon=True).start()
213
+ return
213
214
 
214
- # هندل پیام جدید متنی
215
215
  if update.get("type") == "NewMessage":
216
216
  msg = update.get("new_message", {})
217
- chat_id = update.get("chat_id")
218
- message_id = msg.get("message_id")
219
- sender_id = msg.get("sender_id")
220
- text = msg.get("text")
221
-
222
- # فیلتر زمان پیام (مثلاً پیام‌های خیلی قدیمی)
223
217
  try:
224
218
  if msg.get("time") and (time.time() - float(msg["time"])) > 20:
225
219
  return
226
220
  except Exception:
227
221
  return
228
222
 
229
- # ساخت context پیام
230
- context = Message(bot=self, chat_id=chat_id, message_id=message_id, sender_id=sender_id, text=text, raw_data=msg)
223
+ context = Message(bot=self,
224
+ chat_id=update.get("chat_id"),
225
+ message_id=msg.get("message_id"),
226
+ sender_id=msg.get("sender_id"),
227
+ text=msg.get("text"),
228
+ raw_data=msg)
231
229
 
232
- # هندل callback ها (دکمه‌ها)
233
- if context.aux_data and hasattr(self, "_callback_handlers"):
230
+ if context.aux_data and self._callback_handlers:
234
231
  for handler in self._callback_handlers:
235
232
  if not handler["button_id"] or context.aux_data.button_id == handler["button_id"]:
236
233
  threading.Thread(target=handler["func"], args=(self, context), daemon=True).start()
237
- return # فقط یک callback اجرا شود
238
-
239
- # هندل پیام‌های متنی معمولی
240
- if hasattr(self, "_message_handler") and self._message_handler:
241
- handler = self._message_handler
242
-
243
- if handler["commands"]:
244
- if not context.text or not context.text.startswith("/"):
245
- return
246
- parts = context.text.split()
247
- cmd = parts[0][1:]
248
- if cmd not in handler["commands"]:
249
234
  return
250
- context.args = parts[1:]
251
235
 
252
- if handler["filters"] and not handler["filters"](context):
236
+ if self._message_handlers:
237
+ for handler in self._message_handlers:
238
+ if handler["commands"]:
239
+ if not context.text or not context.text.startswith("/"):
240
+ continue
241
+ parts = context.text.split()
242
+ cmd = parts[0][1:]
243
+ if cmd not in handler["commands"]:
244
+ continue
245
+ context.args = parts[1:]
246
+
247
+ if handler["filters"] and not handler["filters"](context):
248
+ continue
249
+
250
+ threading.Thread(target=handler["func"], args=(self, context), daemon=True).start()
253
251
  return
254
252
 
255
- threading.Thread(target=handler["func"], args=(self, context), daemon=True).start()
256
-
257
-
258
-
259
-
260
-
261
253
  def get_updates(
262
254
  self,
263
255
  offset_id: Optional[str] = None,
@@ -284,12 +276,10 @@ class Robot:
284
276
  def _is_duplicate(self, message_id: str, max_age_sec: int = 300) -> bool:
285
277
  now = time.time()
286
278
 
287
- # حذف پیام‌های قدیمی‌تر از max_age_sec
288
279
  expired = [mid for mid, ts in self._processed_message_ids.items() if now - ts > max_age_sec]
289
280
  for mid in expired:
290
281
  del self._processed_message_ids[mid]
291
282
 
292
- # بررسی تکراری بودن پیام
293
283
  if message_id in self._processed_message_ids:
294
284
  return True
295
285
 
@@ -303,7 +293,6 @@ class Robot:
303
293
  def run(self):
304
294
  print("Bot started running...")
305
295
  self._processed_message_ids: Dict[str, float] = {}
306
- # self._offset_id می‌تواند قبلاً مقداردهی شده باشد
307
296
 
308
297
  while True:
309
298
  try:
@@ -314,12 +303,11 @@ class Robot:
314
303
  for item in updates:
315
304
  data = item.get("data", {})
316
305
 
317
- # بررسی زمان دریافت پیام و رد کردن پیام‌های قدیمی (بیش از 20 ثانیه)
318
306
  received_at_str = item.get("received_at")
319
307
  if received_at_str:
320
308
  received_at_ts = datetime.datetime.strptime(received_at_str, "%Y-%m-%d %H:%M:%S").timestamp()
321
309
  if time.time() - received_at_ts > 20:
322
- continue # رد کردن پیام قدیمی
310
+ continue
323
311
 
324
312
  update = None
325
313
  if "update" in data:
@@ -348,7 +336,6 @@ class Robot:
348
336
 
349
337
  self._process_update(update)
350
338
 
351
- # ثبت پیام به عنوان پردازش شده
352
339
  if message_id:
353
340
  self._processed_message_ids[message_id] = time.time()
354
341
 
@@ -436,6 +423,10 @@ class Robot:
436
423
 
437
424
  return False
438
425
 
426
+ def get_url_file(self,file_id):
427
+ data = self._post("getFile", {'file_id': file_id})
428
+ return data.get("data").get("download_url")
429
+
439
430
 
440
431
  def get_all_member(
441
432
  self,
@@ -484,7 +475,6 @@ class Robot:
484
475
  "reply_to_message_id": reply_to_message_id,
485
476
  "chat_keypad_type": chat_keypad_type
486
477
  }
487
- # Remove None values
488
478
  payload = {k: v for k, v in payload.items() if v is not None}
489
479
  return self._post("sendLocation", payload)
490
480
 
@@ -504,7 +494,63 @@ class Robot:
504
494
  "last_name": last_name,
505
495
  "phone_number": phone_number
506
496
  })
497
+ def download(self,file_id: str, save_as: str = None, chunk_size: int = 1024 * 512, timeout_sec: int = 60, verbose: bool = False):
498
+ """
499
+ Download a file from server using its file_id with chunked transfer,
500
+ progress bar, file extension detection, custom filename, and timeout.
501
+
502
+ If save_as is not provided, filename will be extracted from
503
+ Content-Disposition header or Content-Type header extension.
507
504
 
505
+ Parameters:
506
+ file_id (str): The file ID to fetch the download URL.
507
+ save_as (str, optional): Custom filename to save. If None, automatically detected.
508
+ chunk_size (int, optional): Size of each chunk in bytes. Default 512KB.
509
+ timeout_sec (int, optional): HTTP timeout in seconds. Default 60.
510
+ verbose (bool, optional): Show progress messages. Default True.
511
+
512
+ Returns:
513
+ bool: True if success, raises exceptions otherwise.
514
+ """
515
+ try:
516
+ url = self.get_url_file(file_id)
517
+ if not url:
518
+ raise ValueError("Download URL not found in response.")
519
+ except Exception as e:
520
+ raise ValueError(f"Failed to get download URL: {e}")
521
+
522
+ try:
523
+ with requests.get(url, stream=True, timeout=timeout_sec) as resp:
524
+ if resp.status_code != 200:
525
+ raise requests.HTTPError(f"Failed to download file. Status code: {resp.status_code}")
526
+
527
+ if not save_as:
528
+ content_disp = resp.headers.get("Content-Disposition", "")
529
+ match = re.search(r'filename="?([^\";]+)"?', content_disp)
530
+ if match:
531
+ save_as = match.group(1)
532
+ else:
533
+ content_type = resp.headers.get("Content-Type", "").split(";")[0]
534
+ extension = mimetypes.guess_extension(content_type) or ".bin"
535
+ save_as = f"{file_id}{extension}"
536
+
537
+ total_size = int(resp.headers.get("Content-Length", 0))
538
+ progress = tqdm(total=total_size, unit="B", unit_scale=True)
539
+
540
+ with open(save_as, "wb") as f:
541
+ for chunk in resp.iter_content(chunk_size=chunk_size):
542
+ if chunk:
543
+ f.write(chunk)
544
+ progress.update(len(chunk))
545
+
546
+ progress.close()
547
+ if verbose:
548
+ print(f"File saved as: {save_as}")
549
+
550
+ return True
551
+
552
+ except Exception as e:
553
+ raise RuntimeError(f"Download failed: {e}")
508
554
  def get_chat(self, chat_id: str) -> Dict[str, Any]:
509
555
  """Get chat info."""
510
556
  return self._post("getChat", {"chat_id": chat_id})
@@ -605,7 +651,64 @@ class Robot:
605
651
  payload["reply_to_message_id"] = str(reply_to_message_id)
606
652
 
607
653
  return self._post("sendFile", payload)
608
-
654
+ def send_file(
655
+ self,
656
+ chat_id: str,
657
+ path: Optional[Union[str, Path]] = None,
658
+ file_id: Optional[str] = None,
659
+ caption: Optional[str] = None,
660
+ file_name: Optional[str] = None,
661
+ inline_keypad: Optional[Dict[str, Any]] = None,
662
+ chat_keypad: Optional[Dict[str, Any]] = None,
663
+ reply_to_message_id: Optional[str] = None,
664
+ disable_notification: bool = False,
665
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
666
+ ) -> Dict[str, Any]:
667
+ if path:
668
+ file_name = file_name or Path(path).name
669
+ upload_url = self.get_upload_url("File")
670
+ file_id = self.upload_media_file(upload_url, file_name, path)
671
+ if not file_id:
672
+ raise ValueError("Either path or file_id must be provided.")
673
+ return self._send_uploaded_file(
674
+ chat_id=chat_id,
675
+ file_id=file_id,
676
+ text=caption,
677
+ inline_keypad=inline_keypad,
678
+ chat_keypad=chat_keypad,
679
+ reply_to_message_id=reply_to_message_id,
680
+ disable_notification=disable_notification,
681
+ chat_keypad_type=chat_keypad_type
682
+ )
683
+ def re_send_file(
684
+ self,
685
+ chat_id: str,
686
+ path: Optional[Union[str, Path]] = None,
687
+ file_id: Optional[str] = None,
688
+ caption: Optional[str] = None,
689
+ file_name: Optional[str] = None,
690
+ inline_keypad: Optional[Dict[str, Any]] = None,
691
+ chat_keypad: Optional[Dict[str, Any]] = None,
692
+ reply_to_message_id: Optional[str] = None,
693
+ disable_notification: bool = False,
694
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
695
+ ) -> Dict[str, Any]:
696
+ if path:
697
+ file_name = file_name or Path(path).name
698
+ upload_url = self.get_upload_url("File")
699
+ file_id = self.upload_media_file(upload_url, file_name, path)
700
+ if not file_id:
701
+ raise ValueError("Either path or file_id must be provided.")
702
+ return self._send_uploaded_file(
703
+ chat_id=chat_id,
704
+ file_id=file_id,
705
+ text=caption,
706
+ inline_keypad=inline_keypad,
707
+ chat_keypad=chat_keypad,
708
+ reply_to_message_id=reply_to_message_id,
709
+ disable_notification=disable_notification,
710
+ chat_keypad_type=chat_keypad_type
711
+ )
609
712
  def send_document(
610
713
  self,
611
714
  chat_id: str,
@@ -617,7 +720,7 @@ class Robot:
617
720
  chat_keypad: Optional[Dict[str, Any]] = None,
618
721
  reply_to_message_id: Optional[str] = None,
619
722
  disable_notification: bool = False,
620
- chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New"
723
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
621
724
  ) -> Dict[str, Any]:
622
725
  if path:
623
726
  file_name = file_name or Path(path).name
@@ -646,7 +749,7 @@ class Robot:
646
749
  chat_keypad: Optional[Dict[str, Any]] = None,
647
750
  reply_to_message_id: Optional[str] = None,
648
751
  disable_notification: bool = False,
649
- chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New"
752
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
650
753
  ) -> Dict[str, Any]:
651
754
  if path:
652
755
  file_name = file_name or Path(path).name
@@ -675,7 +778,7 @@ class Robot:
675
778
  chat_keypad: Optional[Dict[str, Any]] = None,
676
779
  reply_to_message_id: Optional[str] = None,
677
780
  disable_notification: bool = False,
678
- chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New"
781
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
679
782
  ) -> Dict[str, Any]:
680
783
  if path:
681
784
  file_name = file_name or Path(path).name
@@ -704,7 +807,7 @@ class Robot:
704
807
  chat_keypad: Optional[Dict[str, Any]] = None,
705
808
  reply_to_message_id: Optional[str] = None,
706
809
  disable_notification: bool = False,
707
- chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New"
810
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
708
811
  ) -> Dict[str, Any]:
709
812
  if path:
710
813
  file_name = file_name or Path(path).name
@@ -733,7 +836,7 @@ class Robot:
733
836
  chat_keypad: Optional[Dict[str, Any]] = None,
734
837
  reply_to_message_id: Optional[str] = None,
735
838
  disable_notification: bool = False,
736
- chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New"
839
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
737
840
  ) -> Dict[str, Any]:
738
841
  if path:
739
842
  file_name = file_name or Path(path).name
@@ -762,7 +865,7 @@ class Robot:
762
865
  chat_keypad: Optional[Dict[str, Any]] = None,
763
866
  reply_to_message_id: Optional[str] = None,
764
867
  disable_notification: bool = False,
765
- chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New"
868
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
766
869
  ) -> Dict[str, Any]:
767
870
  if path:
768
871
  file_name = file_name or Path(path).name
rubka/asynco.py CHANGED
@@ -8,8 +8,12 @@ from .logger import logger
8
8
  try:
9
9
  from .context import Message, InlineMessage
10
10
  except (ImportError, ModuleNotFoundError):
11
- # اگر به صورت مستقیم اجرا شود، از این حالت استفاده می‌کند
12
11
  from context import Message, InlineMessage
12
+
13
+ from tqdm.asyncio import tqdm
14
+ from urllib.parse import urlparse, parse_qs
15
+
16
+ import mimetypes
13
17
  from pathlib import Path
14
18
  import time
15
19
  import datetime
@@ -119,6 +123,7 @@ class Robot:
119
123
  self._inline_query_handler = None
120
124
  self._callback_handlers: List[dict] = []
121
125
  self._processed_message_ids: Dict[str, float] = {}
126
+ self._message_handlers: List[dict] = []
122
127
 
123
128
  logger.info(f"Initialized RubikaBot with token: {token[:8]}***")
124
129
 
@@ -184,11 +189,11 @@ class Robot:
184
189
 
185
190
  def on_message(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
186
191
  def decorator(func: Callable[[Any, Message], None]):
187
- self._message_handler = {
192
+ self._message_handlers.append({
188
193
  "func": func,
189
194
  "filters": filters,
190
195
  "commands": commands
191
- }
196
+ })
192
197
  return func
193
198
  return decorator
194
199
 
@@ -244,27 +249,40 @@ class Robot:
244
249
  text=msg.get("text"),
245
250
  raw_data=msg)
246
251
 
252
+ # پردازش دکمه‌های شیشه‌ای (بدون تغییر)
247
253
  if context.aux_data and self._callback_handlers:
248
254
  for handler in self._callback_handlers:
249
255
  if not handler["button_id"] or context.aux_data.button_id == handler["button_id"]:
250
256
  asyncio.create_task(handler["func"](self, context))
251
257
  return
252
258
 
253
- if self._message_handler:
254
- handler_info = self._message_handler
255
- if handler_info["commands"]:
256
- if not context.text or not context.text.startswith("/"):
257
- return
258
- parts = context.text.split()
259
- cmd = parts[0][1:]
260
- if cmd not in handler_info["commands"]:
261
- return
262
- context.args = parts[1:]
263
-
264
- if handler_info["filters"] and not handler_info["filters"](context):
265
- return
266
-
267
- asyncio.create_task(handler_info["func"](self, context))
259
+ # پردازش پیام‌های متنی با حلقه روی تمام هندلرها
260
+ if self._message_handlers:
261
+ for handler_info in self._message_handlers:
262
+ # بررسی شرط دستورات (commands)
263
+ if handler_info["commands"]:
264
+ if not context.text or not context.text.startswith("/"):
265
+ continue # اگر پیام کامند نبود، این هندلر را رد کن
266
+ parts = context.text.split()
267
+ cmd = parts[0][1:]
268
+ if cmd not in handler_info["commands"]:
269
+ continue # اگر کامند مطابقت نداشت، این هندلر را رد کن
270
+ context.args = parts[1:]
271
+
272
+ # بررسی شرط فیلترها (filters)
273
+ if handler_info["filters"]:
274
+ if not handler_info["filters"](context):
275
+ continue # اگر فیلتر برقرار نبود، این هندلر را رد کن
276
+
277
+ # اگر هندلری برای همه پیام‌ها باشد (بدون کامند و فیلتر)
278
+ if not handler_info["commands"] and not handler_info["filters"]:
279
+ asyncio.create_task(handler_info["func"](self, context))
280
+ return # بعد از یافتن هندلر مناسب، از حلقه خارج شو
281
+
282
+ # اگر شرایط کامند یا فیلتر برقرار بود
283
+ if handler_info["commands"] or handler_info["filters"]:
284
+ asyncio.create_task(handler_info["func"](self, context))
285
+ return # بعد از یافتن هندلر مناسب، از حلقه خارج شو
268
286
 
269
287
  async def get_updates(self, offset_id: Optional[str] = None, limit: Optional[int] = None) -> Dict[str, Any]:
270
288
  data = {}
@@ -293,6 +311,7 @@ class Robot:
293
311
 
294
312
  self._processed_message_ids[message_id] = now
295
313
  return False
314
+
296
315
 
297
316
  async def run(self):
298
317
  """
@@ -370,14 +389,40 @@ class Robot:
370
389
  await self._aiohttp_session.close()
371
390
  print("Bot stopped and session closed.")
372
391
 
373
- async def send_message(self, chat_id: str, text: str, 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) -> Dict[str, Any]:
374
- payload = {"chat_id": chat_id, "text": text, "disable_notification": disable_notification}
375
- if chat_keypad: payload["chat_keypad"] = chat_keypad
376
- if inline_keypad: payload["inline_keypad"] = inline_keypad
377
- if reply_to_message_id: payload["reply_to_message_id"] = reply_to_message_id
378
- if chat_keypad_type: payload["chat_keypad_type"] = chat_keypad_type
392
+ async def send_message(
393
+ self,
394
+ chat_id: str,
395
+ text: str,
396
+ chat_keypad: Optional[Dict[str, Any]] = None,
397
+ inline_keypad: Optional[Dict[str, Any]] = None,
398
+ disable_notification: bool = False,
399
+ reply_to_message_id: Optional[str] = None,
400
+ chat_keypad_type: Optional[Literal["New", "Removed"]] = None
401
+ ) -> Dict[str, Any]:
402
+ payload = {
403
+ "chat_id": chat_id,
404
+ "text": text,
405
+ "disable_notification": disable_notification,
406
+ }
407
+
408
+ if chat_keypad:
409
+ payload["chat_keypad"] = chat_keypad
410
+ payload["chat_keypad_type"] = chat_keypad_type or "New"
411
+
412
+ if inline_keypad:
413
+ payload["inline_keypad"] = inline_keypad
414
+
415
+ if reply_to_message_id:
416
+ payload["reply_to_message_id"] = reply_to_message_id
417
+
379
418
  return await self._post("sendMessage", payload)
380
419
 
420
+
421
+
422
+ async def get_url_file(self,file_id):
423
+ data = await self._post("getFile", {'file_id': file_id})
424
+ return data.get("data").get("download_url")
425
+
381
426
  def _get_client(self) -> Client_get:
382
427
  if self.session_name:
383
428
  return Client_get(self.session_name, self.auth, self.Key, self.platform)
@@ -440,7 +485,7 @@ class Robot:
440
485
  path = temp_file.name
441
486
  is_temp_file = True
442
487
 
443
- file_size = os.path.getsize(path) # Note: os.path.getsize is sync, but fast enough for most cases. aiofiles can be used for async alternative if needed on huge file lists.
488
+ file_size = os.path.getsize(path)
444
489
 
445
490
  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)
446
491
 
@@ -464,9 +509,100 @@ class Robot:
464
509
  json_data = await response.json()
465
510
  if is_temp_file:
466
511
  os.remove(path)
467
-
512
+ print(json_data)
468
513
  return json_data.get('data', {}).get('file_id')
469
514
 
515
+
516
+ def get_extension(content_type: str) -> str:
517
+ ext = mimetypes.guess_extension(content_type)
518
+ return ext if ext else ''
519
+
520
+ async def download(self, file_id: str, save_as: str = None, chunk_size: int = 1024 * 512,timeout_sec: int = 60, verbose: bool = False):
521
+ """
522
+ Download a file from server using its file_id with chunked transfer,
523
+ progress bar, file extension detection, custom filename, and timeout.
524
+
525
+ If save_as is not provided, filename will be extracted from
526
+ Content-Disposition header or Content-Type header extension.
527
+
528
+ Parameters:
529
+ file_id (str): The file ID to fetch the download URL.
530
+ save_as (str, optional): Custom filename to save. If None, automatically detected.
531
+ chunk_size (int, optional): Size of each chunk in bytes. Default 512KB.
532
+ timeout_sec (int, optional): HTTP timeout in seconds. Default 60.
533
+ verbose (bool, optional): Show progress messages. Default True.
534
+
535
+ Returns:
536
+ bool: True if success, raises exceptions otherwise.
537
+ """
538
+
539
+ try:
540
+ url = await self.get_url_file(file_id)
541
+ if not url:
542
+ raise ValueError("Download URL not found in response.")
543
+ except Exception as e:
544
+ raise ValueError(f"Failed to get download URL: {e}")
545
+
546
+ timeout = aiohttp.ClientTimeout(total=timeout_sec)
547
+
548
+ try:
549
+ async with aiohttp.ClientSession(timeout=timeout) as session:
550
+ async with session.get(url) as resp:
551
+ if resp.status != 200:
552
+ raise aiohttp.ClientResponseError(
553
+ request_info=resp.request_info,
554
+ history=resp.history,
555
+ status=resp.status,
556
+ message="Failed to download file.",
557
+ headers=resp.headers
558
+ )
559
+
560
+ if not save_as:
561
+ content_disp = resp.headers.get("Content-Disposition", "")
562
+ import re
563
+ match = re.search(r'filename="?([^\";]+)"?', content_disp)
564
+ if match:
565
+ save_as = match.group(1)
566
+ else:
567
+ content_type = resp.headers.get("Content-Type", "").split(";")[0]
568
+ extension = mimetypes.guess_extension(content_type) or ".bin"
569
+ save_as = f"{file_id}{extension}"
570
+
571
+ total_size = int(resp.headers.get("Content-Length", 0))
572
+ progress = tqdm(total=total_size, unit="B", unit_scale=True, disable=not verbose)
573
+
574
+ async with aiofiles.open(save_as, "wb") as f:
575
+ async for chunk in resp.content.iter_chunked(chunk_size):
576
+ await f.write(chunk)
577
+ progress.update(len(chunk))
578
+
579
+ progress.close()
580
+ if verbose:
581
+ print(f"✅ File saved as: {save_as}")
582
+
583
+ return True
584
+
585
+ except aiohttp.ClientError as e:
586
+ raise aiohttp.ClientError(f"HTTP error occurred: {e}")
587
+ except asyncio.TimeoutError:
588
+ raise asyncio.TimeoutError("Download timed out.")
589
+ except Exception as e:
590
+ raise Exception(f"Error downloading file: {e}")
591
+
592
+ except aiohttp.ClientError as e:
593
+ raise aiohttp.ClientError(f"HTTP error occurred: {e}")
594
+ except asyncio.TimeoutError:
595
+ raise asyncio.TimeoutError("The download operation timed out.")
596
+ except Exception as e:
597
+ raise Exception(f"An error occurred while downloading the file: {e}")
598
+
599
+ except aiohttp.ClientError as e:
600
+ raise aiohttp.ClientError(f"HTTP error occurred: {e}")
601
+ except asyncio.TimeoutError:
602
+ raise asyncio.TimeoutError("The download operation timed out.")
603
+ except Exception as e:
604
+ raise Exception(f"An error occurred while downloading the file: {e}")
605
+
470
606
  async def get_upload_url(self, media_type: Literal['File', 'Image', 'Voice', 'Music', 'Gif', 'Video']) -> str:
471
607
  allowed = ['File', 'Image', 'Voice', 'Music', 'Gif', 'Video']
472
608
  if media_type not in allowed:
@@ -490,22 +626,25 @@ class Robot:
490
626
  raise ValueError("Either path or file_id must be provided.")
491
627
  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)
492
628
 
493
- 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"]] = "New") -> Dict[str, Any]:
629
+ 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]:
494
630
  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)
495
-
496
- 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] = 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"]] = "New") -> Dict[str, Any]:
631
+ 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]:
632
+ 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)
633
+ 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]:
634
+ 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)
635
+ 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] = 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]:
497
636
  return await self._send_file_generic("Music", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
498
637
 
499
- 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"]] = "New") -> Dict[str, Any]:
638
+ 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]:
500
639
  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)
501
640
 
502
- 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"]] = "New") -> Dict[str, Any]:
641
+ 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]:
503
642
  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)
504
643
 
505
- 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"]] = "New") -> Dict[str, Any]:
644
+ 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]:
506
645
  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)
507
646
 
508
- 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"]] = "New") -> Dict[str, Any]:
647
+ 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]:
509
648
  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)
510
649
 
511
650
  async def forward_message(self, from_chat_id: str, message_id: str, to_chat_id: str, disable_notification: bool = False) -> Dict[str, Any]:
rubka/context.py CHANGED
@@ -242,6 +242,31 @@ class Message:
242
242
  chat_keypad_type: Optional[str] = "None",
243
243
  disable_notification: bool = False
244
244
  ):
245
+ if chat_keypad and chat_keypad_type == "none":chat_keypad_type == "New"
246
+ return self.bot.send_document(
247
+ chat_id=self.chat_id,
248
+ path=path,
249
+ file_id=file_id,
250
+ text=text,
251
+ chat_keypad=chat_keypad,
252
+ inline_keypad=inline_keypad,
253
+ chat_keypad_type=chat_keypad_type,
254
+ disable_notification=disable_notification,
255
+ reply_to_message_id=self.message_id
256
+ )
257
+ def reply_file(
258
+ self,
259
+ path: Optional[Union[str, Path]] = None,
260
+ file_id: Optional[str] = None,
261
+ text: Optional[str] = None,
262
+ chat_keypad: Optional[Dict[str, Any]] = None,
263
+ inline_keypad: Optional[Dict[str, Any]] = None,
264
+ chat_keypad_type: Optional[str] = "None",
265
+ disable_notification: bool = False
266
+ ):
267
+ if chat_keypad and chat_keypad_type == "none":
268
+ chat_keypad_type == "New"
269
+
245
270
  return self.bot.send_document(
246
271
  chat_id=self.chat_id,
247
272
  path=path,
@@ -264,6 +289,8 @@ class Message:
264
289
  chat_keypad_type: Optional[str] = "None",
265
290
  disable_notification: bool = False
266
291
  ):
292
+ if chat_keypad and chat_keypad_type == "none":
293
+ chat_keypad_type == "New"
267
294
  return self.bot.send_image(
268
295
  chat_id=self.chat_id,
269
296
  path=path,
@@ -286,6 +313,8 @@ class Message:
286
313
  chat_keypad_type: Optional[str] = "None",
287
314
  disable_notification: bool = False
288
315
  ):
316
+ if chat_keypad and chat_keypad_type == "none":
317
+ chat_keypad_type == "New"
289
318
  return self.bot.send_music(
290
319
  chat_id=self.chat_id,
291
320
  path=path,
@@ -308,6 +337,8 @@ class Message:
308
337
  chat_keypad_type: Optional[str] = "None",
309
338
  disable_notification: bool = False
310
339
  ):
340
+ if chat_keypad and chat_keypad_type == "none":
341
+ chat_keypad_type == "New"
311
342
  return self.bot.send_voice(
312
343
  chat_id=self.chat_id,
313
344
  path=path,
@@ -330,6 +361,7 @@ class Message:
330
361
  chat_keypad_type: Optional[str] = "None",
331
362
  disable_notification: bool = False
332
363
  ):
364
+ if chat_keypad and chat_keypad_type == "none":chat_keypad_type == "New"
333
365
  return self.bot.send_gif(
334
366
  chat_id=self.chat_id,
335
367
  path=path,
@@ -388,14 +420,6 @@ class Message:
388
420
  **kwargs
389
421
  })
390
422
 
391
- def reply_file(self, file_id: str, **kwargs) -> Dict[str, Any]:
392
- return self.bot._post("sendFile", {
393
- "chat_id": self.chat_id,
394
- "file_id": file_id,
395
- "reply_to_message_id": self.message_id,
396
- **kwargs
397
- })
398
-
399
423
  def edit(self, new_text: str) -> Dict[str, Any]:
400
424
  return self.bot.edit_message_text(
401
425
  chat_id=self.chat_id,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Rubka
3
- Version: 4.5.0
3
+ Version: 4.5.11
4
4
  Summary: A Python library for interacting with Rubika Bot API.
5
5
  Home-page: https://github.com/Mahdy-Ahmadi/Rubka
6
6
  Download-URL: https://github.com/Mahdy-Ahmadi/rubka/blob/main/project_library.zip
@@ -1,9 +1,9 @@
1
1
  rubka/__init__.py,sha256=TR1DABU5Maz2eO62ZEFiwOqNU0dH6l6HZfqRUxeo4eY,194
2
- rubka/api.py,sha256=KGcbBPJh7RaoYPuJTcpzAfhbgZ1PerSA5wn_tzelDq0,35045
3
- rubka/asynco.py,sha256=osfR2hKKEOT1Pqp0Meq3Iu-_Oox_SjwEesKJzv_Jqgo,30581
2
+ rubka/api.py,sha256=0gj52uxIGmLCVRUxnvjIHF6MVBiXnVBdVr_SRXp2I4M,39416
3
+ rubka/asynco.py,sha256=DDAeES9qDQ9vegHT-7X7RL72G510YObFN7qqJ948NEQ,37647
4
4
  rubka/button.py,sha256=4fMSZR7vUADxSmw1R3_pZ4dw5uMLZX5sOkwPPyNTBDE,8437
5
5
  rubka/config.py,sha256=Bck59xkOiqioLv0GkQ1qPGnBXVctz1hKk6LT4h2EPx0,78
6
- rubka/context.py,sha256=j1scXTy_wBY52MmMixfZyIQnB0sdYjRwx17-8ZZmyB4,17017
6
+ rubka/context.py,sha256=KXfDqn3vir6oIupF7piKD89Sqj1MjWoSerS4WyyHXBw,18062
7
7
  rubka/decorators.py,sha256=hGwUoE4q2ImrunJIGJ_kzGYYxQf1ueE0isadqraKEts,1157
8
8
  rubka/exceptions.py,sha256=tujZt1XrhWaw-lmdeVadVceUptpw4XzNgE44sAAY0gs,90
9
9
  rubka/jobs.py,sha256=GvLMLsVhcSEzRTgkvnPISPEBN71suW2xXI0hUaUZPTo,378
@@ -33,7 +33,7 @@ rubka/adaptorrubka/types/socket/message.py,sha256=0WgLMZh4eow8Zn7AiSX4C3GZjQTkIg
33
33
  rubka/adaptorrubka/utils/__init__.py,sha256=OgCFkXdNFh379quNwIVOAWY2NP5cIOxU5gDRRALTk4o,54
34
34
  rubka/adaptorrubka/utils/configs.py,sha256=nMUEOJh1NqDJsf9W9PurkN_DLYjO6kKPMm923i4Jj_A,492
35
35
  rubka/adaptorrubka/utils/utils.py,sha256=5-LioLNYX_TIbQGDeT50j7Sg9nAWH2LJUUs-iEXpsUY,8816
36
- rubka-4.5.0.dist-info/METADATA,sha256=3xURNUuDQNQR2PrKrMgqOn11urYLW82eYtHEmGyMoOw,33216
37
- rubka-4.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
- rubka-4.5.0.dist-info/top_level.txt,sha256=vy2A4lot11cRMdQS-F4HDCIXL3JK8RKfu7HMDkezJW4,6
39
- rubka-4.5.0.dist-info/RECORD,,
36
+ rubka-4.5.11.dist-info/METADATA,sha256=_9gtchOWeb6ROIeF58OO4Egk7uDSL6Rc-xvcvAasyKg,33217
37
+ rubka-4.5.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
+ rubka-4.5.11.dist-info/top_level.txt,sha256=vy2A4lot11cRMdQS-F4HDCIXL3JK8RKfu7HMDkezJW4,6
39
+ rubka-4.5.11.dist-info/RECORD,,
File without changes