bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 17쪽 중 1쪽
# Changelog
Source: https://docs.towns.com/build/bots/changelog
# Events
Source: https://docs.towns.com/build/bots/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)
```
# External Integrations
Source: https://docs.towns.com/build/bots/external-interactions
Towns bots are built on [Hono](https://hono.dev). While `/webhook` is reserved for Towns events, you can add custom routes for external webhooks, APIs, and scheduled tasks.
## Using Bot Methods Anywhere
Bot methods like `sendMessage()` can be called directly on the bot instance, outside of event handlers. This enables integration with external services.
```ts theme={null}
import { Hono } from 'hono'
import { makeTownsBot } from '@towns-protocol/bot'
const bot = await makeTownsBot(privateData, jwtSecret)
const { jwtMiddleware, handler } = bot.start()
const app = new Hono()
// Towns webhook (required)
app.post('/webhook', jwtMiddleware, handler)
// Custom API endpoint
app.post('/api/send', async (c) => {
const { channelId, message } = await c.req.json()
await bot.sendMessage(channelId, message)
return c.json({ ok: true })
})
export default app
```
## External Webhooks
Integrate with external services like GitHub, Linear, Stripe, or custom APIs.
```ts theme={null}
import { Hono } from 'hono'
import { makeTownsBot } from '@towns-protocol/bot'
const bot = await makeTownsBot(privateData, jwtSecret, { commands })
let notificationChannelId: string | null = null
// Configure channel for notifications
bot.onSlashCommand('setup', async (handler, event) => {
notificationChannelId = event.channelId
await handler.sendMessage(event.channelId, 'Notifications configured')
})
const { jwtMiddleware, handler } = bot.start()
const app = new Hono()
app.post('/webhook', jwtMiddleware, handler)
// External webhook endpoint
app.post('/external-webhook', async (c) => {
const payload = await c.req.json()
if (!notificationChannelId) {
return c.json({ error: 'No channel configured' }, 400)
}
// Send events to Towns channel
await bot.sendMessage(
notificationChannelId,
`Event received: ${payload.type}`
)
return c.json({ success: true })
})
export default app
```
You can use this pattern with any service that has webhook integration. Alchemy, GitHub, Linear, etc.
## Scheduled Tasks
Use intervals or cron jobs to send periodic updates.
```ts theme={null}
const bot = await makeTownsBot(privateData, jwtSecret)
const channels = new Set()
bot.onSlashCommand('alerts', async (handler, event) => {
channels.add(event.channelId)
await handler.sendMessage(event.channelId, 'Alerts enabled')
})
// Run every 5 minutes
setInterval(async () => {
const data = await fetch('https://api.example.com/data').then(r => r.json())
for (const channelId of channels) {
await bot.sendMessage(channelId, `Update: ${data.value}`)
}
}, 5 * 60 * 1000)
```
## Persistent Storage
<Warning>
In-memory storage (Map, Set) only works on always-on servers. Use a database for production.
</Warning>
Popular options:
* **SQLite/Turso** - Simple, serverless-compatible
* **Redis** - Fast caching and rate limiting
* **PostgreSQL** - Full-featured relational database
* **Supabase** - Managed PostgreSQL with APIs
## Next Steps
* Explore [onchain integrations](/build/bots/onchain-integrations)
* Learn about [slash commands](/build/bots/slash-commands)
* Read about [event handlers](/build/bots/events)
# Getting Started
Source: https://docs.towns.com/build/bots/getting-started
This guide will walk you through creating, developing, and deploying your first Towns bot. You'll learn how to:
1. [Create a bot in Towns](#create-a-bot)
2. [Set up your development environment](#develop-your-bot)
3. [Deploy to Render.com](#deploy-your-bot)
4. [Configure your bot in Towns](#configure-in-towns)
## Prerequisites
Before you begin, make sure you have:
* [Node.js v20+](https://nodejs.org/en/download) installed (download for your OS)
* [Bun](https://bun.sh/docs/installation) installed (download for your OS)
* A [GitHub account](https://github.com/signup) with SSH key configured (for pushing your bot code)
* [Create SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent)
* [Upload SSH key to GitHub](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account)
## Create a Bot
1. **Visit the Developer Portal**
Go to [app.towns.com/developer](https://app.towns.com/developer)
2. **Create New Bot**
Fill in the details and click "Create Bot".
3. **Save Your Credentials**
After creation, you'll receive two critical values:
* `APP_PRIVATE_DATA` - Your bot's private key and encryption device (base64 encoded string)
* `JWT_SECRET` - Used to verify webhook requests from Towns servers
<Note>
Store these securely - you'll need them for deployment.
</Note>
After following the steps above, you should be redirected to the bot settings page.
Follow the instructions below to deploy your bot and set your webhook url.
## Develop Your Bot
Now let's set up your bot development environment.
### Initialize Your Project
Create a new bot project using the Towns bot CLI:
```bash theme={null}
bunx towns-bot init my-bot
```
This creates a new directory with a basic bot template.
### Install Dependencies
```bash theme={null}
cd my-bot
bun install
```
### Add Bot Discovery Endpoint
Open `src/index.ts` and add this route after the `/webhook` route:
```ts theme={null}
// After your /webhook route
app.get('/.well-known/agent-metadata.json', async (c) => {
return c.json(await bot.getIdentityMetadata())
})
```
This endpoint is **required** for bot directories to discover and index your bot. After deploying your bot, go to the [developer dashboard](https://app.towns.com/developer/dashboard) and click the **Boost** button to submit your bot for indexing.
### Configure Environment
Copy the `.env.sample` file in your project root:
```bash theme={null}
cp .env.sample .env
```
### Push to GitHub
Create a [new repository on GitHub](https://github.com/new), follow their instructions, commit all your changes and push your bot code to it.
## Deploy Your Bot
We'll deploy to Render.com, which offers a generous free tier perfect for bot hosting.
1. **Sign up for Render**
Go to프로젝트로 돌아가기