288 lines
8.9 KiB
JavaScript
288 lines
8.9 KiB
JavaScript
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 <channel>"' 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}`);
|
|
}); |