@mtcute/core 0.30.1 → 0.30.2

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 (83) hide show
  1. package/highlevel/methods/files/download-iterable.cjs +80 -45
  2. package/highlevel/methods/files/download-iterable.js +80 -45
  3. package/highlevel/methods/files/normalize-input-media.cjs +1 -1
  4. package/highlevel/methods/files/normalize-input-media.js +1 -1
  5. package/highlevel/methods/files/upload-file.cjs +6 -1
  6. package/highlevel/methods/files/upload-file.js +6 -1
  7. package/highlevel/methods/messages/normalize-rich-message.cjs +12 -1
  8. package/highlevel/methods/messages/normalize-rich-message.js +12 -1
  9. package/highlevel/methods/messages/send-common.cjs +3 -2
  10. package/highlevel/methods/messages/send-common.d.cts +4 -1
  11. package/highlevel/methods/messages/send-common.d.ts +4 -1
  12. package/highlevel/methods/messages/send-common.js +3 -2
  13. package/highlevel/types/files/utils.d.cts +9 -0
  14. package/highlevel/types/files/utils.d.ts +9 -0
  15. package/highlevel/updates/manager.cjs +6 -169
  16. package/highlevel/updates/manager.js +6 -169
  17. package/highlevel/utils/index.d.cts +1 -0
  18. package/highlevel/utils/index.d.ts +1 -0
  19. package/highlevel/utils/walk-page-blocks.cjs +164 -0
  20. package/highlevel/utils/walk-page-blocks.d.cts +9 -0
  21. package/highlevel/utils/walk-page-blocks.d.ts +9 -0
  22. package/highlevel/utils/walk-page-blocks.js +159 -0
  23. package/network/authorization.cjs +1 -1
  24. package/network/authorization.js +1 -1
  25. package/network/delay-gate.cjs +29 -0
  26. package/network/delay-gate.d.cts +5 -0
  27. package/network/delay-gate.d.ts +5 -0
  28. package/network/delay-gate.js +24 -0
  29. package/network/delay-gate.test.d.cts +1 -0
  30. package/network/delay-gate.test.d.ts +1 -0
  31. package/network/message-id-store.cjs +30 -0
  32. package/network/message-id-store.d.cts +7 -0
  33. package/network/message-id-store.d.ts +7 -0
  34. package/network/message-id-store.js +25 -0
  35. package/network/message-id-store.test.d.cts +1 -0
  36. package/network/message-id-store.test.d.ts +1 -0
  37. package/network/middlewares/bundle.cjs +2 -0
  38. package/network/middlewares/bundle.d.cts +1 -0
  39. package/network/middlewares/bundle.d.ts +1 -0
  40. package/network/middlewares/bundle.js +2 -0
  41. package/network/middlewares/default.cjs +6 -1
  42. package/network/middlewares/default.js +6 -1
  43. package/network/middlewares/flood-waiter.test.d.cts +1 -0
  44. package/network/middlewares/flood-waiter.test.d.ts +1 -0
  45. package/network/middlewares/internal-errors.test.d.cts +1 -0
  46. package/network/middlewares/internal-errors.test.d.ts +1 -0
  47. package/network/middlewares/media-throttle.cjs +65 -0
  48. package/network/middlewares/media-throttle.d.cts +5 -0
  49. package/network/middlewares/media-throttle.d.ts +5 -0
  50. package/network/middlewares/media-throttle.js +60 -0
  51. package/network/middlewares/media-throttle.test.d.cts +1 -0
  52. package/network/middlewares/media-throttle.test.d.ts +1 -0
  53. package/network/middlewares/on-error.test.d.cts +1 -0
  54. package/network/middlewares/on-error.test.d.ts +1 -0
  55. package/network/middlewares/on-method.test.d.cts +1 -0
  56. package/network/middlewares/on-method.test.d.ts +1 -0
  57. package/network/mtproto-session.cjs +9 -2
  58. package/network/mtproto-session.d.cts +5 -2
  59. package/network/mtproto-session.d.ts +5 -2
  60. package/network/mtproto-session.js +9 -2
  61. package/network/multi-session-connection.cjs +7 -0
  62. package/network/multi-session-connection.d.cts +2 -0
  63. package/network/multi-session-connection.d.ts +2 -0
  64. package/network/multi-session-connection.js +7 -0
  65. package/network/network-manager.cjs +7 -1
  66. package/network/network-manager.js +7 -1
  67. package/network/resource-limiter.cjs +60 -0
  68. package/network/resource-limiter.d.cts +11 -0
  69. package/network/resource-limiter.d.ts +11 -0
  70. package/network/resource-limiter.js +55 -0
  71. package/network/resource-limiter.test.d.cts +1 -0
  72. package/network/resource-limiter.test.d.ts +1 -0
  73. package/network/server-salt.cjs +7 -3
  74. package/network/server-salt.d.cts +2 -0
  75. package/network/server-salt.d.ts +2 -0
  76. package/network/server-salt.js +7 -3
  77. package/network/session-connection.cjs +106 -20
  78. package/network/session-connection.d.cts +11 -0
  79. package/network/session-connection.d.ts +11 -0
  80. package/network/session-connection.js +106 -20
  81. package/package.json +1 -1
  82. package/utils.cjs +2 -0
  83. package/utils.js +2 -0
@@ -7,7 +7,9 @@ if (typeof globalThis !== "undefined" && !globalThis._MTCUTE_CJS_DEPRECATION_WAR
7
7
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
8
  const utils = require("@fuman/utils");
9
9
  const fileId = require("@mtcute/file-id");
10
+ const delayGate = require("../../../network/delay-gate.cjs");
10
11
  const errors$1 = require("../../../types/errors.cjs");
12
+ const abortSignal = require("../../../utils/abort-signal.cjs");
11
13
  const peerUtils = require("../../../utils/peer-utils.cjs");
12
14
  const convertFileId = require("../../utils/convert-file-id.cjs");
13
15
  const fileUtils = require("../../utils/file-utils.cjs");
@@ -90,8 +92,17 @@ async function* downloadAsIterable(client, input, params) {
90
92
  let dcId = params?.dcId ?? normalized?.dcId;
91
93
  const primaryDcId = await client.getPrimaryDcId();
92
94
  if (!dcId) dcId = primaryDcId;
93
- const abortSignal = params?.abortSignal;
95
+ const stallTimeout = params?.stallTimeout;
96
+ const stallAbort = stallTimeout != null ? new AbortController() : void 0;
97
+ const abortSignal$1 = stallAbort ? params?.abortSignal ? abortSignal.combineAbortSignals(params.abortSignal, stallAbort.signal) : stallAbort.signal : params?.abortSignal;
94
98
  let ended = false;
99
+ let stallTimer;
100
+ const resetStallTimer = stallAbort && stallTimeout != null ? () => {
101
+ if (stallTimer) utils.timers.clearTimeout(stallTimer);
102
+ stallTimer = utils.timers.setTimeout(() => {
103
+ stallAbort.abort(new errors$1.MtTimeoutError(stallTimeout));
104
+ }, stallTimeout);
105
+ } : void 0;
95
106
  const partSizeKb = params?.partSize ?? (fileSize ? fileUtils.determinePartSize(fileSize) : 64);
96
107
  if (partSizeKb % 4 !== 0) {
97
108
  throw new errors$1.MtArgumentError(`Invalid part size: ${partSizeKb}. Must be divisible by 4.`);
@@ -120,6 +131,7 @@ async function* downloadAsIterable(client, input, params) {
120
131
  connectionKind = "download";
121
132
  }
122
133
  const poolSize = await client.getPoolSize(connectionKind, dcId);
134
+ const delayGate$1 = isSmall ? void 0 : new delayGate.DownloadDelayGate();
123
135
  client.log.debug(
124
136
  "Downloading file of size %d from dc %d using %s connection pool (pool size: %d)",
125
137
  limitBytes,
@@ -127,6 +139,44 @@ async function* downloadAsIterable(client, input, params) {
127
139
  connectionKind,
128
140
  poolSize
129
141
  );
142
+ const callPart = async (chunk) => {
143
+ while (true) {
144
+ try {
145
+ return await client.call(
146
+ {
147
+ _: isWeb ? "upload.getWebFile" : "upload.getFile",
148
+ // eslint-disable-next-line
149
+ location,
150
+ offset: offset + chunkSize * chunk,
151
+ limit: chunkSize
152
+ },
153
+ {
154
+ dcId,
155
+ kind: connectionKind,
156
+ maxRetryCount: Infinity,
157
+ // retry until explicitly aborted (or finished)
158
+ floodSleepThreshold: Infinity,
159
+ abortSignal: abortSignal$1
160
+ }
161
+ );
162
+ } catch (e) {
163
+ if (e instanceof DOMException && e.name === "AbortError") return void 0;
164
+ if (!innerTl.RpcError.is(e)) throw e;
165
+ if (e.is("FILE_MIGRATE_%d")) {
166
+ dcId = e.newDc;
167
+ continue;
168
+ } else if (e.is("FILEREF_UPGRADE_NEEDED")) {
169
+ throw new errors$1.MtUnsupportedError("File ref expired!");
170
+ } else if (e.is("MTPROTO_CLUSTER_INVALID") && dcId != null) {
171
+ client.log.debug("received cluster invalid error for dc %d, recreating dc", dcId);
172
+ await client.recreateDc(dcId);
173
+ continue;
174
+ } else {
175
+ throw e;
176
+ }
177
+ }
178
+ }
179
+ };
130
180
  const downloadChunk = async (chunk = nextWorkerChunkIdx++) => {
131
181
  let result;
132
182
  if (ended) {
@@ -136,40 +186,20 @@ async function* downloadAsIterable(client, input, params) {
136
186
  if (throttlePromise) {
137
187
  await throttlePromise;
138
188
  }
189
+ if (ended) {
190
+ return;
191
+ }
139
192
  try {
140
- result = await client.call(
141
- {
142
- _: isWeb ? "upload.getWebFile" : "upload.getFile",
143
- // eslint-disable-next-line
144
- location,
145
- offset: offset + chunkSize * chunk,
146
- limit: chunkSize
147
- },
148
- {
149
- dcId,
150
- kind: connectionKind,
151
- maxRetryCount: Infinity,
152
- // retry until explicitly aborted (or finished)
153
- floodSleepThreshold: Infinity,
154
- abortSignal
155
- }
156
- );
193
+ if (delayGate$1) await delayGate$1.wait(abortSignal$1);
194
+ result = await callPart(chunk);
157
195
  } catch (e) {
158
196
  if (e instanceof DOMException && e.name === "AbortError") return;
159
- if (!innerTl.RpcError.is(e)) throw e;
160
- if (e.is("FILE_MIGRATE_%d")) {
161
- dcId = e.newDc;
162
- return downloadChunk(chunk);
163
- } else if (e.is("FILEREF_UPGRADE_NEEDED")) {
164
- throw new errors$1.MtUnsupportedError("File ref expired!");
165
- } else if (e.is("MTPROTO_CLUSTER_INVALID") && dcId != null) {
166
- client.log.debug("received cluster invalid error for dc %d, recreating dc", dcId);
167
- await client.recreateDc(dcId);
168
- return downloadChunk(chunk);
169
- } else {
170
- throw e;
171
- }
197
+ throw e;
172
198
  }
199
+ if (!result) {
200
+ return;
201
+ }
202
+ resetStallTimer?.();
173
203
  if (result._ === "upload.fileCdnRedirect") {
174
204
  throw new errors$1.MtUnsupportedError("Received CDN redirect, which is not supported (yet)");
175
205
  }
@@ -189,6 +219,7 @@ async function* downloadAsIterable(client, input, params) {
189
219
  }
190
220
  };
191
221
  let error;
222
+ resetStallTimer?.();
192
223
  void Promise.all(Array.from({
193
224
  length: Math.min(poolSize * (isSmall ? 1 : REQUESTS_PER_CONNECTION), numChunks)
194
225
  }, downloadChunk)).catch((e) => {
@@ -199,27 +230,31 @@ async function* downloadAsIterable(client, input, params) {
199
230
  }).then(() => {
200
231
  client.log.debug("download workers finished");
201
232
  });
202
- abortSignal?.addEventListener("abort", () => {
233
+ abortSignal$1?.addEventListener("abort", () => {
203
234
  client.log.debug("download aborted");
204
- error = abortSignal.reason;
235
+ error = abortSignal$1.reason;
205
236
  ended = true;
206
237
  nextChunkCv.notify();
207
238
  });
208
239
  let position = offset;
209
- while (position < limitBytes) {
210
- await nextChunkCv.wait();
211
- if (error) throw error;
212
- while (nextChunkIdx in buffer) {
213
- const buf = buffer[nextChunkIdx];
214
- delete buffer[nextChunkIdx];
215
- position += buf.length;
216
- params?.progressCallback?.(position, limitBytes);
217
- yield buf;
218
- nextChunkIdx++;
219
- if (buf.length < chunkSize) {
220
- return;
240
+ try {
241
+ while (position < limitBytes) {
242
+ await nextChunkCv.wait();
243
+ if (error) throw error;
244
+ while (nextChunkIdx in buffer) {
245
+ const buf = buffer[nextChunkIdx];
246
+ delete buffer[nextChunkIdx];
247
+ position += buf.length;
248
+ params?.progressCallback?.(position, limitBytes);
249
+ yield buf;
250
+ nextChunkIdx++;
251
+ if (buf.length < chunkSize) {
252
+ return;
253
+ }
221
254
  }
222
255
  }
256
+ } finally {
257
+ if (stallTimer) utils.timers.clearTimeout(stallTimer);
223
258
  }
224
259
  }
225
260
  exports._normalizeFileDownloadLocation = _normalizeFileDownloadLocation;
@@ -1,6 +1,8 @@
1
- import { ConditionVariable } from "@fuman/utils";
1
+ import { ConditionVariable, timers } from "@fuman/utils";
2
2
  import { parseFileId } from "@mtcute/file-id";
3
- import { MtArgumentError, MtUnsupportedError } from "../../../types/errors.js";
3
+ import { DownloadDelayGate } from "../../../network/delay-gate.js";
4
+ import { MtArgumentError, MtTimeoutError, MtUnsupportedError } from "../../../types/errors.js";
5
+ import { combineAbortSignals } from "../../../utils/abort-signal.js";
4
6
  import { toggleChannelIdMark } from "../../../utils/peer-utils.js";
5
7
  import { fileIdToInputWebFileLocation, fileIdToInputFileLocation } from "../../utils/convert-file-id.js";
6
8
  import { determinePartSize } from "../../utils/file-utils.js";
@@ -83,8 +85,17 @@ async function* downloadAsIterable(client, input, params) {
83
85
  let dcId = params?.dcId ?? normalized?.dcId;
84
86
  const primaryDcId = await client.getPrimaryDcId();
85
87
  if (!dcId) dcId = primaryDcId;
86
- const abortSignal = params?.abortSignal;
88
+ const stallTimeout = params?.stallTimeout;
89
+ const stallAbort = stallTimeout != null ? new AbortController() : void 0;
90
+ const abortSignal = stallAbort ? params?.abortSignal ? combineAbortSignals(params.abortSignal, stallAbort.signal) : stallAbort.signal : params?.abortSignal;
87
91
  let ended = false;
92
+ let stallTimer;
93
+ const resetStallTimer = stallAbort && stallTimeout != null ? () => {
94
+ if (stallTimer) timers.clearTimeout(stallTimer);
95
+ stallTimer = timers.setTimeout(() => {
96
+ stallAbort.abort(new MtTimeoutError(stallTimeout));
97
+ }, stallTimeout);
98
+ } : void 0;
88
99
  const partSizeKb = params?.partSize ?? (fileSize ? determinePartSize(fileSize) : 64);
89
100
  if (partSizeKb % 4 !== 0) {
90
101
  throw new MtArgumentError(`Invalid part size: ${partSizeKb}. Must be divisible by 4.`);
@@ -113,6 +124,7 @@ async function* downloadAsIterable(client, input, params) {
113
124
  connectionKind = "download";
114
125
  }
115
126
  const poolSize = await client.getPoolSize(connectionKind, dcId);
127
+ const delayGate = isSmall ? void 0 : new DownloadDelayGate();
116
128
  client.log.debug(
117
129
  "Downloading file of size %d from dc %d using %s connection pool (pool size: %d)",
118
130
  limitBytes,
@@ -120,6 +132,44 @@ async function* downloadAsIterable(client, input, params) {
120
132
  connectionKind,
121
133
  poolSize
122
134
  );
135
+ const callPart = async (chunk) => {
136
+ while (true) {
137
+ try {
138
+ return await client.call(
139
+ {
140
+ _: isWeb ? "upload.getWebFile" : "upload.getFile",
141
+ // eslint-disable-next-line
142
+ location,
143
+ offset: offset + chunkSize * chunk,
144
+ limit: chunkSize
145
+ },
146
+ {
147
+ dcId,
148
+ kind: connectionKind,
149
+ maxRetryCount: Infinity,
150
+ // retry until explicitly aborted (or finished)
151
+ floodSleepThreshold: Infinity,
152
+ abortSignal
153
+ }
154
+ );
155
+ } catch (e) {
156
+ if (e instanceof DOMException && e.name === "AbortError") return void 0;
157
+ if (!RpcError.is(e)) throw e;
158
+ if (e.is("FILE_MIGRATE_%d")) {
159
+ dcId = e.newDc;
160
+ continue;
161
+ } else if (e.is("FILEREF_UPGRADE_NEEDED")) {
162
+ throw new MtUnsupportedError("File ref expired!");
163
+ } else if (e.is("MTPROTO_CLUSTER_INVALID") && dcId != null) {
164
+ client.log.debug("received cluster invalid error for dc %d, recreating dc", dcId);
165
+ await client.recreateDc(dcId);
166
+ continue;
167
+ } else {
168
+ throw e;
169
+ }
170
+ }
171
+ }
172
+ };
123
173
  const downloadChunk = async (chunk = nextWorkerChunkIdx++) => {
124
174
  let result;
125
175
  if (ended) {
@@ -129,40 +179,20 @@ async function* downloadAsIterable(client, input, params) {
129
179
  if (throttlePromise) {
130
180
  await throttlePromise;
131
181
  }
182
+ if (ended) {
183
+ return;
184
+ }
132
185
  try {
133
- result = await client.call(
134
- {
135
- _: isWeb ? "upload.getWebFile" : "upload.getFile",
136
- // eslint-disable-next-line
137
- location,
138
- offset: offset + chunkSize * chunk,
139
- limit: chunkSize
140
- },
141
- {
142
- dcId,
143
- kind: connectionKind,
144
- maxRetryCount: Infinity,
145
- // retry until explicitly aborted (or finished)
146
- floodSleepThreshold: Infinity,
147
- abortSignal
148
- }
149
- );
186
+ if (delayGate) await delayGate.wait(abortSignal);
187
+ result = await callPart(chunk);
150
188
  } catch (e) {
151
189
  if (e instanceof DOMException && e.name === "AbortError") return;
152
- if (!RpcError.is(e)) throw e;
153
- if (e.is("FILE_MIGRATE_%d")) {
154
- dcId = e.newDc;
155
- return downloadChunk(chunk);
156
- } else if (e.is("FILEREF_UPGRADE_NEEDED")) {
157
- throw new MtUnsupportedError("File ref expired!");
158
- } else if (e.is("MTPROTO_CLUSTER_INVALID") && dcId != null) {
159
- client.log.debug("received cluster invalid error for dc %d, recreating dc", dcId);
160
- await client.recreateDc(dcId);
161
- return downloadChunk(chunk);
162
- } else {
163
- throw e;
164
- }
190
+ throw e;
165
191
  }
192
+ if (!result) {
193
+ return;
194
+ }
195
+ resetStallTimer?.();
166
196
  if (result._ === "upload.fileCdnRedirect") {
167
197
  throw new MtUnsupportedError("Received CDN redirect, which is not supported (yet)");
168
198
  }
@@ -182,6 +212,7 @@ async function* downloadAsIterable(client, input, params) {
182
212
  }
183
213
  };
184
214
  let error;
215
+ resetStallTimer?.();
185
216
  void Promise.all(Array.from({
186
217
  length: Math.min(poolSize * (isSmall ? 1 : REQUESTS_PER_CONNECTION), numChunks)
187
218
  }, downloadChunk)).catch((e) => {
@@ -199,20 +230,24 @@ async function* downloadAsIterable(client, input, params) {
199
230
  nextChunkCv.notify();
200
231
  });
201
232
  let position = offset;
202
- while (position < limitBytes) {
203
- await nextChunkCv.wait();
204
- if (error) throw error;
205
- while (nextChunkIdx in buffer) {
206
- const buf = buffer[nextChunkIdx];
207
- delete buffer[nextChunkIdx];
208
- position += buf.length;
209
- params?.progressCallback?.(position, limitBytes);
210
- yield buf;
211
- nextChunkIdx++;
212
- if (buf.length < chunkSize) {
213
- return;
233
+ try {
234
+ while (position < limitBytes) {
235
+ await nextChunkCv.wait();
236
+ if (error) throw error;
237
+ while (nextChunkIdx in buffer) {
238
+ const buf = buffer[nextChunkIdx];
239
+ delete buffer[nextChunkIdx];
240
+ position += buf.length;
241
+ params?.progressCallback?.(position, limitBytes);
242
+ yield buf;
243
+ nextChunkIdx++;
244
+ if (buf.length < chunkSize) {
245
+ return;
246
+ }
214
247
  }
215
248
  }
249
+ } finally {
250
+ if (stallTimer) timers.clearTimeout(stallTimer);
216
251
  }
217
252
  }
218
253
  export {
@@ -283,7 +283,7 @@ async function _normalizeInputMedia(client, media, params = {}, uploadMedia = fa
283
283
  fileReference: res.photo.fileReference
284
284
  },
285
285
  ttlSeconds: media.ttlSeconds,
286
- spoiler: media.type === "video" && media.spoiler,
286
+ spoiler: "spoiler" in media ? media.spoiler : false,
287
287
  livePhoto: Boolean(livePhotoVideo),
288
288
  video: livePhotoVideo
289
289
  };
@@ -276,7 +276,7 @@ async function _normalizeInputMedia(client, media, params = {}, uploadMedia = fa
276
276
  fileReference: res.photo.fileReference
277
277
  },
278
278
  ttlSeconds: media.ttlSeconds,
279
- spoiler: media.type === "video" && media.spoiler,
279
+ spoiler: "spoiler" in media ? media.spoiler : false,
280
280
  livePhoto: Boolean(livePhotoVideo),
281
281
  video: livePhotoVideo
282
282
  };
@@ -174,7 +174,12 @@ async function uploadFile(client, params) {
174
174
  filePart: thisIdx,
175
175
  bytes: part
176
176
  };
177
- const result = await client.call(request, { kind: connectionKind, abortSignal });
177
+ const result = await client.call(request, {
178
+ kind: connectionKind,
179
+ maxRetryCount: Infinity,
180
+ floodSleepThreshold: Infinity,
181
+ abortSignal
182
+ });
178
183
  if (!result) throw new Error(`Failed to upload part ${idx}`);
179
184
  if (abortSignal?.aborted) return;
180
185
  pos += part.length;
@@ -167,7 +167,12 @@ async function uploadFile(client, params) {
167
167
  filePart: thisIdx,
168
168
  bytes: part
169
169
  };
170
- const result = await client.call(request, { kind: connectionKind, abortSignal });
170
+ const result = await client.call(request, {
171
+ kind: connectionKind,
172
+ maxRetryCount: Infinity,
173
+ floodSleepThreshold: Infinity,
174
+ abortSignal
175
+ });
171
176
  if (!result) throw new Error(`Failed to upload part ${idx}`);
172
177
  if (abortSignal?.aborted) return;
173
178
  pos += part.length;
@@ -8,7 +8,10 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
8
  const utils = require("@fuman/utils");
9
9
  const errors = require("../../../types/errors.cjs");
10
10
  const typeAssertions = require("../../../utils/type-assertions.cjs");
11
+ const peerUtils = require("../../utils/peer-utils.cjs");
12
+ const walkPageBlocks = require("../../utils/walk-page-blocks.cjs");
11
13
  const normalizeInputMedia = require("../files/normalize-input-media.cjs");
14
+ const resolvePeerMany = require("../users/resolve-peer-many.cjs");
12
15
  function _richMediaCacheKey(media) {
13
16
  if (typeof media === "string") return `auto:${media}`;
14
17
  if ("_" in media) return void 0;
@@ -103,6 +106,13 @@ async function _normalizeInputRichMessage(client, peer, input, params) {
103
106
  }
104
107
  }
105
108
  const blocks = input.blocks.map(processBlock);
109
+ const mentionIds = /* @__PURE__ */ new Set();
110
+ walkPageBlocks.walkPageBlocks(blocks, {
111
+ richText: (text) => {
112
+ if (text._ === "textMentionName") mentionIds.add(text.userId);
113
+ }
114
+ });
115
+ const users = mentionIds.size ? await resolvePeerMany.resolvePeerMany(client, [...mentionIds], peerUtils.toInputUser) : void 0;
106
116
  const uploaded = await utils.parallelMap(
107
117
  fileBlocks,
108
118
  async ({ block, media }, idx) => {
@@ -151,7 +161,8 @@ async function _normalizeInputRichMessage(client, peer, input, params) {
151
161
  noautolink: input.skipEntityDetection,
152
162
  blocks,
153
163
  photos: photos.length ? photos : void 0,
154
- documents: documents.length ? documents : void 0
164
+ documents: documents.length ? documents : void 0,
165
+ users: users?.length ? users : void 0
155
166
  };
156
167
  }
157
168
  exports._normalizeInputRichMessage = _normalizeInputRichMessage;
@@ -1,7 +1,10 @@
1
1
  import { parallelMap } from "@fuman/utils";
2
2
  import { MtArgumentError, MtTypeAssertionError } from "../../../types/errors.js";
3
3
  import { assertTypeIs } from "../../../utils/type-assertions.js";
4
+ import { toInputUser } from "../../utils/peer-utils.js";
5
+ import { walkPageBlocks } from "../../utils/walk-page-blocks.js";
4
6
  import { _normalizeInputMedia } from "../files/normalize-input-media.js";
7
+ import { resolvePeerMany } from "../users/resolve-peer-many.js";
5
8
  function _richMediaCacheKey(media) {
6
9
  if (typeof media === "string") return `auto:${media}`;
7
10
  if ("_" in media) return void 0;
@@ -96,6 +99,13 @@ async function _normalizeInputRichMessage(client, peer, input, params) {
96
99
  }
97
100
  }
98
101
  const blocks = input.blocks.map(processBlock);
102
+ const mentionIds = /* @__PURE__ */ new Set();
103
+ walkPageBlocks(blocks, {
104
+ richText: (text) => {
105
+ if (text._ === "textMentionName") mentionIds.add(text.userId);
106
+ }
107
+ });
108
+ const users = mentionIds.size ? await resolvePeerMany(client, [...mentionIds], toInputUser) : void 0;
99
109
  const uploaded = await parallelMap(
100
110
  fileBlocks,
101
111
  async ({ block, media }, idx) => {
@@ -144,7 +154,8 @@ async function _normalizeInputRichMessage(client, peer, input, params) {
144
154
  noautolink: input.skipEntityDetection,
145
155
  blocks,
146
156
  photos: photos.length ? photos : void 0,
147
- documents: documents.length ? documents : void 0
157
+ documents: documents.length ? documents : void 0,
158
+ users: users?.length ? users : void 0
148
159
  };
149
160
  }
150
161
  export {
@@ -48,11 +48,12 @@ async function _processCommonSendParameters(client, chatId, params) {
48
48
  throw new errors.MtArgumentError("replyTo/commentTo and replyToStory cannot be used together");
49
49
  }
50
50
  let tlReplyTo;
51
- if (replyTo) {
51
+ if (replyTo || params.threadId) {
52
52
  tlReplyTo = {
53
53
  _: "inputReplyToMessage",
54
- replyToMsgId: replyTo,
54
+ replyToMsgId: replyTo ?? params.threadId,
55
55
  replyToPeerId: replyToPeer,
56
+ topMsgId: params.threadId,
56
57
  quoteText: params.quote?.text,
57
58
  quoteEntities: params.quote?.entities,
58
59
  quoteOffset: params.quoteOffset,
@@ -7,11 +7,14 @@ export interface CommonSendParams {
7
7
  /**
8
8
  * Message to reply to. Either a message object or message ID.
9
9
  *
10
- * For forums - can also be an ID of the topic (i.e. its top message ID)
10
+ * For forums - can also be an ID of the topic (i.e. its top message ID),
11
+ * but prefer using {@link threadId} for that instead.
11
12
  *
12
13
  * Can also be a message from another chat, in which case a quote will be sent.
13
14
  */
14
15
  replyTo?: number | Message;
16
+ /** ID of the thread to reply to */
17
+ threadId?: number;
15
18
  /**
16
19
  * If this is a reply to a specific todo item, ID of that item
17
20
  */
@@ -7,11 +7,14 @@ export interface CommonSendParams {
7
7
  /**
8
8
  * Message to reply to. Either a message object or message ID.
9
9
  *
10
- * For forums - can also be an ID of the topic (i.e. its top message ID)
10
+ * For forums - can also be an ID of the topic (i.e. its top message ID),
11
+ * but prefer using {@link threadId} for that instead.
11
12
  *
12
13
  * Can also be a message from another chat, in which case a quote will be sent.
13
14
  */
14
15
  replyTo?: number | Message;
16
+ /** ID of the thread to reply to */
17
+ threadId?: number;
15
18
  /**
16
19
  * If this is a reply to a specific todo item, ID of that item
17
20
  */
@@ -41,11 +41,12 @@ async function _processCommonSendParameters(client, chatId, params) {
41
41
  throw new MtArgumentError("replyTo/commentTo and replyToStory cannot be used together");
42
42
  }
43
43
  let tlReplyTo;
44
- if (replyTo) {
44
+ if (replyTo || params.threadId) {
45
45
  tlReplyTo = {
46
46
  _: "inputReplyToMessage",
47
- replyToMsgId: replyTo,
47
+ replyToMsgId: replyTo ?? params.threadId,
48
48
  replyToPeerId: replyToPeer,
49
+ topMsgId: params.threadId,
49
50
  quoteText: params.quote?.text,
50
51
  quoteEntities: params.quote?.entities,
51
52
  quoteOffset: params.quoteOffset,
@@ -93,4 +93,13 @@ export interface FileDownloadParameters {
93
93
  * Abort signal that can be used to cancel the download.
94
94
  */
95
95
  abortSignal?: AbortSignal;
96
+ /**
97
+ * If set, the download will be aborted (and an `MtTimeoutError` will be thrown)
98
+ * if no new chunk has been received within this amount of time (in milliseconds).
99
+ *
100
+ * Useful for bulk downloads where some files might be unavailable server-side
101
+ * (and would otherwise cause the download to hang indefinitely due to the server
102
+ * repeatedly responding with `FLOOD_WAIT` or `-503 Timeout`).
103
+ */
104
+ stallTimeout?: number;
96
105
  }
@@ -93,4 +93,13 @@ export interface FileDownloadParameters {
93
93
  * Abort signal that can be used to cancel the download.
94
94
  */
95
95
  abortSignal?: AbortSignal;
96
+ /**
97
+ * If set, the download will be aborted (and an `MtTimeoutError` will be thrown)
98
+ * if no new chunk has been received within this amount of time (in milliseconds).
99
+ *
100
+ * Useful for bulk downloads where some files might be unavailable server-side
101
+ * (and would otherwise cause the download to hang indefinitely due to the server
102
+ * repeatedly responding with `FLOOD_WAIT` or `-503 Timeout`).
103
+ */
104
+ stallTimeout?: number;
96
105
  }