it works!!!

This commit is contained in:
Rory& 2025-09-20 21:56:06 +02:00
parent cd9b914455
commit a15e296918
4 changed files with 170 additions and 181 deletions

View file

@ -1,5 +1,13 @@
MATRIX_BASE_URL =
MATRIX_ACCESS_TOKEN =
MATRIX_ROOM_ID =
DISCORD_WEBHOOK_URL = DISCORD_WEBHOOK_URL =
DATABASE_USER = statsBot DATABASE_USER = statsBot
DATABASE_PASSWORD = statsBot DATABASE_PASSWORD = statsBot
DATABASE_HOST = 127.0.0.1 DATABASE_HOST = 127.0.0.1
DATABASE_NAME = asteriskcdrdb DATABASE_NAME = asteriskcdrdb
# Extra debugging options:
#NOOP=true
#RUN_ONCE=true
#DATABASE_LOG_TIMINGS=true
#LOG_MESSAGES=true

View file

@ -7,9 +7,24 @@ export class CallStats {
this[key] = value; this[key] = value;
} }
/**
* @type {number}
*/
totalCallsThisMonth; totalCallsThisMonth;
/**
* @type {number}
*/
totalCallsEverPlaced; totalCallsEverPlaced;
/**
* @type {number}
*/
totalCallsMade; totalCallsMade;
/**
* @type {string}
*/
allTimeRecord; allTimeRecord;
/**
* @type {boolean}
*/
isNewRecord = false; isNewRecord = false;
} }

296
index.js
View file

@ -7,7 +7,6 @@ const { TimeSpan } = require("./timeSpan");
const { DateBuilder } = require("./dateBuilder"); const { DateBuilder } = require("./dateBuilder");
const { CallStats } = require("./callStats"); const { CallStats } = require("./callStats");
const { CallRecord, Records } = require("./records"); const { CallRecord, Records } = require("./records");
const fs = require('fs').promises;
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; 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(); const records = fsSync.existsSync(JSON_FILE) ? Records.fromJSONFile(JSON_FILE) : new Records();
function getYesterday() { function getYesterday() {
return new TimeSpan( return new TimeSpan(new DateBuilder().addDays(-1).atStartOfDay().build().getTime(), new DateBuilder().addDays(-1).atEndOfDay().build().getTime());
new DateBuilder().addDays(-1).atStartOfDay().build().getTime(),
new DateBuilder().addDays(-1).atEndOfDay().build().getTime()
);
} }
/** /**
* @param {string} query * @param {string} query
* @param {any} params * @param {any} params
* @returns {Promise<int>} * @returns {Promise<int>}
*/ */
async function queryScalarAsync(query, ...params) { async function queryScalarAsync(query, ...params) {
const start = Date.now(); const start = Date.now();
const connection = await mysql.createConnection({ const connection = await mysql.createConnection({
host: process.env.DATABASE_HOST, host: process.env.DATABASE_HOST,
user: process.env.DATABASE_USER, user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD, password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME database: process.env.DATABASE_NAME
}); });
await connection.connect(); await connection.connect();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query(query, params, (err, results) => { connection.query(query, params, (err, results) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
if (process.env.DATABASE_LOG_TIMINGS) console.log(`Query took ${Date.now() - start}ms:`, query, params, "=>", results); 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]]); resolve(results[0][Object.keys(results[0])[0]]);
} }
connection.end(); connection.end();
}); });
}); });
} }
/** /**
@ -55,35 +50,29 @@ async function queryScalarAsync(query, ...params) {
* @returns {Promise<CallStats>} * @returns {Promise<CallStats>}
*/ */
async function getPreviousDayData() { async function getPreviousDayData() {
const [ callsYesterday, callsThisMonth, callsTotal ] = await Promise.all([ const [ callsYesterday, callsThisMonth, callsTotal ] = await Promise.all([ queryScalarAsync(`
queryScalarAsync(` SELECT COUNT(DISTINCT uniqueid) AS call_count
SELECT COUNT(DISTINCT uniqueid) AS call_count FROM cdr
FROM cdr WHERE calldate BETWEEN ? AND ?;
WHERE calldate BETWEEN ? AND ?; `, getYesterday().startDate, getYesterday().endDate), queryScalarAsync(`
`, getYesterday().startDate, getYesterday().endDate), SELECT COUNT(DISTINCT uniqueid) AS call_count
queryScalarAsync(` FROM cdr
SELECT COUNT(DISTINCT uniqueid) AS call_count WHERE MONTH (calldate) = ? AND YEAR (calldate) = ?;
FROM cdr `, getYesterday().startDate.getMonth(), getYesterday().startDate.getFullYear()), queryScalarAsync(`
WHERE MONTH (calldate) = ? AND YEAR (calldate) = ?; SELECT COUNT(DISTINCT uniqueid) AS call_count
`, getYesterday().startDate.getMonth(), getYesterday().startDate.getFullYear()), FROM cdr;
queryScalarAsync(` `) ]);
SELECT COUNT(DISTINCT uniqueid) AS call_count const stats = new CallStats({
FROM cdr; totalCallsMade: callsYesterday, totalCallsThisMonth: callsThisMonth, totalCallsEverPlaced: callsTotal
`) });
]); console.log("Got stats:", stats, "built from query results:", { callsYesterday, callsThisMonth, callsTotal });
const stats = new CallStats({ return stats;
totalCallsMade: callsYesterday,
totalCallsThisMonth: callsThisMonth,
totalCallsEverPlaced: callsTotal
});
console.log("Got stats:", stats, "built from query results:", { callsYesterday, callsThisMonth, callsTotal });
return stats;
} }
function getSystemUptime() { function getSystemUptime() {
const uptime = os.uptime(); const uptime = os.uptime();
const now = new Date(); const now = new Date();
return new TimeSpan(now - (uptime * 1000), now.getTime()); return new TimeSpan(now - (uptime * 1000), now.getTime());
} }
/** /**
@ -93,147 +82,132 @@ function getSystemUptime() {
* @returns {CallStats} * @returns {CallStats}
*/ */
function updateRecords(callStats, records) { function updateRecords(callStats, records) {
const yesterday = getYesterday().startDate; const yesterday = getYesterday().startDate;
const yesterdayDateString = yesterday.toISOString().split('T')[0]; const yesterdayDateString = yesterday.toISOString().split('T')[0];
let isNewRecord = false; let isNewRecord = false;
// Update all-time record // Update all-time record
const allTimeRecord = records.callRecord || new CallRecord({ date: yesterdayDateString, count: 0 }); const allTimeRecord = records.callRecord || new CallRecord({ date: yesterdayDateString, count: 0 });
if (!records.callRecord) { if (!records.callRecord) {
records.callRecord = { date: yesterdayDateString, count: callStats.totalCallsMade }; records.callRecord = { date: yesterdayDateString, count: callStats.totalCallsMade };
isNewRecord = true; isNewRecord = true;
} else if (allTimeRecord.count < callStats.totalCallsThisMonth) { } else if (allTimeRecord.count < callStats.totalCallsThisMonth) {
allTimeRecord.count = callStats.totalCallsThisMonth; allTimeRecord.count = callStats.totalCallsThisMonth;
isNewRecord = true; isNewRecord = true;
} }
callStats.allTimeRecord = `${allTimeRecord.count} calls on ${allTimeRecord.date}`; callStats.allTimeRecord = `${allTimeRecord.count} calls on ${allTimeRecord.date}`;
// Update total calls ever placed // Update total calls ever placed
records.totalCallsEverPlaced = callStats.totalCallsEverPlaced; records.totalCallsEverPlaced = callStats.totalCallsEverPlaced;
// Update monthly totals // Update monthly totals
if (!records.monthlyTotals) records.monthlyTotals = {}; if (!records.monthlyTotals) records.monthlyTotals = {};
if (!records.monthlyTotals[yesterday.getFullYear().toString()]) records.monthlyTotals[yesterday.getFullYear().toString()] = {}; if (!records.monthlyTotals[yesterday.getFullYear().toString()]) records.monthlyTotals[yesterday.getFullYear().toString()] = {};
records.monthlyTotals[yesterday.getFullYear().toString()][yesterday.getMonth().toString()] = callStats.totalCallsThisMonth; records.monthlyTotals[yesterday.getFullYear().toString()][yesterday.getMonth().toString()] = callStats.totalCallsThisMonth;
if (isNewRecord) { if (isNewRecord) {
callStats.isNewRecord = true; callStats.isNewRecord = true;
} }
return callStats; 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("Updating records..."); console.log("Updating records...");
const stats = await updateRecords(data, records); const stats = await updateRecords(data, records);
console.log("Saving."); console.log("Saving.");
await records.toJSONFile(JSON_FILE); await records.toJSONFile(JSON_FILE);
const yesterday = getYesterday(); const yesterday = getYesterday();
await sendSummaryDiscord(yesterday, stats) await sendSummaryDiscord(yesterday, stats)
await sendSummaryMatrix(yesterday, stats) await sendSummaryMatrix(yesterday, stats)
} }
async function sendSummaryMatrix(yesterday, stats) { async function sendSummaryMatrix(yesterday, stats) {
const message = { const message = {
"msgtype": "m.text", "msgtype": "m.text",
"format": "org.matrix.custom.html", "format": "org.matrix.custom.html",
"body": `Summary from ${new Date(yesterday.start).toDateString()} to ${new Date(yesterday.end).toDateString()}\n "body": `Summary from ${new Date(yesterday.start).toDateString()} to ${new Date(yesterday.end).toDateString()}\n
Calls Made: ${stats.totalCallsMade} Calls Made: ${stats.totalCallsMade}
Monthly Total: ${stats.totalCallsThisMonth} Monthly Total: ${stats.totalCallsThisMonth}
Total Calls Ever Placed: ${stats.totalCallsEverPlaced} Total Calls Ever Placed: ${stats.totalCallsEverPlaced}
System Uptime: ${getSystemUptime().toString(false, false)} System Uptime: ${getSystemUptime().toString(false, false)}
All Time Record: ${stats.allTimeRecord} All Time Record: ${stats.allTimeRecord}
${stats.isNewRecord ? `🎉 NEW RECORD! 🎉 A new record has been set, at ${stats.totalCallsMade} calls in a day!` : ''}`.trim(), ${stats.isNewRecord ? `🎉 NEW RECORD! 🎉 A new record has been set, at ${stats.totalCallsMade} calls in a day!` : ''}`.trim(),
"formatted_body": ` "formatted_body": `
<table> <table>
<thead> <thead>
<tr><td colspan="2"><strong>Summary from ${yesterday.startDate.toString()} to ${yesterday.endDate.toString()}</strong></td></tr> <tr><td colspan="2"><strong>Summary from ${yesterday.startDate.toString()} to ${yesterday.endDate.toString()}</strong></td></tr>
</thead> </thead>
<tbody> <tbody>
<tr><td>Calls made</td><td>${stats.totalCallsMade}</td></tr> <tr><td>Calls made</td><td>${stats.totalCallsMade}</td></tr>
<tr><td>Monthly total</td><td>${stats.totalCallsThisMonth}</td></tr> <tr><td>Monthly total</td><td>${stats.totalCallsThisMonth}</td></tr>
<tr><td>Total calls ever placed</td><td>${stats.totalCallsEverPlaced}</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>System uptime</td><td>${getSystemUptime().toString(false, false)}</td></tr>
<tr><td>All-time record</td><td>${stats.allTimeRecord}</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>` : ''} ${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> </tbody>
</table> </table>
`.split('\n').map(s => s.trim()).join(''), `.split('\n').map(s => s.trim()).join(''),
"tel.litenet.call_stats_summary": { "tel.litenet.call_stats_summary": {
...stats, ...stats, date: yesterday, allTimeRecordData: records.callRecord
date: yesterday, }
allTimeRecordData: records.callRecord }
}
}
if (process.env.LOG_MESSAGES) { if (process.env.LOG_MESSAGES) {
console.log("Sending Matrix message:", JSON.stringify(message, null, 2)); console.log("Sending Matrix message:", JSON.stringify(message, null, 2));
console.log("Plaintext:\n", message.body); console.log("Plaintext:\n", message.body);
console.log("HTML:\n", message.formatted_body); console.log("HTML:\n", message.formatted_body);
} }
if (!process.env.NOOP) { if (!process.env.NOOP) {
if (!process.env.MATRIX_BASE_URL || !process.env.MATRIX_ROOM_ID || !process.env.MATRIX_ACCESS_TOKEN) { 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."); console.warn("MATRIX_BASE_URL, MATRIX_ROOM_ID or MATRIX_ACCESS_TOKEN not set, skipping Matrix message.");
return; 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()}`, { 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', method: 'PUT', body: JSON.stringify(message, null, 2), headers: {
body: JSON.stringify(message, null, 2), "Authorization": `Bearer ${process.env.MATRIX_ACCESS_TOKEN}`, "Content-Type": "application/json"
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 {
if (!resp.ok) { console.log("Matrix message sent successfully.");
console.error("Failed to send Matrix message:", resp.status, await resp.text()); }
} else { }
console.log("Matrix message sent successfully.");
}
}
} }
async function sendSummaryDiscord(yesterday, stats) { async function sendSummaryDiscord(yesterday, stats) {
const makeField = (name, value) => ({ const makeField = (name, value) => ({
name, name, value: value === undefined ? "***ERR: UNDEFINED***" : value.toString(), inline: false
value: value === undefined ? "***ERR: UNDEFINED***" : value.toString(), });
inline: false
});
let embed = { let embed = {
title: `Summary from <t:${Math.floor(yesterday.start / 1000)}:f> to <t:${Math.floor(yesterday.end / 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: [ 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) ],
makeField("Calls Made", stats.totalCallsMade), timestamp: new Date(),
makeField("Monthly Total", stats.totalCallsThisMonth), footer: {}
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) { if (stats.isNewRecord) {
embed.color = 0xFFD700; // Gold color for new record 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!`)); 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) if (process.env.LOG_MESSAGES) console.log("Sending Discord message:", JSON.stringify(payload, null, 2));
console.log("Sending Discord message:", JSON.stringify(payload, null, 2));
if (hook && !process.env.NOOP) if (hook && !process.env.NOOP) await hook.send(payload);
await hook.send(payload);
} }
if (process.env.NOOP || process.env.RUN_ONCE) { if (process.env.NOOP || process.env.RUN_ONCE) {
sendSummary(); sendSummary();
return; return;
} }
console.log("Scheduling..."); console.log("Scheduling...");

View file

@ -2,24 +2,16 @@ import fs from "fs";
export class Records { export class Records {
constructor(records = {}) { constructor(records = {}) {
if (typeof records.records === "object") if (typeof records.records === "object") for (const [ key, value ] of Object.entries(records.records)) {
for (const [ key, value ] of Object.entries(records.records)) { const oldTableMatches = key.match(/^monthly_total_(\d{4})-(\d{2})$/);
const oldTableMatches = key.match(/^monthly_total_(\d{4})-(\d{2})$/); if (oldTableMatches) {
if (oldTableMatches) { const year = oldTableMatches[1];
const year = oldTableMatches[1]; const month = oldTableMatches[2];
const month = oldTableMatches[2]; if (!this.monthlyTotals) this.monthlyTotals = {};
if (!this.monthlyTotals) this.monthlyTotals = {}; if (!this.monthlyTotals[year]) this.monthlyTotals[year] = {};
if (!this.monthlyTotals[year]) this.monthlyTotals[year] = {}; this.monthlyTotals[year][month] = value;
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 if (key === "record_calls" && typeof value === "object" && value !== null) } else for (const [ key, value ] of Object.entries(records)) this[key] = value;
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) { static fromJSONFile(path) {