Discord Bots with Discord webhooks and bot APIs
Connecting a text-based MUD to Discord requires translating decades-old Telnet protocols into modern REST APIs without compromising game security or flooding channels with combat spam. This guide implements a production-ready bridge using GMCP (Generic Mud Communication Protocol) events when available, falling back to ANSI-stripped text parsing for legacy servers. You will configure a Discord bot with proper gateway intents, establish resilient Telnet connections with exponential backoff reconnection logic, and implement channel-specific rate limiting to prevent API bans during high-load game events.

Architecture Selection: Bot User vs Webhook
Bot users maintain persistent Gateway connections required for bidirectional chat (Discord-to-MUD), while webhooks only support unidirectional pushes. For MUD integration, use a bot user to handle reconnection state and channel-specific rate limits. Create the application at discord.com/developers and enable the Message Content privileged intent.
⚠ Common Pitfalls
- •Webhooks cannot read Discord messages, preventing MUD players from seeing Discord chat
- •Bot tokens committed to public repositories allow attackers to spam your Discord guild
Configure Gateway Intents and Channel Permissions
In the Discord Developer Portal, enable Message Content Intent to receive message content. Restrict the bot's channel access to only public chat channels; explicitly deny access to staff channels, admin logs, and private discussion categories to prevent accidental data leakage.
{
"intents": [
"GUILDS",
"GUILD_MESSAGES",
"MESSAGE_CONTENT"
],
"permissions": "274877910016"
}⚠ Common Pitfalls
- •Missing Message Content Intent results in empty message payloads without error warnings
- •Over-scoped permissions allow the bot to log sensitive immortal channel discussions
Implement Telnet Connection with State Recovery
Connect via raw Telnet or GMCP-enabled port. Implement NAWS (Negotiate About Window Size) and handle MCCP (Mud Client Compression Protocol) by decompressing streams with zlib. Use exponential backoff (2^n seconds) for reconnections to avoid hammering the MUD server during restarts.
import asyncio, telnetlib3, zlib
class MUDConnection:
def __init__(self):
self.reader = None
self.writer = None
self.retry_delay = 1
async def connect(self, host, port):
while True:
try:
self.reader, self.writer = await telnetlib3.open_connection(host, port)
self.retry_delay = 1
await self._negotiate()
await self._read_loop()
except ConnectionRefusedError:
await asyncio.sleep(min(self.retry_delay, 60))
self.retry_delay *= 2
async def _negotiate(self):
# Send IAC WILL GMCP
self.writer.write(b'\xff\xfb\xc9')⚠ Common Pitfalls
- •Linear reconnect loops without backoff trigger MUD server flood protection
- •Ignoring Telnet IAC sequences corrupts the byte stream and breaks parsing
Parse GMCP Events or ANSI-Stripped Text
If the MUD supports GMCP, subscribe to comm.channel events for structured data. Otherwise, strip ANSI codes using regex \x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]) and parse channel tags like [OOC] or [Newbie]. Map MUD channels to specific Discord channel IDs using a configuration dictionary.
const GMCP_PARSER = /\x1b\[\d+z<GMCP>(.*?)<\x1b\[\d+z/;
const ANSI_STRIP = /\x1B\[[0-?]*[ -/]*[@-~]/g;
function parseLine(line, channelMap) {
const clean = line.replace(ANSI_STRIP, '');
const gmcp = line.match(GMCP_PARSER);
if (gmcp) {
const data = JSON.parse(gmcp[1]);
return channelMap[data.channel];
}
// Fallback regex for legacy MUDs
const match = clean.match(/^\[(\w+)\]\s+(.+)/);
return match ? channelMap[match[1]] : null;
}⚠ Common Pitfalls
- •Parsing without ANSI stripping captures color codes as player names
- •Assuming GMCP availability breaks compatibility with DikuMUD derivatives
Implement Rate-Limited Message Queue
Discord allows 5 messages/second per channel. MUD combat generates burst traffic. Implement a per-channel queue with token bucket algorithm: buffer messages during bursts, drop or aggregate non-critical lines (combat spam), and prioritize tells and say commands.
import asyncio, time
from collections import deque
class RateLimiter:
def __init__(self, rate=5, per=1.0):
self.rate = rate
self.per = per
self.allowance = rate
self.last_check = time.time()
self.queue = deque()
async def send(self, message, channel):
current = time.time()
time_passed = current - self.last_check
self.last_check = current
self.allowance += time_passed * (self.rate / self.per)
if self.allowance > self.rate:
self.allowance = self.rate
if self.allowance < 1:
await asyncio.sleep(1 - self.allowance)
await channel.send(message)⚠ Common Pitfalls
- •Sending raw combat logs triggers instant rate limits and potential IP bans
- •No prioritization causes critical admin alerts to drop behind player chatter
Filter Sensitive Communication and Staff Channels
Maintain a blocklist of channel names (immortal, admin, private, password) and regex patterns for sensitive data (password prompts, email addresses, IP logs). Never relay tells, whispers, or private messages to Discord. Log filtering decisions for audit purposes.
⚠ Common Pitfalls
- •Relaying private tells violates player privacy and local data protection laws
- •Logging staff channels exposes game mechanics and anti-cheat systems
Handle Bidirectional Discord-to-MUD Relay
Listen for Discord message_create events, filter bot messages to prevent loops, and inject validated text into the MUD via Telnet write commands. Prefix Discord usernames to distinguish from in-game players (e.g., [D]Username: message). Sanitize input to prevent command injection.
client.on('messageCreate', async message => {
if (message.author.bot) return;
if (message.channel.id !== config.mudBridgeChannel) return;
const sanitized = message.content
.replace(/[^\w\s\p{P}]/gu, '')
.substring(0, 200);
const formatted = `chat [Discord] ${message.author.username}: ${sanitized}\n`;
mudConnection.writer.write(formatted);
});⚠ Common Pitfalls
- •Infinite loops occur when the bot relays its own messages back to Discord
- •Unsanitized Discord input allows players to execute MUD admin commands
Deploy with Process Management and Health Monitoring
Use systemd or PM2 to ensure the bridge restarts on crash. Implement health checks that verify both Discord Gateway heartbeat and MUD Telnet connection status. Alert admins via Discord DM if the MUD connection drops for >5 minutes.
⚠ Common Pitfalls
- •Docker containers without proper signal handling leave zombie Telnet processes
- •Silent failures in the Discord Gateway cause missed messages without error logs
What you built
A production MUD-to-Discord bridge requires defensive programming against unstable network conditions, strict data filtering to protect player privacy, and careful rate management to avoid API bans. Monitor your integration logs for GMCP parsing failures and Discord rate limit headers. Test reconnection scenarios by manually dropping the Telnet session during peak game hours to verify your backoff logic. Maintain separate configurations for development and production environments to prevent test messages from reaching your live community.