Rubka 4.4.20__py3-none-any.whl → 4.5.2__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
@@ -7,13 +7,14 @@ from typing import Callable
7
7
  from .context import Message,InlineMessage
8
8
  from typing import Optional, Union, Literal, Dict, Any
9
9
  from pathlib import Path
10
- import requests
11
10
  import time
12
11
  import datetime
12
+ import tempfile
13
+ from tqdm import tqdm
14
+ import os
13
15
  API_URL = "https://botapi.rubika.ir/v3"
14
16
  import sys
15
17
  import subprocess
16
- import requests
17
18
  def install_package(package_name):
18
19
  try:
19
20
  subprocess.check_call([sys.executable, "-m", "pip", "install", package_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
@@ -78,7 +79,7 @@ class Robot:
78
79
  Initialized with bot token.
79
80
  """
80
81
 
81
- 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):
82
+ 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):
82
83
  """
83
84
  راه‌اندازی اولیه ربات روبیکا و پیکربندی پارامترهای پایه.
84
85
 
@@ -103,6 +104,7 @@ class Robot:
103
104
  self._inline_query_handlers: List[dict] = []
104
105
  self.timeout = timeout
105
106
  self.auth = auth
107
+ self.show_progress = show_progress
106
108
  self.session_name = session_name
107
109
  self.Key = Key
108
110
  self.platform = platform
@@ -113,6 +115,7 @@ class Robot:
113
115
  self._callback_handler = None
114
116
  self._message_handler = None
115
117
  self._inline_query_handler = None
118
+ self._message_handlers: List[dict] = []
116
119
  self._callback_handlers = None
117
120
  self._callback_handlers = [] # ✅ این خط مهمه
118
121
  if web_hook:
@@ -159,11 +162,11 @@ class Robot:
159
162
  return self._post("getMe", {})
160
163
  def on_message(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
161
164
  def decorator(func: Callable[[Any, Message], None]):
162
- self._message_handler = {
165
+ self._message_handlers.append({
163
166
  "func": func,
164
167
  "filters": filters,
165
168
  "commands": commands
166
- }
169
+ })
167
170
  return func
168
171
  return decorator
169
172
 
@@ -198,64 +201,58 @@ class Robot:
198
201
 
199
202
 
200
203
  def _process_update(self, update: dict):
201
- import threading, time
202
-
203
- # هندل پیام inline (معمولاً type = ReceiveQuery)
204
+ import threading
205
+ # هندل پیام inline (بدون تغییر)
204
206
  if update.get("type") == "ReceiveQuery":
205
207
  msg = update.get("inline_message", {})
206
- if update.get("type") == "ReceiveQuery":
207
- msg = update.get("inline_message", {})
208
- context = InlineMessage(bot=self, raw_data=msg)
209
- threading.Thread(target=self._handle_inline_query, args=(context,), daemon=True).start()
210
- return
208
+ context = InlineMessage(bot=self, raw_data=msg)
209
+ threading.Thread(target=self._handle_inline_query, args=(context,), daemon=True).start()
210
+ return
211
211
 
212
212
  # هندل پیام جدید متنی
213
213
  if update.get("type") == "NewMessage":
214
214
  msg = update.get("new_message", {})
215
- chat_id = update.get("chat_id")
216
- message_id = msg.get("message_id")
217
- sender_id = msg.get("sender_id")
218
- text = msg.get("text")
219
-
220
- # فیلتر زمان پیام (مثلاً پیام‌های خیلی قدیمی)
221
215
  try:
222
216
  if msg.get("time") and (time.time() - float(msg["time"])) > 20:
223
217
  return
224
218
  except Exception:
225
219
  return
226
220
 
227
- # ساخت context پیام
228
- context = Message(bot=self, chat_id=chat_id, message_id=message_id, sender_id=sender_id, text=text, raw_data=msg)
221
+ context = Message(bot=self,
222
+ chat_id=update.get("chat_id"),
223
+ message_id=msg.get("message_id"),
224
+ sender_id=msg.get("sender_id"),
225
+ text=msg.get("text"),
226
+ raw_data=msg)
229
227
 
230
- # هندل callback ها (دکمه‌ها)
231
- if context.aux_data and hasattr(self, "_callback_handlers"):
228
+ # هندل callback ها (دکمه‌ها) - بدون تغییر
229
+ if context.aux_data and self._callback_handlers:
232
230
  for handler in self._callback_handlers:
233
231
  if not handler["button_id"] or context.aux_data.button_id == handler["button_id"]:
234
232
  threading.Thread(target=handler["func"], args=(self, context), daemon=True).start()
235
- return # فقط یک callback اجرا شود
236
-
237
- # هندل پیام‌های متنی معمولی
238
- if hasattr(self, "_message_handler") and self._message_handler:
239
- handler = self._message_handler
240
-
241
- if handler["commands"]:
242
- if not context.text or not context.text.startswith("/"):
243
- return
244
- parts = context.text.split()
245
- cmd = parts[0][1:]
246
- if cmd not in handler["commands"]:
247
233
  return
248
- context.args = parts[1:]
249
234
 
250
- if handler["filters"] and not handler["filters"](context):
235
+ # هندل پیام‌های متنی با حلقه روی تمام هندلرها
236
+ if self._message_handlers:
237
+ for handler in self._message_handlers:
238
+ # بررسی شرط دستورات (commands)
239
+ if handler["commands"]:
240
+ if not context.text or not context.text.startswith("/"):
241
+ continue
242
+ parts = context.text.split()
243
+ cmd = parts[0][1:]
244
+ if cmd not in handler["commands"]:
245
+ continue
246
+ context.args = parts[1:]
247
+
248
+ # بررسی شرط فیلترها (filters)
249
+ if handler["filters"] and not handler["filters"](context):
250
+ continue
251
+
252
+ # اگر شرایط بالا برقرار بود یا هندلر عمومی بود، اجرا کن و خارج شو
253
+ threading.Thread(target=handler["func"], args=(self, context), daemon=True).start()
251
254
  return
252
255
 
253
- threading.Thread(target=handler["func"], args=(self, context), daemon=True).start()
254
-
255
-
256
-
257
-
258
-
259
256
  def get_updates(
260
257
  self,
261
258
  offset_id: Optional[str] = None,
@@ -506,9 +503,10 @@ class Robot:
506
503
  def get_chat(self, chat_id: str) -> Dict[str, Any]:
507
504
  """Get chat info."""
508
505
  return self._post("getChat", {"chat_id": chat_id})
506
+
509
507
  def upload_media_file(self, upload_url: str, name: str, path: Union[str, Path]) -> str:
510
- # بررسی اینکه ورودی URL است یا مسیر محلی
511
508
  is_temp_file = False
509
+
512
510
  if isinstance(path, str) and path.startswith("http"):
513
511
  response = requests.get(path)
514
512
  if response.status_code != 200:
@@ -519,22 +517,59 @@ class Robot:
519
517
  path = temp_file.name
520
518
  is_temp_file = True
521
519
 
522
- with open(path, 'rb') as file:
520
+ file_size = os.path.getsize(path)
521
+
522
+ with open(path, 'rb') as f:
523
+ progress_bar = None
524
+
525
+ if self.show_progress:
526
+ progress_bar = tqdm(
527
+ total=file_size,
528
+ unit='B',
529
+ unit_scale=True,
530
+ unit_divisor=1024,
531
+ desc=f'Uploading : {name}',
532
+ bar_format='{l_bar}{bar:100}{r_bar}',
533
+ colour='cyan'
534
+ )
535
+
536
+ class FileWithProgress:
537
+ def __init__(self, file, progress):
538
+ self.file = file
539
+ self.progress = progress
540
+
541
+ def read(self, size=-1):
542
+ data = self.file.read(size)
543
+ if self.progress:
544
+ self.progress.update(len(data))
545
+ return data
546
+
547
+ def __getattr__(self, attr):
548
+ return getattr(self.file, attr)
549
+
550
+ file_with_progress = FileWithProgress(f, progress_bar)
551
+
523
552
  files = {
524
- 'file': (name, file, 'application/octet-stream')
553
+ 'file': (name, file_with_progress, 'application/octet-stream')
525
554
  }
555
+
526
556
  response = requests.post(upload_url, files=files)
527
- if response.status_code != 200:
528
- raise Exception(f"Upload failed ({response.status_code}): {response.text}")
529
- data = response.json()
557
+
558
+ if progress_bar:
559
+ progress_bar.close()
530
560
 
531
561
  if is_temp_file:
532
- os.unlink(path) # حذف فایل موقت
562
+ os.remove(path)
533
563
 
564
+ if response.status_code != 200:
565
+ raise Exception(f"Upload failed ({response.status_code}): {response.text}")
566
+
567
+ data = response.json()
534
568
  return data.get('data', {}).get('file_id')
535
569
 
536
- def get_upload_url(self, media_type: Literal['File', 'Image', 'Voice', 'Music', 'Gif']) -> str:
537
- allowed = ['File', 'Image', 'Voice', 'Music', 'Gif']
570
+
571
+ def get_upload_url(self, media_type: Literal['File', 'Image', 'Voice', 'Music', 'Gif','Video']) -> str:
572
+ allowed = ['File', 'Image', 'Voice', 'Music', 'Gif','Video']
538
573
  if media_type not in allowed:
539
574
  raise ValueError(f"Invalid media type. Must be one of {allowed}")
540
575
  result = self._post("requestSendFile", {"type": media_type})
@@ -624,6 +659,35 @@ class Robot:
624
659
  disable_notification=disable_notification,
625
660
  chat_keypad_type=chat_keypad_type
626
661
  )
662
+ def send_video(
663
+ self,
664
+ chat_id: str,
665
+ path: Optional[Union[str, Path]] = None,
666
+ file_id: Optional[str] = None,
667
+ text: Optional[str] = None,
668
+ file_name: Optional[str] = None,
669
+ inline_keypad: Optional[Dict[str, Any]] = None,
670
+ chat_keypad: Optional[Dict[str, Any]] = None,
671
+ reply_to_message_id: Optional[str] = None,
672
+ disable_notification: bool = False,
673
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New"
674
+ ) -> Dict[str, Any]:
675
+ if path:
676
+ file_name = file_name or Path(path).name
677
+ upload_url = self.get_upload_url("Video")
678
+ file_id = self.upload_media_file(upload_url, file_name, path)
679
+ if not file_id:
680
+ raise ValueError("Either path or file_id must be provided.")
681
+ return self._send_uploaded_file(
682
+ chat_id=chat_id,
683
+ file_id=file_id,
684
+ text=text,
685
+ inline_keypad=inline_keypad,
686
+ chat_keypad=chat_keypad,
687
+ reply_to_message_id=reply_to_message_id,
688
+ disable_notification=disable_notification,
689
+ chat_keypad_type=chat_keypad_type
690
+ )
627
691
  def send_voice(
628
692
  self,
629
693
  chat_id: str,
rubka/asynco.py ADDED
@@ -0,0 +1,563 @@
1
+ import asyncio
2
+ import aiohttp
3
+ import aiofiles
4
+ from typing import List, Optional, Dict, Any, Literal, Callable, Union
5
+ from .exceptions import APIRequestError
6
+ from .adaptorrubka import Client as Client_get
7
+ from .logger import logger
8
+ try:
9
+ from .context import Message, InlineMessage
10
+ except (ImportError, ModuleNotFoundError):
11
+ # اگر به صورت مستقیم اجرا شود، از این حالت استفاده می‌کند
12
+ from context import Message, InlineMessage
13
+ from pathlib import Path
14
+ import time
15
+ import datetime
16
+ import tempfile
17
+ from tqdm import tqdm
18
+ import os
19
+ import sys
20
+ import subprocess
21
+
22
+ API_URL = "https://botapi.rubika.ir/v3"
23
+
24
+ def install_package(package_name: str) -> bool:
25
+ """Installs a package using pip."""
26
+ try:
27
+ subprocess.check_call([sys.executable, "-m", "pip", "install", package_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
28
+ return True
29
+ except Exception:
30
+ return False
31
+
32
+ def get_importlib_metadata():
33
+ """Dynamically imports and returns metadata functions from importlib."""
34
+ try:
35
+ from importlib.metadata import version, PackageNotFoundError
36
+ return version, PackageNotFoundError
37
+ except ImportError:
38
+ if install_package("importlib-metadata"):
39
+ try:
40
+ from importlib_metadata import version, PackageNotFoundError
41
+ return version, PackageNotFoundError
42
+ except ImportError:
43
+ return None, None
44
+ return None, None
45
+
46
+ version, PackageNotFoundError = get_importlib_metadata()
47
+
48
+ def get_installed_version(package_name: str) -> Optional[str]:
49
+ """Gets the installed version of a package."""
50
+ if version is None:
51
+ return "unknown"
52
+ try:
53
+ return version(package_name)
54
+ except PackageNotFoundError:
55
+ return None
56
+
57
+ async def get_latest_version(package_name: str) -> Optional[str]:
58
+ """Fetches the latest version of a package from PyPI asynchronously."""
59
+ url = f"https://pypi.org/pypi/{package_name}/json"
60
+ try:
61
+ async with aiohttp.ClientSession() as session:
62
+ async with session.get(url, timeout=5) as resp:
63
+ resp.raise_for_status()
64
+ data = await resp.json()
65
+ return data.get("info", {}).get("version")
66
+ except Exception:
67
+ return None
68
+
69
+ async def check_rubka_version():
70
+ """Checks for outdated 'rubka' package and warns the user."""
71
+ package_name = "rubka"
72
+ installed_version = get_installed_version(package_name)
73
+ if installed_version is None:
74
+ return
75
+
76
+ latest_version = await get_latest_version(package_name)
77
+ if latest_version is None:
78
+ return
79
+
80
+ if installed_version != latest_version:
81
+ print(f"\n\nWARNING: Your installed version of '{package_name}' is OUTDATED and may cause errors or security risks!")
82
+ print(f"Installed version : {installed_version}")
83
+ print(f"Latest available version : {latest_version}")
84
+ print(f"Please update IMMEDIATELY by running:")
85
+ print(f"\npip install {package_name}=={latest_version}\n")
86
+ print("Not updating may lead to malfunctions or incompatibility.")
87
+ print("To see new methods : @rubka_library\n\n")
88
+
89
+ # To run the check at startup in an async context
90
+ # asyncio.run(check_rubka_version())
91
+
92
+ def show_last_six_words(text: str) -> str:
93
+ """Returns the last 6 characters of a stripped string."""
94
+ text = text.strip()
95
+ return text[-6:]
96
+
97
+
98
+ class Robot:
99
+ """
100
+ Main async class to interact with Rubika Bot API.
101
+ Initialized with a bot token.
102
+ """
103
+
104
+ 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):
105
+ self.token = token
106
+ self._inline_query_handlers: List[dict] = []
107
+ self.timeout = timeout
108
+ self.auth = auth
109
+ self.show_progress = show_progress
110
+ self.session_name = session_name
111
+ self.Key = Key
112
+ self.platform = platform
113
+ self.web_hook = web_hook
114
+ self._offset_id: Optional[str] = None
115
+ self._aiohttp_session: aiohttp.ClientSession = None
116
+ self.sessions: Dict[str, Dict[str, Any]] = {}
117
+ self._callback_handler = None
118
+ self._message_handler = None
119
+ self._inline_query_handler = None
120
+ self._callback_handlers: List[dict] = []
121
+ self._processed_message_ids: Dict[str, float] = {}
122
+ self._message_handlers: List[dict] = []
123
+
124
+ logger.info(f"Initialized RubikaBot with token: {token[:8]}***")
125
+
126
+ async def _get_session(self) -> aiohttp.ClientSession:
127
+ """Lazily creates and returns the aiohttp session."""
128
+ if self._aiohttp_session is None or self._aiohttp_session.closed:
129
+ self._aiohttp_session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.timeout))
130
+ return self._aiohttp_session
131
+
132
+ async def _initialize_webhook(self):
133
+ """Initializes and sets the webhook endpoint if provided."""
134
+ if not self.web_hook:
135
+ return
136
+
137
+ session = await self._get_session()
138
+ try:
139
+ async with session.get(self.web_hook, timeout=self.timeout) as response:
140
+ response.raise_for_status()
141
+ data = await response.json()
142
+ print(data)
143
+ json_url = data.get('url', self.web_hook)
144
+ print(self.web_hook)
145
+
146
+ for endpoint_type in [
147
+ "ReceiveUpdate",
148
+ "ReceiveInlineMessage",
149
+ "ReceiveQuery",
150
+ "GetSelectionItem",
151
+ "SearchSelectionItems"
152
+ ]:
153
+ result = await self.update_bot_endpoint(self.web_hook, endpoint_type)
154
+ print(result)
155
+ self.web_hook = json_url
156
+ except Exception as e:
157
+ logger.error(f"Failed to set webhook from {self.web_hook}: {e}")
158
+ self.web_hook = None
159
+
160
+
161
+ async def _post(self, method: str, data: Dict[str, Any]) -> Dict[str, Any]:
162
+ url = f"{API_URL}/{self.token}/{method}"
163
+ session = await self._get_session()
164
+ try:
165
+ async with session.post(url, json=data) as response:
166
+ response.raise_for_status()
167
+ try:
168
+ json_resp = await response.json()
169
+ except aiohttp.ContentTypeError:
170
+ text_resp = await response.text()
171
+ logger.error(f"Invalid JSON response from {method}: {text_resp}")
172
+ raise APIRequestError(f"Invalid JSON response: {text_resp}")
173
+
174
+ if method != "getUpdates":
175
+ logger.debug(f"API Response from {method}: {json_resp}")
176
+
177
+ return json_resp
178
+ except aiohttp.ClientError as e:
179
+ logger.error(f"API request failed: {e}")
180
+ raise APIRequestError(f"API request failed: {e}") from e
181
+
182
+ async def get_me(self) -> Dict[str, Any]:
183
+ """Get info about the bot itself."""
184
+ return await self._post("getMe", {})
185
+
186
+ def on_message(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
187
+ def decorator(func: Callable[[Any, Message], None]):
188
+ self._message_handlers.append({
189
+ "func": func,
190
+ "filters": filters,
191
+ "commands": commands
192
+ })
193
+ return func
194
+ return decorator
195
+
196
+ def on_callback(self, button_id: Optional[str] = None):
197
+ def decorator(func: Callable[[Any, Message], None]):
198
+ if not hasattr(self, "_callback_handlers"):
199
+ self._callback_handlers = []
200
+ self._callback_handlers.append({
201
+ "func": func,
202
+ "button_id": button_id
203
+ })
204
+ return func
205
+ return decorator
206
+
207
+ async def _handle_inline_query(self, inline_message: InlineMessage):
208
+ aux_button_id = inline_message.aux_data.button_id if inline_message.aux_data else None
209
+
210
+ for handler in self._inline_query_handlers:
211
+ if handler["button_id"] is None or handler["button_id"] == aux_button_id:
212
+ try:
213
+ await handler["func"](self, inline_message)
214
+ except Exception as e:
215
+ print(f"Error in inline query handler: {e}")
216
+
217
+ def on_inline_query(self, button_id: Optional[str] = None):
218
+ def decorator(func: Callable[[Any, InlineMessage], None]):
219
+ self._inline_query_handlers.append({
220
+ "func": func,
221
+ "button_id": button_id
222
+ })
223
+ return func
224
+ return decorator
225
+
226
+ async def _process_update(self, update: dict):
227
+ if update.get("type") == "ReceiveQuery":
228
+ msg = update.get("inline_message", {})
229
+ context = InlineMessage(bot=self, raw_data=msg)
230
+ asyncio.create_task(self._handle_inline_query(context))
231
+ return
232
+
233
+ if update.get("type") == "NewMessage":
234
+ msg = update.get("new_message", {})
235
+ try:
236
+ if msg.get("time") and (time.time() - float(msg["time"])) > 20:
237
+ return
238
+ except (ValueError, TypeError):
239
+ return
240
+
241
+ context = Message(bot=self,
242
+ chat_id=update.get("chat_id"),
243
+ message_id=msg.get("message_id"),
244
+ sender_id=msg.get("sender_id"),
245
+ text=msg.get("text"),
246
+ raw_data=msg)
247
+
248
+ # پردازش دکمه‌های شیشه‌ای (بدون تغییر)
249
+ if context.aux_data and self._callback_handlers:
250
+ for handler in self._callback_handlers:
251
+ if not handler["button_id"] or context.aux_data.button_id == handler["button_id"]:
252
+ asyncio.create_task(handler["func"](self, context))
253
+ return
254
+
255
+ # پردازش پیام‌های متنی با حلقه روی تمام هندلرها
256
+ if self._message_handlers:
257
+ for handler_info in self._message_handlers:
258
+ # بررسی شرط دستورات (commands)
259
+ if handler_info["commands"]:
260
+ if not context.text or not context.text.startswith("/"):
261
+ continue # اگر پیام کامند نبود، این هندلر را رد کن
262
+ parts = context.text.split()
263
+ cmd = parts[0][1:]
264
+ if cmd not in handler_info["commands"]:
265
+ continue # اگر کامند مطابقت نداشت، این هندلر را رد کن
266
+ context.args = parts[1:]
267
+
268
+ # بررسی شرط فیلترها (filters)
269
+ if handler_info["filters"]:
270
+ if not handler_info["filters"](context):
271
+ continue # اگر فیلتر برقرار نبود، این هندلر را رد کن
272
+
273
+ # اگر هندلری برای همه پیام‌ها باشد (بدون کامند و فیلتر)
274
+ if not handler_info["commands"] and not handler_info["filters"]:
275
+ asyncio.create_task(handler_info["func"](self, context))
276
+ return # بعد از یافتن هندلر مناسب، از حلقه خارج شو
277
+
278
+ # اگر شرایط کامند یا فیلتر برقرار بود
279
+ if handler_info["commands"] or handler_info["filters"]:
280
+ asyncio.create_task(handler_info["func"](self, context))
281
+ return # بعد از یافتن هندلر مناسب، از حلقه خارج شو
282
+
283
+ async def get_updates(self, offset_id: Optional[str] = None, limit: Optional[int] = None) -> Dict[str, Any]:
284
+ data = {}
285
+ if offset_id: data["offset_id"] = offset_id
286
+ if limit: data["limit"] = limit
287
+ return await self._post("getUpdates", data)
288
+
289
+ async def update_webhook(self, offset_id: Optional[str] = None, limit: Optional[int] = None) -> List[Dict[str, Any]]:
290
+ session = await self._get_session()
291
+ params = {}
292
+ if offset_id: params['offset_id'] = offset_id
293
+ if limit: params['limit'] = limit
294
+ async with session.get(self.web_hook, params=params) as response:
295
+ response.raise_for_status()
296
+ # وب‌هوک باید لیستی از رویدادها را برگرداند
297
+ return await response.json()
298
+
299
+ def _is_duplicate(self, message_id: str, max_age_sec: int = 300) -> bool:
300
+ now = time.time()
301
+ expired = [mid for mid, ts in self._processed_message_ids.items() if now - ts > max_age_sec]
302
+ for mid in expired:
303
+ del self._processed_message_ids[mid]
304
+
305
+ if message_id in self._processed_message_ids:
306
+ return True
307
+
308
+ self._processed_message_ids[message_id] = now
309
+ return False
310
+
311
+ async def run(self):
312
+ """
313
+ Starts the bot.
314
+ This method is now corrected to handle webhook updates similarly to the original synchronous code.
315
+ """
316
+ await check_rubka_version()
317
+ await self._initialize_webhook()
318
+ print("Bot started running...")
319
+
320
+ try:
321
+ while True:
322
+ try:
323
+ if self.web_hook:
324
+ # ----- منطق وب‌هوک (اصلاح شده) -----
325
+ # آپدیت‌ها مستقیما از وب‌هوک گرفته و پردازش می‌شوند
326
+ webhook_data = await self.update_webhook()
327
+ if isinstance(webhook_data, list):
328
+ for item in webhook_data:
329
+ data = item.get("data", {})
330
+
331
+ received_at_str = item.get("received_at")
332
+ if received_at_str:
333
+ try:
334
+ received_at_ts = datetime.datetime.strptime(received_at_str, "%Y-%m-%d %H:%M:%S").timestamp()
335
+ if time.time() - received_at_ts > 20:
336
+ continue
337
+ except (ValueError, TypeError):
338
+ pass # رد شدن در صورت فرمت اشتباه زمان
339
+
340
+ update = None
341
+ if "update" in data:
342
+ update = data["update"]
343
+ elif "inline_message" in data:
344
+ update = {"type": "ReceiveQuery", "inline_message": data["inline_message"]}
345
+ else:
346
+ continue
347
+
348
+ message_id = None
349
+ if update.get("type") == "NewMessage":
350
+ message_id = update.get("new_message", {}).get("message_id")
351
+ elif update.get("type") == "ReceiveQuery":
352
+ message_id = update.get("inline_message", {}).get("message_id")
353
+ elif "message_id" in update:
354
+ message_id = update.get("message_id")
355
+
356
+ if message_id and not self._is_duplicate(str(message_id)):
357
+ await self._process_update(update)
358
+
359
+ else:
360
+ # ----- منطق Polling (بدون تغییر) -----
361
+ get_updates_response = await self.get_updates(offset_id=self._offset_id, limit=100)
362
+ if get_updates_response and get_updates_response.get("data"):
363
+ updates = get_updates_response["data"].get("updates", [])
364
+ self._offset_id = get_updates_response["data"].get("next_offset_id", self._offset_id)
365
+
366
+ for update in updates:
367
+ message_id = None
368
+ if update.get("type") == "NewMessage":
369
+ message_id = update.get("new_message", {}).get("message_id")
370
+ elif update.get("type") == "ReceiveQuery":
371
+ message_id = update.get("inline_message", {}).get("message_id")
372
+ elif "message_id" in update:
373
+ message_id = update.get("message_id")
374
+
375
+ if message_id and not self._is_duplicate(str(message_id)):
376
+ await self._process_update(update)
377
+
378
+ await asyncio.sleep(0.1) # وقفه کوتاه برای جلوگیری از مصرف CPU
379
+ except Exception as e:
380
+ print(f"❌ Error in run loop: {e}")
381
+ await asyncio.sleep(5) # وقفه طولانی‌تر در صورت بروز خطا
382
+ finally:
383
+ if self._aiohttp_session:
384
+ await self._aiohttp_session.close()
385
+ print("Bot stopped and session closed.")
386
+
387
+ 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]:
388
+ payload = {"chat_id": chat_id, "text": text, "disable_notification": disable_notification}
389
+ if chat_keypad: payload["chat_keypad"] = chat_keypad
390
+ if inline_keypad: payload["inline_keypad"] = inline_keypad
391
+ if reply_to_message_id: payload["reply_to_message_id"] = reply_to_message_id
392
+ if chat_keypad_type: payload["chat_keypad_type"] = chat_keypad_type
393
+ return await self._post("sendMessage", payload)
394
+
395
+ def _get_client(self) -> Client_get:
396
+ if self.session_name:
397
+ return Client_get(self.session_name, self.auth, self.Key, self.platform)
398
+ else:
399
+ return Client_get(show_last_six_words(self.token), self.auth, self.Key, self.platform)
400
+
401
+ async def check_join(self, channel_guid: str, chat_id: str = None) -> Union[bool, list[str]]:
402
+ client = self._get_client()
403
+
404
+ if chat_id:
405
+ chat_info_data = await self.get_chat(chat_id)
406
+ chat_info = chat_info_data.get('data', {}).get('chat', {})
407
+ username = chat_info.get('username')
408
+ user_id = chat_info.get('user_id')
409
+
410
+ # Since client methods are sync, run them in a thread pool
411
+ if username:
412
+ result = await asyncio.to_thread(self.get_all_member, channel_guid, search_text=username)
413
+ members = result.get('in_chat_members', [])
414
+ return any(m.get('username') == username for m in members)
415
+ elif user_id:
416
+ member_guids = await asyncio.to_thread(client.get_all_members, channel_guid, just_get_guids=True)
417
+ return user_id in member_guids
418
+ return False
419
+
420
+ def get_all_member(self, channel_guid: str, search_text: str = None, start_id: str = None, just_get_guids: bool = False):
421
+ # This is a sync method that will be called with asyncio.to_thread
422
+ client = self._get_client()
423
+ return client.get_all_members(channel_guid, search_text, start_id, just_get_guids)
424
+
425
+ async def send_poll(self, chat_id: str, question: str, options: List[str]) -> Dict[str, Any]:
426
+ return await self._post("sendPoll", {"chat_id": chat_id, "question": question, "options": options})
427
+
428
+ 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]:
429
+ payload = {"chat_id": chat_id, "latitude": latitude, "longitude": longitude, "disable_notification": disable_notification}
430
+ if inline_keypad: payload["inline_keypad"] = inline_keypad
431
+ if reply_to_message_id: payload["reply_to_message_id"] = reply_to_message_id
432
+ if chat_keypad_type: payload["chat_keypad_type"] = chat_keypad_type
433
+ return await self._post("sendLocation", {k: v for k, v in payload.items() if v is not None})
434
+
435
+ async def send_contact(self, chat_id: str, first_name: str, last_name: str, phone_number: str) -> Dict[str, Any]:
436
+ return await self._post("sendContact", {"chat_id": chat_id, "first_name": first_name, "last_name": last_name, "phone_number": phone_number})
437
+
438
+ async def get_chat(self, chat_id: str) -> Dict[str, Any]:
439
+ return await self._post("getChat", {"chat_id": chat_id})
440
+
441
+ async def upload_media_file(self, upload_url: str, name: str, path: Union[str, Path]) -> str:
442
+ is_temp_file = False
443
+ session = await self._get_session()
444
+
445
+ if isinstance(path, str) and path.startswith("http"):
446
+ async with session.get(path) as response:
447
+ if response.status != 200:
448
+ raise Exception(f"Failed to download file from URL ({response.status})")
449
+
450
+ content = await response.read()
451
+
452
+ with tempfile.NamedTemporaryFile(delete=False) as temp_file:
453
+ temp_file.write(content)
454
+ path = temp_file.name
455
+ is_temp_file = True
456
+
457
+ 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.
458
+
459
+ 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)
460
+
461
+ async def file_progress_generator(file_path, chunk_size=8192):
462
+ async with aiofiles.open(file_path, 'rb') as f:
463
+ while True:
464
+ chunk = await f.read(chunk_size)
465
+ if not chunk:
466
+ break
467
+ progress_bar.update(len(chunk))
468
+ yield chunk
469
+
470
+ data = aiohttp.FormData()
471
+ data.add_field('file', file_progress_generator(path), filename=name, content_type='application/octet-stream')
472
+
473
+ async with session.post(upload_url, data=data) as response:
474
+ progress_bar.close()
475
+ if response.status != 200:
476
+ raise Exception(f"Upload failed ({response.status}): {await response.text()}")
477
+
478
+ json_data = await response.json()
479
+ if is_temp_file:
480
+ os.remove(path)
481
+
482
+ return json_data.get('data', {}).get('file_id')
483
+
484
+ async def get_upload_url(self, media_type: Literal['File', 'Image', 'Voice', 'Music', 'Gif', 'Video']) -> str:
485
+ allowed = ['File', 'Image', 'Voice', 'Music', 'Gif', 'Video']
486
+ if media_type not in allowed:
487
+ raise ValueError(f"Invalid media type. Must be one of {allowed}")
488
+ result = await self._post("requestSendFile", {"type": media_type})
489
+ return result.get("data", {}).get("upload_url")
490
+
491
+ async def _send_uploaded_file(self, chat_id: str, file_id: str, 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]:
492
+ payload = {"chat_id": chat_id, "file_id": file_id, "text": text, "disable_notification": disable_notification, "chat_keypad_type": chat_keypad_type}
493
+ if chat_keypad: payload["chat_keypad"] = chat_keypad
494
+ if inline_keypad: payload["inline_keypad"] = inline_keypad
495
+ if reply_to_message_id: payload["reply_to_message_id"] = str(reply_to_message_id)
496
+ return await self._post("sendFile", payload)
497
+
498
+ 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):
499
+ if path:
500
+ file_name = file_name or Path(path).name
501
+ upload_url = await self.get_upload_url(media_type)
502
+ file_id = await self.upload_media_file(upload_url, file_name, path)
503
+ if not file_id:
504
+ raise ValueError("Either path or file_id must be provided.")
505
+ 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)
506
+
507
+ 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]:
508
+ 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)
509
+
510
+ 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]:
511
+ 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)
512
+
513
+ 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]:
514
+ 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)
515
+
516
+ 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]:
517
+ 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)
518
+
519
+ 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]:
520
+ 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)
521
+
522
+ 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]:
523
+ 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)
524
+
525
+ async def forward_message(self, from_chat_id: str, message_id: str, to_chat_id: str, disable_notification: bool = False) -> Dict[str, Any]:
526
+ 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})
527
+
528
+ async def edit_message_text(self, chat_id: str, message_id: str, text: str) -> Dict[str, Any]:
529
+ return await self._post("editMessageText", {"chat_id": chat_id, "message_id": message_id, "text": text})
530
+
531
+ async def edit_inline_keypad(self, chat_id: str, message_id: str, inline_keypad: Dict[str, Any]) -> Dict[str, Any]:
532
+ return await self._post("editMessageKeypad", {"chat_id": chat_id, "message_id": message_id, "inline_keypad": inline_keypad})
533
+
534
+ async def delete_message(self, chat_id: str, message_id: str) -> Dict[str, Any]:
535
+ return await self._post("deleteMessage", {"chat_id": chat_id, "message_id": message_id})
536
+
537
+ async def set_commands(self, bot_commands: List[Dict[str, str]]) -> Dict[str, Any]:
538
+ return await self._post("setCommands", {"bot_commands": bot_commands})
539
+
540
+ async def update_bot_endpoint(self, url: str, type: str) -> Dict[str, Any]:
541
+ return await self._post("updateBotEndpoints", {"url": url, "type": type})
542
+
543
+ async def remove_keypad(self, chat_id: str) -> Dict[str, Any]:
544
+ return await self._post("editChatKeypad", {"chat_id": chat_id, "chat_keypad_type": "Removed"})
545
+
546
+ async def edit_chat_keypad(self, chat_id: str, chat_keypad: Dict[str, Any]) -> Dict[str, Any]:
547
+ return await self._post("editChatKeypad", {"chat_id": chat_id, "chat_keypad_type": "New", "chat_keypad": chat_keypad})
548
+
549
+ async def get_name(self, chat_id: str) -> str:
550
+ try:
551
+ chat = await self.get_chat(chat_id)
552
+ chat_info = chat.get("data", {}).get("chat", {})
553
+ first_name = chat_info.get("first_name", "")
554
+ last_name = chat_info.get("last_name", "")
555
+
556
+ full_name = f"{first_name} {last_name}".strip()
557
+ return full_name if full_name else "Unknown"
558
+ except Exception:
559
+ return "Unknown"
560
+
561
+ async def get_username(self, chat_id: str) -> str:
562
+ chat_info = await self.get_chat(chat_id)
563
+ return chat_info.get("data", {}).get("chat", {}).get("username", "None")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Rubka
3
- Version: 4.4.20
3
+ Version: 4.5.2
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,5 +1,6 @@
1
1
  rubka/__init__.py,sha256=TR1DABU5Maz2eO62ZEFiwOqNU0dH6l6HZfqRUxeo4eY,194
2
- rubka/api.py,sha256=NICbYs-4hpnsypGaJeDZ0xoBiuvLV0whSkak9sxM_Bw,32719
2
+ rubka/api.py,sha256=gzaH2nfZjpxlMwzNsgA4FFBR38bAfQK2-2oTPs555Xw,35160
3
+ rubka/asynco.py,sha256=m3WztgmkzK6Jvrko8HxqLlIigWTbtJkdbDdQq3NtbMo,31994
3
4
  rubka/button.py,sha256=4fMSZR7vUADxSmw1R3_pZ4dw5uMLZX5sOkwPPyNTBDE,8437
4
5
  rubka/config.py,sha256=Bck59xkOiqioLv0GkQ1qPGnBXVctz1hKk6LT4h2EPx0,78
5
6
  rubka/context.py,sha256=j1scXTy_wBY52MmMixfZyIQnB0sdYjRwx17-8ZZmyB4,17017
@@ -32,7 +33,7 @@ rubka/adaptorrubka/types/socket/message.py,sha256=0WgLMZh4eow8Zn7AiSX4C3GZjQTkIg
32
33
  rubka/adaptorrubka/utils/__init__.py,sha256=OgCFkXdNFh379quNwIVOAWY2NP5cIOxU5gDRRALTk4o,54
33
34
  rubka/adaptorrubka/utils/configs.py,sha256=nMUEOJh1NqDJsf9W9PurkN_DLYjO6kKPMm923i4Jj_A,492
34
35
  rubka/adaptorrubka/utils/utils.py,sha256=5-LioLNYX_TIbQGDeT50j7Sg9nAWH2LJUUs-iEXpsUY,8816
35
- rubka-4.4.20.dist-info/METADATA,sha256=zVVTaP_jxIAlAD1TyHLWyJjv6y138FScv3g104EBPHo,33217
36
- rubka-4.4.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
- rubka-4.4.20.dist-info/top_level.txt,sha256=vy2A4lot11cRMdQS-F4HDCIXL3JK8RKfu7HMDkezJW4,6
38
- rubka-4.4.20.dist-info/RECORD,,
36
+ rubka-4.5.2.dist-info/METADATA,sha256=xgwDJfrcrmaI2Hr9YEugIt8_3OzJiAXBHx2sldoW7ZU,33216
37
+ rubka-4.5.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
+ rubka-4.5.2.dist-info/top_level.txt,sha256=vy2A4lot11cRMdQS-F4HDCIXL3JK8RKfu7HMDkezJW4,6
39
+ rubka-4.5.2.dist-info/RECORD,,
File without changes