| | 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 }); |
| | } |
| | } |
| |
|