GithubHelp home page GithubHelp logo

fasterspeeding / tanjun Goto Github PK

View Code? Open in Web Editor NEW
89.0 1.0 14.0 59 MB

A flexible command framework designed to extend the Hikari experience

Home Page: https://tanjun.cursed.solutions/usage/

License: BSD 3-Clause "New" or "Revised" License

Python 100.00%
discord slash-commands discord-api discord-bot discord-bot-framework hikari tanjun hacktoberfest dependency-injection

tanjun's Introduction

Tanjun

A flexible command framework designed to extend Hikari.

Installation

You can install Tanjun from PyPI using the following command in any Python 3.9 or above environment.

python -m pip install -U hikari-tanjun

Quick Usage

For usage see the documentation, usage guide, and tutorials and articles.

Community Resources

  • Tan-chan is a general utility library for Tanjun. It includes a command annotation parsing extension which parses docstrings to get the descriptions of slash commands and their options.
  • Redis based implementations of the async cache dependency can be found in hikari-sake >=v1.0.1a1 (exposed by [sake.redis.ResourceClient.add_to_tanjun][]).

Support

Hikari's support guild provides for support for Tanjun.

Contributing

Before contributing you should read through the contributing guidelines and the code of conduct.

tanjun's People

Contributors

a5rocks avatar atomicparade avatar crazygmr101 avatar dependabot[bot] avatar fasterspeeding avatar github-actions[bot] avatar googlegenius avatar ikbenolie5 avatar nereg avatar nxtlo avatar patchwork-systems avatar pre-commit-ci[bot] avatar thesadru avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

tanjun's Issues

Commands with tanjun.with_all_checks(...) raise TypeError on calling

Context

When defining a tanjun command with a tanjun.with_all_checks(...) decorator, the command raises TypeError on calling and fails to execute.
I have just updated my tanjun version to v2.5.2a1 and this issue started happening from that point onwards.
I have checked that this issue is not a duplicate and removed all sensitive info from the included code.

I am using Python 3.10.5.

Steps to Reproduce

  1. pip install hikari-tanjun==v2.5.2a1
  2. Run the code below:
import hikari as hk
import tanjun as tj

TOKEN = '...'
GUILD = ...


comp = tj.Component()


def check_1(ctx: tj.abc.Context):
    return False


@comp.with_command
@tj.as_message_command_group('abc')
async def group1(ctx: tj.abc.MessageContext):
    await ctx.respond('foo')


@comp.with_command
@tj.with_all_checks(check_1)
@tj.as_slash_command('abc', "abc desc")
async def subcmd2(ctx: tj.abc.SlashContext):
    await ctx.respond('bar')


client = (
    tj.Client.from_gateway_bot(
        bot := hk.GatewayBot(TOKEN), declare_global_commands=GUILD
    )
    .add_component(comp)
    .add_prefix('!')
)


bot.run()
  1. Go to the bot's assigned guild and use the command /abc or !abc

Expected Results

Nothing should be raised and both commands should execute properly; responding with a message "foo" and "bar" respectively.

Actual Results

The message command !abc executes properly, but a TypeError is raised when called the slash command /abc with the following traceback;

E 2022-07-03 03:01:28,429 hikari.event_manager: an exception occurred handling an event (GuildMessageCreateEvent)
Traceback (most recent call last):
  File "/.../.venv/lib/python3.10/site-packages/tanjun/clients.py", line 2505, in on_message_create_event
    if await component.execute_message(ctx, hooks=hooks):
  File "/.../.venv/lib/python3.10/site-packages/tanjun/components.py", line 1258, in execute_message
    async for name, command in self._check_message_context(ctx):
  File "/.../.venv/lib/python3.10/site-packages/tanjun/components.py", line 1145, in _check_message_context
    if await command.check_context(ctx):
  File "/.../.lyra/.venv/lib/python3.10/site-packages/tanjun/commands/message.py", line 291, in check_context
    result = await utilities.gather_checks(ctx, self._checks)
  File "/.../.venv/lib/python3.10/site-packages/tanjun/utilities.py", line 88, in gather_checks
    await asyncio.gather(*(_execute_check(ctx, check) for check in checks))
  File "/.../.venv/lib/python3.10/site-packages/tanjun/utilities.py", line 66, in _execute_check
    if result := await foo:
  File "/.../.venv/lib/python3.10/site-packages/alluka/_client.py", line 317, in call_with_async_di
    return await self._injection_client.call_with_ctx_async(self, callback, *args, **kwargs)
  File "/.../.venv/lib/python3.10/site-packages/alluka/_client.py", line 226, in call_with_ctx_async
    if descriptors := self._build_descriptors(callback):
  File "/.../.venv/lib/python3.10/site-packages/alluka/_client.py", line 159, in _build_descriptors
    return self._descriptors[callback]
  File "/usr/lib/python3.10/weakref.py", line 416, in __getitem__
    return self.data[ref(key)]
TypeError: cannot create weak reference to '_AllChecks' object

Additional Info

I have changed some parts of this code and tested each change; I have noticed that:

  • The exception will only be raised only on the command with the decorator
  • The exception will still be raised whether the command was a slash command, message command, message menu command, or user menu command.
  • The exception will still be raised regardless of how many checks were given in with_all_checks(...)
  • When I downgraded tanjun to version v2.5.1a1, This problem does not persist.

Subcommand object not accessible from context in checks added to groups

When adding checks to command groups, it is not possible to get access to the executed subcommand object from the context, which would be required for getting data from it, for example, in a subcommand permission system. Example:
Example code:

import hikari

import tanjun


def check(ctx: tanjun.abc.SlashContext) -> bool:
    print(ctx.command.metadata.get("required_subcommand_permission"))
    return True


group = tanjun.slash_command_group("testgroup", ".")
group.add_check(check)


@group.as_sub_command("test", ".")
async def test_command(ctx: tanjun.abc.SlashContext) -> None:
    await ctx.respond(f"{ctx.command.metadata.get('required_subcommand_permission')}")


test_command.set_metadata("required_subcommand_permission", "some permission")

loader = tanjun.Component().load_from_scope().make_loader()

bot = hikari.impl.GatewayBot("MTE3MjQ5OTUzMDE5Mzg0NjI5Mg.GS629g.jhAkLkkPG9ixAliL_2TW2xYCDiWIFf5i_BXE5Y") # Doesn't exist
client = tanjun.Client.from_gateway_bot(bot, declare_global_commands=True)
client.load_modules("main")

if __name__ == "__main__":
    bot.run()

'_CommandBuilder' object has no attribute 'description'

Hello,

I'm trying to implement a basic Discord bot with slash commands. To do this, I've defined a component and a function with the slash command decorators as follows, based on the example here:

import tanjun

component = tanjun.Component()

@component.with_slash_command
@tanjun.as_slash_command("play", "Find out what your User ID is!")
async def play(ctx: tanjun.abc.Context) -> None:
    await ctx.respond(f"Hi {ctx.author.mention}! \nYour User ID is: ```{ctx.author.id}```")

loader = component.make_loader()

I've also got a hikari bot / tanjun client defined:

bot = hikari.GatewayBot(token=CONFIG.BOT_TOKEN)

client = ( 
    tanjun.Client.from_gateway_bot(
        bot,
        mention_prefix=True,
        declare_global_commands=CONFIG.GUILD_ID if CONFIG.GUILD_ID else False
    )
)

from app.commands.play import component as play_component
client.add_component(play_component)

However, when I call bot.run(), I see an uncaught exception in the terminal, and the slash command doesn't seem to get added to my guild:
AttributeError: '_CommandBuilder' object has no attribute 'description'

The stacktrace says it's coming from line 807 in _CommandBuilder.copy(), where self.description is undefined:

    def copy(self) -> _CommandBuilder:  # TODO: can we just del _CommandBuilder.__copy__ to go back to the default?
        builder = _CommandBuilder(self.name, self.description, self._sort_options, id=self.id)

        for option in self._options:
            builder.add_option(option)

        return builder

The _CommandBuilder constructor seems to pass its description param to the base class hikari.impl.CommandBuilder but this class doesn't seem to do anything with it, which I imagine is a bug.

Here's the lib versions I'm running:

name         : hikari
version      : 2.0.0.dev106

name         : hikari-tanjun
version      : 2.3.1a1

dependencies
 - hikari >=2.0.0.dev105,<2.1.0

MessageCommand arg parsing not passing args correctly

I updated Tanjun this morning and realized some of my message commands were not working.
Am I misusing something, or is this a bug?

Reproduction:

import os

import hikari
import tanjun
from dotenv import load_dotenv

load_dotenv()


async def prefix_getter(_: tanjun.abc.MessageContext) -> tuple[str]:
    return ("$",)

bot = hikari.GatewayBot(os.environ["TOKEN"])
client = (
    tanjun.Client.from_gateway_bot(bot, mention_prefix=True)
    .set_prefix_getter(prefix_getter)
)

component = tanjun.Component()
client.add_component(component)

@component.with_command
@tanjun.with_greedy_argument("value")
@tanjun.with_argument("name")
@tanjun.with_parser
@tanjun.as_message_command("test")
async def test_command(ctx: tanjun.abc.MessageContext, name: str, value: str) -> None:
    await ctx.respond(f"name was {name!r}, value was {value!r}.")

bot.run()

with Tanjun version 2.3.0a1:
Bot responds as expected.

with Tanjun version 2.3.1a1:

Traceback (most recent call last):
  File "/home/jonx/projects/tanjun-test/.venv/lib/python3.10/site-packages/tanjun/clients.py", line 2075, in on_message_create_event
    if await component.execute_message(ctx, hooks=hooks):
  File "/home/jonx/projects/tanjun-test/.venv/lib/python3.10/site-packages/tanjun/components.py", line 1017, in execute_message
    await command.execute(ctx, hooks=hooks)
  File "/home/jonx/projects/tanjun-test/.venv/lib/python3.10/site-packages/tanjun/commands.py", line 2304, in execute
    await self._callback.resolve_with_command_context(ctx, ctx, **kwargs)
  File "/home/jonx/projects/tanjun-test/.venv/lib/python3.10/site-packages/tanjun/injecting.py", line 489, in resolve
    result = self._callback(*args, **sub_results, **kwargs)
TypeError: test_command() missing 1 required positional argument: 'value'

Documentation tells a wrong way to listen to events in components.

Issue

Currently the docs tell a non-working way to listen to an event inside of a component. Here is the code it tells to use:

component = tanjun.Component()

@component.with_command
@tanjun.as_slash_command("name", "description")
async def slash_command(ctx: tanjun.abc.SlashContext) -> None:
    ...

@tanjun.with_listener
async def event_listener(event: hikari.Event) -> None:
    ...

but if you use that code you will get this error:

Traceback (most recent call last):
  File "/src/main.py", line 12, in <module>
    from components.metrics import PromMetrics
  File "/src/components/metrics.py", line 81, in <module>
    @tanjun.with_listener
     ^^^^^^^^^^^^^^^^^^^^
AttributeError: module 'tanjun' has no attribute 'with_listener'

And to fix the issue you would need to change @tanjun.with_listener with @component.with_listener()

So I propose to change @tanjun.with_listener to @component.with_listener() where applicable

Fix typo

Under the Quick Usage section, there is a typo, "For usage see the the documentation..."

FR: Match `hikari`'s way of listening for multiple event types

FR: Match hikari's way of listening for multiple event types

Currently if you want to listen for more than one event type on a single function you have to use multiple decorators. Recently, hikari added the option to pass multiple events into the @bot.listen() decorator. It would make sense to add that feature to tanjun.Component.with_listener() and tanjun.Client.with_listener().

hikari-py/hikari#1103

Parent attribute of subcommand objects return None when loaded their components from another module

When loading a component that contains commands groups (whether message or slash) from another module, the parent attribute of subcommand objects will returns None. This does not happen when such a component is initialized in the same module as where the tanjun.Client object is initialized; so it doesn't need to call .load_modules(...) and instead just adds the component from .add_component(...).

Tanjun Ver. : 2.5.0a1
Python Ver. : 3.10.4
Steps to reproduce:

  1. make main.py as such
import hikari as hk
import tanjun as tj
import alluka as al

TOKEN = '...'
GUILD = ...

client = (
    tj.Client.from_gateway_bot(
        bot := hk.GatewayBot(TOKEN), declare_global_commands=GUILD
    )
    .load_modules('mod1')
)


@client.with_listener(hk.StartedEvent)
async def on_started(_: hk.StartedEvent, client_: al.Injected[tj.abc.Client]):
    for groups in client_.iter_commands():
        for cmd in getattr(groups, 'commands'):
            print(cmd.parent) # prints out the parent command 


bot.run()
  1. make mod1.py as such
import tanjun as tj


comp = tj.Component()


@comp.with_command
@tj.as_message_command_group('group1')
async def group1(ctx: tj.abc.MessageContext):
    await ctx.respond(...)


@group1.with_command
@tj.as_message_command('subcmd1')
async def subcmd1(ctx: tj.abc.MessageContext):
    await ctx.respond('subcmd1 response')
    

group2 = comp.with_command(tj.slash_command_group('group2', "group2 desc"))


@group2.with_command
@tj.as_slash_command('subcmd2', "subcmd2 desc")
async def subcmd2(ctx: tj.abc.SlashContext):
    await ctx.respond('subcmd2 response')
    
    
loader = comp.make_loader()
  1. run main.py. It prints None and None, as opposed to the expected CommandGroup <1: ['group1']> and <tanjun.commands.slash.SlashCommandGroup object at 0x...>. This happens whether importing the modules from a pathlib.Path object or via a string.

To reproduce the expected result instead, have the component and its commands defined right in main.py, remove the loader, and replace .load_modules('mod1') with .add_component(comp)

Unclear error message

I haven't figured out a generic reproducible way to get this error, but tanjun will respond with Command not found in discord when a slash command throws an exception, which is both incorrect and unclear.

Hikari: 2.0.0.dev103 (hikari-py/hikari@39cf4c2)
Tanjun: 2.0.0a4 (e937a4b)

Sample code;

@settings_group.with_command
@tanjun.with_str_slash_option("value", "The value to set it to")
@tanjun.with_str_slash_option("setting", "The setting to set",
                              choices=[(choice[1][3], choice[0]) for choice in DatabaseProto.VALID_SETTINGS.items()])
@tanjun.as_slash_command("set", "Set config option")
async def set_setting(ctx: tanjun.SlashContext, setting: str, value: str,
                      _db: DatabaseProto = tanjun.injected(type=DatabaseProto)):
    try:
        await _db.set_setting(ctx.author.id, setting, value)
        await ctx.respond(f"Set {DatabaseProto.VALID_SETTINGS[setting][3]} to {value}")
    except ValueError as err:
        await ctx.respond(embed=_db.error_embed("Invalid value", str(err)))

Stack trace: https://pastebin.com/vVJZ56wY

Discord reponse:
image

Allow passing in a function as prefix

You should be able to pass in a function that takes the Client and Message as arguments and expect it to return either a string or an Iterable of strings. Great work from what I have seen until now

Some commands cause bot to redeclare commands on every launch

Description

If a bot has a slash command that meets these criteria, then the bot will redeclare all commands on every launch:

  1. The command is in a command group
  2. The command has no options

This is adding some friction to my development flow, due to the rate limit on command registration API.

Reproduction

import os

import hikari
import tanjun

TOKEN = os.environ["TOKEN"]
GUILD_ID = int(os.environ["GUILD_ID"])

test_group = tanjun.slash_command_group("test", "test")

@test_group.add_command
@tanjun.as_slash_command("test", "asdf")
async def test(ctx: tanjun.abc.SlashContext) -> None:
    await ctx.respond("test")

component = tanjun.Component().add_slash_command(test_group)

bot = hikari.GatewayBot(token=TOKEN)
tanjun.Client.from_gateway_bot(
    bot, declare_global_commands=GUILD_ID
).add_component(component)

bot.run()

Every time I run this bot, I get the log line:

I 2024-01-22 19:16:39,429 hikari.tanjun.clients: Bulk declaring 1 guild <GUILD_ID> application commands

If I add an option to test, the bot will instead skip the command declaration after the first run:

I 2024-01-22 19:16:43,572 hikari.tanjun.clients: Skipping bulk declare for guild <GUILD_ID> application commands since they're already declared

Cause

When Tanjun starts up, it queries Discord for the currently declared commands to see if anything has changed:

Tanjun/tanjun/clients.py

Lines 1540 to 1541 in 25413f2

registered_commands = await self._rest.fetch_application_commands(application, guild=guild)
if _internal.cmp_all_commands(registered_commands, builders):

The query will return something like this for test_group in the example above:

{
  "name": "test_group",
  "options": [
    {
      "name": "test",
      "options": None,
      ...
    },
  ],
  ...
}

The internal command builder test_group something like this instead:

{
  "name": "test_group",
  "options": [
    {
      "name": "test",
      "options": [],
      ...
    },
  ],
  ...
}

The options for the two representations will eventually compared in this line:

return len(opts) == len(other_opts) and all(itertools.starmap(operator.eq, zip(opts, other_opts)))

Since they're compared directly, the discrepancy in the subcommand option representation isn't taken into account, and the check fails.

Client callback feature

Allow Client.add_client_callback take a tuple of callables instead of chaining calls.

i.e.

client.add_client_callback(STARTING, (http.open, server.open, ...))

Add a higher level abstraction of creating and syncing commands to a specific guild

I thought it might be a nice idea to provide a way to add specific guilds per a command and easily sync to those specific guilds.

Right now, it's more complicated because currently, Tanjun only syncs commands with is_global=True to guilds passed in declare_global_commands. There is no easy way to sync commands to a specific guild and have some commands global, etc.

Please let me know what you think of this suggestion.
Thanks!

Attempting to load modules using a list of Paths always raises an error

Tanjun/tanjun/clients.py

Lines 1199 to 1213 in 2de1d6b

for module_path in modules:
if isinstance(module_path, str):
module = importlib.import_module(module_path)
else:
spec = importlib_util.spec_from_file_location(
module_path.name.rsplit(".", 1)[0], str(module_path.absolute())
)
# https://github.com/python/typeshed/issues/2793
if spec and isinstance(spec.loader, importlib_abc.Loader):
module = importlib_util.module_from_spec(spec)
spec.loader.exec_module(module)
raise RuntimeError(f"Unknown or invalid module provided {module_path}")

The problem seems to originate from this error always being raised if the argument type is not str, despite loading correctly (presumably, anyways).

Incorrect message menu command name validation

When creating a new message menu command with a name not matching the regex ^[\w-]{1,32}, ValueError is raised.

When the command name is "ABC":

...
  File "C:\Users\...\AppData\Roaming\Python\Python310\site-packages\tanjun\commands\menu.py", line 88, in decorator
    return MenuCommand(
  File "C:\Users\...\AppData\Roaming\Python\Python310\site-packages\tanjun\commands\menu.py", line 292, in __init__
    slash.validate_name(name)
  File "C:\Users\...\AppData\Roaming\Python\Python310\site-packages\tanjun\commands\slash.py", line 98, in validate_name
    raise ValueError(f"Invalid name provided, {name!r} must be lowercase")
ValueError: Invalid name provided, 'ABC' must be lowercase

When the command name is "a b c":

...
  File "C:\Users\...\AppData\Roaming\Python\Python310\site-packages\tanjun\commands\menu.py", line 88, in decorator
    return MenuCommand(
  File "C:\Users\...\AppData\Roaming\Python\Python310\site-packages\tanjun\commands\menu.py", line 292, in __init__
    slash.validate_name(name)
  File "C:\Users\...\AppData\Roaming\Python\Python310\site-packages\tanjun\commands\slash.py", line 95, in validate_name
    raise ValueError(f"Invalid name provided, {name!r} doesn't match the required regex `^\\w{{1,32}}$`")
ValueError: Invalid name provided, 'a b c' doesn't match the required regex `^\w{1,32}$`

This is not the intended behaviour as documented in the discord's documentation ;It need not to match such regex

Test Code Used:

comp = tanjun.Component()

@comp.with_menu_command
@tanjun.as_message_menu('a b c')
async def test(
    ctx: tj.abc.MenuContext,
    msg: hk.Message,
) -> None:
    assert msg.content
    ...

@tanjun.as_loader
def load_component(client: tanjun.abc.Client) -> None:
    client.add_component(comp.copy())

Hikari version: 2.0.0.dev106
Tanjun version: 2.4.0a1
Python version: 3.10.1

Set_auto_defer does not defer with the correct flags

When set_auto_defer_after kicks in, it doesnt defer with the correct flags.

Steps to reproduce:

  • Set the client auto_defer_after() to 0 --> client.set_auto_defer_after(0)
@tanjun.as_slash_command("test_defer", "test", always_defer=True, default_to_ephemeral=True)
async def command_test(ctx):
    await ctx.respond("Done!")

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.