const express = require('express'); const { exec } = require('child_process'); require('dotenv').config(); const phonesCfg = require('./phones.json'); const buttonsCfg = require('./buttons.json'); const fs = require('fs'); const path = require('path'); const bcrypt = require('bcrypt'); if (!fs.existsSync(path.join(__dirname, 'slingtoken.txt'))) { // Create empty file fs.writeFileSync(path.join(__dirname, 'slingtoken.txt'), '', 'utf8'); console.warn('slingtoken.txt file created. Please add your Sling token to this file to use sling messaging!'); } var slingToken = fs.readFileSync(path.join(__dirname, 'slingtoken.txt'), 'utf8').trim(); const contexts = {}; // Generate contexts from buttonsCfg Object.keys(buttonsCfg).forEach(category => { buttonsCfg[category].forEach(button => { if (button.name && button.context) { contexts[button.name] = { context: button.context.context, timeout: button.context.timeout, cid: button.context.cid, ...(button.context.dial && { dial: button.context.dial }), ...(button.sling_chat_id && { sling_chat_id: button.sling_chat_id }), ...(button.sling_chat_message && { sling_chat_message: button.sling_chat_message }) }; } }); }); //console.log('Generated contexts:', contexts); function trigCall(pageType, phone) { // If contexts[pageType] does not exist, return an error if (!contexts[pageType]) { throw new Error(`Invalid page type: ${pageType}`); } const { context, timeout, cid, dial, sling_chat_id, sling_chat_message } = contexts[pageType]; const targetNumber = dial || phone; if (!targetNumber) { throw new Error(`Phone number is required for page type: ${pageType}`); } // Slink chat notification if (sling_chat_id && sling_chat_message) { fetch(`https://api.getsling.com/v1/conversations/${sling_chat_id}/messages`, { method: 'POST', headers: { 'Authorization': `${slingToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ 'content': sling_chat_message }) }).then(res => res.json()).then(data => { if (data && data.success) { console.log('Sling chat message sent successfully.'); } else { console.error('Error sending Sling chat message:', data); } }); } originateCall(targetNumber, context, 0, timeout, cid).then((output) => { console.log(`Call originated: ${output}`); }).catch((error) => { console.error(`Error originating call: ${error}`); }); return true; } function originateCall(number, context, delay, timeout, cid, variables = {}) { // Build the base command let command = `/usr/bin/ast_originate ${number} ${context} ${delay} ${timeout} ${Buffer.from(cid).toString('base64')}`; // Add variables if provided if (variables && typeof variables === 'object') { const varString = Object.entries(variables) .map(([key, value]) => `${key}=${value}`) .join(' '); if (varString) { command += ` ${varString}`; } } return new Promise((resolve, reject) => { exec(command, (error, stdout, stderr) => { if (error) { reject(error); } else { resolve(stdout); } }); }); } // Sling chat stuff; Check current token with GET https://api.getsling.com/v1/account/session, refresh with POST https://api.getsling.com/v1/account/session (GET NEW TOKEN FROM RETURN AUTHORIZATION HEADER) async function slingAuthLoop() { console.log("Start slingAuthLoop") if (!slingToken) { console.warn('No Sling token provided.'); return; } setTimeout(slingAuthLoop, 15 * 60 * 1000); // Refresh every 15 minutes const sessionCheck = await fetch('https://api.getsling.com/v1/account/session', { method: 'GET', headers: { 'Authorization': `${slingToken}` } }); console.log(sessionCheck) if (sessionCheck && sessionCheck.ok) { console.log('Sling session is valid. Refreshing token...'); const sessionRefresh = await fetch('https://api.getsling.com/v1/account/session', { method: 'POST', headers: { 'Authorization': `${slingToken}` } }); const newToken = sessionRefresh.headers.get('Authorization'); if (newToken) { slingToken = newToken; fs.writeFileSync(path.join(__dirname, 'slingtoken.txt'), newToken, 'utf8'); console.log('Sling token refreshed.'); } } else { console.error('Sling session is invalid. Please get a new token manually!'); } } slingAuthLoop(); global.contexts = contexts; global.trigCall = trigCall; global.buttonsCfg = buttonsCfg; global.phonesCfg = phonesCfg; global.slingToken = slingToken; global.originateCall = originateCall; global.exec = exec; const app = express(); const HOST = process.env.HOST || 'localhost'; const PORT = process.env.PORT || 3000; app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.set('view engine', 'ejs'); app.set('views', './views'); app.use(express.static('static')); app.use(require('express-session')({ secret: process.env.SESSION_SECRET || 'fallback-secret-key', resave: false, saveUninitialized: false, cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 }, store: new (require('session-file-store')(require('express-session')))({ path: path.join(__dirname, 'sessions'), ttl: 24 * 60 * 60, // 1 day retries: 1, reapInterval: 60 * 60 // prune expired sessions every hour }), })); function auth(req, res, next) { if (!req.session || !req.session.authenticated) { return res.redirect('/login'); } if (req.session.user && req.session.user.remember) { // Extend session expiration req.session.cookie.maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days } next(); } function apiAuth(req, res, next) { console.log(req.headers) if (req.headers["authorization"]) { if (req.headers["authorization"] === process.env.API_TOKEN) { return next(); } else { console.log('Unauthorized API access attempt with invalid token'); return res.status(401).json({ success: false, message: 'Unauthorized' }); } } if (!req.session || !req.session.authenticated) { console.log('Unauthorized API access attempt without valid session'); return res.status(401).json({ success: false, message: 'Unauthorized' }); } if (req.session.user && req.session.user.remember) { // Extend session expiration req.session.cookie.maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days } next(); } global.auth = auth; global.apiAuth = apiAuth; // folder where routes live const ROUTES_DIR = path.join(__dirname, "routes"); // Recursive route loader function loadRoutes(dir, baseRoute = "") { fs.readdirSync(dir).forEach((file) => { const fullPath = path.join(dir, file); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { // If folder, recurse deeper const nestedBase = baseRoute + "/" + file; loadRoutes(fullPath, nestedBase); } else if (file.endsWith(".js")) { // If .js file → mount route const routePath = baseRoute + "/" + file.replace(".js", "").replace("index", ""); const router = require(fullPath); console.log(`Mounting route: ${routePath || "/"}`); app.use(routePath || "/", router); } }); } // Load everything in routes/ loadRoutes(ROUTES_DIR); app.post('/trig', async (req, res) => { console.log('Triggering call with data:', req.body); trigCall(req.body.pageType, req.body.phone); res.status(200).send('Call triggered'); }); app.post('/stop', async (req, res) => { console.log('Stopping page for phone:', req.body.phone); // Logic to stop the page would go here. // For now we will just log it, as the specific asterisk command to hangup a channel depends on implementation details not provided. // Typically it might involve 'asterisk -rx "channel request hangup "' or similar via AMI. // Assuming we might want to run a command similar to originate but for hangup if we knew the channel. // Since we don't have the channel ID easily available without tracking it, we might need to implement channel tracking or use a broad command. exec(`/usr/bin/ast_drop ${process.env.PAGE_GROUP || '9000'}`, (error, stdout, stderr) => { if (error) { console.error(`Error stopping page: ${error}`); return res.status(500).send('Error stopping page'); } console.log(`Page stopped: ${stdout}`); }); res.status(200).send('Stop request received'); }); const convertPasswords = () => { // Read auth.json and convert plaintext to hashed passwords const authFilePath = path.join(__dirname, 'auth.json'); if (fs.existsSync(authFilePath)) { let authData = JSON.parse(fs.readFileSync(authFilePath, 'utf8')); let updated = false; authData = authData.map(user => { if (user.password) { const hashedPassword = bcrypt.hashSync(user.password, 10); updated = true; return { ...user, passwordHash: hashedPassword, password: undefined }; } return user; }); if (updated) { fs.writeFileSync(authFilePath, JSON.stringify(authData, null, 4), 'utf8'); console.log('Converted plaintext passwords to hashed passwords in auth.json'); } } else { console.warn('auth.json file not found for password conversion.'); } global.authUsers = JSON.parse(fs.readFileSync(authFilePath, 'utf8')); }; convertPasswords(); app.listen(PORT, HOST, () => { console.log(`Server running on http://${HOST}:${PORT}`); });