collabSupport/commands/registry.js
Joseph McQueen 7aa8c37d1d Add Twilio /calltest for store and direct-dial voice path testing.
Enables outbound PSTN probes via TwiML webhooks with Webex result cards, status polling, and optional store CDR enrichment.
2026-07-23 17:59:10 -04:00

126 lines
6 KiB
JavaScript

// src/commands/registry.js
//
// Single source of truth for every bot command.
//
// Both dispatch paths in index.js consume this registry:
// - Webex chat: framework.hears(/.*/, …) parses the text, then runs the
// entry whose name (or alias) matches the first token.
// - HTTP API: app.get('/:command', …) looks up the same registry and runs
// the entry's handler against a captured-output mock bot.
//
// Adding a new command means appending one entry below. Both Webex and HTTP
// pick it up automatically (subject to `mutating` / `http` flags).
import { handleHelp } from './help.js';
import { handleAvStatus } from './avStatus.js';
import { handlePhoneStatus } from './phoneStatus.js';
import { handleDectStatus } from './dectStatus.js';
import { handleProvisionDect } from './provisionDect.js';
import { handleWoHistory } from './woHistory.js';
import { handleWoSummary } from './woSummary.js';
import { handleWoAttachments } from './woAttachments.js';
import { handleJiraHistory } from './jiraHistory.js';
import { handleJiraTicket } from './jiraTicket.js';
import { handleJiraPoll } from './jiraPoll.js';
import { handleProvisionVc } from './vcProvision.js';
import { handleVcMonitor } from './vcMonitor.js';
import { handleOffboardUser } from './offboardUser.js';
import { handleWebexHost } from './webexHost.js';
import { handleVoiceDiag } from './voiceDiag.js';
import { handleBulkAvStatusCSV } from './bulkAvStatusCSV.js';
import { handleBulkAvSwitchCSV } from './bulkAvSwitchCSV.js';
import { handleTestDevicesByModel } from './testDevicesByModel.js';
import { handleCallTest } from './callTest.js';
/**
* Each entry:
* - name: canonical name (lowercase, no leading slash).
* - aliases: additional names that route to the same handler.
* - handler: async (bot, trigger) => void
* - mutating: when true, the HTTP /:command path requires HTTP_API_TOKEN.
* - http: defaults to true. Set false for commands that only make
* sense over Webex chat (e.g. `help`).
*/
export const commands = [
{ name: 'help', handler: handleHelp, mutating: false, http: false },
{ name: 'avstatus', aliases: ['wostatus'], handler: handleAvStatus, mutating: false },
{ name: 'phonestatus', handler: handlePhoneStatus, mutating: false },
// /dectstatus — full DECT base dump via the on-prem relay + chat-only
// reboot/factory-reset cards. Read path is non-mutating; card submits
// only fire over Webex (see commands/dectStatus.js).
{ name: 'dectstatus', aliases: ['dect'], handler: handleDectStatus, mutating: false },
// /voicediag reads per-user Webex Calling features via
// /v1/people/{id}/features/* and offers per-issue adaptive-card
// remediation. Reads are non-mutating but the confirm buttons on
// the cards perform PUTs, so the HTTP path is gated
// (mutating: true). See commands/voiceDiag.js + services/voiceDiag/
// for the full behavior contract + required scopes.
{ name: 'voicediag', handler: handleVoiceDiag, mutating: true },
{ name: 'wohistory', handler: handleWoHistory, mutating: false },
{ name: 'wosummary', handler: handleWoSummary, mutating: false },
{ name: 'woattachments', handler: handleWoAttachments, mutating: false },
{ name: 'jirahistory', handler: handleJiraHistory, mutating: false },
{ name: 'jiraticket', handler: handleJiraTicket, mutating: false },
// /jirapoll [prime] — trigger the hourly Jira poller on demand. Writes
// Jira comments + labels, so mutating: true (HTTP path gated by
// HTTP_API_TOKEN). See commands/jiraPoll.js for the behavior contract.
{ name: 'jirapoll', aliases: ['pollnow'], handler: handleJiraPoll, mutating: true },
{ name: 'provision-dect', aliases: ['provisiondect'], handler: handleProvisionDect, mutating: true },
{ name: 'provision-vc', aliases: ['vcprovision'], handler: handleProvisionVc, mutating: true },
{ name: 'vcmonitor', handler: handleVcMonitor, mutating: true },
// /calltest places real Twilio PSTN calls (store AA path or direct dial).
{ name: 'calltest', handler: handleCallTest, mutating: true },
{ name: 'offboarduser', handler: handleOffboardUser, mutating: true },
{ name: 'webexhost', handler: handleWebexHost, mutating: true },
// bulkavstatuscsv delivers a CSV attachment via BotClient.sendWithAttachment,
// which requires a Webex roomId. The HTTP mock trigger has no roomId, so
// the command is chat-only; the runtime guard in the handler backstops this.
{ name: 'bulkavstatuscsv', handler: handleBulkAvStatusCSV, mutating: true, http: false },
{ name: 'bulkavswitchcsv', handler: handleBulkAvSwitchCSV, mutating: true },
{ name: 'devicesbymodel', handler: handleTestDevicesByModel, mutating: true },
];
const byName = new Map();
for (const cmd of commands) {
const all = [cmd.name, ...(cmd.aliases || [])].map(n => n.toLowerCase());
for (const key of all) {
if (byName.has(key)) {
// Fail loud on misconfiguration — silent alias overwrite is the kind of
// drift this registry is designed to prevent.
throw new Error(`Duplicate command registration: "${key}"`);
}
byName.set(key, cmd);
}
}
/**
* Look up a command by canonical name or alias. Case-insensitive.
* @returns {{name: string, handler: Function, mutating: boolean, http?: boolean, aliases?: string[]} | null}
*/
export function getCommand(name) {
if (!name) return null;
return byName.get(String(name).toLowerCase().trim()) || null;
}
/** Names + aliases of commands exposed over HTTP (used for /:command 404 hints). */
export const ALL_HTTP_COMMAND_KEYS = (() => {
const keys = [];
for (const cmd of commands) {
if (cmd.http === false) continue;
keys.push(cmd.name, ...(cmd.aliases || []));
}
return keys.sort();
})();
/** Set of names + aliases that require HTTP_API_TOKEN on the HTTP path. */
export const MUTATING_COMMAND_KEYS = (() => {
const keys = new Set();
for (const cmd of commands) {
if (!cmd.mutating) continue;
keys.add(cmd.name);
for (const a of cmd.aliases || []) keys.add(a);
}
return keys;
})();