diff --git a/go.mod b/go.mod index bd09602..2cb3b52 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.25.3 require ( github.com/eclipse/paho.mqtt.golang v1.5.1 + github.com/emiago/diago v0.23.1-0.20251211215055-e1d875617111 + github.com/emiago/sipgo v1.0.1-alpha.0.20251212165843-9c9bcdf9126f github.com/expr-lang/expr v1.17.6 github.com/jwetzell/free-d-go v0.1.0 github.com/jwetzell/osc-go v0.1.0 @@ -19,20 +21,33 @@ require ( require ( github.com/creack/goselect v0.1.2 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-audio/riff v1.0.0 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/gobwas/ws v1.4.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect + github.com/icholy/digest v1.1.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.16 // indirect + github.com/pion/rtp v1.8.26 // indirect + github.com/pion/srtp/v3 v3.0.9 // indirect + github.com/pion/transport/v3 v3.1.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/zaf/g711 v1.4.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/crypto v0.42.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/net v0.44.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 // indirect modernc.org/libc v1.66.10 // indirect modernc.org/libquickjs v0.12.2 // indirect modernc.org/mathutil v1.7.1 // indirect diff --git a/go.sum b/go.sum index b796e10..06fb05f 100644 --- a/go.sum +++ b/go.sum @@ -6,16 +6,30 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE= github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= +github.com/emiago/diago v0.23.1-0.20251211215055-e1d875617111 h1:jqhOZbH40pf2jiUhGaBNk334wOtNYvAaXg/mHOXhy/Y= +github.com/emiago/diago v0.23.1-0.20251211215055-e1d875617111/go.mod h1:3vLCCq8/G/Ei5I64IHtrmBTag+nPLcgXcKeN1KkLtuc= +github.com/emiago/sipgo v1.0.1-alpha.0.20251212165843-9c9bcdf9126f h1:n3wpx7ZyJBr0popXyATnTUZp/AdLIHjKbnKzr4ruF7g= +github.com/emiago/sipgo v1.0.1-alpha.0.20251212165843-9c9bcdf9126f/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY= github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec= github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= +github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= +github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4= +github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= github.com/jwetzell/free-d-go v0.1.0 h1:xHt6dvyit98X+OC3jVzV0aLidxbyzi3vI9QiYkteEtA= github.com/jwetzell/free-d-go v0.1.0/go.mod h1:KmrkooRARRaxJTBSPvwt/6IMAIaHH1R8bSA8cwbbELw= github.com/jwetzell/osc-go v0.1.0 h1:EXxup5VWBErHot2Ri4MFToPf6KCzLDTbCt2x6GLfw8I= @@ -24,6 +38,10 @@ github.com/jwetzell/psn-go v0.3.0 h1:WVpCEmExYE8a+I5hQak5jNJJp2x35VdGX/VuMUKPmhY github.com/jwetzell/psn-go v0.3.0/go.mod h1:bcEAeti4sQM375buujb3mIfmUstD4Aby18gq3ENb6+o= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= @@ -34,14 +52,40 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= +github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= +github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= +github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= +github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= +github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU= +github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc= +github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM= +github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= +github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= +github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY= +github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= +github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo= github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +github.com/zaf/g711 v1.4.0 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c= +github.com/zaf/g711 v1.4.0/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo= gitlab.com/gomidi/midi/v2 v2.3.16 h1:yufWSENyjnJ4LFQa9BerzUm4E4aLfTyzw5nmnCteO0c= gitlab.com/gomidi/midi/v2 v2.3.16/go.mod h1:jDpP4O4skYi+7iVwt6Zyp18bd2M4hkjtMuw2cmgKgfw= go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= @@ -58,17 +102,22 @@ golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 h1:xeVptzkP8BuJhoIjNizd2bRHfq9KB9HfOLZu90T04XM= +gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302/go.mod h1:/L5E7a21VWl8DeuCPKxQBdVG5cy+L0MRZ08B1wnqt7g= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= diff --git a/internal/module/sip-call-server.go b/internal/module/sip-call-server.go new file mode 100644 index 0000000..9f1e0d4 --- /dev/null +++ b/internal/module/sip-call-server.go @@ -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 +} diff --git a/internal/module/sip-dtmf-server.go b/internal/module/sip-dtmf-server.go new file mode 100644 index 0000000..9225a13 --- /dev/null +++ b/internal/module/sip-dtmf-server.go @@ -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") +}