From 617e4605be651d66f721f1aef0a1be77f5e33875 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sun, 21 Sep 2025 14:24:44 +0200 Subject: [PATCH] h --- .env.example | 1 + callStats.js | 2 +- dateBuilder.test.js | 137 +++++++++++++++++++++++++++ index.js | 88 +++++++++-------- timespan.test.js => timeSpan.test.js | 18 ++-- 5 files changed, 195 insertions(+), 51 deletions(-) rename timespan.test.js => timeSpan.test.js (85%) diff --git a/.env.example b/.env.example index fb136d1..c563fe3 100644 --- a/.env.example +++ b/.env.example @@ -11,3 +11,4 @@ DATABASE_NAME = asteriskcdrdb #RUN_ONCE=true #DATABASE_LOG_TIMINGS=true #LOG_MESSAGES=true +#NO_SAVE_RECORDS=true diff --git a/callStats.js b/callStats.js index 6da5cb0..09c6361 100644 --- a/callStats.js +++ b/callStats.js @@ -20,7 +20,7 @@ export class CallStats { */ totalCallsMade; /** - * @type {string} + * @type {CallRecord} */ allTimeRecord; /** diff --git a/dateBuilder.test.js b/dateBuilder.test.js index e69de29..506d84b 100644 --- a/dateBuilder.test.js +++ b/dateBuilder.test.js @@ -0,0 +1,137 @@ +// noinspection ES6ConvertRequireIntoImport - this is CJS, not MJS + +const { test } = require("node:test"); +const assert = require("node:assert/strict"); +const { DateBuilder } = require("./dateBuilder"); + +test("DateBuilder should be able to be initialised", () => { + const db = new DateBuilder(); + assert.equal(db instanceof DateBuilder, true); +}); + +test("DateBuilder should be able to build current date", () => { + const now = new Date(); + const db = new DateBuilder(); + const built = db.build(); + assert.equal(built.getFullYear(), now.getFullYear()); + assert.equal(built.getMonth(), now.getMonth()); + assert.equal(built.getDate(), now.getDate()); + assert.equal(built.getHours(), now.getHours()); + assert.equal(built.getMinutes(), now.getMinutes()); + assert.equal(built.getSeconds(), now.getSeconds()); +}); + +test("DateBuilder should be able to add days", () => { + const db = new DateBuilder(new Date(2024, 0, 1)); // Jan 1, 2024 + db.addDays(30); + const built = db.build(); + assert.equal(built.getFullYear(), 2024); + assert.equal(built.getMonth(), 0); // January + assert.equal(built.getDate(), 31); // January has 31 days +}); + +test("DateBuilder should be able to add months", () => { + const db = new DateBuilder(new Date(2024, 0, 1)); // Jan 31, 2024 + db.addMonths(1); + const built = db.build(); + assert.equal(built.getFullYear(), 2024); + assert.equal(built.getMonth(), 1); // February +}); + +test("DateBuilder should be able to add years", () => { + const db = new DateBuilder(new Date(2020, 1, 1)); + db.addYears(1); + const built = db.build(); + assert.equal(built.getFullYear(), 2021); + assert.equal(built.getMonth(), 1); // February + assert.equal(built.getDate(), 1); +}); + +test("DateBuilder should be able to set date", () => { + const db = new DateBuilder(); + db.withDate(2022, 12, 25); // Dec 25, 2022 + const built = db.build(); + assert.equal(built.getFullYear(), 2022); + assert.equal(built.getMonth(), 11); // December + assert.equal(built.getDate(), 25); +}); + +test("DateBuilder should be able to set time", () => { + const db = new DateBuilder(); + db.withTime(15, 30, 45, 123); // 15:30:45.123 + const built = db.build(); + assert.equal(built.getHours(), 15); + assert.equal(built.getMinutes(), 30); + assert.equal(built.getSeconds(), 45); + assert.equal(built.getMilliseconds(), 123); +}); + +test("DateBuilder should be able to set start of day", () => { + const db = new DateBuilder(new Date(2024, 5, 15, 10, 20, 30, 456)); // June 15, 2024, 10:20:30.456 + db.atStartOfDay(); + const built = db.build(); + assert.equal(built.getFullYear(), 2024); + assert.equal(built.getMonth(), 5); // June + assert.equal(built.getDate(), 15); + assert.equal(built.getHours(), 0); + assert.equal(built.getMinutes(), 0); + assert.equal(built.getSeconds(), 0); + assert.equal(built.getMilliseconds(), 0); +}); + +test("DateBuilder should be able to set end of day", () => { + const db = new DateBuilder(new Date(2024, 5, 15, 10, 20, 30, 456)); // June 15, 2024, 10:20:30.456 + db.atEndOfDay(); + const built = db.build(); + assert.equal(built.getFullYear(), 2024); + assert.equal(built.getMonth(), 5); // June + assert.equal(built.getDate(), 15); + assert.equal(built.getHours(), 23); + assert.equal(built.getMinutes(), 59); + assert.equal(built.getSeconds(), 59); + assert.equal(built.getMilliseconds(), 999); +}); + +test("DateBuilder should be able to chain methods", () => { + const db = new DateBuilder(new Date(2024, 0, 1)); // Jan 1, 2024 + db.addDays(1).addMonths(1).addYears(1).withTime(12, 0, 0).atEndOfDay(); + const built = db.build(); + assert.equal(built.getFullYear(), 2025); + assert.equal(built.getMonth(), 1); // March + assert.equal(built.getDate(), 2); + assert.equal(built.getHours(), 23); + assert.equal(built.getMinutes(), 59); + assert.equal(built.getSeconds(), 59); + assert.equal(built.getMilliseconds(), 999); +}); + +test("DateBuilder should not mutate original date", () => { + const original = new Date(2024, 0, 1); // Jan 1, 2024 + const db = new DateBuilder(original); + db.addDays(10); + const built = db.build(); + assert.equal(original.getFullYear(), 2024); + assert.equal(original.getMonth(), 0); // January + assert.equal(original.getDate(), 1); // Original date should remain unchanged + assert.equal(built.getFullYear(), 2024); + assert.equal(built.getMonth(), 0); // January + assert.equal(built.getDate(), 11); // New date should be Jan 11, 2024 +}); + +test("DateBuilder should handle leap years correctly", () => { + const db = new DateBuilder(new Date(2020, 1, 29)); // Feb 29, 2020 (leap year) + db.addYears(1); + const built = db.build(); + assert.equal(built.getFullYear(), 2021); + assert.equal(built.getMonth(), 2); // March + assert.equal(built.getDate(), 1); // March 1, 2021 (not a leap year) +}); + +test("DateBuilder should handle month overflow correctly", () => { + const db = new DateBuilder(new Date(2024, 0, 31)); // Jan 31, 2024 + db.addDays(1); + const built = db.build(); + assert.equal(built.getFullYear(), 2024); + assert.equal(built.getMonth(), 1); // February + assert.equal(built.getDate(), 1); // Feb 29, 2024 (leap year) +}); diff --git a/index.js b/index.js index 13b623e..6ca6d96 100644 --- a/index.js +++ b/index.js @@ -1,13 +1,13 @@ require("dotenv").config(); const cron = require("node-cron"); const os = require("os"); -const Discord = require('discord.js'); -const mysql = require('mysql'); +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 fsSync = require("fs"); const hook = !!process.env.DISCORD_WEBHOOK_URL ? new Discord.WebhookClient({ url: process.env.DISCORD_WEBHOOK_URL }) : null; @@ -51,16 +51,16 @@ async function queryScalarAsync(query, ...params) { */ async function getPreviousDayData() { const [ callsYesterday, callsThisMonth, callsTotal ] = await Promise.all([ queryScalarAsync(` - SELECT COUNT(DISTINCT uniqueid) AS call_count - FROM cdr - WHERE calldate BETWEEN ? AND ?; + 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) = ?; + 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; + SELECT COUNT(DISTINCT uniqueid) AS call_count + FROM cdr; `) ]); const stats = new CallStats({ totalCallsMade: callsYesterday, totalCallsThisMonth: callsThisMonth, totalCallsEverPlaced: callsTotal @@ -78,26 +78,27 @@ function getSystemUptime() { /** * Update records with new data * @param {CallStats} callStats - * @param {Records} records * @returns {CallStats} */ -function updateRecords(callStats, records) { +function updateRecords(callStats) { const yesterday = getYesterday().startDate; - const yesterdayDateString = yesterday.toISOString().split('T')[0]; - let isNewRecord = false; + const yesterdayDateString = yesterday.toISOString().split("T")[0]; // Update all-time record - const allTimeRecord = records.callRecord || new CallRecord({ date: yesterdayDateString, count: 0 }); + const previousRecord = records.callRecord || new CallRecord({ date: yesterdayDateString, count: 0 }); + callStats.isNewRecord = false; + if (!records.callRecord) { - records.callRecord = { date: yesterdayDateString, count: callStats.totalCallsMade }; - isNewRecord = true; - console.warn("No previous call record found, initializing new record."); - } else if (allTimeRecord.count < callStats.totalCallsThisMonth) { - allTimeRecord.count = callStats.totalCallsThisMonth; - isNewRecord = true; - console.log(`New all-time record: ${allTimeRecord.count} calls on ${yesterdayDateString}, previous record was ${records.callRecord.count} calls on ${records.callRecord.date}`); + 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) { + previousRecord.count = callStats.totalCallsMade; + previousRecord.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}`); } - callStats.allTimeRecord = `${allTimeRecord.count} calls on ${allTimeRecord.date}`; + callStats.allTimeRecord = previousRecord; // Update total calls ever placed records.totalCallsEverPlaced = callStats.totalCallsEverPlaced; @@ -107,9 +108,6 @@ function updateRecords(callStats, records) { 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; } @@ -117,14 +115,16 @@ 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); + 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) + await sendSummaryDiscord(yesterday, stats); + await sendSummaryMatrix(yesterday, stats); } async function sendSummaryMatrix(yesterday, stats) { @@ -136,8 +136,8 @@ 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(), +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": ` @@ -148,15 +148,15 @@ ${stats.isNewRecord ? `🎉 NEW RECORD! 🎉 A new record has been set, at ${sta - - ${stats.isNewRecord ? `` : ''} + + ${stats.isNewRecord ? `` : ""}
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!
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(''), + `.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)); @@ -170,7 +170,7 @@ ${stats.isNewRecord ? `🎉 NEW RECORD! 🎉 A new record has been set, at ${sta 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: { + method: "PUT", body: JSON.stringify(message, null, 2), headers: { "Authorization": `Bearer ${process.env.MATRIX_ACCESS_TOKEN}`, "Content-Type": "application/json" } }); @@ -190,10 +190,16 @@ async function sendSummaryDiscord(yesterday, stats) { 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) ], + 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 diff --git a/timespan.test.js b/timeSpan.test.js similarity index 85% rename from timespan.test.js rename to timeSpan.test.js index 2388b16..dbf0f37 100644 --- a/timespan.test.js +++ b/timeSpan.test.js @@ -1,21 +1,21 @@ // noinspection ES6ConvertRequireIntoImport - this is CJS, not MJS -const { it } = require("node:test"); +const { test } = require("node:test"); const assert = require("node:assert/strict"); const { TimeSpan } = require("./timeSpan"); -it("should be able to return zero", () => { +test("TimeSpan should be able to return zero", () => { const ts = new TimeSpan(); assert.equal(ts.totalMillis, 0); }); -it("should be able to return timespan from milliseconds", () => { +test("TimeSpan should be able to return timespan from milliseconds", () => { const ts = TimeSpan.fromMillis(1000); assert.equal(ts.totalMillis, 1000); assert.equal(ts.totalSeconds, 1); }); -it("should be able to return timespan from seconds", () => { +test("TimeSpan should be able to return timespan from seconds", () => { const ts = TimeSpan.fromSeconds(60); assert.equal(ts.totalMillis, 60000); assert.equal(ts.totalSeconds, 60); @@ -25,8 +25,8 @@ it("should be able to return timespan from seconds", () => { assert.equal(ts.days, 0); }); -it("should be pure", () => { - const count = 1000; +test("TimeSpan should be pure", () => { + const count = 10; const timestamps = []; for (let i = 0; i < count; i++) { timestamps.push(TimeSpan.fromMillis(8972347984)); @@ -53,7 +53,7 @@ it("should be pure", () => { } }); -it("should be able to stringify", () => { +test("TimeSpan should be able to stringify", () => { const ts = TimeSpan.fromMillis(8972347984); assert.equal(ts.toString(), "3 months, 1 weeks, 5 days, 20 hours, 19 minutes, 7 seconds, 984 milliseconds"); assert.equal(ts.toString(true), "3 months, 1 weeks, 5 days, 20 hours, 19 minutes, 7 seconds, 984 milliseconds"); @@ -62,7 +62,7 @@ it("should be able to stringify", () => { assert.equal(ts.toString(false, false), "3 months, 12 days, 20 hours, 19 minutes, 7 seconds"); }); -it("should be able to shortStringify", () => { +test("TimeSpan should be able to shortStringify", () => { const ts = TimeSpan.fromMillis(8972347984); assert.equal(ts.toShortString(), "3mo1w5d20h19m7s984ms"); assert.equal(ts.toShortString(true), "3mo1w5d20h19m7s984ms"); @@ -71,7 +71,7 @@ it("should be able to shortStringify", () => { assert.equal(ts.toShortString(false, false), "3mo12d20h19m7s"); }); -it("should be able to shortStringify with spaces", () => { +test("TimeSpan should be able to shortStringify with spaces", () => { const ts = TimeSpan.fromMillis(8972347984); assert.equal(ts.toShortString(undefined, undefined, true), "3mo 1w 5d 20h 19m 7s 984ms"); assert.equal(ts.toShortString(true, undefined, true), "3mo 1w 5d 20h 19m 7s 984ms");