Rubka 4.4.19__py3-none-any.whl → 4.5.0__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
@@ -115,11 +117,23 @@ class Robot:
115
117
  self._inline_query_handler = None
116
118
  self._callback_handlers = None
117
119
  self._callback_handlers = [] # ✅ این خط مهمه
118
- json_url = requests.get(web_hook).json()['url']
119
120
  if web_hook:
120
- for endpoint_type in ["ReceiveUpdate", "ReceiveInlineMessage", "ReceiveQuery", "GetSelectionItem", "SearchSelectionItems"]:
121
- print(self.update_bot_endpoint(web_hook, endpoint_type))
122
- self.web_hook = json_url
121
+ try:
122
+ json_url = requests.get(web_hook, timeout=self.timeout).json().get('url', web_hook)
123
+ for endpoint_type in [
124
+ "ReceiveUpdate",
125
+ "ReceiveInlineMessage",
126
+ "ReceiveQuery",
127
+ "GetSelectionItem",
128
+ "SearchSelectionItems"
129
+ ]:
130
+ print(self.update_bot_endpoint(self.web_hook, endpoint_type))
131
+ self.web_hook = json_url
132
+ except Exception as e:
133
+ logger.error(f"Failed to set webhook from {web_hook}: {e}")
134
+ else:
135
+ self.web_hook = None
136
+
123
137
 
124
138
 
125
139
  logger.info(f"Initialized RubikaBot with token: {token[:8]}***")
@@ -366,15 +380,6 @@ class Robot:
366
380
 
367
381
  except Exception as e:
368
382
  print(f"❌ Error in run loop: {e}")
369
-
370
-
371
-
372
-
373
-
374
-
375
-
376
-
377
-
378
383
  def send_message(
379
384
  self,
380
385
  chat_id: str,
@@ -503,9 +508,10 @@ class Robot:
503
508
  def get_chat(self, chat_id: str) -> Dict[str, Any]:
504
509
  """Get chat info."""
505
510
  return self._post("getChat", {"chat_id": chat_id})
511
+
506
512
  def upload_media_file(self, upload_url: str, name: str, path: Union[str, Path]) -> str:
507
- # بررسی اینکه ورودی URL است یا مسیر محلی
508
513
  is_temp_file = False
514
+
509
515
  if isinstance(path, str) and path.startswith("http"):
510
516
  response = requests.get(path)
511
517
  if response.status_code != 200:
@@ -516,22 +522,59 @@ class Robot:
516
522
  path = temp_file.name
517
523
  is_temp_file = True
518
524
 
519
- with open(path, 'rb') as file:
525
+ file_size = os.path.getsize(path)
526
+
527
+ with open(path, 'rb') as f:
528
+ progress_bar = None
529
+
530
+ if self.show_progress:
531
+ progress_bar = tqdm(
532
+ total=file_size,
533
+ unit='B',
534
+ unit_scale=True,
535
+ unit_divisor=1024,
536
+ desc=f'Uploading : {name}',
537
+ bar_format='{l_bar}{bar:100}{r_bar}',
538
+ colour='cyan'
539
+ )
540
+
541
+ class FileWithProgress:
542
+ def __init__(self, file, progress):
543
+ self.file = file
544
+ self.progress = progress
545
+
546
+ def read(self, size=-1):
547
+ data = self.file.read(size)
548
+ if self.progress:
549
+ self.progress.update(len(data))
550
+ return data
551
+
552
+ def __getattr__(self, attr):
553
+ return getattr(self.file, attr)
554
+
555
+ file_with_progress = FileWithProgress(f, progress_bar)
556
+
520
557
  files = {
521
- 'file': (name, file, 'application/octet-stream')
558
+ 'file': (name, file_with_progress, 'application/octet-stream')
522
559
  }
560
+
523
561
  response = requests.post(upload_url, files=files)
524
- if response.status_code != 200:
525
- raise Exception(f"Upload failed ({response.status_code}): {response.text}")
526
- data = response.json()
562
+
563
+ if progress_bar:
564
+ progress_bar.close()
527
565
 
528
566
  if is_temp_file:
529
- os.unlink(path) # حذف فایل موقت
567
+ os.remove(path)
568
+
569
+ if response.status_code != 200:
570
+ raise Exception(f"Upload failed ({response.status_code}): {response.text}")
530
571
 
572
+ data = response.json()
531
573
  return data.get('data', {}).get('file_id')
532
574
 
533
- def get_upload_url(self, media_type: Literal['File', 'Image', 'Voice', 'Music', 'Gif']) -> str:
534
- allowed = ['File', 'Image', 'Voice', 'Music', 'Gif']
575
+
576
+ def get_upload_url(self, media_type: Literal['File', 'Image', 'Voice', 'Music', 'Gif','Video']) -> str:
577
+ allowed = ['File', 'Image', 'Voice', 'Music', 'Gif','Video']
535
578
  if media_type not in allowed:
536
579
  raise ValueError(f"Invalid media type. Must be one of {allowed}")
537
580
  result = self._post("requestSendFile", {"type": media_type})
@@ -621,6 +664,35 @@ class Robot:
621
664
  disable_notification=disable_notification,
622
665
  chat_keypad_type=chat_keypad_type
623
666
  )
667
+ def send_video(
668
+ self,
669
+ chat_id: str,
670
+ path: Optional[Union[str, Path]] = None,
671
+ file_id: Optional[str] = None,
672
+ text: Optional[str] = None,
673
+ file_name: Optional[str] = None,
674
+ inline_keypad: Optional[Dict[str, Any]] = None,
675
+ chat_keypad: Optional[Dict[str, Any]] = None,
676
+ reply_to_message_id: Optional[str] = None,
677
+ disable_notification: bool = False,
678
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New"
679
+ ) -> Dict[str, Any]:
680
+ if path:
681
+ file_name = file_name or Path(path).name
682
+ upload_url = self.get_upload_url("Video")
683
+ file_id = self.upload_media_file(upload_url, file_name, path)
684
+ if not file_id:
685
+ raise ValueError("Either path or file_id must be provided.")
686
+ return self._send_uploaded_file(
687
+ chat_id=chat_id,
688
+ file_id=file_id,
689
+ text=text,
690
+ inline_keypad=inline_keypad,
691
+ chat_keypad=chat_keypad,
692
+ reply_to_message_id=reply_to_message_id,
693
+ disable_notification=disable_notification,
694
+ chat_keypad_type=chat_keypad_type
695
+ )
624
696
  def send_voice(
625
697
  self,
626
698
  chat_id: str,
rubka/asynco.py ADDED
@@ -0,0 +1,549 @@
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
+
123
+ logger.info(f"Initialized RubikaBot with token: {token[:8]}***")
124
+
125
+ async def _get_session(self) -> aiohttp.ClientSession:
126
+ """Lazily creates and returns the aiohttp session."""
127
+ if self._aiohttp_session is None or self._aiohttp_session.closed:
128
+ self._aiohttp_session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.timeout))
129
+ return self._aiohttp_session
130
+
131
+ async def _initialize_webhook(self):
132
+ """Initializes and sets the webhook endpoint if provided."""
133
+ if not self.web_hook:
134
+ return
135
+
136
+ session = await self._get_session()
137
+ try:
138
+ async with session.get(self.web_hook, timeout=self.timeout) as response:
139
+ response.raise_for_status()
140
+ data = await response.json()
141
+ print(data)
142
+ json_url = data.get('url', self.web_hook)
143
+ print(self.web_hook)
144
+
145
+ for endpoint_type in [
146
+ "ReceiveUpdate",
147
+ "ReceiveInlineMessage",
148
+ "ReceiveQuery",
149
+ "GetSelectionItem",
150
+ "SearchSelectionItems"
151
+ ]:
152
+ result = await self.update_bot_endpoint(self.web_hook, endpoint_type)
153
+ print(result)
154
+ self.web_hook = json_url
155
+ except Exception as e:
156
+ logger.error(f"Failed to set webhook from {self.web_hook}: {e}")
157
+ self.web_hook = None
158
+
159
+
160
+ async def _post(self, method: str, data: Dict[str, Any]) -> Dict[str, Any]:
161
+ url = f"{API_URL}/{self.token}/{method}"
162
+ session = await self._get_session()
163
+ try:
164
+ async with session.post(url, json=data) as response:
165
+ response.raise_for_status()
166
+ try:
167
+ json_resp = await response.json()
168
+ except aiohttp.ContentTypeError:
169
+ text_resp = await response.text()
170
+ logger.error(f"Invalid JSON response from {method}: {text_resp}")
171
+ raise APIRequestError(f"Invalid JSON response: {text_resp}")
172
+
173
+ if method != "getUpdates":
174
+ logger.debug(f"API Response from {method}: {json_resp}")
175
+
176
+ return json_resp
177
+ except aiohttp.ClientError as e:
178
+ logger.error(f"API request failed: {e}")
179
+ raise APIRequestError(f"API request failed: {e}") from e
180
+
181
+ async def get_me(self) -> Dict[str, Any]:
182
+ """Get info about the bot itself."""
183
+ return await self._post("getMe", {})
184
+
185
+ def on_message(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
186
+ def decorator(func: Callable[[Any, Message], None]):
187
+ self._message_handler = {
188
+ "func": func,
189
+ "filters": filters,
190
+ "commands": commands
191
+ }
192
+ return func
193
+ return decorator
194
+
195
+ def on_callback(self, button_id: Optional[str] = None):
196
+ def decorator(func: Callable[[Any, Message], None]):
197
+ if not hasattr(self, "_callback_handlers"):
198
+ self._callback_handlers = []
199
+ self._callback_handlers.append({
200
+ "func": func,
201
+ "button_id": button_id
202
+ })
203
+ return func
204
+ return decorator
205
+
206
+ async def _handle_inline_query(self, inline_message: InlineMessage):
207
+ aux_button_id = inline_message.aux_data.button_id if inline_message.aux_data else None
208
+
209
+ for handler in self._inline_query_handlers:
210
+ if handler["button_id"] is None or handler["button_id"] == aux_button_id:
211
+ try:
212
+ await handler["func"](self, inline_message)
213
+ except Exception as e:
214
+ print(f"Error in inline query handler: {e}")
215
+
216
+ def on_inline_query(self, button_id: Optional[str] = None):
217
+ def decorator(func: Callable[[Any, InlineMessage], None]):
218
+ self._inline_query_handlers.append({
219
+ "func": func,
220
+ "button_id": button_id
221
+ })
222
+ return func
223
+ return decorator
224
+
225
+ async def _process_update(self, update: dict):
226
+ if update.get("type") == "ReceiveQuery":
227
+ msg = update.get("inline_message", {})
228
+ context = InlineMessage(bot=self, raw_data=msg)
229
+ asyncio.create_task(self._handle_inline_query(context))
230
+ return
231
+
232
+ if update.get("type") == "NewMessage":
233
+ msg = update.get("new_message", {})
234
+ try:
235
+ if msg.get("time") and (time.time() - float(msg["time"])) > 20:
236
+ return
237
+ except (ValueError, TypeError):
238
+ return
239
+
240
+ context = Message(bot=self,
241
+ chat_id=update.get("chat_id"),
242
+ message_id=msg.get("message_id"),
243
+ sender_id=msg.get("sender_id"),
244
+ text=msg.get("text"),
245
+ raw_data=msg)
246
+
247
+ if context.aux_data and self._callback_handlers:
248
+ for handler in self._callback_handlers:
249
+ if not handler["button_id"] or context.aux_data.button_id == handler["button_id"]:
250
+ asyncio.create_task(handler["func"](self, context))
251
+ return
252
+
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))
268
+
269
+ async def get_updates(self, offset_id: Optional[str] = None, limit: Optional[int] = None) -> Dict[str, Any]:
270
+ data = {}
271
+ if offset_id: data["offset_id"] = offset_id
272
+ if limit: data["limit"] = limit
273
+ return await self._post("getUpdates", data)
274
+
275
+ async def update_webhook(self, offset_id: Optional[str] = None, limit: Optional[int] = None) -> List[Dict[str, Any]]:
276
+ session = await self._get_session()
277
+ params = {}
278
+ if offset_id: params['offset_id'] = offset_id
279
+ if limit: params['limit'] = limit
280
+ async with session.get(self.web_hook, params=params) as response:
281
+ response.raise_for_status()
282
+ # وب‌هوک باید لیستی از رویدادها را برگرداند
283
+ return await response.json()
284
+
285
+ def _is_duplicate(self, message_id: str, max_age_sec: int = 300) -> bool:
286
+ now = time.time()
287
+ expired = [mid for mid, ts in self._processed_message_ids.items() if now - ts > max_age_sec]
288
+ for mid in expired:
289
+ del self._processed_message_ids[mid]
290
+
291
+ if message_id in self._processed_message_ids:
292
+ return True
293
+
294
+ self._processed_message_ids[message_id] = now
295
+ return False
296
+
297
+ async def run(self):
298
+ """
299
+ Starts the bot.
300
+ This method is now corrected to handle webhook updates similarly to the original synchronous code.
301
+ """
302
+ await check_rubka_version()
303
+ await self._initialize_webhook()
304
+ print("Bot started running...")
305
+
306
+ try:
307
+ while True:
308
+ try:
309
+ if self.web_hook:
310
+ # ----- منطق وب‌هوک (اصلاح شده) -----
311
+ # آپدیت‌ها مستقیما از وب‌هوک گرفته و پردازش می‌شوند
312
+ webhook_data = await self.update_webhook()
313
+ if isinstance(webhook_data, list):
314
+ for item in webhook_data:
315
+ data = item.get("data", {})
316
+
317
+ received_at_str = item.get("received_at")
318
+ if received_at_str:
319
+ try:
320
+ received_at_ts = datetime.datetime.strptime(received_at_str, "%Y-%m-%d %H:%M:%S").timestamp()
321
+ if time.time() - received_at_ts > 20:
322
+ continue
323
+ except (ValueError, TypeError):
324
+ pass # رد شدن در صورت فرمت اشتباه زمان
325
+
326
+ update = None
327
+ if "update" in data:
328
+ update = data["update"]
329
+ elif "inline_message" in data:
330
+ update = {"type": "ReceiveQuery", "inline_message": data["inline_message"]}
331
+ else:
332
+ continue
333
+
334
+ message_id = None
335
+ if update.get("type") == "NewMessage":
336
+ message_id = update.get("new_message", {}).get("message_id")
337
+ elif update.get("type") == "ReceiveQuery":
338
+ message_id = update.get("inline_message", {}).get("message_id")
339
+ elif "message_id" in update:
340
+ message_id = update.get("message_id")
341
+
342
+ if message_id and not self._is_duplicate(str(message_id)):
343
+ await self._process_update(update)
344
+
345
+ else:
346
+ # ----- منطق Polling (بدون تغییر) -----
347
+ get_updates_response = await self.get_updates(offset_id=self._offset_id, limit=100)
348
+ if get_updates_response and get_updates_response.get("data"):
349
+ updates = get_updates_response["data"].get("updates", [])
350
+ self._offset_id = get_updates_response["data"].get("next_offset_id", self._offset_id)
351
+
352
+ for update in updates:
353
+ message_id = None
354
+ if update.get("type") == "NewMessage":
355
+ message_id = update.get("new_message", {}).get("message_id")
356
+ elif update.get("type") == "ReceiveQuery":
357
+ message_id = update.get("inline_message", {}).get("message_id")
358
+ elif "message_id" in update:
359
+ message_id = update.get("message_id")
360
+
361
+ if message_id and not self._is_duplicate(str(message_id)):
362
+ await self._process_update(update)
363
+
364
+ await asyncio.sleep(0.1) # وقفه کوتاه برای جلوگیری از مصرف CPU
365
+ except Exception as e:
366
+ print(f"❌ Error in run loop: {e}")
367
+ await asyncio.sleep(5) # وقفه طولانی‌تر در صورت بروز خطا
368
+ finally:
369
+ if self._aiohttp_session:
370
+ await self._aiohttp_session.close()
371
+ print("Bot stopped and session closed.")
372
+
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
379
+ return await self._post("sendMessage", payload)
380
+
381
+ def _get_client(self) -> Client_get:
382
+ if self.session_name:
383
+ return Client_get(self.session_name, self.auth, self.Key, self.platform)
384
+ else:
385
+ return Client_get(show_last_six_words(self.token), self.auth, self.Key, self.platform)
386
+
387
+ async def check_join(self, channel_guid: str, chat_id: str = None) -> Union[bool, list[str]]:
388
+ client = self._get_client()
389
+
390
+ if chat_id:
391
+ chat_info_data = await self.get_chat(chat_id)
392
+ chat_info = chat_info_data.get('data', {}).get('chat', {})
393
+ username = chat_info.get('username')
394
+ user_id = chat_info.get('user_id')
395
+
396
+ # Since client methods are sync, run them in a thread pool
397
+ if username:
398
+ result = await asyncio.to_thread(self.get_all_member, channel_guid, search_text=username)
399
+ members = result.get('in_chat_members', [])
400
+ return any(m.get('username') == username for m in members)
401
+ elif user_id:
402
+ member_guids = await asyncio.to_thread(client.get_all_members, channel_guid, just_get_guids=True)
403
+ return user_id in member_guids
404
+ return False
405
+
406
+ def get_all_member(self, channel_guid: str, search_text: str = None, start_id: str = None, just_get_guids: bool = False):
407
+ # This is a sync method that will be called with asyncio.to_thread
408
+ client = self._get_client()
409
+ return client.get_all_members(channel_guid, search_text, start_id, just_get_guids)
410
+
411
+ async def send_poll(self, chat_id: str, question: str, options: List[str]) -> Dict[str, Any]:
412
+ return await self._post("sendPoll", {"chat_id": chat_id, "question": question, "options": options})
413
+
414
+ 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]:
415
+ payload = {"chat_id": chat_id, "latitude": latitude, "longitude": longitude, "disable_notification": disable_notification}
416
+ if inline_keypad: payload["inline_keypad"] = inline_keypad
417
+ if reply_to_message_id: payload["reply_to_message_id"] = reply_to_message_id
418
+ if chat_keypad_type: payload["chat_keypad_type"] = chat_keypad_type
419
+ return await self._post("sendLocation", {k: v for k, v in payload.items() if v is not None})
420
+
421
+ async def send_contact(self, chat_id: str, first_name: str, last_name: str, phone_number: str) -> Dict[str, Any]:
422
+ return await self._post("sendContact", {"chat_id": chat_id, "first_name": first_name, "last_name": last_name, "phone_number": phone_number})
423
+
424
+ async def get_chat(self, chat_id: str) -> Dict[str, Any]:
425
+ return await self._post("getChat", {"chat_id": chat_id})
426
+
427
+ async def upload_media_file(self, upload_url: str, name: str, path: Union[str, Path]) -> str:
428
+ is_temp_file = False
429
+ session = await self._get_session()
430
+
431
+ if isinstance(path, str) and path.startswith("http"):
432
+ async with session.get(path) as response:
433
+ if response.status != 200:
434
+ raise Exception(f"Failed to download file from URL ({response.status})")
435
+
436
+ content = await response.read()
437
+
438
+ with tempfile.NamedTemporaryFile(delete=False) as temp_file:
439
+ temp_file.write(content)
440
+ path = temp_file.name
441
+ is_temp_file = True
442
+
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.
444
+
445
+ 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
+
447
+ async def file_progress_generator(file_path, chunk_size=8192):
448
+ async with aiofiles.open(file_path, 'rb') as f:
449
+ while True:
450
+ chunk = await f.read(chunk_size)
451
+ if not chunk:
452
+ break
453
+ progress_bar.update(len(chunk))
454
+ yield chunk
455
+
456
+ data = aiohttp.FormData()
457
+ data.add_field('file', file_progress_generator(path), filename=name, content_type='application/octet-stream')
458
+
459
+ async with session.post(upload_url, data=data) as response:
460
+ progress_bar.close()
461
+ if response.status != 200:
462
+ raise Exception(f"Upload failed ({response.status}): {await response.text()}")
463
+
464
+ json_data = await response.json()
465
+ if is_temp_file:
466
+ os.remove(path)
467
+
468
+ return json_data.get('data', {}).get('file_id')
469
+
470
+ async def get_upload_url(self, media_type: Literal['File', 'Image', 'Voice', 'Music', 'Gif', 'Video']) -> str:
471
+ allowed = ['File', 'Image', 'Voice', 'Music', 'Gif', 'Video']
472
+ if media_type not in allowed:
473
+ raise ValueError(f"Invalid media type. Must be one of {allowed}")
474
+ result = await self._post("requestSendFile", {"type": media_type})
475
+ return result.get("data", {}).get("upload_url")
476
+
477
+ 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]:
478
+ payload = {"chat_id": chat_id, "file_id": file_id, "text": text, "disable_notification": disable_notification, "chat_keypad_type": chat_keypad_type}
479
+ if chat_keypad: payload["chat_keypad"] = chat_keypad
480
+ if inline_keypad: payload["inline_keypad"] = inline_keypad
481
+ if reply_to_message_id: payload["reply_to_message_id"] = str(reply_to_message_id)
482
+ return await self._post("sendFile", payload)
483
+
484
+ 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):
485
+ if path:
486
+ file_name = file_name or Path(path).name
487
+ upload_url = await self.get_upload_url(media_type)
488
+ file_id = await self.upload_media_file(upload_url, file_name, path)
489
+ if not file_id:
490
+ raise ValueError("Either path or file_id must be provided.")
491
+ 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
+
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]:
494
+ 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]:
497
+ 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
+
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]:
500
+ 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
+
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]:
503
+ 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
+
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]:
506
+ 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
+
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]:
509
+ 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
+
511
+ async def forward_message(self, from_chat_id: str, message_id: str, to_chat_id: str, disable_notification: bool = False) -> Dict[str, Any]:
512
+ 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})
513
+
514
+ async def edit_message_text(self, chat_id: str, message_id: str, text: str) -> Dict[str, Any]:
515
+ return await self._post("editMessageText", {"chat_id": chat_id, "message_id": message_id, "text": text})
516
+
517
+ async def edit_inline_keypad(self, chat_id: str, message_id: str, inline_keypad: Dict[str, Any]) -> Dict[str, Any]:
518
+ return await self._post("editMessageKeypad", {"chat_id": chat_id, "message_id": message_id, "inline_keypad": inline_keypad})
519
+
520
+ async def delete_message(self, chat_id: str, message_id: str) -> Dict[str, Any]:
521
+ return await self._post("deleteMessage", {"chat_id": chat_id, "message_id": message_id})
522
+
523
+ async def set_commands(self, bot_commands: List[Dict[str, str]]) -> Dict[str, Any]:
524
+ return await self._post("setCommands", {"bot_commands": bot_commands})
525
+
526
+ async def update_bot_endpoint(self, url: str, type: str) -> Dict[str, Any]:
527
+ return await self._post("updateBotEndpoints", {"url": url, "type": type})
528
+
529
+ async def remove_keypad(self, chat_id: str) -> Dict[str, Any]:
530
+ return await self._post("editChatKeypad", {"chat_id": chat_id, "chat_keypad_type": "Removed"})
531
+
532
+ async def edit_chat_keypad(self, chat_id: str, chat_keypad: Dict[str, Any]) -> Dict[str, Any]:
533
+ return await self._post("editChatKeypad", {"chat_id": chat_id, "chat_keypad_type": "New", "chat_keypad": chat_keypad})
534
+
535
+ async def get_name(self, chat_id: str) -> str:
536
+ try:
537
+ chat = await self.get_chat(chat_id)
538
+ chat_info = chat.get("data", {}).get("chat", {})
539
+ first_name = chat_info.get("first_name", "")
540
+ last_name = chat_info.get("last_name", "")
541
+
542
+ full_name = f"{first_name} {last_name}".strip()
543
+ return full_name if full_name else "Unknown"
544
+ except Exception:
545
+ return "Unknown"
546
+
547
+ async def get_username(self, chat_id: str) -> str:
548
+ chat_info = await self.get_chat(chat_id)
549
+ 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.19
3
+ Version: 4.5.0
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=h818Eoi84-odyf2Ed1H_suL8VSAHCN-TYM1Jaww2LD4,32370
2
+ rubka/api.py,sha256=KGcbBPJh7RaoYPuJTcpzAfhbgZ1PerSA5wn_tzelDq0,35045
3
+ rubka/asynco.py,sha256=osfR2hKKEOT1Pqp0Meq3Iu-_Oox_SjwEesKJzv_Jqgo,30581
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.19.dist-info/METADATA,sha256=HUXTIlzStSyrHVnjRxFCByBBrZd6TH8RpFYa9_tpe_s,33217
36
- rubka-4.4.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
- rubka-4.4.19.dist-info/top_level.txt,sha256=vy2A4lot11cRMdQS-F4HDCIXL3JK8RKfu7HMDkezJW4,6
38
- rubka-4.4.19.dist-info/RECORD,,
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,,
File without changes