Compare commits

..

No commits in common. "9493eb6e5ed1509c742074e8a4d0f57f8c213d78" and "c80e595585dc452db506fc4a3ae237127503d2f3" have entirely different histories.

4 changed files with 12 additions and 86 deletions

2
.gitignore vendored
View File

@ -33,4 +33,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Finder (MacOS) folder config # Finder (MacOS) folder config
.DS_Store .DS_Store
.claude.worktrees/ .claude

View File

@ -18,7 +18,6 @@ import GPIO from "./pins"
import { Agent } from "./agent" import { Agent } from "./agent"
import { searchWeb } from "./agent/tools" import { searchWeb } from "./agent/tools"
import { ring } from "./utils" import { ring } from "./utils"
import { getTwilioAccountInfo, formatTwilioError } from "./utils/twilio"
import { getSound, WaitingSounds } from "./utils/waiting-sounds" import { getSound, WaitingSounds } from "./utils/waiting-sounds"
type CancelableTask = () => void type CancelableTask = () => void
@ -124,24 +123,14 @@ const startBaresip = async (phoneService: PhoneService, hook: GPIO.Input, ringer
phoneService.send({ type: "error", message: error.message }) phoneService.send({ type: "error", message: error.message })
}) })
baresip.error.on(async ({ message, statusCode, reason }) => { baresip.error.on(async ({ message }) => {
let errorMessage = message log.error("🐻 error:", message)
phoneService.send({ type: "error", message })
if (statusCode) { for (let i = 0; i < 4; i++) {
const twilioInfo = await getTwilioAccountInfo() await ring(ringer, 500)
if (twilioInfo && twilioInfo.status !== "active") { await sleep(250)
errorMessage = formatTwilioError(twilioInfo)
} else {
errorMessage = `Registration failed: ${statusCode} ${reason}`
}
} }
process.exit(1)
log.error("🐻 error:", errorMessage)
// Don't send error to state machine - we're retrying, not giving up
log("🔄 Retrying registration in 2 minutes...")
await sleep(2 * 60 * 1000)
baresip.restart()
}) })
return baresip return baresip

View File

@ -8,7 +8,7 @@ export class Baresip {
callEstablished = new Emitter<{ contact: string }>() callEstablished = new Emitter<{ contact: string }>()
callReceived = new Emitter<{ contact: string }>() callReceived = new Emitter<{ contact: string }>()
hungUp = new Emitter() hungUp = new Emitter()
error = new Emitter<{ message: string; statusCode?: string; reason?: string }>() error = new Emitter<{ message: string }>()
registrationSuccess = new Emitter() registrationSuccess = new Emitter()
constructor(baresipArgs: string[]) { constructor(baresipArgs: string[]) {
@ -52,7 +52,6 @@ export class Baresip {
this.callReceived.removeAllListeners() this.callReceived.removeAllListeners()
this.hungUp.removeAllListeners() this.hungUp.removeAllListeners()
this.registrationSuccess.removeAllListeners() this.registrationSuccess.removeAllListeners()
this.error.removeAllListeners()
} }
kill() { kill() {
@ -62,14 +61,6 @@ export class Baresip {
this.process = undefined this.process = undefined
} }
async restart() {
if (this.process) {
this.process.kill()
this.process = undefined
}
await this.connect()
}
#parseLine(line: string) { #parseLine(line: string) {
log.debug(`📞 Baresip: ${line}`) log.debug(`📞 Baresip: ${line}`)
const callEstablishedMatch = line.match(/Call established: (.+)/) const callEstablishedMatch = line.match(/Call established: (.+)/)
@ -100,14 +91,10 @@ export class Baresip {
this.registrationSuccess.emit() this.registrationSuccess.emit()
} }
const registrationFailedMatch = line.match(/reg: sip:\S+ .*?(\d{3}) (\w+)/) const registrationFailedMatch = line.match(/reg: sip:\S+ 403 Forbidden/)
const socketInUseMatch = line.match(/tcp: sock_bind:/) const socketInUseMatch = line.match(/tcp: sock_bind:/)
if (registrationFailedMatch) { if (registrationFailedMatch || socketInUseMatch) {
const [, statusCode, reason] = registrationFailedMatch log.error(`⁉️ NOT HANDLED: Registration failed with "${line}"`)
log.error(`Registration failed: ${statusCode} ${reason}`)
this.error.emit({ message: line, statusCode, reason })
} else if (socketInUseMatch) {
log.error(`Registration failed: socket in use`)
this.error.emit({ message: line }) this.error.emit({ message: line })
} }
} }

View File

@ -1,50 +0,0 @@
const accountSid = process.env.TWILIO_ACCOUNT_SID
const authToken = process.env.TWILIO_AUTH_TOKEN
type AccountStatus = "active" | "suspended" | "closed"
interface TwilioAccountInfo {
status: AccountStatus
balance?: string
currency?: string
}
export async function getTwilioAccountInfo(): Promise<TwilioAccountInfo | undefined> {
if (!accountSid || !authToken) {
return undefined
}
const credentials = Buffer.from(`${accountSid}:${authToken}`).toString("base64")
const headers = { Authorization: `Basic ${credentials}` }
const [accountRes, balanceRes] = await Promise.all([
fetch(`https://api.twilio.com/2010-04-01/Accounts/${accountSid}.json`, { headers }),
fetch(`https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Balance.json`, { headers }),
])
if (!accountRes.ok) {
return undefined
}
const account = (await accountRes.json()) as { status: AccountStatus }
const info: TwilioAccountInfo = { status: account.status }
if (balanceRes.ok) {
const balance = (await balanceRes.json()) as { balance: string; currency: string }
info.balance = balance.balance
info.currency = balance.currency
}
return info
}
export function formatTwilioError(info: TwilioAccountInfo): string {
if (info.status === "suspended") {
const balanceInfo = info.balance ? ` (balance: ${info.balance} ${info.currency})` : ""
return `Twilio account suspended${balanceInfo} - add funds at twilio.com/console`
}
if (info.status === "closed") {
return "Twilio account is closed"
}
return `Twilio account status: ${info.status}`
}