Some further work?

This commit is contained in:
Rory& 2025-09-20 19:19:07 +02:00
parent f9d6e977e9
commit 36fc4c4a84
7 changed files with 294 additions and 136 deletions

13
callStats.js Normal file
View file

@ -0,0 +1,13 @@
export class CallStats {
/**
* @param {CallStats} stats
*/
constructor(stats) {
}
totalCallsThisMonth;
totalCallsEverPlaced;
totalCallsMade;
allTimeRecord;
isNewRecord = false;
}

69
dateBuilder.js Normal file
View file

@ -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
}
}

0
dateBuilder.test.js Normal file
View file

246
index.js
View file

@ -2,152 +2,156 @@ require("dotenv").config();
const cron = require("node-cron"); const cron = require("node-cron");
const os = require("os"); const os = require("os");
const Discord = require('discord.js'); const Discord = require('discord.js');
const { connect } = require("http2");
const mysql = require('mysql'); 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 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 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() { function getStartOfYesterdayTimestamp() {
const today = new Date(); const today = new Date();
// Set the date to yesterday // Set the date to yesterday
today.setDate(today.getDate() - 1); today.setDate(today.getDate() - 1);
// Create a new Date object for the start of yesterday // Create a new Date object for the start of yesterday
const startOfYesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); const startOfYesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
return startOfYesterday.getTime(); // Returns the timestamp in milliseconds 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);
} }
/**
* Fetch call statistics
* @returns {Promise<CallStats>}
*/
async function getPreviousDayData() { async function getPreviousDayData() {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const previousDay = new Date(getStartOfYesterdayTimestamp()) // 24 hours ago const yesterday = getYesterday();
const startTime = new Date(previousDay.setHours(0, 0, 0, 0)); const connection = await mysql.createConnection({
const endTime = new Date(previousDay.setHours(23, 59, 59, 999)); host: process.env.DATABASE_HOST,
const connection = await mysql.createConnection({ user: process.env.DATABASE_USER,
host: process.env.DATABASE_HOST, password: process.env.DATABASE_PASSWORD,
user: process.env.DATABASE_USER, database: process.env.DATABASE_NAME,
password: process.env.DATABASE_PASSWORD, multipleStatements: true
database: process.env.DATABASE_NAME, });
multipleStatements: true await connection.connect();
}); await connection.query(`
await connection.connect(); SELECT COUNT(DISTINCT uniqueid) AS call_count
let callsMade; FROM cdr
let recordForToday; WHERE calldate BETWEEN ? AND ?;
let monthlyTotal; SELECT COUNT(DISTINCT uniqueid) AS call_count
let totalCalls; FROM cdr
await connection.query(` WHERE MONTH (calldate) = MONTH (?) AND YEAR (calldate) = YEAR (?);
SELECT COUNT(DISTINCT uniqueid) AS call_count SELECT COUNT(DISTINCT uniqueid) AS call_count
FROM cdr FROM cdr;
WHERE calldate BETWEEN ? AND ?; `, [ yesterday.start, yesterday.end, yesterday.start, yesterday.start ], (err, res) => {
SELECT COUNT(DISTINCT uniqueid) AS call_count if (err) {
FROM cdr reject(err);
WHERE MONTH (calldate) = MONTH (?) AND YEAR (calldate) = YEAR (?); }
SELECT COUNT(DISTINCT uniqueid) AS call_count connection.end();
FROM cdr; // let output = {
`, [ startTime, endTime, previousDay, previousDay ], (err, res) => { // "Calls Made": res[0][0].call_count,
if (err) { // "Monthly Total": res[1][0].call_count,
reject(err); // "Total Calls Ever Placed": res[2][0].call_count,
} // "System Uptime": getSystemUptime().toString(false, false),
connection.end(); // "All Time Record": null, // Placeholder
let output = { // }
"Calls Made": res[0][0].call_count,
"Monthly Total": res[1][0].call_count, const stats = new CallStats({
"Total Calls Ever Placed": res[2][0].call_count, callsMadeToday: res[0][0].call_count,
"System Uptime": getSystemUptime(), totalCallsThisMonth: res[1][0].call_count,
"All Time Record": null, // Placeholder totalCallsEverPlaced: res[2][0].call_count,
} allTimeRecord: null // Placeholder
console.log(output); });
resolve(output);
}); console.log(stats);
}); resolve(stats);
});
});
} }
function getSystemUptime() { function getSystemUptime() {
const uptime = os.uptime(); const uptime = os.uptime();
const days = Math.floor(uptime / 86400); const now = new Date();
const hours = Math.floor((uptime % 86400) / 3600); return new TimeSpan(now - (uptime * 1000), now.getTime());
const minutes = Math.floor((uptime % 3600) / 60);
const seconds = Math.floor(uptime % 60);
return `${days} days, ${hours} hours, ${minutes} minutes, ${seconds} seconds`;
} }
function updateRecords(data, root) { /**
const currentDate = new Date(getStartOfYesterdayTimestamp()).toISOString().split('T')[0]; * Update records with new data
const month = currentDate.slice(0, 7); * @param {CallStats} callStats
let isNewRecord = false; * @param {Records} records
* @returns {CallStats}
*/
function updateRecords(callStats, records) {
const yesterday = getYesterday().startDate;
let isNewRecord = false;
// Update all-time record // Update all-time record
const allTimeRecord = root.records.record_calls || { date: currentDate, count: 0 }; const allTimeRecord = records.callRecord || new CallRecord({ date: yesterday, count: 0 });
if (!root.records.record_calls) { if (!records.callRecord) {
root.records.record_calls = { date: currentDate, count: data["Calls Made"] }; records.callRecord = { date: currentDate, count: callStats.totalCallsMade };
isNewRecord = true; isNewRecord = true;
} else if (parseInt(allTimeRecord.count) < data["Calls Made"]) { } else if (parseInt(allTimeRecord.count) < callStats.totalCallsThisMonth) {
allTimeRecord.count = data["Calls Made"]; allTimeRecord.count = callStats.totalCallsThisMonth;
isNewRecord = true; isNewRecord = true;
} }
data["All Time Record"] = `${allTimeRecord.count} calls on ${allTimeRecord.date}`; callStats.allTimeRecord = `${allTimeRecord.count} calls on ${allTimeRecord.date}`;
// Update total calls ever placed // Update total calls ever placed
root.records.total_calls_ever_placed = data["Total Calls Ever Placed"]; records.totalCallsEverPlaced = callStats.totalCallsEverPlaced;
// Update monthly total // Update monthly totals
root.records[`monthly_total_${month}`] = data["Monthly Total"]; records.monthlyTotals[yesterday.getFullYear().toString()][yesterday.getMonth().toString()] = callStats.totalCallsThisMonth;
if (isNewRecord) { if (isNewRecord) {
data["NEW RECORD"] = true; callStats.isNewRecord = true;
} }
return data; return callStats;
} }
async function sendSummary() { async function sendSummary() {
console.log("Preparing summary."); console.log("Preparing summary.");
const data = await getPreviousDayData(); const data = await getPreviousDayData();
console.log("Loading records."); console.log("Updating records...");
const root = await loadRecords(); const updatedData = await updateRecords(data, records);
console.log("Updating records..."); console.log("Saving.");
const updatedData = await updateRecords(data, root); await records.toJSONFile(JSON_FILE);
console.log("Saving.");
await saveRecords(root);
const previousDayStart = new Date(getStartOfYesterdayTimestamp()); const yesterday = getYesterday();
const previousDayEnd = new Date(previousDayStart);
previousDayEnd.setHours(23, 59, 59, 999);
let embed = { let embed = {
title: `Summary from <t:${Math.floor(previousDayStart / 1000)}:f> to <t:${Math.floor(previousDayEnd / 1000)}:f>`, title: `Summary from <t:${Math.floor(yesterday.start / 1000)}:f> to <t:${Math.floor(yesterday.end / 1000)}:f>`,
color: 0x1E90FF, color: 0x1E90FF,
fields: [], fields: [],
timestamp: new Date(), timestamp: new Date(),
footer: {} footer: {}
} }
for (const [ key, value ] of Object.entries(updatedData)) { for (const [ key, value ] of Object.entries(updatedData)) {
if (key === "NEW RECORD") { if (key === "NEW RECORD") {
embed.fields.push({ name: "NEW RECORD!", value: "A new record has been set!", inline: false }); embed.fields.push({ name: "NEW RECORD!", value: "A new record has been set!", inline: false });
} else { } else {
embed.fields.push({ name: key, value: value, inline: false }); embed.fields.push({ name: key, value: value, inline: false });
} }
} }
console.log("Sending message."); const payload = { embeds: [ embed ] };
await hook.send({ embeds: [ embed ] }); console.log("Sending Discord message:", payload);
if (hook)
await hook.send(payload);
} }
if (process.env.NOOP) {
sendSummary();
return;
}
console.log("Scheduling..."); console.log("Scheduling...");
const schedule = cron.schedule("0 1 * * *", sendSummary); const schedule = cron.schedule("0 1 * * *", sendSummary);

72
records.js Normal file
View file

@ -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));
}

View file

@ -1,7 +1,7 @@
/** /**
* Represents a timespan with a start and end time. * Represents a timespan with a start and end time.
*/ */
export class Timespan { export class TimeSpan {
// constructors // constructors
constructor(start = Date.now(), end = Date.now()) { constructor(start = Date.now(), end = Date.now()) {
if (start > end) { if (start > end) {
@ -12,15 +12,15 @@ export class Timespan {
} }
static fromDates(startDate, endDate) { static fromDates(startDate, endDate) {
return new Timespan(startDate, endDate); return new TimeSpan(startDate, endDate);
} }
static fromMillis(millis) { static fromMillis(millis) {
return new Timespan(0, millis); return new TimeSpan(0, millis);
} }
static fromSeconds(seconds) { static fromSeconds(seconds) {
return Timespan.fromMillis(seconds * 1000); return TimeSpan.fromMillis(seconds * 1000);
} }
// methods // methods
@ -118,11 +118,11 @@ export class Timespan {
return parts.join(withSpaces ? " " : ""); return parts.join(withSpaces ? " " : "");
} }
get startTime() { get startDate() {
return new Date(this.start); return new Date(this.start);
} }
get endTime() { get endDate() {
return new Date(this.end); return new Date(this.end);
} }

View file

@ -2,21 +2,21 @@
const { it } = require("node:test"); const { it } = require("node:test");
const assert = require("node:assert/strict"); const assert = require("node:assert/strict");
const { Timespan } = require("./timespan"); const { TimeSpan } = require("./timeSpan");
it("should be able to return zero", () => { it("should be able to return zero", () => {
const ts = new Timespan(); const ts = new TimeSpan();
assert.equal(ts.totalMillis, 0); assert.equal(ts.totalMillis, 0);
}); });
it("should be able to return timespan from milliseconds", () => { 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.totalMillis, 1000);
assert.equal(ts.totalSeconds, 1); assert.equal(ts.totalSeconds, 1);
}); });
it("should be able to return timespan from seconds", () => { 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.totalMillis, 60000);
assert.equal(ts.totalSeconds, 60); assert.equal(ts.totalSeconds, 60);
assert.equal(ts.totalMinutes, 1); assert.equal(ts.totalMinutes, 1);
@ -29,7 +29,7 @@ it("should be pure", () => {
const count = 1000; const count = 1000;
const timestamps = []; const timestamps = [];
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
timestamps.push(Timespan.fromMillis(8972347984)); timestamps.push(TimeSpan.fromMillis(8972347984));
for (const ts2 of timestamps) { for (const ts2 of timestamps) {
assert.equal(ts2.totalMillis, 8972347984); assert.equal(ts2.totalMillis, 8972347984);
assert.equal(ts2.totalSeconds, 8972347); assert.equal(ts2.totalSeconds, 8972347);
@ -54,7 +54,7 @@ it("should be pure", () => {
}); });
it("should be able to stringify", () => { 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(), "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), "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"); 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", () => { 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(), "3mo1w5d20h19m7s984ms");
assert.equal(ts.toShortString(true), "3mo1w5d20h19m7s984ms"); assert.equal(ts.toShortString(true), "3mo1w5d20h19m7s984ms");
assert.equal(ts.toShortString(true, false), "3mo1w5d20h19m7s"); 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", () => { 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(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, undefined, true), "3mo 1w 5d 20h 19m 7s 984ms");
assert.equal(ts.toShortString(true, false, true), "3mo 1w 5d 20h 19m 7s"); assert.equal(ts.toShortString(true, false, true), "3mo 1w 5d 20h 19m 7s");