Pterohost docs

Outbound webhooks TeamSpeak 6: настройка и приём событий сервера

Как работают outbound webhooks в TeamSpeak 6: какие события пушит сервер, как настроить получателя, готовый приёмник на Python и Node, проверка подлинности и защита эндпоинта.

Один из самых практичных сдвигов TeamSpeak 6 относительно третьей версии - outbound webhooks: возможность заставить сервер самому пушить события в ваше приложение вместо постоянного опроса. Для ботов логирования, Discord-интеграций или систем автовыдачи прав это снимает необходимость держать вечный процесс с открытой query-сессией. В этой статье - зачем нужны webhooks, какие события они покрывают, как поднять приёмник на Python и Node, как защитить эндпоинт от чужих запросов и что делать с повторной доставкой одного события.

Зачем нужны webhooks вместо поллинга

Классический подход к интеграции с игровым или голосовым сервером - поллинг: приложение раз в несколько секунд опрашивает сервер и сравнивает состояние с предыдущим снимком, чтобы заметить изменения. В TeamSpeak 3 так исторически и делают большинство ботов поверх ServerQuery, либо держат открытую сессию с подпиской на события внутри неё - подробности в статье про ServerQuery TeamSpeak 3. У поллинга два системных недостатка: задержка между событием и реакцией (зависит от интервала опроса) и лишняя нагрузка на сервер и сеть при частых запросах, большая часть которых возвращает “ничего не изменилось”.

Webhooks переворачивают модель: сервер сам знает, когда что-то произошло, и сам отправляет HTTP-запрос вашему приложению в момент события. Задержка минимальна, нагрузка на сервер не растёт с частотой опроса (потому что опроса нет), а ваше приложение может вообще не иметь постоянно работающего процесса - только HTTP-эндпоинт, поднимающийся по запросу.

Какие события поддерживаются

По данным обсуждений в сообществе TeamSpeak 6, набор событий webhooks покрывает типичные для голосового сервера триггеры:

  • подключение и отключение клиента к серверу;
  • создание и удаление канала, изменение параметров канала;
  • отправка текстового сообщения (в канал или на сервер);
  • изменение прав/групп.

Точный список событий, их названия и формат JSON-payload относятся к части API, которая ещё меняется между бета-сборками TeamSpeak 6 - официального стабильного справочника на момент написания статьи нет, а часть деталей можно найти только в файле документации внутри дистрибутива сервера (doc/webquery.md) или через community-форум. Прежде чем завязывать продакшн-логику на конкретный набор полей payload, разверните тестовый вебхук на своей версии сервера и посмотрите реальную структуру запроса.

Настройка получателя на стороне TeamSpeak 6

Регистрация webhooks выполняется через тот же query-интерфейс, что и создание API-ключей - смотрите статью про REST API TeamSpeak 6 для базовой аутентификации. Общий паттерн для систем такого рода - передать серверу URL, на который слать запросы, и список интересующих событий (либо все события через wildcard). Точный метод регистрации (конкретная query-команда или HTTP-эндпоинт с телом запроса) в вашей сборке может отличаться от других версий - сверяйтесь с документацией установленного релиза, а не переносите синтаксис из статей про другие бета-сборки без проверки.

Практическая рекомендация вне зависимости от точного синтаксиса: регистрируйте webhook на HTTPS-адрес, а не на голый HTTP - иначе payload события (который может содержать данные о клиентах) уйдёт по сети в открытом виде.

Приёмник на Python (Flask)

Минимальный сервер, принимающий webhook и логирующий событие:

from flask import Flask, request, jsonify
import hmac
import hashlib
import os

app = Flask(__name__)

WEBHOOK_SECRET = os.environ.get("TS_WEBHOOK_SECRET", "")
seen_event_ids = set()  # для простой идемпотентности, в проде - Redis/БД с TTL


def verify_signature(payload: bytes, signature_header: str) -> bool:
    if not WEBHOOK_SECRET or not signature_header:
        return False
    expected = hmac.new(
        WEBHOOK_SECRET.encode(), payload, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)


@app.route("/ts6/webhook", methods=["POST"])
def ts6_webhook():
    raw_body = request.get_data()
    signature = request.headers.get("X-Signature", "")

    # если сервер не отправляет подпись - хотя бы проверяйте секретный
    # query-параметр или заголовок, который вы сами прописали при регистрации
    if WEBHOOK_SECRET and not verify_signature(raw_body, signature):
        return jsonify({"error": "invalid signature"}), 401

    data = request.get_json(silent=True)
    if not data:
        return jsonify({"error": "invalid json"}), 400

    event_id = data.get("event_id") or data.get("id")
    if event_id and event_id in seen_event_ids:
        # уже обработали это событие - подтверждаем без повторной обработки
        return jsonify({"status": "duplicate"}), 200
    if event_id:
        seen_event_ids.add(event_id)

    event_type = data.get("event") or data.get("type")
    handle_event(event_type, data)

    return jsonify({"status": "ok"}), 200


def handle_event(event_type, data):
    if event_type in ("client_connect", "client_disconnect"):
        print(f"[TS6] {event_type}: {data}")
    elif event_type in ("channel_create", "channel_delete"):
        print(f"[TS6] изменение канала: {data}")
    else:
        print(f"[TS6] событие {event_type}: {data}")


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

Тот же приёмник на FastAPI, если нужен async-стек:

from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import os

app = FastAPI()
WEBHOOK_SECRET = os.environ.get("TS_WEBHOOK_SECRET", "")
seen_event_ids: set[str] = set()


def verify_signature(payload: bytes, signature_header: str) -> bool:
    if not WEBHOOK_SECRET or not signature_header:
        return False
    expected = hmac.new(WEBHOOK_SECRET.encode(), payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header)


@app.post("/ts6/webhook")
async def ts6_webhook(request: Request):
    raw_body = await request.body()
    signature = request.headers.get("x-signature", "")

    if WEBHOOK_SECRET and not verify_signature(raw_body, signature):
        raise HTTPException(status_code=401, detail="invalid signature")

    data = await request.json()
    event_id = data.get("event_id") or data.get("id")
    if event_id and event_id in seen_event_ids:
        return {"status": "duplicate"}
    if event_id:
        seen_event_ids.add(event_id)

    print(f"[TS6] событие: {data}")
    return {"status": "ok"}

Pterohost - серверы TeamSpeak 6 с открытыми портами под интеграции, легко подключить бота с приёмником webhooks на своём хостинге приложения. Промокод 4START даёт -20% на первый заказ. Арендовать TeamSpeak 6

Приёмник на Node.js

Тот же принцип на Express:

const express = require("express");
const crypto = require("crypto");

const app = express();
app.use(express.raw({ type: "application/json" }));

const WEBHOOK_SECRET = process.env.TS_WEBHOOK_SECRET || "";
const seenEventIds = new Set();

function verifySignature(rawBody, signatureHeader) {
  if (!WEBHOOK_SECRET || !signatureHeader) return false;
  const expected = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}

app.post("/ts6/webhook", (req, res) => {
  const rawBody = req.body; // Buffer, благодаря express.raw
  const signature = req.headers["x-signature"] || "";

  if (WEBHOOK_SECRET && !verifySignature(rawBody, signature)) {
    return res.status(401).json({ error: "invalid signature" });
  }

  let data;
  try {
    data = JSON.parse(rawBody.toString("utf8"));
  } catch (e) {
    return res.status(400).json({ error: "invalid json" });
  }

  const eventId = data.event_id || data.id;
  if (eventId && seenEventIds.has(eventId)) {
    return res.status(200).json({ status: "duplicate" });
  }
  if (eventId) seenEventIds.add(eventId);

  console.log(`[TS6] событие ${data.event || data.type}:`, data);
  res.status(200).json({ status: "ok" });
});

app.listen(8080, () => console.log("TS6 webhook receiver on :8080"));

Оба приёмника - универсальный каркас, не завязанный на конкретные имена полей payload TeamSpeak 6, потому что точная схема ещё не зафиксирована между бета-сборками. Подставьте реальные имена полей после того, как посмотрите живой запрос от своего сервера (например, залогировав raw_body целиком при первом тестовом событии).

Проверка подлинности и защита эндпоинта

Публичный HTTP-эндпоинт, принимающий webhooks, - потенциальная точка атаки: если кто-то узнает URL, он может слать поддельные события вашему приложению. Базовые меры защиты:

  • HTTPS обязателен для продакшн-эндпоинта, чтобы payload и подпись не перехватили по пути.
  • Проверка подписи или секретного заголовка - если ваша сборка TS6 подписывает запросы, сверяйте подпись с ожидаемой через hmac.compare_digest (или аналог с защитой от timing-атак), а не простым сравнением строк. Если сервер подписи не отправляет, используйте секретный путь или query-параметр, известный только серверу и вашему приложению.
  • Ограничение по IP на firewall - эндпоинт webhooks должен принимать запросы только с IP-адреса вашего сервера TeamSpeak. Настройка правил разобрана в статье про firewall для игровых серверов.
  • Валидация структуры JSON перед обработкой - не доверяйте входящим данным вслепую, проверяйте наличие ожидаемых полей и типы значений.
  • Быстрый ответ 200 - обрабатывайте событие асинхронно (очередь, фоновая задача), если логика небыстрая, а сразу отвечайте серверу подтверждением получения, чтобы не спровоцировать таймаут и повторную доставку.

Ретраи и идемпотентность

Как и большинство систем доставки webhooks, TeamSpeak 6 может повторно отправить один и тот же запрос, если ваш эндпоинт не ответил вовремя, вернул ошибку или сетевое соединение оборвалось. Это значит, что ваш приёмник обязан быть идемпотентным: обработка одного и того же события дважды не должна приводить к двойному эффекту (например, к двум одинаковым сообщениям в Discord или двойной выдаче роли).

Практический паттерн - использовать уникальный идентификатор события из payload (если он есть) и хранить множество уже обработанных ID с TTL (в примерах выше - упрощённо, set() в памяти; в проде правильнее Redis или БД с истечением записей, чтобы множество не росло бесконечно). Если сервер не присылает явный ID события, можно построить свой ключ идемпотентности из комбинации типа события, ID клиента/канала и метки времени с округлением до секунды.

Практические сценарии применения

Лог событий в Discord или Telegram. Приёмник webhook при получении client_connect/client_disconnect форматирует сообщение и шлёт его в канал через Discord webhook или Telegram Bot API - классическая связка “голосовой сервер -> текстовый чат сообщества” без поллинга.

Автовыдача прав. При событии подключения клиента можно синхронно проверить его статус во внешней системе (например, в вашей базе донатов или ролей Discord через связанный аккаунт) и через REST API TeamSpeak 6 выставить нужную серверную группу - без того, чтобы бот постоянно держал сессию и сверял список клиентов вручную.

Статистика онлайна. Накопление событий подключения/отключения в базе данных или временных рядах (InfluxDB, Prometheus pushgateway) позволяет строить графики онлайна сервера во времени без необходимости постоянного опроса - события сами прилетают в момент изменения.

Pterohost - хостинг голосовых серверов с DDoS-защитой Guard Rise, подходит и под ботов с webhooks, и под классические интеграции. Промокод 4START даёт -20% на первый заказ. Заказать сервер TeamSpeak 6

Часто задаваемые вопросы

Что такое outbound webhooks в TeamSpeak 6?

Это механизм, при котором сервер сам отправляет HTTP-запрос на указанный вами адрес при наступлении события - подключении клиента, смене канала, серверном событии - вместо того чтобы ваше приложение постоянно опрашивало сервер или держало открытую query-сессию для получения уведомлений.

Какие события отправляет TeamSpeak 6 через webhooks?

В обсуждениях функциональности упоминаются события подключения и отключения клиента, создания и удаления каналов, текстовых сообщений и изменений прав. Точный список событий и формат payload зависят от версии сервера и могут меняться между бета-сборками - проверяйте документацию своей установки перед тем, как полагаться на конкретный набор событий в проде.

Как защитить эндпоинт, принимающий webhooks от TeamSpeak 6?

Держите эндпоинт за HTTPS, проверяйте подпись или секретный заголовок запроса, если сервер его отправляет, ограничивайте доступ к порту приёмника через firewall по IP сервера TeamSpeak, и обязательно валидируйте структуру входящего JSON перед обработкой, не доверяя данным вслепую.

Нужно ли делать приёмник webhooks идемпотентным?

Да. Как и большинство систем с доставкой webhooks, TeamSpeak 6 может повторно отправить событие при сетевом сбое или таймауте на вашей стороне. Приёмник должен уметь безопасно обработать один и тот же запрос дважды - например, проверяя уникальный идентификатор события и не выполняя повторно необратимые действия.

Можно ли получать события TeamSpeak 6 без вебхуков, поллингом?

Да, через REST API/ServerQuery можно периодически опрашивать состояние сервера, как это исторически делалось в TeamSpeak 3. Но поллинг создаёт задержку между событием и реакцией и лишнюю нагрузку на сервер при частых опросах - webhooks для сценариев вроде логирования онлайна в реальном времени эффективнее.