A high-performance Discord API library with Rust core
Project description
RustCord
RustCord is a high-performance asynchronous Python library for interacting with the Discord API, with core functionality implemented in Rust.
Features
- Asynchronous Python interface using
asyncio - High-performance Rust core for Discord API interactions
- Support for Discord REST API and Gateway (WebSocket) connections
- Resilient WebSocket connection with:
- Automatic reconnection with exponential backoff
- Session resuming to prevent missed events
- Heartbeat jitter to prevent thundering herd
- Proper connection state management
- Comprehensive error handling and recovery
- Event-based architecture for handling Discord events
- Slash command support with command registration and interaction responses
- Support for ephemeral responses, embeds, and deferred responses
- Support for autocomplete responses
- Support for permission based interaction commands
- Minimal memory footprint and CPU usage
- Clean and intuitive Python API
Current Status
This project is in active development. Currently:
- The Python interface has been fully designed and implemented
- A complete implementation of the Discord API client in Python is available
- Bot functionality is working with the actual Discord API
- Enhanced WebSocket connection with robust error handling and reconnection logic
- Support for Discord slash commands and interactions
- Proper command registration and interaction responses with deferred responses
- Rich embeds system for creating visually appealing messages with fields, images, and formatting
- Fully functional event dispatching system with coroutine support
- Connection state tracking and smart reconnection strategies
- Voice channel support for joining/leaving voice channels and audio playback
- UI Components (buttons, select menus, etc.) for interactive bot experiences
- Rust backend implementation is ready, providing optimized performance for core functionality
Installation
From PyPI
pip install rustcord
For Development
Clone the repository and install in development mode:
git clone https://github.com/ghulq/rustcord.git
cd rustcord
pip install -e .
Dependencies
RustCord requires the following Python packages:
pip install aiohttp websockets
For bot functionality, you'll need to set the DISCORD_TOKEN environment variable:
export DISCORD_TOKEN=your_bot_token_here
Usage Examples
Basic Bot Example
import asyncio
import os
from rustcord import Client, Intents
# Create Discord client with default intents
client = Client(intents=Intents.DEFAULT)
@client.event("ready")
async def on_ready(data):
"""Called when the bot is ready and connected to Discord"""
me = await client.get_current_user()
print(f"Logged in as {me.username}#{me.discriminator}")
# Print list of connected guilds
guilds = await client.get_current_guilds()
print(f"Connected to {len(guilds)} guilds:")
for guild in guilds:
print(f" - {guild.name} (ID: {guild.id})")
@client.event("message")
async def on_message(data):
"""Called when a message is received"""
content = data.get("content", "")
author = data.get("author", {})
author_username = author.get("username", "")
channel_id = data.get("channel_id", "")
# Don't respond to our own messages
if author.get("id") == (await client.get_current_user()).id:
return
print(f"Received message from {author_username}: {content}")
# Simple command handler
if content.startswith("!ping"):
await client.send_message(channel_id, "Pong!")
elif content.startswith("!hello"):
await client.send_message(channel_id, f"Hello, {author_username}!")
async def main():
# Connect to Discord
try:
await client.start()
# Keep the bot running
while True:
await asyncio.sleep(1)
except KeyboardInterrupt:
print("Bot is shutting down...")
await client.disconnect()
if __name__ == "__main__":
# Get token from environment variable
token = os.environ.get("DISCORD_TOKEN")
if not token:
print("Please set the DISCORD_TOKEN environment variable")
exit(1)
# Run the bot
asyncio.run(main())
Rich Embed Bot Example
import asyncio
import os
from typing import Any
from rustcord import Client, Intents
from rustcord.embeds import Embed, Color
# Create Discord client
client = Client(intents=Intents.DEFAULT)
@client.event("ready")
async def on_ready(data: dict[str, Any]):
"""Called when the bot is ready"""
me = await client.get_current_user()
print(f"Logged in as {me.username}")
print("Ready to send rich embeds!")
@client.event("message")
async def on_message(data: dict[str, Any]):
"""Called when a message is received"""
content = data.get("content", "")
author = data.get("author", {})
author_id = author.get("id", "")
channel_id = data.get("channel_id", "")
# Don't respond to our own messages
if author_id == (await client.get_current_user()).id:
return
# Send info embed
if content == "!info":
# Create a rich embed with various features
embed = Embed(
title="RustCord Information",
description="A high-performance Discord API library with Rust core",
color=Color.BLURPLE
)
# Add author information
embed.set_author(
name=author.get("username", "User"),
icon_url=f"https://cdn.discordapp.com/avatars/{author_id}/{author.get('avatar')}.png"
)
# Add fields - some inline, some not
embed.add_field("Version", "1.0.0", inline=True)
embed.add_field("Library", "RustCord", inline=True)
embed.add_field("Language", "Python + Rust", inline=True)
embed.add_field("Features", "• Fast WebSocket connections\n• Rich embeds\n• Slash commands", inline=False)
# Add footer
embed.set_footer(text="Powered by RustCord", icon_url="https://example.com/icon.png")
# Add timestamp
embed.set_timestamp()
# Send the embed
await client.send_message(channel_id, embed=embed)
# Send multiple embeds
elif content == "!colors":
# Create multiple embeds to demonstrate colors
embeds = [
Embed(title="Red Embed", description="This embed is red", color=Color.RED),
Embed(title="Green Embed", description="This embed is green", color=Color.GREEN),
Embed(title="Blue Embed", description="This embed is blue", color=Color.BLUE),
Embed(title="Custom Color", description="This embed has a custom purple color",
color=Color.from_rgb(128, 0, 255))
]
# Send multiple embeds in one message
await client.send_message(channel_id, content="Here are some colored embeds:", embeds=embeds)
async def main():
"""Main function"""
try:
await client.start()
# Keep the bot running
while True:
await asyncio.sleep(1)
except KeyboardInterrupt:
print("Bot is shutting down...")
await client.disconnect()
if __name__ == "__main__":
# Run the bot
asyncio.run(main())
Slash Commands Example
import asyncio
import os
from rustcord import Client
from rustcord.models import CommandOption, CommandOptionType, Interaction
# Create Discord client
client = Client()
@client.event("ready")
async def on_ready(data):
"""Called when the bot is ready"""
me = await client.get_current_user()
print(f"Logged in as {me.tag}")
print("Bot is ready!")
# Define a simple ping command
@client.command(
name="ping",
description="Checks if the bot is responding"
)
async def ping_command(interaction: Interaction):
"""Handles the /ping command"""
await interaction.respond("Pong! 🏓")
# Define a command with options
@client.command(
name="echo",
description="Echoes your message back to you",
options=[
CommandOption(
type=CommandOptionType.STRING,
name="message",
description="The message to echo",
required=True
),
CommandOption(
type=CommandOptionType.BOOLEAN,
name="ephemeral",
description="Whether the response should be ephemeral (only visible to you)",
required=False
)
]
)
async def echo_command(interaction: Interaction):
"""Handles the /echo command"""
# Get the command options
message = interaction.get_option_value("message", "")
ephemeral = interaction.get_option_value("ephemeral", False)
# Respond to the interaction
await interaction.respond(
content=f"You said: {message}",
ephemeral=ephemeral
)
async def main():
"""Main function"""
try:
# Connect to Discord
await client.start()
# Keep the bot running
while True:
await asyncio.sleep(1)
except KeyboardInterrupt:
print("Bot is shutting down...")
await client.disconnect()
if __name__ == "__main__":
# Run the bot
asyncio.run(main())
Advanced Features
The library currently supports:
- Slash Commands & Discord Interactions API
- Command Registration (both global and guild-specific)
- Command Options (arguments for slash commands)
- Subcommands and option choices
- Ephemeral Responses (responses only visible to the command user)
- Rich Embeds with fields, images, colors, and formatting
- Deferred Responses (for commands that take longer to process)
- Autocompletion for Interaction Options
- Permission based Interaction Commands
- Voice Channels support:
- Joining and leaving voice channels
- Audio playback from files
- Volume control
- Sharding support for scaling bots to large guild counts
Coming soon:
- Type-safe API interfaces with Rust validation
- Modal Forms
- Voice recording and streaming
Documentation
Connection Architecture
RustCord's WebSocket connection to the Discord Gateway implements several important reliability features:
-
Connection State Management
- Tracks the WebSocket connection through multiple states (Disconnected, Connecting, Connected, Reconnecting, Resuming, Identifying)
- Safe state transitions to prevent race conditions with mutex locks
-
Automatic Reconnection
- Exponential backoff strategy for reconnection attempts
- Jitter added to prevent thundering herd problems
- Configurable max reconnection attempts and backoff limits
-
Session Resuming
- Maintains session information to resume instead of creating new sessions
- Properly tracks sequence numbers to prevent missed events
- Handles Discord's INVALID_SESSION events appropriately
-
Heartbeat Management
- Immediate first heartbeat to quickly establish connection
- Automatic heartbeat acknowledgement tracking with timeout detection
- Jitter added to heartbeat timing to distribute load
-
Error Recovery
- Comprehensive error handling for all Gateway events
- Automatic recovery from common errors
- Special handling for fatal connection errors (authentication failures, etc.)
- Connection timeouts to detect stalled connections
Main Components
Client
The Client class is the main entry point for interacting with Discord:
from rustcord import Client, Intents
# Create a client with specific intents
client = Client(intents=Intents.DEFAULT)
# Register event handlers
@client.event("ready")
async def on_ready(data):
print("Bot is ready!")
# Start the client
await client.start()
Intents
Discord requires specifying which events your bot wants to receive through Gateway Intents:
from rustcord import Intents
# Use predefined intent groups
client = Client(intents=Intents.DEFAULT) # Common intents for most bots
client = Client(intents=Intents.ALL) # All intents (privileged intents require approval)
client = Client(intents=Intents.NONE) # No intents
# Or combine specific intents
intents = Intents.GUILDS | Intents.GUILD_MESSAGES | Intents.DIRECT_MESSAGES
client = Client(intents=intents)
Events
Register handlers for Discord Gateway events:
@client.event("ready")
async def on_ready(data):
print("Bot is ready!")
@client.event("message")
async def on_message(data):
print(f"Received message: {data.get('content')}")
Slash Commands
Register and handle slash commands:
@client.command(
name="ping",
description="Checks if the bot is responding"
)
async def ping_command(interaction):
await interaction.respond("Pong!")
Commands with options:
@client.command(
name="echo",
description="Echoes your message",
options=[
CommandOption(
type=CommandOptionType.STRING,
name="message",
description="The message to echo",
required=True
)
]
)
async def echo_command(interaction):
message = interaction.get_option_value("message")
await interaction.respond(f"You said: {message}")
Guild-specific commands:
@client.command(
name="guild_only",
description="This command only works in a specific guild",
guild_id="123456789012345678" # Your guild ID here
)
async def guild_command(interaction):
await interaction.respond("This command only works in this server!")
Rich Embeds
Create rich embed messages with various formatting options:
from rustcord import Client
from rustcord.embeds import Embed, Color
# Create a simple embed
embed = Embed(
title="Hello, world!",
description="This is a rich embed message",
color=Color.BLUE
)
# Add fields
embed.add_field("Regular Field", "This is a regular field", inline=False)
embed.add_field("Inline Field 1", "This is inline", inline=True)
embed.add_field("Inline Field 2", "This is also inline", inline=True)
# Add author information
embed.set_author(
name="RustCord Bot",
icon_url="https://example.com/icon.png"
)
# Add footer
embed.set_footer(
text="Powered by RustCord",
icon_url="https://example.com/footer_icon.png"
)
# Add images
embed.set_thumbnail("https://example.com/thumbnail.png")
embed.set_image("https://example.com/image.png")
# Send the embed
await client.send_message(channel_id, embed=embed)
# Send multiple embeds
embeds = [
Embed(title="First Embed", color=Color.RED),
Embed(title="Second Embed", color=Color.GREEN)
]
await client.send_message(channel_id, embeds=embeds)
# Color utilities
custom_color = Color.from_rgb(255, 0, 255) # Creates purple color
Interaction Responses
Respond to interactions in various ways:
# Basic text response
await interaction.respond("Hello!")
# Ephemeral response (only visible to the command user)
await interaction.respond("This is private", ephemeral=True)
# Response with embeds using the Embed class
from rustcord.embeds import Embed, Color
embed = Embed(
title="Embed Title",
description="This is an embed",
color=Color.BLURPLE
)
embed.add_field("Field 1", "Value 1", inline=True)
embed.add_field("Field 2", "Value 2", inline=True)
await interaction.respond(embed=embed)
# Deferred response (for commands that take time to process)
await interaction.defer_response()
# ... do some work ...
await client.edit_interaction_response(
interaction.token,
{"content": "Here's the result after processing!"}
)
Voice Features
Connect to voice channels and play audio:
from rustcord import Client, Intents
from rustcord.models import VoiceConnection, AudioPlayer
# Create Discord client with voice intents
intents = Intents.DEFAULT | Intents.GUILD_VOICE_STATES
client = Client(intents=intents)
# Store active voice connections and players
voice_connections = {}
audio_players = {}
@client.command("join", "Join a voice channel")
async def join_command(interaction):
guild_id = interaction.guild_id
channel_id = interaction.get_option_value("channel")
# Join the voice channel
connection = await client.join_voice_channel(guild_id, channel_id)
if connection:
voice_connections[guild_id] = connection
# Create an audio player for this connection
player = await client.create_audio_player()
audio_players[guild_id] = player
# Attach the player to the connection
await player.attach(connection)
await interaction.respond("Joined voice channel successfully!")
@client.command("play", "Play audio in the voice channel")
async def play_command(interaction):
guild_id = interaction.guild_id
file_path = interaction.get_option_value("file")
# Check if we're in a voice channel
if guild_id not in audio_players:
await interaction.respond("I need to join a voice channel first!")
return
# Play audio
player = audio_players[guild_id]
success = await player.play_file(file_path)
if success:
await interaction.respond(f"Now playing: {file_path}")
else:
await interaction.respond("Failed to play the file.")
@client.command("leave", "Leave the voice channel")
async def leave_command(interaction):
guild_id = interaction.guild_id
# Leave the voice channel
if guild_id in voice_connections:
# Stop any audio playback
if guild_id in audio_players:
await audio_players[guild_id].stop()
# Leave the channel
success = await client.leave_voice_channel(guild_id)
if success:
await interaction.respond("Left voice channel!")
Sharding
For bots in large numbers of guilds (approaching Discord's limit of 2500 guilds per connection), sharding is necessary:
from rustcord import Client, Intents
# Create a sharded client
client = Client(
intents=Intents.DEFAULT,
shard_id=0, # Current shard ID (0-based)
shard_count=2 # Total number of shards
)
# The client will only receive events for guilds that match:
# guild_id % shard_count = shard_id
# Multiple shards can be run in separate processes
# For example, to run shard 1 of 2:
client_shard1 = Client(
intents=Intents.DEFAULT,
shard_id=1,
shard_count=2
)
# Rest of the code is the same as a non-sharded bot
@client.event("ready")
async def on_ready(data):
print(f"Shard 0 is ready!")
@client_shard1.event("ready")
async def on_ready_shard1(data):
print(f"Shard 1 is ready!")
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rustcord-0.2.0.tar.gz.
File metadata
- Download URL: rustcord-0.2.0.tar.gz
- Upload date:
- Size: 77.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
571a61436f9a381129107e942e1d0d10141f784ade4e8f231aceb959a936848b
|
|
| MD5 |
3d47900dcb86851466f7042cd825aa5c
|
|
| BLAKE2b-256 |
85cdbec477388c3d695af5d9b2495407d5733c112edcf93d4442351f7313b820
|
File details
Details for the file rustcord-0.2.0-cp39-abi3-win_amd64.whl.
File metadata
- Download URL: rustcord-0.2.0-cp39-abi3-win_amd64.whl
- Upload date:
- Size: 2.3 MB
- Tags: CPython 3.9+, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aa257259c421ac1a53228b323f0de45b4d636d3d69bba6c59b1082d750e80ffe
|
|
| MD5 |
2bf9d05f89650272d98adfa3fe32b5c7
|
|
| BLAKE2b-256 |
10691fba5b25984070cafbb2587c2c7b1cd77c801463b9435498aadc5c589b34
|