Introduces a new diagnostic command that walks a registry of check
modules against a store user's Webex Calling configuration and
surfaces per-issue adaptive-card remediation for the fixable ones.
Checks (services/voiceDiag/checks/): dnd, callForwarding, callWaiting,
callIntercept, voicemail, hoteling, executiveAssistant,
outgoingPermission, phoneOnline. Remediations offered for DND,
forwarding, waiting, and intercept.
Uses the /v1/people/{id}/features/* admin surface (spark-admin:people_read
+ spark-admin:people_write scopes we already hold) — the earlier
telephony/config/people/*/callSettings/* path scheme returns 404 from
the Webex gateway and is not a live surface. Runner distinguishes
routing-404s ("URL moved") from "not applicable" 404s ("no calling
license") via the response body.
Arg parser accepts detail/detailed/--detail/--detailed and normalises
macOS smart-dashes so --detailed doesn't die when auto-correct
turns it into an em-dash.
Wires a VOICEDIAG_ACTIONS dispatcher in index.js mirroring the IGMP
branch, and registers /voicediag in commands/registry.js. 170 tests
pass (52 new: 39 check + 12 renderer + 5 arg-normalization).
Docs updated in .env.example, services/phoneService.js:467, and a new
services/voiceDiag/README.md that includes a "how to add a check"
recipe plus a note on the earlier wrong URL scheme.
Co-authored-by: Cursor <cursoragent@cursor.com>
118 lines
5.5 KiB
JavaScript
118 lines
5.5 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 { 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';
|
|
|
|
/**
|
|
* 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 },
|
|
// /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 },
|
|
{ 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;
|
|
})();
|