KI-AGENT: Easybell SIP-Trunk integrieren
This commit is contained in:
@@ -64,11 +64,34 @@ const testAccounts = () => [
|
||||
},
|
||||
]
|
||||
|
||||
const trunkProviders = {
|
||||
telekom: {
|
||||
key: "telekom",
|
||||
label: "Telekom",
|
||||
defaultRegistrar: "tel.t-online.de",
|
||||
aorContactIncludesUser: false,
|
||||
},
|
||||
easybell: {
|
||||
key: "easybell",
|
||||
label: "Easybell",
|
||||
defaultRegistrar: "voip.easybell.de",
|
||||
aorContactIncludesUser: true,
|
||||
},
|
||||
} as const
|
||||
|
||||
type TrunkProviderKey = keyof typeof trunkProviders
|
||||
|
||||
const normalizeTrunkProvider = (provider: string | null | undefined): TrunkProviderKey =>
|
||||
provider === "easybell" ? "easybell" : "telekom"
|
||||
|
||||
const trunkProviderConfig = (provider: string | null | undefined) =>
|
||||
trunkProviders[normalizeTrunkProvider(provider)]
|
||||
|
||||
const sanitizeTrunk = (trunk: any) => ({
|
||||
id: trunk?.id || null,
|
||||
provider: trunk?.provider || "telekom",
|
||||
provider: normalizeTrunkProvider(trunk?.provider),
|
||||
enabled: Boolean(trunk?.enabled),
|
||||
registrar: trunk?.registrar || "tel.t-online.de",
|
||||
registrar: trunk?.registrar || trunkProviderConfig(trunk?.provider).defaultRegistrar,
|
||||
sipUser: trunk?.sipUser || "",
|
||||
authUser: trunk?.authUser || "",
|
||||
passwordConfigured: Boolean(trunk?.password),
|
||||
@@ -146,45 +169,51 @@ const durationSeconds = (startedAt?: Date | null, endedAt?: Date | null) => {
|
||||
const asteriskValue = (value: string | null | undefined) =>
|
||||
String(value || "").replace(/[\r\n]/g, "").trim()
|
||||
|
||||
const renderTelekomPjsipConfig = (trunk: any) => {
|
||||
const renderProviderPjsipConfig = (trunk: any) => {
|
||||
const provider = trunkProviderConfig(trunk?.provider)
|
||||
|
||||
if (!trunk?.enabled) {
|
||||
return [
|
||||
"; Von FEDEO generiert.",
|
||||
"; Telekom-Trunk ist deaktiviert.",
|
||||
`; ${provider.label}-Trunk ist deaktiviert.`,
|
||||
"",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
const registrar = asteriskValue(trunk.registrar) || "tel.t-online.de"
|
||||
const providerKey = provider.key
|
||||
const registrar = asteriskValue(trunk.registrar) || provider.defaultRegistrar
|
||||
const sipUser = asteriskValue(trunk.sipUser)
|
||||
const authUser = asteriskValue(trunk.authUser) || sipUser
|
||||
const password = asteriskValue(trunk.password)
|
||||
const callerId = asteriskValue(trunk.callerId) || sipUser
|
||||
const externalMediaAddress = asteriskValue(trunk.externalMediaAddress || trunk.externalSignalingAddress)
|
||||
const aorContact = provider.aorContactIncludesUser && sipUser
|
||||
? `sip:${sipUser}@${registrar}`
|
||||
: `sip:${registrar}`
|
||||
|
||||
return [
|
||||
"; Von FEDEO generiert. Änderungen im Container können überschrieben werden.",
|
||||
"[telekom-auth]",
|
||||
`[${providerKey}-auth]`,
|
||||
"type=auth",
|
||||
"auth_type=userpass",
|
||||
`username=${authUser}`,
|
||||
`password=${password}`,
|
||||
"",
|
||||
"[telekom-aor]",
|
||||
`[${providerKey}-aor]`,
|
||||
"type=aor",
|
||||
`contact=sip:${registrar}`,
|
||||
`contact=${aorContact}`,
|
||||
"",
|
||||
"[telekom]",
|
||||
`[${providerKey}]`,
|
||||
"type=endpoint",
|
||||
"transport=transport-udp",
|
||||
"context=from-telekom",
|
||||
`context=from-${providerKey}`,
|
||||
"disallow=all",
|
||||
"allow=alaw,ulaw",
|
||||
"aors=telekom-aor",
|
||||
"outbound_auth=telekom-auth",
|
||||
`aors=${providerKey}-aor`,
|
||||
`outbound_auth=${providerKey}-auth`,
|
||||
`from_user=${sipUser}`,
|
||||
`from_domain=${registrar}`,
|
||||
`callerid=Telekom <${callerId}>`,
|
||||
`callerid=${provider.label} <${callerId}>`,
|
||||
...(externalMediaAddress ? [`media_address=${externalMediaAddress}`] : []),
|
||||
"direct_media=no",
|
||||
"force_rport=yes",
|
||||
@@ -192,15 +221,15 @@ const renderTelekomPjsipConfig = (trunk: any) => {
|
||||
"rtp_symmetric=yes",
|
||||
"timers=no",
|
||||
"",
|
||||
"[telekom-identify]",
|
||||
`[${providerKey}-identify]`,
|
||||
"type=identify",
|
||||
"endpoint=telekom",
|
||||
`endpoint=${providerKey}`,
|
||||
`match=${registrar}`,
|
||||
"",
|
||||
"[telekom-registration]",
|
||||
`[${providerKey}-registration]`,
|
||||
"type=registration",
|
||||
"transport=transport-udp",
|
||||
"outbound_auth=telekom-auth",
|
||||
`outbound_auth=${providerKey}-auth`,
|
||||
`server_uri=sip:${registrar}`,
|
||||
`client_uri=sip:${sipUser}@${registrar}`,
|
||||
`contact_user=${sipUser}`,
|
||||
@@ -208,20 +237,23 @@ const renderTelekomPjsipConfig = (trunk: any) => {
|
||||
"forbidden_retry_interval=300",
|
||||
"expiration=480",
|
||||
"line=yes",
|
||||
"endpoint=telekom",
|
||||
`endpoint=${providerKey}`,
|
||||
"",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
const renderTelekomExtensionsConfig = (trunk: any) => {
|
||||
const renderProviderExtensionsConfig = (trunk: any) => {
|
||||
const provider = trunkProviderConfig(trunk?.provider)
|
||||
|
||||
if (!trunk?.enabled) {
|
||||
return [
|
||||
"; Von FEDEO generiert.",
|
||||
"; Telekom-Routing ist deaktiviert.",
|
||||
`; ${provider.label}-Routing ist deaktiviert.`,
|
||||
"",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
const providerKey = provider.key
|
||||
const inboundExtension = asteriskValue(trunk.inboundExtension) || "1001"
|
||||
const outboundPrefix = asteriskValue(trunk.outboundPrefix) || "0"
|
||||
const escapedPrefix = outboundPrefix.replace(/[^0-9*#+]/g, "")
|
||||
@@ -231,23 +263,23 @@ const renderTelekomExtensionsConfig = (trunk: any) => {
|
||||
"; Von FEDEO generiert. Änderungen im Container können überschrieben werden.",
|
||||
"[fedeo-local]",
|
||||
escapedPrefix
|
||||
? `exten => _${escapedPrefix}X.,1,NoOp(FEDEO ausgehend über Telekom: $` + "{EXTEN})"
|
||||
: "exten => _X.,1,NoOp(FEDEO ausgehend über Telekom: ${EXTEN})",
|
||||
? `exten => _${escapedPrefix}X.,1,NoOp(FEDEO ausgehend über ${provider.label}: $` + "{EXTEN})"
|
||||
: `exten => _X.,1,NoOp(FEDEO ausgehend über ${provider.label}: $` + "{EXTEN})",
|
||||
` same => n,Set(CALLERID(num)=${callerId})`,
|
||||
" same => n,Dial(PJSIP/${EXTEN}@telekom,60)",
|
||||
` same => n,Dial(PJSIP/$` + `{EXTEN}@${providerKey},60)`,
|
||||
" same => n,Hangup()",
|
||||
"",
|
||||
"exten => _+X.,1,NoOp(FEDEO ausgehend über Telekom: ${EXTEN})",
|
||||
`exten => _+X.,1,NoOp(FEDEO ausgehend über ${provider.label}: $` + "{EXTEN})",
|
||||
` same => n,Set(CALLERID(num)=${callerId})`,
|
||||
" same => n,Dial(PJSIP/${EXTEN}@telekom,60)",
|
||||
` same => n,Dial(PJSIP/$` + `{EXTEN}@${providerKey},60)`,
|
||||
" same => n,Hangup()",
|
||||
"",
|
||||
"[from-telekom]",
|
||||
"exten => s,1,NoOp(FEDEO eingehend über Telekom)",
|
||||
`[from-${providerKey}]`,
|
||||
`exten => s,1,NoOp(FEDEO eingehend über ${provider.label})`,
|
||||
` same => n,Dial(PJSIP/${inboundExtension},30)`,
|
||||
" same => n,Hangup()",
|
||||
"",
|
||||
"exten => _X!,1,NoOp(FEDEO eingehend über Telekom: ${EXTEN})",
|
||||
`exten => _X!,1,NoOp(FEDEO eingehend über ${provider.label}: $` + "{EXTEN})",
|
||||
` same => n,Dial(PJSIP/${inboundExtension},30)`,
|
||||
" same => n,Hangup()",
|
||||
"",
|
||||
@@ -290,11 +322,11 @@ const writeAsteriskTrunkConfig = async (trunk: any) => {
|
||||
const files = [
|
||||
{
|
||||
name: "pjsip.telekom.conf",
|
||||
content: renderTelekomPjsipConfig(trunk),
|
||||
content: renderProviderPjsipConfig(trunk),
|
||||
},
|
||||
{
|
||||
name: "extensions.telekom.conf",
|
||||
content: renderTelekomExtensionsConfig(trunk),
|
||||
content: renderProviderExtensionsConfig(trunk),
|
||||
},
|
||||
{
|
||||
name: "pjsip.transport.conf",
|
||||
@@ -351,11 +383,12 @@ const runAsteriskAmiCommand = async (command: string, timeoutMs = 5000) => {
|
||||
})
|
||||
}
|
||||
|
||||
const runAsteriskReload = async () => {
|
||||
const runAsteriskReload = async (trunk: any) => {
|
||||
const provider = trunkProviderConfig(trunk?.provider)
|
||||
const commands = [
|
||||
"module reload res_pjsip.so",
|
||||
"dialplan reload",
|
||||
"pjsip send register telekom-registration",
|
||||
`pjsip send register ${provider.key}-registration`,
|
||||
]
|
||||
const results = []
|
||||
|
||||
@@ -369,14 +402,17 @@ const runAsteriskReload = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const readAsteriskTrunkStatus = async () => {
|
||||
const readAsteriskTrunkStatus = async (trunk?: any) => {
|
||||
const provider = trunkProviderConfig(trunk?.provider)
|
||||
const registrationName = `${provider.key}-registration`
|
||||
const registrations = await runAsteriskAmiCommand("pjsip show registrations")
|
||||
const raw = registrations.raw || ""
|
||||
|
||||
return {
|
||||
reachable: registrations.ok,
|
||||
registered: /telekom-registration[\s\S]*Registered/i.test(raw),
|
||||
hasRegistration: raw.includes("telekom-registration"),
|
||||
provider: provider.key,
|
||||
registered: new RegExp(`${registrationName}[\\s\\S]*Registered`, "i").test(raw),
|
||||
hasRegistration: raw.includes(registrationName),
|
||||
registrations: raw,
|
||||
}
|
||||
}
|
||||
@@ -397,19 +433,34 @@ export default async function telephonyRoutes(server: FastifyInstance) {
|
||||
return trunk || null
|
||||
}
|
||||
|
||||
const loadActiveTenantTrunk = async (tenantId: number | null) => {
|
||||
if (!tenantId) return null
|
||||
|
||||
const trunks = await server.db
|
||||
.select()
|
||||
.from(telephonyTrunks)
|
||||
.where(eq(telephonyTrunks.tenantId, tenantId))
|
||||
|
||||
return trunks.find((trunk) => trunk.enabled)
|
||||
|| trunks.find((trunk) => trunk.provider === "easybell")
|
||||
|| trunks.find((trunk) => trunk.provider === "telekom")
|
||||
|| trunks[0]
|
||||
|| null
|
||||
}
|
||||
|
||||
const externalTelephonyConfig = async (tenantId: number | null) => {
|
||||
const trunk = await loadTenantTrunk(tenantId)
|
||||
const trunk = await loadActiveTenantTrunk(tenantId)
|
||||
if (trunk) {
|
||||
return {
|
||||
enabled: trunk.enabled,
|
||||
provider: trunk.provider,
|
||||
inboundExtension: trunk.inboundExtension,
|
||||
outboundPrefix: trunk.outboundPrefix,
|
||||
registrar: trunk.registrar,
|
||||
externalSignalingAddress: trunk.externalSignalingAddress,
|
||||
externalMediaAddress: trunk.externalMediaAddress,
|
||||
localNetworks: trunk.localNetworks,
|
||||
sipUserConfigured: Boolean(trunk.sipUser),
|
||||
inboundExtension: trunk.inboundExtension,
|
||||
outboundPrefix: trunk.outboundPrefix,
|
||||
registrar: trunk.registrar,
|
||||
externalSignalingAddress: trunk.externalSignalingAddress,
|
||||
externalMediaAddress: trunk.externalMediaAddress,
|
||||
localNetworks: trunk.localNetworks,
|
||||
sipUserConfigured: Boolean(trunk.sipUser),
|
||||
authUserConfigured: Boolean(trunk.authUser),
|
||||
passwordConfigured: Boolean(trunk.password),
|
||||
callerIdConfigured: Boolean(trunk.callerId),
|
||||
@@ -483,23 +534,26 @@ export default async function telephonyRoutes(server: FastifyInstance) {
|
||||
|
||||
server.get("/telephony/trunk-config", async (req) => {
|
||||
const tenantId = requireTenant(req.user.tenant_id)
|
||||
const trunk = await loadTenantTrunk(tenantId)
|
||||
const provider = normalizeTrunkProvider((req.query as { provider?: string })?.provider)
|
||||
const trunk = await loadTenantTrunk(tenantId, provider)
|
||||
|
||||
return sanitizeTrunk(trunk)
|
||||
return sanitizeTrunk(trunk || { provider })
|
||||
})
|
||||
|
||||
server.put("/telephony/trunk-config", async (req, reply) => {
|
||||
const tenantId = requireTenant(req.user.tenant_id)
|
||||
const body = (req.body || {}) as any
|
||||
const existing = await loadTenantTrunk(tenantId)
|
||||
const provider = normalizeTrunkProvider(bodyString(body, "provider"))
|
||||
const providerConfig = trunkProviderConfig(provider)
|
||||
const existing = await loadTenantTrunk(tenantId, provider)
|
||||
const password = bodyString(body, "password")
|
||||
const clearPassword = body?.clearPassword === true
|
||||
const now = new Date()
|
||||
const values = {
|
||||
tenantId,
|
||||
provider: "telekom",
|
||||
provider,
|
||||
enabled: Boolean(body.enabled),
|
||||
registrar: bodyString(body, "registrar") || "tel.t-online.de",
|
||||
registrar: bodyString(body, "registrar") || providerConfig.defaultRegistrar,
|
||||
sipUser: bodyString(body, "sipUser"),
|
||||
authUser: bodyString(body, "authUser"),
|
||||
callerId: bodyString(body, "callerId"),
|
||||
@@ -525,10 +579,21 @@ export default async function telephonyRoutes(server: FastifyInstance) {
|
||||
.set(values)
|
||||
.where(and(
|
||||
eq(telephonyTrunks.tenantId, tenantId),
|
||||
eq(telephonyTrunks.provider, "telekom")
|
||||
eq(telephonyTrunks.provider, provider)
|
||||
))
|
||||
.returning()
|
||||
|
||||
if (values.enabled) {
|
||||
await server.db
|
||||
.update(telephonyTrunks)
|
||||
.set({ enabled: false, updatedAt: now, updatedBy: req.user.user_id })
|
||||
.where(and(
|
||||
eq(telephonyTrunks.tenantId, tenantId),
|
||||
eq(telephonyTrunks.enabled, true),
|
||||
eq(telephonyTrunks.provider, provider === "easybell" ? "telekom" : "easybell")
|
||||
))
|
||||
}
|
||||
|
||||
return sanitizeTrunk(updated)
|
||||
}
|
||||
|
||||
@@ -541,12 +606,23 @@ export default async function telephonyRoutes(server: FastifyInstance) {
|
||||
})
|
||||
.returning()
|
||||
|
||||
if (values.enabled) {
|
||||
await server.db
|
||||
.update(telephonyTrunks)
|
||||
.set({ enabled: false, updatedAt: now, updatedBy: req.user.user_id })
|
||||
.where(and(
|
||||
eq(telephonyTrunks.tenantId, tenantId),
|
||||
eq(telephonyTrunks.enabled, true),
|
||||
eq(telephonyTrunks.provider, provider === "easybell" ? "telekom" : "easybell")
|
||||
))
|
||||
}
|
||||
|
||||
return sanitizeTrunk(created)
|
||||
})
|
||||
|
||||
server.post("/telephony/trunk-config/apply", async (req, reply) => {
|
||||
const tenantId = requireTenant(req.user.tenant_id)
|
||||
const trunk = await loadTenantTrunk(tenantId)
|
||||
const trunk = await loadActiveTenantTrunk(tenantId)
|
||||
|
||||
if (!trunk) {
|
||||
return reply.code(404).send({ error: "Telefonie-Trunk ist noch nicht konfiguriert." })
|
||||
@@ -564,8 +640,8 @@ export default async function telephonyRoutes(server: FastifyInstance) {
|
||||
let warning: string | null = null
|
||||
|
||||
try {
|
||||
reload = await runAsteriskReload()
|
||||
status = await readAsteriskTrunkStatus()
|
||||
reload = await runAsteriskReload(trunk)
|
||||
status = await readAsteriskTrunkStatus(trunk)
|
||||
} catch (error: any) {
|
||||
warning = error?.message || "Asterisk konnte nicht neu geladen werden."
|
||||
}
|
||||
@@ -581,10 +657,11 @@ export default async function telephonyRoutes(server: FastifyInstance) {
|
||||
})
|
||||
|
||||
server.get("/telephony/trunk-status", async (req, reply) => {
|
||||
requireTenant(req.user.tenant_id)
|
||||
const tenantId = requireTenant(req.user.tenant_id)
|
||||
const trunk = await loadActiveTenantTrunk(tenantId)
|
||||
|
||||
try {
|
||||
return await readAsteriskTrunkStatus()
|
||||
return await readAsteriskTrunkStatus(trunk)
|
||||
} catch (error: any) {
|
||||
return reply.code(200).send({
|
||||
reachable: false,
|
||||
|
||||
Reference in New Issue
Block a user