@honeybbq/teamspeak-client 0.3.1 → 1.0.0-beta.1
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 +322 -60
- package/dist/abort.d.ts +5 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/address.d.ts +8 -0
- package/dist/address.d.ts.map +1 -1
- package/dist/api/voice.d.ts +24 -0
- package/dist/api/voice.d.ts.map +1 -0
- package/dist/capabilities.d.ts +62 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/client.d.ts +172 -39
- package/dist/client.d.ts.map +1 -1
- package/dist/commands.d.ts +45 -27
- package/dist/commands.d.ts.map +1 -1
- package/dist/crypto/crypt.d.ts +19 -4
- package/dist/crypto/crypt.d.ts.map +1 -1
- package/dist/crypto/identity.d.ts +87 -8
- package/dist/crypto/identity.d.ts.map +1 -1
- package/dist/crypto/index.d.ts +2 -1
- package/dist/crypto/index.d.ts.map +1 -1
- package/dist/diagnostics.d.ts +45 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/discovery/resolver.d.ts +21 -3
- package/dist/discovery/resolver.d.ts.map +1 -1
- package/dist/errors.d.ts +70 -16
- package/dist/errors.d.ts.map +1 -1
- package/dist/events.d.ts +4 -5
- package/dist/events.d.ts.map +1 -1
- package/dist/handshake/crypt-handshake.d.ts +1 -0
- package/dist/handshake/crypt-handshake.d.ts.map +1 -1
- package/dist/handshake/crypt-init2.d.ts.map +1 -1
- package/dist/handshake/license.d.ts +7 -0
- package/dist/handshake/license.d.ts.map +1 -1
- package/dist/handshake.d.ts +14 -5
- package/dist/handshake.d.ts.map +1 -1
- package/dist/helpers.d.ts +0 -8
- package/dist/helpers.d.ts.map +1 -1
- package/dist/index.d.ts +20 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3967 -0
- package/dist/index.js.map +1 -0
- package/dist/notifications.d.ts +7 -15
- package/dist/notifications.d.ts.map +1 -1
- package/dist/reconnect.d.ts +69 -0
- package/dist/reconnect.d.ts.map +1 -0
- package/dist/throttle.d.ts +9 -0
- package/dist/throttle.d.ts.map +1 -1
- package/dist/transfer.d.ts +27 -25
- package/dist/transfer.d.ts.map +1 -1
- package/dist/transport/handler.d.ts +37 -5
- package/dist/transport/handler.d.ts.map +1 -1
- package/dist/transport/quicklz.d.ts +19 -0
- package/dist/transport/quicklz.d.ts.map +1 -1
- package/dist/types.d.ts +277 -97
- package/dist/types.d.ts.map +1 -1
- package/package.json +21 -56
- package/src/abort.ts +24 -0
- package/src/address.ts +74 -0
- package/src/api/voice.ts +61 -0
- package/src/capabilities.ts +92 -0
- package/src/client.ts +1392 -0
- package/src/commands.ts +193 -0
- package/src/crypto/crypt-ops.ts +6 -0
- package/src/crypto/crypt.ts +274 -0
- package/src/crypto/eax.ts +152 -0
- package/src/crypto/identity.ts +484 -0
- package/src/crypto/index.ts +21 -0
- package/src/crypto/primitives.ts +141 -0
- package/src/diagnostics.ts +58 -0
- package/src/discovery/index.ts +1 -0
- package/src/discovery/resolver.ts +272 -0
- package/src/errors.ts +156 -0
- package/src/events.ts +28 -0
- package/src/handshake/crypt-handshake.ts +135 -0
- package/src/handshake/crypt-init2.ts +48 -0
- package/src/handshake/index.ts +3 -0
- package/src/handshake/license.test.ts +49 -0
- package/src/handshake/license.ts +208 -0
- package/src/handshake.ts +136 -0
- package/src/helpers.ts +10 -0
- package/src/index.ts +74 -0
- package/src/notifications.ts +136 -0
- package/src/reconnect.ts +141 -0
- package/src/throttle.ts +92 -0
- package/src/transfer.ts +127 -0
- package/src/transport/generation-window.ts +77 -0
- package/src/transport/handler.ts +914 -0
- package/src/transport/index.ts +14 -0
- package/src/transport/packet.ts +69 -0
- package/src/transport/quicklz.ts +209 -0
- package/src/types.ts +349 -0
- package/dist/address-BR-5unVo.cjs +0 -2
- package/dist/address-BR-5unVo.cjs.map +0 -1
- package/dist/address-BfmovVpp.js +0 -65
- package/dist/address-BfmovVpp.js.map +0 -1
- package/dist/api.d.ts +0 -17
- package/dist/api.d.ts.map +0 -1
- package/dist/command/command.d.ts +0 -12
- package/dist/command/command.d.ts.map +0 -1
- package/dist/command/index.cjs +0 -1
- package/dist/command/index.d.ts +0 -4
- package/dist/command/index.d.ts.map +0 -1
- package/dist/command/index.mjs +0 -3
- package/dist/command/parser.d.ts +0 -3
- package/dist/command/parser.d.ts.map +0 -1
- package/dist/command-BLe-C2L2.js +0 -74
- package/dist/command-BLe-C2L2.js.map +0 -1
- package/dist/command-DAK4pUBP.cjs +0 -4
- package/dist/command-DAK4pUBP.cjs.map +0 -1
- package/dist/crypt-handshake-CQkQreLx.cjs +0 -2
- package/dist/crypt-handshake-CQkQreLx.cjs.map +0 -1
- package/dist/crypt-handshake-DziOY4Lq.js +0 -55
- package/dist/crypt-handshake-DziOY4Lq.js.map +0 -1
- package/dist/crypt-init2-D70ha4LB.js +0 -158
- package/dist/crypt-init2-D70ha4LB.js.map +0 -1
- package/dist/crypt-init2-DparLekm.cjs +0 -2
- package/dist/crypt-init2-DparLekm.cjs.map +0 -1
- package/dist/crypto/index.cjs +0 -1
- package/dist/crypto/index.mjs +0 -3
- package/dist/crypto-Bg02m4ZE.js +0 -165
- package/dist/crypto-Bg02m4ZE.js.map +0 -1
- package/dist/crypto-CDlweUL3.cjs +0 -2
- package/dist/crypto-CDlweUL3.cjs.map +0 -1
- package/dist/discovery/index.cjs +0 -1
- package/dist/discovery/index.mjs +0 -2
- package/dist/handler-DI_abe9c.js +0 -483
- package/dist/handler-DI_abe9c.js.map +0 -1
- package/dist/handler-DuwoO5RC.cjs +0 -2
- package/dist/handler-DuwoO5RC.cjs.map +0 -1
- package/dist/handshake/index.cjs +0 -1
- package/dist/handshake/index.mjs +0 -3
- package/dist/index.cjs +0 -4
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs +0 -827
- package/dist/index.mjs.map +0 -1
- package/dist/parser-CL33R9tr.cjs +0 -2
- package/dist/parser-CL33R9tr.cjs.map +0 -1
- package/dist/parser-DZMv3Jm-.js +0 -33
- package/dist/parser-DZMv3Jm-.js.map +0 -1
- package/dist/primitives-cNNAeFCo.cjs +0 -2
- package/dist/primitives-cNNAeFCo.cjs.map +0 -1
- package/dist/primitives-xIr6Tjk4.js +0 -1457
- package/dist/primitives-xIr6Tjk4.js.map +0 -1
- package/dist/resolver-6B8dI9gr.js +0 -141
- package/dist/resolver-6B8dI9gr.js.map +0 -1
- package/dist/resolver-DCcmyWSK.cjs +0 -4
- package/dist/resolver-DCcmyWSK.cjs.map +0 -1
- package/dist/transport/index.cjs +0 -1
- package/dist/transport/index.mjs +0 -2
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@ Compatible with TeamSpeak 3, 5 & 6. No proprietary SDK. No copy-pasted code.
|
|
|
13
13
|
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
|
+
Upgrading from 0.x? See the [migration guide](https://github.com/HoneyBBQ/teamspeak-js/blob/main/docs/migrating-to-1.0.md).
|
|
17
|
+
|
|
16
18
|
## Features
|
|
17
19
|
|
|
18
20
|
- **Full protocol handshake** — ECDH key exchange, RSA puzzle, EAX-encrypted transport
|
|
@@ -20,11 +22,15 @@ Compatible with TeamSpeak 3, 5 & 6. No proprietary SDK. No copy-pasted code.
|
|
|
20
22
|
- **Event-driven API** — Register handlers for text messages, client enter/leave, channel moves, kicks, etc.
|
|
21
23
|
- **Voice data** — Send Opus voice packets (codec 4 & 5)
|
|
22
24
|
- **File transfers** — Upload, download, and delete files on the server
|
|
23
|
-
- **Address resolution** — SRV records, TSDNS, and direct
|
|
25
|
+
- **Address resolution** — SRV records, TSDNS, and direct addresses; server nicknames on request (`resolveNicknames: true`), since that lookup goes to named.myteamspeak.com
|
|
24
26
|
- **Middleware** — Pluggable command and event middleware chains
|
|
25
27
|
- **Built-in rate limiter** — Token-bucket throttling to prevent server-side flood kicks
|
|
26
28
|
- **Identity management** — Generate, import/export, and upgrade security level of identities
|
|
27
|
-
- **
|
|
29
|
+
- **Full command set** — 111 typed functions covering every command the voice protocol answers
|
|
30
|
+
- **Reconnection** — Opt-in retry with backoff, tuned for TeamSpeak's antiflood ban
|
|
31
|
+
- **Server capabilities** — Reports the server version and the uid hash it uses (SHA-1 on TS3, SHA-256 on TS6)
|
|
32
|
+
- **Custom transports** — Supply your own socket, for proxies, tunnels or tests
|
|
33
|
+
- **ESM with types** — Ships ES modules with full TypeScript declarations
|
|
28
34
|
- **Zero native deps** — Pure TypeScript, no native addons required
|
|
29
35
|
|
|
30
36
|
## Installation
|
|
@@ -43,7 +49,7 @@ Requires **Node.js 20.19** or later.
|
|
|
43
49
|
import { Client, generateIdentity } from "@honeybbq/teamspeak-client";
|
|
44
50
|
|
|
45
51
|
// Generate a new identity (or load an existing one)
|
|
46
|
-
const identity = generateIdentity(8);
|
|
52
|
+
const identity = await generateIdentity(8);
|
|
47
53
|
|
|
48
54
|
// Create the client
|
|
49
55
|
const client = new Client(identity, "localhost", "TSBot", {
|
|
@@ -65,11 +71,8 @@ client.on("disconnected", (err) => {
|
|
|
65
71
|
console.log("Disconnected:", err?.message ?? "clean");
|
|
66
72
|
});
|
|
67
73
|
|
|
68
|
-
// Connect
|
|
69
|
-
await client.connect();
|
|
70
|
-
|
|
71
|
-
// Wait until connected (with 15s timeout)
|
|
72
|
-
await client.waitConnected(AbortSignal.timeout(15_000));
|
|
74
|
+
// Connect; resolves once the handshake completes.
|
|
75
|
+
await client.connect({ signal: AbortSignal.timeout(15_000) });
|
|
73
76
|
|
|
74
77
|
// Stay connected until interrupted
|
|
75
78
|
process.on("SIGINT", async () => {
|
|
@@ -81,12 +84,26 @@ process.on("SIGINT", async () => {
|
|
|
81
84
|
|
|
82
85
|
### Client Lifecycle
|
|
83
86
|
|
|
84
|
-
| Method | Description
|
|
85
|
-
| --------------------------------------------- |
|
|
86
|
-
| `new Client(identity, addr, nickname, opts?)` | Create a new client
|
|
87
|
-
| `connect()`
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
87
|
+
| Method | Description |
|
|
88
|
+
| --------------------------------------------- | ------------------------------------------------- |
|
|
89
|
+
| `new Client(identity, addr, nickname, opts?)` | Create a new client |
|
|
90
|
+
| `connect({ signal? })` | Connect; resolves once the handshake completes |
|
|
91
|
+
| `disconnect({ signal? })` | Leave the server and close the connection |
|
|
92
|
+
| `status` | `"disconnected"`, `"connecting"` or `"connected"` |
|
|
93
|
+
| `clientID` | Our clid, assigned by the server |
|
|
94
|
+
| `channelID` | The channel we are in |
|
|
95
|
+
| `serverCapabilities` | Server version, family and uid hash |
|
|
96
|
+
| `uid` | Our own uid, hashed as the server does |
|
|
97
|
+
|
|
98
|
+
`connect()` rejects with an `InvalidAddressError` for an address that cannot be
|
|
99
|
+
one — no host, or a port outside 1–65535 — before anything starts; a host name
|
|
100
|
+
that does not resolve fails with a `TeamspeakError` whose `cause` is the lookup's
|
|
101
|
+
error. A `connect()` called while `disconnect()` is still saying goodbye waits
|
|
102
|
+
for it to finish, then connects.
|
|
103
|
+
|
|
104
|
+
Commands, raw `exec`/`execNoWait`, voice and whispers need a connected client:
|
|
105
|
+
until `connect()` resolves, and from the moment `disconnect()` begins, they fail
|
|
106
|
+
with a `NotConnectedError` and send nothing.
|
|
90
107
|
|
|
91
108
|
### Events
|
|
92
109
|
|
|
@@ -99,59 +116,194 @@ process.on("SIGINT", async () => {
|
|
|
99
116
|
| `on("clientLeave", handler)` | Fires when a client leaves |
|
|
100
117
|
| `on("clientMoved", handler)` | Fires when a client moves channels |
|
|
101
118
|
| `on("kicked", handler)` | Fires when the bot is kicked |
|
|
102
|
-
| `on("
|
|
103
|
-
| `on("
|
|
119
|
+
| `on("poked", handler)` | Fires when poked by a client |
|
|
120
|
+
| `on("voiceData", handler)` | Fires on incoming voice data |
|
|
121
|
+
|
|
122
|
+
`on()` and `once()` return a function that removes the listener, which is also
|
|
123
|
+
a `Disposable` (`using listener = client.on(...)`). Listeners run one at a
|
|
124
|
+
time, in the order their events arrived, so a listener may call the client and
|
|
125
|
+
await an answer. `disconnected` delivers `undefined` after `disconnect()`, and
|
|
126
|
+
the cause otherwise — a `ConnectionTimeoutError`, or a `RemovedError` for a kick, a ban
|
|
127
|
+
or a server stopping. `await using client = new Client(...)` disconnects when
|
|
128
|
+
the scope ends.
|
|
129
|
+
|
|
130
|
+
`connected` is delivered after `connect()` resolves, so a listener added right
|
|
131
|
+
after `await client.connect()` still hears it. `disconnect()` waits a second at
|
|
132
|
+
most for the server to acknowledge its goodbye, less if its `signal` aborts
|
|
133
|
+
sooner, and resolves once `disconnected` has been delivered. A `connect()` waits
|
|
134
|
+
for an earlier session's `disconnected` to be delivered before it starts, so no
|
|
135
|
+
listener or `events()` iterator of the new session hears the old one end.
|
|
136
|
+
|
|
137
|
+
A listener that throws does not stop the others: the event still reaches the
|
|
138
|
+
listeners after it, and the exception surfaces as an `uncaughtException`, as an
|
|
139
|
+
EventEmitter listener's would.
|
|
140
|
+
|
|
141
|
+
Voice is the exception to "every event in order": while more than 256
|
|
142
|
+
deliveries are waiting, voice frames are dropped (and counted in a warning at
|
|
143
|
+
most every ten seconds) rather than queued without bound.
|
|
144
|
+
|
|
145
|
+
`events(name, { signal })` is the same as an async iterator, buffering events
|
|
146
|
+
that arrive while the loop body runs:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
for await (const msg of client.events("textMessage", { signal })) {
|
|
150
|
+
await reply(msg);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`notification` delivers every notification as it arrived, for the ones the SDK
|
|
155
|
+
does not model.
|
|
104
156
|
|
|
105
157
|
### Commands
|
|
106
158
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
159
|
+
Every command the voice protocol answers is a typed function, grouped by area.
|
|
160
|
+
Each is addressed by branded ids, so a `ChannelID` cannot be passed where a
|
|
161
|
+
`ClientID` is meant. The constructors — `asChannelID(5)`, `asClientID(3)` and
|
|
162
|
+
the rest — throw a `RangeError` for anything but an integer in the id's wire
|
|
163
|
+
range: 0–65535 for a `ClientID` and the file transfer ids, 0–2^32−1 for a
|
|
164
|
+
`PermissionID` (an `IconID` also takes the signed form), 0–2^64−1 for the
|
|
165
|
+
`bigint` ids. Required arguments are positional; everything optional
|
|
166
|
+
goes in one options object, always the last argument, which also carries
|
|
167
|
+
`signal` and `timeout` to abandon the command or bound how long it waits.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import {
|
|
171
|
+
listChannels,
|
|
172
|
+
createChannel,
|
|
173
|
+
moveClient,
|
|
174
|
+
kickClient,
|
|
175
|
+
sendChannelMessage,
|
|
176
|
+
addServerGroupPermission,
|
|
177
|
+
iterateClientDatabase,
|
|
178
|
+
permissionByName,
|
|
179
|
+
asClientID,
|
|
180
|
+
Codec,
|
|
181
|
+
Reason,
|
|
182
|
+
} from "@honeybbq/teamspeak-client";
|
|
183
|
+
|
|
184
|
+
const channels = await listChannels(client, { signal: AbortSignal.timeout(5_000) });
|
|
185
|
+
|
|
186
|
+
const cid = await createChannel(client, {
|
|
187
|
+
name: "Lounge",
|
|
188
|
+
topic: "the quiet one",
|
|
189
|
+
codec: Codec.OpusVoice,
|
|
190
|
+
isPermanent: true,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await moveClient(client, client.clientID, cid, { password: "channel-password" });
|
|
194
|
+
await sendChannelMessage(client, "hello");
|
|
195
|
+
await kickClient(client, someone, Reason.KickChannel, { reasonMessage: "take a break" });
|
|
196
|
+
|
|
197
|
+
await addServerGroupPermission(client, sgid, {
|
|
198
|
+
permission: permissionByName("i_client_talk_power"),
|
|
199
|
+
value: 50,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
for await (const entry of iterateClientDatabase(client)) {
|
|
203
|
+
console.log(entry.nickname, entry.lastConnectedAt);
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
A command that sends content — `createChannel`'s channel, `editChannel`'s
|
|
208
|
+
settings, `updateClient`, `editClient`, `editClientDatabase`,
|
|
209
|
+
`addServerGroupPermission`'s grant, `addBan`'s `BanFields` — takes it as its own
|
|
210
|
+
argument, and `{ signal, timeout }` as a separate one after it: set among the
|
|
211
|
+
content, they are not read.
|
|
212
|
+
|
|
213
|
+
Options objects use camelCase names (`maxClients` for `channel_maxclients`),
|
|
214
|
+
and only the fields you set are sent: an unset field leaves the setting alone,
|
|
215
|
+
while `""` sends it empty — `editChannel(client, cid, { password: "" })` clears
|
|
216
|
+
a password. A key an options type does not name is never sent. The exceptions
|
|
217
|
+
are `editServer` and `editInstance`, which take the raw `virtualserver_*` and
|
|
218
|
+
`serverinstance_*` properties as `getServerInfo` reports them.
|
|
219
|
+
|
|
220
|
+
Results are camelCase too. Timestamps are `Date`s (`undefined` where the server
|
|
221
|
+
sends 0 for "never"), durations are milliseconds with an `Ms` suffix
|
|
222
|
+
(`idleTimeMs`, `durationMs`), and a permission is a `Permission`, built with
|
|
223
|
+
`permissionByID` or `permissionByName`. Small enumerations — `GroupType`,
|
|
224
|
+
`Codec`, `PrivilegeKeyType`, `LogLevel` and the rest — are objects of named values with
|
|
225
|
+
a type of the same name.
|
|
226
|
+
|
|
227
|
+
| Module | Covers |
|
|
228
|
+
| ------------ | -------------------------------------------------------------- |
|
|
229
|
+
| `channels` | Create, edit, move, delete, subscribe, channel permissions |
|
|
230
|
+
| `clients` | List, move, kick, poke, edit, the client database, permissions |
|
|
231
|
+
| `server` | Server and instance settings, groups, temp passwords, bindings |
|
|
232
|
+
| `groups` | Channel groups, the permission catalogue, privilege keys |
|
|
233
|
+
| `moderation` | Bans, complaints, offline messages, text messages, the log |
|
|
234
|
+
| `files` | List, delete, rename, create directories, transfer negotiation |
|
|
235
|
+
|
|
236
|
+
Many of these commands are answered over the voice protocol with a
|
|
237
|
+
notification rather than data rows — `servergrouplist` with
|
|
238
|
+
`notifyservergrouplist`, `ftinitupload` with `notifystartupload` after the
|
|
239
|
+
command has already succeeded. The client pairs each answer with its command,
|
|
240
|
+
so this is invisible to callers; [docs/voice-protocol-responses.md](https://github.com/HoneyBBQ/teamspeak-js/blob/main/docs/voice-protocol-responses.md)
|
|
241
|
+
has the measured table. A refusal is always thrown, never returned as an empty
|
|
242
|
+
result. A list or find with nothing to report answers `[]`. A command that
|
|
243
|
+
exists to return something — `createChannel`'s id, `initFileUpload`'s key,
|
|
244
|
+
`getClientInfo`'s fields — throws a `NoDataError` when the server answers
|
|
245
|
+
without it, rather than returning an id of 0 or an empty object.
|
|
246
|
+
|
|
247
|
+
For anything outside the typed set, `client.exec(line, { signal?, timeout? })`
|
|
248
|
+
sends a raw command line and resolves to the rows that answer it.
|
|
119
249
|
|
|
120
250
|
### File Transfers
|
|
121
251
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
252
|
+
Negotiate with the `files` module, then move the bytes over TCP:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import { Readable } from "node:stream";
|
|
256
|
+
import { initFileUpload } from "@honeybbq/teamspeak-client";
|
|
257
|
+
|
|
258
|
+
const info = await initFileUpload(client, cid, "/notes.txt", BigInt(data.length));
|
|
259
|
+
await client.uploadFileData(host, info, Readable.from([data]));
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
An upload does not replace a file already at the path unless passed
|
|
263
|
+
`{ overwrite: true }`.
|
|
264
|
+
|
|
265
|
+
| Function | Description |
|
|
266
|
+
| ------------------------------------------------------- | --------------------------------- |
|
|
267
|
+
| `initFileUpload(client, ...)` | Negotiate an upload |
|
|
268
|
+
| `initFileDownload(client, ...)` | Negotiate a download |
|
|
269
|
+
| `client.uploadFileData(host, info, data, { signal? })` | Transfer file data to the server |
|
|
270
|
+
| `client.downloadFileData(host, info, out, { signal? })` | Receive file data from the server |
|
|
129
271
|
|
|
130
272
|
### Identity
|
|
131
273
|
|
|
132
274
|
```typescript
|
|
133
|
-
import { generateIdentity,
|
|
275
|
+
import { generateIdentity, parseIdentity } from "@honeybbq/teamspeak-client";
|
|
134
276
|
|
|
135
277
|
// Generate a new identity with security level 8
|
|
136
|
-
const identity = generateIdentity(8);
|
|
278
|
+
const identity = await generateIdentity(8);
|
|
137
279
|
|
|
138
280
|
// Export to string for persistent storage
|
|
139
|
-
const exported = identity.
|
|
281
|
+
const exported = identity.export();
|
|
140
282
|
|
|
141
283
|
// Import from a previously exported string
|
|
142
|
-
const restored =
|
|
284
|
+
const restored = parseIdentity(exported);
|
|
143
285
|
|
|
144
|
-
//
|
|
145
|
-
|
|
286
|
+
// Raise the security level (CPU-intensive). Identities are immutable: this
|
|
287
|
+
// returns a new one with the same key.
|
|
288
|
+
const stronger = await identity.upgradeToLevel(10, { signal });
|
|
289
|
+
|
|
290
|
+
// The unique identifier, hashed as a TS3 (sha1) or TS6 (sha256) server does
|
|
291
|
+
const uid = identity.uid("sha1");
|
|
146
292
|
```
|
|
147
293
|
|
|
294
|
+
The private key never leaves the identity except through `export()`:
|
|
295
|
+
`toString()`, `toJSON()` and `util.inspect()` describe it without the key.
|
|
296
|
+
`parseIdentity` takes exactly what `export()` writes — the key as canonical
|
|
297
|
+
base64 of at most 32 bytes, a colon, and a decimal offset that fits in 64 bits
|
|
298
|
+
— and throws an `InvalidIdentityError` for anything else.
|
|
299
|
+
|
|
148
300
|
### Options
|
|
149
301
|
|
|
150
302
|
```typescript
|
|
151
303
|
const client = new Client(identity, "ts.example.com", "Bot", {
|
|
152
|
-
logger: consoleLogger,
|
|
304
|
+
logger: consoleLogger, // silent unless set
|
|
153
305
|
resolver: customResolver,
|
|
154
|
-
commandMiddleware: [
|
|
306
|
+
commandMiddleware: [timing],
|
|
155
307
|
eventMiddleware: [filterMiddleware],
|
|
156
308
|
serverPassword: process.env.TEAMSPEAK_SERVER_PASSWORD,
|
|
157
309
|
defaultChannel: "Lobby",
|
|
@@ -159,48 +311,158 @@ const client = new Client(identity, "ts.example.com", "Bot", {
|
|
|
159
311
|
});
|
|
160
312
|
```
|
|
161
313
|
|
|
314
|
+
Address resolution — the built-in one or a custom `resolver` — has ten seconds
|
|
315
|
+
in all: the `signal` a resolver is given aborts then, and the client connects
|
|
316
|
+
to the address as given.
|
|
317
|
+
|
|
318
|
+
`commandMiddleware` wraps the typed commands: each middleware receives the
|
|
319
|
+
request (its command name and parameters) with its options and returns the
|
|
320
|
+
answering rows, so it can time, retry or answer a command itself. Raw `exec`
|
|
321
|
+
and `execNoWait` lines do not pass through it.
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
const timing: CommandMiddleware = (next) => async (request, options) => {
|
|
325
|
+
const started = Date.now();
|
|
326
|
+
try {
|
|
327
|
+
return await next(request, options);
|
|
328
|
+
} finally {
|
|
329
|
+
console.log(request.command, Date.now() - started, "ms");
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
```
|
|
333
|
+
|
|
162
334
|
Connection auth options:
|
|
163
335
|
|
|
164
336
|
- `serverPassword`: plain-text server password; sent as the TeamSpeak protocol hash during `clientinit`
|
|
165
337
|
- `defaultChannel`: default channel name requested during `clientinit`
|
|
166
338
|
- `defaultChannelPassword`: plain-text password for `defaultChannel`; sent as the TeamSpeak protocol hash
|
|
167
339
|
|
|
168
|
-
|
|
340
|
+
### Diagnostics
|
|
169
341
|
|
|
170
|
-
The
|
|
342
|
+
The client logs nothing unless given a `logger`. For tracing, it publishes on
|
|
343
|
+
two [`diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html)
|
|
344
|
+
channels, which cost nothing without a subscriber:
|
|
345
|
+
|
|
346
|
+
- `honeybbq:teamspeak:command` (`commandChannel`), a tracing channel: `start`
|
|
347
|
+
when a command is sent, `asyncEnd` when its answer arrives, `error` when it
|
|
348
|
+
fails.
|
|
349
|
+
- `honeybbq:teamspeak:packet` (`packetChannel`): every command the server sends.
|
|
350
|
+
|
|
351
|
+
Both carry the command's name, never its parameters, which hold password hashes
|
|
352
|
+
and privilege keys.
|
|
353
|
+
|
|
354
|
+
### Reconnection
|
|
355
|
+
|
|
356
|
+
Off by default: a silent reconnect makes an outage look like a healthy session.
|
|
357
|
+
An explicit `disconnect()` never reconnects, whatever the policy says.
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
const client = new Client(identity, "ts.example.com", "Bot", {
|
|
361
|
+
reconnect: {
|
|
362
|
+
backoff: { initialDelayMs: 1_000, maxDelayMs: 60_000, factor: 2, jitter: 0.2 },
|
|
363
|
+
// Optional: decide for yourself which failures are worth retrying.
|
|
364
|
+
shouldRetry: (err, attempt) => attempt < 10,
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
The defaults are conservative because TeamSpeak **bans an IP after roughly
|
|
370
|
+
three rapid connects** — a retry loop without a delay bans itself, and the ban
|
|
371
|
+
outlasts the outage it was recovering from. Recovery is also not instant even
|
|
372
|
+
when it succeeds: the server holds the dropped session for a while and refuses a
|
|
373
|
+
reconnect with `521 too many clones already connected` until it reaps it, so the
|
|
374
|
+
loop keeps trying rather than giving up on the first refusal. It stops once a
|
|
375
|
+
`connect()` of your own has connected the client, and it does not retry an
|
|
376
|
+
address that cannot be one (`InvalidAddressError`).
|
|
377
|
+
|
|
378
|
+
### Custom transports
|
|
379
|
+
|
|
380
|
+
`fileTransferSocketFactory` and `socketFactory` are the only seams into the
|
|
381
|
+
network layer. Without them the SDK always creates its own sockets, so an
|
|
382
|
+
application running behind a proxy has no way through. `socketFactory` returns
|
|
383
|
+
a `DatagramSocket` — a `node:dgram` socket, or anything with the few methods
|
|
384
|
+
it names — and `fileTransferSocketFactory` any `Duplex` stream, such as the
|
|
385
|
+
socket a SOCKS library returns.
|
|
171
386
|
|
|
172
387
|
```typescript
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
388
|
+
const client = new Client(identity, "ts.example.com", "Bot", {
|
|
389
|
+
// UDP, for the voice and command channel.
|
|
390
|
+
socketFactory: (family) => createSocket(family === 6 ? "udp6" : "udp4"),
|
|
391
|
+
// TCP, dialled separately by each file transfer.
|
|
392
|
+
fileTransferSocketFactory: (host, port) => createConnection({ host, port }),
|
|
393
|
+
});
|
|
178
394
|
```
|
|
179
395
|
|
|
396
|
+
### Server capabilities
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
const caps = client.serverCapabilities;
|
|
400
|
+
// { version: "6.0.0-beta13.1", major: 6, family: "ts6", uidAlgorithm: "sha256" }
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Read from `initserver` during the handshake, so it costs no extra round trip.
|
|
404
|
+
The uid hash matters because TS3 hashes a client's public key with SHA-1 and TS6
|
|
405
|
+
with SHA-256 — a client that assumes either one computes an identifier the other
|
|
406
|
+
server never sends.
|
|
407
|
+
|
|
408
|
+
## Compatibility
|
|
409
|
+
|
|
410
|
+
The packages follow semver from 1.0. A few sets are open by nature and may
|
|
411
|
+
gain members in a minor release: the events (`EventMap`, `ServerEventName`),
|
|
412
|
+
`ClientStatus`, `ServerFamily`, `UIDAlgorithm`, `ErrorCode`, `Reason` and the
|
|
413
|
+
enumeration tables. Write a `switch` over any of them with a default case. A
|
|
414
|
+
field the server sends as a number that one of those tables names — a codec, a
|
|
415
|
+
text message's target mode, a group type — is a plain `number`, since a server
|
|
416
|
+
may send a value newer than the table.
|
|
417
|
+
|
|
418
|
+
`DatagramSocket` describes the part of a socket the client uses; should a
|
|
419
|
+
later release use more, the new members will be optional.
|
|
420
|
+
|
|
421
|
+
The [Go SDK](https://github.com/honeybbq/teamspeak-go) makes the same calls
|
|
422
|
+
with the same semantics, apart from what the languages make differ:
|
|
423
|
+
|
|
424
|
+
- Backoff fields: JavaScript takes `undefined` as "the default" and `0` as 0.
|
|
425
|
+
Go cannot tell an unset field from 0, so there a zero field takes the default
|
|
426
|
+
and a negative jitter disables it.
|
|
427
|
+
- A listener that throws: here the exception is rethrown from a microtask, an
|
|
428
|
+
uncaught exception as an EventEmitter listener's would be, and the other
|
|
429
|
+
listeners still get the event. Go recovers a panicking handler, logs it at
|
|
430
|
+
Error, and keeps delivering.
|
|
431
|
+
- Durations are milliseconds with an `Ms` suffix here, `time.Duration` in Go;
|
|
432
|
+
`listClients`' `uid` flag (on by default) is Go's `OmitUID`.
|
|
433
|
+
|
|
434
|
+
## Packages
|
|
435
|
+
|
|
436
|
+
This package is the voice client. It depends on
|
|
437
|
+
[`@honeybbq/teamspeak-core`](https://github.com/HoneyBBQ/teamspeak-js/tree/main/packages/core) — the command language, ids, errors and
|
|
438
|
+
typed commands it shares with the ServerQuery client — and re-exports all of
|
|
439
|
+
it, so this is the only package to install.
|
|
440
|
+
|
|
441
|
+
The protocol internals (crypto, handshake, transport) are not exported. They
|
|
442
|
+
change with the protocol, and exporting them would have made every change a
|
|
443
|
+
breaking one.
|
|
444
|
+
|
|
180
445
|
## Architecture
|
|
181
446
|
|
|
182
447
|
```
|
|
183
|
-
|
|
448
|
+
packages/client/
|
|
184
449
|
├── src/
|
|
185
450
|
│ ├── client.ts # Client lifecycle, connection management
|
|
186
|
-
│ ├── api.ts # High-level API (messages, channels, clients)
|
|
187
451
|
│ ├── commands.ts # Command sending and response tracking
|
|
452
|
+
│ ├── api/voice.ts # Commands only the voice protocol has
|
|
453
|
+
│ ├── capabilities.ts # Server version and uid hash detection
|
|
454
|
+
│ ├── reconnect.ts # Backoff policy and permanent-failure detection
|
|
188
455
|
│ ├── events.ts # Event handler registration and middleware
|
|
189
456
|
│ ├── notifications.ts # Server notification parsing and dispatch
|
|
190
457
|
│ ├── handshake.ts # Protocol handshake orchestration
|
|
191
|
-
│ ├── transfer.ts # File transfer
|
|
458
|
+
│ ├── transfer.ts # File transfer data connections
|
|
192
459
|
│ ├── throttle.ts # Token-bucket rate limiter
|
|
193
|
-
│ ├── types.ts # Public type definitions
|
|
194
|
-
│ ├── errors.ts # Error classes
|
|
195
460
|
│ ├── crypto/ # ECDH, EAX encryption, identity management
|
|
196
461
|
│ ├── handshake/ # Crypto handshake and license verification
|
|
197
462
|
│ ├── transport/ # UDP packet framing, ACK, compression
|
|
198
|
-
│ ├── command/ # Command builder and parser
|
|
199
463
|
│ └── discovery/ # SRV / TSDNS / direct address resolution
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
├── vite.config.ts # Build configuration (Vite library mode)
|
|
203
|
-
└── tsconfig.json
|
|
464
|
+
└── examples/
|
|
465
|
+
└── connect.ts # Minimal connection example
|
|
204
466
|
```
|
|
205
467
|
|
|
206
468
|
## Related
|
package/dist/abort.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** The reason an aborted signal gives, as an error. */
|
|
2
|
+
export declare function abortReason(signal: AbortSignal): unknown;
|
|
3
|
+
/** Settle with `promise`, or reject as soon as `signal` aborts, whichever is first. */
|
|
4
|
+
export declare function abortable<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T>;
|
|
5
|
+
//# sourceMappingURL=abort.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abort.d.ts","sourceRoot":"","sources":["../src/abort.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAExD;AAED,uFAAuF;AACvF,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,CAiB7F"}
|
package/dist/address.d.ts
CHANGED
|
@@ -4,6 +4,14 @@ export interface HostPort {
|
|
|
4
4
|
}
|
|
5
5
|
/** Parse a TeamSpeak address without confusing an IPv6 colon for a port separator. */
|
|
6
6
|
export declare function splitHostPort(address: string, defaultPort?: string): HostPort;
|
|
7
|
+
/**
|
|
8
|
+
* Parse an address to connect to, and check it can be one: a host, which
|
|
9
|
+
* contains nothing a host name or IP address cannot, and a port from 1 to
|
|
10
|
+
* 65535. Throws an InvalidAddressError otherwise.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseConnectAddress(address: string): HostPort & {
|
|
13
|
+
portNumber: number;
|
|
14
|
+
};
|
|
7
15
|
export declare function joinHostPort(host: string, port: string): string;
|
|
8
16
|
export declare function isIpAddress(host: string): boolean;
|
|
9
17
|
//# sourceMappingURL=address.d.ts.map
|
package/dist/address.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,SAAS,GAAG,QAAQ,CA0B7E;AAKD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAatF;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commands only the voice protocol has.
|
|
3
|
+
*
|
|
4
|
+
* ServerQuery has no subscriptions — a query client hears every channel's
|
|
5
|
+
* events once it registers for them — and no `channelgetdescription`, since its
|
|
6
|
+
* `channelinfo` already carries the description. These run on the voice
|
|
7
|
+
* client alone, which is why they take one rather than any CommandExecutor.
|
|
8
|
+
*/
|
|
9
|
+
import type { Client } from "../client.js";
|
|
10
|
+
import type { ChannelID, CommandOptions } from "@honeybbq/teamspeak-core";
|
|
11
|
+
/** The channel's description, which `listChannels` omits for bandwidth. */
|
|
12
|
+
export declare function getChannelDescription(client: Client, cid: ChannelID, options?: CommandOptions): Promise<string>;
|
|
13
|
+
/**
|
|
14
|
+
* Subscribe to a channel: receive its clients' events and voice without
|
|
15
|
+
* joining it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function subscribeChannel(client: Client, cid: ChannelID, options?: CommandOptions): Promise<void>;
|
|
18
|
+
/** Stop receiving a subscribed channel's events and voice. */
|
|
19
|
+
export declare function unsubscribeChannel(client: Client, cid: ChannelID, options?: CommandOptions): Promise<void>;
|
|
20
|
+
/** Subscribe to every channel on the server. */
|
|
21
|
+
export declare function subscribeAllChannels(client: Client, options?: CommandOptions): Promise<void>;
|
|
22
|
+
/** Unsubscribe from every channel on the server. */
|
|
23
|
+
export declare function unsubscribeAllChannels(client: Client, options?: CommandOptions): Promise<void>;
|
|
24
|
+
//# sourceMappingURL=voice.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice.d.ts","sourceRoot":"","sources":["../../src/api/voice.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1E,2EAA2E;AAC3E,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,SAAS,EACd,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,MAAM,CAAC,CAMjB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,SAAS,EACd,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,8DAA8D;AAC9D,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,SAAS,EACd,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,gDAAgD;AAChD,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,oDAAoD;AACpD,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC,CAEf"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the connected server is, and what that means for the caller.
|
|
3
|
+
*
|
|
4
|
+
* TS3 and TS6 differ in ways that stay invisible until something breaks. The
|
|
5
|
+
* clearest: the server hashes a client's public key with SHA-1 on TS3 and
|
|
6
|
+
* SHA-256 on TS6, so a client that assumes either one computes a
|
|
7
|
+
* `client_unique_identifier` the other server never sends.
|
|
8
|
+
*
|
|
9
|
+
* Everything below comes from the handshake — `initserver` carries
|
|
10
|
+
* `virtualserver_version`, the one place the server names itself, along with
|
|
11
|
+
* its name, icon, banner and welcome message — so reading it costs no extra
|
|
12
|
+
* round trip.
|
|
13
|
+
*/
|
|
14
|
+
import type { IconID } from "@honeybbq/teamspeak-core";
|
|
15
|
+
import { type UIDAlgorithm } from "./crypto/identity.js";
|
|
16
|
+
/**
|
|
17
|
+
* The server generation a version string belongs to. May gain members in a
|
|
18
|
+
* minor release, as server generations appear: a `switch` over it needs a
|
|
19
|
+
* default case.
|
|
20
|
+
*/
|
|
21
|
+
export type ServerFamily = "ts3" | "ts6" | "unknown";
|
|
22
|
+
/** What a server said about itself in the handshake. */
|
|
23
|
+
export interface ServerCapabilities {
|
|
24
|
+
/** The `virtualserver_version` string, exactly as the server sent it. */
|
|
25
|
+
readonly version: string;
|
|
26
|
+
/** Major version number, or 0 when it could not be parsed. */
|
|
27
|
+
readonly major: number;
|
|
28
|
+
/** Which server generation this is. */
|
|
29
|
+
readonly family: ServerFamily;
|
|
30
|
+
/**
|
|
31
|
+
* The hash the server uses for `client_unique_identifier`.
|
|
32
|
+
*
|
|
33
|
+
* Read from the family rather than offered as a setting: the server decides
|
|
34
|
+
* this, and a client that picks differently never matches.
|
|
35
|
+
*/
|
|
36
|
+
readonly uidAlgorithm: UIDAlgorithm;
|
|
37
|
+
/** The virtual server's name (`virtualserver_name`). */
|
|
38
|
+
readonly name: string;
|
|
39
|
+
/**
|
|
40
|
+
* The server's icon (`virtualserver_icon_id`), or `undefined` for none. The
|
|
41
|
+
* server sends the icon's CRC32, sometimes as a negative 32-bit number; this
|
|
42
|
+
* is the unsigned value, which names the icon file.
|
|
43
|
+
*/
|
|
44
|
+
readonly iconID: IconID | undefined;
|
|
45
|
+
/** The host banner image's URL (`virtualserver_hostbanner_gfx_url`), if one is set. */
|
|
46
|
+
readonly hostBannerImageURL: string | undefined;
|
|
47
|
+
/** The welcome message shown on connecting (`virtualserver_welcomemessage`). */
|
|
48
|
+
readonly welcomeMessage: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Read what a server says about itself in `initserver`, or a version string
|
|
52
|
+
* alone, such as "6.0.0-beta13.1 [Build: 1]".
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseServerCapabilities(initserver: string | Readonly<Record<string, string>>): ServerCapabilities;
|
|
55
|
+
/**
|
|
56
|
+
* Capabilities of a server that has not been connected to yet.
|
|
57
|
+
*
|
|
58
|
+
* The family is `unknown` — nothing has said which server this is — and
|
|
59
|
+
* `uidAlgorithm` is SHA-1, the TS3 hash.
|
|
60
|
+
*/
|
|
61
|
+
export declare function unknownServerCapabilities(): ServerCapabilities;
|
|
62
|
+
//# sourceMappingURL=capabilities.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAEvD,OAAO,EAA0B,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEjF;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,KAAK,GAAG,SAAS,CAAC;AAErD,wDAAwD;AACxD,MAAM,WAAW,kBAAkB;IACjC,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uCAAuC;IACvC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,wDAAwD;IACxD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,uFAAuF;IACvF,QAAQ,CAAC,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IAChD,gFAAgF;IAChF,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACpD,kBAAkB,CAqBpB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,IAAI,kBAAkB,CAE9D"}
|