Merge pull request #14 from jwetzell/feat/sip-server

add some SIP modules
This commit is contained in:
Joel Wetzell
2025-12-13 13:37:20 -06:00
committed by GitHub
4 changed files with 421 additions and 9 deletions

View File

@@ -0,0 +1,185 @@
package module
import (
"context"
"fmt"
"io"
"log/slog"
"time"
"github.com/emiago/diago"
"github.com/emiago/diago/media"
"github.com/emiago/sipgo"
"github.com/emiago/sipgo/sip"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
)
type SIPCallServer struct {
config config.ModuleConfig
ctx context.Context
router route.RouteIO
IP string
Port int
Transport string
UserAgent string
dg *diago.Diago
}
type SIPCallMessage struct {
To string
}
func init() {
RegisterModule(ModuleRegistration{
Type: "sip.call.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.call.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.call.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.call.server transport must be a string")
}
transportString = specificTransportString
}
userAgentString := "showbridge"
userAgent, ok := params["userAgent"]
if ok {
specificTransportString, ok := userAgent.(string)
if !ok {
return nil, fmt.Errorf("sip.call.server userAgent must be a string")
}
userAgentString = specificTransportString
}
return &SIPCallServer{config: config, ctx: ctx, router: router, IP: ipString, Port: int(portNum), Transport: transportString, UserAgent: userAgentString}, nil
},
})
}
func (sds *SIPCallServer) Id() string {
return sds.config.Id
}
func (sds *SIPCallServer) Type() string {
return sds.config.Type
}
func (sds *SIPCallServer) Run() error {
diagoLogger := slog.New(slog.NewJSONHandler(io.Discard, nil))
ua, _ := sipgo.NewUA(
sipgo.WithUserAgent(sds.UserAgent),
sipgo.WithUserAgentTransportLayerOptions(sip.WithTransportLayerLogger(diagoLogger)),
sipgo.WithUserAgentTransactionLayerOptions(sip.WithTransactionLayerLogger(diagoLogger)),
)
defer ua.Close()
sip.SetDefaultLogger(diagoLogger)
media.SetDefaultLogger(diagoLogger)
dg := diago.NewDiago(ua, diago.WithLogger(diagoLogger), diago.WithTransport(
diago.Transport{
Transport: sds.Transport,
BindHost: sds.IP,
BindPort: sds.Port,
},
))
go func() {
dg.Serve(sds.ctx, func(inDialog *diago.DialogServerSession) {
sds.HandleCall(inDialog)
})
}()
sds.dg = dg
<-sds.ctx.Done()
slog.Debug("router context done in module", "id", sds.Id())
return nil
}
func (sds *SIPCallServer) HandleCall(inDialog *diago.DialogServerSession) {
inDialog.Trying()
inDialog.Ringing()
inDialog.Answer()
sds.router.HandleInput(sds.Id(), SIPCallMessage{
To: inDialog.ToUser(),
})
<-inDialog.Context().Done()
}
func (sds *SIPCallServer) Output(payload any) error {
payloadMsg, ok := payload.(string)
if !ok {
return fmt.Errorf("sip.call.server output payload must be of type string")
}
if sds.dg == nil {
return fmt.Errorf("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: %v", err)
}
outDialog, err := sds.dg.NewDialog(uri, diago.NewDialogOptions{
Transport: sds.Transport,
})
if err != nil {
return fmt.Errorf("sip.call.server failed to create new dialog: %v", err)
}
err = outDialog.Invite(sds.ctx, diago.InviteClientOptions{})
if err != nil {
return fmt.Errorf("sip.call.server failed to send invite: %v", err)
}
err = outDialog.Ack(sds.ctx)
if err != nil {
return fmt.Errorf("sip.call.server failed to send ack: %v", err)
}
// TODO(jwetzell): make this configurable
// NOTE(jwetzell): wait 5 seconds before hanging up the call
time.Sleep(5 * time.Second)
err = outDialog.Hangup(sds.ctx)
if err != nil {
return fmt.Errorf("sip.call.server failed to hangup call: %v", err)
}
return nil
}

View File

@@ -0,0 +1,163 @@
package module
import (
"context"
"fmt"
"io"
"log/slog"
"strings"
"time"
"github.com/emiago/diago"
"github.com/emiago/diago/media"
"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
}
type SIPDTMFMessage struct {
To string
Digits 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)),
sipgo.WithUserAgentTransactionLayerOptions(sip.WithTransactionLayerLogger(diagoLogger)),
)
defer ua.Close()
sip.SetDefaultLogger(diagoLogger)
media.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(), SIPDTMFMessage{
To: inDialog.ToUser(),
Digits: 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")
}