@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
@@ -24,6 +24,8 @@ import { Reason } from "./reason.js";
24
24
  import { applyClientChanges } from "./api/clients.js";
25
25
  import { parseInt10, parseUint16, parseUint64 } from "./helpers.js";
26
26
  import type { Command } from "./command/command.js";
27
+ import type { ChannelID } from "./command/ids.js";
28
+ import type { ClientEntry } from "./api/clients.js";
27
29
  import type {
28
30
  ChannelCreatedEvent,
29
31
  ChannelDeletedEvent,
@@ -33,6 +35,7 @@ import type {
33
35
  ChannelPasswordChangedEvent,
34
36
  ChannelSubscribedEvent,
35
37
  ChannelUnsubscribedEvent,
38
+ ClientChatClosedEvent,
36
39
  ClientChatComposingEvent,
37
40
  ClientEnterEvent,
38
41
  ClientChannelGroupChangedEvent,
@@ -70,6 +73,7 @@ export type NotificationResult =
70
73
  | { kind: "channelUnsubscribed"; event: ChannelUnsubscribedEvent }
71
74
  | { kind: "pluginCommand"; event: PluginCommandEvent }
72
75
  | { kind: "clientChatComposing"; event: ClientChatComposingEvent }
76
+ | { kind: "clientChatClosed"; event: ClientChatClosedEvent }
73
77
  | { kind: "unknown" };
74
78
 
75
79
  /**
@@ -87,7 +91,7 @@ export type NotificationResult =
87
91
  export function handleNotification(
88
92
  cmd: Command,
89
93
  selfCLID: number,
90
- clients: Map<number, ClientEnterEvent>,
94
+ clients: Map<number, ClientEntry>,
91
95
  ): NotificationResult {
92
96
  switch (cmd.name) {
93
97
  case "notifycliententerview":
@@ -137,6 +141,8 @@ export function handleNotification(
137
141
  };
138
142
  case "notifyclientchatcomposing":
139
143
  return handleChatComposing(cmd);
144
+ case "notifyclientchatclosed":
145
+ return handleChatClosed(cmd);
140
146
  default:
141
147
  return { kind: "unknown" };
142
148
  }
@@ -144,19 +150,41 @@ export function handleNotification(
144
150
 
145
151
  function handleClientEnterView(
146
152
  cmd: Command,
147
- clients: Map<number, ClientEnterEvent>,
153
+ clients: Map<number, ClientEntry>,
148
154
  ): NotificationResult {
149
155
  const info = parseClient(cmd.params);
150
156
  // No client has id 0: a row without one describes nobody.
151
157
  if (info.id === 0) return { kind: "unknown" };
158
+ // The roster keeps the record alone; the event adds where it came from and why.
152
159
  clients.set(info.id, info);
153
- return { kind: "clientEnter", info };
160
+ return {
161
+ kind: "clientEnter",
162
+ info: {
163
+ ...info,
164
+ ...channelOf(cmd.params, "cfid", "fromChannelID"),
165
+ ...(cmd.params["reasonid"] === undefined
166
+ ? {}
167
+ : { reason: parseInt10(cmd.params["reasonid"]) }),
168
+ },
169
+ };
170
+ }
171
+
172
+ /** A channel id the row names under `key`, as `{ [name]: id }`; nothing when the row lacks the key. */
173
+ function channelOf<N extends string>(
174
+ params: Readonly<Record<string, string>>,
175
+ key: string,
176
+ name: N,
177
+ ): { [K in N]?: ChannelID } {
178
+ const raw = params[key];
179
+ return raw === undefined
180
+ ? {}
181
+ : ({ [name]: asChannelID(parseUint64(raw)) } as { [K in N]?: ChannelID });
154
182
  }
155
183
 
156
184
  function handleClientLeftView(
157
185
  cmd: Command,
158
186
  selfCLID: number,
159
- clients: Map<number, ClientEnterEvent>,
187
+ clients: Map<number, ClientEntry>,
160
188
  ): NotificationResult {
161
189
  const clid = asClientID(parseUint16(cmd.params["clid"] ?? ""));
162
190
  if (clid === 0) return { kind: "unknown" };
@@ -166,6 +194,8 @@ function handleClientLeftView(
166
194
  kind: "clientLeave",
167
195
  event: {
168
196
  clientID: clid,
197
+ ...channelOf(cmd.params, "cfid", "fromChannelID"),
198
+ ...channelOf(cmd.params, "ctid", "targetChannelID"),
169
199
  reason: parseInt10(cmd.params["reasonid"] ?? ""),
170
200
  reasonMessage: cmd.params["reasonmsg"] ?? "",
171
201
  invokerID: asClientID(parseUint16(cmd.params["invokerid"] ?? "")),
@@ -176,10 +206,7 @@ function handleClientLeftView(
176
206
  };
177
207
  }
178
208
 
179
- function handleClientMoved(
180
- cmd: Command,
181
- clients: Map<number, ClientEnterEvent>,
182
- ): NotificationResult {
209
+ function handleClientMoved(cmd: Command, clients: Map<number, ClientEntry>): NotificationResult {
183
210
  const clid = asClientID(parseUint16(cmd.params["clid"] ?? ""));
184
211
  const ctid = asChannelID(parseUint64(cmd.params["ctid"] ?? ""));
185
212
  if (clid === 0) return { kind: "unknown" };
@@ -197,6 +224,7 @@ function handleClientMoved(
197
224
  event: {
198
225
  clientID: clid,
199
226
  targetChannelID: ctid,
227
+ ...channelOf(cmd.params, "cfid", "fromChannelID"),
200
228
  reason: parseInt10(cmd.params["reasonid"] ?? ""),
201
229
  reasonMessage: cmd.params["reasonmsg"] ?? "",
202
230
  invokerID,
@@ -206,10 +234,7 @@ function handleClientMoved(
206
234
  };
207
235
  }
208
236
 
209
- function handleTextMessage(
210
- cmd: Command,
211
- clients: Map<number, ClientEnterEvent>,
212
- ): NotificationResult {
237
+ function handleTextMessage(cmd: Command, clients: Map<number, ClientEntry>): NotificationResult {
213
238
  const invokerID = asClientID(parseUint16(cmd.params["invokerid"] ?? ""));
214
239
  const invokerInfo = clients.get(invokerID);
215
240
 
@@ -246,10 +271,7 @@ function invoked(params: Readonly<Record<string, string>>): Invoked {
246
271
  };
247
272
  }
248
273
 
249
- function handleClientUpdated(
250
- cmd: Command,
251
- clients: Map<number, ClientEnterEvent>,
252
- ): NotificationResult {
274
+ function handleClientUpdated(cmd: Command, clients: Map<number, ClientEntry>): NotificationResult {
253
275
  const clid = asClientID(parseUint16(cmd.params["clid"] ?? ""));
254
276
  if (clid === 0) return { kind: "unknown" };
255
277
  const changes = parseClientChanges(cmd.params);
@@ -282,7 +304,7 @@ function handleServerGroupMember(
282
304
 
283
305
  function handleClientChannelGroupChanged(
284
306
  cmd: Command,
285
- clients: Map<number, ClientEnterEvent>,
307
+ clients: Map<number, ClientEntry>,
286
308
  ): NotificationResult {
287
309
  const clid = asClientID(parseUint16(cmd.params["clid"] ?? ""));
288
310
  if (clid === 0) return { kind: "unknown" };
@@ -402,3 +424,12 @@ function handleChatComposing(cmd: Command): NotificationResult {
402
424
  event: { clientID: clid, uid: asUID(cmd.params["cluid"] ?? "") },
403
425
  };
404
426
  }
427
+
428
+ function handleChatClosed(cmd: Command): NotificationResult {
429
+ const clid = asClientID(parseUint16(cmd.params["clid"] ?? ""));
430
+ if (clid === 0) return { kind: "unknown" };
431
+ return {
432
+ kind: "clientChatClosed",
433
+ event: { clientID: clid, uid: asUID(cmd.params["cluid"] ?? "") },
434
+ };
435
+ }
package/src/core/types.ts CHANGED
@@ -148,3 +148,17 @@ export type ClientStatus = (typeof ClientStatus)[keyof typeof ClientStatus];
148
148
  * default case.
149
149
  */
150
150
  export type ServerFamily = "ts3" | "ts6" | "unknown";
151
+
152
+ /**
153
+ * Pick the server generation from a `virtualserver_version` string such as
154
+ * "6.0.0-beta13.1" or "3.13.8 [Build: 1779874471]": major version 3 is TS3,
155
+ * 6 and later TS6, anything else, including a string with no number, unknown.
156
+ * Both clients use it, so they cannot disagree about the same server.
157
+ */
158
+ export function serverFamilyForVersion(version: string): ServerFamily {
159
+ const major = Number.parseInt(version, 10);
160
+ if (Number.isNaN(major)) return "unknown";
161
+ if (major === 3) return "ts3";
162
+ if (major >= 6) return "ts6";
163
+ return "unknown";
164
+ }
@@ -9,7 +9,7 @@ import {
9
9
  TeamspeakError,
10
10
  backoffDelay,
11
11
  noopLogger,
12
- type ClientEnterEvent,
12
+ type ClientEntry,
13
13
  type CommandExecutor,
14
14
  type CommandMiddleware,
15
15
  type CommandOptions,
@@ -18,6 +18,7 @@ import {
18
18
  type Listener,
19
19
  type Logger,
20
20
  type ReconnectOptions,
21
+ serverFamilyForVersion,
21
22
  type ServerEventMap,
22
23
  type ServerFamily,
23
24
  type ServerID,
@@ -33,7 +34,7 @@ import {
33
34
  parseServerError,
34
35
  resolveBackoffPolicy,
35
36
  } from "../core/internal.js";
36
- import type { QueryTransport } from "./transport.js";
37
+ import type { QueryTransport, QueryTransportStats } from "./transport.js";
37
38
  import { connectTcp } from "./tcp.js";
38
39
  import { connectSsh } from "./ssh.js";
39
40
  import { WebQueryTransport } from "./web.js";
@@ -154,7 +155,7 @@ export class QueryClient implements CommandExecutor, AsyncDisposable {
154
155
  * 0 until it is known.
155
156
  */
156
157
  #clid = 0;
157
- #clients = new Map<number, ClientEnterEvent>();
158
+ #clients = new Map<number, ClientEntry>();
158
159
 
159
160
  #reconnectOptions: ReconnectOptions | undefined;
160
161
  #reconnecting = false;
@@ -231,12 +232,32 @@ export class QueryClient implements CommandExecutor, AsyncDisposable {
231
232
  return this.#version;
232
233
  }
233
234
 
235
+ /**
236
+ * What the current connection reports about itself: its kind, where it
237
+ * points, when it opened and how many bytes went each way, for a metrics
238
+ * exporter. Read-only. A client with no connection, and a `custom` transport
239
+ * that keeps no counts, report no kind, an empty address, no open time and
240
+ * zero bytes. A reconnection starts the counts again.
241
+ *
242
+ * @experimental Part of the entry, which may change in a minor release.
243
+ */
244
+ get stats(): QueryTransportStats {
245
+ return (
246
+ this.#transport?.stats?.() ?? {
247
+ connectedAt: undefined,
248
+ transport: undefined,
249
+ address: "",
250
+ bytesSent: 0,
251
+ bytesReceived: 0,
252
+ }
253
+ );
254
+ }
255
+
234
256
  /**
235
257
  * The transport carrying this connection, or `undefined` when there is none.
236
258
  *
237
259
  * Read-only, and the transport is the interface the client itself talks
238
- * over — counts of what went out and came in, and which connection it is,
239
- * can be read from it for metrics. The socket underneath is not exposed:
260
+ * over. The socket underneath is not exposed:
240
261
  * a caller that needs it supplies the seam instead (the `connect` option of
241
262
  * a `tcp` transport, or `connection` of an `ssh` one), where it is theirs to
242
263
  * keep.
@@ -384,7 +405,7 @@ export class QueryClient implements CommandExecutor, AsyncDisposable {
384
405
  // Not `use()`: this session learns who it is, and who is there, once, below.
385
406
  if (server !== undefined) await this.#select(server, { nickname, signal });
386
407
  this.#version = await this.#getVersion(signal);
387
- this.#serverFamily = serverFamilyOf(this.#version);
408
+ this.#serverFamily = serverFamilyForVersion(this.#version);
388
409
  // Like the version, a failure here fails the connect: a client that cannot
389
410
  // tell which one it is would miss its own kick without saying so.
390
411
  await this.#learnSelf(signal);
@@ -1020,6 +1041,7 @@ export class QueryClient implements CommandExecutor, AsyncDisposable {
1020
1041
  case "channelUnsubscribed":
1021
1042
  case "pluginCommand":
1022
1043
  case "clientChatComposing":
1044
+ case "clientChatClosed":
1023
1045
  // Neither a TeamSpeak 3 ServerQuery connection nor a TeamSpeak 6 one over
1024
1046
  // SSH is sent these (measured), so a query client does not offer them.
1025
1047
  // Should a server send one, the clients it knows are still kept current
@@ -1269,15 +1291,6 @@ function serverIDOf(server: ResolvedQueryOptions["server"]): ServerID | number |
1269
1291
  return server !== undefined && "id" in server ? server.id : undefined;
1270
1292
  }
1271
1293
 
1272
- /** Which server generation a `version` string names. */
1273
- function serverFamilyOf(version: string): ServerFamily {
1274
- const major = Number.parseInt(version, 10);
1275
- if (Number.isNaN(major)) return "unknown";
1276
- if (major === 3) return "ts3";
1277
- if (major >= 6) return "ts6";
1278
- return "unknown";
1279
- }
1280
-
1281
1294
  /** Sleep, or wake early once `signal` aborts. Resolves either way, never rejects. */
1282
1295
  function sleep(ms: number, signal: AbortSignal): Promise<void> {
1283
1296
  if (ms <= 0 || signal.aborted) return Promise.resolve();
@@ -31,7 +31,7 @@ export type {
31
31
  QueryTransportOptions,
32
32
  } from "./client.js";
33
33
 
34
- export type { QueryTransport } from "./transport.js";
34
+ export type { QueryTransport, QueryTransportStats } from "./transport.js";
35
35
 
36
36
  // Reconnection: the client's default retry decision, and the shared options.
37
37
  export { defaultShouldRetry } from "./reconnect.js";
@@ -46,9 +46,17 @@ export type QueryTransportOptions =
46
46
  | ({
47
47
  /** WebQuery, the HTTP interface of TeamSpeak 3 (3.12 and later) and TeamSpeak 6. */
48
48
  readonly kind: "web";
49
- /** The server's base URL, such as `http://127.0.0.1:10080`. */
49
+ /**
50
+ * The server's base URL, such as `http://127.0.0.1:10080` or
51
+ * `https://ts.example.com:10443`.
52
+ */
50
53
  readonly url: string;
51
- /** The `fetch` to use; a small one over `node:http` by default. */
54
+ /**
55
+ * The `fetch` to use; by default a small one over `node:http` or
56
+ * `node:https` by the URL's scheme, verifying an HTTPS certificate
57
+ * against Node's trust store. For a private CA, pass a `fetch` that
58
+ * trusts it, or start Node with `NODE_EXTRA_CA_CERTS`.
59
+ */
52
60
  readonly fetch?: typeof fetch | undefined;
53
61
  } & WebCredentials)
54
62
  | {
@@ -67,7 +75,10 @@ export type QueryTransportOptions =
67
75
  * `servernotifyregister`.
68
76
  *
69
77
  * `bans` is not registered by default: TeamSpeak 3 refuses it with 1539,
70
- * which would fail the registration of every category after it.
78
+ * which would fail the registration of every category after it. `tokenused`
79
+ * is not registered by default either: both servers accept it (measured) and
80
+ * then send `notifytokenused`, which carries a privilege key and has no typed
81
+ * event, so it reaches `notification` only.
71
82
  */
72
83
  export type QueryEventCategory =
73
84
  | "server"
@@ -75,7 +86,8 @@ export type QueryEventCategory =
75
86
  | "textserver"
76
87
  | "textchannel"
77
88
  | "textprivate"
78
- | "bans";
89
+ | "bans"
90
+ | "tokenused";
79
91
 
80
92
  /**
81
93
  * Which notification categories to register for.
@@ -95,7 +107,8 @@ export type QueryEventCategory =
95
107
  * | `server` | `serverEdited` |
96
108
  * | `channel` (registered for every channel) | `channelCreated`, `channelEdited`, `channelMoved`, `channelDeleted`, `channelDescriptionChanged`, `channelPasswordChanged`, `clientEnter`, `clientLeave`, `clientMoved` |
97
109
  * | `textserver`, `textchannel`, `textprivate` | `textMessage`, for the server, the channel and private messages |
98
- * | `bans` | none |
110
+ * | `bans` | `banUpdated` |
111
+ * | `tokenused` | none (`notification` only) |
99
112
  *
100
113
  * Leaving a category out leaves its events out: `{ categories: ["textprivate"] }`
101
114
  * delivers no channel event. The voice client's events about clients' updates,
@@ -158,6 +171,7 @@ const CATEGORIES: ReadonlySet<string> = new Set([
158
171
  "textchannel",
159
172
  "textprivate",
160
173
  "bans",
174
+ "tokenused",
161
175
  ]);
162
176
 
163
177
  /** A transport with its defaults filled, and its fields copied. */
package/src/query/ssh.ts CHANGED
@@ -538,7 +538,12 @@ export async function connectSsh(options: SshOptions): Promise<StreamTransport>
538
538
  // stream closes with it.
539
539
  ssh.on("error", (failure) => channel.emit("error", failure));
540
540
  channel.once("close", () => ssh.end());
541
- resolve(new StreamTransport(channel, () => ssh.end()));
541
+ resolve(
542
+ new StreamTransport(channel, () => ssh.end(), {
543
+ transport: "ssh",
544
+ address: `${login === undefined ? "" : `${login.username}@`}${host}:${port}`,
545
+ }),
546
+ );
542
547
  });
543
548
  };
544
549
 
@@ -1,5 +1,5 @@
1
1
  import { LineReader } from "./lines.js";
2
- import type { QueryTransport } from "./transport.js";
2
+ import type { QueryTransport, QueryTransportStats } from "./transport.js";
3
3
 
4
4
  /**
5
5
  * The part of a duplex byte stream a line transport uses. A `node:net` socket
@@ -43,14 +43,24 @@ export class StreamTransport implements QueryTransport {
43
43
  /** Wakes a `close()` that is waiting for the server to end the session. */
44
44
  #onEnded: (() => void) | undefined;
45
45
  #error: Error | undefined;
46
+ #identity: { readonly transport: "tcp" | "ssh"; readonly address: string } | undefined;
47
+ #openedAt = new Date();
48
+ #bytesSent = 0;
49
+ #bytesReceived = 0;
46
50
 
47
51
  /**
48
52
  * `onEnd` runs once the stream is closed, to release what the stream sits
49
- * on: an SSH session's connection, say.
53
+ * on: an SSH session's connection, say. `identity` says which kind of
54
+ * connection this is and where it points, for {@link StreamTransport.stats}.
50
55
  */
51
- constructor(stream: QueryStream, onEnd?: () => void) {
56
+ constructor(
57
+ stream: QueryStream,
58
+ onEnd?: () => void,
59
+ identity?: { readonly transport: "tcp" | "ssh"; readonly address: string },
60
+ ) {
52
61
  this.#stream = stream;
53
62
  this.#onEnd = onEnd;
63
+ this.#identity = identity;
54
64
  stream.on("data", (chunk) => this.#handleData(chunk));
55
65
  stream.on("error", (error) => {
56
66
  this.#error = error;
@@ -61,10 +71,26 @@ export class StreamTransport implements QueryTransport {
61
71
  send(line: string): Promise<void> {
62
72
  if (this.#ended) return Promise.reject(new Error("the query connection is closed"));
63
73
  return new Promise((resolve, reject) => {
64
- this.#stream.write(`${line}\n`, (error) => (error ? reject(error) : resolve()));
74
+ this.#stream.write(`${line}\n`, (error) => {
75
+ if (error) reject(error);
76
+ else {
77
+ this.#bytesSent += Buffer.byteLength(line) + 1;
78
+ resolve();
79
+ }
80
+ });
65
81
  });
66
82
  }
67
83
 
84
+ stats(): QueryTransportStats {
85
+ return {
86
+ connectedAt: this.#openedAt,
87
+ transport: this.#identity?.transport,
88
+ address: this.#identity?.address ?? "",
89
+ bytesSent: this.#bytesSent,
90
+ bytesReceived: this.#bytesReceived,
91
+ };
92
+ }
93
+
68
94
  /**
69
95
  * Say `quit`, give the server a moment to end the session, and close.
70
96
  *
@@ -103,6 +129,7 @@ export class StreamTransport implements QueryTransport {
103
129
  }
104
130
 
105
131
  #handleData(chunk: Uint8Array): void {
132
+ this.#bytesReceived += chunk.byteLength;
106
133
  for (const line of this.#reader.push(chunk)) {
107
134
  if (this.#banner > 0) {
108
135
  this.#banner--;
package/src/query/tcp.ts CHANGED
@@ -43,7 +43,7 @@ export async function connectTcp(options: TcpOptions): Promise<StreamTransport>
43
43
  signal?.throwIfAborted();
44
44
  const stream = await connect(host, port);
45
45
  await opened(stream, signal);
46
- return new StreamTransport(stream);
46
+ return new StreamTransport(stream, undefined, { transport: "tcp", address: `${host}:${port}` });
47
47
  }
48
48
 
49
49
  /** A connected `node:net` socket, with whole lines sent as they are written. */