From 9da28e6d7c25bc13d266c6127a63e0d5587409a7 Mon Sep 17 00:00:00 2001 From: Dan Bruce Date: Fri, 9 Jan 2026 14:02:46 -0500 Subject: [PATCH 1/5] change to suit medcon --- .env.example | 23 ++-- api.json.example | 31 +++++ server.js | 311 ++++++++++++++-------------------------------- sups.json.example | 27 ---- 4 files changed, 140 insertions(+), 252 deletions(-) create mode 100644 api.json.example delete mode 100644 sups.json.example diff --git a/.env.example b/.env.example index 103df26..ae61207 100644 --- a/.env.example +++ b/.env.example @@ -4,12 +4,17 @@ FREEPBX_GQL_URL=https://pbx.example.com/admin/api/api/gql FREEPBX_CLIENT_ID=superlongclientidhere FREEPBX_CLIENT_SECRET=superlongclientsecrethere FREEPBX_SCOPE=gql:ringgroups gql:framework -DS_RINGGROUP=9002 -BACKUP_DS_RINGGROUP=9003 -DS_CALLER_ID=<6175550000> -NIGHT_CRON_STRING=0 18 * * * -DAY_CRON_STRING=0 6 * * * -DS_URL=https://ambulanceschedule.example.com/dialert.php -DS_URL_TOKEN=securetokenhere -SUPERVISOR_INFO_FILE=sups.json -TZ=America/New_York \ No newline at end of file +RG1=process.env.RG1; +RG2=process.env.RG2; +RG3=process.env.RG3; +PBX_CID=process.env.PBX_CID; +CRON_STRING=process.env.CRON_STRING; +SCHEDULE_URL=process.env.SCHEDULE_URL; +SCHEDULE_TOKEN=process.env.SCHEDULE_TOKEN; +CRON_STRING=* * * * * +TZ=America/New_York +ERROR_EMAIL_ADDRESS=process.env.ERROR_EMAIL_ADDRESS; +SMTP_SERVER=process.env.SMTP_SERVER; +SMTP_PORT=process.env.SMTP_PORT; +SMTP_USER=process.env.SMTP_USER; +SMTP_PASS=process.env.SMTP_PASS; \ No newline at end of file diff --git a/api.json.example b/api.json.example new file mode 100644 index 0000000..e1c40e5 --- /dev/null +++ b/api.json.example @@ -0,0 +1,31 @@ +{ + + "startTime": "2026-01-09T12:00:00-0500", + "endTime": "2026-12-31T23:59:59-05:00", + "lastUpdated": "2026-01-09T11:56:39-05:00", + "timezone": "America/New_York", + "recipients": [ + { + "role": "primary", + "priority": 1, + "firstName": "", + "lastName": "", + "number": "+15085551234" + }, + { + "role": "secondary", + "priority": 2, + "firstName": "", + "lastName": "", + "number": "+15085554321" + }, + { + "role": "tertiary", + "priority": 3, + "firstName": "", + "lastName": "", + "number": "+15085557654" + } + ], + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +} diff --git a/server.js b/server.js index 215988f..1d5a2e5 100644 --- a/server.js +++ b/server.js @@ -1,7 +1,6 @@ //node packages -const axios = require("axios"); -const fs = require("fs").promises; const cron = require("node-cron"); +const nodemailer = require("nodemailer"); require("dotenv").config(); //globals @@ -12,18 +11,26 @@ const FREEPBX_GQL_URL = process.env.FREEPBX_GQL_URL; const FREEPBX_CLIENT_ID = process.env.FREEPBX_CLIENT_ID; const FREEPBX_CLIENT_SECRET = process.env.FREEPBX_CLIENT_SECRET; const FREEPBX_SCOPE = process.env.FREEPBX_SCOPE; -const DS_RINGGROUP = process.env.DS_RINGGROUP; -const BACKUP_DS_RINGGROUP = process.env.BACKUP_DS_RINGGROUP; -const DS_CALLER_ID = process.env.DS_CALLER_ID; -const NIGHT_CRON_STRING = process.env.NIGHT_CRON_STRING; -const DAY_CRON_STRING = process.env.DAY_CRON_STRING; -const DS_URL = process.env.DS_URL; -const DS_URL_TOKEN = process.env.DS_URL_TOKEN; -const SUPERVISOR_INFO_FILE = process.env.SUPERVISOR_INFO_FILE; +const RG1 = process.env.RG1; +const RG2 = process.env.RG2; +const RG3 = process.env.RG3; +const PBX_CID = process.env.PBX_CID; +const CRON_STRING = process.env.CRON_STRING; +const SCHEDULE_URL = process.env.SCHEDULE_URL; +const SCHEDULE_TOKEN = process.env.SCHEDULE_TOKEN; const TZ = process.env.TZ; +const ERROR_EMAIL_ADDRESS = process.env.ERROR_EMAIL_ADDRESS; +const SMTP_SERVER = process.env.SMTP_SERVER; +const SMTP_PORT = process.env.SMTP_PORT; +const SMTP_USER = process.env.SMTP_USER; +const SMTP_PASS = process.env.SMTP_PASS; -//oauth stuff +const ringgroups = [RG1, RG2, RG3]; +let currentRecipients = {}; +let hash; + +//oauth config const config = { client: { id: FREEPBX_CLIENT_ID, @@ -36,78 +43,48 @@ const config = { }; const { ClientCredentials } = require("simple-oauth2"); - const client = new ClientCredentials(config); - const tokenParams = { scope: FREEPBX_SCOPE.split(" "), }; -//helper functions - -const getNightSupId = async () => { - const { data: nightSup } = await axios.get(`${DS_URL}?token=${DS_URL_TOKEN}`); - return nightSup; -}; - -const getSups = async () => { - const sups = await fs.readFile(`./${SUPERVISOR_INFO_FILE}`, "utf-8"); - return JSON.parse(sups); -}; - -const generateDsNumberList = async (nightSup) => { - const { supervisors: sups } = await getSups(); - let dsNumberList = ""; - for (const sup of sups) { - if (sup.id == nightSup) { - for (num of sup.external_phone_numbers) { - dsNumberList += `${num}#-`; - } - for (num of sup.internal_extensions) { - dsNumberList += `${num}-`; - } - } - } - dsNumberList = dsNumberList.slice(0, -1); - return dsNumberList; -}; +//mailer config +const transporter = nodemailer.createTransport({ + host: SMTP_SERVER, + port: SMTP_PORT, + secure: SMTP_PORT == 465, // Use true for port 465, false for port 587 + auth: { + user: SMTP_USER, + pass: SMTP_PASS, + }, +}); -const generateBackupDsNumberList = async () => { - const { supervisors: sups } = await getSups(); - let backupDsNumberList = ""; - for (const sup of sups) { - if (sup.backup_night) { - for (num of sup.external_phone_numbers) { - backupDsNumberList += `${num}#-`; - } - for (num of sup.internal_extensions) { - backupDsNumberList += `${num}-`; - } - } +//helper functions +const handleError = async (msg) => { + const info = await transporter.sendMail({ + from: '"DiALERT Error" ', + to: ERROR_EMAIL_ADDRESS, + subject: "DiALERT Error Notification", + text: `An error occurred: ${msg}\n\n\nCurrent time: ${new Date().toString()}`, + html: `An error occurred: ${msg}\n

Current time: ${new Date().toString()}`, + }); + console.error("Error email sent: ", msg); + console.error("Message ID: ", info.messageId); +} + +const getCurrentSchedule = async () => { + const res = await fetch(`${SCHEDULE_URL}?token=${SCHEDULE_TOKEN}`, { + method: "GET", + }); + if (!res.ok) { + handleError(`Failed to fetch schedule: ${res.status} ${res.statusText}\nSchedule URL: ${SCHEDULE_URL}`); + return; } - backupDsNumberList = backupDsNumberList.slice(0, -1); - return backupDsNumberList; -}; -const generateDayDsNumberList = async () => { - const { supervisors: sups } = await getSups(); - let dayDsNumberList = ""; - for (const sup of sups) { - if (sup.day) { - for (num of sup.external_phone_numbers) { - dayDsNumberList += `${num}#-`; - } - for (num of sup.internal_extensions) { - dayDsNumberList += `${num}-`; - } - } - } - dayDsNumberList = dayDsNumberList.slice(0, -1); - return dayDsNumberList; -}; + return {hash: res.body.hash,recipients: res.body.recipients}; +} -const handleDay = async () => { - const dsNumberList = await generateDayDsNumberList(); +const updatePbx = async (recipients) => { let accessToken; try { accessToken = await client.getToken(tokenParams, { json: true }); @@ -115,50 +92,42 @@ const handleDay = async () => { } catch (error) { console.log("Access token error: ", error.message); } - const res = await axios.post( - FREEPBX_GQL_URL, - { - query: `mutation{ + + let statuses = []; + + for (let x = 0; x < 3; x++) { + const res = await fetch(FREEPBX_GQL_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken.token.access_token}`, + }, + body: JSON.stringify({ + query: `mutation{ updateRingGroup(input:{ - groupNumber:${DS_RINGGROUP} + groupNumber:${ringgroups[x]} description:"Main DS" - extensionList:"${dsNumberList}" - strategy:"ringall" - ringTime: "60" - changecid: "fixed" - fixedcid: "${DS_CALLER_ID}" - }) { - message status - } - }`, - }, - { headers: { Authorization: `Bearer ${accessToken.token.access_token}` } } - ); - - const backupRes = await axios.post( - FREEPBX_GQL_URL, - { - query: `mutation{ - updateRingGroup(input:{ - groupNumber:${BACKUP_DS_RINGGROUP} - description:"Backup DS" - extensionList:"${dsNumberList}" + extensionList:"${recipients[x]}" strategy:"ringall" ringTime: "60" - callRecording: "force" changecid: "fixed" - fixedcid: "${DS_CALLER_ID}" + fixedcid: "${PBX_CID}" }) { message status } }`, + }), + }); + statuses.push(res.status); + } + + const reloadRes = await fetch(FREEPBX_GQL_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken.token.access_token}`, }, - { headers: { Authorization: `Bearer ${accessToken.token.access_token}` } } - ); - - const reloadRes = await axios.post( - FREEPBX_GQL_URL, - { + body: JSON.stringify({ query: `mutation{ doreload(input:{}) { message @@ -166,127 +135,37 @@ const handleDay = async () => { transaction_id } }`, - }, - { headers: { Authorization: `Bearer ${accessToken.token.access_token}` } } - ); - + }), + }); + return { - main: res.status, - backup: backupRes.status, + main: statuses, reload: reloadRes.status, }; }; -const handleNight = async (nightSup) => { - let dsNumberList; - let backupDsNumberList; - if (nightSup <= 0) { - dsNumberList = await generateBackupDsNumberList(); - } else { - dsNumberList = await generateDsNumberList(nightSup); - backupDsNumberList = await generateBackupDsNumberList(); - } - let accessToken; - try { - accessToken = await client.getToken(tokenParams, { json: true }); - // console.log(accessToken.token.access_token); - } catch (error) { - console.log("Access token error: ", error.message); - } - - const res = await axios.post( - FREEPBX_GQL_URL, - { - query: `mutation{ - updateRingGroup(input:{ - groupNumber:${DS_RINGGROUP} - description:"Main DS" - extensionList:"${dsNumberList}" - strategy:"ringall" - ringTime: "15" - callRecording: "force" - changecid: "fixed" - fixedcid: "${DS_CALLER_ID}" - }) { - message status - } - }`, - }, - { headers: { Authorization: `Bearer ${accessToken.token.access_token}` } } - ); - - const backupRes = await axios.post( - FREEPBX_GQL_URL, - { - query: `mutation{ - updateRingGroup(input:{ - groupNumber:${BACKUP_DS_RINGGROUP} - description:"Backup DS" - extensionList:"${backupDsNumberList}" - strategy:"ringall" - ringTime: "60" - callRecording: "force" - changecid: "fixed" - fixedcid: "${DS_CALLER_ID}" - }) { - message status - } - }`, - }, - { headers: { Authorization: `Bearer ${accessToken.token.access_token}` } } - ); - const reloadRes = await axios.post( - FREEPBX_GQL_URL, - { - query: `mutation{ - doreload(input:{}) { - message - status - transaction_id - } - }`, - }, - { headers: { Authorization: `Bearer ${accessToken.token.access_token}` } } - ); - - return { - main: res.status, - backup: backupRes.status, - reload: reloadRes.status, - }; +const run = async () => { + const res = await getCurrentSchedule(); + if (res.hash === hash) { + console.log(`No changes in schedule at ${new Date().toString()}...`); + return; + } + hash = res.hash; + console.log(`Schedule change detected at ${new Date().toString()}, updating PBX...`); + const updateRes = await updatePbx(res.recipients); + console.debug(`PBX updated...\n${JSON.stringify(updateRes)}`); }; //cron scheduling - -cron.schedule( - NIGHT_CRON_STRING, - async () => { - handleNight(await getNightSupId()); - }, - { timezone: TZ } -); - cron.schedule( - DAY_CRON_STRING, + CRON_STRING, () => { - handleDay(); + run(); }, { timezone: TZ } ); (async () => { - const dayArr = DAY_CRON_STRING.split(" "); - const nightArr = NIGHT_CRON_STRING.split(" "); - const h = new Date().getHours(); - - //check on application start (i.e. the - //cronjob hasn't run yet) - if (h >= dayArr[1] && h < nightArr[1]) { - //it's daytime - handleDay(); - } else { - //it's nighttime - handleNight(await getNightSupId()); - } -})(); + run(); +})(); \ No newline at end of file diff --git a/sups.json.example b/sups.json.example deleted file mode 100644 index b5dc3fb..0000000 --- a/sups.json.example +++ /dev/null @@ -1,27 +0,0 @@ -{ - "supervisors": [ - { - "name": "Dan", - "id": 123, - "external_phone_numbers": [ - "5085551234", - "6175559090" - ], - "internal_extensions": [ - "901" - ], - "day": true, - "backup_night": true - }, - { - "name": "Sparky", - "id": 122, - "external_phone_numbers": [ - "6175551234" - ], - "internal_extensions": [], - "day": false, - "backup_night": false - } - ] -} From 2d77d6fd853f9f3230e2e36e1db2b8d45e27640e Mon Sep 17 00:00:00 2001 From: Dan Bruce Date: Mon, 2 Feb 2026 13:40:55 -0500 Subject: [PATCH 2/5] work but not yet working... --- .env.example | 24 +- package-lock.json | 596 ++++++++++++++++++++++++++++++++++++---------- package.json | 3 +- server.js | 56 +++-- 4 files changed, 521 insertions(+), 158 deletions(-) diff --git a/.env.example b/.env.example index ae61207..ff577ff 100644 --- a/.env.example +++ b/.env.example @@ -4,17 +4,17 @@ FREEPBX_GQL_URL=https://pbx.example.com/admin/api/api/gql FREEPBX_CLIENT_ID=superlongclientidhere FREEPBX_CLIENT_SECRET=superlongclientsecrethere FREEPBX_SCOPE=gql:ringgroups gql:framework -RG1=process.env.RG1; -RG2=process.env.RG2; -RG3=process.env.RG3; -PBX_CID=process.env.PBX_CID; -CRON_STRING=process.env.CRON_STRING; -SCHEDULE_URL=process.env.SCHEDULE_URL; -SCHEDULE_TOKEN=process.env.SCHEDULE_TOKEN; +RG1=1001 +RG2=1002 +RG3=1003 +PBX_CID=1234567890 +CRON_STRING=* * * * * +SCHEDULE_URL=http://schedule.example.com/api/schedule +SCHEDULE_TOKEN=superlongscheduletokenhere CRON_STRING=* * * * * TZ=America/New_York -ERROR_EMAIL_ADDRESS=process.env.ERROR_EMAIL_ADDRESS; -SMTP_SERVER=process.env.SMTP_SERVER; -SMTP_PORT=process.env.SMTP_PORT; -SMTP_USER=process.env.SMTP_USER; -SMTP_PASS=process.env.SMTP_PASS; \ No newline at end of file +ERROR_EMAIL_ADDRESS=errors@example.com +SMTP_SERVER=smtp.example.com +SMTP_PORT=587 +SMTP_USER=user@example.com +SMTP_PASS=superlongsmtppasswordhere \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 066f672..10d6ea0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,32 +11,37 @@ "dependencies": { "axios": "^1.1.3", "dotenv": "^16.0.3", - "node-cron": "^3.0.2", + "node-cron": "^3.0.0", + "nodemailer": "^7.0.13", "simple-oauth2": "^5.0.0" } }, "node_modules/@hapi/boom": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.0.tgz", - "integrity": "sha512-1YVs9tLHhypBqqinKQRqh7FUERIolarQApO37OWkzD+z6y6USi871Sv746zBPKcIOBuI6g6y4FrwX87mmJ90Gg==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.1.tgz", + "integrity": "sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==", + "license": "BSD-3-Clause", "dependencies": { - "@hapi/hoek": "10.x.x" + "@hapi/hoek": "^11.0.2" } }, "node_modules/@hapi/bourne": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", - "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==" + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==", + "license": "BSD-3-Clause" }, "node_modules/@hapi/hoek": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-10.0.1.tgz", - "integrity": "sha512-CvlW7jmOhWzuqOqiJQ3rQVLMcREh0eel4IBnxDx2FAcK8g7qoJRQK4L1CPBASoCY6y8e6zuCy3f2g+HWdkzcMw==" + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" }, "node_modules/@hapi/topo": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.0.0" } @@ -44,22 +49,25 @@ "node_modules/@hapi/topo/node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" }, "node_modules/@hapi/wreck": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-18.0.0.tgz", - "integrity": "sha512-Yk9STxoM06Hjjq58cH0KFG91u9F2h9eVE72o8vUr3AfK80qt7I2POG5+cDGTEntbnvvzm0ERow2sjG3QsqCWUA==", + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-18.1.0.tgz", + "integrity": "sha512-0z6ZRCmFEfV/MQqkQomJ7sl/hyxvcZM7LtuVqN3vdAO4vM9eBbowl0kaqQj9EJJQab+3Uuh1GxbGIBFy4NfJ4w==", + "license": "BSD-3-Clause", "dependencies": { - "@hapi/boom": "^10.0.0", + "@hapi/boom": "^10.0.1", "@hapi/bourne": "^3.0.0", - "@hapi/hoek": "^10.0.0" + "@hapi/hoek": "^11.0.2" } }, "node_modules/@sideway/address": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.0.0" } @@ -67,37 +75,56 @@ "node_modules/@sideway/address/node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" }, "node_modules/@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" }, "node_modules/@sideway/pinpoint": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/axios": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", - "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -106,11 +133,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -125,6 +153,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -137,16 +166,76 @@ "node": ">=12" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -157,39 +246,151 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { "node": ">= 6" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/joi": { - "version": "17.7.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.7.0.tgz", - "integrity": "sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg==", + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", "dependencies": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", - "@sideway/formula": "^3.0.0", + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "node_modules/joi/node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -198,6 +399,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -205,54 +407,79 @@ "node": ">= 0.6" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/node-cron": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.2.tgz", - "integrity": "sha512-iP8l0yGlNpE0e6q1o185yOApANRe47UPbLf4YxfbiNHt/RU5eBcGB/e0oudruheSf+LQeDMezqC5BVAb5wwRcQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.0.tgz", + "integrity": "sha512-DDwIvvuCwrNiaU7HEivFDULcaQualDv7KoNlB/UU1wPW0n1tDEmBJKhEIE6DlF2FuoOHcNbLJ8ITL2Iv/3AWmA==", + "license": "ISC", "dependencies": { - "uuid": "8.3.2" + "moment-timezone": "^0.5.31" }, "engines": { "node": ">=6.0.0" } }, + "node_modules/nodemailer": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz", + "integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/simple-oauth2": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.0.0.tgz", - "integrity": "sha512-8291lo/z5ZdpmiOFzOs1kF3cxn22bMj5FFH+DNUppLJrpoIlM1QnFiE7KpshHu3J3i21TVcx4yW+gXYjdCKDLQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.1.0.tgz", + "integrity": "sha512-gWDa38Ccm4MwlG5U7AlcJxPv3lvr80dU7ARJWrGdgvOKyzSj1gr3GBPN1rABTedAYvC/LsGYoFuFxwDBPtGEbw==", + "license": "Apache-2.0", "dependencies": { - "@hapi/hoek": "^10.0.1", + "@hapi/hoek": "^11.0.4", "@hapi/wreck": "^18.0.0", "debug": "^4.3.4", "joi": "^17.6.4" } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } } }, "dependencies": { "@hapi/boom": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.0.tgz", - "integrity": "sha512-1YVs9tLHhypBqqinKQRqh7FUERIolarQApO37OWkzD+z6y6USi871Sv746zBPKcIOBuI6g6y4FrwX87mmJ90Gg==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.1.tgz", + "integrity": "sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==", "requires": { - "@hapi/hoek": "10.x.x" + "@hapi/hoek": "^11.0.2" } }, "@hapi/bourne": { @@ -261,9 +488,9 @@ "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==" }, "@hapi/hoek": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-10.0.1.tgz", - "integrity": "sha512-CvlW7jmOhWzuqOqiJQ3rQVLMcREh0eel4IBnxDx2FAcK8g7qoJRQK4L1CPBASoCY6y8e6zuCy3f2g+HWdkzcMw==" + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==" }, "@hapi/topo": { "version": "5.1.0", @@ -281,19 +508,19 @@ } }, "@hapi/wreck": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-18.0.0.tgz", - "integrity": "sha512-Yk9STxoM06Hjjq58cH0KFG91u9F2h9eVE72o8vUr3AfK80qt7I2POG5+cDGTEntbnvvzm0ERow2sjG3QsqCWUA==", + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-18.1.0.tgz", + "integrity": "sha512-0z6ZRCmFEfV/MQqkQomJ7sl/hyxvcZM7LtuVqN3vdAO4vM9eBbowl0kaqQj9EJJQab+3Uuh1GxbGIBFy4NfJ4w==", "requires": { - "@hapi/boom": "^10.0.0", + "@hapi/boom": "^10.0.1", "@hapi/bourne": "^3.0.0", - "@hapi/hoek": "^10.0.0" + "@hapi/hoek": "^11.0.2" } }, "@sideway/address": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", "requires": { "@hapi/hoek": "^9.0.0" }, @@ -306,9 +533,9 @@ } }, "@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" }, "@sideway/pinpoint": { "version": "2.0.0", @@ -321,15 +548,24 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "axios": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", - "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", "requires": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -339,11 +575,11 @@ } }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "delayed-stream": { @@ -356,30 +592,128 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, + "es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==" }, "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "requires": { + "has-symbols": "^1.0.3" + } + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, "joi": { - "version": "17.7.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.7.0.tgz", - "integrity": "sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg==", + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", "requires": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", - "@sideway/formula": "^3.0.0", + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" }, "dependencies": { @@ -390,6 +724,11 @@ } } }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -403,39 +742,52 @@ "mime-db": "1.52.0" } }, + "moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==" + }, + "moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "requires": { + "moment": "^2.29.4" + } + }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node-cron": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.2.tgz", - "integrity": "sha512-iP8l0yGlNpE0e6q1o185yOApANRe47UPbLf4YxfbiNHt/RU5eBcGB/e0oudruheSf+LQeDMezqC5BVAb5wwRcQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.0.tgz", + "integrity": "sha512-DDwIvvuCwrNiaU7HEivFDULcaQualDv7KoNlB/UU1wPW0n1tDEmBJKhEIE6DlF2FuoOHcNbLJ8ITL2Iv/3AWmA==", "requires": { - "uuid": "8.3.2" + "moment-timezone": "^0.5.31" } }, + "nodemailer": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz", + "integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==" + }, "proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "simple-oauth2": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.0.0.tgz", - "integrity": "sha512-8291lo/z5ZdpmiOFzOs1kF3cxn22bMj5FFH+DNUppLJrpoIlM1QnFiE7KpshHu3J3i21TVcx4yW+gXYjdCKDLQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.1.0.tgz", + "integrity": "sha512-gWDa38Ccm4MwlG5U7AlcJxPv3lvr80dU7ARJWrGdgvOKyzSj1gr3GBPN1rABTedAYvC/LsGYoFuFxwDBPtGEbw==", "requires": { - "@hapi/hoek": "^10.0.1", + "@hapi/hoek": "^11.0.4", "@hapi/wreck": "^18.0.0", "debug": "^4.3.4", "joi": "^17.6.4" } - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" } } } diff --git a/package.json b/package.json index fe28773..1c2bf67 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "dependencies": { "axios": "^1.1.3", "dotenv": "^16.0.3", - "node-cron": "3.0.0", + "node-cron": "^3.0.0", + "nodemailer": "^7.0.13", "simple-oauth2": "^5.0.0" } } diff --git a/server.js b/server.js index 1d5a2e5..720bdc3 100644 --- a/server.js +++ b/server.js @@ -4,9 +4,7 @@ const nodemailer = require("nodemailer"); require("dotenv").config(); //globals -const FREEPBX_TOKEN_URL = process.env.FREEPBX_TOKEN_URL; const FREEPBX_API_URL = process.env.FREEPBX_API_URL; -const FREEPBX_AUTH_URL = process.env.FREEPBX_AUTH_URL; const FREEPBX_GQL_URL = process.env.FREEPBX_GQL_URL; const FREEPBX_CLIENT_ID = process.env.FREEPBX_CLIENT_ID; const FREEPBX_CLIENT_SECRET = process.env.FREEPBX_CLIENT_SECRET; @@ -38,8 +36,12 @@ const config = { }, auth: { tokenHost: FREEPBX_API_URL, - tokenPath: "token", + tokenPath: "token" }, + http: { + json: "strict", + redirects: true + } }; const { ClientCredentials } = require("simple-oauth2"); @@ -62,70 +64,78 @@ const transporter = nodemailer.createTransport({ //helper functions const handleError = async (msg) => { const info = await transporter.sendMail({ - from: '"DiALERT Error" ', + from: '"DiALERT Error" ', to: ERROR_EMAIL_ADDRESS, subject: "DiALERT Error Notification", text: `An error occurred: ${msg}\n\n\nCurrent time: ${new Date().toString()}`, html: `An error occurred: ${msg}\n

Current time: ${new Date().toString()}`, }); console.error("Error email sent: ", msg); - console.error("Message ID: ", info.messageId); + return console.error("Message ID: ", info.messageId); } const getCurrentSchedule = async () => { - const res = await fetch(`${SCHEDULE_URL}?token=${SCHEDULE_TOKEN}`, { + const res = await fetch(SCHEDULE_URL, { method: "GET", + headers: { + "x-api-key": SCHEDULE_TOKEN, + }, }); if (!res.ok) { - handleError(`Failed to fetch schedule: ${res.status} ${res.statusText}\nSchedule URL: ${SCHEDULE_URL}`); + return handleError(`Failed to fetch schedule: ${res.status} ${res.statusText}\nSchedule URL: ${SCHEDULE_URL}`); + } + const body = await res.json(); + if (body.error) { + console.error("Schedule API error: ", body.error); return; } - - return {hash: res.body.hash,recipients: res.body.recipients}; + return {hash: body.hash, recipients: body.recipients}; } const updatePbx = async (recipients) => { let accessToken; try { - accessToken = await client.getToken(tokenParams, { json: true }); - // console.log(accessToken.token.access_token); + accessToken = await client.getToken(tokenParams); } catch (error) { console.log("Access token error: ", error.message); } let statuses = []; - + for (let x = 0; x < 3; x++) { + console.log(`Updating ring group ${ringgroups[x]} with recipient ${recipients[x].number}...`); const res = await fetch(FREEPBX_GQL_URL, { method: "POST", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${accessToken.token.access_token}`, + "Authorization": `Bearer ${accessToken.token.access_token}` }, body: JSON.stringify({ query: `mutation{ updateRingGroup(input:{ - groupNumber:${ringgroups[x]} - description:"Main DS" - extensionList:"${recipients[x]}" - strategy:"ringall" - ringTime: "60" + groupNumber: "${ringgroups[x]}" + description: "DiALERT Medcon ${x+1}" + extensionList: "${recipients[x].number}#" + strategy: "ringall" + ringTime: "${x==0 ? 30 : 20}" changecid: "fixed" fixedcid: "${PBX_CID}" }) { message status } - }`, - }), + }` + }) }); + console.log(res); statuses.push(res.status); } - + + console.log("Reloading PBX configuration..."); const reloadRes = await fetch(FREEPBX_GQL_URL, { method: "POST", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${accessToken.token.access_token}`, + "Authorization": `Bearer ${accessToken.token.access_token}` }, body: JSON.stringify({ query: `mutation{ @@ -154,7 +164,7 @@ const run = async () => { hash = res.hash; console.log(`Schedule change detected at ${new Date().toString()}, updating PBX...`); const updateRes = await updatePbx(res.recipients); - console.debug(`PBX updated...\n${JSON.stringify(updateRes)}`); + console.debug(`PBX update statuses: ${JSON.stringify(updateRes)}`); }; //cron scheduling From ff31473e876dfb867f8dba45612ffb2dcebbace2 Mon Sep 17 00:00:00 2001 From: Dan Bruce Date: Wed, 4 Mar 2026 12:21:51 -0500 Subject: [PATCH 3/5] working --- server.js | 48 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/server.js b/server.js index 720bdc3..a50e0f6 100644 --- a/server.js +++ b/server.js @@ -101,14 +101,48 @@ const updatePbx = async (recipients) => { } let statuses = []; - - for (let x = 0; x < 3; x++) { + + for (let x = 0; x < recipients.length; x++) { console.log(`Updating ring group ${ringgroups[x]} with recipient ${recipients[x].number}...`); + + const scheduledCount = recipients.length; + let ringTime; + let postAnswer = null; + if (scheduledCount === 2) { + if (x === 0) postAnswer = `ext-group,${ringgroups[1]},1`; + else if (x === 1) postAnswer = `app-blackhole,busy,1`; + } else if (scheduledCount === 3) { + if (x === 0) postAnswer = `ext-group,${ringgroups[1]},1`; + else if (x === 1) postAnswer = `ext-group,${ringgroups[2]},1`; + else if (x === 2) postAnswer = `app-blackhole,busy,1`; + } + + switch (scheduledCount) { + case 1: + ringTime = 120; + postAnswer = `app-blackhole,busy,1`; + break; + case 2: + ringTime = 60; + if (x === 0) postAnswer = `ext-group,${ringgroups[1]},1`; + else if (x === 1) postAnswer = `app-blackhole,busy,1`; + break; + case 3: + ringTime = 60; + if (x === 0 || x === 1) postAnswer = `ext-group,${ringgroups[x+1]},1`; + else if (x === 2) postAnswer = `app-blackhole,busy,1`; + break; + } + + console.debug(`ringTime for ringgroup ${ringgroups[x]}: ${ringTime}`); + console.debug(`postAnswer for ringgroup ${ringgroups[x]}: ${postAnswer}`); + + const res = await fetch(FREEPBX_GQL_URL, { method: "POST", headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${accessToken.token.access_token}` + "Authorization": `Bearer ${accessToken.token.access_token}`, + "Content-Type": "application/json" }, body: JSON.stringify({ query: `mutation{ @@ -117,7 +151,8 @@ const updatePbx = async (recipients) => { description: "DiALERT Medcon ${x+1}" extensionList: "${recipients[x].number}#" strategy: "ringall" - ringTime: "${x==0 ? 30 : 20}" + ringTime: "${ringTime}" + ${postAnswer ? `postAnswer: "${postAnswer}"` : ""} changecid: "fixed" fixedcid: "${PBX_CID}" }) { @@ -126,10 +161,9 @@ const updatePbx = async (recipients) => { }` }) }); - console.log(res); statuses.push(res.status); } - + console.log("Reloading PBX configuration..."); const reloadRes = await fetch(FREEPBX_GQL_URL, { method: "POST", From 2f9219de1907e1593e86c9111e1d4987974f6d33 Mon Sep 17 00:00:00 2001 From: Dan Bruce Date: Wed, 4 Mar 2026 12:47:05 -0500 Subject: [PATCH 4/5] change ringgroup timeouts --- server.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server.js b/server.js index a50e0f6..5ea4949 100644 --- a/server.js +++ b/server.js @@ -119,16 +119,16 @@ const updatePbx = async (recipients) => { switch (scheduledCount) { case 1: - ringTime = 120; + ringTime = 60; postAnswer = `app-blackhole,busy,1`; break; case 2: - ringTime = 60; + ringTime = 30; if (x === 0) postAnswer = `ext-group,${ringgroups[1]},1`; else if (x === 1) postAnswer = `app-blackhole,busy,1`; break; case 3: - ringTime = 60; + ringTime = 30; if (x === 0 || x === 1) postAnswer = `ext-group,${ringgroups[x+1]},1`; else if (x === 2) postAnswer = `app-blackhole,busy,1`; break; From 37ce1509fefb4488d52408137ee9f20ac266124a Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Sun, 31 May 2026 18:05:31 -0400 Subject: [PATCH 5/5] Support arbitrary ring group chaining --- .env.example | 4 +-- server.js | 79 ++++++++++++++++++++++------------------------------ 2 files changed, 34 insertions(+), 49 deletions(-) diff --git a/.env.example b/.env.example index ff577ff..678e2c1 100644 --- a/.env.example +++ b/.env.example @@ -4,9 +4,7 @@ FREEPBX_GQL_URL=https://pbx.example.com/admin/api/api/gql FREEPBX_CLIENT_ID=superlongclientidhere FREEPBX_CLIENT_SECRET=superlongclientsecrethere FREEPBX_SCOPE=gql:ringgroups gql:framework -RG1=1001 -RG2=1002 -RG3=1003 +RING_GROUP_PREFIX=100 PBX_CID=1234567890 CRON_STRING=* * * * * SCHEDULE_URL=http://schedule.example.com/api/schedule diff --git a/server.js b/server.js index 5ea4949..6b83235 100644 --- a/server.js +++ b/server.js @@ -9,9 +9,7 @@ const FREEPBX_GQL_URL = process.env.FREEPBX_GQL_URL; const FREEPBX_CLIENT_ID = process.env.FREEPBX_CLIENT_ID; const FREEPBX_CLIENT_SECRET = process.env.FREEPBX_CLIENT_SECRET; const FREEPBX_SCOPE = process.env.FREEPBX_SCOPE; -const RG1 = process.env.RG1; -const RG2 = process.env.RG2; -const RG3 = process.env.RG3; +const RING_GROUP_PREFIX = process.env.RING_GROUP_PREFIX; const PBX_CID = process.env.PBX_CID; const CRON_STRING = process.env.CRON_STRING; const SCHEDULE_URL = process.env.SCHEDULE_URL; @@ -23,8 +21,6 @@ const SMTP_PORT = process.env.SMTP_PORT; const SMTP_USER = process.env.SMTP_USER; const SMTP_PASS = process.env.SMTP_PASS; -const ringgroups = [RG1, RG2, RG3]; -let currentRecipients = {}; let hash; @@ -89,10 +85,24 @@ const getCurrentSchedule = async () => { console.error("Schedule API error: ", body.error); return; } - return {hash: body.hash, recipients: body.recipients}; + + let groups; + if (body.groups) { + // New API shape: groups with per-group recipients + groups = body.groups + .sort((a, b) => a.priority - b.priority) + .map((group) => ({ recipients: group.recipients })); + } else { + // Old API shape: each recipient becomes its own group + groups = body.recipients + .sort((a, b) => a.priority - b.priority) + .map((recipient) => ({ recipients: [recipient] })); + } + + return {hash: body.hash, groups}; } -const updatePbx = async (recipients) => { +const updatePbx = async (groups) => { let accessToken; try { accessToken = await client.getToken(tokenParams); @@ -102,41 +112,18 @@ const updatePbx = async (recipients) => { let statuses = []; - for (let x = 0; x < recipients.length; x++) { - console.log(`Updating ring group ${ringgroups[x]} with recipient ${recipients[x].number}...`); - - const scheduledCount = recipients.length; - let ringTime; - let postAnswer = null; - if (scheduledCount === 2) { - if (x === 0) postAnswer = `ext-group,${ringgroups[1]},1`; - else if (x === 1) postAnswer = `app-blackhole,busy,1`; - } else if (scheduledCount === 3) { - if (x === 0) postAnswer = `ext-group,${ringgroups[1]},1`; - else if (x === 1) postAnswer = `ext-group,${ringgroups[2]},1`; - else if (x === 2) postAnswer = `app-blackhole,busy,1`; - } - - switch (scheduledCount) { - case 1: - ringTime = 60; - postAnswer = `app-blackhole,busy,1`; - break; - case 2: - ringTime = 30; - if (x === 0) postAnswer = `ext-group,${ringgroups[1]},1`; - else if (x === 1) postAnswer = `app-blackhole,busy,1`; - break; - case 3: - ringTime = 30; - if (x === 0 || x === 1) postAnswer = `ext-group,${ringgroups[x+1]},1`; - else if (x === 2) postAnswer = `app-blackhole,busy,1`; - break; - } - - console.debug(`ringTime for ringgroup ${ringgroups[x]}: ${ringTime}`); - console.debug(`postAnswer for ringgroup ${ringgroups[x]}: ${postAnswer}`); - + for (let x = 0; x < groups.length; x++) { + const ringGroup = `${RING_GROUP_PREFIX}${x + 1}`; + const extensionList = groups[x].recipients.map(r => r.number + "#").join("-"); + const ringTime = groups.length === 1 ? 60 : 30; + const nextRingGroup = `${RING_GROUP_PREFIX}${x + 2}`; + const postAnswer = x === groups.length - 1 + ? `app-blackhole,busy,1` + : `ext-group,${nextRingGroup},1`; + + console.log(`Updating ring group ${ringGroup} with extensions ${extensionList}...`); + console.debug(`ringTime for ringgroup ${ringGroup}: ${ringTime}`); + console.debug(`postAnswer for ringgroup ${ringGroup}: ${postAnswer}`); const res = await fetch(FREEPBX_GQL_URL, { method: "POST", @@ -147,12 +134,12 @@ const updatePbx = async (recipients) => { body: JSON.stringify({ query: `mutation{ updateRingGroup(input:{ - groupNumber: "${ringgroups[x]}" + groupNumber: "${ringGroup}" description: "DiALERT Medcon ${x+1}" - extensionList: "${recipients[x].number}#" + extensionList: "${extensionList}" strategy: "ringall" ringTime: "${ringTime}" - ${postAnswer ? `postAnswer: "${postAnswer}"` : ""} + postAnswer: "${postAnswer}" changecid: "fixed" fixedcid: "${PBX_CID}" }) { @@ -197,7 +184,7 @@ const run = async () => { } hash = res.hash; console.log(`Schedule change detected at ${new Date().toString()}, updating PBX...`); - const updateRes = await updatePbx(res.recipients); + const updateRes = await updatePbx(res.groups); console.debug(`PBX update statuses: ${JSON.stringify(updateRes)}`); };