diff --git a/internal/module/sip-call-server.go b/internal/module/sip-call-server.go index 17d161e..4cf326b 100644 --- a/internal/module/sip-call-server.go +++ b/internal/module/sip-call-server.go @@ -6,6 +6,8 @@ import ( "fmt" "io" "log/slog" + "os" + "sync" "time" "github.com/emiago/diago" @@ -13,6 +15,7 @@ import ( "github.com/emiago/sipgo" "github.com/emiago/sipgo/sip" "github.com/jwetzell/showbridge-go/internal/config" + "github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/route" ) @@ -32,6 +35,13 @@ type SIPCallMessage struct { To string } +type SIPCall struct { + inDialog *diago.DialogServerSession + lock sync.Mutex +} + +type sipCallContextKey string + func init() { RegisterModule(ModuleRegistration{ Type: "sip.call.server", @@ -143,51 +153,76 @@ func (scs *SIPCallServer) HandleCall(inDialog *diago.DialogServerSession) { inDialog.Trying() inDialog.Ringing() inDialog.Answer() - scs.router.HandleInput(scs.ctx, scs.Id(), SIPCallMessage{ + + dialogContext := context.WithValue(scs.ctx, sipCallContextKey("call"), &SIPCall{ + inDialog: inDialog, + }) + scs.router.HandleInput(dialogContext, scs.Id(), SIPCallMessage{ To: inDialog.ToUser(), }) - <-inDialog.Context().Done() + fmt.Println(inDialog.LoadState()) } func (scs *SIPCallServer) Output(ctx context.Context, payload any) error { - payloadMsg, ok := payload.(string) + call, ok := ctx.Value(sipCallContextKey("call")).(*SIPCall) + if !ok { - return errors.New("sip.call.server output payload must be of type string") + return errors.New("sip.call.server output must originate from sip.call.server input") } - if scs.dg == nil { - return errors.New("sip.call.server diago is not initialized") + gotLock := call.lock.TryLock() + + if !gotLock { + return errors.New("sip.call.server call is already locked") } - 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) + if call.inDialog.LoadState() == sip.DialogStateEnded { + return errors.New("sip.call.server inDialog already ended") } - err = outDialog.Invite(scs.ctx, diago.InviteClientOptions{}) - if err != nil { - return fmt.Errorf("sip.call.server failed to send invite: %s", err) + 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 } - err = outDialog.Ack(scs.ctx) - if err != nil { - return fmt.Errorf("sip.call.server failed to send ack: %s", err) + 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 } - // 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 errors.New("sip.dtmf.server can only output SipDTMFResponse or SipAudioFileResponse") } diff --git a/internal/module/sip-dtmf-server.go b/internal/module/sip-dtmf-server.go index ebc2efb..3758332 100644 --- a/internal/module/sip-dtmf-server.go +++ b/internal/module/sip-dtmf-server.go @@ -3,9 +3,12 @@ package module import ( "context" "errors" + "fmt" "io" "log/slog" + "os" "strings" + "sync" "time" "github.com/emiago/diago" @@ -13,6 +16,7 @@ import ( "github.com/emiago/sipgo" "github.com/emiago/sipgo/sip" "github.com/jwetzell/showbridge-go/internal/config" + "github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/route" ) @@ -32,6 +36,11 @@ type SIPDTMFMessage struct { Digits string } +type SIPDTMFCall struct { + inDialog *diago.DialogServerSession + lock sync.Mutex +} + func init() { RegisterModule(ModuleRegistration{ Type: "sip.dtmf.server", @@ -148,10 +157,14 @@ func (sds *SIPDTMFServer) HandleCall(inDialog *diago.DialogServerSession) error reader := inDialog.AudioReaderDTMF() userString := "" + return reader.Listen(func(dtmf rune) error { if dtmf == rune(sds.Separator[0]) { 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(), Digits: userString, }) @@ -165,5 +178,65 @@ func (sds *SIPDTMFServer) HandleCall(inDialog *diago.DialogServerSession) 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") } diff --git a/internal/processor/sip-response-audio-create.go b/internal/processor/sip-response-audio-create.go new file mode 100644 index 0000000..4f964a0 --- /dev/null +++ b/internal/processor/sip-response-audio-create.go @@ -0,0 +1,97 @@ +package processor + +import ( + "bytes" + "context" + "errors" + "text/template" + + "github.com/jwetzell/showbridge-go/internal/config" +) + +type SipResponseAudioCreate struct { + config config.ProcessorConfig + PreWait int + PostWait int + AudioFile *template.Template +} + +type SipAudioFileResponse struct { + PreWait int + PostWait int + AudioFile string +} + +func (scc *SipResponseAudioCreate) Process(ctx context.Context, payload any) (any, error) { + + var audioFileBuffer bytes.Buffer + err := scc.AudioFile.Execute(&audioFileBuffer, payload) + + if err != nil { + return nil, err + } + + audioFileString := audioFileBuffer.String() + + return SipAudioFileResponse{ + PreWait: scc.PreWait, + PostWait: scc.PostWait, + AudioFile: audioFileString, + }, nil +} + +func (scc *SipResponseAudioCreate) Type() string { + return scc.config.Type +} + +func init() { + RegisterProcessor(ProcessorRegistration{ + Type: "sip.response.audio.create", + New: func(config config.ProcessorConfig) (Processor, error) { + params := config.Params + + preWait, ok := params["preWait"] + + if !ok { + return nil, errors.New("sip.response.audio.create requires a preWait parameter") + } + + preWaitNum, ok := preWait.(float64) + + if !ok { + return nil, errors.New("sip.response.audio.create preWait must be a number") + } + + postWait, ok := params["postWait"] + + if !ok { + return nil, errors.New("sip.response.audio.create requires a postWait parameter") + } + + postWaitNum, ok := postWait.(float64) + + if !ok { + return nil, errors.New("sip.response.audio.create postWait must be a number") + } + + audioFile, ok := params["audioFile"] + + if !ok { + return nil, errors.New("sip.response.audio.create requires a audioFile parameter") + } + + audioFileString, ok := audioFile.(string) + + if !ok { + return nil, errors.New("sip.response.audio.create audioFile must be a string") + } + + audioFileTemplate, err := template.New("audioFile").Parse(audioFileString) + + if err != nil { + return nil, err + } + return &SipResponseAudioCreate{config: config, AudioFile: audioFileTemplate, PreWait: int(preWaitNum), PostWait: int(postWaitNum)}, nil + }, + }) +} diff --git a/internal/processor/sip-response-dtmf-create.go b/internal/processor/sip-response-dtmf-create.go new file mode 100644 index 0000000..55f3967 --- /dev/null +++ b/internal/processor/sip-response-dtmf-create.go @@ -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 + }, + }) +}