@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
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import type { IconID, UID } from "@honeybbq/teamspeak";
|
|
24
24
|
import type { Client } from "../client.js";
|
|
25
25
|
import type { FileTransferOptions } from "../transfer.js";
|
|
26
|
+
import { verifyStored, withDialCleanup } from "./upload-checks.js";
|
|
26
27
|
|
|
27
28
|
/** Options for the avatar and icon transfers. */
|
|
28
29
|
export type AvatarTransferOptions = Pick<FileTransferOptions, "signal" | "trace">;
|
|
@@ -84,7 +85,14 @@ export async function uploadAvatar(
|
|
|
84
85
|
overwrite: true,
|
|
85
86
|
...(options.signal ? { signal: options.signal } : {}),
|
|
86
87
|
});
|
|
87
|
-
await
|
|
88
|
+
await withDialCleanup(client, info.serverFileTransferID, () =>
|
|
89
|
+
client.uploadFileData(client.host, info, Readable.from([data]), options),
|
|
90
|
+
);
|
|
91
|
+
// Stored under a name the server derives from the uploader's uid, not the
|
|
92
|
+
// one asked for (measured on TeamSpeak 3 3.13.8 and TeamSpeak 6 6.0.0).
|
|
93
|
+
await verifyStored(client, CHANNEL_ZERO, `/${avatarFileName(client.uid)}`, BigInt(data.length), {
|
|
94
|
+
signal: options.signal,
|
|
95
|
+
});
|
|
88
96
|
await updateClient(client, { avatar: md5 });
|
|
89
97
|
return md5;
|
|
90
98
|
}
|
|
@@ -150,7 +158,12 @@ export async function uploadIcon(
|
|
|
150
158
|
overwrite: true,
|
|
151
159
|
...(options.signal ? { signal: options.signal } : {}),
|
|
152
160
|
});
|
|
153
|
-
await
|
|
161
|
+
await withDialCleanup(client, info.serverFileTransferID, () =>
|
|
162
|
+
client.uploadFileData(client.host, info, Readable.from([data]), options),
|
|
163
|
+
);
|
|
164
|
+
await verifyStored(client, CHANNEL_ZERO, iconFileName(id), BigInt(data.length), {
|
|
165
|
+
signal: options.signal,
|
|
166
|
+
});
|
|
154
167
|
return id;
|
|
155
168
|
}
|
|
156
169
|
|
package/src/client/api/files.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* (error 5120), on both servers.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { Readable, Writable } from "node:stream";
|
|
14
|
-
import { initFileDownload, initFileUpload } from "@honeybbq/teamspeak";
|
|
13
|
+
import { Readable, Transform, Writable, pipeline } from "node:stream";
|
|
14
|
+
import { initFileDownload, initFileUpload, stopFileTransfer } from "@honeybbq/teamspeak";
|
|
15
15
|
import type {
|
|
16
16
|
ChannelID,
|
|
17
17
|
CommandExecutor,
|
|
@@ -22,15 +22,15 @@ import type {
|
|
|
22
22
|
} from "@honeybbq/teamspeak";
|
|
23
23
|
import { FileTransferError } from "../errors.js";
|
|
24
24
|
import { downloadFileData, uploadFileData } from "../transfer.js";
|
|
25
|
+
import { verifyStored, withDialCleanup } from "./upload-checks.js";
|
|
25
26
|
import type { FileTransferOptions } from "../transfer.js";
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* How {@link uploadFile} negotiates and moves the file: the options of
|
|
29
|
-
* `initFileUpload
|
|
30
|
-
*
|
|
31
|
-
* signal to the transfer too.
|
|
30
|
+
* `initFileUpload` and those of `uploadFileData`. `signal` and `timeoutMs`
|
|
31
|
+
* apply to the negotiation, and the signal to the transfer too.
|
|
32
32
|
*/
|
|
33
|
-
export interface UploadFileOptions extends
|
|
33
|
+
export interface UploadFileOptions extends FileUploadOptions, FileTransferOptions {
|
|
34
34
|
/**
|
|
35
35
|
* The host of the file transfer port. A voice client sends to the host it
|
|
36
36
|
* connected to, so it needs none; any other connection must name it. A host
|
|
@@ -40,26 +40,30 @@ export interface UploadFileOptions extends Omit<FileUploadOptions, "resume">, Fi
|
|
|
40
40
|
/**
|
|
41
41
|
* The size in bytes of a stream passed as the data, which the server needs
|
|
42
42
|
* before it takes the file. Required for a `Readable`; for bytes it is their
|
|
43
|
-
* length, and a different value here is refused.
|
|
43
|
+
* length, and a different value here is refused. A stream that gives fewer
|
|
44
|
+
* or more bytes fails the upload.
|
|
44
45
|
*/
|
|
45
46
|
readonly size?: bigint | number | undefined;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
/**
|
|
50
|
+
* How {@link downloadFile} negotiates and moves the file. With a
|
|
51
|
+
* `seekPosition` the bytes are the file's from there to its end.
|
|
52
|
+
*/
|
|
53
|
+
export interface DownloadFileOptions extends FileDownloadOptions, FileTransferOptions {
|
|
51
54
|
/** The host of the file transfer port, as for {@link UploadFileOptions}. */
|
|
52
55
|
readonly host?: string | undefined;
|
|
53
56
|
/**
|
|
54
57
|
* Write the file into this stream instead of returning its bytes. The stream
|
|
55
|
-
* is
|
|
58
|
+
* is neither ended nor destroyed, as for `downloadFileData`, so it may take
|
|
59
|
+
* several downloads; a failed transfer still rejects the call.
|
|
56
60
|
*/
|
|
57
61
|
readonly dest?: Writable | undefined;
|
|
58
62
|
/**
|
|
59
|
-
* The
|
|
60
|
-
*
|
|
61
|
-
* {@link FileTransferError} before any byte is read
|
|
62
|
-
* negotiated is
|
|
63
|
+
* The most bytes to hold in memory when no `dest` is given; 268435456
|
|
64
|
+
* (256 MiB) by default. A larger download is refused with a
|
|
65
|
+
* {@link FileTransferError} before any byte is read, and the transfer the
|
|
66
|
+
* server negotiated is stopped.
|
|
63
67
|
*/
|
|
64
68
|
readonly maxSize?: number | undefined;
|
|
65
69
|
}
|
|
@@ -147,12 +151,43 @@ function sizeOf(data: Uint8Array | Readable, declared: bigint | number | undefin
|
|
|
147
151
|
return size;
|
|
148
152
|
}
|
|
149
153
|
|
|
154
|
+
/** Drops the first `count` bytes of a stream. */
|
|
155
|
+
function skipBytes(count: number): Transform {
|
|
156
|
+
let left = count;
|
|
157
|
+
return new Transform({
|
|
158
|
+
transform(chunk: Buffer, _encoding, callback) {
|
|
159
|
+
if (left >= chunk.length) {
|
|
160
|
+
left -= chunk.length;
|
|
161
|
+
callback();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const rest = chunk.subarray(left);
|
|
165
|
+
left = 0;
|
|
166
|
+
callback(null, rest);
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
150
171
|
/**
|
|
151
172
|
* Upload a file into a channel's file area: negotiate the transfer
|
|
152
173
|
* (`initFileUpload`) and send the bytes (`uploadFileData`), in one call.
|
|
153
174
|
*
|
|
154
175
|
* `data` is the bytes, or a `Readable` with `options.size` the number of bytes
|
|
155
|
-
* it will give.
|
|
176
|
+
* it will give. It resolves once the server has closed the transfer
|
|
177
|
+
* connection and, by `ftgetfileinfo` (one more command), holds a file of
|
|
178
|
+
* exactly that size: the server does not say whether it stored a file, so a
|
|
179
|
+
* close alone could be a transfer someone stopped. A size that differs, or a
|
|
180
|
+
* file that is not there, fails the call with a {@link FileTransferError}; a
|
|
181
|
+
* client with no browse power cannot ask (2568) and resolves on the close. A
|
|
182
|
+
* stream that gives fewer or more bytes than `size` fails the call, and the
|
|
183
|
+
* connection is closed. When the connection to the file transfer port is
|
|
184
|
+
* refused or times out, the negotiated transfer is stopped on the server (else
|
|
185
|
+
* it lingers in `ftlist` for about 10 seconds), as it is for `downloadFile`.
|
|
186
|
+
*
|
|
187
|
+
* With `resume`, `data` is still the whole file: the server says how much it
|
|
188
|
+
* already holds from an interrupted upload (a file that is not there is
|
|
189
|
+
* refused with 2054), and only the rest is sent; `overwrite` cannot go with it
|
|
190
|
+
* (2056). An existing file is not replaced unless `overwrite` is set, and
|
|
156
191
|
* the directory the file goes in must exist (a missing one is refused with
|
|
157
192
|
* 2054, invalid file path): make it with `createDirectory` first. A refusal of
|
|
158
193
|
* the negotiation is thrown as the ServerError it is, before anything is sent;
|
|
@@ -175,11 +210,21 @@ export async function uploadFile(
|
|
|
175
210
|
const info = await initFileUpload(client, channelID, path, size, {
|
|
176
211
|
password: options.password,
|
|
177
212
|
overwrite: options.overwrite,
|
|
213
|
+
resume: options.resume,
|
|
178
214
|
signal: options.signal,
|
|
179
215
|
timeoutMs: options.timeoutMs,
|
|
180
216
|
});
|
|
181
|
-
|
|
182
|
-
|
|
217
|
+
// What the server holds is not sent again; a position past the end is for
|
|
218
|
+
// the transfer to refuse.
|
|
219
|
+
const skip = Number(info.seekPosition);
|
|
220
|
+
let source: Readable;
|
|
221
|
+
if (data instanceof Uint8Array) source = Readable.from([data.subarray(skip)]);
|
|
222
|
+
else if (skip === 0) source = data;
|
|
223
|
+
else source = pipeline(data, skipBytes(skip), () => {});
|
|
224
|
+
await withDialCleanup(client, info.serverFileTransferID, () =>
|
|
225
|
+
route.upload(info, source, transferOptions(options)),
|
|
226
|
+
);
|
|
227
|
+
await verifyStored(client, channelID, path, size, options);
|
|
183
228
|
}
|
|
184
229
|
|
|
185
230
|
/**
|
|
@@ -188,7 +233,11 @@ export async function uploadFile(
|
|
|
188
233
|
* call. With `options.dest` the bytes go into that stream instead and nothing
|
|
189
234
|
* is held, which is the way to fetch a large file.
|
|
190
235
|
*
|
|
191
|
-
*
|
|
236
|
+
* With `seekPosition` the bytes are the file's from there to its end, which is
|
|
237
|
+
* how to resume a partial download: append them to what was already fetched.
|
|
238
|
+
*
|
|
239
|
+
* More than `maxSize` bytes (256 MiB unless set) is refused when no `dest` is
|
|
240
|
+
* given, and the transfer the server negotiated is stopped.
|
|
192
241
|
* A file that is not there is the ServerError of the negotiation: 2051 on TS3
|
|
193
242
|
* 3.13.8 and TS6 beta13.1. A path that names a directory is not refused by the
|
|
194
243
|
* server: it announces a size and sends nothing, so the call rejects with a
|
|
@@ -223,16 +272,25 @@ export async function downloadFile(
|
|
|
223
272
|
const route = routeFor(client, options);
|
|
224
273
|
const info = await initFileDownload(client, channelID, path, {
|
|
225
274
|
password: options.password,
|
|
275
|
+
seekPosition: options.seekPosition,
|
|
226
276
|
signal: options.signal,
|
|
227
277
|
timeoutMs: options.timeoutMs,
|
|
228
278
|
});
|
|
229
279
|
if (options.dest !== undefined) {
|
|
230
|
-
|
|
280
|
+
const dest = options.dest;
|
|
281
|
+
await withDialCleanup(client, info.serverFileTransferID, () =>
|
|
282
|
+
route.download(info, dest, transferOptions(options)),
|
|
283
|
+
);
|
|
231
284
|
return;
|
|
232
285
|
}
|
|
233
|
-
|
|
286
|
+
const wanted = info.size - info.seekPosition;
|
|
287
|
+
if (wanted > BigInt(maxSize)) {
|
|
288
|
+
// The server holds the negotiated transfer, and lists it among the active
|
|
289
|
+
// ones, until it is used or stopped. It is this connection's own, which is
|
|
290
|
+
// the only kind `ftstop` is safe to send.
|
|
291
|
+
await stopFileTransfer(client, info.serverFileTransferID).catch(() => {});
|
|
234
292
|
throw new FileTransferError(
|
|
235
|
-
`the
|
|
293
|
+
`the download is ${wanted} bytes, over the ${maxSize} bytes maxSize allows in memory`,
|
|
236
294
|
);
|
|
237
295
|
}
|
|
238
296
|
const chunks: Buffer[] = [];
|
|
@@ -242,7 +300,9 @@ export async function downloadFile(
|
|
|
242
300
|
done();
|
|
243
301
|
},
|
|
244
302
|
});
|
|
245
|
-
await
|
|
303
|
+
await withDialCleanup(client, info.serverFileTransferID, () =>
|
|
304
|
+
route.download(info, sink, transferOptions(options)),
|
|
305
|
+
);
|
|
246
306
|
return Buffer.concat(chunks);
|
|
247
307
|
}
|
|
248
308
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an upload does around its data phase: stop a transfer whose connection
|
|
3
|
+
* was never made, and check what the server stored once it closed the
|
|
4
|
+
* connection. Shared by the file and avatar helpers; not exported from the
|
|
5
|
+
* package.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ErrorCode, ServerError, getFileInfo, stopFileTransfer } from "@honeybbq/teamspeak";
|
|
9
|
+
import type { ChannelID, CommandExecutor, FileUploadInfo } from "@honeybbq/teamspeak";
|
|
10
|
+
import { FileTransferError } from "../errors.js";
|
|
11
|
+
import { failedToConnect } from "../transfer.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The server drops a transfer nobody dialled after about 10 seconds (measured
|
|
15
|
+
* on TeamSpeak 3 3.13.8 and TeamSpeak 6 6.0.0: it is gone from `ftlist` at
|
|
16
|
+
* 10.05 s, and the voice client hears 2059). `ftstop` of a transfer the server
|
|
17
|
+
* no longer has crashes TeamSpeak 6, so a stop is only sent well inside that.
|
|
18
|
+
*/
|
|
19
|
+
const STOP_WITHIN_MS = 5_000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Runs the data phase of a transfer the server has negotiated. When it fails
|
|
23
|
+
* because the connection to the file transfer port was refused or timed out,
|
|
24
|
+
* nothing has reached the server, and the transfer would sit in its list until
|
|
25
|
+
* it expires: stop it, as a refused `maxSize` download does. The stop is best
|
|
26
|
+
* effort and never replaces the dial's error. Not sent once the connection
|
|
27
|
+
* opened (the key may be written and the server may have dropped the transfer),
|
|
28
|
+
* nor after `STOP_WITHIN_MS`.
|
|
29
|
+
*/
|
|
30
|
+
export async function withDialCleanup(
|
|
31
|
+
client: CommandExecutor,
|
|
32
|
+
serverFileTransferID: FileUploadInfo["serverFileTransferID"],
|
|
33
|
+
move: () => Promise<void>,
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
const negotiatedAt = performance.now();
|
|
36
|
+
try {
|
|
37
|
+
await move();
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (failedToConnect(error) && performance.now() - negotiatedAt < STOP_WITHIN_MS) {
|
|
40
|
+
await stopFileTransfer(client, serverFileTransferID).catch(() => {});
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Checks that the server holds the whole file after an upload's connection
|
|
48
|
+
* closed. Measured on TeamSpeak 3 3.13.8 and TeamSpeak 6 6.0.0: a completed
|
|
49
|
+
* upload gets no status from the server, so a close after the last byte looks
|
|
50
|
+
* the same whether the file was stored or the transfer was stopped (the voice
|
|
51
|
+
* protocol reports that, 2066, only after the close), and `ftgetfileinfo`
|
|
52
|
+
* already answers the final size 6 to 45 ms after the close, in every case.
|
|
53
|
+
* Permissions are checked at the negotiation only, so no refusal comes later.
|
|
54
|
+
*
|
|
55
|
+
* A client without browse power gets 2568 for `ftgetfileinfo`: the file cannot
|
|
56
|
+
* be checked, and the upload resolves on the close. Any other failure of the
|
|
57
|
+
* command is thrown as it is.
|
|
58
|
+
*/
|
|
59
|
+
export async function verifyStored(
|
|
60
|
+
client: CommandExecutor,
|
|
61
|
+
channelID: ChannelID,
|
|
62
|
+
path: string,
|
|
63
|
+
size: bigint,
|
|
64
|
+
options: {
|
|
65
|
+
readonly password?: string | undefined;
|
|
66
|
+
readonly signal?: AbortSignal | undefined;
|
|
67
|
+
readonly timeoutMs?: number | undefined;
|
|
68
|
+
},
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
let stored: bigint;
|
|
71
|
+
try {
|
|
72
|
+
stored = (
|
|
73
|
+
await getFileInfo(client, channelID, path, {
|
|
74
|
+
password: options.password,
|
|
75
|
+
signal: options.signal,
|
|
76
|
+
timeoutMs: options.timeoutMs,
|
|
77
|
+
})
|
|
78
|
+
).size;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof ServerError) {
|
|
81
|
+
if (error.code === ErrorCode.PermissionsClientInsufficient) return;
|
|
82
|
+
// 2051 on TeamSpeak 6, 2052 on TeamSpeak 3 for a file that is not there.
|
|
83
|
+
if (error.code === ErrorCode.FileNotFound || error.code === ErrorCode.FileIOError) {
|
|
84
|
+
throw new FileTransferError(`the server closed the upload but has no file ${path}`, {
|
|
85
|
+
cause: error,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
if (stored !== size) {
|
|
92
|
+
throw new FileTransferError(
|
|
93
|
+
`the server closed the upload but holds ${stored} of the ${size} bytes of ${path}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/client/api/voice.ts
CHANGED
|
@@ -14,7 +14,8 @@ import type { ChannelID, ClientID, CommandOptions } from "@honeybbq/teamspeak";
|
|
|
14
14
|
/**
|
|
15
15
|
* The channel's description, which `listChannels` omits for bandwidth. The
|
|
16
16
|
* server answers with a `notifychanneledited` that the client does not report as
|
|
17
|
-
* a `channelEdited` event, since it is no edit.
|
|
17
|
+
* a `channelEdited` event, since it is no edit. Rejects with a NoDataError if
|
|
18
|
+
* the server sends no such answer.
|
|
18
19
|
*/
|
|
19
20
|
export async function getChannelDescription(
|
|
20
21
|
client: Client,
|
|
@@ -29,7 +30,9 @@ export async function getChannelDescription(
|
|
|
29
30
|
// can arrive while the command waits, a real edit with other properties,
|
|
30
31
|
// and the first row is then not the answer.
|
|
31
32
|
const answer = rows.find((row) => row["cid"] === String(cid) && "channel_description" in row);
|
|
32
|
-
|
|
33
|
+
// A missing answer is not an empty description.
|
|
34
|
+
if (answer === undefined) throw new NoDataError("channelgetdescription");
|
|
35
|
+
return answer["channel_description"] ?? "";
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/**
|