Towns 백서

Towns

원문

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
프로젝트로 돌아가기