> ## Documentation Index
> Fetch the complete documentation index at: https://docs.towns.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Events

This guide covers what you need to know about events and how to handle them in your bot. You'll learn about:

* [Event anatomy](#event-anatomy)
* [Event listeners](#event-listeners)
* [Sending events](#sending-events)
* [Permissions](#permissions)
* [Moderation](#moderation)
* [Snapshot data](#snapshot-data)

## Event Anatomy

All events share a common `BasePayload` structure:

* `userId`: The user who triggered the event.
* `spaceId`: The space that the event occurred in. **Note:** This is `null` for Direct Message (DM) channels, as these channels don't belong to a space.
* `channelId`: The channel that the event occurred in.
* `eventId`: The unique event ID.
* `createdAt`: The date and time the event occurred.
* `isDm`: A flag indicating if the event occurred in a DM channel.

Different event types extend this base with additional fields.

<Note>
  When handling events in DM channels, `spaceId` will be `null`. Use the `isDm` flag to check if an event is from a DM channel.
</Note>

## Message Forwarding Modes

Bots have three message forwarding modes that control which events they receive. You configure this in your bot settings at [app.towns.com/developer](https://app.towns.com/developer).

### Available Modes

**All Messages**

* Bot receives every event in channels it's in
* All event handlers are available
* Use for: AI agents, moderation bots, analytics, or when you need `onTip`, `onChannelJoin`, `onChannelLeave`, or `onEventRevoke` handlers
* ⚠️ Warning: High volume in active channels

**Mentions, Commands, Replies & Reactions** (Default)

* Bot receives: messages where it's mentioned, replies to its messages, reactions, and slash commands
* Event handlers available: `onMessage` (filtered), `onSlashCommand`, `onReaction`, `onMessageEdit`, `onRedaction`
* Event handlers NOT available: `onTip`, `onChannelJoin`, `onChannelLeave`, `onEventRevoke`
* Use for: Most interactive bots that respond to direct user interaction

**No Messages**

* Bot receives no message events
* Use for: External-only bots triggered by [custom webhooks](/build/bots/external-interactions) or timers

## Event Listeners

All event listeners are asynchronous functions that receive a `handler` and an `event` object.

The `handler` is the bot instance and the `event` contains the `BasePayload` and additional fields specific to the event type.

### onMessage

Fires when a message is sent in a channel the bot is in (subject to forwarding settings).

* `message`: The decrypted message text.
* `replyId`: The `eventId` of the message being replied to (if reply).
* `threadId`: The `eventId` of the thread's first message (if in thread).
* `mentions`: List of users mentioned in the message. Each mention contains the user's address and display name.
* `isMentioned`: True if the bot was @mentioned.

### onSlashCommand

Fires when a user invokes one of your bot's slash commands. For more information about slash commands, see the [Slash Commands guide](/build/bots/slash-commands).

* `command`: The name of the command invoked by the user.
* `args`: The arguments passed to the command.
* `mentions`: List of users mentioned in the command.
* `replyId`: The message being replied to (if any).
* `threadId`: The thread context (if any).

### onReaction

<Warning>
  You only get the `reaction` and `messageId` - not the message content! Store messages externally if you need context.
</Warning>

* `reaction`: The reaction emoji.
* `messageId`: The `eventId` of the message being reacted to.

### onMessageEdit

Fires when a message is edited.

* `message`: The new message content.
* `refEventId`: The `eventId` of the message being edited.
* `replyId`: If the edited message was a reply.
* `threadId`: If the edited message was in a thread.
* `mentions`: Array of mentioned users in the edited message.
* `isMentioned`: True if the bot is mentioned in the edited message.

### onRedaction

Fires when a message is deleted by a user.

* `refEventId`: The `eventId` of the message being deleted.

### onEventRevoke

Fires when a message is revoked, either by the user that sent it or by an admin with the `Redact` permission.

<Note>
  This event handler only works in "All Messages" mode. See [Message Forwarding Modes](#message-forwarding-modes).
</Note>

* `refEventId`: The `eventId` of the message being revoked.

### onTip

Fires when a user sends a cryptocurrency tip on a message.

<Note>
  This event handler only works in "All Messages" mode. See [Message Forwarding Modes](#message-forwarding-modes).
</Note>

* `messageId`: The `eventId` of the message that received the tip.
* `senderAddress`: The address of the user who sent the tip.
* `receiverAddress`: The address of the user who received the tip.
* `amount`: The amount of the tip.
* `currency`: The currency of the tip.

It can fire for any tip, not just tips to the bot.

### onChannelJoin

Fires when users join a channel.

<Note>
  This event handler only works in "All Messages" mode. See [Message Forwarding Modes](#message-forwarding-modes).
</Note>

* `userId`: The user who joined or left the channel.
* `channelId`: The channel that the user joined or left.

### onChannelLeave

Fires when users leave a channel.

<Note>
  This event handler only works in "All Messages" mode. See [Message Forwarding Modes](#message-forwarding-modes).
</Note>

* `userId`: The user who left the channel.
* `channelId`: The channel that the user left.

## Sending events

You can use bot actions to send events to Towns Network.

### sendMessage

Send a message to a channel.

```ts theme={null}
const { eventId } = await bot.sendMessage(channelId, message, opts)
```

* `channelId`: The channel to send the message to.
* `message`: The message to send.
* `opts`: The options for the message.
  * `threadId`: The thread to send the message to.
  * `replyId`: The message to reply to.
  * `mentions`: The users to mention in the message. Accepts a list of `userId` and `displayName`.
  * `attachments`: The attachments to send with the message.
  * `ephemeral`: Whether the message should be ephemeral (wont be stored in the channel history)

Returns the `eventId` of the sent message.

#### Sending attachments

The bot framework supports four types of attachments with automatic validation and encryption.

##### Image Attachments from URLs

Send images by URL with automatic validation and dimension detection:

```ts theme={null}
await bot.sendMessage(channelId, message, {
  attachments: [
    {
       type: 'image', 
       url: 'https://example.com/image.png', 
       alt: 'Example image'
    } 
  ]
})
```

##### Miniapp Attachments

You can use it to send a miniapp. It will be rendered in the message as a miniapp.

```ts theme={null}
await bot.sendMessage(channelId, "Check this out!", {
  attachments: [{
    type: 'miniapp',
    url: 'https://example.com/miniapp'
  }]
})
```

##### Link Attachments

You can use it to send any link.

```ts theme={null}
await bot.sendMessage(channelId, "Check this out!", {
  attachments: [{ type: 'link', url: 'https://docs.towns.com' }]
})
```

##### Chunked Media Attachments

Send binary data (videos, screenshots, generated images). Framework automatically encrypts and chunks (1.2MB per chunk).

**Video:**

```ts theme={null}
import { readFileSync } from 'node:fs'

bot.onSlashCommand("video", async (handler, { channelId }) => {
  const videoData = readFileSync('./video.mp4')

  await handler.sendMessage(channelId, "Check this out!", {
    attachments: [{
      type: 'chunked',
      data: videoData,        // Uint8Array
      filename: 'video.mp4',
      mimetype: 'video/mp4',  // Required for Uint8Array
      width: 1920,            // Optional
      height: 1080
    }]
  })
})
```

**Canvas:**

```ts theme={null}
import { createCanvas } from '@napi-rs/canvas'

bot.onSlashCommand("chart", async (handler, { channelId, args }) => {
  const value = parseInt(args[0]) || 50

  const canvas = createCanvas(400, 300)
  const ctx = canvas.getContext('2d')
  ctx.fillStyle = '#2c3e50'
  ctx.fillRect(0, 0, 400, 300)
  ctx.fillStyle = '#3498db'
  ctx.fillRect(50, 300 - value * 2, 300, value * 2)

  const blob = await canvas.encode('png')

  await handler.sendMessage(channelId, "Your chart:", {
    attachments: [{
      type: 'chunked',
      data: blob,             // Blob (mimetype automatic)
      filename: 'chart.png'
    }]
  })
})
```

**Screenshot:**

```ts theme={null}
bot.onSlashCommand("screenshot", async (handler, { channelId }) => {
  const screenshotBuffer = await captureScreen()

  await handler.sendMessage(channelId, "Screenshot:", {
    attachments: [{
      type: 'chunked',
      data: screenshotBuffer, // Uint8Array
      filename: 'screen.png',
      mimetype: 'image/png'   // Required for Uint8Array
    }]
  })
})
```

<Note>
  * Uint8Array requires `mimetype` parameter
  * Blob has automatic mimetype
  * Image dimensions auto-detected for `image/*` mimetypes
</Note>

### sendReaction

Send a reaction to a message.

```ts theme={null}
await bot.sendReaction(channelId, eventId, reaction)
```

* `channelId`: The channel to send the reaction to.
* `eventId`: The `eventId` of the message to react to.
* `reaction`: The reaction to send.

Returns the `eventId` of the sent reaction.

Example:

```ts theme={null}
bot.onMessage(async (handler, { channelId, eventId, message }) => {
  if (message.includes('Towns')) {
    await handler.sendReaction(channelId, eventId, '🔥')
  }
})
```

### editMessage

Edit a message.

```ts theme={null}
const { eventId } = await bot.editMessage(channelId, eventId, message, opts)
```

* `channelId`: The channel to edit the message in.
* `eventId`: The `eventId` of the message to edit.
* `message`: The new message content.
* `opts?`: Optional options for the edit.
  * `threadId`: Change the thread of the message.
  * `replyId`: Change the reply of the message.
  * `mentions`: Change the mentions of the message. Accepts a list of `userId` and `displayName`.
  * `attachments`: Change the attachments of the message.

Returns the `eventId` of the edited message.

### removeEvent

Remove any event that was sent by the bot.

```ts theme={null}
await bot.removeEvent(channelId, eventId)
```

* `channelId`: The channel to remove the event from.
* `eventId`: The `eventId` of the event to remove.

Returns the `eventId` of the removed event.

### adminRemoveEvent

Remove any event from a channel. Requires the `Redact` permission.

```ts theme={null}
await bot.adminRemoveEvent(channelId, eventId)
```

* `channelId`: The channel to remove the event from.
* `eventId`: The `eventId` of the event to remove.

Returns the `eventId` of the redaction event.

### sendTip

Send a cryptocurrency tip to a user on a message.

<Note>
  Your **gas wallet** needs Base ETH to pay for gas fees, and your **bot treasury wallet** needs to hold the ETH that will be sent as tips. See [Understanding Your Bot's Wallet Architecture](/build/bots/getting-started#understanding-your-bots-wallet-architecture) for details.
</Note>

```ts theme={null}
import { parseEther } from 'viem'

await bot.sendTip({
  userId,
  messageId,
  channelId,
  amount: parseEther('0.001')
})
```

* `userId`: The user to tip
* `messageId`: The `eventId` of the message to tip
* `channelId`: The channel containing the message
* `amount`: The tip amount in wei

Returns the `eventId` of the tip event.

Example - tip users for helpful messages:

```ts theme={null}
bot.onMessage(async (handler, { channelId, eventId, userId, message }) => {
  const shouldTip = checkIfDeserveTip(message)
  if (shouldTip) {
    await handler.sendTip({
      userId,
      amount: parseEther('0.001'),
      messageId: eventId,
      channelId
    })
  }
})
```

## Permissions

Check user permissions before executing operations.

### hasAdminPermission

Check if user is a space admin (has `ModifyBanning` permission).

```ts theme={null}
const isAdmin = await handler.hasAdminPermission(userId, spaceId)
```

### checkPermission

Check specific permission for a user.

```ts theme={null}
import { Permission } from '@towns-protocol/web3'

const canRedact = await handler.checkPermission(
  channelId,
  userId,
  Permission.Redact
)
```

**Available permissions:**

* `Permission.Read`
* `Permission.Write`
* `Permission.Redact`
* `Permission.ModifyBanning`
* `Permission.PinMessage`
* `Permission.AddRemoveChannels`
* `Permission.ModifySpaceSettings`

**Example:**

```ts theme={null}
bot.onSlashCommand('ban', async (handler, event) => {
  // Check if user has admin permission
  if (!await handler.hasAdminPermission(event.userId, event.spaceId)) {
    await handler.sendMessage(event.channelId, 'No permission')
    return
  }
  
  const userToBan = event.mentions[0]?.userId
  if (userToBan) {
    try {
      await handler.ban(userToBan, event.spaceId)
      await handler.sendMessage(event.channelId, 'User banned')
    } catch (error) {
      await handler.sendMessage(event.channelId, 'Failed to ban user')
    }
  }
})
```

## Moderation

Ban and unban users.

<Warning>
  Your **bot** must have `ModifyBanning` permission in the space. Your **gas wallet** needs Base ETH to pay for gas fees.
</Warning>

### ban

```ts theme={null}
await handler.ban(userId, spaceId)
```

### unban

```ts theme={null}
await handler.unban(userId, spaceId)
```

## Snapshot Data

Bots have access to snapshot data for reading channel settings and membership information.

<Warning>
  Snapshot data may not reflect the latest state. It's cached and updated periodically.
</Warning>

### getChannelInception

Get channel settings and inception data.

```ts theme={null}
const channelData = await bot.snapshot.getChannelInception(streamId)
```

### getUserMemberships

Get a user's space memberships.

```ts theme={null}
const memberships = await bot.snapshot.getUserMemberships(userId)
```

### getSpaceMemberships

Get all members of a space.

```ts theme={null}
const members = await bot.snapshot.getSpaceMemberships(spaceId)
```
