Reputation: 2501
I'm working on a bot which keeps track of various text-based games in different channels. Commands used outside the channel in which the relevant game is running should do nothing of course, and neither should they activate is the game is not running (for example, when a new game is starting soon). Therefore, almost all my commands start with the same few lines of code
@commands.command()
async def example_command(self, ctx):
game = self.game_manager.get_game(ctx.channel.id)
if not game or game.state == GameState.FINISHED:
return
I'd prefer to just decorate all these methods instead. Discord.py handily provides a system of "check" decorators to automate these kinds of checks, but this does not allow me to pass on the game
object to the command. As every command needs a reference to this object, I'd have to retrieve it every time again anyway, and ideally I'd like to just pass it along to the command.
My naive attempt at a decorator looks as follows
def is_game_running(func):
async def wrapper(self, ctx):
# Retrieve `game` object here and do some checks
game = ...
return await func(self, ctx, game)
wrapper.__name__ = func.__name__
return wrapper
# Somewhere in the class
@commands.command()
@is_game_running
async def example_command(self, ctx, game):
pass
However this gives me the quite cryptic error "discord.ext.commands.errors.MissingRequiredArgument: ctx is a required argument that is missing."
I've tried a few variants of this, using *args
etc... but nothing seems to work.
Upvotes: 4
Views: 620
Reputation: 3495
You can keep the decorator outside of the class and handle the passing of the context
object by using *args
. In this case, args
will be a tuple of size 2 that first contains the class instance and then the context
object. You can thus access the context
object by using args[1]
.
For illustration purposes the below code saves each item of the args
tuple into its own variable. This is not strictly needed since you could create a new 3 size tuple of (class instance, context, game)
and pass that to func
inside wrapper
.
import asyncio
import discord
import functools
from discord.ext import commands
def is_game_running(func):
@functools.wraps(func)
async def wrapper(*args):
# Retrieve `game` object here and do some checks
class_instance = args[0] # this is self from the class
ctx = args[1] # this is the context object
game = ctx.channel.id
# example_command requires variables: self, ctx, game
return await func(class_instance, ctx, game)
return wrapper
class Test(commands.Cog):
def __init__(self, bot):
self.bot = bot
# Somewhere in the class
@commands.command()
@is_game_running
async def example_command(self, ctx, game):
await ctx.send(game)
client = commands.Bot(intents=discord.Intents.all(), command_prefix='!')
async def main():
async with client:
await client.add_cog(Test(client))
await client.start('token')
asyncio.run(main())
Upvotes: 1
Reputation: 718
discord.py has provided a convenient bot attribute for the context object that lets you get the bot instance from the context. You can get your game from there.
I'll use discord.py's check system for the example.
def is_game_running():
def predicate(ctx):
game = ctx.bot.game_manager.get_game(ctx.channel.id)
... # do some checks
return result # result of the check. Must be true or false
return commands.check(predicate)
@commands.command()
@is_game_running()
async def example_command(self, ctx, game):
pass
Upvotes: 0