itd-api 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -12
- package/dist/events/index.cjs +53 -0
- package/dist/events/index.d.cts +3 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.js +5 -0
- package/dist/index.cjs +215 -194
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -63
- package/dist/index.d.ts +66 -63
- package/dist/index.js +197 -179
- package/dist/index.js.map +1 -1
- package/dist/node/index.cjs +4 -4
- package/dist/node/index.d.cts +1 -1
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +4 -4
- package/dist/rest/index.cjs +20 -26
- package/dist/rest/index.cjs.map +1 -1
- package/dist/rest/index.d.cts +7 -7
- package/dist/rest/index.d.ts +7 -7
- package/dist/rest/index.js +14 -20
- package/dist/rest/index.js.map +1 -1
- package/dist/shared/{errors-DfU8M5eS.cjs → errors-BmP3TKoW.cjs} +3 -3
- package/dist/shared/{errors-DfU8M5eS.cjs.map → errors-BmP3TKoW.cjs.map} +1 -1
- package/dist/shared/{errors-Bhrd2fJd.js → errors-GI10kZxk.js} +3 -3
- package/dist/shared/{errors-Bhrd2fJd.js.map → errors-GI10kZxk.js.map} +1 -1
- package/dist/shared/events-Br2r7coi.d.cts +698 -0
- package/dist/shared/events-QyhQZrnC.d.ts +698 -0
- package/dist/shared/{websocket-BLR8eVJV.js → events-TfhKh26b.js} +770 -490
- package/dist/shared/events-TfhKh26b.js.map +1 -0
- package/dist/shared/{websocket-C_eI4H2o.cjs → events-iTteVsd_.cjs} +834 -518
- package/dist/shared/events-iTteVsd_.cjs.map +1 -0
- package/dist/shared/{multi-storage-CkvTUC5m.js → multi-storage-1AEZTczk.js} +4 -4
- package/dist/shared/{multi-storage-CkvTUC5m.js.map → multi-storage-1AEZTczk.js.map} +1 -1
- package/dist/shared/{multi-storage--yTEqiod.cjs → multi-storage-CCUgkqvI.cjs} +6 -6
- package/dist/shared/{multi-storage--yTEqiod.cjs.map → multi-storage-CCUgkqvI.cjs.map} +1 -1
- package/dist/shared/{options-DtATYdLr.js → options-BNP633A5.js} +8 -6
- package/dist/shared/options-BNP633A5.js.map +1 -0
- package/dist/shared/{options-Dg5N3r1V.cjs → options-BUfoHE9G.cjs} +8 -6
- package/dist/shared/options-BUfoHE9G.cjs.map +1 -0
- package/dist/shared/{cookies-DZwFq6kr.cjs → redact-DB9EMGJx.cjs} +301 -2
- package/dist/shared/redact-DB9EMGJx.cjs.map +1 -0
- package/dist/shared/{cookies-tX2sNwxb.js → redact-FNK29Fv-.js} +224 -3
- package/dist/shared/redact-FNK29Fv-.js.map +1 -0
- package/dist/shared/{render-DO0F5YSm.d.cts → render-BszcKF7S.d.cts} +430 -313
- package/dist/shared/{render-C6HRPs10.js → render-CiL1VS0b.js} +925 -582
- package/dist/shared/render-CiL1VS0b.js.map +1 -0
- package/dist/shared/{render-mcuELiYi.cjs → render-DNTESBSl.cjs} +967 -618
- package/dist/shared/render-DNTESBSl.cjs.map +1 -0
- package/dist/shared/{render-CgwKdOzu.d.ts → render-DVB7T8pm.d.ts} +430 -313
- package/dist/shared/{storage-BPJR_k4-.cjs → storage-BLmXmzKF.cjs} +2 -2
- package/dist/shared/{storage-BPJR_k4-.cjs.map → storage-BLmXmzKF.cjs.map} +1 -1
- package/dist/shared/{storage-D86edNCB.js → storage-CCxn0Kkv.js} +2 -2
- package/dist/shared/{storage-D86edNCB.js.map → storage-CCxn0Kkv.js.map} +1 -1
- package/dist/shared/{url-IU0xN9wX.js → url-B3ocFlHj.js} +434 -408
- package/dist/shared/url-B3ocFlHj.js.map +1 -0
- package/dist/shared/{url-DTfZ2toq.d.cts → url-Be9jlWRv.d.cts} +1228 -1188
- package/dist/shared/{url-DTfZ2toq.d.ts → url-Be9jlWRv.d.ts} +1228 -1188
- package/dist/shared/{url-BaMCQpYH.cjs → url-CFN1sT9S.cjs} +497 -465
- package/dist/shared/url-CFN1sT9S.cjs.map +1 -0
- package/dist/web/index.cjs +2 -2
- package/dist/web/index.js +2 -2
- package/package.json +16 -15
- package/dist/realtime/index.cjs +0 -165
- package/dist/realtime/index.cjs.map +0 -1
- package/dist/realtime/index.d.cts +0 -51
- package/dist/realtime/index.d.ts +0 -51
- package/dist/realtime/index.js +0 -120
- package/dist/realtime/index.js.map +0 -1
- package/dist/shared/auth-provider-CG8oCQ9F.cjs +0 -108
- package/dist/shared/auth-provider-CG8oCQ9F.cjs.map +0 -1
- package/dist/shared/auth-provider-mYqxsSVa.js +0 -91
- package/dist/shared/auth-provider-mYqxsSVa.js.map +0 -1
- package/dist/shared/cookies-DZwFq6kr.cjs.map +0 -1
- package/dist/shared/cookies-tX2sNwxb.js.map +0 -1
- package/dist/shared/options-Dg5N3r1V.cjs.map +0 -1
- package/dist/shared/options-DtATYdLr.js.map +0 -1
- package/dist/shared/render-C6HRPs10.js.map +0 -1
- package/dist/shared/render-mcuELiYi.cjs.map +0 -1
- package/dist/shared/url-BaMCQpYH.cjs.map +0 -1
- package/dist/shared/url-IU0xN9wX.js.map +0 -1
- package/dist/shared/websocket-BLR8eVJV.js.map +0 -1
- package/dist/shared/websocket-C_eI4H2o.cjs.map +0 -1
- package/dist/shared/websocket-DF7XIMiX.d.cts +0 -562
- package/dist/shared/websocket-DYKBr8HF.d.ts +0 -562
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const require_url = require("./url-
|
|
2
|
-
const require_errors = require("./errors-
|
|
3
|
-
const
|
|
4
|
-
const require_options = require("./options-
|
|
1
|
+
const require_url = require("./url-CFN1sT9S.cjs");
|
|
2
|
+
const require_errors = require("./errors-BmP3TKoW.cjs");
|
|
3
|
+
const require_redact = require("./redact-DB9EMGJx.cjs");
|
|
4
|
+
const require_options = require("./options-BUfoHE9G.cjs");
|
|
5
5
|
//#region src/core/feature-catalog.ts
|
|
6
6
|
/**
|
|
7
7
|
* Изменяемый слой над каталогом конкретного API.
|
|
@@ -32,6 +32,9 @@ var ExtensibleOperationCatalog = class {
|
|
|
32
32
|
get defaultBucket() {
|
|
33
33
|
return this.#base.defaultBucket;
|
|
34
34
|
}
|
|
35
|
+
definitionOf(id) {
|
|
36
|
+
return this.#operations.get(id) ?? this.#base.definitionOf(id);
|
|
37
|
+
}
|
|
35
38
|
retrySafetyOf(id) {
|
|
36
39
|
return this.#operations.get(id)?.retrySafety ?? this.#base.retrySafetyOf(id);
|
|
37
40
|
}
|
|
@@ -49,7 +52,10 @@ var ExtensibleOperationCatalog = class {
|
|
|
49
52
|
if (this.#base.isKnownBucket(name) || this.#bucketOwners.has(name)) throw new require_errors.ItdConfigError(`feature «${owner}»: бакет «${name}» уже зарегистрирован`);
|
|
50
53
|
this.#bucketOwners.set(name, owner);
|
|
51
54
|
if (definition.limit !== void 0) this.#bucketLimits[name] = definition.limit;
|
|
52
|
-
if (definition.concurrency !== void 0) this.#bucketOverrides[name] = Object.freeze({
|
|
55
|
+
if (definition.concurrency !== void 0 || definition.rps !== void 0) this.#bucketOverrides[name] = Object.freeze({
|
|
56
|
+
...definition.concurrency === void 0 ? {} : { concurrency: definition.concurrency },
|
|
57
|
+
...definition.rps === void 0 ? {} : { rps: definition.rps }
|
|
58
|
+
});
|
|
53
59
|
return () => {
|
|
54
60
|
if (this.#bucketOwners.get(name) !== owner) return;
|
|
55
61
|
this.#bucketOwners.delete(name);
|
|
@@ -74,22 +80,311 @@ var ExtensibleOperationCatalog = class {
|
|
|
74
80
|
}
|
|
75
81
|
};
|
|
76
82
|
//#endregion
|
|
83
|
+
//#region src/core/attachments/url-source.ts
|
|
84
|
+
/** Убирает параметры MIME и приводит его к форме для сравнения. */
|
|
85
|
+
function normalizeMimeType(contentType) {
|
|
86
|
+
return contentType?.split(";", 1)[0]?.trim().toLowerCase() || void 0;
|
|
87
|
+
}
|
|
88
|
+
/** Достаёт имя файла из пути URL. */
|
|
89
|
+
function filenameFromUrl(url) {
|
|
90
|
+
const last = url.pathname.split("/").pop();
|
|
91
|
+
if (!last) return void 0;
|
|
92
|
+
try {
|
|
93
|
+
return decodeURIComponent(last) || void 0;
|
|
94
|
+
} catch {
|
|
95
|
+
return last;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Проверяет объявленный размер и возвращает его, если заголовок корректен. */
|
|
99
|
+
function declaredSize(response, maxBytes, url) {
|
|
100
|
+
const header = response.headers.get("content-length");
|
|
101
|
+
if (header === null) return void 0;
|
|
102
|
+
const size = Number(header);
|
|
103
|
+
if (!Number.isFinite(size) || size < 0 || !Number.isInteger(size)) return void 0;
|
|
104
|
+
if (maxBytes !== void 0 && size > maxBytes) throw require_options.fileTooLarge(url, maxBytes, size);
|
|
105
|
+
return size;
|
|
106
|
+
}
|
|
107
|
+
/** Получает HTTP-ответ источника и проверяет его статус. */
|
|
108
|
+
async function fetchFile(target, options, context) {
|
|
109
|
+
let requested;
|
|
110
|
+
try {
|
|
111
|
+
requested = new URL(target);
|
|
112
|
+
} catch {
|
|
113
|
+
throw new require_errors.ItdConfigError(`«${require_redact.redactUrl(target)}» не разбирается как адрес`);
|
|
114
|
+
}
|
|
115
|
+
if (requested.protocol !== "http:" && requested.protocol !== "https:") throw new require_errors.ItdConfigError(`вложение по адресу поддерживает только http и https, получено: ${requested.protocol}`);
|
|
116
|
+
let response;
|
|
117
|
+
try {
|
|
118
|
+
response = await context.fetch(requested, { ...context.signal ? { signal: context.signal } : {} });
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (context.signal?.aborted || error instanceof Error && error.name === "AbortError") throw error;
|
|
121
|
+
const safeUrl = require_redact.redactUrl(requested.href);
|
|
122
|
+
throw new require_errors.ItdFileError(`не удалось получить файл по адресу ${safeUrl}`, {
|
|
123
|
+
reason: require_errors.ItdFileErrorReason.Network,
|
|
124
|
+
url: safeUrl,
|
|
125
|
+
retryable: true,
|
|
126
|
+
cause: require_redact.redactErrorCause(error)
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const finalUrl = response.url ? new URL(response.url) : requested;
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
await response.body?.cancel().catch(() => {});
|
|
132
|
+
const safeUrl = require_redact.redactUrl(finalUrl.href);
|
|
133
|
+
throw new require_errors.ItdFileError(`источник ${safeUrl} ответил статусом ${response.status}`, {
|
|
134
|
+
reason: require_errors.ItdFileErrorReason.Http,
|
|
135
|
+
url: safeUrl,
|
|
136
|
+
status: response.status,
|
|
137
|
+
retryable: response.status === 408 || response.status === 429 || response.status >= 500
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const { maxBytes } = require_options.resolveFileStreamOptions(options, require_options.DEFAULT_URL_FILE_MAX_BYTES);
|
|
141
|
+
let size;
|
|
142
|
+
try {
|
|
143
|
+
size = declaredSize(response, maxBytes, finalUrl.href);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
await response.body?.cancel().catch(() => {});
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
response,
|
|
150
|
+
url: finalUrl,
|
|
151
|
+
size
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Читает ответ с контролем размера до создания итогового `Blob`. */
|
|
155
|
+
async function responseBlob(response, url, maxBytes, streamBufferBytes, signal) {
|
|
156
|
+
if (!response.body) {
|
|
157
|
+
const blob = await response.blob();
|
|
158
|
+
if (maxBytes !== void 0 && blob.size > maxBytes) throw require_options.fileTooLarge(url, maxBytes, blob.size);
|
|
159
|
+
return blob;
|
|
160
|
+
}
|
|
161
|
+
const chunks = [];
|
|
162
|
+
const reader = require_options.boundedFileStream(response.body, {
|
|
163
|
+
...maxBytes !== void 0 ? { maxBytes } : {},
|
|
164
|
+
streamBufferBytes,
|
|
165
|
+
...signal ? { signal } : {},
|
|
166
|
+
url,
|
|
167
|
+
retryableRead: true
|
|
168
|
+
}).getReader();
|
|
169
|
+
try {
|
|
170
|
+
for (;;) {
|
|
171
|
+
const next = await reader.read();
|
|
172
|
+
if (next.done) break;
|
|
173
|
+
chunks.push(next.value);
|
|
174
|
+
}
|
|
175
|
+
} finally {
|
|
176
|
+
reader.releaseLock();
|
|
177
|
+
}
|
|
178
|
+
return new Blob(chunks.map((chunk) => Uint8Array.from(chunk).buffer));
|
|
179
|
+
}
|
|
180
|
+
/** Скачивает файл целиком с ограничением размера. @internal */
|
|
181
|
+
async function downloadFile(target, options, context) {
|
|
182
|
+
const resolved = require_options.resolveFileStreamOptions(options, require_options.DEFAULT_URL_FILE_MAX_BYTES);
|
|
183
|
+
const { response, url } = await fetchFile(target, options, context);
|
|
184
|
+
const blob = await responseBlob(response, url.href, resolved.maxBytes, resolved.streamBufferBytes, context.signal);
|
|
185
|
+
const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
|
|
186
|
+
const filename = options.filename ?? filenameFromUrl(url);
|
|
187
|
+
return {
|
|
188
|
+
file: new Blob([blob], { type: contentType ?? "" }),
|
|
189
|
+
...filename ? { filename } : {},
|
|
190
|
+
...contentType ? { contentType } : {}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/** Открывает HTTP-ответ как ограниченный поток. @internal */
|
|
194
|
+
async function openUrlFile(target, options, context) {
|
|
195
|
+
const resolved = require_options.resolveFileStreamOptions(options, require_options.DEFAULT_URL_FILE_MAX_BYTES);
|
|
196
|
+
const { response, url, size } = await fetchFile(target, options, context);
|
|
197
|
+
if (!response.body) {
|
|
198
|
+
const safeUrl = require_redact.redactUrl(url.href);
|
|
199
|
+
throw new require_errors.ItdFileError(`источник ${safeUrl} не предоставил потоковое тело`, {
|
|
200
|
+
reason: require_errors.ItdFileErrorReason.StreamUnavailable,
|
|
201
|
+
url: safeUrl
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const stream = require_options.boundedFileStream(response.body, {
|
|
205
|
+
...resolved.maxBytes !== void 0 ? { maxBytes: resolved.maxBytes } : {},
|
|
206
|
+
streamBufferBytes: resolved.streamBufferBytes,
|
|
207
|
+
...context.signal ? { signal: context.signal } : {},
|
|
208
|
+
url: url.href,
|
|
209
|
+
retryableRead: true
|
|
210
|
+
});
|
|
211
|
+
const filename = options.filename ?? filenameFromUrl(url);
|
|
212
|
+
const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
|
|
213
|
+
return {
|
|
214
|
+
stream,
|
|
215
|
+
...filename ? { filename } : {},
|
|
216
|
+
...contentType ? { contentType } : {},
|
|
217
|
+
...size !== void 0 ? { size } : {},
|
|
218
|
+
close: () => stream.cancel().catch(() => {})
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/core/attachments/resolver.ts
|
|
223
|
+
/** Возвращает функцию освобождения, которая исполняет действие не более одного раза. */
|
|
224
|
+
function closeOnce(action) {
|
|
225
|
+
let closing;
|
|
226
|
+
return () => closing ??= Promise.resolve().then(action);
|
|
227
|
+
}
|
|
228
|
+
/** Добавляет поле только для непустого значения, не выдумывая имя или MIME. */
|
|
229
|
+
function optionalText(name, value) {
|
|
230
|
+
const normalized = value?.trim();
|
|
231
|
+
return normalized ? { [name]: normalized } : {};
|
|
232
|
+
}
|
|
233
|
+
/** Единая реализация распознавания и открытия `FileInput`. @internal */
|
|
234
|
+
var DefaultFileResolver = class {
|
|
235
|
+
#fetch;
|
|
236
|
+
constructor(fetchImplementation) {
|
|
237
|
+
this.#fetch = fetchImplementation;
|
|
238
|
+
}
|
|
239
|
+
mode(input) {
|
|
240
|
+
if (typeof input !== "object" || input === null) return require_options.FileTransferMode.Buffer;
|
|
241
|
+
if ("open" in input) return require_options.FileTransferMode.Stream;
|
|
242
|
+
if ("url" in input) return require_options.resolveFileStreamOptions(input).mode;
|
|
243
|
+
return require_options.FileTransferMode.Buffer;
|
|
244
|
+
}
|
|
245
|
+
async resolve(input, options = {}, context = {}) {
|
|
246
|
+
const attempt = context.attempt ?? 1;
|
|
247
|
+
if (!Number.isInteger(attempt) || attempt < 1) throw new require_errors.ItdConfigError(`attempt должен быть целым числом от 1, получено: ${attempt}`);
|
|
248
|
+
const maxBytes = require_options.optionalBytes(options.maxBytes, "maxBytes");
|
|
249
|
+
if (context.signal?.aborted) throw context.signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
250
|
+
const fileContext = {
|
|
251
|
+
fetch: this.#fetch,
|
|
252
|
+
attempt,
|
|
253
|
+
...context.signal ? { signal: context.signal } : {}
|
|
254
|
+
};
|
|
255
|
+
try {
|
|
256
|
+
const source = this.mode(input) === require_options.FileTransferMode.Stream ? await this.#openStream(input, options, fileContext, maxBytes) : await this.#loadBuffer(input, options, fileContext, maxBytes);
|
|
257
|
+
if (context.signal?.aborted) {
|
|
258
|
+
if (source.mode === require_options.FileTransferMode.Stream) await source.close();
|
|
259
|
+
throw context.signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
260
|
+
}
|
|
261
|
+
return source;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (error instanceof require_errors.ItdError || context.signal?.aborted) throw error;
|
|
264
|
+
throw new require_errors.ItdFileError("не удалось получить содержимое вложения", {
|
|
265
|
+
reason: require_errors.ItdFileErrorReason.Read,
|
|
266
|
+
retryable: true,
|
|
267
|
+
cause: require_redact.redactErrorCause(error)
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async #loadBuffer(input, options, context, maxBytes) {
|
|
272
|
+
const content = await this.#resolveBuffer(input, context);
|
|
273
|
+
if (typeof content !== "object" || content === null || !("file" in content)) throw new require_errors.ItdConfigError("буферный источник должен вернуть объект с полем file");
|
|
274
|
+
if (!require_redact.isBlob(content.file) && !(content.file instanceof ArrayBuffer) && !ArrayBuffer.isView(content.file)) throw new require_errors.ItdConfigError("поле file должно содержать Blob, ArrayBuffer или Uint8Array");
|
|
275
|
+
const contentType = normalizeMimeType(options.contentType ?? content.contentType) ?? normalizeMimeType(require_redact.isBlob(content.file) ? content.file.type : void 0);
|
|
276
|
+
const blob = require_redact.isBlob(content.file) && (!contentType || content.file.type === contentType) ? content.file : new Blob([content.file], { type: contentType ?? "" });
|
|
277
|
+
if (maxBytes !== void 0 && blob.size > maxBytes) throw require_options.fileTooLarge(void 0, maxBytes, blob.size);
|
|
278
|
+
return {
|
|
279
|
+
mode: require_options.FileTransferMode.Buffer,
|
|
280
|
+
blob,
|
|
281
|
+
size: blob.size,
|
|
282
|
+
...optionalText("filename", options.filename ?? content.filename),
|
|
283
|
+
contentType
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
async #openStream(input, options, context, maxBytes) {
|
|
287
|
+
const opened = await this.#resolveStream(input, context);
|
|
288
|
+
if (!opened || !require_options.isReadableByteStream(opened.stream)) {
|
|
289
|
+
await opened?.close?.();
|
|
290
|
+
throw new require_errors.ItdConfigError("потоковый источник должен вернуть { stream: ReadableStream }");
|
|
291
|
+
}
|
|
292
|
+
let handedOff = false;
|
|
293
|
+
const closeOpened = closeOnce(async () => {
|
|
294
|
+
await opened.close?.();
|
|
295
|
+
});
|
|
296
|
+
try {
|
|
297
|
+
require_options.optionalBytes(opened.size, "size");
|
|
298
|
+
if (maxBytes !== void 0 && opened.size !== void 0 && opened.size > maxBytes) throw require_options.fileTooLarge(void 0, maxBytes, opened.size);
|
|
299
|
+
if (options.requireKnownSize === true && opened.size === void 0) throw new require_errors.ItdConfigError("размер потокового вложения должен быть известен до чтения");
|
|
300
|
+
const limits = require_options.resolveFileStreamOptions({
|
|
301
|
+
mode: require_options.FileTransferMode.Stream,
|
|
302
|
+
...maxBytes !== void 0 ? { maxBytes } : {},
|
|
303
|
+
...options.streamBufferBytes !== void 0 ? { streamBufferBytes: options.streamBufferBytes } : {}
|
|
304
|
+
});
|
|
305
|
+
const stream = require_options.isBoundedFileStream(opened.stream) && options.maxBytes === void 0 && options.streamBufferBytes === void 0 ? opened.stream : require_options.boundedFileStream(opened.stream, {
|
|
306
|
+
...limits.maxBytes !== void 0 ? { maxBytes: limits.maxBytes } : {},
|
|
307
|
+
streamBufferBytes: limits.streamBufferBytes,
|
|
308
|
+
...context.signal ? { signal: context.signal } : {}
|
|
309
|
+
});
|
|
310
|
+
let detachAbort;
|
|
311
|
+
const close = closeOnce(async () => {
|
|
312
|
+
detachAbort?.();
|
|
313
|
+
detachAbort = void 0;
|
|
314
|
+
await stream.cancel().catch(() => {});
|
|
315
|
+
await closeOpened();
|
|
316
|
+
});
|
|
317
|
+
if (context.signal) {
|
|
318
|
+
const onAbort = () => {
|
|
319
|
+
close().catch(() => {});
|
|
320
|
+
};
|
|
321
|
+
if (context.signal.aborted) {
|
|
322
|
+
await close();
|
|
323
|
+
throw context.signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
324
|
+
} else {
|
|
325
|
+
context.signal.addEventListener("abort", onAbort, { once: true });
|
|
326
|
+
detachAbort = () => context.signal?.removeEventListener("abort", onAbort);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
handedOff = true;
|
|
330
|
+
return {
|
|
331
|
+
mode: require_options.FileTransferMode.Stream,
|
|
332
|
+
stream,
|
|
333
|
+
size: opened.size,
|
|
334
|
+
...optionalText("filename", options.filename ?? opened.filename),
|
|
335
|
+
contentType: normalizeMimeType(options.contentType ?? opened.contentType),
|
|
336
|
+
close
|
|
337
|
+
};
|
|
338
|
+
} finally {
|
|
339
|
+
if (!handedOff) await closeOpened();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
#resolveBuffer(input, context) {
|
|
343
|
+
if (input instanceof ArrayBuffer || ArrayBuffer.isView(input) || require_redact.isBlob(input)) return {
|
|
344
|
+
file: input,
|
|
345
|
+
...require_redact.isFile(input) ? { filename: input.name } : {}
|
|
346
|
+
};
|
|
347
|
+
if (typeof input !== "object" || input === null) throw new require_errors.ItdConfigError(`вложение задано значением типа ${typeof input}; ожидается бинарное значение или объект { file }, { url }, { load }, { open }. Для файла на диске используйте fromPath('./photo.jpg') из itd-api/node`);
|
|
348
|
+
if ("load" in input) return input.load(context);
|
|
349
|
+
if ("url" in input) {
|
|
350
|
+
const { url, ...urlOptions } = input;
|
|
351
|
+
return downloadFile(url, urlOptions, context);
|
|
352
|
+
}
|
|
353
|
+
if ("file" in input) return input;
|
|
354
|
+
if ("open" in input) throw new require_errors.ItdConfigError("потоковый источник нельзя использовать как буферный");
|
|
355
|
+
throw new require_errors.ItdConfigError("вложение не распознано: ожидается { file }, { url }, { load } или { open }");
|
|
356
|
+
}
|
|
357
|
+
#resolveStream(input, context) {
|
|
358
|
+
if (typeof input === "object" && input !== null && "open" in input) return input.open(context);
|
|
359
|
+
if (typeof input === "object" && input !== null && "url" in input) {
|
|
360
|
+
const { url, ...urlOptions } = input;
|
|
361
|
+
return openUrlFile(url, urlOptions, context);
|
|
362
|
+
}
|
|
363
|
+
throw new require_errors.ItdConfigError("потоковое вложение должно иметь форму { open } или { url, mode }");
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
/** Создаёт механизм подготовки файлов, привязанный к настроенному `fetch` клиента. @internal */
|
|
367
|
+
function createFileResolver(fetchImplementation) {
|
|
368
|
+
return new DefaultFileResolver(fetchImplementation);
|
|
369
|
+
}
|
|
370
|
+
//#endregion
|
|
77
371
|
//#region src/core/features.ts
|
|
78
372
|
function requireName(value, kind) {
|
|
79
373
|
if (typeof value !== "string" || value.trim() === "") throw new require_errors.ItdConfigError(`${kind} должен иметь непустое имя`);
|
|
80
374
|
return value.trim();
|
|
81
375
|
}
|
|
82
|
-
function
|
|
83
|
-
const name = requireName(value,
|
|
84
|
-
if (name
|
|
376
|
+
function requireFeatureName(value) {
|
|
377
|
+
const name = requireName(value, "Feature");
|
|
378
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new require_errors.ItdConfigError(`имя feature «${name}» должно соответствовать [a-z][a-z0-9-]*`);
|
|
85
379
|
return name;
|
|
86
380
|
}
|
|
87
381
|
function validateBucket(owner, localName, bucket) {
|
|
88
382
|
if (typeof bucket !== "object" || bucket === null || Array.isArray(bucket)) throw new require_errors.ItdConfigError(`feature «${owner}»: бакет «${localName}» должен быть объектом`);
|
|
89
383
|
if (bucket.limit !== void 0 && (!Number.isFinite(bucket.limit) || bucket.limit <= 0)) throw new require_errors.ItdConfigError(`feature «${owner}»: limit бакета «${localName}» должен быть положительным числом`);
|
|
90
384
|
if (bucket.concurrency !== void 0 && (!Number.isInteger(bucket.concurrency) || bucket.concurrency < 1)) throw new require_errors.ItdConfigError(`feature «${owner}»: concurrency бакета «${localName}» должен быть целым числом от 1`);
|
|
385
|
+
if (bucket.rps !== void 0 && (!Number.isFinite(bucket.rps) || bucket.rps <= 0)) throw new require_errors.ItdConfigError(`feature «${owner}»: rps бакета «${localName}» должен быть положительным числом`);
|
|
91
386
|
}
|
|
92
|
-
/** Реестр
|
|
387
|
+
/** Реестр подключаемых модулей клиента. @internal */
|
|
93
388
|
var FeatureRegistry = class {
|
|
94
389
|
#deps;
|
|
95
390
|
#features = /* @__PURE__ */ new Map();
|
|
@@ -102,7 +397,7 @@ var FeatureRegistry = class {
|
|
|
102
397
|
this.#deps.assertActive("установить feature");
|
|
103
398
|
if (this.#disposed) throw new require_errors.ItdStateError("Реестр feature уже освобождён");
|
|
104
399
|
if (typeof feature !== "object" || feature === null) throw new require_errors.ItdConfigError("feature должен быть объектом");
|
|
105
|
-
const name =
|
|
400
|
+
const name = requireFeatureName(feature.name);
|
|
106
401
|
if (this.#features.has(name)) throw new require_errors.ItdConfigError(`feature «${name}» уже установлен`);
|
|
107
402
|
if (typeof feature.setup !== "function") throw new require_errors.ItdConfigError(`feature «${name}» должен предоставлять setup()`);
|
|
108
403
|
if (typeof feature.operations !== "object" || feature.operations === null || Array.isArray(feature.operations)) throw new require_errors.ItdConfigError(`feature «${name}» должен объявлять operations`);
|
|
@@ -114,6 +409,11 @@ var FeatureRegistry = class {
|
|
|
114
409
|
const operations = /* @__PURE__ */ new Map();
|
|
115
410
|
const controller = new AbortController();
|
|
116
411
|
let committed = false;
|
|
412
|
+
const reportRollbackError = (message, error) => {
|
|
413
|
+
try {
|
|
414
|
+
this.#deps.logger?.error(message, error);
|
|
415
|
+
} catch {}
|
|
416
|
+
};
|
|
117
417
|
try {
|
|
118
418
|
for (const service of feature.services ?? []) {
|
|
119
419
|
const serviceName = requireName(service?.name, `Сервис feature «${name}»`);
|
|
@@ -147,7 +447,7 @@ var FeatureRegistry = class {
|
|
|
147
447
|
bucketNames.set(localName, globalName);
|
|
148
448
|
}
|
|
149
449
|
for (const [rawLocalName, definition] of Object.entries(feature.operations)) {
|
|
150
|
-
const localName =
|
|
450
|
+
const localName = requireName(rawLocalName, `Операция feature «${name}»`);
|
|
151
451
|
if (typeof definition !== "object" || definition === null) throw new require_errors.ItdConfigError(`feature «${name}»: операция «${localName}» должна быть объектом`);
|
|
152
452
|
if (![
|
|
153
453
|
"GET",
|
|
@@ -159,19 +459,27 @@ var FeatureRegistry = class {
|
|
|
159
459
|
if (!Object.values(require_url.RetrySafety).includes(definition.retrySafety)) throw new require_errors.ItdConfigError(`feature «${name}»: операция «${localName}» содержит неизвестную retrySafety`);
|
|
160
460
|
if (definition.service !== void 0 && typeof definition.service !== "string") throw new require_errors.ItdConfigError(`feature «${name}»: service операции «${localName}» должен быть строкой`);
|
|
161
461
|
if (definition.bucket !== void 0 && typeof definition.bucket !== "string") throw new require_errors.ItdConfigError(`feature «${name}»: bucket операции «${localName}» должен быть строкой`);
|
|
462
|
+
if (definition.read !== void 0 && typeof definition.read !== "function") throw new require_errors.ItdConfigError(`feature «${name}»: read операции «${localName}» должна быть функцией`);
|
|
463
|
+
if (definition.annotations !== void 0 && (typeof definition.annotations !== "object" || definition.annotations === null || Array.isArray(definition.annotations))) throw new require_errors.ItdConfigError(`feature «${name}»: annotations операции «${localName}» должны быть объектом`);
|
|
162
464
|
if (definition.service !== void 0 && !services.has(definition.service)) throw new require_errors.ItdConfigError(`feature «${name}»: операция «${localName}» ссылается на необъявленный сервис «${definition.service}»`);
|
|
163
465
|
const bucket = definition.bucket === void 0 ? this.#deps.catalog.defaultBucket : bucketNames.get(definition.bucket);
|
|
164
466
|
if (bucket === void 0) throw new require_errors.ItdConfigError(`feature «${name}»: операция «${localName}» ссылается на необъявленный бакет «${definition.bucket}»`);
|
|
165
467
|
const operationId = `${name}.${localName}`;
|
|
166
|
-
const
|
|
468
|
+
const contract = require_url.defineOperation(operationId, {
|
|
167
469
|
method: definition.method,
|
|
168
470
|
retrySafety: definition.retrySafety,
|
|
169
|
-
bucket
|
|
471
|
+
bucket,
|
|
472
|
+
...definition.annotations === void 0 ? {} : { annotations: definition.annotations }
|
|
473
|
+
}, definition.read ?? require_url.identityResult);
|
|
474
|
+
const unregister = this.#deps.catalog.registerOperation(name, operationId, {
|
|
475
|
+
method: contract.method,
|
|
476
|
+
retrySafety: contract.retrySafety,
|
|
477
|
+
bucket,
|
|
478
|
+
...contract.annotations === void 0 ? {} : { annotations: contract.annotations }
|
|
170
479
|
});
|
|
171
480
|
rollback.push(unregister);
|
|
172
481
|
operations.set(localName, {
|
|
173
|
-
|
|
174
|
-
method: definition.method,
|
|
482
|
+
contract,
|
|
175
483
|
service: definition.service
|
|
176
484
|
});
|
|
177
485
|
}
|
|
@@ -181,6 +489,15 @@ var FeatureRegistry = class {
|
|
|
181
489
|
signal: controller.signal,
|
|
182
490
|
clock: this.#deps.clock,
|
|
183
491
|
logger: this.#deps.logger,
|
|
492
|
+
files: Object.freeze({ resolve: (input, options, context) => {
|
|
493
|
+
try {
|
|
494
|
+
this.#deps.assertActive(`открыть файловый источник feature «${name}»`);
|
|
495
|
+
if (!committed || controller.signal.aborted) throw new require_errors.ItdStateError(`feature «${name}» не активен`);
|
|
496
|
+
return this.#deps.files.resolve(input, options, context);
|
|
497
|
+
} catch (error) {
|
|
498
|
+
return Promise.reject(error);
|
|
499
|
+
}
|
|
500
|
+
} }),
|
|
184
501
|
request: (operation, options) => {
|
|
185
502
|
try {
|
|
186
503
|
this.#deps.assertActive(`выполнить операцию feature «${name}»`);
|
|
@@ -194,10 +511,8 @@ var FeatureRegistry = class {
|
|
|
194
511
|
delete scoped.baseUrl;
|
|
195
512
|
delete scoped.retrySafety;
|
|
196
513
|
delete scoped.rateLimitBucket;
|
|
197
|
-
return this.#deps.http.
|
|
514
|
+
return this.#deps.http.execute(resolved.contract, {
|
|
198
515
|
...scoped,
|
|
199
|
-
operationId: resolved.operationId,
|
|
200
|
-
method: resolved.method,
|
|
201
516
|
...resolved.service === void 0 ? {} : { service: resolved.service }
|
|
202
517
|
});
|
|
203
518
|
} catch (error) {
|
|
@@ -207,6 +522,43 @@ var FeatureRegistry = class {
|
|
|
207
522
|
serviceBaseUrl: (serviceName) => {
|
|
208
523
|
if (!services.has(serviceName)) throw new require_errors.ItdConfigError(`feature «${name}» не объявляет сервис «${serviceName}»`);
|
|
209
524
|
return this.#deps.services.resolveBaseUrl(serviceName);
|
|
525
|
+
},
|
|
526
|
+
connection: (serviceName) => {
|
|
527
|
+
if (!services.has(serviceName)) throw new require_errors.ItdConfigError(`feature «${name}» не объявляет сервис «${serviceName}»`);
|
|
528
|
+
return this.#deps.connection(serviceName);
|
|
529
|
+
},
|
|
530
|
+
manage: (resource) => {
|
|
531
|
+
this.#deps.assertActive(`зарегистрировать ресурс feature «${name}»`);
|
|
532
|
+
if (controller.signal.aborted) throw new require_errors.ItdStateError(`feature «${name}» не активен`);
|
|
533
|
+
const unregister = this.#deps.manage(resource);
|
|
534
|
+
let registered = true;
|
|
535
|
+
const release = () => {
|
|
536
|
+
if (!registered) return;
|
|
537
|
+
registered = false;
|
|
538
|
+
unregister();
|
|
539
|
+
};
|
|
540
|
+
if (!committed) rollback.push(() => {
|
|
541
|
+
if (!registered) return;
|
|
542
|
+
if (committed) {
|
|
543
|
+
release();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
resource.stop();
|
|
548
|
+
} catch (error) {
|
|
549
|
+
reportRollbackError(`Не удалось остановить ресурс «${resource.kind}» при откате feature «${name}»`, error);
|
|
550
|
+
} finally {
|
|
551
|
+
release();
|
|
552
|
+
}
|
|
553
|
+
Promise.resolve().then(() => resource.drain()).catch((error) => {
|
|
554
|
+
reportRollbackError(`Не удалось освободить ресурс «${resource.kind}» при откате feature «${name}»`, error);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
return release;
|
|
558
|
+
},
|
|
559
|
+
assertActive: (action) => {
|
|
560
|
+
this.#deps.assertActive(action);
|
|
561
|
+
if (!committed || controller.signal.aborted) throw new require_errors.ItdStateError(`feature «${name}» не активен`);
|
|
210
562
|
}
|
|
211
563
|
});
|
|
212
564
|
const installation = feature.setup(context);
|
|
@@ -235,36 +587,166 @@ var FeatureRegistry = class {
|
|
|
235
587
|
return this.#features.has(name);
|
|
236
588
|
}
|
|
237
589
|
async close() {
|
|
238
|
-
const errors = [];
|
|
239
|
-
for (const feature of [...this.#features.values()].reverse()) try {
|
|
240
|
-
await feature.close?.();
|
|
241
|
-
} catch (error) {
|
|
242
|
-
errors.push(error);
|
|
243
|
-
}
|
|
590
|
+
const errors = (await Promise.allSettled([...this.#features.values()].reverse().map(async (feature) => feature.close?.()))).filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
244
591
|
if (errors.length > 0) throw new AggregateError(errors, "Не удалось закрыть feature");
|
|
245
592
|
}
|
|
246
593
|
async dispose() {
|
|
247
594
|
if (this.#disposed) return;
|
|
248
595
|
this.#disposed = true;
|
|
249
596
|
const errors = [];
|
|
250
|
-
|
|
597
|
+
const disposals = [];
|
|
598
|
+
const features = [...this.#features.values()].reverse();
|
|
599
|
+
for (const feature of features) {
|
|
251
600
|
feature.controller.abort(new require_errors.ItdAbortError(`Feature «${feature.name}» освобождён`));
|
|
252
601
|
try {
|
|
253
|
-
|
|
254
|
-
} catch (error) {
|
|
255
|
-
errors.push(error);
|
|
256
|
-
}
|
|
257
|
-
for (const cleanup of [...feature.cleanup].reverse()) try {
|
|
258
|
-
cleanup();
|
|
602
|
+
disposals.push(Promise.resolve(feature.dispose?.()).then(() => void 0));
|
|
259
603
|
} catch (error) {
|
|
260
604
|
errors.push(error);
|
|
261
605
|
}
|
|
262
606
|
}
|
|
607
|
+
for (const feature of features) for (const cleanup of [...feature.cleanup].reverse()) try {
|
|
608
|
+
cleanup();
|
|
609
|
+
} catch (error) {
|
|
610
|
+
errors.push(error);
|
|
611
|
+
}
|
|
263
612
|
this.#features.clear();
|
|
613
|
+
const results = await Promise.allSettled(disposals);
|
|
614
|
+
for (const result of results) if (result.status === "rejected") errors.push(result.reason);
|
|
264
615
|
if (errors.length > 0) throw new AggregateError(errors, "Не удалось освободить feature");
|
|
265
616
|
}
|
|
266
617
|
};
|
|
267
618
|
//#endregion
|
|
619
|
+
//#region src/core/managed-resources.ts
|
|
620
|
+
/** Реестр активных ресурсов клиента. @internal */
|
|
621
|
+
var ManagedResourceRegistry = class {
|
|
622
|
+
#resources = /* @__PURE__ */ new Set();
|
|
623
|
+
#disposed = false;
|
|
624
|
+
register(resource) {
|
|
625
|
+
if (this.#disposed) throw new require_errors.ItdStateError("Lifecycle клиента уже освобождён");
|
|
626
|
+
if (typeof resource !== "object" || resource === null || typeof resource.kind !== "string" || resource.kind.trim() === "" || typeof resource.stop !== "function" || typeof resource.drain !== "function") throw new require_errors.ItdConfigError("Managed resource должен предоставлять kind, stop() и drain()");
|
|
627
|
+
this.#resources.add(resource);
|
|
628
|
+
let registered = true;
|
|
629
|
+
return () => {
|
|
630
|
+
if (!registered) return;
|
|
631
|
+
registered = false;
|
|
632
|
+
this.#resources.delete(resource);
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
/** Останавливает snapshot синхронно, чтобы новые auth-данные ещё не были опубликованы. */
|
|
636
|
+
stopAll() {
|
|
637
|
+
const resources = [...this.#resources];
|
|
638
|
+
const errors = [];
|
|
639
|
+
for (const resource of resources) try {
|
|
640
|
+
resource.stop();
|
|
641
|
+
} catch (error) {
|
|
642
|
+
errors.push(error);
|
|
643
|
+
}
|
|
644
|
+
return {
|
|
645
|
+
resources,
|
|
646
|
+
errors
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
dispose() {
|
|
650
|
+
this.#disposed = true;
|
|
651
|
+
this.#resources.clear();
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region src/core/feature-host.ts
|
|
656
|
+
/**
|
|
657
|
+
* Управляет подключаемыми модулями и их ресурсами.
|
|
658
|
+
*
|
|
659
|
+
* @internal
|
|
660
|
+
*/
|
|
661
|
+
var ClientFeatureHost = class {
|
|
662
|
+
#runtime;
|
|
663
|
+
#features;
|
|
664
|
+
#resources = new ManagedResourceRegistry();
|
|
665
|
+
/** Общий файловый порт ресурсов и подключаемых модулей этого клиента. @internal */
|
|
666
|
+
files;
|
|
667
|
+
constructor(runtime, options) {
|
|
668
|
+
this.#runtime = runtime;
|
|
669
|
+
this.files = createFileResolver(runtime.config.fetch);
|
|
670
|
+
this.#features = new FeatureRegistry({
|
|
671
|
+
http: runtime.http,
|
|
672
|
+
services: runtime.services,
|
|
673
|
+
serviceOverrides: runtime.config.services,
|
|
674
|
+
catalog: options.catalog,
|
|
675
|
+
baseUrl: runtime.config.baseUrl,
|
|
676
|
+
clock: runtime.config.clock,
|
|
677
|
+
logger: runtime.config.logger,
|
|
678
|
+
files: this.files,
|
|
679
|
+
assertActive: options.assertActive,
|
|
680
|
+
connection: (serviceName) => runtime.connection(serviceName),
|
|
681
|
+
manage: (resource) => this.#resources.register(resource),
|
|
682
|
+
registerBucket: (name, definition) => runtime.registerRateLimitBucket(name, definition)
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
install(feature) {
|
|
686
|
+
return this.#features.install(feature);
|
|
687
|
+
}
|
|
688
|
+
names() {
|
|
689
|
+
return this.#features.names();
|
|
690
|
+
}
|
|
691
|
+
has(name) {
|
|
692
|
+
return this.#features.has(name);
|
|
693
|
+
}
|
|
694
|
+
/** Регистрирует ресурс самого клиента. @internal */
|
|
695
|
+
manage(resource) {
|
|
696
|
+
return this.#resources.register(resource);
|
|
697
|
+
}
|
|
698
|
+
/** Останавливает ресурсы перед сменой авторизации. */
|
|
699
|
+
stop() {
|
|
700
|
+
const { errors } = this.#resources.stopAll();
|
|
701
|
+
if (errors.length > 0) this.#runtime.config.logger?.error("Не удалось остановить ресурсы клиента", errors);
|
|
702
|
+
}
|
|
703
|
+
/** Останавливает ресурсы и ждёт функции закрытия модулей. */
|
|
704
|
+
async close() {
|
|
705
|
+
const snapshot = this.#resources.stopAll();
|
|
706
|
+
const errors = [...snapshot.errors];
|
|
707
|
+
const deadline = require_url.createDeadline(this.#runtime.config.shutdownTimeout, this.#runtime.config.clock);
|
|
708
|
+
const tasks = [...snapshot.resources.map((resource) => ({
|
|
709
|
+
kind: resource.kind,
|
|
710
|
+
promise: Promise.resolve().then(() => resource.drain())
|
|
711
|
+
})), {
|
|
712
|
+
kind: "feature lifecycle",
|
|
713
|
+
promise: Promise.resolve().then(() => this.#features.close())
|
|
714
|
+
}];
|
|
715
|
+
try {
|
|
716
|
+
const stuck = (await Promise.all(tasks.map(async ({ kind, promise }) => {
|
|
717
|
+
const result = promise.catch((error) => {
|
|
718
|
+
errors.push(error);
|
|
719
|
+
});
|
|
720
|
+
return await deadline.wait(result) ? void 0 : kind;
|
|
721
|
+
}))).filter((kind) => kind !== void 0);
|
|
722
|
+
if (stuck.length > 0) errors.push(new require_errors.ItdStateError(`ресурсы (${stuck.join(", ")}) не завершились за ${this.#runtime.config.shutdownTimeout} мс`));
|
|
723
|
+
} finally {
|
|
724
|
+
deadline.cancel();
|
|
725
|
+
}
|
|
726
|
+
if (errors.length === 1) throw errors[0];
|
|
727
|
+
if (errors.length > 1) throw new AggregateError(errors, "Не удалось закрыть feature-host");
|
|
728
|
+
}
|
|
729
|
+
async dispose() {
|
|
730
|
+
this.#resources.dispose();
|
|
731
|
+
const deadline = require_url.createDeadline(this.#runtime.config.shutdownTimeout, this.#runtime.config.clock);
|
|
732
|
+
let failed = false;
|
|
733
|
+
let failure;
|
|
734
|
+
const settled = this.#features.dispose().catch((error) => {
|
|
735
|
+
failed = true;
|
|
736
|
+
failure = error;
|
|
737
|
+
});
|
|
738
|
+
try {
|
|
739
|
+
if (!await deadline.wait(settled)) throw new require_errors.ItdStateError(`освобождение подключаемых модулей не завершилось за ${this.#runtime.config.shutdownTimeout} мс`);
|
|
740
|
+
} finally {
|
|
741
|
+
deadline.cancel();
|
|
742
|
+
}
|
|
743
|
+
if (failed) throw failure;
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
function createFeatureHost(runtime, options) {
|
|
747
|
+
return new ClientFeatureHost(runtime, options);
|
|
748
|
+
}
|
|
749
|
+
//#endregion
|
|
268
750
|
//#region src/resources/pagination.ts
|
|
269
751
|
/** Схема пагинации эндпоинта. */
|
|
270
752
|
const PaginationMode = Object.freeze({
|
|
@@ -382,6 +864,10 @@ function readOffsetPage(body, field, offset) {
|
|
|
382
864
|
raw: body
|
|
383
865
|
};
|
|
384
866
|
}
|
|
867
|
+
/** Создаёт единый контракт пагинированной операции. */
|
|
868
|
+
function pageOperation(id, read) {
|
|
869
|
+
return require_url.defineBuiltInOperation(id, read);
|
|
870
|
+
}
|
|
385
871
|
/**
|
|
386
872
|
* Перебор страниц списка.
|
|
387
873
|
*
|
|
@@ -507,6 +993,10 @@ var BaseResource = class {
|
|
|
507
993
|
constructor(http) {
|
|
508
994
|
this.http = http;
|
|
509
995
|
}
|
|
996
|
+
/** Выполняет операцию без возвращаемого значения и отбрасывает служебное тело ответа. */
|
|
997
|
+
voidOperation(operation, options) {
|
|
998
|
+
return this.http.execute(operation, options);
|
|
999
|
+
}
|
|
510
1000
|
/**
|
|
511
1001
|
* Собирает перебор страниц.
|
|
512
1002
|
*
|
|
@@ -532,24 +1022,22 @@ var BaseResource = class {
|
|
|
532
1022
|
* @example
|
|
533
1023
|
* ```ts
|
|
534
1024
|
* #feed = this.paginated<Post, FeedParams>({
|
|
535
|
-
*
|
|
1025
|
+
* operation: POSTS_LIST,
|
|
536
1026
|
* path: () => '/api/posts',
|
|
537
1027
|
* query: (p) => ({ tab: p.tab, limit: p.limit }),
|
|
538
1028
|
* start: (p) => (p.cursor ? { cursor: p.cursor } : {}),
|
|
539
|
-
* read: (body) => readCursorPage<Post>(body, 'posts'),
|
|
540
1029
|
* mode: PaginationMode.Cursor,
|
|
541
1030
|
* });
|
|
542
1031
|
* ```
|
|
543
1032
|
*/
|
|
544
1033
|
paginated(spec) {
|
|
545
1034
|
const load = async (params, state, options = {}) => {
|
|
546
|
-
const
|
|
547
|
-
|
|
1035
|
+
const operation = typeof spec.operation === "function" ? spec.operation(params) : spec.operation;
|
|
1036
|
+
return this.http.execute(operation, {
|
|
548
1037
|
path: spec.path(params),
|
|
549
1038
|
query: withPageState(spec.query(params), state),
|
|
550
1039
|
...options
|
|
551
1040
|
});
|
|
552
|
-
return spec.read(body, state);
|
|
553
1041
|
};
|
|
554
1042
|
return {
|
|
555
1043
|
list: (params, options) => load(params, spec.start(params), options),
|
|
@@ -574,6 +1062,163 @@ function withPageState(query, state) {
|
|
|
574
1062
|
};
|
|
575
1063
|
}
|
|
576
1064
|
//#endregion
|
|
1065
|
+
//#region src/resources/notifications.ts
|
|
1066
|
+
const NOTIFICATIONS_LIST = pageOperation("notifications.list", (body, request) => {
|
|
1067
|
+
const offset = Number(request.query?.offset ?? 0);
|
|
1068
|
+
const page = readOffsetPage(body, "notifications", Number.isFinite(offset) ? offset : 0);
|
|
1069
|
+
return {
|
|
1070
|
+
...page,
|
|
1071
|
+
items: page.items.map(require_url.normalizeNotification)
|
|
1072
|
+
};
|
|
1073
|
+
});
|
|
1074
|
+
const NOTIFICATIONS_COUNT = require_url.defineBuiltInOperation("notifications.count", (body) => require_url.pickNumber(body, "count", 0));
|
|
1075
|
+
const markedCountOperation = (id) => require_url.defineBuiltInOperation(id, (body) => require_url.pickNumber(body, "markedCount", 0));
|
|
1076
|
+
const NOTIFICATIONS_MARK_READ = markedCountOperation("notifications.markRead");
|
|
1077
|
+
const NOTIFICATIONS_MARK_READ_BATCH = markedCountOperation("notifications.markReadBatch");
|
|
1078
|
+
const NOTIFICATIONS_MARK_ALL_READ = markedCountOperation("notifications.markAllRead");
|
|
1079
|
+
const NOTIFICATIONS_GET_SETTINGS = require_url.defineBuiltInOperation("notifications.getSettings", readSettings);
|
|
1080
|
+
const NOTIFICATIONS_UPDATE_SETTINGS = require_url.defineBuiltInOperation("notifications.updateSettings", readSettings);
|
|
1081
|
+
const NOTIFICATION_SETTING_KEYS = [
|
|
1082
|
+
"enabled",
|
|
1083
|
+
"sound",
|
|
1084
|
+
"follows",
|
|
1085
|
+
"wallPosts",
|
|
1086
|
+
"likes",
|
|
1087
|
+
"comments",
|
|
1088
|
+
"mentions"
|
|
1089
|
+
];
|
|
1090
|
+
/**
|
|
1091
|
+
* Сколько идентификаторов уходит в одном запросе на отметку прочтения.
|
|
1092
|
+
*
|
|
1093
|
+
* Столько же отправляет сайт итд.com — значит на сервере, скорее всего, есть ограничение.
|
|
1094
|
+
*/
|
|
1095
|
+
const READ_BATCH_SIZE = 20;
|
|
1096
|
+
/**
|
|
1097
|
+
* Читает настройки уведомлений.
|
|
1098
|
+
*
|
|
1099
|
+
* Сервер отдаёт плоский объект: `enabled`, `sound`, `follows`, `wallPosts`, `likes`,
|
|
1100
|
+
* `comments`, `mentions`. Отсутствующая настройка считается включённой — так же
|
|
1101
|
+
* ведёт себя сайт итд.com.
|
|
1102
|
+
*/
|
|
1103
|
+
function readSettings(body) {
|
|
1104
|
+
const settings = {};
|
|
1105
|
+
for (const key of NOTIFICATION_SETTING_KEYS) settings[key] = require_url.pickBoolean(body, key, true);
|
|
1106
|
+
return settings;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Уведомления: список, счётчик, отметки о прочтении, настройки.
|
|
1110
|
+
*
|
|
1111
|
+
* Доступна как `itd.notifications`. Все уведомления приведены к единой форме, поэтому
|
|
1112
|
+
* объекты отсюда и из потока событий можно складывать в один список.
|
|
1113
|
+
*/
|
|
1114
|
+
var NotificationsResource = class extends BaseResource {
|
|
1115
|
+
/** Уведомления: `/api/notifications/`, пагинация по смещению. */
|
|
1116
|
+
#list = this.paginated({
|
|
1117
|
+
operation: NOTIFICATIONS_LIST,
|
|
1118
|
+
path: () => "/api/notifications/",
|
|
1119
|
+
query: (p) => ({ limit: p.limit }),
|
|
1120
|
+
start: (p) => ({ offset: p.offset ?? 0 }),
|
|
1121
|
+
mode: PaginationMode.Offset
|
|
1122
|
+
});
|
|
1123
|
+
/**
|
|
1124
|
+
* Загружает страницу уведомлений.
|
|
1125
|
+
*
|
|
1126
|
+
* Пагинация здесь основана на смещении.
|
|
1127
|
+
*
|
|
1128
|
+
* @example
|
|
1129
|
+
* ```ts
|
|
1130
|
+
* const page = await itd.notifications.list({ limit: 20 });
|
|
1131
|
+
* const next = await itd.notifications.list({ limit: 20, offset: page.nextOffset });
|
|
1132
|
+
* ```
|
|
1133
|
+
*/
|
|
1134
|
+
list(params = {}, options = {}) {
|
|
1135
|
+
return this.#list.list(params, options);
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Перебирает уведомления.
|
|
1139
|
+
*
|
|
1140
|
+
* @example
|
|
1141
|
+
* ```ts
|
|
1142
|
+
* for await (const notification of itd.notifications.iterate()) {
|
|
1143
|
+
* console.log(formatNotificationText(notification));
|
|
1144
|
+
* }
|
|
1145
|
+
* ```
|
|
1146
|
+
*/
|
|
1147
|
+
iterate(params = {}, options = {}) {
|
|
1148
|
+
return this.#list.iterate(params, options);
|
|
1149
|
+
}
|
|
1150
|
+
/** Загружает число непрочитанных уведомлений. */
|
|
1151
|
+
count(options = {}) {
|
|
1152
|
+
return this.http.execute(NOTIFICATIONS_COUNT, {
|
|
1153
|
+
path: "/api/notifications/count",
|
|
1154
|
+
...options
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Отмечает уведомление прочитанным.
|
|
1159
|
+
*
|
|
1160
|
+
* @returns сколько записей отметил сервер
|
|
1161
|
+
*/
|
|
1162
|
+
markRead(notificationId, options = {}) {
|
|
1163
|
+
return this.http.execute(NOTIFICATIONS_MARK_READ, {
|
|
1164
|
+
path: `/api/notifications/${require_redact.encodePathSegment(notificationId, "notificationId")}/read`,
|
|
1165
|
+
...options
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Отмечает прочитанными сразу несколько уведомлений.
|
|
1170
|
+
*
|
|
1171
|
+
* Список автоматически режется на части по 20 идентификаторов — столько же отправляет
|
|
1172
|
+
* сайт итд.com, поэтому на сервере вероятен предел. Части уходят последовательно,
|
|
1173
|
+
* результат суммируется.
|
|
1174
|
+
*
|
|
1175
|
+
* @returns сколько записей отметил сервер суммарно
|
|
1176
|
+
*/
|
|
1177
|
+
async markReadBatch(ids, options = {}) {
|
|
1178
|
+
let marked = 0;
|
|
1179
|
+
for (let index = 0; index < ids.length; index += READ_BATCH_SIZE) {
|
|
1180
|
+
const chunk = ids.slice(index, index + READ_BATCH_SIZE);
|
|
1181
|
+
marked += await this.http.execute(NOTIFICATIONS_MARK_READ_BATCH, {
|
|
1182
|
+
path: "/api/notifications/read-batch",
|
|
1183
|
+
body: { ids: chunk },
|
|
1184
|
+
...options
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
return marked;
|
|
1188
|
+
}
|
|
1189
|
+
/** Отмечает прочитанными все уведомления. */
|
|
1190
|
+
markAllRead(options = {}) {
|
|
1191
|
+
return this.http.execute(NOTIFICATIONS_MARK_ALL_READ, {
|
|
1192
|
+
path: "/api/notifications/read-all",
|
|
1193
|
+
...options
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
/** Загружает настройки уведомлений. */
|
|
1197
|
+
getSettings(options = {}) {
|
|
1198
|
+
return this.http.execute(NOTIFICATIONS_GET_SETTINGS, {
|
|
1199
|
+
path: "/api/notifications/settings",
|
|
1200
|
+
...options
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Обновляет настройки уведомлений.
|
|
1205
|
+
*
|
|
1206
|
+
* Отправляются только изменяемые поля, в том же виде, в каком сервер их возвращает.
|
|
1207
|
+
*/
|
|
1208
|
+
async updateSettings(input, options = {}) {
|
|
1209
|
+
const payload = {};
|
|
1210
|
+
for (const key of NOTIFICATION_SETTING_KEYS) {
|
|
1211
|
+
const value = input[key];
|
|
1212
|
+
if (value !== void 0) payload[key] = value;
|
|
1213
|
+
}
|
|
1214
|
+
return this.http.execute(NOTIFICATIONS_UPDATE_SETTINGS, {
|
|
1215
|
+
path: "/api/notifications/settings",
|
|
1216
|
+
body: payload,
|
|
1217
|
+
...options
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
//#endregion
|
|
577
1222
|
//#region src/domain/time.ts
|
|
578
1223
|
const NAIVE_STAMP = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/;
|
|
579
1224
|
/**
|
|
@@ -627,11 +1272,11 @@ var StatusResource = class {
|
|
|
627
1272
|
constructor(context) {
|
|
628
1273
|
this.#context = context;
|
|
629
1274
|
}
|
|
630
|
-
|
|
631
|
-
return
|
|
1275
|
+
get(options = {}) {
|
|
1276
|
+
return this.#context.request("get", {
|
|
632
1277
|
path: "/api/status",
|
|
633
1278
|
...options
|
|
634
|
-
})
|
|
1279
|
+
});
|
|
635
1280
|
}
|
|
636
1281
|
};
|
|
637
1282
|
/** Встроенный модуль страницы состояния платформы. @internal */
|
|
@@ -646,7 +1291,8 @@ function createStatusFeature() {
|
|
|
646
1291
|
operations: { get: {
|
|
647
1292
|
method: "GET",
|
|
648
1293
|
retrySafety: require_url.RetrySafety.Safe,
|
|
649
|
-
service: require_url.STATUS_SERVICE
|
|
1294
|
+
service: require_url.STATUS_SERVICE,
|
|
1295
|
+
read: (body) => normalizeStatus(body)
|
|
650
1296
|
} },
|
|
651
1297
|
setup: (context) => ({ api: new StatusResource(context) })
|
|
652
1298
|
};
|
|
@@ -811,6 +1457,13 @@ function resolveComment(input, allowReplyTo = false) {
|
|
|
811
1457
|
}
|
|
812
1458
|
//#endregion
|
|
813
1459
|
//#region src/resources/comments.ts
|
|
1460
|
+
const COMMENTS_REPLIES = pageOperation("comments.replies", (body) => readPagedPage(body, "replies"));
|
|
1461
|
+
const COMMENTS_REPLY = require_url.passthroughOperation("comments.reply");
|
|
1462
|
+
const COMMENTS_UPDATE = require_url.passthroughOperation("comments.update");
|
|
1463
|
+
const COMMENTS_RESTORE = require_url.passthroughOperation("comments.restore");
|
|
1464
|
+
const COMMENTS_LIKE = require_url.passthroughOperation("comments.like");
|
|
1465
|
+
const COMMENTS_UNLIKE = require_url.passthroughOperation("comments.unlike");
|
|
1466
|
+
const COMMENTS_REMOVE = require_url.voidOperation("comments.remove");
|
|
814
1467
|
/**
|
|
815
1468
|
* Комментарии и ответы на них.
|
|
816
1469
|
*
|
|
@@ -821,11 +1474,10 @@ var CommentsResource = class extends BaseResource {
|
|
|
821
1474
|
#uploadFiles;
|
|
822
1475
|
/** Ответы на комментарий: `/api/comments/{id}/replies`, постраничная пагинация. */
|
|
823
1476
|
#replies = this.paginated({
|
|
824
|
-
|
|
825
|
-
path: (p) => `/api/comments/${
|
|
1477
|
+
operation: COMMENTS_REPLIES,
|
|
1478
|
+
path: (p) => `/api/comments/${require_redact.encodePathSegment(p.commentId, "commentId")}/replies`,
|
|
826
1479
|
query: (p) => ({ limit: p.limit }),
|
|
827
1480
|
start: (p) => p.page !== void 0 ? { page: p.page } : {},
|
|
828
|
-
read: (body) => readPagedPage(body, "replies"),
|
|
829
1481
|
mode: PaginationMode.Page
|
|
830
1482
|
});
|
|
831
1483
|
constructor(http, deps) {
|
|
@@ -864,8 +1516,8 @@ var CommentsResource = class extends BaseResource {
|
|
|
864
1516
|
const existing = data.attachmentIds ?? [];
|
|
865
1517
|
const files = data.files ?? [];
|
|
866
1518
|
const attachmentIds = files.length > 0 ? [...existing, ...await this.#uploadFiles(files, options)] : existing;
|
|
867
|
-
return this.http.
|
|
868
|
-
path: `/api/comments/${
|
|
1519
|
+
return this.http.execute(COMMENTS_REPLY, {
|
|
1520
|
+
path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}/replies`,
|
|
869
1521
|
body: {
|
|
870
1522
|
content: data.content ?? "",
|
|
871
1523
|
attachmentIds,
|
|
@@ -876,175 +1528,41 @@ var CommentsResource = class extends BaseResource {
|
|
|
876
1528
|
}
|
|
877
1529
|
/** Редактирует текст комментария. */
|
|
878
1530
|
update(commentId, content, options = {}) {
|
|
879
|
-
return this.http.
|
|
880
|
-
path: `/api/comments/${
|
|
1531
|
+
return this.http.execute(COMMENTS_UPDATE, {
|
|
1532
|
+
path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}`,
|
|
881
1533
|
body: { content },
|
|
882
1534
|
...options
|
|
883
1535
|
});
|
|
884
1536
|
}
|
|
885
1537
|
/** Удаляет комментарий. Восстановить его можно через {@link restore}. */
|
|
886
1538
|
remove(commentId, options = {}) {
|
|
887
|
-
return this.
|
|
888
|
-
path: `/api/comments/${
|
|
889
|
-
...options
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
/** Восстанавливает удалённый комментарий. */
|
|
893
|
-
restore(commentId, options = {}) {
|
|
894
|
-
return this.http.operation("comments.restore", {
|
|
895
|
-
path: `/api/comments/${require_cookies.encodePathSegment(commentId, "commentId")}/restore`,
|
|
896
|
-
...options
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
/** Ставит реакцию на комментарий. */
|
|
900
|
-
like(commentId, options = {}) {
|
|
901
|
-
return this.http.operation("comments.like", {
|
|
902
|
-
path: `/api/comments/${require_cookies.encodePathSegment(commentId, "commentId")}/like`,
|
|
903
|
-
...options
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
/** Убирает реакцию с комментария. */
|
|
907
|
-
unlike(commentId, options = {}) {
|
|
908
|
-
return this.http.operation("comments.unlike", {
|
|
909
|
-
path: `/api/comments/${require_cookies.encodePathSegment(commentId, "commentId")}/like`,
|
|
1539
|
+
return this.voidOperation(COMMENTS_REMOVE, {
|
|
1540
|
+
path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}`,
|
|
910
1541
|
...options
|
|
911
1542
|
});
|
|
912
1543
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
}
|
|
920
|
-
/** Достаёт имя файла из пути URL. */
|
|
921
|
-
function filenameFromUrl(url) {
|
|
922
|
-
const last = url.pathname.split("/").pop();
|
|
923
|
-
if (!last) return void 0;
|
|
924
|
-
try {
|
|
925
|
-
return decodeURIComponent(last) || void 0;
|
|
926
|
-
} catch {
|
|
927
|
-
return last;
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
/** Проверяет объявленный размер и возвращает его, если заголовок корректен. */
|
|
931
|
-
function declaredSize(response, maxBytes, url) {
|
|
932
|
-
const header = response.headers.get("content-length");
|
|
933
|
-
if (header === null) return void 0;
|
|
934
|
-
const size = Number(header);
|
|
935
|
-
if (!Number.isFinite(size) || size < 0 || !Number.isInteger(size)) return void 0;
|
|
936
|
-
if (maxBytes !== void 0 && size > maxBytes) throw require_options.fileTooLarge(url, maxBytes, size);
|
|
937
|
-
return size;
|
|
938
|
-
}
|
|
939
|
-
/** Получает HTTP-ответ источника и проверяет его статус. */
|
|
940
|
-
async function fetchFile(target, options, context) {
|
|
941
|
-
let requested;
|
|
942
|
-
try {
|
|
943
|
-
requested = new URL(target);
|
|
944
|
-
} catch {
|
|
945
|
-
throw new require_errors.ItdConfigError(`«${target}» не разбирается как адрес`);
|
|
946
|
-
}
|
|
947
|
-
if (requested.protocol !== "http:" && requested.protocol !== "https:") throw new require_errors.ItdConfigError(`вложение по адресу поддерживает только http и https, получено: ${requested.protocol}`);
|
|
948
|
-
let response;
|
|
949
|
-
try {
|
|
950
|
-
response = await context.fetch(requested, { ...context.signal ? { signal: context.signal } : {} });
|
|
951
|
-
} catch (error) {
|
|
952
|
-
if (context.signal?.aborted || error instanceof Error && error.name === "AbortError") throw error;
|
|
953
|
-
throw new require_errors.ItdFileError(`не удалось получить файл по адресу ${requested.href}`, {
|
|
954
|
-
reason: require_errors.ItdFileErrorReason.Network,
|
|
955
|
-
url: requested.href,
|
|
956
|
-
retryable: true,
|
|
957
|
-
cause: error
|
|
958
|
-
});
|
|
959
|
-
}
|
|
960
|
-
const finalUrl = response.url ? new URL(response.url) : requested;
|
|
961
|
-
if (!response.ok) {
|
|
962
|
-
await response.body?.cancel().catch(() => {});
|
|
963
|
-
throw new require_errors.ItdFileError(`источник ${finalUrl.href} ответил статусом ${response.status}`, {
|
|
964
|
-
reason: require_errors.ItdFileErrorReason.Http,
|
|
965
|
-
url: finalUrl.href,
|
|
966
|
-
status: response.status,
|
|
967
|
-
retryable: response.status === 408 || response.status === 429 || response.status >= 500
|
|
968
|
-
});
|
|
969
|
-
}
|
|
970
|
-
const { maxBytes } = require_options.resolveFileStreamOptions(options, require_options.DEFAULT_URL_FILE_MAX_BYTES);
|
|
971
|
-
let size;
|
|
972
|
-
try {
|
|
973
|
-
size = declaredSize(response, maxBytes, finalUrl.href);
|
|
974
|
-
} catch (error) {
|
|
975
|
-
await response.body?.cancel().catch(() => {});
|
|
976
|
-
throw error;
|
|
977
|
-
}
|
|
978
|
-
return {
|
|
979
|
-
response,
|
|
980
|
-
url: finalUrl,
|
|
981
|
-
size
|
|
982
|
-
};
|
|
983
|
-
}
|
|
984
|
-
/** Читает ответ с контролем размера до создания итогового `Blob`. */
|
|
985
|
-
async function responseBlob(response, url, maxBytes, streamBufferBytes, signal) {
|
|
986
|
-
if (!response.body) {
|
|
987
|
-
const blob = await response.blob();
|
|
988
|
-
if (maxBytes !== void 0 && blob.size > maxBytes) throw require_options.fileTooLarge(url, maxBytes, blob.size);
|
|
989
|
-
return blob;
|
|
990
|
-
}
|
|
991
|
-
const chunks = [];
|
|
992
|
-
const reader = require_options.boundedFileStream(response.body, {
|
|
993
|
-
...maxBytes !== void 0 ? { maxBytes } : {},
|
|
994
|
-
streamBufferBytes,
|
|
995
|
-
...signal ? { signal } : {},
|
|
996
|
-
url,
|
|
997
|
-
retryableRead: true
|
|
998
|
-
}).getReader();
|
|
999
|
-
try {
|
|
1000
|
-
for (;;) {
|
|
1001
|
-
const next = await reader.read();
|
|
1002
|
-
if (next.done) break;
|
|
1003
|
-
chunks.push(next.value);
|
|
1004
|
-
}
|
|
1005
|
-
} finally {
|
|
1006
|
-
reader.releaseLock();
|
|
1544
|
+
/** Восстанавливает удалённый комментарий. */
|
|
1545
|
+
restore(commentId, options = {}) {
|
|
1546
|
+
return this.http.execute(COMMENTS_RESTORE, {
|
|
1547
|
+
path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}/restore`,
|
|
1548
|
+
...options
|
|
1549
|
+
});
|
|
1007
1550
|
}
|
|
1008
|
-
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
/** Открывает HTTP-ответ как ограниченный поток. @internal */
|
|
1024
|
-
async function openUrlFile(target, options, context) {
|
|
1025
|
-
const resolved = require_options.resolveFileStreamOptions(options, require_options.DEFAULT_URL_FILE_MAX_BYTES);
|
|
1026
|
-
const { response, url, size } = await fetchFile(target, options, context);
|
|
1027
|
-
if (!response.body) throw new require_errors.ItdFileError(`источник ${url.href} не предоставил потоковое тело`, {
|
|
1028
|
-
reason: require_errors.ItdFileErrorReason.StreamUnavailable,
|
|
1029
|
-
url: url.href
|
|
1030
|
-
});
|
|
1031
|
-
const stream = require_options.boundedFileStream(response.body, {
|
|
1032
|
-
...resolved.maxBytes !== void 0 ? { maxBytes: resolved.maxBytes } : {},
|
|
1033
|
-
streamBufferBytes: resolved.streamBufferBytes,
|
|
1034
|
-
...context.signal ? { signal: context.signal } : {},
|
|
1035
|
-
url: url.href,
|
|
1036
|
-
retryableRead: true
|
|
1037
|
-
});
|
|
1038
|
-
const filename = options.filename ?? filenameFromUrl(url);
|
|
1039
|
-
const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
|
|
1040
|
-
return {
|
|
1041
|
-
stream,
|
|
1042
|
-
...filename ? { filename } : {},
|
|
1043
|
-
...contentType ? { contentType } : {},
|
|
1044
|
-
...size !== void 0 ? { size } : {},
|
|
1045
|
-
close: () => stream.cancel().catch(() => {})
|
|
1046
|
-
};
|
|
1047
|
-
}
|
|
1551
|
+
/** Ставит реакцию на комментарий. */
|
|
1552
|
+
like(commentId, options = {}) {
|
|
1553
|
+
return this.http.execute(COMMENTS_LIKE, {
|
|
1554
|
+
path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}/like`,
|
|
1555
|
+
...options
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
/** Убирает реакцию с комментария. */
|
|
1559
|
+
unlike(commentId, options = {}) {
|
|
1560
|
+
return this.http.execute(COMMENTS_UNLIKE, {
|
|
1561
|
+
path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}/like`,
|
|
1562
|
+
...options
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
};
|
|
1048
1566
|
//#endregion
|
|
1049
1567
|
//#region src/core/multipart.ts
|
|
1050
1568
|
const encoder = new TextEncoder();
|
|
@@ -1182,12 +1700,17 @@ function assertAllowedMime(mimeType, filename) {
|
|
|
1182
1700
|
if (!mimeType) throw new require_errors.ItdConfigError(`Не удалось определить тип файла${filename ? ` «${filename}»` : ""}. Укажите его явно: { file, filename, contentType }.`);
|
|
1183
1701
|
if (!isAllowedMime(mimeType)) throw new require_errors.ItdConfigError(`Тип «${mimeType}» не поддерживается загрузкой. Допустимые: ${ALLOWED_MIME_TYPES.join(", ")}.`);
|
|
1184
1702
|
}
|
|
1703
|
+
//#endregion
|
|
1704
|
+
//#region src/resources/files.ts
|
|
1705
|
+
const FILES_UPLOAD = require_url.passthroughOperation("files.upload");
|
|
1706
|
+
const FILES_GET = require_url.passthroughOperation("files.get");
|
|
1707
|
+
const FILES_REMOVE = require_url.voidOperation("files.remove");
|
|
1185
1708
|
/** Файлы и медиа. */
|
|
1186
1709
|
var FilesResource = class extends BaseResource {
|
|
1187
|
-
#
|
|
1710
|
+
#files;
|
|
1188
1711
|
constructor(http, deps) {
|
|
1189
1712
|
super(http);
|
|
1190
|
-
this.#
|
|
1713
|
+
this.#files = deps.files;
|
|
1191
1714
|
}
|
|
1192
1715
|
/**
|
|
1193
1716
|
* Загружает файл и возвращает его идентификатор.
|
|
@@ -1197,7 +1720,7 @@ var FilesResource = class extends BaseResource {
|
|
|
1197
1720
|
*/
|
|
1198
1721
|
upload(input, uploadOptions = {}, requestOptions = {}) {
|
|
1199
1722
|
const bodyFactory = this.#createBodyFactory(input, uploadOptions);
|
|
1200
|
-
return this.http.
|
|
1723
|
+
return this.http.execute(FILES_UPLOAD, {
|
|
1201
1724
|
path: "/api/files/upload",
|
|
1202
1725
|
bodyFactory,
|
|
1203
1726
|
...requestOptions,
|
|
@@ -1216,15 +1739,15 @@ var FilesResource = class extends BaseResource {
|
|
|
1216
1739
|
* Для ещё не прикреплённого файла сервер может ответить `404`.
|
|
1217
1740
|
*/
|
|
1218
1741
|
get(fileId, options = {}) {
|
|
1219
|
-
return this.http.
|
|
1220
|
-
path: `/api/files/${
|
|
1742
|
+
return this.http.execute(FILES_GET, {
|
|
1743
|
+
path: `/api/files/${require_redact.encodePathSegment(fileId, "fileId")}`,
|
|
1221
1744
|
...options
|
|
1222
1745
|
});
|
|
1223
1746
|
}
|
|
1224
1747
|
/** Удаляет загруженный файл. */
|
|
1225
1748
|
remove(fileId, options = {}) {
|
|
1226
|
-
return this.
|
|
1227
|
-
path: `/api/files/${
|
|
1749
|
+
return this.voidOperation(FILES_REMOVE, {
|
|
1750
|
+
path: `/api/files/${require_redact.encodePathSegment(fileId, "fileId")}`,
|
|
1228
1751
|
...options
|
|
1229
1752
|
});
|
|
1230
1753
|
}
|
|
@@ -1233,140 +1756,70 @@ var FilesResource = class extends BaseResource {
|
|
|
1233
1756
|
* заново, поэтому каждая транспортная попытка получает непрочитанное тело.
|
|
1234
1757
|
*/
|
|
1235
1758
|
#createBodyFactory(input, options) {
|
|
1236
|
-
const streamMode = this.#
|
|
1759
|
+
const streamMode = this.#files.mode(input) === require_options.FileTransferMode.Stream;
|
|
1237
1760
|
let buffered;
|
|
1238
1761
|
return async ({ signal, attempt }) => {
|
|
1239
|
-
|
|
1240
|
-
|
|
1762
|
+
let prepared;
|
|
1763
|
+
if (streamMode) prepared = await this.#files.resolve(input, options, {
|
|
1241
1764
|
signal,
|
|
1242
1765
|
attempt
|
|
1243
|
-
};
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
});
|
|
1252
|
-
prepared = await buffered;
|
|
1253
|
-
}
|
|
1254
|
-
} catch (error) {
|
|
1255
|
-
if (error instanceof require_errors.ItdError || signal.aborted) throw error;
|
|
1256
|
-
throw new require_errors.ItdFileError("не удалось получить содержимое вложения", {
|
|
1257
|
-
reason: require_errors.ItdFileErrorReason.Read,
|
|
1258
|
-
retryable: true,
|
|
1259
|
-
cause: error
|
|
1766
|
+
});
|
|
1767
|
+
else {
|
|
1768
|
+
buffered ??= this.#files.resolve(input, options, {
|
|
1769
|
+
signal,
|
|
1770
|
+
attempt
|
|
1771
|
+
}).then((source) => source).catch((error) => {
|
|
1772
|
+
buffered = void 0;
|
|
1773
|
+
throw error;
|
|
1260
1774
|
});
|
|
1775
|
+
prepared = await buffered;
|
|
1261
1776
|
}
|
|
1262
|
-
if (prepared.mode ===
|
|
1777
|
+
if (prepared.mode === require_options.FileTransferMode.Buffer) {
|
|
1778
|
+
const filename = this.#filename(prepared, options);
|
|
1779
|
+
const contentType = prepared.contentType ?? mimeFromFilename(filename);
|
|
1780
|
+
if (options.validateMime !== false) assertAllowedMime(contentType, filename);
|
|
1781
|
+
const blob = !contentType || prepared.blob.type === contentType ? prepared.blob : new Blob([prepared.blob], { type: contentType });
|
|
1263
1782
|
const form = new FormData();
|
|
1264
|
-
form.set("file",
|
|
1783
|
+
form.set("file", blob, filename);
|
|
1265
1784
|
return { body: form };
|
|
1266
1785
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
const contentType = normalizeMimeType(options.contentType ?? content.contentType) ?? normalizeMimeType(require_url.isBlob(content.file) ? content.file.type : void 0) ?? mimeFromFilename(filename);
|
|
1290
|
-
if (options.validateMime !== false) assertAllowedMime(contentType, filename);
|
|
1291
|
-
const blob = require_url.isBlob(content.file) && (!contentType || content.file.type === contentType) ? content.file : new Blob([content.file], { type: contentType ?? "" });
|
|
1292
|
-
const limits = require_options.resolveFileStreamOptions(options);
|
|
1293
|
-
if (limits.maxBytes !== void 0 && blob.size > limits.maxBytes) throw new require_errors.ItdFileError(`файл больше предела в ${limits.maxBytes} байт: ${blob.size}`, {
|
|
1294
|
-
reason: require_errors.ItdFileErrorReason.TooLarge,
|
|
1295
|
-
limit: limits.maxBytes,
|
|
1296
|
-
actual: blob.size
|
|
1297
|
-
});
|
|
1298
|
-
return {
|
|
1299
|
-
mode: "buffer",
|
|
1300
|
-
blob,
|
|
1301
|
-
filename
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
/** Открывает и проверяет потоковый источник. */
|
|
1305
|
-
async #prepareStream(input, options, context) {
|
|
1306
|
-
let opened;
|
|
1307
|
-
if (typeof input === "object" && input !== null && "open" in input) opened = await input.open(context);
|
|
1308
|
-
else if (typeof input === "object" && input !== null && "url" in input) {
|
|
1309
|
-
const { url, ...urlOptions } = input;
|
|
1310
|
-
opened = await openUrlFile(url, urlOptions, context);
|
|
1311
|
-
} else throw new require_errors.ItdConfigError("потоковое вложение должно иметь форму { open } или { url, mode }");
|
|
1312
|
-
if (!opened || typeof ReadableStream === "undefined" || !(opened.stream instanceof ReadableStream)) {
|
|
1313
|
-
await opened?.close?.();
|
|
1314
|
-
throw new require_errors.ItdConfigError("потоковый источник должен вернуть { stream: ReadableStream }");
|
|
1315
|
-
}
|
|
1316
|
-
try {
|
|
1317
|
-
const filename = options.filename ?? opened.filename ?? this.#nameFromMime(options.contentType ?? opened.contentType);
|
|
1318
|
-
const contentType = normalizeMimeType(options.contentType ?? opened.contentType) ?? mimeFromFilename(filename);
|
|
1319
|
-
if (options.validateMime !== false) assertAllowedMime(contentType, filename);
|
|
1320
|
-
const limits = require_options.resolveFileStreamOptions({
|
|
1321
|
-
mode: require_options.FileTransferMode.Stream,
|
|
1322
|
-
...options.maxBytes !== void 0 ? { maxBytes: options.maxBytes } : {},
|
|
1323
|
-
...options.streamBufferBytes !== void 0 ? { streamBufferBytes: options.streamBufferBytes } : {}
|
|
1324
|
-
});
|
|
1325
|
-
if (limits.maxBytes !== void 0 && opened.size !== void 0 && opened.size > limits.maxBytes) throw new require_errors.ItdFileError(`файл больше предела в ${limits.maxBytes} байт: ${opened.size}`, {
|
|
1326
|
-
reason: require_errors.ItdFileErrorReason.TooLarge,
|
|
1327
|
-
limit: limits.maxBytes,
|
|
1328
|
-
actual: opened.size
|
|
1329
|
-
});
|
|
1330
|
-
return {
|
|
1331
|
-
mode: "stream",
|
|
1332
|
-
stream: require_options.isBoundedFileStream(opened.stream) && options.maxBytes === void 0 && options.streamBufferBytes === void 0 ? opened.stream : require_options.boundedFileStream(opened.stream, {
|
|
1333
|
-
...limits.maxBytes !== void 0 ? { maxBytes: limits.maxBytes } : {},
|
|
1334
|
-
streamBufferBytes: limits.streamBufferBytes,
|
|
1335
|
-
signal: context.signal
|
|
1336
|
-
}),
|
|
1337
|
-
filename,
|
|
1338
|
-
contentType: contentType ?? "",
|
|
1339
|
-
...opened.close ? { close: opened.close } : {}
|
|
1340
|
-
};
|
|
1341
|
-
} catch (error) {
|
|
1342
|
-
await opened.close?.();
|
|
1343
|
-
throw error;
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
/** Разрешает только буферные формы. */
|
|
1347
|
-
#resolveBuffer(input, context) {
|
|
1348
|
-
if (input instanceof ArrayBuffer || ArrayBuffer.isView(input) || require_url.isBlob(input)) return {
|
|
1349
|
-
file: input,
|
|
1350
|
-
...require_url.isFile(input) ? { filename: input.name } : {}
|
|
1786
|
+
let multipart;
|
|
1787
|
+
try {
|
|
1788
|
+
const filename = this.#filename(prepared, options);
|
|
1789
|
+
const contentType = prepared.contentType ?? mimeFromFilename(filename);
|
|
1790
|
+
if (options.validateMime !== false) assertAllowedMime(contentType, filename);
|
|
1791
|
+
multipart = createMultipartFileBody({
|
|
1792
|
+
stream: prepared.stream,
|
|
1793
|
+
filename,
|
|
1794
|
+
contentType: contentType ?? ""
|
|
1795
|
+
});
|
|
1796
|
+
return {
|
|
1797
|
+
body: multipart.body,
|
|
1798
|
+
headers: { "Content-Type": multipart.contentType },
|
|
1799
|
+
cleanup: async () => {
|
|
1800
|
+
await multipart?.cancel();
|
|
1801
|
+
await prepared.close();
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
await prepared.close();
|
|
1806
|
+
throw error;
|
|
1807
|
+
}
|
|
1351
1808
|
};
|
|
1352
|
-
if (typeof input !== "object" || input === null) throw new require_errors.ItdConfigError(`вложение задано значением типа ${typeof input}; ожидается бинарное значение или объект { file }, { url }, { load }, { open }. Для файла на диске используйте fromPath('./photo.jpg') из itd-api/node`);
|
|
1353
|
-
if ("load" in input) return input.load(context);
|
|
1354
|
-
if ("url" in input) {
|
|
1355
|
-
const { url, ...urlOptions } = input;
|
|
1356
|
-
return downloadFile(url, urlOptions, context);
|
|
1357
|
-
}
|
|
1358
|
-
if ("file" in input) return input;
|
|
1359
|
-
if ("open" in input) throw new require_errors.ItdConfigError("потоковый источник нельзя использовать как буферный");
|
|
1360
|
-
throw new require_errors.ItdConfigError("вложение не распознано: ожидается { file }, { url }, { load } или { open }");
|
|
1361
1809
|
}
|
|
1362
1810
|
/** Подбирает непустое имя, обязательное для multipart. */
|
|
1363
|
-
#
|
|
1364
|
-
|
|
1811
|
+
#filename(source, options) {
|
|
1812
|
+
if (options.filename) return options.filename;
|
|
1813
|
+
if (source.filename) return source.filename;
|
|
1814
|
+
const extension = options.contentType?.split(";", 1)[0]?.trim().split("/")[1];
|
|
1365
1815
|
return extension ? `file.${extension}` : "file";
|
|
1366
1816
|
}
|
|
1367
1817
|
};
|
|
1368
1818
|
//#endregion
|
|
1369
1819
|
//#region src/resources/hashtags.ts
|
|
1820
|
+
const HASHTAG_POSTS = pageOperation("hashtags.posts", (body) => readCursorPage(body, "posts"));
|
|
1821
|
+
const HASHTAGS_SEARCH = require_url.defineBuiltInOperation("hashtags.search", (body) => require_url.pickArray(body, "hashtags"));
|
|
1822
|
+
const HASHTAGS_TRENDING = require_url.defineBuiltInOperation("hashtags.trending", (body) => require_url.pickArray(body, "hashtags"));
|
|
1370
1823
|
/**
|
|
1371
1824
|
* Хэштеги.
|
|
1372
1825
|
*
|
|
@@ -1375,11 +1828,10 @@ var FilesResource = class extends BaseResource {
|
|
|
1375
1828
|
var HashtagsResource = class extends BaseResource {
|
|
1376
1829
|
/** Посты по хэштегу: `/api/hashtags/{tag}/posts`, курсорная пагинация. */
|
|
1377
1830
|
#posts = this.paginated({
|
|
1378
|
-
|
|
1379
|
-
path: (p) => `/api/hashtags/${
|
|
1831
|
+
operation: HASHTAG_POSTS,
|
|
1832
|
+
path: (p) => `/api/hashtags/${require_redact.encodePathSegment(p.tag, "tag")}/posts`,
|
|
1380
1833
|
query: (p) => ({ limit: p.limit }),
|
|
1381
1834
|
start: (p) => p.cursor ? { cursor: p.cursor } : {},
|
|
1382
|
-
read: (body) => readCursorPage(body, "posts"),
|
|
1383
1835
|
mode: PaginationMode.Cursor
|
|
1384
1836
|
});
|
|
1385
1837
|
/**
|
|
@@ -1387,8 +1839,8 @@ var HashtagsResource = class extends BaseResource {
|
|
|
1387
1839
|
*
|
|
1388
1840
|
* Без строки запроса возвращает общий список.
|
|
1389
1841
|
*/
|
|
1390
|
-
|
|
1391
|
-
|
|
1842
|
+
search(query, params = {}, options = {}) {
|
|
1843
|
+
return this.http.execute(HASHTAGS_SEARCH, {
|
|
1392
1844
|
path: "/api/hashtags",
|
|
1393
1845
|
query: {
|
|
1394
1846
|
q: query,
|
|
@@ -1396,16 +1848,14 @@ var HashtagsResource = class extends BaseResource {
|
|
|
1396
1848
|
},
|
|
1397
1849
|
...options
|
|
1398
1850
|
});
|
|
1399
|
-
return require_url.pickArray(body, "hashtags");
|
|
1400
1851
|
}
|
|
1401
1852
|
/** Загружает трендовые хэштеги. */
|
|
1402
|
-
|
|
1403
|
-
|
|
1853
|
+
trending(params = {}, options = {}) {
|
|
1854
|
+
return this.http.execute(HASHTAGS_TRENDING, {
|
|
1404
1855
|
path: "/api/hashtags/trending",
|
|
1405
1856
|
query: { limit: params.limit },
|
|
1406
1857
|
...options
|
|
1407
1858
|
});
|
|
1408
|
-
return require_url.pickArray(body, "hashtags");
|
|
1409
1859
|
}
|
|
1410
1860
|
/**
|
|
1411
1861
|
* Загружает страницу постов по хэштегу.
|
|
@@ -1428,160 +1878,11 @@ var HashtagsResource = class extends BaseResource {
|
|
|
1428
1878
|
}
|
|
1429
1879
|
};
|
|
1430
1880
|
//#endregion
|
|
1431
|
-
//#region src/resources/notifications.ts
|
|
1432
|
-
const NOTIFICATION_SETTING_KEYS = [
|
|
1433
|
-
"enabled",
|
|
1434
|
-
"sound",
|
|
1435
|
-
"follows",
|
|
1436
|
-
"wallPosts",
|
|
1437
|
-
"likes",
|
|
1438
|
-
"comments",
|
|
1439
|
-
"mentions"
|
|
1440
|
-
];
|
|
1441
|
-
/**
|
|
1442
|
-
* Сколько идентификаторов уходит в одном запросе на отметку прочтения.
|
|
1443
|
-
*
|
|
1444
|
-
* Столько же отправляет сайт итд.com — значит на сервере, скорее всего, есть ограничение.
|
|
1445
|
-
*/
|
|
1446
|
-
const READ_BATCH_SIZE = 20;
|
|
1447
|
-
/**
|
|
1448
|
-
* Читает настройки уведомлений.
|
|
1449
|
-
*
|
|
1450
|
-
* Сервер отдаёт плоский объект: `enabled`, `sound`, `follows`, `wallPosts`, `likes`,
|
|
1451
|
-
* `comments`, `mentions`. Отсутствующая настройка считается включённой — так же
|
|
1452
|
-
* ведёт себя сайт итд.com.
|
|
1453
|
-
*/
|
|
1454
|
-
function readSettings(body) {
|
|
1455
|
-
const settings = {};
|
|
1456
|
-
for (const key of NOTIFICATION_SETTING_KEYS) settings[key] = require_url.pickBoolean(body, key, true);
|
|
1457
|
-
return settings;
|
|
1458
|
-
}
|
|
1459
|
-
/**
|
|
1460
|
-
* Уведомления: список, счётчик, отметки о прочтении, настройки.
|
|
1461
|
-
*
|
|
1462
|
-
* Доступна как `itd.notifications`. Все уведомления приведены к единой форме, поэтому
|
|
1463
|
-
* объекты отсюда и из потока событий можно складывать в один список.
|
|
1464
|
-
*/
|
|
1465
|
-
var NotificationsResource = class extends BaseResource {
|
|
1466
|
-
/** Уведомления: `/api/notifications/`, пагинация по смещению. */
|
|
1467
|
-
#list = this.paginated({
|
|
1468
|
-
operationId: "notifications.list",
|
|
1469
|
-
path: () => "/api/notifications/",
|
|
1470
|
-
query: (p) => ({ limit: p.limit }),
|
|
1471
|
-
start: (p) => ({ offset: p.offset ?? 0 }),
|
|
1472
|
-
read: (body, state) => {
|
|
1473
|
-
const page = readOffsetPage(body, "notifications", state.offset ?? 0);
|
|
1474
|
-
return {
|
|
1475
|
-
...page,
|
|
1476
|
-
items: page.items.map(require_url.normalizeNotification)
|
|
1477
|
-
};
|
|
1478
|
-
},
|
|
1479
|
-
mode: PaginationMode.Offset
|
|
1480
|
-
});
|
|
1481
|
-
/**
|
|
1482
|
-
* Загружает страницу уведомлений.
|
|
1483
|
-
*
|
|
1484
|
-
* Пагинация здесь основана на смещении.
|
|
1485
|
-
*
|
|
1486
|
-
* @example
|
|
1487
|
-
* ```ts
|
|
1488
|
-
* const page = await itd.notifications.list({ limit: 20 });
|
|
1489
|
-
* const next = await itd.notifications.list({ limit: 20, offset: page.nextOffset });
|
|
1490
|
-
* ```
|
|
1491
|
-
*/
|
|
1492
|
-
list(params = {}, options = {}) {
|
|
1493
|
-
return this.#list.list(params, options);
|
|
1494
|
-
}
|
|
1495
|
-
/**
|
|
1496
|
-
* Перебирает уведомления.
|
|
1497
|
-
*
|
|
1498
|
-
* @example
|
|
1499
|
-
* ```ts
|
|
1500
|
-
* for await (const notification of itd.notifications.iterate()) {
|
|
1501
|
-
* console.log(formatNotificationText(notification));
|
|
1502
|
-
* }
|
|
1503
|
-
* ```
|
|
1504
|
-
*/
|
|
1505
|
-
iterate(params = {}, options = {}) {
|
|
1506
|
-
return this.#list.iterate(params, options);
|
|
1507
|
-
}
|
|
1508
|
-
/** Загружает число непрочитанных уведомлений. */
|
|
1509
|
-
async count(options = {}) {
|
|
1510
|
-
const body = await this.http.operation("notifications.count", {
|
|
1511
|
-
path: "/api/notifications/count",
|
|
1512
|
-
...options
|
|
1513
|
-
});
|
|
1514
|
-
return require_url.pickNumber(body, "count", 0);
|
|
1515
|
-
}
|
|
1516
|
-
/**
|
|
1517
|
-
* Отмечает уведомление прочитанным.
|
|
1518
|
-
*
|
|
1519
|
-
* @returns сколько записей отметил сервер
|
|
1520
|
-
*/
|
|
1521
|
-
async markRead(notificationId, options = {}) {
|
|
1522
|
-
const body = await this.http.operation("notifications.markRead", {
|
|
1523
|
-
path: `/api/notifications/${require_cookies.encodePathSegment(notificationId, "notificationId")}/read`,
|
|
1524
|
-
...options
|
|
1525
|
-
});
|
|
1526
|
-
return require_url.pickNumber(body, "markedCount", 0);
|
|
1527
|
-
}
|
|
1528
|
-
/**
|
|
1529
|
-
* Отмечает прочитанными сразу несколько уведомлений.
|
|
1530
|
-
*
|
|
1531
|
-
* Список автоматически режется на части по 20 идентификаторов — столько же отправляет
|
|
1532
|
-
* сайт итд.com, поэтому на сервере вероятен предел. Части уходят последовательно,
|
|
1533
|
-
* результат суммируется.
|
|
1534
|
-
*
|
|
1535
|
-
* @returns сколько записей отметил сервер суммарно
|
|
1536
|
-
*/
|
|
1537
|
-
async markReadBatch(ids, options = {}) {
|
|
1538
|
-
let marked = 0;
|
|
1539
|
-
for (let index = 0; index < ids.length; index += READ_BATCH_SIZE) {
|
|
1540
|
-
const chunk = ids.slice(index, index + READ_BATCH_SIZE);
|
|
1541
|
-
const body = await this.http.operation("notifications.markReadBatch", {
|
|
1542
|
-
path: "/api/notifications/read-batch",
|
|
1543
|
-
body: { ids: chunk },
|
|
1544
|
-
...options
|
|
1545
|
-
});
|
|
1546
|
-
marked += require_url.pickNumber(body, "markedCount", 0);
|
|
1547
|
-
}
|
|
1548
|
-
return marked;
|
|
1549
|
-
}
|
|
1550
|
-
/** Отмечает прочитанными все уведомления. */
|
|
1551
|
-
async markAllRead(options = {}) {
|
|
1552
|
-
const body = await this.http.operation("notifications.markAllRead", {
|
|
1553
|
-
path: "/api/notifications/read-all",
|
|
1554
|
-
...options
|
|
1555
|
-
});
|
|
1556
|
-
return require_url.pickNumber(body, "markedCount", 0);
|
|
1557
|
-
}
|
|
1558
|
-
/** Загружает настройки уведомлений. */
|
|
1559
|
-
async getSettings(options = {}) {
|
|
1560
|
-
return readSettings(await this.http.operation("notifications.getSettings", {
|
|
1561
|
-
path: "/api/notifications/settings",
|
|
1562
|
-
...options
|
|
1563
|
-
}));
|
|
1564
|
-
}
|
|
1565
|
-
/**
|
|
1566
|
-
* Обновляет настройки уведомлений.
|
|
1567
|
-
*
|
|
1568
|
-
* Отправляются только изменяемые поля, в том же виде, в каком сервер их возвращает.
|
|
1569
|
-
*/
|
|
1570
|
-
async updateSettings(input, options = {}) {
|
|
1571
|
-
const payload = {};
|
|
1572
|
-
for (const key of NOTIFICATION_SETTING_KEYS) {
|
|
1573
|
-
const value = input[key];
|
|
1574
|
-
if (value !== void 0) payload[key] = value;
|
|
1575
|
-
}
|
|
1576
|
-
return readSettings(await this.http.operation("notifications.updateSettings", {
|
|
1577
|
-
path: "/api/notifications/settings",
|
|
1578
|
-
body: payload,
|
|
1579
|
-
...options
|
|
1580
|
-
}));
|
|
1581
|
-
}
|
|
1582
|
-
};
|
|
1583
|
-
//#endregion
|
|
1584
1881
|
//#region src/resources/platform.ts
|
|
1882
|
+
const PLATFORM_CHANGELOG = require_url.defineBuiltInOperation("platform.changelog", (body) => Array.isArray(body) ? body : []);
|
|
1883
|
+
const PLATFORM_ANNOUNCEMENTS = require_url.defineBuiltInOperation("platform.announcements", (body) => require_url.pickArray(body, "announcements"));
|
|
1884
|
+
const PLATFORM_VERSION = require_url.passthroughOperation("platform.version");
|
|
1885
|
+
const PLATFORM_PORTAL = require_url.passthroughOperation("platform.portal");
|
|
1585
1886
|
/**
|
|
1586
1887
|
* Сведения о платформе: версии приложений, изменения, анонсы, баннер события.
|
|
1587
1888
|
*
|
|
@@ -1606,31 +1907,29 @@ var PlatformResource = class extends BaseResource {
|
|
|
1606
1907
|
* ```
|
|
1607
1908
|
*/
|
|
1608
1909
|
version(options = {}) {
|
|
1609
|
-
return this.http.
|
|
1910
|
+
return this.http.execute(PLATFORM_VERSION, {
|
|
1610
1911
|
path: "/api/platform/version",
|
|
1611
1912
|
skipAuth: true,
|
|
1612
1913
|
...options
|
|
1613
1914
|
});
|
|
1614
1915
|
}
|
|
1615
1916
|
/** Загружает журнал изменений. */
|
|
1616
|
-
|
|
1617
|
-
|
|
1917
|
+
changelog(options = {}) {
|
|
1918
|
+
return this.http.execute(PLATFORM_CHANGELOG, {
|
|
1618
1919
|
path: "/api/platform/changelog",
|
|
1619
1920
|
...options
|
|
1620
1921
|
});
|
|
1621
|
-
return Array.isArray(body) ? body : [];
|
|
1622
1922
|
}
|
|
1623
1923
|
/** Загружает анонсы платформы. */
|
|
1624
|
-
|
|
1625
|
-
|
|
1924
|
+
announcements(options = {}) {
|
|
1925
|
+
return this.http.execute(PLATFORM_ANNOUNCEMENTS, {
|
|
1626
1926
|
path: "/api/platform/announcements",
|
|
1627
1927
|
...options
|
|
1628
1928
|
});
|
|
1629
|
-
return require_url.pickArray(body, "announcements");
|
|
1630
1929
|
}
|
|
1631
1930
|
/** Загружает баннер текущего события — виджет «портал». */
|
|
1632
1931
|
portal(options = {}) {
|
|
1633
|
-
return this.http.
|
|
1932
|
+
return this.http.execute(PLATFORM_PORTAL, {
|
|
1634
1933
|
path: "/api/v1/portal",
|
|
1635
1934
|
...options
|
|
1636
1935
|
});
|
|
@@ -2347,7 +2646,7 @@ function autoSpans(text, options = {}) {
|
|
|
2347
2646
|
});
|
|
2348
2647
|
}
|
|
2349
2648
|
if (includeLinks) spans.push(...links);
|
|
2350
|
-
if (includeHashtags || includeMentions) for (const match of text.matchAll(/#([\p{L}\p{M}\p{N}_]+)|@([a-z\d][a-z\d
|
|
2649
|
+
if (includeHashtags || includeMentions) for (const match of text.matchAll(/#([\p{L}\p{M}\p{N}_]+)|@([a-z\d](?:[a-z\d_]*[a-z\d])?)/giu)) {
|
|
2351
2650
|
if (match.index === void 0) continue;
|
|
2352
2651
|
const offset = match.index;
|
|
2353
2652
|
const previous = previousCodePoint(text, offset);
|
|
@@ -2355,7 +2654,7 @@ function autoSpans(text, options = {}) {
|
|
|
2355
2654
|
const end = offset + match[0].length;
|
|
2356
2655
|
if (links.some((span) => overlaps(span, offset, end))) continue;
|
|
2357
2656
|
const hashtag = match[1];
|
|
2358
|
-
const username = match[2]
|
|
2657
|
+
const username = match[2];
|
|
2359
2658
|
if (hashtag !== void 0 && includeHashtags) spans.push({
|
|
2360
2659
|
type: require_url.SpanType.Hashtag,
|
|
2361
2660
|
offset,
|
|
@@ -2748,6 +3047,24 @@ function resolvePostUpdate(input) {
|
|
|
2748
3047
|
}
|
|
2749
3048
|
//#endregion
|
|
2750
3049
|
//#region src/resources/posts.ts
|
|
3050
|
+
const POSTS_LIST = pageOperation("posts.list", (body) => readCursorPage(body, "posts"));
|
|
3051
|
+
const POSTS_BY_USER = pageOperation("posts.byUser", (body) => readCursorPage(body, "posts"));
|
|
3052
|
+
const POSTS_LIKED_BY_USER = pageOperation("posts.likedByUser", (body) => readCursorPage(body, "posts"));
|
|
3053
|
+
const POSTS_COMMENTS = pageOperation("posts.comments", (body) => readFlatCursorPage(body, "comments"));
|
|
3054
|
+
const POSTS_STATS = require_url.defineBuiltInOperation("posts.stats", (body) => require_url.pickArray(body, "posts"));
|
|
3055
|
+
const POSTS_CREATE = require_url.passthroughOperation("posts.create");
|
|
3056
|
+
const POSTS_GET = require_url.passthroughOperation("posts.get");
|
|
3057
|
+
const POSTS_UPDATE = require_url.passthroughOperation("posts.update");
|
|
3058
|
+
const POSTS_RESTORE = require_url.passthroughOperation("posts.restore");
|
|
3059
|
+
const POSTS_LIKE = require_url.passthroughOperation("posts.like");
|
|
3060
|
+
const POSTS_UNLIKE = require_url.passthroughOperation("posts.unlike");
|
|
3061
|
+
const POSTS_REPOST = require_url.passthroughOperation("posts.repost");
|
|
3062
|
+
const POSTS_PIN = require_url.passthroughOperation("posts.pin");
|
|
3063
|
+
const POSTS_UNPIN = require_url.passthroughOperation("posts.unpin");
|
|
3064
|
+
const POSTS_VOTE = require_url.passthroughOperation("posts.vote");
|
|
3065
|
+
const POSTS_COMMENT = require_url.passthroughOperation("posts.comment");
|
|
3066
|
+
const POSTS_REMOVE = require_url.voidOperation("posts.remove");
|
|
3067
|
+
const POSTS_UNREPOST = require_url.voidOperation("posts.unrepost");
|
|
2751
3068
|
/** Курсорная позиция из параметров: если курсор задан — с него, иначе с начала. */
|
|
2752
3069
|
function cursorStart(params) {
|
|
2753
3070
|
return params.cursor ? { cursor: params.cursor } : {};
|
|
@@ -2761,48 +3078,44 @@ var PostsResource = class extends BaseResource {
|
|
|
2761
3078
|
#uploadFiles;
|
|
2762
3079
|
/** Лента: `/api/posts`, курсорная пагинация. */
|
|
2763
3080
|
#feed = this.paginated({
|
|
2764
|
-
|
|
3081
|
+
operation: POSTS_LIST,
|
|
2765
3082
|
path: () => "/api/posts",
|
|
2766
3083
|
query: (p) => ({
|
|
2767
3084
|
tab: p.tab,
|
|
2768
3085
|
limit: p.limit
|
|
2769
3086
|
}),
|
|
2770
3087
|
start: cursorStart,
|
|
2771
|
-
read: (body) => readCursorPage(body, "posts"),
|
|
2772
3088
|
mode: PaginationMode.Cursor
|
|
2773
3089
|
});
|
|
2774
3090
|
/** Стена пользователя: `/api/posts/user/{user}`. */
|
|
2775
3091
|
#wall = this.paginated({
|
|
2776
|
-
|
|
2777
|
-
path: (p) => `/api/posts/user/${
|
|
3092
|
+
operation: POSTS_BY_USER,
|
|
3093
|
+
path: (p) => `/api/posts/user/${require_redact.encodePathSegment(p.user, "user")}`,
|
|
2778
3094
|
query: (p) => ({
|
|
2779
3095
|
limit: p.limit,
|
|
2780
3096
|
sort: p.sort,
|
|
2781
3097
|
pinnedPostId: p.pinnedPostId
|
|
2782
3098
|
}),
|
|
2783
3099
|
start: cursorStart,
|
|
2784
|
-
read: (body) => readCursorPage(body, "posts"),
|
|
2785
3100
|
mode: PaginationMode.Cursor
|
|
2786
3101
|
});
|
|
2787
3102
|
/** Понравившиеся посты пользователя: `/api/posts/user/{user}/liked`. */
|
|
2788
3103
|
#liked = this.paginated({
|
|
2789
|
-
|
|
2790
|
-
path: (p) => `/api/posts/user/${
|
|
3104
|
+
operation: POSTS_LIKED_BY_USER,
|
|
3105
|
+
path: (p) => `/api/posts/user/${require_redact.encodePathSegment(p.user, "user")}/liked`,
|
|
2791
3106
|
query: (p) => ({ limit: p.limit }),
|
|
2792
3107
|
start: cursorStart,
|
|
2793
|
-
read: (body) => readCursorPage(body, "posts"),
|
|
2794
3108
|
mode: PaginationMode.Cursor
|
|
2795
3109
|
});
|
|
2796
3110
|
/** Комментарии к посту: курсор лежит рядом со списком, поэтому свой reader. */
|
|
2797
3111
|
#comments = this.paginated({
|
|
2798
|
-
|
|
2799
|
-
path: (p) => `/api/posts/${
|
|
3112
|
+
operation: POSTS_COMMENTS,
|
|
3113
|
+
path: (p) => `/api/posts/${require_redact.encodePathSegment(p.postId, "postId")}/comments`,
|
|
2800
3114
|
query: (p) => ({
|
|
2801
3115
|
limit: p.limit,
|
|
2802
3116
|
sort: p.sort
|
|
2803
3117
|
}),
|
|
2804
3118
|
start: cursorStart,
|
|
2805
|
-
read: (body) => readFlatCursorPage(body, "comments"),
|
|
2806
3119
|
mode: PaginationMode.Cursor
|
|
2807
3120
|
});
|
|
2808
3121
|
constructor(http, deps) {
|
|
@@ -2849,7 +3162,7 @@ var PostsResource = class extends BaseResource {
|
|
|
2849
3162
|
async create(input, options = {}) {
|
|
2850
3163
|
const data = resolvePost(input);
|
|
2851
3164
|
const attachmentIds = await this.#collectAttachments(data, options);
|
|
2852
|
-
return this.http.
|
|
3165
|
+
return this.http.execute(POSTS_CREATE, {
|
|
2853
3166
|
path: "/api/posts",
|
|
2854
3167
|
body: {
|
|
2855
3168
|
content: data.content ?? "",
|
|
@@ -2867,8 +3180,8 @@ var PostsResource = class extends BaseResource {
|
|
|
2867
3180
|
* В отличие от списков, здесь у поста заполнено поле `comments`.
|
|
2868
3181
|
*/
|
|
2869
3182
|
get(postId, options = {}) {
|
|
2870
|
-
return this.http.
|
|
2871
|
-
path: `/api/posts/${
|
|
3183
|
+
return this.http.execute(POSTS_GET, {
|
|
3184
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}`,
|
|
2872
3185
|
...options
|
|
2873
3186
|
});
|
|
2874
3187
|
}
|
|
@@ -2881,8 +3194,8 @@ var PostsResource = class extends BaseResource {
|
|
|
2881
3194
|
*/
|
|
2882
3195
|
update(postId, input, options = {}) {
|
|
2883
3196
|
const data = resolvePostUpdate(input);
|
|
2884
|
-
return this.http.
|
|
2885
|
-
path: `/api/posts/${
|
|
3197
|
+
return this.http.execute(POSTS_UPDATE, {
|
|
3198
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}`,
|
|
2886
3199
|
body: {
|
|
2887
3200
|
content: data.content,
|
|
2888
3201
|
...data.spans ? { spans: data.spans } : {}
|
|
@@ -2892,29 +3205,29 @@ var PostsResource = class extends BaseResource {
|
|
|
2892
3205
|
}
|
|
2893
3206
|
/** Удаляет пост. Восстановить его можно через {@link restore}. */
|
|
2894
3207
|
remove(postId, options = {}) {
|
|
2895
|
-
return this.
|
|
2896
|
-
path: `/api/posts/${
|
|
3208
|
+
return this.voidOperation(POSTS_REMOVE, {
|
|
3209
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}`,
|
|
2897
3210
|
...options
|
|
2898
3211
|
});
|
|
2899
3212
|
}
|
|
2900
3213
|
/** Восстанавливает удалённый пост. */
|
|
2901
3214
|
restore(postId, options = {}) {
|
|
2902
|
-
return this.http.
|
|
2903
|
-
path: `/api/posts/${
|
|
3215
|
+
return this.http.execute(POSTS_RESTORE, {
|
|
3216
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/restore`,
|
|
2904
3217
|
...options
|
|
2905
3218
|
});
|
|
2906
3219
|
}
|
|
2907
3220
|
/** Ставит реакцию на пост. */
|
|
2908
3221
|
like(postId, options = {}) {
|
|
2909
|
-
return this.http.
|
|
2910
|
-
path: `/api/posts/${
|
|
3222
|
+
return this.http.execute(POSTS_LIKE, {
|
|
3223
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/like`,
|
|
2911
3224
|
...options
|
|
2912
3225
|
});
|
|
2913
3226
|
}
|
|
2914
3227
|
/** Убирает реакцию с поста. */
|
|
2915
3228
|
unlike(postId, options = {}) {
|
|
2916
|
-
return this.http.
|
|
2917
|
-
path: `/api/posts/${
|
|
3229
|
+
return this.http.execute(POSTS_UNLIKE, {
|
|
3230
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/like`,
|
|
2918
3231
|
...options
|
|
2919
3232
|
});
|
|
2920
3233
|
}
|
|
@@ -2925,30 +3238,30 @@ var PostsResource = class extends BaseResource {
|
|
|
2925
3238
|
* для файлов здесь нет.
|
|
2926
3239
|
*/
|
|
2927
3240
|
repost(postId, content = "", options = {}) {
|
|
2928
|
-
return this.http.
|
|
2929
|
-
path: `/api/posts/${
|
|
3241
|
+
return this.http.execute(POSTS_REPOST, {
|
|
3242
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/repost`,
|
|
2930
3243
|
body: { content },
|
|
2931
3244
|
...options
|
|
2932
3245
|
});
|
|
2933
3246
|
}
|
|
2934
3247
|
/** Отменяет репост. */
|
|
2935
3248
|
unrepost(postId, options = {}) {
|
|
2936
|
-
return this.
|
|
2937
|
-
path: `/api/posts/${
|
|
3249
|
+
return this.voidOperation(POSTS_UNREPOST, {
|
|
3250
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/repost`,
|
|
2938
3251
|
...options
|
|
2939
3252
|
});
|
|
2940
3253
|
}
|
|
2941
3254
|
/** Закрепляет пост в профиле. */
|
|
2942
3255
|
pin(postId, options = {}) {
|
|
2943
|
-
return this.http.
|
|
2944
|
-
path: `/api/posts/${
|
|
3256
|
+
return this.http.execute(POSTS_PIN, {
|
|
3257
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/pin`,
|
|
2945
3258
|
...options
|
|
2946
3259
|
});
|
|
2947
3260
|
}
|
|
2948
3261
|
/** Открепляет пост. */
|
|
2949
3262
|
unpin(postId, options = {}) {
|
|
2950
|
-
return this.http.
|
|
2951
|
-
path: `/api/posts/${
|
|
3263
|
+
return this.http.execute(POSTS_UNPIN, {
|
|
3264
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/pin`,
|
|
2952
3265
|
...options
|
|
2953
3266
|
});
|
|
2954
3267
|
}
|
|
@@ -2958,20 +3271,19 @@ var PostsResource = class extends BaseResource {
|
|
|
2958
3271
|
* @param optionIds выбранные варианты; несколько допустимы только при `multipleChoice`
|
|
2959
3272
|
*/
|
|
2960
3273
|
vote(postId, optionIds, options = {}) {
|
|
2961
|
-
return this.http.
|
|
2962
|
-
path: `/api/posts/${
|
|
3274
|
+
return this.http.execute(POSTS_VOTE, {
|
|
3275
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/poll/vote`,
|
|
2963
3276
|
body: { optionIds },
|
|
2964
3277
|
...options
|
|
2965
3278
|
});
|
|
2966
3279
|
}
|
|
2967
3280
|
/** Запрашивает счётчики сразу для нескольких постов. */
|
|
2968
|
-
|
|
2969
|
-
|
|
3281
|
+
stats(ids, options = {}) {
|
|
3282
|
+
return this.http.execute(POSTS_STATS, {
|
|
2970
3283
|
path: "/api/posts/stats",
|
|
2971
3284
|
body: { ids },
|
|
2972
3285
|
...options
|
|
2973
3286
|
});
|
|
2974
|
-
return require_url.pickArray(body, "posts");
|
|
2975
3287
|
}
|
|
2976
3288
|
/**
|
|
2977
3289
|
* Загружает страницу стены пользователя.
|
|
@@ -3041,8 +3353,8 @@ var PostsResource = class extends BaseResource {
|
|
|
3041
3353
|
async comment(postId, input, options = {}) {
|
|
3042
3354
|
const data = resolveComment(typeof input === "string" ? { content: input } : input);
|
|
3043
3355
|
const attachmentIds = await this.#collectAttachments(data, options);
|
|
3044
|
-
return this.http.
|
|
3045
|
-
path: `/api/posts/${
|
|
3356
|
+
return this.http.execute(POSTS_COMMENT, {
|
|
3357
|
+
path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/comments`,
|
|
3046
3358
|
body: {
|
|
3047
3359
|
content: data.content ?? "",
|
|
3048
3360
|
attachmentIds
|
|
@@ -3162,6 +3474,7 @@ function resolveReport(input) {
|
|
|
3162
3474
|
}
|
|
3163
3475
|
//#endregion
|
|
3164
3476
|
//#region src/resources/reports.ts
|
|
3477
|
+
const REPORTS_CREATE = require_url.passthroughOperation("reports.create");
|
|
3165
3478
|
/**
|
|
3166
3479
|
* Жалобы на контент и пользователей.
|
|
3167
3480
|
*
|
|
@@ -3182,7 +3495,7 @@ var ReportsResource = class extends BaseResource {
|
|
|
3182
3495
|
*/
|
|
3183
3496
|
create(input, options = {}) {
|
|
3184
3497
|
const data = resolveReport(input);
|
|
3185
|
-
return this.http.
|
|
3498
|
+
return this.http.execute(REPORTS_CREATE, {
|
|
3186
3499
|
path: "/api/reports",
|
|
3187
3500
|
body: data,
|
|
3188
3501
|
...options
|
|
@@ -3191,6 +3504,10 @@ var ReportsResource = class extends BaseResource {
|
|
|
3191
3504
|
};
|
|
3192
3505
|
//#endregion
|
|
3193
3506
|
//#region src/resources/search.ts
|
|
3507
|
+
const SEARCH_ALL = require_url.defineBuiltInOperation("search.all", (body) => ({
|
|
3508
|
+
users: require_url.pickArray(body, "users"),
|
|
3509
|
+
hashtags: require_url.pickArray(body, "hashtags")
|
|
3510
|
+
}));
|
|
3194
3511
|
/**
|
|
3195
3512
|
* Глобальный поиск.
|
|
3196
3513
|
*
|
|
@@ -3205,20 +3522,23 @@ var SearchResource = class extends BaseResource {
|
|
|
3205
3522
|
* const { users, hashtags } = await itd.search.all('арт');
|
|
3206
3523
|
* ```
|
|
3207
3524
|
*/
|
|
3208
|
-
|
|
3209
|
-
|
|
3525
|
+
all(query, options = {}) {
|
|
3526
|
+
return this.http.execute(SEARCH_ALL, {
|
|
3210
3527
|
path: "/api/search",
|
|
3211
3528
|
query: { q: query },
|
|
3212
3529
|
...options
|
|
3213
3530
|
});
|
|
3214
|
-
return {
|
|
3215
|
-
users: require_url.pickArray(body, "users"),
|
|
3216
|
-
hashtags: require_url.pickArray(body, "hashtags")
|
|
3217
|
-
};
|
|
3218
3531
|
}
|
|
3219
3532
|
};
|
|
3220
3533
|
//#endregion
|
|
3221
3534
|
//#region src/resources/subscription.ts
|
|
3535
|
+
const SUBSCRIPTION_METHODS = require_url.defineBuiltInOperation("subscription.methods", (body) => Array.isArray(body) ? body : []);
|
|
3536
|
+
const SUBSCRIPTION_STATUS = require_url.passthroughOperation("subscription.status");
|
|
3537
|
+
const SUBSCRIPTION_PAY = require_url.passthroughOperation("subscription.pay");
|
|
3538
|
+
const SUBSCRIPTION_SET_AUTO_RENEWAL = require_url.passthroughOperation("subscription.setAutoRenewal");
|
|
3539
|
+
const SUBSCRIPTION_BIND_CARD = require_url.passthroughOperation("subscription.bindCard");
|
|
3540
|
+
const SUBSCRIPTION_SET_DEFAULT_METHOD = require_url.passthroughOperation("subscription.setDefaultMethod");
|
|
3541
|
+
const SUBSCRIPTION_REMOVE_METHOD = require_url.voidOperation("subscription.removeMethod");
|
|
3222
3542
|
/**
|
|
3223
3543
|
* Подписка и способы оплаты.
|
|
3224
3544
|
*
|
|
@@ -3227,7 +3547,7 @@ var SearchResource = class extends BaseResource {
|
|
|
3227
3547
|
var SubscriptionResource = class extends BaseResource {
|
|
3228
3548
|
/** Загружает состояние подписки и её цену. */
|
|
3229
3549
|
status(options = {}) {
|
|
3230
|
-
return this.http.
|
|
3550
|
+
return this.http.execute(SUBSCRIPTION_STATUS, {
|
|
3231
3551
|
path: "/api/v1/subscription/",
|
|
3232
3552
|
...options
|
|
3233
3553
|
});
|
|
@@ -3238,14 +3558,14 @@ var SubscriptionResource = class extends BaseResource {
|
|
|
3238
3558
|
* Форма ответа в документации API не описана, поэтому тип результата не уточняется.
|
|
3239
3559
|
*/
|
|
3240
3560
|
pay(options = {}) {
|
|
3241
|
-
return this.http.
|
|
3561
|
+
return this.http.execute(SUBSCRIPTION_PAY, {
|
|
3242
3562
|
path: "/api/v1/subscription/pay",
|
|
3243
3563
|
...options
|
|
3244
3564
|
});
|
|
3245
3565
|
}
|
|
3246
3566
|
/** Включает или отключает автопродление. */
|
|
3247
3567
|
setAutoRenewal(enabled, options = {}) {
|
|
3248
|
-
return this.http.
|
|
3568
|
+
return this.http.execute(SUBSCRIPTION_SET_AUTO_RENEWAL, {
|
|
3249
3569
|
path: "/api/v1/subscription/auto-renewal",
|
|
3250
3570
|
body: { enabled },
|
|
3251
3571
|
...options
|
|
@@ -3253,30 +3573,29 @@ var SubscriptionResource = class extends BaseResource {
|
|
|
3253
3573
|
}
|
|
3254
3574
|
/** Запускает привязку карты. */
|
|
3255
3575
|
bindCard(options = {}) {
|
|
3256
|
-
return this.http.
|
|
3576
|
+
return this.http.execute(SUBSCRIPTION_BIND_CARD, {
|
|
3257
3577
|
path: "/api/v1/subscription/bind-card",
|
|
3258
3578
|
...options
|
|
3259
3579
|
});
|
|
3260
3580
|
}
|
|
3261
3581
|
/** Загружает список способов оплаты. Пустой массив, если карт нет. */
|
|
3262
|
-
|
|
3263
|
-
|
|
3582
|
+
methods(options = {}) {
|
|
3583
|
+
return this.http.execute(SUBSCRIPTION_METHODS, {
|
|
3264
3584
|
path: "/api/v1/subscription/methods",
|
|
3265
3585
|
...options
|
|
3266
3586
|
});
|
|
3267
|
-
return Array.isArray(body) ? body : [];
|
|
3268
3587
|
}
|
|
3269
3588
|
/** Делает способ оплаты основным. */
|
|
3270
3589
|
setDefaultMethod(methodId, options = {}) {
|
|
3271
|
-
return this.http.
|
|
3272
|
-
path: `/api/v1/subscription/methods/${
|
|
3590
|
+
return this.http.execute(SUBSCRIPTION_SET_DEFAULT_METHOD, {
|
|
3591
|
+
path: `/api/v1/subscription/methods/${require_redact.encodePathSegment(methodId, "methodId")}/default`,
|
|
3273
3592
|
...options
|
|
3274
3593
|
});
|
|
3275
3594
|
}
|
|
3276
3595
|
/** Удаляет способ оплаты. */
|
|
3277
3596
|
removeMethod(methodId, options = {}) {
|
|
3278
|
-
return this.
|
|
3279
|
-
path: `/api/v1/subscription/methods/${
|
|
3597
|
+
return this.voidOperation(SUBSCRIPTION_REMOVE_METHOD, {
|
|
3598
|
+
path: `/api/v1/subscription/methods/${require_redact.encodePathSegment(methodId, "methodId")}`,
|
|
3280
3599
|
...options
|
|
3281
3600
|
});
|
|
3282
3601
|
}
|
|
@@ -3434,7 +3753,7 @@ var TelemetryBatchImpl = class {
|
|
|
3434
3753
|
close() {
|
|
3435
3754
|
return this.#close(false);
|
|
3436
3755
|
}
|
|
3437
|
-
/** Внутренняя
|
|
3756
|
+
/** Внутренняя отправка данных после вызова `dispose()`. */
|
|
3438
3757
|
closeForDispose() {
|
|
3439
3758
|
this.prepareForDispose();
|
|
3440
3759
|
return this.#close(true);
|
|
@@ -3504,7 +3823,7 @@ var TelemetryResource = class extends BaseResource {
|
|
|
3504
3823
|
}
|
|
3505
3824
|
/** Идентификатор сессии телеметрии, общий для всех событий этого ресурса. */
|
|
3506
3825
|
get sessionId() {
|
|
3507
|
-
this.#sessionId ??=
|
|
3826
|
+
this.#sessionId ??= require_redact.createDeviceId();
|
|
3508
3827
|
return this.#sessionId;
|
|
3509
3828
|
}
|
|
3510
3829
|
/** Отправляет события просмотра постов (`POST /api/v1/i`). */
|
|
@@ -3530,7 +3849,7 @@ var TelemetryResource = class extends BaseResource {
|
|
|
3530
3849
|
},
|
|
3531
3850
|
...requestOptions
|
|
3532
3851
|
};
|
|
3533
|
-
return disposeCleanup ? this.http.cleanupOperation(
|
|
3852
|
+
return disposeCleanup ? this.http.cleanupOperation(TELEMETRY_DWELL, options) : this.http.execute(TELEMETRY_DWELL, options);
|
|
3534
3853
|
}
|
|
3535
3854
|
/** Отправляет события взаимодействия с контентом (`POST /api/v1/x`). */
|
|
3536
3855
|
interaction(entries, telemetryOptions = {}, requestOptions = {}) {
|
|
@@ -3554,7 +3873,7 @@ var TelemetryResource = class extends BaseResource {
|
|
|
3554
3873
|
},
|
|
3555
3874
|
...requestOptions
|
|
3556
3875
|
};
|
|
3557
|
-
return disposeCleanup ? this.http.cleanupOperation(
|
|
3876
|
+
return disposeCleanup ? this.http.cleanupOperation(TELEMETRY_INTERACTION, options) : this.http.execute(TELEMETRY_INTERACTION, options);
|
|
3558
3877
|
}
|
|
3559
3878
|
/** Начинает измерять время просмотра и отправляет результат после `finish()`. */
|
|
3560
3879
|
startView(input, options = {}, requestOptions = {}) {
|
|
@@ -3597,12 +3916,43 @@ var TelemetryResource = class extends BaseResource {
|
|
|
3597
3916
|
function prepareTelemetryForDispose(resource) {
|
|
3598
3917
|
if (resource) TELEMETRY_DISPOSE_PREPARERS.get(resource)?.();
|
|
3599
3918
|
}
|
|
3600
|
-
/**
|
|
3919
|
+
/** Отправляет и закрывает созданные накопители после вызова `dispose()`. @internal */
|
|
3601
3920
|
function closeTelemetryForDispose(resource) {
|
|
3602
3921
|
return resource ? TELEMETRY_DISPOSE_CLOSERS.get(resource)?.() ?? Promise.resolve() : Promise.resolve();
|
|
3603
3922
|
}
|
|
3923
|
+
const TELEMETRY_DWELL = require_url.defineBuiltInOperation("telemetry.dwell");
|
|
3924
|
+
const TELEMETRY_INTERACTION = require_url.defineBuiltInOperation("telemetry.interaction");
|
|
3604
3925
|
//#endregion
|
|
3605
3926
|
//#region src/resources/users.ts
|
|
3927
|
+
const userListOperation = (id) => pageOperation(id, (body) => readPagedPage(body, "users", "followers", "following", "blocked"));
|
|
3928
|
+
const USER_LIST_OPERATIONS = {
|
|
3929
|
+
"users.followers": userListOperation("users.followers"),
|
|
3930
|
+
"users.following": userListOperation("users.following"),
|
|
3931
|
+
"users.blocked": userListOperation("users.blocked")
|
|
3932
|
+
};
|
|
3933
|
+
const USERS_CHECK_USERNAME = require_url.defineBuiltInOperation("users.checkUsername", (body) => require_url.pickBoolean(body, "available"));
|
|
3934
|
+
const USERS_SEARCH = require_url.defineBuiltInOperation("users.search", (body) => require_url.pickArray(body, "users"));
|
|
3935
|
+
const USERS_WHO_TO_FOLLOW = require_url.defineBuiltInOperation("users.whoToFollow", (body) => require_url.pickArray(body, "users"));
|
|
3936
|
+
const USERS_TOP_CLANS = require_url.defineBuiltInOperation("users.topClans", (body) => require_url.pickArray(body, "clans"));
|
|
3937
|
+
const USERS_PINS = require_url.defineBuiltInOperation("users.pins", (body) => ({
|
|
3938
|
+
pins: require_url.pickArray(body, "pins"),
|
|
3939
|
+
activePin: require_url.pickString(body, "activePin") ?? null
|
|
3940
|
+
}));
|
|
3941
|
+
const USERS_ME = require_url.passthroughOperation("users.me");
|
|
3942
|
+
const USERS_UPDATE_ME = require_url.passthroughOperation("users.updateMe");
|
|
3943
|
+
const USERS_CREATE_PROFILE = require_url.passthroughOperation("users.createProfile");
|
|
3944
|
+
const USERS_GET = require_url.passthroughOperation("users.get");
|
|
3945
|
+
const USERS_FOLLOW = require_url.passthroughOperation("users.follow");
|
|
3946
|
+
const USERS_FOLLOW_STATUS = require_url.passthroughOperation("users.followStatus");
|
|
3947
|
+
const USERS_GET_PRIVACY = require_url.passthroughOperation("users.getPrivacy");
|
|
3948
|
+
const USERS_UPDATE_PRIVACY = require_url.passthroughOperation("users.updatePrivacy");
|
|
3949
|
+
const USERS_DEACTIVATE = require_url.voidOperation("users.deactivate");
|
|
3950
|
+
const USERS_RESTORE = require_url.voidOperation("users.restore");
|
|
3951
|
+
const USERS_UNFOLLOW = require_url.voidOperation("users.unfollow");
|
|
3952
|
+
const USERS_BLOCK = require_url.voidOperation("users.block");
|
|
3953
|
+
const USERS_UNBLOCK = require_url.voidOperation("users.unblock");
|
|
3954
|
+
const USERS_SET_PIN = require_url.voidOperation("users.setPin");
|
|
3955
|
+
const USERS_REMOVE_PIN = require_url.voidOperation("users.removePin");
|
|
3606
3956
|
/**
|
|
3607
3957
|
* Пользователи: профили, подписки, блокировки, приватность.
|
|
3608
3958
|
*
|
|
@@ -3623,23 +3973,22 @@ var UsersResource = class extends BaseResource {
|
|
|
3623
3973
|
* сервер его сейчас не читает (см. {@link followers}): когда починят — заработает само.
|
|
3624
3974
|
*/
|
|
3625
3975
|
#userList = this.paginated({
|
|
3626
|
-
|
|
3976
|
+
operation: (p) => p.operation,
|
|
3627
3977
|
path: (p) => p.path,
|
|
3628
3978
|
query: (p) => ({ limit: p.limit }),
|
|
3629
3979
|
start: (p) => p.page !== void 0 ? { page: p.page } : {},
|
|
3630
|
-
read: (body) => readPagedPage(body, "users", "followers", "following", "blocked"),
|
|
3631
3980
|
mode: PaginationMode.Page
|
|
3632
3981
|
});
|
|
3633
3982
|
/** Загружает свой профиль — с подпиской и признаком подтверждённого телефона. */
|
|
3634
3983
|
me(options = {}) {
|
|
3635
|
-
return this.http.
|
|
3984
|
+
return this.http.execute(USERS_ME, {
|
|
3636
3985
|
path: "/api/users/me",
|
|
3637
3986
|
...options
|
|
3638
3987
|
});
|
|
3639
3988
|
}
|
|
3640
3989
|
/** Обновляет свой профиль. Передавайте только изменяемые поля. */
|
|
3641
3990
|
updateMe(input, options = {}) {
|
|
3642
|
-
return this.http.
|
|
3991
|
+
return this.http.execute(USERS_UPDATE_ME, {
|
|
3643
3992
|
path: "/api/users/me",
|
|
3644
3993
|
body: input,
|
|
3645
3994
|
...options
|
|
@@ -3666,21 +4015,21 @@ var UsersResource = class extends BaseResource {
|
|
|
3666
4015
|
}
|
|
3667
4016
|
/** Деактивирует аккаунт. Вернуть его можно через {@link restore}. */
|
|
3668
4017
|
deactivate(options = {}) {
|
|
3669
|
-
return this.
|
|
4018
|
+
return this.voidOperation(USERS_DEACTIVATE, {
|
|
3670
4019
|
path: "/api/users/me",
|
|
3671
4020
|
...options
|
|
3672
4021
|
});
|
|
3673
4022
|
}
|
|
3674
4023
|
/** Восстанавливает деактивированный аккаунт. */
|
|
3675
4024
|
restore(options = {}) {
|
|
3676
|
-
return this.
|
|
4025
|
+
return this.voidOperation(USERS_RESTORE, {
|
|
3677
4026
|
path: "/api/users/me/restore",
|
|
3678
4027
|
...options
|
|
3679
4028
|
});
|
|
3680
4029
|
}
|
|
3681
4030
|
/** Создаёт профиль после регистрации. */
|
|
3682
4031
|
createProfile(input, options = {}) {
|
|
3683
|
-
return this.http.
|
|
4032
|
+
return this.http.execute(USERS_CREATE_PROFILE, {
|
|
3684
4033
|
path: "/api/users/profile",
|
|
3685
4034
|
body: input,
|
|
3686
4035
|
...options
|
|
@@ -3698,23 +4047,22 @@ var UsersResource = class extends BaseResource {
|
|
|
3698
4047
|
* ```
|
|
3699
4048
|
*/
|
|
3700
4049
|
get(user, options = {}) {
|
|
3701
|
-
return this.http.
|
|
3702
|
-
path: `/api/users/${
|
|
4050
|
+
return this.http.execute(USERS_GET, {
|
|
4051
|
+
path: `/api/users/${require_redact.encodePathSegment(user, "user")}`,
|
|
3703
4052
|
...options
|
|
3704
4053
|
});
|
|
3705
4054
|
}
|
|
3706
4055
|
/** Проверяет, свободно ли имя пользователя. */
|
|
3707
|
-
|
|
3708
|
-
|
|
4056
|
+
checkUsername(username, options = {}) {
|
|
4057
|
+
return this.http.execute(USERS_CHECK_USERNAME, {
|
|
3709
4058
|
path: "/api/users/check-username",
|
|
3710
4059
|
query: { username },
|
|
3711
4060
|
...options
|
|
3712
4061
|
});
|
|
3713
|
-
return require_url.pickBoolean(body, "available");
|
|
3714
4062
|
}
|
|
3715
4063
|
/** Ищет пользователей по строке запроса. */
|
|
3716
|
-
|
|
3717
|
-
|
|
4064
|
+
search(query, params = {}, options = {}) {
|
|
4065
|
+
return this.http.execute(USERS_SEARCH, {
|
|
3718
4066
|
path: "/api/users/search",
|
|
3719
4067
|
query: {
|
|
3720
4068
|
q: query,
|
|
@@ -3722,23 +4070,20 @@ var UsersResource = class extends BaseResource {
|
|
|
3722
4070
|
},
|
|
3723
4071
|
...options
|
|
3724
4072
|
});
|
|
3725
|
-
return require_url.pickArray(body, "users");
|
|
3726
4073
|
}
|
|
3727
4074
|
/** Загружает рекомендации, на кого подписаться. */
|
|
3728
|
-
|
|
3729
|
-
|
|
4075
|
+
whoToFollow(options = {}) {
|
|
4076
|
+
return this.http.execute(USERS_WHO_TO_FOLLOW, {
|
|
3730
4077
|
path: "/api/users/suggestions/who-to-follow",
|
|
3731
4078
|
...options
|
|
3732
4079
|
});
|
|
3733
|
-
return require_url.pickArray(body, "users");
|
|
3734
4080
|
}
|
|
3735
4081
|
/** Загружает рейтинг кланов. */
|
|
3736
|
-
|
|
3737
|
-
|
|
4082
|
+
topClans(options = {}) {
|
|
4083
|
+
return this.http.execute(USERS_TOP_CLANS, {
|
|
3738
4084
|
path: "/api/users/stats/top-clans",
|
|
3739
4085
|
...options
|
|
3740
4086
|
});
|
|
3741
|
-
return require_url.pickArray(body, "clans");
|
|
3742
4087
|
}
|
|
3743
4088
|
/**
|
|
3744
4089
|
* Подписывается на пользователя.
|
|
@@ -3746,16 +4091,16 @@ var UsersResource = class extends BaseResource {
|
|
|
3746
4091
|
* У закрытого профиля вместо подписки отправляется заявка — это видно по полю `status`.
|
|
3747
4092
|
*/
|
|
3748
4093
|
follow(user, options = {}) {
|
|
3749
|
-
return this.http.
|
|
3750
|
-
path: `/api/users/${
|
|
4094
|
+
return this.http.execute(USERS_FOLLOW, {
|
|
4095
|
+
path: `/api/users/${require_redact.encodePathSegment(user, "user")}/follow`,
|
|
3751
4096
|
body: {},
|
|
3752
4097
|
...options
|
|
3753
4098
|
});
|
|
3754
4099
|
}
|
|
3755
4100
|
/** Отписывается от пользователя. */
|
|
3756
4101
|
unfollow(user, options = {}) {
|
|
3757
|
-
return this.
|
|
3758
|
-
path: `/api/users/${
|
|
4102
|
+
return this.voidOperation(USERS_UNFOLLOW, {
|
|
4103
|
+
path: `/api/users/${require_redact.encodePathSegment(user, "user")}/follow`,
|
|
3759
4104
|
...options
|
|
3760
4105
|
});
|
|
3761
4106
|
}
|
|
@@ -3771,7 +4116,7 @@ var UsersResource = class extends BaseResource {
|
|
|
3771
4116
|
* на проверенных аккаунтах занижено примерно на 1–4%.
|
|
3772
4117
|
*/
|
|
3773
4118
|
followers(user, params = {}, options = {}) {
|
|
3774
|
-
return this.#userPage("users.followers", `/api/users/${
|
|
4119
|
+
return this.#userPage("users.followers", `/api/users/${require_redact.encodePathSegment(user, "user")}/followers`, params, options);
|
|
3775
4120
|
}
|
|
3776
4121
|
/**
|
|
3777
4122
|
* Перебирает подписчиков.
|
|
@@ -3780,15 +4125,15 @@ var UsersResource = class extends BaseResource {
|
|
|
3780
4125
|
* см. {@link followers}. Метод оставлен на случай, если пагинацию починят.
|
|
3781
4126
|
*/
|
|
3782
4127
|
iterateFollowers(user, params = {}, options = {}) {
|
|
3783
|
-
return this.#userPaginator("users.followers", `/api/users/${
|
|
4128
|
+
return this.#userPaginator("users.followers", `/api/users/${require_redact.encodePathSegment(user, "user")}/followers`, params, options);
|
|
3784
4129
|
}
|
|
3785
4130
|
/** Загружает подписки пользователя. Ограничения те же, что у {@link followers}. */
|
|
3786
4131
|
following(user, params = {}, options = {}) {
|
|
3787
|
-
return this.#userPage("users.following", `/api/users/${
|
|
4132
|
+
return this.#userPage("users.following", `/api/users/${require_redact.encodePathSegment(user, "user")}/following`, params, options);
|
|
3788
4133
|
}
|
|
3789
4134
|
/** Перебирает подписки. Закончится после первых 20 записей — см. {@link followers}. */
|
|
3790
4135
|
iterateFollowing(user, params = {}, options = {}) {
|
|
3791
|
-
return this.#userPaginator("users.following", `/api/users/${
|
|
4136
|
+
return this.#userPaginator("users.following", `/api/users/${require_redact.encodePathSegment(user, "user")}/following`, params, options);
|
|
3792
4137
|
}
|
|
3793
4138
|
/**
|
|
3794
4139
|
* Проверяет, подписаны ли вы, сразу для нескольких пользователей.
|
|
@@ -3802,7 +4147,7 @@ var UsersResource = class extends BaseResource {
|
|
|
3802
4147
|
* ```
|
|
3803
4148
|
*/
|
|
3804
4149
|
followStatus(userIds, options = {}) {
|
|
3805
|
-
return this.http.
|
|
4150
|
+
return this.http.execute(USERS_FOLLOW_STATUS, {
|
|
3806
4151
|
path: "/api/users/follow-status",
|
|
3807
4152
|
body: { userIds },
|
|
3808
4153
|
...options
|
|
@@ -3810,16 +4155,16 @@ var UsersResource = class extends BaseResource {
|
|
|
3810
4155
|
}
|
|
3811
4156
|
/** Блокирует пользователя. */
|
|
3812
4157
|
block(user, options = {}) {
|
|
3813
|
-
return this.
|
|
3814
|
-
path: `/api/users/${
|
|
4158
|
+
return this.voidOperation(USERS_BLOCK, {
|
|
4159
|
+
path: `/api/users/${require_redact.encodePathSegment(user, "user")}/block`,
|
|
3815
4160
|
body: {},
|
|
3816
4161
|
...options
|
|
3817
4162
|
});
|
|
3818
4163
|
}
|
|
3819
4164
|
/** Снимает блокировку. */
|
|
3820
4165
|
unblock(user, options = {}) {
|
|
3821
|
-
return this.
|
|
3822
|
-
path: `/api/users/${
|
|
4166
|
+
return this.voidOperation(USERS_UNBLOCK, {
|
|
4167
|
+
path: `/api/users/${require_redact.encodePathSegment(user, "user")}/block`,
|
|
3823
4168
|
...options
|
|
3824
4169
|
});
|
|
3825
4170
|
}
|
|
@@ -3833,14 +4178,14 @@ var UsersResource = class extends BaseResource {
|
|
|
3833
4178
|
}
|
|
3834
4179
|
/** Загружает настройки приватности. */
|
|
3835
4180
|
getPrivacy(options = {}) {
|
|
3836
|
-
return this.http.
|
|
4181
|
+
return this.http.execute(USERS_GET_PRIVACY, {
|
|
3837
4182
|
path: "/api/users/me/privacy",
|
|
3838
4183
|
...options
|
|
3839
4184
|
});
|
|
3840
4185
|
}
|
|
3841
4186
|
/** Обновляет настройки приватности. Передавайте только изменяемые поля. */
|
|
3842
4187
|
updatePrivacy(input, options = {}) {
|
|
3843
|
-
return this.http.
|
|
4188
|
+
return this.http.execute(USERS_UPDATE_PRIVACY, {
|
|
3844
4189
|
path: "/api/users/me/privacy",
|
|
3845
4190
|
body: input,
|
|
3846
4191
|
...options
|
|
@@ -3851,19 +4196,15 @@ var UsersResource = class extends BaseResource {
|
|
|
3851
4196
|
*
|
|
3852
4197
|
* `activePin` — строка-идентификатор, а не объект.
|
|
3853
4198
|
*/
|
|
3854
|
-
|
|
3855
|
-
|
|
4199
|
+
pins(options = {}) {
|
|
4200
|
+
return this.http.execute(USERS_PINS, {
|
|
3856
4201
|
path: "/api/users/me/pins",
|
|
3857
4202
|
...options
|
|
3858
4203
|
});
|
|
3859
|
-
return {
|
|
3860
|
-
pins: require_url.pickArray(body, "pins"),
|
|
3861
|
-
activePin: require_url.pickString(body, "activePin") ?? null
|
|
3862
|
-
};
|
|
3863
4204
|
}
|
|
3864
4205
|
/** Выбирает активный значок профиля. */
|
|
3865
4206
|
setPin(slug, options = {}) {
|
|
3866
|
-
return this.
|
|
4207
|
+
return this.voidOperation(USERS_SET_PIN, {
|
|
3867
4208
|
path: "/api/users/me/pin",
|
|
3868
4209
|
body: { slug },
|
|
3869
4210
|
...options
|
|
@@ -3871,7 +4212,7 @@ var UsersResource = class extends BaseResource {
|
|
|
3871
4212
|
}
|
|
3872
4213
|
/** Снимает активный значок. */
|
|
3873
4214
|
removePin(options = {}) {
|
|
3874
|
-
return this.
|
|
4215
|
+
return this.voidOperation(USERS_REMOVE_PIN, {
|
|
3875
4216
|
path: "/api/users/me/pin",
|
|
3876
4217
|
...options
|
|
3877
4218
|
});
|
|
@@ -3879,20 +4220,22 @@ var UsersResource = class extends BaseResource {
|
|
|
3879
4220
|
#userPage(operationId, path, params, options) {
|
|
3880
4221
|
return this.#userList.list({
|
|
3881
4222
|
...params,
|
|
3882
|
-
operationId,
|
|
4223
|
+
operation: USER_LIST_OPERATIONS[operationId],
|
|
3883
4224
|
path
|
|
3884
4225
|
}, options);
|
|
3885
4226
|
}
|
|
3886
4227
|
#userPaginator(operationId, path, params, options) {
|
|
3887
4228
|
return this.#userList.iterate({
|
|
3888
4229
|
...params,
|
|
3889
|
-
operationId,
|
|
4230
|
+
operation: USER_LIST_OPERATIONS[operationId],
|
|
3890
4231
|
path
|
|
3891
4232
|
}, options);
|
|
3892
4233
|
}
|
|
3893
4234
|
};
|
|
3894
4235
|
//#endregion
|
|
3895
4236
|
//#region src/resources/verification.ts
|
|
4237
|
+
const VERIFICATION_STATUS = require_url.passthroughOperation("verification.status");
|
|
4238
|
+
const VERIFICATION_SUBMIT = require_url.passthroughOperation("verification.submit");
|
|
3896
4239
|
/**
|
|
3897
4240
|
* Верификация профиля.
|
|
3898
4241
|
*
|
|
@@ -3901,14 +4244,14 @@ var UsersResource = class extends BaseResource {
|
|
|
3901
4244
|
var VerificationResource = class extends BaseResource {
|
|
3902
4245
|
/** Загружает статус заявки. Значение `none` означает, что заявка не подавалась. */
|
|
3903
4246
|
status(options = {}) {
|
|
3904
|
-
return this.http.
|
|
4247
|
+
return this.http.execute(VERIFICATION_STATUS, {
|
|
3905
4248
|
path: "/api/verification/status",
|
|
3906
4249
|
...options
|
|
3907
4250
|
});
|
|
3908
4251
|
}
|
|
3909
4252
|
/** Подаёт заявку на верификацию с видео. */
|
|
3910
4253
|
submit(videoUrl, options = {}) {
|
|
3911
|
-
return this.http.
|
|
4254
|
+
return this.http.execute(VERIFICATION_SUBMIT, {
|
|
3912
4255
|
path: "/api/verification/submit",
|
|
3913
4256
|
body: { videoUrl },
|
|
3914
4257
|
...options
|
|
@@ -3953,11 +4296,11 @@ function createResources(deps) {
|
|
|
3953
4296
|
return comments;
|
|
3954
4297
|
},
|
|
3955
4298
|
get files() {
|
|
3956
|
-
files ??= new FilesResource(deps.http, {
|
|
4299
|
+
files ??= new FilesResource(deps.http, { files: deps.files });
|
|
3957
4300
|
return files;
|
|
3958
4301
|
},
|
|
3959
4302
|
get notifications() {
|
|
3960
|
-
notifications ??=
|
|
4303
|
+
notifications ??= deps.createNotifications(deps.http);
|
|
3961
4304
|
return notifications;
|
|
3962
4305
|
},
|
|
3963
4306
|
get hashtags() {
|
|
@@ -4021,7 +4364,7 @@ function fromStream(factory, options = {}) {
|
|
|
4021
4364
|
throw new require_errors.ItdFileError("не удалось открыть поток вложения", {
|
|
4022
4365
|
reason: require_errors.ItdFileErrorReason.Read,
|
|
4023
4366
|
retryable: true,
|
|
4024
|
-
cause: error
|
|
4367
|
+
cause: require_redact.redactErrorCause(error)
|
|
4025
4368
|
});
|
|
4026
4369
|
}
|
|
4027
4370
|
const content = require_options.isReadableByteStream(opened) ? { stream: opened } : opened;
|
|
@@ -4308,16 +4651,16 @@ Object.defineProperty(exports, "ExtensibleOperationCatalog", {
|
|
|
4308
4651
|
return ExtensibleOperationCatalog;
|
|
4309
4652
|
}
|
|
4310
4653
|
});
|
|
4311
|
-
Object.defineProperty(exports, "
|
|
4654
|
+
Object.defineProperty(exports, "IMAGE_MIME_TYPES", {
|
|
4312
4655
|
enumerable: true,
|
|
4313
4656
|
get: function() {
|
|
4314
|
-
return
|
|
4657
|
+
return IMAGE_MIME_TYPES;
|
|
4315
4658
|
}
|
|
4316
4659
|
});
|
|
4317
|
-
Object.defineProperty(exports, "
|
|
4660
|
+
Object.defineProperty(exports, "NotificationsResource", {
|
|
4318
4661
|
enumerable: true,
|
|
4319
4662
|
get: function() {
|
|
4320
|
-
return
|
|
4663
|
+
return NotificationsResource;
|
|
4321
4664
|
}
|
|
4322
4665
|
});
|
|
4323
4666
|
Object.defineProperty(exports, "PaginationMode", {
|
|
@@ -4356,6 +4699,12 @@ Object.defineProperty(exports, "comment", {
|
|
|
4356
4699
|
return comment;
|
|
4357
4700
|
}
|
|
4358
4701
|
});
|
|
4702
|
+
Object.defineProperty(exports, "createFeatureHost", {
|
|
4703
|
+
enumerable: true,
|
|
4704
|
+
get: function() {
|
|
4705
|
+
return createFeatureHost;
|
|
4706
|
+
}
|
|
4707
|
+
});
|
|
4359
4708
|
Object.defineProperty(exports, "createResources", {
|
|
4360
4709
|
enumerable: true,
|
|
4361
4710
|
get: function() {
|
|
@@ -4459,4 +4808,4 @@ Object.defineProperty(exports, "utcStampToIso", {
|
|
|
4459
4808
|
}
|
|
4460
4809
|
});
|
|
4461
4810
|
|
|
4462
|
-
//# sourceMappingURL=render-
|
|
4811
|
+
//# sourceMappingURL=render-DNTESBSl.cjs.map
|