Module 3

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.

7 steps
Intermediate

Learning Objectives

By the end of this module, you will be able to:

Understand all available event handler types and their use cases
Implement comprehensive message event handling with filtering
Build interactive reaction-based features and voting systems
Create welcome systems for new members joining channels
Handle thread conversations and reply contexts
Combine multiple event handlers for advanced bot behaviors
Build robust error handling and logging for all event types
1

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

bot.onMessage()

All messages

bot.onSlashCommand()

Slash commands

bot.onReaction()

Emoji reactions

bot.onTip()

Tips

bot.onMessageEdit()

Message edits

bot.onRedaction()

Message deletions

bot.onChannelJoin()

User joins

bot.onChannelLeave()

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
2

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.

Basic Handler Setup
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.

Message Type Routing
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.

Pattern-Based Responses
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.

Autoresponder PlaygroundTypeScript
Fill in the code and run validation.
Validation Log
No logs yet. Run validation to see results.

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
3

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.

Basic Reaction Handler
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.

Simple Voting System
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.

Reaction Milestone PlaygroundTypeScript
Fill in the code and run validation.
Validation Log
No logs yet. Run validation to see results.
4

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.

Simple Welcome Handler
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.

Rich Welcome Message
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&apos;re excited to have you in our community! Here&apos;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&apos;re ready!

💡 **Quick Tips:**
• Be respectful and kind to all members
• Ask questions - we&apos;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.

Welcome System PlaygroundTypeScript
Update the code, then validate.
Validation Log
No logs yet. Run validation to see results.
5

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.

Basic Thread Handler
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.

Thread Support Bot
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.

Thread Support PlaygroundTypeScript
Update the code, then validate.
Validation Log
No logs yet. Run validation to see results.
6

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.

7

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.

Error Logger Helper
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 await on 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.

What You've Mastered:

All event handler types and patterns
Message processing with context awareness
Interactive reaction-based features
Welcome and user lifecycle systems
Thread conversation handling
Error handling and debugging

Ready For:

Admin permission systems
Message deletion and moderation
Database integration
Production deployment