Callbacks & Customization¶
Learn how to customize d-back's behavior using callbacks to override default functionality, integrate with external data sources, and implement custom authentication logic.
Introduction¶
d-back provides a powerful callback system that allows you to customize nearly every aspect of server behavior. Callbacks are async functions that you register with the server to replace default implementations, making it easy to:
- Replace mock data with real Discord API data
- Implement custom authentication and authorization
- Integrate with databases or other data sources
- Customize static file serving
- Add logging and monitoring
All callbacks are async functions, designed to work seamlessly with Python's asyncio framework. The callback system is defined in the WebSocketServer class and provides hooks at key points in the request lifecycle.
Available Callbacks¶
on_get_server_data Callback¶
Purpose: Provide a list of available Discord servers (guilds) to clients.
Signature:
Expected Return Format:
{
"server_id_1": {
"id": "server_id_1",
"name": "My Discord Server",
"passworded": False,
"default": True
},
"server_id_2": {
"id": "server_id_2",
"name": "Another Server",
"passworded": True,
"default": False
}
}
Example:
async def get_my_servers():
"""Fetch available Discord servers from a database."""
# Your custom logic here
return {
"232769614004748288": {
"id": "232769614004748288",
"name": "d-world",
"passworded": False,
"default": True
}
}
server = WebSocketServer()
server.on_get_server_data(get_my_servers)
Use Case: Integrate with the Discord API to fetch the list of servers your bot is in, or fetch from a database of configured servers.
on_get_user_data Callback¶
Purpose: Provide user data (presence, roles, etc.) for a specific Discord server.
Signature:
Parameters:
- server_id (str): Discord server snowflake ID
Expected Return Format:
{
"user_id_1": {
"uid": "user_id_1",
"username": "CoolUser",
"status": "online", # online, idle, dnd, offline
"roleColor": "#ff6b6b" # Hex color code
},
"user_id_2": {
"uid": "user_id_2",
"username": "AnotherUser",
"status": "idle",
"roleColor": "#5bc0de"
}
}
Example:
async def my_user_data_provider(server_id: str):
"""Fetch real user data from Discord API."""
# Your custom logic here
return {
"user123": {
"uid": "user123",
"username": "MyUser",
"status": "online",
"roleColor": "#ff6b6b"
}
}
server = WebSocketServer()
server.on_get_user_data(my_user_data_provider)
Use Case: Fetch real Discord member data, query a database, or implement custom presence tracking.
on_static_request Callback¶
Purpose: Handle custom static file requests with dynamic generation or custom routing.
Signature:
Parameters:
- path (str): Requested file path (e.g., "/index.html", "/api/data")
Expected Return:
- Tuple[str, str]: (content_type, content) if handled - both strings
- None: Fall back to default static file serving
Example:
async def custom_static_handler(path: str):
"""Dynamically generate or route static files."""
if path == "/api/status":
# Return dynamic JSON
data = {"status": "ok", "version": "1.0.0"}
return ("application/json", json.dumps(data))
# Return None to use default static file serving
return None
server = WebSocketServer()
server.on_static_request(custom_static_handler)
Use Cases: - Dynamic file generation - Custom API endpoints - CDN integration - A/B testing different frontend versions - Server-side rendering
on_validate_discord_user Callback¶
Purpose: Validate Discord OAuth2 tokens and verify user membership in a server.
Signature:
Parameters:
- token (str): Discord OAuth2 access token
- user_info (Dict[str, Any]): User information from Discord OAuth2
- server_id (str): Discord server ID to validate membership in
Expected Return:
- bool: True if the token is valid and user is a member, False otherwise
Example:
import aiohttp
async def validate_token(token: str, user_info: Dict[str, Any], server_id: str) -> bool:
"""Validate Discord OAuth2 token and check server membership."""
try:
# Validate token with Discord API
async with aiohttp.ClientSession() as session:
headers = {"Authorization": f"Bearer {token}"}
# Check server membership
async with session.get(f"https://discord.com/api/users/@me/guilds", headers=headers) as resp:
if resp.status != 200:
return False
guilds = await resp.json()
# Check if user is in the requested server
is_member = any(guild["id"] == server_id for guild in guilds)
return is_member
except Exception as e:
print(f"Token validation error: {e}")
return False
server = WebSocketServer()
server.on_validate_discord_user(validate_token)
Use Case: Implement real Discord OAuth2 validation for protected servers.
Security Note
Always validate tokens server-side. Never trust client-provided user IDs without token validation.
on_get_client_id Callback¶
Purpose: Provide the Discord OAuth2 client ID for a specific server.
Signature:
Parameters:
- server_id (str): Discord server ID
Expected Return:
- str: OAuth2 client ID string
Example:
async def get_client_id(server_id: str) -> str:
"""Return OAuth2 client ID for a server."""
# Could fetch from database or configuration
client_ids = {
"232769614004748288": "123456789012345678",
"482241773318701056": "987654321098765432"
}
return client_ids.get(server_id, "default_client_id")
server = WebSocketServer()
server.on_get_client_id(get_client_id)
Use Case: Support different OAuth2 applications per server for multi-tenant setups.
Registering Callbacks¶
To register callbacks with d-back, call the callback method with your async function as an argument:
Async Requirement
All callbacks must be async functions (defined with async def). Synchronous functions will cause errors.
Complete Example with Multiple Callbacks¶
import asyncio
from d_back.server import WebSocketServer
# Define callbacks
async def get_servers():
return {
"232769614004748288": {
"id": "232769614004748288",
"name": "d-world",
"passworded": False,
"default": True
}
}
async def get_users(server_id: str):
# Fetch from database or Discord API
return {
"user123": {
"uid": "user123",
"username": "TestUser",
"status": "online",
"roleColor": "#ff6b6b"
}
}
async def validate_user(token: str, user_info: Dict[str, Any], server_id: str):
# Implement real OAuth2 validation
return True
# Create and configure server
async def main():
server = WebSocketServer(port=3000, host="localhost")
# Register callbacks
server.on_get_server_data(get_servers)
server.on_get_user_data(get_users)
server.on_validate_discord_user(validate_user)
# Start server
await server.start()
asyncio.run(main())
Broadcasting Methods¶
In addition to receiving data through callbacks, d-back provides methods to broadcast updates to all connected clients.
broadcast_message¶
Purpose: Send chat messages to all clients subscribed to a server.
Signature:
Parameters:
- server (str): Discord server ID
- uid (str): User ID sending the message
- message (str): Message content
- channel (str): Channel ID where message was sent
Example:
# When a Discord message is received
await server.broadcast_message(
server="232769614004748288",
uid="user123",
message="Hello, d-zone!",
channel="channel456"
)
broadcast_presence¶
Purpose: Send user presence updates (status changes, online/offline events).
Signature:
async def broadcast_presence(
server: str,
uid: str,
status: str,
username: Optional[str] = None,
role_color: Optional[str] = None,
delete: bool = False
)
Parameters:
- server (str): Discord server ID
- uid (str): User ID
- status (str): User status ("online", "idle", "dnd", "offline")
- username (Optional[str]): Username (required for new users)
- role_color (Optional[str]): Role color hex code
- delete (bool): Whether to remove the user (for offline/leave events)
Example - User comes online:
await server.broadcast_presence(
server="232769614004748288",
uid="user123",
status="online",
username="CoolUser",
role_color="#ff6b6b"
)
Example - User goes offline:
await server.broadcast_presence(
server="232769614004748288",
uid="user123",
status="offline",
delete=True
)
broadcast_client_id_update¶
Purpose: Send OAuth2 client ID updates to clients.
Signature:
Parameters:
- server (str): Discord server ID
- client_id (str): New OAuth2 client ID
Example:
# When OAuth2 configuration changes
await server.broadcast_client_id_update(
server="232769614004748288",
client_id="new_client_id_here"
)
Use Case: Dynamic OAuth2 configuration updates without reconnecting clients.
OAuth2 Integration¶
d-back includes built-in support for Discord OAuth2 authentication, allowing you to restrict access to specific servers.
OAuth2 Flow¶
- Client requests connection to a protected server
- Server checks if the server requires authentication (passworded: true)
- Client obtains OAuth2 token from Discord
- Client sends token to d-back
- d-back validates token using
on_validate_discord_usercallback - Server grants or denies access based on validation result
Implementing OAuth2 Validation¶
import aiohttp
async def oauth2_validator(token: str, user_info: Dict[str, Any], server_id: str) -> bool:
"""Validate Discord OAuth2 token."""
try:
async with aiohttp.ClientSession() as session:
# Validate with Discord API
headers = {"Authorization": f"Bearer {token}"}
async with session.get("https://discord.com/api/users/@me/guilds", headers=headers) as resp:
if resp.status == 200:
guilds = await resp.json()
# Check membership
for guild in guilds:
if guild["id"] == server_id:
return True
return False
except Exception as e:
print(f"OAuth2 validation error: {e}")
return False
server = WebSocketServer()
server.on_validate_discord_user(oauth2_validator)
Security Considerations¶
Security Best Practices
- Always validate tokens server-side, never trust client claims
- Use HTTPS in production to protect tokens in transit
- Implement rate limiting to prevent token brute-forcing
- Log authentication attempts for security monitoring
- Rotate OAuth2 client secrets regularly
- Set appropriate token expiration times
Error Handling¶
Robust error handling in callbacks is essential for server stability.
Best Practices¶
import logging
logger = logging.getLogger(__name__)
async def safe_user_data_provider(server_id: str):
"""User data provider with comprehensive error handling."""
try:
# Your data fetching logic
data = await fetch_user_data(server_id)
# Validate data format
if not isinstance(data, dict):
logger.error(f"Invalid data format for server {server_id}")
return {}
return data
except ConnectionError as e:
logger.error(f"Connection error fetching user data: {e}")
# Return empty dict or cached data
return {}
except Exception as e:
logger.exception(f"Unexpected error in user data provider: {e}")
# Return safe default
return {}
server = WebSocketServer()
server.on_get_user_data(safe_user_data_provider)
Error Handling Checklist¶
- ✅ Use try-except blocks to catch exceptions
- ✅ Log errors for debugging and monitoring
- ✅ Return safe defaults on error (empty dict, None, etc.)
- ✅ Validate input parameters before processing
- ✅ Validate output format before returning
- ✅ Don't let exceptions propagate to crash the server
- ✅ Implement retries for transient failures (with backoff)
Best Practices¶
Keep Callbacks Lightweight¶
Callbacks are called frequently, so performance matters:
# ❌ Bad: Expensive operation on every call
async def slow_callback(server_id: str):
# This queries the database every single time
data = await database.query_all_users(server_id)
return data
# ✅ Good: Use caching
from functools import lru_cache
import time
_cache = {}
_cache_ttl = 60 # seconds
async def fast_callback(server_id: str):
# Check cache first
if server_id in _cache:
cached_data, cached_time = _cache[server_id]
if time.time() - cached_time < _cache_ttl:
return cached_data
# Cache miss, fetch from database
data = await database.query_all_users(server_id)
_cache[server_id] = (data, time.time())
return data
Validate Input Parameters¶
async def validated_callback(server_id: str):
# Validate input
if not server_id or not isinstance(server_id, str):
logger.warning(f"Invalid server_id: {server_id}")
return {}
if not server_id.isdigit():
logger.warning(f"server_id is not a snowflake: {server_id}")
return {}
# Proceed with valid input
return await fetch_data(server_id)
Return Consistent Data Structures¶
# Always return the expected format
async def consistent_callback(server_id: str):
try:
data = await fetch_data(server_id)
# Ensure correct format
if not isinstance(data, dict):
return {}
# Validate required fields
for user_id, user_data in data.items():
required_fields = ["uid", "username", "status", "roleColor"]
if not all(field in user_data for field in required_fields):
logger.warning(f"User {user_id} missing required fields")
# Fix or skip invalid user
continue
return data
except Exception:
# Always return expected type on error
return {}
Test Callbacks Independently¶
import pytest
@pytest.mark.asyncio
async def test_user_data_callback():
"""Test user data callback returns correct format."""
data = await my_user_data_provider("232769614004748288")
# Check return type
assert isinstance(data, dict)
# Check user data format
for user_id, user_data in data.items():
assert "uid" in user_data
assert "username" in user_data
assert "status" in user_data
assert user_data["status"] in ["online", "idle", "dnd", "offline"]
assert "roleColor" in user_data
assert user_data["roleColor"].startswith("#")
What's Next?¶
Now that you understand callbacks, explore how to implement complete custom data providers:
- Custom Data Providers: Full Discord API integration examples
- API Reference: Detailed API documentation
- Configuration: Server configuration options