Skip to content
This repository has been archived by the owner on Jan 5, 2022. It is now read-only.

Use own command handler #10

Draft
wants to merge 93 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
ace390f
Start migrating to v2
norinorin Nov 11, 2021
bcaddbb
Rewrite all the important things
norinorin Nov 13, 2021
4be5db6
Fix things
norinorin Nov 13, 2021
6266ba4
Add hidden attr
norinorin Nov 13, 2021
aa6dc9b
Small bug fix
norinorin Nov 13, 2021
62deacd
Use the unused var
norinorin Nov 13, 2021
c7e1b60
Some bug fixes
norinorin Nov 13, 2021
24b9e11
Small bugs
norinorin Nov 16, 2021
1096cbc
Fix help command
norinorin Nov 16, 2021
86b32b1
Ensure to return the message
norinorin Nov 16, 2021
c9aaadd
Make the prefix optional
norinorin Nov 16, 2021
2a73720
Instantiate the context directly
norinorin Nov 18, 2021
64ae085
Fix converter
norinorin Nov 18, 2021
77801f2
Minor change
norinorin Nov 18, 2021
3067426
AttributeError fix
norinorin Nov 19, 2021
7c8f175
Fix if statement logic
norinorin Nov 19, 2021
b84a63a
Make use of top-gg/python-sdk
norinorin Nov 23, 2021
b19e914
Rename some methods
norinorin Nov 23, 2021
a17d52b
Fix wrong reference
norinorin Nov 23, 2021
f75ec8e
Make use of lifetime events
norinorin Nov 23, 2021
1283bb0
Fix invalid reference
norinorin Nov 23, 2021
bd3e586
Fix aliasing
norinorin Nov 23, 2021
f2de72b
Fix listener not registered
norinorin Nov 23, 2021
10e853a
Add kita
norinorin Dec 5, 2021
d457dde
Update .gitignore
norinorin Dec 5, 2021
a5371b0
Implement basic responses
norinorin Dec 5, 2021
9aa6a68
Implement more stuff
norinorin Dec 5, 2021
6d8abb2
Add interaction to the extra env
norinorin Dec 5, 2021
ddfb2f9
Remove py.typed
norinorin Dec 5, 2021
994a6e0
Remove lightbulb from dependency
norinorin Dec 5, 2021
046ae94
Update tox
norinorin Dec 5, 2021
20c57e7
Reuse tox env
norinorin Dec 5, 2021
5e031ab
Prepend python -m
norinorin Dec 5, 2021
1d0a4d2
Can you not
norinorin Dec 5, 2021
ef9bb1e
Reuse env
norinorin Dec 5, 2021
ab01df1
Pluralization
norinorin Dec 5, 2021
5755d11
Fix typehint
norinorin Dec 5, 2021
16fc2a9
Score pylint up
norinorin Dec 5, 2021
f768d7d
Target tox to kita instead
norinorin Dec 5, 2021
7241fd6
Use mypy at master
norinorin Dec 5, 2021
8da42b9
Implement extensions and fix type errors
norinorin Dec 6, 2021
05b3f63
Remove unused import
norinorin Dec 6, 2021
aac9832
Add and fix more things
norinorin Dec 6, 2021
4f3df7d
Tidy up things
norinorin Dec 6, 2021
073f3e0
Make the data covariant
norinorin Dec 6, 2021
58331a1
Make the initializer and finalizer global
norinorin Dec 6, 2021
384fe39
Fix extension hooks
norinorin Dec 6, 2021
cd4b060
Add examples
norinorin Dec 6, 2021
f20498d
Tidy up things
norinorin Dec 7, 2021
eb62240
Implement checks
norinorin Dec 7, 2021
ff680fe
Minor changes
norinorin Dec 7, 2021
1da8005
Update examples
norinorin Dec 7, 2021
a4e8100
Merge branch 'master' into kita
norinorin Dec 7, 2021
f8fd9dc
Fix typing
norinorin Dec 7, 2021
1a8db5b
Minor typing changes
norinorin Dec 7, 2021
f049302
Implement cooldowns
norinorin Dec 7, 2021
f29e8fa
Update examples
norinorin Dec 7, 2021
021c482
Fix typecheck error
norinorin Dec 7, 2021
82460ce
Make use of list comp
norinorin Dec 7, 2021
b15d4e0
Some internal changes
norinorin Dec 7, 2021
8c8e862
Rename is_inactive to is_expired
norinorin Dec 7, 2021
7ec521b
Implement contexts
norinorin Dec 8, 2021
e3bb7c5
Don't wrap KitaError instance
norinorin Dec 8, 2021
6f3e5ec
Add utils.get
norinorin Dec 8, 2021
e8148f8
Minor naming change
norinorin Dec 8, 2021
9b9fda7
Remove redundant stuff
norinorin Dec 8, 2021
ed1a230
Rename CommandInCooldown
norinorin Dec 8, 2021
7c94372
Start rewriting nokari
norinorin Dec 8, 2021
034d09c
Fix wrong reference
norinorin Dec 8, 2021
f9de6db
Fix things
norinorin Dec 8, 2021
64a2c6f
Load commands and listeners by default
norinorin Dec 8, 2021
cef341a
Make it possible to reuse another command's bucket manager
norinorin Dec 8, 2021
a37fbbf
Handler error with ephemeral messages
norinorin Dec 8, 2021
d0df521
Fix prompt
norinorin Dec 9, 2021
1ae85b4
Fix some of mypy errors
norinorin Dec 9, 2021
d509101
Lint back nokari
norinorin Dec 9, 2021
10fc165
Fix circular imports
norinorin Dec 9, 2021
b8d2a0d
Fix uninstantiated object
norinorin Dec 9, 2021
6cae276
Fix responses
norinorin Dec 9, 2021
7cf1c20
Fix error handler
norinorin Dec 9, 2021
d3f2c9f
Move the key popping
norinorin Dec 9, 2021
474d29a
Defer only when fetching the objects
norinorin Dec 9, 2021
668b8e5
Invalidate object caches on unload
norinorin Dec 9, 2021
305c87e
Tidy up the logic
norinorin Dec 9, 2021
b0d745f
Make the second style the default style
norinorin Dec 10, 2021
18b0b3b
Fix error handler
norinorin Dec 11, 2021
13e2d4c
Change value for consistency
norinorin Dec 11, 2021
efed4c7
Move the popping, again
norinorin Dec 11, 2021
dd5b663
Fix annotation check
norinorin Dec 11, 2021
0028dcf
Fix 404 responses
norinorin Dec 13, 2021
070e6a3
Replace lightbulb with kita
norinorin Dec 14, 2021
090777b
Implement dynamic cooldown
norinorin Dec 29, 2021
afade7b
Fix error when no guild ids were provided
norinorin Dec 29, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ SPOTIPY_CLIENT_SECRET=SPOTIFY_CLIENT_SECRET
POSTGRESQL_DSN=postgresql://user:pass@ip:port/database
LOG_LEVEL=INFO # Defaults to INFO if not present.
GUILD_LOGS_WEBHOOK_URL=
TOPGG_TOKEN=
TOPGG_WEBHOOK_AUTH=
GUILD_IDS=1234,5678

# This one is optional, use it at your own risk.
DISCORD_BROWSER=
9 changes: 3 additions & 6 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,11 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
- name: Format, lint and run tests
run: |
python -m pip install --upgrade pip
pip install tox
tox -epy39
- name: Format, lint and run tests
run: |
tox -eisort,black,lint,typecheck,test
tox -eisort,black,lint,typecheck
- name: Check for modified files
id: git-check
run: echo ::set-output name=modified::$(if git diff-index --quiet HEAD --; then echo "false"; else echo "true"; fi)
Expand All @@ -33,4 +30,4 @@ jobs:
git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com'
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
git commit -am "Apply auto-formatting" -m "Based on isort and black."
git push
git push
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ nokari.log
nokari.pyz

# tmp
tmp/
tmp/
kita_test/
6 changes: 4 additions & 2 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ disable=
protected-access,
too-few-public-methods,
format,
cyclic-import
cyclic-import,
redefined-builtin

[BASIC]
argument-naming-style=snake_case
Expand All @@ -31,4 +32,5 @@ ignore-imports=no
min-similarity-lines=4

[DESIGN]
max-parents=8
max-parents=8
max-args=10
18 changes: 18 additions & 0 deletions kita/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
__author__ = "Norizon"
__version__ = "SNAPSHOT"
__license__ = "MPL-2.0"

from kita.buckets import *
from kita.checks import *
from kita.command_handlers import *
from kita.commands import *
from kita.contexts import *
from kita.cooldowns import *
from kita.data import *
from kita.errors import *
from kita.events import *
from kita.extensions import *
from kita.options import *
from kita.responses import *
from kita.typedefs import *
from kita.utils import *
180 changes: 180 additions & 0 deletions kita/buckets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
from __future__ import annotations

import asyncio
import logging
from time import monotonic
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union, cast

from hikari.events.interaction_events import InteractionCreateEvent

from kita.typedefs import BucketHash, HashGetter, LimitGetter, PeriodGetter

if TYPE_CHECKING:
from kita.command_handlers import GatewayCommandHandler

EXPIRE_AFTER = 30.0

__all__ = ("Bucket", "BucketManager")
_LOGGER = logging.getLogger("kita.buckets")


class EmptyBucketError(Exception):
def __init__(self, *args: Any, retry_after: float) -> None:
self.retry_after = retry_after
super().__init__(*args)


class Bucket:
__slots__ = ("manager", "tokens", "reset_at", "hash", "limit", "period")

def __init__(self, manager: BucketManager, hash_: BucketHash) -> None:
self.manager: BucketManager = manager
self.tokens: int = 0
self.reset_at: float = 0.0
self.hash = hash_
self.limit: int
self.period: float

def set_constraints(
self, limit: Optional[int] = None, period: Optional[float] = None
) -> None:
if limit is not None:
self.limit = limit

if period is not None:
self.period = period

def consume_token(self) -> None:
self.tokens -= 1

def acquire(self) -> None:
if self.is_exhausted:
raise EmptyBucketError(
"you run out of tokens.", retry_after=self.next_window
)

self.consume_token()
return None

@property
def is_exhausted(self) -> bool:
if self.reset_at > (now := monotonic()):
return self.tokens <= 0

self.reset_at = now + self.period
self.tokens = self.limit
return False

@property
def next_window(self) -> float:
return self.reset_at - monotonic()

@property
def is_expired(self) -> bool:
return self.reset_at + EXPIRE_AFTER < monotonic()

def invalidate(self) -> None:
del self.manager.buckets[self.hash]

def __repr__(self) -> str:
return (
f"<Bucket manager={self.manager.name!r} "
f"limit={self.limit} period={self.period} "
f"tokens={self.tokens} next_window={self.next_window}>"
)


class BucketManager:
__slots__ = ("name", "buckets", "hash_getter", "period", "limit", "gc_task")

def __init__(
self,
name: str,
hash_getter: HashGetter,
limit: Union[LimitGetter, int],
period: Union[PeriodGetter, float],
):
self.name = name
self.buckets: Dict[BucketHash, Bucket] = {}
self.hash_getter = hash_getter
self.limit = limit
self.period = period
self.gc_task: Optional[asyncio.Task[None]] = None

async def get_limit(self, handler: GatewayCommandHandler, hash_: BucketHash) -> int:
return (
await handler._invoke_callback(self.limit, hash_)
if callable(self.limit)
else self.limit
)

async def get_period(
self, handler: GatewayCommandHandler, hash_: BucketHash
) -> float:
return (
await handler._invoke_callback(self.period, hash_)
if callable(self.period)
else self.period
)

async def get_or_create_bucket(
self, handler: GatewayCommandHandler, event: InteractionCreateEvent
) -> Bucket:
bucket_hash = await handler._invoke_callback(self.hash_getter, event)
if not (bucket := self.buckets.get(bucket_hash)):
self.buckets[bucket_hash] = bucket = Bucket(self, bucket_hash)

bucket.set_constraints(
await self.get_limit(handler, bucket_hash),
await self.get_period(handler, bucket_hash),
)

self.ensure_gc_task()
return bucket

def ensure_gc_task(self) -> None:
if self.is_running:
return

self.gc_task = asyncio.create_task(self._do_gc())

def close(self) -> None:
if not self.is_running:
return

assert self.gc_task is not None
self.gc_task.cancel()
self.gc_task = None

async def _do_gc(self) -> None:
_LOGGER.debug("started running gc (%s bucket manager)", self.name)
while 1:
# runs periodically every `EXPIRE_AFTER` seconds
await asyncio.sleep(EXPIRE_AFTER)

if not self.buckets:
# if there's no bucket yet or all the buckets have been dead
# the task shall stop.
_LOGGER.debug(
"no buckets were found, stopping the task... (%s bucket manager)",
self.name,
)
self.close()
return

dead_buckets = [
bucket for bucket in self.buckets.values() if bucket.is_expired
]

for bucket in dead_buckets:
bucket.invalidate()

_LOGGER.debug(
"%d buckets were invalidated (%s bucket manager)",
len(dead_buckets),
self.name,
)

@property
def is_running(self) -> bool:
return self.gc_task is not None
112 changes: 112 additions & 0 deletions kita/checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from typing import Any, Callable, Literal, MutableMapping, Type, cast

from hikari.events.interaction_events import InteractionCreateEvent
from hikari.interactions.command_interactions import CommandInteraction
from hikari.permissions import Permissions

from kita.command_handlers import GatewayCommandHandler
from kita.contexts import Context
from kita.data import data
from kita.errors import (
CheckAnyError,
DMOnlyError,
GuildOnlyError,
MissingAnyPermissionsError,
MissingPermissionsError,
OwnerOnlyError,
)
from kita.typedefs import CallableProto, ICommandCallback
from kita.utils import ensure_checks

__all__ = (
"with_check",
"with_check_any",
"guild_only",
"dm_only",
"owner_only",
"has_all_permissions",
"has_any_permissions",
)


def with_check(
predicate: CallableProto,
) -> Callable[[CallableProto], ICommandCallback]:
def decorator(_command: CallableProto) -> ICommandCallback:
command = cast(ICommandCallback, _command)
ensure_checks(command)
command.__checks__.append(predicate)
return command

return decorator


def with_check_any(
*predicates: CallableProto,
) -> Callable[[CallableProto], ICommandCallback]:
async def _inner(
ctx: Context = data(Context),
) -> Literal[True]:
exceptions = []
extra_env: MutableMapping[Type[Any], Any] = {
InteractionCreateEvent: ctx.event,
CommandInteraction: ctx.interaction,
type(ctx): ctx,
}
for predicate in predicates:
try:
if await ctx.handler._invoke_callback(predicate, extra_env=extra_env):
return True
except Exception as exc:
exceptions.append(exc)

raise CheckAnyError(predicates, exceptions)

return with_check(_inner)


def guild_only(
interaction: CommandInteraction = data(CommandInteraction),
) -> Literal[True]:
if interaction.guild_id is None:
raise GuildOnlyError(f"command {interaction.command_name!r} is guild only.")
return True


def dm_only(
interaction: CommandInteraction = data(CommandInteraction),
) -> Literal[True]:
if interaction.guild_id is not None:
raise DMOnlyError(f"command {interaction.command_name!r} is dm only.")
return True


def owner_only(
interaction: CommandInteraction = data(CommandInteraction),
handler: GatewayCommandHandler = data(GatewayCommandHandler),
) -> Literal[True]:
if interaction.user.id not in handler.owner_ids:
raise OwnerOnlyError(f"command {interaction.command_name!r} is owner only.")
return True


def has_all_permissions(perms: Permissions) -> CallableProto:
def inner(
interaction: CommandInteraction = data(CommandInteraction),
) -> Literal[True]:
if not (member := interaction.member) or (member.permissions & perms) != perms:
raise MissingPermissionsError(perms)
return True

return inner


def has_any_permissions(perms: Permissions) -> CallableProto:
def inner(
interaction: CommandInteraction = data(CommandInteraction),
) -> Literal[True]:
if not ((member := interaction.member) and member.permissions & perms):
raise MissingAnyPermissionsError(perms)
return True

return inner
Loading