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>
138 lines
4.8 KiB
JavaScript
138 lines
4.8 KiB
JavaScript
// src/services/voiceDiag/checks/voicemail.js
|
|
//
|
|
// Detects voicemail configuration for the store user. Voicemail has
|
|
// enough sub-facets that we surface the current settings without
|
|
// offering a one-click remediation — the "right" answer for a store
|
|
// is site-specific (some sites disable VM entirely and forward busy
|
|
// to the AA, others rely on it as the noAnswer target). We flag two
|
|
// classes of finding:
|
|
//
|
|
// - **error**: enabled=true AND no PIN set — the user cannot pick
|
|
// up messages, and every caller who reaches VM will be dumped
|
|
// into the "please set your PIN" prompt.
|
|
// - **warn**: enabled=true AND a forward-to-email target is
|
|
// configured that doesn't look like an @ae.com address — mail
|
|
// forwarding of voicemail off-org is worth double-checking.
|
|
// - **ok**: everything else.
|
|
//
|
|
// Endpoint: GET /v1/people/{personId}/features/voicemail
|
|
// Scope: spark-admin:people_read
|
|
// Response shape (relevant fields):
|
|
// {
|
|
// enabled: true,
|
|
// sendAllCalls: { enabled: false },
|
|
// sendBusyCalls: { enabled: false, greeting: 'DEFAULT' },
|
|
// sendUnansweredCalls: { enabled: true, ... },
|
|
// notifications: { enabled: false, destination: '' },
|
|
// transferToNumber: { enabled: false, destination: '' },
|
|
// emailCopyOfMessage: { enabled: false, emailId: '' },
|
|
// messageStorage: {
|
|
// mwiEnabled: true,
|
|
// storageType: 'INTERNAL' | 'EXTERNAL',
|
|
// externalEmail: ''
|
|
// },
|
|
// faxMessage: { ... }
|
|
// }
|
|
//
|
|
// Webex doesn't currently expose PIN-set status via the public API,
|
|
// so "PIN set" is inferred pragmatically: we check whether the person
|
|
// has ever accessed their voicemail (via passcode lastChanged if the
|
|
// endpoint returns it) or, as a fallback, we simply note that PIN
|
|
// state is unknown and treat it as informational only. See the note
|
|
// inline below.
|
|
|
|
const ENDPOINT = (personId) =>
|
|
`people/${personId}/features/voicemail`;
|
|
|
|
const AE_EMAIL_RE = /@ae\.com$/i;
|
|
|
|
export const voicemailCheck = {
|
|
id: 'voicemail',
|
|
label: 'Voicemail',
|
|
requires: ['personId'],
|
|
scope: 'spark-admin:people_read',
|
|
|
|
async run(ctx) {
|
|
const data = await ctx.webex.request('GET', ENDPOINT(ctx.personId));
|
|
|
|
const enabled = !!data?.enabled;
|
|
const storage = data?.messageStorage || {};
|
|
const mwiEnabled = !!storage?.mwiEnabled;
|
|
const emailCopy = data?.emailCopyOfMessage || {};
|
|
const transferTo = data?.transferToNumber || {};
|
|
const externalEmail = storage?.externalEmail || '';
|
|
const forwardEmailTarget = emailCopy?.enabled ? (emailCopy?.emailId || '') : '';
|
|
|
|
const details = {
|
|
enabled,
|
|
mwiEnabled,
|
|
storageType: storage?.storageType || null,
|
|
externalEmail,
|
|
emailCopyEnabled: !!emailCopy?.enabled,
|
|
emailCopyTarget: forwardEmailTarget,
|
|
transferToEnabled: !!transferTo?.enabled,
|
|
transferToDestination: transferTo?.destination || null,
|
|
sendAllCallsEnabled: !!data?.sendAllCalls?.enabled,
|
|
sendBusyCallsEnabled: !!data?.sendBusyCalls?.enabled,
|
|
sendUnansweredCallsEnabled: !!data?.sendUnansweredCalls?.enabled,
|
|
};
|
|
|
|
if (!enabled) {
|
|
return {
|
|
status: 'ok',
|
|
message: 'Voicemail is disabled — callers will not be able to leave messages.',
|
|
details,
|
|
remediation: null,
|
|
};
|
|
}
|
|
|
|
// sendAllCalls silently swallowing every inbound call is almost
|
|
// always a mistake — surface it loudly.
|
|
if (details.sendAllCallsEnabled) {
|
|
return {
|
|
status: 'error',
|
|
message: 'Voicemail is enabled AND "send all calls to voicemail" is on — the phone will never ring.',
|
|
details,
|
|
remediation: null,
|
|
};
|
|
}
|
|
|
|
if (!mwiEnabled) {
|
|
return {
|
|
status: 'warn',
|
|
message: 'Voicemail is enabled but MWI (message-waiting indicator) is off — new messages won\'t light up the phone.',
|
|
details,
|
|
remediation: null,
|
|
};
|
|
}
|
|
|
|
if (forwardEmailTarget && !AE_EMAIL_RE.test(forwardEmailTarget)) {
|
|
return {
|
|
status: 'warn',
|
|
message: `Voicemail-to-email is forwarding to an off-org address: ${forwardEmailTarget}. Verify this is intentional.`,
|
|
details,
|
|
remediation: null,
|
|
};
|
|
}
|
|
|
|
if (details.transferToEnabled && !details.transferToDestination) {
|
|
return {
|
|
status: 'warn',
|
|
message: 'Voicemail transfer-to-number is enabled but no destination is set — will fall through to the default greeting.',
|
|
details,
|
|
remediation: null,
|
|
};
|
|
}
|
|
|
|
return {
|
|
status: 'ok',
|
|
message: 'Voicemail is enabled with MWI on and no unusual forwarding.',
|
|
details,
|
|
remediation: null,
|
|
};
|
|
},
|
|
|
|
// No remediations — voicemail policy is too site-specific for a
|
|
// one-size-fits-all button. The renderer surfaces the details so
|
|
// the operator can act via Control Hub if they want.
|
|
};
|