320 lines
12 KiB
PHP
320 lines
12 KiB
PHP
<?php
|
|
|
|
namespace routes;
|
|
|
|
use classes\bird;
|
|
use classes\shelly;
|
|
use classes\slack;
|
|
use modules\shelly\helpers\shelly_device_switch;
|
|
use objects\department_gates_o;
|
|
use objects\department_relays_o;
|
|
use objects\departments_o;
|
|
use objects\groups_o;
|
|
use objects\subuser_grants_o;
|
|
use objects\subusers_o;
|
|
use objects\users_o;
|
|
use traits\bird_route_helpers_t;
|
|
use traits\route_t;
|
|
|
|
class birdVoiceWebhooksRoute
|
|
{
|
|
use route_t, bird_route_helpers_t;
|
|
|
|
public function run(): void
|
|
{
|
|
$this->post('/bird/voice/calls/webhook/inbound', function () {
|
|
global $response;
|
|
|
|
//self::requirePermission('modules_bird_voice_call_webhooks_trigger');
|
|
$client = new bird();
|
|
$support_phone_number_forwarding = "+4555299500"; // Used when the user is not found, and the caller stays on the line to be forwarded to support instead of the gate opening.
|
|
|
|
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
|
|
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
|
|
if ($ws === '') {
|
|
$ws = $this->getConfiguredWorkspaceId($client);
|
|
}
|
|
if ($ch === '') {
|
|
$ch = $this->getConfiguredChannelId($client);
|
|
}
|
|
if ($ws === '' || $ch === '') {
|
|
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
|
}
|
|
|
|
$payload = $this->getParametersAsArray();
|
|
$callId = $this->normalizeOptionalString($this->fromRequest('call_id') ?? $this->fromQuery('call_id'));
|
|
$digits = $this->normalizeOptionalString($payload['digits'] ?? $this->fromRequest('digits') ?? $this->fromQuery('digits'));
|
|
|
|
$caller = $this->extractCallerPhone($payload);
|
|
if ($caller === null) {
|
|
$this->say($client, $ws, $ch, $callId, 'Vi kunne ikke identificere dit telefonnummer. Kontakt venligst support.');
|
|
return;
|
|
}
|
|
|
|
[$countryCode, $localPhone] = $caller;
|
|
|
|
if ($digits !== '' && $callId !== '') {
|
|
$this->handleGatherResponse($client, $ws, $ch, $callId, $digits, $countryCode, $localPhone);
|
|
return;
|
|
}
|
|
|
|
$slack = new Slack();
|
|
$slack->send_message('Webhook received for incoming voice call (ID: ' . $callId . '): ' . json_encode($payload), 'Bird Voice Call Webhooks');
|
|
|
|
if (!$this->isRegisteredCaller($countryCode, $localPhone)) {
|
|
$this->say($client, $ws, $ch, $callId, 'Du er ikke sat op til automatisk portåbning med landekode ' . $countryCode . ' og telefonnummer ' . $localPhone . '. Kontakt venligst support for at blive sat op.');
|
|
return;
|
|
}
|
|
|
|
// Identify caller's matching gates
|
|
$matchingGates = $this->getCallerMatchingGates($countryCode, $localPhone);
|
|
if (empty($matchingGates)) {
|
|
$this->say($client, $ws, $ch, $callId, 'Vi kunne ikke finde nogen porte tilknyttet din profil. Kontakt venligst support.');
|
|
return;
|
|
}
|
|
|
|
// Prompt for selection
|
|
$this->promptForGateSelection($client, $ws, $ch, $callId, $matchingGates);
|
|
|
|
}, [
|
|
'modules_bird_voice_call_webhooks_trigger' => 'Trigger a voice call via Bird webhooks',
|
|
]);
|
|
}
|
|
|
|
protected function handleGatherResponse(bird $client, string $ws, string $ch, string $callId, string $digits, int $countryCode, int $phone): void
|
|
{
|
|
$matchingGates = $this->getCallerMatchingGates($countryCode, $phone);
|
|
|
|
$index = (int)$digits - 1;
|
|
if (!isset($matchingGates[$index])) {
|
|
$this->say($client, $ws, $ch, $callId, 'Ugyldigt valg. Ring venligst op igen.', true);
|
|
return;
|
|
}
|
|
|
|
$gate = $matchingGates[$index];
|
|
|
|
if ($this->callGateToOpen($gate)) {
|
|
$this->say($client, $ws, $ch, $callId, 'Åbner port: ' . $gate->name->value(), true);
|
|
} else {
|
|
$this->say($client, $ws, $ch, $callId, 'Kunne ikke aktivere relæet for ' . $gate->name->value() . '. Kontakt venligst support.', true);
|
|
}
|
|
}
|
|
|
|
protected function getCallerMatchingGates(int $countryCode, int $phone): array
|
|
{
|
|
$departments = $this->getCallerDepartments($countryCode, $phone);
|
|
$matchingGates = [];
|
|
$seenGateIds = [];
|
|
foreach ($departments as $deptId) {
|
|
$deptGates = (new department_gates_o())->getDepartmentGates($deptId);
|
|
foreach ($deptGates as $gate) {
|
|
if (!in_array((int)$gate->id, $seenGateIds)) {
|
|
$matchingGates[] = $gate;
|
|
$seenGateIds[] = (int)$gate->id;
|
|
}
|
|
}
|
|
}
|
|
return $matchingGates;
|
|
}
|
|
|
|
protected function promptForGateSelection(bird $client, string $ws, string $ch, string $callId, array $gates): void
|
|
{
|
|
$options = [];
|
|
foreach ($gates as $i => $gate) {
|
|
$options[] = 'Tast ' . ($i + 1) . ' for ' . $gate->name->value();
|
|
}
|
|
$text = 'Der blev fundet flere porte. ' . implode('. ', $options) . '. Afslut med firkantstasten.';
|
|
$this->say($client, $ws, $ch, $callId, $text);
|
|
|
|
|
|
header('Content-Type: application/json');
|
|
echo json_encode([]);
|
|
exit;
|
|
}
|
|
|
|
protected function getCallerDepartments(int $countryCode, int $phone): array
|
|
{
|
|
// Return all, since callers don't have specific department access, but the gates they have access to are determined by the departments they are in, and we want to include all possible gates for them.
|
|
$tmp = (new departments_o())->getFieldsWhere([
|
|
'visible' => 1,
|
|
], ['id']);
|
|
return array_map(function ($row) {
|
|
return (int)$row['id'];
|
|
}, $tmp);
|
|
}
|
|
|
|
|
|
|
|
public function callGateToOpen(department_gates_o $gate): bool
|
|
{
|
|
$config = (array)$gate->config->value();
|
|
|
|
if (!isset($config['type']) || $config['type'] !== 'PHONE_CALL') {
|
|
return false;
|
|
}
|
|
if (!isset($config['phone_number'])) {
|
|
return false;
|
|
}
|
|
$phoneConfig = $config['phone_number'];
|
|
$normalized = $this->normalizePhoneCandidate($phoneConfig);
|
|
if ($normalized === null) {
|
|
return false;
|
|
}
|
|
[$countryCode, $phone] = $normalized;
|
|
|
|
// Use the configured threshold or default to 10 seconds.
|
|
$timeout = (int)($config['call_duration_threshold'] ?? 10);
|
|
|
|
// Use bird voice call to call the phone number, and then hang up after it is accepted to trigger the gate.
|
|
$client = new bird();
|
|
try {
|
|
$client->callGateAndHangupWhenAccepted(
|
|
(int)$countryCode,
|
|
(int)$phone,
|
|
$timeout,
|
|
);
|
|
return true;
|
|
} catch (\Throwable $e) {
|
|
// If the call fails, log the error and return false
|
|
$slack = new Slack();
|
|
$slack->send_message('Failed to call gate for phone ' . $countryCode . $phone . ': ' . $e->getMessage(), 'Bird Voice Call Webhooks');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
protected function extractCallerPhone(array $payload): ?array
|
|
{
|
|
$candidates = [
|
|
$payload['phone_number'] ?? null,
|
|
$payload['source'] ?? null,
|
|
$payload['from'] ?? null,
|
|
];
|
|
|
|
foreach ($candidates as $candidate) {
|
|
$normalized = $this->normalizePhoneCandidate($candidate);
|
|
if ($normalized !== null) {
|
|
return $normalized;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected function normalizePhoneCandidate(mixed $candidate): ?array
|
|
{
|
|
if (is_array($candidate)) {
|
|
// Determine the country code from the phone number if possible, otherwise default to 45 (Denmark)
|
|
$phone = $candidate['phone_number'] ?? null; // E.g. +4512345678
|
|
// If the phone number starts with a + followed by the country code and then the local phone number, we can extract the country code and local phone number
|
|
|
|
if ($phone !== null) {
|
|
$country = $this->extractCountryCodeFromPhoneNumber($phone);
|
|
return $this->normalizePhone((string)$phone, $country);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
if (is_string($candidate) && trim($candidate) !== '') {
|
|
return $this->normalizePhone($candidate);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected function normalizePhone(string $raw, ?int $defaultCountryCode = 45): ?array
|
|
{
|
|
$trimmed = trim($raw);
|
|
if ($trimmed === '') {
|
|
return null;
|
|
}
|
|
$digits = preg_replace('/\D+/', '', $trimmed);
|
|
if (!is_string($digits) || $digits === '') {
|
|
return null;
|
|
}
|
|
|
|
$country = $defaultCountryCode;
|
|
$phone = $digits;
|
|
|
|
if (str_starts_with($trimmed, '+')) {
|
|
$extractedCountry = $this->extractCountryCodeFromPhoneNumber($trimmed);
|
|
if ($extractedCountry !== null) {
|
|
$country = $extractedCountry;
|
|
$phone = substr($digits, strlen((string)$extractedCountry));
|
|
} elseif (strlen($digits) > 8) {
|
|
$country = (int)substr($digits, 0, 2);
|
|
$phone = substr($digits, 2);
|
|
}
|
|
} elseif ($country !== null && str_starts_with($digits, (string)$country) && strlen($digits) > 8) {
|
|
$phone = substr($digits, strlen((string)$country));
|
|
}
|
|
|
|
if ($country === null || $country <= 0 || $phone === '') {
|
|
return null;
|
|
}
|
|
|
|
return [$country, (int)$phone];
|
|
}
|
|
|
|
protected function isRegisteredCaller(int $countryCode, int $phone): bool
|
|
{
|
|
$userRows = (new users_o())->getFieldsWhere([
|
|
'phone_country_code' => $countryCode,
|
|
'phone' => $phone,
|
|
], ['id']);
|
|
if (!empty($userRows)) {
|
|
return true;
|
|
}
|
|
|
|
return (new subusers_o())->getSubuserByPhone($countryCode, $phone) !== null;
|
|
}
|
|
|
|
protected function say(bird $client, string $ws, string $ch, string $callId, string $text, bool $hangup = false): void
|
|
{
|
|
global $response;
|
|
if ($callId !== '') {
|
|
try {
|
|
$payload = ['text' => $text];
|
|
if ($hangup) {
|
|
$payload['hangup'] = true;
|
|
}
|
|
$client->sayMessage($ws, $ch, $callId, $payload);
|
|
header('Content-Type: application/json');
|
|
echo json_encode([]);
|
|
exit;
|
|
} catch (\Throwable $e) {
|
|
// If REST API fails, fallback to standard response
|
|
throw new \Exception('Failed to say message and hang up: ' . $e->getMessage(), 0, $e);
|
|
}
|
|
} else {
|
|
throw new \Exception('Call ID is missing, cannot say message');
|
|
}
|
|
}
|
|
|
|
|
|
protected function extractCountryCodeFromPhoneNumber(string $phone): ?int
|
|
{
|
|
if (str_starts_with($phone, '+45')) {
|
|
return 45;
|
|
}
|
|
if (str_starts_with($phone, '+46')) {
|
|
return 46;
|
|
}
|
|
if (str_starts_with($phone, '+47')) {
|
|
return 47;
|
|
}
|
|
if (str_starts_with($phone, '+358')) {
|
|
return 358;
|
|
}
|
|
if (str_starts_with($phone, '+49')) {
|
|
return 49;
|
|
}
|
|
if (str_starts_with($phone, '+44')) {
|
|
return 44;
|
|
}
|
|
if (str_starts_with($phone, '+1')) {
|
|
return 1;
|
|
}
|
|
return null;
|
|
}
|
|
}
|