0
# Nextcord Command Framework
1
2
Traditional text-based command system with the commands extension, providing prefix commands, command groups, converters, and checks for comprehensive bot functionality.
3
4
## Bot Class
5
6
Enhanced client with command processing capabilities and prefix-based command handling.
7
8
### Bot Constructor and Setup { .api }
9
10
```python
11
import nextcord
12
from nextcord.ext import commands
13
from nextcord.ext.commands import Bot, AutoShardedBot, Context
14
from typing import Optional, List, Union, Callable, Any
15
16
class Bot(commands.Bot):
17
"""A subclass of nextcord.Client that has command functionality.
18
19
This allows for a prefix-based command system alongside application commands.
20
21
Attributes
22
----------
23
command_prefix: Union[str, List[str], Callable]
24
The prefix(es) that the bot will respond to.
25
case_insensitive: bool
26
Whether commands are case insensitive.
27
description: Optional[str]
28
The bot's description.
29
help_command: Optional[HelpCommand]
30
The help command implementation.
31
owner_id: Optional[int]
32
The bot owner's user ID.
33
owner_ids: Optional[Set[int]]
34
Set of bot owner user IDs.
35
"""
36
37
def __init__(
38
self,
39
command_prefix: Union[str, List[str], Callable],
40
*,
41
help_command: Optional[commands.HelpCommand] = commands.DefaultHelpCommand(),
42
description: Optional[str] = None,
43
intents: nextcord.Intents = nextcord.Intents.default(),
44
case_insensitive: bool = False,
45
strip_after_prefix: bool = False,
46
**options
47
):
48
"""Initialize the bot.
49
50
Parameters
51
----------
52
command_prefix: Union[str, List[str], Callable]
53
The prefix that will trigger bot commands.
54
help_command: Optional[commands.HelpCommand]
55
The help command implementation. Pass None to disable.
56
description: Optional[str]
57
A description for the bot.
58
intents: nextcord.Intents
59
Gateway intents for the bot.
60
case_insensitive: bool
61
Whether commands should be case insensitive.
62
strip_after_prefix: bool
63
Whether to strip whitespace after the prefix.
64
**options
65
Additional options passed to the Client constructor.
66
"""
67
...
68
69
async def get_prefix(self, message: nextcord.Message) -> Union[str, List[str]]:
70
"""Get the prefix for a message.
71
72
This can be overridden for dynamic prefixes.
73
74
Parameters
75
----------
76
message: nextcord.Message
77
The message to get the prefix for.
78
79
Returns
80
-------
81
Union[str, List[str]]
82
The prefix(es) for this message.
83
"""
84
...
85
86
async def get_context(
87
self,
88
message: nextcord.Message,
89
*,
90
cls: type = Context
91
) -> Context:
92
"""Get the command context from a message.
93
94
Parameters
95
----------
96
message: nextcord.Message
97
The message to create context from.
98
cls: type
99
The context class to use.
100
101
Returns
102
-------
103
Context
104
The command context.
105
"""
106
...
107
108
# Basic bot setup
109
bot = commands.Bot(
110
command_prefix='!',
111
description='A helpful bot',
112
intents=nextcord.Intents.default(),
113
case_insensitive=True
114
)
115
116
# Dynamic prefix example
117
async def get_prefix(bot, message):
118
"""Dynamic prefix function."""
119
# Default prefixes
120
prefixes = ['!', '?']
121
122
# Add custom guild prefixes (from database)
123
if message.guild:
124
# This would typically query a database
125
guild_prefix = get_guild_prefix(message.guild.id) # Implement this
126
if guild_prefix:
127
prefixes.append(guild_prefix)
128
129
# Allow bot mention as prefix
130
return commands.when_mentioned_or(*prefixes)(bot, message)
131
132
# Bot with dynamic prefix
133
bot = commands.Bot(command_prefix=get_prefix)
134
135
@bot.event
136
async def on_ready():
137
"""Bot ready event."""
138
print(f'{bot.user} has connected to Discord!')
139
print(f'Bot is in {len(bot.guilds)} guilds')
140
141
@bot.event
142
async def on_message(message):
143
"""Process messages for commands."""
144
# Ignore bot messages
145
if message.author.bot:
146
return
147
148
# Process commands
149
await bot.process_commands(message)
150
```
151
152
## Commands
153
154
Individual command definitions with parameters, converters, and error handling.
155
156
### Command Decorator { .api }
157
158
```python
159
def command(
160
name: Optional[str] = None,
161
*,
162
cls: Optional[type] = None,
163
**attrs
164
) -> Callable:
165
"""Decorator to create a command.
166
167
Parameters
168
----------
169
name: Optional[str]
170
The name of the command. If not given, uses the function name.
171
cls: Optional[type]
172
The class to construct the command with.
173
**attrs
174
Additional attributes for the command.
175
"""
176
...
177
178
# Basic command examples
179
@bot.command()
180
async def ping(ctx):
181
"""Check the bot's latency."""
182
latency = round(bot.latency * 1000)
183
await ctx.send(f'Pong! {latency}ms')
184
185
@bot.command(name='hello', aliases=['hi', 'hey'])
186
async def greet(ctx, *, name: str = None):
187
"""Greet a user.
188
189
Parameters
190
----------
191
ctx: commands.Context
192
The command context.
193
name: str, optional
194
The name to greet. If not provided, greets the command author.
195
"""
196
target = name or ctx.author.display_name
197
await ctx.send(f'Hello, {target}!')
198
199
@bot.command()
200
async def userinfo(ctx, member: nextcord.Member = None):
201
"""Get information about a user.
202
203
Parameters
204
----------
205
ctx: commands.Context
206
The command context.
207
member: nextcord.Member, optional
208
The member to get information about. Defaults to command author.
209
"""
210
user = member or ctx.author
211
212
embed = nextcord.Embed(
213
title=f"User Info: {user.display_name}",
214
color=user.color or nextcord.Color.blue()
215
)
216
217
embed.set_thumbnail(url=user.display_avatar.url)
218
embed.add_field(name="Username", value=str(user), inline=True)
219
embed.add_field(name="ID", value=user.id, inline=True)
220
embed.add_field(name="Joined", value=user.joined_at.strftime("%Y-%m-%d") if user.joined_at else "Unknown", inline=True)
221
222
await ctx.send(embed=embed)
223
224
# Command with multiple parameters
225
@bot.command()
226
async def ban(ctx, member: nextcord.Member, *, reason: str = "No reason provided"):
227
"""Ban a member from the server.
228
229
Parameters
230
----------
231
ctx: commands.Context
232
The command context.
233
member: nextcord.Member
234
The member to ban.
235
reason: str
236
The reason for the ban.
237
"""
238
# Check permissions
239
if not ctx.author.guild_permissions.ban_members:
240
await ctx.send("❌ You don't have permission to ban members.")
241
return
242
243
try:
244
await member.ban(reason=f"{reason} | Banned by {ctx.author}")
245
await ctx.send(f"✅ Banned {member.mention} for: {reason}")
246
except nextcord.Forbidden:
247
await ctx.send("❌ I don't have permission to ban this member.")
248
except nextcord.HTTPException:
249
await ctx.send("❌ Failed to ban member.")
250
251
# Command with converters
252
@bot.command()
253
async def role_info(ctx, role: nextcord.Role):
254
"""Get information about a role.
255
256
Parameters
257
----------
258
ctx: commands.Context
259
The command context.
260
role: nextcord.Role
261
The role to get information about.
262
"""
263
embed = nextcord.Embed(
264
title=f"Role Info: {role.name}",
265
color=role.color
266
)
267
268
embed.add_field(name="ID", value=role.id, inline=True)
269
embed.add_field(name="Position", value=role.position, inline=True)
270
embed.add_field(name="Members", value=len(role.members), inline=True)
271
embed.add_field(name="Mentionable", value="Yes" if role.mentionable else "No", inline=True)
272
embed.add_field(name="Hoisted", value="Yes" if role.hoist else "No", inline=True)
273
embed.add_field(name="Managed", value="Yes" if role.managed else "No", inline=True)
274
275
if role.permissions.administrator:
276
embed.add_field(name="⚠️ Warning", value="This role has Administrator permission!", inline=False)
277
278
await ctx.send(embed=embed)
279
```
280
281
### Command Context { .api }
282
283
```python
284
class Context:
285
"""The context for a command.
286
287
Provides information about the command invocation and convenience methods.
288
289
Attributes
290
----------
291
message: nextcord.Message
292
The message that triggered the command.
293
bot: Bot
294
The bot instance.
295
args: List[Any]
296
The arguments passed to the command.
297
kwargs: Dict[str, Any]
298
The keyword arguments passed to the command.
299
prefix: str
300
The prefix used to invoke the command.
301
command: Command
302
The command that was invoked.
303
invoked_with: str
304
The alias used to invoke the command.
305
invoked_parents: List[str]
306
The parent commands used to invoke a subcommand.
307
invoked_subcommand: Optional[Command]
308
The subcommand that was invoked.
309
subcommand_passed: Optional[str]
310
The string passed after a group command.
311
guild: Optional[nextcord.Guild]
312
The guild the command was used in.
313
channel: Union[nextcord.abc.Messageable]
314
The channel the command was used in.
315
author: Union[nextcord.Member, nextcord.User]
316
The user who invoked the command.
317
me: Union[nextcord.Member, nextcord.ClientUser]
318
The bot's member object in the guild.
319
"""
320
321
async def send(
322
self,
323
content: Optional[str] = None,
324
*,
325
tts: bool = False,
326
embed: Optional[nextcord.Embed] = None,
327
embeds: Optional[List[nextcord.Embed]] = None,
328
file: Optional[nextcord.File] = None,
329
files: Optional[List[nextcord.File]] = None,
330
stickers: Optional[List[nextcord.GuildSticker]] = None,
331
delete_after: Optional[float] = None,
332
nonce: Optional[Union[str, int]] = None,
333
allowed_mentions: Optional[nextcord.AllowedMentions] = None,
334
reference: Optional[Union[nextcord.Message, nextcord.MessageReference]] = None,
335
mention_author: Optional[bool] = None,
336
view: Optional[nextcord.ui.View] = None,
337
suppress_embeds: bool = False,
338
) -> nextcord.Message:
339
"""Send a message to the channel.
340
341
This is a shortcut for ctx.channel.send().
342
343
Parameters are the same as nextcord.abc.Messageable.send().
344
345
Returns
346
-------
347
nextcord.Message
348
The message that was sent.
349
"""
350
...
351
352
async def reply(
353
self,
354
content: Optional[str] = None,
355
**kwargs
356
) -> nextcord.Message:
357
"""Reply to the message that invoked the command.
358
359
Parameters are the same as ctx.send().
360
361
Returns
362
-------
363
nextcord.Message
364
The message that was sent.
365
"""
366
...
367
368
def typing(self):
369
"""Return a typing context manager."""
370
return self.channel.typing()
371
372
@property
373
def valid(self) -> bool:
374
"""bool: Whether the context is valid for command processing."""
375
...
376
377
@property
378
def clean_prefix(self) -> str:
379
"""str: The cleaned up invoke prefix."""
380
...
381
382
@property
383
def cog(self) -> Optional["Cog"]:
384
"""Optional[Cog]: The cog the command belongs to."""
385
...
386
387
# Context usage examples
388
@bot.command()
389
async def ctx_demo(ctx):
390
"""Demonstrate context usage."""
391
embed = nextcord.Embed(title="Context Information")
392
393
embed.add_field(name="Author", value=ctx.author.mention, inline=True)
394
embed.add_field(name="Channel", value=ctx.channel.mention, inline=True)
395
embed.add_field(name="Guild", value=ctx.guild.name if ctx.guild else "DM", inline=True)
396
embed.add_field(name="Prefix", value=ctx.prefix, inline=True)
397
embed.add_field(name="Command", value=ctx.command.name, inline=True)
398
embed.add_field(name="Invoked With", value=ctx.invoked_with, inline=True)
399
400
await ctx.send(embed=embed)
401
402
@bot.command()
403
async def slow_command(ctx):
404
"""A command that takes some time to process."""
405
async with ctx.typing():
406
# Simulate slow processing
407
import asyncio
408
await asyncio.sleep(3)
409
await ctx.send("Processing complete!")
410
411
@bot.command()
412
async def reply_demo(ctx):
413
"""Demonstrate reply functionality."""
414
await ctx.reply("This is a reply to your command!")
415
416
# Custom context class
417
class CustomContext(commands.Context):
418
"""Custom context with additional features."""
419
420
async def send_success(self, message: str):
421
"""Send a success message."""
422
embed = nextcord.Embed(
423
title="✅ Success",
424
description=message,
425
color=nextcord.Color.green()
426
)
427
await self.send(embed=embed)
428
429
async def send_error(self, message: str):
430
"""Send an error message."""
431
embed = nextcord.Embed(
432
title="❌ Error",
433
description=message,
434
color=nextcord.Color.red()
435
)
436
await self.send(embed=embed)
437
438
async def confirm(self, message: str, timeout: float = 30.0) -> bool:
439
"""Ask for user confirmation."""
440
embed = nextcord.Embed(
441
title="Confirmation Required",
442
description=f"{message}\n\nReact with ✅ to confirm or ❌ to cancel.",
443
color=nextcord.Color.orange()
444
)
445
446
msg = await self.send(embed=embed)
447
await msg.add_reaction("✅")
448
await msg.add_reaction("❌")
449
450
def check(reaction, user):
451
return (
452
user == self.author and
453
reaction.message.id == msg.id and
454
str(reaction.emoji) in ["✅", "❌"]
455
)
456
457
try:
458
reaction, user = await self.bot.wait_for('reaction_add', check=check, timeout=timeout)
459
return str(reaction.emoji) == "✅"
460
except asyncio.TimeoutError:
461
await msg.edit(embed=nextcord.Embed(
462
title="Confirmation Timed Out",
463
description="No response received within 30 seconds.",
464
color=nextcord.Color.red()
465
))
466
return False
467
468
# Use custom context
469
async def get_context(message, *, cls=CustomContext):
470
return await super(Bot, bot).get_context(message, cls=cls)
471
472
bot.get_context = get_context
473
474
@bot.command()
475
async def delete_all_messages(ctx):
476
"""Delete all messages (with confirmation)."""
477
confirmed = await ctx.confirm("Are you sure you want to delete all messages? This cannot be undone!")
478
479
if confirmed:
480
# Perform the dangerous action
481
await ctx.send_success("Messages deleted successfully!")
482
else:
483
await ctx.send("Operation cancelled.")
484
```
485
486
## Command Groups
487
488
Hierarchical command organization with subcommands and groups.
489
490
### Group Commands { .api }
491
492
```python
493
def group(
494
name: Optional[str] = None,
495
*,
496
cls: Optional[type] = None,
497
invoke_without_command: bool = False,
498
**attrs
499
) -> Callable:
500
"""Decorator to create a command group.
501
502
Parameters
503
----------
504
name: Optional[str]
505
The name of the group.
506
cls: Optional[type]
507
The class to construct the group with.
508
invoke_without_command: bool
509
Whether the group can be invoked without a subcommand.
510
**attrs
511
Additional attributes for the group.
512
"""
513
...
514
515
class Group(commands.Command):
516
"""A command that can contain subcommands.
517
518
Attributes
519
----------
520
invoke_without_command: bool
521
Whether this group can be invoked without a subcommand.
522
commands: Dict[str, Command]
523
The subcommands in this group.
524
"""
525
526
def add_command(self, command: commands.Command) -> None:
527
"""Add a subcommand to this group."""
528
...
529
530
def remove_command(self, name: str) -> Optional[commands.Command]:
531
"""Remove a subcommand from this group."""
532
...
533
534
def get_command(self, name: str) -> Optional[commands.Command]:
535
"""Get a subcommand by name."""
536
...
537
538
# Group command examples
539
@bot.group(invoke_without_command=True)
540
async def config(ctx):
541
"""Server configuration commands.
542
543
Use subcommands to configure various aspects of the server.
544
"""
545
if ctx.invoked_subcommand is None:
546
embed = nextcord.Embed(
547
title="Server Configuration",
548
description="Available configuration options:",
549
color=nextcord.Color.blue()
550
)
551
552
embed.add_field(
553
name="Subcommands",
554
value=(
555
"`!config prefix <new_prefix>` - Change bot prefix\n"
556
"`!config welcome <channel>` - Set welcome channel\n"
557
"`!config autorole <role>` - Set auto role for new members\n"
558
"`!config logs <channel>` - Set log channel"
559
),
560
inline=False
561
)
562
563
await ctx.send(embed=embed)
564
565
@config.command()
566
async def prefix(ctx, new_prefix: str):
567
"""Change the bot prefix for this server."""
568
if not ctx.author.guild_permissions.manage_guild:
569
await ctx.send("❌ You need Manage Server permission to change the prefix.")
570
return
571
572
if len(new_prefix) > 5:
573
await ctx.send("❌ Prefix must be 5 characters or less.")
574
return
575
576
# Save to database (implement this)
577
save_guild_prefix(ctx.guild.id, new_prefix)
578
579
await ctx.send(f"✅ Bot prefix changed to `{new_prefix}`")
580
581
@config.command()
582
async def welcome(ctx, channel: nextcord.TextChannel):
583
"""Set the welcome channel for new members."""
584
if not ctx.author.guild_permissions.manage_guild:
585
await ctx.send("❌ You need Manage Server permission to set the welcome channel.")
586
return
587
588
# Save to database (implement this)
589
save_welcome_channel(ctx.guild.id, channel.id)
590
591
await ctx.send(f"✅ Welcome channel set to {channel.mention}")
592
593
@config.command()
594
async def autorole(ctx, role: nextcord.Role):
595
"""Set the auto role for new members."""
596
if not ctx.author.guild_permissions.manage_roles:
597
await ctx.send("❌ You need Manage Roles permission to set auto roles.")
598
return
599
600
# Check if bot can assign the role
601
if role.position >= ctx.me.top_role.position:
602
await ctx.send("❌ I cannot assign this role (it's higher than my highest role).")
603
return
604
605
# Save to database (implement this)
606
save_auto_role(ctx.guild.id, role.id)
607
608
await ctx.send(f"✅ Auto role set to {role.mention}")
609
610
# Nested groups
611
@bot.group()
612
async def admin(ctx):
613
"""Admin-only commands."""
614
pass
615
616
@admin.group()
617
async def user(ctx):
618
"""User management commands."""
619
pass
620
621
@user.command()
622
async def ban_user(ctx, member: nextcord.Member, *, reason: str = "No reason provided"):
623
"""Ban a user (admin command)."""
624
# Implementation here
625
await ctx.send(f"Banned {member.mention} for: {reason}")
626
627
@user.command()
628
async def unban(ctx, user_id: int):
629
"""Unban a user by ID."""
630
try:
631
user = await bot.fetch_user(user_id)
632
await ctx.guild.unban(user)
633
await ctx.send(f"✅ Unbanned {user}")
634
except nextcord.NotFound:
635
await ctx.send("❌ User not found or not banned.")
636
except nextcord.Forbidden:
637
await ctx.send("❌ I don't have permission to unban users.")
638
```
639
640
## Converters
641
642
Type conversion for command arguments with custom parsing and validation.
643
644
### Built-in Converters { .api }
645
646
```python
647
# Common built-in converters
648
class MemberConverter:
649
"""Converts argument to nextcord.Member."""
650
async def convert(self, ctx: Context, argument: str) -> nextcord.Member:
651
...
652
653
class UserConverter:
654
"""Converts argument to nextcord.User."""
655
async def convert(self, ctx: Context, argument: str) -> nextcord.User:
656
...
657
658
class TextChannelConverter:
659
"""Converts argument to nextcord.TextChannel."""
660
async def convert(self, ctx: Context, argument: str) -> nextcord.TextChannel:
661
...
662
663
class RoleConverter:
664
"""Converts argument to nextcord.Role."""
665
async def convert(self, ctx: Context, argument: str) -> nextcord.Role:
666
...
667
668
class ColourConverter:
669
"""Converts argument to nextcord.Colour."""
670
async def convert(self, ctx: Context, argument: str) -> nextcord.Colour:
671
...
672
673
# Using built-in converters
674
@bot.command()
675
async def give_role(ctx, member: nextcord.Member, role: nextcord.Role):
676
"""Give a role to a member."""
677
if role in member.roles:
678
await ctx.send(f"{member.mention} already has the {role.mention} role.")
679
return
680
681
try:
682
await member.add_roles(role)
683
await ctx.send(f"✅ Gave {role.mention} to {member.mention}")
684
except nextcord.Forbidden:
685
await ctx.send("❌ I don't have permission to assign roles.")
686
687
@bot.command()
688
async def color_role(ctx, role: nextcord.Role, color: nextcord.Colour):
689
"""Change a role's color."""
690
try:
691
await role.edit(color=color)
692
await ctx.send(f"✅ Changed {role.mention} color to {color}")
693
except nextcord.Forbidden:
694
await ctx.send("❌ I don't have permission to edit roles.")
695
696
# Custom converters
697
class DurationConverter(commands.Converter):
698
"""Convert duration strings like '1h30m' to seconds."""
699
700
async def convert(self, ctx: Context, argument: str) -> int:
701
import re
702
703
# Parse duration string (e.g., "1h30m15s")
704
time_regex = re.compile(r"(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?")
705
match = time_regex.match(argument.lower())
706
707
if not match or not any(match.groups()):
708
raise commands.BadArgument(f"Invalid duration format: {argument}")
709
710
hours, minutes, seconds = match.groups()
711
total_seconds = 0
712
713
if hours:
714
total_seconds += int(hours) * 3600
715
if minutes:
716
total_seconds += int(minutes) * 60
717
if seconds:
718
total_seconds += int(seconds)
719
720
if total_seconds <= 0:
721
raise commands.BadArgument("Duration must be greater than 0")
722
723
return total_seconds
724
725
class TemperatureConverter(commands.Converter):
726
"""Convert temperature between Celsius and Fahrenheit."""
727
728
async def convert(self, ctx: Context, argument: str) -> dict:
729
import re
730
731
# Match patterns like "25c", "77f", "25°C", "77°F"
732
pattern = r"^(-?\d+(?:\.\d+)?)([cf])$"
733
match = re.match(pattern, argument.lower().replace("°", ""))
734
735
if not match:
736
raise commands.BadArgument(f"Invalid temperature format: {argument}")
737
738
value, unit = match.groups()
739
value = float(value)
740
741
if unit == 'c':
742
celsius = value
743
fahrenheit = (value * 9/5) + 32
744
else: # fahrenheit
745
fahrenheit = value
746
celsius = (value - 32) * 5/9
747
748
return {
749
'celsius': round(celsius, 2),
750
'fahrenheit': round(fahrenheit, 2),
751
'original_unit': unit.upper()
752
}
753
754
# Using custom converters
755
@bot.command()
756
async def timeout(ctx, member: nextcord.Member, duration: DurationConverter):
757
"""Timeout a member for a specified duration."""
758
if not ctx.author.guild_permissions.moderate_members:
759
await ctx.send("❌ You don't have permission to timeout members.")
760
return
761
762
from datetime import datetime, timedelta
763
764
until = datetime.utcnow() + timedelta(seconds=duration)
765
766
try:
767
await member.timeout(until=until)
768
769
# Convert duration back to readable format
770
hours, remainder = divmod(duration, 3600)
771
minutes, seconds = divmod(remainder, 60)
772
773
duration_str = []
774
if hours:
775
duration_str.append(f"{hours}h")
776
if minutes:
777
duration_str.append(f"{minutes}m")
778
if seconds:
779
duration_str.append(f"{seconds}s")
780
781
await ctx.send(f"✅ Timed out {member.mention} for {' '.join(duration_str)}")
782
783
except nextcord.Forbidden:
784
await ctx.send("❌ I don't have permission to timeout this member.")
785
786
@bot.command()
787
async def temp(ctx, temperature: TemperatureConverter):
788
"""Convert temperature between Celsius and Fahrenheit."""
789
embed = nextcord.Embed(title="Temperature Conversion", color=nextcord.Color.blue())
790
791
embed.add_field(name="Celsius", value=f"{temperature['celsius']}°C", inline=True)
792
embed.add_field(name="Fahrenheit", value=f"{temperature['fahrenheit']}°F", inline=True)
793
embed.add_field(name="Original", value=f"Input was in {temperature['original_unit']}", inline=False)
794
795
await ctx.send(embed=embed)
796
797
# Union converters for multiple types
798
from typing import Union
799
800
@bot.command()
801
async def info(ctx, target: Union[nextcord.Member, nextcord.TextChannel, nextcord.Role]):
802
"""Get information about a member, channel, or role."""
803
embed = nextcord.Embed(color=nextcord.Color.blue())
804
805
if isinstance(target, nextcord.Member):
806
embed.title = f"Member: {target.display_name}"
807
embed.add_field(name="ID", value=target.id, inline=True)
808
embed.add_field(name="Joined", value=target.joined_at.strftime("%Y-%m-%d") if target.joined_at else "Unknown", inline=True)
809
embed.set_thumbnail(url=target.display_avatar.url)
810
811
elif isinstance(target, nextcord.TextChannel):
812
embed.title = f"Channel: #{target.name}"
813
embed.add_field(name="ID", value=target.id, inline=True)
814
embed.add_field(name="Category", value=target.category.name if target.category else "None", inline=True)
815
embed.add_field(name="Topic", value=target.topic or "No topic", inline=False)
816
817
elif isinstance(target, nextcord.Role):
818
embed.title = f"Role: @{target.name}"
819
embed.add_field(name="ID", value=target.id, inline=True)
820
embed.add_field(name="Members", value=len(target.members), inline=True)
821
embed.add_field(name="Position", value=target.position, inline=True)
822
embed.color = target.color
823
824
await ctx.send(embed=embed)
825
```
826
827
## Checks
828
829
Permission and condition checking decorators for command access control.
830
831
### Built-in Checks { .api }
832
833
```python
834
from nextcord.ext import commands
835
836
# Permission-based checks
837
@commands.has_permissions(manage_messages=True)
838
@bot.command()
839
async def purge(ctx, amount: int):
840
"""Delete messages (requires manage_messages permission)."""
841
if amount > 100:
842
await ctx.send("❌ Cannot delete more than 100 messages at once.")
843
return
844
845
deleted = await ctx.channel.purge(limit=amount + 1) # +1 for command message
846
await ctx.send(f"✅ Deleted {len(deleted) - 1} messages.", delete_after=5)
847
848
@commands.has_any_role("Admin", "Moderator")
849
@bot.command()
850
async def mod_command(ctx):
851
"""Command available to users with Admin or Moderator role."""
852
await ctx.send("This is a moderator command!")
853
854
@commands.has_role("VIP")
855
@bot.command()
856
async def vip_command(ctx):
857
"""Command available only to VIP members."""
858
await ctx.send("Welcome to the VIP area!")
859
860
# Context-based checks
861
@commands.guild_only()
862
@bot.command()
863
async def server_info(ctx):
864
"""Get server information (guild only)."""
865
guild = ctx.guild
866
867
embed = nextcord.Embed(title=guild.name, color=nextcord.Color.blue())
868
embed.set_thumbnail(url=guild.icon.url if guild.icon else None)
869
870
embed.add_field(name="Members", value=guild.member_count, inline=True)
871
embed.add_field(name="Channels", value=len(guild.channels), inline=True)
872
embed.add_field(name="Roles", value=len(guild.roles), inline=True)
873
embed.add_field(name="Owner", value=guild.owner.mention, inline=True)
874
embed.add_field(name="Created", value=guild.created_at.strftime("%Y-%m-%d"), inline=True)
875
876
await ctx.send(embed=embed)
877
878
@commands.dm_only()
879
@bot.command()
880
async def secret(ctx):
881
"""A command that only works in DMs."""
882
await ctx.send("🤫 This is a secret message that only works in DMs!")
883
884
@commands.is_owner()
885
@bot.command()
886
async def shutdown(ctx):
887
"""Shut down the bot (owner only)."""
888
await ctx.send("Shutting down...")
889
await bot.close()
890
891
# Cooldown checks
892
@commands.cooldown(1, 30.0, commands.BucketType.user)
893
@bot.command()
894
async def daily(ctx):
895
"""Claim daily reward (once per 30 seconds per user)."""
896
# This would typically involve a database
897
reward = 100
898
await ctx.send(f"🎁 You claimed your daily reward of {reward} coins!")
899
900
@commands.cooldown(2, 60.0, commands.BucketType.guild)
901
@bot.command()
902
async def server_command(ctx):
903
"""A command with server-wide cooldown (2 uses per minute per guild)."""
904
await ctx.send("This command has a server-wide cooldown!")
905
906
# Custom checks
907
def is_in_voice():
908
"""Check if user is in a voice channel."""
909
async def predicate(ctx):
910
return ctx.author.voice is not None
911
return commands.check(predicate)
912
913
def bot_has_permissions(**perms):
914
"""Check if bot has specific permissions."""
915
async def predicate(ctx):
916
bot_perms = ctx.channel.permissions_for(ctx.me)
917
missing = [perm for perm, value in perms.items() if getattr(bot_perms, perm) != value]
918
if missing:
919
raise commands.BotMissingPermissions(missing)
920
return True
921
return commands.check(predicate)
922
923
@is_in_voice()
924
@bot.command()
925
async def voice_info(ctx):
926
"""Get information about your current voice channel."""
927
voice = ctx.author.voice
928
channel = voice.channel
929
930
embed = nextcord.Embed(
931
title=f"Voice Channel: {channel.name}",
932
color=nextcord.Color.green()
933
)
934
935
embed.add_field(name="Members", value=len(channel.members), inline=True)
936
embed.add_field(name="Bitrate", value=f"{channel.bitrate // 1000}kbps", inline=True)
937
embed.add_field(name="User Limit", value=channel.user_limit or "No limit", inline=True)
938
939
if voice.self_mute:
940
embed.add_field(name="Status", value="🔇 Self-muted", inline=True)
941
if voice.self_deaf:
942
embed.add_field(name="Status", value="🔇 Self-deafened", inline=True)
943
944
await ctx.send(embed=embed)
945
946
@bot_has_permissions(embed_links=True, attach_files=True)
947
@bot.command()
948
async def fancy_command(ctx):
949
"""A command that requires bot to have embed and file permissions."""
950
embed = nextcord.Embed(title="Fancy Command", description="This requires special permissions!")
951
952
# Create a simple text file
953
import io
954
file_content = "This is a generated file!"
955
file = nextcord.File(io.BytesIO(file_content.encode()), filename="generated.txt")
956
957
await ctx.send(embed=embed, file=file)
958
959
# Global check for all commands
960
@bot.check
961
async def globally_block_dms(ctx):
962
"""Global check to block all commands in DMs."""
963
return ctx.guild is not None
964
965
# Per-command check override
966
@bot.command()
967
@commands.check(lambda ctx: True) # Override global check
968
async def help_dm(ctx):
969
"""Help command that works in DMs despite global check."""
970
await ctx.send("This help command works everywhere!")
971
```
972
973
## Error Handling
974
975
Comprehensive error handling for command execution with custom error responses.
976
977
### Error Handlers { .api }
978
979
```python
980
@bot.event
981
async def on_command_error(ctx, error):
982
"""Global command error handler."""
983
984
# Ignore errors from commands that have local error handlers
985
if hasattr(ctx.command, 'on_error'):
986
return
987
988
# Ignore errors from cogs that have local error handlers
989
if ctx.cog and ctx.cog.has_error_handler():
990
return
991
992
# Handle specific error types
993
if isinstance(error, commands.CommandNotFound):
994
# Optionally ignore or suggest similar commands
995
return
996
997
elif isinstance(error, commands.MissingRequiredArgument):
998
embed = nextcord.Embed(
999
title="❌ Missing Argument",
1000
description=f"Missing required argument: `{error.param.name}`",
1001
color=nextcord.Color.red()
1002
)
1003
1004
# Show command usage
1005
if ctx.command.help:
1006
embed.add_field(name="Usage", value=f"`{ctx.prefix}{ctx.command.qualified_name} {ctx.command.signature}`", inline=False)
1007
embed.add_field(name="Help", value=ctx.command.help, inline=False)
1008
1009
await ctx.send(embed=embed)
1010
1011
elif isinstance(error, commands.BadArgument):
1012
embed = nextcord.Embed(
1013
title="❌ Invalid Argument",
1014
description=str(error),
1015
color=nextcord.Color.red()
1016
)
1017
await ctx.send(embed=embed)
1018
1019
elif isinstance(error, commands.MissingPermissions):
1020
missing_perms = ", ".join(error.missing_permissions)
1021
await ctx.send(f"❌ You're missing required permissions: {missing_perms}")
1022
1023
elif isinstance(error, commands.BotMissingPermissions):
1024
missing_perms = ", ".join(error.missing_permissions)
1025
await ctx.send(f"❌ I'm missing required permissions: {missing_perms}")
1026
1027
elif isinstance(error, commands.NoPrivateMessage):
1028
await ctx.send("❌ This command cannot be used in private messages.")
1029
1030
elif isinstance(error, commands.PrivateMessageOnly):
1031
await ctx.send("❌ This command can only be used in private messages.")
1032
1033
elif isinstance(error, commands.CheckFailure):
1034
await ctx.send("❌ You don't have permission to use this command.")
1035
1036
elif isinstance(error, commands.CommandOnCooldown):
1037
time_left = round(error.retry_after, 1)
1038
await ctx.send(f"⏰ Command is on cooldown. Try again in {time_left} seconds.")
1039
1040
elif isinstance(error, commands.MaxConcurrencyReached):
1041
await ctx.send(f"❌ Too many people are using this command. Try again later.")
1042
1043
elif isinstance(error, commands.CommandInvokeError):
1044
# Handle errors that occur during command execution
1045
original_error = error.original
1046
1047
if isinstance(original_error, nextcord.Forbidden):
1048
await ctx.send("❌ I don't have permission to perform this action.")
1049
elif isinstance(original_error, nextcord.NotFound):
1050
await ctx.send("❌ The requested resource was not found.")
1051
elif isinstance(original_error, nextcord.HTTPException):
1052
await ctx.send("❌ An error occurred while communicating with Discord.")
1053
else:
1054
# Log unexpected errors
1055
print(f"Unexpected error in {ctx.command}: {original_error}")
1056
await ctx.send("❌ An unexpected error occurred. Please try again later.")
1057
1058
else:
1059
# Log unhandled errors
1060
print(f"Unhandled error type: {type(error).__name__}: {error}")
1061
await ctx.send("❌ An error occurred while processing the command.")
1062
1063
# Command-specific error handler
1064
@bot.command()
1065
async def divide(ctx, a: float, b: float):
1066
"""Divide two numbers."""
1067
try:
1068
result = a / b
1069
await ctx.send(f"{a} ÷ {b} = {result}")
1070
except ZeroDivisionError:
1071
await ctx.send("❌ Cannot divide by zero!")
1072
1073
@divide.error
1074
async def divide_error(ctx, error):
1075
"""Local error handler for the divide command."""
1076
if isinstance(error, commands.BadArgument):
1077
await ctx.send("❌ Please provide valid numbers for division.")
1078
1079
# Custom exceptions
1080
class CustomCommandError(commands.CommandError):
1081
"""Base class for custom command errors."""
1082
pass
1083
1084
class InsufficientFundsError(CustomCommandError):
1085
"""Raised when user doesn't have enough money."""
1086
def __init__(self, required: int, available: int):
1087
self.required = required
1088
self.available = available
1089
super().__init__(f"Insufficient funds: need {required}, have {available}")
1090
1091
@bot.command()
1092
async def buy(ctx, item: str, amount: int):
1093
"""Buy an item from the shop."""
1094
# Mock data
1095
user_balance = 50
1096
item_price = 25
1097
total_cost = item_price * amount
1098
1099
if user_balance < total_cost:
1100
raise InsufficientFundsError(total_cost, user_balance)
1101
1102
# Process purchase
1103
await ctx.send(f"✅ Purchased {amount}x {item} for {total_cost} coins!")
1104
1105
@buy.error
1106
async def buy_error(ctx, error):
1107
"""Error handler for buy command."""
1108
if isinstance(error, InsufficientFundsError):
1109
embed = nextcord.Embed(
1110
title="💰 Insufficient Funds",
1111
description=f"You need {error.required} coins but only have {error.available}.",
1112
color=nextcord.Color.red()
1113
)
1114
embed.add_field(name="Shortfall", value=f"{error.required - error.available} coins", inline=True)
1115
await ctx.send(embed=embed)
1116
```
1117
1118
## Advanced Features
1119
1120
Advanced command framework features including dynamic command loading and help systems.
1121
1122
### Custom Help Command { .api }
1123
1124
```python
1125
class CustomHelpCommand(commands.DefaultHelpCommand):
1126
"""Custom help command with embeds and better formatting."""
1127
1128
def __init__(self):
1129
super().__init__(
1130
command_attrs={
1131
'name': 'help',
1132
'aliases': ['h'],
1133
'help': 'Shows help about the bot, a command, or a category'
1134
}
1135
)
1136
1137
async def send_bot_help(self, mapping):
1138
"""Send help for the entire bot."""
1139
embed = nextcord.Embed(
1140
title="Bot Help",
1141
description="Here are all available commands:",
1142
color=nextcord.Color.blue()
1143
)
1144
1145
for cog, commands in mapping.items():
1146
filtered_commands = await self.filter_commands(commands, sort=True)
1147
if not filtered_commands:
1148
continue
1149
1150
cog_name = getattr(cog, 'qualified_name', 'No Category')
1151
command_list = [f"`{c.name}`" for c in filtered_commands]
1152
1153
embed.add_field(
1154
name=cog_name,
1155
value=" • ".join(command_list) or "No commands",
1156
inline=False
1157
)
1158
1159
embed.set_footer(text=f"Use {self.context.prefix}help <command> for more info on a command.")
1160
1161
channel = self.get_destination()
1162
await channel.send(embed=embed)
1163
1164
async def send_command_help(self, command):
1165
"""Send help for a specific command."""
1166
embed = nextcord.Embed(
1167
title=f"Command: {command.qualified_name}",
1168
description=command.help or "No description available",
1169
color=nextcord.Color.blue()
1170
)
1171
1172
embed.add_field(
1173
name="Usage",
1174
value=f"`{self.context.prefix}{command.qualified_name} {command.signature}`",
1175
inline=False
1176
)
1177
1178
if command.aliases:
1179
embed.add_field(
1180
name="Aliases",
1181
value=", ".join(f"`{alias}`" for alias in command.aliases),
1182
inline=False
1183
)
1184
1185
if isinstance(command, commands.Group):
1186
subcommands = [f"`{c.name}`" for c in command.commands]
1187
if subcommands:
1188
embed.add_field(
1189
name="Subcommands",
1190
value=" • ".join(subcommands),
1191
inline=False
1192
)
1193
1194
channel = self.get_destination()
1195
await channel.send(embed=embed)
1196
1197
async def send_group_help(self, group):
1198
"""Send help for a command group."""
1199
embed = nextcord.Embed(
1200
title=f"Command Group: {group.qualified_name}",
1201
description=group.help or "No description available",
1202
color=nextcord.Color.blue()
1203
)
1204
1205
embed.add_field(
1206
name="Usage",
1207
value=f"`{self.context.prefix}{group.qualified_name} {group.signature}`",
1208
inline=False
1209
)
1210
1211
filtered_commands = await self.filter_commands(group.commands, sort=True)
1212
if filtered_commands:
1213
command_list = []
1214
for command in filtered_commands:
1215
command_list.append(f"`{command.name}` - {command.short_doc or 'No description'}")
1216
1217
embed.add_field(
1218
name="Subcommands",
1219
value="\n".join(command_list),
1220
inline=False
1221
)
1222
1223
channel = self.get_destination()
1224
await channel.send(embed=embed)
1225
1226
# Set custom help command
1227
bot.help_command = CustomHelpCommand()
1228
1229
# Pagination for long help outputs
1230
class PaginatedHelpCommand(commands.HelpCommand):
1231
"""Help command with pagination for large outputs."""
1232
1233
def __init__(self):
1234
super().__init__(
1235
command_attrs={
1236
'name': 'help',
1237
'help': 'Shows help with pagination'
1238
}
1239
)
1240
1241
async def send_bot_help(self, mapping):
1242
"""Send paginated bot help."""
1243
pages = []
1244
1245
for cog, commands in mapping.items():
1246
filtered_commands = await self.filter_commands(commands, sort=True)
1247
if not filtered_commands:
1248
continue
1249
1250
embed = nextcord.Embed(
1251
title=f"Commands: {getattr(cog, 'qualified_name', 'No Category')}",
1252
color=nextcord.Color.blue()
1253
)
1254
1255
for command in filtered_commands:
1256
embed.add_field(
1257
name=f"{self.context.prefix}{command.qualified_name}",
1258
value=command.short_doc or "No description",
1259
inline=False
1260
)
1261
1262
pages.append(embed)
1263
1264
if not pages:
1265
await self.get_destination().send("No commands available.")
1266
return
1267
1268
# Simple pagination (you could use a more sophisticated system)
1269
for i, page in enumerate(pages):
1270
page.set_footer(text=f"Page {i+1}/{len(pages)}")
1271
await self.get_destination().send(embed=page)
1272
1273
# Dynamic command loading
1274
async def load_extension_command(ctx, extension: str):
1275
"""Dynamically load a bot extension."""
1276
try:
1277
bot.load_extension(f"cogs.{extension}")
1278
await ctx.send(f"✅ Loaded extension: {extension}")
1279
except commands.ExtensionError as e:
1280
await ctx.send(f"❌ Failed to load extension: {e}")
1281
1282
async def unload_extension_command(ctx, extension: str):
1283
"""Dynamically unload a bot extension."""
1284
try:
1285
bot.unload_extension(f"cogs.{extension}")
1286
await ctx.send(f"✅ Unloaded extension: {extension}")
1287
except commands.ExtensionError as e:
1288
await ctx.send(f"❌ Failed to unload extension: {e}")
1289
1290
async def reload_extension_command(ctx, extension: str):
1291
"""Dynamically reload a bot extension."""
1292
try:
1293
bot.reload_extension(f"cogs.{extension}")
1294
await ctx.send(f"✅ Reloaded extension: {extension}")
1295
except commands.ExtensionError as e:
1296
await ctx.send(f"❌ Failed to reload extension: {e}")
1297
```
1298
1299
This comprehensive documentation covers all major aspects of nextcord's traditional command framework, providing developers with the tools needed to create sophisticated text-based bot interfaces alongside modern application commands.