The ext.bridge extension allows you to create commands that work seamlessly as both slash commands and traditional prefix-based commands, reducing code duplication.
Installation
from discord.ext import bridge
Bridge Bot
Use bridge.Bot instead of commands.Bot to enable bridge command support.
import discord
from discord.ext import bridge, commands
intents = discord.Intents.default()
intents.message_content = True # Required for prefix commands
bot = bridge.Bot(
command_prefix="!",
intents=intents
)
Bridge commands require the message_content privileged intent for the prefix version to work.
Creating Bridge Commands
Basic Bridge Command
@bot.bridge_command()
async def ping(ctx: bridge.BridgeContext):
"""Responds with Pong! Works as both /ping and !ping"""
await ctx.respond("Pong!")
# Users can invoke this as:
# - Slash command: /ping
# - Prefix command: !ping
Bridge Command with Parameters
@bot.bridge_command()
async def greet(ctx: bridge.BridgeContext, member: discord.Member):
"""Greets a member."""
await ctx.respond(f"Hello {member.mention}!")
# Usage:
# - Slash: /greet @User
# - Prefix: !greet @User
Bridge Command with Options
@bot.bridge_command()
@discord.option("value", description="Choose a value", choices=[1, 2, 3])
async def choose(ctx: bridge.BridgeContext, value: int):
"""Choose a value from 1-3."""
await ctx.respond(f"You chose: {value}!")
# Slash version shows dropdown with choices
# Prefix version accepts numeric input
Bridge Context
The BridgeContext provides a unified interface for both command types.
@bot.bridge_command()
async def info(ctx: bridge.BridgeContext):
"""Shows context information."""
# Check which type of command was used
if ctx.is_app:
command_type = "Slash Command"
else:
command_type = "Prefix Command"
await ctx.respond(
f"Command Type: {command_type}\n"
f"Author: {ctx.author.mention}\n"
f"Guild: {ctx.guild.name}"
)
Context Methods
ctx.respond() - Works for both slash and prefix commands
ctx.defer() - Defers the response (both types)
ctx.followup.send() - Send followup (slash only)
ctx.is_app - Check if it’s a slash command
Bridge Command Groups
Creating Groups
@bot.bridge_group()
async def config(ctx: bridge.BridgeContext):
"""Configuration commands."""
if ctx.invoked_subcommand is None:
await ctx.respond("Use a subcommand: config prefix, config role")
@config.command()
async def prefix(ctx: bridge.BridgeContext, new_prefix: str):
"""Changes the bot prefix."""
# Save prefix logic
await ctx.respond(f"Prefix changed to: {new_prefix}")
@config.command()
async def role(ctx: bridge.BridgeContext, role: discord.Role):
"""Sets the admin role."""
await ctx.respond(f"Admin role set to: {role.name}")
# Usage:
# - Slash: /config prefix ?, /config role @Admin
# - Prefix: !config prefix ?, !config role @Admin
Map To Decorator
The map_to decorator allows you to map the base group command to a slash subcommand.
@bot.bridge_group(invoke_without_command=True)
@bridge.map_to("help")
async def admin(ctx: bridge.BridgeContext):
"""Admin commands."""
await ctx.respond("This is the admin help!")
@admin.command()
async def ban(ctx: bridge.BridgeContext, user: discord.User):
"""Bans a user."""
await ctx.respond(f"{user.mention} has been banned!")
# Usage:
# - Slash: /admin help (base command), /admin ban @User
# - Prefix: !admin (base command), !admin ban @User
Handling Different Command Types
Type-Specific Logic
@bot.bridge_command()
async def notify(ctx: bridge.BridgeContext, message: str):
"""Sends a notification."""
await ctx.respond(f"📢 {message}")
if ctx.is_app:
# Slash command - use followup
await ctx.followup.send("Notification sent!", ephemeral=True)
else:
# Prefix command - DM the user
await ctx.author.send("Notification sent!")
Attachments in Bridge Commands
@bot.bridge_command()
async def upload(ctx: bridge.BridgeContext, attachment: discord.Attachment):
"""Processes an uploaded file."""
await ctx.respond(f"Received file: {attachment.filename}")
# Get the file URL
url = attachment.url
if ctx.is_app:
await ctx.followup.send(f"File URL: {url}", ephemeral=True)
else:
await ctx.author.send(f"File URL: {url}")
Bridge Options
Use bridge.BridgeOption for advanced option configuration.
from discord.ext.bridge import BridgeOption
@bot.bridge_command()
async def timeout(
ctx: bridge.BridgeContext,
member: discord.Member,
duration: BridgeOption(
int,
description="Duration in minutes",
min_value=1,
max_value=1440
)
):
"""Times out a member."""
from datetime import timedelta
await member.timeout_for(timedelta(minutes=duration))
await ctx.respond(f"{member.mention} timed out for {duration} minutes.")
Permissions and Checks
Using Decorators
from discord.ext.bridge import guild_only, has_permissions
@bot.bridge_command()
@guild_only()
@has_permissions(manage_messages=True)
async def clear(ctx: bridge.BridgeContext, amount: int):
"""Clears messages (guild only, requires Manage Messages)."""
await ctx.channel.purge(limit=amount + 1)
await ctx.respond(f"Cleared {amount} messages!", ephemeral=True)
Custom Checks
def is_moderator():
async def predicate(ctx):
if ctx.guild is None:
return False
mod_role = discord.utils.get(ctx.guild.roles, name="Moderator")
return mod_role in ctx.author.roles
return commands.check(predicate)
@bot.bridge_command()
@is_moderator()
async def announce(ctx: bridge.BridgeContext, *, message: str):
"""Makes an announcement (moderators only)."""
await ctx.respond(f"📢 **Announcement:** {message}")
Error Handling
Global Bridge Error Handler
@bot.event
async def on_bridge_command_error(ctx: bridge.BridgeContext, error):
"""Handles errors for all bridge commands."""
if isinstance(error, commands.MissingPermissions):
await ctx.respond(
"❌ You don't have permission to use this command!",
ephemeral=True
)
elif isinstance(error, commands.MissingRequiredArgument):
await ctx.respond(
f"❌ Missing required argument: {error.param.name}",
ephemeral=True
)
elif isinstance(error, commands.BadArgument):
await ctx.respond(
"❌ Invalid argument provided!",
ephemeral=True
)
else:
# Re-raise unexpected errors
raise error
Local Error Handler
@bot.bridge_command()
async def divide(ctx: bridge.BridgeContext, a: int, b: int):
"""Divides two numbers."""
result = a / b
await ctx.respond(f"{a} / {b} = {result}")
@divide.error
async def divide_error(ctx: bridge.BridgeContext, error):
if isinstance(error, ZeroDivisionError):
await ctx.respond("❌ Cannot divide by zero!", ephemeral=True)
elif isinstance(error, commands.BadArgument):
await ctx.respond("❌ Please provide valid numbers!", ephemeral=True)
Bridge Cogs
Organize bridge commands in cogs for better structure.
from discord.ext import bridge, commands
class ModerationCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
@bridge.bridge_command()
@bridge.guild_only()
@bridge.has_permissions(kick_members=True)
async def kick(self, ctx: bridge.BridgeContext, member: discord.Member, *, reason: str = None):
"""Kicks a member from the server."""
await member.kick(reason=reason)
await ctx.respond(f"✅ {member.mention} was kicked.")
@bridge.bridge_command()
@bridge.guild_only()
@bridge.has_permissions(ban_members=True)
async def ban(self, ctx: bridge.BridgeContext, user: discord.User, *, reason: str = None):
"""Bans a user from the server."""
await ctx.guild.ban(user, reason=reason)
await ctx.respond(f"✅ {user.mention} was banned.")
@bridge.bridge_command()
@bridge.guild_only()
@bridge.has_permissions(manage_messages=True)
async def purge(self, ctx: bridge.BridgeContext, amount: int):
"""Purges messages from the channel."""
deleted = await ctx.channel.purge(limit=amount + 1)
await ctx.respond(
f"✅ Deleted {len(deleted) - 1} messages.",
ephemeral=True
)
async def setup(bot):
await bot.add_cog(ModerationCog(bot))
Complete Example
import discord
from discord.ext import bridge, commands
import asyncio
intents = discord.Intents.default()
intents.message_content = True
bot = bridge.Bot(
command_prefix=commands.when_mentioned_or("!"),
intents=intents
)
@bot.event
async def on_ready():
print(f"Logged in as {bot.user}")
@bot.bridge_command()
async def ping(ctx: bridge.BridgeContext):
"""Check bot latency."""
latency = round(bot.latency * 1000)
await ctx.respond(f"🏓 Pong! Latency: {latency}ms")
@bot.bridge_command()
@discord.option("seconds", description="Seconds to wait", min_value=1, max_value=60)
async def wait(ctx: bridge.BridgeContext, seconds: int = 5):
"""Waits for specified seconds."""
await ctx.defer()
await asyncio.sleep(seconds)
await ctx.respond(f"⏰ Waited for {seconds} seconds!")
@bot.bridge_command()
async def userinfo(ctx: bridge.BridgeContext, user: discord.User = None):
"""Shows information about a user."""
user = user or ctx.author
embed = discord.Embed(
title=f"User Information: {user.name}",
color=discord.Color.blue()
)
embed.set_thumbnail(url=user.display_avatar.url)
embed.add_field(name="ID", value=user.id, inline=True)
embed.add_field(name="Created", value=user.created_at.strftime("%Y-%m-%d"), inline=True)
await ctx.respond(embed=embed)
@bot.bridge_group()
async def settings(ctx: bridge.BridgeContext):
"""Bot settings commands."""
if ctx.invoked_subcommand is None:
await ctx.respond("Use a subcommand: settings prefix, settings color")
@settings.command()
@bridge.guild_only()
@bridge.has_permissions(administrator=True)
async def prefix(ctx: bridge.BridgeContext, new_prefix: str):
"""Changes the bot prefix."""
# Save prefix to database (implementation depends on your setup)
await ctx.respond(f"✅ Prefix changed to: `{new_prefix}`")
@settings.command()
async def color(ctx: bridge.BridgeContext, color: str):
"""Sets your preferred color."""
# Save user color preference
await ctx.respond(f"✅ Color set to: {color}")
@bot.event
async def on_bridge_command_error(ctx: bridge.BridgeContext, error):
"""Global error handler for bridge commands."""
if isinstance(error, commands.MissingPermissions):
await ctx.respond(
"❌ You don't have permission to use this command!",
ephemeral=True
)
elif isinstance(error, commands.MissingRequiredArgument):
await ctx.respond(
f"❌ Missing argument: `{error.param.name}`",
ephemeral=True
)
elif isinstance(error, bridge.BridgeCommandError):
await ctx.respond(f"❌ An error occurred: {error}", ephemeral=True)
else:
print(f"Unhandled error: {error}")
raise error
bot.run("TOKEN")
Differences Between Command Types
Context
Responses
Deferring
Slash Commands:
- Use
discord.ApplicationContext
- Access via
ctx.interaction
- Support ephemeral responses
- Have built-in autocomplete
Prefix Commands:
- Use
discord.ext.commands.Context
- Access via
ctx.message
- Regular message responses
- Manual argument parsing
Slash Commands:await ctx.respond("Hello", ephemeral=True)
await ctx.followup.send("Followup")
Prefix Commands:await ctx.send("Hello")
await ctx.reply("Reply")
Slash Commands:await ctx.defer(ephemeral=True)
# Do long operation
await ctx.respond("Done!")
Prefix Commands:async with ctx.typing():
# Do long operation
await ctx.send("Done!")
Best Practices
Use ctx.respond()
Always use ctx.respond() instead of ctx.send() for compatibility with both command types.
Check Command Type When Needed
Use ctx.is_app to implement type-specific logic when necessary.
Handle Ephemeral Appropriately
Remember that ephemeral responses only work with slash commands.
Test Both Versions
Always test your bridge commands as both slash and prefix commands.
Use Type Hints
Add proper type hints to command parameters for automatic conversion.
Bridge commands require the message_content privileged intent to be enabled for prefix command functionality.
The @discord.option() decorator provides additional configuration for slash commands, while prefix commands use standard type hints and converters.