@honeybbq/teamspeak 1.0.0-beta.19 → 1.0.0-beta.21
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/dist/client/api/avatars.d.ts.map +1 -1
- package/dist/client/api/avatars.js +11 -2
- package/dist/client/api/avatars.js.map +1 -1
- package/dist/client/api/files.d.ts +36 -14
- package/dist/client/api/files.d.ts.map +1 -1
- package/dist/client/api/files.js +58 -9
- package/dist/client/api/files.js.map +1 -1
- package/dist/client/api/upload-checks.d.ts +36 -0
- package/dist/client/api/upload-checks.d.ts.map +1 -0
- package/dist/client/api/upload-checks.js +77 -0
- package/dist/client/api/upload-checks.js.map +1 -0
- package/dist/client/api/voice.d.ts +2 -1
- package/dist/client/api/voice.d.ts.map +1 -1
- package/dist/client/api/voice.js +6 -2
- package/dist/client/api/voice.js.map +1 -1
- package/dist/client/client.d.ts +13 -3
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +151 -49
- package/dist/client/client.js.map +1 -1
- package/dist/client/crypto/crypt.d.ts +6 -0
- package/dist/client/crypto/crypt.d.ts.map +1 -1
- package/dist/client/crypto/crypt.js +13 -7
- package/dist/client/crypto/crypt.js.map +1 -1
- package/dist/client/crypto/eax.d.ts +4 -0
- package/dist/client/crypto/eax.d.ts.map +1 -1
- package/dist/client/crypto/eax.js +27 -21
- package/dist/client/crypto/eax.js.map +1 -1
- package/dist/client/errors.d.ts +13 -0
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js +23 -0
- package/dist/client/errors.js.map +1 -1
- package/dist/client/handshake/crypt-handshake.d.ts +2 -1
- package/dist/client/handshake/crypt-handshake.d.ts.map +1 -1
- package/dist/client/handshake/crypt-handshake.js +4 -2
- package/dist/client/handshake/crypt-handshake.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/throttle.d.ts +11 -0
- package/dist/client/throttle.d.ts.map +1 -1
- package/dist/client/throttle.js +38 -3
- package/dist/client/throttle.js.map +1 -1
- package/dist/client/transfer.d.ts +37 -4
- package/dist/client/transfer.d.ts.map +1 -1
- package/dist/client/transfer.js +155 -15
- package/dist/client/transfer.js.map +1 -1
- package/dist/client/transport/handler.d.ts.map +1 -1
- package/dist/client/transport/handler.js +47 -43
- package/dist/client/transport/handler.js.map +1 -1
- package/dist/core/api/clients.d.ts +15 -15
- package/dist/core/api/clients.d.ts.map +1 -1
- package/dist/core/api/clients.js.map +1 -1
- package/dist/core/api/command.d.ts +8 -6
- package/dist/core/api/command.d.ts.map +1 -1
- package/dist/core/api/command.js +1 -1
- package/dist/core/api/command.js.map +1 -1
- package/dist/core/api/files.d.ts +23 -6
- package/dist/core/api/files.d.ts.map +1 -1
- package/dist/core/api/files.js +12 -9
- package/dist/core/api/files.js.map +1 -1
- package/dist/core/api/groups.d.ts +17 -2
- package/dist/core/api/groups.d.ts.map +1 -1
- package/dist/core/api/groups.js +13 -11
- package/dist/core/api/groups.js.map +1 -1
- package/dist/core/api/parse.d.ts +5 -5
- package/dist/core/api/parse.d.ts.map +1 -1
- package/dist/core/api/parse.js +16 -15
- package/dist/core/api/parse.js.map +1 -1
- package/dist/core/command/request.d.ts +6 -0
- package/dist/core/command/request.d.ts.map +1 -1
- package/dist/core/command/request.js +12 -0
- package/dist/core/command/request.js.map +1 -1
- package/dist/core/errors.d.ts +46 -3
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +73 -20
- package/dist/core/errors.js.map +1 -1
- package/dist/core/events.d.ts +24 -8
- package/dist/core/events.d.ts.map +1 -1
- package/dist/core/internal.d.ts +1 -1
- package/dist/core/internal.d.ts.map +1 -1
- package/dist/core/internal.js +1 -1
- package/dist/core/internal.js.map +1 -1
- package/dist/core/notifications.d.ts +1 -1
- package/dist/core/notifications.js +9 -8
- package/dist/core/notifications.js.map +1 -1
- package/dist/core/reconnect.d.ts +16 -5
- package/dist/core/reconnect.d.ts.map +1 -1
- package/dist/core/reconnect.js +32 -4
- package/dist/core/reconnect.js.map +1 -1
- package/dist/core/types.d.ts +15 -16
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/query/client.d.ts +4 -0
- package/dist/query/client.d.ts.map +1 -1
- package/dist/query/client.js +145 -23
- package/dist/query/client.js.map +1 -1
- package/dist/query/errors.d.ts +20 -0
- package/dist/query/errors.d.ts.map +1 -1
- package/dist/query/errors.js +31 -0
- package/dist/query/errors.js.map +1 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/index.js.map +1 -1
- package/dist/query/lines.d.ts +11 -0
- package/dist/query/lines.d.ts.map +1 -1
- package/dist/query/lines.js +36 -6
- package/dist/query/lines.js.map +1 -1
- package/dist/query/options.d.ts +30 -0
- package/dist/query/options.d.ts.map +1 -1
- package/dist/query/options.js +7 -0
- package/dist/query/options.js.map +1 -1
- package/dist/query/reconnect.d.ts +5 -0
- package/dist/query/reconnect.d.ts.map +1 -1
- package/dist/query/reconnect.js +26 -7
- package/dist/query/reconnect.js.map +1 -1
- package/dist/query/ssh.js +9 -4
- package/dist/query/ssh.js.map +1 -1
- package/dist/query/stream.d.ts +2 -0
- package/dist/query/stream.d.ts.map +1 -1
- package/dist/query/stream.js +19 -1
- package/dist/query/stream.js.map +1 -1
- package/dist/query/transport.d.ts +12 -2
- package/dist/query/transport.d.ts.map +1 -1
- package/dist/query/web.d.ts +3 -1
- package/dist/query/web.d.ts.map +1 -1
- package/dist/query/web.js +82 -18
- package/dist/query/web.js.map +1 -1
- package/dist/stream/published.d.ts.map +1 -1
- package/dist/stream/published.js +61 -25
- package/dist/stream/published.js.map +1 -1
- package/dist/stream/streams.d.ts.map +1 -1
- package/dist/stream/streams.js +51 -10
- package/dist/stream/streams.js.map +1 -1
- package/dist/stream/wire.d.ts +2 -2
- package/dist/stream/wire.d.ts.map +1 -1
- package/dist/stream/wire.js +27 -7
- package/dist/stream/wire.js.map +1 -1
- package/package.json +1 -1
- package/src/client/api/avatars.ts +15 -2
- package/src/client/api/files.ts +83 -23
- package/src/client/api/upload-checks.ts +96 -0
- package/src/client/api/voice.ts +5 -2
- package/src/client/client.ts +154 -42
- package/src/client/crypto/crypt.ts +15 -7
- package/src/client/crypto/eax.ts +27 -22
- package/src/client/errors.ts +25 -0
- package/src/client/handshake/crypt-handshake.ts +6 -3
- package/src/client/index.ts +1 -0
- package/src/client/throttle.ts +41 -3
- package/src/client/transfer.ts +172 -28
- package/src/client/transport/handler.ts +54 -46
- package/src/core/api/clients.ts +24 -21
- package/src/core/api/command.ts +9 -7
- package/src/core/api/files.ts +30 -16
- package/src/core/api/groups.ts +32 -13
- package/src/core/api/parse.ts +16 -15
- package/src/core/command/request.ts +18 -0
- package/src/core/errors.ts +74 -20
- package/src/core/events.ts +24 -8
- package/src/core/internal.ts +7 -1
- package/src/core/notifications.ts +9 -8
- package/src/core/reconnect.ts +60 -8
- package/src/core/types.ts +15 -16
- package/src/query/client.ts +144 -20
- package/src/query/errors.ts +34 -0
- package/src/query/index.ts +1 -0
- package/src/query/lines.ts +38 -6
- package/src/query/options.ts +36 -0
- package/src/query/reconnect.ts +27 -7
- package/src/query/ssh.ts +11 -4
- package/src/query/stream.ts +17 -1
- package/src/query/testing/live.ts +4 -4
- package/src/query/transport.ts +10 -2
- package/src/query/web.ts +76 -21
- package/src/stream/published.ts +70 -23
- package/src/stream/streams.ts +47 -13
- package/src/stream/wire.ts +30 -11
package/src/core/api/files.ts
CHANGED
|
@@ -37,10 +37,6 @@ import type {
|
|
|
37
37
|
import { asClientFileTransferID, asClientID, asServerFileTransferID } from "../command/ids.js";
|
|
38
38
|
import type { FileDownloadInfo, FileUploadInfo } from "../types.js";
|
|
39
39
|
|
|
40
|
-
/** The `ip` of a negotiation answer, when the server sent one. */
|
|
41
|
-
const ipField = (row: Record<string, string>): { readonly ip?: string } =>
|
|
42
|
-
str(row, "ip") === "" ? {} : { ip: str(row, "ip") };
|
|
43
|
-
|
|
44
40
|
const asPath = (path: string): string => (path.startsWith("/") ? path : `/${path}`);
|
|
45
41
|
|
|
46
42
|
/** One entry of a directory listing, or one file's details. */
|
|
@@ -109,9 +105,10 @@ export interface FileTreeEntry extends FileEntry {
|
|
|
109
105
|
/** How {@link listFilesRecursive} walks the file area. */
|
|
110
106
|
export interface FileTreeOptions extends FileOptions {
|
|
111
107
|
/**
|
|
112
|
-
* How many directory levels to
|
|
113
|
-
* starting directory only, as {@link listFiles} does;
|
|
114
|
-
* directories in it; and so on. 16 by default
|
|
108
|
+
* How many directory levels to list, counted as `find -maxdepth` counts: 1
|
|
109
|
+
* lists the starting directory only, as {@link listFiles} does; 2 also lists
|
|
110
|
+
* the directories in it; and so on. 16 by default; there is no unbounded
|
|
111
|
+
* walk, since every level is a round trip. A positive integer.
|
|
115
112
|
*/
|
|
116
113
|
readonly maxDepth?: number | undefined;
|
|
117
114
|
}
|
|
@@ -148,8 +145,8 @@ export async function listFilesRecursive(
|
|
|
148
145
|
options: FileTreeOptions = {},
|
|
149
146
|
): Promise<FileTreeEntry[]> {
|
|
150
147
|
const { maxDepth = DEFAULT_MAX_DEPTH, ...listOptions } = options;
|
|
151
|
-
if (!Number.isInteger(maxDepth) || maxDepth <
|
|
152
|
-
throw new RangeError(`maxDepth is a
|
|
148
|
+
if (!Number.isInteger(maxDepth) || maxDepth < 1) {
|
|
149
|
+
throw new RangeError(`maxDepth is a positive integer, not ${String(maxDepth)}`);
|
|
153
150
|
}
|
|
154
151
|
const found: FileTreeEntry[] = [];
|
|
155
152
|
const walk = async (directory: string, depth: number): Promise<void> => {
|
|
@@ -159,7 +156,7 @@ export async function listFilesRecursive(
|
|
|
159
156
|
if (entry.isDirectory && depth < maxDepth) await walk(entryPath, depth + 1);
|
|
160
157
|
}
|
|
161
158
|
};
|
|
162
|
-
await walk(asPath(path),
|
|
159
|
+
await walk(asPath(path), 1);
|
|
163
160
|
return found;
|
|
164
161
|
}
|
|
165
162
|
|
|
@@ -299,7 +296,11 @@ export interface FileTransferStopOptions extends CommandOptions {
|
|
|
299
296
|
* Stop only your own transfers: TeamSpeak 6 6.0.0-beta13.1 crashes on a
|
|
300
297
|
* ServerQuery `ftstop` for a transfer that is not the connection's own (one
|
|
301
298
|
* that does not exist, or another client's), and nothing here guards against
|
|
302
|
-
* it.
|
|
299
|
+
* it. A transfer of your own is gone once it has finished, or when nobody
|
|
300
|
+
* connected to it within about 10 seconds of its being negotiated (measured on
|
|
301
|
+
* TeamSpeak 3 3.13.8 and TeamSpeak 6 6.0.0), so stopping it then crashes
|
|
302
|
+
* TeamSpeak 6 too. See "Server behaviour the SDK works around" in
|
|
303
|
+
* `docs/serverquery-responses.md`.
|
|
303
304
|
*/
|
|
304
305
|
export async function stopFileTransfer(
|
|
305
306
|
client: CommandExecutor,
|
|
@@ -395,7 +396,14 @@ export interface FileUploadOptions extends CommandOptions {
|
|
|
395
396
|
* destroys a file unless asked to (`overwrite`, always sent).
|
|
396
397
|
*/
|
|
397
398
|
readonly overwrite?: boolean | undefined;
|
|
398
|
-
/**
|
|
399
|
+
/**
|
|
400
|
+
* Continue a partial upload; false by default (`resume`). The server keeps
|
|
401
|
+
* what an interrupted upload had received, and `seekPosition` of the answer
|
|
402
|
+
* is how much that is: send the file from there. Measured on TeamSpeak 3
|
|
403
|
+
* 3.13.8 and TeamSpeak 6 6.0.0: resuming a file that is not there is refused
|
|
404
|
+
* with 2054 (invalid file path), and `overwrite` together with `resume` with
|
|
405
|
+
* 2056 (overwrite excludes resume).
|
|
406
|
+
*/
|
|
399
407
|
readonly resume?: boolean | undefined;
|
|
400
408
|
}
|
|
401
409
|
|
|
@@ -436,10 +444,10 @@ export async function initFileUpload(
|
|
|
436
444
|
return {
|
|
437
445
|
fileTransferKey: str(row, "ftkey"),
|
|
438
446
|
port: num(row, "port"),
|
|
447
|
+
size,
|
|
439
448
|
seekPosition: big(row, "seekpos"),
|
|
440
449
|
clientFileTransferID: asClientFileTransferID(uint(row, "clientftfid", U16)),
|
|
441
450
|
serverFileTransferID: asServerFileTransferID(uint(row, "serverftfid", U16)),
|
|
442
|
-
...ipField(row),
|
|
443
451
|
};
|
|
444
452
|
}
|
|
445
453
|
|
|
@@ -447,7 +455,12 @@ export async function initFileUpload(
|
|
|
447
455
|
export interface FileDownloadOptions extends CommandOptions {
|
|
448
456
|
/** The channel's password, in cleartext; "" by default (`cpw`). */
|
|
449
457
|
readonly password?: string | undefined;
|
|
450
|
-
/**
|
|
458
|
+
/**
|
|
459
|
+
* The byte offset to start from, to resume; 0 by default (`seekpos`). The
|
|
460
|
+
* answer's `size` stays the whole file's, and the bytes sent are those from
|
|
461
|
+
* here to the end; an offset past the end is refused with 2057 (invalid file
|
|
462
|
+
* size), measured on TeamSpeak 3 3.13.8 and TeamSpeak 6 6.0.0.
|
|
463
|
+
*/
|
|
451
464
|
readonly seekPosition?: bigint | undefined;
|
|
452
465
|
}
|
|
453
466
|
|
|
@@ -463,6 +476,7 @@ export async function initFileDownload(
|
|
|
463
476
|
path: string,
|
|
464
477
|
options: FileDownloadOptions = {},
|
|
465
478
|
): Promise<FileDownloadInfo> {
|
|
479
|
+
const seekPosition = options.seekPosition ?? 0n;
|
|
466
480
|
const rows = await query(
|
|
467
481
|
client,
|
|
468
482
|
"ftinitdownload",
|
|
@@ -471,7 +485,7 @@ export async function initFileDownload(
|
|
|
471
485
|
["name", asPath(path)],
|
|
472
486
|
["cid", cid],
|
|
473
487
|
["cpw", options.password ?? ""],
|
|
474
|
-
["seekpos",
|
|
488
|
+
["seekpos", seekPosition],
|
|
475
489
|
["proto", 0],
|
|
476
490
|
],
|
|
477
491
|
options,
|
|
@@ -482,8 +496,8 @@ export async function initFileDownload(
|
|
|
482
496
|
fileTransferKey: str(row, "ftkey"),
|
|
483
497
|
port: num(row, "port"),
|
|
484
498
|
size: big(row, "size"),
|
|
499
|
+
seekPosition,
|
|
485
500
|
clientFileTransferID: asClientFileTransferID(uint(row, "clientftfid", U16)),
|
|
486
501
|
serverFileTransferID: asServerFileTransferID(uint(row, "serverftfid", U16)),
|
|
487
|
-
...ipField(row),
|
|
488
502
|
};
|
|
489
503
|
}
|
package/src/core/api/groups.ts
CHANGED
|
@@ -408,7 +408,10 @@ export async function getPermissionIDByName(
|
|
|
408
408
|
* What a permission is set on, as `permfind` and `permoverview` report it:
|
|
409
409
|
* discriminated on `target`, so each kind carries its own typed ids. The wire
|
|
410
410
|
* sends them as `t`, `id1` and `id2`, whose meaning depends on `t`; a row with
|
|
411
|
-
* a target this table does not name is
|
|
411
|
+
* a target this table does not name is passed through as the last kind, with
|
|
412
|
+
* the number and the raw ids. Narrowing on `target` therefore leaves that
|
|
413
|
+
* kind in the union: compare `target` with {@link PermissionTarget} and
|
|
414
|
+
* handle the rest in a default case.
|
|
412
415
|
*/
|
|
413
416
|
export type PermissionLocation =
|
|
414
417
|
| {
|
|
@@ -454,10 +457,20 @@ export type PermissionLocation =
|
|
|
454
457
|
readonly databaseID: ClientDBID;
|
|
455
458
|
/** The permission (`p`). */
|
|
456
459
|
readonly permission: PermissionID;
|
|
460
|
+
}
|
|
461
|
+
| {
|
|
462
|
+
/** Set on something this table does not name: the number the server sent (`t`). */
|
|
463
|
+
readonly target: number;
|
|
464
|
+
/** The first id as sent (`id1`); what it names depends on `target`. */
|
|
465
|
+
readonly id1: bigint;
|
|
466
|
+
/** The second id as sent (`id2`). */
|
|
467
|
+
readonly id2: bigint;
|
|
468
|
+
/** The permission (`p`). */
|
|
469
|
+
readonly permission: PermissionID;
|
|
457
470
|
};
|
|
458
471
|
|
|
459
|
-
/** Read a location from a `permfind` or `permoverview` row
|
|
460
|
-
function parseLocation(row: Record<string, string>): PermissionLocation
|
|
472
|
+
/** Read a location from a `permfind` or `permoverview` row. */
|
|
473
|
+
function parseLocation(row: Record<string, string>): PermissionLocation {
|
|
461
474
|
const permission = asPermissionID(uint(row, "p", U32));
|
|
462
475
|
const id1 = big(row, "id1");
|
|
463
476
|
const id2 = big(row, "id2");
|
|
@@ -487,7 +500,7 @@ function parseLocation(row: Record<string, string>): PermissionLocation | undefi
|
|
|
487
500
|
permission,
|
|
488
501
|
};
|
|
489
502
|
default:
|
|
490
|
-
return
|
|
503
|
+
return { target: num(row, "t"), id1, id2, permission };
|
|
491
504
|
}
|
|
492
505
|
}
|
|
493
506
|
|
|
@@ -498,7 +511,7 @@ export async function findPermission(
|
|
|
498
511
|
options?: CommandOptions,
|
|
499
512
|
): Promise<PermissionLocation[]> {
|
|
500
513
|
const rows = await list(client, "permfind", [permissionParam(permission)], options);
|
|
501
|
-
return rows.
|
|
514
|
+
return rows.map(parseLocation);
|
|
502
515
|
}
|
|
503
516
|
|
|
504
517
|
/** One of this connection's own permissions, as `permget` reports it. */
|
|
@@ -573,11 +586,12 @@ export async function getPermissionOverview(
|
|
|
573
586
|
],
|
|
574
587
|
options,
|
|
575
588
|
);
|
|
576
|
-
return rows.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
589
|
+
return rows.map((row) => ({
|
|
590
|
+
...parseLocation(row),
|
|
591
|
+
value: num(row, "v"),
|
|
592
|
+
negated: bool(row, "n"),
|
|
593
|
+
skip: bool(row, "s"),
|
|
594
|
+
}));
|
|
581
595
|
}
|
|
582
596
|
|
|
583
597
|
/**
|
|
@@ -605,7 +619,10 @@ export interface PrivilegeKey {
|
|
|
605
619
|
readonly token: string;
|
|
606
620
|
/** Which kind of group the key grants, one of {@link PrivilegeKeyType}. */
|
|
607
621
|
readonly type: number;
|
|
608
|
-
/**
|
|
622
|
+
/**
|
|
623
|
+
* The server group a `PrivilegeKeyType.ServerGroup` key grants; undefined
|
|
624
|
+
* for any other type, including one this SDK does not name.
|
|
625
|
+
*/
|
|
609
626
|
readonly serverGroupID: ServerGroupID | undefined;
|
|
610
627
|
/** The channel group a `PrivilegeKeyType.ChannelGroup` key grants; undefined otherwise. */
|
|
611
628
|
readonly channelGroupID: ChannelGroupID | undefined;
|
|
@@ -625,15 +642,17 @@ export interface PrivilegeKey {
|
|
|
625
642
|
/**
|
|
626
643
|
* Read a key. The wire carries the group as `token_id1` and the channel as
|
|
627
644
|
* `token_id2`, whichever kind of group it is; which typed field each belongs in
|
|
628
|
-
* depends on the token type.
|
|
645
|
+
* depends on the token type. A type this SDK does not name gets neither group
|
|
646
|
+
* nor channel: what its ids mean is not known.
|
|
629
647
|
*/
|
|
630
648
|
const parseKey = (row: Record<string, string>): PrivilegeKey => {
|
|
631
649
|
const type = num(row, "token_type");
|
|
632
650
|
const channelGroup = type === PrivilegeKeyType.ChannelGroup;
|
|
651
|
+
const serverGroup = type === PrivilegeKeyType.ServerGroup;
|
|
633
652
|
return {
|
|
634
653
|
token: str(row, "token"),
|
|
635
654
|
type,
|
|
636
|
-
serverGroupID:
|
|
655
|
+
serverGroupID: serverGroup ? asServerGroupID(big(row, "token_id1")) : undefined,
|
|
637
656
|
channelGroupID: channelGroup ? asChannelGroupID(big(row, "token_id1")) : undefined,
|
|
638
657
|
channelID: channelGroup ? asChannelID(big(row, "token_id2")) : undefined,
|
|
639
658
|
description: str(row, "token_description"),
|
package/src/core/api/parse.ts
CHANGED
|
@@ -45,39 +45,40 @@ const flag = (key: string): FieldSpec<boolean> => ({ keys: [key], read: bool });
|
|
|
45
45
|
/**
|
|
46
46
|
* The wire key of every client field but `id`, and how each is read: the one
|
|
47
47
|
* table `parseClient` and `parseClientChanges` share, so a row, a listing and
|
|
48
|
-
* a change cannot disagree about a name. `always` marks the fields
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* a change cannot disagree about a name. `always` marks the fields every
|
|
49
|
+
* source carries, which read as a default from a row without them; every
|
|
50
|
+
* other field is left out when the row lacks it.
|
|
51
51
|
*/
|
|
52
52
|
export const clientFields: {
|
|
53
53
|
readonly [K in Exclude<keyof ClientEntry, "id">]-?: FieldSpec<ClientEntry[K]>;
|
|
54
54
|
} = {
|
|
55
55
|
nickname: { keys: ["client_nickname"], read: str, always: true },
|
|
56
|
-
uid: { keys: ["client_unique_identifier"], read: (r, k) => asUID(str(r, k)), always: true },
|
|
57
56
|
// A listing names the channel `cid`, an enter notification `ctid`.
|
|
58
57
|
channelID: { keys: ["cid", "ctid"], read: (r, k) => asChannelID(big(r, k)), always: true },
|
|
59
58
|
type: { keys: ["client_type"], read: num, always: true },
|
|
59
|
+
uid: { keys: ["client_unique_identifier"], read: (r, k) => asUID(str(r, k)) },
|
|
60
60
|
serverGroups: {
|
|
61
61
|
keys: ["client_servergroups"],
|
|
62
62
|
read: (r, k) => parseServerGroups(r[k] ?? ""),
|
|
63
|
-
always: true,
|
|
64
63
|
},
|
|
65
|
-
isAway:
|
|
66
|
-
isRecording:
|
|
67
|
-
isTalker:
|
|
64
|
+
isAway: flag("client_away"),
|
|
65
|
+
isRecording: flag("client_is_recording"),
|
|
66
|
+
isTalker: flag("client_is_talker"),
|
|
68
67
|
channelGroupID: {
|
|
69
68
|
keys: ["client_channel_group_id"],
|
|
70
69
|
read: (r, k) => asChannelGroupID(big(r, k)),
|
|
71
|
-
always: true,
|
|
72
70
|
},
|
|
73
|
-
createdAt: { keys: ["client_created"], read: date
|
|
74
|
-
lastConnectedAt: { keys: ["client_lastconnected"], read: date
|
|
71
|
+
createdAt: { keys: ["client_created"], read: date },
|
|
72
|
+
lastConnectedAt: { keys: ["client_lastconnected"], read: date },
|
|
75
73
|
idleTimeMs: {
|
|
76
74
|
keys: ["client_idle_time"],
|
|
77
|
-
read: (r, k) =>
|
|
75
|
+
read: (r, k) => durationMs(r, k, MILLISECOND),
|
|
76
|
+
},
|
|
77
|
+
databaseID: {
|
|
78
|
+
keys: ["client_database_id"],
|
|
79
|
+
read: (r, k) => asClientDBID(big(r, k)),
|
|
78
80
|
always: true,
|
|
79
81
|
},
|
|
80
|
-
databaseID: { keys: ["client_database_id"], read: (r, k) => asClientDBID(big(r, k)) },
|
|
81
82
|
awayMessage: text("client_away_message"),
|
|
82
83
|
isInputMuted: flag("client_input_muted"),
|
|
83
84
|
isOutputMuted: flag("client_output_muted"),
|
|
@@ -120,8 +121,8 @@ export const clientFields: {
|
|
|
120
121
|
* which names its channel `ctid` where the list says `cid`. Reached by the
|
|
121
122
|
* client module through `./internal`.
|
|
122
123
|
*
|
|
123
|
-
* The fields
|
|
124
|
-
*
|
|
124
|
+
* The fields every source carries read as a default from a row without
|
|
125
|
+
* them; the others are `undefined` instead.
|
|
125
126
|
*/
|
|
126
127
|
export function parseClient(row: Record<string, string>): ClientEntry {
|
|
127
128
|
return {
|
|
@@ -40,6 +40,12 @@ export interface CommandOptions {
|
|
|
40
40
|
readonly signal?: AbortSignal | undefined;
|
|
41
41
|
/** Milliseconds to wait for the answer before failing with a CommandTimeoutError. */
|
|
42
42
|
readonly timeoutMs?: number | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Called once, when the command has left the flood throttle and was handed to
|
|
45
|
+
* the connection. A command that fails without it was never sent; one that
|
|
46
|
+
* fails after it may have been carried out.
|
|
47
|
+
*/
|
|
48
|
+
readonly onSent?: (() => void) | undefined;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
/**
|
|
@@ -96,6 +102,13 @@ export function encodeRequest(
|
|
|
96
102
|
return [line, ...rest.map((row) => buildCommandOrdered("", encode(row)).slice(1))].join("|");
|
|
97
103
|
}
|
|
98
104
|
|
|
105
|
+
/**
|
|
106
|
+
* A parameter name: letters, digits and underscores, with one leading `-` for a
|
|
107
|
+
* flag such as `-uid`. Every key measured on TeamSpeak 3 and 6, in a command or
|
|
108
|
+
* an answer, is of this form.
|
|
109
|
+
*/
|
|
110
|
+
const VALID_KEY = /^-?[A-Za-z0-9_]+$/;
|
|
111
|
+
|
|
99
112
|
function encodeParams(
|
|
100
113
|
params: ReadonlyArray<readonly [string, ParamValue]>,
|
|
101
114
|
passwords: readonly string[],
|
|
@@ -103,6 +116,11 @@ function encodeParams(
|
|
|
103
116
|
): Array<readonly [string, string]> {
|
|
104
117
|
const out: Array<readonly [string, string]> = [];
|
|
105
118
|
for (const [key, value] of params) {
|
|
119
|
+
// A key is a protocol identifier, written to the line as it is: one with a
|
|
120
|
+
// space, a newline or a pipe in it would add parameters, rows or a whole
|
|
121
|
+
// command to what the caller meant. Only a key a server could have named
|
|
122
|
+
// is sent.
|
|
123
|
+
if (!VALID_KEY.test(key)) throw new TypeError(`${JSON.stringify(key)} is not a parameter name`);
|
|
106
124
|
// Flags (`-uid`) are switches: present when true, absent otherwise.
|
|
107
125
|
if (key.startsWith("-")) {
|
|
108
126
|
if (value === true) out.push([key, ""]);
|
package/src/core/errors.ts
CHANGED
|
@@ -231,13 +231,78 @@ export class NoDataError extends TeamspeakError {
|
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
234
|
+
* `raw` as an unsigned 32-bit number, or undefined unless it is nothing but
|
|
235
|
+
* digits and fits: "12abc", " 7", "-1" and "4294967296" are not numbers the
|
|
236
|
+
* server sends, and are not read as 12, 7, 1 or a code no one defined.
|
|
237
|
+
*/
|
|
238
|
+
function parseUint32(raw: string | undefined): number | undefined {
|
|
239
|
+
if (raw === undefined || !/^\d+$/.test(raw)) return undefined;
|
|
240
|
+
const value = Number(raw);
|
|
241
|
+
return value <= 0xffff_ffff ? value : undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Read an error id. One that is missing or unreadable (not an unsigned 32-bit
|
|
246
|
+
* number) is reported as {@link ErrorCode.Undefined}: an error line that does
|
|
247
|
+
* not say what went wrong has still not said that nothing did.
|
|
237
248
|
*/
|
|
238
249
|
export function parseErrorCode(raw: string | undefined): number {
|
|
239
|
-
|
|
240
|
-
|
|
250
|
+
return parseUint32(raw) ?? ErrorCode.Undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* How many times a command refused as flooding is sent again after the pause
|
|
255
|
+
* the server asked for. On the voice protocol the first resend is usually
|
|
256
|
+
* refused once more with a shorter wait, and the second goes through (measured
|
|
257
|
+
* on TeamSpeak 3 and 6); on ServerQuery a refusal can be answered with a
|
|
258
|
+
* longer wait, which the caller's timeout then decides whether to sit out.
|
|
259
|
+
*/
|
|
260
|
+
export const FLOOD_RETRIES = 3;
|
|
261
|
+
/**
|
|
262
|
+
* How long to pause for a refusal that asked for `waitMs`. The wait is a floor:
|
|
263
|
+
* on the voice protocol a resend after exactly the wait is refused again, and
|
|
264
|
+
* one after half as long again goes through; on ServerQuery a resend after
|
|
265
|
+
* exactly 1 second was refused 2 times in 5, and one after 2.2 seconds went
|
|
266
|
+
* through 5 times in 5 (TeamSpeak 3 3.13.8 and TeamSpeak 6 6.0.0). Every
|
|
267
|
+
* refusal costs a retry and lengthens the server's pause, so the pause is
|
|
268
|
+
* twice the wait.
|
|
269
|
+
*/
|
|
270
|
+
export function floodHoldMs(waitMs: number): number {
|
|
271
|
+
return waitMs * 2;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* How long the server asks a client it refused as flooding to wait, in
|
|
276
|
+
* milliseconds. Undefined for any other error, or one that names no wait.
|
|
277
|
+
*
|
|
278
|
+
* Measured on TeamSpeak 3 3.13.8 and TeamSpeak 6 6.0.0: a voice client whose
|
|
279
|
+
* antiflood points pass `virtualserver_antiflood_points_needed_command_block`
|
|
280
|
+
* gets `error id=524 msg=client\sis\sflooding extra_msg=retry\sin\s1985ms`
|
|
281
|
+
* for each command, and the command is not run. The wait is a floor, not a
|
|
282
|
+
* promise: a command sent as the wait ends is refused again with a shorter one,
|
|
283
|
+
* and the second try went through in every measurement.
|
|
284
|
+
*
|
|
285
|
+
* A ServerQuery client (raw TCP and SSH; over WebQuery the server closes the
|
|
286
|
+
* connection and says nothing) that sends faster than
|
|
287
|
+
* `serverinstance_serverquery_flood_commands` per `_flood_time` gets the same
|
|
288
|
+
* error with the wait in whole seconds,
|
|
289
|
+
* `extra_msg=please\swait\s1\sseconds`, and the command is not run. The first
|
|
290
|
+
* refusals say 1; later ones say the instance's
|
|
291
|
+
* `serverinstance_serverquery_ban_time`, as the address is banned for that long
|
|
292
|
+
* (new connections are dropped meanwhile). Asking again every half second kept
|
|
293
|
+
* the ban going for as long as it was kept up (9 seconds against a ban time of
|
|
294
|
+
* 4), while waiting out the pause and asking once got through. The wait is a
|
|
295
|
+
* floor here too: about one resend in three after exactly the wait was refused
|
|
296
|
+
* again, with the ban time.
|
|
297
|
+
*/
|
|
298
|
+
export function floodRetryDelayMs(error: Error): number | undefined {
|
|
299
|
+
if (!(error instanceof ServerError) || error.code !== ErrorCode.ClientIsFlooding)
|
|
300
|
+
return undefined;
|
|
301
|
+
const extra = error.extraMessage ?? "";
|
|
302
|
+
const millis = /^retry in (\d{1,9})ms$/.exec(extra);
|
|
303
|
+
if (millis !== null) return Number(millis[1]);
|
|
304
|
+
const seconds = /^please wait (\d{1,9}) seconds$/.exec(extra);
|
|
305
|
+
return seconds === null ? undefined : Number(seconds[1]) * 1000;
|
|
241
306
|
}
|
|
242
307
|
|
|
243
308
|
/**
|
|
@@ -249,28 +314,17 @@ export function parseServerError(params: Record<string, string>): {
|
|
|
249
314
|
rc: number | null;
|
|
250
315
|
} {
|
|
251
316
|
const code = parseErrorCode(params["id"]);
|
|
252
|
-
const rcStr = params["return_code"];
|
|
253
317
|
|
|
254
318
|
let err: ServerError | null = null;
|
|
255
319
|
if (code !== ErrorCode.OK) {
|
|
256
|
-
const failedPermid = params["failed_permid"];
|
|
320
|
+
const failedPermid = parseUint32(params["failed_permid"]);
|
|
257
321
|
err = new ServerError(code, params["msg"] ?? "", {
|
|
258
322
|
extraMessage: params["extra_msg"] || undefined,
|
|
259
|
-
failedPermissionID:
|
|
260
|
-
failedPermid !== undefined &&
|
|
261
|
-
/^\d{1,10}$/.test(failedPermid) &&
|
|
262
|
-
Number(failedPermid) <= 0xffff_ffff
|
|
263
|
-
? asPermissionID(Number(failedPermid))
|
|
264
|
-
: undefined,
|
|
323
|
+
failedPermissionID: failedPermid === undefined ? undefined : asPermissionID(failedPermid),
|
|
265
324
|
failedPermission: params["failed_permission"] || undefined,
|
|
266
325
|
});
|
|
267
326
|
}
|
|
268
327
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const parsed = parseInt(rcStr, 10);
|
|
272
|
-
if (!isNaN(parsed)) rc = parsed;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return { err, rc };
|
|
328
|
+
// A return code that is not an unsigned 32-bit number names no command.
|
|
329
|
+
return { err, rc: parseUint32(params["return_code"]) ?? null };
|
|
276
330
|
}
|
package/src/core/events.ts
CHANGED
|
@@ -20,6 +20,11 @@ export interface TextMessageEvent {
|
|
|
20
20
|
* The sender's server groups. A text message does not carry them; they are
|
|
21
21
|
* those of the sender as the client knows it, so they are `undefined` for a
|
|
22
22
|
* sender the client has not seen.
|
|
23
|
+
*
|
|
24
|
+
* A ServerQuery connection is not sent `notifyclientupdated`, so for a query
|
|
25
|
+
* client they are the groups from the listing taken on connecting, kept
|
|
26
|
+
* current only by clients entering, leaving and moving: a change of groups
|
|
27
|
+
* made since is not in them.
|
|
23
28
|
*/
|
|
24
29
|
readonly invokerGroups: readonly ServerGroupID[] | undefined;
|
|
25
30
|
/**
|
|
@@ -38,15 +43,21 @@ export interface TextMessageEvent {
|
|
|
38
43
|
|
|
39
44
|
/** A client changing channel, of its own accord or moved by someone. */
|
|
40
45
|
export interface ClientMovedEvent {
|
|
41
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Who moved the client; "" when the server names no one, which it does not
|
|
48
|
+
* for a move of the client's own (reason 0).
|
|
49
|
+
*/
|
|
42
50
|
readonly invokerName: string;
|
|
43
|
-
/** The mover's unique identifier. */
|
|
51
|
+
/** The mover's unique identifier; "" when none is named. */
|
|
44
52
|
readonly invokerUID: UID;
|
|
45
53
|
/** The channel the client is now in. */
|
|
46
54
|
readonly targetChannelID: ChannelID;
|
|
47
55
|
/**
|
|
48
|
-
* The channel the client came from
|
|
49
|
-
*
|
|
56
|
+
* The channel the client came from: `cfid`, or when the server sends none
|
|
57
|
+
* (TS3 3.13.8 sends no `cfid` on a move, TS6 6.0.0 does) the channel the
|
|
58
|
+
* client was known to be in. Absent only when the server sent none and the
|
|
59
|
+
* client was not known. A client's own roster entry is updated before the
|
|
60
|
+
* event is delivered, so this is the only place the previous channel is.
|
|
50
61
|
*/
|
|
51
62
|
readonly fromChannelID?: ChannelID | undefined;
|
|
52
63
|
/** Why the client moved; compare it with `Reason`. */
|
|
@@ -55,7 +66,7 @@ export interface ClientMovedEvent {
|
|
|
55
66
|
readonly reasonMessage: string;
|
|
56
67
|
/** The client that moved. */
|
|
57
68
|
readonly clientID: ClientID;
|
|
58
|
-
/** The mover. */
|
|
69
|
+
/** The mover; 0 when the server names no one, as for a move of the client's own. */
|
|
59
70
|
readonly invokerID: ClientID;
|
|
60
71
|
}
|
|
61
72
|
|
|
@@ -113,8 +124,9 @@ export interface KickedEvent {
|
|
|
113
124
|
/**
|
|
114
125
|
* A client coming into view: the record `listClients` returns, so one type
|
|
115
126
|
* describes a client however it was learned of, with what only the
|
|
116
|
-
* notification says. Its own id is `id`, as in every record. The
|
|
117
|
-
* `clientlist
|
|
127
|
+
* notification says. Its own id is `id`, as in every record. The notification
|
|
128
|
+
* carries every field but the ones only `clientlist` reports (`-times`,
|
|
129
|
+
* `-info`, `-ip`), which are undefined here.
|
|
118
130
|
*
|
|
119
131
|
* `fromChannelID` and `reason` belong to the event: the roster and
|
|
120
132
|
* `view.clients` keep the record without them.
|
|
@@ -365,7 +377,11 @@ export interface VoiceDataEvent {
|
|
|
365
377
|
* server may relay a codec that table does not name.
|
|
366
378
|
*/
|
|
367
379
|
readonly codec: number;
|
|
368
|
-
/**
|
|
380
|
+
/**
|
|
381
|
+
* One encoded frame, copied: it stays valid after the listener returns. Empty
|
|
382
|
+
* when the speaker stopped talking: it is the end of that speech, not a
|
|
383
|
+
* frame to decode.
|
|
384
|
+
*/
|
|
369
385
|
readonly data: Uint8Array;
|
|
370
386
|
}
|
|
371
387
|
|
package/src/core/internal.ts
CHANGED
|
@@ -20,6 +20,12 @@ export type { NotificationResult } from "./notifications.js";
|
|
|
20
20
|
export { parseIconID } from "./command/ids.js";
|
|
21
21
|
export { responseSpec } from "./command/responses.js";
|
|
22
22
|
export type { ResponseSpec } from "./command/responses.js";
|
|
23
|
-
export {
|
|
23
|
+
export {
|
|
24
|
+
floodHoldMs,
|
|
25
|
+
FLOOD_RETRIES,
|
|
26
|
+
floodRetryDelayMs,
|
|
27
|
+
parseErrorCode,
|
|
28
|
+
parseServerError,
|
|
29
|
+
} from "./errors.js";
|
|
24
30
|
export { splitCommandRows, parseUint64, parseUint16, parseInt10, sleep } from "./helpers.js";
|
|
25
31
|
export { jitteredDelay, resolveBackoffPolicy } from "./reconnect.js";
|
|
@@ -81,7 +81,7 @@ export type NotificationResult =
|
|
|
81
81
|
* client's own id (0 when it is not known yet), and `clients` the clients the
|
|
82
82
|
* caller knows, which the enter, leave, move, update and channel group
|
|
83
83
|
* notifications keep current and the others read: a text message carries no
|
|
84
|
-
* groups, and a move
|
|
84
|
+
* groups, and a move carries no `cfid` on TS3. An entry is
|
|
85
85
|
* replaced, never edited, so an event that holds one stays as it was.
|
|
86
86
|
*
|
|
87
87
|
* The server-group notifications leave the clients alone: the update that
|
|
@@ -214,22 +214,23 @@ function handleClientMoved(cmd: Command, clients: Map<number, ClientEntry>): Not
|
|
|
214
214
|
const existing = clients.get(clid);
|
|
215
215
|
if (existing) clients.set(clid, { ...existing, channelID: ctid });
|
|
216
216
|
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
const
|
|
217
|
+
// Where the client came from: `cfid` when the server sends it, and when it
|
|
218
|
+
// does not (TS3 sends none on a move) the channel the roster had it in,
|
|
219
|
+
// which this notification is about to replace.
|
|
220
|
+
const from = cmd.params["cfid"] === undefined ? existing?.channelID : undefined;
|
|
221
221
|
|
|
222
|
+
// A move the client made itself (reason 0) names no invoker, and the fields
|
|
223
|
+
// are left empty: it is not filled in from the roster.
|
|
222
224
|
return {
|
|
223
225
|
kind: "clientMoved",
|
|
224
226
|
event: {
|
|
225
227
|
clientID: clid,
|
|
226
228
|
targetChannelID: ctid,
|
|
227
229
|
...channelOf(cmd.params, "cfid", "fromChannelID"),
|
|
230
|
+
...(from === undefined ? {} : { fromChannelID: from }),
|
|
228
231
|
reason: parseInt10(cmd.params["reasonid"] ?? ""),
|
|
229
232
|
reasonMessage: cmd.params["reasonmsg"] ?? "",
|
|
230
|
-
|
|
231
|
-
invokerName: cmd.params["invokername"] ?? invoker?.nickname ?? "",
|
|
232
|
-
invokerUID: asUID(cmd.params["invokeruid"] ?? invoker?.uid ?? ""),
|
|
233
|
+
...invoked(cmd.params),
|
|
233
234
|
},
|
|
234
235
|
};
|
|
235
236
|
}
|