TeamSpeak echoes a bot's own channel/server messages back to itself.
Without filtering, a chunked reply re-entered the command path: !help's
output exceeds the ~1024-byte per-message cap, so splitTextIntoChunks
splits it, and the second chunk (which starts with "!artist ...") was
parsed as a new !artist command, loading 20 search results and starting
playback.
Drop messages whose invokerID matches the bot's own clientId at the
transport boundary, before they reach any consumer.
Silent failure: logs showed "Client properties updated" / "Description
updated" / "Avatar updated" even though the bot's nickname never
changed and the avatar stayed as "loading image" on clients.
Root causes:
- TS6HttpQuery.clientUpdate ignored non-2xx responses, so 400 (bad
parameter) and 403 (insufficient permission) were reported as success.
- updateClientProperties built TS3-escaped strings (\\s for space) then
split them back into JSON props, so TS6 received literal backslashes
and rejected the nickname silently.
- handleFeatureError only matched textual "permission" errors; HTTP
4xx statuses weren't recognised and the feature retried every song.
- doAvatarUpload had no per-step logging, making it impossible to tell
whether a broken avatar came from init, the TCP 30033 transfer, or
the client_flag_avatar command.
Fixes:
- Add HttpQueryError (status/body/path); clientUpdate throws on non-2xx.
- Build a raw property map in updateClientProperties; escape only on
the TS3 wire path.
- Log HTTP status and updated prop names on success.
- handleFeatureError now treats HTTP 400/401/403 as unrecoverable.
- Debug-log each step of doAvatarUpload plus bytes/elapsedMs on success.
https://claude.ai/code/session_018NrpGWbQQTrahUVXyea5Jy
Add BotProfileManager that updates the bot's TeamSpeak presence when
songs change: album cover as avatar, song info in nickname, away status
toggled on stop/play. Each feature is independently configurable via
REST API and persisted to the database. Permission-safe — features that
fail due to insufficient server permissions are silently disabled until
reconnect. Description falls back to nickname display on TS3 (only
supported via TS6 HTTP Query).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Version 0.2.1 ships a universal clientinit format that works natively
against both TS3 and TS6 servers:
client_version: "3.?.? [Build: 5680278000]"
client_version_sign: DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOo
BzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg==
The old ts6-compat.ts workaround (monkey-patching handler.sendPacket to
rewrite clientinit's client_version to "3.6.2") is now actively wrong:
it replaces the library's new correct version/signature pair with a
stale one that TS6 servers reject, which is why the first 0.2.1 TS6
handshake attempt still hung at `received initivexpand2`.
Changes:
- package.json: "@honeybbq/teamspeak-client": "^0.1.0" -> "^0.2.1"
- src/ts-protocol/client.ts: remove patchClientInitVersion import and
the sendPacket monkey-patch block. Leave an inline comment so anyone
reading the git blame understands why the shim is gone.
- Delete src/ts-protocol/ts6-compat.ts and ts6-compat.test.ts — no
callers remain.
Other 0.2.x notes worth knowing (no code change here, just documenting):
- ClientOptions gained serverPassword / defaultChannel /
defaultChannelPassword that are sent DURING clientinit. We still call
our own joinChannel() post-connect because the existing flow works
and switching is an orthogonal refactor.
- 0.1.1 contains the P-256 DER encoding fix (PR #5 by ZHANGTIANYAO1).
Identities generated by 0.1.0 are cryptographically incompatible with
0.2.x's corrected handshake path — a bot whose identity column was
populated before this upgrade will hang at `received initivexpand2`
and fall through the 15s connect deadline. Workaround: clear the
identity column so the next start generates a fresh key. Server
groups assigned to the old UID must be re-granted once against the
new one.
Verification:
- tsc --noEmit clean
- vitest: 93/93 unit tests pass (1 test file removed with ts6-compat)
- scripts/test_full_feature.py against a local TS6 server: 51/51 pass,
including handshake, voice playback, identity persistence, WebSocket
stateChange broadcasts, and all corner-case regressions.
- Live: bot connected to TS6 in ~80ms after identity regeneration,
played NetEase audio through the voice channel, clean stop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: on Docker restart, loadSavedBots() unconditionally connected
ALL saved bots regardless of autoStart flag, causing a rapid
connect/disconnect loop. Additionally, bot identities were regenerated
on every restart, causing TS server conflicts with stale sessions.
Changes:
- loadSavedBots() now only auto-connects bots with autoStart=true
- startBot/stopBot persist autoStart state so restart behavior matches
user intent
- Bot TS3 identity is persisted to database and reused across restarts
- Database schema migrated to include identity column
- TS3Client.connect() cleans up existing connection before reconnecting
- Stagger bot connections by 1s to avoid overwhelming the TS server
https://claude.ai/code/session_01L2kEV2M1QFWMCyPtLU5LgC
1. Move TS6 handler patch to after client.connect() — the library's
connect() internally replaces handler via #S(), discarding any
patch applied beforehand. Patching after connect() is safe because
clientinit is sent in async message callbacks after Init1 round-trips.
2. Preserve detectedProtocol across reconnect — disconnect() resets it
to "unknown", causing the TS6 patch to be skipped on reconnect.
3. Prevent double "disconnected" event in BotInstance — disconnect()
emitted it directly AND the async TS3Client disconnect triggered
another through the event chain.
4. Guard playNext() against running after disconnect — check connected
flag to avoid ghost queue processing.
5. Fix UDP error timer leak — clear previous timer before setting a new
one to prevent accumulation.
6. Fix isPortFree() FD leak — close the test server on error path.
https://claude.ai/code/session_01QzvMLUT3UkhsffShcY1qzD
The @honeybbq/teamspeak-client library sends clientinit directly via
handler.sendPacket() during the handshake, bypassing the commandMiddleware
chain entirely. This meant the ts6VersionMiddleware never intercepted the
handshake clientinit, so TS6 servers always received version 3.5.3 and
silently rejected it (never responding with initserver), causing idle timeout.
Fix: monkey-patch handler.sendPacket() to intercept clientinit packets and
upgrade the version to 3.6.2 before they're sent over the wire.
https://claude.ai/code/session_01QzvMLUT3UkhsffShcY1qzD
clientinit compatibility:
- Analyzed @honeybbq/teamspeak-client's clientinit: it already sends a
clean 14-field payload without problematic fields (no badges,
integrations, security_hash, etc.)
- The key difference vs NeteaseTSBot is client_version: our library
sends 3.5.3, NeteaseTSBot sends 3.6.2. TS6 servers may reject
older versions.
- Add ts6-compat.ts with CommandMiddleware that patches clientinit
to use version 3.6.2 + matching ECDSA signature when connecting
to detected TS6 servers
- Middleware is automatically applied when detectedProtocol === "ts6"
Pre-existing bug fix:
- Fix playNext() in instance.ts where successful retry still fell
through to player.stop(), killing the just-started playback
https://claude.ai/code/session_016WhH58avUD9xy2dgADJgTh
Critical:
- Persist serverProtocol/ts6ApiKey in database schema so TS6 config
survives restarts (added columns + manager save/load)
Medium:
- Clear udpErrorTimer on disconnect to prevent memory leak and stale
log messages from firing after teardown
- Guard against double connect() by disconnecting old client first
- Add settled guard in TS6HttpQuery.request() to prevent double
reject when both res error and req error fire
- Add res.on("error") handler to TS6HttpQuery response stream
Low:
- Cap probeTS3Query banner buffer at 256 bytes to prevent memory abuse
from non-TS3 services sending large data on port 10011
- Remove unnecessary EventEmitter inheritance from TS6HttpQuery
- Update database test fixtures with new serverProtocol/ts6ApiKey fields
https://claude.ai/code/session_016WhH58avUD9xy2dgADJgTh
- Fix double-resolve race in probeTS3Query (data event vs connect timer)
by guarding with a resolved flag
- Fix double-resolve in probeTS6HttpQuery similarly
- Support custom query ports in detectServerProtocol via DetectOptions
- Clean up httpQuery and detectedProtocol on disconnect()
- Add res.on("error") handler in HTTP Query client to avoid unhandled errors
- Improve logging: warn with actionable message when protocol is unknown
https://claude.ai/code/session_016WhH58avUD9xy2dgADJgTh
The @honeybbq/teamspeak-client library already handles TS6 license block
type 8 (Ts5Server) in its handshake, so voice connections work with both
TS3 and TS6 servers. This commit adds the surrounding infrastructure:
- protocol-detect.ts: Auto-detect server type by probing TS3 ServerQuery
(port 10011) and TS6 HTTP Query (port 10080) in parallel
- http-query.ts: TS6 HTTP Query client replacing the raw-TCP ServerQuery
that TS6 servers no longer support (ports 10080/10443)
- client.ts: Protocol-aware connection with auto-detection, TS6 HTTP
Query setup, and forced protocol override option
- manager.ts: Pass through serverProtocol and ts6ApiKey config options
- connection.ts: Mark legacy TS3 ServerQuery as deprecated for TS6
https://claude.ai/code/session_016WhH58avUD9xy2dgADJgTh
- Global uncaughtException/unhandledRejection handlers in index.ts
- FFmpeg stdout/stderr stream error handlers in player.ts
- HTTP server and WebSocket server error handlers in server.ts
- Safe WebSocket broadcast with try-catch in websocket.ts
- Catch async errors from textMessage handler in instance.ts
- Reset voiceFramesSent counter on reconnect in client.ts
https://claude.ai/code/session_01EjpEsC2GCsvwbu4n3XC8EE
Adds info-level logs at each stage of the audio pipeline to help
diagnose why playback produces no audible output:
- FFmpeg first PCM data received
- FFmpeg process exit code
- FFmpeg stderr (errors/HTTP/stream info at info level)
- First opus frame encoded and emitted
- First voice packet sent to TeamSpeak
- Error catching in frame encoding loop
https://claude.ai/code/session_01EjpEsC2GCsvwbu4n3XC8EE
Bugs fixed:
- play-at: stop current playback before jumping to queue index
- playNext retry: use break instead of return to ensure stateChange emits
- sendVoiceData: skip if disconnecting to avoid errors during teardown
UI: player-left and lyrics button now toggle lyrics page (open/close)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add isAdvancing guard to playNext() to prevent concurrent calls from
skipping songs (trackEnd + user !next race condition)
- Replace removeAllListeners in WebSocket setup with tracked named
listeners, attach only once per bot instead of every 5 seconds
- Add .catch() to unhandled playNext() in cmdVote
- Add sessionId counter to AudioPlayer to discard stale setTimeout
callbacks after stop()+play() transitions
- Defer nulling TS3 client until disconnect() promise resolves
- Add null checks for providers in play-by-id and add-by-id endpoints
- Add PCM buffer backpressure (pause FFmpeg at 960KB, resume at 480KB)
- Change volume slider from @input to @change to avoid flooding server
- Wrap error-reporting sendTextMessage in try/catch to prevent
unhandled rejection in error handler
- Clamp elapsed getter to song duration
- Log FFmpeg stderr at debug level instead of swallowing
- Return null from prev() in Sequential mode instead of wrapping
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Switch from TCP ServerQuery (port 10011, invisible) to real TS3 client
protocol via @honeybbq/teamspeak-client (UDP 9987, ECDH handshake,
AES-EAX encryption). Bot now appears as a regular client visible to
all users in TeamSpeak.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>