Event Handler Mastery
Master all event types in the Towns Protocol Bot SDK. Learn to handle messages, reactions, user joins, threads, and advanced event patterns. Build sophisticated bot behaviors that respond intelligently to user interactions and community events.
Learning Objectives
By the end of this module, you will be able to:
Event Handler Overview
Learn about all available event handlers in the Towns Protocol Bot SDK and when to use each one.
All Available Event Handlers
All messages
Slash commands
Emoji reactions
Tips
Message edits
Message deletions
User joins
User leaves
Event Handler Best Practices
✅ Always Do
- • Wrap all handlers in try-catch blocks
- • Log important events for debugging
- • Validate event data before processing
- • Use specific handlers for specific purposes
❌ Never Do
- • Let handlers crash without error handling
- • Process bot's own messages (creates infinite loops)
- • Block handlers with long-running operations
- • Ignore event validation
- • Mix different event types in one handler
Message Event Handling
Master the most important event handler - processing all incoming messages with proper filtering and routing.
Step 1: Basic Message Handler Structure
Every message handler starts with this foundation. The critical first step is filtering out your bot's own messages to prevent infinite loops.
bot.onMessage(async (handler, event) => {
try {
const { message, userId, channelId, spaceId, eventId, isDm, isGdm } = event
// Log message for debugging (optional in production)
console.log(`💬 Message from \${userId.slice(0, 8)}...: \${message.substring(0, 50)}...\`),
// Handle different message types
if (isDm) {
// Direct message logic
await handleDirectMessage(handler, event)
} else if (isGdm) {
// Group DM logic
await handleGroupDM(handler, event)
} else {
// Public channel message logic
await handleChannelMessage(handler, event)
}
// Track message statistics (we'll implement this in Module 5)
await trackMessage(userId, spaceId)
} catch (error) {
console.error('Message handler error:', error)
// NEVER let handlers crash - always catch errors
}
})
// Handle direct messages
// NOTE: Bots can RECEIVE DMs but CANNOT send DM responses
// They can only send messages to channels
async function handleDirectMessage(handler: any, event: any) {
const { message, userId, channelId } = event
// Log the DM for monitoring/analytics
console.log(`📱 DM from \${userId.slice(0, 8)}...: \${message}\`),
// ⚠️ Bots cannot respond to DMs directly
// If you need to respond, direct the user to a channel
// Or use this for logging/notifications only
}
// Handle group DMs
async function handleGroupDM(handler: any, event: any) {
const { message, userId, channelId } = event
console.log(\`👥 GDM message from \${userId.slice(0, 8)}...: \${message}\`),
if (message.toLowerCase().includes('help')) {
await handler.sendMessage(channelId, `🤖 **Bot Help (Group DM)**\n\nI have limited functionality in group DMs.\nFor full features, use me in community channels!\n\n• /help - Show available commands\n• /ping - Check bot status\`),
}
}
// Handle public channel messages
async function handleChannelMessage(handler: any, event: any) {
const { message, userId, channelId, spaceId } = event
const lowerMessage = message.toLowerCase()
if (lowerMessage.includes('gm') || lowerMessage.includes('good morning')) {
await handler.sendMessage(channelId, `GM <@\${userId}>! ☀️\`),
return
}
if (lowerMessage.includes('gn') || lowerMessage.includes('good night')) {
await handler.sendMessage(channelId, `Good night <@\${userId}>! 🌙\`),
return
}
if (lowerMessage.match(/(hello|hi|hey)/)) {
await handler.sendMessage(channelId, `Hello <@\${userId}>! 👋\`),
return
}
if (lowerMessage.includes('wagmi')) {
await handler.sendReaction(channelId, event.eventId, '🚀')
return
}
if (lowerMessage.includes('moon')) {
await handler.sendReaction(channelId, event.eventId, '🌙')
return
}
if (lowerMessage.includes('bot help') || lowerMessage.includes('!help')) {
await handler.sendMessage(channelId, `💡 **Tip:** Use `/help` for a better experience!\n\nSlash commands provide a much better interface than text commands.\nJust type `/` to see all available commands.\`),
}
}Step 2: Understanding Message Context
Bots operate within spaces and channels. Messages can be regular channel messages or thread replies - route them appropriately based on context.
bot.onMessage(async (handler, event) => {
const { message, userId, channelId, isThread } = event
// Route based on message context
if (isThread) {
// Thread reply - focused discussion
await handler.sendMessage(channelId, `Thread reply from <@${userId}>: "${message}"`)
} else {
// Regular channel message - most common
await handler.sendMessage(channelId, `Hey <@${userId}>, I saw your message in the channel!`)
}
// You can also check the channel type
const channelInfo = await handler.getChannel(channelId)
console.log(`Message in channel: ${channelInfo.name}`)
})Channel Messages
Messages sent in space channels - the primary way users interact with bots
Thread Replies
Messages within a thread - use isThread to detect
Step 3: Responding to Patterns
Detect keywords and patterns in messages to trigger specific responses. Use toLowerCase() for case-insensitive matching.
bot.onMessage(async (handler, event) => {
const { message, userId, channelId } = event
if (userId === bot.botId) return
const lowerMessage = message.toLowerCase()
// Greeting detection
if (lowerMessage.includes('gm') || lowerMessage.includes('good morning')) {
await handler.sendMessage(channelId, `GM <@${userId}>! ☀️`)
return
}
// Help request
if (lowerMessage.includes('help')) {
await handler.sendMessage(channelId, `
💡 **How I can help:**
• Use `/help` for commands
• Ask questions anytime
• Tag me with @bot
`)
return
}
// Emoji reactions for keywords
if (lowerMessage.includes('wagmi')) {
await handler.sendReaction(channelId, event.eventId, '🚀')
}
})Pro Tip: Early Returns
Use return after handling a message to prevent multiple responses. This keeps your bot's behavior predictable and avoids spam.
Practice: Greeting Auto-Responder
Update the template so your bot responds to wagmi with WAGMI 🚀 and returns immediately. Use the validator to confirm you covered all requirements.
Step 4: Performance & Best Practices
Message handlers fire for every incoming message. Keep them fast and efficient.
✅ Do This
- • Filter bot messages first
- • Use early returns
- • Keep logic simple and fast
- • Log important events
- • Always use try-catch
❌ Avoid This
- • Processing bot's own messages
- • Heavy database queries
- • Blocking operations
- • Multiple responses per message
- • Letting handlers crash
Reaction Interactions & Voting Systems
Build interactive features using emoji reactions for voting, verification, and community engagement.
Step 1: Basic Reaction Handler
Reactions are powerful for building interactive features. Start with the basic handler structure to detect and respond to emoji reactions.
bot.onReaction(async (handler, event) => {
try {
const { reaction, refEventId, userId, channelId, spaceId } = event
console.log(\`👍 Reaction: \${reaction} from \${userId.slice(0, 8)}... on message \${refEventId.slice(0, 8)}...\`),
if (reaction === 'white_check_mark') {
await handleVerificationReaction(handler, event)
return
}
if (reaction === '👍' || reaction === '👎') {
await handleVotingReaction(handler, event)
return
}
if (/^[1-9]️⃣$/.test(reaction)) {
await handlePollReaction(handler, event)
return
}
switch (reaction) {
case '❤️':
await handleLoveReaction(handler, event)
break
case '🔥':
await handleFireReaction(handler, event)
break
case '💯':
await handleHundredReaction(handler, event)
break
case '🎉':
await handleCelebrationReaction(handler, event)
break
default:
await trackReaction(userId, spaceId, reaction)
}
} catch (error) {
console.error('Reaction handler error:', error)
}
})
async function handleVerificationReaction(handler: any, event: any) {
const { refEventId, userId, channelId, spaceId } = event
const isVerificationMessage = await isWelcomeMessage(refEventId, userId, spaceId)
if (isVerificationMessage) {
await verifyUser(userId, spaceId)
await handler.sendMessage(channelId, `✅ <@\${userId}> verified successfully! Welcome to the community!\`),
}
}
async function handleVotingReaction(handler: any, event: any) {
const { reaction, refEventId, userId, channelId, spaceId } = event
await storeVote(refEventId, userId, reaction, spaceId)
const votes = await getVoteCounts(refEventId)
if (votes.total > 0 && votes.total % 5 === 0) {
await handler.sendMessage(channelId, `📊 **Vote Update**\n\n👍 Upvotes: \${votes.upvotes}\n👎 Downvotes: \${votes.downvotes}\n📈 Total: \${votes.total}\`),
}
}
async function handlePollReaction(handler: any, event: any) {
const { reaction, refEventId, userId, channelId, spaceId } = event
const optionNumber = parseInt(reaction.charAt(0))
await storePollVote(refEventId, userId, optionNumber, spaceId)
console.log(\`📊 Poll vote: Option \${optionNumber} by \${userId.slice(0, 8)}...\`),
}
async function handleLoveReaction(handler: any, event: any) {
const { userId, channelId } = event
const loveResponses = [
`❤️ <@${userId}> spread some love!`,
`💕 Love is in the air thanks to <@${userId}>!`,
`🥰 <@${userId}> made someone's day!`,
`💖 Feeling the love from <@${userId}>!`
]
const randomResponse = loveResponses[Math.floor(Math.random() * loveResponses.length)]
if (Math.random() < 0.2) {
await handler.sendMessage(channelId, randomResponse)
}
}
async function handleFireReaction(handler: any, event: any) {
const { userId, channelId, refEventId } = event
const fireEmojis = ['🔥', '💥', '⚡', '🌟']
const randomFire = fireEmojis[Math.floor(Math.random() * fireEmojis.length)]
if (Math.random() < 0.3) {
await handler.sendReaction(channelId, refEventId, randomFire)
}
}
async function handleHundredReaction(handler: any, event: any) {
const { userId, channelId } = event
const celebrations = [
`💯 Perfect score from <@${userId}>!`,
`🎯 <@${userId}> thinks this is 100% amazing!`,
`⭐ <@${userId}> gives this a perfect rating!`
]
const randomCelebration = celebrations[Math.floor(Math.random() * celebrations.length)]
if (Math.random() < 0.25) {
await handler.sendMessage(channelId, randomCelebration)
}
}💡 Reaction Event Properties
- • reaction: The emoji that was added (e.g., "👍", "🔥")
- • messageId: ID of the message that was reacted to
- • userId: Who added the reaction
- • channelId: Where the reaction happened
Step 2: Building Voting Systems
Use reactions to create simple voting mechanisms. Track thumbs up/down or use number emojis for polls.
bot.onReaction(async (handler, event) => {
const { reaction, userId, channelId, refEventId } = event
// Simple yes/no voting
if (reaction === '👍' || reaction === '👎') {
const message = await handler.getMessage(channelId, refEventId)
const upvotes = message.reactions['👍'] || 0
const downvotes = message.reactions['👎'] || 0
console.log(`Vote tally - Yes: ${upvotes}, No: ${downvotes}`)
// Announce result at 10 total votes
if (upvotes + downvotes === 10) {
const result = upvotes > downvotes ? 'passed' : 'failed'
await handler.sendMessage(channelId, `📊 Vote ${result}! (${upvotes} yes, ${downvotes} no)`)
}
}
})🎯 Common Use Cases
- • Verification: ✅ for approvals
- • Voting: 👍👎 for decisions
- • Polls: 1️⃣2️⃣3️⃣ for choices
- • Engagement: ❤️🔥 for hype
⚡ Best Practices
- • Use probability for spam prevention
- • Add delays between reactions
- • Track statistics for insights
- • Validate context before acting
Practice: Reaction Milestone Tracker
Detect when a message gets 10 celebration reactions and send a milestone message.
Welcoming New Members
Create a warm, welcoming experience for new members joining your community channels.
Step 1: Basic Channel Join Handler
Detect when users join a channel and send them a friendly welcome message. This is perfect for onboarding new community members.
bot.onChannelJoin(async (handler, event) => {
try {
const { userId, channelId, spaceId } = event
// Skip when bot joins channels
if (userId === bot.botId) {
console.log('Bot joined a channel')
return
}
// Welcome the new member
await handler.sendMessage(
channelId,
`👋 Welcome <@${userId}>! We're excited to have you here!`
)
} catch (error) {
console.error('Channel join handler error:', error)
}
})💡 Channel Join Event
The onChannelJoin event fires whenever a user enters a channel. Always filter out the bot's own joins to avoid unnecessary messages.
Step 2: Personalized Welcome Messages
Create rich welcome messages with helpful information, guidelines, and next steps for new members.
import { isDefaultChannelId } from '@towns-protocol/sdk'
bot.onChannelJoin(async (handler, event) => {
const { userId, channelId, spaceId } = event
// Check if this is the main space join (not just a channel join)
if (isDefaultChannelId(channelId, spaceId)) {
// Send a warm, informative welcome message
await handler.sendMessage(channelId, `
🎉 **Welcome <@${userId}>!**
We're excited to have you in our community! Here's how to get started:
📋 **Getting Started:**
• Explore our channels and join conversations
• Use `/help` to see available commands
• Check pinned messages for important info
• Introduce yourself when you're ready!
💡 **Quick Tips:**
• Be respectful and kind to all members
• Ask questions - we're here to help!
• Have fun and engage with the community
Welcome aboard! 🚀
`)
}
})🎯 isDefaultChannelId
Use isDefaultChannelId(channelId, spaceId) to detect when someone joins the entire space, not just an individual channel. This prevents spam when users navigate between channels.
✨ Welcome Best Practices
- • Keep messages friendly and concise
- • Include helpful next steps
- • Link to community guidelines
- • Make new members feel valued
Practice: Welcome Message
Create a welcome message for new members joining your space. Use isDefaultChannelId to detect space joins.
Thread Conversations & Replies
Handle threaded conversations where users reply to messages, creating focused discussions.
Step 1: Detecting Thread Messages
Threads allow focused discussions. Detect when messages are sent in threads versus regular channels.
bot.onMessage(async (handler, event) => {
const { message, userId, channelId, isThread, threadId } = event
if (isThread) {
// This is a thread reply
console.log(`🧵 Thread message in thread ${threadId}`)
await handler.sendMessage(
channelId,
`Got your thread message: "${message}"`,
{ threadId } // Reply in the same thread
)
} else {
// Regular channel message
await handler.sendMessage(channelId, `Regular message received!`)
}
})💡 Thread Context
Use isThread to detect thread messages. When replying to threads, always pass { threadId } to keep the conversation in context.
Step 2: Responding in Threads
Keep conversations organized by replying within threads. This is great for support, Q&A, and focused discussions.
bot.onMessage(async (handler, event) => {
const { message, userId, channelId, isThread, threadId } = event
// Handle support requests in threads
if (isThread && message.toLowerCase().includes('help')) {
await handler.sendMessage(
channelId,
`
🛠️ **Support Request Received**
Hi <@${userId}>! I'm here to help. Here's what I can assist with:
• Technical questions
• Account issues
• Feature requests
• General guidance
Please describe your issue and I'll do my best to help!
`,
{ threadId }
)
}
})🎯 Thread Use Cases
- • Support: Help requests in threads
- • Q&A: Answer questions contextually
- • Discussions: Focused conversations
- • Moderation: Handle reports privately
✨ Thread Best Practices
- • Always pass
{ threadId } - • Keep replies focused and relevant
- • Use threads for longer conversations
- • Avoid spamming thread participants
Practice: Thread Bug Reporter
When someone mentions "bug" in a thread, reply with helpful bug report instructions. Remember to pass { threadId } to keep it in the thread.
Combining Event Handlers
Build powerful bot experiences by combining multiple event handlers together.
Real-World Bot Patterns
Here are practical examples of how to combine the event handlers you've learned to create useful bots.
🎮 Quest Companion
- • Use
bot.onMessage()to track quest keywords - • Store progress in a database (Module 5)
- • Reward users with reactions when they complete quests
- • Send congratulations messages in channels
🛡️ Moderation Guardian
- • Filter messages for banned phrases in
onMessage - • Use
bot.onRedaction()to audit message deletions - • Send warnings in channel when rules are broken
- • Log violations to admin channels
💬 Support Copilot
- • Respond to help requests in threads
- • Escalate complex issues to admin channels
- • Track support metrics per handler
- • Provide FAQ answers automatically
📊 Analytics Broadcaster
- • Track reactions and votes with
onReaction - • Count new members with
onChannelJoin - • Send daily summaries to admin channels
- • Create engagement leaderboards
💡 Combining Handlers
The key to powerful bots is combining handlers strategically. For example, a Quest Bot uses onMessage to detect quest keywords, onReaction to confirm completion, and onChannelJoin to welcome new questers. Each handler does one thing well, and together they create a complete experience.
Error Handling & Debugging
Catch and log errors properly to make debugging easier and keep your bot running smoothly.
Centralized Error Logging
Create a logging helper to consistently capture errors with useful context across all your event handlers.
const handlerLogger = (scope: string) => ({
info: (...args: any[]) => console.log(`[${scope}]`, ...args),
warn: (...args: any[]) => console.warn(`[${scope}]`, ...args),
error: (...args: any[]) => console.error(`[${scope}]`, ...args),
})
const messageLogger = handlerLogger('MESSAGE')
bot.onMessage(async (handler, event) => {
try {
// Your message logic here
await handler.sendMessage(event.channelId, 'Hello!')
} catch (error) {
messageLogger.error('Failed handling message', error, {
spaceId: event.spaceId,
channelId: event.channelId,
userId: event.userId,
eventId: event.eventId,
})
}
})Common Mistakes
- •Missing await: Forgetting
awaiton async functions - •Silent failures: Catching errors without logging them
- •Blocking operations: Heavy database work slowing down handlers
Best Practices
- •Always use try-catch: Wrap handler logic in error handling
- •Log context: Include spaceId, channelId, userId, eventId
- •Use scoped loggers: Different loggers for different handlers
- •Keep handlers fast: Delegate heavy work to background jobs
Module 3 Complete!
Outstanding work! You've mastered comprehensive event handling in Towns Protocol. Your bot can now respond intelligently to all types of user interactions, from messages and reactions to joins and complex event patterns. You've built a sophisticated event-driven system with robust error handling.