phone/src/sip.ts
2025-11-18 16:53:41 -08:00

115 lines
3.2 KiB
TypeScript

import { log } from "./utils/log.ts"
import { Signal } from "./utils/signal.ts"
import { processStdout, processStderr } from "./utils/stdio.ts"
export class Baresip {
baresipArgs: string[]
process?: Bun.PipedSubprocess
callEstablished = new Signal<{ contact: string }>()
callReceived = new Signal<{ contact: string }>()
hungUp = new Signal()
error = new Signal<{ message: string }>()
registrationSuccess = new Signal()
constructor(baresipArgs: string[]) {
this.baresipArgs = baresipArgs
}
async connect() {
this.process = Bun.spawn(this.baresipArgs, {
stdout: "pipe",
stderr: "pipe",
onExit: (_proc, exitCode, signalCode, error) => {
log.debug(`📞 Baresip process exited (code: ${exitCode}, signal: ${signalCode})`)
if (error) {
log.error("Process error:", error)
}
},
})
Promise.all([
processStdout(this.process, (line) => this.#parseLine(line)),
processStderr(this.process),
]).catch((error) => {
log.error("Error processing output:", error)
})
}
accept() {
executeCommand("a")
}
dial(phoneNumber: string) {
executeCommand(`d${phoneNumber}`)
}
hangUp() {
executeCommand("b")
}
disconnectAll() {
this.callEstablished.disconnect()
this.callReceived.disconnect()
this.hungUp.disconnect()
this.registrationSuccess.disconnect()
}
kill() {
if (!this.process) throw new Error("Process not started")
this.process.kill()
this.disconnectAll()
this.process = undefined
}
#parseLine(line: string) {
log.debug(`📞 Baresip: ${line}`)
const callEstablishedMatch = line.match(/Call established: (.+)/)
if (callEstablishedMatch) {
log.debug(`Call established with "${line}"`)
this.callEstablished.emit({ contact: callEstablishedMatch[1]! })
}
const callReceivedMatch = line.match(/Incoming call from: \+\d+ (\S+) -/)
if (callReceivedMatch) {
log.debug(`Incoming call from "${line}"`)
this.callReceived.emit({ contact: callReceivedMatch[1]!?.trim() })
}
const hangUpMatch = line.match(/(.+): session closed/)
if (hangUpMatch) {
log.debug(`Call hung up with "${line}"`)
this.hungUp.emit()
}
const callTerminatedMatch = line.match(/(.+) terminated \(duration: /)
if (callTerminatedMatch) {
log.debug(`⁉️ NOT HANDLED: Call terminated with "${line}"`)
}
const registrationSuccessMatch = line.match(/\[\d+ bindings?\]/)
if (registrationSuccessMatch) {
this.registrationSuccess.emit()
}
const registrationFailedMatch = line.match(/reg: sip:\S+ 403 Forbidden/)
const socketInUseMatch = line.match(/tcp: sock_bind:/)
if (registrationFailedMatch || socketInUseMatch) {
log.error(`⁉️ NOT HANDLED: Registration failed with "${line}"`)
this.error.emit({ message: line })
}
}
}
const executeCommand = async (command: string) => {
try {
const url = new URL(`/?${command}`, "http://127.0.0.1:8000")
const response = await Bun.fetch(url)
if (!response.ok) {
throw new Error(`Error executing command: ${response.statusText}`)
}
} catch (error) {
log.error("Failed to execute command:", error)
}
}