From 36fc4c4a8417f8eb1e7937629cb8bea23979dbe7 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 20 Sep 2025 19:19:07 +0200 Subject: [PATCH] Some further work? --- callStats.js | 13 ++ dateBuilder.js | 69 +++++++++++ dateBuilder.test.js | 0 index.js | 248 +++++++++++++++++++------------------ records.js | 72 +++++++++++ timespan.js => timeSpan.js | 12 +- timespan.test.js | 16 +-- 7 files changed, 294 insertions(+), 136 deletions(-) create mode 100644 callStats.js create mode 100644 dateBuilder.js create mode 100644 dateBuilder.test.js create mode 100644 records.js rename timespan.js => timeSpan.js (94%) diff --git a/callStats.js b/callStats.js new file mode 100644 index 0000000..ea925a8 --- /dev/null +++ b/callStats.js @@ -0,0 +1,13 @@ +export class CallStats { + /** + * @param {CallStats} stats + */ + constructor(stats) { + } + + totalCallsThisMonth; + totalCallsEverPlaced; + totalCallsMade; + allTimeRecord; + isNewRecord = false; +} \ No newline at end of file diff --git a/dateBuilder.js b/dateBuilder.js new file mode 100644 index 0000000..c9c247a --- /dev/null +++ b/dateBuilder.js @@ -0,0 +1,69 @@ +export class DateBuilder { + // constructors + constructor(date = new Date()) { + if (!(date instanceof Date)) { + throw new Error("Invalid date object."); + } + this.date = new Date(date.getTime()); // Create a copy to avoid mutating the original date + } + + // methods + addYears(years) { + this.date.setFullYear(this.date.getFullYear() + years); + return this; + } + + addMonths(months) { + this.date.setMonth(this.date.getMonth() + months); + return this; + } + + addDays(days) { + this.date.setDate(this.date.getDate() + days); + return this; + } + + addHours(hours) { + this.date.setHours(this.date.getHours() + hours); + return this; + } + + addMinutes(minutes) { + this.date.setMinutes(this.date.getMinutes() + minutes); + return this; + } + + addSeconds(seconds) { + this.date.setSeconds(this.date.getSeconds() + seconds); + return this; + } + + addMillis(millis) { + this.date.setTime(this.date.getTime() + millis); + return this; + } + + withDate(year, month, day) { + this.date.setFullYear(year, month - 1, day); // month is 0-based + return this; + } + + withTime(hour, minute = 0, second = 0, millisecond = 0) { + this.date.setHours(hour, minute, second, millisecond); + return this; + } + + atStartOfDay() { + this.date.setHours(0, 0, 0, 0); + return this; + } + + atEndOfDay() { + this.date.setHours(23, 59, 59, 999); + return this; + } + + build() { + return new Date(this.date.getTime()); // Return a copy to avoid external mutation + } +} \ No newline at end of file diff --git a/dateBuilder.test.js b/dateBuilder.test.js new file mode 100644 index 0000000..e69de29 diff --git a/index.js b/index.js index 3158a41..99c0d94 100644 --- a/index.js +++ b/index.js @@ -2,152 +2,156 @@ require("dotenv").config(); const cron = require("node-cron"); const os = require("os"); const Discord = require('discord.js'); -const { connect } = require("http2"); const mysql = require('mysql'); +const { TimeSpan } = require("./timeSpan"); +const { DateBuilder } = require("./dateBuilder"); +const { CallStats } = require("./callStats"); +const { CallRecord, Records } = require("./records"); const fs = require('fs').promises; -const hook = new Discord.WebhookClient({ url: process.env.DISCORD_WEBHOOK_URL }); +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 = Records.fromJSONFile(JSON_FILE); + +function getYesterday() { + return new TimeSpan( + new DateBuilder().addDays(-1).atStartOfDay().build().getTime(), + new DateBuilder().addDays(-1).atEndOfDay().build().getTime() + ); +} function getStartOfYesterdayTimestamp() { - const today = new Date(); - // Set the date to yesterday - today.setDate(today.getDate() - 1); - // Create a new Date object for the start of yesterday - const startOfYesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); - return startOfYesterday.getTime(); // Returns the timestamp in milliseconds -} - -async function loadRecords() { - try { - const data = await fs.readFile(JSON_FILE, 'utf-8'); - return JSON.parse(data); - } catch (error) { - if (error.code === 'ENOENT') { - return { records: {} }; // Return empty records if file doesn't exist - } - throw error; - } -} - -async function saveRecords(root) { - const json = JSON.stringify(root, null, 2); - await fs.writeFile(JSON_FILE, json); + const today = new Date(); + // Set the date to yesterday + today.setDate(today.getDate() - 1); + // Create a new Date object for the start of yesterday + const startOfYesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + return startOfYesterday.getTime(); // Returns the timestamp in milliseconds } +/** + * Fetch call statistics + * @returns {Promise} + */ async function getPreviousDayData() { - return new Promise(async (resolve, reject) => { - const previousDay = new Date(getStartOfYesterdayTimestamp()) // 24 hours ago - const startTime = new Date(previousDay.setHours(0, 0, 0, 0)); - const endTime = new Date(previousDay.setHours(23, 59, 59, 999)); - 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, - multipleStatements: true - }); - await connection.connect(); - let callsMade; - let recordForToday; - let monthlyTotal; - let totalCalls; - await connection.query(` - SELECT COUNT(DISTINCT uniqueid) AS call_count - FROM cdr - WHERE calldate BETWEEN ? AND ?; - SELECT COUNT(DISTINCT uniqueid) AS call_count - FROM cdr - WHERE MONTH (calldate) = MONTH (?) AND YEAR (calldate) = YEAR (?); - SELECT COUNT(DISTINCT uniqueid) AS call_count - FROM cdr; - `, [ startTime, endTime, previousDay, previousDay ], (err, res) => { - if (err) { - reject(err); - } - connection.end(); - let output = { - "Calls Made": res[0][0].call_count, - "Monthly Total": res[1][0].call_count, - "Total Calls Ever Placed": res[2][0].call_count, - "System Uptime": getSystemUptime(), - "All Time Record": null, // Placeholder - } - console.log(output); - resolve(output); - }); - }); + return new Promise(async (resolve, reject) => { + const yesterday = getYesterday(); + 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, + multipleStatements: true + }); + await connection.connect(); + await connection.query(` + SELECT COUNT(DISTINCT uniqueid) AS call_count + FROM cdr + WHERE calldate BETWEEN ? AND ?; + SELECT COUNT(DISTINCT uniqueid) AS call_count + FROM cdr + WHERE MONTH (calldate) = MONTH (?) AND YEAR (calldate) = YEAR (?); + SELECT COUNT(DISTINCT uniqueid) AS call_count + FROM cdr; + `, [ yesterday.start, yesterday.end, yesterday.start, yesterday.start ], (err, res) => { + if (err) { + reject(err); + } + connection.end(); + // let output = { + // "Calls Made": res[0][0].call_count, + // "Monthly Total": res[1][0].call_count, + // "Total Calls Ever Placed": res[2][0].call_count, + // "System Uptime": getSystemUptime().toString(false, false), + // "All Time Record": null, // Placeholder + // } + + const stats = new CallStats({ + callsMadeToday: res[0][0].call_count, + totalCallsThisMonth: res[1][0].call_count, + totalCallsEverPlaced: res[2][0].call_count, + allTimeRecord: null // Placeholder + }); + + console.log(stats); + resolve(stats); + }); + }); } function getSystemUptime() { - const uptime = os.uptime(); - const days = Math.floor(uptime / 86400); - const hours = Math.floor((uptime % 86400) / 3600); - const minutes = Math.floor((uptime % 3600) / 60); - const seconds = Math.floor(uptime % 60); - return `${days} days, ${hours} hours, ${minutes} minutes, ${seconds} seconds`; + const uptime = os.uptime(); + const now = new Date(); + return new TimeSpan(now - (uptime * 1000), now.getTime()); } -function updateRecords(data, root) { - const currentDate = new Date(getStartOfYesterdayTimestamp()).toISOString().split('T')[0]; - const month = currentDate.slice(0, 7); - let isNewRecord = false; +/** + * Update records with new data + * @param {CallStats} callStats + * @param {Records} records + * @returns {CallStats} + */ +function updateRecords(callStats, records) { + const yesterday = getYesterday().startDate; + let isNewRecord = false; - // Update all-time record - const allTimeRecord = root.records.record_calls || { date: currentDate, count: 0 }; - if (!root.records.record_calls) { - root.records.record_calls = { date: currentDate, count: data["Calls Made"] }; - isNewRecord = true; - } else if (parseInt(allTimeRecord.count) < data["Calls Made"]) { - allTimeRecord.count = data["Calls Made"]; - isNewRecord = true; - } - data["All Time Record"] = `${allTimeRecord.count} calls on ${allTimeRecord.date}`; + // Update all-time record + const allTimeRecord = records.callRecord || new CallRecord({ date: yesterday, count: 0 }); + if (!records.callRecord) { + records.callRecord = { date: currentDate, count: callStats.totalCallsMade }; + isNewRecord = true; + } else if (parseInt(allTimeRecord.count) < callStats.totalCallsThisMonth) { + allTimeRecord.count = callStats.totalCallsThisMonth; + isNewRecord = true; + } + callStats.allTimeRecord = `${allTimeRecord.count} calls on ${allTimeRecord.date}`; - // Update total calls ever placed - root.records.total_calls_ever_placed = data["Total Calls Ever Placed"]; + // Update total calls ever placed + records.totalCallsEverPlaced = callStats.totalCallsEverPlaced; - // Update monthly total - root.records[`monthly_total_${month}`] = data["Monthly Total"]; + // Update monthly totals + records.monthlyTotals[yesterday.getFullYear().toString()][yesterday.getMonth().toString()] = callStats.totalCallsThisMonth; - if (isNewRecord) { - data["NEW RECORD"] = true; - } - return data; + if (isNewRecord) { + callStats.isNewRecord = true; + } + return callStats; } async function sendSummary() { - console.log("Preparing summary."); - const data = await getPreviousDayData(); - console.log("Loading records."); - const root = await loadRecords(); - console.log("Updating records..."); - const updatedData = await updateRecords(data, root); - console.log("Saving."); - await saveRecords(root); + console.log("Preparing summary."); + const data = await getPreviousDayData(); + console.log("Updating records..."); + const updatedData = await updateRecords(data, records); + console.log("Saving."); + await records.toJSONFile(JSON_FILE); - const previousDayStart = new Date(getStartOfYesterdayTimestamp()); - const previousDayEnd = new Date(previousDayStart); - previousDayEnd.setHours(23, 59, 59, 999); + const yesterday = getYesterday(); - let embed = { - title: `Summary from to `, - color: 0x1E90FF, - fields: [], - timestamp: new Date(), - footer: {} - } - for (const [ key, value ] of Object.entries(updatedData)) { - if (key === "NEW RECORD") { - embed.fields.push({ name: "NEW RECORD!", value: "A new record has been set!", inline: false }); - } else { - embed.fields.push({ name: key, value: value, inline: false }); - } - } - console.log("Sending message."); - await hook.send({ embeds: [ embed ] }); + let embed = { + title: `Summary from to `, + color: 0x1E90FF, + fields: [], + timestamp: new Date(), + footer: {} + } + for (const [ key, value ] of Object.entries(updatedData)) { + if (key === "NEW RECORD") { + embed.fields.push({ name: "NEW RECORD!", value: "A new record has been set!", inline: false }); + } else { + embed.fields.push({ name: key, value: value, inline: false }); + } + } + const payload = { embeds: [ embed ] }; + console.log("Sending Discord message:", payload); + if (hook) + await hook.send(payload); } +if (process.env.NOOP) { + sendSummary(); + return; +} console.log("Scheduling..."); -const schedule = cron.schedule("0 1 * * *", sendSummary); \ No newline at end of file +const schedule = cron.schedule("0 1 * * *", sendSummary); diff --git a/records.js b/records.js new file mode 100644 index 0000000..1b2f77f --- /dev/null +++ b/records.js @@ -0,0 +1,72 @@ +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; + } + + static fromJSONFile(path) { + const data = fs.readFileSync(path, "utf-8"); + const obj = JSON.parse(data); + + return new Records(obj); + } + + toJSONFile(path) { + const data = JSON.stringify(this, null, 2); + fs.writeFileSync(path, data, "utf-8"); + } + + /** + * @type {CallRecord} + */ + callRecord; + /** + * @type {number} + */ + totalCallsEverPlaced; + /** + * @type {{[year: string]: {[month: string]: number}}} + */ + monthlyTotals; +} + +export class CallRecord { + constructor(date, count) { + this.date = date; + this.count = count; + } + + /** + * @type {string} YYYY-MM-DD + */ + date; + /** + * @type {number} + */ + count; +} + +// test (mjs) +if (import.meta.url === `file://${process.argv[1]}`) { + const records = Records.fromJSONFile("records.json"); + console.log(records); + console.log(JSON.stringify(records, null, 2)); +} \ No newline at end of file diff --git a/timespan.js b/timeSpan.js similarity index 94% rename from timespan.js rename to timeSpan.js index 5963840..e0cc8b0 100644 --- a/timespan.js +++ b/timeSpan.js @@ -1,7 +1,7 @@ /** * Represents a timespan with a start and end time. */ -export class Timespan { +export class TimeSpan { // constructors constructor(start = Date.now(), end = Date.now()) { if (start > end) { @@ -12,15 +12,15 @@ export class Timespan { } static fromDates(startDate, endDate) { - return new Timespan(startDate, endDate); + return new TimeSpan(startDate, endDate); } static fromMillis(millis) { - return new Timespan(0, millis); + return new TimeSpan(0, millis); } static fromSeconds(seconds) { - return Timespan.fromMillis(seconds * 1000); + return TimeSpan.fromMillis(seconds * 1000); } // methods @@ -118,11 +118,11 @@ export class Timespan { return parts.join(withSpaces ? " " : ""); } - get startTime() { + get startDate() { return new Date(this.start); } - get endTime() { + get endDate() { return new Date(this.end); } diff --git a/timespan.test.js b/timespan.test.js index 00a7a1f..2388b16 100644 --- a/timespan.test.js +++ b/timespan.test.js @@ -2,21 +2,21 @@ const { it } = require("node:test"); const assert = require("node:assert/strict"); -const { Timespan } = require("./timespan"); +const { TimeSpan } = require("./timeSpan"); it("should be able to return zero", () => { - const ts = new Timespan(); + const ts = new TimeSpan(); assert.equal(ts.totalMillis, 0); }); it("should be able to return timespan from milliseconds", () => { - const ts = Timespan.fromMillis(1000); + const ts = TimeSpan.fromMillis(1000); assert.equal(ts.totalMillis, 1000); assert.equal(ts.totalSeconds, 1); }); it("should be able to return timespan from seconds", () => { - const ts = Timespan.fromSeconds(60); + const ts = TimeSpan.fromSeconds(60); assert.equal(ts.totalMillis, 60000); assert.equal(ts.totalSeconds, 60); assert.equal(ts.totalMinutes, 1); @@ -29,7 +29,7 @@ it("should be pure", () => { const count = 1000; const timestamps = []; for (let i = 0; i < count; i++) { - timestamps.push(Timespan.fromMillis(8972347984)); + timestamps.push(TimeSpan.fromMillis(8972347984)); for (const ts2 of timestamps) { assert.equal(ts2.totalMillis, 8972347984); assert.equal(ts2.totalSeconds, 8972347); @@ -54,7 +54,7 @@ it("should be pure", () => { }); it("should be able to stringify", () => { - const ts = Timespan.fromMillis(8972347984); + 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"); assert.equal(ts.toString(true, false), "3 months, 1 weeks, 5 days, 20 hours, 19 minutes, 7 seconds"); @@ -63,7 +63,7 @@ it("should be able to stringify", () => { }); it("should be able to shortStringify", () => { - const ts = Timespan.fromMillis(8972347984); + const ts = TimeSpan.fromMillis(8972347984); assert.equal(ts.toShortString(), "3mo1w5d20h19m7s984ms"); assert.equal(ts.toShortString(true), "3mo1w5d20h19m7s984ms"); assert.equal(ts.toShortString(true, false), "3mo1w5d20h19m7s"); @@ -72,7 +72,7 @@ it("should be able to shortStringify", () => { }); it("should be able to shortStringify with spaces", () => { - const ts = Timespan.fromMillis(8972347984); + 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"); assert.equal(ts.toShortString(true, false, true), "3mo 1w 5d 20h 19m 7s");