Guides

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.

60-90 minutes8 steps
Discord Bots with Discord webhooks and bot APIs illustration
Placeholder illustration shown while custom artwork is being produced.
1

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
2

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-config.json
{
  "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
3

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.

mud_client.py
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
4

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.

parser.js
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
5

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.

rate_limiter.py
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
6

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
7

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.

discord_listener.js
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
8

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.