Add support for sending stats to Matrix, split out some code into more generic classes. #1

Open
rory.gay wants to merge 35 commits from rory.gay/freepbx-stats:main into main
4 changed files with 170 additions and 181 deletions
Showing only changes of commit a15e296918 - Show all commits

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

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,13 +15,9 @@ 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
@ -55,26 +50,20 @@ 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), `, getYesterday().startDate, getYesterday().endDate), queryScalarAsync(`
queryScalarAsync(`
SELECT COUNT(DISTINCT uniqueid) AS call_count SELECT COUNT(DISTINCT uniqueid) AS call_count
FROM cdr FROM cdr
WHERE MONTH (calldate) = ? AND YEAR (calldate) = ?; WHERE MONTH (calldate) = ? AND YEAR (calldate) = ?;
`, getYesterday().startDate.getMonth(), getYesterday().startDate.getFullYear()), `, getYesterday().startDate.getMonth(), getYesterday().startDate.getFullYear()), queryScalarAsync(`
queryScalarAsync(`
SELECT COUNT(DISTINCT uniqueid) AS call_count SELECT COUNT(DISTINCT uniqueid) AS call_count
FROM cdr; FROM cdr;
`) `) ]);
]);
const stats = new CallStats({ const stats = new CallStats({
totalCallsMade: callsYesterday, totalCallsMade: callsYesterday, totalCallsThisMonth: callsThisMonth, totalCallsEverPlaced: callsTotal
totalCallsThisMonth: callsThisMonth,
totalCallsEverPlaced: callsTotal
}); });
console.log("Got stats:", stats, "built from query results:", { callsYesterday, callsThisMonth, callsTotal }); console.log("Got stats:", stats, "built from query results:", { callsYesterday, callsThisMonth, callsTotal });
return stats; return stats;
@ -163,9 +152,7 @@ ${stats.isNewRecord ? `🎉 NEW RECORD! 🎉 A new record has been set, at ${sta
</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
} }
} }
@ -181,11 +168,8 @@ ${stats.isNewRecord ? `🎉 NEW RECORD! 🎉 A new record has been set, at ${sta
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) { if (!resp.ok) {
@ -198,21 +182,13 @@ ${stats.isNewRecord ? `🎉 NEW RECORD! 🎉 A new record has been set, at ${sta
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),
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)
],
timestamp: new Date(), timestamp: new Date(),
footer: {} footer: {}
} }
@ -224,11 +200,9 @@ async function sendSummaryDiscord(yesterday, stats) {
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) {

View file

@ -2,8 +2,7 @@ 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];
@ -11,15 +10,8 @@ export class Records {
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) } 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}`);
this.callRecord = new CallRecord(value.date, value.count); } else for (const [ key, value ] of Object.entries(records)) this[key] = value;
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) {