Rubka 5.2.0__py3-none-any.whl → 6.4.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/asynco.py CHANGED
@@ -190,6 +190,324 @@ class Robot:
190
190
  """Check if the bot token is valid."""
191
191
  if (await self.get_me())['status'] != "OK":
192
192
  raise InvalidTokenError("The provided bot token is invalid or expired.")
193
+ from typing import Callable, Any, Optional, List
194
+
195
+ def on_message_private(
196
+ self,
197
+ chat_id: Optional[Union[str, List[str]]] = None,
198
+ commands: Optional[List[str]] = None,
199
+ filters: Optional[Callable[[Message], bool]] = None,
200
+ sender_id: Optional[Union[str, List[str]]] = None,
201
+ sender_type: Optional[str] = None,
202
+ allow_forwarded: bool = True,
203
+ allow_files: bool = True,
204
+ allow_stickers: bool = True,
205
+ allow_polls: bool = True,
206
+ allow_contacts: bool = True,
207
+ allow_locations: bool = True,
208
+ min_text_length: Optional[int] = None,
209
+ max_text_length: Optional[int] = None,
210
+ contains: Optional[str] = None,
211
+ startswith: Optional[str] = None,
212
+ endswith: Optional[str] = None,
213
+ case_sensitive: bool = False
214
+ ):
215
+ """
216
+ Advanced decorator for handling only private messages with extended filters.
217
+ """
218
+
219
+ def decorator(func: Callable[[Any, Message], None]):
220
+ async def wrapper(bot, message: Message):
221
+
222
+ if not message.is_private:
223
+ return
224
+
225
+
226
+ if chat_id:
227
+ if isinstance(chat_id, str) and message.chat_id != chat_id:
228
+ return
229
+ if isinstance(chat_id, list) and message.chat_id not in chat_id:
230
+ return
231
+
232
+
233
+ if sender_id:
234
+ if isinstance(sender_id, str) and message.sender_id != sender_id:
235
+ return
236
+ if isinstance(sender_id, list) and message.sender_id not in sender_id:
237
+ return
238
+
239
+
240
+ if sender_type and message.sender_type != sender_type:
241
+ return
242
+
243
+
244
+ if not allow_forwarded and message.forwarded_from:
245
+ return
246
+
247
+
248
+ if not allow_files and message.file:
249
+ return
250
+ if not allow_stickers and message.sticker:
251
+ return
252
+ if not allow_polls and message.poll:
253
+ return
254
+ if not allow_contacts and message.contact_message:
255
+ return
256
+ if not allow_locations and (message.location or message.live_location):
257
+ return
258
+
259
+
260
+ if message.text:
261
+ text = message.text if case_sensitive else message.text.lower()
262
+ if min_text_length and len(message.text) < min_text_length:
263
+ return
264
+ if max_text_length and len(message.text) > max_text_length:
265
+ return
266
+ if contains and (contains if case_sensitive else contains.lower()) not in text:
267
+ return
268
+ if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
269
+ return
270
+ if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
271
+ return
272
+
273
+
274
+ if commands:
275
+ if not message.text:
276
+ return
277
+ parts = message.text.strip().split()
278
+ cmd = parts[0].lstrip("/")
279
+ if cmd not in commands:
280
+ return
281
+ message.args = parts[1:]
282
+
283
+
284
+ if filters and not filters(message):
285
+ return
286
+
287
+ return await func(bot, message)
288
+
289
+ self._message_handlers.append({
290
+ "func": wrapper,
291
+ "filters": filters,
292
+ "commands": commands,
293
+ "chat_id": chat_id,
294
+ "private_only": True,
295
+ "sender_id": sender_id,
296
+ "sender_type": sender_type
297
+ })
298
+ return wrapper
299
+ return decorator
300
+ def on_message_channel(
301
+ self,
302
+ chat_id: Optional[Union[str, List[str]]] = None,
303
+ commands: Optional[List[str]] = None,
304
+ filters: Optional[Callable[[Message], bool]] = None,
305
+ sender_id: Optional[Union[str, List[str]]] = None,
306
+ sender_type: Optional[str] = None,
307
+ allow_forwarded: bool = True,
308
+ allow_files: bool = True,
309
+ allow_stickers: bool = True,
310
+ allow_polls: bool = True,
311
+ allow_contacts: bool = True,
312
+ allow_locations: bool = True,
313
+ min_text_length: Optional[int] = None,
314
+ max_text_length: Optional[int] = None,
315
+ contains: Optional[str] = None,
316
+ startswith: Optional[str] = None,
317
+ endswith: Optional[str] = None,
318
+ case_sensitive: bool = False
319
+ ):
320
+ """
321
+ Advanced decorator for handling only channel messages with extended filters.
322
+ """
323
+
324
+ def decorator(func: Callable[[Any, Message], None]):
325
+ async def wrapper(bot, message: Message):
326
+
327
+ if not message.is_channel:
328
+ return
329
+
330
+
331
+ if chat_id:
332
+ if isinstance(chat_id, str) and message.chat_id != chat_id:
333
+ return
334
+ if isinstance(chat_id, list) and message.chat_id not in chat_id:
335
+ return
336
+
337
+
338
+ if sender_id:
339
+ if isinstance(sender_id, str) and message.sender_id != sender_id:
340
+ return
341
+ if isinstance(sender_id, list) and message.sender_id not in sender_id:
342
+ return
343
+
344
+
345
+ if sender_type and message.sender_type != sender_type:
346
+ return
347
+
348
+
349
+ if not allow_forwarded and message.forwarded_from:
350
+ return
351
+
352
+
353
+ if not allow_files and message.file:
354
+ return
355
+ if not allow_stickers and message.sticker:
356
+ return
357
+ if not allow_polls and message.poll:
358
+ return
359
+ if not allow_contacts and message.contact_message:
360
+ return
361
+ if not allow_locations and (message.location or message.live_location):
362
+ return
363
+
364
+
365
+ if message.text:
366
+ text = message.text if case_sensitive else message.text.lower()
367
+ if min_text_length and len(message.text) < min_text_length:
368
+ return
369
+ if max_text_length and len(message.text) > max_text_length:
370
+ return
371
+ if contains and (contains if case_sensitive else contains.lower()) not in text:
372
+ return
373
+ if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
374
+ return
375
+ if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
376
+ return
377
+
378
+
379
+ if commands:
380
+ if not message.text:
381
+ return
382
+ parts = message.text.strip().split()
383
+ cmd = parts[0].lstrip("/")
384
+ if cmd not in commands:
385
+ return
386
+ message.args = parts[1:]
387
+
388
+
389
+ if filters and not filters(message):
390
+ return
391
+
392
+ return await func(bot, message)
393
+
394
+ self._message_handlers.append({
395
+ "func": wrapper,
396
+ "filters": filters,
397
+ "commands": commands,
398
+ "chat_id": chat_id,
399
+ "group_only": True,
400
+ "sender_id": sender_id,
401
+ "sender_type": sender_type
402
+ })
403
+ return wrapper
404
+ return decorator
405
+ def on_message_group(
406
+ self,
407
+ chat_id: Optional[Union[str, List[str]]] = None,
408
+ commands: Optional[List[str]] = None,
409
+ filters: Optional[Callable[[Message], bool]] = None,
410
+ sender_id: Optional[Union[str, List[str]]] = None,
411
+ sender_type: Optional[str] = None,
412
+ allow_forwarded: bool = True,
413
+ allow_files: bool = True,
414
+ allow_stickers: bool = True,
415
+ allow_polls: bool = True,
416
+ allow_contacts: bool = True,
417
+ allow_locations: bool = True,
418
+ min_text_length: Optional[int] = None,
419
+ max_text_length: Optional[int] = None,
420
+ contains: Optional[str] = None,
421
+ startswith: Optional[str] = None,
422
+ endswith: Optional[str] = None,
423
+ case_sensitive: bool = False
424
+ ):
425
+ """
426
+ Advanced decorator for handling only group messages with extended filters.
427
+ """
428
+
429
+ def decorator(func: Callable[[Any, Message], None]):
430
+ async def wrapper(bot, message: Message):
431
+
432
+ if not message.is_group:
433
+ return
434
+
435
+
436
+ if chat_id:
437
+ if isinstance(chat_id, str) and message.chat_id != chat_id:
438
+ return
439
+ if isinstance(chat_id, list) and message.chat_id not in chat_id:
440
+ return
441
+
442
+
443
+ if sender_id:
444
+ if isinstance(sender_id, str) and message.sender_id != sender_id:
445
+ return
446
+ if isinstance(sender_id, list) and message.sender_id not in sender_id:
447
+ return
448
+
449
+
450
+ if sender_type and message.sender_type != sender_type:
451
+ return
452
+
453
+
454
+ if not allow_forwarded and message.forwarded_from:
455
+ return
456
+
457
+
458
+ if not allow_files and message.file:
459
+ return
460
+ if not allow_stickers and message.sticker:
461
+ return
462
+ if not allow_polls and message.poll:
463
+ return
464
+ if not allow_contacts and message.contact_message:
465
+ return
466
+ if not allow_locations and (message.location or message.live_location):
467
+ return
468
+
469
+
470
+ if message.text:
471
+ text = message.text if case_sensitive else message.text.lower()
472
+ if min_text_length and len(message.text) < min_text_length:
473
+ return
474
+ if max_text_length and len(message.text) > max_text_length:
475
+ return
476
+ if contains and (contains if case_sensitive else contains.lower()) not in text:
477
+ return
478
+ if startswith and not text.startswith(startswith if case_sensitive else startswith.lower()):
479
+ return
480
+ if endswith and not text.endswith(endswith if case_sensitive else endswith.lower()):
481
+ return
482
+
483
+
484
+ if commands:
485
+ if not message.text:
486
+ return
487
+ parts = message.text.strip().split()
488
+ cmd = parts[0].lstrip("/")
489
+ if cmd not in commands:
490
+ return
491
+ message.args = parts[1:]
492
+
493
+
494
+ if filters and not filters(message):
495
+ return
496
+
497
+ return await func(bot, message)
498
+
499
+ self._message_handlers.append({
500
+ "func": wrapper,
501
+ "filters": filters,
502
+ "commands": commands,
503
+ "chat_id": chat_id,
504
+ "group_only": True,
505
+ "sender_id": sender_id,
506
+ "sender_type": sender_type
507
+ })
508
+ return wrapper
509
+ return decorator
510
+
193
511
  def on_message(self, filters: Optional[Callable[[Message], bool]] = None, commands: Optional[List[str]] = None):
194
512
  def decorator(func: Callable[[Any, Message], None]):
195
513
  self._message_handlers.append({
@@ -219,6 +537,16 @@ class Robot:
219
537
  })
220
538
  return func
221
539
  return decorator
540
+ def on_callback_query(self, button_id: Optional[str] = None):
541
+ def decorator(func: Callable[[Any, Union[Message, InlineMessage]], None]):
542
+ if not hasattr(self, "_callback_handlers"):
543
+ self._callback_handlers = []
544
+ self._callback_handlers.append({
545
+ "func": func,
546
+ "button_id": button_id
547
+ })
548
+ return func
549
+ return decorator
222
550
  def callback_query_handler(self, button_id: Optional[str] = None):
223
551
  def decorator(func: Callable[[Any, Message], None]):
224
552
  if not hasattr(self, "_callback_handlers"):
@@ -1014,49 +1342,90 @@ class Robot:
1014
1342
  return user_id in member_guids
1015
1343
  return False
1016
1344
  async def send_button_join(
1017
- self,
1018
- chat_id,
1019
- title_button,
1020
- username,
1021
- text,
1022
- reply_to_message_id=None,
1023
- id="None"):
1345
+ self,
1346
+ chat_id,
1347
+ title_button : Union[str, list],
1348
+ username : Union[str, list],
1349
+ text,
1350
+ reply_to_message_id=None,
1351
+ id="None"):
1024
1352
  from .button import InlineBuilder
1025
- return await self.send_message(
1026
- chat_id=chat_id,
1027
- text=text,
1028
- inline_keypad=InlineBuilder()
1029
- .row(
1030
- InlineBuilder()
1031
- .button_join_channel(
1353
+ builder = InlineBuilder()
1354
+
1355
+ if isinstance(username, (list, tuple)) and isinstance(title_button, (list, tuple)):
1356
+ for t, u in zip(title_button, username):
1357
+ builder = builder.row(
1358
+ InlineBuilder().button_join_channel(
1359
+ text=t,
1360
+ id=id,
1361
+ username=u
1362
+ )
1363
+ )
1364
+ elif isinstance(username, (list, tuple)) and isinstance(title_button, str):
1365
+ for u in username:
1366
+ builder = builder.row(
1367
+ InlineBuilder().button_join_channel(
1368
+ text=title_button,
1369
+ id=id,
1370
+ username=u
1371
+ )
1372
+ )
1373
+ else:
1374
+ builder = builder.row(
1375
+ InlineBuilder().button_join_channel(
1032
1376
  text=title_button,
1033
1377
  id=id,
1034
1378
  username=username
1035
1379
  )
1036
- ).build(),
1380
+ )
1381
+ return await self.send_message(
1382
+ chat_id=chat_id,
1383
+ text=text,
1384
+ inline_keypad=builder.build(),
1037
1385
  reply_to_message_id=reply_to_message_id
1038
1386
  )
1039
1387
  async def send_button_link(
1040
- self,
1041
- chat_id,
1042
- title_button,
1043
- url,
1044
- text,
1045
- reply_to_message_id=None,
1046
- id="None"):
1388
+ self,
1389
+ chat_id,
1390
+ title_button: Union[str, list],
1391
+ url: Union[str, list],
1392
+ text,
1393
+ reply_to_message_id=None,
1394
+ id="None"
1395
+ ):
1047
1396
  from .button import InlineBuilder
1048
- return await self.send_message(
1049
- chat_id=chat_id,
1050
- text=text,
1051
- inline_keypad=InlineBuilder()
1052
- .row(
1053
- InlineBuilder()
1054
- .button_url_link(
1397
+ builder = InlineBuilder()
1398
+ if isinstance(url, (list, tuple)) and isinstance(title_button, (list, tuple)):
1399
+ for t, u in zip(title_button, url):
1400
+ builder = builder.row(
1401
+ InlineBuilder().button_url_link(
1402
+ text=t,
1403
+ id=id,
1404
+ url=u
1405
+ )
1406
+ )
1407
+ elif isinstance(url, (list, tuple)) and isinstance(title_button, str):
1408
+ for u in url:
1409
+ builder = builder.row(
1410
+ InlineBuilder().button_url_link(
1411
+ text=title_button,
1412
+ id=id,
1413
+ url=u
1414
+ )
1415
+ )
1416
+ else:
1417
+ builder = builder.row(
1418
+ InlineBuilder().button_url_link(
1055
1419
  text=title_button,
1056
1420
  id=id,
1057
1421
  url=url
1058
1422
  )
1059
- ).build(),
1423
+ )
1424
+
1425
+ return await self.send_message(
1426
+ chat_id=chat_id,
1427
+ text=text,
1428
+ inline_keypad=builder.build(),
1060
1429
  reply_to_message_id=reply_to_message_id
1061
1430
  )
1062
1431
 
rubka/button.py CHANGED
@@ -370,4 +370,35 @@ class InlineBuilder:
370
370
  }
371
371
 
372
372
  def build(self) -> Dict:
373
- return {"rows": self.rows}
373
+ return {"rows": self.rows}
374
+ class ChatKeypadBuilder:
375
+ def __init__(self):
376
+ self.rows: List[Dict[str, List[Dict[str, str]]]] = []
377
+
378
+ def row(self, *buttons: Dict[str, str]) -> "ChatKeypadBuilder":
379
+ """
380
+ یک ردیف دکمه به کی‌پد اضافه می‌کند.
381
+ ورودی: چند دیکشنری که نماینده دکمه‌ها هستند.
382
+ """
383
+ self.rows.append({"buttons": list(buttons)})
384
+ return self
385
+
386
+ def button(self, id: str, text: str, type: str = "Simple") -> Dict[str, str]:
387
+ """
388
+ دیکشنری یک دکمه می‌سازد.
389
+ """
390
+ return {"id": id, "type": type, "button_text": text}
391
+
392
+ def build(
393
+ self,
394
+ resize_keyboard: bool = True,
395
+ on_time_keyboard: bool = False
396
+ ) -> Dict[str, object]:
397
+ """
398
+ ساختار نهایی chat_keypad را می‌سازد.
399
+ """
400
+ return {
401
+ "rows": self.rows,
402
+ "resize_keyboard": resize_keyboard,
403
+ "on_time_keyboard": on_time_keyboard
404
+ }
rubka/context.py CHANGED
@@ -199,6 +199,10 @@ class Message:
199
199
  self.is_edited: bool = self.raw_data.get("is_edited", False)
200
200
  self.sender_type: str = self.raw_data.get("sender_type")
201
201
  self.args = []
202
+ self.is_user = self.chat_id.startswith("b")
203
+ self.is_private = self.chat_id.startswith("b")
204
+ self.is_group = self.chat_id.startswith("g")
205
+ self.is_channel = self.chat_id.startswith("c")
202
206
  self.reply_to_message_id: Optional[str] = self.raw_data.get("reply_to_message_id")
203
207
  self.forwarded_from = ForwardedFrom(self.raw_data["forwarded_from"]) if "forwarded_from" in self.raw_data else None
204
208
  self.file = File(self.raw_data["file"]) if "file" in self.raw_data else None