Оформление
Эхо-бот целиком
Готовый бот, который отвечает на каждое сообщение в комнате. Два варианта — выберите по тому, как хотите получать события:
- через WebSocket — проще, работает с любой машины, с него и стоит начать;
- через вебхук — если у бота есть публичный https-адрес.
Перед запуском: создайте приложение и установите его на канал.
Вариант: WebSocket
Нужен Node 18+ и два пакета:
bash
npm i centrifuge wsjs
// echo-bot.mjs
// Запуск: BOT_TOKEN=emb_ваш_ключ node echo-bot.mjs
import { Centrifuge } from 'centrifuge'
import WebSocket from 'ws'
const API = 'https://emotify.ru/api/v1'
const TOKEN = process.env.BOT_TOKEN
if (!TOKEN) {
console.error('Задайте переменную окружения BOT_TOKEN')
process.exit(1)
}
// Отправка ответа в комнату
async function reply(channelId, roomId, text) {
const res = await fetch(`${API}/rooms/${roomId}/messages`, {
method: 'POST',
headers: { Authorization: `Bot ${TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ channelId, text })
})
if (!res.ok) {
// 403 — у роли бота нет права писать; 429 — превышена квота 20 сообщений/мин
console.error('Не удалось ответить:', res.status, await res.text())
}
}
// Подключение к событиям
async function connect() {
const res = await fetch(`${API}/events/token`, {
headers: { Authorization: `Bot ${TOKEN}` }
})
const { token, channel, url } = await res.json()
const client = new Centrifuge(url, { token, websocket: WebSocket })
client
.newSubscription(channel)
.on('publication', ({ data: event }) => {
if (event.event !== 'message.created') return
const { roomId, message } = event.data
// Свои сообщения боту не приходят, поэтому зацикливания не будет
reply(event.channelId, roomId, `Вы сказали: ${message.text}`).catch(console.error)
})
.subscribe()
client.connect()
console.log('Эхо-бот подключён и слушает события')
}
connect().catch(console.error)
// Токен подписки живёт 6 часов — обновляемся заранее
setInterval(() => connect().catch(console.error), 5 * 60 * 60 * 1000)Запустите и напишите что-нибудь в комнате канала, где установлен бот, — он ответит.
Вариант: вебхук
Нужен Node 18+, зависимостей нет. И нужен публичный https-адрес — для отладки на своей машине поднимите туннель:
bash
ngrok http 8787Шаг 1. Скажите Emotify, куда слать события, и сохраните secret из ответа:
bash
curl -X PUT https://emotify.ru/api/v1/webhook \
-H "Authorization: Bot $BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://ваш-адрес/emotify"}'Шаг 2. Запустите приёмник:
js
// echo-bot-webhook.mjs
// Запуск: BOT_TOKEN=emb_… WEBHOOK_SECRET=… node echo-bot-webhook.mjs
import { createHmac, timingSafeEqual } from 'node:crypto'
import { createServer } from 'node:http'
const API = 'https://emotify.ru/api/v1'
const TOKEN = process.env.BOT_TOKEN
const SECRET = process.env.WEBHOOK_SECRET
const PORT = Number(process.env.PORT ?? 8787)
if (!TOKEN || !SECRET) {
console.error('Задайте переменные BOT_TOKEN и WEBHOOK_SECRET')
process.exit(1)
}
// Проверка подписи — обязательна: адрес публичный, и без неё он примет
// что угодно от кого угодно. HMAC считается от СЫРОГО тела запроса.
function isSignatureValid(rawBody, header) {
const expected = `sha256=${createHmac('sha256', SECRET).update(rawBody).digest('hex')}`
const a = Buffer.from(expected)
const b = Buffer.from(header ?? '')
return a.length === b.length && timingSafeEqual(a, b)
}
async function reply(channelId, roomId, text) {
const res = await fetch(`${API}/rooms/${roomId}/messages`, {
method: 'POST',
headers: { Authorization: `Bot ${TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ channelId, text })
})
if (!res.ok) {
console.error('Не удалось ответить:', res.status, await res.text())
}
}
function handleEvent(event) {
if (event.event !== 'message.created') return
const { roomId, message } = event.data
reply(event.channelId, roomId, `Вы сказали: ${message.text}`).catch(console.error)
}
createServer((req, res) => {
if (req.method !== 'POST') {
res.writeHead(405).end()
return
}
const chunks = []
req.on('data', (chunk) => chunks.push(chunk))
req.on('end', () => {
const rawBody = Buffer.concat(chunks)
if (!isSignatureValid(rawBody, req.headers['x-emotify-signature'])) {
res.writeHead(401).end()
return
}
// Отвечаем сразу, обрабатываем после: медленный ответ Emotify считает
// неудачей и повторяет доставку, а серия неудач отключает вебхук
res.writeHead(204).end()
try {
handleEvent(JSON.parse(rawBody.toString('utf8')))
} catch (error) {
console.error('Событие не разобрано:', error)
}
})
}).listen(PORT, () => console.log(`Эхо-бот слушает порт ${PORT}`))Идеи для развития
Команды. Префикс на платформе один —
/. Реагируйте только на сообщения, которые с него начинаются:jsif (!message.text.startsWith('/')) return const [command, ...args] = message.text.slice(1).split(' ')⚠️ И объявите список через
PUT /commands— иначе портал о ваших командах не знает и людям их не покажет: ни кнопкой у поля ввода, ни подсказкой по/. Сам эхо-бот команд не имеет — он отвечает на обычную речь, и объявлять ему нечего.Реакция вместо ответа — иногда уместнее молча поставить 👍:
jsawait fetch(`${API}/rooms/${roomId}/messages/${message.id}/reactions`, { method: 'POST', headers: { Authorization: `Bot ${TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ channelId, emoji: '👍' }) })Голос.
POST /rooms/{id}/voiceдаёт токен LiveKit — дальше публикуйте звук через@livekit/rtc-node.Сайт с входом через Emotify — страница OAuth.