Skip to main content
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

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

Best Practices

1

Use ctx.respond()

Always use ctx.respond() instead of ctx.send() for compatibility with both command types.
2

Check Command Type When Needed

Use ctx.is_app to implement type-specific logic when necessary.
3

Handle Ephemeral Appropriately

Remember that ephemeral responses only work with slash commands.
4

Test Both Versions

Always test your bridge commands as both slash and prefix commands.
5

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.

Build docs developers (and LLMs) love