freepbx-stats/index.js
2025-09-21 14:36:07 +02:00

229 lines
8.7 KiB
JavaScript

require("dotenv").config();
const cron = require("node-cron");
const os = require("os");
const Discord = require("discord.js");
const mysql = require("mysql");
const { TimeSpan } = require("./timeSpan");
const { DateBuilder } = require("./dateBuilder");
const { CallStats } = require("./callStats");
const { CallRecord, Records } = require("./records");
const fsSync = require("fs");
const hook = !!process.env.DISCORD_WEBHOOK_URL ? new Discord.WebhookClient({ url: process.env.DISCORD_WEBHOOK_URL }) : null;
const JSON_FILE = process.env.JSON_FILE || "records.json";
const records = fsSync.existsSync(JSON_FILE) ? Records.fromJSONFile(JSON_FILE) : new Records();
function getYesterday() {
return new TimeSpan(new DateBuilder().addDays(-1).atStartOfDay().build().getTime(), new DateBuilder().addDays(-1).atEndOfDay().build().getTime());
}
/**
* @param {string} query
* @param {any} params
* @returns {Promise<int>}
*/
async function queryScalarAsync(query, ...params) {
const start = Date.now();
const connection = await mysql.createConnection({
host: process.env.DATABASE_HOST,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME
});
await connection.connect();
return new Promise((resolve, reject) => {
connection.query(query, params, (err, results) => {
if (err) {
reject(err);
} else {
if (process.env.DATABASE_LOG_TIMINGS) console.log(`Query took ${Date.now() - start}ms:`, query, params, "=>", results);
resolve(results[0][Object.keys(results[0])[0]]);
}
connection.end();
});
});
}
/**
* Fetch call statistics
* @returns {Promise<CallStats>}
*/
async function getPreviousDayData() {
const [ callsYesterday, callsThisMonth, callsTotal ] = await Promise.all([
queryScalarAsync(`
SELECT COUNT(DISTINCT uniqueid) AS call_count
FROM cdr
WHERE calldate BETWEEN ? AND ?;
`, getYesterday().startDate, getYesterday().endDate), queryScalarAsync(`
SELECT COUNT(DISTINCT uniqueid) AS call_count
FROM cdr
WHERE MONTH (calldate) = ? AND YEAR (calldate) = ?;
`, getYesterday().startDate.getMonth(), getYesterday().startDate.getFullYear()), queryScalarAsync(`
SELECT COUNT(DISTINCT uniqueid) AS call_count
FROM cdr;
`)
]);
const stats = new CallStats({
totalCallsMade: callsYesterday, totalCallsThisMonth: callsThisMonth, totalCallsEverPlaced: callsTotal
});
console.log("Got stats:", stats, "built from query results:", { callsYesterday, callsThisMonth, callsTotal });
return stats;
}
function getSystemUptime() {
const uptime = os.uptime();
const now = new Date();
return new TimeSpan(now - (uptime * 1000), now.getTime());
}
/**
* Update records with new data
* @param {CallStats} callStats
* @returns {CallStats}
*/
function updateRecords(callStats) {
const yesterday = getYesterday().startDate;
const yesterdayDateString = yesterday.toISOString().split("T")[0];
// Update all-time record
const previousRecord = records.callRecord || new CallRecord({ date: yesterdayDateString, count: 0 });
callStats.isNewRecord = false;
if (!records.callRecord) {
records.callRecord = new CallRecord({ date: yesterdayDateString, count: callStats.totalCallsMade });
callStats.isNewRecord = true;
console.warn("No previous call record found, initializing new record.");
} else if (callStats.totalCallsMade > previousRecord.count) {
records.callRecord.count = callStats.totalCallsMade;
records.callRecord.date = yesterdayDateString;
callStats.isNewRecord = true;
console.log(`New all-time record: ${previousRecord.count} calls on ${yesterdayDateString}, previous record was ${records.callRecord.count} calls on ${records.callRecord.date}`);
} else {
console.log(`No new record. Yesterday: ${callStats.totalCallsMade}, Record: ${previousRecord.count} on ${previousRecord.date}`);
}
// pass record to call stats for reporting
callStats.allTimeRecord = records.callRecord;
// Update total calls ever placed
records.totalCallsEverPlaced = callStats.totalCallsEverPlaced;
// Update monthly totals
if (!records.monthlyTotals) records.monthlyTotals = {};
if (!records.monthlyTotals[yesterday.getFullYear().toString()]) records.monthlyTotals[yesterday.getFullYear().toString()] = {};
records.monthlyTotals[yesterday.getFullYear().toString()][yesterday.getMonth().toString()] = callStats.totalCallsThisMonth;
return callStats;
}
async function sendSummary() {
console.log("Preparing summary.");
const data = await getPreviousDayData();
console.log("Updating records...");
const stats = await updateRecords(data);
if (!process.env.NO_SAVE_RECORDS) {
console.log("Saving.");
await records.toJSONFile(JSON_FILE);
}
const yesterday = getYesterday();
await sendSummaryDiscord(yesterday, stats);
await sendSummaryMatrix(yesterday, stats);
}
async function sendSummaryMatrix(yesterday, stats) {
const message = {
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"body": `Summary from ${new Date(yesterday.start).toDateString()} to ${new Date(yesterday.end).toDateString()}\n
Calls Made: ${stats.totalCallsMade}
Monthly Total: ${stats.totalCallsThisMonth}
Total Calls Ever Placed: ${stats.totalCallsEverPlaced}
System Uptime: ${getSystemUptime().toString(false, false)}
All Time Record: ${stats.allTimeRecord.count} calls on ${stats.allTimeRecord.date}
${stats.isNewRecord ? `🎉 NEW RECORD! 🎉 A new record has been set, at ${stats.totalCallsMade} calls in a day!` : ""}`.trim(),
"formatted_body": `
<table>
<thead>
<tr><td colspan="2"><strong>Summary from ${yesterday.startDate.toString()} to ${yesterday.endDate.toString()}</strong></td></tr>
</thead>
<tbody>
<tr><td>Calls made</td><td>${stats.totalCallsMade}</td></tr>
<tr><td>Monthly total</td><td>${stats.totalCallsThisMonth}</td></tr>
<tr><td>Total calls ever placed</td><td>${stats.totalCallsEverPlaced}</td></tr>
<tr><td>System uptime</td><td>${getSystemUptime().toString(false, false)}</td></tr>
<tr><td>All-time record</td><td>${stats.allTimeRecord.count} calls on ${stats.allTimeRecord.date}</td></tr>
${stats.isNewRecord ? `<tr><td colspan="2"><span data-mx-color="#FFD700"><font color="#FFD700">🎉 NEW RECORD! 🎉</font></span> A new record has been set, at ${stats.totalCallsMade} calls in a day!</td></tr>` : ""}
</tbody>
</table>
`.split("\n").map(s => s.trim()).join(""),
"tel.litenet.call_stats_summary": {
...stats, date: yesterday, allTimeRecordData: records.callRecord
}
};
if (process.env.LOG_MESSAGES) {
console.log("Sending Matrix message:", JSON.stringify(message, null, 2));
console.log("Plaintext:\n", message.body);
console.log("HTML:\n", message.formatted_body);
}
if (!process.env.NOOP) {
if (!process.env.MATRIX_BASE_URL || !process.env.MATRIX_ROOM_ID || !process.env.MATRIX_ACCESS_TOKEN) {
console.warn("MATRIX_BASE_URL, MATRIX_ROOM_ID or MATRIX_ACCESS_TOKEN not set, skipping Matrix message.");
return;
}
const resp = await fetch(`${process.env.MATRIX_BASE_URL}/_matrix/client/v3/rooms/${encodeURIComponent(process.env.MATRIX_ROOM_ID)}/send/m.room.message/${Math.random()}`, {
method: "PUT", body: JSON.stringify(message, null, 2), headers: {
"Authorization": `Bearer ${process.env.MATRIX_ACCESS_TOKEN}`, "Content-Type": "application/json"
}
});
if (!resp.ok) {
console.error("Failed to send Matrix message:", resp.status, await resp.text());
} else {
console.log("Matrix message sent successfully.");
}
}
}
async function sendSummaryDiscord(yesterday, stats) {
const makeField = (name, value) => ({
name, value: value === undefined ? "***ERR: UNDEFINED***" : value.toString(), inline: false
});
let embed = {
title: `Summary from <t:${Math.floor(yesterday.start / 1000)}:f> to <t:${Math.floor(yesterday.end / 1000)}:f>`,
color: 0x1E90FF,
fields: [
makeField("Calls Made", stats.totalCallsMade),
makeField("Monthly Total", stats.totalCallsThisMonth),
makeField("Total Calls Ever Placed", stats.totalCallsEverPlaced),
makeField("System Uptime", getSystemUptime().toString(false, false)),
makeField("All Time Record", `${stats.allTimeRecord.count} calls on ${stats.allTimeRecord.date}`)
],
timestamp: new Date(),
footer: {}
};
if (stats.isNewRecord) {
embed.color = 0xFFD700; // Gold color for new record
embed.fields.push(makeField("🎉 NEW RECORD! 🎉", `A new record has been set, at ${stats.totalCallsMade} calls in a day!`));
}
const payload = { embeds: [ embed ] };
if (process.env.LOG_MESSAGES) console.log("Sending Discord message:", JSON.stringify(payload, null, 2));
if (hook && !process.env.NOOP) await hook.send(payload);
}
if (process.env.NOOP || process.env.RUN_ONCE) {
sendSummary();
return;
}
console.log("Scheduling...");
const schedule = cron.schedule("0 1 * * *", sendSummary);