| import { NextRequest, NextResponse } from 'next/server'; |
| import { uploadToTelegram, sendLog } from '@/lib/telegram'; |
| import { uploadToHuggingFace, isHuggingFaceConfigured } from '@/lib/huggingface'; |
| import { saveImage, generateId, rateLimit, getUserWebhooks, triggerWebhook } from '@/lib/db'; |
| import { authenticateRequest } from '@/lib/auth'; |
|
|
| export async function POST(req: NextRequest) { |
| |
| const auth = await authenticateRequest(req); |
| |
| |
| const ip = req.headers.get('x-forwarded-for') || 'anonymous'; |
| let rateLimitKey: string; |
| let rateLimitValue: number; |
| let rateLimitWindow: number; |
|
|
| if (auth?.apiKey) { |
| |
| rateLimitKey = `upload:apikey:${auth.apiKey.id}`; |
| rateLimitValue = auth.apiKey.rate_limit || 100; |
| rateLimitWindow = 60; |
| } else if (auth?.userId) { |
| |
| rateLimitKey = `upload:user:${auth.userId}`; |
| rateLimitValue = 50; |
| rateLimitWindow = 60; |
| } else { |
| |
| rateLimitKey = `upload:${ip}`; |
| rateLimitValue = 20; |
| rateLimitWindow = 60; |
| } |
|
|
| const limit = await rateLimit(rateLimitKey, rateLimitValue, rateLimitWindow); |
|
|
| if (!limit.success) { |
| return NextResponse.json({ |
| success: false, |
| error: { |
| code: 'RATE_LIMIT_EXCEEDED', |
| message: `Too many uploads. Try again in ${limit.remaining === 0 ? 'a minute' : 'a moment'}.` |
| } |
| }, { |
| status: 429, |
| headers: { |
| 'X-RateLimit-Limit': (limit.limit ?? rateLimitValue).toString(), |
| 'X-RateLimit-Remaining': (limit.remaining ?? 0).toString() |
| } |
| }); |
| } |
|
|
| try { |
| const formData = await req.formData(); |
| const file = formData.get('file') as Blob; |
| const customId = formData.get('customId') as string; |
|
|
| if (!file) { |
| return NextResponse.json({ |
| success: false, |
| error: { code: 'MISSING_FILE', message: 'No file provided in request' } |
| }, { status: 400 }); |
| } |
|
|
| |
| const MAX_SIZE = 2 * 1024 * 1024 * 1024; |
| if (file.size > MAX_SIZE) { |
| return NextResponse.json({ |
| success: false, |
| error: { code: 'FILE_TOO_LARGE', message: 'File too large. Max size is 2GB.' } |
| }, { status: 400 }); |
| } |
|
|
| |
| const id = customId ? customId.toLowerCase().replace(/[^a-z0-9-]/g, '-') : generateId(); |
|
|
| |
| |
| const LARGE_FILE_THRESHOLD = 50 * 1024 * 1024; |
| const isLargeFile = file.size > LARGE_FILE_THRESHOLD; |
| const useHFForLargeFiles = isHuggingFaceConfigured() && process.env.USE_HF_FOR_LARGE_FILES === 'true'; |
| const useHFStorage = isHuggingFaceConfigured() && (process.env.USE_HF_STORAGE === 'true' || process.env.SPACE_ID); |
| const isHFEnvironment = !!process.env.SPACE_ID; |
|
|
| |
| let storageResult: { file_id: string; file_url?: string }; |
| let storageType: 'telegram' | 'huggingface' = 'telegram'; |
|
|
| |
| if (useHFStorage) { |
| |
| try { |
| const fileName = customId || `upload-${id}`; |
| const hfResult = await uploadToHuggingFace(file, fileName, id); |
| storageResult = { |
| file_id: hfResult.file_id, |
| file_url: hfResult.file_url |
| }; |
| storageType = 'huggingface'; |
| |
| |
| try { |
| let mediaType: 'photo' | 'animation' | 'video' = 'photo'; |
| if (file.type.startsWith('video/')) mediaType = 'video'; |
| if (file.type === 'image/gif') mediaType = 'animation'; |
| await uploadToTelegram(file, fileName, `π¦ <b>Uploaded via API v2 (HF Hub)</b>\nπ <b>HF URL:</b> ${hfResult.file_url}`, mediaType); |
| } catch (tgError) { |
| console.error('Failed to forward HF upload to Telegram chat (optional):', tgError); |
| |
| } |
| } catch (hfError: any) { |
| console.error('HF upload failed:', hfError); |
| |
| if (isHFEnvironment) { |
| throw new Error(`Hugging Face upload failed: ${hfError.message}. Please check HF_TOKEN and HF_REPO_ID configuration.`); |
| } |
| |
| try { |
| let mediaType: 'photo' | 'animation' | 'video' = 'photo'; |
| if (file.type.startsWith('video/')) mediaType = 'video'; |
| if (file.type === 'image/gif') mediaType = 'animation'; |
| const telegramResult = await uploadToTelegram(file, 'upload', 'π¦ <b>Uploaded via API v2</b>', mediaType); |
| storageResult = { file_id: telegramResult.file_id }; |
| } catch (tgError: any) { |
| throw new Error(`Both HF and Telegram uploads failed. HF: ${hfError.message}, Telegram: ${tgError.message}`); |
| } |
| } |
| } else if (useHFForLargeFiles && isLargeFile) { |
| |
| try { |
| const fileName = customId || `upload-${id}`; |
| const hfResult = await uploadToHuggingFace(file, fileName, id); |
| storageResult = { |
| file_id: hfResult.file_id, |
| file_url: hfResult.file_url |
| }; |
| storageType = 'huggingface'; |
| |
| |
| try { |
| let mediaType: 'photo' | 'animation' | 'video' = 'photo'; |
| if (file.type.startsWith('video/')) mediaType = 'video'; |
| if (file.type === 'image/gif') mediaType = 'animation'; |
| await uploadToTelegram(file, fileName, `π¦ <b>Uploaded via API v2 (HF Hub)</b>\nπ <b>HF URL:</b> ${hfResult.file_url}`, mediaType); |
| } catch (tgError) { |
| console.error('Failed to forward HF upload to Telegram chat (optional):', tgError); |
| |
| } |
| } catch (hfError: any) { |
| console.error('HF upload failed, falling back to Telegram:', hfError); |
| |
| try { |
| let mediaType: 'photo' | 'animation' | 'video' = 'photo'; |
| if (file.type.startsWith('video/')) mediaType = 'video'; |
| if (file.type === 'image/gif') mediaType = 'animation'; |
| const telegramResult = await uploadToTelegram(file, 'upload', 'π¦ <b>Uploaded via API v2</b>', mediaType); |
| storageResult = { file_id: telegramResult.file_id }; |
| } catch (tgError: any) { |
| throw new Error(`Upload failed. HF: ${hfError.message}, Telegram: ${tgError.message}`); |
| } |
| } |
| } else { |
| |
| if (isHFEnvironment && !isHuggingFaceConfigured()) { |
| throw new Error('Telegram API is not accessible in Hugging Face Spaces. Please configure HF_TOKEN and HF_REPO_ID to use Hugging Face storage.'); |
| } |
| |
| try { |
| let mediaType: 'photo' | 'animation' | 'video' = 'photo'; |
| if (file.type.startsWith('video/')) mediaType = 'video'; |
| if (file.type === 'image/gif') mediaType = 'animation'; |
| const telegramResult = await uploadToTelegram(file, 'upload', 'π¦ <b>Uploaded via API v2</b>', mediaType); |
| storageResult = { file_id: telegramResult.file_id }; |
| } catch (tgError: any) { |
| |
| if (isHuggingFaceConfigured()) { |
| console.error('Telegram upload failed, trying HF as fallback:', tgError); |
| try { |
| const fileName = customId || `upload-${id}`; |
| const hfResult = await uploadToHuggingFace(file, fileName, id); |
| storageResult = { |
| file_id: hfResult.file_id, |
| file_url: hfResult.file_url |
| }; |
| storageType = 'huggingface'; |
| } catch (hfError: any) { |
| throw new Error(`Both Telegram and HF uploads failed. Telegram: ${tgError.message}, HF: ${hfError.message}`); |
| } |
| } else { |
| throw new Error(`Telegram upload failed: ${tgError.message}. Consider configuring Hugging Face storage.`); |
| } |
| } |
| } |
|
|
| const record: any = { |
| id, |
| telegram_file_id: storageResult.file_id, |
| storage_type: storageType, |
| storage_url: storageResult.file_url, |
| created_at: Date.now(), |
| metadata: { |
| size: file.size, |
| type: file.type, |
| version: 'v2' |
| } |
| }; |
|
|
| |
| if (auth?.userId) { |
| record.user_id = auth.userId; |
| } |
|
|
| |
| await saveImage(record, 'web', auth?.userId); |
|
|
| const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || |
| (req.headers.get('host') ? `http://${req.headers.get('host')}` : ''); |
|
|
| const publicUrl = `${baseUrl}/i/${id}`; |
|
|
| |
| try { |
| const authInfo = auth?.apiKey ? `API Key: ${auth.apiKey.prefix}...` : auth?.userId ? `User: ${auth.userId}` : 'Anonymous'; |
| await sendLog(`π <b>New API v2 Upload</b>\n\n${authInfo}\nType: ${file.type}\nSize: ${(file.size / 1024 / 1024).toFixed(2)} MB\nLink: ${publicUrl}`); |
| } catch (logError) { |
| console.error('Failed to send log to Telegram (optional):', logError); |
| } |
|
|
| |
| if (auth?.userId) { |
| const webhooks = await getUserWebhooks(auth.userId); |
| for (const webhook of webhooks) { |
| await triggerWebhook(webhook, 'upload', { |
| id, |
| url: publicUrl, |
| size: file.size, |
| type: file.type, |
| created_at: record.created_at |
| }); |
| } |
| } |
|
|
| return NextResponse.json({ |
| success: true, |
| data: { |
| id, |
| url: `${baseUrl}/i/${id}`, |
| direct_url: `${baseUrl}/i/${id}.jpg`, |
| timestamp: record.created_at, |
| authenticated: !!auth |
| } |
| }, { |
| headers: { |
| 'X-RateLimit-Limit': (limit.limit ?? rateLimitValue).toString(), |
| 'X-RateLimit-Remaining': (limit.remaining ?? 0).toString() |
| } |
| }); |
|
|
| } catch (error: any) { |
| console.error('Upload API Error:', error); |
| |
| try { |
| await sendLog(`β <b>API v2 Upload Error</b>\n\nError: ${error.message || error}`); |
| } catch (logError) { |
| console.error('Failed to send error log to Telegram (optional):', logError); |
| } |
| return NextResponse.json({ |
| success: false, |
| error: { code: 'INTERNAL_ERROR', message: error.message || 'Server processed request failed' } |
| }, { status: 500 }); |
| } |
| } |
|
|