WalleGriffkinder commited on
Commit
4bfc698
·
verified ·
1 Parent(s): 28ab557

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +175 -43
server.js CHANGED
@@ -4,8 +4,9 @@ import fssync from 'fs';
4
  import path from 'path';
5
  import fetch from 'node-fetch';
6
  import { glob } from 'glob';
7
- import { createProxyMiddleware } from 'http-proxy-middleware';
8
  import crypto from 'crypto';
 
9
 
10
  const APP_PORT = parseInt(process.env.PORT || '7860', 10);
11
  const INTERNAL_TELEGRAM_API_PORT = parseInt(process.env.INTERNAL_TELEGRAM_API_PORT || '8081', 10);
@@ -17,6 +18,8 @@ const GITHUB_USERNAME = process.env.GITHUB_USERNAME || '';
17
  const GITHUB_TOKEN = process.env.GITHUB_TOKEN || '';
18
  const ENV_GIST_ID = process.env.ENV_GIST_ID || '';
19
  const LINK_ENCRYPTION_KEY_PASSPHRASE = process.env.LINK_ENCRYPTION_KEY;
 
 
20
 
21
  const app = express();
22
 
@@ -31,6 +34,11 @@ const CRYPTO_ALGORITHM = 'aes-256-gcm';
31
  const CRYPTO_IV_LENGTH_BYTES = 12;
32
  const CRYPTO_AUTH_TAG_LENGTH_BYTES = 16;
33
 
 
 
 
 
 
34
  function encryptPayload(payloadObject) {
35
  if (!encryptionKeyBuffer) return null;
36
  try {
@@ -141,7 +149,7 @@ async function updateEnvGistInGithub() {
141
  const gistFilename = `hf_space_tg_tools_info.json`;
142
  let aggregateData = {
143
  description: `Hugging Face Spaces with TgAPIToolsTunnel - Aggregated Info. Maintained by TgAPIToolsTunnel instances.`,
144
- schema_version: "1.1.0",
145
  spaces_info: {}
146
  };
147
 
@@ -156,22 +164,11 @@ async function updateEnvGistInGithub() {
156
  const parsedContent = JSON.parse(existingGist.files[gistFilename].content);
157
  if (parsedContent && typeof parsedContent.spaces_info === 'object') {
158
  aggregateData = parsedContent;
159
- console.log(`Successfully fetched and parsed existing Gist file: ${gistFilename}`);
160
- } else {
161
- console.warn(`Gist file ${gistFilename} content not in expected format. Initializing.`);
162
  }
163
- } catch (parseError) {
164
- console.warn(`Error parsing Gist file ${gistFilename}: ${parseError.message}. Initializing.`);
165
- }
166
- } else {
167
- console.log(`Gist file ${gistFilename} not found in Gist ${ENV_GIST_ID}. Will create it.`);
168
  }
169
- } else if (gistFetchResponse.status !== 404) {
170
- console.warn(`Failed to fetch Gist ${ENV_GIST_ID}: ${gistFetchResponse.status} ${await gistFetchResponse.text()}. Proceeding with local data.`);
171
  }
172
- } catch (fetchError) {
173
- console.error(`Error fetching Gist ${ENV_GIST_ID}: ${fetchError.message}. Proceeding with local data.`);
174
- }
175
 
176
  const spaceId = process.env.SPACE_ID || 'unknown_space';
177
  const spaceHost = process.env.SPACE_HOST;
@@ -196,6 +193,7 @@ async function updateEnvGistInGithub() {
196
  internal_telegram_api_port: INTERNAL_TELEGRAM_API_PORT,
197
  files_ttl_hours: FILES_TTL_HOURS > 0 ? FILES_TTL_HOURS : 'disabled',
198
  link_encryption_enabled: !!encryptionKeyBuffer,
 
199
  };
200
 
201
  if (!aggregateData.spaces_info) aggregateData.spaces_info = {};
@@ -345,24 +343,37 @@ fileRouter.get('/downloadEncrypted', async (req, res) => {
345
  if (!encryptionKeyBuffer) return res.status(503).json({ error: 'Encryption/decryption is not configured on the server (LINK_ENCRYPTION_KEY not set).' });
346
 
347
  const decrypted = decryptPayload(encryptedPayload);
348
- if (!decrypted || !decrypted.bot_token || !decrypted.file_id) {
349
  return res.status(400).json({ error: 'Invalid or undecryptable payload.' });
350
  }
351
 
352
- const { bot_token, file_id } = decrypted;
353
-
 
 
 
 
 
354
  try {
355
- const tgApiResponse = await fetch(`http://localhost:${INTERNAL_TELEGRAM_API_PORT}/bot${bot_token}/getFile?file_id=${file_id}`);
356
- if (!tgApiResponse.ok) {
357
- const errorData = await tgApiResponse.json().catch(() => ({ description: "Unknown error from Telegram API" }));
358
- return res.status(tgApiResponse.status).json({ error: `Telegram API error: ${errorData.description || tgApiResponse.statusText}` });
359
- }
360
- const fileInfo = await tgApiResponse.json();
361
- if (!fileInfo.ok || !fileInfo.result || !fileInfo.result.file_path) {
362
- return res.status(404).json({ error: 'File not found or path not available via Telegram API.', details: fileInfo });
 
 
 
 
 
 
 
 
 
363
  }
364
-
365
- const filePathOnDisk = fileInfo.result.file_path;
366
  const resolvedDataDir = path.resolve(TELEGRAM_DATA_DIR);
367
  const resolvedFilePathOnDisk = path.resolve(filePathOnDisk);
368
 
@@ -372,14 +383,14 @@ fileRouter.get('/downloadEncrypted', async (req, res) => {
372
  }
373
 
374
  if (fssync.existsSync(resolvedFilePathOnDisk) && fssync.statSync(resolvedFilePathOnDisk).isFile()) {
375
- res.sendFile(resolvedFilePathOnDisk, { headers: { 'Content-Disposition': `attachment; filename="${path.basename(fileInfo.result.file_path)}"` } }, err => {
376
  if (err && !res.headersSent) res.status(500).send('Error sending file.');
377
  });
378
  } else {
379
  res.status(404).send('File not found on disk.');
380
  }
381
  } catch (error) {
382
- console.error(`Error in /downloadEncrypted for file_id ${file_id}:`, error);
383
  res.status(500).json({ error: 'Server error during encrypted download process.', details: error.message });
384
  }
385
  });
@@ -387,6 +398,7 @@ fileRouter.get('/downloadEncrypted', async (req, res) => {
387
  fileRouter.get('/:botToken/getFile', async (req, res) => {
388
  const { botToken } = req.params;
389
  const { file_id } = req.query;
 
390
 
391
  if (!botToken || !file_id) {
392
  return res.status(400).json({ error: 'Missing botToken in path or file_id in query.' });
@@ -406,19 +418,16 @@ fileRouter.get('/:botToken/getFile', async (req, res) => {
406
 
407
  const apiFilePath = fileInfo.result.file_path;
408
  let relativePathForLink = "";
409
-
410
  const resolvedNormalizedDataDir = path.resolve(TELEGRAM_DATA_DIR);
411
  const resolvedApiFilePath = path.resolve(apiFilePath);
412
 
413
  if (resolvedApiFilePath.startsWith(resolvedNormalizedDataDir)) {
414
  relativePathForLink = path.relative(resolvedNormalizedDataDir, resolvedApiFilePath);
415
  } else {
416
- console.warn(`[Warning] file_path from API (${apiFilePath}) does not seem to be inside TELEGRAM_DATA_DIR (${TELEGRAM_DATA_DIR}). Using file_path as is for link, which might be incorrect.`);
417
  relativePathForLink = apiFilePath;
418
  }
419
 
420
  if (relativePathForLink.startsWith('..')) {
421
- console.error(`[Security] Calculated relative path for link starts with '..': ${relativePathForLink}. Original API path: ${apiFilePath}`);
422
  fileInfo.result.direct_download_link_by_path = "Error: could not construct safe relative path.";
423
  } else {
424
  fileInfo.result.direct_download_link_by_path = `${publicSpaceUrl}/file/${relativePathForLink.split(path.sep).join('/')}`;
@@ -427,14 +436,17 @@ fileRouter.get('/:botToken/getFile', async (req, res) => {
427
  fileInfo.result.direct_download_link_by_id = `${publicSpaceUrl}/file/${botToken}/downloadFile?file_id=${file_id}`;
428
 
429
  if (encryptionKeyBuffer) {
430
- const encryptedPayload = encryptPayload({ bot_token: botToken, file_id: file_id });
431
- if (encryptedPayload) {
432
- fileInfo.result.encrypted_download_link = `${publicSpaceUrl}/file/downloadEncrypted?payload=${encryptedPayload}`;
433
- } else {
434
- fileInfo.result.encrypted_download_link = "Encryption failed or disabled server-side.";
 
 
 
 
 
435
  }
436
- } else {
437
- fileInfo.result.encrypted_download_link = "Encryption not configured on server (LINK_ENCRYPTION_KEY not set).";
438
  }
439
  }
440
  res.json(fileInfo);
@@ -504,15 +516,133 @@ fileRouter.get('/:filepath(*)', (req, res) => {
504
 
505
  app.use('/file', fileRouter);
506
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
  app.use(PROXY_TELEGRAM_PATH_PREFIX, createProxyMiddleware({
508
  target: `http://localhost:${INTERNAL_TELEGRAM_API_PORT}`,
509
  changeOrigin: true,
 
510
  pathRewrite: (pathValue, req) => {
 
 
 
 
511
  return pathValue.startsWith(PROXY_TELEGRAM_PATH_PREFIX) ? pathValue.substring(PROXY_TELEGRAM_PATH_PREFIX.length) : pathValue;
512
  },
513
  onProxyReq: (proxyReq, req, res) => {
514
  console.log(`[Proxy] Request to Telegram API: ${req.method} ${proxyReq.path}`);
515
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
  onError: (err, req, res) => {
517
  console.error('[Proxy] Error:', err);
518
  if (!res.headersSent) {
@@ -538,18 +668,19 @@ app.get('/', (req, res) => {
538
  file_operations: {
539
  _base: `${baseUrl}/file`,
540
  list_files_for_bot: `${baseUrl}/file/list?token=${botTokenPlaceholder}`,
541
- get_file_info_and_links: `${baseUrl}/file/${botTokenPlaceholder}/getFile?file_id=${fileIdPlaceholder}`,
542
  download_file_by_id: `${baseUrl}/file/${botTokenPlaceholder}/downloadFile?file_id=${fileIdPlaceholder}`,
543
  download_file_by_path: `${baseUrl}/file/${filePathPlaceholder}`,
544
  delete_file_by_path: `${baseUrl}/file/deleteFile?file=${filePathPlaceholder}`,
545
  delete_file_by_id: `${baseUrl}/file/${botTokenPlaceholder}/deleteById?file_id=${fileIdPlaceholder}`,
546
- download_via_encrypted_payload: `${baseUrl}/file/downloadEncrypted?payload=<ENCRYPTED_PAYLOAD_FROM_GETFILE_ENDPOINT>`,
547
  },
548
  gist_info_if_configured: ENV_GIST_ID ? `https://gist.github.com/${GITHUB_USERNAME || "_"}/${ENV_GIST_ID}` : "Gist update not configured",
549
  },
550
  notes: {
551
  file_paths_for_delete_or_direct_download: "Should be like 'botTOKEN/photos/file_XYZ.jpg'",
552
- link_encryption: encryptionKeyBuffer ? "Enabled" : "Disabled (LINK_ENCRYPTION_KEY not set in secrets)"
 
553
  }
554
  });
555
  });
@@ -560,6 +691,7 @@ app.listen(APP_PORT, () => {
560
  console.log(`Statistics available at /stats`);
561
  console.log(`File operations available under /file/`);
562
  console.log(`Telegram API proxied from ${PROXY_TELEGRAM_PATH_PREFIX}/`);
 
563
 
564
  updateEnvGistInGithub().catch(console.error);
565
 
 
4
  import path from 'path';
5
  import fetch from 'node-fetch';
6
  import { glob } from 'glob';
7
+ import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middleware';
8
  import crypto from 'crypto';
9
+ import zlib from 'zlib';
10
 
11
  const APP_PORT = parseInt(process.env.PORT || '7860', 10);
12
  const INTERNAL_TELEGRAM_API_PORT = parseInt(process.env.INTERNAL_TELEGRAM_API_PORT || '8081', 10);
 
18
  const GITHUB_TOKEN = process.env.GITHUB_TOKEN || '';
19
  const ENV_GIST_ID = process.env.ENV_GIST_ID || '';
20
  const LINK_ENCRYPTION_KEY_PASSPHRASE = process.env.LINK_ENCRYPTION_KEY;
21
+ const DEFAULT_LINK_EXPIRY_HOURS = parseInt(process.env.DEFAULT_LINK_EXPIRY_HOURS || '24', 10);
22
+
23
 
24
  const app = express();
25
 
 
34
  const CRYPTO_IV_LENGTH_BYTES = 12;
35
  const CRYPTO_AUTH_TAG_LENGTH_BYTES = 16;
36
 
37
+ function calculateExpiryTimestamp(hours) {
38
+ if (hours === -1) return null;
39
+ return Date.now() + hours * 60 * 60 * 1000;
40
+ }
41
+
42
  function encryptPayload(payloadObject) {
43
  if (!encryptionKeyBuffer) return null;
44
  try {
 
149
  const gistFilename = `hf_space_tg_tools_info.json`;
150
  let aggregateData = {
151
  description: `Hugging Face Spaces with TgAPIToolsTunnel - Aggregated Info. Maintained by TgAPIToolsTunnel instances.`,
152
+ schema_version: "1.2.0",
153
  spaces_info: {}
154
  };
155
 
 
164
  const parsedContent = JSON.parse(existingGist.files[gistFilename].content);
165
  if (parsedContent && typeof parsedContent.spaces_info === 'object') {
166
  aggregateData = parsedContent;
 
 
 
167
  }
168
+ } catch (parseError) {}
 
 
 
 
169
  }
 
 
170
  }
171
+ } catch (fetchError) {}
 
 
172
 
173
  const spaceId = process.env.SPACE_ID || 'unknown_space';
174
  const spaceHost = process.env.SPACE_HOST;
 
193
  internal_telegram_api_port: INTERNAL_TELEGRAM_API_PORT,
194
  files_ttl_hours: FILES_TTL_HOURS > 0 ? FILES_TTL_HOURS : 'disabled',
195
  link_encryption_enabled: !!encryptionKeyBuffer,
196
+ default_link_expiry_hours: DEFAULT_LINK_EXPIRY_HOURS
197
  };
198
 
199
  if (!aggregateData.spaces_info) aggregateData.spaces_info = {};
 
343
  if (!encryptionKeyBuffer) return res.status(503).json({ error: 'Encryption/decryption is not configured on the server (LINK_ENCRYPTION_KEY not set).' });
344
 
345
  const decrypted = decryptPayload(encryptedPayload);
346
+ if (!decrypted) {
347
  return res.status(400).json({ error: 'Invalid or undecryptable payload.' });
348
  }
349
 
350
+ if (decrypted.expiresAt && Date.now() > decrypted.expiresAt) {
351
+ return res.status(410).json({ error: 'Link expired.' });
352
+ }
353
+
354
+ let filePathOnDisk;
355
+ let originalFilename;
356
+
357
  try {
358
+ if (decrypted.type === 'id' && decrypted.bot_token && decrypted.file_id) {
359
+ const tgApiResponse = await fetch(`http://localhost:${INTERNAL_TELEGRAM_API_PORT}/bot${decrypted.bot_token}/getFile?file_id=${decrypted.file_id}`);
360
+ if (!tgApiResponse.ok) {
361
+ const errorData = await tgApiResponse.json().catch(() => ({ description: "Unknown error from Telegram API" }));
362
+ return res.status(tgApiResponse.status).json({ error: `Telegram API error: ${errorData.description || tgApiResponse.statusText}` });
363
+ }
364
+ const fileInfo = await tgApiResponse.json();
365
+ if (!fileInfo.ok || !fileInfo.result || !fileInfo.result.file_path) {
366
+ return res.status(404).json({ error: 'File not found or path not available via Telegram API.', details: fileInfo });
367
+ }
368
+ filePathOnDisk = fileInfo.result.file_path;
369
+ originalFilename = path.basename(filePathOnDisk);
370
+ } else if (decrypted.type === 'path' && decrypted.file_path_relative_to_data_dir) {
371
+ filePathOnDisk = path.join(TELEGRAM_DATA_DIR, decrypted.file_path_relative_to_data_dir);
372
+ originalFilename = path.basename(filePathOnDisk);
373
+ } else {
374
+ return res.status(400).json({ error: 'Invalid payload structure.' });
375
  }
376
+
 
377
  const resolvedDataDir = path.resolve(TELEGRAM_DATA_DIR);
378
  const resolvedFilePathOnDisk = path.resolve(filePathOnDisk);
379
 
 
383
  }
384
 
385
  if (fssync.existsSync(resolvedFilePathOnDisk) && fssync.statSync(resolvedFilePathOnDisk).isFile()) {
386
+ res.sendFile(resolvedFilePathOnDisk, { headers: { 'Content-Disposition': `attachment; filename="${originalFilename}"` } }, err => {
387
  if (err && !res.headersSent) res.status(500).send('Error sending file.');
388
  });
389
  } else {
390
  res.status(404).send('File not found on disk.');
391
  }
392
  } catch (error) {
393
+ console.error(`Error in /downloadEncrypted:`, error);
394
  res.status(500).json({ error: 'Server error during encrypted download process.', details: error.message });
395
  }
396
  });
 
398
  fileRouter.get('/:botToken/getFile', async (req, res) => {
399
  const { botToken } = req.params;
400
  const { file_id } = req.query;
401
+ const customExpiryHours = req.query.link_expiry_hours ? parseInt(req.query.link_expiry_hours, 10) : DEFAULT_LINK_EXPIRY_HOURS;
402
 
403
  if (!botToken || !file_id) {
404
  return res.status(400).json({ error: 'Missing botToken in path or file_id in query.' });
 
418
 
419
  const apiFilePath = fileInfo.result.file_path;
420
  let relativePathForLink = "";
 
421
  const resolvedNormalizedDataDir = path.resolve(TELEGRAM_DATA_DIR);
422
  const resolvedApiFilePath = path.resolve(apiFilePath);
423
 
424
  if (resolvedApiFilePath.startsWith(resolvedNormalizedDataDir)) {
425
  relativePathForLink = path.relative(resolvedNormalizedDataDir, resolvedApiFilePath);
426
  } else {
 
427
  relativePathForLink = apiFilePath;
428
  }
429
 
430
  if (relativePathForLink.startsWith('..')) {
 
431
  fileInfo.result.direct_download_link_by_path = "Error: could not construct safe relative path.";
432
  } else {
433
  fileInfo.result.direct_download_link_by_path = `${publicSpaceUrl}/file/${relativePathForLink.split(path.sep).join('/')}`;
 
436
  fileInfo.result.direct_download_link_by_id = `${publicSpaceUrl}/file/${botToken}/downloadFile?file_id=${file_id}`;
437
 
438
  if (encryptionKeyBuffer) {
439
+ const expiresAt = calculateExpiryTimestamp(customExpiryHours);
440
+ const encryptedPayloadById = encryptPayload({ type: 'id', bot_token: botToken, file_id: file_id, expiresAt });
441
+ if (encryptedPayloadById) {
442
+ fileInfo.result.encrypted_download_link_by_id = `${publicSpaceUrl}/file/downloadEncrypted?payload=${encryptedPayloadById}`;
443
+ }
444
+ if (!relativePathForLink.startsWith('..') && relativePathForLink) {
445
+ const encryptedPayloadByPath = encryptPayload({ type: 'path', file_path_relative_to_data_dir: relativePathForLink, expiresAt });
446
+ if (encryptedPayloadByPath) {
447
+ fileInfo.result.encrypted_download_link_by_path = `${publicSpaceUrl}/file/downloadEncrypted?payload=${encryptedPayloadByPath}`;
448
+ }
449
  }
 
 
450
  }
451
  }
452
  res.json(fileInfo);
 
516
 
517
  app.use('/file', fileRouter);
518
 
519
+ const getBotTokenFromApiFilePath = (apiFilePath) => {
520
+ if (!apiFilePath || typeof apiFilePath !== 'string') return null;
521
+ const dataDirPrefix = TELEGRAM_DATA_DIR.endsWith('/') ? TELEGRAM_DATA_DIR : TELEGRAM_DATA_DIR + '/';
522
+ if (apiFilePath.startsWith(dataDirPrefix)) {
523
+ const pathPartAfterDataDir = apiFilePath.substring(dataDirPrefix.length);
524
+ const token = pathPartAfterDataDir.split('/')[0];
525
+ if (token && token.includes(':')) return token;
526
+ }
527
+ const match = apiFilePath.match(/\/([0-9]+:[a-zA-Z0-9_-]+)\//);
528
+ return match ? match[1] : null;
529
+ };
530
+
531
+ const augmentFileObject = (fileObj, req) => {
532
+ if (!fileObj || typeof fileObj !== 'object') return;
533
+ if (fileObj.file_id && fileObj.file_path) {
534
+ const spaceHost = process.env.SPACE_HOST;
535
+ const publicSpaceUrl = spaceHost ? `https://${spaceHost}` : `${req.protocol}://${req.get('host')}`;
536
+
537
+ const apiFilePath = fileObj.file_path;
538
+ const fileId = fileObj.file_id;
539
+ const botToken = getBotTokenFromApiFilePath(apiFilePath) || req.proxiedBotToken;
540
+
541
+ if (!botToken) {
542
+ console.warn(`Could not determine botToken for file_path: ${apiFilePath}`);
543
+ return;
544
+ }
545
+
546
+ let relativePathForLink = "";
547
+ const resolvedNormalizedDataDir = path.resolve(TELEGRAM_DATA_DIR);
548
+ const resolvedApiFilePath = path.resolve(apiFilePath);
549
+
550
+ if (resolvedApiFilePath.startsWith(resolvedNormalizedDataDir)) {
551
+ relativePathForLink = path.relative(resolvedNormalizedDataDir, resolvedApiFilePath);
552
+ } else {
553
+ relativePathForLink = apiFilePath;
554
+ }
555
+
556
+ const customExpiryHours = req.query.link_expiry_hours ? parseInt(req.query.link_expiry_hours, 10) : DEFAULT_LINK_EXPIRY_HOURS;
557
+ const expiresAt = calculateExpiryTimestamp(customExpiryHours);
558
+
559
+ if (!relativePathForLink.startsWith('..') && relativePathForLink) {
560
+ fileObj.direct_download_link_by_path = `${publicSpaceUrl}/file/${relativePathForLink.split(path.sep).join('/')}`;
561
+ fileObj.delete_link_by_path = `${publicSpaceUrl}/file/deleteFile?file=${encodeURIComponent(relativePathForLink.split(path.sep).join('/'))}`;
562
+ if (encryptionKeyBuffer) {
563
+ const encryptedPayloadByPath = encryptPayload({ type: 'path', file_path_relative_to_data_dir: relativePathForLink, expiresAt });
564
+ if (encryptedPayloadByPath) {
565
+ fileObj.encrypted_download_link_by_path = `${publicSpaceUrl}/file/downloadEncrypted?payload=${encryptedPayloadByPath}`;
566
+ }
567
+ }
568
+ }
569
+
570
+ fileObj.direct_download_link_by_id = `${publicSpaceUrl}/file/${botToken}/downloadFile?file_id=${fileId}`;
571
+ fileObj.delete_link_by_id = `${publicSpaceUrl}/file/${botToken}/deleteById?file_id=${fileId}`;
572
+ if (encryptionKeyBuffer) {
573
+ const encryptedPayloadById = encryptPayload({ type: 'id', bot_token: botToken, file_id: fileId, expiresAt });
574
+ if (encryptedPayloadById) {
575
+ fileObj.encrypted_download_link_by_id = `${publicSpaceUrl}/file/downloadEncrypted?payload=${encryptedPayloadById}`;
576
+ }
577
+ }
578
+ }
579
+ };
580
+
581
+ const recursivelyAugmentFileObjects = (data, req) => {
582
+ if (Array.isArray(data)) {
583
+ data.forEach(item => recursivelyAugmentFileObjects(item, req));
584
+ } else if (data && typeof data === 'object') {
585
+ augmentFileObject(data, req);
586
+ if (data.result && typeof data.result === 'object') {
587
+ recursivelyAugmentFileObjects(data.result, req);
588
+ } else {
589
+ for (const key in data) {
590
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
591
+ recursivelyAugmentFileObjects(data[key], req);
592
+ }
593
+ }
594
+ }
595
+ }
596
+ };
597
+
598
+
599
  app.use(PROXY_TELEGRAM_PATH_PREFIX, createProxyMiddleware({
600
  target: `http://localhost:${INTERNAL_TELEGRAM_API_PORT}`,
601
  changeOrigin: true,
602
+ selfHandleResponse: true,
603
  pathRewrite: (pathValue, req) => {
604
+ const tokenMatch = pathValue.match(/^\/bot([0-9]+:[a-zA-Z0-9_-]+)\//);
605
+ if (tokenMatch && tokenMatch[1]) {
606
+ req.proxiedBotToken = tokenMatch[1];
607
+ }
608
  return pathValue.startsWith(PROXY_TELEGRAM_PATH_PREFIX) ? pathValue.substring(PROXY_TELEGRAM_PATH_PREFIX.length) : pathValue;
609
  },
610
  onProxyReq: (proxyReq, req, res) => {
611
  console.log(`[Proxy] Request to Telegram API: ${req.method} ${proxyReq.path}`);
612
  },
613
+ onProxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => {
614
+ const contentType = proxyRes.headers['content-type'];
615
+ const contentEncoding = proxyRes.headers['content-encoding'];
616
+
617
+ if (contentType && contentType.includes('application/json')) {
618
+ let responseData;
619
+ try {
620
+ if (contentEncoding === 'gzip') {
621
+ responseData = zlib.gunzipSync(responseBuffer).toString('utf8');
622
+ } else if (contentEncoding === 'deflate') {
623
+ responseData = zlib.inflateSync(responseBuffer).toString('utf8');
624
+ } else {
625
+ responseData = responseBuffer.toString('utf8');
626
+ }
627
+
628
+ const jsonData = JSON.parse(responseData);
629
+ recursivelyAugmentFileObjects(jsonData, req);
630
+
631
+ let finalResponseBuffer = Buffer.from(JSON.stringify(jsonData));
632
+ if (contentEncoding === 'gzip') {
633
+ finalResponseBuffer = zlib.gzipSync(finalResponseBuffer);
634
+ } else if (contentEncoding === 'deflate') {
635
+ finalResponseBuffer = zlib.deflateSync(finalResponseBuffer);
636
+ }
637
+ return finalResponseBuffer;
638
+
639
+ } catch (err) {
640
+ console.error('Error processing proxied JSON response:', err);
641
+ return responseBuffer;
642
+ }
643
+ }
644
+ return responseBuffer;
645
+ }),
646
  onError: (err, req, res) => {
647
  console.error('[Proxy] Error:', err);
648
  if (!res.headersSent) {
 
668
  file_operations: {
669
  _base: `${baseUrl}/file`,
670
  list_files_for_bot: `${baseUrl}/file/list?token=${botTokenPlaceholder}`,
671
+ get_file_info_and_links: `${baseUrl}/file/${botTokenPlaceholder}/getFile?file_id=${fileIdPlaceholder}&link_expiry_hours=<HOURS>`,
672
  download_file_by_id: `${baseUrl}/file/${botTokenPlaceholder}/downloadFile?file_id=${fileIdPlaceholder}`,
673
  download_file_by_path: `${baseUrl}/file/${filePathPlaceholder}`,
674
  delete_file_by_path: `${baseUrl}/file/deleteFile?file=${filePathPlaceholder}`,
675
  delete_file_by_id: `${baseUrl}/file/${botTokenPlaceholder}/deleteById?file_id=${fileIdPlaceholder}`,
676
+ download_via_encrypted_payload: `${baseUrl}/file/downloadEncrypted?payload=<ENCRYPTED_PAYLOAD_FROM_GETFILE_ENDPOINT_OR_PROXY>`,
677
  },
678
  gist_info_if_configured: ENV_GIST_ID ? `https://gist.github.com/${GITHUB_USERNAME || "_"}/${ENV_GIST_ID}` : "Gist update not configured",
679
  },
680
  notes: {
681
  file_paths_for_delete_or_direct_download: "Should be like 'botTOKEN/photos/file_XYZ.jpg'",
682
+ link_encryption: encryptionKeyBuffer ? "Enabled" : "Disabled (LINK_ENCRYPTION_KEY not set in secrets)",
683
+ default_link_expiry_hours: DEFAULT_LINK_EXPIRY_HOURS + " (use -1 for indefinite, override with ?link_expiry_hours=N in proxied requests or /getFile)"
684
  }
685
  });
686
  });
 
691
  console.log(`Statistics available at /stats`);
692
  console.log(`File operations available under /file/`);
693
  console.log(`Telegram API proxied from ${PROXY_TELEGRAM_PATH_PREFIX}/`);
694
+ console.log(`Default link expiry: ${DEFAULT_LINK_EXPIRY_HOURS} hours (-1 for indefinite)`);
695
 
696
  updateEnvGistInGithub().catch(console.error);
697