Compare commits
2 Commits
27aa62f950
...
2428afd3db
| Author | SHA1 | Date | |
|---|---|---|---|
| 2428afd3db | |||
| b53a4197c6 |
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -33,4 +33,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
||||
.claude
|
||||
.claude.worktrees/
|
||||
|
|
|
|||
25
src/phone.ts
25
src/phone.ts
|
|
@ -18,6 +18,7 @@ import GPIO from "./pins"
|
|||
import { Agent } from "./agent"
|
||||
import { searchWeb } from "./agent/tools"
|
||||
import { ring } from "./utils"
|
||||
import { getTwilioAccountInfo, formatTwilioError } from "./utils/twilio"
|
||||
import { getSound, WaitingSounds } from "./utils/waiting-sounds"
|
||||
|
||||
type CancelableTask = () => void
|
||||
|
|
@ -123,14 +124,24 @@ const startBaresip = async (phoneService: PhoneService, hook: GPIO.Input, ringer
|
|||
phoneService.send({ type: "error", message: error.message })
|
||||
})
|
||||
|
||||
baresip.error.on(async ({ message }) => {
|
||||
log.error("🐻 error:", message)
|
||||
phoneService.send({ type: "error", message })
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await ring(ringer, 500)
|
||||
await sleep(250)
|
||||
baresip.error.on(async ({ message, statusCode, reason }) => {
|
||||
let errorMessage = message
|
||||
|
||||
if (statusCode) {
|
||||
const twilioInfo = await getTwilioAccountInfo()
|
||||
if (twilioInfo && twilioInfo.status !== "active") {
|
||||
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
|
||||
|
|
|
|||
21
src/sip.ts
21
src/sip.ts
|
|
@ -8,7 +8,7 @@ export class Baresip {
|
|||
callEstablished = new Emitter<{ contact: string }>()
|
||||
callReceived = new Emitter<{ contact: string }>()
|
||||
hungUp = new Emitter()
|
||||
error = new Emitter<{ message: string }>()
|
||||
error = new Emitter<{ message: string; statusCode?: string; reason?: string }>()
|
||||
registrationSuccess = new Emitter()
|
||||
|
||||
constructor(baresipArgs: string[]) {
|
||||
|
|
@ -52,6 +52,7 @@ export class Baresip {
|
|||
this.callReceived.removeAllListeners()
|
||||
this.hungUp.removeAllListeners()
|
||||
this.registrationSuccess.removeAllListeners()
|
||||
this.error.removeAllListeners()
|
||||
}
|
||||
|
||||
kill() {
|
||||
|
|
@ -61,6 +62,14 @@ export class Baresip {
|
|||
this.process = undefined
|
||||
}
|
||||
|
||||
async restart() {
|
||||
if (this.process) {
|
||||
this.process.kill()
|
||||
this.process = undefined
|
||||
}
|
||||
await this.connect()
|
||||
}
|
||||
|
||||
#parseLine(line: string) {
|
||||
log.debug(`📞 Baresip: ${line}`)
|
||||
const callEstablishedMatch = line.match(/Call established: (.+)/)
|
||||
|
|
@ -91,10 +100,14 @@ export class Baresip {
|
|||
this.registrationSuccess.emit()
|
||||
}
|
||||
|
||||
const registrationFailedMatch = line.match(/reg: sip:\S+ 403 Forbidden/)
|
||||
const registrationFailedMatch = line.match(/reg: sip:\S+ .*?(\d{3}) (\w+)/)
|
||||
const socketInUseMatch = line.match(/tcp: sock_bind:/)
|
||||
if (registrationFailedMatch || socketInUseMatch) {
|
||||
log.error(`⁉️ NOT HANDLED: Registration failed with "${line}"`)
|
||||
if (registrationFailedMatch) {
|
||||
const [, statusCode, reason] = registrationFailedMatch
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
50
src/utils/twilio.ts
Normal file
50
src/utils/twilio.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
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}`
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user