├── services ├── nsqinit.go ├── callstatsgen.go ├── heartbeatgen.go ├── nsqproducer.go └── mqttchannel.go ├── images ├── logo.png └── cpass.jpeg ├── server ├── server.go └── router.go ├── models ├── heartbeat.go ├── recordUploader.go ├── errors.go ├── rateroute.go ├── inbound.go ├── response.go ├── xml.go ├── webhook.go └── request.go ├── .idea ├── modules.xml └── neoms.iml ├── systemctl └── neoms.service ├── constant ├── error.go └── constant.go ├── .gitignore ├── managers ├── callstats │ ├── gocache.go │ └── callstats.go ├── tinixml │ ├── reject.go │ ├── pause.go │ ├── hangup.go │ ├── redirect.go │ ├── play.go │ ├── number.go │ ├── conference.go │ ├── gather.go │ ├── sip.go │ ├── record.go │ └── speak.go ├── confmanager.go ├── recordingsmanager.go ├── webhooks │ ├── dialstatus.go │ ├── sipstatus.go │ ├── numberstatus.go │ └── recordingstatus.go ├── heartbeatmanager.go ├── rateroute │ └── ratingmanager.go ├── appmanager.go ├── callbackmanager.go ├── callmanager.go └── xmlmanager.go ├── main.go ├── helper ├── phonenumber.go ├── jwt.go ├── sanity.go ├── fscmd.go └── http.go ├── middlewares └── auth.go ├── adapters ├── callstate.go ├── factory │ └── factory.go ├── mediaserver.go └── callstate │ └── redis.go ├── go.mod ├── Dockerfile ├── utils └── xmlvalidate.go ├── entrypoint.sh ├── Jenkinsfile ├── config.toml ├── config └── config.go ├── controller └── callctrl.go ├── logger └── logger.go ├── freeswitch └── dialplan │ └── tiniyo_inbound.xml ├── README.md ├── LICENSE ├── TinyMLSchema.xsd └── go.sum /services/nsqinit.go: -------------------------------------------------------------------------------- 1 | package services 2 | -------------------------------------------------------------------------------- /services/callstatsgen.go: -------------------------------------------------------------------------------- 1 | package services 2 | -------------------------------------------------------------------------------- /services/heartbeatgen.go: -------------------------------------------------------------------------------- 1 | package services 2 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiniyo/neoms/HEAD/images/logo.png -------------------------------------------------------------------------------- /images/cpass.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiniyo/neoms/HEAD/images/cpass.jpeg -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/tiniyo/neoms/config" 6 | ) 7 | 8 | func Init() { 9 | gin.SetMode(config.Config.Server.GinMode) 10 | r := NewRouter() 11 | r.Run(config.Config.Server.Port) 12 | } 13 | -------------------------------------------------------------------------------- /models/heartbeat.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type HeartBeatPrivRequest struct { 4 | AccountID string `json:"accountid"` 5 | CallID string `json:"callid"` 6 | Rate float64 `json:"rate"` 7 | Pulse int64 `json:"pulse"` 8 | Duration int64 `json:"duration"` 9 | } 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /systemctl/neoms.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=NeoMs service 3 | After=network.target 4 | [Service] 5 | Type=simple 6 | LimitNOFILE=49152 7 | Restart=always 8 | RestartSec=1 9 | User=root 10 | ExecStart=/usr/local/bin/neoms 11 | [Install] 12 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /constant/error.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrEmptyXML = errors.New("XML Repsonse is empty") 7 | ErrInvalidXML = errors.New("XML Response is invalid") 8 | ErrEmptyRedirect = errors.New("XML Redirect is empty") 9 | ErrGatherTimeout = errors.New("gather Timeout") 10 | ) 11 | -------------------------------------------------------------------------------- /models/recordUploader.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type RecordJob struct { 4 | Args struct { 5 | FileName string `json:"file_name"` 6 | FilePath string `json:"file_path"` 7 | JobID int64 `json:"job_id"` 8 | } `json:"args"` 9 | ID string `json:"id"` 10 | Name string `json:"name"` 11 | T int64 `json:"t"` 12 | } 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /.idea/neoms.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /managers/callstats/gocache.go: -------------------------------------------------------------------------------- 1 | package callstats 2 | 3 | func SetLocalCache(key string, value []byte) { 4 | err := callStatObj.Set(key, value, 10) 5 | if err != nil { 6 | return 7 | } 8 | } 9 | 10 | func GetLocalCache(key string) interface{} { 11 | value, err := callStatObj.Get(key) 12 | if err == nil { 13 | return value 14 | } 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/tiniyo/neoms/config" 5 | "github.com/tiniyo/neoms/logger" 6 | "github.com/tiniyo/neoms/server" 7 | ) 8 | 9 | /* 10 | WebMediaServer :- Initialize server and configuration 11 | */ 12 | func main() { 13 | config.InitConfig() 14 | //helper.InitHttpConnPool() 15 | logger.InitLogger() 16 | server.Init() 17 | } 18 | -------------------------------------------------------------------------------- /services/nsqproducer.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "log" 5 | 6 | nsq "github.com/nsqio/go-nsq" 7 | ) 8 | 9 | var w *nsq.Producer 10 | 11 | func InitializeNsqProducer() { 12 | config := nsq.NewConfig() 13 | w, _ = nsq.NewProducer("127.0.0.1:4150", config) 14 | } 15 | 16 | func ProduceInfo() { 17 | err := w.Publish("write_test", []byte("test")) 18 | if err != nil { 19 | log.Panic("Could not connect") 20 | } 21 | } 22 | 23 | func ShutdownProducer() { 24 | w.Stop() 25 | } 26 | -------------------------------------------------------------------------------- /helper/phonenumber.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import "regexp" 4 | 5 | /* 6 | We will keep all phone number related utility here 7 | */ 8 | func NumberSanity(number string) string { 9 | reg, err := regexp.Compile("[^0-9]+") 10 | if err != nil { 11 | return number 12 | } 13 | return reg.ReplaceAllString(number, "") 14 | } 15 | 16 | func RemovePlus(number string) string { 17 | reg, err := regexp.Compile("[^0-9]+") 18 | if err != nil { 19 | return number 20 | } 21 | return reg.ReplaceAllString(number, "") 22 | } 23 | -------------------------------------------------------------------------------- /middlewares/auth.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func AuthMiddleware() gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | user, password, ok := c.Request.BasicAuth() 12 | 13 | if ok && user == "admin" && password == "admin" { 14 | c.Next() 15 | } else { 16 | response := gin.H{ 17 | "status": "error", 18 | "message": "Not Authorized", 19 | } 20 | c.JSON(http.StatusUnauthorized, response) 21 | c.Abort() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /constant/constant.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | var neoMsConst = map[string]interface{}{ 4 | "GatewayDialString":"sofia/gateway/pstn_trunk", 5 | "SipGatwayDialString": "sofia/gateway/pstn_trunk", 6 | "DefaultVendorAuthId": "TINIYO1SECRET1AUTHID", 7 | "ExportFsVars": "'tiniyo_accid,parent_call_sid,parent_call_uuid'", 8 | "NumberExportFsVars": "'tiniyo_accid,parent_call_sid,parent_call_uuid,tiniyo_did_number'", 9 | "ApiVersion": "2010-04-01", 10 | } 11 | 12 | func GetConstant(varName string) interface{} { 13 | return neoMsConst[varName] 14 | } -------------------------------------------------------------------------------- /adapters/callstate.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | /* 4 | Golang Says Interface name should name with er 5 | */ 6 | type CallStateAdapter interface { 7 | Get(key string) ([]byte, error) 8 | Del(key string) error 9 | Set(key string, state []byte, expire ...int) error 10 | GetMembersScore(key string) (map[string]int64, error) 11 | IncrKeyMemberScore(key string, member string, score int) (int64, error) 12 | DelKeyMember(key string, member string) error 13 | SetRecordingJob(state []byte) error 14 | KeyExist(key string) (bool, error) 15 | AddSetMember(key string, member string, expired ...int) error 16 | } 17 | -------------------------------------------------------------------------------- /models/errors.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "net/http" 4 | 5 | type RequestError struct { 6 | StatusCode int 7 | Err error 8 | } 9 | 10 | func (r *RequestError) Error() string { 11 | return r.Err.Error() 12 | } 13 | 14 | func (r *RequestError) RatingRoutingMissing() bool { 15 | return r.StatusCode == http.StatusServiceUnavailable 16 | } 17 | 18 | func (r *RequestError) NestedDialElement() bool { 19 | return r.StatusCode == http.StatusMethodNotAllowed 20 | } 21 | 22 | func (r *RequestError) PaymentRequired() bool { 23 | return r.StatusCode == http.StatusPaymentRequired 24 | } 25 | 26 | func (r *RequestError) BadCallerID() bool { 27 | return r.StatusCode == http.StatusBadRequest 28 | } -------------------------------------------------------------------------------- /managers/tinixml/reject.go: -------------------------------------------------------------------------------- 1 | package tinixml 2 | 3 | import ( 4 | "github.com/beevik/etree" 5 | "github.com/tiniyo/neoms/adapters" 6 | "github.com/tiniyo/neoms/logger" 7 | ) 8 | 9 | /* 10 | 11 | 12 | 13 | */ 14 | 15 | func ProcessReject(msAdapter *adapters.MediaServer, uuid string, element *etree.Element) error { 16 | reason := "CALL_REJECTED" 17 | for _, attr := range element.Attr { 18 | logger.Logger.Debug("ATTR: %s=%s\n", attr.Key, attr.Value) 19 | if attr.Key == "reason" && attr.Value == "busy" { 20 | reason = "USER_BUSY" 21 | } 22 | } 23 | err := (*msAdapter).CallHangupWithReason(uuid, reason) 24 | return err 25 | } 26 | -------------------------------------------------------------------------------- /managers/confmanager.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/tiniyo/neoms/logger" 6 | ) 7 | 8 | type ConfManagerInterface interface { 9 | CreateConf(from string, to string) 10 | GetConf(confId string) 11 | DeleteConf(confId string) 12 | CreateConference(uuid string, name string, authId string) string 13 | } 14 | 15 | type ConfManager struct{} 16 | 17 | func (cm ConfManager) CreateConf(from string, to string) { 18 | 19 | } 20 | 21 | func (cm ConfManager) GetConf(confId string) { 22 | 23 | } 24 | 25 | func (cm ConfManager) DeleteConf(confId string) { 26 | 27 | } 28 | 29 | func (cm ConfManager) CreateConference(uuid string, name string, authId string) string { 30 | logger.Logger.Debug("Creating Conference for " + uuid + "with name " + name) 31 | confName := fmt.Sprintf("%s-%s@tiniyo", authId, name) 32 | return confName 33 | } 34 | -------------------------------------------------------------------------------- /server/router.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/tiniyo/neoms/controller" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func NewRouter() *gin.Engine { 10 | router := gin.New() 11 | router.Use(gin.Logger()) 12 | router.Use(gin.Recovery()) 13 | 14 | callController := new(controller.CallController) 15 | 16 | callController.InitializeCallController() 17 | 18 | rootPath := router.Group("/api") 19 | { 20 | versionPath := rootPath.Group("v1") 21 | { 22 | account := versionPath.Group("account") 23 | { 24 | account.POST(":account_id/Call/:call_id", callController.CreateCall) 25 | account.PUT(":account_id/Call/:call_id", callController.UpdateCall) 26 | account.GET(":account_id/Call/:call_id", callController.GetCall) 27 | account.DELETE(":account_id/Call/:call_id", callController.DeleteCall) 28 | } 29 | 30 | versionPath.GET("health", callController.GetHealth) 31 | } 32 | } 33 | return router 34 | } 35 | -------------------------------------------------------------------------------- /helper/jwt.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "github.com/dgrijalva/jwt-go" 5 | "os" 6 | "time" 7 | ) 8 | 9 | type JwtTokenInfo struct { 10 | Ip string 11 | Username string 12 | Password string 13 | } 14 | 15 | type JwtTokenInfos []JwtTokenInfo 16 | 17 | func CreateToken(jwtInfos JwtTokenInfos) (string, error) { 18 | var err error 19 | //Creating Access Token 20 | os.Setenv("ACCESS_SECRET", "RZ+w1Vr/dk4nZHvd/B7av/pOGiNzYlPZ") //this should be in an env file 21 | atClaims := jwt.MapClaims{} 22 | 23 | for _, jwtInfo := range jwtInfos { 24 | atClaims[jwtInfo.Ip] = jwtInfo.Username 25 | atClaims[jwtInfo.Username] = jwtInfo.Password 26 | } 27 | atClaims["authorized"] = true 28 | atClaims["exp"] = time.Now().Add(time.Minute * 15).Unix() 29 | at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims) 30 | token, err := at.SignedString([]byte(os.Getenv("ACCESS_SECRET"))) 31 | if err != nil { 32 | return "", err 33 | } 34 | return token, nil 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tiniyo/neoms 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 7 | github.com/beevik/etree v1.1.0 8 | github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 // indirect 9 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 10 | github.com/eclipse/paho.mqtt.golang v1.3.1 11 | github.com/evalphobia/logrus_sentry v0.8.2 12 | github.com/fiorix/go-eventsocket v0.0.0-20180331081222-a4a0ee7bd315 13 | github.com/getsentry/raven-go v0.2.0 // indirect 14 | github.com/gin-gonic/gin v1.6.3 15 | github.com/go-redis/redis/v8 v8.4.10 16 | github.com/go-resty/resty/v2 v2.4.0 17 | github.com/hb-go/json v0.0.0-20170624084651-15ef86c8b796 18 | github.com/json-iterator/go v1.1.10 19 | github.com/nsqio/go-nsq v1.0.8 20 | github.com/patrickmn/go-cache v2.1.0+incompatible 21 | github.com/pkg/errors v0.9.1 // indirect 22 | github.com/satori/go.uuid v1.2.0 23 | github.com/sirupsen/logrus v1.7.0 24 | github.com/terminalstatic/go-xsd-validate v0.1.4 25 | ) 26 | -------------------------------------------------------------------------------- /managers/recordingsmanager.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/tiniyo/neoms/config" 6 | "github.com/tiniyo/neoms/helper" 7 | "github.com/tiniyo/neoms/logger" 8 | ) 9 | 10 | func postRecordingData(callSid string, evHeader []byte) { 11 | dataMap := make(map[string]interface{}) 12 | if err := json.Unmarshal(evHeader, &dataMap); err != nil { 13 | logger.UuidLog("Err", callSid, fmt.Sprint("post recordings failed - ", err)) 14 | return 15 | } 16 | recordingServiceUrl := fmt.Sprintf("%s", config.Config.RecordingService.BaseUrl) 17 | statusCode, _, err := helper.Post(callSid,dataMap, recordingServiceUrl) 18 | if err != nil { 19 | logger.UuidLog("Err", callSid, fmt.Sprint("post recordings failed - ", err)) 20 | } else if statusCode != 200 && statusCode != 201 { 21 | logger.UuidLog("Err", callSid, fmt.Sprint("post recordings failed - ", statusCode)) 22 | } else { 23 | logger.UuidLog("Info", callSid, "recordings success response received") 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /managers/tinixml/pause.go: -------------------------------------------------------------------------------- 1 | package tinixml 2 | 3 | import ( 4 | "time" 5 | "strconv" 6 | "github.com/beevik/etree" 7 | "github.com/tiniyo/neoms/logger" 8 | ) 9 | 10 | /* 11 | 12 | 13 | 14 | */ 15 | 16 | func ProcessPause(uuid string, element *etree.Element) { 17 | pauseTime := time.Duration(1) 18 | for _, attr := range element.Attr { 19 | logger.Logger.Debug("ATTR: %s=%s\n", attr.Key, attr.Value) 20 | if attr.Key == "length"{ 21 | pauseT, err := strconv.Atoi(attr.Value) 22 | if pauseT == 0 || err != nil{ 23 | pauseTime = time.Duration(1) 24 | }else{ 25 | pauseTime = time.Duration(pauseT) 26 | } 27 | } 28 | } 29 | time.Sleep(pauseTime * time.Second) 30 | return 31 | } 32 | 33 | 34 | func ProcessPauseTime(dur int) { 35 | pauseTime := time.Duration(dur) 36 | time.Sleep(pauseTime * time.Second) 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16.3-alpine as builder 2 | ENV GO111MODULE=on 3 | WORKDIR /go/src/github.com/tiniyo/neoms 4 | COPY . . 5 | RUN apk update 6 | RUN apk upgrade 7 | RUN apk add --update gcc g++ libxml2-dev 8 | 9 | RUN GOOS=linux go build -a -installsuffix cgo -o app . 10 | 11 | FROM alpine:latest 12 | RUN apk --no-cache add ca-certificates curl bash libxml2 13 | WORKDIR / 14 | COPY --from=builder /go/src/github.com/tiniyo/neoms/app . 15 | COPY --from=builder /go/src/github.com/tiniyo/neoms/config.toml /etc/ 16 | COPY --from=builder /go/src/github.com/tiniyo/neoms/entrypoint.sh . 17 | COPY --from=builder /go/src/github.com/tiniyo/neoms/TinyMLSchema.xsd . 18 | 19 | # Health Check for the service 20 | HEALTHCHECK --timeout=5s --interval=3s --retries=3 CMD curl --fail http://localhost:9092/api/v1/health || exit 1 21 | 22 | # Expose the application on port 8080. 23 | # This should be the same as in the app.conf file 24 | EXPOSE 9092 25 | 26 | RUN chmod 755 /entrypoint.sh && \ 27 | chown root:root /entrypoint.sh 28 | 29 | CMD ["/entrypoint.sh"] 30 | -------------------------------------------------------------------------------- /utils/xmlvalidate.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | 6 | xsdvalidate "github.com/terminalstatic/go-xsd-validate" 7 | ) 8 | 9 | var xsdhandler *xsdvalidate.XsdHandler 10 | 11 | func ValidateXML(inXml []byte) (bool, string) { 12 | xmlhandler, err := xsdvalidate.NewXmlHandlerMem(inXml, xsdvalidate.ParsErrDefault) 13 | if err != nil { 14 | panic(err) 15 | } 16 | 17 | err = xsdhandler.Validate(xmlhandler, xsdvalidate.ValidErrDefault) 18 | if err != nil { 19 | switch err.(type) { 20 | case xsdvalidate.ValidationError: 21 | fmt.Println(err) 22 | fmt.Printf("Error in line: %d\n", err.(xsdvalidate.ValidationError).Errors[0].Line) 23 | fmt.Println(err.(xsdvalidate.ValidationError).Errors[0].Message) 24 | return false, err.(xsdvalidate.ValidationError).Errors[0].Message 25 | default: 26 | fmt.Println(err) 27 | } 28 | return false, err.Error() 29 | } 30 | return true, "" 31 | } 32 | 33 | func init() { 34 | xsdvalidate.Init() 35 | var err error 36 | xsdhandler, err = xsdvalidate.NewXsdHandlerUrl("./TinyMLSchema.xsd", xsdvalidate.ParsErrDefault) 37 | if err != nil { 38 | panic(err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | REGION=$REGION 6 | SER_USER=$SER_USER 7 | SER_SECRET=$SER_SECRET 8 | SIP_SERVICE=$SIP_SERVICE 9 | KAMGO_SERVICE=$KAMGO_SERVICE 10 | NUMBER_SERVICE=$NUMBER_SERVICE 11 | HEARTBEAT_SERVICE=$HEARTBEAT_SERVICE 12 | RATING_ROUTING_SERVICE=$RATING_ROUTING_SERVICE 13 | CDR_SERVICE=$CDR_SERVICE 14 | RECORDING_SERVICE=$RECORDING_SERVICE 15 | 16 | pushd /etc 17 | echo "[ENTRYPOINT] - Updating config.toml" 18 | sed -i "s//$REGION/g w /dev/stdout" config.toml 19 | sed -i "s//$SER_USER/g w /dev/stdout" config.toml 20 | sed -i "s//$SER_SECRET/g w /dev/stdout" config.toml 21 | sed -i "s##$HEARTBEAT_SERVICE#g w /dev/stdout" config.toml 22 | sed -i "s##$RATING_ROUTING_SERVICE#g w /dev/stdout" config.toml 23 | sed -i "s##$NUMBER_SERVICE#g w /dev/stdout" config.toml 24 | sed -i "s##$SIP_SERVICE#g w /dev/stdout" config.toml 25 | sed -i "s##$KAMGO_SERVICE#g w /dev/stdout" config.toml 26 | sed -i "s##$RECORDING_SERVICE#g w /dev/stdout" config.toml 27 | popd 28 | 29 | /app 30 | -------------------------------------------------------------------------------- /managers/tinixml/hangup.go: -------------------------------------------------------------------------------- 1 | package tinixml 2 | 3 | import ( 4 | "github.com/beevik/etree" 5 | "github.com/tiniyo/neoms/adapters" 6 | "github.com/tiniyo/neoms/logger" 7 | ) 8 | 9 | /* 10 | 11 | 12 | 13 | */ 14 | 15 | func ProcessHangup(msAdapter *adapters.MediaServer, uuid string, element *etree.Element) error { 16 | reason := "CALL_REJECTED" 17 | for _, attr := range element.Attr { 18 | logger.Logger.Debug("ATTR: %s=%s\n", attr.Key, attr.Value) 19 | if attr.Key == "reason" { 20 | reason = string(attr.Value) 21 | } 22 | } 23 | err := (*msAdapter).CallHangupWithReason(uuid, reason) 24 | if err != nil { 25 | return err 26 | } 27 | return nil 28 | } 29 | 30 | func ProcessHangupWithTiniyoReason(msAdapter *adapters.MediaServer, uuid string, reason string) error { 31 | err := (*msAdapter).CallHangupWithReason(uuid, reason) 32 | if err != nil { 33 | return err 34 | } 35 | return nil 36 | } 37 | 38 | func ProcessSyncHangup(msAdapter *adapters.MediaServer, uuid string, reason string) error { 39 | err := (*msAdapter).CallHangupWithSync(uuid, reason) 40 | if err != nil { 41 | return err 42 | } 43 | return nil 44 | } -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | def CONTAINER_NAME="neoms" 2 | def CONTAINER_TAG="latest" 3 | node { 4 | 5 | 6 | stage('Initialize') 7 | { 8 | def dockerHome = tool 'mydocker' 9 | env.PATH = "${dockerHome}/bin:${env.PATH}" 10 | } 11 | stage('Checkout') 12 | { 13 | checkout scm 14 | } 15 | 16 | 17 | stage('Build Image'){ 18 | imageBuild(CONTAINER_NAME, CONTAINER_TAG) 19 | } 20 | 21 | stage('Push to Docker Registry'){ 22 | withCredentials([usernamePassword(credentialsId: 'docker_registry', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) { 23 | pushToImage(CONTAINER_NAME, CONTAINER_TAG, USERNAME, PASSWORD) 24 | } 25 | } 26 | } 27 | 28 | 29 | def imageBuild(containerName, tag){ 30 | sh "docker build -t $containerName:$tag -t $containerName --pull --no-cache ." 31 | echo "Image build complete" 32 | } 33 | 34 | def pushToImage(containerName, tag, dockerUser, dockerPassword){ 35 | sh "docker login -u $dockerUser -p $dockerPassword https://registry.tiniyo.com" 36 | sh "docker tag $containerName:$tag registry.tiniyo.com/$containerName:$tag" 37 | sh "docker push registry.tiniyo.com/$containerName:$tag" 38 | echo "Image push complete" 39 | } 40 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | 2 | title = "NeoMs service configuration" 3 | 4 | [owner] 5 | name = "Komal Patel" 6 | organization = "Tiniyo" 7 | bio = "Founder" 8 | 9 | [server] 10 | port = ":9092" 11 | queuelen = 0 12 | errorqueuelen = 10000 13 | ginmode = "debug" 14 | 15 | [logging] 16 | facility = "local1" 17 | level = "debug" 18 | tag = "NeoMs" 19 | syslog = "127.0.0.1:514" 20 | 21 | [fs] 22 | fshost = "mediaserver" 23 | fsport = "8021" 24 | fspassword = "Tiniyo1234" 25 | fstimeout = 10 26 | 27 | [heartbeat] 28 | baseurl = "" 29 | username = "" 30 | secret = "" 31 | 32 | [rating] 33 | baseurl = "" 34 | username = "" 35 | secret = "" 36 | region = "" 37 | 38 | [numbers] 39 | baseurl = "" 40 | username = "" 41 | secret = "" 42 | 43 | [sipendpoint] 44 | baseurl = "" 45 | username = "" 46 | secret = "" 47 | 48 | [kamgo] 49 | baseurl = "" 50 | username = "" 51 | secret = "" 52 | 53 | [redis] 54 | redishost = "redis" 55 | redisport = "6379" 56 | redispassword = "" 57 | redisdb = 0 58 | 59 | [recordingservice] 60 | baseurl = "" 61 | username = "" 62 | secret = "" -------------------------------------------------------------------------------- /managers/tinixml/redirect.go: -------------------------------------------------------------------------------- 1 | package tinixml 2 | 3 | import ( 4 | "fmt" 5 | "github.com/beevik/etree" 6 | "github.com/tiniyo/neoms/adapters" 7 | "github.com/tiniyo/neoms/constant" 8 | "github.com/tiniyo/neoms/logger" 9 | "github.com/tiniyo/neoms/models" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | func ProcessRedirect(callSid string,msAdapter *adapters.MediaServer, data models.CallRequest,element *etree.Element) (error,string, string) { 15 | redirectUrl := element.Text() 16 | redirectMethod := "POST" 17 | if redirectUrl == "" { 18 | logger.UuidLog("Info",callSid,fmt.Sprintf("Received Empty Redirect url, Processing the current xml sequence - %s", redirectUrl)) 19 | return constant.ErrEmptyRedirect, "","" 20 | } 21 | logger.UuidLog("Info",callSid, fmt.Sprintf("Received Empty Redirect url, Processing the current xml sequence - %s", redirectUrl)) 22 | for _, attr := range element.Attr { 23 | if attr.Key == "method" { 24 | redirectMethod = strings.ToUpper(attr.Value) 25 | } 26 | } 27 | 28 | _ = (*msAdapter).PlayMediaFile(data.CallSid, "silence_stream://500", "1") 29 | 30 | for { 31 | if emptyUuidQueue, err := (*msAdapter).UuidQueueCount(callSid); err != nil { 32 | break 33 | } else if !emptyUuidQueue { 34 | time.Sleep(10 * time.Millisecond) 35 | } else { 36 | break 37 | } 38 | } 39 | 40 | return nil, redirectUrl, redirectMethod 41 | } 42 | 43 | -------------------------------------------------------------------------------- /models/rateroute.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type OriginationRate struct { 4 | Rate float32 `json:"rate_per_minute"` 5 | InitialPulse int64 `json:"initial_pulse"` 6 | SubPulse int64 `json:"sub_pulse"` 7 | } 8 | 9 | type TerminationRate struct { 10 | Rate float32 `json:"rate_per_minute"` 11 | InitialPulse int64 `json:"initial_pulse"` 12 | SubPulse int64 `json:"sub_pulse"` 13 | PrimaryIP string `json:"primary_ip"` 14 | FailoverIP string `json:"secondary_ip"` 15 | Prefix string `json:"match_prefix"` 16 | Priority int64 `json:"priority"` 17 | TrunkPrefix string `json:"trunk_prefix"` 18 | RemovePrefix string `json:"remove_prefix"` 19 | SipPilotNumber string `json:"sip_pilot_number"` 20 | FromRemovePrefix string `json:"from_remove_prefix"` 21 | Username string `json:"username"` 22 | Password string `json:"password"` 23 | } 24 | 25 | type InternalRateRoute struct { 26 | PulseRate float64 27 | Pulse int64 28 | RoutingGatewayString string 29 | RoutingUserAuthToken string 30 | SipPilotNumber string 31 | TrunkPrefix string 32 | RemovePrefix string /**/ 33 | FromRemovePrefix string 34 | } 35 | 36 | type RatingRoutingResponse struct { 37 | Orig OriginationRate `json:"origination"` 38 | Term []*TerminationRate `json:"termination"` 39 | } 40 | -------------------------------------------------------------------------------- /managers/webhooks/dialstatus.go: -------------------------------------------------------------------------------- 1 | package webhooks 2 | 3 | import ( 4 | "fmt" 5 | "github.com/tiniyo/neoms/helper" 6 | "github.com/tiniyo/neoms/logger" 7 | "github.com/tiniyo/neoms/models" 8 | "strings" 9 | ) 10 | 11 | func ProcessStatusCallbackUrl(dataCallRequest models.CallRequest, state string) { 12 | var err error 13 | callSid := dataCallRequest.CallSid 14 | dataMap := make(map[string]interface{}) 15 | if callbackByte, err := json.Marshal(dataCallRequest.Callback); err == nil { 16 | if err := json.Unmarshal(callbackByte, &dataMap); err != nil { 17 | logger.UuidLog("Err", callSid, fmt.Sprint("call back map conversion issue - ", err)) 18 | } 19 | } 20 | if strings.Contains(dataCallRequest.StatusCallbackEvent, state) && 21 | len(dataCallRequest.StatusCallback) > 0 && dataCallRequest.StatusCallbackMethod == "GET" { 22 | _, _, err = helper.Get(callSid,dataMap, dataCallRequest.StatusCallback) 23 | if err != nil { 24 | logger.UuidLog("Err", callSid, fmt.Sprint("failed status callback with error ", 25 | err, " url ", dataCallRequest.StatusCallback)) 26 | } 27 | } else if strings.Contains(dataCallRequest.StatusCallbackEvent, state) && 28 | len(dataCallRequest.StatusCallback) > 0 { 29 | _, _, err = helper.Post(callSid,dataMap, dataCallRequest.StatusCallback) 30 | if err != nil { 31 | logger.UuidLog("Err", callSid, fmt.Sprint("failed status callback with error ", 32 | err, " url ", dataCallRequest.StatusCallback)) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /managers/tinixml/play.go: -------------------------------------------------------------------------------- 1 | package tinixml 2 | 3 | import ( 4 | "fmt" 5 | "github.com/beevik/etree" 6 | "github.com/tiniyo/neoms/adapters" 7 | "github.com/tiniyo/neoms/logger" 8 | "github.com/tiniyo/neoms/models" 9 | "strconv" 10 | ) 11 | 12 | /* 13 | playing from xml 14 | 15 | https://s3.amazonaws.com/tiniyo/Trumpet.mp3 16 | 17 | */ 18 | func ProcessPlay(msAdapter *adapters.MediaServer, data models.CallRequest, element *etree.Element) error { 19 | callSid := data.CallSid 20 | if data.Status != "in-progress"{ 21 | (*msAdapter).AnswerCall(data.CallSid) 22 | } 23 | 24 | loopCount := 1 25 | for _, attr := range element.Attr { 26 | logger.Logger.Debug("ATTR: %s=%s\n", attr.Key, attr.Value) 27 | if attr.Key == "loop" { 28 | loopCount, _ = strconv.Atoi(attr.Value) 29 | if loopCount == 0 { 30 | loopCount = 1 31 | } 32 | } 33 | } 34 | strLoopCount := strconv.Itoa(loopCount) 35 | if err := (*msAdapter).PlayMediaFile(callSid, element.Text(), strLoopCount);err != nil { 36 | logger.UuidLog("Err", callSid, fmt.Sprint("error while playing media file", err)) 37 | return err 38 | } 39 | return nil 40 | } 41 | 42 | /* 43 | playing from rest 44 | { 45 | "to":"your_destination", 46 | "from":"your_callerId", 47 | "play":"url for file" 48 | } 49 | */ 50 | func ProcessPlayFile(msAdapter *adapters.MediaServer, uuid string, fileUrl string) error { 51 | loopCount := 3 52 | strLoopCount := strconv.Itoa(loopCount) 53 | err := (*msAdapter).PlayMediaFile(uuid, fileUrl, strLoopCount) 54 | if err != nil { 55 | return err 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /managers/webhooks/sipstatus.go: -------------------------------------------------------------------------------- 1 | package webhooks 2 | 3 | import ( 4 | "fmt" 5 | jsoniter "github.com/json-iterator/go" 6 | "github.com/tiniyo/neoms/helper" 7 | "github.com/tiniyo/neoms/logger" 8 | "github.com/tiniyo/neoms/models" 9 | "strings" 10 | ) 11 | 12 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 13 | 14 | func ProcessDialSipStatusCallbackUrl(dataCallRequest models.CallRequest, state string) { 15 | if dataCallRequest.DialSipStatusCallback == "" { 16 | return 17 | } 18 | var err error 19 | callSid := dataCallRequest.CallSid 20 | dataMap := make(map[string]interface{}) 21 | if callbackByte, err := json.Marshal(dataCallRequest.Callback); err == nil { 22 | if err := json.Unmarshal(callbackByte, &dataMap); err != nil { 23 | logger.UuidLog("Err", callSid, fmt.Sprint("call back map conversion issue - ", err)) 24 | } 25 | } 26 | if strings.Contains(dataCallRequest.DialSipStatusCallbackEvent, state) && 27 | dataCallRequest.DialSipStatusCallbackMethod == "GET" { 28 | _, _, err = helper.Get(callSid,dataMap, dataCallRequest.DialSipStatusCallback) 29 | if err != nil { 30 | logger.UuidLog("Err", callSid, fmt.Sprint("failed status callback with error ", 31 | err, " url ", dataCallRequest.DialSipStatusCallback)) 32 | } 33 | 34 | } else if strings.Contains(dataCallRequest.DialSipStatusCallbackEvent, state) { 35 | _, _, err = helper.Post(callSid,dataMap, dataCallRequest.DialSipStatusCallback) 36 | if err != nil { 37 | logger.UuidLog("Err", callSid, fmt.Sprint("failed status callback with error ", 38 | err, " url ", dataCallRequest.DialSipStatusCallback)) 39 | } 40 | 41 | } 42 | } -------------------------------------------------------------------------------- /managers/webhooks/numberstatus.go: -------------------------------------------------------------------------------- 1 | package webhooks 2 | 3 | import ( 4 | "fmt" 5 | "github.com/tiniyo/neoms/helper" 6 | "github.com/tiniyo/neoms/logger" 7 | "github.com/tiniyo/neoms/models" 8 | "strings" 9 | ) 10 | 11 | func ProcessDialNumberStatusCallbackUrl(dataCallRequest models.CallRequest, state string) { 12 | if dataCallRequest.DialNumberStatusCallback == "" { 13 | return 14 | } 15 | var err error 16 | callSid := dataCallRequest.CallSid 17 | dataMap := make(map[string]interface{}) 18 | if callbackByte, err := json.Marshal(dataCallRequest.Callback); err == nil { 19 | if err := json.Unmarshal(callbackByte, &dataMap); err != nil { 20 | logger.UuidLog("Err", callSid, fmt.Sprint("call back map conversion issue - ", err)) 21 | } 22 | } 23 | if strings.Contains(dataCallRequest.DialNumberStatusCallbackEvent, state) && 24 | len(dataCallRequest.DialNumberStatusCallback) > 0 && dataCallRequest.DialNumberStatusCallbackMethod == "GET" { 25 | _, _, err = helper.Get(callSid,dataMap, dataCallRequest.DialNumberStatusCallback) 26 | if err != nil { 27 | logger.UuidLog("Err", callSid, fmt.Sprint("failed status callback with error ", 28 | err, " url ", dataCallRequest.DialNumberStatusCallback)) 29 | } 30 | } else if strings.Contains(dataCallRequest.DialNumberStatusCallbackEvent, state) && 31 | len(dataCallRequest.DialNumberStatusCallback) > 0 { 32 | _, _, err = helper.Post(callSid,dataMap, dataCallRequest.DialNumberStatusCallback) 33 | if err != nil { 34 | logger.UuidLog("Err", callSid, fmt.Sprint("failed status callback with error ", 35 | err, " url ", dataCallRequest.DialNumberStatusCallback)) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /managers/tinixml/number.go: -------------------------------------------------------------------------------- 1 | package tinixml 2 | 3 | import ( 4 | "fmt" 5 | "github.com/beevik/etree" 6 | "github.com/tiniyo/neoms/logger" 7 | "github.com/tiniyo/neoms/models" 8 | ) 9 | 10 | /* 11 | 12 | 13 | 1234567890 14 | 15 | 16 | */ 17 | /* 18 | 19 | 20 | 1234567890 21 | 22 | 23 | */ 24 | 25 | func ProcessNumber(dialVars, destination string) string { 26 | dialNode := fmt.Sprintf("[%s,ignore_early_media=true,absolute_codec_string='PCMU,PCMA'," + 27 | "call_type=Number]sofia/gateway/pstn_trunk/%s", dialVars, destination) 28 | return dialNode 29 | } 30 | 31 | func ProcessNumberAttr(data *models.CallRequest, child *etree.Element) { 32 | data.DialNumberAttr.DialNumberMethod = "POST" 33 | data.DialNumberAttr.DialNumberStatusCallbackEvent = "completed" 34 | data.DialNumberAttr.DialNumberStatusCallbackMethod = "POST" 35 | data.DestType = "number" 36 | for _, attr := range child.Attr { 37 | switch attr.Key { 38 | case "method": 39 | data.DialNumberAttr.DialNumberMethod = attr.Value 40 | case "sendDigits": 41 | data.DialNumberAttr.DialNumberSendDigits= attr.Value 42 | case "url": 43 | data.DialNumberAttr.DialNumberUrl = attr.Value 44 | case "statusCallback": 45 | data.DialNumberAttr.DialNumberStatusCallback = attr.Value 46 | case "statusCallbackEvent": 47 | data.DialNumberAttr.DialNumberStatusCallbackEvent = attr.Value 48 | case "statusCallbackMethod": 49 | data.DialNumberAttr.DialNumberStatusCallbackMethod = attr.Value 50 | case "byoc": 51 | data.DialNumberAttr.DialNumberByoc = attr.Value 52 | default: 53 | logger.UuidLog("Err", data.ParentCallSid, fmt.Sprint("Attribute not supported - ", attr.Key)) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /managers/tinixml/conference.go: -------------------------------------------------------------------------------- 1 | package tinixml 2 | 3 | import ( 4 | "fmt" 5 | "github.com/beevik/etree" 6 | "net/url" 7 | "github.com/tiniyo/neoms/logger" 8 | "github.com/tiniyo/neoms/models" 9 | ) 10 | 11 | /* 12 | 13 | 14 | Test1234 15 | 16 | 17 | 18 | Attributes: 19 | muted - Whether or not a caller can speak in a conference. Default is false. 20 | beep - Whether or not a sound is played when callers leave or enter a conference. Default is true. 21 | startConferenceOnEnter - If a participant joins and startConferenceOnEnter is false, 22 | that participant will hear background music and stay muted until 23 | a participant with startConferenceOnEnter set.Default is true. 24 | endConferenceOnExit - If a participant with endConferenceOnExit set to true leaves a conference, 25 | the conference terminates and all participants drop out of the call. Default is false. 26 | 27 | */ 28 | func ProcessConference(callSid string, element *etree.Element, authId string) string { 29 | confName := fmt.Sprintf("%s-%s@tiniyo", authId, url.PathEscape(element.Text())) 30 | //check if conference already running 31 | //on which mediaserver its running 32 | //loopback to that mediaserver bridge conference 33 | //save attr to centralise system 34 | //query using conference name get attr also 35 | logger.UuidLog("Info", callSid, fmt.Sprintf("conference name is %s", confName)) 36 | return confName 37 | } 38 | 39 | func ProcessConferenceAttr(data *models.CallRequest, child *etree.Element) { 40 | data.DialConferenceAttr.DialConferenceBeep = "true" 41 | for _, attr := range child.Attr { 42 | switch attr.Key { 43 | case "beep": 44 | if attr.Value == "false" || attr.Value == "onEnter" || attr.Value == "onExit" { 45 | data.DialConferenceAttr.DialConferenceBeep = attr.Value 46 | } 47 | default: 48 | logger.UuidLog("Err", data.ParentCallSid, fmt.Sprint("Attribute not supported - ", attr.Key)) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /helper/sanity.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | ) 7 | 8 | func IsValidRecordingStatusCallbackEvent(event string) bool { 9 | switch event { 10 | case 11 | "in-progress", 12 | "completed", 13 | "absent": 14 | return true 15 | } 16 | return false 17 | } 18 | func IsValidRecordValue(record string) bool { 19 | switch record { 20 | case 21 | "true", 22 | "false", 23 | "record-from-answer", 24 | "record-from-ringing", 25 | "record-from-answer-dual", 26 | "do-not-record", 27 | "record-from-ringing-dual": 28 | return true 29 | } 30 | return false 31 | } 32 | func IsValidRecordingTrack(track string) bool { 33 | switch track { 34 | case 35 | "inbound", 36 | "outbound", 37 | "both": 38 | return true 39 | } 40 | return false 41 | } 42 | 43 | func IsValidTrim(trim string) bool { 44 | switch trim { 45 | case 46 | "trim-silence", 47 | "do-not-trim": 48 | return true 49 | } 50 | return false 51 | } 52 | 53 | func IsValidCallReason(reason string) bool { 54 | return len(reason) <= 50 55 | } 56 | 57 | func IsValidTimeOut(timeout string) bool { 58 | intTimeout, err := strconv.Atoi(timeout) 59 | if err != nil { 60 | return false 61 | } 62 | if intTimeout > 600 { 63 | intTimeout = 600 64 | } 65 | return true 66 | } 67 | 68 | func IsValidTimeLimit(timeLimit string) bool { 69 | _, err := strconv.Atoi(timeLimit) 70 | if err != nil { 71 | return false 72 | } 73 | return true 74 | } 75 | 76 | func IsValidRingTone(ringtone string) bool { 77 | switch ringtone { 78 | case 79 | "at", 80 | "au", "bg", "br", "be", "ch", 81 | "cl", "cn", "cz", "de", "dk", 82 | "ee", "es", "fi", "fr", "gr", 83 | "hu", "il", "in", "it", "lt", 84 | "jp", "mx", "my", "nl", "no", 85 | "nz", "ph", "pl", "pt", "ru", 86 | "se", "sg", "th", "uk", "us", "us-old", "tw", "ve", "za": 87 | return true 88 | } 89 | return false 90 | } 91 | 92 | func DtmfSanity(number string) string { 93 | reg, err := regexp.Compile("w#*[^0-9]+") 94 | if err != nil { 95 | return number 96 | } 97 | return reg.ReplaceAllString(number, "") 98 | } -------------------------------------------------------------------------------- /models/inbound.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type PhoneNumberInfo struct { 6 | PhoneNumber string `json:"phone_number"` 7 | Rate float64 `json:"rps"` 8 | AuthID string `json:"acc_id"` 9 | VendorAuthID string `json:"parent_act_id"` 10 | InitPulse int64 `json:"initial_pulse"` 11 | SubPulse int64 `json:"sub_pulse"` 12 | Application Application `json:"application"` 13 | Host string `json:"host"` 14 | } 15 | 16 | type Application struct { 17 | Name string `json:"AppName"` 18 | InboundURL string `json:"Url"` 19 | InboundMethod string `json:"Method"` 20 | FallbackMethod string `json:"FallbackMethod"` 21 | FallbackUrl string `json:"FallbackUrl"` 22 | StatusCallback string `json:"StatusCallback"` 23 | StatusCallbackMethod string `json:"StatusCallbackMethod"` 24 | StatusCallbackEvent string `json:"StatusCallbackEvent"` 25 | } 26 | type NumberAPIResponse struct { 27 | AppResponse PhoneNumberInfo `json:"message"` 28 | } 29 | 30 | type SipLocation struct { 31 | ID string `json:"id"` 32 | Ruid string `json:"ruid"` 33 | Username string `json:"username"` 34 | Domain string `json:"domain"` 35 | Contact string `json:"contact"` 36 | Received string `json:"received"` 37 | Path string `json:"path"` 38 | Expires time.Time `json:"expires"` 39 | Q string `json:"q"` 40 | Callid string `json:"callid"` 41 | Cseq string `json:"cseq"` 42 | LastModified time.Time `json:"last_modified"` 43 | Flags string `json:"flags"` 44 | Cflags string `json:"cflags"` 45 | UserAgent string `json:"user_agent"` 46 | Socket string `json:"socket"` 47 | Methods string `json:"methods"` 48 | Instance string `json:"instance"` 49 | RegID string `json:"reg_id"` 50 | ServerID string `json:"server_id"` 51 | ConnectionID string `json:"connection_id"` 52 | Keepalive string `json:"keepalive"` 53 | Partition string `json:"partition"` 54 | } 55 | -------------------------------------------------------------------------------- /models/response.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | 4 | type CallResponse struct { 5 | DateUpdated interface{} `json:"date_updated,omitempty"` 6 | PriceUnit string `json:"price_unit,omitempty"` 7 | ParentCallSid interface{} `json:"parent_call_sid,omitempty"` 8 | CallerName interface{} `json:"caller_name,omitempty"` 9 | Duration interface{} `json:"duration,omitempty"` 10 | From string `json:"from,omitempty"` 11 | To string `json:"to,omitempty"` 12 | Annotation interface{} `json:"annotation,omitempty"` 13 | AnsweredBy interface{} `json:"answered_by,omitempty"` 14 | Sid string `json:"sid,omitempty"` 15 | QueueTime string `json:"queue_time,omitempty"` 16 | Price interface{} `json:"price,omitempty"` 17 | APIVersion string `json:"api_version,omitempty"` 18 | Status string `json:"status,omitempty"` 19 | Direction string `json:"direction,omitempty"` 20 | StartTime interface{} `json:"start_time,omitempty"` 21 | DateCreated interface{} `json:"date_created,omitempty"` 22 | FromFormatted string `json:"from_formatted,omitempty"` 23 | GroupSid interface{} `json:"group_sid,omitempty"` 24 | TrunkSid interface{} `json:"trunk_sid,omitempty"` 25 | ForwardedFrom interface{} `json:"forwarded_from,omitempty"` 26 | URI string `json:"uri,omitempty"` 27 | AccountSid string `json:"account_sid,omitempty"` 28 | EndTime interface{} `json:"end_time,omitempty"` 29 | ToFormatted string `json:"to_formatted,omitempty"` 30 | PhoneNumberSid string `json:"phone_number_sid,omitempty"` 31 | SubresourceUris struct { 32 | Notifications string `json:"notifications,omitempty"` 33 | Recordings string `json:"recordings,omitempty"` 34 | Payments string `json:"payments,omitempty"` 35 | Feedback string `json:"feedback,omitempty"` 36 | Events string `json:"events,omitempty"` 37 | FeedbackSummaries string `json:"feedback_summaries,omitempty"` 38 | } `json:"subresource_uris,omitempty"` 39 | } 40 | 41 | -------------------------------------------------------------------------------- /managers/webhooks/recordingstatus.go: -------------------------------------------------------------------------------- 1 | package webhooks 2 | 3 | import ( 4 | "fmt" 5 | "github.com/tiniyo/neoms/helper" 6 | "github.com/tiniyo/neoms/logger" 7 | "github.com/tiniyo/neoms/models" 8 | "strings" 9 | ) 10 | 11 | func ProcessRecordingStatusCallbackUrl(dataCallRequest models.CallRequest, state string) { 12 | if dataCallRequest.RecordingStatusCallback == ""{ 13 | return 14 | } 15 | callSid := dataCallRequest.CallSid 16 | if callSid == ""{ 17 | callSid = dataCallRequest.Sid 18 | } 19 | var err error 20 | dataMap := make(map[string]interface{}) 21 | 22 | dataCallRequest.RecordCallback.RecordCallSid = dataCallRequest.ParentCallSid 23 | if dataCallRequest.RecordingStatusCallbackEvent == ""{ 24 | dataCallRequest.RecordingStatusCallbackEvent = "completed" 25 | } 26 | 27 | if dataCallRequest.RecordCallback.RecordingDuration == "0" && state == "completed" { 28 | state = "absent" 29 | dataCallRequest.RecordCallback.RecordingStatus = "absent" 30 | } 31 | if callbackByte, err := json.Marshal(dataCallRequest.RecordCallback); err == nil { 32 | if err := json.Unmarshal(callbackByte, &dataMap); err != nil { 33 | logger.UuidLog("Err", callSid, fmt.Sprint("update issue - ", err)) 34 | } 35 | } 36 | //get the child uuid and get the dial recordcallback 37 | if strings.Contains(dataCallRequest.RecordingStatusCallbackEvent, state) && 38 | len(dataCallRequest.RecordingStatusCallback) > 0 && dataCallRequest.RecordingStatusCallbackMethod == "GET" { 39 | if _, _, err = helper.Get(callSid,dataMap, dataCallRequest.RecordingStatusCallback); err != nil { 40 | logger.UuidLog("Err", callSid, fmt.Sprint("failed recording status callback with error ", 41 | err, " url ", dataCallRequest.RecordingStatusCallback)) 42 | } 43 | } else if strings.Contains(dataCallRequest.RecordingStatusCallbackEvent, state) && 44 | len(dataCallRequest.RecordingStatusCallback) > 0 { 45 | if _, _, err = helper.Post(callSid,dataMap, dataCallRequest.RecordingStatusCallback); err != nil { 46 | logger.UuidLog("Err", callSid, fmt.Sprint("failed recording status callback with error ", 47 | err, " url ", dataCallRequest.RecordingStatusCallback)) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/BurntSushi/toml" 7 | ) 8 | 9 | // TomlConfig represent the root of configuration 10 | type TomlConfig struct { 11 | Title string 12 | Owner Owner 13 | Server Server 14 | Logging Logging 15 | Fs Freeswitch 16 | Redis Redis 17 | Heartbeat Heartbeat 18 | RecordingService RecordingService 19 | Rating Rating 20 | Numbers Numbers 21 | SipEndpoint SipEndpoint 22 | Kamgo Kamgo 23 | } 24 | 25 | type Heartbeat struct { 26 | BaseUrl string 27 | UserName string 28 | Secret string 29 | } 30 | 31 | type RecordingService struct { 32 | BaseUrl string 33 | UserName string 34 | Secret string 35 | } 36 | 37 | type Rating struct { 38 | BaseUrl string 39 | UserName string 40 | Secret string 41 | Region string 42 | } 43 | type Numbers struct { 44 | BaseUrl string 45 | UserName string 46 | Secret string 47 | } 48 | type SipEndpoint struct { 49 | BaseUrl string 50 | UserName string 51 | Secret string 52 | } 53 | 54 | type Kamgo struct { 55 | BaseUrl string 56 | UserName string 57 | Secret string 58 | } 59 | 60 | /* Owner represent owner of the module*/ 61 | type Owner struct { 62 | Name string 63 | Org string `toml:"organization"` 64 | Bio string 65 | } 66 | 67 | type Server struct { 68 | Port string 69 | QueueLen int 70 | ErrorQueueLen int 71 | GinMode string 72 | } 73 | 74 | type Logging struct { 75 | Facility string 76 | Level string 77 | Tag string 78 | Syslog string 79 | Sentry string 80 | } 81 | 82 | type Freeswitch struct { 83 | FsHost string 84 | FsPort string 85 | FsPassword string 86 | FsTimeout int 87 | } 88 | 89 | type Redis struct { 90 | RedisHost string 91 | RedisPort string 92 | RedisPassword string 93 | RedisDB int 94 | } 95 | 96 | var Config TomlConfig 97 | 98 | func InitConfig() { 99 | var err error 100 | 101 | configFile := os.Getenv("WEBFS_CONFIG") 102 | if len(configFile) == 0 { 103 | configFile = "/etc/config.toml" 104 | } 105 | 106 | if _, err = toml.DecodeFile(configFile, &Config); err != nil { 107 | return 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /adapters/factory/factory.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "github.com/tiniyo/neoms/adapters" 5 | "github.com/tiniyo/neoms/adapters/mediaserver" 6 | "log" 7 | "sync" 8 | "sync/atomic" 9 | ) 10 | 11 | // Once is an object that will perform exactly one action. 12 | type Once struct { 13 | m sync.Mutex 14 | done uint32 15 | } 16 | 17 | // Do calls the function f if and only if Do is being called for the 18 | // first time for this instance of Once. In other words, given 19 | // var once Once 20 | // if once.Do(f) is called multiple times, only the first call will invoke f, 21 | // even if f has a different value in each invocation. A new instance of 22 | // Once is required for each function to execute. 23 | // 24 | // Do is intended for initialization that must be run exactly once. Since f 25 | // is niladic, it may be necessary to use a function literal to capture the 26 | // arguments to a function to be invoked by Do: 27 | // config.once.Do(func() { config.init(filename) }) 28 | // 29 | // Because no call to Do returns until the one call to f returns, if f causes 30 | // Do to be called, it will deadlock. 31 | // 32 | // If f panics, Do considers it to have returned; future calls of Do return 33 | // without calling f. 34 | // 35 | func (o *Once) Do(f func()) { 36 | if atomic.LoadUint32(&o.done) == 1 { // <-- Check 37 | return 38 | } 39 | // Slow-path. 40 | o.m.Lock() // <-- Lock 41 | defer o.m.Unlock() 42 | if o.done == 0 { // <-- Check 43 | defer atomic.StoreUint32(&o.done, 1) 44 | f() 45 | } 46 | } 47 | 48 | var instance *mediaserver.MsFreeSWITCHGiz 49 | var once sync.Once 50 | 51 | func GetMSInstance() adapters.MediaServer { 52 | once.Do(func() { 53 | instance = new(mediaserver.MsFreeSWITCHGiz) 54 | }) 55 | return adapters.MediaServer(instance) 56 | } 57 | 58 | type MediaServerFactory func(conf map[string]string) (adapters.MediaServer, error) 59 | 60 | var mediaServerFactories = make(map[string]MediaServerFactory) 61 | 62 | func RegisterMediaServer(name string, factory MediaServerFactory) { 63 | if factory == nil { 64 | log.Panicf("Datastore factory %s does not exist.", name) 65 | } 66 | _, registered := mediaServerFactories[name] 67 | if registered { 68 | log.Panicf("Datastore factory %s already registered. Ignoring.", name) 69 | } 70 | mediaServerFactories[name] = factory 71 | } 72 | -------------------------------------------------------------------------------- /adapters/mediaserver.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | type MediaServer interface { 4 | // Initialize 5 | InitializeCallbackMediaServers(cb MediaServerCallbacker) error 6 | //Call 7 | AnswerCall(uuid string) error 8 | PreAnswerCall(uuid string) error 9 | PlayMediaFile(uuid string, fileUrl string, loopCount string) error 10 | PlayBeep(uuid string) error 11 | Speak(uuid string, voiceId, text string) error 12 | CallNewOutbound(cmd string) error 13 | CallTransfer() error 14 | CallSendDTMF(uuid string, dtmf string) error 15 | BreakAllUuid(uuid string) error 16 | CallReceiveDTMF(uuid string) error 17 | SetRecordStereo(uuid string) error 18 | Set(uuid, value string) error 19 | UuidQueueCount(uuid string) (bool,error) 20 | MultiSet(uuid, value string) error 21 | CallRecord(uuid string, recordFile string) error 22 | Record(uuid string, recordFile string, maxDuration string, silenceSeconds string) error 23 | CallBridge(uuid string, otherUuid string) error 24 | CallIntercept(uuid string, otherUuid string) error 25 | CallHangup(uuid string) error 26 | CallHangupWithReason(uuid string, reason string) error 27 | CallHangupWithSync(uuid string, reason string) error 28 | EnableSessionHeartBeat(uuid, interval string) error 29 | //Conference 30 | ConfCreate(uuid, conferenceName string) error 31 | ConfBridge(uuid, bridgeArgs string) error 32 | ConfSetAutoCall(uuid, bridgeArgs string) error 33 | ConfAddMember() error 34 | ConfRemoveMember() error 35 | } 36 | 37 | // MediaServerCallbackInterface callback of the media server 38 | type MediaServerCallbacker interface { 39 | //Status 40 | CallBackMediaServerStatus(status int) error 41 | CallBackDTMFDetected(uuid string, evHeader []byte) error 42 | CallBackProgress(uuid string) error 43 | CallBackAnswered(uuid string, evHeader []byte) error 44 | CallBackProgressMedia(uuid string, evHeader []byte) error 45 | CallBackHangup(uuid string) error 46 | CallBackPark(uuid string, evHeader []byte) error 47 | CallBackDestroy(uuid string) error 48 | CallBackExecuteComplete(uuid string) error 49 | CallBackHangupComplete(uuid string, evHeader []byte) error 50 | CallBackRecordingStart(uuid string,evHeader []byte) error 51 | CallBackRecordingStop(uuid string,evHeader []byte) error 52 | CallBackBridged(uuid string) error 53 | CallBackUnBridged(uuid string) error 54 | CallBackSessionHeartBeat(puuid, uuid string) error 55 | CallBackMessage(uuid string) error 56 | CallBackCustom(uuid string) error 57 | CallBackOriginate(uuid string,evHeader []byte) error 58 | } 59 | -------------------------------------------------------------------------------- /services/mqttchannel.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/url" 7 | "time" 8 | 9 | mqtt "github.com/eclipse/paho.mqtt.golang" 10 | "github.com/hb-go/json" 11 | ) 12 | 13 | type TiniyoMqttClient struct { 14 | topic string 15 | client mqtt.Client 16 | } 17 | 18 | func connect(clientId string, uri *url.URL) mqtt.Client { 19 | opts := createClientOptions(clientId, uri) 20 | client := mqtt.NewClient(opts) 21 | token := client.Connect() 22 | for !token.WaitTimeout(3 * time.Second) { 23 | } 24 | if err := token.Error(); err != nil { 25 | log.Fatal(err) 26 | } 27 | return client 28 | } 29 | 30 | func createClientOptions(clientId string, uri *url.URL) *mqtt.ClientOptions { 31 | opts := mqtt.NewClientOptions() 32 | opts.AddBroker(fmt.Sprintf("tcp://%s", uri.Host)) 33 | opts.SetUsername(uri.User.Username()) 34 | password, _ := uri.User.Password() 35 | opts.SetPassword(password) 36 | opts.SetClientID(clientId) 37 | return opts 38 | } 39 | 40 | func listen(uri *url.URL, topic string) { 41 | client := connect("sub", uri) 42 | client.Subscribe(topic, 0, func(client mqtt.Client, msg mqtt.Message) { 43 | fmt.Printf("* [%s] %s\n", msg.Topic(), string(msg.Payload())) 44 | }) 45 | } 46 | 47 | type ConferencePacket struct { 48 | InfoType string `json:"type"` /* request / notify / response */ 49 | UserName string `json:"user_name"` 50 | UserID string `json:"user_id"` 51 | EventType string `json:"event_type"` 52 | EventName string `json:"event_name"` 53 | EventData string `json:"event_data"` 54 | } 55 | 56 | // export CLOUDMQTT_URL=mqtt://:@.cloudmqtt.com:/ 57 | 58 | func (tmc *TiniyoMqttClient) Publish(cp ConferencePacket) { 59 | b, _ := json.Marshal(cp) 60 | tmc.client.Publish(tmc.topic, 0, false, b) 61 | } 62 | 63 | func (tmc *TiniyoMqttClient) Initialize(topic string) { 64 | tmc.topic = topic 65 | uri, err := url.Parse("mqtt://127.0.0.1:1883/" + topic) 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | go listen(uri, topic) 70 | tmc.client = connect("pub", uri) 71 | } 72 | 73 | /* 74 | func main() { 75 | tmc := new(TiniyoMqttClient) 76 | tmc.Initialize("timetest") 77 | 78 | timer := time.NewTicker(2 * time.Second) 79 | cp := ConferencePacket{} 80 | cp.InfoType = "notify" 81 | cp.UserName = "shailesh" 82 | cp.UserID = "part-495e77c5-238e-46b8-9dc6-97202b0bb1fe" 83 | cp.EventType = "add-member" 84 | 85 | for t := range timer.C { 86 | cp.EventData = t.String() 87 | tmc.Publish(cp) 88 | } 89 | } 90 | */ 91 | -------------------------------------------------------------------------------- /managers/callstats/callstats.go: -------------------------------------------------------------------------------- 1 | package callstats 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/patrickmn/go-cache" 7 | "github.com/tiniyo/neoms/adapters" 8 | "github.com/tiniyo/neoms/adapters/callstate" 9 | "github.com/tiniyo/neoms/logger" 10 | "github.com/tiniyo/neoms/models" 11 | "time" 12 | ) 13 | 14 | type CallStatManager struct { 15 | } 16 | 17 | var appCache *cache.Cache 18 | var callStatObj adapters.CallStateAdapter 19 | 20 | func (cs *CallStatManager) InitCallStatManager() { 21 | callStatObj, _ = callstate.NewCallStateAdapter() 22 | appCache = cache.New(5*time.Minute, 10*time.Minute) 23 | } 24 | 25 | func SetCallDetailByUUID(cr *models.CallRequest) error { 26 | if cr.CallSid == "" { 27 | cr.CallSid = cr.Sid 28 | } 29 | //get the json request of call request 30 | jsonCallRequestData, err := json.Marshal(cr) 31 | if err != nil { 32 | return err 33 | } 34 | /* Store Call State */ 35 | err = callStatObj.Set(cr.CallSid, jsonCallRequestData) 36 | if err != nil { 37 | logger.Logger.Error("SetCallState Failed", err) 38 | return err 39 | } 40 | return nil 41 | } 42 | 43 | func GetCallDetailByUUID(uuid string) (*models.CallRequest, error) { 44 | var data models.CallRequest 45 | if val, err := callStatObj.Get(uuid); err == nil { 46 | logger.Logger.WithField("uuid", uuid).Info("call details are - ", string(val)) 47 | if err := json.Unmarshal(val, &data); err != nil { 48 | logger.Logger.WithField("uuid", uuid).Error("error while unmarshal - ", err) 49 | return nil, err 50 | } 51 | return &data, nil 52 | } 53 | return nil, nil 54 | } 55 | 56 | func GetCallBackDetailByUUID(callSid string) (*models.Callback, error) { 57 | var data models.Callback 58 | statusCallbackKey := fmt.Sprintf("statusCallback:%s", callSid) 59 | if val, err := callStatObj.Get(statusCallbackKey); err == nil { 60 | logger.Logger.WithField("uuid", callSid).Info("call details are - ", string(val)) 61 | if err := json.Unmarshal(val, &data); err != nil { 62 | logger.Logger.WithField("uuid", callSid).Error("error while unmarshal - ", err) 63 | return nil, err 64 | } 65 | return &data, nil 66 | } 67 | return nil, nil 68 | } 69 | 70 | func GetLiveCallStatus(callSid string) string { 71 | var statusCallback models.Callback 72 | statusCallbackKey := fmt.Sprintf("statusCallback:%s", callSid) 73 | logger.UuidLog("Info", callSid, fmt.Sprintf("Getting current status callback with key - %s", statusCallbackKey)) 74 | if currentState, err := callStatObj.Get(statusCallbackKey); err == nil { 75 | if err := json.Unmarshal(currentState, &statusCallback); err != nil { 76 | logger.UuidLog("Err", callSid, fmt.Sprintf("triggerCallBack - error while unmarshal - %s", err.Error())) 77 | return "no_status" 78 | } 79 | return statusCallback.CallStatus 80 | } 81 | return "no_status" 82 | } 83 | -------------------------------------------------------------------------------- /managers/tinixml/gather.go: -------------------------------------------------------------------------------- 1 | package tinixml 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/beevik/etree" 7 | "github.com/tiniyo/neoms/adapters" 8 | "github.com/tiniyo/neoms/logger" 9 | "github.com/tiniyo/neoms/managers/callstats" 10 | "github.com/tiniyo/neoms/models" 11 | "strconv" 12 | ) 13 | 14 | func ProcessGather(msAdapter *adapters.MediaServer, data *models.CallRequest, child *etree.Element) (bool, error) { 15 | if data.Status != "in-progress" { 16 | _ = (*msAdapter).AnswerCall(data.CallSid) 17 | } 18 | callSid := data.CallSid 19 | ProcessGatherAttr(data, child) 20 | ProcessGatherChild(msAdapter, data, child) 21 | if timeout, err := strconv.Atoi(data.GatherTimeout); err == nil { 22 | ProcessPauseTime(timeout+5) 23 | } 24 | if currentState, err := callstats.GetCallBackDetailByUUID(callSid); currentState != nil && err ==nil { 25 | logger.UuidLog("Err", data.ParentCallSid, fmt.Sprint(currentState)) 26 | if currentState.DtmfInputType == "dtmf" { 27 | return false, nil 28 | }else if data.GatherActionOnEmptyResult == "true"{ 29 | //here we need to call dtmf timeout url 30 | //we will create a function in webhook and call it from here 31 | return false, errors.New("TIMEOUT") 32 | }else{ 33 | return true, nil 34 | } 35 | }else { 36 | return false, nil 37 | } 38 | } 39 | 40 | func ProcessGatherAttr(data *models.CallRequest, child *etree.Element) { 41 | data.GatherAttr.GatherFinishOnKey = "#" 42 | data.GatherAttr.GatherTimeout = "5" 43 | data.GatherAttr.GatherMethod = "POST" 44 | data.GatherAttr.GatherActionOnEmptyResult = "false" 45 | for _, attr := range child.Attr { 46 | switch attr.Key { 47 | case "method": 48 | data.GatherAttr.GatherMethod = attr.Value 49 | case "action": 50 | data.GatherAttr.GatherAction = attr.Value 51 | case "finishOnKey": 52 | data.GatherAttr.GatherFinishOnKey = attr.Value 53 | case "numDigits": 54 | data.GatherAttr.GatherNumDigit = attr.Value 55 | case "actionOnEmptyResult": 56 | data.GatherAttr.GatherActionOnEmptyResult = attr.Value 57 | case "timeout": 58 | data.GatherAttr.GatherTimeout = attr.Value 59 | default: 60 | logger.UuidLog("Err", data.ParentCallSid, fmt.Sprint("Attribute not supported - ", attr.Key)) 61 | } 62 | } 63 | /* Store Call State */ 64 | err := callstats.SetCallDetailByUUID(data) 65 | if err != nil { 66 | logger.Logger.Error("SetCallState Failed", err) 67 | } 68 | } 69 | 70 | func ProcessGatherChild(msAdapter *adapters.MediaServer, data *models.CallRequest, child *etree.Element) { 71 | for _, dialChild := range child.ChildElements() { 72 | switch dialChild.Tag { 73 | case "Say", "Speak": 74 | _ = ProcessSpeak(msAdapter, *data, dialChild) 75 | case "Pause": 76 | ProcessPause(data.CallSid, dialChild) 77 | case "Play": 78 | _ = ProcessPlay(msAdapter, *data, dialChild) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /adapters/callstate/redis.go: -------------------------------------------------------------------------------- 1 | package callstate 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/go-redis/redis/v8" 7 | "github.com/tiniyo/neoms/adapters" 8 | "github.com/tiniyo/neoms/config" 9 | "github.com/tiniyo/neoms/logger" 10 | "time" 11 | ) 12 | 13 | var ctx = context.Background() 14 | 15 | type CallState struct { 16 | client *redis.Client 17 | } 18 | 19 | func NewCallStateAdapter() (adapters.CallStateAdapter,error) { 20 | redisHostPort := config.Config.Redis.RedisHost + ":" + config.Config.Redis.RedisPort 21 | logger.Logger.Debug("Redis Config :", redisHostPort) 22 | return &CallState{ 23 | client: redis.NewClient(&redis.Options{ 24 | Addr: redisHostPort, 25 | MinIdleConns: 5, 26 | MaxRetries: 3, 27 | Password: "", // no password set 28 | DB: 0, // use default DB 29 | }), 30 | }, nil 31 | } 32 | 33 | func (cs CallState) Get(callUUID string) ([]byte, error) { 34 | val, err := cs.client.Get(ctx, callUUID).Bytes() 35 | if err != nil { 36 | return []byte("UNKNOWN"), err 37 | } 38 | return val, nil 39 | } 40 | 41 | func (cs CallState) Set(callUuid string, state []byte, expired ...int) error { 42 | expire := 0 * time.Second 43 | if len(expired) > 0 { 44 | expire = time.Duration(expired[0]) 45 | } 46 | err := cs.client.Set(ctx, callUuid, state, expire*time.Second).Err() 47 | return err 48 | } 49 | 50 | func (cs CallState) Del(callUUID string) error { 51 | return cs.client.Del(ctx, callUUID).Err() 52 | } 53 | 54 | 55 | 56 | func (cs CallState) KeyExist(key string) (bool, error) { 57 | val, err := cs.client.Exists(ctx, key).Result() 58 | if val != 1 || err != nil { 59 | return false, err 60 | } 61 | return true, nil 62 | } 63 | 64 | func (cs CallState) SetRecordingJob(state []byte) error { 65 | recordingJobKey := fmt.Sprint("tiniyo_namespace:jobs:s3_upload") 66 | recordingRangeKey := "tiniyo_namespace:known_jobs" 67 | var err error 68 | if err = cs.client.LPush(ctx, recordingJobKey, state).Err(); err == nil { 69 | err = cs.client.SAdd(ctx, recordingRangeKey, "s3_upload").Err() 70 | } 71 | return err 72 | } 73 | 74 | func (cs CallState) AddSetMember(key string, member string, expired ...int) error { 75 | err := cs.client.ZAdd(ctx, key, &redis.Z{ 76 | Score: 0, 77 | Member: member, 78 | }).Err() 79 | return err 80 | } 81 | 82 | func (cs CallState) GetMembersScore(key string) (map[string]int64, error) { 83 | //parentSidRelationKey := fmt.Sprintf("parent:%s",callUuid) 84 | var resultState = make(map[string]int64) 85 | if result, err := cs.client.ZRangeWithScores(ctx, key, 0, -1).Result(); err == nil { 86 | for _, v := range result { 87 | resultState[v.Member.(string)] = int64(v.Score) 88 | } 89 | return resultState, err 90 | } else { 91 | return resultState, err 92 | } 93 | } 94 | 95 | func (cs CallState) IncrKeyMemberScore(key string, member string, score int) (int64, error) { 96 | val, err := cs.client.ZIncr(ctx, key, &redis.Z{ 97 | Score: float64(score), 98 | Member: member, 99 | }).Result() 100 | return int64(val), err 101 | } 102 | 103 | func (cs CallState) DelKeyMember(key string, member string) error { 104 | err := cs.client.ZRem(ctx, key, member).Err() 105 | return err 106 | } 107 | -------------------------------------------------------------------------------- /managers/tinixml/sip.go: -------------------------------------------------------------------------------- 1 | package tinixml 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/beevik/etree" 7 | "github.com/tiniyo/neoms/config" 8 | "github.com/tiniyo/neoms/helper" 9 | "github.com/tiniyo/neoms/logger" 10 | "github.com/tiniyo/neoms/models" 11 | "strings" 12 | ) 13 | 14 | /* 15 | 16 | 17 | sip:chandra@phone.tiniyo.com 18 | 19 | 20 | */ 21 | /* 22 | 23 | 24 | sip:chandra@phone.tiniyo.com 25 | 26 | 27 | */ 28 | func ProcessSip(dialVars, sipDestination string) string { 29 | toUser := strings.Split(sipDestination, "@")[0] 30 | sipTo := strings.Split(toUser, ":") 31 | if sipTo[0] == "sip" { 32 | toUser = sipTo[1] 33 | } else { 34 | toUser = sipTo[0] 35 | } 36 | 37 | dialNode := "" 38 | 39 | if locationData := locationLookup(sipDestination); locationData != "" { 40 | if strings.Contains(locationData, "transport=ws") { 41 | dialNode = fmt.Sprintf("[%s,sip_ignore_183nosdp=true," + 42 | "webrtc_enable_dtls=true,media_webrtc=true,sip_h_X-Tiniyo-Sip=%s," + 43 | "absolute_codec_string='opus@20i,PCMU@20i',ignore_early_media=true,"+ 44 | "call_type=Sip,sip_h_X-Tiniyo-Phone=user]sofia/gateway/pstn_trunk/%s", 45 | dialVars, sipDestination,toUser) 46 | } else { 47 | dialNode = fmt.Sprintf("[%s,absolute_codec_string='PCMU,PCMA'," + 48 | "sip_ignore_183nosdp=true,ignore_early_media=true,"+ 49 | "call_type=Sip,sip_h_X-Tiniyo-Phone=user,sip_h_X-Tiniyo-Sip=%s]sofia/gateway/pstn_trunk/%s", 50 | dialVars, sipDestination, toUser) 51 | } 52 | } else { 53 | dialNode = fmt.Sprintf("[%s,ignore_early_media=true,"+ 54 | "sip_h_X-Tiniyo-Gateway=%s,absolute_codec_string='PCMU,PCMA',call_type=Sip,"+ 55 | "sip_h_X-Tiniyo-Phone=sip,sip_h_X-Tiniyo-Sip=%s]sofia/gateway/pstn_trunk/%s", 56 | dialVars, sipDestination, sipDestination,toUser) 57 | } 58 | return dialNode 59 | } 60 | 61 | func ProcessSipAttr(data *models.CallRequest, child *etree.Element) { 62 | data.DialSipAttr.DialSipMethod = "POST" 63 | data.DialSipAttr.DialSipStatusCallbackEvent = "completed" 64 | data.DialSipAttr.DialSipStatusCallbackMethod = "POST" 65 | data.DestType = "sip" 66 | for _, attr := range child.Attr { 67 | switch attr.Key { 68 | case "method": 69 | data.DialSipAttr.DialSipMethod = attr.Value 70 | case "password": 71 | data.DialSipAttr.DialSipPassword = attr.Value 72 | case "url": 73 | data.DialSipAttr.DialSipUrl = attr.Value 74 | case "statusCallback": 75 | data.DialSipAttr.DialSipStatusCallback = attr.Value 76 | case "statusCallbackEvent": 77 | data.DialSipAttr.DialSipStatusCallbackEvent = attr.Value 78 | case "statusCallbackMethod": 79 | data.DialSipAttr.DialSipStatusCallbackMethod = attr.Value 80 | case "username": 81 | data.DialSipAttr.DialSipUsername = attr.Value 82 | default: 83 | logger.UuidLog("Err", data.ParentCallSid, fmt.Sprint("Attribute not supported - ", attr.Key)) 84 | } 85 | } 86 | } 87 | 88 | /* 89 | Location lookup will be in kamgo instead of going to public 90 | */ 91 | func locationLookup(sipUser string) string { 92 | location := models.SipLocation{} 93 | url := fmt.Sprintf("%s/v1/Subscribers/%s/Locations", config.Config.Kamgo.BaseUrl, sipUser) 94 | statusCode, respBody, err := helper.Get(sipUser, nil, url) 95 | if err != nil || statusCode != 200 { 96 | return "" 97 | } 98 | if err = json.Unmarshal(respBody, &location);err != nil { 99 | return "" 100 | } 101 | return location.Contact 102 | } 103 | -------------------------------------------------------------------------------- /controller/callctrl.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "net/http" 7 | "github.com/tiniyo/neoms/logger" 8 | "github.com/tiniyo/neoms/managers" 9 | "github.com/tiniyo/neoms/managers/callstats" 10 | "github.com/tiniyo/neoms/models" 11 | ) 12 | 13 | type CallController struct { 14 | callManage managers.CallManagerInterface 15 | } 16 | 17 | func (u *CallController) InitializeCallController() { 18 | u.callManage = new(managers.CallManager) 19 | u.callManage = managers.NewCallManager() 20 | new(callstats.CallStatManager).InitCallStatManager() 21 | } 22 | 23 | /* 24 | Create Call Request [POST] 25 | */ 26 | func (u CallController) CreateCall(c *gin.Context) { 27 | authSid := c.Param("account_id") 28 | callSid := c.Param("call_id") 29 | logger.UuidLog("Info", callSid, "call create request") 30 | cr := models.CallRequest{} 31 | var err error 32 | if err = c.BindJSON(&cr); err == nil { 33 | cr.AccountSid = authSid 34 | cr.CallSid = callSid 35 | cr.Sid = callSid 36 | callResp, err := u.callManage.CreateCall(&cr) 37 | //we need to get callResponse here 38 | if err != nil { 39 | logger.UuidLog("Err", callSid, fmt.Sprint("JSON Parsing Failed :", err.Error())) 40 | c.JSON(http.StatusBadGateway, gin.H{"status": "failed", "request_uuid": cr.CallSid, "api_id": cr.CallSid}) 41 | return 42 | } 43 | logger.UuidLog("Info", callSid, fmt.Sprint("call created success :")) 44 | c.JSON(http.StatusOK, callResp) 45 | return 46 | } 47 | logger.UuidLog("Err", callSid, fmt.Sprint("JSON Parsing Failed :", err.Error())) 48 | c.JSON(http.StatusBadGateway, gin.H{"status": "failed", "request_uuid": cr.CallSid, "api_id": cr.CallSid}) 49 | } 50 | 51 | /* 52 | Update Call Request [PUT] 53 | */ 54 | func (u CallController) UpdateCall(c *gin.Context) { 55 | callSid := c.Param("call_id") 56 | logger.UuidLog("Info", callSid, "call update request") 57 | cr := models.CallUpdateRequest{} 58 | if err := c.BindJSON(&cr); err == nil { 59 | callResponse, err := u.callManage.UpdateCall(cr) 60 | if err != nil || callResponse == nil || callResponse.Sid == "" { 61 | logger.UuidLog("Err", callSid, fmt.Sprint("call update failed, call is not active :", err.Error())) 62 | c.JSON(http.StatusUnprocessableEntity, gin.H{"status": "failed", "request_uuid": cr.Sid, "api_id": cr.Sid}) 63 | return 64 | } 65 | logger.UuidLog("Info", callSid, fmt.Sprint("call updated success :")) 66 | c.JSON(http.StatusOK, callResponse) 67 | } 68 | c.JSON(http.StatusBadRequest, "Bad Request") 69 | } 70 | 71 | /* 72 | GET Call Request [GET] 73 | */ 74 | func (u CallController) GetCall(c *gin.Context) { 75 | accountID := c.Param("account_id") 76 | callID := c.Param("call_id") 77 | logger.Logger.Debug("Account ID :", accountID, " CallID :", callID) 78 | callResponse, err := u.callManage.GetCall(callID) 79 | if err != nil { 80 | c.JSON(http.StatusNotFound, gin.H{"status": "failed", "request_uuid": callID, "api_id": callID}) 81 | return 82 | } 83 | c.JSON(http.StatusOK, callResponse) 84 | return 85 | } 86 | 87 | /* 88 | Delete Call Request [DELETE] 89 | */ 90 | func (u CallController) DeleteCall(c *gin.Context) { 91 | accountID := c.Param("account_id") 92 | callID := c.Param("call_id") 93 | logger.Logger.Debug("Account ID :", accountID, " CallID :", callID) 94 | u.callManage.DeleteCallWithReason(callID, "DELETE_API_HANGUP") 95 | } 96 | 97 | func (u CallController) GetHealth(c *gin.Context) { 98 | /*msg, err := managers.HealthCheck() 99 | if err != nil { 100 | logger.Error(msg) 101 | c.String(503, msg) 102 | } else { 103 | c.String(200, msg) 104 | }*/ 105 | c.JSON(200, gin.H{ 106 | "Status": "0", 107 | }) 108 | } -------------------------------------------------------------------------------- /managers/heartbeatmanager.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/tiniyo/neoms/adapters" 6 | "github.com/tiniyo/neoms/config" 7 | "github.com/tiniyo/neoms/helper" 8 | "github.com/tiniyo/neoms/logger" 9 | "github.com/tiniyo/neoms/models" 10 | ) 11 | 12 | type HeartBeatManagerInterface interface { 13 | enableHeartbeat(data models.CallRequest) 14 | makeHeartBeatRequest(callSid string, heartbeatCount int64) error 15 | sendHeartBeat(accId string, callSid string, rate float64, duration int64) 16 | } 17 | 18 | type HeartBeatManager struct { 19 | callState adapters.CallStateAdapter 20 | } 21 | 22 | func NewHeartBeatManager(callState adapters.CallStateAdapter) HeartBeatManagerInterface { 23 | return HeartBeatManager{ 24 | callState: callState, 25 | } 26 | } 27 | 28 | func (hb HeartBeatManager) enableHeartbeat(data models.CallRequest) { 29 | callSid := data.CallSid 30 | 31 | logger.UuidLog("Info", callSid, fmt.Sprintf("enabling hearbeat first time,parent call sid is %s ", callSid)) 32 | parentSidRelationKey := fmt.Sprintf("parent:%s", data.ParentCallSid) 33 | if err := hb.callState.AddSetMember(parentSidRelationKey, callSid); err != nil { 34 | logger.UuidLog("Err", callSid, fmt.Sprintf("Trouble while setting up child "+ 35 | "and parent relationship in redis - %#v\n", err)) 36 | } 37 | 38 | logger.UuidLog("Info", callSid, fmt.Sprintf("sending heartbeat with rate %f %d ", data.Rate, data.Pulse)) 39 | 40 | rate := data.Rate 41 | pulse := data.Pulse 42 | accId := data.AccountSid 43 | if err := MsAdapter.EnableSessionHeartBeat(callSid, "1"); err != nil { 44 | logger.UuidLog("Err", callSid, fmt.Sprintf("Trouble while enabling session heartbeat - %#v\n", err)) 45 | } 46 | go hb.sendHeartBeat(accId, callSid, rate, pulse) 47 | } 48 | 49 | func (hb HeartBeatManager) makeHeartBeatRequest(callSid string, heartbeatCount int64) error { 50 | msg := fmt.Sprintf("heartbeat count is %d", heartbeatCount) 51 | logger.UuidLog("Info", callSid, msg) 52 | val, err := hb.callState.Get(callSid) 53 | if err == nil { 54 | /* Get answer url and its method */ 55 | var data models.CallRequest 56 | if err := json.Unmarshal(val, &data); err != nil { 57 | logger.Logger.WithField("uuid", callSid).Error(" error while unmarshal, heartbeat processing failed - ", err) 58 | return err 59 | } 60 | rate := data.Rate 61 | pulse := data.Pulse 62 | accId := data.AccountSid 63 | if heartbeatCount >= pulse { 64 | go hb.sendHeartBeat(accId, callSid, rate, pulse) 65 | parentSidRelationKey := fmt.Sprintf("parent:%s", data.ParentCallSid) 66 | _, _ = hb.callState.IncrKeyMemberScore(parentSidRelationKey, callSid, -int(pulse)) 67 | //reset the score here 68 | } 69 | } 70 | return nil 71 | } 72 | 73 | func (hb HeartBeatManager) sendHeartBeat(accId string, callSid string, rate float64, duration int64) { 74 | dataMap := make(map[string]interface{}) 75 | 76 | hearBeatUrl := fmt.Sprintf("%s/%s/Heartbeat/%s", config.Config.Heartbeat.BaseUrl, accId, callSid) 77 | hbRequest := models.HeartBeatPrivRequest{AccountID: accId, 78 | CallID: callSid, 79 | Rate: rate, 80 | Pulse: duration, 81 | Duration: duration} 82 | 83 | logger.UuidLog("Info", callSid, fmt.Sprint("heartbeat request - ", hbRequest, " heartbeat url - ", hearBeatUrl)) 84 | 85 | if byteData, err := json.Marshal(hbRequest); err == nil { 86 | if err := json.Unmarshal(byteData, &dataMap); err != nil { 87 | logger.UuidLog("Err", callSid, fmt.Sprint("send heartbeat failed - ", err)) 88 | return 89 | } 90 | } else { 91 | logger.UuidLog("Err", callSid, fmt.Sprint("send heartbeat failed - ", err)) 92 | return 93 | } 94 | 95 | logger.UuidLog("Info", callSid, fmt.Sprint("heartbeat request before post - ", dataMap, 96 | " heartbeat url - ", hearBeatUrl)) 97 | 98 | statusCode, _, err := helper.Post(callSid, dataMap, hearBeatUrl) 99 | if err != nil { 100 | logger.UuidLog("Err", callSid, fmt.Sprint("send heartbeat failed - ", err)) 101 | } else if statusCode != 200 { 102 | logger.UuidLog("Err", callSid, fmt.Sprint("send heartbeat failed - ", statusCode)) 103 | 104 | } else { 105 | logger.UuidLog("Info", callSid, "heartbeat success response received") 106 | } 107 | return 108 | } 109 | -------------------------------------------------------------------------------- /models/xml.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type DialAttr struct { 4 | DialAnswerOnBridge string `json:"DialAnswerOnBridge"` 5 | DialCallerId string `json:"DialCallerId"` 6 | DialHangupOnStar string `json:"DialHangupOnStar"` 7 | DialAction string `json:"DialAction"` 8 | DialMethod string `json:"DialMethod"` 9 | DialRingTone string `json:"DialRingTone"` 10 | DialTimeLimit string `json:"DialTimeLimit"` 11 | DialTimeout string `json:"DialTimeout"` 12 | } 13 | 14 | type DialRecordAttr struct { 15 | RecordTimeout string `json:"RecordTimeout"` 16 | RecordFinishOnKey string `json:"RecordFinishOnKey"` 17 | RecordMaxLength string `json:"RecordMaxLength"` 18 | RecordPlayBeep string `json:"RecordPlayBeep"` 19 | RecordAction string `json:"RecordAction"` 20 | RecordMethod string `json:"RecordMethod"` 21 | RecordStorageUrl string `json:"RecordStorageUrl"` 22 | RecordStorageUrlMethod string `json:"RecordStorageUrlMethod"` 23 | RecordTranscribe string `json:"RecordTranscribe"` 24 | RecordTranscribeCallback string `json:"RecordTranscribeCallback"` 25 | } 26 | 27 | type DialSipAttr struct { 28 | DialSipMethod string `json:"DialSipMethod"` 29 | DialSipPassword string `json:"DialSipPassword"` 30 | DialSipStatusCallbackEvent string `json:"DialSipStatusCallbackEvent"` 31 | DialSipStatusCallback string `json:"DialSipStatusCallback"` 32 | DialSipStatusCallbackMethod string `json:"DialSipStatusCallbackMethod"` 33 | DialSipUrl string `json:"DialSipUrl"` 34 | DialSipUsername string `json:"DialSipUsername"` 35 | } 36 | 37 | type DialNumberAttr struct { 38 | DialNumberMethod string `json:"DialNumberMethod"` 39 | DialNumberSendDigits string `json:"DialNumberSendDigits"` 40 | DialNumberStatusCallbackEvent string `json:"DialNumberStatusCallbackEvent"` 41 | DialNumberStatusCallback string `json:"DialNumberStatusCallback"` 42 | DialNumberStatusCallbackMethod string `json:"DialNumberStatusCallbackMethod"` 43 | DialNumberUrl string `json:"DialNumberUrl"` 44 | DialNumberByoc string `json:"DialNumberByoc"` 45 | } 46 | 47 | type GatherAttr struct { 48 | GatherFinishOnKey string `json:"GatherFinishOnKey"` 49 | GatherTimeout string `json:"GatherTimeout"` 50 | GatherAction string `json:"GatherAction"` 51 | GatherMethod string `json:"GatherMethod"` 52 | GatherNumDigit string `json:"GatherNumDigit"` 53 | GatherActionOnEmptyResult string `json:"GatherActionOnEmptyResult"` 54 | GatherEnhanced string `json:"GatherEnhanced"` 55 | } 56 | 57 | type DialConferenceAttr struct { 58 | DialConferenceMuted string `json:"DialConferenceMuted"` 59 | DialConferenceBeep string `json:"DialConferenceBeep"` 60 | DialConferenceStartConferenceOnEnter string `json:"DialConferenceStartConferenceOnEnter"` 61 | DialConferenceEndConferenceOnExit string `json:"DialConferenceEndConferenceOnExit"` 62 | DialConferenceParticipantLabel string `json:"DialConferenceParticipantLabel"` 63 | DialConferenceStatusCallbackEvent string `json:"DialConferenceStatusCallbackEvent"` 64 | DialConferenceStatusCallback string `json:"DialConferenceStatusCallback"` 65 | DialConferenceStatusCallbackMethod string `json:"DialConferenceStatusCallbackMethod"` 66 | DialConferenceJitterBufferSize string `json:"DialConferenceJitterBufferSize"` 67 | DialConferenceWaitUrl string `json:"DialConferenceWaitUrl"` 68 | DialConferenceWaitMethod string `json:"DialConferenceWaitMethod"` 69 | DialConferenceMaxParticipants string `json:"DialConferenceMaxParticipants"` 70 | DialConferenceRecord string `json:"DialConferenceRecord"` 71 | DialConferenceRegion string `json:"DialConferenceRegion"` 72 | DialConferenceTrim string `json:"DialConferenceTrim"` 73 | DialConferenceCoach string `json:"DialConferenceCoach"` 74 | DialConferenceRecordingStatusCallback string `json:"DialConferenceRecordingStatusCallback"` 75 | DialConferenceRecordingStatusCallbackEvent string `json:"DialConferenceRecordingStatusCallbackEvent"` 76 | DialConferenceRecordingStatusCallbackMethod string `json:"DialConferenceRecordingStatusCallbackMethod"` 77 | } 78 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "github.com/evalphobia/logrus_sentry" 6 | "github.com/go-resty/resty/v2" 7 | "github.com/sirupsen/logrus" 8 | logrus_syslog "github.com/sirupsen/logrus/hooks/syslog" 9 | "log/syslog" 10 | "os" 11 | "github.com/tiniyo/neoms/config" 12 | "strconv" 13 | ) 14 | 15 | var logLevel = map[string]logrus.Level{ 16 | "debug": logrus.DebugLevel, 17 | "info": logrus.InfoLevel, 18 | "warn": logrus.WarnLevel, 19 | } 20 | 21 | var facilityLevel = map[string]syslog.Priority{ 22 | "local0": syslog.LOG_LOCAL0, 23 | "local1": syslog.LOG_LOCAL1, 24 | "local2": syslog.LOG_LOCAL2, 25 | "local3": syslog.LOG_LOCAL3, 26 | } 27 | 28 | var Logger *logrus.Logger 29 | 30 | func InitLogger() { 31 | var err error 32 | Logger, err = NewLogger(config.Config.Logging.Level, config.Config.Logging.Facility, config.Config.Logging.Tag, 33 | config.Config.Logging.Sentry, config.Config.Logging.Syslog) 34 | Logger.SetFormatter(&logrus.JSONFormatter{}) 35 | Logger.SetReportCaller(true) 36 | if err != nil { 37 | return 38 | } 39 | } 40 | 41 | func GuardCritical(msg string, err error) { 42 | if err != nil { 43 | fmt.Printf("CRITICAL: %s: %v\n", msg, err) 44 | os.Exit(-1) 45 | } 46 | } 47 | 48 | func NewLogger(level, facility, tag string, sentry string, syslogAddr string) (*logrus.Logger, error) { 49 | l := logrus.New() 50 | 51 | fmt.Println("Log leven is ", level) 52 | ll, ok := logLevel[level] 53 | if !ok { 54 | fmt.Println("Unsupported loglevel, falling back to debug!") 55 | ll = logLevel["debug"] 56 | } 57 | l.Level = ll 58 | 59 | if sentry != "" { 60 | hostname, err := os.Hostname() 61 | GuardCritical("determining hostname failed", err) 62 | 63 | tags := map[string]string{ 64 | "tag": tag, 65 | "hostname": hostname, 66 | } 67 | 68 | sentryLevels := []logrus.Level{ 69 | logrus.PanicLevel, 70 | logrus.FatalLevel, 71 | logrus.ErrorLevel, 72 | } 73 | sentHook, err := logrus_sentry.NewWithTagsSentryHook(sentry, tags, sentryLevels) 74 | GuardCritical("configuring sentry failed", err) 75 | 76 | l.Hooks.Add(sentHook) 77 | } 78 | 79 | if syslogAddr != "" { 80 | lf, ok := facilityLevel[facility] 81 | if !ok { 82 | fmt.Println("Unsupported log facility, falling back to local0") 83 | lf = facilityLevel["local0"] 84 | } 85 | sysHook, err := logrus_syslog.NewSyslogHook("udp", syslogAddr, lf, tag) 86 | if err != nil { 87 | return l, err 88 | } 89 | l.Hooks.Add(sysHook) 90 | } 91 | return l, nil 92 | } 93 | 94 | func BuildLogEntry(l *logrus.Entry, in map[string]string) *logrus.Entry { 95 | for k, v := range in { 96 | l = l.WithField(k, v) 97 | } 98 | return l 99 | } 100 | func UuidLog(logLevel, uuid, message string) { 101 | if logLevel == "Err" { 102 | Logger.WithField("uuid", uuid).Error(message) 103 | } else if logLevel == "Info" { 104 | Logger.WithField("uuid", uuid).Info(message) 105 | } else { 106 | Logger.WithField("uuid", uuid).Debug(message) 107 | } 108 | } 109 | 110 | func UuidInboundLog(logLevel, uuid, message string) { 111 | if unQuoteMsg, err := strconv.Unquote(message); err == nil { 112 | message = unQuoteMsg 113 | } 114 | if logLevel == "Err" { 115 | Logger.WithField("uuid", uuid).WithField("direction", "inbound").Error(message) 116 | } else if logLevel == "Info" { 117 | Logger.WithField("uuid", uuid).WithField("direction", "inbound").Info(message) 118 | } else { 119 | Logger.WithField("uuid", uuid).WithField("direction", "inbound").Debug(message) 120 | } 121 | } 122 | 123 | func UuidHttpLog(uuid string, resp *resty.Response) { 124 | if resp != nil { 125 | ti := resp.Request.TraceInfo() 126 | Logger.WithField("uuid", uuid).WithField("Status", resp.Status()). 127 | WithField(" DNSLookup :", ti.DNSLookup). 128 | WithField(" ConnTime :", ti.ConnTime). 129 | WithField(" TCPConnTime :", ti.TCPConnTime). 130 | WithField(" TLSHandshake :", ti.TLSHandshake). 131 | WithField(" ServerTime :", ti.ServerTime). 132 | WithField(" ResponseTime :", ti.ResponseTime). 133 | WithField(" TotalTime :", ti.TotalTime). 134 | WithField(" IsConnReused :", ti.IsConnReused). 135 | WithField(" IsConnWasIdle :", ti.IsConnWasIdle). 136 | WithField(" ConnIdleTime :", ti.ConnIdleTime). 137 | WithField(" RequestAttempt:", ti.RequestAttempt). 138 | //WithField(" RemoteAddr :", ti.RemoteAddr.String()). 139 | Info("Http Response Received") 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /helper/fscmd.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | uuid4 "github.com/satori/go.uuid" 6 | "github.com/tiniyo/neoms/logger" 7 | "github.com/tiniyo/neoms/models" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | SipCallIdentityHeader = "sip_h_X-Tiniyo-Phone" 14 | ExportVars = "'tiniyo_accid\\,tiniyo_rate\\,tiniyo_pulse\\,parent_call_uuid\\,parent_call_sid'" 15 | DefaultCodecs = "'PCMU,PCMA'" 16 | DefaultCallerIdType = "pid" 17 | InstantRingback = "true" 18 | RingBack = "'%(2000\\,4000\\,440\\,480)'" 19 | ) 20 | 21 | func IsSipCall(dest string) bool { 22 | if strings.HasPrefix(dest, "sip:") { 23 | return true 24 | } 25 | return false 26 | } 27 | 28 | 29 | /* 30 | generating the dial string from vars 31 | */ 32 | func ConvertMapToDialString(dialVars map[string]string) (string, bool) { 33 | logger.Logger.Debug("In getDialString - ", dialVars) 34 | dialVarsCount := 0 35 | originateStr := "" 36 | isSipDestination := false 37 | for key, element := range dialVars { 38 | if key == SipCallIdentityHeader && element == "true" { 39 | isSipDestination = true 40 | } 41 | if dialVarsCount == 0 { 42 | originateStr = fmt.Sprintf("%s=%s", key, element) 43 | } else { 44 | originateStr = fmt.Sprintf("%s,%s=%s", originateStr, key, element) 45 | } 46 | dialVarsCount++ 47 | } 48 | return originateStr, isSipDestination 49 | } 50 | 51 | /* 52 | creating map of dial vars 53 | */ 54 | func GenDialString(cr *models.CallRequest) string { 55 | originateVars := make(map[string]string) 56 | if strings.HasPrefix(cr.To, "sip:") || strings.HasPrefix(cr.To, "sips:") { 57 | originateVars[SipCallIdentityHeader] = "true" 58 | } 59 | if cr.MaxDuration > 0 { 60 | strMaxDuration := fmt.Sprintf("+%d", cr.MaxDuration) 61 | retainDuration := fmt.Sprintf("'sched_hangup %s %s alotted_timeout'", strMaxDuration, cr.Sid) 62 | originateVars["api_on_answer"] = retainDuration 63 | } 64 | 65 | if cr.SipPilotNumber != ""{ 66 | originateVars["sip_h_X-Tiniyo-Pilot-Number"] = cr.SipPilotNumber 67 | } 68 | originateVars["originate_timeout"] = "65" 69 | originateVars["ignore_early_media"] = "ring_ready" 70 | if cr.Timeout != "" { 71 | if timeout, err := strconv.Atoi(cr.Timeout); err == nil && timeout < 600 { 72 | timeout = timeout + 5 73 | originateVars["originate_timeout"] = fmt.Sprintf("%d", timeout) 74 | } 75 | } 76 | originateVars["bridge_answer_timeout"] = originateVars["originate_timeout"] 77 | 78 | if cr.Record == "true" { 79 | recordDir := "/call_recordings" 80 | originateVars["recording_sid"] = uuid4.NewV4().String() 81 | recordFile := fmt.Sprintf("%s/%s-%s.mp3", recordDir, cr.AccountSid, cr.Sid) 82 | recordString := fmt.Sprintf("'record_session %s'", recordFile) 83 | originateVars["media_bug_answer_req"] = "true" 84 | originateVars["execute_on_answer_1"] = recordString 85 | switch cr.RecordingTrack { 86 | case "inbound": 87 | originateVars["RECORD_READ_ONLY"] = "true" 88 | case "outbound": 89 | originateVars["RECORD_WRITE_ONLY"] = "true" 90 | default: 91 | if cr.RecordingChannels == "dual" { 92 | originateVars["RECORD_STEREO"] = "true" 93 | } 94 | } 95 | } 96 | 97 | if strings.HasPrefix(cr.To, "sip:") { 98 | originateVars["call_type"] = "Sip" 99 | } else { 100 | originateVars["call_type"] = "Number" 101 | } 102 | 103 | originateVars["ringback"] = RingBack 104 | originateVars["instant_ringback"] = InstantRingback 105 | strCallerID := fmt.Sprintf("%s", cr.From) 106 | originateVars["origination_caller_id_number"] = strCallerID 107 | originateVars["origination_caller_id_name"] = strCallerID 108 | originateVars["sip_cid_type"] = DefaultCallerIdType 109 | originateVars["absolute_codec_string"] = DefaultCodecs 110 | originateVars["call_sid"] = cr.Sid 111 | originateVars["origination_uuid"] = cr.Sid 112 | originateVars["parent_call_sid"] = cr.ParentCallSid 113 | originateVars["parent_call_uuid"] = cr.ParentCallSid 114 | originateVars["tiniyo_accid"] = cr.AccountSid 115 | originateVars["direction"] = "outbound-api" 116 | strRate := fmt.Sprintf("%f", cr.Rate) 117 | originateVars["tiniyo_rate"] = strRate 118 | if cr.SendDigits != "" { 119 | originateVars["execute_on_answer_2"] = fmt.Sprintf("'send_dtmf %s'", cr.SendDigits) 120 | } 121 | strPulse := fmt.Sprintf("%d", cr.Pulse) 122 | originateVars["tiniyo_pulse"] = strPulse 123 | originateVars["export_vars"] = ExportVars 124 | originateString, _ := ConvertMapToDialString(originateVars) 125 | return originateString 126 | } -------------------------------------------------------------------------------- /managers/rateroute/ratingmanager.go: -------------------------------------------------------------------------------- 1 | package rateroute 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | "github.com/tiniyo/neoms/config" 8 | "github.com/tiniyo/neoms/helper" 9 | "github.com/tiniyo/neoms/logger" 10 | "github.com/tiniyo/neoms/managers/callstats" 11 | "github.com/tiniyo/neoms/models" 12 | "strings" 13 | ) 14 | 15 | func GetOutboundRateRoutes(callSid, vendorAuthId, authId, phoneNumber string) (string, *models.RatingRoutingResponse) { 16 | baseUrl := config.Config.Rating.BaseUrl 17 | region := config.Config.Rating.Region 18 | 19 | if vendorAuthId == "" || len(vendorAuthId)<12{ 20 | vendorAuthId = "TINIYO1SECRET1AUTHID" 21 | } 22 | 23 | url := fmt.Sprintf("%s/%s/Tenants/%s/%s/%s", baseUrl, vendorAuthId, authId, region, phoneNumber) 24 | logger.UuidLog("Info", callSid, fmt.Sprint("fetching rates with url - ", url)) 25 | 26 | var data models.RatingRoutingResponse 27 | if statusCode, respBody, err := helper.Get(callSid, nil, url); statusCode != 200 || err != nil || respBody == nil { 28 | logger.UuidLog("Err", callSid, fmt.Sprint("Error while fetching rates - failed", err)) 29 | return "failed", nil 30 | } else if err = json.Unmarshal(respBody, &data); err != nil { 31 | logger.UuidLog("Err", callSid, fmt.Sprint("Error while unmarshal rates - failed", err)) 32 | return "failed", nil 33 | } 34 | 35 | logger.UuidLog("Info", callSid, fmt.Sprint("termination route response is - ", data.Term)) 36 | logger.UuidLog("Info", callSid, fmt.Sprint("Origination rate response is - ", data.Orig)) 37 | 38 | return "success", &data 39 | } 40 | 41 | func GetSetOutboundRateRoutes(data *models.CallRequest, destNumber string) *models.InternalRateRoute { 42 | routingString := "" 43 | var rateRouteRes = new(models.InternalRateRoute) 44 | data.CallResponse.Direction = "outbound-call" 45 | if net.ParseIP(destNumber) != nil || 46 | strings.HasPrefix(destNumber, "sip:") || strings.Contains(destNumber, "@") { 47 | if !strings.HasPrefix(data.To, "sip:") { 48 | destNumber = fmt.Sprintf("sip:%s", data.To) 49 | } 50 | } else { 51 | destNumber = helper.NumberSanity(destNumber) 52 | } 53 | callSid := data.CallSid 54 | status, rateRoutes := GetOutboundRateRoutes(callSid, data.VendorAuthID, data.AccountSid, destNumber) 55 | if status == "failed" || rateRoutes == nil { 56 | logger.Logger.Error("Failed to get rates, rate not found") 57 | return nil 58 | } 59 | pulse := rateRoutes.Orig.InitialPulse 60 | data.Pulse = pulse 61 | perSecondRate := float64(rateRoutes.Orig.Rate / 60) 62 | rateInPulse := perSecondRate * float64(pulse) 63 | data.Rate = rateInPulse 64 | data.To = destNumber 65 | 66 | var routingTokenArray = helper.JwtTokenInfos{} 67 | 68 | switch data.DestType { 69 | case "sip", "Sip": 70 | logger.Logger.Info("SIP Destination, Skipping termination route processing") 71 | case "number", "Number": 72 | data.DestType = "number" 73 | logger.Logger.Info("Number Destination, termination route processing") 74 | if rateRoutes.Term == nil { 75 | logger.Logger.Error("No routes found, exit the call") 76 | return nil 77 | } 78 | for _, rt := range rateRoutes.Term { 79 | rateRouteRes.RemovePrefix = rt.RemovePrefix 80 | rateRouteRes.TrunkPrefix = rt.TrunkPrefix 81 | rateRouteRes.SipPilotNumber = rt.SipPilotNumber 82 | var routingToken = helper.JwtTokenInfo{} 83 | if rt.RemovePrefix != "" { 84 | destNumber = strings.TrimPrefix(destNumber, "+") 85 | destNumber = strings.TrimPrefix(destNumber, rt.RemovePrefix) 86 | } 87 | if rt.FromRemovePrefix != "" { 88 | rateRouteRes.FromRemovePrefix = rt.FromRemovePrefix 89 | } 90 | if rt.TrunkPrefix != "" { 91 | destNumber = fmt.Sprintf("%s%s", rt.TrunkPrefix, destNumber) 92 | } 93 | if routingString == "" { 94 | routingString = fmt.Sprintf("sip:%s@%s", destNumber, rt.PrimaryIP) 95 | } else { 96 | routingString = fmt.Sprintf("%s^sip:%s@%s", routingString, destNumber, rt.PrimaryIP) 97 | } 98 | if rt.Username != "" { 99 | routingToken.Ip = rt.PrimaryIP 100 | routingToken.Username = rt.Username 101 | routingToken.Password = rt.Password 102 | routingTokenArray = append(routingTokenArray, routingToken) 103 | } 104 | if rt.FailoverIP != "" { 105 | routingString = fmt.Sprintf("%s^sip:%s@%s", routingString, destNumber, rt.FailoverIP) 106 | if rt.Username != "" { 107 | routingToken.Ip = rt.FailoverIP 108 | routingToken.Username = rt.Username 109 | routingToken.Password = rt.Password 110 | routingTokenArray = append(routingTokenArray, routingToken) 111 | } 112 | } 113 | } 114 | default: 115 | } 116 | 117 | logger.UuidLog("Info", data.CallSid, fmt.Sprintf("Routing string for call is - %s", routingString)) 118 | 119 | if rateRouteRes.FromRemovePrefix != "" { 120 | data.FromRemovePrefix = rateRouteRes.FromRemovePrefix 121 | logger.UuidLog("Info", data.CallSid, fmt.Sprintf("From Remove Prefix is - %s", data.FromRemovePrefix)) 122 | } 123 | 124 | /* Store Call State */ 125 | err := callstats.SetCallDetailByUUID(data) 126 | if err != nil { 127 | logger.Logger.Error("SetCallState Failed", err) 128 | } 129 | 130 | /* Get JWT token for username and password based trunk */ 131 | if routingTokenArray != nil && len(routingTokenArray) > 0 { 132 | if jwtRouteToken, err := helper.CreateToken(routingTokenArray); err == nil { 133 | rateRouteRes.RoutingUserAuthToken = jwtRouteToken 134 | } 135 | } 136 | 137 | rateRouteRes.Pulse = pulse 138 | rateRouteRes.PulseRate = rateInPulse 139 | rateRouteRes.RoutingGatewayString = routingString 140 | return rateRouteRes 141 | } 142 | -------------------------------------------------------------------------------- /models/webhook.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ( 4 | Callback struct { 5 | Called string `json:"Called,omitempty" form:"Called" query:"Called" freeswitch:"Caller-Destination-Number"` 6 | Direction string `json:"Direction,omitempty" form:"Direction" query:"Direction" freeswitch:"Variable_direction"` 7 | Timestamp string `json:"Timestamp,omitempty" form:"Timestamp" query:"Timestamp" freeswitch:"Event-Date-GMT"` 8 | CallSid string `json:"CallSid,omitempty" form:"CallSid" query:"CallSid" freeswitch:"Variable_call_sid"` 9 | To string `json:"To,omitempty" form:"To" query:"To" freeswitch:"Variable_sip_req_user"` 10 | AccountSid string `json:"AccountSid,omitempty" form:"AccountSid" query:"AccountSid" freeswitch:"Variable_tiniyo_accid"` 11 | Caller string `json:"Caller,omitempty" form:"Caller" query:"Caller" freeswitch:"Caller-ANI"` 12 | From string `json:"From,omitempty" form:"From" query:"From" freeswitch:"Variable_sip_from_user"` 13 | ParentCallSid string `json:"ParentCallSid,omitempty" form:"ParentCallSid" query:"ParentCallSid" freeswitch:"Variable_parent_call_sid"` 14 | CallStatus string `json:"CallStatus,omitempty" form:"CallStatus" query:"CallStatus" freeswitch:"Hangup-Cause"` 15 | CallDuration string `json:"CallDuration,omitempty" form:"CallDuration" query:"CallDuration" freeswitch:"Variable_billsec"` 16 | ToState string `json:"ToState,omitempty" form:"ToState" query:"ToState"` 17 | CallerCountry string `json:"CallerCountry,omitempty" form:"CallerCountry" query:"CallerCountry" ` 18 | CallbackSource string `json:"CallbackSource,omitempty" form:"CallbackSource" query:"CallbackSource"` 19 | SipResponseCode string `json:"SipResponseCode,omitempty" form:"SipResponseCode" query:"SipResponseCode"` 20 | CallerState string `json:"CallerState,omitempty" form:"CallerState" query:"CallerState"` 21 | ToZip string `json:"ToZip,omitempty" form:"ToZip" query:"ToZip"` 22 | SequenceNumber string `json:"SequenceNumber,omitempty" form:"SequenceNumber" query:"SequenceNumber"` 23 | CallerZip string `json:"CallerZip,omitempty" form:"CallerZip" query:"CallerZip"` 24 | ToCountry string `json:"ToCountry,omitempty" form:"ToCountry" query:"ToCountry"` 25 | CalledZip string `json:"CalledZip,omitempty" form:"CalledZip" query:"CalledZip"` 26 | ApiVersion string `json:"ApiVersion,omitempty" form:"ApiVersion" query:"ApiVersion"` 27 | CalledCity string `json:"CalledCity,omitempty" form:"CalledCity" query:"CalledCity"` 28 | CalledCountry string `json:"CalledCountry,omitempty" form:"CalledCountry" query:"CalledCountry"` 29 | CallerCity string `json:"CallerCity,omitempty" form:"CallerCity" query:"CallerCity"` 30 | ToCity string `json:"ToCity,omitempty" form:"ToCity" query:"ToCity"` 31 | FromCountry string `json:"FromCountry,omitempty" form:"FromCountry" query:"FromCountry"` 32 | FromCity string `json:"FromCity,omitempty" form:"FromCity" query:"FromCity"` 33 | CalledState string `json:"CalledState,omitempty" form:"CalledState" query:"CalledState"` 34 | FromZip string `json:"FromZip,omitempty" form:"FromZip" query:"FromZip"` 35 | FromState string `json:"FromState,omitempty" form:"FromState" query:"FromState"` 36 | InitiationTime string `json:"InitiationTime,omitempty" form:"InitiationTime" query:"InitiationTime"` 37 | AnswerTime string `json:"AnswerTime,omitempty" form:"AnswerTime" query:"AnswerTime"` 38 | RingTime string `json:"RingTime,omitempty" form:"RingTime" query:"RingTime"` 39 | HangupTime string `json:"HangupTime,omitempty" form:"HangupTime" query:"HangupTime"` 40 | Digits string `json:"Digits,omitempty" form:"Digits" query:"Digits"` 41 | DtmfInputType string `json:"DtmfInputType,omitempty" form:"DtmfInputType" query:"DtmfInputType"` 42 | DialCallStatus string `json:"DialCallStatus,omitempty"` 43 | DialCallSid string `json:"DialCallSid,omitempty"` 44 | DialCallDuration string `json:"DialCallDuration,omitempty"` 45 | RecordingUrl string `json:"RecordingUrl,omitempty" form:"RecordingUrl" query:"RecordingUrl" freeswitch:"Variable_tiniyo_recording_file"` 46 | PriceUnit string `json:"price_unit,omitempty"` 47 | } 48 | 49 | 50 | RecordCallback struct { 51 | RecordEventTimestamp string `json:"Timestamp,omitempty" form:"Timestamp" query:"Timestamp" freeswitch:"Event-Date-GMT"` 52 | RecordingSource string `json:"RecordingSource,omitempty" form:"RecordingSource" query:"RecordingSource"` 53 | RecordingTrack string `json:"RecordingTrack,omitempty" form:"RecordingTrack" query:"RecordingTrack"` 54 | RecordingSid string `json:"RecordingSid,omitempty" form:"RecordingSid" query:"RecordingSid"` 55 | RecordingUrl string `json:"RecordingUrl,omitempty" form:"RecordingUrl" query:"RecordingUrl" freeswitch:"Record-File-Path"` 56 | RecordingStatus string `json:"RecordingStatus,omitempty" form:"RecordingStatus" query:"RecordingStatus"` 57 | RecordingChannels string `json:"RecordingChannels,omitempty" form:"RecordingChannels" query:"RecordingChannels"` 58 | ErrorCode string `json:"ErrorCode,omitempty" form:"ErrorCode" query:"ErrorCode"` 59 | RecordCallSid string `json:"CallSid,omitempty" form:"CallSid" query:"CallSid"` 60 | RecordingStartTime string `json:"RecordingStartTime,omitempty" form:"RecordingStartTime" query:"RecordingStartTime"` 61 | RecordAccountSid string `json:"AccountSid,omitempty" form:"AccountSid" query:"AccountSid"` 62 | RecordingDuration string `json:"RecordingDuration,omitempty" form:"RecordingDuration" query:"RecordingDuration" freeswitch:"Variable_record_seconds"` 63 | } 64 | 65 | DialActionUrlCallback struct { 66 | } 67 | ) 68 | -------------------------------------------------------------------------------- /models/request.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | /* 4 | authid -> call_uuid 5 | call_uuid -> call_request 6 | get auth id from the response and from that response. 7 | */ 8 | 9 | type CallRequest struct { 10 | AsyncAmdStatusCallbackMethod string `json:"AsyncAmdStatusCallbackMethod"` 11 | AsyncAmdStatusCallback string `json:"AsyncAmdStatusCallback"` 12 | AsyncAmd string `json:"AsyncAmd"` 13 | MachineDetectionSilenceTimeout string `json:"MachineDetectionSilenceTimeout"` 14 | MachineDetectionSpeechEndThreshold string `json:"MachineDetectionSpeechEndThreshold"` 15 | MachineDetectionSpeechThreshold string `json:"MachineDetectionSpeechThreshold"` 16 | MachineDetectionTimeout string `json:"MachineDetectionTimeout"` 17 | MachineDetection string `json:"MachineDetection"` 18 | SipAuthPassword string `json:"SipAuthPassword"` 19 | SipAuthUsername string `json:"SipAuthUsername"` 20 | LoopPlay string `json:"loop_play" example:"3"` 21 | Timeout string `json:"Timeout"` 22 | From string `json:"From" example:"15677654321"` 23 | To string `json:"To" example:"15677654321"` 24 | CallerName string `json:"caller_name" example:"Tiniyo"` 25 | CallerId string `json:"CallerId"` 26 | Byoc string `json:"Byoc"` 27 | CallReason string `json:"CallReason"` 28 | Trim string `json:"Trim"` 29 | RecordingStatusCallbackEvent string `json:"RecordingStatusCallbackEvent"` 30 | RecordingTrack string `json:"RecordingTrack"` 31 | RecordingChannels string `json:"RecordingChannels"` 32 | ParentCallSid string `json:"parent_call_sid"` 33 | AccountSid string `json:"AccountSid"` 34 | Record string `json:"Record"` 35 | SendDigits string `json:"SendDigits"` 36 | Play string `json:"play" example:"https://tiniyo.s3.amazonaws.com/MissionImpossible.mp3"` 37 | Speak string `json:"speak" example:"Hello Dear, Thanks for using our service"` 38 | ApplicationSid string `json:"ApplicationSid" example:"your tiniyo application id"` 39 | TinyML string `json:"TinyML" example:"Hello World"` 40 | Url string `json:"Url" example:"https://raw.githubusercontent.com/tiniyo/public/master/answer.xml"` 41 | Method string `json:"Method" example:"GET"` 42 | FallbackMethod string `json:"FallbackMethod"` 43 | FallbackUrl string `json:"FallbackUrl"` 44 | StatusCallback string `json:"StatusCallback"` 45 | StatusCallbackMethod string `json:"StatusCallbackMethod"` 46 | StatusCallbackEvent string `json:"StatusCallbackEvent"` 47 | RecordingStatusCallback string `json:"RecordingStatusCallback"` 48 | RecordingStatusCallbackMethod string `json:"RecordingStatusCallbackMethod"` 49 | Rate float64 `json:"rate"` 50 | Pulse int64 `json:"pulse"` 51 | MaxDuration int64 `json:"max_duration"` 52 | DestType string `json:"DestType"` 53 | SrcType string `json:"SrcType"` 54 | VendorAuthID string `json:"ParentAuthId"` 55 | SipPilotNumber string `json:"SipPilotNumber"` 56 | Sid string `json:"Sid"` 57 | Bridge string `json:"Bridge"` 58 | Host string `json:"Host"` 59 | SrcDirection string `json:"SrcDirection"` 60 | IsCallerId string `json:"IsCallerId"` 61 | SipTrunk string `json:"SipTrunk"` 62 | FromRemovePrefix string `json:"FromRemovePrefix"` 63 | DialAttr 64 | DialRecordAttr 65 | Callback 66 | RecordCallback 67 | CallResponse 68 | DialSipAttr 69 | DialNumberAttr 70 | GatherAttr 71 | DialConferenceAttr 72 | } 73 | 74 | type CallUpdateRequest struct { 75 | Sid string `json:"Sid,omitempty"` 76 | Url string `json:"Url"` 77 | Method string `json:"Method" example:"GET"` 78 | FallbackMethod string `json:"FallbackMethod"` // `json:"FallbackMethod"` 79 | FallbackUrl string `json:"FallbackUrl"` // `json:"FallbackUrl"` 80 | StatusCallback string `json:"StatusCallback"` // `json:"StatusCallback"` 81 | StatusCallbackMethod string `json:"StatusCallbackMethod"` // `json:"StatusCallbackMethod"` 82 | Status string `json:"Status"` // `json:"StatusCallback"` 83 | } 84 | type CallPlayRequest struct { 85 | Urls string `json:"urls"` 86 | Length int `json:"length"` 87 | Legs string `json:"legs"` 88 | Loop int `json:"loop"` 89 | Mix bool `json:"mix"` 90 | } 91 | 92 | type CallSpeakRequest struct { 93 | Text string `json:"text"` 94 | Voice string `json:"voice"` 95 | Language string `json:"language"` 96 | Legs string `json:"legs"` 97 | Loop bool `json:"loop"` 98 | Mix bool `json:"mix"` 99 | } 100 | 101 | type CallEventRequest struct { 102 | SendDigits string `json:"digits"` 103 | DigitsReceived string `json:"digitsReceived"` 104 | Leg string `json:"leg"` 105 | } 106 | 107 | type Call struct { 108 | uuid string 109 | cr CallRequest 110 | } 111 | -------------------------------------------------------------------------------- /managers/tinixml/record.go: -------------------------------------------------------------------------------- 1 | package tinixml 2 | 3 | import ( 4 | "fmt" 5 | "github.com/beevik/etree" 6 | uuid4 "github.com/satori/go.uuid" 7 | "os" 8 | "github.com/tiniyo/neoms/adapters" 9 | "github.com/tiniyo/neoms/logger" 10 | "github.com/tiniyo/neoms/models" 11 | "strconv" 12 | ) 13 | 14 | func ProcessRecord(msAdapter *adapters.MediaServer, data *models.CallRequest, child *etree.Element) error { 15 | if data.Status != "in-progress" { 16 | (*msAdapter).AnswerCall(data.CallSid) 17 | } 18 | var err error 19 | handleRecordAttribute(data, *child) 20 | recordingSid := uuid4.NewV4().String() 21 | recordMultiSet := fmt.Sprintf("^^:recording_sid=%s", recordingSid) 22 | if data.RecordFinishOnKey != "" { 23 | recordMultiSet = fmt.Sprintf(":playback_terminators=%s", data.RecordFinishOnKey) 24 | } 25 | if err = (*msAdapter).MultiSet(data.Sid, recordMultiSet); err != nil { 26 | return err 27 | } 28 | if data.RecordPlayBeep == "true" { 29 | if err = (*msAdapter).PlayBeep(data.Sid); err != nil { 30 | return err 31 | } 32 | } 33 | //now next is record_session with file name with record_stereo 34 | recordDir := "/call_recordings" 35 | recordFile := fmt.Sprintf("%s/%s-%s.mp3", recordDir, data.AccountSid, data.CallSid) 36 | if err = (*msAdapter).Record(data.Sid, recordFile, data.RecordMaxLength, data.RecordTimeout); err != nil { 37 | return err 38 | } 39 | return err 40 | } 41 | 42 | func handleRecordAttribute(data *models.CallRequest, child etree.Element) { 43 | callSid := data.Sid 44 | data.RecordingSource = "RecordVerb" 45 | data.RecordPlayBeep = "true" 46 | data.RecordMaxLength = "3600" 47 | data.RecordTimeout = "5" 48 | data.RecordFinishOnKey = "1234567890*#" 49 | logger.UuidLog("Info", callSid, fmt.Sprint("dial elements are", child)) 50 | //recording attribute of file 51 | data.RecordMethod = "POST" 52 | for _, attr := range child.Attr { 53 | switch attr.Key { 54 | case "action": 55 | data.RecordAction = attr.Value 56 | case "method": 57 | data.RecordMethod = attr.Value 58 | case "timeout": 59 | if _, err := strconv.Atoi(attr.Value); err == nil { 60 | data.RecordTimeout = attr.Value 61 | } 62 | case "finishOnKey": 63 | data.RecordFinishOnKey = attr.Value 64 | case "maxLength": 65 | maxLen, err := strconv.Atoi(attr.Value) 66 | if err == nil && maxLen < 3600 { 67 | data.RecordMaxLength = attr.Value 68 | } 69 | case "playBeep": 70 | data.RecordPlayBeep = attr.Value 71 | case "trim": 72 | data.Trim = attr.Value 73 | case "recordingStatusCallback": 74 | data.RecordingStatusCallback = attr.Value 75 | case "recordingStatusCallbackMethod": 76 | data.RecordingStatusCallbackMethod = attr.Value 77 | case "recordingStatusCallbackEvent": 78 | data.RecordingStatusCallbackEvent = attr.Value 79 | case "storageUrl": 80 | data.RecordStorageUrl = attr.Value 81 | case "storageUrlMethod": 82 | data.RecordStorageUrlMethod = attr.Value 83 | case "transcribe": 84 | data.RecordTranscribe = attr.Value 85 | case "transcribeCallback": 86 | data.RecordTranscribeCallback = attr.Value 87 | default: 88 | logger.UuidLog("Err", callSid, fmt.Sprint("Attribute not supported - ", attr.Key)) 89 | } 90 | } 91 | logger.UuidLog("Info", callSid, fmt.Sprint("dial variables are - ", data)) 92 | } 93 | 94 | //We need to return variable string set to be send together with originate 95 | func ProcessRecordDialAttribute(data models.CallRequest) string { 96 | recordString := "" 97 | recordDir := "/call_recordings" 98 | 99 | //ensureRecordDir(recordDir) 100 | recordFile := fmt.Sprintf("%s/%s-%s.mp3", recordDir, data.AccountSid, data.Sid) 101 | switch data.Record { 102 | case "true", "record-from-answer": 103 | recordString = fmt.Sprintf("media_bug_answer_req=true,"+ 104 | "api_on_answer_1='sched_api +1 none uuid_record %s start %s'", data.Sid,recordFile) 105 | if data.RecordingTrack == "inbound" { 106 | recordString = fmt.Sprintf("RECORD_READ_ONLY=true,media_bug_answer_req=true,"+ 107 | "api_on_answer_1='sched_api +1 none uuid_record %s start %s'", data.Sid,recordFile) 108 | } else if data.RecordingTrack == "outbound" { 109 | recordString = fmt.Sprintf("RECORD_WRITE_ONLY=true,media_bug_answer_req=true,"+ 110 | "api_on_answer_1='sched_api +1 none uuid_record %s start %s'", data.Sid,recordFile) 111 | } 112 | case "record-from-ringing": 113 | recordString = fmt.Sprintf("api_on_media_1='sched_api +1 none uuid_record %s start %s'", data.Sid,recordFile) 114 | if data.RecordingTrack == "inbound" { 115 | recordString = fmt.Sprintf("RECORD_READ_ONLY=true,api_on_media_1='sched_api +1 none uuid_record %s start %s'", 116 | data.Sid,recordFile) 117 | } else if data.RecordingTrack == "outbound" { 118 | recordString = fmt.Sprintf("RECORD_WRITE_ONLY=true,api_on_media_1='sched_api +1 none uuid_record %s start %s'", 119 | data.Sid,recordFile) 120 | } 121 | case "record-from-answer-dual": 122 | recordString = fmt.Sprintf("media_bug_answer_req=true,RECORD_STEREO=true,"+ 123 | "api_on_answer_1='sched_api +1 none uuid_record %s start %s'", data.Sid,recordFile) 124 | if data.RecordingTrack == "inbound" { 125 | recordString = fmt.Sprintf("RECORD_READ_ONLY=true,"+ 126 | "api_on_answer_1='sched_api +1 none uuid_record %s start %s'", data.Sid,recordFile) 127 | } else if data.RecordingTrack == "outbound" { 128 | recordString = fmt.Sprintf("RECORD_WRITE_ONLY=true,"+ 129 | "api_on_answer_1='sched_api +1 none uuid_record %s start %s'", data.Sid,recordFile) 130 | } 131 | case "record-from-ringing-dual": 132 | recordString = fmt.Sprintf("RECORD_STEREO=true,api_on_media_1='sched_api +1 none uuid_record %s start %s'", 133 | data.Sid,recordFile) 134 | if data.RecordingTrack == "inbound" { 135 | recordString = fmt.Sprintf("RECORD_READ_ONLY=true,api_on_media_1='sched_api +1 none uuid_record %s start %s'", 136 | data.Sid,recordFile) 137 | } else if data.RecordingTrack == "outbound" { 138 | recordString = fmt.Sprintf("RECORD_WRITE_ONLY=true,api_on_media_1='sched_api +1 none uuid_record %s start %s'", 139 | data.Sid,recordFile) 140 | } 141 | default: 142 | } 143 | 144 | return recordString 145 | } 146 | 147 | func ensureRecordDir(dirName string) error { 148 | err := os.Mkdir(dirName, 0755) 149 | if err == nil || os.IsExist(err) { 150 | return nil 151 | } else { 152 | return err 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /managers/appmanager.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "github.com/tiniyo/neoms/constant" 7 | 8 | "github.com/tiniyo/neoms/config" 9 | "github.com/tiniyo/neoms/helper" 10 | "github.com/tiniyo/neoms/logger" 11 | "github.com/tiniyo/neoms/models" 12 | ) 13 | 14 | type VoiceAppManagerInterface interface { 15 | getXMLApplication(evHeaderMap map[string]string) *models.CallRequest 16 | } 17 | 18 | type VoiceAppManager struct { 19 | 20 | } 21 | 22 | func NewVoiceAppManager() VoiceAppManagerInterface { 23 | return VoiceAppManager{ 24 | } 25 | } 26 | 27 | func (vAppMgr VoiceAppManager) getXMLApplication(evHeaderMap map[string]string) *models.CallRequest { 28 | 29 | var data models.NumberAPIResponse 30 | phoneNumber := evHeaderMap["Variable_sip_req_user"] 31 | toPhoneNumber := evHeaderMap["Variable_sip_to_user"] 32 | if phoneNumber == "" { 33 | phoneNumber = toPhoneNumber 34 | } 35 | callType := evHeaderMap["Variable_call_type"] 36 | callSid := evHeaderMap["Variable_call_sid"] 37 | callerId := evHeaderMap["Variable_sip_from_user"] 38 | fromUser := evHeaderMap["Variable_sip_from_user"] 39 | sipUser := evHeaderMap["Variable_sip_user"] 40 | parentCallSid := callSid 41 | url := "" 42 | 43 | if callType == "number" { 44 | logger.UuidInboundLog("Info", callSid, fmt.Sprint("sip call received on did number")) 45 | url = fmt.Sprintf("%s/%s", config.Config.Numbers.BaseUrl, numberSanity(phoneNumber)) 46 | } else if callType == "number_tata" { 47 | logger.UuidInboundLog("Info", callSid, fmt.Sprint("sip call received on did number")) 48 | url = fmt.Sprintf("%s/%s", config.Config.Numbers.BaseUrl, numberSanity(toPhoneNumber)) 49 | callType = "number" 50 | } else { 51 | logger.UuidInboundLog("Info", callSid, fmt.Sprint("sip call received from sip user :", callType, " ", sipUser)) 52 | url = fmt.Sprintf("%s/Endpoints/%s", config.Config.SipEndpoint.BaseUrl, sipUser) 53 | fromUser = sipUser 54 | //callType = "sip" 55 | } 56 | 57 | if callType == "Wss" || callType == "wss" || callType == "Ws" { 58 | callType = "wss" 59 | } else { 60 | callType = "sip" 61 | } 62 | 63 | logger.UuidInboundLog("Info", callSid, fmt.Sprint("get application url - ", url)) 64 | 65 | statusCode, respBody, err := helper.Get(callSid, nil, url) 66 | if err != nil || statusCode != 200 { 67 | logger.UuidInboundLog("Err", callSid, fmt.Sprintf("url for response status code is %d - %#v", 68 | statusCode, err)) 69 | return nil 70 | } 71 | logger.UuidInboundLog("Info", callSid, fmt.Sprintf("get application response - %s", string(respBody))) 72 | err = json.Unmarshal(respBody, &data) 73 | if err != nil { 74 | logger.UuidInboundLog("Err", callSid, fmt.Sprintf("unmarshal application json failed, rejecting the calls %#v", err)) 75 | return nil 76 | } 77 | 78 | if data.AppResponse.AuthID == "" { 79 | logger.UuidInboundLog("Err", callSid, fmt.Sprintf("Phone-number %s is not attach "+ 80 | "with any account, rejecting the calls ", phoneNumber)) 81 | return nil 82 | } 83 | 84 | if data.AppResponse.Application == (models.Application{}) { 85 | logger.UuidInboundLog("Err", callSid, fmt.Sprintf("Application is not attach with sip user or phone numebr")) 86 | return nil 87 | } 88 | 89 | if data.AppResponse.Application.InboundURL == "" { 90 | logger.UuidInboundLog("Err", callSid, fmt.Sprintf("Application is not attach with sip user or phone numebr")) 91 | return nil 92 | } 93 | 94 | pulse := float64(data.AppResponse.InitPulse) 95 | rate := pulse * data.AppResponse.Rate 96 | VendorAuthID := data.AppResponse.VendorAuthID 97 | if VendorAuthID == "" || len(VendorAuthID) < 12 { 98 | VendorAuthID = "TINIYO1SECRET1AUTHID" 99 | } 100 | cr := models.CallRequest{} 101 | if data.AppResponse.Application.Name == "SIP_TRUNK" { 102 | logger.UuidInboundLog("Err", callSid, fmt.Sprintf("Application is SIP_TRUNK, We are not going to check for callerid")) 103 | cr.IsCallerId = "false" 104 | cr.SipTrunk = "true" 105 | } 106 | cr.CallSid = callSid 107 | cr.Sid = callSid 108 | cr.From = fromUser 109 | cr.ParentCallSid = parentCallSid 110 | cr.To = phoneNumber 111 | cr.CallResponse.Direction = "inbound" 112 | cr.Callback.Direction = "inbound" 113 | cr.Rate = rate 114 | cr.SrcDirection = "inbound" 115 | cr.SrcType = callType 116 | cr.CallerId = callerId 117 | cr.Caller = callerId 118 | cr.VendorAuthID = VendorAuthID 119 | cr.AccountSid = data.AppResponse.AuthID 120 | cr.Pulse = data.AppResponse.InitPulse 121 | cr.Url = data.AppResponse.Application.InboundURL 122 | cr.Method = data.AppResponse.Application.InboundMethod 123 | cr.StatusCallback = data.AppResponse.Application.StatusCallback 124 | cr.StatusCallbackMethod = data.AppResponse.Application.StatusCallbackMethod 125 | cr.StatusCallbackEvent = data.AppResponse.Application.StatusCallbackEvent 126 | cr.FallbackUrl = data.AppResponse.Application.FallbackUrl 127 | cr.FallbackMethod = data.AppResponse.Application.FallbackMethod 128 | cr.Host = data.AppResponse.Host 129 | if data.AppResponse.Host == "" { 130 | cr.Host = "tiniyo.com" 131 | } 132 | 133 | if callType == "number" { 134 | exportVars := constant.GetConstant("NumberExportFsVars").(string) 135 | authIdSet := fmt.Sprintf("^^:tiniyo_accid=%s:tiniyo_rate=%f:"+ 136 | "tiniyo_pulse=%d:"+ 137 | "call_sid=%s:"+ 138 | "call_type=Number:"+ 139 | "parent_call_sid=%s:"+ 140 | "parent_call_uuid=%s:"+ 141 | "tiniyo_did_number=%s:"+ 142 | "tiniyo_host=%s:"+ 143 | "export_vars=%s", cr.AccountSid, 144 | cr.Rate, cr.Pulse, cr.Sid, cr.ParentCallSid, 145 | cr.ParentCallSid, numberSanity(phoneNumber), data.AppResponse.Host, exportVars) 146 | _ = MsAdapter.MultiSet(cr.CallSid, authIdSet) 147 | } else { 148 | exportVars := constant.GetConstant("ExportFsVars").(string) 149 | authIdSet := fmt.Sprintf("^^:tiniyo_accid=%s:tiniyo_rate=%f:"+ 150 | "tiniyo_pulse=%d:"+ 151 | "call_sid=%s:"+ 152 | "call_type=Sip:"+ 153 | "parent_call_uuid=%s:"+ 154 | "parent_call_sid=%s:"+ 155 | "tiniyo_host=%s:"+ 156 | "export_vars=%s", cr.AccountSid, 157 | cr.Rate, cr.Pulse, cr.Sid, cr.ParentCallSid, cr.ParentCallSid, data.AppResponse.Host, exportVars) 158 | _ = MsAdapter.MultiSet(cr.CallSid, authIdSet) 159 | } 160 | return &cr 161 | } 162 | 163 | func numberSanity(number string) string { 164 | reg, err := regexp.Compile("[^0-9]+") 165 | if err != nil { 166 | return number 167 | } 168 | return reg.ReplaceAllString(number, "") 169 | } 170 | -------------------------------------------------------------------------------- /helper/http.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | "time" 10 | 11 | "github.com/go-resty/resty/v2" 12 | "github.com/tiniyo/neoms/logger" 13 | ) 14 | 15 | /* 16 | var hbConnections []*resty.Client 17 | var rrConnections []*resty.Client 18 | var sipConnections []*resty.Client 19 | var numberConnections []*resty.Client 20 | var genConnections []*resty.Client 21 | 22 | var hbindex = 0 23 | var rrindex = 0 24 | var sipindex = 0 25 | var numberindex = 0 26 | var genindex = 0 27 | 28 | var maxsize = 100 29 | var mutext sync.Mutex 30 | 31 | func InitHttpConnPool() { 32 | CreateHttpClientPool("heartbeat") 33 | CreateHttpClientPool("rateroute") 34 | CreateHttpClientPool("sip") 35 | CreateHttpClientPool("number") 36 | CreateHttpClientPool("gen") 37 | } 38 | 39 | func CreateHttpClientPool(clientType string) { 40 | mutext.Lock() 41 | for i := 0; i < maxsize; i++ { 42 | 43 | switch clientType { 44 | case "heartbeat": 45 | connection := resty.New() 46 | connection.SetBasicAuth("FAIVAYIEPHAHGAIKOLOH", "KO66!RWHFh>9J!~;oFCV[lPN0") 47 | hbConnections = append(hbConnections, connection) 48 | case "rateroute": 49 | connection := resty.New() 50 | connection.SetBasicAuth("FAIVAYIEPHAHGAIKOLOH", "KO66!RWHFh>9J!~;oFCV[lPN0") 51 | rrConnections = append(rrConnections, connection) 52 | case "sip": 53 | connection := resty.New() 54 | connection.SetBasicAuth("FAIVAYIEPHAHGAIKOLOH", "KO66!RWHFh>9J!~;oFCV[lPN0") 55 | sipConnections = append(sipConnections, connection) 56 | case "number": 57 | connection := resty.New() 58 | connection.SetBasicAuth("FAIVAYIEPHAHGAIKOLOH", "KO66!RWHFh>9J!~;oFCV[lPN0") 59 | numberConnections = append(numberConnections, connection) 60 | default: 61 | connection := resty.New() 62 | genConnections = append(genConnections, connection) 63 | } 64 | } 65 | mutext.Unlock() 66 | } 67 | */ 68 | func createTransport(localAddr net.Addr) *http.Transport { 69 | dialer := &net.Dialer{ 70 | Timeout: 30 * time.Second, 71 | KeepAlive: 30 * time.Second, 72 | DualStack: true, 73 | } 74 | if localAddr != nil { 75 | dialer.LocalAddr = localAddr 76 | } 77 | return &http.Transport{ 78 | Proxy: http.ProxyFromEnvironment, 79 | DialContext: dialer.DialContext, 80 | ForceAttemptHTTP2: true, 81 | MaxIdleConns: 1000, 82 | IdleConnTimeout: 90 * time.Second, 83 | TLSHandshakeTimeout: 10 * time.Second, 84 | ExpectContinueTimeout: 1 * time.Second, 85 | MaxIdleConnsPerHost: 1000, 86 | MaxConnsPerHost: 1000, 87 | } 88 | } 89 | 90 | var reusableConnection *resty.Client 91 | 92 | func getConnection2() *resty.Client { 93 | if reusableConnection != nil { 94 | return reusableConnection 95 | } 96 | reusableConnection := resty.New() 97 | transport := createTransport(nil) 98 | reusableConnection.SetTransport(transport) 99 | return reusableConnection 100 | } 101 | 102 | /* 103 | func getConnection(clientType string) *resty.Client { 104 | var conn *resty.Client 105 | mutext.Lock() 106 | switch clientType { 107 | case "heartbeat": 108 | hbindex = hbindex % maxsize 109 | conn = hbConnections[hbindex] 110 | hbindex++ 111 | case "rateroute": 112 | rrindex = rrindex % maxsize 113 | conn = rrConnections[rrindex] 114 | rrindex++ 115 | case "sip": 116 | sipindex = rrindex % maxsize 117 | conn = sipConnections[sipindex] 118 | sipindex++ 119 | case "number": 120 | numberindex = rrindex % maxsize 121 | conn = numberConnections[numberindex] 122 | numberindex++ 123 | default: 124 | genindex = rrindex % maxsize 125 | conn = genConnections[genindex] 126 | genindex++ 127 | } 128 | mutext.Unlock() 129 | return conn 130 | } 131 | */ 132 | 133 | func Get(callSid string, restData map[string]interface{}, urls string) (int, []byte, error) { 134 | var clientType = getClientType(urls) 135 | var restClient = getConnection2() 136 | if clientType == "heartbeat" || clientType == "rateroute" || clientType == "sip" || clientType == "number" { 137 | restClient.SetBasicAuth("FAIVAYIEPHAHGAIKOLOH", "KO66!RWHFh>9J!~;oFCV[lPN0") 138 | } else { 139 | u, err := url.Parse(urls) 140 | if err != nil { 141 | logger.UuidLog("Err", callSid, err.Error()) 142 | return 400, nil, err 143 | } 144 | if u.User != nil { 145 | if pwd, ok := u.User.Password(); ok { 146 | restClient.SetBasicAuth(u.User.Username(), pwd) 147 | baseUrlStr := fmt.Sprint(u.Scheme, "://", u.Host, u.Path) 148 | base, err := url.Parse(baseUrlStr) 149 | if err != nil { 150 | return 400, nil, err 151 | } 152 | urls = fmt.Sprint(base.ResolveReference(u)) 153 | logger.UuidLog("Info", callSid, urls) 154 | } 155 | } 156 | } 157 | if restData == nil { 158 | resp, err := restClient.R(). 159 | EnableTrace(). 160 | SetHeader("Accept", "application/json"). 161 | Get(urls) 162 | logger.UuidHttpLog(callSid, resp) 163 | if resp == nil { 164 | return 400, nil, err 165 | } 166 | return resp.StatusCode(), resp.Body(), err 167 | } 168 | reqData := make(map[string]string) 169 | for key, value := range restData { 170 | strKey := fmt.Sprintf("%v", key) 171 | strValue := fmt.Sprintf("%v", value) 172 | reqData[strKey] = strValue 173 | } 174 | resp, err := restClient.R(). 175 | SetQueryParams(reqData). 176 | EnableTrace(). 177 | SetHeader("Accept", "application/json"). 178 | Get(urls) 179 | logger.UuidHttpLog(callSid, resp) 180 | 181 | // Close the connection to reuse it 182 | if resp == nil { 183 | return 400, nil, err 184 | } 185 | return resp.StatusCode(), resp.Body(), err 186 | } 187 | 188 | func Post(callSid string, restData map[string]interface{}, urls string) (int, []byte, error) { 189 | var clientType = getClientType(urls) 190 | var restClient = getConnection2() 191 | if clientType == "heartbeat" || clientType == "recording" || clientType == "rateroute" || clientType == "sip" || clientType == "number" { 192 | restClient.SetBasicAuth("FAIVAYIEPHAHGAIKOLOH", "KO66!RWHFh>9J!~;oFCV[lPN0") 193 | } else { 194 | u, err := url.Parse(urls) 195 | if err != nil { 196 | logger.UuidLog("Err", callSid, err.Error()) 197 | return 400, nil, err 198 | } 199 | if u.User != nil { 200 | logger.UuidLog("Info", callSid, "found username and password in url ") 201 | if pwd, ok := u.User.Password(); ok { 202 | restClient.SetBasicAuth(u.User.Username(), pwd) 203 | baseUrlStr := fmt.Sprint(u.Scheme, "://", u.Host, u.Path) 204 | base, err := url.Parse(baseUrlStr) 205 | if err != nil { 206 | return 400, nil, err 207 | } 208 | urls = fmt.Sprint(base.ResolveReference(u)) 209 | logger.UuidLog("Info", callSid, urls) 210 | } 211 | } 212 | } 213 | if restData == nil { 214 | resp, err := restClient.R(). 215 | EnableTrace(). 216 | SetHeader("Accept", "application/json"). 217 | Post(urls) 218 | logger.UuidHttpLog(callSid, resp) 219 | if resp == nil { 220 | return 400, nil, err 221 | } 222 | return resp.StatusCode(), resp.Body(), err 223 | } 224 | resp, err := restClient.R(). 225 | EnableTrace(). 226 | SetBody(restData). 227 | SetHeader("Accept", "application/json"). 228 | Post(urls) 229 | logger.UuidHttpLog(callSid, resp) 230 | if resp == nil { 231 | return 400, nil, err 232 | } 233 | return resp.StatusCode(), resp.Body(), err 234 | } 235 | 236 | func getClientType(url string) string { 237 | var clientType = "" 238 | if strings.Contains(url, "HeartBeat") { 239 | clientType = "heartbeat" 240 | } else if strings.Contains(url, "RateRoutes") { 241 | clientType = "rateroute" 242 | } else if strings.Contains(url, "PhoneNumbers") { 243 | clientType = "number" 244 | } else if strings.Contains(url, "Sips") { 245 | clientType = "sip" 246 | } else if strings.Contains(url, "Recordings") { 247 | clientType = "recording" 248 | } else { 249 | clientType = "generic" 250 | } 251 | return clientType 252 | } 253 | -------------------------------------------------------------------------------- /freeswitch/dialplan/tiniyo_inbound.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /managers/callbackmanager.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "fmt" 5 | jsoniter "github.com/json-iterator/go" 6 | "github.com/tiniyo/neoms/adapters" 7 | "github.com/tiniyo/neoms/adapters/callstate" 8 | "github.com/tiniyo/neoms/logger" 9 | ) 10 | 11 | type CallBackManager struct { 12 | callState adapters.CallStateAdapter 13 | voiceAppMgr VoiceAppManagerInterface 14 | heartBeatMgr HeartBeatManagerInterface 15 | webhookMgr WebHookManagerInterface 16 | } 17 | 18 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 19 | 20 | func (msCB *CallBackManager) InitCallBackManager() { 21 | msCB.callState, _ = callstate.NewCallStateAdapter() 22 | msCB.voiceAppMgr = NewVoiceAppManager() 23 | msCB.heartBeatMgr = NewHeartBeatManager(msCB.callState) 24 | xmlMgr := NewXmlManager(msCB.callState) 25 | msCB.webhookMgr = NewWebHookManager(msCB.callState, msCB.heartBeatMgr, xmlMgr) 26 | } 27 | 28 | func (msCB CallBackManager) CallBackMediaServerStatus(status int) error { 29 | if status > 0 { 30 | logger.Logger.Info(" MediaServer is Running ") 31 | } else { 32 | logger.Logger.Error(" MediaServer is Disconnected ") 33 | } 34 | return nil 35 | } 36 | 37 | func (msCB CallBackManager) CallBackOriginate(callSid string, evHeader []byte) error { 38 | logger.UuidLog("Info", callSid, "call initiated callback") 39 | err := msCB.webhookMgr.triggerCallBack("initiated", callSid, evHeader) 40 | if err != nil { 41 | logger.UuidLog("Err", callSid, fmt.Sprint("call initiated callback failed - ", err)) 42 | } 43 | return err 44 | } 45 | 46 | func (msCB CallBackManager) CallBackProgressMedia(callSid string, evHeader []byte) error { 47 | logger.UuidLog("Info", callSid, "call ringing callback") 48 | err := msCB.webhookMgr.triggerCallBack("ringing", callSid, evHeader) 49 | if err != nil { 50 | logger.UuidLog("Err", callSid, fmt.Sprint("call ringing callback failed - ", err)) 51 | } 52 | return err 53 | } 54 | 55 | func (msCB CallBackManager) CallBackHangup(uuid string) error { 56 | //logger.Logger.WithField("uuid", uuid).Info("callback hangup received") 57 | return nil 58 | } 59 | 60 | func (msCB CallBackManager) CallBackHangupComplete(callSid string, evHeader []byte) error { 61 | logger.UuidLog("Info", callSid, "callback hangup complete") 62 | err := msCB.webhookMgr.triggerCallBack("completed", callSid, evHeader) 63 | if err != nil { 64 | logger.UuidLog("Err", callSid, fmt.Sprint("call hangup complete callback failed - ", err)) 65 | } 66 | return err 67 | } 68 | 69 | func (msCB CallBackManager) CallBackAnswered(callSid string, evHeader []byte) error { 70 | logger.UuidLog("Info", callSid, "call answer callback") 71 | err := msCB.webhookMgr.triggerCallBack("answered", callSid, evHeader) 72 | if err != nil { 73 | logger.UuidLog("Err", callSid, fmt.Sprint("call answer callback failed - ", err)) 74 | } 75 | return err 76 | } 77 | 78 | func (msCB CallBackManager) CallBackDTMFDetected(callSid string, evHeader []byte) error { 79 | dtmfMap := make(map[string]string) 80 | if err := json.Unmarshal(evHeader, &dtmfMap); err != nil { 81 | return err 82 | } 83 | callSid = dtmfMap["Unique-Id"] 84 | logger.UuidLog("Info", callSid, fmt.Sprint("dtmf detected - ", dtmfMap["Dtmf-Digit"])) 85 | err := msCB.webhookMgr.triggerDTMFCallBack(callSid, dtmfMap["Dtmf-Digit"]) 86 | if err != nil { 87 | logger.UuidLog("Err", callSid, fmt.Sprint("call dtmf callback failed - ", err)) 88 | } 89 | return err 90 | } 91 | 92 | func (msCB CallBackManager) CallBackRecordingStart(callSid string, evHeader []byte) error { 93 | logger.Logger.WithField("uuid", callSid).Info("callback start recording received") 94 | err := msCB.webhookMgr.triggerRecordingCallBack("in-progress", callSid, evHeader) 95 | if err != nil { 96 | logger.UuidLog("Err", callSid, fmt.Sprint("call record start callback failed - ", err)) 97 | } 98 | return err 99 | } 100 | 101 | func (msCB CallBackManager) CallBackRecordingStop(callSid string, evHeader []byte) error { 102 | logger.Logger.WithField("uuid", callSid).Info("callback stop recording received") 103 | go postRecordingData(callSid, evHeader) 104 | err := msCB.webhookMgr.triggerRecordingCallBack("completed", callSid, evHeader) 105 | if err != nil { 106 | logger.UuidLog("Err", callSid, fmt.Sprint("call record stop callback failed - ", err)) 107 | } 108 | return err 109 | } 110 | 111 | func (msCB CallBackManager) CallBackProgress(callSid string) error { 112 | // logger.Logger.WithField("uuid", callSid).Info("callback progress received") 113 | return nil 114 | } 115 | 116 | /* 117 | Getting Park Events - Its useful to get the parked inbound call control from mediaserver to webfs 118 | */ 119 | func (msCB CallBackManager) CallBackPark(callSid string, evHeader []byte) error { 120 | logger.UuidLog("Info", callSid, "channel park callback") 121 | evHeaderMap := make(map[string]string) 122 | 123 | if err := json.Unmarshal(evHeader, &evHeaderMap); err != nil { 124 | logger.UuidInboundLog("Err", callSid, fmt.Sprint("error while unmarshal - ", 125 | err, " sending UNALLOCATED_NUMBER")) 126 | return nil 127 | } 128 | 129 | destType := evHeaderMap["Variable_tiniyo_destination"] 130 | switch destType { 131 | case "InboundXMLApp": 132 | logger.UuidInboundLog("Info", callSid, "inbound call received") 133 | callRequest := msCB.voiceAppMgr.getXMLApplication(evHeaderMap) 134 | if callRequest == nil { 135 | logger.UuidInboundLog("Err", callSid, "not able to process the request, sending UNALLOCATED_NUMBER") 136 | return MsAdapter.CallHangupWithReason(callSid, "UNALLOCATED_NUMBER") 137 | } 138 | 139 | /* Store Call State */ 140 | jsonCallRequestByte, err := json.Marshal(callRequest) 141 | if err != nil { 142 | logger.UuidInboundLog("Err", callSid, "not able to process the request, sending UNALLOCATED_NUMBER") 143 | return MsAdapter.CallHangupWithReason(callSid, "UNALLOCATED_NUMBER") 144 | } 145 | if err = msCB.callState.Set(callRequest.Sid, jsonCallRequestByte); err != nil { 146 | logger.UuidInboundLog("Err", callSid, "not able to process the request, sending UNALLOCATED_NUMBER") 147 | return MsAdapter.CallHangupWithReason(callSid, "UNALLOCATED_NUMBER") 148 | } 149 | 150 | err = msCB.webhookMgr.triggerCallBack("initiated", callSid, evHeader) 151 | if err != nil { 152 | logger.UuidInboundLog("Err", callSid, fmt.Sprint("error while sending initiated callback - ", err)) 153 | } 154 | return err 155 | case "Conf": 156 | authId := evHeaderMap["Variable_sip_h_x-tiniyo-authid"] 157 | confId := evHeaderMap["Variable_sip_h_x-tiniyo-conf"] 158 | confName := fmt.Sprintf("%s-%s@tiniyo+flags{moderator}", authId, confId) 159 | confBridgeCmd := fmt.Sprintf("%s", confName) 160 | err := MsAdapter.ConfBridge(callSid, confBridgeCmd) 161 | if err != nil { 162 | return err 163 | } 164 | default: 165 | logger.UuidInboundLog("Info", callSid, "inbound call received") 166 | } 167 | return nil 168 | } 169 | 170 | func (msCB CallBackManager) CallBackDestroy(uuid string) error { 171 | //logger.Logger.Debug("CallDestroy Value : uuid ", uuid) 172 | return nil 173 | } 174 | 175 | func (msCB CallBackManager) CallBackExecuteComplete(uuid string) error { 176 | //logger.Logger.Debug("CallExecuteComplete Value : uuid ", uuid) 177 | return nil 178 | } 179 | 180 | func (msCB CallBackManager) CallBackBridged(callSid string) error { 181 | //logger.Logger.WithField("callSid", callSid).Debug(" CallBackBridged - ") 182 | return nil 183 | } 184 | 185 | func (msCB CallBackManager) CallBackUnBridged(callSid string) error { 186 | //logger.Logger.WithField("uuid", callSid).Debug(" CallBackUnBridged - ") 187 | return nil 188 | } 189 | 190 | func (msCB CallBackManager) CallBackSessionHeartBeat(parentSid, callSid string) error { 191 | logger.Logger.WithField("uuid", callSid). 192 | WithField("parent", parentSid).Debug(" CallBackSessionHeartBeat - ") 193 | var callIds map[string]int64 194 | var err error 195 | parentSidRelationKey := fmt.Sprintf("parent:%s", parentSid) 196 | if callIds, err = msCB.callState.GetMembersScore(parentSidRelationKey); err != nil { 197 | logger.UuidLog("Err", callSid, fmt.Sprintf("Trouble while getting up child "+ 198 | "and parent relationship in redis - %#v\n", err)) 199 | return err 200 | } 201 | for callId, Score := range callIds { 202 | logger.UuidLog("Err", callSid, fmt.Sprintf("sending heartbeat")) 203 | if _, err = msCB.callState.IncrKeyMemberScore(parentSidRelationKey, callId, 1); err!=nil{ 204 | //handle it later 205 | } 206 | err = msCB.heartBeatMgr.makeHeartBeatRequest(callId, Score+1) 207 | if err != nil { 208 | return err 209 | } 210 | } 211 | return nil 212 | } 213 | 214 | func (msCB CallBackManager) CallBackMessage(callSid string) error { 215 | //logger.Logger.WithField("uuid", callSid).Debug(" CallBackMessage - ") 216 | return nil 217 | } 218 | 219 | func (msCB CallBackManager) CallBackCustom(callSid string) error { 220 | //logger.Logger.WithField("uuid", callSid).Debug(" CallBackCustom - ") 221 | return nil 222 | } 223 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 9 | 10 | 11 | 12 | 13 | 20 | [![Contributors][contributors-shield]][contributors-url] 21 | [![Forks][forks-shield]][forks-url] 22 | [![Stargazers][stars-shield]][stars-url] 23 | [![Issues][issues-shield]][issues-url] 24 | [![MIT License][license-shield]][license-url] 25 | [![LinkedIn][linkedin-shield]][linkedin-url] 26 | 27 | 28 | 29 | 30 |
31 |
32 | 33 | Logo 34 | 35 | 36 |

NeoMs

37 | 38 |

39 | The open-source alternative to Twilio. 40 |
41 | Explore the docs » 42 |
43 |
44 | View Demo 45 | · 46 | Report Bug 47 | · 48 | Request Feature 49 |

50 |
51 | 52 | 53 | 54 | 55 |
56 | Table of Contents 57 |
    58 |
  1. 59 | About The Project 60 | 63 |
  2. 64 |
  3. 65 | Getting Started 66 | 70 |
  4. 71 |
  5. Usage
  6. 72 |
  7. Roadmap
  8. 73 |
  9. Contributing
  10. 74 |
  11. License
  12. 75 |
  13. Contact
  14. 76 |
  15. Acknowledgments
  16. 77 |
78 |
79 | 80 | 81 | 82 | 83 | ## About The Project 84 | 85 | [![Product Name Screen Shot][product-screenshot]](https://tiniyo.com) 86 | 87 | Project NeoMs is open-source alternative to twilio voice api. It helps software developer or enterprises to build CPaaS like twilio with their infrastructure. 88 | 89 |

(back to top)

90 | 91 | 92 | 93 | ### Built With 94 | 95 | * [FreeSWITCH](https://github.com/signalwire/freeswitch) 96 | * [GoLang](https://golang.org/) 97 | * [Redis](https://redis.io/) 98 | 99 |

(back to top)

100 | 101 | 102 | 103 | ## Getting Started 104 | 105 | ### Prerequisites 106 | 107 | NeoMs required local redis on each server where you setup NeoMs. Redis is used for live call stats. 108 | 109 | * Redis 110 | ```sh 111 | sudo apt install redis-server 112 | ``` 113 | 114 | * FreeSWITCH (You may install your desired version) 115 | ```sh 116 | sudo apt-get update && apt-get install -y gnupg2 wget lsb-release 117 | wget -O - https://files.freeswitch.org/repo/deb/debian-release/fsstretch-archive-keyring.asc | apt-key add - 118 | echo "deb http://files.freeswitch.org/repo/deb/debian-release/ `lsb_release -sc` main" > /etc/apt/sources.list.d/freeswitch.list 119 | echo "deb-src http://files.freeswitch.org/repo/deb/debian-release/ `lsb_release -sc` main" >> /etc/apt/sources.list.d/freeswitch.list 120 | # you may want to populate /etc/freeswitch at this point. 121 | # if /etc/freeswitch does not exist, the standard vanilla configuration is deployed 122 | # apt-get update && apt-get install -y freeswitch-meta-all 123 | ``` 124 | 125 | ### Installation 126 | 127 | 1. Clone the repo 128 | ```sh 129 | git clone https://github.com/tiniyo/neoms.git 130 | ``` 131 | 2. Install the neoms package dependency using go mod. 132 | ```sh 133 | cd neoms 134 | go mod download 135 | ``` 136 | 3. Please enter your api configurations using environment variable. 137 | ```sh 138 | EXPORT REGION=ALL 139 | EXPORT SER_USER=API_BASIC_AUTH_USER 140 | EXPORT SER_SECRET=API_BASIC_AUTH_SECRET 141 | EXPORT SIP_SERVICE=SIP_SERVICE_URL 142 | EXPORT KAMGO_SERVICE=LOCATION_SERVICE_URL 143 | EXPORT NUMBER_SERVICE=NUMBER_SERVICE_URL 144 | EXPORT HEARTBEAT_SERVICE=HEARTBEAT_SERVICE_URL 145 | EXPORT RATING_ROUTING_SERVICE=RATING_ROUTING_SERVICE_URL 146 | EXPORT CDR_SERVICE=CDR_SERVICE_URL 147 | EXPORT RECORDING_SERVICE=RECORDING_SERVICE_URL 148 | ``` 149 | 4. build the neoms and run the n 150 | ```sh 151 | go build -o neoms main.go 152 | ./neoms 153 | ``` 154 | 155 |

(back to top)

156 | 157 | 158 | 159 | 160 | ## Usage 161 | 162 | _For more examples, please refer to the [Documentation](https://tiniyo.com)_ 163 | 164 |

(back to top)

165 | 166 | 167 | 168 | 169 | ## Roadmap 170 | 171 | - [X] XML Parser 172 | - [X] Play, Say, Dial, Sip, Number, Record, Hangup, Reject, Pause, Gather , Redirect Twilio elements support 173 | - [] Conference 174 | - [] Conference API 175 | - [] Inbound XML Support 176 | - [] Outbound XML Support 177 | - [X] HeartBeat Event for billing 178 | - [X] CDR Post to api 179 | - [X] Initiated, Ringing, In-Progress, Hangup, Record Start, Record Stop Event for Twilio based callback for customer Url 180 | - [X] HeartBeat Event for billing 181 | - [X] Text2Speech Support 182 | - [] Speech2Text Support 183 | - [X] DTMF Support 184 | - [] Speech Detection 185 | 186 | See the [open issues](https://github.com/tiniyo/neoms/issues) for a full list of proposed features (and known issues). 187 | 188 |

(back to top)

189 | 190 | 191 | 192 | 193 | ## Contributing 194 | 195 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 196 | 197 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 198 | Don't forget to give the project a star! Thanks again! 199 | 200 | 1. Fork the Project 201 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 202 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 203 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 204 | 5. Open a Pull Request 205 | 206 |

(back to top)

207 | 208 | 209 | 210 | 211 | ## License 212 | 213 | Distributed under the MIT License. See `LICENSE.txt` for more information. 214 | 215 |

(back to top)

216 | 217 | 218 | 219 | 220 | ## Contact 221 | 222 | Your Name - [@twitter_handle](https://twitter.com/twitter_handle) - support@tiniyo.com 223 | 224 | Project Link: [https://github.com/tiniyo/neoms](https://github.com/tiniyo/neoms) 225 | 226 |

(back to top)

227 | 228 | 229 | 230 | 231 | ## Acknowledgments 232 | 233 | * []() 234 | * []() 235 | * []() 236 | 237 |

(back to top)

238 | 239 | 240 | 241 | 242 | 243 | [contributors-shield]: https://img.shields.io/github/contributors/tiniyo/neoms.svg?style=for-the-badge 244 | [contributors-url]: https://github.com/tiniyo/neoms/graphs/contributors 245 | [forks-shield]: https://img.shields.io/github/forks/tiniyo/neoms.svg?style=for-the-badge 246 | [forks-url]: https://github.com/tiniyo/neoms/network/members 247 | [stars-shield]: https://img.shields.io/github/stars/tiniyo/neoms.svg?style=for-the-badge 248 | [stars-url]: https://github.com/tiniyo/neoms/stargazers 249 | [issues-shield]: https://img.shields.io/github/issues/tiniyo/neoms.svg?style=for-the-badge 250 | [issues-url]: https://github.com/tiniyo/neoms/issues 251 | [license-shield]: https://img.shields.io/github/license/tiniyo/neoms.svg?style=for-the-badge 252 | [license-url]: https://github.com/tiniyo/neoms/blob/master/LICENSE.txt 253 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 254 | [linkedin-url]: https://in.linkedin.com/company/tiniyo 255 | [product-screenshot]: images/cpass.jpeg 256 | -------------------------------------------------------------------------------- /managers/callmanager.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/tiniyo/neoms/constant" 7 | "strings" 8 | 9 | "github.com/tiniyo/neoms/adapters" 10 | "github.com/tiniyo/neoms/adapters/callstate" 11 | "github.com/tiniyo/neoms/adapters/factory" 12 | "github.com/tiniyo/neoms/helper" 13 | "github.com/tiniyo/neoms/logger" 14 | "github.com/tiniyo/neoms/managers/rateroute" 15 | "github.com/tiniyo/neoms/models" 16 | ) 17 | 18 | type CallManagerInterface interface { 19 | CreateCall(cr *models.CallRequest) (*models.CallResponse, error) 20 | DeleteCallWithReason(callSid string, reason string) 21 | DeleteCall(callSid string) 22 | GetCall(callSid string) (*models.CallResponse, error) 23 | UpdateCall(cr models.CallUpdateRequest) (*models.CallResponse, error) 24 | } 25 | 26 | type CallManager struct { 27 | callState adapters.CallStateAdapter 28 | } 29 | 30 | var MsAdapter adapters.MediaServer 31 | 32 | func NewCallManager() CallManagerInterface { 33 | var err error 34 | cm := CallManager{} 35 | if cm.callState, err = callstate.NewCallStateAdapter(); err!=nil{ 36 | return nil 37 | } 38 | MsAdapter = factory.GetMSInstance() 39 | msCallBacker := new(CallBackManager) 40 | msCallBacker.InitCallBackManager() 41 | //here we initialize 2 connections 1 for callback and 1 for sending command 42 | if err = MsAdapter.InitializeCallbackMediaServers(msCallBacker);err != nil { 43 | return nil 44 | } 45 | return cm 46 | } 47 | 48 | func (cm CallManager) CreateCall(cr *models.CallRequest) (*models.CallResponse, error) { 49 | var routingString = "" 50 | var destination = cr.To 51 | callResponse := models.CallResponse{} 52 | 53 | if cr.VendorAuthID == "" { 54 | cr.VendorAuthID = constant.GetConstant("DefaultVendorAuthId").(string) 55 | } 56 | 57 | status, rateRoutes := rateroute.GetOutboundRateRoutes(cr.Sid, cr.VendorAuthID, cr.AccountSid, destination) 58 | if status == "failed" || rateRoutes == nil { 59 | logger.UuidLog("Err", cr.Sid, fmt.Sprintf("rateroute not found")) 60 | return nil, &models.RequestError{ 61 | StatusCode: 415, 62 | Err: errors.New("rates not set for destination"), 63 | } 64 | } 65 | 66 | if helper.IsSipCall(destination) { 67 | logger.Logger.Info("sip call processing, skipping route processing") 68 | } else { 69 | logger.Logger.Info("pstn call, termination route processing") 70 | if rateRoutes.Term == nil { 71 | logger.Logger.Error("No routes found, exit the call") 72 | return nil, &models.RequestError{ 73 | StatusCode: 415, 74 | Err: errors.New("routes not set for destination"), 75 | } 76 | } 77 | var routingTokenArray = helper.JwtTokenInfos{} 78 | for _, rt := range rateRoutes.Term { 79 | var routingToken = helper.JwtTokenInfo{} 80 | 81 | if rt.RemovePrefix != "" { 82 | cr.To = strings.TrimPrefix(cr.To, "+") 83 | cr.To = strings.TrimPrefix(cr.To, rt.RemovePrefix) 84 | } 85 | if rt.FromRemovePrefix != "" { 86 | cr.From = strings.TrimPrefix(cr.From, "+") 87 | cr.From = strings.TrimPrefix(cr.From, rt.FromRemovePrefix) 88 | cr.FromRemovePrefix = rt.FromRemovePrefix 89 | } 90 | if rt.TrunkPrefix != "" { 91 | cr.To = fmt.Sprintf("%s%s", rt.TrunkPrefix, cr.To) 92 | } 93 | 94 | if rt.SipPilotNumber != "" { 95 | cr.SipPilotNumber = rt.SipPilotNumber 96 | } 97 | if routingString == "" { 98 | routingString = fmt.Sprintf("sip:%s@%s", cr.To, rt.PrimaryIP) 99 | } else { 100 | routingString = fmt.Sprintf("%s^sip:%s@%s", routingString, cr.To, rt.PrimaryIP) 101 | } 102 | if rt.Username != "" { 103 | routingToken.Ip = rt.PrimaryIP 104 | routingToken.Username = rt.Username 105 | routingToken.Password = rt.Password 106 | routingTokenArray = append(routingTokenArray, routingToken) 107 | } 108 | if rt.FailoverIP != "" { 109 | routingString = fmt.Sprintf("%s^sip:%s@%s", routingString, cr.To, rt.FailoverIP) 110 | if rt.Username != "" { 111 | routingToken.Ip = rt.FailoverIP 112 | routingToken.Username = rt.Username 113 | routingToken.Password = rt.Password 114 | routingTokenArray = append(routingTokenArray, routingToken) 115 | } 116 | } 117 | } 118 | } 119 | 120 | logger.Logger.Info("Routing string for call is - ", routingString) 121 | pulse := rateRoutes.Orig.SubPulse 122 | rateInSecond := rateRoutes.Orig.Rate / 60 123 | rateInPulse := rateInSecond * float32(pulse) 124 | 125 | if cr.Record != "" { 126 | cr.RecordingSource = "OutboundAPI" 127 | if cr.RecordingTrack == "" { 128 | cr.RecordingTrack = "both" 129 | } 130 | if cr.RecordingChannels == "" { 131 | cr.RecordingChannels = "2" 132 | } 133 | if cr.RecordingStatusCallbackEvent == "" { 134 | cr.RecordingStatusCallbackEvent = "completed" 135 | } 136 | } 137 | 138 | cr.Rate = float64(rateInPulse) 139 | cr.Pulse = pulse 140 | cr.ParentCallSid = cr.Sid 141 | cr.Callback.CallSid = cr.Sid 142 | callResponse.Direction = "outbound-api" 143 | callResponse.PriceUnit = "USD" 144 | callResponse.To = cr.To 145 | callResponse.From = cr.From 146 | callResponse.Sid = cr.Sid 147 | cr.DestType = "api" 148 | callResponse.AccountSid = cr.AccountSid 149 | callResponse.APIVersion = constant.GetConstant("ApiVersion").(string) 150 | callResponse.Status = "queued" 151 | callResponse.FromFormatted = cr.From 152 | callResponse.ToFormatted = cr.To 153 | callResponse.URI = fmt.Sprintf("/v1/Account/%s/Call/%s", cr.AccountSid, cr.Sid) 154 | cr.CallResponse = callResponse 155 | //get the json request of call request 156 | jsonCallRequestData, err := json.Marshal(cr) 157 | if err != nil { 158 | return nil, err 159 | } 160 | /* Store Call State */ 161 | err = cm.callState.Set(cr.Sid, jsonCallRequestData) 162 | if err != nil { 163 | logger.Logger.Error("SetCallState Failed", err) 164 | } 165 | cr.SendDigits = helper.DtmfSanity(cr.SendDigits) 166 | originateStr := helper.GenDialString(cr) 167 | 168 | cmd := "" 169 | 170 | toUser := strings.Split(destination, "@")[0] 171 | sipTo := strings.Split(toUser, ":") 172 | if sipTo[0] == "sip" { 173 | toUser = sipTo[1] 174 | } else { 175 | toUser = sipTo[0] 176 | } 177 | 178 | gwDialStr := constant.GetConstant("GatewayDialString").(string) 179 | gwSipDialStr := constant.GetConstant("SipGatwayDialString").(string) 180 | 181 | if routingString != "" { 182 | cmd = fmt.Sprintf("bgapi originate {%s,sip_h_X-Tiniyo-Gateway=%s}%s/%s &park", 183 | originateStr, routingString, gwDialStr, toUser) 184 | } else if helper.IsSipCall(destination) && strings.Contains(destination, "phone.tiniyo.com") { 185 | cmd = fmt.Sprintf("bgapi originate {%s,sip_h_X-Tiniyo-Sip=%s,sip_h_X-Tiniyo-Phone=user}%s/%s &park", 186 | originateStr, destination, gwSipDialStr, toUser) 187 | } else { 188 | cmd = fmt.Sprintf("bgapi originate {%s,sip_h_X-Tiniyo-Gateway=%s,sip_h_X-Tiniyo-Phone=sip}%s/%s &park", 189 | originateStr, destination, gwSipDialStr, toUser) 190 | } 191 | 192 | logger.Logger.Debugln("Command : ", cmd) 193 | /* Make Call to the Call State */ 194 | go func() { 195 | _ = MsAdapter.CallNewOutbound(cmd) 196 | }() 197 | return &callResponse, err 198 | } 199 | 200 | func (cm CallManager) UpdateCall(cr models.CallUpdateRequest) (*models.CallResponse, error) { 201 | callSid := cr.Sid 202 | logger.UuidLog("Info", callSid, "get current call status") 203 | data := models.CallRequest{} 204 | val, err := cm.callState.Get(callSid) 205 | 206 | if err == nil { 207 | if err := json.Unmarshal(val, &data); err != nil { 208 | logger.UuidLog("Err", callSid, fmt.Sprintf("no active call with callsid %s", err.Error())) 209 | return nil, err 210 | } 211 | 212 | if cr.StatusCallback != "" { 213 | data.StatusCallback = cr.StatusCallback 214 | } 215 | if cr.StatusCallbackMethod != "" { 216 | data.StatusCallback = cr.StatusCallbackMethod 217 | } 218 | if cr.Url != "" { 219 | data.Url = cr.Url 220 | } 221 | if cr.Method != "" { 222 | data.Method = cr.Method 223 | } 224 | if cr.FallbackUrl != "" { 225 | data.FallbackUrl = cr.FallbackUrl 226 | } 227 | if cr.FallbackMethod != "" { 228 | data.FallbackMethod = cr.FallbackMethod 229 | } 230 | 231 | if cr.Status == "canceled" && data.Status != "in-progress" { 232 | cm.DeleteCall(callSid) 233 | data.Status = "canceled" 234 | } else if cr.Status == "completed" { 235 | cm.DeleteCall(callSid) 236 | data.Status = "completed" 237 | } 238 | 239 | if dataByte, err := json.Marshal(data); err == nil { 240 | if err := cm.callState.Set(callSid, dataByte); err != nil { 241 | logger.UuidLog("Err", callSid, fmt.Sprint(" triggerCallBack - callback state update issue - ", err)) 242 | } 243 | } 244 | return &(data.CallResponse), nil 245 | } 246 | logger.UuidLog("Err", callSid, fmt.Sprintf("no active call with callsid %s", err.Error())) 247 | return nil, err 248 | } 249 | 250 | func (cm CallManager) GetCall(callSid string) (*models.CallResponse, error) { 251 | logger.UuidLog("Info", callSid, "get current call status") 252 | data := models.CallRequest{} 253 | val, err := cm.callState.Get(callSid) 254 | if err == nil { 255 | if err := json.Unmarshal(val, &data); err != nil { 256 | logger.UuidLog("Err", callSid, fmt.Sprintf("no active call with callsid %s", err.Error())) 257 | return nil, err 258 | } 259 | logger.UuidLog("Info", callSid, fmt.Sprint("current call status is - ", data)) 260 | return &(data.CallResponse), nil 261 | } 262 | logger.UuidLog("Err", callSid, fmt.Sprintf("no active call with callsid %s", err.Error())) 263 | return nil, err 264 | } 265 | 266 | func (cm CallManager) DeleteCall(callSid string) { 267 | _ = MsAdapter.CallHangup(callSid) 268 | } 269 | func (cm CallManager) DeleteCallWithReason(callSid string, reason string) { 270 | _ = MsAdapter.CallHangupWithReason(callSid, reason) 271 | } 272 | -------------------------------------------------------------------------------- /managers/tinixml/speak.go: -------------------------------------------------------------------------------- 1 | package tinixml 2 | 3 | import ( 4 | "github.com/beevik/etree" 5 | "github.com/tiniyo/neoms/adapters" 6 | "github.com/tiniyo/neoms/logger" 7 | "github.com/tiniyo/neoms/models" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | var awsVoiceId = map[string]map[string]string{ 13 | "en-US": {}, "en-GB": {}, "de-DE": {}, 14 | "en-AU": {}, "en-CA": {}, "en-IN": {}, 15 | "sv-SE": {}, "zh-CN": {}, "pl-PL": {}, 16 | "pt-BR": {}, "fr-FR": {}, "ja-JP": {}, 17 | "pt-PT": {}, "it-IT": {}, "ko-KR": {}, 18 | "ru-RU": {}, "nb-NO": {}, "nl-NL": {}, 19 | "fr-CA": {}, "ca-ES": {}, "es-ES": {}, 20 | "es-MX": {}, "fi-FI": {}, "zh-HK": {}, 21 | "zh-TW": {}, "da-DK": {}, 22 | } 23 | 24 | var validAwsPollyVoiceId = "Aditi | Amy | Astrid | Bianca | Brian |" + 25 | "Camila | Carla | Carmen | Celine | Chantal | Conchita |" + 26 | " Cristiano | Dora | Emma | Enrique | Ewa | Filiz |" + 27 | " Geraint | Giorgio | Gwyneth | Hans | Ines | Ivy |" + 28 | " Jacek | Jan | Joanna | Joey | Justin | Karl | Kendra | Kevin |" + 29 | " Kimberly | Lea | Liv | Lotte | Lucia | Lupe | Mads | Maja |" + 30 | " Marlene | Mathieu | Matthew | Maxim | Mia | Miguel | Mizuki |" + 31 | " Naja | Nicole | Penelope | Raveena | Ricardo | Ruben | Russell |" + 32 | " Salli | Seoyeon | Takumi | Tatyana | Vicki | Vitoria | Zeina | Zhiyu" 33 | 34 | func ProcessSpeak(msAdapter *adapters.MediaServer, data models.CallRequest, element *etree.Element) error { 35 | callSid := data.CallSid 36 | if data.Status != "in-progress" { 37 | _ = (*msAdapter).AnswerCall(data.CallSid) 38 | /* 39 | If its webrtc calls then send silence stream here for 2.5 second 40 | */ 41 | if data.SrcType == "Wss" || data.DestType == "Wss"{ 42 | _ = (*msAdapter).PlayMediaFile(data.CallSid, "silence_stream://2000", "1") 43 | } 44 | } 45 | loopCount := 1 46 | voice := "default" 47 | language := "en-US" 48 | var err error 49 | for _, attr := range element.Attr { 50 | switch attr.Key { 51 | case "voice": 52 | voice = attr.Value 53 | case "loop": 54 | loopCount, err = strconv.Atoi(attr.Value) 55 | if err != nil { 56 | loopCount = 1 57 | } 58 | if loopCount == 0 { 59 | loopCount = 1000 60 | } 61 | case "language": 62 | language = attr.Value 63 | } 64 | } 65 | /* 66 | restClient := resty.New() 67 | resp, _ := restClient.R(). 68 | SetBody(map[string]interface{}{"outputFormat": ttsOutputFormat, 69 | "sampleRate": ttsSampleRate, 70 | "inputText": element.Text(), "voiceId": ttsVoiceId}).Post(ttsUrl) 71 | ttsResponse := make(map[string]interface{}) 72 | json.Unmarshal(resp.Body(), &ttsResponse) 73 | ttsURL := ttsResponse["tts_file"].(string) 74 | logger.Logger.Debug("tts response uuid=% ttsURL=%s", ttsURL) 75 | strLoopCount := strconv.Itoa(loopCount) 76 | err = (*msAdapter).PlayMediaFile(uuid, ttsURL, strLoopCount) 77 | return err 78 | */ 79 | if !isValidAliceLanguage("language") { 80 | language = "en-US" 81 | } 82 | if !isValidVoice(voice) { 83 | voice = "default" 84 | } 85 | 86 | voiceText := strings.Replace(element.Text(), "\n", "", -1) 87 | logger.Logger.WithField("uuid", callSid).Info("voice is ", voice, " language is ", language) 88 | voiceId := getAwsVoiceId(voice, language) 89 | logger.Logger.WithField("uuid", callSid).Info("voice id is ", voiceId) 90 | err = (*msAdapter).Speak(callSid, voiceId, voiceText) 91 | if err != nil { 92 | return err 93 | } 94 | loopCount = loopCount - 1 95 | for loopCount > 0 { 96 | if err := (*msAdapter).Speak(callSid, voiceId, element.Text()); err != nil { 97 | return err 98 | } 99 | loopCount = loopCount - 1 100 | } 101 | return err 102 | } 103 | 104 | /* 105 | speak from rest 106 | { 107 | "to":"your_destination", 108 | "from":"your_callerId", 109 | "speak":"Welcome to tiniyo, We are here to help you" 110 | } 111 | */ 112 | func ProcessSpeakText(msAdapter *adapters.MediaServer, uuid string, speakText string) error { 113 | voiceId := "Salli" 114 | loopCount := 3 115 | var err error 116 | if err = (*msAdapter).Speak(uuid, voiceId, speakText); err != nil { 117 | return err 118 | } 119 | loopCount = loopCount - 1 120 | for loopCount > 0 { 121 | if err = (*msAdapter).Speak(uuid, voiceId, speakText); err != nil { 122 | return err 123 | } 124 | loopCount = loopCount - 1 125 | } 126 | return err 127 | } 128 | 129 | func isValidManLanguage(lang string) bool { 130 | switch lang { 131 | case "en-US", "en-GB", 132 | "es-ES", "fr-FR", "de-DE": 133 | return true 134 | } 135 | return false 136 | } 137 | 138 | func isValidAliceLanguage(lang string) bool { 139 | switch lang { 140 | case "en-US", "de-DE", 141 | "en-AU", "en-CA", 142 | "en-GB", "en-IN", 143 | "sv-SE", "zh-CN", 144 | "pl-PL", "pt-BR", 145 | "pt-PT", "ru-RU", 146 | "fr-CA", "fr-FR", 147 | "it-IT", "ja-JP", 148 | "ko-KR", "nb-NO", 149 | "nl-NL", "ca-ES", 150 | "es-ES", "es-MX", 151 | "fi-FI", "zh-HK", 152 | "zh-TW", "da-DK": 153 | return true 154 | } 155 | return false 156 | } 157 | 158 | func isValidVoice(voice string) bool { 159 | if strings.Contains(voice, "Polly") { 160 | voice = "Polly" 161 | } 162 | switch voice { 163 | case "man", "woman", "Polly", "alice", "default": 164 | return true 165 | } 166 | return false 167 | } 168 | 169 | func isValidLoop(loop int) bool { 170 | return false 171 | } 172 | 173 | func getAwsVoiceId(voice, lang string) string { 174 | voiceList := strings.SplitN(voice, ".", -1) 175 | if voiceList[0] == "Polly" { 176 | voiceId := voiceList[1] 177 | if strings.Contains(validAwsPollyVoiceId, voiceId) { 178 | return voiceId 179 | } 180 | voice = "default" 181 | } 182 | 183 | if lang == "" { 184 | lang = "en-US" 185 | } 186 | if voice == "" { 187 | voice = "default" 188 | } 189 | awsVoiceId["en-US"]["man"] = "Joey" 190 | awsVoiceId["en-US"]["woman"] = "Salli" 191 | awsVoiceId["en-US"]["alice"] = "Kendra" 192 | awsVoiceId["en-US"]["default"] = "Salli" 193 | 194 | awsVoiceId["de-DE"]["man"] = "Hans" 195 | awsVoiceId["de-DE"]["woman"] = "Marlene" 196 | awsVoiceId["de-DE"]["alice"] = "Vicki" 197 | awsVoiceId["de-DE"]["default"] = "Marlene" 198 | 199 | awsVoiceId["en-AU"]["man"] = "Russell" 200 | awsVoiceId["en-AU"]["woman"] = "Nicole" 201 | awsVoiceId["en-AU"]["alice"] = "Olivia" 202 | awsVoiceId["en-AU"]["default"] = "Nicole" 203 | 204 | awsVoiceId["en-GB"]["man"] = "Brian" 205 | awsVoiceId["en-GB"]["woman"] = "Emma" 206 | awsVoiceId["en-GB"]["alice"] = "Amy" 207 | awsVoiceId["en-GB"]["default"] = "Emma" 208 | 209 | awsVoiceId["en-IN"]["man"] = "Raveena" 210 | awsVoiceId["en-IN"]["woman"] = "Aditi" 211 | awsVoiceId["en-IN"]["alice"] = "Raveena" 212 | awsVoiceId["en-IN"]["default"] = "Raveena" 213 | 214 | awsVoiceId["da-DK"]["man"] = "Mads" 215 | awsVoiceId["da-DK"]["woman"] = "Naja" 216 | awsVoiceId["da-DK"]["alice"] = "Naja" 217 | awsVoiceId["da-DK"]["default"] = "Naja" 218 | 219 | awsVoiceId["nl-NL"]["man"] = "Ruben" 220 | awsVoiceId["nl-NL"]["woman"] = "Lotte" 221 | awsVoiceId["nl-NL"]["alice"] = "Lotte" 222 | awsVoiceId["nl-NL"]["default"] = "Lotte" 223 | 224 | awsVoiceId["es-MX"]["man"] = "Mia" 225 | awsVoiceId["es-MX"]["woman"] = "Mia" 226 | awsVoiceId["es-MX"]["alice"] = "Mia" 227 | awsVoiceId["es-MX"]["default"] = "Mia" 228 | 229 | awsVoiceId["sv-SE"]["man"] = "Astrid" 230 | awsVoiceId["sv-SE"]["woman"] = "Astrid" 231 | awsVoiceId["sv-SE"]["alice"] = "Astrid" 232 | awsVoiceId["sv-SE"]["default"] = "Astrid" 233 | 234 | awsVoiceId["pl-PL"]["man"] = "Jan" 235 | awsVoiceId["pl-PL"]["woman"] = "Ewa" 236 | awsVoiceId["pl-PL"]["alice"] = "Maja" 237 | awsVoiceId["pl-PL"]["default"] = "Ewa" 238 | 239 | awsVoiceId["pt-BR"]["man"] = "Camila" 240 | awsVoiceId["pt-BR"]["woman"] = "Camila" 241 | awsVoiceId["pt-BR"]["alice"] = "Camila" 242 | awsVoiceId["pt-BR"]["default"] = "Camila" 243 | 244 | awsVoiceId["ja-JP"]["man"] = "Takumi" 245 | awsVoiceId["ja-JP"]["woman"] = "Mizuki" 246 | awsVoiceId["ja-JP"]["alice"] = "Mizuki" 247 | awsVoiceId["ja-JP"]["default"] = "Mizuki" 248 | 249 | awsVoiceId["ko-KR"]["man"] = "Seoyeon" 250 | awsVoiceId["ko-KR"]["woman"] = "Seoyeon" 251 | awsVoiceId["ko-KR"]["alice"] = "Seoyeon" 252 | awsVoiceId["ko-KR"]["default"] = "Seoyeon" 253 | 254 | awsVoiceId["nb-NO"]["man"] = "Liv" 255 | awsVoiceId["nb-NO"]["woman"] = "Liv" 256 | awsVoiceId["nb-NO"]["alice"] = "Liv" 257 | awsVoiceId["nb-NO"]["default"] = "Liv" 258 | 259 | awsVoiceId["pt-PT"]["man"] = "Cristiano" 260 | awsVoiceId["pt-PT"]["woman"] = "Ines" 261 | awsVoiceId["pt-PT"]["alice"] = "Ines" 262 | awsVoiceId["pt-PT"]["default"] = "Ines" 263 | 264 | awsVoiceId["ru-RU"]["man"] = "Maxim" 265 | awsVoiceId["ru-RU"]["woman"] = "Tatyana" 266 | awsVoiceId["ru-RU"]["alice"] = "Tatyana" 267 | awsVoiceId["ru-RU"]["default"] = "Tatyana" 268 | 269 | awsVoiceId["fr-CA"]["man"] = "Chantal" 270 | awsVoiceId["fr-CA"]["woman"] = "Chantal" 271 | awsVoiceId["fr-CA"]["alice"] = "Chantal" 272 | awsVoiceId["fr-CA"]["default"] = "Chantal" 273 | 274 | awsVoiceId["fr-FR"]["man"] = "Mathieu" 275 | awsVoiceId["fr-FR"]["woman"] = "Celine" 276 | awsVoiceId["fr-FR"]["alice"] = "Celine" 277 | awsVoiceId["fr-FR"]["default"] = "Celine" 278 | 279 | awsVoiceId["it-IT"]["man"] = "Giorgio" 280 | awsVoiceId["it-IT"]["woman"] = "Carla" 281 | awsVoiceId["it-IT"]["alice"] = "Carla" 282 | awsVoiceId["it-IT"]["default"] = "Carla" 283 | 284 | awsVoiceId["es-ES"]["man"] = "Enrique" 285 | awsVoiceId["es-ES"]["woman"] = "Conchita" 286 | awsVoiceId["es-ES"]["alice"] = "Conchita" 287 | awsVoiceId["es-ES"]["default"] = "Conchita" 288 | 289 | awsVoiceId["en-CA"]["man"] = "" 290 | awsVoiceId["en-CA"]["woman"] = "" 291 | awsVoiceId["en-CA"]["alice"] = "" 292 | awsVoiceId["en-CA"]["default"] = "" 293 | 294 | awsVoiceId["zh-CN"]["man"] = "" 295 | awsVoiceId["zh-CN"]["woman"] = "" 296 | awsVoiceId["zh-CN"]["alice"] = "" 297 | awsVoiceId["zh-CN"]["default"] = "" 298 | 299 | awsVoiceId["ca-ES"]["man"] = "" 300 | awsVoiceId["ca-ES"]["woman"] = "" 301 | awsVoiceId["ca-ES"]["alice"] = "" 302 | awsVoiceId["ca-ES"]["default"] = "" 303 | 304 | awsVoiceId["fi-FI"]["man"] = "" 305 | awsVoiceId["fi-FI"]["woman"] = "" 306 | awsVoiceId["fi-FI"]["alice"] = "" 307 | awsVoiceId["fi-FI"]["default"] = "" 308 | 309 | awsVoiceId["zh-HK"]["man"] = "" 310 | awsVoiceId["zh-HK"]["woman"] = "" 311 | awsVoiceId["zh-HK"]["alice"] = "" 312 | awsVoiceId["zh-HK"]["default"] = "" 313 | 314 | awsVoiceId["zh-TW"]["man"] = "" 315 | awsVoiceId["zh-TW"]["woman"] = "" 316 | awsVoiceId["zh-TW"]["alice"] = "" 317 | awsVoiceId["zh-TW"]["default"] = "" 318 | 319 | if awsVoiceId[lang][voice] == "" { 320 | return "Salli" 321 | } 322 | return awsVoiceId[lang][voice] 323 | } 324 | -------------------------------------------------------------------------------- /managers/xmlmanager.go: -------------------------------------------------------------------------------- 1 | package managers 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "github.com/tiniyo/neoms/adapters" 7 | "github.com/tiniyo/neoms/constant" 8 | "strings" 9 | "time" 10 | 11 | "github.com/beevik/etree" 12 | "github.com/tiniyo/neoms/helper" 13 | "github.com/tiniyo/neoms/logger" 14 | . "github.com/tiniyo/neoms/managers/tinixml" 15 | "github.com/tiniyo/neoms/models" 16 | ) 17 | 18 | type XmlManagerInterface interface { 19 | ParseTinyXml(data models.CallRequest, resp []byte) error 20 | ProcessXmlResponse(data models.CallRequest) 21 | handleXmlUrl(data models.CallRequest) error 22 | requestForXml(xmlUrl string, xmlMethod string, callSid string, dataMap map[string]interface{}) (bool, []byte) 23 | } 24 | 25 | type XmlManager struct { 26 | callState adapters.CallStateAdapter 27 | } 28 | 29 | func NewXmlManager(callStateAdapter adapters.CallStateAdapter) XmlManagerInterface { 30 | return XmlManager{ 31 | callState: callStateAdapter, 32 | } 33 | } 34 | 35 | /* 36 | for inbound call - ProcessXmlResponse will call when call_park event received 37 | for outbound-api call - ProcessXmlResponse will get call when call_answer event received 38 | */ 39 | 40 | func (xmlMgr XmlManager) ParseTinyXml(data models.CallRequest, resp []byte) error { 41 | var err error 42 | nextElement := true 43 | intercept := false 44 | isRedirect := false 45 | doc := etree.NewDocument() 46 | uuid := data.CallSid 47 | var root = new(etree.Element) 48 | 49 | if uuid == "" { 50 | uuid = data.Sid 51 | data.CallSid = uuid 52 | } 53 | 54 | //before processing first stop other processing if any other xml is on going on same call 55 | // we might need to check on redis if any xml is getting processed in this call 56 | if err = MsAdapter.BreakAllUuid(data.CallSid); err != nil { 57 | logger.UuidLog("Err", uuid, fmt.Sprintf("sending uuid break command failed, live with it - %#v", err)) 58 | } 59 | 60 | logger.UuidLog("Info", uuid, fmt.Sprintf("xml parsing started")) 61 | if resp == nil { 62 | logger.UuidLog("Err", uuid, fmt.Sprintf("xml parsing stopped,hangup call")) 63 | } else if err = doc.ReadFromBytes(resp); err != nil { 64 | logger.UuidLog("Err", uuid, fmt.Sprintf("xml parsing stopped,hangup call")) 65 | } else if doc == nil { 66 | logger.UuidLog("Err", uuid, fmt.Sprintf("xml parsing stopped,hangup call")) 67 | } else if root = doc.SelectElement("Response"); root != nil { 68 | xmlChildes := root.ChildElements() 69 | 70 | for _, xmlChild := range xmlChildes { 71 | switch xmlChild.Tag { 72 | case "Reject": 73 | nextElement = false 74 | err = ProcessReject(&MsAdapter, uuid, xmlChild) 75 | case "Play": 76 | err = ProcessPlay(&MsAdapter, data, xmlChild) 77 | case "Dial": 78 | if data.DialNumberUrl == "" { 79 | nextElement, intercept, err = ProcessDial(&MsAdapter, data, xmlChild) 80 | } 81 | // we need to wait for dial to success or fail here 82 | 83 | if intercept { 84 | //this is special condition we are going to wait here xml to finish at other leg 85 | for { 86 | time.Sleep(1 * time.Second) 87 | parentCallSid := fmt.Sprintf("intercept:%s", data.CallSid) 88 | if val, err := xmlMgr.callState.KeyExist(parentCallSid); err != nil || !val { 89 | logger.UuidLog("Err", uuid, fmt.Sprintf("key does not set for intercept - wait")) 90 | } else { 91 | logger.UuidLog("Err", uuid, fmt.Sprintf("key set for intercept - processing with next element")) 92 | intercept = false 93 | break 94 | } 95 | } 96 | } 97 | case "Stream": 98 | case "Siprec": 99 | case "Refer": 100 | case "Record": 101 | nextElement = false 102 | if err = ProcessRecord(&MsAdapter, &data, xmlChild); err == nil { 103 | //get the json request of call request 104 | if dataByte, err := json.Marshal(data); err == nil { 105 | _ = xmlMgr.callState.Set(uuid, dataByte) 106 | } 107 | } 108 | case "Pay": 109 | case "Leave": 110 | case "Gather": 111 | nextElement, err = ProcessGather(&MsAdapter, &data, xmlChild) 112 | if err != nil && err.Error() == "TIMEOUT" { 113 | return constant.ErrGatherTimeout 114 | } 115 | case "Autopilot": 116 | case "Enqueue": 117 | case "Speak", "Say": 118 | err = ProcessSpeak(&MsAdapter, data, xmlChild) 119 | case "Redirect": 120 | if err, redirectUrl, redirectMethod := ProcessRedirect(uuid, &MsAdapter, data, xmlChild); err == nil { 121 | nextElement = false 122 | isRedirect = true 123 | statusCallbackKey := fmt.Sprintf("statusCallback:%s", uuid) 124 | val, err := xmlMgr.callState.Get(statusCallbackKey) 125 | if err != nil { 126 | logger.UuidLog("Err", uuid, fmt.Sprintf("redirect url - unmarshal failed %s", err.Error())) 127 | } else if err = json.Unmarshal(val, &data); err != nil { 128 | logger.UuidLog("Err", uuid, fmt.Sprintf("redirect url - unmarshal failed %s", err.Error())) 129 | } else if data.HangupTime == "" { 130 | data.Url = redirectUrl 131 | data.Method = redirectMethod 132 | _ = xmlMgr.handleXmlUrl(data) 133 | } 134 | } 135 | case "Hangup": 136 | nextElement = false 137 | if err = ProcessHangup(&MsAdapter, uuid, xmlChild); err != nil { 138 | // handle error here 139 | } 140 | break 141 | case "Pause": 142 | ProcessPause(data.Sid, xmlChild) 143 | default: 144 | logger.UuidLog("Info", uuid, fmt.Sprintf("xml child tag is %s", xmlChild.Tag)) 145 | } 146 | 147 | if !nextElement { 148 | break 149 | } 150 | } 151 | } 152 | 153 | if isRedirect { 154 | return nil 155 | } else if data.DialNumberUrl != "" { 156 | logger.UuidLog("Info", uuid, fmt.Sprintf("We are going to bridge parent and child call here")) 157 | if err = MsAdapter.CallIntercept(data.CallSid, data.ParentCallSid); err != nil { 158 | if err = ProcessSyncHangup(&MsAdapter, data.CallSid, "XML_CallFlow_Complete"); err != nil { 159 | logger.UuidLog("Err", uuid, fmt.Sprintf("sending call hangup event failed - %s", err.Error())) 160 | return err 161 | } 162 | } 163 | } 164 | 165 | logger.UuidLog("Info", uuid, fmt.Sprintf("sending synchronous call hangup")) 166 | if err = ProcessSyncHangup(&MsAdapter, data.CallSid, "XML_CallFlow_Complete"); err != nil { 167 | logger.UuidLog("Err", uuid, fmt.Sprintf("call hangup failed - %s", err.Error())) 168 | return err 169 | } 170 | return nil 171 | } 172 | 173 | func (xmlMgr XmlManager) ProcessXmlResponse(data models.CallRequest) { 174 | uuid := data.Sid 175 | logger.UuidLog("Info", uuid, fmt.Sprintf("processing xml response")) 176 | if data.Speak != "" { 177 | logger.Logger.Info(data.Speak) 178 | _ = ProcessSpeakText(&MsAdapter, uuid, data.Speak) 179 | _ = ProcessHangupWithTiniyoReason(&MsAdapter, uuid, "NORMAL_CLEARING") 180 | } else if data.Play != "" { 181 | _ = ProcessPlayFile(&MsAdapter, uuid, data.Play) 182 | _ = ProcessHangupWithTiniyoReason(&MsAdapter, uuid, "NORMAL_CLEARING") 183 | } else { 184 | logger.UuidLog("Info", uuid, fmt.Sprintf("url found getting xml")) 185 | _ = xmlMgr.handleXmlUrl(data) 186 | } 187 | } 188 | 189 | func (xmlMgr XmlManager) handleXmlUrl(data models.CallRequest) error { 190 | xmlUrl := data.Url 191 | callSid := data.Sid 192 | xmlMethod := strings.ToUpper(data.Method) 193 | dataMap := make(map[string]interface{}) 194 | 195 | if data.SipTrunk == "true" { 196 | xmlData := []byte(` 197 | 198 | 199 | ` + data.To + ` 200 | 201 | `) 202 | return xmlMgr.ParseTinyXml(data, xmlData) 203 | } 204 | 205 | if data.TinyML != "" { 206 | logger.UuidLog("Info", callSid, fmt.Sprintf("handleXmlUrl tinyml - %s", data.TinyML)) 207 | if tinyMl, err := url.QueryUnescape(data.TinyML); err == nil { 208 | return xmlMgr.ParseTinyXml(data, []byte(tinyMl)) 209 | } 210 | logger.UuidLog("Err", callSid, fmt.Sprint("handleXmlUrl tinyml parsing error - ")) 211 | return ProcessHangupWithTiniyoReason(&MsAdapter, callSid, "UNALLOCATED_NUMBER") 212 | } 213 | 214 | logger.UuidLog("Info", callSid, fmt.Sprintf("handleXmlUrl url - %s, method - %s", xmlUrl, xmlMethod)) 215 | if byteData, err := json.Marshal(data.Callback); err == nil { 216 | if err = json.Unmarshal(byteData, &dataMap); err != nil { 217 | logger.UuidLog("Err", callSid, fmt.Sprint("send url request failed - ", err)) 218 | return ProcessHangupWithTiniyoReason(&MsAdapter, callSid, "UNALLOCATED_NUMBER") 219 | } 220 | } else { 221 | logger.UuidLog("Err", callSid, fmt.Sprint("send url request failed - ", err)) 222 | return ProcessHangupWithTiniyoReason(&MsAdapter, callSid, "UNALLOCATED_NUMBER") 223 | } 224 | 225 | if status, respBody := xmlMgr.requestForXml(xmlUrl, xmlMethod, callSid, dataMap); status { 226 | return xmlMgr.ParseTinyXml(data, respBody) 227 | } 228 | 229 | xmlUrl = data.FallbackUrl 230 | xmlMethod = data.FallbackMethod 231 | 232 | if status, respBody := xmlMgr.requestForXml(xmlUrl, xmlMethod, callSid, dataMap); status { 233 | return xmlMgr.ParseTinyXml(data, respBody) 234 | } 235 | return ProcessHangupWithTiniyoReason(&MsAdapter, callSid, "Failed_To_Get_XML") 236 | 237 | } 238 | 239 | func (xmlMgr XmlManager) requestForXml(xmlUrl string, xmlMethod string, callSid string, dataMap map[string]interface{}) (bool, []byte) { 240 | if xmlUrl == "" { 241 | return false, nil 242 | } 243 | if xmlMethod == "" { 244 | xmlMethod = "POST" 245 | } 246 | switch xmlMethod { 247 | case "GET", "get", "Get": 248 | statusCode, respBody, err := helper.Get(callSid, dataMap, xmlUrl) 249 | if err != nil || statusCode != 200 || respBody == nil { 250 | logger.UuidLog("Err", callSid, fmt.Sprintf("Error while getting the GET XML %v", err)) 251 | return false, nil 252 | } 253 | logger.UuidLog("Info", callSid, fmt.Sprintf(" GET XML success %v", statusCode)) 254 | /*if len(respBody) > 0 { 255 | isValid, errstr := utils.ValidateXML(respBody) 256 | if isValid == false { 257 | logger.UuidLog("Err", callSid, fmt.Sprintf("Error while parsing the XML %v", errstr)) 258 | return false, nil 259 | } 260 | }*/ 261 | return true, respBody 262 | case "POST", "post", "Post": 263 | statusCode, respBody, err := helper.Post(callSid, dataMap, xmlUrl) 264 | if err != nil || statusCode != 200 || respBody == nil { 265 | logger.UuidLog("Err", callSid, fmt.Sprintf("Error while getting the POST XML %v", err)) 266 | return false, nil 267 | } 268 | logger.UuidLog("Info", callSid, fmt.Sprintf(" POST XML success %v", statusCode)) 269 | /*if len(respBody) > 0 { 270 | isValid, errstr := utils.ValidateXML(respBody) 271 | if isValid == false { 272 | logger.UuidLog("Err", callSid, fmt.Sprintf("Error while parsing the XML %v", errstr)) 273 | return false, nil 274 | } 275 | }*/ 276 | return true, respBody 277 | default: 278 | logger.UuidLog("Info", callSid, fmt.Sprintf("Unknown Method url - %s, method - %s", xmlUrl, xmlMethod)) 279 | _ = ProcessHangupWithTiniyoReason(&MsAdapter, callSid, "Failed_To_Get_XML") 280 | } 281 | return false, nil 282 | } 283 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /TinyMLSchema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tiniyo API TinyML XML Schema Copyright Tiniyo Private Limited. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= 4 | github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= 5 | github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 h1:uH66TXeswKn5PW5zdZ39xEwfS9an067BirqA+P4QaLI= 6 | github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= 7 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 8 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 13 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 15 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 16 | github.com/eclipse/paho.mqtt.golang v1.3.1 h1:6F5FYb1hxVSZS+p0ji5xBQamc5ltOolTYRy5R15uVmI= 17 | github.com/eclipse/paho.mqtt.golang v1.3.1/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= 18 | github.com/evalphobia/logrus_sentry v0.8.2 h1:dotxHq+YLZsT1Bb45bB5UQbfCh3gM/nFFetyN46VoDQ= 19 | github.com/evalphobia/logrus_sentry v0.8.2/go.mod h1:pKcp+vriitUqu9KiWj/VRFbRfFNUwz95/UkgG8a6MNc= 20 | github.com/fiorix/go-eventsocket v0.0.0-20180331081222-a4a0ee7bd315 h1:GBEsz9T7y00+a7w2uHdeVEqVHJ1xj6egPAWeQD3FRTw= 21 | github.com/fiorix/go-eventsocket v0.0.0-20180331081222-a4a0ee7bd315/go.mod h1:1h02GKxDM9bGVz8tbnYb2xcRRCz6Ae2c1tI367JP144= 22 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 23 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 24 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 25 | github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= 26 | github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= 27 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 28 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 29 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 30 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 31 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 32 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 33 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 34 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 35 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 36 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 37 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 38 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 39 | github.com/go-redis/redis/v8 v8.4.10 h1:fWdl0RBmVibUDOp8bqz1e2Yy9dShOeIeWsiAifYk06Y= 40 | github.com/go-redis/redis/v8 v8.4.10/go.mod h1:d5yY/TlkQyYBSBHnXUmnf1OrHbyQere5JV4dLKwvXmo= 41 | github.com/go-resty/resty/v2 v2.4.0 h1:s6TItTLejEI+2mn98oijC5w/Rk2YU+OA6x0mnZN6r6k= 42 | github.com/go-resty/resty/v2 v2.4.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA= 43 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 44 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 45 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 46 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 47 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 48 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 49 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 50 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 51 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 52 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 53 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 54 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 55 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 56 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 57 | github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= 58 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 59 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 60 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 61 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 62 | github.com/hb-go/json v0.0.0-20170624084651-15ef86c8b796 h1:5hKFj2wOUlc1+kHBXHJLgYU4CSJ3dh77AD2KUwCO8go= 63 | github.com/hb-go/json v0.0.0-20170624084651-15ef86c8b796/go.mod h1:om5F1+fU4K+Db/Ofe5QuuoDk0d4Nwu7ECY+gbA9CJRc= 64 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 65 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 66 | github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= 67 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 68 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 69 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 70 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 71 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 72 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 73 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 74 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 75 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 76 | github.com/nsqio/go-nsq v1.0.8 h1:3L2F8tNLlwXXlp2slDUrUWSBn2O3nMh8R1/KEDFTHPk= 77 | github.com/nsqio/go-nsq v1.0.8/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY= 78 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 79 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 80 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 81 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 82 | github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= 83 | github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 84 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 85 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 86 | github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U= 87 | github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ= 88 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 89 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 90 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 91 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 92 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 93 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 94 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 95 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 96 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 97 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 98 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 99 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 100 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 101 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 102 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 103 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 104 | github.com/terminalstatic/go-xsd-validate v0.1.4 h1:fL4kJdcFCmDaIpy9kXZSv/NapJsJUC8RDO6P8TdOSQk= 105 | github.com/terminalstatic/go-xsd-validate v0.1.4/go.mod h1:e6HmA0iGH3en7FFUPH9iE6EtwbAw6oWWH4nn41hNozM= 106 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 107 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 108 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 109 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 110 | go.opentelemetry.io/otel v0.16.0 h1:uIWEbdeb4vpKPGITLsRVUS44L5oDbDUCZxn8lkxhmgw= 111 | go.opentelemetry.io/otel v0.16.0/go.mod h1:e4GKElweB8W2gWUqbghw0B8t5MCTccc9212eNHnOHwA= 112 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 113 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 114 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 115 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 116 | golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 117 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 118 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 119 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw= 120 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 121 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 122 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 123 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 124 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 125 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 128 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 129 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 130 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 131 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 132 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 133 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= 134 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 135 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 136 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 137 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 138 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 139 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 140 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 141 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 142 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 143 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 144 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 145 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 146 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 147 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 148 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 149 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 150 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 151 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 152 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 153 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 154 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 155 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 156 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 157 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 158 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 159 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 160 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 161 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 162 | --------------------------------------------------------------------------------