| |
|
| | import fs from 'fs/promises'; |
| | import path from 'path'; |
| | import { Redis } from '@upstash/redis'; |
| |
|
| | const DB_PATH = path.join(process.cwd(), 'db.json'); |
| |
|
| | const redis = (process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN) |
| | ? new Redis({ |
| | url: process.env.UPSTASH_REDIS_REST_URL, |
| | token: process.env.UPSTASH_REDIS_REST_TOKEN, |
| | }) |
| | : null; |
| |
|
| | const useCloud = () => !!redis; |
| |
|
| | export interface ImageRecord { |
| | id: string; |
| | telegram_file_id: string; |
| | created_at: number; |
| | views: number; |
| | metadata: { |
| | size: number; |
| | type: string; |
| | }; |
| | } |
| |
|
| | async function ensureLocalDb() { |
| | try { |
| | await fs.access(DB_PATH); |
| | } catch { |
| | await fs.writeFile(DB_PATH, JSON.stringify({ images: [] })); |
| | } |
| | } |
| |
|
| | export async function saveImage(record: any, source: 'web' | 'bot' = 'web', userId?: string | number) { |
| | if (useCloud() && redis) { |
| | |
| | const pipeline = redis.pipeline(); |
| | pipeline.hset(`snap:${record.id}`, { |
| | ...record, |
| | views: 0, |
| | metadata: JSON.stringify(record.metadata) |
| | }); |
| |
|
| | |
| | pipeline.incr('stats:total_uploads'); |
| |
|
| | |
| | |
| | if (source === 'web') { |
| | pipeline.incr('stats:web_uploads'); |
| | } else if (source === 'bot') { |
| | pipeline.incr('stats:bot_uploads'); |
| | if (userId) { |
| | |
| | pipeline.sadd('stats:users', userId); |
| | } |
| | } |
| |
|
| | |
| | const type = record.metadata.type || ''; |
| | if (type.startsWith('video/') || type === 'image/gif') { |
| | pipeline.incr('stats:videos'); |
| | } else { |
| | pipeline.incr('stats:images'); |
| | } |
| |
|
| | await pipeline.exec(); |
| | return; |
| | } |
| |
|
| | await ensureLocalDb(); |
| | const content = await fs.readFile(DB_PATH, 'utf-8'); |
| | const db = JSON.parse(content); |
| | db.images.push({ ...record, views: 0 }); |
| | |
| | await fs.writeFile(DB_PATH, JSON.stringify(db, null, 2)); |
| | } |
| |
|
| | export async function getStats() { |
| | if (useCloud() && redis) { |
| | const start = Date.now(); |
| | const [totalUploads, totalUsers, webUploads, botUploads, totalImages, totalVideos] = await Promise.all([ |
| | redis.get('stats:total_uploads'), |
| | redis.scard('stats:users'), |
| | redis.get('stats:web_uploads'), |
| | redis.get('stats:bot_uploads'), |
| | redis.get('stats:images'), |
| | redis.get('stats:videos') |
| | ]); |
| | const ping = Date.now() - start; |
| |
|
| | return { |
| | totalUploads: parseInt(totalUploads as string || '0'), |
| | totalUsers: totalUsers || 0, |
| | webUploads: parseInt(webUploads as string || '0'), |
| | botUploads: parseInt(botUploads as string || '0'), |
| | totalImages: parseInt(totalImages as string || '0'), |
| | totalVideos: parseInt(totalVideos as string || '0'), |
| | ping |
| | }; |
| | } |
| | return { |
| | totalUploads: 0, |
| | totalUsers: 0, |
| | webUploads: 0, |
| | botUploads: 0, |
| | totalImages: 0, |
| | totalVideos: 0, |
| | ping: 0 |
| | }; |
| | } |
| |
|
| | export async function getImage(id: string): Promise<ImageRecord | null> { |
| | if (useCloud() && redis) { |
| | |
| | await redis.hincrby(`snap:${id}`, 'views', 1); |
| | const data: any = await redis.hgetall(`snap:${id}`); |
| |
|
| | if (!data || Object.keys(data).length === 0) return null; |
| |
|
| | return { |
| | ...data, |
| | id, |
| | views: parseInt(data.views || '0'), |
| | created_at: parseInt(data.created_at), |
| | metadata: typeof data.metadata === 'string' ? JSON.parse(data.metadata) : data.metadata |
| | } as ImageRecord; |
| | } |
| |
|
| | try { |
| | await ensureLocalDb(); |
| | const content = await fs.readFile(DB_PATH, 'utf-8'); |
| | const db = JSON.parse(content); |
| | const index = db.images.findIndex((img: any) => img.id === id); |
| | if (index !== -1) { |
| | db.images[index].views = (db.images[index].views || 0) + 1; |
| | await fs.writeFile(DB_PATH, JSON.stringify(db, null, 2)); |
| | return db.images[index]; |
| | } |
| | return null; |
| | } catch { |
| | return null; |
| | } |
| | } |
| |
|
| | export async function rateLimit(key: string, limit: number, windowSeconds: number) { |
| | if (!redis) return { success: true, count: 0 }; |
| |
|
| | const fullKey = `ratelimit:${key}`; |
| | const count = await redis.incr(fullKey); |
| |
|
| | if (count === 1) { |
| | await redis.expire(fullKey, windowSeconds); |
| | } |
| |
|
| | return { |
| | success: count <= limit, |
| | limit, |
| | remaining: Math.max(0, limit - count), |
| | count |
| | }; |
| | } |
| |
|
| | export function generateId() { |
| | return Math.random().toString(36).substring(2, 10); |
| | } |
| |
|
| | export async function registerUser(userId: string | number) { |
| | if (useCloud() && redis) { |
| | await redis.sadd('stats:users', userId); |
| | } |
| | } |
| |
|
| | |
| |
|
| | export interface User { |
| | id: string; |
| | email: string; |
| | password_hash: string; |
| | created_at: number; |
| | last_login?: number; |
| | } |
| |
|
| | export interface ApiKey { |
| | id: string; |
| | user_id: string; |
| | key_hash: string; |
| | name: string; |
| | prefix: string; |
| | rate_limit: number; |
| | created_at: number; |
| | last_used?: number; |
| | is_active: boolean; |
| | } |
| |
|
| | export interface Webhook { |
| | id: string; |
| | user_id: string; |
| | url: string; |
| | events: string[]; |
| | secret?: string; |
| | is_active: boolean; |
| | created_at: number; |
| | } |
| |
|
| | |
| | export async function createUser(email: string, passwordHash: string): Promise<User> { |
| | const userId = `user_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; |
| | const user: User = { |
| | id: userId, |
| | email: email.toLowerCase(), |
| | password_hash: passwordHash, |
| | created_at: Date.now() |
| | }; |
| |
|
| | if (useCloud() && redis) { |
| | await redis.hset(`user:${userId}`, { |
| | id: user.id, |
| | email: user.email, |
| | password_hash: user.password_hash, |
| | created_at: user.created_at.toString() |
| | }); |
| | await redis.set(`user:email:${user.email}`, userId); |
| | } else { |
| | await ensureLocalDb(); |
| | const content = await fs.readFile(DB_PATH, 'utf-8'); |
| | const db = JSON.parse(content); |
| | if (!db.users) db.users = []; |
| | db.users.push(user); |
| | await fs.writeFile(DB_PATH, JSON.stringify(db, null, 2)); |
| | } |
| |
|
| | return user; |
| | } |
| |
|
| | export async function getUserByEmail(email: string): Promise<User | null> { |
| | if (useCloud() && redis) { |
| | const userId = await redis.get(`user:email:${email.toLowerCase()}`); |
| | if (!userId) return null; |
| | const data: any = await redis.hgetall(`user:${userId}`); |
| | if (!data || Object.keys(data).length === 0) return null; |
| | return { |
| | ...data, |
| | created_at: parseInt(data.created_at), |
| | last_login: data.last_login ? parseInt(data.last_login) : undefined |
| | } as User; |
| | } |
| |
|
| | try { |
| | await ensureLocalDb(); |
| | const content = await fs.readFile(DB_PATH, 'utf-8'); |
| | const db = JSON.parse(content); |
| | if (!db.users) return null; |
| | return db.users.find((u: User) => u.email.toLowerCase() === email.toLowerCase()) || null; |
| | } catch { |
| | return null; |
| | } |
| | } |
| |
|
| | export async function getUserById(userId: string): Promise<User | null> { |
| | if (useCloud() && redis) { |
| | const data: any = await redis.hgetall(`user:${userId}`); |
| | if (!data || Object.keys(data).length === 0) return null; |
| | return { |
| | ...data, |
| | created_at: parseInt(data.created_at), |
| | last_login: data.last_login ? parseInt(data.last_login) : undefined |
| | } as User; |
| | } |
| |
|
| | try { |
| | await ensureLocalDb(); |
| | const content = await fs.readFile(DB_PATH, 'utf-8'); |
| | const db = JSON.parse(content); |
| | if (!db.users) return null; |
| | return db.users.find((u: User) => u.id === userId) || null; |
| | } catch { |
| | return null; |
| | } |
| | } |
| |
|
| | export async function updateUserLastLogin(userId: string) { |
| | if (useCloud() && redis) { |
| | await redis.hset(`user:${userId}`, { last_login: Date.now().toString() }); |
| | } else { |
| | await ensureLocalDb(); |
| | const content = await fs.readFile(DB_PATH, 'utf-8'); |
| | const db = JSON.parse(content); |
| | if (db.users) { |
| | const user = db.users.find((u: User) => u.id === userId); |
| | if (user) { |
| | user.last_login = Date.now(); |
| | await fs.writeFile(DB_PATH, JSON.stringify(db, null, 2)); |
| | } |
| | } |
| | } |
| | } |
| |
|
| | |
| | export async function createApiKey(userId: string, name: string, keyHash: string, keyPrefix: string, rateLimit: number = 100): Promise<ApiKey> { |
| | const apiKeyId = `key_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; |
| | const apiKey: ApiKey = { |
| | id: apiKeyId, |
| | user_id: userId, |
| | key_hash: keyHash, |
| | name, |
| | prefix: keyPrefix, |
| | rate_limit: rateLimit, |
| | created_at: Date.now(), |
| | is_active: true |
| | }; |
| |
|
| | if (useCloud() && redis) { |
| | await redis.hset(`apikey:${apiKeyId}`, { |
| | id: apiKey.id, |
| | user_id: apiKey.user_id, |
| | key_hash: apiKey.key_hash, |
| | name: apiKey.name, |
| | prefix: apiKey.prefix, |
| | rate_limit: apiKey.rate_limit.toString(), |
| | created_at: apiKey.created_at.toString(), |
| | is_active: apiKey.is_active.toString() |
| | }); |
| | await redis.sadd(`user:${userId}:keys`, apiKeyId); |
| | } else { |
| | await ensureLocalDb(); |
| | const content = await fs.readFile(DB_PATH, 'utf-8'); |
| | const db = JSON.parse(content); |
| | if (!db.apiKeys) db.apiKeys = []; |
| | db.apiKeys.push(apiKey); |
| | await fs.writeFile(DB_PATH, JSON.stringify(db, null, 2)); |
| | } |
| |
|
| | return apiKey; |
| | } |
| |
|
| | export async function getApiKeyByHash(keyHash: string): Promise<ApiKey | null> { |
| | if (useCloud() && redis) { |
| | |
| | |
| | const apiKeyId = await redis.get(`apikey:hash:${keyHash}`); |
| | if (!apiKeyId) return null; |
| | const data: any = await redis.hgetall(`apikey:${apiKeyId}`); |
| | if (!data || Object.keys(data).length === 0) return null; |
| | return { |
| | ...data, |
| | rate_limit: parseInt(data.rate_limit), |
| | created_at: parseInt(data.created_at), |
| | last_used: data.last_used ? parseInt(data.last_used) : undefined, |
| | is_active: data.is_active === 'true' || data.is_active === true |
| | } as ApiKey; |
| | } |
| |
|
| | try { |
| | await ensureLocalDb(); |
| | const content = await fs.readFile(DB_PATH, 'utf-8'); |
| | const db = JSON.parse(content); |
| | if (!db.apiKeys) return null; |
| | return db.apiKeys.find((k: ApiKey) => k.key_hash === keyHash && k.is_active) || null; |
| | } catch { |
| | return null; |
| | } |
| | } |
| |
|
| | export async function getApiKeyById(apiKeyId: string): Promise<ApiKey | null> { |
| | if (useCloud() && redis) { |
| | const data: any = await redis.hgetall(`apikey:${apiKeyId}`); |
| | if (!data || Object.keys(data).length === 0) return null; |
| | return { |
| | ...data, |
| | rate_limit: parseInt(data.rate_limit), |
| | created_at: parseInt(data.created_at), |
| | last_used: data.last_used ? parseInt(data.last_used) : undefined, |
| | is_active: data.is_active === 'true' || data.is_active === true |
| | } as ApiKey; |
| | } |
| |
|
| | try { |
| | await ensureLocalDb(); |
| | const content = await fs.readFile(DB_PATH, 'utf-8'); |
| | const db = JSON.parse(content); |
| | if (!db.apiKeys) return null; |
| | return db.apiKeys.find((k: ApiKey) => k.id === apiKeyId) || null; |
| | } catch { |
| | return null; |
| | } |
| | } |
| |
|
| | export async function getUserApiKeys(userId: string): Promise<ApiKey[]> { |
| | if (useCloud() && redis) { |
| | const keyIds = await redis.smembers(`user:${userId}:keys`); |
| | if (!keyIds || keyIds.length === 0) return []; |
| | const keys = await Promise.all(keyIds.map(id => getApiKeyById(id as string))); |
| | return keys.filter(k => k !== null) as ApiKey[]; |
| | } |
| |
|
| | try { |
| | await ensureLocalDb(); |
| | const content = await fs.readFile(DB_PATH, 'utf-8'); |
| | const db = JSON.parse(content); |
| | if (!db.apiKeys) return []; |
| | return db.apiKeys.filter((k: ApiKey) => k.user_id === userId); |
| | } catch { |
| | return []; |
| | } |
| | } |
| |
|
| | export async function updateApiKeyLastUsed(apiKeyId: string) { |
| | if (useCloud() && redis) { |
| | await redis.hset(`apikey:${apiKeyId}`, { last_used: Date.now().toString() }); |
| | } else { |
| | await ensureLocalDb(); |
| | const content = await fs.readFile(DB_PATH, 'utf-8'); |
| | const db = JSON.parse(content); |
| | if (db.apiKeys) { |
| | const key = db.apiKeys.find((k: ApiKey) => k.id === apiKeyId); |
| | if (key) { |
| | key.last_used = Date.now(); |
| | await fs.writeFile(DB_PATH, JSON.stringify(db, null, 2)); |
| | } |
| | } |
| | } |
| | } |
| |
|
| | export async function revokeApiKey(apiKeyId: string) { |
| | if (useCloud() && redis) { |
| | await redis.hset(`apikey:${apiKeyId}`, { is_active: 'false' }); |
| | } else { |
| | await ensureLocalDb(); |
| | const content = await fs.readFile(DB_PATH, 'utf-8'); |
| | const db = JSON.parse(content); |
| | if (db.apiKeys) { |
| | const key = db.apiKeys.find((k: ApiKey) => k.id === apiKeyId); |
| | if (key) { |
| | key.is_active = false; |
| | await fs.writeFile(DB_PATH, JSON.stringify(db, null, 2)); |
| | } |
| | } |
| | } |
| | } |
| |
|
| | export async function deleteApiKey(apiKeyId: string, userId: string) { |
| | if (useCloud() && redis) { |
| | await redis.del(`apikey:${apiKeyId}`); |
| | await redis.srem(`user:${userId}:keys`, apiKeyId); |
| | } else { |
| | await ensureLocalDb(); |
| | const content = await fs.readFile(DB_PATH, 'utf-8'); |
| | const db = JSON.parse(content); |
| | if (db.apiKeys) { |
| | db.apiKeys = db.apiKeys.filter((k: ApiKey) => k.id !== apiKeyId); |
| | await fs.writeFile(DB_PATH, JSON.stringify(db, null, 2)); |
| | } |
| | } |
| | } |
| |
|
| | |
| | export async function storeApiKeyHashMapping(keyHash: string, apiKeyId: string) { |
| | if (useCloud() && redis) { |
| | await redis.set(`apikey:hash:${keyHash}`, apiKeyId); |
| | } |
| | } |
| |
|
| | |
| | export async function createWebhook(userId: string, url: string, events: string[], secret?: string): Promise<Webhook> { |
| | const webhookId = `wh_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; |
| | const webhook: Webhook = { |
| | id: webhookId, |
| | user_id: userId, |
| | url, |
| | events, |
| | secret, |
| | is_active: true, |
| | created_at: Date.now() |
| | }; |
| |
|
| | if (useCloud() && redis) { |
| | await redis.hset(`webhook:${webhookId}`, { |
| | id: webhook.id, |
| | user_id: webhook.user_id, |
| | url: webhook.url, |
| | events: JSON.stringify(events), |
| | secret: webhook.secret || '', |
| | is_active: webhook.is_active.toString(), |
| | created_at: webhook.created_at.toString() |
| | }); |
| | await redis.sadd(`user:${userId}:webhooks`, webhookId); |
| | } else { |
| | await ensureLocalDb(); |
| | const content = await fs.readFile(DB_PATH, 'utf-8'); |
| | const db = JSON.parse(content); |
| | if (!db.webhooks) db.webhooks = []; |
| | db.webhooks.push(webhook); |
| | await fs.writeFile(DB_PATH, JSON.stringify(db, null, 2)); |
| | } |
| |
|
| | return webhook; |
| | } |
| |
|
| | export async function getUserWebhooks(userId: string): Promise<Webhook[]> { |
| | if (useCloud() && redis) { |
| | const webhookIds = await redis.smembers(`user:${userId}:webhooks`); |
| | if (!webhookIds || webhookIds.length === 0) return []; |
| | const webhooks = await Promise.all(webhookIds.map(async (id) => { |
| | const data: any = await redis.hgetall(`webhook:${id}`); |
| | if (!data || Object.keys(data).length === 0) return null; |
| | return { |
| | ...data, |
| | events: typeof data.events === 'string' ? JSON.parse(data.events) : data.events, |
| | created_at: parseInt(data.created_at), |
| | is_active: data.is_active === 'true' || data.is_active === true |
| | } as Webhook; |
| | })); |
| | return webhooks.filter(w => w !== null) as Webhook[]; |
| | } |
| |
|
| | try { |
| | await ensureLocalDb(); |
| | const content = await fs.readFile(DB_PATH, 'utf-8'); |
| | const db = JSON.parse(content); |
| | if (!db.webhooks) return []; |
| | return db.webhooks.filter((w: Webhook) => w.user_id === userId); |
| | } catch { |
| | return []; |
| | } |
| | } |
| |
|
| | export async function triggerWebhook(webhook: Webhook, event: string, data: any) { |
| | if (!webhook.is_active || !webhook.events.includes(event)) return; |
| |
|
| | try { |
| | const payload = { |
| | event, |
| | timestamp: Date.now(), |
| | data |
| | }; |
| |
|
| | const headers: Record<string, string> = { |
| | 'Content-Type': 'application/json', |
| | 'User-Agent': 'VoltEdge-Webhook/1.0' |
| | }; |
| |
|
| | if (webhook.secret) { |
| | |
| | headers['X-VoltEdge-Signature'] = webhook.secret; |
| | } |
| |
|
| | await fetch(webhook.url, { |
| | method: 'POST', |
| | headers, |
| | body: JSON.stringify(payload) |
| | }); |
| | } catch (error) { |
| | console.error('Webhook delivery failed:', error); |
| | } |
| | } |