From a15e29691803f624dfdfdf772c8c5a986f4bb980 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 20 Sep 2025 21:56:06 +0200 Subject: [PATCH] it works!!! --- .env.example | 12 ++- callStats.js | 15 +++ index.js | 296 +++++++++++++++++++++++---------------------------- records.js | 28 ++--- 4 files changed, 170 insertions(+), 181 deletions(-) diff --git a/.env.example b/.env.example index d4e10e0..fb136d1 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,13 @@ -DISCORD_WEBHOOK_URL = +MATRIX_BASE_URL = +MATRIX_ACCESS_TOKEN = +MATRIX_ROOM_ID = +DISCORD_WEBHOOK_URL = DATABASE_USER = statsBot DATABASE_PASSWORD = statsBot DATABASE_HOST = 127.0.0.1 -DATABASE_NAME = asteriskcdrdb \ No newline at end of file +DATABASE_NAME = asteriskcdrdb +# Extra debugging options: +#NOOP=true +#RUN_ONCE=true +#DATABASE_LOG_TIMINGS=true +#LOG_MESSAGES=true diff --git a/callStats.js b/callStats.js index 851f8f4..6da5cb0 100644 --- a/callStats.js +++ b/callStats.js @@ -7,9 +7,24 @@ export class CallStats { this[key] = value; } + /** + * @type {number} + */ totalCallsThisMonth; + /** + * @type {number} + */ totalCallsEverPlaced; + /** + * @type {number} + */ totalCallsMade; + /** + * @type {string} + */ allTimeRecord; + /** + * @type {boolean} + */ isNewRecord = false; } \ No newline at end of file diff --git a/index.js b/index.js index bde5070..a034ef0 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,6 @@ const { TimeSpan } = require("./timeSpan"); const { DateBuilder } = require("./dateBuilder"); const { CallStats } = require("./callStats"); const { CallRecord, Records } = require("./records"); -const fs = require('fs').promises; const fsSync = require('fs'); const hook = !!process.env.DISCORD_WEBHOOK_URL ? new Discord.WebhookClient({ url: process.env.DISCORD_WEBHOOK_URL }) : null; @@ -16,38 +15,34 @@ 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() - ); + 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(); - }); - }); + 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(); + }); + }); } /** @@ -55,35 +50,29 @@ async function queryScalarAsync(query, ...params) { * @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; + 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()); + const uptime = os.uptime(); + const now = new Date(); + return new TimeSpan(now - (uptime * 1000), now.getTime()); } /** @@ -93,147 +82,132 @@ function getSystemUptime() { * @returns {CallStats} */ function updateRecords(callStats, records) { - const yesterday = getYesterday().startDate; - const yesterdayDateString = yesterday.toISOString().split('T')[0]; - let isNewRecord = false; + const yesterday = getYesterday().startDate; + const yesterdayDateString = yesterday.toISOString().split('T')[0]; + let isNewRecord = false; - // Update all-time record - const allTimeRecord = records.callRecord || new CallRecord({ date: yesterdayDateString, count: 0 }); - if (!records.callRecord) { - records.callRecord = { date: yesterdayDateString, count: callStats.totalCallsMade }; - isNewRecord = true; - } else if (allTimeRecord.count < callStats.totalCallsThisMonth) { - allTimeRecord.count = callStats.totalCallsThisMonth; - isNewRecord = true; - } - callStats.allTimeRecord = `${allTimeRecord.count} calls on ${allTimeRecord.date}`; + // Update all-time record + const allTimeRecord = records.callRecord || new CallRecord({ date: yesterdayDateString, count: 0 }); + if (!records.callRecord) { + records.callRecord = { date: yesterdayDateString, count: callStats.totalCallsMade }; + isNewRecord = true; + } else if (allTimeRecord.count < callStats.totalCallsThisMonth) { + allTimeRecord.count = callStats.totalCallsThisMonth; + isNewRecord = true; + } + callStats.allTimeRecord = `${allTimeRecord.count} calls on ${allTimeRecord.date}`; - // Update total calls ever placed - records.totalCallsEverPlaced = callStats.totalCallsEverPlaced; + // 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; + // 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; - if (isNewRecord) { - callStats.isNewRecord = true; - } - return callStats; + if (isNewRecord) { + callStats.isNewRecord = true; + } + return callStats; } async function sendSummary() { - console.log("Preparing summary."); - const data = await getPreviousDayData(); - console.log("Updating records..."); - const stats = await updateRecords(data, records); - console.log("Saving."); - await records.toJSONFile(JSON_FILE); + console.log("Preparing summary."); + const data = await getPreviousDayData(); + console.log("Updating records..."); + const stats = await updateRecords(data, records); + console.log("Saving."); + await records.toJSONFile(JSON_FILE); - const yesterday = getYesterday(); + const yesterday = getYesterday(); - await sendSummaryDiscord(yesterday, stats) - await sendSummaryMatrix(yesterday, stats) + 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 + 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} ${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}
🎉 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 - } - } + "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}
🎉 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.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."); - } - } + 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 - }); + 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) - ], - timestamp: new Date(), - footer: {} - } + 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) ], + 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!`)); - } + 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 ] }; + const payload = { embeds: [ embed ] }; - if (process.env.LOG_MESSAGES) - console.log("Sending Discord message:", JSON.stringify(payload, null, 2)); + 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 (hook && !process.env.NOOP) await hook.send(payload); } if (process.env.NOOP || process.env.RUN_ONCE) { - sendSummary(); - return; + sendSummary(); + return; } console.log("Scheduling..."); diff --git a/records.js b/records.js index 1b2f77f..5cef7a4 100644 --- a/records.js +++ b/records.js @@ -2,24 +2,16 @@ import fs from "fs"; export class Records { constructor(records = {}) { - if (typeof records.records === "object") - for (const [ key, value ] of Object.entries(records.records)) { - const oldTableMatches = key.match(/^monthly_total_(\d{4})-(\d{2})$/); - if (oldTableMatches) { - const year = oldTableMatches[1]; - const month = oldTableMatches[2]; - if (!this.monthlyTotals) this.monthlyTotals = {}; - if (!this.monthlyTotals[year]) this.monthlyTotals[year] = {}; - this.monthlyTotals[year][month] = value; - } else if (key === "record_calls" && typeof value === "object" && value !== null) - this.callRecord = new CallRecord(value.date, value.count); - else if (key === "total_calls_ever_placed" && typeof value === "number") - this.totalCallsEverPlaced = value; - else throw new Error(`Unknown legacy record key: ${key}`); - } - else - for (const [ key, value ] of Object.entries(records)) - this[key] = value; + if (typeof records.records === "object") for (const [ key, value ] of Object.entries(records.records)) { + const oldTableMatches = key.match(/^monthly_total_(\d{4})-(\d{2})$/); + if (oldTableMatches) { + const year = oldTableMatches[1]; + const month = oldTableMatches[2]; + if (!this.monthlyTotals) this.monthlyTotals = {}; + if (!this.monthlyTotals[year]) this.monthlyTotals[year] = {}; + this.monthlyTotals[year][month] = value; + } else if (key === "record_calls" && typeof value === "object" && value !== null) this.callRecord = new CallRecord(value.date, value.count); else if (key === "total_calls_ever_placed" && typeof value === "number") this.totalCallsEverPlaced = value; else throw new Error(`Unknown legacy record key: ${key}`); + } else for (const [ key, value ] of Object.entries(records)) this[key] = value; } static fromJSONFile(path) {