target-supabase-sdk 0.2.17 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/node/telegram/api-client.d.ts +12 -0
- package/dist/node/telegram/api-client.d.ts.map +1 -0
- package/dist/node/telegram/chunk-locator.d.ts +10 -0
- package/dist/node/telegram/chunk-locator.d.ts.map +1 -0
- package/dist/node/telegram/chunk.d.ts +4 -0
- package/dist/node/telegram/chunk.d.ts.map +1 -0
- package/dist/node/telegram/index.d.ts +8 -0
- package/dist/node/telegram/index.d.ts.map +1 -0
- package/dist/node/telegram/storage-provider.d.ts +32 -0
- package/dist/node/telegram/storage-provider.d.ts.map +1 -0
- package/dist/node/telegram/types.d.ts +53 -0
- package/dist/node/telegram/types.d.ts.map +1 -0
- package/dist/node.d.ts +2 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +295 -5
- package/dist/node.js.map +1 -1
- package/package.json +8 -1
- package/src/node/telegram/api-client.ts +146 -0
- package/src/node/telegram/chunk-locator.ts +23 -0
- package/src/node/telegram/chunk.ts +33 -0
- package/src/node/telegram/index.ts +20 -0
- package/src/node/telegram/storage-provider.ts +182 -0
- package/src/node/telegram/types.ts +82 -0
- package/src/node.ts +23 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "target-supabase-sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
4
4
|
"description": "Supabase client initializer and data access APIs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "KyLoc20",
|
|
@@ -69,8 +69,14 @@
|
|
|
69
69
|
"@supabase/postgrest-js": "^1.19.0",
|
|
70
70
|
"@supabase/supabase-js": "^2.50.0",
|
|
71
71
|
"lodash-es": "^4.17.21",
|
|
72
|
+
"undici": "^7.10.0",
|
|
72
73
|
"zod": "^3.23.0"
|
|
73
74
|
},
|
|
75
|
+
"peerDependenciesMeta": {
|
|
76
|
+
"undici": {
|
|
77
|
+
"optional": true
|
|
78
|
+
}
|
|
79
|
+
},
|
|
74
80
|
"devDependencies": {
|
|
75
81
|
"@biomejs/biome": "^2.5.3",
|
|
76
82
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
@@ -84,6 +90,7 @@
|
|
|
84
90
|
"tslib": "^2.8.1",
|
|
85
91
|
"tsx": "^4.19.4",
|
|
86
92
|
"typescript": "~5.7.2",
|
|
93
|
+
"undici": "^7.10.0",
|
|
87
94
|
"zod": "^3.24.0"
|
|
88
95
|
},
|
|
89
96
|
"publishConfig": {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { type Dispatcher, ProxyAgent } from "undici";
|
|
2
|
+
import {
|
|
3
|
+
createLogger,
|
|
4
|
+
type FetchInitFactory,
|
|
5
|
+
fetchBinaryWithRetry,
|
|
6
|
+
fetchWithRetry,
|
|
7
|
+
isRetryableHttpStatus,
|
|
8
|
+
} from "../../browser";
|
|
9
|
+
import type { ResolvedTelegramStorageOptions, TelegramApiResponse } from "./types";
|
|
10
|
+
|
|
11
|
+
const logger = createLogger({ module: "telegram" });
|
|
12
|
+
|
|
13
|
+
let cachedDispatcher: Dispatcher | undefined;
|
|
14
|
+
let cachedProxyUrl: string | undefined;
|
|
15
|
+
|
|
16
|
+
/** Per-request undici dispatcher when proxyUrl is set (Supabase stays direct). */
|
|
17
|
+
function telegramFetchDispatcher(proxyUrl: string | undefined): Dispatcher | undefined {
|
|
18
|
+
if (proxyUrl == null) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
if (cachedDispatcher != null && cachedProxyUrl === proxyUrl) {
|
|
22
|
+
return cachedDispatcher;
|
|
23
|
+
}
|
|
24
|
+
cachedDispatcher = new ProxyAgent(proxyUrl);
|
|
25
|
+
cachedProxyUrl = proxyUrl;
|
|
26
|
+
return cachedDispatcher;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function telegramNetworkHint(proxyUrl: string | undefined): string {
|
|
30
|
+
return proxyUrl
|
|
31
|
+
? ` (proxy=${proxyUrl}, ensure Clash/V2Ray is running)`
|
|
32
|
+
: " (set proxyUrl / TELEGRAM_PROXY if api.telegram.org is blocked)";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CallTelegramOptions {
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
retryAttempts?: number;
|
|
38
|
+
retryBaseMs?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function botApiUrl(options: ResolvedTelegramStorageOptions, method: string): string {
|
|
42
|
+
return `${options.apiBaseUrl}/bot${options.botToken}/${method}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function telegramFileDownloadUrl(options: ResolvedTelegramStorageOptions, filePath: string): string {
|
|
46
|
+
const normalized = filePath.replace(/^\/+/, "");
|
|
47
|
+
return `${options.apiBaseUrl}/file/bot${options.botToken}/${normalized}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sleep(ms: number): Promise<void> {
|
|
51
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function telegramBackoff(attempt: number, retryBaseMs: number): Promise<void> {
|
|
55
|
+
return sleep(retryBaseMs * 2 ** (attempt - 1));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Bot API JSON call — transport retry via SDK fetch; outer loop retries Telegram `{ ok: false }`. */
|
|
59
|
+
export async function callTelegramApi<T>(
|
|
60
|
+
options: ResolvedTelegramStorageOptions,
|
|
61
|
+
method: string,
|
|
62
|
+
initOrFactory: RequestInit | FetchInitFactory,
|
|
63
|
+
callOptions: CallTelegramOptions = {},
|
|
64
|
+
): Promise<T> {
|
|
65
|
+
const timeoutMs = callOptions.timeoutMs ?? options.fetchTimeoutMs;
|
|
66
|
+
const maxAttempts = callOptions.retryAttempts ?? 1;
|
|
67
|
+
const retryBaseMs = callOptions.retryBaseMs ?? options.uploadRetryBaseMs;
|
|
68
|
+
const url = botApiUrl(options, method);
|
|
69
|
+
const label = `Telegram ${method}`;
|
|
70
|
+
const dispatcher = telegramFetchDispatcher(options.proxyUrl);
|
|
71
|
+
const hint = telegramNetworkHint(options.proxyUrl);
|
|
72
|
+
|
|
73
|
+
let lastError: Error | null = null;
|
|
74
|
+
|
|
75
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
76
|
+
const response = await fetchWithRetry(url, initOrFactory, {
|
|
77
|
+
label,
|
|
78
|
+
timeoutMs,
|
|
79
|
+
maxAttempts: attempt === 1 ? maxAttempts : 1,
|
|
80
|
+
retryBaseMs,
|
|
81
|
+
dispatcher,
|
|
82
|
+
hint,
|
|
83
|
+
logger,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
let payload: TelegramApiResponse<T>;
|
|
87
|
+
try {
|
|
88
|
+
payload = (await response.json()) as TelegramApiResponse<T>;
|
|
89
|
+
} catch {
|
|
90
|
+
lastError = new Error(`${label}: invalid JSON response (HTTP ${response.status})`);
|
|
91
|
+
if (attempt < maxAttempts && isRetryableHttpStatus(response.status)) {
|
|
92
|
+
await telegramBackoff(attempt, retryBaseMs);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
throw lastError;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!payload.ok) {
|
|
99
|
+
const description = payload.description ?? `HTTP ${response.status}`;
|
|
100
|
+
lastError = new Error(`${label}: ${description}`);
|
|
101
|
+
if (attempt < maxAttempts && isRetryableHttpStatus(response.status)) {
|
|
102
|
+
logger.warn("retrying after API error", {
|
|
103
|
+
topic: "telegram",
|
|
104
|
+
data: { method, attempt, description },
|
|
105
|
+
});
|
|
106
|
+
await telegramBackoff(attempt, retryBaseMs);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
throw lastError;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (payload.result == null) {
|
|
113
|
+
throw new Error(`${label}: empty result in response`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (attempt > 1) {
|
|
117
|
+
logger.info("request succeeded after retry", { topic: "telegram", data: { method, attempt } });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return payload.result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
throw lastError ?? new Error(`${label}: request failed`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function telegramDownloadBinary(
|
|
127
|
+
options: ResolvedTelegramStorageOptions,
|
|
128
|
+
url: string,
|
|
129
|
+
label: string,
|
|
130
|
+
callOptions: CallTelegramOptions = {},
|
|
131
|
+
): Promise<ArrayBuffer> {
|
|
132
|
+
const dispatcher = telegramFetchDispatcher(options.proxyUrl);
|
|
133
|
+
return fetchBinaryWithRetry(
|
|
134
|
+
url,
|
|
135
|
+
{ method: "GET" },
|
|
136
|
+
{
|
|
137
|
+
label: `Telegram ${label}`,
|
|
138
|
+
timeoutMs: callOptions.timeoutMs ?? options.fetchTimeoutMs,
|
|
139
|
+
maxAttempts: callOptions.retryAttempts ?? 1,
|
|
140
|
+
retryBaseMs: callOptions.retryBaseMs ?? options.uploadRetryBaseMs,
|
|
141
|
+
dispatcher,
|
|
142
|
+
hint: telegramNetworkHint(options.proxyUrl),
|
|
143
|
+
logger,
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram chunk URL encoding: `${messageId}|${fileId}`.
|
|
3
|
+
* Legacy parcels store bare `file_id` (no messageId → cannot deleteMessage).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const SEPARATOR = "|";
|
|
7
|
+
|
|
8
|
+
export function encodeTelegramChunkUrl(messageId: number, fileId: string): string {
|
|
9
|
+
return `${messageId}${SEPARATOR}${fileId}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parseTelegramChunkUrl(locator: string): { messageId: number | null; fileId: string } {
|
|
13
|
+
const idx = locator.indexOf(SEPARATOR);
|
|
14
|
+
if (idx <= 0) {
|
|
15
|
+
return { messageId: null, fileId: locator };
|
|
16
|
+
}
|
|
17
|
+
const mid = Number(locator.slice(0, idx));
|
|
18
|
+
const fileId = locator.slice(idx + 1);
|
|
19
|
+
if (!Number.isInteger(mid) || mid <= 0 || fileId === "") {
|
|
20
|
+
return { messageId: null, fileId: locator };
|
|
21
|
+
}
|
|
22
|
+
return { messageId: mid, fileId };
|
|
23
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { callTelegramApi, telegramDownloadBinary, telegramFileDownloadUrl } from "./api-client";
|
|
2
|
+
import type { ResolvedTelegramStorageOptions, TelegramFile } from "./types";
|
|
3
|
+
|
|
4
|
+
/** Download chunk bytes by Telegram `file_id` stored in Chunk.url. */
|
|
5
|
+
export async function downloadTelegramChunk(
|
|
6
|
+
options: ResolvedTelegramStorageOptions,
|
|
7
|
+
fileId: string,
|
|
8
|
+
): Promise<ArrayBuffer> {
|
|
9
|
+
const file = await callTelegramApi<TelegramFile>(
|
|
10
|
+
options,
|
|
11
|
+
`getFile?file_id=${encodeURIComponent(fileId)}`,
|
|
12
|
+
{ method: "GET" },
|
|
13
|
+
{
|
|
14
|
+
retryAttempts: options.uploadRetryAttempts,
|
|
15
|
+
retryBaseMs: options.uploadRetryBaseMs,
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
if (file.file_path == null || file.file_path === "") {
|
|
20
|
+
throw new Error(`Telegram getFile: missing file_path for file_id ${fileId}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return telegramDownloadBinary(
|
|
24
|
+
options,
|
|
25
|
+
telegramFileDownloadUrl(options, file.file_path),
|
|
26
|
+
`download:${fileId.slice(0, 12)}…`,
|
|
27
|
+
{
|
|
28
|
+
timeoutMs: options.uploadTimeoutMs,
|
|
29
|
+
retryAttempts: options.uploadRetryAttempts,
|
|
30
|
+
retryBaseMs: options.uploadRetryBaseMs,
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type { CallTelegramOptions } from "./api-client";
|
|
2
|
+
export { callTelegramApi, telegramDownloadBinary, telegramFileDownloadUrl } from "./api-client";
|
|
3
|
+
export { downloadTelegramChunk } from "./chunk";
|
|
4
|
+
export { encodeTelegramChunkUrl, parseTelegramChunkUrl } from "./chunk-locator";
|
|
5
|
+
export {
|
|
6
|
+
createTelegramStorageProvider,
|
|
7
|
+
PROVIDER_TELEGRAM,
|
|
8
|
+
type TelegramChunkDeleteResult,
|
|
9
|
+
type TelegramStorageProvider,
|
|
10
|
+
} from "./storage-provider";
|
|
11
|
+
export type {
|
|
12
|
+
ResolvedTelegramStorageOptions,
|
|
13
|
+
TelegramApiResponse,
|
|
14
|
+
TelegramDocument,
|
|
15
|
+
TelegramFile,
|
|
16
|
+
TelegramMessage,
|
|
17
|
+
TelegramStorageProviderOptions,
|
|
18
|
+
TelegramUploadResult,
|
|
19
|
+
} from "./types";
|
|
20
|
+
export { resolveTelegramStorageOptions } from "./types";
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { CreateUploadAdapterOptions, Parcel, StorageProviderModule, UploadTracker } from "../../browser";
|
|
2
|
+
import { isOpaqueChunkUrl } from "../../browser";
|
|
3
|
+
import { callTelegramApi } from "./api-client";
|
|
4
|
+
import { downloadTelegramChunk } from "./chunk";
|
|
5
|
+
import { encodeTelegramChunkUrl, parseTelegramChunkUrl } from "./chunk-locator";
|
|
6
|
+
import {
|
|
7
|
+
type ResolvedTelegramStorageOptions,
|
|
8
|
+
resolveTelegramStorageOptions,
|
|
9
|
+
type TelegramMessage,
|
|
10
|
+
type TelegramStorageProviderOptions,
|
|
11
|
+
type TelegramUploadResult,
|
|
12
|
+
} from "./types";
|
|
13
|
+
|
|
14
|
+
export const PROVIDER_TELEGRAM = "telegram";
|
|
15
|
+
|
|
16
|
+
export interface TelegramChunkDeleteResult {
|
|
17
|
+
deleted: number;
|
|
18
|
+
failed: number;
|
|
19
|
+
skipped: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TelegramStorageProvider extends StorageProviderModule {
|
|
23
|
+
readonly provider: typeof PROVIDER_TELEGRAM;
|
|
24
|
+
/** Low-level upload for probes/CLIs. */
|
|
25
|
+
uploadChunk(data: ArrayBuffer, filename: string): Promise<TelegramUploadResult>;
|
|
26
|
+
/** Best-effort deleteMessage for rollback / probe cleanup. */
|
|
27
|
+
deleteMessage(messageId: number): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Best-effort delete Telegram messages for parcel chunks.
|
|
30
|
+
* Chunks without an encoded messageId (legacy bare file_id) are skipped.
|
|
31
|
+
*/
|
|
32
|
+
deleteChunkBlobs(parcel: Parcel): Promise<TelegramChunkDeleteResult>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function uploadChunkToTelegram(
|
|
36
|
+
options: ResolvedTelegramStorageOptions,
|
|
37
|
+
data: ArrayBuffer,
|
|
38
|
+
filename: string,
|
|
39
|
+
): Promise<TelegramUploadResult> {
|
|
40
|
+
const sizeMb = (data.byteLength / (1024 * 1024)).toFixed(2);
|
|
41
|
+
|
|
42
|
+
const message = await callTelegramApi<TelegramMessage>(
|
|
43
|
+
options,
|
|
44
|
+
"sendDocument",
|
|
45
|
+
() => {
|
|
46
|
+
const form = new FormData();
|
|
47
|
+
form.append("chat_id", options.chatId);
|
|
48
|
+
form.append("document", new Blob([data]), filename);
|
|
49
|
+
return { method: "POST", body: form };
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
timeoutMs: options.uploadTimeoutMs,
|
|
53
|
+
retryAttempts: options.uploadRetryAttempts,
|
|
54
|
+
retryBaseMs: options.uploadRetryBaseMs,
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const fileId = message.document?.file_id;
|
|
59
|
+
if (fileId == null || fileId === "") {
|
|
60
|
+
throw new Error(`Telegram sendDocument: missing file_id (chunk ${filename}, ${sizeMb} MB)`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { fileId, messageId: message.message_id };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function deleteTelegramMessage(options: ResolvedTelegramStorageOptions, messageId: number): Promise<void> {
|
|
67
|
+
await callTelegramApi<boolean>(
|
|
68
|
+
options,
|
|
69
|
+
"deleteMessage",
|
|
70
|
+
() => {
|
|
71
|
+
const form = new FormData();
|
|
72
|
+
form.append("chat_id", options.chatId);
|
|
73
|
+
form.append("message_id", String(messageId));
|
|
74
|
+
return { method: "POST", body: form };
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
retryAttempts: 3,
|
|
78
|
+
retryBaseMs: 1_000,
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function rollbackTelegramUploads(options: ResolvedTelegramStorageOptions, handles: unknown[]): Promise<void> {
|
|
84
|
+
for (const item of handles) {
|
|
85
|
+
const upload = item as TelegramUploadResult;
|
|
86
|
+
if (upload?.messageId == null) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await deleteTelegramMessage(options, upload.messageId);
|
|
91
|
+
} catch {
|
|
92
|
+
// Best-effort cleanup
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function assertTelegramParcel(parcel: Parcel): void {
|
|
98
|
+
const chunks = parcel.details.chunkList;
|
|
99
|
+
if (chunks.length === 0) {
|
|
100
|
+
throw new Error("Parcel has no chunks");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const nonTelegram = chunks.filter((chunk) => chunk.provider != null && chunk.provider !== PROVIDER_TELEGRAM);
|
|
104
|
+
if (nonTelegram.length > 0) {
|
|
105
|
+
const providers = [...new Set(nonTelegram.map((chunk) => chunk.provider))];
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Parcel is not telegram-only (found providers: ${providers.join(", ")}). ` +
|
|
108
|
+
"Multi-provider restore is not yet supported for this parcel.",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build a Telegram {@link StorageProviderModule}. Caller registers it — no side effects.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* import { createStorageProviderRegistry, createTelegramStorageProvider } from "target-supabase-sdk/node";
|
|
119
|
+
* const registry = createStorageProviderRegistry();
|
|
120
|
+
* registry.register(createTelegramStorageProvider({ botToken, chatId, proxyUrl }));
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export function createTelegramStorageProvider(input: TelegramStorageProviderOptions): TelegramStorageProvider {
|
|
124
|
+
const options = resolveTelegramStorageOptions(input);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
provider: PROVIDER_TELEGRAM,
|
|
128
|
+
uploadChunk: (data, filename) => uploadChunkToTelegram(options, data, filename),
|
|
129
|
+
deleteMessage: (messageId) => deleteTelegramMessage(options, messageId),
|
|
130
|
+
async deleteChunkBlobs(parcel) {
|
|
131
|
+
let deleted = 0;
|
|
132
|
+
let failed = 0;
|
|
133
|
+
let skipped = 0;
|
|
134
|
+
for (const chunk of parcel.details.chunkList) {
|
|
135
|
+
if (chunk.provider != null && chunk.provider !== PROVIDER_TELEGRAM) {
|
|
136
|
+
skipped += 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const { messageId } = parseTelegramChunkUrl(chunk.url);
|
|
140
|
+
if (messageId == null) {
|
|
141
|
+
skipped += 1;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
await deleteTelegramMessage(options, messageId);
|
|
146
|
+
deleted += 1;
|
|
147
|
+
} catch {
|
|
148
|
+
failed += 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { deleted, failed, skipped };
|
|
152
|
+
},
|
|
153
|
+
createUploadAdapter(adapterOptions?: CreateUploadAdapterOptions) {
|
|
154
|
+
const tracker = adapterOptions?.tracker as UploadTracker<TelegramUploadResult> | undefined;
|
|
155
|
+
return {
|
|
156
|
+
provider: PROVIDER_TELEGRAM,
|
|
157
|
+
async upload(data, pathOrKey) {
|
|
158
|
+
const result = await uploadChunkToTelegram(options, data, pathOrKey);
|
|
159
|
+
tracker?.push(result);
|
|
160
|
+
// Persist messageId so later deleteParcel can call deleteMessage.
|
|
161
|
+
return { url: encodeTelegramChunkUrl(result.messageId, result.fileId) };
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
resolveChunk: (locator) => {
|
|
166
|
+
const { fileId } = parseTelegramChunkUrl(locator);
|
|
167
|
+
return downloadTelegramChunk(options, fileId);
|
|
168
|
+
},
|
|
169
|
+
matchesOpaqueUrl: isOpaqueChunkUrl,
|
|
170
|
+
async probe() {
|
|
171
|
+
try {
|
|
172
|
+
const me = await callTelegramApi<{ username?: string }>(options, "getMe", { method: "GET" });
|
|
173
|
+
return { ok: true as const, detail: { username: me.username ?? "unknown" } };
|
|
174
|
+
} catch (error) {
|
|
175
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
176
|
+
return { ok: false as const, error: message };
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
rollbackUpload: (handles) => rollbackTelegramUploads(options, handles),
|
|
180
|
+
assertParcelRestorable: assertTelegramParcel,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export interface TelegramApiResponse<T> {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
description?: string;
|
|
4
|
+
result?: T;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface TelegramDocument {
|
|
8
|
+
file_id: string;
|
|
9
|
+
file_unique_id: string;
|
|
10
|
+
file_size?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TelegramMessage {
|
|
14
|
+
message_id: number;
|
|
15
|
+
document?: TelegramDocument;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TelegramFile {
|
|
19
|
+
file_id: string;
|
|
20
|
+
file_unique_id: string;
|
|
21
|
+
file_size?: number;
|
|
22
|
+
file_path?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TelegramUploadResult {
|
|
26
|
+
fileId: string;
|
|
27
|
+
messageId: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Injected config for Telegram Bot API storage — do not read process.env inside SDK. */
|
|
31
|
+
export interface TelegramStorageProviderOptions {
|
|
32
|
+
botToken: string;
|
|
33
|
+
chatId: string;
|
|
34
|
+
/** Default `https://api.telegram.org`. */
|
|
35
|
+
apiBaseUrl?: string;
|
|
36
|
+
/** HTTP(S) proxy for Telegram only (e.g. Clash). Supabase stays direct. */
|
|
37
|
+
proxyUrl?: string;
|
|
38
|
+
/** Default 30s — getMe / getFile. */
|
|
39
|
+
fetchTimeoutMs?: number;
|
|
40
|
+
/** Default 120s — sendDocument / file download. */
|
|
41
|
+
uploadTimeoutMs?: number;
|
|
42
|
+
/** Default 3. */
|
|
43
|
+
uploadRetryAttempts?: number;
|
|
44
|
+
/** Default 2000 — backoff base for attempt N: base * 2^(N-1). */
|
|
45
|
+
uploadRetryBaseMs?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ResolvedTelegramStorageOptions {
|
|
49
|
+
botToken: string;
|
|
50
|
+
chatId: string;
|
|
51
|
+
apiBaseUrl: string;
|
|
52
|
+
proxyUrl?: string;
|
|
53
|
+
fetchTimeoutMs: number;
|
|
54
|
+
uploadTimeoutMs: number;
|
|
55
|
+
uploadRetryAttempts: number;
|
|
56
|
+
uploadRetryBaseMs: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveTelegramStorageOptions(options: TelegramStorageProviderOptions): ResolvedTelegramStorageOptions {
|
|
60
|
+
const botToken = options.botToken.trim();
|
|
61
|
+
const chatId = options.chatId.trim();
|
|
62
|
+
if (botToken === "") {
|
|
63
|
+
throw new Error("TelegramStorageProviderOptions.botToken is required");
|
|
64
|
+
}
|
|
65
|
+
if (chatId === "") {
|
|
66
|
+
throw new Error("TelegramStorageProviderOptions.chatId is required");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const apiBaseUrl = (options.apiBaseUrl?.trim() || "https://api.telegram.org").replace(/\/$/, "");
|
|
70
|
+
const proxyUrl = options.proxyUrl?.trim() || undefined;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
botToken,
|
|
74
|
+
chatId,
|
|
75
|
+
apiBaseUrl,
|
|
76
|
+
proxyUrl: proxyUrl !== "" ? proxyUrl : undefined,
|
|
77
|
+
fetchTimeoutMs: options.fetchTimeoutMs ?? 30_000,
|
|
78
|
+
uploadTimeoutMs: options.uploadTimeoutMs ?? 120_000,
|
|
79
|
+
uploadRetryAttempts: options.uploadRetryAttempts ?? 3,
|
|
80
|
+
uploadRetryBaseMs: options.uploadRetryBaseMs ?? 2_000,
|
|
81
|
+
};
|
|
82
|
+
}
|
package/src/node.ts
CHANGED
|
@@ -134,6 +134,29 @@ export type {
|
|
|
134
134
|
SingleProcessServiceOptions,
|
|
135
135
|
} from "./node/service-host";
|
|
136
136
|
export { createL3ChildLauncher, createServiceHost, runSingleProcessService } from "./node/service-host";
|
|
137
|
+
export type {
|
|
138
|
+
CallTelegramOptions,
|
|
139
|
+
ResolvedTelegramStorageOptions,
|
|
140
|
+
TelegramApiResponse,
|
|
141
|
+
TelegramChunkDeleteResult,
|
|
142
|
+
TelegramDocument,
|
|
143
|
+
TelegramFile,
|
|
144
|
+
TelegramMessage,
|
|
145
|
+
TelegramStorageProvider,
|
|
146
|
+
TelegramStorageProviderOptions,
|
|
147
|
+
TelegramUploadResult,
|
|
148
|
+
} from "./node/telegram";
|
|
149
|
+
export {
|
|
150
|
+
callTelegramApi,
|
|
151
|
+
createTelegramStorageProvider,
|
|
152
|
+
downloadTelegramChunk,
|
|
153
|
+
encodeTelegramChunkUrl,
|
|
154
|
+
PROVIDER_TELEGRAM,
|
|
155
|
+
parseTelegramChunkUrl,
|
|
156
|
+
resolveTelegramStorageOptions,
|
|
157
|
+
telegramDownloadBinary,
|
|
158
|
+
telegramFileDownloadUrl,
|
|
159
|
+
} from "./node/telegram";
|
|
137
160
|
export { RepoManager } from "./repo/repo-manager";
|
|
138
161
|
export { runCollectLogTick } from "./shared/log/spool/collector";
|
|
139
162
|
export { LOG_SPOOL_SERVICE_ID_ENV } from "./shared/log/spool/config";
|