#!/usr/bin/env php , examples?: list}> */ function commandCatalog(): array { return [ 'getBalance' => [ 'category' => 'General', 'description' => 'Retrieve account balance. Add advanced=true for balance and call statistics.', 'params' => ['advanced'], 'examples' => ['getBalance', 'getBalance advanced=true'], ], 'getIP' => [ 'category' => 'General', 'description' => 'Show the public IP address seen by the VoIP.ms API for whitelist setup.', 'examples' => ['getIP'], ], 'getServersInfo' => [ 'category' => 'General', 'description' => 'Retrieve VoIP.ms server information, optionally filtered by server_pop.', 'params' => ['server_pop'], ], 'getTransactionHistory' => [ 'category' => 'General', 'description' => 'Retrieve transaction history between two dates.', 'params' => ['from', 'to'], ], 'getSubAccounts' => [ 'category' => 'Accounts', 'description' => 'Retrieve all subaccounts or one account when account is provided.', 'params' => ['account', 'client'], ], 'getRegistrationStatus' => [ 'category' => 'Accounts', 'description' => 'Retrieve registration status for all accounts or a specific account.', 'params' => ['account'], 'examples' => ['getRegistrationStatus account=100000_device'], ], 'setSubAccount' => [ 'category' => 'Accounts', 'description' => 'Update subaccount information.', 'params' => ['id', 'description', 'auth_type', 'password', 'ip', 'device_type', 'callerid_number', 'canada_routing', 'lock_international', 'international_route'], 'examples' => ['setSubAccount id=10236 callerid_number=5551234567'], ], 'getCDR' => [ 'category' => 'Call Detail Records', 'description' => 'Retrieve call detail records using the raw VoIP.ms API parameter names.', 'params' => ['date_from', 'date_to', 'timezone', 'answered', 'noanswer', 'busy', 'failed', 'calltype', 'callbilling', 'account'], 'examples' => ['getCDR date_from=2026-04-01 date_to=2026-04-20 timezone=-4 calltype=incoming callbilling=all account=all'], ], 'cdr' => [ 'category' => 'Call Detail Records', 'description' => 'Friendly alias for getCDR with clearer date, timezone, type, billing, account, and disposition options.', 'params' => ['--from', '--to', '--timezone', '--type', '--billing', '--account', '--answered', '--no-answer', '--busy', '--failed'], 'examples' => [ 'cdr 2026-04-01 2026-04-20 --timezone=-4', 'cdr 2026-04-01 2026-04-20 --timezone=-4 --answered --no-answer', 'cdr --from 2026-04-01 --to 2026-04-20 --timezone=-4 --type incoming --billing billed', ], ], 'getCallAccounts' => [ 'category' => 'Call Detail Records', 'description' => 'List account filter values accepted by CDR and call recording commands. Use the returned value field as account.', 'params' => ['client'], 'examples' => ['getCallAccounts', 'getCallAccounts client=561115'], ], 'getAccounts' => [ 'category' => 'Call Detail Records', 'description' => 'Friendly alias for getCallAccounts. This does not list VoIP.ms login users; it lists account filter values for getCDR and call recording commands.', 'params' => ['client'], 'examples' => ['getAccounts', 'getAccounts client=561115'], ], 'getCallBilling' => [ 'category' => 'Call Detail Records', 'description' => 'List billing filter values accepted by getCDR callbilling.', ], 'getCallTypes' => [ 'category' => 'Call Detail Records', 'description' => 'List call type filter values accepted by getCDR calltype, including incoming, outgoing, and DID numbers.', 'params' => ['client'], ], 'getRates' => [ 'category' => 'Call Detail Records', 'description' => 'Retrieve rates for a package and search term.', 'params' => ['package', 'query'], ], 'getTerminationRates' => [ 'category' => 'Call Detail Records', 'description' => 'Retrieve termination rates for a route and search term.', 'params' => ['route', 'query'], ], 'getCallRecordings' => [ 'category' => 'Call Recordings', 'description' => 'Retrieve call recordings. account is required and comes from getAccounts/getCallAccounts; call_type may be all, incoming, or outgoing.', 'params' => ['account', 'date_from', 'date_to', 'start', 'length', 'call_type'], 'examples' => [ 'getAccounts', 'getCallRecordings account=all date_from=2026-04-01 date_to=2026-04-20 call_type=all', ], ], 'getCallRecording' => [ 'category' => 'Call Recordings', 'description' => 'Retrieve one call recording and MP3 data.', 'params' => ['account', 'callrecording'], 'examples' => ['getCallRecording account=100000_VoIP callrecording='], ], 'sendCallRecordingEmail' => [ 'category' => 'Call Recordings', 'description' => 'Email one call recording.', 'params' => ['account', 'email', 'callrecording'], 'examples' => ['sendCallRecordingEmail account=100000_VoIP email=you@example.com callrecording='], ], 'getDIDsInfo' => [ 'category' => 'DIDs', 'description' => 'Retrieve DID information for all DIDs, a reseller client, a subaccount, or a specific DID.', 'params' => ['did', 'client', 'account'], 'examples' => ['getDIDsInfo', 'getDIDsInfo did=15551234567'], ], 'setDIDRouting' => [ 'category' => 'DIDs', 'description' => 'Update routing for a DID.', 'params' => ['did', 'routing', 'account', 'voicemail', 'forwarding'], ], 'setDIDInfo' => [ 'category' => 'DIDs', 'description' => 'Update DID information, including incoming CNAM lookup and Caller ID prefix.', 'params' => ['did', 'routing', 'failover_busy', 'failover_unreachable', 'failover_noanswer', 'voicemail', 'pop', 'dialtime', 'cnam', 'callerid_prefix', 'record_calls', 'note', 'port_out_pin', 'billing_type'], 'examples' => [ 'setDIDInfo did=5551234567 cnam=1 callerid_prefix="SALES" note="Main line"', 'setDIDInfo did=5551234567 routing=account:100001_VoIP pop=3 dialtime=60 cnam=1 billing_type=2', ], ], 'getCallerIDFiltering' => [ 'category' => 'Caller ID', 'description' => 'Retrieve CallerID Filtering rules, or one rule when filtering is provided.', 'params' => ['filtering'], 'examples' => ['getCallerIDFiltering', 'getCallerIDFiltering filtering=18915'], ], 'setCallerIDFiltering' => [ 'category' => 'Caller ID', 'description' => 'Create or update a CallerID Filtering rule for incoming calls.', 'params' => ['filter', 'callerid', 'did', 'routing', 'failover_unreachable', 'failover_busy', 'failover_noanswer', 'note'], 'examples' => [ 'setCallerIDFiltering callerid=5551234567 did=all routing=sys:busy note="Block caller"', 'setCallerIDFiltering callerid=0 did=5552223333 routing=none: note="Anonymous caller filter"', 'setCallerIDFiltering callerid=p:3220 did=all routing=account:100001_VoIP note="Phonebook group VIP route"', ], ], 'delCallerIDFiltering' => [ 'category' => 'Caller ID', 'description' => 'Delete a CallerID Filtering rule by filtering ID.', 'params' => ['filtering'], 'examples' => ['delCallerIDFiltering filtering=18915'], ], 'getForwardings' => [ 'category' => 'DIDs', 'description' => 'Retrieve forwarding entries.', 'params' => ['forwarding'], ], 'setForwarding' => [ 'category' => 'DIDs', 'description' => 'Create or update a forwarding entry, optionally with Caller ID override.', 'params' => ['forwarding', 'phone_number', 'callerid_override', 'description', 'dtmf_digits', 'pause'], 'examples' => ['setForwarding phone_number=5553334444 callerid_override=5551234567 description="After hours cell"'], ], 'getPhonebook' => [ 'category' => 'DIDs', 'description' => 'Retrieve phonebook entries. A bare numeric argument is treated as a group ID; a bare text argument is treated as a group name.', 'params' => ['phonebook', 'name', 'group', 'group_name'], 'examples' => [ 'getPhonebook', 'getPhonebook 3220', 'getPhonebook Spam', 'getPhonebook name=Jane', 'getPhonebook group=3220', 'getPhonebook group_name=Spam', 'getPhonebook phonebook=32207', ], ], 'getPhonebookGroups' => [ 'category' => 'DIDs', 'description' => 'Retrieve phonebook groups. A bare numeric argument is treated as a group ID; a bare text argument is treated as a group name.', 'params' => ['group', 'name'], 'examples' => ['getPhonebookGroups', 'getPhonebookGroups 3220', 'getPhonebookGroups Work', 'getPhonebookGroups name=Work'], ], 'setPhonebook' => [ 'category' => 'DIDs', 'description' => 'Create or update a phonebook entry.', 'params' => ['phonebook', 'speed_dial', 'name', 'number', 'callerid', 'note', 'group'], 'examples' => [ 'setPhonebook name="Jane Smith" number=5553334444 note="Janes Mobile"', 'setPhonebook phonebook=32207 name="Jane Smith" number=5553334444 group=3220', ], ], 'setPhonebookGroup' => [ 'category' => 'DIDs', 'description' => 'Create or update a phonebook group. members is a semicolon-separated list of phonebook entry IDs.', 'params' => ['group', 'name', 'members'], 'examples' => [ 'setPhonebookGroup name=Work', 'setPhonebookGroup group=3220 name=Work members="32207;32208"', ], ], 'delPhonebook' => [ 'category' => 'DIDs', 'description' => 'Delete a phonebook entry by phonebook ID.', 'params' => ['phonebook'], 'examples' => ['delPhonebook phonebook=32207'], ], 'delPhonebookGroup' => [ 'category' => 'DIDs', 'description' => 'Delete a phonebook group by group ID.', 'params' => ['group'], 'examples' => ['delPhonebookGroup group=3220'], ], 'getSMS' => [ 'category' => 'DIDs', 'description' => 'Retrieve SMS messages by date range, type, DID, and contact.', 'params' => ['date_from', 'date_to', 'type', 'did', 'contact'], 'examples' => ['getSMS date_from=2026-04-01 date_to=2026-04-20 did=15551234567'], ], 'sendSMS' => [ 'category' => 'DIDs', 'description' => 'Send an SMS message. Business A2P verification may be required by VoIP.ms.', 'params' => ['did', 'dst', 'message'], 'examples' => ['sendSMS did=15551234567 dst=15557654321 message="Hello"'], ], 'getMMS' => [ 'category' => 'DIDs', 'description' => 'Retrieve MMS messages by date range, type, DID, and contact.', 'params' => ['date_from', 'date_to', 'type', 'did', 'contact'], ], 'sendMMS' => [ 'category' => 'DIDs', 'description' => 'Send an MMS message. Business A2P verification may be required by VoIP.ms.', 'params' => ['did', 'dst', 'message', 'media1'], ], 'getVoicemailSetups' => [ 'category' => 'DIDs', 'description' => 'Retrieve voicemail setup options.', 'params' => ['voicemail_setup'], ], 'getFaxMessages' => [ 'category' => 'Fax', 'description' => 'Retrieve fax messages.', 'params' => ['fax', 'folder', 'message'], ], 'getFaxMessagePDF' => [ 'category' => 'Fax', 'description' => 'Retrieve a fax message as base64 PDF data.', 'params' => ['message'], ], 'sendFaxMessage' => [ 'category' => 'Fax', 'description' => 'Send a fax message to a destination number.', 'params' => ['fax', 'dst', 'file'], ], 'e911Info' => [ 'category' => 'e911', 'description' => 'Retrieve e911 information for a DID.', 'params' => ['did'], ], 'e911Validate' => [ 'category' => 'e911', 'description' => 'Validate e911 information before provisioning.', 'params' => ['did', 'address_type', 'address', 'city', 'state', 'zip', 'country'], ], 'getLNPStatus' => [ 'category' => 'Local Number Portability', 'description' => 'Retrieve status for a portability process.', 'params' => ['port'], ], 'getLNPList' => [ 'category' => 'Local Number Portability', 'description' => 'Retrieve the full list of portability processes.', ], ]; } function main(array $argv): int { array_shift($argv); if ($argv === [] || in_array($argv[0], ['-h', '--help', 'help'], true)) { $command = $argv[1] ?? null; printHelp(is_string($command) ? $command : null); return 0; } if ($argv[0] === 'list') { printCommandList(); return 0; } if ($argv[0] === 'completion') { $shell = $argv[1] ?? 'bash'; if ($shell !== 'bash') { fwrite(STDERR, "Only bash completion is supported right now.\n"); return 64; } printBashCompletion(); return 0; } $method = array_shift($argv); if ($method === null || $method === '') { printHelp(null); return 64; } if (($argv[0] ?? null) === 'help') { printHelp($method); return 0; } try { if ($method === 'getAccounts') { $method = 'getCallAccounts'; $parsed = parseArguments($argv); } elseif ($method === 'cdr') { $method = 'getCDR'; $parsed = parseCdrArguments($argv); } elseif (in_array($method, ['getPhonebook', 'getPhonebookGroups'], true)) { $parsed = parsePhonebookLookupArguments($method, $argv); } else { $parsed = parseArguments($argv); } } catch (InvalidArgumentException $exception) { fwrite(STDERR, $exception->getMessage() . "\n"); return 64; } $username = $parsed['options']['username'] ?? getenv('VOIPMS_API_USERNAME') ?: ''; $password = $parsed['options']['password'] ?? getenv('VOIPMS_API_PASSWORD') ?: ''; $endpoint = $parsed['options']['endpoint'] ?? getenv('VOIPMS_API_ENDPOINT') ?: VoipMsClient::DEFAULT_ENDPOINT; $format = $parsed['options']['format'] ?? 'pretty'; if (($parsed['options']['dry-run'] ?? '') === '1') { printDryRun($method, $parsed['params'], $format); return 0; } if ($username === '' || $password === '') { fwrite(STDERR, "Missing credentials. Set VOIPMS_API_USERNAME and VOIPMS_API_PASSWORD, or pass --username and --password.\n"); return 78; } $client = new VoipMsClient($username, $password, $endpoint); try { $response = $client->call($method, $parsed['params']); } catch (RuntimeException $exception) { fwrite(STDERR, $exception->getMessage() . "\n"); return 69; } printResponse($response, $format); printActionableHint($method, $response); $decoded = $response['decoded']; if (is_array($decoded) && isset($decoded['status']) && $decoded['status'] !== 'success') { return 70; } return $response['http_code'] >= 400 ? 69 : 0; } /** * @param list $args * @return array{options: array, params: array} */ function parseArguments(array $args): array { $options = []; $params = []; for ($i = 0; $i < count($args); $i++) { $arg = $args[$i]; if (str_starts_with($arg, '--')) { $option = substr($arg, 2); $value = null; if (str_contains($option, '=')) { [$option, $value] = explode('=', $option, 2); } if ($option === 'param') { $param = $value ?? $args[++$i] ?? null; if ($param === null || !str_contains($param, '=')) { throw new InvalidArgumentException('--param expects key=value.'); } [$key, $paramValue] = explode('=', $param, 2); $params[$key] = $paramValue; continue; } if ($option === 'dry-run') { $options['dry-run'] = '1'; continue; } if (!in_array($option, ['username', 'password', 'endpoint', 'format'], true)) { throw new InvalidArgumentException("Unknown option --{$option}."); } $options[$option] = $value ?? $args[++$i] ?? ''; if ($options[$option] === '') { throw new InvalidArgumentException("--{$option} expects a value."); } continue; } if (!str_contains($arg, '=')) { throw new InvalidArgumentException("Unexpected argument '{$arg}'. Use key=value for API parameters."); } [$key, $value] = explode('=', $arg, 2); if ($key === '') { throw new InvalidArgumentException("Invalid empty parameter name in '{$arg}'."); } $params[$key] = $value; } if (isset($options['format']) && !in_array($options['format'], ['pretty', 'json', 'raw'], true)) { throw new InvalidArgumentException('--format must be one of: pretty, json, raw.'); } return ['options' => $options, 'params' => $params]; } /** * @param list $args * @return array{options: array, params: array} */ function parsePhonebookLookupArguments(string $method, array $args): array { $parsed = parseLenientArguments($args); $positionals = $parsed['positionals']; unset($parsed['positionals']); if (count($positionals) > 1) { throw new InvalidArgumentException("{$method} accepts at most one bare argument."); } if (isset($parsed['params']['key'])) { $positionals[] = $parsed['params']['key']; unset($parsed['params']['key']); } if (isset($positionals[0])) { $value = $positionals[0]; if ($method === 'getPhonebookGroups') { $parsed['params'][ctype_digit($value) ? 'group' : 'name'] = $value; } else { $parsed['params'][ctype_digit($value) ? 'group' : 'group_name'] = $value; } } return $parsed; } /** * @param list $args * @return array{options: array, params: array, positionals: list} */ function parseLenientArguments(array $args): array { $options = []; $params = []; $positionals = []; for ($i = 0; $i < count($args); $i++) { $arg = $args[$i]; if (str_starts_with($arg, '--')) { $option = substr($arg, 2); $value = null; if (str_contains($option, '=')) { [$option, $value] = explode('=', $option, 2); } if ($option === 'param') { $param = $value ?? $args[++$i] ?? null; if ($param === null || !str_contains($param, '=')) { throw new InvalidArgumentException('--param expects key=value.'); } [$key, $paramValue] = explode('=', $param, 2); $params[$key] = $paramValue; continue; } if ($option === 'dry-run') { $options['dry-run'] = '1'; continue; } if (!in_array($option, ['username', 'password', 'endpoint', 'format'], true)) { throw new InvalidArgumentException("Unknown option --{$option}."); } $options[$option] = $value ?? $args[++$i] ?? ''; if ($options[$option] === '') { throw new InvalidArgumentException("--{$option} expects a value."); } continue; } if (str_contains($arg, '=')) { [$key, $value] = explode('=', $arg, 2); if ($key === '') { throw new InvalidArgumentException("Invalid empty parameter name in '{$arg}'."); } $params[$key] = $value; continue; } $positionals[] = $arg; } if (isset($options['format']) && !in_array($options['format'], ['pretty', 'json', 'raw'], true)) { throw new InvalidArgumentException('--format must be one of: pretty, json, raw.'); } return ['options' => $options, 'params' => $params, 'positionals' => $positionals]; } /** * @param list $args * @return array{options: array, params: array} */ function parseCdrArguments(array $args): array { $options = []; $params = []; $positionals = []; for ($i = 0; $i < count($args); $i++) { $arg = $args[$i]; if (str_starts_with($arg, '--')) { $option = substr($arg, 2); $value = null; if (str_contains($option, '=')) { [$option, $value] = explode('=', $option, 2); } if (in_array($option, ['answered', 'busy', 'failed'], true)) { $params[$option] = $value ?? '1'; continue; } if (in_array($option, ['noanswer', 'no-answer'], true)) { $params['noanswer'] = $value ?? '1'; continue; } if ($option === 'dry-run') { $options['dry-run'] = '1'; continue; } if (in_array($option, ['username', 'password', 'endpoint', 'format'], true)) { $options[$option] = $value ?? $args[++$i] ?? ''; if ($options[$option] === '') { throw new InvalidArgumentException("--{$option} expects a value."); } continue; } $paramName = normalizeCdrParamName($option); if ($paramName === null) { throw new InvalidArgumentException("Unknown cdr option --{$option}."); } $params[$paramName] = $value ?? $args[++$i] ?? ''; if ($params[$paramName] === '') { throw new InvalidArgumentException("--{$option} expects a value."); } continue; } if (str_contains($arg, '=')) { [$key, $value] = explode('=', $arg, 2); $paramName = normalizeCdrParamName($key); if ($paramName === null) { throw new InvalidArgumentException("Unknown cdr parameter '{$key}'."); } $params[$paramName] = $value; continue; } $positionals[] = $arg; } if (count($positionals) > 2) { throw new InvalidArgumentException('cdr accepts at most two positional arguments: date_from date_to.'); } if (isset($positionals[0]) && !isset($params['date_from'])) { $params['date_from'] = $positionals[0]; } if (isset($positionals[1]) && !isset($params['date_to'])) { $params['date_to'] = $positionals[1]; } $envTimezone = getenv('VOIPMS_TIMEZONE'); if (!isset($params['timezone']) && is_string($envTimezone) && $envTimezone !== '') { $params['timezone'] = $envTimezone; } foreach (['date_from', 'date_to', 'timezone'] as $required) { if (($params[$required] ?? '') === '') { throw new InvalidArgumentException("cdr requires {$required}. Example: voipms-cli.php cdr 2026-04-01 2026-04-20 --timezone=-4"); } } $callStatuses = ['answered', 'noanswer', 'busy', 'failed']; $hasCallStatus = false; foreach ($callStatuses as $status) { if (isset($params[$status])) { $hasCallStatus = true; break; } } if (!$hasCallStatus) { foreach ($callStatuses as $status) { $params[$status] = '1'; } } if (isset($options['format']) && !in_array($options['format'], ['pretty', 'json', 'raw'], true)) { throw new InvalidArgumentException('--format must be one of: pretty, json, raw.'); } return ['options' => $options, 'params' => $params]; } function normalizeCdrParamName(string $name): ?string { return match ($name) { 'from', 'date-from', 'date_from' => 'date_from', 'to', 'date-to', 'date_to' => 'date_to', 'timezone', 'tz' => 'timezone', 'type', 'call-type', 'call_type', 'calltype', 'did' => 'calltype', 'billing', 'call-billing', 'call_billing', 'callbilling' => 'callbilling', 'account' => 'account', 'answered' => 'answered', 'noanswer', 'no-answer' => 'noanswer', 'busy' => 'busy', 'failed' => 'failed', default => null, }; } function printHelp(?string $command): void { $catalog = commandCatalog(); if ($command !== null) { $entry = $catalog[$command] ?? null; if ($entry === null) { echo "{$command}\n\n"; echo "No local help is available for this method. The CLI can still call it generically:\n"; echo " voipms-cli.php {$command} key=value\n"; echo " php voipms-cli.php {$command} key=value\n"; return; } echo "{$command}\n\n"; echo $entry['description'] . "\n"; if (($entry['params'] ?? []) !== []) { echo "\nCommon parameters:\n"; foreach ($entry['params'] as $param) { echo " {$param}\n"; } } if (($entry['examples'] ?? []) !== []) { echo "\nExamples:\n"; foreach ($entry['examples'] as $example) { echo " voipms-cli.php {$example}\n"; } } return; } echo "voipms-cli.php [key=value ...] [options]\n\n"; echo "Commands:\n"; echo " help [command] Show general help or help for one command\n"; echo " help Show help for one API command\n"; echo " list List locally documented API commands\n"; echo " completion bash Print bash completion code\n"; echo " cdr Friendly alias for getCDR\n"; echo " Call any VoIP.ms REST/JSON API method\n\n"; echo "Options:\n"; echo " --username VALUE API username, defaults to VOIPMS_API_USERNAME\n"; echo " --password VALUE API password, defaults to VOIPMS_API_PASSWORD\n"; echo " --endpoint URL API endpoint, defaults to VOIPMS_API_ENDPOINT or VoIP.ms REST URL\n"; echo " --format FORMAT pretty, json, or raw. Default: pretty\n"; echo " --param key=value Pass an API parameter when shell quoting is awkward\n"; echo " --dry-run Show the API method and parameters without making a request\n\n"; echo "Examples:\n"; echo " VOIPMS_API_USERNAME='you@example.com' VOIPMS_API_PASSWORD='...' voipms-cli.php getBalance\n"; echo " voipms-cli.php cdr 2026-04-01 2026-04-20 --timezone=-4\n"; echo " voipms-cli.php getCDR date_from=2026-04-01 date_to=2026-04-20 timezone=-4\n"; echo " voipms-cli.php sendSMS did=15551234567 dst=15557654321 message='Hello'\n"; } function printCommandList(): void { $byCategory = []; foreach (commandCatalog() as $command => $entry) { $byCategory[$entry['category']][$command] = $entry; } foreach ($byCategory as $category => $commands) { echo "{$category}\n"; foreach ($commands as $command => $entry) { echo " " . str_pad($command, 28) . $entry['description'] . "\n"; } echo "\n"; } } function printBashCompletion(): void { $commands = array_merge(['help', 'list', 'completion'], array_keys(commandCatalog())); sort($commands); $commandWords = implode(' ', $commands); echo <<<'BASH' # Source this file in bash before using it: # source <(./voipms-cli.php completion bash) # # To make it persistent for this project, add that line to your shell profile # or write this output to a file loaded by bash-completion. _voipms_cli() { local cur prev commands COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" BASH; echo "\n commands=\"{$commandWords}\"\n"; echo <<<'BASH' if [[ ${COMP_CWORD} -eq 1 ]]; then COMPREPLY=( $(compgen -W "${commands}" -- "${cur}") ) return 0 fi case "${prev}" in --format) COMPREPLY=( $(compgen -W "pretty json raw" -- "${cur}") ) return 0 ;; --username|--password|--endpoint|--param) return 0 ;; esac if [[ "${cur}" == --* ]]; then COMPREPLY=( $(compgen -W "--username --password --endpoint --format --param --dry-run" -- "${cur}") ) return 0 fi } complete -o default -F _voipms_cli voipms-cli.php ./voipms-cli.php BASH; } /** * @param array{decoded: mixed, raw: string, http_code: int} $response */ function printResponse(array $response, string $format): void { if ($format === 'raw' || $response['decoded'] === null) { echo $response['raw']; if (!str_ends_with($response['raw'], "\n")) { echo "\n"; } return; } $flags = $format === 'pretty' ? JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES : JSON_UNESCAPED_SLASHES; echo json_encode($response['decoded'], $flags) . "\n"; } /** * @param array{decoded: mixed, raw: string, http_code: int} $response */ function printActionableHint(string $method, array $response): void { $decoded = $response['decoded']; if (!is_array($decoded)) { return; } if ($method === 'getCDR' && ($decoded['status'] ?? null) === 'no_callstatus') { fwrite(STDERR, "\nHint: getCDR requires at least one call status. The cdr alias now defaults to all statuses; raw getCDR needs answered=1, noanswer=1, busy=1, or failed=1.\n"); fwrite(STDERR, "Examples:\n"); fwrite(STDERR, " voipms-cli.php cdr 2026-04-01 2026-04-20 --timezone=-4\n"); fwrite(STDERR, " voipms-cli.php getCDR date_from=2026-04-01 date_to=2026-04-20 timezone=-4 answered=1 noanswer=1 busy=1 failed=1\n"); } } /** * @param array $params */ function printDryRun(string $method, array $params, string $format): void { $payload = [ 'method' => $method, 'params' => $params, ]; if ($format === 'raw') { echo http_build_query(array_merge(['method' => $method, 'content_type' => 'json'], $params)) . "\n"; return; } $flags = $format === 'pretty' ? JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES : JSON_UNESCAPED_SLASHES; echo json_encode($payload, $flags) . "\n"; } exit(main($argv));