feat(inbound): add CID name prefix support per trunk and per inbound route

This commit is contained in:
sorcier committed 2026-11-03 20:01:16 +01:00
1 parent 9518e10616
commit 8804501c42
5 files changed
+188 -12

No files matched your search

+8 -3
View File
@@ -1132,6 +1132,7 @@ try {
$custom_reg_params = trim((string)($payload['custom_reg_params'] ?? '')); $custom_reg_params = trim((string)($payload['custom_reg_params'] ?? ''));
$custom_aor_params = trim((string)($payload['custom_aor_params'] ?? '')); $custom_aor_params = trim((string)($payload['custom_aor_params'] ?? ''));
$moh_sound_id = max(0, (int)($payload['moh_sound_id'] ?? 0)); $moh_sound_id = max(0, (int)($payload['moh_sound_id'] ?? 0));
$cid_name_prefix = trim((string)($payload['cid_name_prefix'] ?? ''));
$enabled = isset($payload['enabled']) ? (int)!empty($payload['enabled']) : 1; $enabled = isset($payload['enabled']) ? (int)!empty($payload['enabled']) : 1;
$didRoutesRaw = is_array($payload['did_routes'] ?? null) ? $payload['did_routes'] : []; $didRoutesRaw = is_array($payload['did_routes'] ?? null) ? $payload['did_routes'] : [];
@@ -1201,6 +1202,7 @@ try {
':custom_reg_params' => $custom_reg_params, ':custom_reg_params' => $custom_reg_params,
':custom_aor_params' => $custom_aor_params, ':custom_aor_params' => $custom_aor_params,
':moh_sound_id' => $moh_sound_id, ':moh_sound_id' => $moh_sound_id,
':cid_name_prefix' => $cid_name_prefix,
':enabled' => $enabled, ':enabled' => $enabled,
]; ];
@@ -1227,7 +1229,7 @@ try {
inbound_dest = :inbound_dest, did_routes_json = :did_routes_json, inbound_dest = :inbound_dest, did_routes_json = :did_routes_json,
outbound_prefix = :outbound_prefix, custom_endpoint_params = :custom_endpoint_params, outbound_prefix = :outbound_prefix, custom_endpoint_params = :custom_endpoint_params,
custom_reg_params = :custom_reg_params, custom_aor_params = :custom_aor_params, custom_reg_params = :custom_reg_params, custom_aor_params = :custom_aor_params,
moh_sound_id = :moh_sound_id, moh_sound_id = :moh_sound_id, cid_name_prefix = :cid_name_prefix,
enabled = :enabled enabled = :enabled
WHERE id = :id WHERE id = :id
'); ');
@@ -1242,7 +1244,8 @@ try {
trust_id_inbound, codecs, dtmf_mode, media_encryption, nat_symmetric, direct_media, trust_id_inbound, codecs, dtmf_mode, media_encryption, nat_symmetric, direct_media,
qualify_frequency, max_channels, timers, language, rtp_keepalive, t38_udptl, qualify_frequency, max_channels, timers, language, rtp_keepalive, t38_udptl,
fax_detect, inband_progress, match_ips, context, inbound_dest, did_routes_json, fax_detect, inband_progress, match_ips, context, inbound_dest, did_routes_json,
outbound_prefix, custom_endpoint_params, custom_reg_params, custom_aor_params, moh_sound_id, enabled outbound_prefix, custom_endpoint_params, custom_reg_params, custom_aor_params, moh_sound_id,
cid_name_prefix, enabled
) VALUES ( ) VALUES (
:name, :host, :port, :outbound_proxy, :transport, :auth_mode, :username, :auth_username, :secret, :name, :host, :port, :outbound_proxy, :transport, :auth_mode, :username, :auth_username, :secret,
:registration_mode, :register, :server_uri, :client_uri, :contact_user, :expiration, :retry_interval, :registration_mode, :register, :server_uri, :client_uri, :contact_user, :expiration, :retry_interval,
@@ -1251,7 +1254,8 @@ try {
:trust_id_inbound, :codecs, :dtmf_mode, :media_encryption, :nat_symmetric, :direct_media, :trust_id_inbound, :codecs, :dtmf_mode, :media_encryption, :nat_symmetric, :direct_media,
:qualify_frequency, :max_channels, :timers, :language, :rtp_keepalive, :t38_udptl, :qualify_frequency, :max_channels, :timers, :language, :rtp_keepalive, :t38_udptl,
:fax_detect, :inband_progress, :match_ips, :context, :inbound_dest, :did_routes_json, :fax_detect, :inband_progress, :match_ips, :context, :inbound_dest, :did_routes_json,
:outbound_prefix, :custom_endpoint_params, :custom_reg_params, :custom_aor_params, :moh_sound_id, :enabled :outbound_prefix, :custom_endpoint_params, :custom_reg_params, :custom_aor_params, :moh_sound_id,
:cid_name_prefix, :enabled
) )
ON CONFLICT(name) DO UPDATE SET ON CONFLICT(name) DO UPDATE SET
host = excluded.host, port = excluded.port, outbound_proxy = excluded.outbound_proxy, host = excluded.host, port = excluded.port, outbound_proxy = excluded.outbound_proxy,
@@ -1274,6 +1278,7 @@ try {
outbound_prefix = excluded.outbound_prefix, custom_endpoint_params = excluded.custom_endpoint_params, outbound_prefix = excluded.outbound_prefix, custom_endpoint_params = excluded.custom_endpoint_params,
custom_reg_params = excluded.custom_reg_params, custom_aor_params = excluded.custom_aor_params, custom_reg_params = excluded.custom_reg_params, custom_aor_params = excluded.custom_aor_params,
moh_sound_id = excluded.moh_sound_id, moh_sound_id = excluded.moh_sound_id,
cid_name_prefix = excluded.cid_name_prefix,
enabled = excluded.enabled enabled = excluded.enabled
'); ');
$stmt->execute($params); $stmt->execute($params);
+18 -1
View File
@@ -1646,6 +1646,7 @@ function renderTrunksTable() {
<td class="py-3.5 px-4"> <td class="py-3.5 px-4">
<div class="font-mono text-xs text-emerald-300">→ ${escapeHtml(t.inbound_dest)}</div> <div class="font-mono text-xs text-emerald-300">→ ${escapeHtml(t.inbound_dest)}</div>
<div class="text-[11px] text-slate-400"> <div class="text-[11px] text-slate-400">
${t.cid_name_prefix ? `<span class="px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-300 text-[10px] font-mono mr-1 font-semibold">CID: ${escapeHtml(t.cid_name_prefix)}</span>` : ''}
${didCount > 0 ? `<span class="text-indigo-300 font-semibold">${didCount} règle(s) entrante(s)</span>` : 'Tous numéros (Any)'} ${didCount > 0 ? `<span class="text-indigo-300 font-semibold">${didCount} règle(s) entrante(s)</span>` : 'Tous numéros (Any)'}
${blockCount > 0 ? `<span class="ml-1 px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-300 text-[10px] font-semibold">${blockCount} blocage(s) CID</span>` : ''} ${blockCount > 0 ? `<span class="ml-1 px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-300 text-[10px] font-semibold">${blockCount} blocage(s) CID</span>` : ''}
${tcCount > 0 ? `<span class="ml-1 px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-300 text-[10px] font-semibold">⏱️ ${tcCount} horaire(s)</span>` : ''} ${tcCount > 0 ? `<span class="ml-1 px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-300 text-[10px] font-semibold">⏱️ ${tcCount} horaire(s)</span>` : ''}
@@ -1831,6 +1832,7 @@ function addTrunkInboundRouteRow(cid = 'Any', did = 'Any', description = '', des
const tcFinish = (timeOpts && timeOpts.time_finish) ? timeOpts.time_finish : '18:00'; const tcFinish = (timeOpts && timeOpts.time_finish) ? timeOpts.time_finish : '18:00';
const tcWeekdays = (timeOpts && timeOpts.weekdays) ? timeOpts.weekdays : 'mon-fri'; const tcWeekdays = (timeOpts && timeOpts.weekdays) ? timeOpts.weekdays : 'mon-fri';
const tcClosed = (timeOpts && timeOpts.time_dest_closed) ? timeOpts.time_dest_closed : 'terminate-hangup'; const tcClosed = (timeOpts && timeOpts.time_dest_closed) ? timeOpts.time_dest_closed : 'terminate-hangup';
const tcCidPrefix = (timeOpts && timeOpts.cid_prefix) ? timeOpts.cid_prefix : '';
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'trunk-did-row border-b border-anthracite-700/60 transition hover:bg-anthracite-800/40'; row.className = 'trunk-did-row border-b border-anthracite-700/60 transition hover:bg-anthracite-800/40';
@@ -1899,6 +1901,15 @@ function addTrunkInboundRouteRow(cid = 'Any', did = 'Any', description = '', des
</select> </select>
</div> </div>
</div> </div>
<div class="flex flex-wrap items-center gap-2 pt-2 border-t border-anthracite-800 text-xs">
<label class="text-[11px] font-semibold text-slate-300 flex items-center gap-1">
<span>🏷️ Préfixe CID spécifique à cette règle :</span>
</label>
<input type="text" placeholder="ex: [Livebox] (vide = hériter du Trunk)" value="${escapeHtml(tcCidPrefix)}"
class="trunk-did-prefix px-2.5 py-1 rounded bg-anthracite-900 border border-anthracite-700 text-xs text-white font-mono w-56 focus:outline-none focus:border-indigo-500">
<span class="text-[10px] text-slate-400">Ajouté devant le nom de l'appelant sur le téléphone (laisser vide pour utiliser le préfixe général du Trunk).</span>
</div>
</div> </div>
`; `;
@@ -2310,6 +2321,9 @@ function openTrunkModal(trunk = null) {
// Contexte & Destination entrante (Totalement libres + assistés) // Contexte & Destination entrante (Totalement libres + assistés)
form.elements['context'].value = trunk ? (trunk.context || 'from-pstn-toheader') : 'from-pstn-toheader'; form.elements['context'].value = trunk ? (trunk.context || 'from-pstn-toheader') : 'from-pstn-toheader';
if (form.elements['cid_name_prefix']) {
form.elements['cid_name_prefix'].value = trunk ? (trunk.cid_name_prefix || '') : '';
}
populateInboundDestDropdown(trunk ? (trunk.inbound_dest || '101') : '101'); populateInboundDestDropdown(trunk ? (trunk.inbound_dest || '101') : '101');
// Musique d'attente (MoH) spécifique à ce Trunk // Musique d'attente (MoH) spécifique à ce Trunk
@@ -2368,7 +2382,8 @@ function openTrunkModal(trunk = null) {
time_start: r.time_start || '08:30', time_start: r.time_start || '08:30',
time_finish: r.time_finish || '18:00', time_finish: r.time_finish || '18:00',
weekdays: r.weekdays || 'mon-fri', weekdays: r.weekdays || 'mon-fri',
time_dest_closed: r.time_dest_closed || 'terminate-hangup' time_dest_closed: r.time_dest_closed || 'terminate-hangup',
cid_prefix: r.cid_prefix || ''
} }
); );
}); });
@@ -3712,6 +3727,7 @@ document.addEventListener('DOMContentLoaded', () => {
const tcFinish = row.querySelector('.trunk-tc-finish')?.value || '18:00'; const tcFinish = row.querySelector('.trunk-tc-finish')?.value || '18:00';
const weekdays = row.querySelector('.trunk-tc-weekdays')?.value || 'mon-fri'; const weekdays = row.querySelector('.trunk-tc-weekdays')?.value || 'mon-fri';
const tcClosed = row.querySelector('.trunk-tc-closed-dest')?.value || 'terminate-hangup'; const tcClosed = row.querySelector('.trunk-tc-closed-dest')?.value || 'terminate-hangup';
const cidPrefix = row.querySelector('.trunk-did-prefix')?.value.trim() || '';
if (dest) { if (dest) {
didRoutes.push({ didRoutes.push({
@@ -3719,6 +3735,7 @@ document.addEventListener('DOMContentLoaded', () => {
did, did,
description: desc, description: desc,
destination: dest, destination: dest,
cid_prefix: cidPrefix,
time_condition_enabled: tcEnabled, time_condition_enabled: tcEnabled,
time_start: tcStart, time_start: tcStart,
time_finish: tcFinish, time_finish: tcFinish,
+17
View File
@@ -3540,6 +3540,23 @@ $currentUser = Auth::getUsername();
</div> </div>
</div> </div>
<!-- Préfixe CallerID (Nom affiché sur les téléphones IP pour identifier la ligne entrante) -->
<div class="p-3.5 rounded-xl bg-anthracite-950 border border-anthracite-700/80 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div class="max-w-xl">
<label class="block text-xs font-bold text-white mb-0.5 flex items-center gap-1.5">
<span>🏷️ Préfixe Nom Appelant (CID Name Prefix)</span>
<span class="px-1.5 py-0.2 rounded bg-indigo-500/20 text-indigo-300 text-[10px] font-mono font-semibold">FreePBX CID Prefix</span>
</label>
<p class="text-[11px] text-slate-300">
Préfixe inséré devant le nom ou numéro de l'appelant sur l'écran de vos téléphones IP pour identifier la ligne appelée (ex: <code class="text-emerald-300">[Livebox] </code>, <code class="text-indigo-300">[Orange] </code>, <code class="text-cyan-300">[Fixe] </code>).
</p>
</div>
<div class="min-w-[240px]">
<input type="text" name="cid_name_prefix" placeholder="ex: [Livebox] "
class="w-full px-3 py-2 rounded-xl bg-anthracite-900 border border-anthracite-700 text-xs text-white font-mono focus:outline-none focus:border-indigo-500">
</div>
</div>
<div class="p-4 rounded-xl bg-anthracite-950 border border-anthracite-700 space-y-3"> <div class="p-4 rounded-xl bg-anthracite-950 border border-anthracite-700 space-y-3">
<div class="flex flex-wrap items-center justify-between gap-3"> <div class="flex flex-wrap items-center justify-between gap-3">
<div> <div>
+143 -8
View File
@@ -1309,16 +1309,19 @@ class ConfigGenerator
// 2b-bis. Contextes standards FreePBX / Passerelles FXO : [from-trunk] et [from-pstn] // 2b-bis. Contextes standards FreePBX / Passerelles FXO : [from-trunk] et [from-pstn]
$lines[] = "[from-trunk]"; $lines[] = "[from-trunk]";
$lines[] = "exten => _[+*#0-9a-zA-Z].,1,NoOp(Appel entrant trunk [from-trunk] EXTEN=\${EXTEN} Appelant=\${CALLERID(num)})"; $lines[] = "exten => _[+*#0-9a-zA-Z].,1,NoOp(Appel entrant trunk [from-trunk] EXTEN=\${EXTEN} Endpoint=\${CHANNEL(endpoint)} Appelant=\${CALLERID(num)})";
$lines[] = " same => n,ExecIf(\$[\"\${CALLERID(num):0:3}\" = \"+33\"]?Set(CALLERID(num)=0\${CALLERID(num):3}))"; $lines[] = " same => n,ExecIf(\$[\"\${CALLERID(num):0:3}\" = \"+33\"]?Set(CALLERID(num)=0\${CALLERID(num):3}))";
$lines[] = " same => n,ExecIf(\$[\"\${CALLERID(num):0:4}\" = \"0033\"]?Set(CALLERID(num)=0\${CALLERID(num):4}))"; $lines[] = " same => n,ExecIf(\$[\"\${CALLERID(num):0:4}\" = \"0033\"]?Set(CALLERID(num)=0\${CALLERID(num):4}))";
$lines[] = " same => n,GotoIf(\$[\"\${DB(blacklist/\${CALLERID(num)})}\" != \"\"]?app-blacklisted,s,1)"; $lines[] = " same => n,GotoIf(\$[\"\${DB(blacklist/\${CALLERID(num)})}\" != \"\"]?app-blacklisted,s,1)";
$lines[] = " same => n,ExecIf(\$[\"\${EXTEN:0:3}\" = \"+33\"]?Goto(from-external,0\${EXTEN:3},1))"; $lines[] = " same => n,ExecIf(\$[\"\${EXTEN:0:3}\" = \"+33\"]?Set(EXTEN=0\${EXTEN:3}))";
$lines[] = " same => n,GotoIf(\$[\"\${DIALPLAN_EXISTS(from-trunk-\${CHANNEL(endpoint)},\${EXTEN},1)}\" = \"1\"]?from-trunk-\${CHANNEL(endpoint)},\${EXTEN},1)";
$lines[] = " same => n,GotoIf(\$[\"\${DIALPLAN_EXISTS(from-trunk-\${CHANNEL(endpoint)},s,1)}\" = \"1\"]?from-trunk-\${CHANNEL(endpoint)},s,1)";
$lines[] = " same => n,Goto(from-external,\${EXTEN},1)"; $lines[] = " same => n,Goto(from-external,\${EXTEN},1)";
$lines[] = "exten => s,1,NoOp(Appel entrant trunk [from-trunk] sur s - Appelant=\${CALLERID(num)})"; $lines[] = "exten => s,1,NoOp(Appel entrant trunk [from-trunk] sur s - Endpoint=\${CHANNEL(endpoint)} Appelant=\${CALLERID(num)})";
$lines[] = " same => n,ExecIf(\$[\"\${CALLERID(num):0:3}\" = \"+33\"]?Set(CALLERID(num)=0\${CALLERID(num):3}))"; $lines[] = " same => n,ExecIf(\$[\"\${CALLERID(num):0:3}\" = \"+33\"]?Set(CALLERID(num)=0\${CALLERID(num):3}))";
$lines[] = " same => n,ExecIf(\$[\"\${CALLERID(num):0:4}\" = \"0033\"]?Set(CALLERID(num)=0\${CALLERID(num):4}))"; $lines[] = " same => n,ExecIf(\$[\"\${CALLERID(num):0:4}\" = \"0033\"]?Set(CALLERID(num)=0\${CALLERID(num):4}))";
$lines[] = " same => n,GotoIf(\$[\"\${DB(blacklist/\${CALLERID(num)})}\" != \"\"]?app-blacklisted,s,1)"; $lines[] = " same => n,GotoIf(\$[\"\${DB(blacklist/\${CALLERID(num)})}\" != \"\"]?app-blacklisted,s,1)";
$lines[] = " same => n,GotoIf(\$[\"\${DIALPLAN_EXISTS(from-trunk-\${CHANNEL(endpoint)},s,1)}\" = \"1\"]?from-trunk-\${CHANNEL(endpoint)},s,1)";
$lines[] = " same => n,Goto(from-external,s,1)"; $lines[] = " same => n,Goto(from-external,s,1)";
$lines[] = ""; $lines[] = "";
$lines[] = "[from-pstn]"; $lines[] = "[from-pstn]";
@@ -1326,6 +1329,30 @@ class ConfigGenerator
$lines[] = "exten => s,1,Goto(from-trunk,s,1)"; $lines[] = "exten => s,1,Goto(from-trunk,s,1)";
$lines[] = ""; $lines[] = "";
// 2b-ter. Contextes dédiés par Trunk pour isolation des routes et préfixe CID
foreach ($trunks as $t) {
$epName = ($t['registration_mode'] === 'receive' && trim((string)($t['username'] ?? '')) !== '')
? trim((string)$t['username'])
: trim((string)$t['name']);
if ($epName === '') {
continue;
}
$trunkCtx = "from-trunk-{$epName}";
$lines[] = "; --- Contexte Entrant dédié au Trunk {$t['name']} (Endpoint: {$epName}) ---";
$lines[] = "[{$trunkCtx}]";
foreach ($this->generateTrunkInboundRoutes($t, $mohSoundsMap, "trunk_{$t['id']}") as $l) {
$lines[] = $l;
}
$lines[] = "";
if ($t['name'] !== $epName) {
$lines[] = "[from-trunk-{$t['name']}]";
$lines[] = "exten => _[+*#0-9a-zA-Z].,1,Goto({$trunkCtx},\${EXTEN},1)";
$lines[] = "exten => s,1,Goto({$trunkCtx},s,1)";
$lines[] = "";
}
}
// Contexte pour appels rejetés par la liste noire // Contexte pour appels rejetés par la liste noire
$lines[] = "[app-blacklisted]"; $lines[] = "[app-blacklisted]";
$lines[] = "exten => s,1,NoOp(Appelant \${CALLERID(num)} bloque par la liste noire)"; $lines[] = "exten => s,1,NoOp(Appelant \${CALLERID(num)} bloque par la liste noire)";
@@ -1352,6 +1379,7 @@ class ConfigGenerator
$setMohLines[] = " same => n,Set(CHANNEL(musicclass)={$trunkMohClass})"; $setMohLines[] = " same => n,Set(CHANNEL(musicclass)={$trunkMohClass})";
} }
$trunkCidPrefix = trim((string)($t['cid_name_prefix'] ?? ''));
$routes = self::normalizeInboundRoutes((string)($t['did_routes_json'] ?? '[]')); $routes = self::normalizeInboundRoutes((string)($t['did_routes_json'] ?? '[]'));
$routeIdx = 0; $routeIdx = 0;
foreach ($routes as $rule) { foreach ($routes as $rule) {
@@ -1382,12 +1410,12 @@ class ConfigGenerator
} }
$lines[] = "; Inbound Route [{$desc}] ({$t['name']}) : DID={$didPattern} / Appelant=Any{$timeInfo}"; $lines[] = "; Inbound Route [{$desc}] ({$t['name']}) : DID={$didPattern} / Appelant=Any{$timeInfo}";
$lines[] = "exten => {$didPattern},1,NoOp(Inbound [{$desc}] Appelant=\${CALLERID(num)})"; $lines[] = "exten => {$didPattern},1,NoOp(Inbound [{$desc}] Appelant=\${CALLERID(num)})";
foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$t['id']}_{$routeIdx}_did") as $rl) { foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$t['id']}_{$routeIdx}_did", $trunkCidPrefix) as $rl) {
$lines[] = $rl; $lines[] = $rl;
} }
if ($isAnyDid) { if ($isAnyDid) {
$lines[] = "exten => s,1,NoOp(Inbound [{$desc}] sur s Appelant=\${CALLERID(num)})"; $lines[] = "exten => s,1,NoOp(Inbound [{$desc}] sur s Appelant=\${CALLERID(num)})";
foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$t['id']}_{$routeIdx}_s") as $rl) { foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$t['id']}_{$routeIdx}_s", $trunkCidPrefix) as $rl) {
$lines[] = $rl; $lines[] = $rl;
} }
} }
@@ -1409,12 +1437,12 @@ class ConfigGenerator
foreach ($cidVariants as $cidVar) { foreach ($cidVariants as $cidVar) {
$vIdx++; $vIdx++;
$lines[] = "exten => {$didPattern}/{$cidVar},1,NoOp(Inbound Filter [{$desc}] CID=\${CALLERID(num)})"; $lines[] = "exten => {$didPattern}/{$cidVar},1,NoOp(Inbound Filter [{$desc}] CID=\${CALLERID(num)})";
foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$t['id']}_{$routeIdx}_cid_{$vIdx}") as $rl) { foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$t['id']}_{$routeIdx}_cid_{$vIdx}", $trunkCidPrefix) as $rl) {
$lines[] = $rl; $lines[] = $rl;
} }
if ($isAnyDid) { if ($isAnyDid) {
$lines[] = "exten => s/{$cidVar},1,NoOp(Inbound Filter [{$desc}] sur s CID=\${CALLERID(num)})"; $lines[] = "exten => s/{$cidVar},1,NoOp(Inbound Filter [{$desc}] sur s CID=\${CALLERID(num)})";
foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$t['id']}_{$routeIdx}_scid_{$vIdx}") as $rl) { foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$t['id']}_{$routeIdx}_scid_{$vIdx}", $trunkCidPrefix) as $rl) {
$lines[] = $rl; $lines[] = $rl;
} }
} }
@@ -1918,6 +1946,7 @@ class ConfigGenerator
'did' => $did, 'did' => $did,
'description' => $desc ?: ($cid !== 'Any' ? "Filtre {$cid}" : "Route {$did}"), 'description' => $desc ?: ($cid !== 'Any' ? "Filtre {$cid}" : "Route {$did}"),
'destination' => $dest, 'destination' => $dest,
'cid_prefix' => trim((string)($item['cid_prefix'] ?? '')),
'time_condition_enabled' => (int)$timeCondition, 'time_condition_enabled' => (int)$timeCondition,
'time_start' => $timeStart, 'time_start' => $timeStart,
'time_finish' => $timeFinish, 'time_finish' => $timeFinish,
@@ -1950,7 +1979,106 @@ class ConfigGenerator
return $out; return $out;
} }
private function buildInboundRouteLines(array $rule, array $setMohLines, string $desc, string $tagId): array private function generateTrunkInboundRoutes(array $t, array $mohSoundsMap, string $tagPrefix): array
{
$lines = [];
$mohSoundId = (int)($t['moh_sound_id'] ?? 0);
$trunkMohClass = ($mohSoundId > 0 && isset($mohSoundsMap[$mohSoundId]))
? 'moh-sound-' . $mohSoundsMap[$mohSoundId]['slug']
: 'default';
$setMohLines = [];
if ($trunkMohClass !== 'default') {
$setMohLines[] = " same => n,Set(__MOHCLASS={$trunkMohClass})";
$setMohLines[] = " same => n,Set(CHANNEL(musicclass)={$trunkMohClass})";
}
$trunkCidPrefix = trim((string)($t['cid_name_prefix'] ?? ''));
$routes = self::normalizeInboundRoutes((string)($t['did_routes_json'] ?? '[]'));
$routeIdx = 0;
$hasAnyRoute = false;
foreach ($routes as $rule) {
$routeIdx++;
$cidRaw = trim((string)($rule['cid'] ?? 'Any'));
$didRaw = trim((string)($rule['did'] ?? 'Any'));
$desc = trim((string)($rule['description'] ?? 'Route entrante'));
$dest = trim((string)($rule['destination'] ?? $t['inbound_dest'] ?? 'hangup'));
$isAnyDid = ($didRaw === '' || strcasecmp($didRaw, 'any') === 0 || $didRaw === '*' || $didRaw === '_X.');
$isAnyCid = ($cidRaw === '' || strcasecmp($cidRaw, 'any') === 0 || $cidRaw === '*');
$didPattern = $isAnyDid ? '_X.' : preg_replace('/[^0-9+XZN._\[\]\-]/', '', $didRaw);
if ($didPattern === '') {
$didPattern = '_X.';
}
$timeInfo = !empty($rule['time_condition_enabled'])
? " [Horaires: {$rule['time_start']}-{$rule['time_finish']} {$rule['weekdays']}]"
: "";
if ($isAnyCid) {
if ($didPattern === '_X.') {
$hasAnyRoute = true;
}
$lines[] = "; Inbound Route [{$desc}] : DID={$didPattern} / Appelant=Any{$timeInfo}";
$lines[] = "exten => {$didPattern},1,NoOp(Inbound [{$desc}] Appelant=\${CALLERID(num)})";
foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$tagPrefix}_{$routeIdx}_did", $trunkCidPrefix) as $rl) {
$lines[] = $rl;
}
if ($isAnyDid) {
$lines[] = "exten => s,1,NoOp(Inbound [{$desc}] sur s Appelant=\${CALLERID(num)})";
foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$tagPrefix}_{$routeIdx}_s", $trunkCidPrefix) as $rl) {
$lines[] = $rl;
}
}
} else {
$cidClean = preg_replace('/[^0-9+XZN._\[\]\-]/', '', $cidRaw);
if ($cidClean === '') {
continue;
}
$cidVariants = [$cidClean];
if (str_starts_with($cidClean, '_0') && strlen($cidClean) > 2 && $cidClean[2] !== '0') {
$rest = substr($cidClean, 2);
$cidVariants[] = '_0033' . $rest;
$cidVariants[] = '_+33' . $rest;
}
$lines[] = "; Inbound Route Filtrage Appelant [{$desc}] : DID={$didPattern} / CID={$cidClean}{$timeInfo}";
$vIdx = 0;
foreach ($cidVariants as $cidVar) {
$vIdx++;
$lines[] = "exten => {$didPattern}/{$cidVar},1,NoOp(Inbound Filter [{$desc}] CID=\${CALLERID(num)})";
foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$tagPrefix}_{$routeIdx}_cid_{$vIdx}", $trunkCidPrefix) as $rl) {
$lines[] = $rl;
}
if ($isAnyDid) {
$lines[] = "exten => s/{$cidVar},1,NoOp(Inbound Filter [{$desc}] sur s CID=\${CALLERID(num)})";
foreach ($this->buildInboundRouteLines($rule, $setMohLines, $desc, "{$tagPrefix}_{$routeIdx}_scid_{$vIdx}", $trunkCidPrefix) as $rl) {
$lines[] = $rl;
}
}
}
}
}
if (!$hasAnyRoute) {
$defaultDest = trim((string)($t['inbound_dest'] ?? 'hangup')) ?: 'hangup';
$lines[] = "; Fallback Inbound Trunk {$t['name']}";
$lines[] = "exten => _X.,1,NoOp(Inbound Trunk {$t['name']} par defaut vers \${EXTEN})";
foreach ($this->buildInboundRouteLines(['destination' => $defaultDest], $setMohLines, "Fallback {$t['name']}", "{$tagPrefix}_fb", $trunkCidPrefix) as $rl) {
$lines[] = $rl;
}
$lines[] = "exten => s,1,NoOp(Inbound Trunk {$t['name']} sur s)";
foreach ($this->buildInboundRouteLines(['destination' => $defaultDest], $setMohLines, "Fallback {$t['name']}", "{$tagPrefix}_fbs", $trunkCidPrefix) as $rl) {
$lines[] = $rl;
}
}
return $lines;
}
private function buildInboundRouteLines(array $rule, array $setMohLines, string $desc, string $tagId, string $trunkCidPrefix = ''): array
{ {
$dest = trim((string)($rule['destination'] ?? 'hangup')); $dest = trim((string)($rule['destination'] ?? 'hangup'));
$timeEnabled = !empty($rule['time_condition_enabled']); $timeEnabled = !empty($rule['time_condition_enabled']);
@@ -1965,6 +2093,13 @@ class ConfigGenerator
$out[] = $sml; $out[] = $sml;
} }
$cidPrefix = trim((string)($rule['cid_prefix'] ?? '')) ?: $trunkCidPrefix;
if ($cidPrefix !== '' && !str_starts_with($dest, 'terminate-') && $dest !== 'hangup' && $dest !== 'busy' && $dest !== 'congestion') {
$formattedPrefix = rtrim($cidPrefix) . ' ';
$out[] = " same => n,ExecIf(\$[\"\${CALLERID(name)}\" = \"\"]?Set(CALLERID(name)=\${CALLERID(num)}))";
$out[] = " same => n,Set(CALLERID(name)={$formattedPrefix}\${CALLERID(name)})";
}
if ($timeEnabled) { if ($timeEnabled) {
$tag = "tc_{$tagId}"; $tag = "tc_{$tagId}";
$out[] = " same => n,GotoIfTime({$timeRange},{$weekdays},*,*?{$tag}_open)"; $out[] = " same => n,GotoIfTime({$timeRange},{$weekdays},*,*?{$tag}_open)";
+2
View File
@@ -163,6 +163,7 @@ class Database
did_routes_json TEXT NOT NULL DEFAULT '{}', did_routes_json TEXT NOT NULL DEFAULT '{}',
outbound_prefix VARCHAR(32) NOT NULL DEFAULT '', outbound_prefix VARCHAR(32) NOT NULL DEFAULT '',
custom_endpoint_params TEXT NOT NULL DEFAULT '', custom_endpoint_params TEXT NOT NULL DEFAULT '',
cid_name_prefix VARCHAR(64) NOT NULL DEFAULT '',
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)), enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
@@ -576,6 +577,7 @@ class Database
'custom_reg_params' => "TEXT NOT NULL DEFAULT ''", 'custom_reg_params' => "TEXT NOT NULL DEFAULT ''",
'custom_aor_params' => "TEXT NOT NULL DEFAULT ''", 'custom_aor_params' => "TEXT NOT NULL DEFAULT ''",
'moh_sound_id' => "INTEGER NOT NULL DEFAULT 0", 'moh_sound_id' => "INTEGER NOT NULL DEFAULT 0",
'cid_name_prefix' => "VARCHAR(64) NOT NULL DEFAULT ''",
]; ];
foreach ($extraColumns as $colName => $colDef) { foreach ($extraColumns as $colName => $colDef) {