diff --git a/internal/module/sip-dtmf-server.go b/internal/module/sip-dtmf-server.go new file mode 100644 index 0000000..09810fb --- /dev/null +++ b/internal/module/sip-dtmf-server.go @@ -0,0 +1,152 @@ +package module + +import ( + "context" + "fmt" + "io" + "log/slog" + "strings" + "time" + + "github.com/emiago/diago" + "github.com/emiago/sipgo" + "github.com/emiago/sipgo/sip" + "github.com/jwetzell/showbridge-go/internal/config" + "github.com/jwetzell/showbridge-go/internal/route" +) + +type SIPDTMFServer struct { + config config.ModuleConfig + ctx context.Context + router route.RouteIO + IP string + Port int + Transport string + Separator string +} + +func init() { + RegisterModule(ModuleRegistration{ + Type: "sip.dtmf.server", + New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { + params := config.Params + portNum := 5060 + + port, ok := params["port"] + if ok { + specificPortNum, ok := port.(float64) + + if !ok { + return nil, fmt.Errorf("sip.dtmf.server port must be a number") + } + portNum = int(specificPortNum) + } + + ipString := "0.0.0.0" + + ip, ok := params["ip"] + if ok { + + specificIpString, ok := ip.(string) + + if !ok { + return nil, fmt.Errorf("sip.dtmf.server ip must be a string") + } + ipString = specificIpString + } + + transportString := "udp" + + transport, ok := params["transport"] + if ok { + + specificTransportString, ok := transport.(string) + + if !ok { + return nil, fmt.Errorf("sip.dtmf.server transport must be a string") + } + transportString = specificTransportString + } + + separator, ok := params["separator"] + if !ok { + return nil, fmt.Errorf("sip.dtmf.server requires a separator parameter") + } + separatorString, ok := separator.(string) + if !ok { + return nil, fmt.Errorf("sip.dtmf.server separator must be a string") + } + + if len(separatorString) != 1 { + return nil, fmt.Errorf("sip.dtmf.server separator must be a single character") + } + + if !strings.ContainsRune("0123456789*#ABCD", rune(separatorString[0])) { + return nil, fmt.Errorf("sip.dtmf.server separator must be a valid DTMF character") + } + return &SIPDTMFServer{config: config, ctx: ctx, router: router, IP: ipString, Port: int(portNum), Transport: transportString, Separator: separatorString}, nil + }, + }) +} + +func (sds *SIPDTMFServer) Id() string { + return sds.config.Id +} + +func (sds *SIPDTMFServer) Type() string { + return sds.config.Type +} + +func (sds *SIPDTMFServer) Run() error { + diagoLogger := slog.New(slog.NewJSONHandler(io.Discard, nil)) + + ua, _ := sipgo.NewUA( + sipgo.WithUserAgentTransportLayerOptions(sip.WithTransportLayerLogger(diagoLogger)), + ) + defer ua.Close() + + sip.SetDefaultLogger(diagoLogger) + dg := diago.NewDiago(ua, diago.WithLogger(diagoLogger), diago.WithTransport( + diago.Transport{ + Transport: sds.Transport, + BindHost: sds.IP, + BindPort: sds.Port, + }, + )) + + err := dg.Serve(sds.ctx, func(inDialog *diago.DialogServerSession) { + sds.HandleCall(inDialog) + }) + + if err != nil { + return err + } + + <-sds.ctx.Done() + slog.Debug("router context done in module", "id", sds.Id()) + return nil +} + +func (sds *SIPDTMFServer) HandleCall(inDialog *diago.DialogServerSession) error { + inDialog.Trying() + inDialog.Ringing() + inDialog.Answer() + + 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.Id(), userString) + } + userString = "" + } else { + userString += string(dtmf) + } + return nil + }, 5*time.Second) +} + +func (sds *SIPDTMFServer) Output(payload any) error { + return fmt.Errorf("sip.dtmf.server output is not implemented") +} diff --git a/internal/module/sip-server.go b/internal/module/sip-server.go deleted file mode 100644 index 6dc51f9..0000000 --- a/internal/module/sip-server.go +++ /dev/null @@ -1,92 +0,0 @@ -package module - -import ( - "context" - "fmt" - "io" - "log/slog" - "time" - - "github.com/emiago/diago" - "github.com/emiago/sipgo" - "github.com/emiago/sipgo/sip" - "github.com/jwetzell/showbridge-go/internal/config" - "github.com/jwetzell/showbridge-go/internal/route" -) - -type SIPServer struct { - config config.ModuleConfig - ctx context.Context - router route.RouteIO -} - -func init() { - RegisterModule(ModuleRegistration{ - Type: "net.sip.server", - New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { - - return &SIPServer{config: config, ctx: ctx, router: router}, nil - }, - }) -} - -func (ss *SIPServer) Id() string { - return ss.config.Id -} - -func (ss *SIPServer) Type() string { - return ss.config.Type -} - -func (ss *SIPServer) Run() error { - diagoLogger := slog.New(slog.NewJSONHandler(io.Discard, nil)) - - ua, _ := sipgo.NewUA( - sipgo.WithUserAgentTransportLayerOptions(sip.WithTransportLayerLogger(diagoLogger)), - ) - - sip.SetDefaultLogger(diagoLogger) - dg := diago.NewDiago(ua, diago.WithLogger(diagoLogger), diago.WithTransport( - diago.Transport{ - Transport: "udp", - BindHost: "0.0.0.0", - BindPort: 5060, - }, - )) - - err := dg.Serve(ss.ctx, func(inDialog *diago.DialogServerSession) { - ss.HandleCall(inDialog) - }) - - if err != nil { - return err - } - - <-ss.ctx.Done() - slog.Debug("router context done in module", "id", ss.Id()) - return nil -} - -func (ss *SIPServer) HandleCall(inDialog *diago.DialogServerSession) error { - inDialog.Trying() - inDialog.Ringing() - inDialog.Answer() - - reader := inDialog.AudioReaderDTMF() - userString := "" - return reader.Listen(func(dtmf rune) error { - if dtmf == '#' { - if ss.router != nil { - ss.router.HandleInput(ss.Id(), userString) - } - userString = "" - } else { - userString += string(dtmf) - } - return nil - }, 5*time.Second) -} - -func (ss *SIPServer) Output(payload any) error { - return fmt.Errorf("net.sip.server output is not implemented") -}