@honeybbq/teamspeak 1.0.0-beta.13 → 1.0.0-beta.14
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 +19 -1
- package/dist/client/api/files.d.ts +89 -0
- package/dist/client/api/files.d.ts.map +1 -0
- package/dist/client/api/files.js +125 -0
- package/dist/client/api/files.js.map +1 -0
- package/dist/client/client.d.ts +2 -2
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/core/api/channels.d.ts +53 -1
- package/dist/core/api/channels.d.ts.map +1 -1
- package/dist/core/api/channels.js +64 -1
- package/dist/core/api/channels.js.map +1 -1
- package/dist/core/api/clients.d.ts +87 -1
- package/dist/core/api/clients.d.ts.map +1 -1
- package/dist/core/api/clients.js +95 -1
- package/dist/core/api/clients.js.map +1 -1
- package/dist/core/api/command.d.ts +40 -1
- package/dist/core/api/command.d.ts.map +1 -1
- package/dist/core/api/command.js +26 -1
- package/dist/core/api/command.js.map +1 -1
- package/dist/core/api/files.d.ts +39 -1
- package/dist/core/api/files.d.ts.map +1 -1
- package/dist/core/api/files.js +46 -1
- package/dist/core/api/files.js.map +1 -1
- package/dist/core/api/groups.d.ts +28 -3
- package/dist/core/api/groups.d.ts.map +1 -1
- package/dist/core/api/groups.js +32 -3
- package/dist/core/api/groups.js.map +1 -1
- package/dist/core/api/moderation.d.ts +36 -2
- package/dist/core/api/moderation.d.ts.map +1 -1
- package/dist/core/api/moderation.js +36 -3
- package/dist/core/api/moderation.js.map +1 -1
- package/dist/core/api/server.d.ts +94 -1
- package/dist/core/api/server.d.ts.map +1 -1
- package/dist/core/api/server.js +105 -1
- package/dist/core/api/server.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/query/client.d.ts.map +1 -1
- package/dist/query/client.js +76 -5
- package/dist/query/client.js.map +1 -1
- package/dist/query/late-rows.d.ts +31 -0
- package/dist/query/late-rows.d.ts.map +1 -0
- package/dist/query/late-rows.js +49 -0
- package/dist/query/late-rows.js.map +1 -0
- package/package.json +1 -1
- package/src/client/api/files.ts +249 -0
- package/src/client/client.ts +2 -2
- package/src/client/index.ts +1 -0
- package/src/core/api/channels.ts +122 -1
- package/src/core/api/clients.ts +167 -1
- package/src/core/api/command.ts +74 -1
- package/src/core/api/files.ts +72 -1
- package/src/core/api/groups.ts +64 -3
- package/src/core/api/moderation.ts +52 -3
- package/src/core/api/server.ts +194 -1
- package/src/core/index.ts +8 -1
- package/src/query/client.ts +82 -4
- package/src/query/late-rows.ts +64 -0
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
firstRow,
|
|
11
11
|
exec,
|
|
12
12
|
query,
|
|
13
|
+
queryRows,
|
|
14
|
+
continueOnErrorParams,
|
|
13
15
|
list,
|
|
14
16
|
encodeFields,
|
|
15
17
|
nonEmpty,
|
|
@@ -22,7 +24,7 @@ import {
|
|
|
22
24
|
durationMs,
|
|
23
25
|
SECOND,
|
|
24
26
|
} from "./command.js";
|
|
25
|
-
import type { Params, WireNames } from "./command.js";
|
|
27
|
+
import type { BatchOptions, Params, WireNames } from "./command.js";
|
|
26
28
|
import type { CommandExecutor, CommandOptions, ParamValue } from "../command/request.js";
|
|
27
29
|
import type { BanID, ClientDBID, ClientID, MessageID, UID } from "../command/ids.js";
|
|
28
30
|
import { asBanID, asClientDBID, asMessageID, asUID } from "../command/ids.js";
|
|
@@ -250,8 +252,11 @@ export async function addBan(
|
|
|
250
252
|
*
|
|
251
253
|
* Measured on TS3 3.13.8 and TS6 beta13.1: two bans, one for the uid and one
|
|
252
254
|
* for the address. TS6 has also answered with no `banid` at all though both
|
|
253
|
-
* bans were made (about once in a hundred calls over
|
|
254
|
-
*
|
|
255
|
+
* bans were made (about once in a hundred calls over WebQuery), so an empty
|
|
256
|
+
* result does not mean no ban, and no lookup of the ids is made: a ban has no
|
|
257
|
+
* field that ties it to this call. Over SSH, TS6 may write the `banid` rows
|
|
258
|
+
* after the `error` line; the ServerQuery client waits briefly for them, so
|
|
259
|
+
* they are not lost to that. A caller that must undo the ban lists the
|
|
255
260
|
* bans before and after and removes what is new: a ban on the address refuses
|
|
256
261
|
* every later connection from it.
|
|
257
262
|
*/
|
|
@@ -269,6 +274,50 @@ export async function banClient(
|
|
|
269
274
|
return rows.flatMap((row) => (str(row, "banid") === "" ? [] : [asBanID(big(row, "banid"))]));
|
|
270
275
|
}
|
|
271
276
|
|
|
277
|
+
/** How {@link banClients} bans, and whether it goes on past a failure. */
|
|
278
|
+
export interface BanClientsOptions extends BanClientOptions, BatchOptions {}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Ban several connected clients by clid, in one command, such as
|
|
282
|
+
* `banclient time=60 clid=5|clid=6`, and return the id of every ban the server
|
|
283
|
+
* reports. With no ids nothing is sent and the result is `[]`.
|
|
284
|
+
*
|
|
285
|
+
* Each client costs the bans {@link banClient} describes; clients that share an
|
|
286
|
+
* address share one address ban, so two clients behind one address made three
|
|
287
|
+
* ids. The voice protocol reports no ids, so there this resolves to `[]` even
|
|
288
|
+
* when bans were made.
|
|
289
|
+
*
|
|
290
|
+
* The server reports one answer for the whole call, never one per client: the
|
|
291
|
+
* ids of the bans made, then success, or one error. Measured on TeamSpeak 3
|
|
292
|
+
* 3.13.8 and TeamSpeak 6 beta13.1, over raw ServerQuery, SSH, WebQuery and the
|
|
293
|
+
* voice protocol alike:
|
|
294
|
+
*
|
|
295
|
+
* - Without `continueOnError`, the server first checks that every id names a
|
|
296
|
+
* client: one that does not (512) refuses the call and nothing is banned.
|
|
297
|
+
* Past that, clients are banned in the order given and a failure for one
|
|
298
|
+
* (insufficient permissions, 2568) stops the call: the clients before it are
|
|
299
|
+
* banned, the ones after it are not, and the error is that client's.
|
|
300
|
+
* - With it (`-continueonerror`), a client that cannot be banned is skipped,
|
|
301
|
+
* the others are banned, the ids of the bans made are returned, and the
|
|
302
|
+
* answer is success even when none could be banned.
|
|
303
|
+
*
|
|
304
|
+
* A client that is banned is disconnected.
|
|
305
|
+
*/
|
|
306
|
+
export async function banClients(
|
|
307
|
+
client: CommandExecutor,
|
|
308
|
+
clientIDs: readonly ClientID[],
|
|
309
|
+
options: BanClientsOptions = {},
|
|
310
|
+
): Promise<BanID[]> {
|
|
311
|
+
const rows = await queryRows(
|
|
312
|
+
client,
|
|
313
|
+
"banclient",
|
|
314
|
+
[...continueOnErrorParams(options), ...encodeFields<BanClientOptions>(options, banClientNames)],
|
|
315
|
+
clientIDs.map((clid) => [["clid", clid]]),
|
|
316
|
+
options,
|
|
317
|
+
);
|
|
318
|
+
return rows.flatMap((row) => (str(row, "banid") === "" ? [] : [asBanID(big(row, "banid"))]));
|
|
319
|
+
}
|
|
320
|
+
|
|
272
321
|
/** Remove a ban. */
|
|
273
322
|
export async function deleteBan(
|
|
274
323
|
client: CommandExecutor,
|
package/src/core/api/server.ts
CHANGED
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
U16,
|
|
14
14
|
firstRow,
|
|
15
15
|
exec,
|
|
16
|
+
execRows,
|
|
17
|
+
continueOnErrorParams,
|
|
16
18
|
query,
|
|
17
19
|
list,
|
|
18
20
|
encodeFields,
|
|
@@ -28,7 +30,13 @@ import {
|
|
|
28
30
|
durationMs,
|
|
29
31
|
SECOND,
|
|
30
32
|
} from "./command.js";
|
|
31
|
-
import type {
|
|
33
|
+
import type {
|
|
34
|
+
BatchOptions,
|
|
35
|
+
Params,
|
|
36
|
+
PermissionEntry,
|
|
37
|
+
PermissionValue,
|
|
38
|
+
WireNames,
|
|
39
|
+
} from "./command.js";
|
|
32
40
|
import type { CommandExecutor, CommandOptions } from "../command/request.js";
|
|
33
41
|
import type {
|
|
34
42
|
ChannelID,
|
|
@@ -560,6 +568,67 @@ export async function getServerGroupsByClient(
|
|
|
560
568
|
return rows.map((row) => ({ id: asServerGroupID(big(row, "sgid")), name: str(row, "name") }));
|
|
561
569
|
}
|
|
562
570
|
|
|
571
|
+
/**
|
|
572
|
+
* Add several clients to a server group, in one command, such as
|
|
573
|
+
* `servergroupaddclient sgid=9 cldbid=3|cldbid=4`. With no ids nothing is sent.
|
|
574
|
+
*
|
|
575
|
+
* The server reports one answer for the whole call, never one per client.
|
|
576
|
+
* Measured on TeamSpeak 3 3.13.8 and TeamSpeak 6 beta13.1, over raw
|
|
577
|
+
* ServerQuery, SSH, WebQuery and the voice protocol alike, so this resolves to
|
|
578
|
+
* nothing and a failure says only what the server said:
|
|
579
|
+
*
|
|
580
|
+
* - Without `continueOnError`, the clients are added in the order given and
|
|
581
|
+
* the call stops at the first one that fails (an unknown database id, 512;
|
|
582
|
+
* a client already in the group, 2561): the clients before it are in the
|
|
583
|
+
* group, the ones after it are not, and the error is that client's.
|
|
584
|
+
* - With it (`-continueonerror`), every client is tried and each one that can
|
|
585
|
+
* be added is. The answer is success only if none failed; otherwise it is
|
|
586
|
+
* the error of the last one that failed, so two different failures show
|
|
587
|
+
* only the later. Read the members, as `listServerGroupClients` reports
|
|
588
|
+
* them, to know who was added.
|
|
589
|
+
*/
|
|
590
|
+
export async function addClientsToServerGroup(
|
|
591
|
+
client: CommandExecutor,
|
|
592
|
+
sgid: ServerGroupID,
|
|
593
|
+
databaseIDs: readonly ClientDBID[],
|
|
594
|
+
options: BatchOptions = {},
|
|
595
|
+
): Promise<void> {
|
|
596
|
+
await execRows(
|
|
597
|
+
client,
|
|
598
|
+
"servergroupaddclient",
|
|
599
|
+
[...continueOnErrorParams(options), ["sgid", sgid]],
|
|
600
|
+
databaseIDs.map((cldbid) => [["cldbid", cldbid]]),
|
|
601
|
+
options,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Remove several clients from a server group, in one command
|
|
607
|
+
* (`servergroupdelclient sgid=9 cldbid=3|cldbid=4`). With no ids nothing is
|
|
608
|
+
* sent.
|
|
609
|
+
*
|
|
610
|
+
* As for {@link addClientsToServerGroup}, the server reports one answer for
|
|
611
|
+
* the call. Without `continueOnError` the call stops at the first client that
|
|
612
|
+
* fails, with earlier clients removed and later ones not; with it, every client
|
|
613
|
+
* is tried and the answer is the error of the last that failed. A client that
|
|
614
|
+
* is not in the group fails with 2563 (empty result set), as does an unknown
|
|
615
|
+
* database id.
|
|
616
|
+
*/
|
|
617
|
+
export async function removeClientsFromServerGroup(
|
|
618
|
+
client: CommandExecutor,
|
|
619
|
+
sgid: ServerGroupID,
|
|
620
|
+
databaseIDs: readonly ClientDBID[],
|
|
621
|
+
options: BatchOptions = {},
|
|
622
|
+
): Promise<void> {
|
|
623
|
+
await execRows(
|
|
624
|
+
client,
|
|
625
|
+
"servergroupdelclient",
|
|
626
|
+
[...continueOnErrorParams(options), ["sgid", sgid]],
|
|
627
|
+
databaseIDs.map((cldbid) => [["cldbid", cldbid]]),
|
|
628
|
+
options,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
563
632
|
/** Permissions set on a server group. */
|
|
564
633
|
export async function listServerGroupPermissions(
|
|
565
634
|
client: CommandExecutor,
|
|
@@ -601,6 +670,84 @@ export async function addServerGroupPermission(
|
|
|
601
670
|
);
|
|
602
671
|
}
|
|
603
672
|
|
|
673
|
+
/** One permission to set on a server group, as {@link addServerGroupPermissions} takes it. */
|
|
674
|
+
export interface ServerGroupPermissionValue extends PermissionValue {
|
|
675
|
+
/** Whether the value is negated, so the lowest wins; false by default. Always sent (`permnegated`). */
|
|
676
|
+
readonly negated?: boolean | undefined;
|
|
677
|
+
/** Whether it skips channel and client overrides; false by default. Always sent (`permskip`). */
|
|
678
|
+
readonly skip?: boolean | undefined;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Set several permissions on a server group, in one command: each permission
|
|
683
|
+
* is a row of its own, with all its parameters. With none, nothing is sent.
|
|
684
|
+
*
|
|
685
|
+
* The server checks every row before it applies any: a permission it does not
|
|
686
|
+
* know (2562), a value it cannot read (1540) or a row missing a parameter
|
|
687
|
+
* (1539) refuses the call, and none is set, with or without `continueOnError`.
|
|
688
|
+
* Measured on TeamSpeak 3 3.13.8 and TeamSpeak 6 beta13.1, over raw
|
|
689
|
+
* ServerQuery, SSH, WebQuery and the voice protocol alike. Resolves to
|
|
690
|
+
* nothing: the server never reports an answer per row.
|
|
691
|
+
*
|
|
692
|
+
* A permission the caller has too little modify power for (2570, or 2568 where
|
|
693
|
+
* the group power is short) is another matter: the rows are applied in order.
|
|
694
|
+
* Without `continueOnError` the first row that fails ends the call, so the rows
|
|
695
|
+
* before it are set and the rows after it are not; with it the rows that fail
|
|
696
|
+
* are skipped and the others set. The error is the failing row's, and which row
|
|
697
|
+
* is not reported; with several failing rows which error comes back was not
|
|
698
|
+
* measured. Measured on TeamSpeak 3 3.13.8 and TeamSpeak 6 beta13.1, over raw
|
|
699
|
+
* ServerQuery (TeamSpeak 3), SSH, WebQuery and the voice protocol, in both
|
|
700
|
+
* orders of a free and a locked permission.
|
|
701
|
+
*/
|
|
702
|
+
export async function addServerGroupPermissions(
|
|
703
|
+
client: CommandExecutor,
|
|
704
|
+
sgid: ServerGroupID,
|
|
705
|
+
permissions: readonly ServerGroupPermissionValue[],
|
|
706
|
+
options: BatchOptions = {},
|
|
707
|
+
): Promise<void> {
|
|
708
|
+
await execRows(
|
|
709
|
+
client,
|
|
710
|
+
"servergroupaddperm",
|
|
711
|
+
[...continueOnErrorParams(options), ["sgid", sgid]],
|
|
712
|
+
permissions.map(serverGroupPermissionRow),
|
|
713
|
+
options,
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const serverGroupPermissionRow = (entry: ServerGroupPermissionValue): Params => [
|
|
718
|
+
permissionParam(entry.permission),
|
|
719
|
+
["permvalue", entry.value],
|
|
720
|
+
["permnegated", entry.negated ?? false],
|
|
721
|
+
["permskip", entry.skip ?? false],
|
|
722
|
+
];
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Remove several permissions from a server group, in one command: each
|
|
726
|
+
* permission is a row (`permsid=…|permsid=…`). With none, nothing is sent.
|
|
727
|
+
*
|
|
728
|
+
* As for {@link addServerGroupPermissions}, the server checks every row first
|
|
729
|
+
* and a permission it does not know (2562) refuses the call with none removed,
|
|
730
|
+
* with or without `continueOnError`. Removing a permission the group does not
|
|
731
|
+
* have succeeds. A permission the caller has too little modify power for
|
|
732
|
+
* (2570) ends the call at that row, so the rows before it are removed and the
|
|
733
|
+
* rows after it are not; with `continueOnError` that row is skipped and the
|
|
734
|
+
* others removed.
|
|
735
|
+
*/
|
|
736
|
+
export async function deleteServerGroupPermissions(
|
|
737
|
+
client: CommandExecutor,
|
|
738
|
+
sgid: ServerGroupID,
|
|
739
|
+
permissions: readonly Permission[],
|
|
740
|
+
options: BatchOptions = {},
|
|
741
|
+
): Promise<void> {
|
|
742
|
+
await execRows(
|
|
743
|
+
client,
|
|
744
|
+
"servergroupdelperm",
|
|
745
|
+
[...continueOnErrorParams(options), ["sgid", sgid]],
|
|
746
|
+
permissions.map((permission) => [permissionParam(permission)]),
|
|
747
|
+
options,
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
604
751
|
/** Remove a permission from a server group. */
|
|
605
752
|
export async function deleteServerGroupPermission(
|
|
606
753
|
client: CommandExecutor,
|
|
@@ -643,6 +790,52 @@ export async function addServerGroupPermissionByType(
|
|
|
643
790
|
);
|
|
644
791
|
}
|
|
645
792
|
|
|
793
|
+
/**
|
|
794
|
+
* Set several permissions on every regular server group of a type, on every
|
|
795
|
+
* virtual server of the instance, in one command; see
|
|
796
|
+
* {@link addServerGroupPermissionByType} for what the type selects and what it
|
|
797
|
+
* needs. Each permission is a row of its own. With none, nothing is sent.
|
|
798
|
+
*
|
|
799
|
+
* The server documents no `-continueonerror` for this command, so none is
|
|
800
|
+
* offered. A permission it does not know (2562) refuses the call before any is
|
|
801
|
+
* set; a voice client gets error 2568, as for the single form.
|
|
802
|
+
*/
|
|
803
|
+
export async function addServerGroupPermissionsByType(
|
|
804
|
+
client: CommandExecutor,
|
|
805
|
+
type: GroupAutoUpdateType,
|
|
806
|
+
permissions: readonly ServerGroupPermissionValue[],
|
|
807
|
+
options?: CommandOptions,
|
|
808
|
+
): Promise<void> {
|
|
809
|
+
await execRows(
|
|
810
|
+
client,
|
|
811
|
+
"servergroupautoaddperm",
|
|
812
|
+
[["sgtype", type]],
|
|
813
|
+
permissions.map(serverGroupPermissionRow),
|
|
814
|
+
options,
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Remove several permissions from every regular server group of a type, in one
|
|
820
|
+
* command; see {@link addServerGroupPermissionsByType}. With none, nothing is
|
|
821
|
+
* sent. Needs `b_permission_modify_power_ignore`: a voice client gets error
|
|
822
|
+
* 2568.
|
|
823
|
+
*/
|
|
824
|
+
export async function deleteServerGroupPermissionsByType(
|
|
825
|
+
client: CommandExecutor,
|
|
826
|
+
type: GroupAutoUpdateType,
|
|
827
|
+
permissions: readonly Permission[],
|
|
828
|
+
options?: CommandOptions,
|
|
829
|
+
): Promise<void> {
|
|
830
|
+
await execRows(
|
|
831
|
+
client,
|
|
832
|
+
"servergroupautodelperm",
|
|
833
|
+
[["sgtype", type]],
|
|
834
|
+
permissions.map((permission) => [permissionParam(permission)]),
|
|
835
|
+
options,
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
646
839
|
/**
|
|
647
840
|
* Remove a permission from every regular server group of a type, on every
|
|
648
841
|
* virtual server of the instance; see {@link addServerGroupPermissionByType}.
|
package/src/core/index.ts
CHANGED
|
@@ -150,5 +150,12 @@ export * from "./api/icons.js";
|
|
|
150
150
|
export * from "./api/credentials.js";
|
|
151
151
|
export * from "./api/instance.js";
|
|
152
152
|
export { applyChannelChanges } from "./api/command.js";
|
|
153
|
-
export type {
|
|
153
|
+
export type {
|
|
154
|
+
BatchOptions,
|
|
155
|
+
Channel,
|
|
156
|
+
ChannelChanges,
|
|
157
|
+
PagingOptions,
|
|
158
|
+
PermissionEntry,
|
|
159
|
+
PermissionValue,
|
|
160
|
+
} from "./api/command.js";
|
|
154
161
|
export { ChannelTree } from "./channel-tree.js";
|
package/src/query/client.ts
CHANGED
|
@@ -39,6 +39,7 @@ import { connectTcp } from "./tcp.js";
|
|
|
39
39
|
import { connectSsh } from "./ssh.js";
|
|
40
40
|
import { WebQueryTransport } from "./web.js";
|
|
41
41
|
import { GuestAccessError, QueryAuthError } from "./errors.js";
|
|
42
|
+
import { LATE_ROW_GRACE_MS, LATE_ROW_QUIET_MS, hasLateRow, lateRowsOf } from "./late-rows.js";
|
|
42
43
|
import {
|
|
43
44
|
assertEventsSupported,
|
|
44
45
|
normalizeQueryOptions,
|
|
@@ -103,6 +104,16 @@ interface Pending {
|
|
|
103
104
|
readonly rows: Array<Record<string, string>>;
|
|
104
105
|
resolve(rows: Array<Record<string, string>>): void;
|
|
105
106
|
reject(error: unknown): void;
|
|
107
|
+
/** Set once the `error` line has been read: what follows is a late row or nothing. */
|
|
108
|
+
held?: {
|
|
109
|
+
readonly keys: readonly string[];
|
|
110
|
+
readonly several: boolean;
|
|
111
|
+
readonly any: boolean;
|
|
112
|
+
timer: ReturnType<typeof setTimeout>;
|
|
113
|
+
/** Wait for another row, or for the rows to stop. */
|
|
114
|
+
wait(ms: number): void;
|
|
115
|
+
finish(): void;
|
|
116
|
+
};
|
|
106
117
|
}
|
|
107
118
|
|
|
108
119
|
/**
|
|
@@ -132,6 +143,12 @@ export class QueryClient implements CommandExecutor, AsyncDisposable {
|
|
|
132
143
|
#version = "";
|
|
133
144
|
|
|
134
145
|
#pending: Pending | null = null;
|
|
146
|
+
/**
|
|
147
|
+
* The key of a row that was waited for and did not come before its answer
|
|
148
|
+
* was given up on. Should it arrive later, it belongs to that command and is
|
|
149
|
+
* dropped rather than handed to the next one.
|
|
150
|
+
*/
|
|
151
|
+
#strayKey: string | undefined;
|
|
135
152
|
/**
|
|
136
153
|
* Answers still to come for commands that gave up waiting. ServerQuery has
|
|
137
154
|
* no return_code, only order, so each is read and discarded when it arrives
|
|
@@ -392,6 +409,7 @@ export class QueryClient implements CommandExecutor, AsyncDisposable {
|
|
|
392
409
|
this.#closed = false;
|
|
393
410
|
this.#abandoned = 0;
|
|
394
411
|
this.#pending = null;
|
|
412
|
+
this.#strayKey = undefined;
|
|
395
413
|
this.#clients.clear();
|
|
396
414
|
this.#clid = 0;
|
|
397
415
|
this.#serverFamily = "unknown";
|
|
@@ -933,8 +951,12 @@ export class QueryClient implements CommandExecutor, AsyncDisposable {
|
|
|
933
951
|
clearTimeout(nudger);
|
|
934
952
|
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
935
953
|
if (this.#pending !== null) {
|
|
954
|
+
const gone = this.#pending;
|
|
936
955
|
this.#pending = null;
|
|
937
|
-
if (
|
|
956
|
+
if (gone.held) {
|
|
957
|
+
// Its `error` line was read already, so no answer is still to come.
|
|
958
|
+
clearTimeout(gone.held.timer);
|
|
959
|
+
} else if (!sending.failed) {
|
|
938
960
|
this.#abandoned++;
|
|
939
961
|
abandonedByUs = true;
|
|
940
962
|
}
|
|
@@ -957,9 +979,49 @@ export class QueryClient implements CommandExecutor, AsyncDisposable {
|
|
|
957
979
|
this.#pending = null;
|
|
958
980
|
if (!pending) return;
|
|
959
981
|
const { err } = parseServerError(cmd.params);
|
|
982
|
+
this.#strayKey = undefined;
|
|
960
983
|
const refusal = err ?? transferRefusal(pending.command, pending.rows);
|
|
961
|
-
if (refusal)
|
|
962
|
-
|
|
984
|
+
if (refusal) {
|
|
985
|
+
pending.reject(refusal);
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
const late = this.#options.transport.kind === "web" ? undefined : lateRowsOf(pending.command);
|
|
989
|
+
const hasRow = late !== undefined && hasLateRow(late, pending.rows);
|
|
990
|
+
if (late === undefined || (hasRow && late.several !== true)) {
|
|
991
|
+
pending.resolve(pending.rows);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
// A create that succeeded, with its row not here yet or perhaps not all
|
|
995
|
+
// of them: the server may write them just after this line. Wait briefly,
|
|
996
|
+
// with the command still the one on the wire, so the rows are its own and
|
|
997
|
+
// the next command is not sent before them.
|
|
998
|
+
this.#pending = pending;
|
|
999
|
+
const finish = (): void => {
|
|
1000
|
+
clearTimeout(pending.held?.timer);
|
|
1001
|
+
if (this.#pending === pending) this.#pending = null;
|
|
1002
|
+
pending.resolve(pending.rows);
|
|
1003
|
+
};
|
|
1004
|
+
const held: NonNullable<Pending["held"]> = {
|
|
1005
|
+
keys: late.keys,
|
|
1006
|
+
several: late.several === true,
|
|
1007
|
+
any: late.any === true,
|
|
1008
|
+
timer: setTimeout(() => {}, 0),
|
|
1009
|
+
finish,
|
|
1010
|
+
wait: (ms) => {
|
|
1011
|
+
clearTimeout(held.timer);
|
|
1012
|
+
held.timer = setTimeout(() => {
|
|
1013
|
+
// Given up on, with no row at all: one that still comes is not the
|
|
1014
|
+
// next command's.
|
|
1015
|
+
const got = hasLateRow(late, pending.rows);
|
|
1016
|
+
if (!got && this.#pending === pending && held.keys[0] !== undefined) {
|
|
1017
|
+
this.#strayKey = held.keys[0];
|
|
1018
|
+
}
|
|
1019
|
+
finish();
|
|
1020
|
+
}, ms);
|
|
1021
|
+
},
|
|
1022
|
+
};
|
|
1023
|
+
pending.held = held;
|
|
1024
|
+
held.wait(hasRow || late.any === true ? LATE_ROW_QUIET_MS : LATE_ROW_GRACE_MS);
|
|
963
1025
|
return;
|
|
964
1026
|
}
|
|
965
1027
|
if (cmd.name.startsWith("notify")) {
|
|
@@ -973,7 +1035,23 @@ export class QueryClient implements CommandExecutor, AsyncDisposable {
|
|
|
973
1035
|
if (this.#abandoned > 0) return;
|
|
974
1036
|
for (const row of splitRows(trimmed)) {
|
|
975
1037
|
const parsed = parseCommand(row);
|
|
976
|
-
if (parsed)
|
|
1038
|
+
if (!parsed) continue;
|
|
1039
|
+
const keys = Object.keys(parsed.params);
|
|
1040
|
+
if (this.#strayKey !== undefined && keys.length === 1 && keys[0] === this.#strayKey) {
|
|
1041
|
+
const own = this.#pending === null ? undefined : lateRowsOf(this.#pending.command);
|
|
1042
|
+
this.#strayKey = undefined;
|
|
1043
|
+
// The late row of an earlier command, which gave up waiting for it. A
|
|
1044
|
+
// command whose own answer carries that key keeps it: the two cannot be
|
|
1045
|
+
// told apart, and lateness was measured at under a millisecond against
|
|
1046
|
+
// a wait of 250, so it almost certainly belongs to the command asking.
|
|
1047
|
+
if (!own?.keys.includes(keys[0])) continue;
|
|
1048
|
+
}
|
|
1049
|
+
this.#pending?.rows.push(parsed.params);
|
|
1050
|
+
const held = this.#pending?.held;
|
|
1051
|
+
if (held && (held.any || keys.some((k) => held.keys.includes(k)))) {
|
|
1052
|
+
if (held.several || held.any) held.wait(LATE_ROW_QUIET_MS);
|
|
1053
|
+
else held.finish();
|
|
1054
|
+
}
|
|
977
1055
|
}
|
|
978
1056
|
}
|
|
979
1057
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** The row a command's answer carries, and whether it carries several. */
|
|
2
|
+
export interface LateRows {
|
|
3
|
+
/** The keys the row is known by. */
|
|
4
|
+
readonly keys: readonly string[];
|
|
5
|
+
/**
|
|
6
|
+
* The answer has one row for each of several things it made, and all, some or
|
|
7
|
+
* none of them may follow the `error` line, so it is held until the rows stop.
|
|
8
|
+
*/
|
|
9
|
+
readonly several?: boolean;
|
|
10
|
+
/** Any row is the answer: a read, which has no one key. */
|
|
11
|
+
readonly any?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Commands whose answer carries the row that names what they made, and the key
|
|
16
|
+
* that row is known by.
|
|
17
|
+
*
|
|
18
|
+
* TeamSpeak 6 over SSH has been measured to write that row after the command's
|
|
19
|
+
* `error` line, a fraction of a millisecond later, in a share of the answers.
|
|
20
|
+
* A client that takes the `error` line for the end of the answer loses the row
|
|
21
|
+
* and, worse, hands it to whichever command is sent next.
|
|
22
|
+
*/
|
|
23
|
+
export const LATE_ROWS: ReadonlyMap<string, LateRows> = new Map<string, LateRows>([
|
|
24
|
+
["channelcreate", { keys: ["cid"] }],
|
|
25
|
+
["servergroupadd", { keys: ["sgid"] }],
|
|
26
|
+
["servergroupcopy", { keys: ["sgid"] }],
|
|
27
|
+
["channelgroupadd", { keys: ["cgid"] }],
|
|
28
|
+
["channelgroupcopy", { keys: ["cgid"] }],
|
|
29
|
+
["banadd", { keys: ["banid"] }],
|
|
30
|
+
["banclient", { keys: ["banid"], several: true }],
|
|
31
|
+
["privilegekeyadd", { keys: ["token"] }],
|
|
32
|
+
["tokenadd", { keys: ["token"] }],
|
|
33
|
+
["apikeyadd", { keys: ["apikey"] }],
|
|
34
|
+
["servercreate", { keys: ["sid"] }],
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/** How long a successful answer without its row is held open for the row to arrive. */
|
|
38
|
+
export const LATE_ROW_GRACE_MS = 250;
|
|
39
|
+
|
|
40
|
+
/** For an answer with several rows: how long without a new one counts as its end. */
|
|
41
|
+
export const LATE_ROW_QUIET_MS = 25;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Commands that read something, by their names: a `list`, `info` or `find`, a
|
|
45
|
+
* `get…`, `whoami`, `version`. A plain read has been measured to have its whole
|
|
46
|
+
* answer after the `error` line too, much less often than a create (2 in 3000
|
|
47
|
+
* `clientlist` calls, while clients were connecting), so one that is answered
|
|
48
|
+
* with no row is held briefly as well. An empty success is rare for these: the
|
|
49
|
+
* servers answer an empty list with error 1281.
|
|
50
|
+
*/
|
|
51
|
+
const READS = /(list|info|find|search)$|^(get\w+|whoami|version|permfind|permoverview|permget)$/;
|
|
52
|
+
|
|
53
|
+
/** What is known of where a command's answer rows may fall, or `undefined` for none. */
|
|
54
|
+
export function lateRowsOf(command: string): LateRows | undefined {
|
|
55
|
+
const known = LATE_ROWS.get(command);
|
|
56
|
+
if (known !== undefined) return known;
|
|
57
|
+
return READS.test(command) ? { keys: [], any: true } : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Whether `rows` already hold what {@link lateRowsOf} says to wait for. */
|
|
61
|
+
export function hasLateRow(late: LateRows, rows: ReadonlyArray<Record<string, string>>): boolean {
|
|
62
|
+
if (late.any === true) return rows.length > 0;
|
|
63
|
+
return rows.some((row) => late.keys.some((k) => row[k] !== undefined));
|
|
64
|
+
}
|