Download server.js from 000x/MusicGen: direct link, hf CLI and curl.
- Browser
- Download file 13.4 kB
-
https://huggingface.co/spaces/000x/MusicGen/resolve/main/server.js
- Command line
-
hf download hf://spaces/000x/MusicGen/server.js
-
curl -L -o server.js https://huggingface.co/spaces/000x/MusicGen/resolve/main/server.js
13.4 kB
| ; | |
| require('dotenv').config(); | |
| // Media is encrypted into a temp file before upload. If the drive that holds | |
| // the temp folder is full, WhatsApp sends fail. MEDIA_TMP_DIR lets you point | |
| // that temp folder at a drive with space. Set it before anything is sent. | |
| const MEDIA_TMP_DIR = process.env.MEDIA_TMP_DIR || ''; | |
| if (MEDIA_TMP_DIR) { | |
| require('fs').mkdirSync(MEDIA_TMP_DIR, { recursive: true }); | |
| process.env.TMP = process.env.TEMP = process.env.TMPDIR = MEDIA_TMP_DIR; | |
| } | |
| const fs = require('fs'); | |
| const os = require('os'); | |
| const path = require('path'); | |
| const crypto = require('crypto'); | |
| const express = require('express'); | |
| const pino = require('pino'); | |
| const QRCode = require('qrcode'); | |
| const { | |
| default: makeWASocket, | |
| DisconnectReason, | |
| fetchLatestBaileysVersion, | |
| makeCacheableSignalKeyStore, | |
| } = require('@whiskeysockets/baileys'); | |
| const db = require('./db'); | |
| // --------------------------------------------------------------------------- | |
| // Config | |
| // --------------------------------------------------------------------------- | |
| const PORT = Number(process.env.PORT) || 3000; | |
| const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || ''; | |
| const COOLDOWN_MS = 5000; | |
| if (ADMIN_PASSWORD.length < 8) { | |
| console.error('ADMIN_PASSWORD is missing or shorter than 8 characters. Set it in .env and restart.'); | |
| process.exit(1); | |
| } | |
| const MIME = { | |
| '.mp4': 'video/mp4', '.mkv': 'video/x-matroska', '.mov': 'video/quicktime', | |
| '.pdf': 'application/pdf', '.zip': 'application/zip', '.mp3': 'audio/mpeg', | |
| '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.srt': 'text/plain', | |
| }; | |
| const GB = 1024 ** 3; | |
| const MB = 1024 ** 2; | |
| // --------------------------------------------------------------------------- | |
| // Temp-folder housekeeping | |
| // Baileys leaves "<type><id>-enc" / "-original" files behind when a send | |
| // crashes. Remove those older than 15 minutes so they stop filling the disk. | |
| // --------------------------------------------------------------------------- | |
| const STALE_TEMP = /^(image|video|audio|document|sticker)[0-9a-z]+-(enc|original)$/i; | |
| function sweepStaleMediaTemp() { | |
| const dir = os.tmpdir(); | |
| let removed = 0; | |
| try { | |
| for (const name of fs.readdirSync(dir)) { | |
| if (!STALE_TEMP.test(name)) continue; | |
| const full = path.join(dir, name); | |
| try { | |
| if (Date.now() - fs.statSync(full).mtimeMs > 15 * 60 * 1000) { | |
| fs.unlinkSync(full); | |
| removed += 1; | |
| } | |
| } catch { /* in use or already gone */ } | |
| } | |
| } catch (e) { | |
| console.error('Temp sweep skipped:', e.message); | |
| } | |
| if (removed) console.log(`Removed ${removed} stale media temp file(s).`); | |
| } | |
| // Before sending a URL file, check the temp folder has room for it. | |
| // Returns an error message, or null when it is fine (or the size is unknown). | |
| async function checkTempSpace(url) { | |
| if (typeof fs.statfsSync !== 'function') return null; | |
| let size = 0; | |
| try { | |
| const head = await fetch(url, { method: 'HEAD', redirect: 'follow', signal: AbortSignal.timeout(10000) }); | |
| size = Number(head.headers.get('content-length')) || 0; | |
| } catch { /* server does not answer HEAD: try the send anyway */ } | |
| if (!size) return null; | |
| const st = fs.statfsSync(os.tmpdir()); | |
| const free = Number(st.bavail) * Number(st.bsize); | |
| const needed = size + 100 * MB; | |
| if (free >= needed) return null; | |
| return `Not enough free space for the temp folder (${os.tmpdir()}): need about ${(needed / GB).toFixed(2)} GB, free ${(free / GB).toFixed(2)} GB. Free up disk space or set MEDIA_TMP_DIR to a drive with room.`; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // WhatsApp bot | |
| // --------------------------------------------------------------------------- | |
| let sock = null; | |
| let connected = false; | |
| let qrImage = null; | |
| let starting = false; | |
| let shuttingDown = false; | |
| let server = null; | |
| const cooldown = new Map(); | |
| function scheduleStart(ms) { | |
| if (shuttingDown) return; | |
| setTimeout(() => startBot().catch((e) => { | |
| console.error('Start failed:', e.message); | |
| scheduleStart(8000); | |
| }), ms); | |
| } | |
| async function startBot() { | |
| if (starting || shuttingDown) return; | |
| starting = true; | |
| try { | |
| const { state, saveCreds } = await db.useSqlAuthState(); | |
| let version; | |
| try { ({ version } = await fetchLatestBaileysVersion()); } catch { /* use library default */ } | |
| const logger = pino({ level: 'silent' }); | |
| const s = makeWASocket({ | |
| version, | |
| logger, | |
| printQRInTerminal: false, | |
| markOnlineOnConnect: false, | |
| syncFullHistory: false, | |
| auth: { creds: state.creds, keys: makeCacheableSignalKeyStore(state.keys, logger) }, | |
| }); | |
| sock = s; | |
| // Only the current socket may write credentials (stops an old socket | |
| // from re-saving a session after Unlink). | |
| s.ev.on('creds.update', () => { if (s === sock) saveCreds().catch((e) => console.error('Save creds failed:', e.message)); }); | |
| s.ev.on('connection.update', async ({ connection, lastDisconnect, qr }) => { | |
| if (s !== sock) return; // old socket | |
| try { | |
| if (qr) qrImage = await QRCode.toDataURL(qr); | |
| if (connection === 'open') { | |
| connected = true; | |
| qrImage = null; | |
| console.log('WhatsApp connected.'); | |
| } | |
| if (connection === 'close') { | |
| connected = false; | |
| qrImage = null; | |
| sock = null; | |
| const code = lastDisconnect?.error?.output?.statusCode; | |
| const loggedOut = code === DisconnectReason.loggedOut; | |
| console.log(`Connection closed (code ${code}).`); | |
| if (loggedOut) await db.clearAuth(); | |
| scheduleStart(loggedOut ? 1000 : 3000); | |
| } | |
| } catch (e) { | |
| console.error('connection.update error:', e.message); | |
| } | |
| }); | |
| s.ev.on('messages.upsert', ({ messages, type }) => { | |
| if (type !== 'notify') return; | |
| for (const m of messages) onMessage(s, m).catch((e) => console.error('Message error:', e.message)); | |
| }); | |
| } finally { | |
| starting = false; | |
| } | |
| } | |
| async function onMessage(s, m) { | |
| if (!m.message || m.key.fromMe) return; | |
| const jid = m.key.remoteJid || ''; | |
| if (!/@(s\.whatsapp\.net|lid)$/.test(jid)) return; // private chats only | |
| const text = (m.message.conversation || m.message.extendedTextMessage?.text || '').trim(); | |
| // Accepts "ABC12", "get ABC12", ".get ABC12", "dl ABC12", "code ABC12" | |
| const match = text.match(/^(?:[.!/]?(?:get|dl|code)\s+)?([A-Za-z0-9_-]{3,20})$/i); | |
| if (!match) return; | |
| const item = await db.findFile(match[1]); | |
| const explicit = /^[.!/]?(get|dl|code)\s/i.test(text); | |
| if (!item) { | |
| if (explicit) await s.sendMessage(jid, { text: '❌ Code not found. Please check the code and try again.' }, { quoted: m }); | |
| return; | |
| } | |
| const now = Date.now(); | |
| if (now - (cooldown.get(jid) || 0) < COOLDOWN_MS) return; | |
| if (cooldown.size > 1000) cooldown.clear(); | |
| cooldown.set(jid, now); | |
| await s.sendMessage(jid, { text: `⏳ Sending *${item.title}*, please wait...` }, { quoted: m }); | |
| try { | |
| const problem = await checkTempSpace(item.url); | |
| if (problem) throw new Error(problem); | |
| const ext = path.extname(new URL(item.url).pathname).toLowerCase(); | |
| const caption = `📁 *${item.title}*`; | |
| if (item.type === 'video') { | |
| await s.sendMessage(jid, { video: { url: item.url }, caption, mimetype: MIME[ext] || 'video/mp4' }, { quoted: m }); | |
| } else { | |
| const fileName = `${item.title.replace(/[^\w.-]+/g, '_')}${ext}`; | |
| await s.sendMessage( | |
| jid, | |
| { document: { url: item.url }, mimetype: MIME[ext] || 'application/octet-stream', fileName, caption }, | |
| { quoted: m } | |
| ); | |
| } | |
| await db.markSent(item.code); | |
| } catch (e) { | |
| console.error(`Send failed for ${item.code}:`, e.message); | |
| await s.sendMessage(jid, { text: '⚠️ Could not send the file right now. Please try again later.' }, { quoted: m }); | |
| } | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Admin API | |
| // --------------------------------------------------------------------------- | |
| const app = express(); | |
| app.set('trust proxy', 1); | |
| app.use(express.json({ limit: '100kb' })); | |
| app.use(express.static(path.join(__dirname, 'public'))); | |
| app.get('/health', (req, res) => res.json({ status: 'ok', whatsapp: connected })); | |
| const safeEq = (a, b) => { | |
| const x = Buffer.from(String(a)); | |
| const y = Buffer.from(String(b)); | |
| return x.length === y.length && crypto.timingSafeEqual(x, y); | |
| }; | |
| const failed = new Map(); | |
| function auth(req, res, next) { | |
| const rec = failed.get(req.ip) || { n: 0, until: 0 }; | |
| if (Date.now() < rec.until) { | |
| return res.status(429).json({ error: 'Too many wrong attempts. Try again in a few minutes.' }); | |
| } | |
| if (safeEq(req.get('x-admin-key') || '', ADMIN_PASSWORD)) { | |
| failed.delete(req.ip); | |
| return next(); | |
| } | |
| rec.n += 1; | |
| if (rec.n >= 5) { rec.until = Date.now() + 10 * 60 * 1000; rec.n = 0; } | |
| failed.set(req.ip, rec); | |
| return res.status(401).json({ error: 'Wrong password.' }); | |
| } | |
| app.use('/api', auth); | |
| const wrap = (fn) => (req, res) => Promise.resolve(fn(req, res)).catch((e) => { | |
| console.error('API error:', e.message); | |
| res.status(500).json({ error: 'Server error. Check the console log.' }); | |
| }); | |
| app.get('/api/status', (req, res) => res.json({ connected, qr: qrImage })); | |
| app.post('/api/pair', wrap(async (req, res) => { | |
| const phone = String(req.body?.phone || '').replace(/\D/g, ''); | |
| if (phone.length < 10 || phone.length > 15) { | |
| return res.status(400).json({ error: 'Use country code + number, e.g. 94771234567.' }); | |
| } | |
| if (!sock) return res.status(503).json({ error: 'Bot is starting. Try again in 10 seconds.' }); | |
| if (connected || sock.authState?.creds?.registered) { | |
| return res.status(409).json({ error: 'Already linked. Use "Unlink" first to link a different number.' }); | |
| } | |
| try { | |
| const code = await sock.requestPairingCode(phone); | |
| return res.json({ code }); | |
| } catch (e) { | |
| return res.status(500).json({ error: `Could not create pairing code: ${e.message}` }); | |
| } | |
| })); | |
| app.post('/api/unlink', wrap(async (req, res) => { | |
| const old = sock; | |
| sock = null; | |
| connected = false; | |
| qrImage = null; | |
| try { await old?.logout(); } catch { /* already closed */ } | |
| try { old?.end(undefined); } catch { /* noop */ } | |
| await db.clearAuth(); | |
| scheduleStart(500); | |
| res.json({ ok: true }); | |
| })); | |
| app.get('/api/files', wrap(async (req, res) => res.json(await db.listFiles()))); | |
| app.post('/api/files', wrap(async (req, res) => { | |
| const title = String(req.body?.title || '').trim(); | |
| const url = String(req.body?.url || '').trim(); | |
| const type = req.body?.type === 'video' ? 'video' : 'document'; | |
| let code = String(req.body?.code || '').trim(); | |
| if (!title || title.length > 100) return res.status(400).json({ error: 'Title is required (max 100 characters).' }); | |
| try { | |
| const u = new URL(url); | |
| if (!['http:', 'https:'].includes(u.protocol)) throw new Error('bad protocol'); | |
| } catch { | |
| return res.status(400).json({ error: 'Enter a valid http(s) file URL.' }); | |
| } | |
| if (code) { | |
| if (!/^[A-Za-z0-9_-]{3,20}$/.test(code)) { | |
| return res.status(400).json({ error: 'Code must be 3-20 letters, numbers, - or _.' }); | |
| } | |
| if (await db.findFile(code)) return res.status(409).json({ error: 'That code is already used.' }); | |
| } else { | |
| code = await makeCode(); | |
| } | |
| try { | |
| const item = await db.addFile({ code, title, url, type }); | |
| return res.status(201).json(item); | |
| } catch (e) { | |
| if (e.code === '23505') return res.status(409).json({ error: 'That code is already used.' }); | |
| throw e; | |
| } | |
| })); | |
| app.delete('/api/files/:code', wrap(async (req, res) => { | |
| const n = await db.deleteFile(req.params.code); | |
| if (n === 0) return res.status(404).json({ error: 'Code not found.' }); | |
| return res.json({ ok: true }); | |
| })); | |
| async function makeCode() { | |
| const abc = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; | |
| let code; | |
| do { | |
| code = Array.from(crypto.randomBytes(5), (b) => abc[b % abc.length]).join(''); | |
| } while (await db.findFile(code)); | |
| return code; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Boot + shutdown | |
| // --------------------------------------------------------------------------- | |
| // A full disk makes Baileys' temp-file stream emit an error nobody listens to, | |
| // which would kill the whole app (dashboard included). Log it and keep running. | |
| process.on('uncaughtException', (err) => { | |
| if (err.code === 'ENOSPC') { | |
| console.error('DISK FULL: a write failed with ENOSPC. Free space on the temp drive or set MEDIA_TMP_DIR.'); | |
| } else { | |
| console.error('Uncaught error:', err.message); | |
| } | |
| }); | |
| function shutdown() { | |
| shuttingDown = true; | |
| try { sock?.end(undefined); } catch { /* noop */ } | |
| const done = () => db.pool.end().catch(() => {}).finally(() => process.exit(0)); | |
| if (server) server.close(done); else done(); | |
| setTimeout(() => process.exit(0), 5000).unref(); | |
| } | |
| process.on('SIGTERM', shutdown); | |
| process.on('SIGINT', shutdown); | |
| process.on('unhandledRejection', (r) => console.error('Unhandled rejection:', r?.message || r)); | |
| (async () => { | |
| await db.init(); | |
| sweepStaleMediaTemp(); | |
| setInterval(sweepStaleMediaTemp, 30 * 60 * 1000).unref(); | |
| server = app.listen(PORT, '0.0.0.0', () => console.log(`Dashboard: http://localhost:${PORT}`)); | |
| scheduleStart(0); | |
| })().catch((e) => { | |
| console.error('Startup failed:', e.message); | |
| process.exit(1); | |
| }); | |