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} */ 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} */ 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": ` ${stats.isNewRecord ? `` : ""}
Summary from ${yesterday.startDate.toString()} to ${yesterday.endDate.toString()}
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}
🎉 NEW RECORD! 🎉 A new record has been set, at ${stats.totalCallsMade} calls in a day!
`.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 to `, 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);