@honeybbq/teamspeak 1.0.0-beta.12 → 1.0.0-beta.13

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.
Files changed (139) hide show
  1. package/README.md +40 -5
  2. package/dist/client/capabilities.d.ts +4 -4
  3. package/dist/client/capabilities.d.ts.map +1 -1
  4. package/dist/client/capabilities.js +3 -8
  5. package/dist/client/capabilities.js.map +1 -1
  6. package/dist/client/client.d.ts.map +1 -1
  7. package/dist/client/client.js +50 -9
  8. package/dist/client/client.js.map +1 -1
  9. package/dist/client/crypto/identity.d.ts +2 -0
  10. package/dist/client/crypto/identity.d.ts.map +1 -1
  11. package/dist/client/crypto/identity.js +2 -2
  12. package/dist/client/crypto/identity.js.map +1 -1
  13. package/dist/client/crypto/index.d.ts +1 -1
  14. package/dist/client/crypto/index.d.ts.map +1 -1
  15. package/dist/client/crypto/index.js +1 -1
  16. package/dist/client/crypto/index.js.map +1 -1
  17. package/dist/client/index.d.ts +1 -1
  18. package/dist/client/index.d.ts.map +1 -1
  19. package/dist/client/index.js +1 -1
  20. package/dist/client/index.js.map +1 -1
  21. package/dist/client/trace.d.ts +7 -0
  22. package/dist/client/trace.d.ts.map +1 -1
  23. package/dist/client/transport/handler.d.ts +2 -0
  24. package/dist/client/transport/handler.d.ts.map +1 -1
  25. package/dist/client/transport/handler.js +4 -0
  26. package/dist/client/transport/handler.js.map +1 -1
  27. package/dist/client/types.d.ts +9 -2
  28. package/dist/client/types.d.ts.map +1 -1
  29. package/dist/client/types.js +2 -0
  30. package/dist/client/types.js.map +1 -1
  31. package/dist/client/view.d.ts +3 -3
  32. package/dist/client/view.d.ts.map +1 -1
  33. package/dist/client/view.js.map +1 -1
  34. package/dist/core/api/channels.d.ts +7 -3
  35. package/dist/core/api/channels.d.ts.map +1 -1
  36. package/dist/core/api/channels.js +5 -3
  37. package/dist/core/api/channels.js.map +1 -1
  38. package/dist/core/api/clients.d.ts +38 -1
  39. package/dist/core/api/clients.d.ts.map +1 -1
  40. package/dist/core/api/clients.js +33 -2
  41. package/dist/core/api/clients.js.map +1 -1
  42. package/dist/core/api/command.d.ts +18 -1
  43. package/dist/core/api/command.d.ts.map +1 -1
  44. package/dist/core/api/command.js +7 -1
  45. package/dist/core/api/command.js.map +1 -1
  46. package/dist/core/api/instance.d.ts +10 -2
  47. package/dist/core/api/instance.d.ts.map +1 -1
  48. package/dist/core/api/instance.js +6 -2
  49. package/dist/core/api/instance.js.map +1 -1
  50. package/dist/core/api/moderation.d.ts +7 -0
  51. package/dist/core/api/moderation.d.ts.map +1 -1
  52. package/dist/core/api/moderation.js +7 -0
  53. package/dist/core/api/moderation.js.map +1 -1
  54. package/dist/core/api/parse.d.ts.map +1 -1
  55. package/dist/core/api/parse.js +1 -0
  56. package/dist/core/api/parse.js.map +1 -1
  57. package/dist/core/api/server.d.ts +9 -0
  58. package/dist/core/api/server.d.ts.map +1 -1
  59. package/dist/core/api/server.js +3 -0
  60. package/dist/core/api/server.js.map +1 -1
  61. package/dist/core/command/responses.d.ts.map +1 -1
  62. package/dist/core/command/responses.js +1 -0
  63. package/dist/core/command/responses.js.map +1 -1
  64. package/dist/core/events.d.ts +47 -5
  65. package/dist/core/events.d.ts.map +1 -1
  66. package/dist/core/index.d.ts +2 -2
  67. package/dist/core/index.d.ts.map +1 -1
  68. package/dist/core/index.js +1 -1
  69. package/dist/core/index.js.map +1 -1
  70. package/dist/core/notification-name.d.ts +4 -0
  71. package/dist/core/notification-name.d.ts.map +1 -1
  72. package/dist/core/notification-name.js +4 -0
  73. package/dist/core/notification-name.js.map +1 -1
  74. package/dist/core/notifications.d.ts +6 -2
  75. package/dist/core/notifications.d.ts.map +1 -1
  76. package/dist/core/notifications.js +32 -1
  77. package/dist/core/notifications.js.map +1 -1
  78. package/dist/core/types.d.ts +7 -0
  79. package/dist/core/types.d.ts.map +1 -1
  80. package/dist/core/types.js +16 -0
  81. package/dist/core/types.js.map +1 -1
  82. package/dist/query/client.d.ts +12 -3
  83. package/dist/query/client.d.ts.map +1 -1
  84. package/dist/query/client.js +22 -15
  85. package/dist/query/client.js.map +1 -1
  86. package/dist/query/index.d.ts +1 -1
  87. package/dist/query/index.d.ts.map +1 -1
  88. package/dist/query/options.d.ts +17 -5
  89. package/dist/query/options.d.ts.map +1 -1
  90. package/dist/query/options.js +1 -0
  91. package/dist/query/options.js.map +1 -1
  92. package/dist/query/ssh.d.ts.map +1 -1
  93. package/dist/query/ssh.js +4 -1
  94. package/dist/query/ssh.js.map +1 -1
  95. package/dist/query/stream.d.ts +8 -3
  96. package/dist/query/stream.d.ts.map +1 -1
  97. package/dist/query/stream.js +26 -3
  98. package/dist/query/stream.js.map +1 -1
  99. package/dist/query/tcp.js +1 -1
  100. package/dist/query/tcp.js.map +1 -1
  101. package/dist/query/transport.d.ts +35 -0
  102. package/dist/query/transport.d.ts.map +1 -1
  103. package/dist/query/web.d.ts +17 -5
  104. package/dist/query/web.d.ts.map +1 -1
  105. package/dist/query/web.js +38 -9
  106. package/dist/query/web.js.map +1 -1
  107. package/package.json +6 -5
  108. package/src/client/capabilities.ts +14 -11
  109. package/src/client/client.ts +47 -6
  110. package/src/client/crypto/identity.ts +2 -2
  111. package/src/client/crypto/index.ts +1 -0
  112. package/src/client/index.ts +2 -0
  113. package/src/client/trace.ts +7 -0
  114. package/src/client/transport/handler.ts +5 -0
  115. package/src/client/types.ts +9 -0
  116. package/src/client/view.ts +2 -3
  117. package/src/core/api/channels.ts +9 -3
  118. package/src/core/api/clients.ts +73 -2
  119. package/src/core/api/command.ts +22 -1
  120. package/src/core/api/instance.ts +20 -2
  121. package/src/core/api/moderation.ts +7 -0
  122. package/src/core/api/parse.ts +1 -0
  123. package/src/core/api/server.ts +12 -0
  124. package/src/core/command/responses.ts +1 -0
  125. package/src/core/events.ts +48 -5
  126. package/src/core/index.ts +2 -1
  127. package/src/core/notification-name.ts +4 -0
  128. package/src/core/notifications.ts +48 -17
  129. package/src/core/types.ts +14 -0
  130. package/src/query/client.ts +28 -15
  131. package/src/query/index.ts +1 -1
  132. package/src/query/options.ts +19 -5
  133. package/src/query/ssh.ts +6 -1
  134. package/src/query/stream.ts +31 -4
  135. package/src/query/tcp.ts +1 -1
  136. package/src/query/testing/live.ts +311 -0
  137. package/src/query/testing/ssh-keys.ts +67 -0
  138. package/src/query/transport.ts +36 -0
  139. package/src/query/web.ts +57 -14
@@ -435,6 +435,11 @@ export class PacketHandler {
435
435
  }
436
436
 
437
437
  /** How many packets have been sent again for want of an acknowledgement. */
438
+ /** Whether clientek, sent under the dummy key, has not been acknowledged yet. */
439
+ get clientEkPending(): boolean {
440
+ return this.#dummyCommandIDs.size > 0;
441
+ }
442
+
438
443
  get resends(): number {
439
444
  return this.#resends;
440
445
  }
@@ -4,6 +4,7 @@ import type {
4
4
  ChannelSubscribedEvent,
5
5
  ChannelUnsubscribedEvent,
6
6
  ClientChannelGroupChangedEvent,
7
+ ClientChatClosedEvent,
7
8
  ClientChatComposingEvent,
8
9
  ClientID,
9
10
  ServerGroupID,
@@ -122,6 +123,11 @@ export interface EventMap extends ServerEventMap, SessionEventMap {
122
123
  * sends. Only the target receives it.
123
124
  */
124
125
  clientChatComposing: ClientChatComposingEvent;
126
+ /**
127
+ * A client closing its private chat with this client, which `closePrivateChat`
128
+ * sends. Only a client the sender has messaged privately is told.
129
+ */
130
+ clientChatClosed: ClientChatClosedEvent;
125
131
  }
126
132
 
127
133
  /**
@@ -251,6 +257,8 @@ const eventNameValues = {
251
257
  PluginCommand: "pluginCommand",
252
258
  /** A client typing a private message to this client. */
253
259
  ClientChatComposing: "clientChatComposing",
260
+ /** A client closing its private chat with this client. */
261
+ ClientChatClosed: "clientChatClosed",
254
262
  /** The view being complete; see `ServerView`. */
255
263
  ViewReady: "viewReady",
256
264
  /** The view changing; see `ServerView`. */
@@ -318,6 +326,7 @@ export type ServerEventName =
318
326
  | "channelUnsubscribed"
319
327
  | "pluginCommand"
320
328
  | "clientChatComposing"
329
+ | "clientChatClosed"
321
330
  | "poked"
322
331
  | "voiceData"
323
332
  | "notification";
@@ -9,7 +9,6 @@ import {
9
9
  type ChannelEditedEvent,
10
10
  type ChannelID,
11
11
  type ChannelMovedEvent,
12
- type ClientEnterEvent,
13
12
  type ClientEntry,
14
13
  type ChannelGroup,
15
14
  type ChannelGroupID,
@@ -186,7 +185,7 @@ export interface ServerView {
186
185
  * The clients a client knows, by id. Every change to it, whoever makes it, is
187
186
  * a change to the view, so it counts them.
188
187
  */
189
- export class Roster extends Map<number, ClientEnterEvent> {
188
+ export class Roster extends Map<number, ClientEntry> {
190
189
  #changed: () => void = () => {};
191
190
 
192
191
  /** Call `changed` after each change from now on. */
@@ -194,7 +193,7 @@ export class Roster extends Map<number, ClientEnterEvent> {
194
193
  this.#changed = changed;
195
194
  }
196
195
 
197
- override set(id: number, entry: ClientEnterEvent): this {
196
+ override set(id: number, entry: ClientEntry): this {
198
197
  super.set(id, entry);
199
198
  this.#changed();
200
199
  return this;
@@ -101,6 +101,10 @@ export interface ChannelSettings {
101
101
  readonly deleteDelayMs?: number | undefined;
102
102
  /** How the name is pronounced, for text-to-speech (`channel_name_phonetic`). */
103
103
  readonly namePhonetic?: string | undefined;
104
+ /** The URL of the channel's banner image (`channel_banner_gfx_url`); needs `b_channel_modify_banner`. */
105
+ readonly bannerURL?: string | undefined;
106
+ /** How the banner is scaled (`channel_banner_mode`); needs `b_channel_modify_banner`. */
107
+ readonly bannerMode?: number | undefined;
104
108
  }
105
109
 
106
110
  const settingNames: WireNames<ChannelSettings> = {
@@ -124,6 +128,8 @@ const settingNames: WireNames<ChannelSettings> = {
124
128
  isDefault: "channel_flag_default",
125
129
  deleteDelayMs: ["channel_delete_delay", inSeconds],
126
130
  namePhonetic: "channel_name_phonetic",
131
+ bannerURL: "channel_banner_gfx_url",
132
+ bannerMode: "channel_banner_mode",
127
133
  };
128
134
 
129
135
  /** A new channel's settings. Only `name` is required; see {@link ChannelSettings}. */
@@ -251,9 +257,9 @@ const listFlags: ReadonlyArray<readonly [string, true]> = [
251
257
  * All channels on the server, in the server's own sort order.
252
258
  *
253
259
  * Always asks for the topic, flags, voice, limits, icon, seconds empty and
254
- * banners, which a bare `channellist` leaves out. The last two are asked for
255
- * and not read: `Channel` has no field for them yet. `description` and `uid` are not part of
256
- * `channellist` and stay empty; `getChannelInfo` has them.
260
+ * banners, which a bare `channellist` leaves out. `description`, `uid` and
261
+ * `filePath` are not part of `channellist` and stay empty or undefined;
262
+ * `getChannelInfo` has them.
257
263
  */
258
264
  export async function listChannels(
259
265
  client: CommandExecutor,
@@ -22,6 +22,7 @@ import {
22
22
  str,
23
23
  big,
24
24
  num,
25
+ optNum,
25
26
  date,
26
27
  } from "./command.js";
27
28
  import type { PagingOptions, Params, PermissionEntry, WireNames } from "./command.js";
@@ -108,6 +109,11 @@ export interface ClientEntry {
108
109
  * the request (`client_talk_request`); any non-zero value is true. No flag.
109
110
  */
110
111
  readonly isRequestingTalk?: boolean | undefined;
112
+ /**
113
+ * When the client asked to talk (`client_talk_request`, a unix time; 0 on
114
+ * the wire when there is no request, which reads as undefined); no flag.
115
+ */
116
+ readonly talkRequestedAt?: Date | undefined;
111
117
  /** The message sent with a talk request (`client_talk_request_msg`); no flag. */
112
118
  readonly talkRequestMessage?: string | undefined;
113
119
  /** The client's description (`client_description`); no flag. */
@@ -116,7 +122,7 @@ export interface ClientEntry {
116
122
  readonly metaData?: string | undefined;
117
123
  /** The MD5 of the client's avatar (`client_flag_avatar`); "" for none; no flag. */
118
124
  readonly avatar?: string | undefined;
119
- /** The client's icon (`client_icon_id`); undefined for none as well as for absent; no flag. */
125
+ /** The client's icon (`client_icon_id`), with `icon`; undefined for none as well as for absent. */
120
126
  readonly iconID?: IconID | undefined;
121
127
  /** How the nickname is pronounced (`client_nickname_phonetic`); no flag. */
122
128
  readonly nicknamePhonetic?: string | undefined;
@@ -178,6 +184,11 @@ export function applyClientChanges(entry: ClientEntry, changes: ClientChanges):
178
184
  for (const [key, value] of Object.entries(changes)) {
179
185
  if (value !== undefined) next[key] = value;
180
186
  }
187
+ // A withdrawn request is a 0 on the wire, which reads as no time and so is
188
+ // never in `changes`; the flag going false is what says so.
189
+ if (changes.isRequestingTalk === false && !("talkRequestedAt" in changes)) {
190
+ delete next["talkRequestedAt"];
191
+ }
181
192
  return next as unknown as ClientEntry;
182
193
  }
183
194
 
@@ -205,6 +216,8 @@ export interface ClientListOptions extends CommandOptions {
205
216
  readonly ip?: boolean | undefined;
206
217
  /** The client's badges (`-badges`). */
207
218
  readonly badges?: boolean | undefined;
219
+ /** The client's icon (`-icon`), which fills `ClientEntry.iconID`. */
220
+ readonly icon?: boolean | undefined;
208
221
  }
209
222
 
210
223
  const listFlags: WireNames<ClientListOptions> = {
@@ -217,6 +230,7 @@ const listFlags: WireNames<ClientListOptions> = {
217
230
  country: "-country",
218
231
  ip: "-ip",
219
232
  badges: "-badges",
233
+ icon: "-icon",
220
234
  };
221
235
 
222
236
  /** Every client currently connected. */
@@ -723,6 +737,37 @@ export async function getClientUIDByID(
723
737
  return asUID(str(firstRow(rows, "clientgetuidfromclid", "cluid"), "cluid"));
724
738
  }
725
739
 
740
+ /** What `clientgetuidfromclid` says about a connected client. */
741
+ export interface ClientUIDInfo {
742
+ /** The client's id (`clid`). */
743
+ readonly clientID: ClientID;
744
+ /** The client's unique identifier (`cluid`). */
745
+ readonly uid: UID;
746
+ /** The client's nickname (`nickname`). */
747
+ readonly nickname: string;
748
+ /** The client's myTeamSpeak id (`mytsid`); "" for none, as TS3 3.13.8 and TS6 6.0.0-beta13.1 sent it for a client without one. */
749
+ readonly mytsid: string;
750
+ }
751
+
752
+ /**
753
+ * The uid, nickname and myTeamSpeak id of a connected client.
754
+ * {@link getClientUIDByID} is the same command, keeping only the uid.
755
+ */
756
+ export async function getClientUIDInfoByID(
757
+ client: CommandExecutor,
758
+ clid: ClientID,
759
+ options?: CommandOptions,
760
+ ): Promise<ClientUIDInfo> {
761
+ const rows = await query(client, "clientgetuidfromclid", [["clid", clid]], options);
762
+ const row = firstRow(rows, "clientgetuidfromclid", "cluid");
763
+ return {
764
+ clientID: asClientID(uint(row, "clid", U16)),
765
+ uid: asUID(str(row, "cluid")),
766
+ nickname: str(row, "nickname"),
767
+ mytsid: str(row, "mytsid"),
768
+ };
769
+ }
770
+
726
771
  /** The persistent database id behind a uid. */
727
772
  export async function getClientDBIDByUID(
728
773
  client: CommandExecutor,
@@ -739,10 +784,16 @@ export interface ClientDatabaseName {
739
784
  readonly databaseID: ClientDBID;
740
785
  /** The nickname the client last used. */
741
786
  readonly name: string;
787
+ /** The client's unique identifier (`cluid`); "" when the server sent none. */
788
+ readonly uid: UID;
742
789
  }
743
790
 
744
791
  function parseDatabaseName(row: Record<string, string>): ClientDatabaseName {
745
- return { databaseID: asClientDBID(big(row, "cldbid")), name: str(row, "name") };
792
+ return {
793
+ databaseID: asClientDBID(big(row, "cldbid")),
794
+ name: str(row, "name"),
795
+ uid: asUID(str(row, "cluid")),
796
+ };
746
797
  }
747
798
 
748
799
  /** The database id and nickname recorded for a database id. */
@@ -805,8 +856,22 @@ export interface ClientDatabaseEntry {
805
856
  readonly description: string;
806
857
  /** The address the client last connected from. */
807
858
  readonly lastIP: string;
859
+ /** The client's query account name (`client_login_name`); "" for none; `clientdblist` rows only. */
860
+ readonly loginName?: string | undefined;
861
+ /** The MD5 of the client's avatar (`client_flag_avatar`), "" for none; `getClientDatabaseInfo` only. */
862
+ readonly avatarHash?: string | undefined;
863
+ /** Bytes the client has uploaded in all (`client_total_bytes_uploaded`); `getClientDatabaseInfo` only. */
864
+ readonly totalBytesUploaded?: number | undefined;
865
+ /** Bytes the client has downloaded in all (`client_total_bytes_downloaded`); `getClientDatabaseInfo` only. */
866
+ readonly totalBytesDownloaded?: number | undefined;
867
+ /** Bytes the client has uploaded this month (`client_month_bytes_uploaded`); `getClientDatabaseInfo` only. */
868
+ readonly monthBytesUploaded?: number | undefined;
869
+ /** Bytes the client has downloaded this month (`client_month_bytes_downloaded`); `getClientDatabaseInfo` only. */
870
+ readonly monthBytesDownloaded?: number | undefined;
808
871
  }
809
872
 
873
+ const optStr = (row: Record<string, string>, key: string): string | undefined => row[key];
874
+
810
875
  const parseDatabaseEntry = (row: Record<string, string>): ClientDatabaseEntry => ({
811
876
  databaseID: asClientDBID(big(row, "cldbid")),
812
877
  uid: asUID(str(row, "client_unique_identifier")),
@@ -816,6 +881,12 @@ const parseDatabaseEntry = (row: Record<string, string>): ClientDatabaseEntry =>
816
881
  totalConnections: num(row, "client_totalconnections"),
817
882
  description: str(row, "client_description"),
818
883
  lastIP: str(row, "client_lastip"),
884
+ loginName: optStr(row, "client_login_name"),
885
+ avatarHash: optStr(row, "client_flag_avatar"),
886
+ totalBytesUploaded: optNum(row, "client_total_bytes_uploaded"),
887
+ totalBytesDownloaded: optNum(row, "client_total_bytes_downloaded"),
888
+ monthBytesUploaded: optNum(row, "client_month_bytes_uploaded"),
889
+ monthBytesDownloaded: optNum(row, "client_month_bytes_downloaded"),
819
890
  });
820
891
 
821
892
  /** Which slice of the client database {@link listClientDatabase} reads. */
@@ -367,7 +367,7 @@ const date = (row: Record<string, string>, key: string): Date | undefined => {
367
367
  const durationMs = (row: Record<string, string>, key: string, unitMs: number): number =>
368
368
  num(row, key) * unitMs;
369
369
 
370
- export { str, big, num, uint, U16, U32, bool, icon, date, durationMs };
370
+ export { str, big, num, optNum, uint, U16, U32, bool, icon, date, durationMs };
371
371
 
372
372
  // ---- Read tables ------------------------------------------------------------
373
373
 
@@ -611,6 +611,21 @@ export interface Channel {
611
611
  readonly bannerURL?: string | undefined;
612
612
  /** How the banner is scaled (`channel_banner_mode`). */
613
613
  readonly bannerMode?: number | undefined;
614
+ /**
615
+ * How long the channel has had no client, in milliseconds (`seconds_empty`,
616
+ * which the wire carries in seconds), as of the answer. Undefined for a
617
+ * channel with a client in it: both TS3 3.13.8 and TS6 6.0.0-beta13.1 send
618
+ * -1 for it, which is not a duration. `listChannels` and `getChannelInfo`
619
+ * report it; a notification never does.
620
+ */
621
+ readonly emptyForMs?: number | undefined;
622
+ /**
623
+ * The channel's file storage quota (`channel_storage_quota`); TS6 only.
624
+ * TS6 6.0.0-beta13.1 sent 4294967295 for a channel with none set.
625
+ */
626
+ readonly storageQuota?: number | undefined;
627
+ /** The channel's directory on the server (`channel_filepath`); `getChannelInfo` only. */
628
+ readonly filePath?: string | undefined;
614
629
  }
615
630
 
616
631
  /**
@@ -681,6 +696,12 @@ export const channelFields: {
681
696
  isForcedSilence: { keys: ["channel_forced_silence"], read: bool },
682
697
  bannerURL: { keys: ["channel_banner_gfx_url"], read: str },
683
698
  bannerMode: { keys: ["channel_banner_mode"], read: num },
699
+ emptyForMs: {
700
+ keys: ["seconds_empty"],
701
+ read: (row, key) => (num(row, key) < 0 ? undefined : durationMs(row, key, SECOND)),
702
+ },
703
+ storageQuota: { keys: ["channel_storage_quota"], read: num },
704
+ filePath: { keys: ["channel_filepath"], read: str },
684
705
  };
685
706
 
686
707
  function parseChannel(row: Record<string, string>): Channel {
@@ -60,6 +60,14 @@ export interface VirtualServer {
60
60
  readonly machineID: string | undefined;
61
61
  /** Its unique identifier, with `uid`; "" otherwise (`virtualserver_unique_identifier`). */
62
62
  readonly uid: string;
63
+ /** Its UUID, with `uid`; TS6 only (`virtualserver_uuid`). */
64
+ readonly uuid?: string | undefined;
65
+ /** Whether guests may create channels; TS6 only (`guest_can_create_channels`). */
66
+ readonly guestCanCreateChannels?: boolean | undefined;
67
+ /** Whether guests may create a homebase; TS6 only (`guest_can_create_homebase`). */
68
+ readonly guestCanCreateHomebase?: boolean | undefined;
69
+ /** Whether the server is on the public server list; TS6 only (`virtualserver_weblist_enabled`). */
70
+ readonly weblistEnabled?: boolean | undefined;
63
71
  }
64
72
 
65
73
  const optional = <T>(row: Record<string, string>, key: string, read: () => T): T | undefined =>
@@ -83,6 +91,16 @@ const parseVirtualServer = (row: Record<string, string>): VirtualServer => ({
83
91
  autoStart: optional(row, "virtualserver_autostart", () => bool(row, "virtualserver_autostart")),
84
92
  machineID: row["virtualserver_machine_id"],
85
93
  uid: str(row, "virtualserver_unique_identifier"),
94
+ uuid: row["virtualserver_uuid"],
95
+ guestCanCreateChannels: optional(row, "guest_can_create_channels", () =>
96
+ bool(row, "guest_can_create_channels"),
97
+ ),
98
+ guestCanCreateHomebase: optional(row, "guest_can_create_homebase", () =>
99
+ bool(row, "guest_can_create_homebase"),
100
+ ),
101
+ weblistEnabled: optional(row, "virtualserver_weblist_enabled", () =>
102
+ bool(row, "virtualserver_weblist_enabled"),
103
+ ),
86
104
  });
87
105
 
88
106
  /** Which servers {@link listServers} reports, and with what. */
@@ -109,8 +127,8 @@ const serverListFlags: WireNames<ServerListOptions> = {
109
127
  *
110
128
  * Needs `b_serverinstance_virtualserver_list`, which a voice client's Server
111
129
  * Admin lacks (error 2568). TS6 also reports `guest_can_create_channels`,
112
- * `guest_can_create_homebase` and `virtualserver_weblist_enabled`, which are
113
- * not read here.
130
+ * `guest_can_create_homebase` and `virtualserver_weblist_enabled`, and its
131
+ * UUID with `uid`; TS3 sends none of them.
114
132
  */
115
133
  export async function listServers(
116
134
  client: CommandExecutor,
@@ -247,6 +247,13 @@ export async function addBan(
247
247
  * uid, and on TS6 its myTeamSpeak id — and over ServerQuery answers one
248
248
  * `banid` row for each; returning the first left the others to be found by
249
249
  * hand. The voice protocol reports no ids, so there this resolves to `[]`.
250
+ *
251
+ * Measured on TS3 3.13.8 and TS6 beta13.1: two bans, one for the uid and one
252
+ * 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 SSH and WebQuery), so an
254
+ * empty result does not mean no ban. A caller that must undo the ban lists the
255
+ * bans before and after and removes what is new: a ban on the address refuses
256
+ * every later connection from it.
250
257
  */
251
258
  export async function banClient(
252
259
  client: CommandExecutor,
@@ -89,6 +89,7 @@ export const clientFields: {
89
89
  talkPower: { keys: ["client_talk_power"], read: num },
90
90
  // The wire value is when the request was made; any non-zero is a request.
91
91
  isRequestingTalk: flag("client_talk_request"),
92
+ talkRequestedAt: { keys: ["client_talk_request"], read: date },
92
93
  talkRequestMessage: text("client_talk_request_msg"),
93
94
  description: text("client_description"),
94
95
  metaData: text("client_meta_data"),
@@ -250,6 +250,10 @@ export interface WhoAmI {
250
250
  * it.
251
251
  */
252
252
  readonly uid: UID;
253
+ /** The virtual server's UUID (`virtualserver_uuid`); TS6 only, undefined when not sent. */
254
+ readonly serverUUID?: string | undefined;
255
+ /** The server this client first connected to (`client_origin_server_id`); 0 when not sent. */
256
+ readonly originServerID: ServerID;
253
257
  }
254
258
 
255
259
  /** Which server and client this connection is, as the server sees it. */
@@ -266,6 +270,8 @@ export async function whoAmI(client: CommandExecutor, options?: CommandOptions):
266
270
  databaseID: asClientDBID(big(row, "client_database_id")),
267
271
  loginName: str(row, "client_login_name"),
268
272
  uid: asUID(str(row, "client_unique_identifier")),
273
+ serverUUID: row["virtualserver_uuid"],
274
+ originServerID: asServerID(big(row, "client_origin_server_id")),
269
275
  };
270
276
  }
271
277
 
@@ -674,6 +680,11 @@ export interface TempPassword {
674
680
  readonly endsAt: Date | undefined;
675
681
  /** The channel a client joining with it lands in, or 0 for the default channel. */
676
682
  readonly targetChannelID: ChannelID;
683
+ /**
684
+ * That channel's password, in cleartext (`tcpw`); "" for none. A secret like
685
+ * `password`: store it where it is needed, and do not log it.
686
+ */
687
+ readonly targetChannelPassword: string;
677
688
  }
678
689
 
679
690
  /** The server's temporary passwords. */
@@ -690,6 +701,7 @@ export async function listTempPasswords(
690
701
  startsAt: date(row, "start"),
691
702
  endsAt: date(row, "end"),
692
703
  targetChannelID: asChannelID(big(row, "tcid")),
704
+ targetChannelPassword: str(row, "tcpw"),
693
705
  }));
694
706
  }
695
707
 
@@ -88,6 +88,7 @@ const fileTransfer = (answer: string): ResponseSpec => ({
88
88
  export const RESPONSES: Readonly<Record<string, ResponseSpec>> = {
89
89
  apikeyadd: inline("notifyapikeyadd"),
90
90
  apikeylist: inline("notifyapikeylist"),
91
+ banfind: inline("notifybanfind"),
91
92
  banlist: inline("notifybanlist"),
92
93
  channelclientpermlist: inline("notifychannelclientpermlist"),
93
94
  channelcreate: { notifications: ["notifychannelcreated"], ownRowsOnly: true },
@@ -44,6 +44,11 @@ export interface ClientMovedEvent {
44
44
  readonly invokerUID: UID;
45
45
  /** The channel the client is now in. */
46
46
  readonly targetChannelID: ChannelID;
47
+ /**
48
+ * The channel the client came from (`cfid`). Absent when the server sends
49
+ * none: TS3 3.13.8 sends no `cfid` on a move, TS6 6.0.0-beta13.1 does.
50
+ */
51
+ readonly fromChannelID?: ChannelID | undefined;
47
52
  /** Why the client moved; compare it with `Reason`. */
48
53
  readonly reason: number;
49
54
  /** The message that came with the move, such as a channel kick's reason; "" when none. */
@@ -65,6 +70,16 @@ export interface ClientLeaveEvent {
65
70
  readonly reason: number;
66
71
  /** The client that left. */
67
72
  readonly clientID: ClientID;
73
+ /**
74
+ * The channel the client was in (`cfid`); absent when the server sent none.
75
+ * Both servers sent it on every leave measured.
76
+ */
77
+ readonly fromChannelID?: ChannelID | undefined;
78
+ /**
79
+ * The channel the client went to (`ctid`), named as on the move event; 0
80
+ * when it left the server, and absent when the server sent none.
81
+ */
82
+ readonly targetChannelID?: ChannelID | undefined;
68
83
  /** Who kicked or banned the client; empty otherwise. */
69
84
  readonly invokerName: string;
70
85
  /** The kicker's unique identifier; empty unless kicked or banned. */
@@ -96,12 +111,28 @@ export interface KickedEvent {
96
111
  }
97
112
 
98
113
  /**
99
- * A client coming into view: the same record `listClients` returns, so one
100
- * type describes a client however it was learned of; its own id is `id`, as
101
- * in every record. The fields only `clientlist -times` reports are undefined
102
- * here.
114
+ * A client coming into view: the record `listClients` returns, so one type
115
+ * 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 fields only
117
+ * `clientlist -times` reports are undefined here.
118
+ *
119
+ * `fromChannelID` and `reason` belong to the event: the roster and
120
+ * `view.clients` keep the record without them.
103
121
  */
104
- export type ClientEnterEvent = ClientEntry;
122
+ export interface ClientEnterEvent extends ClientEntry {
123
+ /**
124
+ * The channel the client came from (`cfid`); 0 for a client that joined the
125
+ * server, and absent when the server sent none. Measured on TS3 3.13.8 and
126
+ * TS6 6.0.0-beta13.1: 0 on a join and on a client listed by a subscription.
127
+ */
128
+ readonly fromChannelID?: ChannelID | undefined;
129
+ /**
130
+ * Why the client came into view (`reasonid`); compare it with `Reason`.
131
+ * Measured on both servers: 0 on a join, 2 (`Reason.Subscription`) for a
132
+ * client that was already there and came into view by a subscription.
133
+ */
134
+ readonly reason?: number | undefined;
135
+ }
105
136
 
106
137
  /** This client being poked. */
107
138
  export interface PokeEvent {
@@ -311,6 +342,18 @@ export interface ClientChatComposingEvent {
311
342
  readonly uid: UID;
312
343
  }
313
344
 
345
+ /**
346
+ * A client closed its private chat with this client. It is delivered only to a
347
+ * client the sender has sent a private message to. Delivered by the voice
348
+ * client only.
349
+ */
350
+ export interface ClientChatClosedEvent {
351
+ /** The client that closed the chat. */
352
+ readonly clientID: ClientID;
353
+ /** Its unique identifier. */
354
+ readonly uid: UID;
355
+ }
356
+
314
357
  /** Audio from another client, in the channel or whispered. */
315
358
  export interface VoiceDataEvent {
316
359
  /** Who is speaking. */
package/src/core/index.ts CHANGED
@@ -88,7 +88,7 @@ export { NotificationName } from "./notification-name.js";
88
88
  export { PropertyName } from "./property-name.js";
89
89
  export type { Permission } from "./permission.js";
90
90
 
91
- export { noopLogger, consoleLogger, ClientStatus } from "./types.js";
91
+ export { noopLogger, consoleLogger, ClientStatus, serverFamilyForVersion } from "./types.js";
92
92
  export type {
93
93
  ClientVersion,
94
94
  Listener,
@@ -118,6 +118,7 @@ export type {
118
118
  ChannelUnsubscribedEvent,
119
119
  PluginCommandEvent,
120
120
  ClientChatComposingEvent,
121
+ ClientChatClosedEvent,
121
122
  ClientUpdatedEvent,
122
123
  ClientServerGroupEvent,
123
124
  ClientServerGroupAddedEvent,
@@ -17,8 +17,12 @@ const notificationNameValues = {
17
17
  APIKeyAdd: "notifyapikeyadd",
18
18
  /** `notifyapikeylist`: Reply to apikeylist: one row per API key. */
19
19
  APIKeyList: "notifyapikeylist",
20
+ /** `notifybanfind`: Reply to banfind: one row per ban rule that matches. */
21
+ BanFind: "notifybanfind",
20
22
  /** `notifybanlist`: Reply to banlist: one row per ban rule on the virtual server. */
21
23
  BanList: "notifybanlist",
24
+ /** `notifybanupdate`: A ban changed; sent to a ServerQuery connection registered for `bans`. An addition carries the ban row, a deletion its `banid`, a delete-all neither (TS6 6.0.0-beta13.1, `op` says which). */
25
+ BanUpdate: "notifybanupdate",
22
26
  /** `notifychannelchanged`: A channel's properties changed and this client should fetch them again. */
23
27
  ChannelChanged: "notifychannelchanged",
24
28
  /** `notifychannelclientpermlist`: Reply to channelclientpermlist: one row per permission set for a client in a channel. */