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 для сценариев вроде логирования онлайна в реальном времени эффективнее.