support for responding with dtmf or an audio file to both sip call types

This commit is contained in:
Joel Wetzell
2025-12-28 20:40:09 -06:00
parent bb33974e1c
commit 6e88d259b8
4 changed files with 234 additions and 74 deletions

View File

@@ -3,9 +3,11 @@ package module
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"log/slog" "log/slog"
"os" "os"
"sync"
"time" "time"
"github.com/emiago/diago" "github.com/emiago/diago"
@@ -33,6 +35,11 @@ type SIPCallMessage struct {
To string To string
} }
type SIPCall struct {
inDialog *diago.DialogServerSession
lock sync.Mutex
}
type sipCallContextKey string type sipCallContextKey string
func init() { func init() {
@@ -147,92 +154,75 @@ func (scs *SIPCallServer) HandleCall(inDialog *diago.DialogServerSession) {
inDialog.Ringing() inDialog.Ringing()
inDialog.Answer() inDialog.Answer()
dialogContext := context.WithValue(scs.ctx, sipCallContextKey("inDialog"), inDialog) dialogContext := context.WithValue(scs.ctx, sipCallContextKey("call"), &SIPCall{
inDialog: inDialog,
})
scs.router.HandleInput(dialogContext, scs.Id(), SIPCallMessage{ scs.router.HandleInput(dialogContext, scs.Id(), SIPCallMessage{
To: inDialog.ToUser(), To: inDialog.ToUser(),
}) })
inDialog.Hangup(dialogContext) fmt.Println(inDialog.LoadState())
} }
func (scs *SIPCallServer) Output(ctx context.Context, payload any) error { func (scs *SIPCallServer) Output(ctx context.Context, payload any) error {
inDialog, ok := ctx.Value(sipCallContextKey("inDialog")).(*diago.DialogServerSession) call, ok := ctx.Value(sipCallContextKey("call")).(*SIPCall)
if !ok { if !ok {
return errors.New("sip.call.server output must originate from sip.call.server input") return errors.New("sip.call.server output must originate from sip.call.server input")
} }
// _, ok := payload.([]byte) gotLock := call.lock.TryLock()
// if !ok { if !gotLock {
// return errors.New("sip.call.server is only able to output bytes") return errors.New("sip.call.server call is already locked")
// }
payloadResponse, ok := payload.(processor.SipAudioFileResponse)
if !ok {
return errors.New("sip.call.server is only able to handle SipCallResponse")
} }
audioFile, err := os.Open(payloadResponse.AudioFile) if call.inDialog.LoadState() == sip.DialogStateEnded {
return errors.New("sip.call.server inDialog already ended")
}
payloadDTMFResponse, ok := payload.(processor.SipDTMFResponse)
if ok {
dtmfWriter := call.inDialog.AudioWriterDTMF()
time.Sleep(time.Millisecond * time.Duration(payloadDTMFResponse.PreWait))
for i, dtmfRune := range payloadDTMFResponse.Digits {
err := dtmfWriter.WriteDTMF(dtmfRune)
if err != nil {
return fmt.Errorf("sip.dtmf.server error output dtmf digit at index %d", i)
}
}
time.Sleep(time.Millisecond * time.Duration(payloadDTMFResponse.PreWait))
return nil
}
payloadAudioFileResponse, ok := payload.(processor.SipAudioFileResponse)
if ok {
audioFile, err := os.Open(payloadAudioFileResponse.AudioFile)
if err != nil { if err != nil {
return err return err
} }
defer audioFile.Close() defer audioFile.Close()
playback, err := inDialog.PlaybackCreate() playback, err := call.inDialog.PlaybackCreate()
if err != nil { if err != nil {
return err return err
} }
time.Sleep(time.Millisecond * time.Duration(payloadResponse.PreWait)) time.Sleep(time.Millisecond * time.Duration(payloadAudioFileResponse.PreWait))
_, err = playback.Play(audioFile, "audio/wav") _, err = playback.Play(audioFile, "audio/wav")
time.Sleep(time.Millisecond * time.Duration(payloadResponse.PostWait)) time.Sleep(time.Millisecond * time.Duration(payloadAudioFileResponse.PostWait))
if err != nil { if err != nil {
return err return err
} }
// payloadMsg, ok := payload.(string)
// if !ok {
// return errors.New("sip.call.server output payload must be of type string")
// }
// if scs.dg == nil {
// return errors.New("sip.call.server diago is not initialized")
// }
// var uri sip.Uri
// err := sip.ParseUri(payloadMsg, &uri)
// if err != nil {
// return fmt.Errorf("sip.call.server output payload is not a valid SIP URI: %s", err)
// }
// outDialog, err := scs.dg.NewDialog(uri, diago.NewDialogOptions{
// Transport: scs.Transport,
// })
// if err != nil {
// return fmt.Errorf("sip.call.server failed to create new dialog: %s", err)
// }
// err = outDialog.Invite(scs.ctx, diago.InviteClientOptions{})
// if err != nil {
// return fmt.Errorf("sip.call.server failed to send invite: %s", err)
// }
// err = outDialog.Ack(scs.ctx)
// if err != nil {
// return fmt.Errorf("sip.call.server failed to send ack: %s", err)
// }
// // TODO(jwetzell): make this configurable
// // NOTE(jwetzell): wait 5 seconds before hanging up the call
// time.Sleep(5 * time.Second)
// err = outDialog.Hangup(scs.ctx)
// if err != nil {
// return fmt.Errorf("sip.call.server failed to hangup call: %s", err)
// }
return nil return nil
} }
return errors.New("sip.dtmf.server can only output SipDTMFResponse or SipAudioFileResponse")
}

View File

@@ -3,9 +3,12 @@ package module
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"log/slog" "log/slog"
"os"
"strings" "strings"
"sync"
"time" "time"
"github.com/emiago/diago" "github.com/emiago/diago"
@@ -13,6 +16,7 @@ import (
"github.com/emiago/sipgo" "github.com/emiago/sipgo"
"github.com/emiago/sipgo/sip" "github.com/emiago/sipgo/sip"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route" "github.com/jwetzell/showbridge-go/internal/route"
) )
@@ -32,6 +36,11 @@ type SIPDTMFMessage struct {
Digits string Digits string
} }
type SIPDTMFCall struct {
inDialog *diago.DialogServerSession
lock sync.Mutex
}
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "sip.dtmf.server", Type: "sip.dtmf.server",
@@ -148,10 +157,14 @@ func (sds *SIPDTMFServer) HandleCall(inDialog *diago.DialogServerSession) error
reader := inDialog.AudioReaderDTMF() reader := inDialog.AudioReaderDTMF()
userString := "" userString := ""
return reader.Listen(func(dtmf rune) error { return reader.Listen(func(dtmf rune) error {
if dtmf == rune(sds.Separator[0]) { if dtmf == rune(sds.Separator[0]) {
if sds.router != nil { if sds.router != nil {
sds.router.HandleInput(sds.ctx, sds.Id(), SIPDTMFMessage{ dialogContext := context.WithValue(sds.ctx, sipCallContextKey("call"), &SIPDTMFCall{
inDialog: inDialog,
})
sds.router.HandleInput(dialogContext, sds.Id(), SIPDTMFMessage{
To: inDialog.ToUser(), To: inDialog.ToUser(),
Digits: userString, Digits: userString,
}) })
@@ -165,5 +178,65 @@ func (sds *SIPDTMFServer) HandleCall(inDialog *diago.DialogServerSession) error
} }
func (sds *SIPDTMFServer) Output(ctx context.Context, payload any) error { func (sds *SIPDTMFServer) Output(ctx context.Context, payload any) error {
return errors.New("sip.dtmf.server output is not implemented") call, ok := ctx.Value(sipCallContextKey("call")).(*SIPDTMFCall)
if !ok {
return errors.New("sip.dtmf.server output must originate from sip.dtmf.server input")
}
gotLock := call.lock.TryLock()
if !gotLock {
return errors.New("sip.dtmf.server call is already locked")
}
if call.inDialog.LoadState() == sip.DialogStateEnded {
return errors.New("sip.dtmf.server inDialog already ended")
}
payloadDTMFResponse, ok := payload.(processor.SipDTMFResponse)
if ok {
dtmfWriter := call.inDialog.AudioWriterDTMF()
time.Sleep(time.Millisecond * time.Duration(payloadDTMFResponse.PreWait))
for i, dtmfRune := range payloadDTMFResponse.Digits {
err := dtmfWriter.WriteDTMF(dtmfRune)
if err != nil {
return fmt.Errorf("sip.dtmf.server error output dtmf digit at index %d", i)
}
}
time.Sleep(time.Millisecond * time.Duration(payloadDTMFResponse.PreWait))
return nil
}
payloadAudioFileResponse, ok := payload.(processor.SipAudioFileResponse)
if ok {
audioFile, err := os.Open(payloadAudioFileResponse.AudioFile)
if err != nil {
return err
}
defer audioFile.Close()
playback, err := call.inDialog.PlaybackCreate()
if err != nil {
return err
}
time.Sleep(time.Millisecond * time.Duration(payloadAudioFileResponse.PreWait))
_, err = playback.Play(audioFile, "audio/wav")
time.Sleep(time.Millisecond * time.Duration(payloadAudioFileResponse.PostWait))
if err != nil {
return err
}
return nil
}
return errors.New("sip.dtmf.server can only output SipDTMFResponse or SipAudioFileResponse")
} }

View File

@@ -9,7 +9,7 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type SipResponseCreate struct { type SipResponseAudioCreate struct {
config config.ProcessorConfig config config.ProcessorConfig
PreWait int PreWait int
PostWait int PostWait int
@@ -22,7 +22,7 @@ type SipAudioFileResponse struct {
AudioFile string AudioFile string
} }
func (scc *SipResponseCreate) Process(ctx context.Context, payload any) (any, error) { func (scc *SipResponseAudioCreate) Process(ctx context.Context, payload any) (any, error) {
var audioFileBuffer bytes.Buffer var audioFileBuffer bytes.Buffer
err := scc.AudioFile.Execute(&audioFileBuffer, payload) err := scc.AudioFile.Execute(&audioFileBuffer, payload)
@@ -40,50 +40,50 @@ func (scc *SipResponseCreate) Process(ctx context.Context, payload any) (any, er
}, nil }, nil
} }
func (scc *SipResponseCreate) Type() string { func (scc *SipResponseAudioCreate) Type() string {
return scc.config.Type return scc.config.Type
} }
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "sip.response.create", Type: "sip.response.audio.create",
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
preWait, ok := params["preWait"] preWait, ok := params["preWait"]
if !ok { if !ok {
return nil, errors.New("sip.response.create requires a preWait parameter") return nil, errors.New("sip.response.audio.create requires a preWait parameter")
} }
preWaitNum, ok := preWait.(float64) preWaitNum, ok := preWait.(float64)
if !ok { if !ok {
return nil, errors.New("sip.response.create preWait must be a number") return nil, errors.New("sip.response.audio.create preWait must be a number")
} }
postWait, ok := params["postWait"] postWait, ok := params["postWait"]
if !ok { if !ok {
return nil, errors.New("sip.response.create requires a postWait parameter") return nil, errors.New("sip.response.audio.create requires a postWait parameter")
} }
postWaitNum, ok := postWait.(float64) postWaitNum, ok := postWait.(float64)
if !ok { if !ok {
return nil, errors.New("sip.response.create postWait must be a number") return nil, errors.New("sip.response.audio.create postWait must be a number")
} }
audioFile, ok := params["audioFile"] audioFile, ok := params["audioFile"]
if !ok { if !ok {
return nil, errors.New("sip.response.create requires a audioFile parameter") return nil, errors.New("sip.response.audio.create requires a audioFile parameter")
} }
audioFileString, ok := audioFile.(string) audioFileString, ok := audioFile.(string)
if !ok { if !ok {
return nil, errors.New("sip.response.create audioFile must be a string") return nil, errors.New("sip.response.audio.create audioFile must be a string")
} }
audioFileTemplate, err := template.New("audioFile").Parse(audioFileString) audioFileTemplate, err := template.New("audioFile").Parse(audioFileString)
@@ -91,7 +91,7 @@ func init() {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &SipResponseCreate{config: config, AudioFile: audioFileTemplate, PreWait: int(preWaitNum), PostWait: int(postWaitNum)}, nil return &SipResponseAudioCreate{config: config, AudioFile: audioFileTemplate, PreWait: int(preWaitNum), PostWait: int(postWaitNum)}, nil
}, },
}) })
} }

View File

@@ -0,0 +1,97 @@
package processor
import (
"bytes"
"context"
"errors"
"text/template"
"github.com/jwetzell/showbridge-go/internal/config"
)
type SipResponseDTMFCreate struct {
config config.ProcessorConfig
PreWait int
PostWait int
Digits *template.Template
}
type SipDTMFResponse struct {
PreWait int
PostWait int
Digits string
}
func (scc *SipResponseDTMFCreate) Process(ctx context.Context, payload any) (any, error) {
var digitsBuffer bytes.Buffer
err := scc.Digits.Execute(&digitsBuffer, payload)
if err != nil {
return nil, err
}
digitsString := digitsBuffer.String()
return SipDTMFResponse{
PreWait: scc.PreWait,
PostWait: scc.PostWait,
Digits: digitsString,
}, nil
}
func (scc *SipResponseDTMFCreate) Type() string {
return scc.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "sip.response.dtmf.create",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
preWait, ok := params["preWait"]
if !ok {
return nil, errors.New("sip.response.dtmf.create requires a preWait parameter")
}
preWaitNum, ok := preWait.(float64)
if !ok {
return nil, errors.New("sip.response.dtmf.create preWait must be a number")
}
postWait, ok := params["postWait"]
if !ok {
return nil, errors.New("sip.response.dtmf.create requires a postWait parameter")
}
postWaitNum, ok := postWait.(float64)
if !ok {
return nil, errors.New("sip.response.dtmf.create postWait must be a number")
}
digits, ok := params["digits"]
if !ok {
return nil, errors.New("sip.response.dtmf.create requires a digits parameter")
}
digitsString, ok := digits.(string)
if !ok {
return nil, errors.New("sip.response.dtmf.create digits must be a string")
}
digitsTemplate, err := template.New("digits").Parse(digitsString)
if err != nil {
return nil, err
}
return &SipResponseDTMFCreate{config: config, Digits: digitsTemplate, PreWait: int(preWaitNum), PostWait: int(postWaitNum)}, nil
},
})
}