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