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
This commit is contained in:
NW
2026-06-24 20:13:35 +01:00
parent 94300c7d35
commit 8272f36253
3 changed files with 91 additions and 38 deletions

View File

@@ -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.');
}
}
}
}

View File

@@ -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 = `

View File

@@ -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');
}
// Формируем сообщение с деталями покупки