forked from LiteNet/freepbx-stats
h
This commit is contained in:
parent
fb006a3c46
commit
617e4605be
|
@ -11,3 +11,4 @@ DATABASE_NAME = asteriskcdrdb
|
|||
#RUN_ONCE=true
|
||||
#DATABASE_LOG_TIMINGS=true
|
||||
#LOG_MESSAGES=true
|
||||
#NO_SAVE_RECORDS=true
|
||||
|
|
|
@ -20,7 +20,7 @@ export class CallStats {
|
|||
*/
|
||||
totalCallsMade;
|
||||
/**
|
||||
* @type {string}
|
||||
* @type {CallRecord}
|
||||
*/
|
||||
allTimeRecord;
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
});
|
88
index.js
88
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": `
|
||||
<table>
|
||||
<thead>
|
||||
|
@ -148,15 +148,15 @@ ${stats.isNewRecord ? `🎉 NEW RECORD! 🎉 A new record has been set, at ${sta
|
|||
<tr><td>Monthly total</td><td>${stats.totalCallsThisMonth}</td></tr>
|
||||
<tr><td>Total calls ever placed</td><td>${stats.totalCallsEverPlaced}</td></tr>
|
||||
<tr><td>System uptime</td><td>${getSystemUptime().toString(false, false)}</td></tr>
|
||||
<tr><td>All-time record</td><td>${stats.allTimeRecord}</td></tr>
|
||||
${stats.isNewRecord ? `<tr><td colspan="2"><span data-mx-color="#FFD700"><font color="#FFD700">🎉 NEW RECORD! 🎉</font></span> A new record has been set, at ${stats.totalCallsMade} calls in a day!</td></tr>` : ''}
|
||||
<tr><td>All-time record</td><td>${stats.allTimeRecord.count} calls on ${stats.allTimeRecord.date}</td></tr>
|
||||
${stats.isNewRecord ? `<tr><td colspan="2"><span data-mx-color="#FFD700"><font color="#FFD700">🎉 NEW RECORD! 🎉</font></span> A new record has been set, at ${stats.totalCallsMade} calls in a day!</td></tr>` : ""}
|
||||
</tbody>
|
||||
</table>
|
||||
`.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 <t:${Math.floor(yesterday.start / 1000)}:f> to <t:${Math.floor(yesterday.end / 1000)}:f>`,
|
||||
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
|
||||
|
|
|
@ -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");
|
Loading…
Reference in a new issue