|
3 | 3 | import asyncio
|
4 | 4 | import datetime
|
5 | 5 | import functools
|
| 6 | +import inspect |
6 | 7 | from logging import getLogger
|
7 | 8 | from typing import Callable, Optional, TypeVar, Dict, Union, Type
|
8 | 9 |
|
@@ -438,3 +439,227 @@ def bucket(self) -> CooldownBucketProtocol:
|
438 | 439 | def func(self) -> Optional[Callable]:
|
439 | 440 | """Returns the wrapped function."""
|
440 | 441 | return self._func
|
| 442 | + |
| 443 | + |
| 444 | +class TriggerCooldown: |
| 445 | + """ |
| 446 | + Creates a trigger cooldown. |
| 447 | +
|
| 448 | + This is useful if you want to be able to trigger a specific time_period cooldown |
| 449 | + inside the command itself. |
| 450 | +
|
| 451 | + TriggerCooldown creates two cooldonws in one instance: |
| 452 | +
|
| 453 | + - Normal cooldown. The same cooldown as @cooldowns.cooldown() |
| 454 | + - Trigger cooldown. A secondary cooldown that can only be activate |
| 455 | + with `.trigger()` |
| 456 | +
|
| 457 | + Parameters |
| 458 | + ---------- |
| 459 | + limit : `int` |
| 460 | + How many call's can be made in the time |
| 461 | + period specified by ``time_period``. |
| 462 | +
|
| 463 | + time_period : `Union[float, datetime.timedelta]` |
| 464 | + The time period related to ``limit``. This is seconds. |
| 465 | +
|
| 466 | + bucket : `Optional[CooldownBucketProtocol], optional` |
| 467 | + The :class:`Bucket` implementation to use |
| 468 | + as a bucket to separate cooldown buckets. |
| 469 | +
|
| 470 | + check : `Optional[MaybeCoro], optional` |
| 471 | + A Callable which dictates whether |
| 472 | + to apply the cooldown on current invoke. |
| 473 | +
|
| 474 | + If this Callable returns a truthy value, |
| 475 | + then the cooldown will be used for the current call. |
| 476 | +
|
| 477 | + I.e. If you wished to bypass cooldowns, you |
| 478 | + would return False if you invoked the Callable. |
| 479 | + |
| 480 | + cooldown_id: Optional[Union[int, str]] |
| 481 | + Useful for resetting individual stacked cooldowns. |
| 482 | + This should be unique globally, |
| 483 | + behaviour is not guaranteed if not unique. |
| 484 | +
|
| 485 | + .. note:: |
| 486 | +
|
| 487 | + This check will be given the same arguments as |
| 488 | + the item you are applying the cooldown to. |
| 489 | + |
| 490 | + Usage |
| 491 | + ----- |
| 492 | + - First create an instance of TriggerCooldown() with |
| 493 | + the desired parameters. |
| 494 | +
|
| 495 | + ``` |
| 496 | + trigger_cooldown = cooldowns.TriggerCooldown(1, 5, cooldowns.SlashBucket.author) |
| 497 | + ``` |
| 498 | + |
| 499 | + - Then add the instance as a decorator to your command! |
| 500 | +
|
| 501 | + ``` |
| 502 | + @nextcord.slash_command() |
| 503 | + @trigger_cooldown |
| 504 | + async def command(): |
| 505 | + ``` |
| 506 | +
|
| 507 | + The instance has to be defined in the same scope as the decorator! |
| 508 | + Now, `command()` has applied a normal cooldown of `1 limit` and |
| 509 | + `5 time_period`, as we defined it. |
| 510 | +
|
| 511 | + - Finally, inside your command, you can `trigger` the trigger cooldown: |
| 512 | +
|
| 513 | + ``` |
| 514 | + async def command(): |
| 515 | + # Do things |
| 516 | + trigger_cooldown.trigger(30) |
| 517 | + # You can still do things after this. |
| 518 | + # Even you can `interaction.send()`. |
| 519 | + ``` |
| 520 | +
|
| 521 | + From the moment when the cooldown was triggered by `.trigger(30)`, every |
| 522 | + single call to this command within 30 seconds will raise CallableOnCooldown! |
| 523 | + |
| 524 | + Raises |
| 525 | + ------ |
| 526 | + `RuntimeError` |
| 527 | + Expected the decorated function to be a coroutine. |
| 528 | + `CallableOnCooldown` |
| 529 | + This call resulted in a cooldown being put into effect. |
| 530 | +
|
| 531 | +
|
| 532 | +
|
| 533 | + """ |
| 534 | + def __init__( |
| 535 | + self, |
| 536 | + limit: int, |
| 537 | + time_period: Union[float, datetime.timedelta], |
| 538 | + bucket: Optional[CooldownBucketProtocol] = None, |
| 539 | + *, |
| 540 | + cooldown_id: Optional[Union[int, str]] = None, |
| 541 | + check: Optional[MaybeCoro] = default_check, |
| 542 | + ): |
| 543 | + |
| 544 | + self.triggered = False |
| 545 | + |
| 546 | + self.limit = limit |
| 547 | + self.time_period = time_period |
| 548 | + self.bucket = bucket |
| 549 | + self.cooldown_id = cooldown_id |
| 550 | + self.check = check |
| 551 | + |
| 552 | + # Normal Cooldown |
| 553 | + self.cooldown = Cooldown( |
| 554 | + limit= self.limit, |
| 555 | + time_period= self.time_period, |
| 556 | + bucket= self.bucket, |
| 557 | + cooldown_id= self.cooldown_id, |
| 558 | + check= self.check |
| 559 | + ) |
| 560 | + |
| 561 | + # Trigger Cooldown |
| 562 | + self.trigger_cooldown = Cooldown( |
| 563 | + limit= 1, |
| 564 | + time_period= self.time_period, |
| 565 | + bucket= self.bucket, |
| 566 | + cooldown_id= self.cooldown_id, |
| 567 | + check= self.check |
| 568 | + ) |
| 569 | + |
| 570 | + if cooldown_id: |
| 571 | + utils.shared_cooldown_refs[cooldown_id] = self.cooldown |
| 572 | + |
| 573 | + async def trigger(self, time_period: Union[float, datetime.timedelta]) -> None: |
| 574 | + """|coro| |
| 575 | +
|
| 576 | + Trigger the Trigger Cooldown instantly. Has to be awaited. |
| 577 | +
|
| 578 | + Parameters |
| 579 | + ---------- |
| 580 | + time_period : `Union[float, datetime.timedelta]` |
| 581 | + The time period that cooldwon will remain triggered. |
| 582 | + """ |
| 583 | + self.triggered = True |
| 584 | + self.trigger_cooldown.time_period = ( |
| 585 | + time_period |
| 586 | + if isinstance(time_period, (float, int)) |
| 587 | + else time_period.total_seconds() |
| 588 | + ) |
| 589 | + |
| 590 | + # Triggers the Cooldown leaving bucket.current = 0 |
| 591 | + frame = inspect.currentframe().f_back |
| 592 | + _, _, _, values = inspect.getargvalues(frame) |
| 593 | + args = tuple(values.values()) |
| 594 | + |
| 595 | + async with self.trigger_cooldown(*args): |
| 596 | + return None |
| 597 | + |
| 598 | + |
| 599 | + def __call__(self, func: Callable) -> Callable: |
| 600 | + """ |
| 601 | + |
| 602 | + Called as a decorator. |
| 603 | +
|
| 604 | + Parameters |
| 605 | + ---------- |
| 606 | + func : `Callable` |
| 607 | + The function being decorated. |
| 608 | +
|
| 609 | + Returns |
| 610 | + ------- |
| 611 | + `Callable` |
| 612 | + Decorator |
| 613 | +
|
| 614 | + Raises |
| 615 | + ------ |
| 616 | + `RuntimeError` |
| 617 | + When given function is not coroutine. |
| 618 | + """ |
| 619 | + |
| 620 | + if not asyncio.iscoroutinefunction(func): |
| 621 | + raise RuntimeError( |
| 622 | + f"Expected `func` to be a coroutine, " |
| 623 | + f"found {func} of type {func.__class__.__name__!r} instead" # noqa |
| 624 | + ) |
| 625 | + # Links the cooldowns to the given function. |
| 626 | + self.cooldown._func = func |
| 627 | + self.trigger_cooldown._func = func |
| 628 | + |
| 629 | + @functools.wraps(func) |
| 630 | + async def inner(*args, **kwargs): |
| 631 | + use_cooldown = await maybe_coro(self.check, *args, **kwargs) |
| 632 | + if not use_cooldown: |
| 633 | + return await maybe_coro(func, *args, **kwargs) |
| 634 | + |
| 635 | + self_arg = None |
| 636 | + if "self" in kwargs: |
| 637 | + self_arg = kwargs.pop("self") |
| 638 | + |
| 639 | + # If the cooldown is triggered... |
| 640 | + if self.triggered: |
| 641 | + # If still on triggered cooldown... |
| 642 | + if self.trigger_cooldown.remaining_calls(*args, **kwargs) < 1: |
| 643 | + # Runs the Trigger Cooldown. |
| 644 | + async with self.trigger_cooldown(*args, **kwargs): |
| 645 | + if self_arg: |
| 646 | + kwargs["self"] = self_arg |
| 647 | + result = await func(*args, **kwargs) |
| 648 | + else: |
| 649 | + result = await func(*args, **kwargs) |
| 650 | + return result |
| 651 | + # If not, untrigger the cooldown. |
| 652 | + else: |
| 653 | + self.triggered = False |
| 654 | + # If the cooldown is not triggered. |
| 655 | + # Runs the normal Cooldown. |
| 656 | + async with self.cooldown(*args, **kwargs): |
| 657 | + if self_arg: |
| 658 | + kwargs["self"] = self_arg |
| 659 | + result = await func(*args, **kwargs) |
| 660 | + else: |
| 661 | + result = await func(*args, **kwargs) |
| 662 | + return result |
| 663 | + # Return the decorator. |
| 664 | + return inner |
| 665 | + |
0 commit comments