Vortex

Боты и APIv281

Сервис Vortex (https://vortex-voice.com) · Редакция от 04.09.2026

Бот в Vortex — это обычная учётная запись, которой управляет программа: скрипт подключается к серверу по тому же WebSocket-протоколу, что и браузерный клиент. Отдельной регистрации для ботов не нужно: при первом подключении сервер сам создаёт учётку и выдаёт токен, а дальше бот может читать команды в чатах, отвечать на сообщения и даже заходить в голосовые каналы со своим звуком.

На этой странице — пошаговое создание своего бота и краткий справочник Bot API.

1. Как создать своего бота

Всё, что нужно, — любой язык с WebSocket-библиотекой. Ниже примеры на Python, но протокол тривиален и на другом языке: подключились, обменялись JSON — бот работает.

  1. Придумайте боту постоянный id — строку вида bot-xxxx, которую вы генерируете один раз и больше не меняете. По нему сервер узнаёт бота при каждом входе.
  2. Подключитесь к wss://<ваш-сервер>/ws и первым делом отправьте сообщение hello (см. пример ниже).
  3. Сохраните токен из ответа welcome — это единственный ключ учётки бота. При следующих входах присылайте его в hello, иначе сервер отвергнет подключение.
  4. Пригласите бота на сервер: создайте ссылку-приглашение (настройки сервера → «Пригласить») и отправьте боту команду join_server с кодом из ссылки.
  5. Читайте и пишите сообщения в каналах сервера — бот получает их так же, как обычный клиент.

Минимальный рабочий бот

Эхо-бот, который повторяет всё написанное в канале. Зависимость одна: pip install websockets.

# echo_bot.py — запустите один раз, токен сохранится сам
import asyncio, json, pathlib, uuid, websockets

SERVER  = "wss://vortex-voice.com/ws"
ID_FILE = pathlib.Path("bot_identity.json")            # id и токен бота
NAME    = "🤖 Помощник"                                 # до 32 символов

data    = json.loads(ID_FILE.read_text()) if ID_FILE.exists() else {}
USER_ID = data.get("user_id") or ("bot-" + str(uuid.uuid4()))

async def main():
    async with websockets.connect(SERVER) as ws:
        await ws.send(json.dumps({
            "type": "hello",
            "user_id": USER_ID,                        # постоянный id бота
            "username": NAME,
            "kind": "bot",                             # честно представляемся ботом
            "avatar_color": "#7c5cff",
            "auth_token": data.get("auth_token", ""),  # пусто при первом входе
        }))
        welcome = json.loads(await ws.recv())          # первый ответ сервера
        if welcome.get("type") != "welcome":
            print("Не пустили:", welcome); return
        ID_FILE.write_text(json.dumps({                # id + токен — на диск
            "user_id": USER_ID,
            "auth_token": welcome["auth_token"],
        }))
        print("Бот в сети:", welcome["user"]["username"])

        async def heartbeat():                         # держим соединение живым
            while True:
                await asyncio.sleep(30)
                await ws.send('{"type":"heartbeat"}')

        asyncio.create_task(heartbeat())
        async for raw in ws:                           # простое эхо
            msg = json.loads(raw)
            if msg.get("type") == "chat_message" and msg.get("user_id") != USER_ID:
                await ws.send(json.dumps({
                    "type": "chat_message",
                    "channel_id": msg["channel_id"],
                    "content": "Ты написал: " + msg["content"],
                }))

asyncio.run(main())

Чтобы бот попал на сервер, отправьте ему приглашение — код возьмите из ссылки вида …?invite=a1b2c3&kind=server:

→ {"type": "join_server", "invite_code": "a1b2c3"}
← {"type": "server_joined", "server": {…}, "channels": […]}   // готово
Токен — единственный ключ бота. Храните bot_identity.json надёжно: если файл с токеном потерян, восстановить учётку нельзя (сервер не выдаёт токен повторно — иначе его мог бы перехватить первый встречный). Просто сгенерируйте новый user_id — бот начнёт с чистого листа.

2. Bot API: справочник

Протокол — тот же, что у браузерного клиента: JSON-сообщения по WebSocket. Подключение: wss://<ваш-сервер>/ws.

Подключение и авторизация

Первым сообщением бот отправляет hello. При самом первом входе сервер вернёт welcome со свежим auth_token — сохраните его и присылайте во всех следующих подключениях. Токен привязан к user_id: неверный или отсутствующий токен для существующей учётки — соединение закрывается с причиной auth_required.

// → первое сообщение после подключения
{
  "type": "hello",
  "user_id": "bot-6b944944…",        // стабильный id бота (генерируется один раз)
  "username": "🤖 Мой бот",           // до 32 символов, эмодзи можно
  "kind": "bot",                     // представьтесь ботом — это обязательно
  "avatar_color": "#7c5cff",
  "auth_token": ""                    // пусто при первом входе, дальше — из сохранённого
}

// ← ответ сервера
{ "type": "welcome", "auth_token": "…", "user": {…},
  "servers": […], "channels": […], "users": […] }

Другие причины закрытия: rate_limited (больше 40 hello в минуту с одного IP), banned, account_deleted. Пока соединение открыто, имя и цвет аватарки можно менять, просто переподключившись с новым username в hello. Метка kind:"bot" ставится один раз и персистится — сниматься переподключением без поля нельзя.

Сообщения

ТипНаправлениеНазначение
chat_message↕Сообщение в текстовый канал сервера: {"type":"chat_message","channel_id":"…","content":"текст"}. До 4000 символов; опционально reply_to_id для ответа. Входящие приходят только из каналов серверов, где бот участник.
conversation_message↕Личка и групповые чаты: {"type":"conversation_message","conversation_id":"…","content":"текст"}.
conversation_created←Кто-то открыл личку с ботом — удобно поздороваться и рассказать, что он умеет.
presence←Обновления списка пользователей: статусы, текущий голосовой канал.
heartbeat↕{"type":"heartbeat"} раз в ~30 секунд → в ответ heartbeat_ack. Любое другое исходящее сообщение тоже продлевает жизнь соединения.
join_server / join_private_channel→Вступить по приглашению: {"type":"join_server","invite_code":"…"}. Код — из ссылки ?invite=…&kind=server|channel.
server_joined / channel_created←Результат вступления (сервер + список каналов) и появление новых каналов.
error←Текст ошибки: неверный код приглашения, бан на сервере, «вы заглушены» и т.п.

Голосовые каналы и звук

В голосе бот — WebRTC-пир. Библиотека aiortc (Python) покрывает весь тракт. Каналы бывают двух типов — флаг is_sfu у канала подскажет, как подключаться; он приходит в списках каналов в welcome и server_joined.

Обычные (mesh) каналы — каждый участник соединяется с каждым напрямую. Бот отвечает на офферы участников и офферит сам тем, кто зашёл позже:

→ {"type":"voice_join","channel_id":"…"}      // войти в голосовой канал
← {"type":"voice_peer_join","peer_id":"…"}   // кто-то зашёл — офферим ему сами
← {"type":"webrtc_offer","from":"…","sdp":{…},"kind":"audio"}
→ {"type":"webrtc_answer","target":"…","sdp":{…},"kind":"audio"}
↕ {"type":"webrtc_ice", …}                    // ICE-кандидаты в обе стороны
→ {"type":"voice_leave"}                      // выйти

Mesh прост, но у него есть цена: бот кодирует отдельный поток для каждого слушателя и зависит от качества каждого P2P-пути. Для бота, которого слушают несколько человек, это заметная нагрузка, а при строгом NAT часть участников может вовсе не услышать бота.

SFU-каналы — звук идёт через сервер: у бота один-единственный пир (сам сервер), бот отправляет один поток, сервер раздаёт его всем слушателям. Один поток вместо N, никаких P2P-путей — там, где в mesh кто-то «не слышит бота» или звук лагает, SFU решает обе проблемы. Это тот же транспорт, что используют браузерные клиенты в SFU-каналах:

→ {"type":"voice_join","channel_id":"…"}      // войти в канал, как обычно
→ {"type":"sfu_offer","channel_id":"…",       // публикуем свой звук серверу
     "sdp":{…}}                               //   секция бота — sendonly,
                                              //   + 8 пустых recvonly-секций (слоты)
← {"type":"sfu_answer","sdp":{…},"tracks":{…}} // сервер принял поток
← {"type":"sfu_tracks","tracks":{…}}          // карта «слот → кто говорит»
← {"type":"sfu_server_offer","sdp":{…}}       // редкие пере-переговоры
→ {"type":"sfu_client_answer","sdp":{…}}
→ {"type":"sfu_ice","candidate":{…}}          // ICE к серверу
← {"type":"sfu_republish_request"}            // сервер не видит поток —
                                              // перепубликуйте его заново

Три правила SFU-публикации. Первое: свою секцию объявляйте sendonly, а под слушателей предложите 8 пустых recvonly-секций прямо в первом offer'е — сервер развернёт их и будет класть в них голоса участников без пере-переговоров (как в браузерном клиенте). Второе: чужие дорожки из слотов боту обычно не нужны, но вычитывать их надо (иначе растёт буфер приёмника). Третье: если сервер прислал sfu_republish_request — соберите сессию заново (не чаще раза в 15 секунд).

Свой аудиотрек бот крутит вместо микрофона: 48 кГц, стерео, кадры по 20 мс — так бот может воспроизводить любой звук: музыку, уведомления, синтез речи. Если канал недоступен, сервер ответит voice_join_error с причиной.

3. Ограничения и правила