@honeybbq/teamspeak 1.0.0-beta.17 → 1.0.0-beta.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -1104
- package/dist/core/api/clients.d.ts +11 -0
- package/dist/core/api/clients.d.ts.map +1 -1
- package/dist/core/api/clients.js +17 -0
- package/dist/core/api/clients.js.map +1 -1
- package/package.json +1 -1
- package/src/core/api/clients.ts +27 -0
- package/src/query/testing/trusting-fetch.ts +25 -0
package/README.md
CHANGED
|
@@ -1,1131 +1,106 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
34
|
-
- **
|
|
35
|
-
- **
|
|
36
|
-
- **
|
|
37
|
-
- **
|
|
38
|
-
- **
|
|
39
|
-
- **
|
|
40
|
-
|
|
41
|
-
##
|
|
42
|
-
|
|
43
|
-
1.0 is in beta, published under the `beta` tag. A plain `npm install` still
|
|
44
|
-
gets 0.3, so ask for the beta:
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://honeybbq.github.io/teamspeak-js/">
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/HoneyBBQ/teamspeak-js/main/docs/assets/banner-dark.svg">
|
|
5
|
+
<img alt="teamspeak-js" src="https://raw.githubusercontent.com/HoneyBBQ/teamspeak-js/main/docs/assets/banner-light.svg" width="460">
|
|
6
|
+
</picture>
|
|
7
|
+
</a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<strong>An independent TeamSpeak client protocol library written in pure TypeScript.</strong><br>
|
|
12
|
+
Works with TeamSpeak 3 and TeamSpeak 6 servers. No proprietary SDK, no copied code.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://www.npmjs.com/package/@honeybbq/teamspeak"><img alt="npm version" src="https://img.shields.io/npm/v/@honeybbq/teamspeak"></a>
|
|
17
|
+
<a href="https://github.com/HoneyBBQ/teamspeak-js/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/HoneyBBQ/teamspeak-js/actions/workflows/ci.yml/badge.svg"></a>
|
|
18
|
+
<a href="https://scorecard.dev/viewer/?uri=github.com/HoneyBBQ/teamspeak-js"><img alt="OpenSSF Scorecard" src="https://api.scorecard.dev/projects/github.com/HoneyBBQ/teamspeak-js/badge"></a>
|
|
19
|
+
<a href="https://github.com/HoneyBBQ/teamspeak-js/blob/main/package.json"><img alt="Node.js version" src="https://img.shields.io/node/v/@honeybbq/teamspeak"></a>
|
|
20
|
+
<a href="https://github.com/HoneyBBQ/teamspeak-js/blob/main/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg"></a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<a href="https://honeybbq.github.io/teamspeak-js/">Docs</a> ·
|
|
25
|
+
<a href="https://honeybbq.github.io/teamspeak-js/documents/guide.html">Guide</a> ·
|
|
26
|
+
<a href="https://github.com/HoneyBBQ/teamspeak-js/blob/main/CHANGELOG.md">Changelog</a> ·
|
|
27
|
+
<a href="https://honeybbq.github.io/teamspeak-js/documents/migrating-to-1.0.html">Migrating to 1.0</a>
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
## Why this library
|
|
31
|
+
|
|
32
|
+
- **The voice protocol, end to end.** ECDH key exchange, the RSA puzzle, EAX-encrypted transport, and Opus voice (codecs 4 and 5), in TypeScript with no native addons.
|
|
33
|
+
- **ServerQuery too.** The same typed commands run over raw TCP, SSH (with host-key verification) or WebQuery, so an administration script and a voice bot share one vocabulary.
|
|
34
|
+
- **Typed commands and events.** Every command the voice protocol answers is a typed function, and the server's notifications arrive as typed events with pluggable middleware.
|
|
35
|
+
- **File transfers.** Upload, download and delete files on the server, with avatars and icons covered.
|
|
36
|
+
- **TeamSpeak 6 screen-share signalling.** Find, publish and watch streams, with SDP and ICE as plain strings for any WebRTC stack.
|
|
37
|
+
- **Built for long-running bots.** Opt-in reconnection tuned for TeamSpeak's antiflood ban, and a token-bucket rate limiter that keeps you clear of flood kicks.
|
|
38
|
+
- **Identities and server facts.** Generate, import, export and upgrade identities; read the server version, its uid hash (SHA-1 on TS3, SHA-256 on TS6) and its name, icon and banner.
|
|
39
|
+
- **TeamSpeak 3 and 6, ESM with types.** Pure TypeScript, zero native dependencies, full declarations, and a custom-transport hook for proxies and tests.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
45
42
|
|
|
46
43
|
```bash
|
|
47
|
-
npm install @honeybbq/teamspeak
|
|
48
|
-
# or
|
|
49
|
-
pnpm add @honeybbq/teamspeak@beta
|
|
44
|
+
npm install @honeybbq/teamspeak
|
|
50
45
|
```
|
|
51
46
|
|
|
52
|
-
|
|
47
|
+
1.0 is in beta and published under the `latest` and `beta` tags; Node.js 22.12 or newer is required. Upgrading from 0.x? See [Migrating to 1.0](https://honeybbq.github.io/teamspeak-js/documents/migrating-to-1.0.html).
|
|
53
48
|
|
|
54
|
-
## Quick
|
|
49
|
+
## Quick start
|
|
55
50
|
|
|
56
51
|
```typescript
|
|
57
52
|
import { Client, generateIdentity } from "@honeybbq/teamspeak/client";
|
|
58
53
|
|
|
59
|
-
// Generate a new identity (or load an existing one)
|
|
60
54
|
const identity = await generateIdentity(8);
|
|
61
|
-
|
|
62
|
-
// Create the client
|
|
63
55
|
const client = new Client(identity, "localhost", "TSBot", {
|
|
64
56
|
serverPassword: process.env.TEAMSPEAK_SERVER_PASSWORD,
|
|
65
57
|
defaultChannel: "Lobby",
|
|
66
|
-
defaultChannelPassword: process.env.TEAMSPEAK_DEFAULT_CHANNEL_PASSWORD,
|
|
67
58
|
});
|
|
68
59
|
|
|
69
|
-
// Register event handlers
|
|
70
60
|
client.on("connected", ({ reconnected }) => {
|
|
71
|
-
console.log(reconnected ? "Reconnected
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
client.on("textMessage", (msg) => {
|
|
75
|
-
console.log(`[${msg.invokerName}]: ${msg.message}`);
|
|
61
|
+
console.log(reconnected ? "Reconnected" : "Connected");
|
|
76
62
|
});
|
|
77
|
-
|
|
63
|
+
client.on("textMessage", (msg) => console.log(`[${msg.invokerName}]: ${msg.message}`));
|
|
78
64
|
client.on("disconnected", ({ error, willReconnect }) => {
|
|
79
65
|
console.log("Disconnected:", error?.message ?? "clean", willReconnect ? "(reconnecting)" : "");
|
|
80
66
|
});
|
|
81
67
|
|
|
82
|
-
// Connect; resolves once the handshake completes.
|
|
83
68
|
await client.connect({ signal: AbortSignal.timeout(15_000) });
|
|
84
|
-
|
|
85
|
-
// Stay connected until interrupted
|
|
86
|
-
process.on("SIGINT", async () => {
|
|
87
|
-
await client.disconnect();
|
|
88
|
-
});
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## API Overview
|
|
92
|
-
|
|
93
|
-
### Client Lifecycle
|
|
94
|
-
|
|
95
|
-
| Method | Description |
|
|
96
|
-
| --------------------------------------------- | ------------------------------------------------- |
|
|
97
|
-
| `new Client(identity, addr, nickname, opts?)` | Create a new client |
|
|
98
|
-
| `connect({ signal? })` | Connect; resolves once the handshake completes |
|
|
99
|
-
| `disconnect({ signal? })` | Leave the server and close the connection |
|
|
100
|
-
| `status` | `"disconnected"`, `"connecting"` or `"connected"` |
|
|
101
|
-
| `clientID` | Our clid, assigned by the server |
|
|
102
|
-
| `channelID` | The channel we are in |
|
|
103
|
-
| `serverCapabilities` | Server version, family and uid hash |
|
|
104
|
-
| `uid` | Our own uid, hashed as the server does |
|
|
105
|
-
|
|
106
|
-
`connect()` rejects with an `InvalidAddressError` for an address that cannot be
|
|
107
|
-
one — no host, or a port outside 1–65535 — before anything starts; a host name
|
|
108
|
-
that does not resolve fails with a `TeamspeakError` whose `cause` is the lookup's
|
|
109
|
-
error. A `connect()` called while `disconnect()` is still saying goodbye waits
|
|
110
|
-
for it to finish, then connects.
|
|
111
|
-
|
|
112
|
-
Commands, raw `exec`/`execNoWait`, voice and whispers need a connected client:
|
|
113
|
-
until `connect()` resolves, and from the moment `disconnect()` begins, they fail
|
|
114
|
-
with a `NotConnectedError` and send nothing.
|
|
115
|
-
|
|
116
|
-
### Events
|
|
117
|
-
|
|
118
|
-
Name an event with an `EventName` constant, or with its string: the two are the
|
|
119
|
-
same, and the listener's payload type follows either.
|
|
120
|
-
|
|
121
|
-
```typescript
|
|
122
|
-
import { EventName } from "@honeybbq/teamspeak/client";
|
|
123
|
-
|
|
124
|
-
client.on(EventName.TextMessage, (message) => console.log(message.message));
|
|
125
|
-
client.on("textMessage", (message) => console.log(message.message)); // the same
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
| Method | Description |
|
|
129
|
-
| ------------------------------------------ | -------------------------------------------------------------- |
|
|
130
|
-
| `on("connected", handler)` | Fires when fully connected (`ConnectedEvent`) |
|
|
131
|
-
| `on("disconnected", handler)` | Fires on disconnect (`DisconnectedEvent`) |
|
|
132
|
-
| `on("textMessage", handler)` | Fires on text messages |
|
|
133
|
-
| `on("clientEnter", handler)` | Fires when a client joins |
|
|
134
|
-
| `on("clientLeave", handler)` | Fires when a client leaves |
|
|
135
|
-
| `on("clientMoved", handler)` | Fires when a client moves channels |
|
|
136
|
-
| `on("clientUpdated", handler)` | Fires when a client's properties change (`ClientUpdatedEvent`) |
|
|
137
|
-
| `on("clientServerGroupAdded", handler)` | Fires when a client joins a server group |
|
|
138
|
-
| `on("clientServerGroupRemoved", handler)` | Fires when a client leaves a server group |
|
|
139
|
-
| `on("clientChannelGroupChanged", handler)` | Fires when a client's channel group changes |
|
|
140
|
-
| `on("channelCreated", handler)` | Fires when a channel is created |
|
|
141
|
-
| `on("channelEdited", handler)` | Fires when a channel's properties are edited |
|
|
142
|
-
| `on("channelMoved", handler)` | Fires when a channel is moved or reordered |
|
|
143
|
-
| `on("channelDeleted", handler)` | Fires when a channel is deleted |
|
|
144
|
-
| `on("channelDescriptionChanged", handler)` | Fires when a channel's description changes |
|
|
145
|
-
| `on("channelPasswordChanged", handler)` | Fires when a channel's password changes |
|
|
146
|
-
| `on("serverEdited", handler)` | Fires when the server's settings are edited |
|
|
147
|
-
| `on("channelSubscribed", handler)` | Fires when the bot is subscribed to a channel |
|
|
148
|
-
| `on("channelUnsubscribed", handler)` | Fires when the bot is unsubscribed from a channel |
|
|
149
|
-
| `on("pluginCommand", handler)` | Fires on a plugin command (`sendPluginCommand`) |
|
|
150
|
-
| `on("clientChatComposing", handler)` | Fires when a client is typing a private message |
|
|
151
|
-
| `on("kicked", handler)` | Fires when the bot is kicked |
|
|
152
|
-
| `on("poked", handler)` | Fires when poked by a client |
|
|
153
|
-
| `on("voiceData", handler)` | Fires on incoming voice data |
|
|
154
|
-
|
|
155
|
-
`on()` and `once()` return a function that removes the listener, which is also
|
|
156
|
-
a `Disposable` (`using listener = client.on(...)`). Listeners run one at a
|
|
157
|
-
time, in the order their events arrived, so a listener may call the client and
|
|
158
|
-
await an answer. `disconnected` delivers `undefined` after `disconnect()`, and
|
|
159
|
-
the cause otherwise — a `ConnectionTimeoutError`, or a `RemovedError` for a kick, a ban
|
|
160
|
-
or a server stopping. `await using client = new Client(...)` disconnects when
|
|
161
|
-
the scope ends.
|
|
162
|
-
|
|
163
|
-
`connected` is delivered after `connect()` resolves, so a listener added right
|
|
164
|
-
after `await client.connect()` still hears it. `disconnect()` waits a second at
|
|
165
|
-
most for the server to acknowledge its goodbye, less if its `signal` aborts
|
|
166
|
-
sooner, and resolves once `disconnected` has been delivered. A `connect()` waits
|
|
167
|
-
for an earlier session's `disconnected` to be delivered before it starts, so no
|
|
168
|
-
listener or `events()` iterator of the new session hears the old one end.
|
|
169
|
-
|
|
170
|
-
A listener that throws does not stop the others: the event still reaches the
|
|
171
|
-
listeners after it, and the exception surfaces as an `uncaughtException`, as an
|
|
172
|
-
EventEmitter listener's would.
|
|
173
|
-
|
|
174
|
-
Voice is the exception to "every event in order": while more than 256
|
|
175
|
-
deliveries are waiting, voice frames are dropped (and counted in a warning at
|
|
176
|
-
most every ten seconds) rather than queued without bound.
|
|
177
|
-
|
|
178
|
-
`events(name, { signal })` is the same as an async iterator, buffering events
|
|
179
|
-
that arrive while the loop body runs. The buffer is unbounded (voice excepted,
|
|
180
|
-
as above), so stop iterating when you stop reading. Aborting `signal` stops
|
|
181
|
-
listening and makes the pending and later `next()` calls reject with the
|
|
182
|
-
signal's reason, as Node's `events.on` does; `break`ing out of the loop, or a
|
|
183
|
-
disconnect, ends it normally:
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
for await (const msg of client.events("textMessage", { signal })) {
|
|
187
|
-
await reply(msg);
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
`notification` is the catch-all: it delivers every notification the server
|
|
192
|
-
sends, as it arrived, the ones that also have a typed event among them, after
|
|
193
|
-
that event. It is how to reach one the SDK does not model; `NotificationName`
|
|
194
|
-
names the known ones:
|
|
195
|
-
|
|
196
|
-
```typescript
|
|
197
|
-
import { NotificationName } from "@honeybbq/teamspeak";
|
|
198
|
-
|
|
199
|
-
client.on("notification", (n) => {
|
|
200
|
-
if (n.name === NotificationName.ChannelEdited) console.log(n.params);
|
|
201
|
-
});
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
### Commands
|
|
205
|
-
|
|
206
|
-
Every command the voice protocol answers is a typed function, grouped by area.
|
|
207
|
-
Each is addressed by branded ids, so a `ChannelID` cannot be passed where a
|
|
208
|
-
`ClientID` is meant. The constructors — `asChannelID(5)`, `asClientID(3)` and
|
|
209
|
-
the rest — throw a `RangeError` for anything but an integer in the id's wire
|
|
210
|
-
range: 0–65535 for a `ClientID` and the file transfer ids, 0–2^32−1 for a
|
|
211
|
-
`PermissionID` (an `IconID` also takes the signed form), 0–2^64−1 for the
|
|
212
|
-
`bigint` ids. Required arguments are positional; everything optional
|
|
213
|
-
goes in one options object, always the last argument, which also carries
|
|
214
|
-
`signal` and `timeoutMs` to abandon the command or bound how long it waits.
|
|
215
|
-
|
|
216
|
-
```typescript
|
|
217
|
-
import {
|
|
218
|
-
listChannels,
|
|
219
|
-
createChannel,
|
|
220
|
-
moveClient,
|
|
221
|
-
kickClient,
|
|
222
|
-
sendChannelMessage,
|
|
223
|
-
addServerGroupPermission,
|
|
224
|
-
iterateClientDatabase,
|
|
225
|
-
permissionByName,
|
|
226
|
-
PermissionName,
|
|
227
|
-
asClientID,
|
|
228
|
-
Codec,
|
|
229
|
-
Reason,
|
|
230
|
-
} from "@honeybbq/teamspeak";
|
|
231
|
-
|
|
232
|
-
const channels = await listChannels(client, { signal: AbortSignal.timeout(5_000) });
|
|
233
|
-
|
|
234
|
-
const cid = await createChannel(client, {
|
|
235
|
-
name: "Lounge",
|
|
236
|
-
topic: "the quiet one",
|
|
237
|
-
codec: Codec.OpusVoice,
|
|
238
|
-
isPermanent: true,
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
await moveClient(client, client.clientID, cid, { password: "channel-password" });
|
|
242
|
-
await sendChannelMessage(client, "hello");
|
|
243
|
-
await kickClient(client, someone, Reason.KickChannel, { reasonMessage: "take a break" });
|
|
244
|
-
|
|
245
|
-
await addServerGroupPermission(client, sgid, permissionByName(PermissionName.ClientTalkPower), 50, {
|
|
246
|
-
skip: true,
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
for await (const entry of iterateClientDatabase(client)) {
|
|
250
|
-
console.log(entry.nickname, entry.lastConnectedAt);
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
A command that sends content — `createChannel`'s channel, `editChannel`'s
|
|
255
|
-
settings, `updateClient`, `editClient`, `editClientDatabase`,
|
|
256
|
-
`addBan`'s `BanFields` — takes it as its own
|
|
257
|
-
argument, and `{ signal, timeoutMs }` as a separate one after it: set among the
|
|
258
|
-
content, they are not read.
|
|
259
|
-
|
|
260
|
-
Options objects use camelCase names (`maxClients` for `channel_maxclients`),
|
|
261
|
-
and only the fields you set are sent: an unset field leaves the setting alone,
|
|
262
|
-
while `""` sends it empty — `editChannel(client, cid, { password: "" })` clears
|
|
263
|
-
a password. A key an options type does not name is never sent. The exceptions
|
|
264
|
-
are `editServer` and `editInstance`, which take the raw `virtualserver_*` and
|
|
265
|
-
`serverinstance_*` properties as `getServerInfo` reports them; `PropertyName` names
|
|
266
|
-
the keys.
|
|
267
|
-
|
|
268
|
-
Results are camelCase too. Timestamps are `Date`s (`undefined` where the server
|
|
269
|
-
sends 0 for "never"), durations are milliseconds with an `Ms` suffix
|
|
270
|
-
(`idleTimeMs`, `durationMs`), and a permission is a `Permission`, built with
|
|
271
|
-
`permissionByID` or `permissionByName`. Small enumerations — `GroupType`,
|
|
272
|
-
`Codec`, `PrivilegeKeyType`, `LogLevel` and the rest — are objects of named values with
|
|
273
|
-
a type of the same name.
|
|
274
|
-
|
|
275
|
-
| Module | Covers |
|
|
276
|
-
| ------------ | -------------------------------------------------------------- |
|
|
277
|
-
| `channels` | Create, edit, move, delete, subscribe, channel permissions |
|
|
278
|
-
| `clients` | List, move, kick, poke, edit, the client database, permissions |
|
|
279
|
-
| `server` | Server and instance settings, groups, temp passwords, bindings |
|
|
280
|
-
| `groups` | Channel groups, the permission catalogue, privilege keys |
|
|
281
|
-
| `moderation` | Bans, complaints, offline messages, text messages, the log |
|
|
282
|
-
| `files` | List, delete, rename, create directories, transfer negotiation |
|
|
283
|
-
|
|
284
|
-
Many of these commands are answered over the voice protocol with a
|
|
285
|
-
notification rather than data rows — `servergrouplist` with
|
|
286
|
-
`notifyservergrouplist`, `ftinitupload` with `notifystartupload` after the
|
|
287
|
-
command has already succeeded. The client pairs each answer with its command,
|
|
288
|
-
so this is invisible to callers; [docs/voice-protocol-responses.md](https://github.com/HoneyBBQ/teamspeak-js/blob/main/docs/voice-protocol-responses.md)
|
|
289
|
-
has the measured table. A refusal is always thrown, never returned as an empty
|
|
290
|
-
result. A list or find with nothing to report answers `[]`. A command that
|
|
291
|
-
exists to return something — `createChannel`'s id, `initFileUpload`'s key,
|
|
292
|
-
`getClientProperties`'s fields — throws a `NoDataError` when the server answers
|
|
293
|
-
without it, rather than returning an id of 0 or an empty object.
|
|
294
|
-
|
|
295
|
-
For anything outside the typed set, `client.exec(line, { signal?, timeoutMs? })`
|
|
296
|
-
sends a raw command line and resolves to the rows that answer it.
|
|
297
|
-
|
|
298
|
-
### File Transfers
|
|
299
|
-
|
|
300
|
-
`uploadFile` and `downloadFile` (from `@honeybbq/teamspeak/client`) negotiate the
|
|
301
|
-
transfer and move the bytes in one call, and `listFilesRecursive` (from the
|
|
302
|
-
package) lists a directory and everything under it, each entry with its full path:
|
|
303
|
-
|
|
304
|
-
```typescript
|
|
305
|
-
import { downloadFile, uploadFile } from "@honeybbq/teamspeak/client";
|
|
306
|
-
import { listFilesRecursive } from "@honeybbq/teamspeak";
|
|
307
|
-
|
|
308
|
-
await uploadFile(client, cid, "/docs/notes.txt", data);
|
|
309
|
-
const bytes = await downloadFile(client, cid, "/docs/notes.txt");
|
|
310
|
-
const tree = await listFilesRecursive(client, cid, "/", { maxDepth: 4 });
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
They work over a voice `Client`, and over a `QueryClient` on raw TCP or SSH when
|
|
314
|
-
passed the file transfer host as `{ host }`; WebQuery refuses every file command.
|
|
315
|
-
`downloadFile` takes `{ dest }` to stream into a `Writable` instead of holding
|
|
316
|
-
the file, and refuses a file over `maxSize` (256 MiB) in memory.
|
|
317
|
-
|
|
318
|
-
To control the two halves, negotiate with the `files` module, then move the bytes over TCP:
|
|
319
|
-
|
|
320
|
-
```typescript
|
|
321
|
-
import { Readable } from "node:stream";
|
|
322
|
-
import { initFileUpload } from "@honeybbq/teamspeak";
|
|
323
|
-
|
|
324
|
-
const info = await initFileUpload(client, cid, "/notes.txt", BigInt(data.length));
|
|
325
|
-
await client.uploadFileData(host, info, Readable.from([data]));
|
|
69
|
+
process.on("SIGINT", () => void client.disconnect());
|
|
326
70
|
```
|
|
327
71
|
|
|
328
|
-
|
|
329
|
-
`{ overwrite: true }`.
|
|
330
|
-
|
|
331
|
-
| Function | Description |
|
|
332
|
-
| ---------------------------------------------------- | --------------------------------- |
|
|
333
|
-
| `initFileUpload(client, ...)` | Negotiate an upload |
|
|
334
|
-
| `initFileDownload(client, ...)` | Negotiate a download |
|
|
335
|
-
| `client.uploadFileData(host, info, data, options?)` | Transfer file data to the server |
|
|
336
|
-
| `client.downloadFileData(host, info, out, options?)` | Receive file data from the server |
|
|
337
|
-
|
|
338
|
-
The transfer functions take `FileTransferOptions`:
|
|
339
|
-
|
|
340
|
-
| Option | Description |
|
|
341
|
-
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
342
|
-
| `signal` | An `AbortSignal` that abandons the transfer; it rejects with the signal's reason |
|
|
343
|
-
| `socketFactory` | Opens the connection instead of a direct TCP dial, to route it through a proxy or tunnel |
|
|
344
|
-
| `trace` | Hooks called at each milestone of the transfer (`dialStart`, `dialDone`, `progress`, `done`, `failed`); none sees the transfer key |
|
|
345
|
-
| `idleTimeoutMs` | The longest, in milliseconds, a transfer may go without moving a byte, the key write included. Default 30 000; `0` disables it |
|
|
346
|
-
|
|
347
|
-
A transfer that goes idle for `idleTimeoutMs` fails with a `FileTransferError`
|
|
348
|
-
whose `cause` is a `TimeoutError`, and its connection is destroyed. A download
|
|
349
|
-
that sends more bytes than the negotiated size, or ends short of it, fails with
|
|
350
|
-
a `FileTransferError` too; bytes past the announced size are never written.
|
|
351
|
-
|
|
352
|
-
### Identity
|
|
353
|
-
|
|
354
|
-
```typescript
|
|
355
|
-
import { generateIdentity, parseIdentity } from "@honeybbq/teamspeak/client";
|
|
356
|
-
|
|
357
|
-
// Generate a new identity with security level 8
|
|
358
|
-
const identity = await generateIdentity(8);
|
|
359
|
-
|
|
360
|
-
// Export to string for persistent storage
|
|
361
|
-
const exported = identity.export();
|
|
362
|
-
|
|
363
|
-
// Import from a previously exported string
|
|
364
|
-
const restored = parseIdentity(exported);
|
|
365
|
-
|
|
366
|
-
// Raise the security level (CPU-intensive). Identities are immutable: this
|
|
367
|
-
// returns a new one with the same key.
|
|
368
|
-
const stronger = await identity.upgradeToLevel(10, { signal });
|
|
369
|
-
|
|
370
|
-
// The unique identifier, hashed as a TS3 (sha1) or TS6 (sha256) server does
|
|
371
|
-
const uid = identity.uid("sha1");
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
The private key never leaves the identity except through `export()`:
|
|
375
|
-
`toString()`, `toJSON()` and `util.inspect()` describe it without the key.
|
|
376
|
-
`parseIdentity` takes exactly what `export()` writes — the key as canonical
|
|
377
|
-
base64 of at most 32 bytes, a colon, and a decimal offset that fits in 64 bits
|
|
378
|
-
— and throws an `InvalidIdentityError` for anything else.
|
|
379
|
-
|
|
380
|
-
### Options
|
|
381
|
-
|
|
382
|
-
```typescript
|
|
383
|
-
import { Client } from "@honeybbq/teamspeak/client";
|
|
384
|
-
import { consoleLogger } from "@honeybbq/teamspeak";
|
|
385
|
-
|
|
386
|
-
const client = new Client(identity, "ts.example.com", "Bot", {
|
|
387
|
-
logger: consoleLogger, // silent unless set
|
|
388
|
-
resolver: customResolver,
|
|
389
|
-
commandMiddleware: [timing],
|
|
390
|
-
eventMiddleware: [filterMiddleware],
|
|
391
|
-
serverPassword: process.env.TEAMSPEAK_SERVER_PASSWORD,
|
|
392
|
-
defaultChannel: "Lobby",
|
|
393
|
-
defaultChannelPassword: process.env.TEAMSPEAK_DEFAULT_CHANNEL_PASSWORD,
|
|
394
|
-
});
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
Address resolution — the built-in one or a custom `resolver` — has ten seconds
|
|
398
|
-
in all: the `signal` a resolver is given aborts then, and the client connects
|
|
399
|
-
to the address as given.
|
|
400
|
-
|
|
401
|
-
`commandMiddleware` wraps the typed commands: each middleware receives the
|
|
402
|
-
request (its command name and parameters) with its options and returns the
|
|
403
|
-
answering rows, so it can time, retry or answer a command itself. Raw `exec`
|
|
404
|
-
and `execNoWait` lines do not pass through it.
|
|
405
|
-
|
|
406
|
-
```typescript
|
|
407
|
-
import type { CommandMiddleware } from "@honeybbq/teamspeak";
|
|
408
|
-
|
|
409
|
-
const timing: CommandMiddleware = (next) => async (request, options) => {
|
|
410
|
-
const started = Date.now();
|
|
411
|
-
try {
|
|
412
|
-
return await next(request, options);
|
|
413
|
-
} finally {
|
|
414
|
-
console.log(request.command, Date.now() - started, "ms");
|
|
415
|
-
}
|
|
416
|
-
};
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
Connection auth options:
|
|
420
|
-
|
|
421
|
-
- `serverPassword`: plain-text server password; sent as the TeamSpeak protocol hash during `clientinit`
|
|
422
|
-
- `defaultChannel`: default channel name requested during `clientinit`
|
|
423
|
-
- `defaultChannelPassword`: plain-text password for `defaultChannel`; sent as the TeamSpeak protocol hash
|
|
424
|
-
|
|
425
|
-
### Client version
|
|
426
|
-
|
|
427
|
-
By default `clientinit` sends a recent Windows version. The `version` option
|
|
428
|
-
sends another, and `@honeybbq/teamspeak/versions` has one export for each
|
|
429
|
-
version that is known to work, named `<Platform>_<version>_<build>`:
|
|
430
|
-
|
|
431
|
-
```typescript
|
|
432
|
-
import { Client } from "@honeybbq/teamspeak/client";
|
|
433
|
-
import { Windows_3_6_2_1695203293 } from "@honeybbq/teamspeak/versions";
|
|
434
|
-
|
|
435
|
-
const client = new Client(identity, "ts.example.com", "Bot", {
|
|
436
|
-
version: Windows_3_6_2_1695203293,
|
|
437
|
-
});
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
A `ClientVersion` is a platform, a version string and a signature, and only
|
|
441
|
-
the ones in that entry carry a signature a server accepts. A server refuses a
|
|
442
|
-
build below its configured minimum, so an old version fails to connect on a
|
|
443
|
-
server that has raised it. `new Client` throws when a field is empty or the
|
|
444
|
-
signature is not the base64 of 64 bytes. Importing `@honeybbq/teamspeak/client`
|
|
445
|
-
does not load the table; only the entry you import it from does.
|
|
446
|
-
|
|
447
|
-
### Tracing
|
|
448
|
-
|
|
449
|
-
`trace` takes hooks that are called at each milestone of the client's life, for timing, metrics, logs or a progress display. For each connection attempt, the one `connect()` starts and each one the reconnect loop makes, the milestones come in this order: `resolveStart`, `resolveDone`, `connectStart`, `connectDone`, `handshakeDone`, `sessionStart`, `welcomeDone`. An attempt that never gets a session ends with `attemptFailed` instead, which says in which phase (`"open"`, `"handshake"` or `"join"`) and why. When a session ends, `sessionEnd` is called with the cause (`undefined` after `disconnect()`), preceded on a `disconnect()` by `disconnectStart` and `disconnectDone`, which says whether the server acknowledged the goodbye. `reconnectScheduled` fires when the reconnect loop is about to wait for its next attempt, and `reconnectStopped` when it ends without a session. Every attempt ends with exactly one of `sessionStart` and `attemptFailed`, and every session with exactly one `sessionEnd`.
|
|
450
|
-
|
|
451
|
-
The infos carry what each milestone knows: the resolver's candidates, the local address, the RSA puzzle's level and how long it took, how many packets were sent again, the server's public key (to pin a server's identity), the nickname the server accepted, the server's `initserver` parameters without any secret, and where the client landed with how many channels and clients the welcome delivered. Durations are in milliseconds.
|
|
452
|
-
|
|
453
|
-
```ts
|
|
454
|
-
const client = new Client(identity, "ts.example.com", "Bot", {
|
|
455
|
-
trace: {
|
|
456
|
-
resolveDone: (i) => console.log(`resolved to ${i.target} in ${i.durationMs.toFixed(0)} ms`),
|
|
457
|
-
handshakeDone: (i) =>
|
|
458
|
-
console.log(`encrypted after ${i.durationMs.toFixed(0)} ms, ${i.resends} resends`),
|
|
459
|
-
sessionStart: (i) => console.log(`joined as ${i.nickname} after ${i.durationMs.toFixed(0)} ms`),
|
|
460
|
-
welcomeDone: (i) => console.log(`${i.channels} channels, ${i.clients} clients in view`),
|
|
461
|
-
attemptFailed: (i) => console.log(`attempt ${i.attempt} failed while ${i.phase}:`, i.error),
|
|
462
|
-
sessionEnd: (i) => console.log(`session over after ${(i.durationMs / 1000).toFixed(0)} s`),
|
|
463
|
-
reconnectScheduled: (i) => console.log(`retry ${i.attempt} in ${i.delayMs} ms`),
|
|
464
|
-
},
|
|
465
|
-
});
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
A file transfer has a trace of its own, given in the options of `uploadFileData`, `downloadFileData` and `dialFileTransfer` (and of the client's methods that forward to them): `dialStart`, `dialDone`, `progress` (at most once per 64 KiB, and once more at the end with the final count), then exactly one of `done` and `failed`, which names the stage (`"dial"` or `"transfer"`). The transfer key never appears in any of it.
|
|
469
|
-
|
|
470
|
-
The rules for a hook, in both traces:
|
|
471
|
-
|
|
472
|
-
- It runs synchronously, at the moment of the milestone, and the client waits for it. Keep it quick and hand slow work to your own task.
|
|
473
|
-
- What it returns is ignored. A promise it returns is not awaited.
|
|
474
|
-
- It must not call `connect()` or `disconnect()`. To give up on an attempt, abort the signal you passed to `connect()`; to leave once connected, call `disconnect()` from a `connected` listener.
|
|
475
|
-
- It may read `client.status`, `client.clientID` and the like. `sessionStart` runs before `connect()` resolves and before `connected` is delivered, and `sessionEnd` before `disconnected`.
|
|
476
|
-
- If it throws, or the promise it returns rejects, the error is reported the way a throwing event listener's is, as an uncaught exception, and the client carries on as if nothing happened.
|
|
477
|
-
|
|
478
|
-
The object is copied when the client is made (or the transfer call starts), so changing it afterwards has no effect.
|
|
479
|
-
|
|
480
|
-
### Diagnostics
|
|
481
|
-
|
|
482
|
-
The client logs nothing unless given a `logger`. For tracing, it publishes on
|
|
483
|
-
two [`diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html)
|
|
484
|
-
channels, which cost nothing without a subscriber:
|
|
485
|
-
|
|
486
|
-
- `honeybbq:teamspeak:command` (`commandChannel`), a tracing channel: `start`
|
|
487
|
-
when a command is sent, `asyncEnd` when its answer arrives, `error` when it
|
|
488
|
-
fails.
|
|
489
|
-
- `honeybbq:teamspeak:incoming` (`incomingChannel`): every command the server sends.
|
|
490
|
-
|
|
491
|
-
Both carry the command's name, never its parameters, which hold password hashes
|
|
492
|
-
and privilege keys.
|
|
493
|
-
|
|
494
|
-
### Reconnection
|
|
495
|
-
|
|
496
|
-
Off by default: a silent reconnect makes an outage look like a healthy session.
|
|
497
|
-
An explicit `disconnect()` never reconnects, whatever the policy says.
|
|
498
|
-
|
|
499
|
-
```typescript
|
|
500
|
-
const client = new Client(identity, "ts.example.com", "Bot", {
|
|
501
|
-
reconnect: {
|
|
502
|
-
backoff: { initialDelayMs: 1_000, maxDelayMs: 60_000, factor: 2, jitter: 0.2 },
|
|
503
|
-
// Optional: decide for yourself which failures are worth retrying.
|
|
504
|
-
shouldRetry: (err, attempt) => attempt < 10,
|
|
505
|
-
},
|
|
506
|
-
});
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
The defaults are conservative because TeamSpeak **bans an IP after roughly
|
|
510
|
-
three rapid connects** — a retry loop without a delay bans itself, and the ban
|
|
511
|
-
outlasts the outage it was recovering from. Recovery is also not instant even
|
|
512
|
-
when it succeeds: the server holds the dropped session for a while and refuses a
|
|
513
|
-
reconnect with `521 too many clones already connected` until it reaps it, so the
|
|
514
|
-
loop keeps trying rather than giving up on the first refusal. It stops once a
|
|
515
|
-
`connect()` of your own has connected the client, and it does not retry an
|
|
516
|
-
address that cannot be one (`InvalidAddressError`).
|
|
517
|
-
|
|
518
|
-
### Custom transports
|
|
519
|
-
|
|
520
|
-
`fileTransferSocketFactory` and `socketFactory` are the only seams into the
|
|
521
|
-
network layer. Without them the SDK always creates its own sockets, so an
|
|
522
|
-
application running behind a proxy has no way through. `socketFactory` returns
|
|
523
|
-
a `DatagramSocket` — a `node:dgram` socket, or anything with the few methods
|
|
524
|
-
it names — and `fileTransferSocketFactory` any `Duplex` stream, such as the
|
|
525
|
-
socket a SOCKS library returns.
|
|
526
|
-
|
|
527
|
-
```typescript
|
|
528
|
-
const client = new Client(identity, "ts.example.com", "Bot", {
|
|
529
|
-
// UDP, for the voice and command channel.
|
|
530
|
-
socketFactory: (family) => createSocket(family === 6 ? "udp6" : "udp4"),
|
|
531
|
-
// TCP, dialled separately by each file transfer.
|
|
532
|
-
fileTransferSocketFactory: (host, port) => createConnection({ host, port }),
|
|
533
|
-
});
|
|
534
|
-
```
|
|
535
|
-
|
|
536
|
-
### Server capabilities
|
|
537
|
-
|
|
538
|
-
```typescript
|
|
539
|
-
const caps = client.serverCapabilities;
|
|
540
|
-
// {
|
|
541
|
-
// version: "6.0.0-beta13.1 [Build: 1790080330]", major: 6, family: "ts6",
|
|
542
|
-
// uidAlgorithm: "sha256", name: "TeamSpeak 6 Server", iconID: undefined,
|
|
543
|
-
// hostBannerImageURL: undefined, welcomeMessage: "…"
|
|
544
|
-
// }
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
Read from `initserver` during the handshake, so it costs no extra round trip.
|
|
548
|
-
The uid hash matters because TS3 hashes a client's public key with SHA-1 and TS6
|
|
549
|
-
with SHA-256 — a client that assumes either one computes an identifier the other
|
|
550
|
-
server never sends.
|
|
551
|
-
|
|
552
|
-
### Streams
|
|
553
|
-
|
|
554
|
-
TeamSpeak 6 clients can share a screen, a window or a camera. `@honeybbq/teamspeak/stream`
|
|
555
|
-
does the signalling of that: it finds the streams of the clients in view, publishes one,
|
|
556
|
-
and watches one. The video itself travels between the publisher and each viewer over
|
|
557
|
-
WebRTC, which is yours: `Streams` hands you the SDP and the ICE candidates as strings and
|
|
558
|
-
takes them back, so any WebRTC stack plugs in, in a browser, in Electron or in Node. The
|
|
559
|
-
package has no WebRTC dependency.
|
|
560
|
-
|
|
561
|
-
TeamSpeak does not document this protocol. It was verified against the TeamSpeak 6 server
|
|
562
|
-
6.0.0-beta13.1 and the TeamSpeak 6 client 6.0.0-beta5 (macOS), and a TeamSpeak update may
|
|
563
|
-
break it. A TeamSpeak 3 server does not answer the stream
|
|
564
|
-
commands at all, so `publish` and `watch` throw a `StreamsUnsupportedError` before sending
|
|
565
|
-
anything.
|
|
566
|
-
|
|
567
|
-
Watching: find a stream in `streams.list()` or wait for `started`, ask to watch it, and
|
|
568
|
-
give the publisher's offer to a peer connection. The viewer's connection is receive-only.
|
|
569
|
-
|
|
570
|
-
```ts
|
|
571
|
-
import { Streams } from "@honeybbq/teamspeak/stream";
|
|
572
|
-
|
|
573
|
-
const streams = new Streams(client);
|
|
574
|
-
streams.on("started", (info) => console.log(`${info.name} is streaming`));
|
|
575
|
-
|
|
576
|
-
const view = await streams.watch(info, { message: "may I watch?" });
|
|
577
|
-
|
|
578
|
-
const pc = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.l.google.com:19302" }] });
|
|
579
|
-
pc.ontrack = (event) => (video.srcObject = event.streams[0]);
|
|
580
|
-
pc.onicecandidate = (event) => {
|
|
581
|
-
if (event.candidate) void view.sendCandidate(event.candidate.toJSON());
|
|
582
|
-
};
|
|
583
|
-
// Candidates that arrived with the offer are delivered to the first listener.
|
|
584
|
-
view.on("candidate", (candidate) => void pc.addIceCandidate(candidate));
|
|
585
|
-
view.on("closed", () => pc.close());
|
|
586
|
-
|
|
587
|
-
await pc.setRemoteDescription({ type: "offer", sdp: view.offer });
|
|
588
|
-
const answer = await pc.createAnswer();
|
|
589
|
-
await pc.setLocalDescription(answer);
|
|
590
|
-
await view.answer(answer.sdp!);
|
|
591
|
-
```
|
|
592
|
-
|
|
593
|
-
Publishing: the publisher makes the offer, one peer connection for each viewer. The server
|
|
594
|
-
relays every request to watch, from any channel, and enforces neither `viewerLimit` nor
|
|
595
|
-
`accessibility`: deciding who is let in is the publisher's job, and `request` is emitted
|
|
596
|
-
for each one, with nothing accepted for you.
|
|
597
|
-
|
|
598
|
-
```ts
|
|
599
|
-
const stream = await streams.publish({ name: "My screen", source: "screen" });
|
|
600
|
-
|
|
601
|
-
stream.on("request", async (request) => {
|
|
602
|
-
if (!allowed(request.viewer)) return request.deny("Not today");
|
|
603
|
-
|
|
604
|
-
const pc = new RTCPeerConnection({ iceServers });
|
|
605
|
-
for (const track of capture.getTracks()) pc.addTrack(track, capture);
|
|
606
|
-
// Always offer audio, even with no track to send (see the interop notes below).
|
|
607
|
-
if (!pc.getTransceivers().some((t) => t.receiver.track.kind === "audio")) {
|
|
608
|
-
pc.addTransceiver("audio", { direction: "sendonly" });
|
|
609
|
-
}
|
|
610
|
-
await pc.setLocalDescription(await pc.createOffer());
|
|
611
|
-
|
|
612
|
-
const viewer = await request.accept(pc.localDescription!.sdp);
|
|
613
|
-
pc.onicecandidate = (event) => {
|
|
614
|
-
if (event.candidate) void viewer.sendCandidate(event.candidate.toJSON());
|
|
615
|
-
};
|
|
616
|
-
viewer.on("answer", (sdp) => pc.setRemoteDescription({ type: "answer", sdp }));
|
|
617
|
-
viewer.on("candidate", (candidate) => void pc.addIceCandidate(candidate));
|
|
618
|
-
viewer.on("closed", () => pc.close());
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
await stream.stop();
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
A request that nobody has answered can end: the viewer withdraws it or goes out of view, or
|
|
625
|
-
the stream ends. `request.on("closed", ({ cause }) => …)` fires then, with `"withdrawn"` or
|
|
626
|
-
`"ended"`, so a UI can drop the request without polling `request.state`; `accept` and `deny`
|
|
627
|
-
then throw. It is not emitted for a request that was accepted or denied.
|
|
628
|
-
|
|
629
|
-
#### Interop with the TeamSpeak 6 client
|
|
630
|
-
|
|
631
|
-
What follows was observed with the TeamSpeak 6 client 6.0.0-beta5 (macOS) against a
|
|
632
|
-
TeamSpeak 6 server 6.0.0-beta13.1.
|
|
633
|
-
|
|
634
|
-
- **Offer audio, always.** When you publish, the client answers an offer only if it has an
|
|
635
|
-
`m=audio` section. For a video-only offer it never answers: it shows a fallback picture and
|
|
636
|
-
keeps the "Join" button, which then does nothing, because the server already counts the
|
|
637
|
-
viewer as joined. Add a send-only audio transceiver even when there is no audio track; an
|
|
638
|
-
audio section before the video section was accepted. `accept` and `ViewerPeer.sendOffer`
|
|
639
|
-
send an offer without audio unchanged, and emit a `"warning"` event on `Streams` with the
|
|
640
|
-
code `"offer-without-audio"`.
|
|
641
|
-
- **Trim the codecs.** The client's own offers carry one video and one audio section, about
|
|
642
|
-
3.9 KB. A browser's offer with every codec is about 6 KB with audio and video, and about
|
|
643
|
-
4.8 KB for video alone, against a limit of about 9.2 KB for a command line. Narrowing the
|
|
644
|
-
codecs keeps room for the candidates and the message:
|
|
645
|
-
|
|
646
|
-
```ts
|
|
647
|
-
const video = pc.addTransceiver(track, { direction: "sendonly" });
|
|
648
|
-
const codecs = RTCRtpSender.getCapabilities("video")!.codecs;
|
|
649
|
-
video.setCodecPreferences(codecs.filter((c) => /^video\/(VP8|H264)$/.test(c.mimeType)));
|
|
650
|
-
```
|
|
651
|
-
|
|
652
|
-
- **Accept every request.** A viewer in the client shows "waiting" until the publisher
|
|
653
|
-
answers. When it withdraws and asks again, that is a new request, with a new `JoinRequest`.
|
|
654
|
-
- **A channel switch ends the viewing, not the stream.** The client leaves a stream it
|
|
655
|
-
watches when it changes channel, so the publisher sees that viewer leave. The server does not end a stream when its publisher or a viewer changes channel.
|
|
656
|
-
- Denying, stopping, leaving, disconnecting and several viewers at once, some of them the
|
|
657
|
-
client and some the SDK, behaved as they do between SDK clients.
|
|
658
|
-
|
|
659
|
-
```ts
|
|
660
|
-
streams.on("warning", (w) => console.warn(`${w.code}: ${w.message}`));
|
|
661
|
-
```
|
|
662
|
-
|
|
663
|
-
Things to know:
|
|
664
|
-
|
|
665
|
-
- A client may run several streams, so every stream is known by its id. `list()` is what
|
|
666
|
-
this client has heard of. A stream that ends while its publisher is out of the channel is
|
|
667
|
-
reported to nobody there, and stays listed until the connection ends.
|
|
668
|
-
- A stream survives its publisher changing channel, with no event. It ends when the
|
|
669
|
-
publisher stops it or disconnects.
|
|
670
|
-
- The server drops a command line longer than about 9.2 KB without an answer. Streams
|
|
671
|
-
commands are checked first and throw a `StreamPayloadTooLargeError`; an SDP offer or an
|
|
672
|
-
ICE candidate is a few kilobytes at most.
|
|
673
|
-
- The server lets anyone stop any stream. `Streams` sends `stopstream` only for streams it
|
|
674
|
-
published, and treats a `notifystreamstopped` for any stream as final.
|
|
675
|
-
- The server remembers no decision, so a client can ask again, and every ask is relayed.
|
|
676
|
-
To refuse for good, pass `rememberDenied: true` (the 1024 most recent uids, in memory
|
|
677
|
-
only; a requester out of view is looked up with `clientgetuidfromclid`), or leave the
|
|
678
|
-
request unanswered. The server has no way to remove a viewer: closing a viewer's peer
|
|
679
|
-
ends your side of it, and stopping the stream ends it for everyone.
|
|
680
|
-
- `dispose()` stops the streams published through that `Streams` and leaves the ones it
|
|
681
|
-
watched, while the client is connected. A stream whose `publish` gave up after the command
|
|
682
|
-
went out is stopped when its announcement arrives, so none is left running unseen.
|
|
683
|
-
- A client whose `client_is_streaming` clears, or that comes into view not streaming, has
|
|
684
|
-
its streams dropped, after a grace of two seconds: `stopped` with the cause `"gone"`. The
|
|
685
|
-
server clears the flag right before it reports the stop, so a stop that arrives in that time
|
|
686
|
-
ends the stream with its own cause and reason instead, and a flag set again cancels the drop.
|
|
687
|
-
- The bitrate the server reports for a stream is not the one requested.
|
|
688
|
-
- A client announces that it streams only with `client_is_streaming`, and a stream that was
|
|
689
|
-
running before you came is not announced at all. `Streams` asks for the streams of each
|
|
690
|
-
client that sets the flag, once, on a TeamSpeak 6 server.
|
|
691
|
-
|
|
692
|
-
## Design
|
|
693
|
-
|
|
694
|
-
The conventions below hold across the whole package. Each is stated once here.
|
|
695
|
-
|
|
696
|
-
### Errors
|
|
697
|
-
|
|
698
|
-
An error is one of two kinds:
|
|
699
|
-
|
|
700
|
-
- **A refusal the server reports.** It is a `ServerError` carrying the server's
|
|
701
|
-
numeric `code`, its text as `serverMessage`, and `extraMessage` and
|
|
702
|
-
`failedPermissionID` when the server names them.
|
|
703
|
-
- **A condition the SDK detects itself**: not connected, no data, a timeout, a
|
|
704
|
-
failed handshake, a failed file transfer. Each has its own class.
|
|
705
|
-
|
|
706
|
-
Every error the SDK throws on purpose extends `TeamspeakError`, so
|
|
707
|
-
`instanceof TeamspeakError` tells its failures from anything else. A failure
|
|
708
|
-
that wraps another keeps it in `cause`. Match a refusal by `error.code` against
|
|
709
|
-
`ErrorCode`, and everything else by class:
|
|
710
|
-
|
|
711
|
-
```typescript
|
|
712
|
-
import {
|
|
713
|
-
ErrorCode,
|
|
714
|
-
NotConnectedError,
|
|
715
|
-
ServerError,
|
|
716
|
-
TeamspeakError,
|
|
717
|
-
asChannelID,
|
|
718
|
-
moveClient,
|
|
719
|
-
} from "@honeybbq/teamspeak";
|
|
720
|
-
|
|
721
|
-
try {
|
|
722
|
-
await moveClient(client, client.clientID, asChannelID(5), { password: "wrong" });
|
|
723
|
-
} catch (err) {
|
|
724
|
-
if (err instanceof ServerError && err.code === ErrorCode.ChannelInvalidPassword) {
|
|
725
|
-
// ask for the password again
|
|
726
|
-
} else if (err instanceof NotConnectedError) {
|
|
727
|
-
// the connection is gone
|
|
728
|
-
} else if (err instanceof ServerError) {
|
|
729
|
-
console.log(err.code, err.serverMessage, err.extraMessage, err.failedPermissionID);
|
|
730
|
-
} else if (err instanceof TeamspeakError) {
|
|
731
|
-
console.log(err.name, err.message, err.cause);
|
|
732
|
-
} else {
|
|
733
|
-
throw err;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
```
|
|
737
|
-
|
|
738
|
-
`ErrorCode` names the server's codes as constants. It is the table `errcode`
|
|
739
|
-
is in the Go SDK, with the same numbers. A server may send a code it does not
|
|
740
|
-
name, so `code` is a plain `number`. `instanceof` also works between two
|
|
741
|
-
installed copies of the package.
|
|
742
|
-
|
|
743
|
-
Errors live with the code that produces them, so each entry point exports only
|
|
744
|
-
the errors its own code throws:
|
|
745
|
-
|
|
746
|
-
| Entry point | Errors |
|
|
747
|
-
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
748
|
-
| `@honeybbq/teamspeak` (shared by every connection) | `TeamspeakError`, `ServerError`, `CommandTimeoutError`, `NotConnectedError`, `AlreadyConnectedError`, `NoDataError` |
|
|
749
|
-
| `@honeybbq/teamspeak/client` (voice only) | `ConnectionTimeoutError`, `HandshakeError` and its `PuzzleTooHardError`, `InvalidAddressError`, `InvalidIdentityError`, `FileTransferError`, `RemovedError` (kick, ban or server stop) |
|
|
750
|
-
| `@honeybbq/teamspeak/stream` (streams only) | `StreamsUnsupportedError`, `StreamJoinDeniedError`, `StreamEndedError`, `StreamPayloadTooLargeError` |
|
|
751
|
-
| `@honeybbq/teamspeak/query` (query only) | `QueryAuthError`, `GuestAccessError`, `SshHostKeyError`, `MissingDependencyError`, `WebQueryHttpError` |
|
|
752
|
-
|
|
753
|
-
A call that is given a value it cannot use, such as an out-of-range id or an
|
|
754
|
-
option that contradicts another, throws the language's own `TypeError` or
|
|
755
|
-
`RangeError` before anything is sent. Those are not `TeamspeakError`s: they are
|
|
756
|
-
a bug in the caller, not a condition to handle. `WebQueryHttpError` carries the
|
|
757
|
-
HTTP `status`; a refusal the server itself reports is a `ServerError` over every
|
|
758
|
-
transport.
|
|
759
|
-
|
|
760
|
-
**Cancellation.** An aborted `signal` is the caller's own cancellation, not an
|
|
761
|
-
SDK error. `connect()`, the typed commands, `exec`, file transfers and `events()`
|
|
762
|
-
reject with the signal's `reason` itself, unwrapped: a `DOMException` named
|
|
763
|
-
`AbortError` after a bare `abort()`, a `TimeoutError` after
|
|
764
|
-
`AbortSignal.timeout()`, or whatever value was passed to `abort(reason)`. A
|
|
765
|
-
signal that is already aborted rejects before anything is sent. Aborting a
|
|
766
|
-
`connect()` abandons the attempt and leaves the client disconnected; a command
|
|
767
|
-
that was given up on may still have taken effect on the server. The signal and
|
|
768
|
-
the command's own `timeoutMs` are separate: when `timeoutMs` (ten seconds by
|
|
769
|
-
default) passes first, the command rejects with a `CommandTimeoutError`.
|
|
770
|
-
|
|
771
|
-
**No data.** `NoDataError` means the command succeeded but its answer lacked the
|
|
772
|
-
data it exists to return, such as the id of a channel just created. Treat the
|
|
773
|
-
command as having taken effect and do not retry it blindly, or it creates a
|
|
774
|
-
second object or is refused as a duplicate. `createChannel` and the group
|
|
775
|
-
commands look the new object up themselves and throw it only when nothing
|
|
776
|
-
matches. A list that comes back empty is `[]`, not this error.
|
|
777
|
-
|
|
778
|
-
### Ids
|
|
779
|
-
|
|
780
|
-
Every kind of id is a branded type: `ChannelID`, `ClientID`, `ClientDBID`,
|
|
781
|
-
`ServerGroupID`, `ChannelGroupID`, `PermissionID`, `IconID`, `ServerID`,
|
|
782
|
-
`BanID` and the rest, with `UID` and `ChannelUID` as branded strings. The
|
|
783
|
-
brand costs nothing at runtime, and passing the wrong kind is a compile error.
|
|
784
|
-
Make one with its constructor, which throws a `RangeError` for a value outside
|
|
785
|
-
the id's wire range:
|
|
786
|
-
|
|
787
|
-
```typescript
|
|
788
|
-
import { asChannelID, asClientID, moveClient } from "@honeybbq/teamspeak";
|
|
789
|
-
|
|
790
|
-
const cid = asChannelID(5); // or asChannelID(5n)
|
|
791
|
-
await moveClient(client, client.clientID, cid);
|
|
792
|
-
// moveClient(client, cid, client.clientID) does not compile.
|
|
793
|
-
const clid = asClientID(3);
|
|
794
|
-
```
|
|
795
|
-
|
|
796
|
-
A `ClientID`, `PermissionID`, `IconID` and the file transfer ids are `number`s.
|
|
797
|
-
The ones the protocol makes 64 bits wide are `bigint`s: `ChannelID`,
|
|
798
|
-
`ClientDBID`, `ServerGroupID`, `ChannelGroupID`, `ServerID`, `BanID`,
|
|
799
|
-
`MessageID` and `APIKeyID`. Their constructors take a `number` or a `bigint`.
|
|
800
|
-
|
|
801
|
-
### Options and absent values
|
|
802
|
-
|
|
803
|
-
**Options.** Required arguments are positional. Everything optional is one
|
|
804
|
-
options object, the last argument. A field that is not set is not sent:
|
|
805
|
-
`undefined` leaves the setting alone and `""` sends it empty.
|
|
806
|
-
|
|
807
|
-
```typescript
|
|
808
|
-
import { editChannel } from "@honeybbq/teamspeak";
|
|
809
|
-
|
|
810
|
-
await editChannel(client, cid, {
|
|
811
|
-
name: "Lounge",
|
|
812
|
-
topic: "", // sent empty: clears the topic
|
|
813
|
-
// password is not set: not sent, so the password stays as it is.
|
|
814
|
-
});
|
|
815
|
-
```
|
|
816
|
-
|
|
817
|
-
**Records.** In a record the server's answer fills in, an optional field is
|
|
818
|
-
`undefined` when the server did not send it. That is not the same as `0` or
|
|
819
|
-
`false`: `totalClients` is `undefined` in a `getChannelInfo` result, which does
|
|
820
|
-
not carry it, and `0` in a `listChannels` result of an empty channel.
|
|
821
|
-
|
|
822
|
-
```typescript
|
|
823
|
-
import { getChannelInfo } from "@honeybbq/teamspeak";
|
|
824
|
-
|
|
825
|
-
const info = await getChannelInfo(client, cid);
|
|
826
|
-
if (info.totalClients !== undefined) console.log(info.totalClients, "clients");
|
|
827
|
-
```
|
|
828
|
-
|
|
829
|
-
**Durations and times** are numbers of milliseconds with an `Ms` suffix
|
|
830
|
-
(`idleTimeMs`, `timeoutMs`) and `Date`. A `Date` field is `undefined` where the
|
|
831
|
-
server sends 0 for "never". The Go SDK uses `time.Duration` and `time.Time`.
|
|
832
|
-
|
|
833
|
-
### Events
|
|
834
|
-
|
|
835
|
-
Each server event has a typed name (`"textMessage"`, `"clientEnter"`, ...;
|
|
836
|
-
`EventName` has a constant for each), and `"notification"` delivers every
|
|
837
|
-
notification as the server sent it, including those no typed event models. The
|
|
838
|
-
notification arrives after the typed event it produced. `on()` returns a
|
|
839
|
-
function that removes the listener; `events(name, { signal })` is the same as an
|
|
840
|
-
async iterator.
|
|
841
|
-
|
|
842
|
-
```typescript
|
|
843
|
-
const off = client.on("textMessage", (message) => {
|
|
844
|
-
console.log(message.invokerName, message.message);
|
|
845
|
-
});
|
|
846
|
-
off();
|
|
847
|
-
|
|
848
|
-
client.on("notification", (n) => console.log(n.name, n.params));
|
|
849
|
-
```
|
|
850
|
-
|
|
851
|
-
- **Order.** Listeners run one at a time, in the order the events arrived, so a
|
|
852
|
-
listener may call the client and await an answer.
|
|
853
|
-
- **State.** The client updates its state, such as `client.view`, before it
|
|
854
|
-
queues the event that reports the change, so a listener sees state at least as
|
|
855
|
-
new as its event.
|
|
856
|
-
- **A throw** in a listener does not stop the others. The exception is rethrown
|
|
857
|
-
from a microtask, as an uncaught exception, as an EventEmitter listener's
|
|
858
|
-
would be. The Go SDK recovers a panicking handler and logs it instead.
|
|
859
|
-
- **Middleware.** `eventMiddleware` sees each server event before the listeners
|
|
860
|
-
and may pass it on, replace it or drop it; `connected`, `disconnected`,
|
|
861
|
-
`kicked` and the view events bypass it. `commandMiddleware` wraps the typed commands. Both are
|
|
862
|
-
described above.
|
|
863
|
-
|
|
864
|
-
### Stability
|
|
865
|
-
|
|
866
|
-
From 1.0 the package follows semver, as described under
|
|
867
|
-
[Compatibility](#compatibility): a breaking change to `@honeybbq/teamspeak`,
|
|
868
|
-
`/client` and `/versions` waits for 2.0. The enumerations, `ErrorCode` and the
|
|
869
|
-
event map may gain members in a minor release; give a `switch` over them a
|
|
870
|
-
default case.
|
|
871
|
-
|
|
872
|
-
An API that may still change in a minor release is marked `@experimental` in its TSDoc,
|
|
873
|
-
which editors and the API reference show. Today that is the whole
|
|
874
|
-
`@honeybbq/teamspeak/query` entry point, the server view (`client.view` and the
|
|
875
|
-
view events), the `previous` field of the client-left and channel-deleted
|
|
876
|
-
events, and `getServerVariables`.
|
|
877
|
-
|
|
878
|
-
### Server behaviour the SDK works around
|
|
879
|
-
|
|
880
|
-
The servers answer some commands in ways that differ between TeamSpeak 3 and 6
|
|
881
|
-
and between transports, and the SDK evens those out. The measured tables are in
|
|
882
|
-
[voice-protocol-responses.md](https://github.com/HoneyBBQ/teamspeak-js/blob/main/docs/voice-protocol-responses.md)
|
|
883
|
-
and [serverquery-responses.md](https://github.com/HoneyBBQ/teamspeak-js/blob/main/docs/serverquery-responses.md).
|
|
884
|
-
|
|
885
|
-
## ServerQuery
|
|
886
|
-
|
|
887
|
-
The `@experimental` subpath `@honeybbq/teamspeak/query` administers a server
|
|
888
|
-
the way the voice protocol cannot: the same typed commands the core exports,
|
|
889
|
-
plus the ones only an administrator may run, over the three transports a
|
|
890
|
-
ServerQuery speaks.
|
|
891
|
-
|
|
892
|
-
| Transport | Where | Server | Login | Notifications |
|
|
893
|
-
| --------- | --------------------------- | ---------------- | ----------------- | ------------- |
|
|
894
|
-
| `tcp` | 10011 | TeamSpeak 3 only | `transport.login` | yes |
|
|
895
|
-
| `ssh` | 10022 | both | the SSH login | yes |
|
|
896
|
-
| `web` | 10080 (HTTP), 10443 (HTTPS) | both | the api key | **no** |
|
|
897
|
-
|
|
898
|
-
TeamSpeak 3 (3.12 and later for WebQuery, 3.3 and later for SSH) serves SSH and
|
|
899
|
-
WebQuery as well as raw TCP, but only the raw port is on until
|
|
900
|
-
`query_protocols` lists the others; the official Docker image takes it as
|
|
901
|
-
`TS3SERVER_QUERY_PROTOCOLS=raw,ssh,http`. TeamSpeak 6 has no raw port. Its
|
|
902
|
-
WebQuery also answers a request with no api key as a guest; TeamSpeak 3 refuses
|
|
903
|
-
one.
|
|
904
|
-
|
|
905
|
-
WebQuery is reached over `http:` or `https:`, by the scheme of `url`. TeamSpeak 3
|
|
906
|
-
serves HTTPS on 10443 once `query_protocols` lists `https` and
|
|
907
|
-
`query_https_certificate_file` and `query_https_private_key_file` are set. The
|
|
908
|
-
built-in `fetch` verifies the certificate against the trust store Node uses and
|
|
909
|
-
never skips that. A certificate from a private CA, or a self-signed one, is
|
|
910
|
-
trusted by starting Node with `NODE_EXTRA_CA_CERTS=ca.pem`, or by a `fetch`
|
|
911
|
-
that trusts the CA:
|
|
912
|
-
|
|
913
|
-
```typescript
|
|
914
|
-
import { readFileSync } from "node:fs";
|
|
915
|
-
import { Agent, request } from "node:https";
|
|
916
|
-
|
|
917
|
-
const agent = new Agent({ ca: readFileSync("ca.pem") });
|
|
918
|
-
const trusting = ((url: string, init: RequestInit) =>
|
|
919
|
-
new Promise<Response>((resolve, reject) => {
|
|
920
|
-
const req = request(
|
|
921
|
-
url,
|
|
922
|
-
{ method: init.method, headers: init.headers as never, agent },
|
|
923
|
-
(res) => {
|
|
924
|
-
const chunks: Buffer[] = [];
|
|
925
|
-
res.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
926
|
-
res.on("end", () =>
|
|
927
|
-
resolve(new Response(Buffer.concat(chunks), { status: res.statusCode })),
|
|
928
|
-
);
|
|
929
|
-
},
|
|
930
|
-
);
|
|
931
|
-
req.on("error", reject);
|
|
932
|
-
req.end(init.body as string);
|
|
933
|
-
})) as typeof fetch;
|
|
934
|
-
|
|
935
|
-
new QueryClient({
|
|
936
|
-
transport: { kind: "web", url: "https://ts.example.com:10443", apiKey, fetch: trusting },
|
|
937
|
-
});
|
|
938
|
-
```
|
|
939
|
-
|
|
940
|
-
Only raw TCP and SSH carry notifications: the server pushes them down the same
|
|
941
|
-
connection. WebQuery has no session to push to and refuses to register, so the
|
|
942
|
-
client never tries, and a listener for a server event there throws rather than
|
|
943
|
-
sitting silent. `connected` and `disconnected` work on every transport, since
|
|
944
|
-
they come from the client itself.
|
|
945
|
-
|
|
946
|
-
```typescript
|
|
947
|
-
import { QueryClient } from "@honeybbq/teamspeak/query";
|
|
948
|
-
import { listChannels } from "@honeybbq/teamspeak";
|
|
949
|
-
|
|
950
|
-
const client = new QueryClient({
|
|
951
|
-
transport: {
|
|
952
|
-
kind: "tcp", // 10011
|
|
953
|
-
host: "ts.example.com",
|
|
954
|
-
login: { name: "serveradmin", password: process.env.QUERY_PASSWORD! },
|
|
955
|
-
},
|
|
956
|
-
server: { id: 1 },
|
|
957
|
-
nickname: "bot",
|
|
958
|
-
events: { categories: ["textserver", "textprivate"] },
|
|
959
|
-
reconnect: { backoff: { initialDelayMs: 1_000 } },
|
|
960
|
-
});
|
|
961
|
-
|
|
962
|
-
client.on("textMessage", (message) => console.log(message.invokerName, message.message));
|
|
963
|
-
|
|
964
|
-
await client.connect();
|
|
965
|
-
|
|
966
|
-
for (const channel of await listChannels(client)) console.log(channel.name);
|
|
967
|
-
|
|
968
|
-
await client.disconnect();
|
|
969
|
-
```
|
|
970
|
-
|
|
971
|
-
The three transports are figures of speech the client rebuilds on every
|
|
972
|
-
connection, which is what makes reconnecting possible; `custom` opens one
|
|
973
|
-
itself, for a proxy, a tunnel or a test. Each built-in transport also offers
|
|
974
|
-
the seam into its own connection — `connect` for `tcp`, returning a `Duplex`,
|
|
975
|
-
and `connection` for `ssh`, taking a client or a factory for one — so an
|
|
976
|
-
application behind a TLS terminator or a SOCKS proxy, or one that already
|
|
977
|
-
holds an SSH client, can supply it. `ssh2` is an optional peer dependency:
|
|
978
|
-
without it the other two transports still work, and an `ssh` transport throws
|
|
979
|
-
a `MissingDependencyError` naming the package.
|
|
980
|
-
|
|
981
|
-
A query client delivers the events its categories carry: `serverEdited` comes
|
|
982
|
-
under `server`; the channel events, `clientEnter`, `clientLeave` and
|
|
983
|
-
`clientMoved` under `channel`; and `textMessage` under the three text
|
|
984
|
-
categories. `QueryEventsOption` has the table. The voice client's events about
|
|
985
|
-
clients' updates, group changes, subscriptions, plugin commands and typing are
|
|
986
|
-
not sent to a ServerQuery connection by TeamSpeak 3 or by TeamSpeak 6 over SSH,
|
|
987
|
-
so the query client does not offer them.
|
|
988
|
-
|
|
989
|
-
`serverFamily` reports which generation the server is, read from its own
|
|
990
|
-
`version` answer, and `transport` reads back the live connection for metrics.
|
|
991
|
-
|
|
992
|
-
### SSH host keys
|
|
72
|
+
## Entry points
|
|
993
73
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
`
|
|
997
|
-
|
|
998
|
-
|
|
74
|
+
| Import | What it is | Docs |
|
|
75
|
+
| ------------------------------ | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
76
|
+
| `@honeybbq/teamspeak` | What every connection shares: the command language, ids, errors, typed commands, event types | [Design](https://honeybbq.github.io/teamspeak-js/documents/design.html) |
|
|
77
|
+
| `@honeybbq/teamspeak/client` | The voice client, identities, file transfers, diagnostics and the retry policy | [Guide](https://honeybbq.github.io/teamspeak-js/documents/guide.html) |
|
|
78
|
+
| `@honeybbq/teamspeak/stream` | TeamSpeak 6 screen-share signalling; no WebRTC code | [Streams](https://honeybbq.github.io/teamspeak-js/documents/streams.html) |
|
|
79
|
+
| `@honeybbq/teamspeak/query` | The ServerQuery client over raw TCP, SSH or WebQuery; `@experimental` | [ServerQuery](https://honeybbq.github.io/teamspeak-js/documents/serverquery.html) |
|
|
80
|
+
| `@honeybbq/teamspeak/versions` | Client versions a server accepts, for the `version` option | [Configuration](https://honeybbq.github.io/teamspeak-js/documents/configuration.html) |
|
|
999
81
|
|
|
1000
|
-
|
|
1001
|
-
const client = new QueryClient({
|
|
1002
|
-
transport: {
|
|
1003
|
-
kind: "ssh",
|
|
1004
|
-
host: "ts.example.com",
|
|
1005
|
-
username: "serveradmin",
|
|
1006
|
-
password: process.env.QUERY_PASSWORD!,
|
|
1007
|
-
knownHosts: "/etc/ssh/ssh_known_hosts",
|
|
1008
|
-
},
|
|
1009
|
-
});
|
|
1010
|
-
```
|
|
82
|
+
## Documentation
|
|
1011
83
|
|
|
1012
|
-
|
|
1013
|
-
connection is attempted and never found on its own: `~/.ssh/known_hosts` is
|
|
1014
|
-
used only if you name it. The check is `StrictHostKeyChecking=yes`: a host the
|
|
1015
|
-
file does not mention is refused, so is one recorded with another key, and so
|
|
1016
|
-
is a key a `@revoked` line names. Plain, `[host]:port` and hashed
|
|
1017
|
-
(`ssh-keygen -H`) entries are read. The query port is not 22, so record it as
|
|
1018
|
-
`ssh-keyscan -p 10022 ts.example.com`, which writes `[ts.example.com]:10022`;
|
|
1019
|
-
an entry for port 22 does not vouch for 10022.
|
|
1020
|
-
- `hostKey`: your own check, `(key) => void`, that throws to refuse. `key` has
|
|
1021
|
-
the host, port, `keyType`, the key's bytes and its `fingerprint`, the
|
|
1022
|
-
`SHA256:…` that `ssh-keygen -lf` prints. It has to be synchronous and return
|
|
1023
|
-
nothing: a returned value, a `false` included, or a promise is refused rather
|
|
1024
|
-
than guessed at.
|
|
1025
|
-
- `insecureIgnoreHostKey: true`: accept any key. It gives up what the host key
|
|
1026
|
-
is for, and is for a test or a throwaway server.
|
|
84
|
+
The full API reference is at [honeybbq.github.io/teamspeak-js](https://honeybbq.github.io/teamspeak-js/). The pages below are the long-form guides.
|
|
1027
85
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
86
|
+
- [Guide](https://honeybbq.github.io/teamspeak-js/documents/guide.html): connecting, events, commands, file transfers and identities.
|
|
87
|
+
- [Configuration](https://honeybbq.github.io/teamspeak-js/documents/configuration.html): options, client version, tracing, diagnostics, reconnection, custom transports and server capabilities.
|
|
88
|
+
- [Streams](https://honeybbq.github.io/teamspeak-js/documents/streams.html): TeamSpeak 6 screen-share signalling, and interop with the TeamSpeak 6 client.
|
|
89
|
+
- [ServerQuery](https://honeybbq.github.io/teamspeak-js/documents/serverquery.html): the query client and its SSH host-key policy.
|
|
90
|
+
- [Design](https://honeybbq.github.io/teamspeak-js/documents/design.html): errors, ids, absent values, events, stability, and the server behaviour the SDK works around.
|
|
91
|
+
- [Compatibility](https://honeybbq.github.io/teamspeak-js/documents/compatibility.html): supported environments and the differences from the Go SDK.
|
|
92
|
+
- [Architecture](https://honeybbq.github.io/teamspeak-js/documents/architecture.html): source layout and entry points.
|
|
93
|
+
- [Migrating to 1.0](https://honeybbq.github.io/teamspeak-js/documents/migrating-to-1.0.html), [voice protocol responses](https://honeybbq.github.io/teamspeak-js/documents/voice-protocol-responses.html) and [ServerQuery responses](https://honeybbq.github.io/teamspeak-js/documents/serverquery-responses.html): reference material.
|
|
1036
94
|
|
|
1037
95
|
## Compatibility
|
|
1038
96
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
text message's target mode, a group type — is a plain `number`, since a server
|
|
1045
|
-
may send a value newer than the table.
|
|
1046
|
-
|
|
1047
|
-
`DatagramSocket` describes the part of a socket the client uses; should a
|
|
1048
|
-
later release use more, the new members will be optional.
|
|
1049
|
-
|
|
1050
|
-
### Supported environments
|
|
1051
|
-
|
|
1052
|
-
| Environment | Supported |
|
|
1053
|
-
| ----------- | ------------------------------------------------------------------------------ |
|
|
1054
|
-
| Node.js | The active and maintenance LTS releases, from 22.12; CI tests 22.12, 24 and 26 |
|
|
1055
|
-
| TypeScript | 5.2 and newer, for the published types |
|
|
1056
|
-
| Modules | ESM only |
|
|
1057
|
-
|
|
1058
|
-
When a Node.js release reaches end of life, a minor release may raise
|
|
1059
|
-
`engines.node` past it; that is not a breaking change. The published types use
|
|
1060
|
-
`Disposable`, so the `lib` option must include `esnext.disposable` (or `es2026`
|
|
1061
|
-
or `esnext`), and the subpath exports need `moduleResolution` set to `node16`,
|
|
1062
|
-
`nodenext` or `bundler`. CI type-checks a consumer of the packed package with
|
|
1063
|
-
TypeScript 5.2. Raising that minimum follows TypeScript's own support window,
|
|
1064
|
-
happens in a minor release, and comes with a changelog note.
|
|
1065
|
-
|
|
1066
|
-
### Differences from the Go SDK
|
|
1067
|
-
|
|
1068
|
-
The [Go SDK](https://github.com/honeybbq/teamspeak-go) makes the same calls
|
|
1069
|
-
with the same semantics, apart from what the languages make differ:
|
|
1070
|
-
|
|
1071
|
-
- Backoff fields: JavaScript takes `undefined` as "the default" and `0` as 0.
|
|
1072
|
-
Go cannot tell an unset field from 0, so there a zero field takes the default
|
|
1073
|
-
and a negative jitter disables it.
|
|
1074
|
-
- A listener that throws: here the exception is rethrown from a microtask, an
|
|
1075
|
-
uncaught exception as an EventEmitter listener's would be, and the other
|
|
1076
|
-
listeners still get the event. Go recovers a panicking handler, logs it at
|
|
1077
|
-
Error, and keeps delivering.
|
|
1078
|
-
- Durations are milliseconds with an `Ms` suffix here, `time.Duration` in Go;
|
|
1079
|
-
`listClients`' `uid` flag (on by default) is Go's `OmitUID`.
|
|
97
|
+
| Target | Supported |
|
|
98
|
+
| ----------- | --------------------------------------------------------------------- |
|
|
99
|
+
| TeamSpeak 3 | Voice protocol and ServerQuery |
|
|
100
|
+
| TeamSpeak 6 | Voice protocol, ServerQuery and screen-share signalling |
|
|
101
|
+
| Node.js | 22.12 and newer; CI tests 22.12, 24 and 26. ESM only; TypeScript 5.2+ |
|
|
1080
102
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
| Import from | What it is |
|
|
1084
|
-
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1085
|
-
| `@honeybbq/teamspeak` | What every TeamSpeak connection shares: the command language, ids, errors, the typed commands, the event types and the status and reconnection types |
|
|
1086
|
-
| `@honeybbq/teamspeak/client` | The voice client, identities, file transfers, diagnostics and its retry policy |
|
|
1087
|
-
| `@honeybbq/teamspeak/versions` | Client versions a server accepts, for the client's `version` option; data only, one export each |
|
|
1088
|
-
| `@honeybbq/teamspeak/stream` | TeamSpeak 6 streams, the screen share: finding, publishing and watching them; signalling only, no WebRTC code |
|
|
1089
|
-
| `@honeybbq/teamspeak/query` | The ServerQuery client, running the same typed commands over raw TCP, SSH or WebQuery; `@experimental` |
|
|
1090
|
-
|
|
1091
|
-
Each entry exports only its own symbols. The client entry does not re-export
|
|
1092
|
-
the core, so a program imports the `Client` from `@honeybbq/teamspeak/client`
|
|
1093
|
-
and the typed commands, ids and errors from `@honeybbq/teamspeak`.
|
|
1094
|
-
|
|
1095
|
-
The protocol internals (crypto, handshake, transport) are not exported. They
|
|
1096
|
-
change with the protocol, and exporting them would have made every change a
|
|
1097
|
-
breaking one.
|
|
1098
|
-
|
|
1099
|
-
`@honeybbq/teamspeak-client` and `@honeybbq/teamspeak-core` are deprecated and
|
|
1100
|
-
get no further releases. Coming from 0.x? The
|
|
1101
|
-
[migration guide](https://github.com/HoneyBBQ/teamspeak-js/blob/main/docs/migrating-to-1.0.md) lists every change.
|
|
1102
|
-
|
|
1103
|
-
## Architecture
|
|
1104
|
-
|
|
1105
|
-
```
|
|
1106
|
-
src/
|
|
1107
|
-
├── core/ # Shared (@honeybbq/teamspeak): command language, ids, errors, typed commands
|
|
1108
|
-
├── versions/ # Client versions (@honeybbq/teamspeak/versions)
|
|
1109
|
-
├── client/ # The voice client (@honeybbq/teamspeak/client)
|
|
1110
|
-
│ ├── client.ts # Client lifecycle, connection management
|
|
1111
|
-
│ ├── commands.ts # Command sending and response tracking
|
|
1112
|
-
│ ├── api/voice.ts # Commands only the voice protocol has
|
|
1113
|
-
│ ├── capabilities.ts # Server version and uid hash detection
|
|
1114
|
-
│ ├── reconnect.ts # Backoff policy and permanent-failure detection
|
|
1115
|
-
│ ├── events.ts # Event handler registration and middleware
|
|
1116
|
-
│ ├── notifications.ts # Server notification parsing and dispatch
|
|
1117
|
-
│ ├── handshake.ts # Protocol handshake orchestration
|
|
1118
|
-
│ ├── transfer.ts # File transfer data connections
|
|
1119
|
-
│ ├── throttle.ts # Token-bucket rate limiter
|
|
1120
|
-
│ ├── crypto/ # ECDH, EAX encryption, identity management
|
|
1121
|
-
│ ├── handshake/ # Crypto handshake and license verification
|
|
1122
|
-
│ ├── transport/ # UDP packet framing, ACK, compression
|
|
1123
|
-
│ └── discovery/ # SRV / TSDNS / direct address resolution
|
|
1124
|
-
├── stream/ # TeamSpeak 6 streams (@honeybbq/teamspeak/stream)
|
|
1125
|
-
└── query/ # The ServerQuery client (@honeybbq/teamspeak/query)
|
|
1126
|
-
examples/
|
|
1127
|
-
└── connect.ts # Minimal connection example
|
|
1128
|
-
```
|
|
103
|
+
Details in [the compatibility page](https://honeybbq.github.io/teamspeak-js/documents/compatibility.html).
|
|
1129
104
|
|
|
1130
105
|
## Related
|
|
1131
106
|
|
|
@@ -1135,14 +110,6 @@ examples/
|
|
|
1135
110
|
|
|
1136
111
|
Protocol knowledge was primarily informed by the [TSLib](https://github.com/Splamy/TS3AudioBot) implementation in [TS3AudioBot](https://github.com/Splamy/TS3AudioBot) by Splamy. Huge thanks to the TS3AudioBot project and its contributors.
|
|
1137
112
|
|
|
1138
|
-
## Disclaimer
|
|
1139
|
-
|
|
1140
|
-
TeamSpeak is a registered trademark of [TeamSpeak Systems, Inc.](https://teamspeak.com/). This project is not affiliated with, endorsed by, or associated with TeamSpeak Systems, Inc. in any way.
|
|
1141
|
-
|
|
1142
|
-
This library is an **independent implementation** built from public documentation, observation of network traffic, and independent research. It contains no TeamSpeak proprietary code, headers, libraries, or binaries.
|
|
1143
|
-
|
|
1144
|
-
The project implements the client side only and does not implement a TeamSpeak server.
|
|
1145
|
-
|
|
1146
113
|
## Contributing
|
|
1147
114
|
|
|
1148
115
|
See [CONTRIBUTING.md](https://github.com/HoneyBBQ/teamspeak-js/blob/main/.github/CONTRIBUTING.md) for setup, checks and the integration suite.
|
|
@@ -1150,3 +117,11 @@ See [CONTRIBUTING.md](https://github.com/HoneyBBQ/teamspeak-js/blob/main/.github
|
|
|
1150
117
|
## License
|
|
1151
118
|
|
|
1152
119
|
[MIT](https://github.com/HoneyBBQ/teamspeak-js/blob/main/LICENSE)
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
<sub>TeamSpeak is a registered trademark of [TeamSpeak Systems, Inc.](https://teamspeak.com/). This project is not affiliated with, endorsed by, or associated with TeamSpeak Systems, Inc. in any way.</sub>
|
|
124
|
+
|
|
125
|
+
<sub>This library is an **independent implementation** built from public documentation, observation of network traffic, and independent research. It contains no TeamSpeak proprietary code, headers, libraries, or binaries.</sub>
|
|
126
|
+
|
|
127
|
+
<sub>The project implements the client side only and does not implement a TeamSpeak server.</sub>
|