itd-api 0.0.7 → 0.0.8
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 +20 -0
- package/dist/{chunk-3RNNZJZ4.cjs → chunk-ATCZ4T2K.cjs} +1055 -971
- package/dist/chunk-ATCZ4T2K.cjs.map +1 -0
- package/dist/{chunk-CG4SERVM.js → chunk-JIYN33FG.js} +1055 -971
- package/dist/chunk-JIYN33FG.js.map +1 -0
- package/dist/{index-DNFPX_Z1.d.cts → index-Duh31Wnx.d.cts} +362 -329
- package/dist/{index-DNFPX_Z1.d.ts → index-Duh31Wnx.d.ts} +362 -329
- package/dist/index.cjs +89 -89
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node.cjs +116 -95
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +30 -9
- package/dist/node.js.map +1 -1
- package/package.json +8 -4
- package/dist/chunk-3RNNZJZ4.cjs.map +0 -1
- package/dist/chunk-CG4SERVM.js.map +0 -1
|
@@ -1295,13 +1295,23 @@ function readAccessToken(payload) {
|
|
|
1295
1295
|
const token = payload.accessToken;
|
|
1296
1296
|
return typeof token === "string" && token.length > 0 ? token : void 0;
|
|
1297
1297
|
}
|
|
1298
|
+
function reportListenerError(logger, scope, error) {
|
|
1299
|
+
const message = `\u041E\u0448\u0438\u0431\u043A\u0430 \u0432 \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u0447\u0438\u043A\u0435 \u0441\u043E\u0431\u044B\u0442\u0438\u044F ${scope}`;
|
|
1300
|
+
if (logger) logger.error(message, error);
|
|
1301
|
+
else console.error(`[itd-api] ${message}`, error);
|
|
1302
|
+
}
|
|
1298
1303
|
var AuthManager = class {
|
|
1299
1304
|
#config;
|
|
1300
|
-
#
|
|
1305
|
+
#send;
|
|
1301
1306
|
#jar;
|
|
1302
|
-
#emitter
|
|
1307
|
+
#emitter;
|
|
1303
1308
|
/** `undefined` — сессия ещё не читалась из хранилища. */
|
|
1304
1309
|
#session;
|
|
1310
|
+
/**
|
|
1311
|
+
* Общий промис чтения сессии из хранилища. Дедупликация: параллельные запросы на холодном
|
|
1312
|
+
* старте читают хранилище один раз и не заводят каждый свой `deviceId`.
|
|
1313
|
+
*/
|
|
1314
|
+
#loading = null;
|
|
1305
1315
|
/** Общий промис обновления: к нему присоединяются все, кто получил 401. */
|
|
1306
1316
|
#refreshing = null;
|
|
1307
1317
|
/** Общий промис входа по логину и паролю. */
|
|
@@ -1313,10 +1323,15 @@ var AuthManager = class {
|
|
|
1313
1323
|
* поэтому `clear()` его не трогает.
|
|
1314
1324
|
*/
|
|
1315
1325
|
#deviceId;
|
|
1316
|
-
|
|
1326
|
+
/** Общий промис первичной выдачи `deviceId` — чтобы параллельные запросы получили один. */
|
|
1327
|
+
#deviceIdLoading = null;
|
|
1328
|
+
constructor(config, send, jar) {
|
|
1317
1329
|
this.#config = config;
|
|
1318
|
-
this.#
|
|
1330
|
+
this.#send = send;
|
|
1319
1331
|
this.#jar = jar;
|
|
1332
|
+
this.#emitter = new Emitter(
|
|
1333
|
+
(error) => reportListenerError(config.logger, "\u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0438", error)
|
|
1334
|
+
);
|
|
1320
1335
|
}
|
|
1321
1336
|
/** Подписка на события авторизации. */
|
|
1322
1337
|
get on() {
|
|
@@ -1358,8 +1373,14 @@ var AuthManager = class {
|
|
|
1358
1373
|
* сервер связывает с ним запись в списке сессий, и плавающее значение плодило бы
|
|
1359
1374
|
* по новой сессии на каждый старт.
|
|
1360
1375
|
*/
|
|
1361
|
-
|
|
1362
|
-
if (this.#deviceId) return this.#deviceId;
|
|
1376
|
+
getDeviceId() {
|
|
1377
|
+
if (this.#deviceId) return Promise.resolve(this.#deviceId);
|
|
1378
|
+
this.#deviceIdLoading ??= this.#resolveDeviceId().finally(() => {
|
|
1379
|
+
this.#deviceIdLoading = null;
|
|
1380
|
+
});
|
|
1381
|
+
return this.#deviceIdLoading;
|
|
1382
|
+
}
|
|
1383
|
+
async #resolveDeviceId() {
|
|
1363
1384
|
const session = await this.#loadSession();
|
|
1364
1385
|
const deviceId = this.#config.deviceId ?? session?.deviceId ?? createDeviceId();
|
|
1365
1386
|
this.#deviceId = deviceId;
|
|
@@ -1462,8 +1483,14 @@ var AuthManager = class {
|
|
|
1462
1483
|
if (this.#deviceId) await this.#saveSession({ deviceId: this.#deviceId });
|
|
1463
1484
|
this.#emitter.emit("signOut", void 0);
|
|
1464
1485
|
}
|
|
1465
|
-
|
|
1466
|
-
if (this.#session !== void 0) return this.#session;
|
|
1486
|
+
#loadSession() {
|
|
1487
|
+
if (this.#session !== void 0) return Promise.resolve(this.#session);
|
|
1488
|
+
this.#loading ??= this.#performLoad().finally(() => {
|
|
1489
|
+
this.#loading = null;
|
|
1490
|
+
});
|
|
1491
|
+
return this.#loading;
|
|
1492
|
+
}
|
|
1493
|
+
async #performLoad() {
|
|
1467
1494
|
const stored = await this.#config.storage.get() ?? null;
|
|
1468
1495
|
if (stored?.cookies) this.#jar.deserialize(stored.cookies);
|
|
1469
1496
|
const fromConfig = this.#sessionFromConfig(this.#config.auth);
|
|
@@ -1531,17 +1558,14 @@ var AuthManager = class {
|
|
|
1531
1558
|
return this.#reloginOrNull();
|
|
1532
1559
|
}
|
|
1533
1560
|
try {
|
|
1534
|
-
const payload = await this.#
|
|
1561
|
+
const payload = await this.#send({
|
|
1535
1562
|
method: "POST",
|
|
1536
1563
|
path: AUTH_PATHS.refresh,
|
|
1564
|
+
skipQueue: true,
|
|
1565
|
+
skipAuth: true,
|
|
1566
|
+
skipAuthRefresh: true
|
|
1537
1567
|
// Тела нет намеренно: сервер читает refresh-токен только из cookie — см.
|
|
1538
1568
|
// #seedRefreshCookie. По той же причине не нужен и устаревший Bearer.
|
|
1539
|
-
skipAuth: true,
|
|
1540
|
-
// Без этого 401 на самом обновлении вызвал бы новое обновление — и так по кругу.
|
|
1541
|
-
skipAuthRefresh: true,
|
|
1542
|
-
// Обновление почти всегда запускается изнутри запроса, который занимает место
|
|
1543
|
-
// в очереди и ждёт его результата. Встать в ту же очередь — значит зависнуть.
|
|
1544
|
-
skipQueue: true
|
|
1545
1569
|
});
|
|
1546
1570
|
const accessToken = readAccessToken(payload);
|
|
1547
1571
|
if (!accessToken) return this.#reloginOrNull();
|
|
@@ -1613,15 +1637,13 @@ var AuthManager = class {
|
|
|
1613
1637
|
}
|
|
1614
1638
|
async #performSignIn(credentials) {
|
|
1615
1639
|
const turnstileToken = await this.#resolveTurnstileToken(credentials);
|
|
1616
|
-
const payload = await this.#
|
|
1640
|
+
const payload = await this.#send({
|
|
1617
1641
|
method: "POST",
|
|
1618
1642
|
path: AUTH_PATHS.signIn,
|
|
1619
1643
|
body: { email: credentials.email, password: credentials.password, turnstileToken },
|
|
1644
|
+
skipQueue: true,
|
|
1620
1645
|
skipAuth: true,
|
|
1621
|
-
skipAuthRefresh: true
|
|
1622
|
-
// Отложенный вход происходит при сборке заголовков уже начатого запроса — тот держит
|
|
1623
|
-
// место в очереди и ждёт токена. См. `skipQueue` в RawRequestOptions.
|
|
1624
|
-
skipQueue: true
|
|
1646
|
+
skipAuthRefresh: true
|
|
1625
1647
|
});
|
|
1626
1648
|
const accessToken = readAccessToken(payload);
|
|
1627
1649
|
if (!accessToken) {
|
|
@@ -1747,10 +1769,12 @@ function normalizeBaseUrl(baseUrl) {
|
|
|
1747
1769
|
return parsed.origin + (parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, ""));
|
|
1748
1770
|
}
|
|
1749
1771
|
|
|
1772
|
+
// src/core/version.ts
|
|
1773
|
+
var LIBRARY_VERSION = "0.0.8";
|
|
1774
|
+
|
|
1750
1775
|
// src/core/config.ts
|
|
1751
1776
|
var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
|
|
1752
1777
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1753
|
-
var LIBRARY_VERSION = "0.0.7";
|
|
1754
1778
|
var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION}; +https://github.com/KiowDev/itd-api)`;
|
|
1755
1779
|
var DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([1e3, 5e3, 3e4, 6e4, 9e4]);
|
|
1756
1780
|
function requirePositive(value, name) {
|
|
@@ -1895,183 +1919,520 @@ function resolveConfig(options = {}) {
|
|
|
1895
1919
|
};
|
|
1896
1920
|
}
|
|
1897
1921
|
|
|
1898
|
-
// src/core/
|
|
1899
|
-
var
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
"flowtoken",
|
|
1908
|
-
"token",
|
|
1909
|
-
"turnstiletoken",
|
|
1910
|
-
"otp"
|
|
1911
|
-
]);
|
|
1912
|
-
function maskSecret(value) {
|
|
1913
|
-
if (value.length <= 8) return "\u2026";
|
|
1914
|
-
return `${value.slice(0, 4)}\u2026(${value.length})\u2026${value.slice(-3)}`;
|
|
1915
|
-
}
|
|
1916
|
-
function redactHeaders(headers) {
|
|
1917
|
-
const result = {};
|
|
1918
|
-
headers.forEach((value, name) => {
|
|
1919
|
-
if (SECRET_HEADERS.has(name.toLowerCase())) {
|
|
1920
|
-
const spaceAt = value.indexOf(" ");
|
|
1921
|
-
result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
|
|
1922
|
-
return;
|
|
1923
|
-
}
|
|
1924
|
-
result[name] = value;
|
|
1925
|
-
});
|
|
1926
|
-
return result;
|
|
1927
|
-
}
|
|
1928
|
-
function redactBody(body) {
|
|
1929
|
-
if (body === null || body === void 0) return body;
|
|
1930
|
-
if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
|
|
1931
|
-
if (isBlob(body)) return "[Blob]";
|
|
1932
|
-
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
|
|
1933
|
-
if (Array.isArray(body)) return body.map(redactBody);
|
|
1934
|
-
if (typeof body === "object") {
|
|
1935
|
-
const result = {};
|
|
1936
|
-
for (const [key, value] of Object.entries(body)) {
|
|
1937
|
-
result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[\u0441\u043A\u0440\u044B\u0442\u043E]" : redactBody(value);
|
|
1938
|
-
}
|
|
1939
|
-
return result;
|
|
1922
|
+
// src/core/http.ts
|
|
1923
|
+
var HttpClient = class {
|
|
1924
|
+
#handler;
|
|
1925
|
+
#plugins;
|
|
1926
|
+
#baseUrl;
|
|
1927
|
+
constructor(deps) {
|
|
1928
|
+
this.#handler = deps.handler;
|
|
1929
|
+
this.#plugins = deps.plugins;
|
|
1930
|
+
this.#baseUrl = deps.baseUrl;
|
|
1940
1931
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1932
|
+
/** Базовый URL, к которому обращается клиент. */
|
|
1933
|
+
get baseUrl() {
|
|
1934
|
+
return this.#baseUrl;
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Имена опций запроса, заявленные плагинами.
|
|
1938
|
+
*
|
|
1939
|
+
* Читается ресурсами: они переносят в транспорт только известные поля, а чужие,
|
|
1940
|
+
* если их никто не заявил, отсеивают.
|
|
1941
|
+
*/
|
|
1942
|
+
get pluginOptionKeys() {
|
|
1943
|
+
return this.#plugins.optionKeys;
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Выполняет запрос к API через собранный конвейер.
|
|
1947
|
+
*
|
|
1948
|
+
* @typeParam T ожидаемая форма ответа после снятия обёртки `{ data: … }`
|
|
1949
|
+
* @throws {ItdApiError} если сервер ответил статусом ≥ 400
|
|
1950
|
+
* @throws {ItdTimeoutError} если истёк таймаут
|
|
1951
|
+
* @throws {ItdAbortError} если запрос отменён через `signal`
|
|
1952
|
+
* @throws {ItdNetworkError} если запрос не дошёл до сервера
|
|
1953
|
+
*/
|
|
1954
|
+
request(options) {
|
|
1955
|
+
return this.#handler(options);
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1943
1958
|
|
|
1944
|
-
// src/core/
|
|
1945
|
-
function
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1959
|
+
// src/core/pipeline.ts
|
|
1960
|
+
function composePipeline(middlewares, final) {
|
|
1961
|
+
return middlewares.reduceRight(
|
|
1962
|
+
(next, middleware) => (request) => middleware(request, next),
|
|
1963
|
+
final
|
|
1964
|
+
);
|
|
1950
1965
|
}
|
|
1951
|
-
function
|
|
1952
|
-
return
|
|
1966
|
+
function withLayerHeaders(request, headers) {
|
|
1967
|
+
return { ...request, layerHeaders: { ...request.layerHeaders, ...headers } };
|
|
1953
1968
|
}
|
|
1954
|
-
|
|
1955
|
-
|
|
1969
|
+
|
|
1970
|
+
// src/core/retry.ts
|
|
1971
|
+
var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
1972
|
+
function isRetryable(error, method, retryWrites) {
|
|
1973
|
+
if (error instanceof ItdAbortError) return false;
|
|
1974
|
+
const safeToRepeat = retryWrites || IDEMPOTENT_METHODS.has(method);
|
|
1975
|
+
if (error instanceof ItdApiError) {
|
|
1976
|
+
if (error.status === 429) return true;
|
|
1977
|
+
if (error.status >= 500) return safeToRepeat;
|
|
1978
|
+
return false;
|
|
1979
|
+
}
|
|
1980
|
+
if (error instanceof ItdNetworkError || error instanceof ItdTimeoutError) return safeToRepeat;
|
|
1981
|
+
return false;
|
|
1956
1982
|
}
|
|
1957
|
-
function
|
|
1958
|
-
|
|
1959
|
-
const
|
|
1960
|
-
|
|
1983
|
+
function backoffDelay(attempt, options, random) {
|
|
1984
|
+
const exponential = options.baseDelay * 2 ** (attempt - 1);
|
|
1985
|
+
const capped = Math.min(exponential, options.maxDelay);
|
|
1986
|
+
const spread = capped * options.jitter * (random() * 2 - 1);
|
|
1987
|
+
return Math.max(0, Math.round(capped + spread));
|
|
1961
1988
|
}
|
|
1962
|
-
function
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1989
|
+
function createRetryScheduler(options, random = Math.random) {
|
|
1990
|
+
return (error, attempt, method) => {
|
|
1991
|
+
if (attempt >= options.attempts) return void 0;
|
|
1992
|
+
if (options.shouldRetry) {
|
|
1993
|
+
return options.shouldRetry(error, attempt) ? backoffDelay(attempt, options, random) : void 0;
|
|
1994
|
+
}
|
|
1995
|
+
if (!isRetryable(error, method, options.retryWrites)) return void 0;
|
|
1996
|
+
if (error instanceof ItdApiError && error.retryAfter !== void 0) {
|
|
1997
|
+
return error.retryAfter > options.maxDelay ? void 0 : error.retryAfter;
|
|
1998
|
+
}
|
|
1999
|
+
return backoffDelay(attempt, options, random);
|
|
2000
|
+
};
|
|
1967
2001
|
}
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
return
|
|
2002
|
+
|
|
2003
|
+
// src/core/middleware.ts
|
|
2004
|
+
function sleep(ms) {
|
|
2005
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1972
2006
|
}
|
|
1973
|
-
function
|
|
1974
|
-
|
|
1975
|
-
const value = source[field];
|
|
1976
|
-
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2007
|
+
function createQueueMiddleware(schedule) {
|
|
2008
|
+
return (request, next) => request.skipQueue ? next(request) : schedule(() => next(request));
|
|
1977
2009
|
}
|
|
1978
|
-
function
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2010
|
+
function createPluginsMiddleware(plugins) {
|
|
2011
|
+
return (request, next) => {
|
|
2012
|
+
if (plugins.size === 0) return next(request);
|
|
2013
|
+
return plugins.run(request, next);
|
|
2014
|
+
};
|
|
1982
2015
|
}
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2016
|
+
async function applyAuth(request, deps) {
|
|
2017
|
+
if (request.skipAuth) return request;
|
|
2018
|
+
const headers = await deps.getAuthHeaders();
|
|
2019
|
+
return Object.keys(headers).length > 0 ? withLayerHeaders(request, headers) : request;
|
|
2020
|
+
}
|
|
2021
|
+
function createAuthMiddleware(deps) {
|
|
2022
|
+
return async (request, next) => {
|
|
2023
|
+
const authorized = await applyAuth(request, deps);
|
|
2024
|
+
try {
|
|
2025
|
+
return await next(authorized);
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
if (request.skipAuthRefresh || !deps.autoRefresh || !isItdApiError(error) || error.status !== 401) {
|
|
2028
|
+
throw error;
|
|
1995
2029
|
}
|
|
2030
|
+
const refreshed = await deps.onUnauthorized();
|
|
2031
|
+
if (!refreshed) throw error;
|
|
2032
|
+
const retried = await applyAuth({ ...request, skipAuthRefresh: true }, deps);
|
|
2033
|
+
return next(retried);
|
|
1996
2034
|
}
|
|
1997
|
-
}
|
|
1998
|
-
const violations = source.violations;
|
|
1999
|
-
if (Array.isArray(violations)) {
|
|
2000
|
-
for (const violation of violations) {
|
|
2001
|
-
if (!isRecord(violation)) continue;
|
|
2002
|
-
const field = asString(violation.field) ?? asString(violation.property);
|
|
2003
|
-
const message = asString(violation.message);
|
|
2004
|
-
if (!field || !message) continue;
|
|
2005
|
-
const existing = result[field];
|
|
2006
|
-
if (existing) existing.push(message);
|
|
2007
|
-
else result[field] = [message];
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
return result;
|
|
2011
|
-
}
|
|
2012
|
-
function parseErrorBody(body, status, statusText = "") {
|
|
2013
|
-
const fallbackMessage = statusText ? `HTTP ${status} ${statusText}` : `HTTP ${status}`;
|
|
2014
|
-
if (typeof body === "string") {
|
|
2015
|
-
return {
|
|
2016
|
-
code: "UNKNOWN_ERROR",
|
|
2017
|
-
message: asString(body.trim()) ?? fallbackMessage,
|
|
2018
|
-
detail: void 0,
|
|
2019
|
-
title: void 0,
|
|
2020
|
-
fieldErrors: {},
|
|
2021
|
-
userId: void 0
|
|
2022
|
-
};
|
|
2023
|
-
}
|
|
2024
|
-
if (!isRecord(body)) {
|
|
2025
|
-
return {
|
|
2026
|
-
code: "UNKNOWN_ERROR",
|
|
2027
|
-
message: fallbackMessage,
|
|
2028
|
-
detail: void 0,
|
|
2029
|
-
title: void 0,
|
|
2030
|
-
fieldErrors: {},
|
|
2031
|
-
userId: void 0
|
|
2032
|
-
};
|
|
2033
|
-
}
|
|
2034
|
-
if (body.type === "validation") {
|
|
2035
|
-
const target = asString(body.on);
|
|
2036
|
-
return {
|
|
2037
|
-
code: "VALIDATION_ERROR",
|
|
2038
|
-
message: target ? `\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u043D\u0435 \u043F\u0440\u043E\u0439\u0434\u0435\u043D\u0430: \u043D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 \u0432 \xAB${target}\xBB` : "\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0432\u0445\u043E\u0434\u043D\u044B\u0445 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u0440\u043E\u0439\u0434\u0435\u043D\u0430",
|
|
2039
|
-
detail: void 0,
|
|
2040
|
-
title: void 0,
|
|
2041
|
-
fieldErrors: {},
|
|
2042
|
-
userId: void 0
|
|
2043
|
-
};
|
|
2044
|
-
}
|
|
2045
|
-
const inner = isRecord(body.error) ? body.error : body;
|
|
2046
|
-
const message = asString(inner.message) ?? asString(inner.detail) ?? asString(inner.title) ?? // `{ "error": "Invalid token" }` — так отвечает сервер на недействительный токен.
|
|
2047
|
-
asString(body.error) ?? fallbackMessage;
|
|
2048
|
-
return {
|
|
2049
|
-
code: asString(inner.code) ?? asString(body.code) ?? "UNKNOWN_ERROR",
|
|
2050
|
-
message,
|
|
2051
|
-
detail: asString(inner.detail),
|
|
2052
|
-
title: asString(inner.title),
|
|
2053
|
-
fieldErrors: { ...collectFieldErrors(body), ...collectFieldErrors(inner) },
|
|
2054
|
-
userId: asString(inner.userId) ?? asString(body.userId)
|
|
2055
2035
|
};
|
|
2056
2036
|
}
|
|
2057
|
-
function
|
|
2058
|
-
if (
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
if (Number.isFinite(date)) return Math.max(0, date - now);
|
|
2063
|
-
return void 0;
|
|
2064
|
-
}
|
|
2065
|
-
function readIntHeader(headers, name) {
|
|
2066
|
-
const raw = headers?.get(name);
|
|
2067
|
-
if (raw === null || raw === void 0) return void 0;
|
|
2068
|
-
const value = Number.parseInt(raw, 10);
|
|
2069
|
-
return Number.isFinite(value) ? value : void 0;
|
|
2037
|
+
function resolveBackoff(retry, global) {
|
|
2038
|
+
if (retry === void 0) return global;
|
|
2039
|
+
if (retry === false) return void 0;
|
|
2040
|
+
const resolved = resolveRetry(retry);
|
|
2041
|
+
return resolved ? createRetryScheduler(resolved) : void 0;
|
|
2070
2042
|
}
|
|
2071
|
-
function
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2043
|
+
function createRetryMiddleware(deps) {
|
|
2044
|
+
const globalScheduler = deps.retry ? createRetryScheduler(deps.retry) : void 0;
|
|
2045
|
+
const nextDelay = (error, attempt, method, backoff) => {
|
|
2046
|
+
if (isItdRateLimitError(error)) {
|
|
2047
|
+
const wait = error.retryAfter ?? deps.rateLimitDelays[attempt - 1];
|
|
2048
|
+
if (wait === void 0) return void 0;
|
|
2049
|
+
deps.pauseQueue?.(wait);
|
|
2050
|
+
deps.logger?.debug(`\u043B\u0438\u043C\u0438\u0442 \u0447\u0430\u0441\u0442\u043E\u0442\u044B, \u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempt + 1} \u0447\u0435\u0440\u0435\u0437 ${wait} \u043C\u0441`);
|
|
2051
|
+
return wait;
|
|
2052
|
+
}
|
|
2053
|
+
return backoff?.(error, attempt, method);
|
|
2054
|
+
};
|
|
2055
|
+
return async (request, next) => {
|
|
2056
|
+
const method = request.method.toUpperCase();
|
|
2057
|
+
const backoff = resolveBackoff(request.retry, globalScheduler);
|
|
2058
|
+
for (let attempt = 1; ; attempt++) {
|
|
2059
|
+
try {
|
|
2060
|
+
return await next({ ...request, attempt });
|
|
2061
|
+
} catch (error) {
|
|
2062
|
+
const delay = nextDelay(error, attempt, method, backoff);
|
|
2063
|
+
if (delay === void 0) throw error;
|
|
2064
|
+
await deps.hooks.onRetry?.({
|
|
2065
|
+
method,
|
|
2066
|
+
path: request.path,
|
|
2067
|
+
url: deps.buildUrl(request),
|
|
2068
|
+
headers: new Headers(),
|
|
2069
|
+
attempt,
|
|
2070
|
+
error,
|
|
2071
|
+
delay
|
|
2072
|
+
});
|
|
2073
|
+
deps.logger?.debug(
|
|
2074
|
+
`\u043F\u043E\u0432\u0442\u043E\u0440 ${method} ${request.path}, \u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempt + 1} \u0447\u0435\u0440\u0435\u0437 ${delay} \u043C\u0441`
|
|
2075
|
+
);
|
|
2076
|
+
await sleep(delay);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
// src/core/plugins.ts
|
|
2083
|
+
var NO_KEYS = /* @__PURE__ */ new Set();
|
|
2084
|
+
var RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
2085
|
+
"signal",
|
|
2086
|
+
"timeout",
|
|
2087
|
+
"headers",
|
|
2088
|
+
"retry",
|
|
2089
|
+
"method",
|
|
2090
|
+
"path",
|
|
2091
|
+
"query",
|
|
2092
|
+
"body",
|
|
2093
|
+
"skipAuth",
|
|
2094
|
+
"skipAuthRefresh",
|
|
2095
|
+
"skipQueue",
|
|
2096
|
+
"raw"
|
|
2097
|
+
]);
|
|
2098
|
+
var PluginRegistry = class {
|
|
2099
|
+
#transformers = [];
|
|
2100
|
+
#optionKeys = /* @__PURE__ */ new Set();
|
|
2101
|
+
#names = /* @__PURE__ */ new Set();
|
|
2102
|
+
/** Сколько обёрток подключено. Ноль означает, что запрос идёт прежним путём. */
|
|
2103
|
+
get size() {
|
|
2104
|
+
return this.#transformers.length;
|
|
2105
|
+
}
|
|
2106
|
+
/** Имена опций запроса, заявленные плагинами. */
|
|
2107
|
+
get optionKeys() {
|
|
2108
|
+
return this.#optionKeys.size === 0 ? NO_KEYS : this.#optionKeys;
|
|
2109
|
+
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Подключает плагин.
|
|
2112
|
+
*
|
|
2113
|
+
* @throws {ItdConfigError} если плагин задан неверно, уже подключён или заявил занятое
|
|
2114
|
+
* имя опции
|
|
2115
|
+
*/
|
|
2116
|
+
add(plugin, context) {
|
|
2117
|
+
if (typeof plugin?.install !== "function") {
|
|
2118
|
+
throw new ItdConfigError("\u041F\u043B\u0430\u0433\u0438\u043D \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043E\u0431\u044A\u0435\u043A\u0442\u043E\u043C \u0441 \u043C\u0435\u0442\u043E\u0434\u043E\u043C install()");
|
|
2119
|
+
}
|
|
2120
|
+
const name = plugin.name;
|
|
2121
|
+
if (typeof name !== "string" || name.trim() === "") {
|
|
2122
|
+
throw new ItdConfigError("\u0423 \u043F\u043B\u0430\u0433\u0438\u043D\u0430 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0435 \u0438\u043C\u044F");
|
|
2123
|
+
}
|
|
2124
|
+
if (this.#names.has(name)) {
|
|
2125
|
+
throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0443\u0436\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D`);
|
|
2126
|
+
}
|
|
2127
|
+
const keys = plugin.optionKeys ?? [];
|
|
2128
|
+
for (const key of keys) {
|
|
2129
|
+
if (typeof key !== "string" || key.trim() === "") {
|
|
2130
|
+
throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0437\u0430\u044F\u0432\u0438\u043B \u043F\u0443\u0441\u0442\u043E\u0435 \u0438\u043C\u044F \u043E\u043F\u0446\u0438\u0438`);
|
|
2131
|
+
}
|
|
2132
|
+
if (RESERVED_OPTION_KEYS.has(key)) {
|
|
2133
|
+
throw new ItdConfigError(
|
|
2134
|
+
`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0437\u0430\u044F\u0432\u0438\u043B \u043E\u043F\u0446\u0438\u044E \xAB${key}\xBB: \u044D\u0442\u043E \u043F\u043E\u043B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u0430, \u0438\u043C\u044F \u0437\u0430\u043D\u044F\u0442\u043E. \u0417\u0430\u043D\u044F\u0442\u044B\u0435 \u0438\u043C\u0435\u043D\u0430: ${[...RESERVED_OPTION_KEYS].join(", ")}`
|
|
2135
|
+
);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
const before = this.#transformers.length;
|
|
2139
|
+
try {
|
|
2140
|
+
plugin.install({
|
|
2141
|
+
...context,
|
|
2142
|
+
use: (transformer) => {
|
|
2143
|
+
if (typeof transformer !== "function") {
|
|
2144
|
+
throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u043F\u0435\u0440\u0435\u0434\u0430\u043B \u0432 use() \u043D\u0435 \u0444\u0443\u043D\u043A\u0446\u0438\u044E`);
|
|
2145
|
+
}
|
|
2146
|
+
this.#transformers.push(transformer);
|
|
2147
|
+
}
|
|
2148
|
+
});
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
this.#transformers.length = before;
|
|
2151
|
+
throw error;
|
|
2152
|
+
}
|
|
2153
|
+
this.#names.add(name);
|
|
2154
|
+
for (const key of keys) this.#optionKeys.add(key);
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* Прогоняет запрос через цепочку обёрток.
|
|
2158
|
+
*
|
|
2159
|
+
* Цепочка собирается на каждый запрос заново: плагин можно подключить в любой момент,
|
|
2160
|
+
* а обёрток единицы — экономить тут не на чем.
|
|
2161
|
+
*
|
|
2162
|
+
* @param execute настоящий запрос, вызывается самой внутренней обёрткой
|
|
2163
|
+
*/
|
|
2164
|
+
run(request, execute) {
|
|
2165
|
+
const chain = this.#transformers.reduceRight(
|
|
2166
|
+
(next, transformer) => (current) => transformer(current, next),
|
|
2167
|
+
execute
|
|
2168
|
+
);
|
|
2169
|
+
return chain(request);
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
2172
|
+
|
|
2173
|
+
// src/core/rate-limit.ts
|
|
2174
|
+
var RequestQueue = class {
|
|
2175
|
+
#concurrency;
|
|
2176
|
+
/** Минимальный промежуток между стартами, мс. `0` — без ограничения частоты. */
|
|
2177
|
+
#minGap;
|
|
2178
|
+
#waiting = [];
|
|
2179
|
+
#active = 0;
|
|
2180
|
+
/** Момент, раньше которого следующий запрос стартовать не должен. */
|
|
2181
|
+
#nextSlot = 0;
|
|
2182
|
+
#timer;
|
|
2183
|
+
constructor(options) {
|
|
2184
|
+
this.#concurrency = options.concurrency;
|
|
2185
|
+
this.#minGap = options.rps ? 1e3 / options.rps : 0;
|
|
2186
|
+
}
|
|
2187
|
+
/** Сколько задач выполняется прямо сейчас. */
|
|
2188
|
+
get active() {
|
|
2189
|
+
return this.#active;
|
|
2190
|
+
}
|
|
2191
|
+
/** Сколько задач ждёт очереди. */
|
|
2192
|
+
get pending() {
|
|
2193
|
+
return this.#waiting.length;
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* Ставит задачу в очередь.
|
|
2197
|
+
*
|
|
2198
|
+
* @returns результат задачи; ошибка задачи пробрасывается без изменений
|
|
2199
|
+
*/
|
|
2200
|
+
schedule(task) {
|
|
2201
|
+
return new Promise((resolve, reject) => {
|
|
2202
|
+
const run = () => {
|
|
2203
|
+
this.#active += 1;
|
|
2204
|
+
task().then(resolve, reject).finally(() => {
|
|
2205
|
+
this.#active -= 1;
|
|
2206
|
+
this.#drain();
|
|
2207
|
+
});
|
|
2208
|
+
};
|
|
2209
|
+
this.#waiting.push({ run, cancel: reject });
|
|
2210
|
+
this.#drain();
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Останавливает очередь: снимает отложенную паузу и отклоняет ещё не начатые задачи
|
|
2215
|
+
* ошибкой `ItdAbortError`. Уже выполняющиеся задачи доводятся до конца.
|
|
2216
|
+
*/
|
|
2217
|
+
stop() {
|
|
2218
|
+
if (this.#timer !== void 0) {
|
|
2219
|
+
clearTimeout(this.#timer);
|
|
2220
|
+
this.#timer = void 0;
|
|
2221
|
+
}
|
|
2222
|
+
this.#nextSlot = 0;
|
|
2223
|
+
const pending = this.#waiting.splice(0, this.#waiting.length);
|
|
2224
|
+
for (const task of pending) {
|
|
2225
|
+
task.cancel(new ItdAbortError("\u041A\u043B\u0438\u0435\u043D\u0442 \u0437\u0430\u043A\u0440\u044B\u0442, \u0437\u0430\u043F\u0440\u043E\u0441 \u043E\u0442\u043C\u0435\u043D\u0451\u043D"));
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
/**
|
|
2229
|
+
* Придерживает всю очередь на заданное время.
|
|
2230
|
+
*
|
|
2231
|
+
* Вызывается при получении `429` с заголовком `Retry-After`: тормозить нужно все запросы,
|
|
2232
|
+
* а не только тот, который наткнулся на лимит, — иначе остальные продолжат добивать API.
|
|
2233
|
+
*/
|
|
2234
|
+
pause(ms) {
|
|
2235
|
+
if (ms <= 0) return;
|
|
2236
|
+
this.#nextSlot = Math.max(this.#nextSlot, Date.now() + ms);
|
|
2237
|
+
}
|
|
2238
|
+
/** Запускает столько ожидающих задач, сколько позволяют ограничения. */
|
|
2239
|
+
#drain() {
|
|
2240
|
+
if (this.#waiting.length === 0) return;
|
|
2241
|
+
if (this.#active >= this.#concurrency) return;
|
|
2242
|
+
if (this.#timer !== void 0) return;
|
|
2243
|
+
const now = Date.now();
|
|
2244
|
+
if (this.#nextSlot > now) {
|
|
2245
|
+
this.#timer = setTimeout(() => {
|
|
2246
|
+
this.#timer = void 0;
|
|
2247
|
+
this.#drain();
|
|
2248
|
+
}, this.#nextSlot - now);
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
const next = this.#waiting.shift();
|
|
2252
|
+
if (!next) return;
|
|
2253
|
+
if (this.#minGap > 0) this.#nextSlot = now + this.#minGap;
|
|
2254
|
+
next.run();
|
|
2255
|
+
this.#drain();
|
|
2256
|
+
}
|
|
2257
|
+
};
|
|
2258
|
+
|
|
2259
|
+
// src/core/redact.ts
|
|
2260
|
+
var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "set-cookie", "x-api-key"]);
|
|
2261
|
+
var SECRET_FIELDS = /* @__PURE__ */ new Set([
|
|
2262
|
+
"password",
|
|
2263
|
+
"oldpassword",
|
|
2264
|
+
"newpassword",
|
|
2265
|
+
"accesstoken",
|
|
2266
|
+
"refreshtoken",
|
|
2267
|
+
"currentpassword",
|
|
2268
|
+
"flowtoken",
|
|
2269
|
+
"token",
|
|
2270
|
+
"turnstiletoken",
|
|
2271
|
+
"otp"
|
|
2272
|
+
]);
|
|
2273
|
+
function maskSecret(value) {
|
|
2274
|
+
if (value.length <= 8) return "\u2026";
|
|
2275
|
+
return `${value.slice(0, 4)}\u2026(${value.length})\u2026${value.slice(-3)}`;
|
|
2276
|
+
}
|
|
2277
|
+
function redactHeaders(headers) {
|
|
2278
|
+
const result = {};
|
|
2279
|
+
headers.forEach((value, name) => {
|
|
2280
|
+
if (SECRET_HEADERS.has(name.toLowerCase())) {
|
|
2281
|
+
const spaceAt = value.indexOf(" ");
|
|
2282
|
+
result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
result[name] = value;
|
|
2286
|
+
});
|
|
2287
|
+
return result;
|
|
2288
|
+
}
|
|
2289
|
+
function redactBody(body) {
|
|
2290
|
+
if (body === null || body === void 0) return body;
|
|
2291
|
+
if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
|
|
2292
|
+
if (isBlob(body)) return "[Blob]";
|
|
2293
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
|
|
2294
|
+
if (Array.isArray(body)) return body.map(redactBody);
|
|
2295
|
+
if (typeof body === "object") {
|
|
2296
|
+
const result = {};
|
|
2297
|
+
for (const [key, value] of Object.entries(body)) {
|
|
2298
|
+
result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[\u0441\u043A\u0440\u044B\u0442\u043E]" : redactBody(value);
|
|
2299
|
+
}
|
|
2300
|
+
return result;
|
|
2301
|
+
}
|
|
2302
|
+
return body;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// src/core/unwrap.ts
|
|
2306
|
+
function unwrapData(body) {
|
|
2307
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return body;
|
|
2308
|
+
const keys = Object.keys(body);
|
|
2309
|
+
if (keys.length !== 1 || keys[0] !== "data") return body;
|
|
2310
|
+
return body.data;
|
|
2311
|
+
}
|
|
2312
|
+
function isRecord(value) {
|
|
2313
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2314
|
+
}
|
|
2315
|
+
function asString(value) {
|
|
2316
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2317
|
+
}
|
|
2318
|
+
function pickArray(source, field) {
|
|
2319
|
+
if (typeof source !== "object" || source === null) return [];
|
|
2320
|
+
const value = source[field];
|
|
2321
|
+
return Array.isArray(value) ? value : [];
|
|
2322
|
+
}
|
|
2323
|
+
function pickObject(source, field) {
|
|
2324
|
+
if (typeof source !== "object" || source === null) return void 0;
|
|
2325
|
+
const value = source[field];
|
|
2326
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
2327
|
+
return value;
|
|
2328
|
+
}
|
|
2329
|
+
function pickBoolean(source, field, fallback = false) {
|
|
2330
|
+
if (typeof source !== "object" || source === null) return fallback;
|
|
2331
|
+
const value = source[field];
|
|
2332
|
+
return typeof value === "boolean" ? value : fallback;
|
|
2333
|
+
}
|
|
2334
|
+
function pickNumber(source, field, fallback) {
|
|
2335
|
+
if (typeof source !== "object" || source === null) return fallback;
|
|
2336
|
+
const value = source[field];
|
|
2337
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2338
|
+
}
|
|
2339
|
+
function pickString(source, field) {
|
|
2340
|
+
if (typeof source !== "object" || source === null) return void 0;
|
|
2341
|
+
const value = source[field];
|
|
2342
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// src/core/error-factory.ts
|
|
2346
|
+
function collectFieldErrors(source) {
|
|
2347
|
+
const result = {};
|
|
2348
|
+
const errors = source.errors;
|
|
2349
|
+
if (isRecord(errors)) {
|
|
2350
|
+
for (const [field, value] of Object.entries(errors)) {
|
|
2351
|
+
if (Array.isArray(value)) {
|
|
2352
|
+
const messages = value.filter((item) => typeof item === "string");
|
|
2353
|
+
if (messages.length > 0) result[field] = messages;
|
|
2354
|
+
} else if (typeof value === "string") {
|
|
2355
|
+
result[field] = [value];
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
const violations = source.violations;
|
|
2360
|
+
if (Array.isArray(violations)) {
|
|
2361
|
+
for (const violation of violations) {
|
|
2362
|
+
if (!isRecord(violation)) continue;
|
|
2363
|
+
const field = asString(violation.field) ?? asString(violation.property);
|
|
2364
|
+
const message = asString(violation.message);
|
|
2365
|
+
if (!field || !message) continue;
|
|
2366
|
+
const existing = result[field];
|
|
2367
|
+
if (existing) existing.push(message);
|
|
2368
|
+
else result[field] = [message];
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
return result;
|
|
2372
|
+
}
|
|
2373
|
+
function parseErrorBody(body, status, statusText = "") {
|
|
2374
|
+
const fallbackMessage = statusText ? `HTTP ${status} ${statusText}` : `HTTP ${status}`;
|
|
2375
|
+
if (typeof body === "string") {
|
|
2376
|
+
return {
|
|
2377
|
+
code: "UNKNOWN_ERROR",
|
|
2378
|
+
message: asString(body.trim()) ?? fallbackMessage,
|
|
2379
|
+
detail: void 0,
|
|
2380
|
+
title: void 0,
|
|
2381
|
+
fieldErrors: {},
|
|
2382
|
+
userId: void 0
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
if (!isRecord(body)) {
|
|
2386
|
+
return {
|
|
2387
|
+
code: "UNKNOWN_ERROR",
|
|
2388
|
+
message: fallbackMessage,
|
|
2389
|
+
detail: void 0,
|
|
2390
|
+
title: void 0,
|
|
2391
|
+
fieldErrors: {},
|
|
2392
|
+
userId: void 0
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
if (body.type === "validation") {
|
|
2396
|
+
const target = asString(body.on);
|
|
2397
|
+
return {
|
|
2398
|
+
code: "VALIDATION_ERROR",
|
|
2399
|
+
message: target ? `\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u043D\u0435 \u043F\u0440\u043E\u0439\u0434\u0435\u043D\u0430: \u043D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 \u0432 \xAB${target}\xBB` : "\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0432\u0445\u043E\u0434\u043D\u044B\u0445 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u0440\u043E\u0439\u0434\u0435\u043D\u0430",
|
|
2400
|
+
detail: void 0,
|
|
2401
|
+
title: void 0,
|
|
2402
|
+
fieldErrors: {},
|
|
2403
|
+
userId: void 0
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
const inner = isRecord(body.error) ? body.error : body;
|
|
2407
|
+
const message = asString(inner.message) ?? asString(inner.detail) ?? asString(inner.title) ?? // `{ "error": "Invalid token" }` — так отвечает сервер на недействительный токен.
|
|
2408
|
+
asString(body.error) ?? fallbackMessage;
|
|
2409
|
+
return {
|
|
2410
|
+
code: asString(inner.code) ?? asString(body.code) ?? "UNKNOWN_ERROR",
|
|
2411
|
+
message,
|
|
2412
|
+
detail: asString(inner.detail),
|
|
2413
|
+
title: asString(inner.title),
|
|
2414
|
+
fieldErrors: { ...collectFieldErrors(body), ...collectFieldErrors(inner) },
|
|
2415
|
+
userId: asString(inner.userId) ?? asString(body.userId)
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
function parseRetryAfter(header, now = Date.now()) {
|
|
2419
|
+
if (!header) return void 0;
|
|
2420
|
+
const seconds = Number(header);
|
|
2421
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
2422
|
+
const date = Date.parse(header);
|
|
2423
|
+
if (Number.isFinite(date)) return Math.max(0, date - now);
|
|
2424
|
+
return void 0;
|
|
2425
|
+
}
|
|
2426
|
+
function readIntHeader(headers, name) {
|
|
2427
|
+
const raw = headers?.get(name);
|
|
2428
|
+
if (raw === null || raw === void 0) return void 0;
|
|
2429
|
+
const value = Number.parseInt(raw, 10);
|
|
2430
|
+
return Number.isFinite(value) ? value : void 0;
|
|
2431
|
+
}
|
|
2432
|
+
function readRateLimit(headers) {
|
|
2433
|
+
return {
|
|
2434
|
+
limit: readIntHeader(headers, "x-ratelimit-limit"),
|
|
2435
|
+
remaining: readIntHeader(headers, "x-ratelimit-remaining")
|
|
2075
2436
|
};
|
|
2076
2437
|
}
|
|
2077
2438
|
var REQUEST_ID_HEADERS = ["x-request-id", "x-requestid", "request-id", "x-correlation-id"];
|
|
@@ -2141,11 +2502,7 @@ function createApiError(context) {
|
|
|
2141
2502
|
return new Ctor(init);
|
|
2142
2503
|
}
|
|
2143
2504
|
|
|
2144
|
-
// src/core/
|
|
2145
|
-
function sleep(ms) {
|
|
2146
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2147
|
-
}
|
|
2148
|
-
var EMPTY_KEYS = /* @__PURE__ */ new Set();
|
|
2505
|
+
// src/core/transport.ts
|
|
2149
2506
|
function setHeader(headers, name, value) {
|
|
2150
2507
|
try {
|
|
2151
2508
|
headers.set(name, value);
|
|
@@ -2196,137 +2553,47 @@ function createAbortBundle(userSignal, timeout) {
|
|
|
2196
2553
|
}
|
|
2197
2554
|
};
|
|
2198
2555
|
}
|
|
2199
|
-
var
|
|
2556
|
+
var Transport = class {
|
|
2200
2557
|
#config;
|
|
2201
|
-
#
|
|
2202
|
-
|
|
2203
|
-
constructor(config, collaborators = {}) {
|
|
2558
|
+
#deps;
|
|
2559
|
+
constructor(config, deps) {
|
|
2204
2560
|
this.#config = config;
|
|
2205
|
-
this.#
|
|
2561
|
+
this.#deps = deps;
|
|
2206
2562
|
}
|
|
2207
|
-
/** Базовый URL, к которому обращается
|
|
2563
|
+
/** Базовый URL, к которому обращается транспорт. */
|
|
2208
2564
|
get baseUrl() {
|
|
2209
2565
|
return this.#config.baseUrl;
|
|
2210
2566
|
}
|
|
2211
2567
|
/**
|
|
2212
|
-
*
|
|
2213
|
-
*
|
|
2214
|
-
* Читается ресурсами: они переносят в транспорт только известные поля, а чужие,
|
|
2215
|
-
* если их никто не заявил, отсеивают.
|
|
2216
|
-
*/
|
|
2217
|
-
get pluginOptionKeys() {
|
|
2218
|
-
return this.#plugins?.optionKeys ?? EMPTY_KEYS;
|
|
2219
|
-
}
|
|
2220
|
-
/** Подключает список плагинов. Реестр общий с клиентом и пополняется через `itd.use()`. */
|
|
2221
|
-
usePlugins(plugins) {
|
|
2222
|
-
this.#plugins = plugins;
|
|
2223
|
-
}
|
|
2224
|
-
/**
|
|
2225
|
-
* Подключает недостающие части конвейера.
|
|
2226
|
-
*
|
|
2227
|
-
* Нужно из-за кольцевой зависимости: слой авторизации сам выполняет запросы, поэтому
|
|
2228
|
-
* не может быть передан в конструктор до создания транспорта.
|
|
2229
|
-
*/
|
|
2230
|
-
setCollaborators(collaborators) {
|
|
2231
|
-
this.#collaborators = { ...this.#collaborators, ...collaborators };
|
|
2232
|
-
}
|
|
2233
|
-
/**
|
|
2234
|
-
* Выполняет запрос к API.
|
|
2568
|
+
* Выполняет один сетевой запрос.
|
|
2235
2569
|
*
|
|
2236
|
-
* @typeParam T ожидаемая форма ответа после снятия обёртки `{ data: … }`
|
|
2237
2570
|
* @throws {ItdApiError} если сервер ответил статусом ≥ 400
|
|
2238
2571
|
* @throws {ItdTimeoutError} если истёк таймаут
|
|
2239
2572
|
* @throws {ItdAbortError} если запрос отменён через `signal`
|
|
2240
2573
|
* @throws {ItdNetworkError} если запрос не дошёл до сервера
|
|
2241
2574
|
*/
|
|
2242
|
-
async request
|
|
2243
|
-
const
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
/**
|
|
2248
|
-
* Прогоняет запрос через обёртки плагинов.
|
|
2249
|
-
*
|
|
2250
|
-
* Цепочка стоит **снаружи повторов и внутри очереди**: плагин должен увидеть запрос
|
|
2251
|
-
* и ответ по одному разу, независимо от того, сколько попыток понадобилось, — иначе,
|
|
2252
|
-
* например, текст поста зашифруется повторно на второй попытке.
|
|
2253
|
-
*/
|
|
2254
|
-
#withPlugins(options) {
|
|
2255
|
-
const plugins = this.#plugins;
|
|
2256
|
-
if (!plugins || plugins.size === 0) return this.#withRetries(options);
|
|
2257
|
-
return plugins.run(options, (request) => this.#withRetries(request));
|
|
2258
|
-
}
|
|
2259
|
-
async #withRetries(options) {
|
|
2260
|
-
const method = options.method.toUpperCase();
|
|
2261
|
-
for (let attempt = 1; ; attempt++) {
|
|
2262
|
-
try {
|
|
2263
|
-
return await this.#attempt(options, attempt);
|
|
2264
|
-
} catch (error) {
|
|
2265
|
-
const delay = this.#collaborators.nextRetryDelay?.(error, attempt, method);
|
|
2266
|
-
if (delay === void 0) throw error;
|
|
2267
|
-
await this.#config.hooks.onRetry?.({
|
|
2268
|
-
method,
|
|
2269
|
-
path: options.path,
|
|
2270
|
-
url: this.#buildUrl(options),
|
|
2271
|
-
headers: new Headers(),
|
|
2272
|
-
attempt,
|
|
2273
|
-
error,
|
|
2274
|
-
delay
|
|
2275
|
-
});
|
|
2276
|
-
this.#config.logger?.debug(
|
|
2277
|
-
`\u043F\u043E\u0432\u0442\u043E\u0440 ${method} ${options.path}, \u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempt + 1} \u0447\u0435\u0440\u0435\u0437 ${delay} \u043C\u0441`
|
|
2278
|
-
);
|
|
2279
|
-
await sleep(delay);
|
|
2280
|
-
}
|
|
2281
|
-
}
|
|
2282
|
-
}
|
|
2283
|
-
#buildUrl(options) {
|
|
2284
|
-
return joinUrl(this.#config.baseUrl, options.path) + buildQuery(options.query);
|
|
2285
|
-
}
|
|
2286
|
-
async #buildHeaders(options, url) {
|
|
2287
|
-
const headers = new Headers();
|
|
2288
|
-
headers.set("Accept", "application/json");
|
|
2289
|
-
headers.set("X-Requested-With", "XMLHttpRequest");
|
|
2290
|
-
if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
|
|
2291
|
-
if (this.#collaborators.getDeviceId) {
|
|
2292
|
-
setHeader(headers, "X-Device-Id", await this.#collaborators.getDeviceId());
|
|
2293
|
-
}
|
|
2294
|
-
for (const [name, value] of Object.entries(this.#config.headers))
|
|
2295
|
-
setHeader(headers, name, value);
|
|
2296
|
-
if (!options.skipAuth && this.#collaborators.getAuthHeaders) {
|
|
2297
|
-
const auth = await this.#collaborators.getAuthHeaders();
|
|
2298
|
-
for (const [name, value] of Object.entries(auth)) setHeader(headers, name, value);
|
|
2299
|
-
}
|
|
2300
|
-
if (this.#config.useCookieJar && this.#collaborators.getCookieHeader) {
|
|
2301
|
-
const cookie = this.#collaborators.getCookieHeader(url);
|
|
2302
|
-
if (cookie) setHeader(headers, "Cookie", cookie);
|
|
2303
|
-
}
|
|
2304
|
-
for (const [name, value] of Object.entries(options.headers ?? {})) {
|
|
2305
|
-
setHeader(headers, name, value);
|
|
2306
|
-
}
|
|
2307
|
-
return headers;
|
|
2308
|
-
}
|
|
2309
|
-
async #attempt(options, attempt) {
|
|
2310
|
-
const method = options.method.toUpperCase();
|
|
2311
|
-
const url = this.#buildUrl(options);
|
|
2312
|
-
const headers = await this.#buildHeaders(options, url);
|
|
2575
|
+
send = async (request) => {
|
|
2576
|
+
const method = request.method.toUpperCase();
|
|
2577
|
+
const url = this.buildUrl(request);
|
|
2578
|
+
const headers = await this.#buildHeaders(request, url);
|
|
2579
|
+
const attempt = request.attempt ?? 1;
|
|
2313
2580
|
let body;
|
|
2314
|
-
if (
|
|
2315
|
-
if (isRawBody(
|
|
2316
|
-
body =
|
|
2581
|
+
if (request.body !== void 0 && request.body !== null) {
|
|
2582
|
+
if (isRawBody(request.body)) {
|
|
2583
|
+
body = request.body;
|
|
2317
2584
|
} else {
|
|
2318
|
-
body = JSON.stringify(
|
|
2585
|
+
body = JSON.stringify(request.body);
|
|
2319
2586
|
if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
2320
2587
|
}
|
|
2321
2588
|
}
|
|
2322
|
-
const context = { method, path:
|
|
2589
|
+
const context = { method, path: request.path, url, headers, attempt };
|
|
2323
2590
|
await this.#config.hooks.onRequest?.(context);
|
|
2324
|
-
const timeout =
|
|
2325
|
-
const abort = createAbortBundle(
|
|
2591
|
+
const timeout = request.timeout ?? this.#config.timeout;
|
|
2592
|
+
const abort = createAbortBundle(request.signal, timeout);
|
|
2326
2593
|
const startedAt = Date.now();
|
|
2327
|
-
this.#config.logger?.debug(`\u2192 ${method} ${
|
|
2594
|
+
this.#config.logger?.debug(`\u2192 ${method} ${request.path}`, {
|
|
2328
2595
|
headers: redactHeaders(headers),
|
|
2329
|
-
body: redactBody(
|
|
2596
|
+
body: redactBody(request.body)
|
|
2330
2597
|
});
|
|
2331
2598
|
let response;
|
|
2332
2599
|
try {
|
|
@@ -2339,31 +2606,24 @@ var HttpClient = class {
|
|
|
2339
2606
|
});
|
|
2340
2607
|
} catch (error) {
|
|
2341
2608
|
const duration2 = Date.now() - startedAt;
|
|
2342
|
-
const failure = this.#toTransportError(error, abort,
|
|
2609
|
+
const failure = this.#toTransportError(error, abort, request, method, timeout);
|
|
2343
2610
|
await this.#config.hooks.onError?.({ ...context, duration: duration2, error: failure });
|
|
2344
|
-
this.#config.logger?.warn(`\xD7 ${method} ${
|
|
2611
|
+
this.#config.logger?.warn(`\xD7 ${method} ${request.path} (${duration2} \u043C\u0441): ${failure.message}`);
|
|
2345
2612
|
throw failure;
|
|
2346
2613
|
} finally {
|
|
2347
2614
|
abort.cleanup();
|
|
2348
2615
|
}
|
|
2349
2616
|
const duration = Date.now() - startedAt;
|
|
2350
|
-
if (this.#
|
|
2617
|
+
if (this.#deps.onRateLimit) {
|
|
2351
2618
|
const { limit, remaining } = readRateLimit(response.headers);
|
|
2352
|
-
this.#
|
|
2619
|
+
this.#deps.onRateLimit(limit, remaining);
|
|
2353
2620
|
}
|
|
2354
|
-
if (this.#config.useCookieJar) this.#
|
|
2621
|
+
if (this.#config.useCookieJar) this.#deps.cookies?.setFromResponse(url, response);
|
|
2355
2622
|
const payload = await readBody(response);
|
|
2356
2623
|
if (!response.ok) {
|
|
2357
|
-
if (response.status === 401 && !options.skipAuthRefresh && this.#config.autoRefresh && this.#collaborators.onUnauthorized) {
|
|
2358
|
-
const refreshed = await this.#collaborators.onUnauthorized();
|
|
2359
|
-
if (refreshed) {
|
|
2360
|
-
this.#config.logger?.debug(`\u0442\u043E\u043A\u0435\u043D \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D, \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044E ${method} ${options.path}`);
|
|
2361
|
-
return this.#attempt({ ...options, skipAuthRefresh: true }, attempt);
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
2624
|
const error = createApiError({
|
|
2365
2625
|
method,
|
|
2366
|
-
path:
|
|
2626
|
+
path: request.path,
|
|
2367
2627
|
status: response.status,
|
|
2368
2628
|
statusText: response.statusText,
|
|
2369
2629
|
headers: response.headers,
|
|
@@ -2372,7 +2632,7 @@ var HttpClient = class {
|
|
|
2372
2632
|
});
|
|
2373
2633
|
await this.#config.hooks.onError?.({ ...context, duration, error });
|
|
2374
2634
|
this.#config.logger?.warn(
|
|
2375
|
-
`\u2190 ${response.status} ${method} ${
|
|
2635
|
+
`\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441): ${error.message}`
|
|
2376
2636
|
);
|
|
2377
2637
|
throw error;
|
|
2378
2638
|
}
|
|
@@ -2382,219 +2642,56 @@ var HttpClient = class {
|
|
|
2382
2642
|
duration,
|
|
2383
2643
|
response
|
|
2384
2644
|
});
|
|
2385
|
-
this.#config.logger?.debug(`\u2190 ${response.status} ${method} ${
|
|
2386
|
-
return
|
|
2387
|
-
}
|
|
2388
|
-
/**
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
if (aborted && abort.timedOut()) {
|
|
2392
|
-
return new ItdTimeoutError({ timeout, method, path: options.path });
|
|
2393
|
-
}
|
|
2394
|
-
if (aborted) {
|
|
2395
|
-
return new ItdAbortError(`\u0417\u0430\u043F\u0440\u043E\u0441 ${method} ${options.path} \u043E\u0442\u043C\u0435\u043D\u0451\u043D`);
|
|
2396
|
-
}
|
|
2397
|
-
return new ItdNetworkError(
|
|
2398
|
-
`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C ${method} ${options.path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
2399
|
-
{ method, path: options.path, cause: error }
|
|
2400
|
-
);
|
|
2401
|
-
}
|
|
2402
|
-
};
|
|
2403
|
-
|
|
2404
|
-
// src/core/plugins.ts
|
|
2405
|
-
var NO_KEYS = /* @__PURE__ */ new Set();
|
|
2406
|
-
var RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
2407
|
-
"signal",
|
|
2408
|
-
"timeout",
|
|
2409
|
-
"headers",
|
|
2410
|
-
"retry",
|
|
2411
|
-
"method",
|
|
2412
|
-
"path",
|
|
2413
|
-
"query",
|
|
2414
|
-
"body",
|
|
2415
|
-
"skipAuth",
|
|
2416
|
-
"skipAuthRefresh",
|
|
2417
|
-
"skipQueue",
|
|
2418
|
-
"raw"
|
|
2419
|
-
]);
|
|
2420
|
-
var PluginRegistry = class {
|
|
2421
|
-
#transformers = [];
|
|
2422
|
-
#optionKeys = /* @__PURE__ */ new Set();
|
|
2423
|
-
#names = /* @__PURE__ */ new Set();
|
|
2424
|
-
/** Сколько обёрток подключено. Ноль означает, что запрос идёт прежним путём. */
|
|
2425
|
-
get size() {
|
|
2426
|
-
return this.#transformers.length;
|
|
2427
|
-
}
|
|
2428
|
-
/** Имена опций запроса, заявленные плагинами. */
|
|
2429
|
-
get optionKeys() {
|
|
2430
|
-
return this.#optionKeys.size === 0 ? NO_KEYS : this.#optionKeys;
|
|
2431
|
-
}
|
|
2432
|
-
/**
|
|
2433
|
-
* Подключает плагин.
|
|
2434
|
-
*
|
|
2435
|
-
* @throws {ItdConfigError} если плагин задан неверно, уже подключён или заявил занятое
|
|
2436
|
-
* имя опции
|
|
2437
|
-
*/
|
|
2438
|
-
add(plugin, context) {
|
|
2439
|
-
if (typeof plugin?.install !== "function") {
|
|
2440
|
-
throw new ItdConfigError("\u041F\u043B\u0430\u0433\u0438\u043D \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043E\u0431\u044A\u0435\u043A\u0442\u043E\u043C \u0441 \u043C\u0435\u0442\u043E\u0434\u043E\u043C install()");
|
|
2441
|
-
}
|
|
2442
|
-
const name = plugin.name;
|
|
2443
|
-
if (typeof name !== "string" || name.trim() === "") {
|
|
2444
|
-
throw new ItdConfigError("\u0423 \u043F\u043B\u0430\u0433\u0438\u043D\u0430 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0435 \u0438\u043C\u044F");
|
|
2445
|
-
}
|
|
2446
|
-
if (this.#names.has(name)) {
|
|
2447
|
-
throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0443\u0436\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D`);
|
|
2448
|
-
}
|
|
2449
|
-
const keys = plugin.optionKeys ?? [];
|
|
2450
|
-
for (const key of keys) {
|
|
2451
|
-
if (typeof key !== "string" || key.trim() === "") {
|
|
2452
|
-
throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0437\u0430\u044F\u0432\u0438\u043B \u043F\u0443\u0441\u0442\u043E\u0435 \u0438\u043C\u044F \u043E\u043F\u0446\u0438\u0438`);
|
|
2453
|
-
}
|
|
2454
|
-
if (RESERVED_OPTION_KEYS.has(key)) {
|
|
2455
|
-
throw new ItdConfigError(
|
|
2456
|
-
`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u0437\u0430\u044F\u0432\u0438\u043B \u043E\u043F\u0446\u0438\u044E \xAB${key}\xBB: \u044D\u0442\u043E \u043F\u043E\u043B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u0430, \u0438\u043C\u044F \u0437\u0430\u043D\u044F\u0442\u043E. \u0417\u0430\u043D\u044F\u0442\u044B\u0435 \u0438\u043C\u0435\u043D\u0430: ${[...RESERVED_OPTION_KEYS].join(", ")}`
|
|
2457
|
-
);
|
|
2458
|
-
}
|
|
2459
|
-
}
|
|
2460
|
-
const before = this.#transformers.length;
|
|
2461
|
-
try {
|
|
2462
|
-
plugin.install({
|
|
2463
|
-
...context,
|
|
2464
|
-
use: (transformer) => {
|
|
2465
|
-
if (typeof transformer !== "function") {
|
|
2466
|
-
throw new ItdConfigError(`\u041F\u043B\u0430\u0433\u0438\u043D \xAB${name}\xBB \u043F\u0435\u0440\u0435\u0434\u0430\u043B \u0432 use() \u043D\u0435 \u0444\u0443\u043D\u043A\u0446\u0438\u044E`);
|
|
2467
|
-
}
|
|
2468
|
-
this.#transformers.push(transformer);
|
|
2469
|
-
}
|
|
2470
|
-
});
|
|
2471
|
-
} catch (error) {
|
|
2472
|
-
this.#transformers.length = before;
|
|
2473
|
-
throw error;
|
|
2474
|
-
}
|
|
2475
|
-
this.#names.add(name);
|
|
2476
|
-
for (const key of keys) this.#optionKeys.add(key);
|
|
2477
|
-
}
|
|
2478
|
-
/**
|
|
2479
|
-
* Прогоняет запрос через цепочку обёрток.
|
|
2480
|
-
*
|
|
2481
|
-
* Цепочка собирается на каждый запрос заново: плагин можно подключить в любой момент,
|
|
2482
|
-
* а обёрток единицы — экономить тут не на чем.
|
|
2483
|
-
*
|
|
2484
|
-
* @param execute настоящий запрос, вызывается самой внутренней обёрткой
|
|
2485
|
-
*/
|
|
2486
|
-
run(request, execute) {
|
|
2487
|
-
const chain = this.#transformers.reduceRight(
|
|
2488
|
-
(next, transformer) => (current) => transformer(current, next),
|
|
2489
|
-
execute
|
|
2490
|
-
);
|
|
2491
|
-
return chain(request);
|
|
2492
|
-
}
|
|
2493
|
-
};
|
|
2494
|
-
|
|
2495
|
-
// src/core/rate-limit.ts
|
|
2496
|
-
var RequestQueue = class {
|
|
2497
|
-
#concurrency;
|
|
2498
|
-
/** Минимальный промежуток между стартами, мс. `0` — без ограничения частоты. */
|
|
2499
|
-
#minGap;
|
|
2500
|
-
#waiting = [];
|
|
2501
|
-
#active = 0;
|
|
2502
|
-
/** Момент, раньше которого следующий запрос стартовать не должен. */
|
|
2503
|
-
#nextSlot = 0;
|
|
2504
|
-
#timer;
|
|
2505
|
-
constructor(options) {
|
|
2506
|
-
this.#concurrency = options.concurrency;
|
|
2507
|
-
this.#minGap = options.rps ? 1e3 / options.rps : 0;
|
|
2508
|
-
}
|
|
2509
|
-
/** Сколько задач выполняется прямо сейчас. */
|
|
2510
|
-
get active() {
|
|
2511
|
-
return this.#active;
|
|
2512
|
-
}
|
|
2513
|
-
/** Сколько задач ждёт очереди. */
|
|
2514
|
-
get pending() {
|
|
2515
|
-
return this.#waiting.length;
|
|
2516
|
-
}
|
|
2517
|
-
/**
|
|
2518
|
-
* Ставит задачу в очередь.
|
|
2519
|
-
*
|
|
2520
|
-
* @returns результат задачи; ошибка задачи пробрасывается без изменений
|
|
2521
|
-
*/
|
|
2522
|
-
schedule(task) {
|
|
2523
|
-
return new Promise((resolve, reject) => {
|
|
2524
|
-
const run = () => {
|
|
2525
|
-
this.#active += 1;
|
|
2526
|
-
task().then(resolve, reject).finally(() => {
|
|
2527
|
-
this.#active -= 1;
|
|
2528
|
-
this.#drain();
|
|
2529
|
-
});
|
|
2530
|
-
};
|
|
2531
|
-
this.#waiting.push({ run });
|
|
2532
|
-
this.#drain();
|
|
2533
|
-
});
|
|
2645
|
+
this.#config.logger?.debug(`\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441)`);
|
|
2646
|
+
return request.raw ? payload : unwrapData(payload);
|
|
2647
|
+
};
|
|
2648
|
+
/** Итоговый URL со строкой запроса. Нужен и слою повторов — для хука `onRetry`. */
|
|
2649
|
+
buildUrl(request) {
|
|
2650
|
+
return joinUrl(this.#config.baseUrl, request.path) + buildQuery(request.query);
|
|
2534
2651
|
}
|
|
2535
2652
|
/**
|
|
2536
|
-
*
|
|
2653
|
+
* Собирает заголовки запроса.
|
|
2537
2654
|
*
|
|
2538
|
-
*
|
|
2539
|
-
*
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
if (this.#
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
const
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
}
|
|
2558
|
-
const
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
this.#drain();
|
|
2563
|
-
}
|
|
2564
|
-
};
|
|
2565
|
-
|
|
2566
|
-
// src/core/retry.ts
|
|
2567
|
-
var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
2568
|
-
function isRetryable(error, method, retryWrites) {
|
|
2569
|
-
if (error instanceof ItdAbortError) return false;
|
|
2570
|
-
const safeToRepeat = retryWrites || IDEMPOTENT_METHODS.has(method);
|
|
2571
|
-
if (error instanceof ItdApiError) {
|
|
2572
|
-
if (error.status === 429) return true;
|
|
2573
|
-
if (error.status >= 500) return safeToRepeat;
|
|
2574
|
-
return false;
|
|
2655
|
+
* Порядок важен: сначала умолчания библиотеки, затем заголовки клиента, затем то,
|
|
2656
|
+
* что добавили слои конвейера (авторизация), и только в самом конце — заголовки
|
|
2657
|
+
* конкретного вызова. Так пользователь может переопределить что угодно.
|
|
2658
|
+
*/
|
|
2659
|
+
async #buildHeaders(request, url) {
|
|
2660
|
+
const headers = new Headers();
|
|
2661
|
+
headers.set("Accept", "application/json");
|
|
2662
|
+
headers.set("X-Requested-With", "XMLHttpRequest");
|
|
2663
|
+
if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
|
|
2664
|
+
if (this.#deps.getDeviceId) {
|
|
2665
|
+
setHeader(headers, "X-Device-Id", await this.#deps.getDeviceId());
|
|
2666
|
+
}
|
|
2667
|
+
for (const [name, value] of Object.entries(this.#config.headers))
|
|
2668
|
+
setHeader(headers, name, value);
|
|
2669
|
+
for (const [name, value] of Object.entries(request.layerHeaders ?? {}))
|
|
2670
|
+
setHeader(headers, name, value);
|
|
2671
|
+
if (this.#config.useCookieJar && this.#deps.cookies) {
|
|
2672
|
+
const cookie = this.#deps.cookies.getHeader(url);
|
|
2673
|
+
if (cookie) setHeader(headers, "Cookie", cookie);
|
|
2674
|
+
}
|
|
2675
|
+
for (const [name, value] of Object.entries(request.headers ?? {})) {
|
|
2676
|
+
setHeader(headers, name, value);
|
|
2677
|
+
}
|
|
2678
|
+
return headers;
|
|
2575
2679
|
}
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
const capped = Math.min(exponential, options.maxDelay);
|
|
2582
|
-
const spread = capped * options.jitter * (random() * 2 - 1);
|
|
2583
|
-
return Math.max(0, Math.round(capped + spread));
|
|
2584
|
-
}
|
|
2585
|
-
function createRetryScheduler(options, random = Math.random) {
|
|
2586
|
-
return (error, attempt, method) => {
|
|
2587
|
-
if (attempt >= options.attempts) return void 0;
|
|
2588
|
-
if (options.shouldRetry) {
|
|
2589
|
-
return options.shouldRetry(error, attempt) ? backoffDelay(attempt, options, random) : void 0;
|
|
2680
|
+
/** Превращает исключение `fetch` в понятную ошибку библиотеки. */
|
|
2681
|
+
#toTransportError(error, abort, request, method, timeout) {
|
|
2682
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
2683
|
+
if (aborted && abort.timedOut()) {
|
|
2684
|
+
return new ItdTimeoutError({ timeout, method, path: request.path });
|
|
2590
2685
|
}
|
|
2591
|
-
if (
|
|
2592
|
-
|
|
2593
|
-
return error.retryAfter > options.maxDelay ? void 0 : error.retryAfter;
|
|
2686
|
+
if (aborted) {
|
|
2687
|
+
return new ItdAbortError(`\u0417\u0430\u043F\u0440\u043E\u0441 ${method} ${request.path} \u043E\u0442\u043C\u0435\u043D\u0451\u043D`);
|
|
2594
2688
|
}
|
|
2595
|
-
return
|
|
2596
|
-
|
|
2597
|
-
}
|
|
2689
|
+
return new ItdNetworkError(
|
|
2690
|
+
`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C ${method} ${request.path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
2691
|
+
{ method, path: request.path, cause: error }
|
|
2692
|
+
);
|
|
2693
|
+
}
|
|
2694
|
+
};
|
|
2598
2695
|
|
|
2599
2696
|
// src/notifications/type-map.ts
|
|
2600
2697
|
var NOTIFICATION_TYPE_ALIASES = Object.freeze({
|
|
@@ -3034,7 +3131,7 @@ var RealtimeTransportKind = Object.freeze({
|
|
|
3034
3131
|
var ItdRealtime = class {
|
|
3035
3132
|
#deps;
|
|
3036
3133
|
#options;
|
|
3037
|
-
#emitter
|
|
3134
|
+
#emitter;
|
|
3038
3135
|
#transport;
|
|
3039
3136
|
#maxAttempts;
|
|
3040
3137
|
#controller;
|
|
@@ -3056,6 +3153,11 @@ var ItdRealtime = class {
|
|
|
3056
3153
|
this.#options = options;
|
|
3057
3154
|
this.#maxAttempts = options.maxAttempts ?? MAX_RECONNECT_ATTEMPTS;
|
|
3058
3155
|
this.#transport = this.#createTransport();
|
|
3156
|
+
this.#emitter = new Emitter((error) => {
|
|
3157
|
+
const message = "\u041E\u0448\u0438\u0431\u043A\u0430 \u0432 \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u0447\u0438\u043A\u0435 \u0441\u043E\u0431\u044B\u0442\u0438\u044F realtime";
|
|
3158
|
+
if (deps.logger) deps.logger.error(message, error);
|
|
3159
|
+
else console.error(`[itd-api] ${message}`, error);
|
|
3160
|
+
});
|
|
3059
3161
|
}
|
|
3060
3162
|
/** Текущее состояние соединения. */
|
|
3061
3163
|
get status() {
|
|
@@ -3107,6 +3209,7 @@ var ItdRealtime = class {
|
|
|
3107
3209
|
this.#controller = void 0;
|
|
3108
3210
|
this.#attempt = 0;
|
|
3109
3211
|
this.#setStatus(RealtimeStatus.Disconnected);
|
|
3212
|
+
this.#deps.onClose?.();
|
|
3110
3213
|
}
|
|
3111
3214
|
/** Снимает все подписки. Соединение при этом не закрывается. */
|
|
3112
3215
|
removeAllListeners() {
|
|
@@ -3405,6 +3508,14 @@ var Paginator = class {
|
|
|
3405
3508
|
}
|
|
3406
3509
|
};
|
|
3407
3510
|
|
|
3511
|
+
// src/types/options.ts
|
|
3512
|
+
var REQUEST_OPTION_KEYS = [
|
|
3513
|
+
"signal",
|
|
3514
|
+
"timeout",
|
|
3515
|
+
"headers",
|
|
3516
|
+
"retry"
|
|
3517
|
+
];
|
|
3518
|
+
|
|
3408
3519
|
// src/resources/base.ts
|
|
3409
3520
|
var BaseResource = class {
|
|
3410
3521
|
/** @internal */
|
|
@@ -3413,28 +3524,25 @@ var BaseResource = class {
|
|
|
3413
3524
|
this.http = http;
|
|
3414
3525
|
}
|
|
3415
3526
|
/**
|
|
3416
|
-
* Переносит
|
|
3527
|
+
* Переносит опции запроса в описание транспорта.
|
|
3417
3528
|
*
|
|
3418
|
-
*
|
|
3419
|
-
* {@link RequestOptions} и приносят с собой `limit`, `cursor`
|
|
3420
|
-
* запроса делать нечего.
|
|
3529
|
+
* Копируются только поля {@link REQUEST_OPTION_KEYS} и опции, заявленные плагинами:
|
|
3530
|
+
* параметры методов наследуют {@link RequestOptions} и приносят с собой `limit`, `cursor`
|
|
3531
|
+
* и прочее, чему в описании запроса делать нечего. Чужие опции плагинов библиотека
|
|
3421
3532
|
* не понимает, но обязана донести до обёрток нетронутыми.
|
|
3422
3533
|
*/
|
|
3423
3534
|
requestOptions(options) {
|
|
3424
3535
|
if (!options) return {};
|
|
3425
|
-
const result = {
|
|
3426
|
-
...options.signal !== void 0 ? { signal: options.signal } : {},
|
|
3427
|
-
...options.timeout !== void 0 ? { timeout: options.timeout } : {},
|
|
3428
|
-
...options.headers !== void 0 ? { headers: options.headers } : {},
|
|
3429
|
-
...options.retry !== void 0 ? { retry: options.retry } : {}
|
|
3430
|
-
};
|
|
3431
|
-
const pluginKeys = this.http.pluginOptionKeys;
|
|
3432
|
-
if (pluginKeys.size === 0) return result;
|
|
3433
3536
|
const source = options;
|
|
3434
|
-
const
|
|
3537
|
+
const result = {};
|
|
3538
|
+
for (const key of REQUEST_OPTION_KEYS) {
|
|
3539
|
+
const value = source[key];
|
|
3540
|
+
if (value !== void 0) result[key] = value;
|
|
3541
|
+
}
|
|
3542
|
+
const pluginKeys = this.http.pluginOptionKeys;
|
|
3435
3543
|
for (const key of pluginKeys) {
|
|
3436
3544
|
const value = source[key];
|
|
3437
|
-
if (value !== void 0)
|
|
3545
|
+
if (value !== void 0) result[key] = value;
|
|
3438
3546
|
}
|
|
3439
3547
|
return result;
|
|
3440
3548
|
}
|
|
@@ -3454,6 +3562,42 @@ var BaseResource = class {
|
|
|
3454
3562
|
...options?.start !== void 0 ? { start: options.start } : {}
|
|
3455
3563
|
});
|
|
3456
3564
|
}
|
|
3565
|
+
/**
|
|
3566
|
+
* Собирает пару «загрузка страницы + перебор» из одного описания.
|
|
3567
|
+
*
|
|
3568
|
+
* Путь, параметры запроса и разбор ответа задаются один раз; `list` и `iterate`
|
|
3569
|
+
* строятся из них.
|
|
3570
|
+
*
|
|
3571
|
+
* @example
|
|
3572
|
+
* ```ts
|
|
3573
|
+
* #feed = this.paginated<Post, FeedParams>({
|
|
3574
|
+
* path: () => '/api/posts',
|
|
3575
|
+
* query: (p) => ({ tab: p.tab, limit: p.limit }),
|
|
3576
|
+
* start: (p) => (p.cursor ? { cursor: p.cursor } : {}),
|
|
3577
|
+
* read: (body) => readCursorPage<Post>(body, 'posts'),
|
|
3578
|
+
* mode: PaginationMode.Cursor,
|
|
3579
|
+
* });
|
|
3580
|
+
* ```
|
|
3581
|
+
*/
|
|
3582
|
+
paginated(spec) {
|
|
3583
|
+
const load = async (params, state) => {
|
|
3584
|
+
const body = await this.http.request({
|
|
3585
|
+
method: "GET",
|
|
3586
|
+
path: spec.path(params),
|
|
3587
|
+
query: withPageState(spec.query(params), state),
|
|
3588
|
+
...this.requestOptions(params)
|
|
3589
|
+
});
|
|
3590
|
+
return spec.read(body, state);
|
|
3591
|
+
};
|
|
3592
|
+
return {
|
|
3593
|
+
list: (params) => load(params, spec.start(params)),
|
|
3594
|
+
iterate: (params) => this.paginate(spec.mode, (state) => load(params, state), {
|
|
3595
|
+
...params.maxPages !== void 0 ? { maxPages: params.maxPages } : {},
|
|
3596
|
+
...params.signal !== void 0 ? { signal: params.signal } : {},
|
|
3597
|
+
start: spec.start(params)
|
|
3598
|
+
})
|
|
3599
|
+
};
|
|
3600
|
+
}
|
|
3457
3601
|
};
|
|
3458
3602
|
function withPageState(query, state) {
|
|
3459
3603
|
return {
|
|
@@ -3771,6 +3915,14 @@ var AuthResource = class extends BaseResource {
|
|
|
3771
3915
|
// src/resources/comments.ts
|
|
3772
3916
|
var CommentsResource = class extends BaseResource {
|
|
3773
3917
|
#uploadFiles;
|
|
3918
|
+
/** Ответы на комментарий: `/api/comments/{id}/replies`, постраничная пагинация. */
|
|
3919
|
+
#replies = this.paginated({
|
|
3920
|
+
path: (p) => `/api/comments/${encodePathSegment(p.commentId, "commentId")}/replies`,
|
|
3921
|
+
query: (p) => ({ limit: p.limit }),
|
|
3922
|
+
start: (p) => p.page !== void 0 ? { page: p.page } : {},
|
|
3923
|
+
read: (body) => readPagedPage(body, "replies"),
|
|
3924
|
+
mode: PaginationMode.Page
|
|
3925
|
+
});
|
|
3774
3926
|
constructor(http, deps) {
|
|
3775
3927
|
super(http);
|
|
3776
3928
|
this.#uploadFiles = deps.uploadFiles;
|
|
@@ -3780,31 +3932,12 @@ var CommentsResource = class extends BaseResource {
|
|
|
3780
3932
|
*
|
|
3781
3933
|
* Здесь пагинация **постраничная**, в отличие от комментариев к посту, где курсорная.
|
|
3782
3934
|
*/
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
method: "GET",
|
|
3786
|
-
path: `/api/comments/${encodePathSegment(commentId, "commentId")}/replies`,
|
|
3787
|
-
query: { limit: params.limit, page: params.page },
|
|
3788
|
-
...this.requestOptions(params)
|
|
3789
|
-
});
|
|
3790
|
-
return readPagedPage(body, "replies");
|
|
3935
|
+
replies(commentId, params = {}) {
|
|
3936
|
+
return this.#replies.list({ ...params, commentId });
|
|
3791
3937
|
}
|
|
3792
3938
|
/** Перебирает ответы на комментарий. */
|
|
3793
3939
|
iterateReplies(commentId, params = {}) {
|
|
3794
|
-
|
|
3795
|
-
return this.paginate(
|
|
3796
|
-
PaginationMode.Page,
|
|
3797
|
-
async (state) => {
|
|
3798
|
-
const body = await this.http.request({
|
|
3799
|
-
method: "GET",
|
|
3800
|
-
path,
|
|
3801
|
-
query: withPageState({ limit: params.limit }, state),
|
|
3802
|
-
...this.requestOptions(params)
|
|
3803
|
-
});
|
|
3804
|
-
return readPagedPage(body, "replies");
|
|
3805
|
-
},
|
|
3806
|
-
{ ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
|
|
3807
|
-
);
|
|
3940
|
+
return this.#replies.iterate({ ...params, commentId });
|
|
3808
3941
|
}
|
|
3809
3942
|
/**
|
|
3810
3943
|
* Отвечает на комментарий.
|
|
@@ -3939,19 +4072,14 @@ function assertAllowedMime(mimeType, filename) {
|
|
|
3939
4072
|
var DEFAULT_UPLOAD_TIMEOUT = 3e5;
|
|
3940
4073
|
var FilesResource = class extends BaseResource {
|
|
3941
4074
|
#readFile;
|
|
4075
|
+
/**
|
|
4076
|
+
* @param deps.readFile чтение файлов с диска. Передаёт точка входа `itd-api/node`;
|
|
4077
|
+
* в основном бандле его нет, чтобы браузерные сборщики не пытались разрешить `node:fs`.
|
|
4078
|
+
*/
|
|
3942
4079
|
constructor(http, deps = {}) {
|
|
3943
4080
|
super(http);
|
|
3944
4081
|
this.#readFile = deps.readFile;
|
|
3945
4082
|
}
|
|
3946
|
-
/**
|
|
3947
|
-
* Подключает чтение файлов с диска.
|
|
3948
|
-
*
|
|
3949
|
-
* Вызывается точкой входа `itd-api/node`; в основном бандле работы с файловой
|
|
3950
|
-
* системой нет, чтобы браузерные сборщики не пытались разрешить `node:fs`.
|
|
3951
|
-
*/
|
|
3952
|
-
setFileReader(readFile) {
|
|
3953
|
-
this.#readFile = readFile;
|
|
3954
|
-
}
|
|
3955
4083
|
/**
|
|
3956
4084
|
* Загружает файл и возвращает его идентификатор.
|
|
3957
4085
|
*
|
|
@@ -4061,8 +4189,16 @@ var FilesResource = class extends BaseResource {
|
|
|
4061
4189
|
}
|
|
4062
4190
|
};
|
|
4063
4191
|
|
|
4064
|
-
// src/resources/
|
|
4192
|
+
// src/resources/hashtags.ts
|
|
4065
4193
|
var HashtagsResource = class extends BaseResource {
|
|
4194
|
+
/** Посты по хэштегу: `/api/hashtags/{tag}/posts`, курсорная пагинация. */
|
|
4195
|
+
#posts = this.paginated({
|
|
4196
|
+
path: (p) => `/api/hashtags/${encodePathSegment(p.tag, "tag")}/posts`,
|
|
4197
|
+
query: (p) => ({ limit: p.limit }),
|
|
4198
|
+
start: (p) => p.cursor ? { cursor: p.cursor } : {},
|
|
4199
|
+
read: (body) => readCursorPage(body, "posts"),
|
|
4200
|
+
mode: PaginationMode.Cursor
|
|
4201
|
+
});
|
|
4066
4202
|
/**
|
|
4067
4203
|
* Ищет хэштеги.
|
|
4068
4204
|
*
|
|
@@ -4089,214 +4225,52 @@ var HashtagsResource = class extends BaseResource {
|
|
|
4089
4225
|
}
|
|
4090
4226
|
/**
|
|
4091
4227
|
* Загружает страницу постов по хэштегу.
|
|
4092
|
-
*
|
|
4093
|
-
* @param tag название без решётки; кодируется автоматически, поэтому кириллица
|
|
4094
|
-
* и пробелы допустимы
|
|
4095
|
-
*/
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
});
|
|
4103
|
-
return readCursorPage(body, "posts");
|
|
4104
|
-
}
|
|
4105
|
-
/** Перебирает посты по хэштегу. */
|
|
4106
|
-
iteratePosts(tag, params = {}) {
|
|
4107
|
-
const path = `/api/hashtags/${encodePathSegment(tag, "tag")}/posts`;
|
|
4108
|
-
return this.paginate(
|
|
4109
|
-
PaginationMode.Cursor,
|
|
4110
|
-
async (state) => {
|
|
4111
|
-
const body = await this.http.request({
|
|
4112
|
-
method: "GET",
|
|
4113
|
-
path,
|
|
4114
|
-
query: withPageState({ limit: params.limit }, state),
|
|
4115
|
-
...this.requestOptions(params)
|
|
4116
|
-
});
|
|
4117
|
-
return readCursorPage(body, "posts");
|
|
4118
|
-
},
|
|
4119
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4120
|
-
);
|
|
4121
|
-
}
|
|
4122
|
-
};
|
|
4123
|
-
var SearchResource = class extends BaseResource {
|
|
4124
|
-
/**
|
|
4125
|
-
* Ищет пользователей и хэштеги одним запросом.
|
|
4126
|
-
*
|
|
4127
|
-
* @example
|
|
4128
|
-
* ```ts
|
|
4129
|
-
* const { users, hashtags } = await itd.search.all('арт');
|
|
4130
|
-
* ```
|
|
4131
|
-
*/
|
|
4132
|
-
async all(query, options = {}) {
|
|
4133
|
-
const body = await this.http.request({
|
|
4134
|
-
method: "GET",
|
|
4135
|
-
path: "/api/search",
|
|
4136
|
-
query: { q: query },
|
|
4137
|
-
...this.requestOptions(options)
|
|
4138
|
-
});
|
|
4139
|
-
return {
|
|
4140
|
-
users: pickArray(body, "users"),
|
|
4141
|
-
hashtags: pickArray(body, "hashtags")
|
|
4142
|
-
};
|
|
4143
|
-
}
|
|
4144
|
-
};
|
|
4145
|
-
var ReportsResource = class extends BaseResource {
|
|
4146
|
-
/**
|
|
4147
|
-
* Отправляет жалобу.
|
|
4148
|
-
*
|
|
4149
|
-
* Повторная жалоба на тот же объект отклоняется сервером с сообщением
|
|
4150
|
-
* «Вы уже отправляли жалобу на этот контент».
|
|
4151
|
-
*
|
|
4152
|
-
* @example
|
|
4153
|
-
* ```ts
|
|
4154
|
-
* await itd.reports.create(report.post(postId).reason('spam'));
|
|
4155
|
-
* await itd.reports.create({ targetType: 'user', targetId, reason: 'fraud' });
|
|
4156
|
-
* ```
|
|
4157
|
-
*/
|
|
4158
|
-
create(input, options = {}) {
|
|
4159
|
-
const data = resolveReport(input);
|
|
4160
|
-
return this.http.request({
|
|
4161
|
-
method: "POST",
|
|
4162
|
-
path: "/api/reports",
|
|
4163
|
-
body: data,
|
|
4164
|
-
...this.requestOptions(options)
|
|
4165
|
-
});
|
|
4166
|
-
}
|
|
4167
|
-
};
|
|
4168
|
-
var VerificationResource = class extends BaseResource {
|
|
4169
|
-
/** Загружает статус заявки. Значение `none` означает, что заявка не подавалась. */
|
|
4170
|
-
status(options = {}) {
|
|
4171
|
-
return this.http.request({
|
|
4172
|
-
method: "GET",
|
|
4173
|
-
path: "/api/verification/status",
|
|
4174
|
-
...this.requestOptions(options)
|
|
4175
|
-
});
|
|
4176
|
-
}
|
|
4177
|
-
/** Подаёт заявку на верификацию с видео. */
|
|
4178
|
-
submit(videoUrl, options = {}) {
|
|
4179
|
-
return this.http.request({
|
|
4180
|
-
method: "POST",
|
|
4181
|
-
path: "/api/verification/submit",
|
|
4182
|
-
body: { videoUrl },
|
|
4183
|
-
...this.requestOptions(options)
|
|
4184
|
-
});
|
|
4185
|
-
}
|
|
4186
|
-
};
|
|
4187
|
-
var SubscriptionResource = class extends BaseResource {
|
|
4188
|
-
/** Загружает состояние подписки и её цену. */
|
|
4189
|
-
status(options = {}) {
|
|
4190
|
-
return this.http.request({
|
|
4191
|
-
method: "GET",
|
|
4192
|
-
// Завершающий слэш обязателен.
|
|
4193
|
-
path: "/api/v1/subscription/",
|
|
4194
|
-
...this.requestOptions(options)
|
|
4195
|
-
});
|
|
4196
|
-
}
|
|
4197
|
-
/**
|
|
4198
|
-
* Запускает оплату подписки.
|
|
4199
|
-
*
|
|
4200
|
-
* Форма ответа в документации API не описана, поэтому тип результата не уточняется.
|
|
4201
|
-
*/
|
|
4202
|
-
pay(options = {}) {
|
|
4203
|
-
return this.http.request({
|
|
4204
|
-
method: "POST",
|
|
4205
|
-
path: "/api/v1/subscription/pay",
|
|
4206
|
-
...this.requestOptions(options)
|
|
4207
|
-
});
|
|
4208
|
-
}
|
|
4209
|
-
/** Включает или отключает автопродление. */
|
|
4210
|
-
setAutoRenewal(enabled, options = {}) {
|
|
4211
|
-
return this.http.request({
|
|
4212
|
-
method: "POST",
|
|
4213
|
-
path: "/api/v1/subscription/auto-renewal",
|
|
4214
|
-
body: { enabled },
|
|
4215
|
-
...this.requestOptions(options)
|
|
4216
|
-
});
|
|
4217
|
-
}
|
|
4218
|
-
/** Запускает привязку карты. */
|
|
4219
|
-
bindCard(options = {}) {
|
|
4220
|
-
return this.http.request({
|
|
4221
|
-
method: "POST",
|
|
4222
|
-
path: "/api/v1/subscription/bind-card",
|
|
4223
|
-
...this.requestOptions(options)
|
|
4224
|
-
});
|
|
4225
|
-
}
|
|
4226
|
-
/** Загружает список способов оплаты. Пустой массив, если карт нет. */
|
|
4227
|
-
async methods(options = {}) {
|
|
4228
|
-
const body = await this.http.request({
|
|
4229
|
-
method: "GET",
|
|
4230
|
-
path: "/api/v1/subscription/methods",
|
|
4231
|
-
...this.requestOptions(options)
|
|
4232
|
-
});
|
|
4233
|
-
return Array.isArray(body) ? body : [];
|
|
4234
|
-
}
|
|
4235
|
-
/** Делает способ оплаты основным. */
|
|
4236
|
-
setDefaultMethod(methodId, options = {}) {
|
|
4237
|
-
return this.http.request({
|
|
4238
|
-
method: "POST",
|
|
4239
|
-
path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}/default`,
|
|
4240
|
-
...this.requestOptions(options)
|
|
4241
|
-
});
|
|
4242
|
-
}
|
|
4243
|
-
/** Удаляет способ оплаты. */
|
|
4244
|
-
removeMethod(methodId, options = {}) {
|
|
4245
|
-
return this.http.request({
|
|
4246
|
-
method: "DELETE",
|
|
4247
|
-
path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}`,
|
|
4248
|
-
...this.requestOptions(options)
|
|
4249
|
-
});
|
|
4250
|
-
}
|
|
4251
|
-
};
|
|
4252
|
-
var PlatformResource = class extends BaseResource {
|
|
4253
|
-
/** Загружает журнал изменений. */
|
|
4254
|
-
async changelog(options = {}) {
|
|
4255
|
-
const body = await this.http.request({
|
|
4256
|
-
method: "GET",
|
|
4257
|
-
path: "/api/platform/changelog",
|
|
4258
|
-
...this.requestOptions(options)
|
|
4259
|
-
});
|
|
4260
|
-
return Array.isArray(body) ? body : [];
|
|
4261
|
-
}
|
|
4262
|
-
/** Загружает анонсы платформы. */
|
|
4263
|
-
async announcements(options = {}) {
|
|
4264
|
-
const body = await this.http.request({
|
|
4265
|
-
method: "GET",
|
|
4266
|
-
path: "/api/platform/announcements",
|
|
4267
|
-
...this.requestOptions(options)
|
|
4268
|
-
});
|
|
4269
|
-
return pickArray(body, "announcements");
|
|
4270
|
-
}
|
|
4271
|
-
/** Загружает баннер текущего события — виджет «портал». */
|
|
4272
|
-
portal(options = {}) {
|
|
4273
|
-
return this.http.request({
|
|
4274
|
-
method: "GET",
|
|
4275
|
-
path: "/api/v1/portal",
|
|
4276
|
-
...this.requestOptions(options)
|
|
4277
|
-
});
|
|
4228
|
+
*
|
|
4229
|
+
* @param tag название без решётки; кодируется автоматически, поэтому кириллица
|
|
4230
|
+
* и пробелы допустимы
|
|
4231
|
+
*/
|
|
4232
|
+
posts(tag, params = {}) {
|
|
4233
|
+
return this.#posts.list({ ...params, tag });
|
|
4234
|
+
}
|
|
4235
|
+
/** Перебирает посты по хэштегу. */
|
|
4236
|
+
iteratePosts(tag, params = {}) {
|
|
4237
|
+
return this.#posts.iterate({ ...params, tag });
|
|
4278
4238
|
}
|
|
4279
4239
|
};
|
|
4280
4240
|
|
|
4281
4241
|
// src/resources/notifications.ts
|
|
4242
|
+
var NOTIFICATION_SETTING_KEYS = [
|
|
4243
|
+
"enabled",
|
|
4244
|
+
"sound",
|
|
4245
|
+
"follows",
|
|
4246
|
+
"wallPosts",
|
|
4247
|
+
"likes",
|
|
4248
|
+
"comments",
|
|
4249
|
+
"mentions"
|
|
4250
|
+
];
|
|
4282
4251
|
var READ_BATCH_SIZE = 20;
|
|
4283
4252
|
function readSettings(body) {
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
follows: pickBoolean(body, "follows", true),
|
|
4288
|
-
wallPosts: pickBoolean(body, "wallPosts", true),
|
|
4289
|
-
likes: pickBoolean(body, "likes", true),
|
|
4290
|
-
comments: pickBoolean(body, "comments", true),
|
|
4291
|
-
mentions: pickBoolean(body, "mentions", true)
|
|
4292
|
-
};
|
|
4253
|
+
const settings = {};
|
|
4254
|
+
for (const key of NOTIFICATION_SETTING_KEYS) settings[key] = pickBoolean(body, key, true);
|
|
4255
|
+
return settings;
|
|
4293
4256
|
}
|
|
4294
4257
|
var NotificationsResource = class extends BaseResource {
|
|
4258
|
+
/** Уведомления: `/api/notifications/`, пагинация по смещению. */
|
|
4259
|
+
#list = this.paginated({
|
|
4260
|
+
// Завершающий слэш обязателен: без него сервер отвечает ошибкой.
|
|
4261
|
+
path: () => "/api/notifications/",
|
|
4262
|
+
query: (p) => ({ limit: p.limit }),
|
|
4263
|
+
start: (p) => ({ offset: p.offset ?? 0 }),
|
|
4264
|
+
read: (body, state) => {
|
|
4265
|
+
const page = readOffsetPage(body, "notifications", state.offset ?? 0);
|
|
4266
|
+
return { ...page, items: page.items.map(normalizeNotification) };
|
|
4267
|
+
},
|
|
4268
|
+
mode: PaginationMode.Offset
|
|
4269
|
+
});
|
|
4295
4270
|
/**
|
|
4296
4271
|
* Загружает страницу уведомлений.
|
|
4297
4272
|
*
|
|
4298
|
-
* Пагинация здесь основана на смещении.
|
|
4299
|
-
* и притворяется, что это курсор; библиотека отдаёт честное число.
|
|
4273
|
+
* Пагинация здесь основана на смещении.
|
|
4300
4274
|
*
|
|
4301
4275
|
* @example
|
|
4302
4276
|
* ```ts
|
|
@@ -4305,19 +4279,7 @@ var NotificationsResource = class extends BaseResource {
|
|
|
4305
4279
|
* ```
|
|
4306
4280
|
*/
|
|
4307
4281
|
list(params = {}) {
|
|
4308
|
-
return this.#
|
|
4309
|
-
}
|
|
4310
|
-
/** Общая загрузка страницы для {@link list} и {@link iterate}. */
|
|
4311
|
-
async #loadPage(params, offset) {
|
|
4312
|
-
const body = await this.http.request({
|
|
4313
|
-
method: "GET",
|
|
4314
|
-
// Завершающий слэш обязателен: без него сервер отвечает ошибкой.
|
|
4315
|
-
path: "/api/notifications/",
|
|
4316
|
-
query: { limit: params.limit, offset },
|
|
4317
|
-
...this.requestOptions(params)
|
|
4318
|
-
});
|
|
4319
|
-
const page = readOffsetPage(body, "notifications", offset);
|
|
4320
|
-
return { ...page, items: page.items.map(normalizeNotification) };
|
|
4282
|
+
return this.#list.list(params);
|
|
4321
4283
|
}
|
|
4322
4284
|
/**
|
|
4323
4285
|
* Перебирает уведомления.
|
|
@@ -4330,11 +4292,7 @@ var NotificationsResource = class extends BaseResource {
|
|
|
4330
4292
|
* ```
|
|
4331
4293
|
*/
|
|
4332
4294
|
iterate(params = {}) {
|
|
4333
|
-
return this.
|
|
4334
|
-
PaginationMode.Offset,
|
|
4335
|
-
(state) => this.#loadPage(params, state.offset ?? 0),
|
|
4336
|
-
{ ...params, ...params.offset !== void 0 ? { start: { offset: params.offset } } : {} }
|
|
4337
|
-
);
|
|
4295
|
+
return this.#list.iterate(params);
|
|
4338
4296
|
}
|
|
4339
4297
|
/** Загружает число непрочитанных уведомлений. */
|
|
4340
4298
|
async count(options = {}) {
|
|
@@ -4406,15 +4364,7 @@ var NotificationsResource = class extends BaseResource {
|
|
|
4406
4364
|
*/
|
|
4407
4365
|
async updateSettings(input, options = {}) {
|
|
4408
4366
|
const payload = {};
|
|
4409
|
-
for (const key of
|
|
4410
|
-
"enabled",
|
|
4411
|
-
"sound",
|
|
4412
|
-
"follows",
|
|
4413
|
-
"wallPosts",
|
|
4414
|
-
"likes",
|
|
4415
|
-
"comments",
|
|
4416
|
-
"mentions"
|
|
4417
|
-
]) {
|
|
4367
|
+
for (const key of NOTIFICATION_SETTING_KEYS) {
|
|
4418
4368
|
const value = input[key];
|
|
4419
4369
|
if (value !== void 0) payload[key] = value;
|
|
4420
4370
|
}
|
|
@@ -4428,9 +4378,74 @@ var NotificationsResource = class extends BaseResource {
|
|
|
4428
4378
|
}
|
|
4429
4379
|
};
|
|
4430
4380
|
|
|
4381
|
+
// src/resources/platform.ts
|
|
4382
|
+
var PlatformResource = class extends BaseResource {
|
|
4383
|
+
/** Загружает журнал изменений. */
|
|
4384
|
+
async changelog(options = {}) {
|
|
4385
|
+
const body = await this.http.request({
|
|
4386
|
+
method: "GET",
|
|
4387
|
+
path: "/api/platform/changelog",
|
|
4388
|
+
...this.requestOptions(options)
|
|
4389
|
+
});
|
|
4390
|
+
return Array.isArray(body) ? body : [];
|
|
4391
|
+
}
|
|
4392
|
+
/** Загружает анонсы платформы. */
|
|
4393
|
+
async announcements(options = {}) {
|
|
4394
|
+
const body = await this.http.request({
|
|
4395
|
+
method: "GET",
|
|
4396
|
+
path: "/api/platform/announcements",
|
|
4397
|
+
...this.requestOptions(options)
|
|
4398
|
+
});
|
|
4399
|
+
return pickArray(body, "announcements");
|
|
4400
|
+
}
|
|
4401
|
+
/** Загружает баннер текущего события — виджет «портал». */
|
|
4402
|
+
portal(options = {}) {
|
|
4403
|
+
return this.http.request({
|
|
4404
|
+
method: "GET",
|
|
4405
|
+
path: "/api/v1/portal",
|
|
4406
|
+
...this.requestOptions(options)
|
|
4407
|
+
});
|
|
4408
|
+
}
|
|
4409
|
+
};
|
|
4410
|
+
|
|
4431
4411
|
// src/resources/posts.ts
|
|
4412
|
+
function cursorStart(params) {
|
|
4413
|
+
return params.cursor ? { cursor: params.cursor } : {};
|
|
4414
|
+
}
|
|
4432
4415
|
var PostsResource = class extends BaseResource {
|
|
4433
4416
|
#uploadFiles;
|
|
4417
|
+
/** Лента: `/api/posts`, курсорная пагинация. */
|
|
4418
|
+
#feed = this.paginated({
|
|
4419
|
+
path: () => "/api/posts",
|
|
4420
|
+
query: (p) => ({ tab: p.tab, limit: p.limit }),
|
|
4421
|
+
start: cursorStart,
|
|
4422
|
+
read: (body) => readCursorPage(body, "posts"),
|
|
4423
|
+
mode: PaginationMode.Cursor
|
|
4424
|
+
});
|
|
4425
|
+
/** Стена пользователя: `/api/posts/user/{user}`. */
|
|
4426
|
+
#wall = this.paginated({
|
|
4427
|
+
path: (p) => `/api/posts/user/${encodePathSegment(p.user, "user")}`,
|
|
4428
|
+
query: (p) => ({ limit: p.limit, sort: p.sort, pinnedPostId: p.pinnedPostId }),
|
|
4429
|
+
start: cursorStart,
|
|
4430
|
+
read: (body) => readCursorPage(body, "posts"),
|
|
4431
|
+
mode: PaginationMode.Cursor
|
|
4432
|
+
});
|
|
4433
|
+
/** Понравившиеся посты пользователя: `/api/posts/user/{user}/liked`. */
|
|
4434
|
+
#liked = this.paginated({
|
|
4435
|
+
path: (p) => `/api/posts/user/${encodePathSegment(p.user, "user")}/liked`,
|
|
4436
|
+
query: (p) => ({ limit: p.limit }),
|
|
4437
|
+
start: cursorStart,
|
|
4438
|
+
read: (body) => readCursorPage(body, "posts"),
|
|
4439
|
+
mode: PaginationMode.Cursor
|
|
4440
|
+
});
|
|
4441
|
+
/** Комментарии к посту: курсор лежит рядом со списком, поэтому свой reader. */
|
|
4442
|
+
#comments = this.paginated({
|
|
4443
|
+
path: (p) => `/api/posts/${encodePathSegment(p.postId, "postId")}/comments`,
|
|
4444
|
+
query: (p) => ({ limit: p.limit, sort: p.sort }),
|
|
4445
|
+
start: cursorStart,
|
|
4446
|
+
read: (body) => readFlatCursorPage(body, "comments"),
|
|
4447
|
+
mode: PaginationMode.Cursor
|
|
4448
|
+
});
|
|
4434
4449
|
constructor(http, deps) {
|
|
4435
4450
|
super(http);
|
|
4436
4451
|
this.#uploadFiles = deps.uploadFiles;
|
|
@@ -4444,14 +4459,8 @@ var PostsResource = class extends BaseResource {
|
|
|
4444
4459
|
* const next = await itd.posts.list({ tab: FeedTab.Following, cursor: page.nextCursor ?? undefined });
|
|
4445
4460
|
* ```
|
|
4446
4461
|
*/
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
method: "GET",
|
|
4450
|
-
path: "/api/posts",
|
|
4451
|
-
query: { tab: params.tab, limit: params.limit, cursor: params.cursor },
|
|
4452
|
-
...this.requestOptions(params)
|
|
4453
|
-
});
|
|
4454
|
-
return readCursorPage(body, "posts");
|
|
4462
|
+
list(params = {}) {
|
|
4463
|
+
return this.#feed.list(params);
|
|
4455
4464
|
}
|
|
4456
4465
|
/**
|
|
4457
4466
|
* Перебирает ленту, сама подставляя курсоры.
|
|
@@ -4464,19 +4473,7 @@ var PostsResource = class extends BaseResource {
|
|
|
4464
4473
|
* ```
|
|
4465
4474
|
*/
|
|
4466
4475
|
iterate(params = {}) {
|
|
4467
|
-
return this.
|
|
4468
|
-
PaginationMode.Cursor,
|
|
4469
|
-
async (state) => {
|
|
4470
|
-
const body = await this.http.request({
|
|
4471
|
-
method: "GET",
|
|
4472
|
-
path: "/api/posts",
|
|
4473
|
-
query: withPageState({ tab: params.tab, limit: params.limit }, state),
|
|
4474
|
-
...this.requestOptions(params)
|
|
4475
|
-
});
|
|
4476
|
-
return readCursorPage(body, "posts");
|
|
4477
|
-
},
|
|
4478
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4479
|
-
);
|
|
4476
|
+
return this.#feed.iterate(params);
|
|
4480
4477
|
}
|
|
4481
4478
|
/**
|
|
4482
4479
|
* Публикует пост.
|
|
@@ -4630,66 +4627,20 @@ var PostsResource = class extends BaseResource {
|
|
|
4630
4627
|
*
|
|
4631
4628
|
* Принимает и UUID, и имя пользователя.
|
|
4632
4629
|
*/
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
method: "GET",
|
|
4636
|
-
path: `/api/posts/user/${encodePathSegment(user, "user")}`,
|
|
4637
|
-
query: {
|
|
4638
|
-
limit: params.limit,
|
|
4639
|
-
cursor: params.cursor,
|
|
4640
|
-
sort: params.sort,
|
|
4641
|
-
pinnedPostId: params.pinnedPostId
|
|
4642
|
-
},
|
|
4643
|
-
...this.requestOptions(params)
|
|
4644
|
-
});
|
|
4645
|
-
return readCursorPage(body, "posts");
|
|
4630
|
+
byUser(user, params = {}) {
|
|
4631
|
+
return this.#wall.list({ ...params, user });
|
|
4646
4632
|
}
|
|
4647
4633
|
/** Перебирает стену пользователя. Что именно в неё входит — см. {@link byUser}. */
|
|
4648
4634
|
iterateByUser(user, params = {}) {
|
|
4649
|
-
|
|
4650
|
-
return this.paginate(
|
|
4651
|
-
PaginationMode.Cursor,
|
|
4652
|
-
async (state) => {
|
|
4653
|
-
const body = await this.http.request({
|
|
4654
|
-
method: "GET",
|
|
4655
|
-
path,
|
|
4656
|
-
query: withPageState(
|
|
4657
|
-
{ limit: params.limit, sort: params.sort, pinnedPostId: params.pinnedPostId },
|
|
4658
|
-
state
|
|
4659
|
-
),
|
|
4660
|
-
...this.requestOptions(params)
|
|
4661
|
-
});
|
|
4662
|
-
return readCursorPage(body, "posts");
|
|
4663
|
-
},
|
|
4664
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4665
|
-
);
|
|
4635
|
+
return this.#wall.iterate({ ...params, user });
|
|
4666
4636
|
}
|
|
4667
4637
|
/** Загружает страницу постов, которые пользователь отметил реакцией. */
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
method: "GET",
|
|
4671
|
-
path: `/api/posts/user/${encodePathSegment(user, "user")}/liked`,
|
|
4672
|
-
query: { limit: params.limit, cursor: params.cursor },
|
|
4673
|
-
...this.requestOptions(params)
|
|
4674
|
-
});
|
|
4675
|
-
return readCursorPage(body, "posts");
|
|
4638
|
+
likedByUser(user, params = {}) {
|
|
4639
|
+
return this.#liked.list({ ...params, user });
|
|
4676
4640
|
}
|
|
4677
4641
|
/** Перебирает посты, которые пользователь отметил реакцией. */
|
|
4678
4642
|
iterateLikedByUser(user, params = {}) {
|
|
4679
|
-
|
|
4680
|
-
return this.paginate(
|
|
4681
|
-
PaginationMode.Cursor,
|
|
4682
|
-
async (state) => {
|
|
4683
|
-
const body = await this.http.request({
|
|
4684
|
-
method: "GET",
|
|
4685
|
-
path,
|
|
4686
|
-
query: withPageState({ limit: params.limit }, state),
|
|
4687
|
-
...this.requestOptions(params)
|
|
4688
|
-
});
|
|
4689
|
-
return readCursorPage(body, "posts");
|
|
4690
|
-
},
|
|
4691
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4692
|
-
);
|
|
4643
|
+
return this.#liked.iterate({ ...params, user });
|
|
4693
4644
|
}
|
|
4694
4645
|
/**
|
|
4695
4646
|
* Загружает страницу комментариев к посту.
|
|
@@ -4697,31 +4648,12 @@ var PostsResource = class extends BaseResource {
|
|
|
4697
4648
|
* У этого эндпоинта курсор и признак продолжения лежат рядом со списком, а не внутри
|
|
4698
4649
|
* объекта `pagination`, как у остальных, — разница скрыта внутри.
|
|
4699
4650
|
*/
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
method: "GET",
|
|
4703
|
-
path: `/api/posts/${encodePathSegment(postId, "postId")}/comments`,
|
|
4704
|
-
query: { limit: params.limit, cursor: params.cursor, sort: params.sort },
|
|
4705
|
-
...this.requestOptions(params)
|
|
4706
|
-
});
|
|
4707
|
-
return readFlatCursorPage(body, "comments");
|
|
4651
|
+
comments(postId, params = {}) {
|
|
4652
|
+
return this.#comments.list({ ...params, postId });
|
|
4708
4653
|
}
|
|
4709
4654
|
/** Перебирает комментарии к посту. */
|
|
4710
4655
|
iterateComments(postId, params = {}) {
|
|
4711
|
-
|
|
4712
|
-
return this.paginate(
|
|
4713
|
-
PaginationMode.Cursor,
|
|
4714
|
-
async (state) => {
|
|
4715
|
-
const body = await this.http.request({
|
|
4716
|
-
method: "GET",
|
|
4717
|
-
path,
|
|
4718
|
-
query: withPageState({ limit: params.limit, sort: params.sort }, state),
|
|
4719
|
-
...this.requestOptions(params)
|
|
4720
|
-
});
|
|
4721
|
-
return readFlatCursorPage(body, "comments");
|
|
4722
|
-
},
|
|
4723
|
-
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4724
|
-
);
|
|
4656
|
+
return this.#comments.iterate({ ...params, postId });
|
|
4725
4657
|
}
|
|
4726
4658
|
/**
|
|
4727
4659
|
* Комментирует пост.
|
|
@@ -4766,6 +4698,122 @@ var PostsResource = class extends BaseResource {
|
|
|
4766
4698
|
}
|
|
4767
4699
|
};
|
|
4768
4700
|
|
|
4701
|
+
// src/resources/reports.ts
|
|
4702
|
+
var ReportsResource = class extends BaseResource {
|
|
4703
|
+
/**
|
|
4704
|
+
* Отправляет жалобу.
|
|
4705
|
+
*
|
|
4706
|
+
* Повторная жалоба на тот же объект отклоняется сервером с сообщением
|
|
4707
|
+
* «Вы уже отправляли жалобу на этот контент».
|
|
4708
|
+
*
|
|
4709
|
+
* @example
|
|
4710
|
+
* ```ts
|
|
4711
|
+
* await itd.reports.create(report.post(postId).reason('spam'));
|
|
4712
|
+
* await itd.reports.create({ targetType: 'user', targetId, reason: 'fraud' });
|
|
4713
|
+
* ```
|
|
4714
|
+
*/
|
|
4715
|
+
create(input, options = {}) {
|
|
4716
|
+
const data = resolveReport(input);
|
|
4717
|
+
return this.http.request({
|
|
4718
|
+
method: "POST",
|
|
4719
|
+
path: "/api/reports",
|
|
4720
|
+
body: data,
|
|
4721
|
+
...this.requestOptions(options)
|
|
4722
|
+
});
|
|
4723
|
+
}
|
|
4724
|
+
};
|
|
4725
|
+
|
|
4726
|
+
// src/resources/search.ts
|
|
4727
|
+
var SearchResource = class extends BaseResource {
|
|
4728
|
+
/**
|
|
4729
|
+
* Ищет пользователей и хэштеги одним запросом.
|
|
4730
|
+
*
|
|
4731
|
+
* @example
|
|
4732
|
+
* ```ts
|
|
4733
|
+
* const { users, hashtags } = await itd.search.all('арт');
|
|
4734
|
+
* ```
|
|
4735
|
+
*/
|
|
4736
|
+
async all(query, options = {}) {
|
|
4737
|
+
const body = await this.http.request({
|
|
4738
|
+
method: "GET",
|
|
4739
|
+
path: "/api/search",
|
|
4740
|
+
query: { q: query },
|
|
4741
|
+
...this.requestOptions(options)
|
|
4742
|
+
});
|
|
4743
|
+
return {
|
|
4744
|
+
users: pickArray(body, "users"),
|
|
4745
|
+
hashtags: pickArray(body, "hashtags")
|
|
4746
|
+
};
|
|
4747
|
+
}
|
|
4748
|
+
};
|
|
4749
|
+
|
|
4750
|
+
// src/resources/subscription.ts
|
|
4751
|
+
var SubscriptionResource = class extends BaseResource {
|
|
4752
|
+
/** Загружает состояние подписки и её цену. */
|
|
4753
|
+
status(options = {}) {
|
|
4754
|
+
return this.http.request({
|
|
4755
|
+
method: "GET",
|
|
4756
|
+
// Завершающий слэш обязателен.
|
|
4757
|
+
path: "/api/v1/subscription/",
|
|
4758
|
+
...this.requestOptions(options)
|
|
4759
|
+
});
|
|
4760
|
+
}
|
|
4761
|
+
/**
|
|
4762
|
+
* Запускает оплату подписки.
|
|
4763
|
+
*
|
|
4764
|
+
* Форма ответа в документации API не описана, поэтому тип результата не уточняется.
|
|
4765
|
+
*/
|
|
4766
|
+
pay(options = {}) {
|
|
4767
|
+
return this.http.request({
|
|
4768
|
+
method: "POST",
|
|
4769
|
+
path: "/api/v1/subscription/pay",
|
|
4770
|
+
...this.requestOptions(options)
|
|
4771
|
+
});
|
|
4772
|
+
}
|
|
4773
|
+
/** Включает или отключает автопродление. */
|
|
4774
|
+
setAutoRenewal(enabled, options = {}) {
|
|
4775
|
+
return this.http.request({
|
|
4776
|
+
method: "POST",
|
|
4777
|
+
path: "/api/v1/subscription/auto-renewal",
|
|
4778
|
+
body: { enabled },
|
|
4779
|
+
...this.requestOptions(options)
|
|
4780
|
+
});
|
|
4781
|
+
}
|
|
4782
|
+
/** Запускает привязку карты. */
|
|
4783
|
+
bindCard(options = {}) {
|
|
4784
|
+
return this.http.request({
|
|
4785
|
+
method: "POST",
|
|
4786
|
+
path: "/api/v1/subscription/bind-card",
|
|
4787
|
+
...this.requestOptions(options)
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4790
|
+
/** Загружает список способов оплаты. Пустой массив, если карт нет. */
|
|
4791
|
+
async methods(options = {}) {
|
|
4792
|
+
const body = await this.http.request({
|
|
4793
|
+
method: "GET",
|
|
4794
|
+
path: "/api/v1/subscription/methods",
|
|
4795
|
+
...this.requestOptions(options)
|
|
4796
|
+
});
|
|
4797
|
+
return Array.isArray(body) ? body : [];
|
|
4798
|
+
}
|
|
4799
|
+
/** Делает способ оплаты основным. */
|
|
4800
|
+
setDefaultMethod(methodId, options = {}) {
|
|
4801
|
+
return this.http.request({
|
|
4802
|
+
method: "POST",
|
|
4803
|
+
path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}/default`,
|
|
4804
|
+
...this.requestOptions(options)
|
|
4805
|
+
});
|
|
4806
|
+
}
|
|
4807
|
+
/** Удаляет способ оплаты. */
|
|
4808
|
+
removeMethod(methodId, options = {}) {
|
|
4809
|
+
return this.http.request({
|
|
4810
|
+
method: "DELETE",
|
|
4811
|
+
path: `/api/v1/subscription/methods/${encodePathSegment(methodId, "methodId")}`,
|
|
4812
|
+
...this.requestOptions(options)
|
|
4813
|
+
});
|
|
4814
|
+
}
|
|
4815
|
+
};
|
|
4816
|
+
|
|
4769
4817
|
// src/resources/telemetry.ts
|
|
4770
4818
|
var TelemetryResource = class extends BaseResource {
|
|
4771
4819
|
/** Идентификатор сессии телеметрии, общий для всех событий этого объекта. */
|
|
@@ -4832,6 +4880,21 @@ var TelemetryResource = class extends BaseResource {
|
|
|
4832
4880
|
|
|
4833
4881
|
// src/resources/users.ts
|
|
4834
4882
|
var UsersResource = class extends BaseResource {
|
|
4883
|
+
/**
|
|
4884
|
+
* Списки пользователей: подписчики, подписки, заблокированные.
|
|
4885
|
+
*
|
|
4886
|
+
* Путь приходит в параметрах — так один описатель обслуживает все три эндпоинта. Имена
|
|
4887
|
+
* полей перечислены с запасом: списки приходят под `users`, но альтернативное имя ничего
|
|
4888
|
+
* не стоит и спасает, если эндпоинт назовёт список по-своему. `page` уходит в запрос, хотя
|
|
4889
|
+
* сервер его сейчас не читает (см. {@link followers}): когда починят — заработает само.
|
|
4890
|
+
*/
|
|
4891
|
+
#userList = this.paginated({
|
|
4892
|
+
path: (p) => p.path,
|
|
4893
|
+
query: (p) => ({ limit: p.limit }),
|
|
4894
|
+
start: (p) => p.page !== void 0 ? { page: p.page } : {},
|
|
4895
|
+
read: (body) => readPagedPage(body, "users", "followers", "following", "blocked"),
|
|
4896
|
+
mode: PaginationMode.Page
|
|
4897
|
+
});
|
|
4835
4898
|
/** Загружает свой профиль — с подпиской и признаком подтверждённого телефона. */
|
|
4836
4899
|
me(options = {}) {
|
|
4837
4900
|
return this.http.request({
|
|
@@ -5077,38 +5140,33 @@ var UsersResource = class extends BaseResource {
|
|
|
5077
5140
|
...this.requestOptions(options)
|
|
5078
5141
|
});
|
|
5079
5142
|
}
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5143
|
+
#userPage(path, params) {
|
|
5144
|
+
return this.#userList.list({ ...params, path });
|
|
5145
|
+
}
|
|
5146
|
+
#userPaginator(path, params) {
|
|
5147
|
+
return this.#userList.iterate({ ...params, path });
|
|
5148
|
+
}
|
|
5149
|
+
};
|
|
5150
|
+
|
|
5151
|
+
// src/resources/verification.ts
|
|
5152
|
+
var VerificationResource = class extends BaseResource {
|
|
5153
|
+
/** Загружает статус заявки. Значение `none` означает, что заявка не подавалась. */
|
|
5154
|
+
status(options = {}) {
|
|
5155
|
+
return this.http.request({
|
|
5092
5156
|
method: "GET",
|
|
5093
|
-
path,
|
|
5094
|
-
|
|
5095
|
-
...this.requestOptions(params)
|
|
5157
|
+
path: "/api/verification/status",
|
|
5158
|
+
...this.requestOptions(options)
|
|
5096
5159
|
});
|
|
5097
|
-
return readPagedPage(body, "users", "followers", "following", "blocked");
|
|
5098
5160
|
}
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5161
|
+
/** Подаёт заявку на верификацию с видео. */
|
|
5162
|
+
submit(videoUrl, options = {}) {
|
|
5163
|
+
return this.http.request({
|
|
5164
|
+
method: "POST",
|
|
5165
|
+
path: "/api/verification/submit",
|
|
5166
|
+
body: { videoUrl },
|
|
5167
|
+
...this.requestOptions(options)
|
|
5102
5168
|
});
|
|
5103
5169
|
}
|
|
5104
|
-
#userPaginator(path, params) {
|
|
5105
|
-
return this.paginate(
|
|
5106
|
-
PaginationMode.Page,
|
|
5107
|
-
(state) => this.#loadUserPage(path, params, state),
|
|
5108
|
-
// Без `start` перебор начинался бы с первой страницы, молча игнорируя `page`.
|
|
5109
|
-
{ ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
|
|
5110
|
-
);
|
|
5111
|
-
}
|
|
5112
5170
|
};
|
|
5113
5171
|
|
|
5114
5172
|
// src/client.ts
|
|
@@ -5119,6 +5177,8 @@ var ItdClient = class {
|
|
|
5119
5177
|
#jar;
|
|
5120
5178
|
#queue;
|
|
5121
5179
|
#plugins = new PluginRegistry();
|
|
5180
|
+
/** Порождённые потоки уведомлений — чтобы `close()` мог закрыть их разом. */
|
|
5181
|
+
#streams = /* @__PURE__ */ new Set();
|
|
5122
5182
|
/** Авторизация, сессии и пароли. */
|
|
5123
5183
|
auth;
|
|
5124
5184
|
/** Профили, подписки, блокировки, приватность. */
|
|
@@ -5149,26 +5209,56 @@ var ItdClient = class {
|
|
|
5149
5209
|
* @experimental Недокументированные эндпоинты. Библиотека никогда не отправляет их сама.
|
|
5150
5210
|
*/
|
|
5151
5211
|
telemetry;
|
|
5152
|
-
constructor(options = {}) {
|
|
5153
|
-
|
|
5212
|
+
constructor(options = {}, internals = {}) {
|
|
5213
|
+
const config = resolveConfig(options);
|
|
5214
|
+
this.#config = config;
|
|
5154
5215
|
this.#jar = new CookieJar();
|
|
5155
|
-
|
|
5156
|
-
this.#
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
onUnauthorized: () => this.#authManager.onUnauthorized(),
|
|
5163
|
-
getCookieHeader: (url) => this.#jar.getHeader(url),
|
|
5164
|
-
saveCookies: (url, response) => this.#jar.setFromResponse(url, response),
|
|
5165
|
-
...this.#queue ? { schedule: this.#queue.schedule.bind(this.#queue) } : {},
|
|
5166
|
-
// Планировщик нужен, даже когда обычные повторы выключены: лимит частоты
|
|
5167
|
-
// живёт по своим правилам и настраивается отдельно, в `rateLimit`.
|
|
5168
|
-
...this.#config.retry || this.#config.rateLimit ? { nextRetryDelay: this.#createRetryScheduler() } : {},
|
|
5169
|
-
...this.#queue && this.#config.rateLimit?.respectHeaders ? { onRateLimit: this.#throttleByHeaders.bind(this) } : {}
|
|
5216
|
+
const queue = config.rateLimit ? new RequestQueue(config.rateLimit) : void 0;
|
|
5217
|
+
this.#queue = queue;
|
|
5218
|
+
let authManager;
|
|
5219
|
+
const transport = new Transport(config, {
|
|
5220
|
+
cookies: config.useCookieJar ? this.#jar : void 0,
|
|
5221
|
+
getDeviceId: () => authManager.getDeviceId(),
|
|
5222
|
+
onRateLimit: queue && config.rateLimit?.respectHeaders ? (limit, remaining) => this.#throttleByHeaders(limit, remaining) : void 0
|
|
5170
5223
|
});
|
|
5171
|
-
|
|
5224
|
+
const pluginsLayer = createPluginsMiddleware(this.#plugins);
|
|
5225
|
+
const retriesLayer = createRetryMiddleware({
|
|
5226
|
+
retry: config.retry,
|
|
5227
|
+
rateLimitDelays: config.rateLimit?.retryDelays ?? [],
|
|
5228
|
+
pauseQueue: queue ? (ms) => queue.pause(ms) : void 0,
|
|
5229
|
+
hooks: config.hooks,
|
|
5230
|
+
logger: config.logger,
|
|
5231
|
+
buildUrl: (request) => transport.buildUrl(request)
|
|
5232
|
+
});
|
|
5233
|
+
const authRetry = config.retry ? {
|
|
5234
|
+
attempts: config.retry.attempts,
|
|
5235
|
+
baseDelay: config.retry.baseDelay,
|
|
5236
|
+
maxDelay: config.retry.maxDelay,
|
|
5237
|
+
jitter: config.retry.jitter,
|
|
5238
|
+
retryWrites: true,
|
|
5239
|
+
...config.retry.shouldRetry ? { shouldRetry: config.retry.shouldRetry } : {}
|
|
5240
|
+
} : void 0;
|
|
5241
|
+
const authPipeline = composePipeline([pluginsLayer, retriesLayer], transport.send);
|
|
5242
|
+
const authHandler = (request) => authRetry && request.retry === void 0 ? authPipeline({ ...request, retry: authRetry }) : authPipeline(request);
|
|
5243
|
+
authManager = new AuthManager(config, authHandler, this.#jar);
|
|
5244
|
+
this.#authManager = authManager;
|
|
5245
|
+
const middlewares = [];
|
|
5246
|
+
if (queue) middlewares.push(createQueueMiddleware(queue.schedule.bind(queue)));
|
|
5247
|
+
middlewares.push(pluginsLayer);
|
|
5248
|
+
middlewares.push(retriesLayer);
|
|
5249
|
+
middlewares.push(
|
|
5250
|
+
createAuthMiddleware({
|
|
5251
|
+
getAuthHeaders: () => authManager.getAuthHeaders(),
|
|
5252
|
+
onUnauthorized: () => authManager.onUnauthorized(),
|
|
5253
|
+
autoRefresh: config.autoRefresh
|
|
5254
|
+
})
|
|
5255
|
+
);
|
|
5256
|
+
const handler = composePipeline(middlewares, transport.send);
|
|
5257
|
+
this.#http = new HttpClient({ handler, plugins: this.#plugins, baseUrl: config.baseUrl });
|
|
5258
|
+
this.files = new FilesResource(
|
|
5259
|
+
this.#http,
|
|
5260
|
+
internals.fileReader ? { readFile: internals.fileReader } : {}
|
|
5261
|
+
);
|
|
5172
5262
|
const uploadFiles = (files, requestOptions) => this.files.uploadMany(files, requestOptions ?? {});
|
|
5173
5263
|
this.auth = new AuthResource(this.#http, { auth: this.#authManager });
|
|
5174
5264
|
this.users = new UsersResource(this.#http);
|
|
@@ -5258,17 +5348,44 @@ var ItdClient = class {
|
|
|
5258
5348
|
* ```
|
|
5259
5349
|
*/
|
|
5260
5350
|
realtime(options = {}) {
|
|
5261
|
-
|
|
5351
|
+
let stream;
|
|
5352
|
+
stream = new ItdRealtime(
|
|
5262
5353
|
{
|
|
5263
5354
|
baseUrl: this.#config.baseUrl,
|
|
5264
5355
|
fetch: this.#config.fetch,
|
|
5265
5356
|
getToken: () => this.#authManager.getAccessToken(),
|
|
5266
5357
|
refresh: () => this.#authManager.onUnauthorized(),
|
|
5267
5358
|
fetchUnreadCount: () => this.notifications.count(),
|
|
5359
|
+
onClose: () => this.#streams.delete(stream),
|
|
5268
5360
|
logger: this.#config.logger
|
|
5269
5361
|
},
|
|
5270
5362
|
options
|
|
5271
5363
|
);
|
|
5364
|
+
this.#streams.add(stream);
|
|
5365
|
+
return stream;
|
|
5366
|
+
}
|
|
5367
|
+
/**
|
|
5368
|
+
* Освобождает ресурсы клиента: останавливает очередь запросов (снимает отложенные паузы)
|
|
5369
|
+
* и закрывает все потоки уведомлений, созданные через {@link realtime}.
|
|
5370
|
+
*
|
|
5371
|
+
* После вызова клиентом можно пользоваться снова — новые запросы поднимут всё заново,
|
|
5372
|
+
* но уже созданные потоки останутся закрытыми.
|
|
5373
|
+
*
|
|
5374
|
+
* @example
|
|
5375
|
+
* ```ts
|
|
5376
|
+
* await using itd = new ItdClient({ auth: token });
|
|
5377
|
+
* // …работа…
|
|
5378
|
+
* // close() вызовется сам на выходе из блока
|
|
5379
|
+
* ```
|
|
5380
|
+
*/
|
|
5381
|
+
async close() {
|
|
5382
|
+
for (const stream of this.#streams) stream.disconnect();
|
|
5383
|
+
this.#streams.clear();
|
|
5384
|
+
this.#queue?.stop();
|
|
5385
|
+
}
|
|
5386
|
+
/** Позволяет использовать клиент с `await using`. */
|
|
5387
|
+
[Symbol.asyncDispose]() {
|
|
5388
|
+
return this.close();
|
|
5272
5389
|
}
|
|
5273
5390
|
/** Текущая сессия целиком — чтобы сохранить её самостоятельно. */
|
|
5274
5391
|
getSession() {
|
|
@@ -5278,16 +5395,6 @@ var ItdClient = class {
|
|
|
5278
5395
|
setSession(session) {
|
|
5279
5396
|
return this.#authManager.setSession(session);
|
|
5280
5397
|
}
|
|
5281
|
-
/**
|
|
5282
|
-
* Подключает чтение файлов с диска.
|
|
5283
|
-
*
|
|
5284
|
-
* Вызывается из `itd-api/node`; напрямую обычно не нужно.
|
|
5285
|
-
*
|
|
5286
|
-
* @internal
|
|
5287
|
-
*/
|
|
5288
|
-
setFileReader(readFile) {
|
|
5289
|
-
this.files.setFileReader(readFile);
|
|
5290
|
-
}
|
|
5291
5398
|
/**
|
|
5292
5399
|
* Придерживает очередь, когда лимит сервера исчерпан.
|
|
5293
5400
|
*
|
|
@@ -5308,35 +5415,12 @@ var ItdClient = class {
|
|
|
5308
5415
|
`\u043B\u0438\u043C\u0438\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D (${remaining} \u0438\u0437 ${limit ?? "?"}), \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u0436\u0434\u0451\u0442 ${first} \u043C\u0441`
|
|
5309
5416
|
);
|
|
5310
5417
|
}
|
|
5311
|
-
/**
|
|
5312
|
-
* Собирает планировщик повторов и связывает его с очередью.
|
|
5313
|
-
*
|
|
5314
|
-
* Ответ `429` обрабатывается отдельно от прочих ошибок. Причина в том, что сервер
|
|
5315
|
-
* не присылает `Retry-After` и не сообщает время сброса окна: экспоненциальный откат
|
|
5316
|
-
* в сотни миллисекунд здесь бесполезен, а окно измеряется десятками секунд. Вместо
|
|
5317
|
-
* расчёта берётся лестница пауз `rateLimit.retryDelays`, и она не зависит
|
|
5318
|
-
* от `retry.attempts`, у которого совсем другая задача.
|
|
5319
|
-
*
|
|
5320
|
-
* Пауза накладывается на всю очередь: иначе остальные запросы продолжат добивать API,
|
|
5321
|
-
* пока первый ждёт.
|
|
5322
|
-
*/
|
|
5323
|
-
#createRetryScheduler() {
|
|
5324
|
-
const retry = this.#config.retry;
|
|
5325
|
-
const scheduler = retry ? createRetryScheduler(retry) : void 0;
|
|
5326
|
-
const queue = this.#queue;
|
|
5327
|
-
const delays = this.#config.rateLimit?.retryDelays ?? [];
|
|
5328
|
-
return (error, attempt, method) => {
|
|
5329
|
-
if (isItdRateLimitError(error)) {
|
|
5330
|
-
const wait = error.retryAfter ?? delays[attempt - 1];
|
|
5331
|
-
if (wait === void 0) return void 0;
|
|
5332
|
-
queue?.pause(wait);
|
|
5333
|
-
this.#config.logger?.debug(`\u043B\u0438\u043C\u0438\u0442 \u0447\u0430\u0441\u0442\u043E\u0442\u044B, \u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempt + 1} \u0447\u0435\u0440\u0435\u0437 ${wait} \u043C\u0441`);
|
|
5334
|
-
return wait;
|
|
5335
|
-
}
|
|
5336
|
-
return scheduler?.(error, attempt, method);
|
|
5337
|
-
};
|
|
5338
|
-
}
|
|
5339
5418
|
};
|
|
5419
|
+
if (typeof Symbol.asyncDispose !== "symbol") {
|
|
5420
|
+
const prototype = ItdClient.prototype;
|
|
5421
|
+
prototype[/* @__PURE__ */ Symbol.for("Symbol.asyncDispose")] = prototype.undefined;
|
|
5422
|
+
delete prototype.undefined;
|
|
5423
|
+
}
|
|
5340
5424
|
function createClient(options = {}) {
|
|
5341
5425
|
return new ItdClient(options);
|
|
5342
5426
|
}
|
|
@@ -5536,5 +5620,5 @@ exports.readUnreadCountEvent = readUnreadCountEvent;
|
|
|
5536
5620
|
exports.report = report;
|
|
5537
5621
|
exports.resolveNotificationUrl = resolveNotificationUrl;
|
|
5538
5622
|
exports.toDate = toDate;
|
|
5539
|
-
//# sourceMappingURL=chunk-
|
|
5540
|
-
//# sourceMappingURL=chunk-
|
|
5623
|
+
//# sourceMappingURL=chunk-ATCZ4T2K.cjs.map
|
|
5624
|
+
//# sourceMappingURL=chunk-ATCZ4T2K.cjs.map
|