From 8272f3625398b47ee05605f32a118db9abb80eec Mon Sep 17 00:00:00 2001 From: NW Date: Wed, 24 Jun 2026 20:13:35 +0100 Subject: [PATCH] fix: send photos from disk instead of URL - no ADMIN_URL needed sendPhoto now sends local files from /app/uploads/ instead of requiring a publicly accessible URL. This fixes the issue where onion addresses and private IPs are unreachable by Telegram API servers. - resolvePhotoSource(): http URLs pass through, relative paths resolved to local file path in uploads dir - sendProductPhoto(): sends file directly, falls back to corrupt-photo.jpg - Removed all ADMIN_URL prefix logic for photo URLs - Works without any public IP or domain --- .../adminHandlers/product/viewHandler.js | 44 ++++++++++++------ .../userHandlers/userProductHandler.js | 45 ++++++++++++------- .../userHandlers/userPurchaseHandler.js | 40 +++++++++++++---- 3 files changed, 91 insertions(+), 38 deletions(-) diff --git a/src/handlers/adminHandlers/product/viewHandler.js b/src/handlers/adminHandlers/product/viewHandler.js index 01874c3..3d49303 100644 --- a/src/handlers/adminHandlers/product/viewHandler.js +++ b/src/handlers/adminHandlers/product/viewHandler.js @@ -4,6 +4,32 @@ import userStates from '../../../context/userStates.js'; import LocationService from '../../../services/locationService.js'; import ProductService from '../../../services/productService.js'; import logger from '../../../utils/logger.js'; +import fs from 'fs'; +import path from 'path'; + +const UPLOADS_DIR = path.join(process.cwd(), 'uploads'); +const FALLBACK_PHOTO = path.join(process.cwd(), 'corrupt-photo.jpg'); + +function resolvePhotoSource(photoUrl) { + if (!photoUrl) return null; + if (photoUrl.startsWith('http')) return photoUrl; + const filePath = path.join(UPLOADS_DIR, photoUrl.replace(/^\/uploads\//, '')); + if (fs.existsSync(filePath)) return filePath; + return null; +} + +async function sendProductPhoto(chatId, photoUrl, caption) { + const source = resolvePhotoSource(photoUrl); + if (!source) return null; + try { + return await bot.sendPhoto(chatId, source, { caption }); + } catch (e) { + if (fs.existsSync(FALLBACK_PHOTO)) { + return await bot.sendPhoto(chatId, FALLBACK_PHOTO, { caption }); + } + return null; + } +} export default class ViewHandler { @@ -62,24 +88,14 @@ export default class ViewHandler { let hiddenPhotoMessage; if (product.photo_url) { - const photoUrl = product.photo_url.startsWith('http') ? product.photo_url : `${process.env.ADMIN_URL}${product.photo_url}`; - try { - photoMessage = await bot.sendPhoto(chatId, photoUrl, {caption: 'Public photo'}); - } catch (e) { - photoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Public photo'}) - } + photoMessage = await sendProductPhoto(chatId, product.photo_url, 'Public photo'); } if (product.hidden_photo_url) { - const hiddenPhotoUrl = product.hidden_photo_url.startsWith('http') ? product.hidden_photo_url : `${process.env.ADMIN_URL}${product.hidden_photo_url}`; - try { - hiddenPhotoMessage = await bot.sendPhoto(chatId, hiddenPhotoUrl, {caption: 'Hidden photo'}); - } catch (e) { - hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Hidden photo'}) - } + hiddenPhotoMessage = await sendProductPhoto(chatId, product.hidden_photo_url, 'Hidden photo'); } await userStates.set(chatId, { - msgToDelete: [photoMessage.message_id, hiddenPhotoMessage.message_id] + msgToDelete: [photoMessage?.message_id, hiddenPhotoMessage?.message_id].filter(Boolean) }) await bot.deleteMessage(chatId, messageId); @@ -89,4 +105,4 @@ export default class ViewHandler { await bot.sendMessage(chatId, 'Error loading product details. Please try again.'); } } -} +} \ No newline at end of file diff --git a/src/handlers/userHandlers/userProductHandler.js b/src/handlers/userHandlers/userProductHandler.js index b1ff606..2a1184f 100644 --- a/src/handlers/userHandlers/userProductHandler.js +++ b/src/handlers/userHandlers/userProductHandler.js @@ -6,8 +6,35 @@ import userStates from "../../context/userStates.js"; import ProductService from "../../services/productService.js"; import CategoryService from "../../services/categoryService.js"; import UserService from "../../services/userService.js"; -import PurchaseService from "../../services/purchaseService.js"; +import PurchaseService from '../../services/purchaseService.js'; import Validators from '../../utils/validators.js'; +import fs from 'fs'; +import path from 'path'; + +const UPLOADS_DIR = path.join(process.cwd(), 'uploads'); +const FALLBACK_PHOTO = path.join(process.cwd(), 'corrupt-photo.jpg'); + +function resolvePhotoSource(photoUrl) { + if (!photoUrl) return null; + if (photoUrl.startsWith('http')) return photoUrl; + const filePath = path.join(UPLOADS_DIR, photoUrl.replace(/^\/uploads\//, '')); + if (fs.existsSync(filePath)) return filePath; + return null; +} + +async function sendProductPhoto(chatId, photoUrl, caption) { + const source = resolvePhotoSource(photoUrl); + if (!source) return null; + try { + return await bot.sendPhoto(chatId, source, { caption }); + } catch (e) { + logger.warn({ err: e, photoUrl }, 'Failed to send product photo'); + if (fs.existsSync(FALLBACK_PHOTO)) { + return await bot.sendPhoto(chatId, FALLBACK_PHOTO, { caption }); + } + return null; + } +} import logger from '../../utils/logger.js'; export default class UserProductHandler { @@ -356,13 +383,7 @@ export default class UserProductHandler { // Отправляем фото, если оно существует let photoMessage; if (product.photo_url) { - const photoUrl = product.photo_url.startsWith('http') ? product.photo_url : `${process.env.ADMIN_URL}${product.photo_url}`; - try { - photoMessage = await bot.sendPhoto(chatId, photoUrl, { caption: 'Public photo' }); - } catch (e) { - logger.warn({ err: e }, 'Failed to send product photo'); - photoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Public photo' }); - } + photoMessage = await sendProductPhoto(chatId, product.photo_url, 'Public photo'); } const keyboard = { @@ -708,13 +729,7 @@ export default class UserProductHandler { // Отправляем Hidden Photo let hiddenPhotoMessage; if (product.hidden_photo_url) { - const hiddenPhotoUrl = product.hidden_photo_url.startsWith('http') ? product.hidden_photo_url : `${process.env.ADMIN_URL}${product.hidden_photo_url}`; - try { - hiddenPhotoMessage = await bot.sendPhoto(chatId, hiddenPhotoUrl, { caption: 'Hidden photo' }); - } catch (e) { - logger.warn({ err: e }, 'Failed to send hidden photo'); - hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Hidden photo' }); - } + hiddenPhotoMessage = await sendProductPhoto(chatId, product.hidden_photo_url, 'Hidden photo'); } const message = ` diff --git a/src/handlers/userHandlers/userPurchaseHandler.js b/src/handlers/userHandlers/userPurchaseHandler.js index a4e5881..f63a0f5 100644 --- a/src/handlers/userHandlers/userPurchaseHandler.js +++ b/src/handlers/userHandlers/userPurchaseHandler.js @@ -3,15 +3,42 @@ import config from "../../config/config.js"; import db from '../../config/database.js'; +import fs from 'fs'; +import path from 'path'; +import bot from "../../context/bot.js"; +import logger from "../../utils/logger.js"; import PurchaseService from "../../services/purchaseService.js"; +import ProductService from "../../services/productService.js"; import UserService from "../../services/userService.js"; import LocationService from "../../services/locationService.js"; -import ProductService from "../../services/productService.js"; import CategoryService from "../../services/categoryService.js"; -import bot from "../../context/bot.js"; +import WalletService from "../../services/walletService.js"; import userStates from "../../context/userStates.js"; import Validators from '../../utils/validators.js'; -import logger from '../../utils/logger.js'; + +const UPLOADS_DIR = path.join(process.cwd(), 'uploads'); +const FALLBACK_PHOTO = path.join(process.cwd(), 'corrupt-photo.jpg'); + +function resolvePhotoSource(photoUrl) { + if (!photoUrl) return null; + if (photoUrl.startsWith('http')) return photoUrl; + const filePath = path.join(UPLOADS_DIR, photoUrl.replace(/^\/uploads\//, '')); + if (fs.existsSync(filePath)) return filePath; + return null; +} + +async function sendProductPhoto(chatId, photoUrl, caption) { + const source = resolvePhotoSource(photoUrl); + if (!source) return null; + try { + return await bot.sendPhoto(chatId, source, { caption }); + } catch (e) { + if (fs.existsSync(FALLBACK_PHOTO)) { + return await bot.sendPhoto(chatId, FALLBACK_PHOTO, { caption }); + } + return null; + } +} export default class UserPurchaseHandler { static async viewPurchasePage(userId, page) { @@ -180,12 +207,7 @@ export default class UserPurchaseHandler { // Отправляем Hidden Photo let hiddenPhotoMessage; if (product.hidden_photo_url) { - const hiddenPhotoUrl = product.hidden_photo_url.startsWith('http') ? product.hidden_photo_url : `${process.env.ADMIN_URL}${product.hidden_photo_url}`; - try { - hiddenPhotoMessage = await bot.sendPhoto(chatId, hiddenPhotoUrl, { caption: 'Hidden photo' }); - } catch (e) { - hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Hidden photo' }); - } + hiddenPhotoMessage = await sendProductPhoto(chatId, product.hidden_photo_url, 'Hidden photo'); } // Формируем сообщение с деталями покупки