diff --git a/go.mod b/go.mod index ce91326..92dbd2e 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,12 @@ module github.com/pnguyen215/voipkit go 1.20 require ( - github.com/json-iterator/go v1.1.12 github.com/nyaruka/phonenumbers v1.1.6 golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/protobuf v1.3.2 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect golang.org/x/text v0.3.7 // indirect ) diff --git a/go.sum b/go.sum index ecaed12..55a26c5 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,10 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/nyaruka/phonenumbers v1.1.6 h1:DcueYq7QrOArAprAYNoQfDgp0KetO4LqtnBtQC6Wyes= github.com/nyaruka/phonenumbers v1.1.6/go.mod h1:yShPJHDSH3aTKzCbXyVxNpbl2kA+F+Ne5Pun/MvFRos= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= diff --git a/pkg/ami/ami_cdr.go b/pkg/ami/ami_cdr.go index 92357b2..40cf22c 100644 --- a/pkg/ami/ami_cdr.go +++ b/pkg/ami/ami_cdr.go @@ -13,7 +13,7 @@ import ( func NewAMICdr() *AMICdr { r := &AMICdr{} r.SetEvent(config.AmiListenerEventCdr) - r.SetExtenSplitterSymbol("-") + r.SetSymbol("-") return r } @@ -203,17 +203,17 @@ func (r *AMICdr) SetDirection(value string) *AMICdr { return r } -func (r *AMICdr) SetFlowCall(value string) *AMICdr { - r.FlowCall = value +func (r *AMICdr) SetDesc(value string) *AMICdr { + r.Desc = value return r } -func (r *AMICdr) SetTypeDirection(value string) *AMICdr { - r.TypeDirection = TrimStringSpaces(value) +func (r *AMICdr) SetType(value string) *AMICdr { + r.Type = TrimStringSpaces(value) return r } -func (r *AMICdr) SetUserExten(value string) *AMICdr { +func (r *AMICdr) SetUserExtension(value string) *AMICdr { r.UserExtension = TrimStringSpaces(value) return r } @@ -223,8 +223,8 @@ func (r *AMICdr) SetPhoneNumber(value string) *AMICdr { return r } -func (r *AMICdr) SetExtenSplitterSymbol(value string) *AMICdr { - r.ExtenSplitterSymbol = TrimStringSpaces(value) +func (r *AMICdr) SetSymbol(value string) *AMICdr { + r.symbol = TrimStringSpaces(value) return r } @@ -298,19 +298,19 @@ func (r *AMICdr) IsCdrOutbound() bool { } func (r *AMICdr) IsCdrInboundDial() bool { - return strings.EqualFold(r.TypeDirection, config.AmiTypeInboundDialDirection) + return strings.EqualFold(r.Type, config.AmiTypeInboundDialDirection) } func (r *AMICdr) IsCdrInboundQueue() bool { - return strings.EqualFold(r.TypeDirection, config.AmiTypeInboundQueueDirection) + return strings.EqualFold(r.Type, config.AmiTypeInboundQueueDirection) } func (r *AMICdr) IsCdrOutboundNormal() bool { - return strings.EqualFold(r.TypeDirection, config.AmiTypeOutboundNormalDirection) + return strings.EqualFold(r.Type, config.AmiTypeOutboundNormalDirection) } func (r *AMICdr) IsCdrOutboundChanSpy() bool { - return strings.EqualFold(r.TypeDirection, config.AmiLastApplicationChanSpy) + return strings.EqualFold(r.Type, config.AmiLastApplicationChanSpy) } func ParseCdr(e *AMIMessage, d *AMIDictionary) *AMICdr { @@ -344,14 +344,14 @@ func ParseCdr(e *AMIMessage, d *AMIDictionary) *AMICdr { // detect outbound, inbound // if the field destination is phone number, so mark this cdr belong to outbound, otherwise mark as inbound - form := "flow_call_from_'%v'_to_'%v'" + form := "CDR.call_from_'%v'_to_'%v'" phone := RemoveStringPrefix(r.Destination, e.PhonePrefix...) - if IsPhoneNumberWith(phone, e.Region) { + if VerifyPhoneNo(phone, e.Region) { flow := fmt.Sprintf(form, r.Channel, phone) - r.SetFlowCall(flow) + r.SetDesc(flow) r.SetDirection(config.AmiOutboundDirection) - r.SetTypeDirection(config.AmiTypeOutboundNormalDirection) - r.SetUserExten(strings.Split(r.Channel, r.ExtenSplitterSymbol)[0]) + r.SetType(config.AmiTypeOutboundNormalDirection) + r.SetUserExtension(strings.Split(r.Channel, r.symbol)[0]) r.SetPhoneNumber(phone) } else { var inCase bool = false @@ -359,33 +359,33 @@ func ParseCdr(e *AMIMessage, d *AMIDictionary) *AMICdr { if strings.EqualFold(r.LastApplication, config.AmiLastApplicationChanSpy) { inCase = true flow := fmt.Sprintf(form, r.Channel, r.LastData) - r.SetFlowCall(flow) - r.SetTypeDirection(config.AmiTypeChanSpyDirection) + r.SetDesc(flow) + r.SetType(config.AmiTypeChanSpyDirection) r.SetDirection(config.AmiOutboundDirection) - r.SetUserExten(strings.Split(r.Channel, r.ExtenSplitterSymbol)[0]) + r.SetUserExtension(strings.Split(r.Channel, r.symbol)[0]) } // from inbound dial if strings.EqualFold(r.LastApplication, config.AmiLastApplicationDial) { inCase = true flow := fmt.Sprintf(form, r.Source, r.DestinationChannel) - r.SetFlowCall(flow) + r.SetDesc(flow) r.SetDirection(config.AmiInboundDirection) - r.SetTypeDirection(config.AmiTypeInboundDialDirection) - r.SetUserExten(strings.Split(r.DestinationChannel, r.ExtenSplitterSymbol)[0]) + r.SetType(config.AmiTypeInboundDialDirection) + r.SetUserExtension(strings.Split(r.DestinationChannel, r.symbol)[0]) r.SetPhoneNumber(r.Source) } // from inbound queue if strings.EqualFold(r.LastApplication, config.AmiLastApplicationQueue) { inCase = true flow := fmt.Sprintf(form, r.Source, r.Channel) - r.SetFlowCall(flow) + r.SetDesc(flow) r.SetDirection(config.AmiInboundDirection) - r.SetTypeDirection(config.AmiTypeInboundQueueDirection) - r.SetUserExten(strings.Split(r.Channel, r.ExtenSplitterSymbol)[0]) + r.SetType(config.AmiTypeInboundQueueDirection) + r.SetUserExtension(strings.Split(r.Channel, r.symbol)[0]) r.SetPhoneNumber(r.Source) } if !inCase { - log.Printf("ParseCdr, CDR exception case = %v", JsonString(r)) + log.Printf("ParseCdr, CDR got an error exception case:: %v", JsonString(r)) } } return r diff --git a/pkg/ami/ami_chanspy.go b/pkg/ami/ami_chanspy.go index b7c4a66..4eec06b 100644 --- a/pkg/ami/ami_chanspy.go +++ b/pkg/ami/ami_chanspy.go @@ -40,28 +40,28 @@ func (s *AMIPayloadChanspy) SetChannelProtocol(value string) *AMIPayloadChanspy return s } -func (s *AMIPayloadChanspy) SetAllowDebug(value bool) *AMIPayloadChanspy { - s.AllowDebug = value +func (s *AMIPayloadChanspy) SetDebugMode(value bool) *AMIPayloadChanspy { + s.DebugMode = value return s } -func (s *AMIPayloadChanspy) CommandChanspy(channelExten string) string { +func (s *AMIPayloadChanspy) CommandChanspy(c_extension string) string { if IsStringEmpty(s.Join) { return "" } - if IsStringEmpty(channelExten) { + if IsStringEmpty(c_extension) { return "" } if strings.EqualFold(s.Join, config.AmiChanspySpy) { - return channelExten + return c_extension } if strings.EqualFold(s.Join, config.AmiChanspyWhisper) { - return fmt.Sprintf("%s,w", channelExten) + return fmt.Sprintf("%s,w", c_extension) } if strings.EqualFold(s.Join, config.AmiChanspyBarge) { - return fmt.Sprintf("%s,B", channelExten) + return fmt.Sprintf("%s,B", c_extension) } - return channelExten + return c_extension } // Chanspy @@ -72,31 +72,31 @@ func Chanspy(ctx context.Context, s AMISocket, ch AMIPayloadChanspy) (AMIResultR log.Panic(config.AmiErrorInvalidChanspy, "\n", msg) } if IsStringEmpty(ch.SourceExten) { - return AMIResultRawLevel{}, fmt.Errorf("Source exten is required") + return AMIResultRawLevel{}, fmt.Errorf("Source extension is required") } if IsStringEmpty(ch.CurrentExten) { - return AMIResultRawLevel{}, fmt.Errorf("Current exten is required") + return AMIResultRawLevel{}, fmt.Errorf("Current extension is required") } - sourceValid, err := HasSIPPeerStatus(ctx, s, ch.SourceExten) + source_extension_verify, err := HasSIPPeerStatus(ctx, s, ch.SourceExten) if err != nil { return AMIResultRawLevel{}, err } - if !sourceValid { - return AMIResultRawLevel{}, fmt.Errorf("Source exten '%v' not found", ch.SourceExten) + if !source_extension_verify { + return AMIResultRawLevel{}, fmt.Errorf("Source extension '%v' not found", ch.SourceExten) } - currentValid, err := HasSIPPeerStatus(ctx, s, ch.CurrentExten) + current_extension_verify, err := HasSIPPeerStatus(ctx, s, ch.CurrentExten) if err != nil { return AMIResultRawLevel{}, err } - if !currentValid { - return AMIResultRawLevel{}, fmt.Errorf("Current exten '%v' not found", ch.CurrentExten) + if !current_extension_verify { + return AMIResultRawLevel{}, fmt.Errorf("Current extension '%v' not found", ch.CurrentExten) } channel := NewChannel().SetChannelProtocol(ch.ChannelProtocol) sourceExt := channel.JoinChannelWith(channel.ChannelProtocol, fmt.Sprintf("%v", ch.SourceExten)) currentExt := channel.JoinChannelWith(channel.ChannelProtocol, fmt.Sprintf("%v", ch.CurrentExten)) channelId := ch.CommandChanspy(sourceExt) cmd := fmt.Sprintf("channel originate %s application ChanSpy %s", currentExt, channelId) - if ch.AllowDebug { + if ch.DebugMode { log.Printf("Chanspy command: %v \n", cmd) log.Printf("Chanspy channel_id: %v \n", channelId) log.Printf("Chanspy source.ext: %v \n", sourceExt) diff --git a/pkg/ami/ami_dial.go b/pkg/ami/ami_dial.go new file mode 100644 index 0000000..227ba63 --- /dev/null +++ b/pkg/ami/ami_dial.go @@ -0,0 +1,60 @@ +package ami + +import ( + "bufio" + "context" + "log" + "net" + "net/textproto" + + "github.com/pnguyen215/voipkit/pkg/ami/config" +) + +// OpenContext +func OpenContext(conn net.Conn) (*AMI, context.Context) { + ctx, cancel := context.WithCancel(context.Background()) + client := &AMI{ + Reader: textproto.NewReader(bufio.NewReader(conn)), + Writer: bufio.NewWriter(conn), + Conn: conn, + Cancel: cancel, + } + // checking conn available + if conn != nil { + addr := conn.RemoteAddr().String() + _socket, err := NewAMISocketWith(ctx, addr) + + if err == nil { + client.Socket = _socket + log.Printf("OpenContext, cloning (addr: %v) socket connection succeeded", addr) + } + } + return client, ctx +} + +// OpenDial +func OpenDial(ip string, port int) (net.Conn, error) { + return OpenDialWith(config.AmiNetworkTcpKey, ip, port) +} + +// OpenDialWith +func OpenDialWith(network, ip string, port int) (net.Conn, error) { + if !config.AmiNetworkKeys[network] { + return nil, AMIErrorNew("AMI: Invalid network") + } + if ip == "" { + return nil, AMIErrorNew("AMI: IP must be not empty") + } + if port <= 0 { + return nil, AMIErrorNew("AMI: Port must be positive number") + } + host, _port, _ := DecodeIp(ip) + if len(host) > 0 && len(_port) > 0 { + form := net.JoinHostPort(host, _port) + log.Printf("AMI: (IP decoded) dial connection = %v", form) + return net.Dial(network, form) + } + form := RemoveProtocol(ip, port) + log.Printf("AMI: dial connection = %v", form) + return net.Dial(network, form) +} diff --git a/pkg/ami/ami_helper.go b/pkg/ami/ami_helper.go index 9194b35..42a11e6 100644 --- a/pkg/ami/ami_helper.go +++ b/pkg/ami/ami_helper.go @@ -1,7 +1,6 @@ package ami import ( - "bufio" "bytes" "context" "encoding/base64" @@ -10,7 +9,6 @@ import ( "log" "net" "net/http" - "net/textproto" "net/url" "os" "reflect" @@ -21,104 +19,91 @@ import ( "github.com/nyaruka/phonenumbers" "github.com/pnguyen215/voipkit/pkg/ami/config" - - jsonI "github.com/json-iterator/go" ) -// OpenContext -func OpenContext(conn net.Conn) (*AMI, context.Context) { - ctx, cancel := context.WithCancel(context.Background()) - - client := &AMI{ - Reader: textproto.NewReader(bufio.NewReader(conn)), - Writer: bufio.NewWriter(conn), - Conn: conn, - Cancel: cancel, - } - - // checking conn available - if conn != nil { - addr := conn.RemoteAddr().String() - _socket, err := NewAMISocketWith(ctx, addr) - - if err == nil { - client.Socket = _socket - log.Printf("OpenContext, cloning (addr: %v) socket connection succeeded", addr) - } - } - - return client, ctx -} - -// OpenDial -func OpenDial(ip string, port int) (net.Conn, error) { - return OpenDialWith(config.AmiNetworkTcpKey, ip, port) -} - -// OpenDialWith -func OpenDialWith(network, ip string, port int) (net.Conn, error) { - - if !config.AmiNetworkKeys[network] { - return nil, AMIErrorNew("AMI: Invalid network") - } - - if ip == "" { - return nil, AMIErrorNew("AMI: IP must be not empty") - } - - if port <= 0 { - return nil, AMIErrorNew("AMI: Port must be positive number") - } - - host, _port, _ := DecodeIp(ip) - - if len(host) > 0 && len(_port) > 0 { - form := net.JoinHostPort(host, _port) - log.Printf("AMI: (IP decoded) dial connection = %v", form) - return net.Dial(network, form) - } - - form := RemoveProtocol(ip, port) - log.Printf("AMI: dial connection = %v", form) - return net.Dial(network, form) -} - -// RemoveProtocol -// Return form as string: : +// RemoveProtocol removes the protocol prefix and port from the given IP address. +// If the IP address is empty or the port is negative, the original IP address is returned unchanged. +// The function supports removing both "http://" and "https://" protocol prefixes. +// The resulting IP address is formatted as a string without the protocol prefix and with the specified port, +// or without the port if it was negative or not provided. +// +// Parameters: +// - ip: The input IP address with an optional protocol prefix and port. +// - port: The port number to be included in the formatted IP address. +// If the port is negative, it is excluded from the formatted IP address. +// +// Returns: +// - The formatted IP address as a string without the protocol prefix and with the specified port, +// or without the port if it was negative or not provided. +// // Example: -// Ip: http://127.0.0.1 or https://127.0.0.1 -// Port: 18080 -// Result: 127.0.0.1:18080 +// +// input: "http://127.0.0.1:8088", port: 8088 +// output: "127.0.0.1:8088" +// +// input: "https://example.com", port: -1 +// output: "example.com" +// +// input: "invalid", port: 5060 +// output: "invalid:5060" func RemoveProtocol(ip string, port int) string { - if ip == "" { - return ip - } - - if port < 0 { + if IsStringEmpty(ip) || port < 0 { return ip } - if strings.HasPrefix(ip, config.AmiProtocolHttpKey) { ip = strings.Replace(ip, config.AmiProtocolHttpKey, "", -1) } - if strings.HasPrefix(ip, config.AmiProtocolHttpsKey) { ip = strings.Replace(ip, config.AmiProtocolHttpsKey, "", -1) } - _ip := strings.Split(ip, ":") ip = _ip[0] - form := net.JoinHostPort(ip, strconv.Itoa(port)) return form } -// JoinHostPortString +// JoinHostPortString joins the given IP address and port into a formatted string. +// The function utilizes the RemoveProtocol function to handle the removal of protocol prefixes. +// +// Parameters: +// - ip: The input IP address with an optional protocol prefix and port. +// - port: The port number to be included in the formatted IP address. +// If the port is negative, it is excluded from the formatted IP address. +// +// Returns: +// - The formatted IP address as a string without the protocol prefix and with the specified port, +// or without the port if it was negative or not provided. +// +// Example: +// +// input: "http://127.0.0.1:8088", port: 8088 +// output: "127.0.0.1:8088" +// +// input: "https://example.com", port: -1 +// output: "example.com" +// +// input: "invalid", port: 5060 +// output: "invalid:5060" func JoinHostPortString(ip string, port int) string { return RemoveProtocol(ip, port) } -// JoinHostPortStrings +// JoinHostPortStrings joins the given slice of IP addresses with the specified port into formatted strings. +// Each IP address in the slice may have an optional protocol prefix and port, which is handled by the JoinHostPortString function. +// +// Parameters: +// - ip: The input slice of IP addresses, where each IP address may have an optional protocol prefix and port. +// - port: The port number to be included in the formatted IP addresses. +// If the port is negative, it is excluded from the formatted IP addresses. +// +// Returns: +// - A slice of formatted IP addresses as strings without the protocol prefix and with the specified port, +// or without the port if it was negative or not provided. +// +// Example: +// +// input: []string{"http://127.0.0.1:8088", "https://example.com", "invalid"}, port: 5060 +// output: []string{"127.0.0.1:8088", "example.com", "invalid:5060"} func JoinHostPortStrings(ip []string, port int) (result []string) { if len(ip) == 0 { return ip @@ -129,7 +114,24 @@ func JoinHostPortStrings(ip []string, port int) (result []string) { return result } -// WriteString +// WriteString writes a formatted key-value pair to the provided bytes.Buffer. +// The key (tag) and value are concatenated with appropriate separators, and a newline signal is added at the end. +// If the key (tag) is an empty string, it is excluded from the output. +// +// Parameters: +// - buf: A pointer to the bytes.Buffer where the formatted string will be written. +// - tag: The key (tag) representing the identifier of the value. +// - value: The value associated with the key. +// +// Example: +// +// buf := &bytes.Buffer{} +// WriteString(buf, "Action", "Login") +// result: "Action: Login\r\n" +// +// buf := &bytes.Buffer{} +// WriteString(buf, "", "ValueOnly") +// result: "ValueOnly\r\n" func WriteString(buf *bytes.Buffer, tag, value string) { if len(tag) > 0 { buf.WriteString(tag) @@ -139,6 +141,28 @@ func WriteString(buf *bytes.Buffer, tag, value string) { buf.WriteString(config.AmiSignalLetter) } +// IsOmitempty checks if the given struct field tag contains the "omitempty" flag. +// +// Parameters: +// - tag: The struct field tag to be analyzed. +// +// Returns: +// - A tuple containing the modified tag without the "omitempty" flag (if present), +// a boolean indicating whether "omitempty" was present, and an error (if any). +// +// Example: +// +// tag := `json:"name,omitempty"` +// result, omitempty, err := IsOmitempty(tag) +// // result: "json:\"name\"", omitempty: true, err: nil +// +// tag := `json:"age"` +// result, omitempty, err := IsOmitempty(tag) +// // result: "json:\"age\"", omitempty: false, err: nil +// +// tag := `json:"salary,omitempty,unsupported"` +// result, omitempty, err := IsOmitempty(tag) +// // result: "", omitempty: false, err: "unsupported flag \"unsupported\" in tag \"json:\"salary,omitempty,unsupported\"" func IsOmitempty(tag string) (string, bool, error) { fields := strings.Split(tag, ",") if len(fields) > 1 { @@ -152,6 +176,31 @@ func IsOmitempty(tag string) (string, bool, error) { return tag, false, nil } +// IsZero checks if the given reflect.Value is considered "zero" based on its kind. +// +// Parameters: +// - v: The reflect.Value to be checked for being "zero." +// +// Returns: +// - A boolean indicating whether the provided reflect.Value is considered "zero." +// +// Example: +// +// var str string +// result := IsZero(reflect.ValueOf(str)) +// // result: true +// +// var num int +// result := IsZero(reflect.ValueOf(num)) +// // result: true +// +// var slice []int +// result := IsZero(reflect.ValueOf(slice)) +// // result: true +// +// var ptr *int +// result := IsZero(reflect.ValueOf(ptr)) +// // result: true func IsZero(v reflect.Value) bool { switch v.Kind() { case reflect.String: @@ -177,6 +226,31 @@ func IsZero(v reflect.Value) bool { return false } +// Encode writes the encoded Asterisk Manager Interface (AMI) command parameters to the provided bytes.Buffer. +// The encoding is based on the provided struct field tag and the reflect.Value of the corresponding struct field. +// Supported types for encoding include string, integer types, boolean, floating-point types, pointers, interfaces, structs, maps, and slices. +// +// Parameters: +// - buf: A pointer to the bytes.Buffer where the encoded parameters will be written. +// - tag: The struct field tag specifying the key (identifier) for the AMI command parameter. +// - v: The reflect.Value representing the value to be encoded. +// +// Returns: +// - An error if there's an issue during encoding; otherwise, returns nil. +// +// Example: +// +// type ExampleStruct struct { +// Name string `ami:"Name"` +// Age int `ami:"Age,omitempty"` +// Active bool `ami:"Active"` +// } +// +// buf := &bytes.Buffer{} +// data := ExampleStruct{Name: "John", Age: 30, Active: true} +// err := Encode(buf, "ExampleAction", reflect.ValueOf(data)) +// // Encoded data in buf: +// // "ExampleAction: Name: John\r\nAge: 30\r\nActive: true\r\n" func Encode(buf *bytes.Buffer, tag string, v reflect.Value) error { switch v.Kind() { case reflect.String: @@ -214,6 +288,30 @@ func Encode(buf *bytes.Buffer, tag string, v reflect.Value) error { return nil } +// EncodeStruct writes the encoded Asterisk Manager Interface (AMI) command parameters for a struct to the provided bytes.Buffer. +// The encoding is based on the struct field tags and the reflect.Value of each struct field. +// Supported types for encoding include string, integer types, boolean, floating-point types, pointers, interfaces, structs, maps, and slices. +// +// Parameters: +// - buf: A pointer to the bytes.Buffer where the encoded parameters will be written. +// - v: The reflect.Value representing the struct whose fields are to be encoded. +// +// Returns: +// - An error if there's an issue during encoding; otherwise, returns nil. +// +// Example: +// +// type ExampleStruct struct { +// Name string `ami:"Name"` +// Age int `ami:"Age,omitempty"` +// Active bool `ami:"Active"` +// } +// +// buf := &bytes.Buffer{} +// data := ExampleStruct{Name: "John", Age: 30, Active: true} +// err := EncodeStruct(buf, reflect.ValueOf(data)) +// // Encoded data in buf: +// // "Name: John\r\nAge: 30\r\nActive: true\r\n" func EncodeStruct(buf *bytes.Buffer, v reflect.Value) error { var omitempty bool var err error @@ -242,6 +340,29 @@ func EncodeStruct(buf *bytes.Buffer, v reflect.Value) error { return nil } +// EncodeMap writes the encoded Asterisk Manager Interface (AMI) command parameters for a map to the provided bytes.Buffer. +// The encoding is based on the keys (as tags) and values of the provided reflect.Value representing the map. +// Supported types for encoding include string keys and values, integer types, boolean, floating-point types, pointers, interfaces, structs, maps, and slices. +// +// Parameters: +// - buf: A pointer to the bytes.Buffer where the encoded parameters will be written. +// - v: The reflect.Value representing the map whose keys and values are to be encoded. +// +// Returns: +// - An error if there's an issue during encoding; otherwise, returns nil. +// +// Example: +// +// data := map[string]interface{}{ +// "Name": "John", +// "Age": 30, +// "Active": true, +// } +// +// buf := &bytes.Buffer{} +// err := EncodeMap(buf, reflect.ValueOf(data)) +// // Encoded data in buf: +// // "Name: John\r\nAge: 30\r\nActive: true\r\n" func EncodeMap(buf *bytes.Buffer, v reflect.Value) error { for _, key := range v.MapKeys() { value := v.MapIndex(key) @@ -255,6 +376,28 @@ func EncodeMap(buf *bytes.Buffer, v reflect.Value) error { return nil } +// Marshal serializes the provided data structure into a slice of bytes based on the Asterisk Manager Interface (AMI) command parameters. +// The encoding is performed using reflection, and the resulting byte slice includes the encoded parameters followed by a newline signal. +// +// Parameters: +// - v: The data structure (e.g., struct, map) to be serialized. +// +// Returns: +// - A slice of bytes containing the serialized representation of the provided data structure. +// - An error if there's an issue during serialization; otherwise, returns nil. +// +// Example: +// +// type ExampleStruct struct { +// Name string `ami:"Name"` +// Age int `ami:"Age,omitempty"` +// Active bool `ami:"Active"` +// } +// +// data := ExampleStruct{Name: "John", Age: 30, Active: true} +// result, err := Marshal(data) +// // Serialized data in result: +// // "Name: John\r\nAge: 30\r\nActive: true\r\n" func Marshal(v interface{}) ([]byte, error) { var buf bytes.Buffer if err := Encode(&buf, "", reflect.ValueOf(v)); err != nil { @@ -264,7 +407,17 @@ func Marshal(v interface{}) ([]byte, error) { return buf.Bytes(), nil } -// GenUUID returns a new UUID based on /dev/urandom (unix). +// GenUUID generates a universally unique identifier (UUID) using a cryptographically secure random number source. +// The UUID is represented as a string with the format "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" where each 'x' is a hexadecimal digit. +// +// Returns: +// - A string representing the generated UUID. +// - An error if there's an issue during UUID generation; otherwise, returns nil. +// +// Example: +// +// uuid, err := GenUUID() +// // Example output: "3d96b8b9-4a84-4f69-9f9b-8f7ab9e6a96a" func GenUUID() (string, error) { file, err := os.Open("/dev/urandom") if err != nil { @@ -285,6 +438,17 @@ func GenUUID() (string, error) { return uuid, nil } +// GenUUIDShorten generates a universally unique identifier (UUID) using a cryptographically secure random number source. +// The UUID is represented as a string with the format "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" where each 'x' is a hexadecimal digit. +// +// Returns: +// - A string representing the generated UUID. +// - An error if there's an issue during UUID generation; otherwise, returns nil. +// +// Example: +// +// uuid, err := GenUUID() +// // Example output: "3d96b8b9-4a84-4f69-9f9b-8f7ab9e6a96a" func GenUUIDShorten() string { uuid, err := GenUUID() if err != nil { @@ -293,8 +457,21 @@ func GenUUIDShorten() string { return uuid } -// IsSuccess -// Check event from asterisk feedback to console is succeeded +// IsSuccess checks whether an Asterisk Manager Interface (AMI) command response indicates success. +// It evaluates the provided raw AMI result to determine if the response is non-empty, has a valid response key, +// and the response status is considered successful. +// +// Parameters: +// - raw: The AMIResultRaw representing the raw result of an AMI command response. +// +// Returns: +// - A boolean value indicating whether the response is successful. +// +// Example: +// +// raw := AMIResultRaw{"Response": "Success", "ActionID": "123", "Event": "SomeEvent"} +// success := IsSuccess(raw) +// // success is true if the response indicates success, otherwise false. func IsSuccess(raw AMIResultRaw) bool { if len(raw) == 0 { return false @@ -304,15 +481,38 @@ func IsSuccess(raw AMIResultRaw) bool { strings.EqualFold(response, config.AmiStatusSuccessKey) } -// IsFailure -// Check event from asterisk feedback to console is failure +// IsFailure checks whether an Asterisk Manager Interface (AMI) command response indicates failure. +// It evaluates the provided raw AMI result to determine if the response is non-empty and does not have a successful status. +// +// Parameters: +// - raw: The AMIResultRaw representing the raw result of an AMI command response. +// +// Returns: +// - A boolean value indicating whether the response is a failure. +// +// Example: +// +// raw := AMIResultRaw{"Response": "Error", "ActionID": "123", "Message": "Command failed"} +// failure := IsFailure(raw) +// // failure is true if the response indicates failure, otherwise false. func IsFailure(raw AMIResultRaw) bool { return !IsSuccess(raw) } -// IsEvent -// Check result from asterisk server to console is event? -// Get key `Event` and value of `Event` is not equal whitespace +// IsEvent checks whether an Asterisk Manager Interface (AMI) command response represents an event. +// It evaluates the provided raw AMI result to determine if the response is non-empty and contains a non-whitespace value for the 'Event' key. +// +// Parameters: +// - raw: The AMIResultRaw representing the raw result of an AMI command response. +// +// Returns: +// - A boolean value indicating whether the response is an event. +// +// Example: +// +// raw := AMIResultRaw{"Event": "SomeEvent", "ActionID": "123"} +// isEvent := IsEvent(raw) +// // isEvent is true if the response represents an event, otherwise false. func IsEvent(raw AMIResultRaw) bool { if len(raw) == 0 { return false @@ -429,7 +629,19 @@ func TransformKeyLevel(response AMIResultRawLevel, d *AMIDictionary) AMIResultRa return _m } -func IsPhoneNumber(phone string) bool { +// VerifyPhoneNoCustomize checks whether a given phone number string is in a valid format based on a customized regular expression pattern. +// +// Parameters: +// - phone: The phone number string to be verified. +// +// Returns: +// - A boolean value indicating whether the provided phone number is in a valid format. +// +// Example: +// +// isValid := VerifyPhoneNoCustomize("+1 (123) 456-7890") +// // isValid is true if the phone number is in a valid format, otherwise false. +func VerifyPhoneNoCustomize(phone string) bool { if IsStringEmpty(phone) { return false } @@ -437,7 +649,24 @@ func IsPhoneNumber(phone string) bool { return matcher.MatchString(phone) } -func IsPhoneNumberWith(phone string, region string) bool { +// VerifyPhoneNo checks whether a given phone number string is both a valid and possible phone number +// based on the specified region using the Google's libphonenumber library. +// Additionally, it verifies the phone number using a customized regular expression pattern. +// +// Parameters: +// - phone: The phone number string to be verified. +// - region: The ISO 3166-1 alpha-2 country code representing the region associated with the phone number. +// +// Returns: +// - A boolean value indicating whether the provided phone number is both valid and possible +// for the specified region and passes the additional customization check. +// +// Example: +// +// isValid := VerifyPhoneNo("+1 (123) 456-7890", "US") +// // isValid is true if the phone number is both valid and possible for the US region, +// // and it passes the additional customization check; otherwise, false. +func VerifyPhoneNo(phone string, region string) bool { if IsStringEmpty(phone) { return false } @@ -449,9 +678,22 @@ func IsPhoneNumberWith(phone string, region string) bool { } v := phonenumbers.IsValidNumber(p) l := phonenumbers.IsPossibleNumber(p) - return v && l && IsPhoneNumber(phone) -} - + return v && l && VerifyPhoneNoCustomize(phone) +} + +// RemoveStringPrefix removes specified prefixes from the beginning of a given string. +// +// Parameters: +// - str: The string from which prefixes should be removed. +// - prefix: One or more prefixes to be removed from the beginning of the string. +// +// Returns: +// - A string with the specified prefixes removed from the beginning. +// +// Example: +// +// result := RemoveStringPrefix("example-string", "example-", "prefix-") +// // result is "string" after removing both "example-" and "prefix-" prefixes. func RemoveStringPrefix(str string, prefix ...string) string { if IsStringEmpty(str) { return str @@ -465,13 +707,38 @@ func RemoveStringPrefix(str string, prefix ...string) string { return str } +// IsStringEmpty checks whether a given string is empty or consists only of whitespace characters. +// +// Parameters: +// - str: The string to be checked for emptiness. +// +// Returns: +// - A boolean value indicating whether the provided string is empty or consists only of whitespace characters. +// +// Example: +// +// isEmpty := IsStringEmpty(" ") +// // isEmpty is true if the string is empty or consists only of whitespace characters; otherwise, false. func IsStringEmpty(str string) bool { return len(str) == 0 || str == "" || strings.TrimSpace(str) == "" } -// ForkDictionaryFromLink -// Link must be provided to file formatted as json -// Return maps[string]string +// ForkDictionaryFromLink retrieves a dictionary (map[string]string) from a specified link +// where the content is formatted as JSON. The function performs a GET request to the provided link. +// +// Parameters: +// - link: The URL link to the JSON-formatted file containing the dictionary data. +// - debug: A boolean flag indicating whether to enable debugging for the HTTP requests. +// +// Returns: +// - A pointer to a map[string]string representing the dictionary retrieved from the provided link. +// - An error if the HTTP request or JSON decoding fails. +// +// Example: +// +// dict, err := ForkDictionaryFromLink("https://example.com/dictionary.json", true) +// // dict is a pointer to a map[string]string containing the dictionary data, +// // and err is an error indicating any issues during the retrieval process. func ForkDictionaryFromLink(link string, debug bool) (*map[string]string, error) { c := NewRestify(link) c.SetDebug(debug) @@ -645,50 +912,15 @@ func Base64Decode(encoded string) string { return string(d) } -var _json = jsonI.ConfigCompatibleWithStandardLibrary - func JsonString(data interface{}) string { s, ok := data.(string) if ok { return s } result, err := json.Marshal(data) - // result, err := MarshalToString(data) if err != nil { log.Printf(err.Error()) return "" } return string(result) } - -func JsonStringify(data interface{}) string { - s, ok := data.(string) - if ok { - return s - } - result, err := MarshalIndent(data, "", " ") - if err != nil { - return "" - } - return string(result) -} - -func MarshalToString(v interface{}) (string, error) { - return _json.MarshalToString(v) -} - -func MarshalJsonIterator(v interface{}) ([]byte, error) { - return _json.Marshal(v) -} - -func Unmarshal(data []byte, v interface{}) error { - return _json.Unmarshal(data, v) -} - -func UnmarshalFromString(str string, v interface{}) error { - return _json.UnmarshalFromString(str, v) -} - -func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { - return _json.MarshalIndent(v, prefix, indent) -} diff --git a/pkg/ami/ami_logger.go b/pkg/ami/ami_logger.go new file mode 100644 index 0000000..4021dbe --- /dev/null +++ b/pkg/ami/ami_logger.go @@ -0,0 +1,181 @@ +package ami + +import ( + "encoding/json" + "fmt" + "log" + "os" + "time" +) + +// LogLevel represents different log levels. +type LogLevel string + +// Log levels +const ( + Info LogLevel = "INFO" + Debug LogLevel = "DEBUG" + Warn LogLevel = "WARN" + Error LogLevel = "ERROR" + Fatal LogLevel = "FATAL" +) + +// Logger represents a JSON logger. +type Logger struct { + Level LogLevel `json:"level"` + IsEnabled bool `json:"enabled"` +} + +// LoggerEntry represents a log entry. +type LoggerEntry struct { + Timestamp time.Time `json:"timestamp,omitempty"` + Level LogLevel `json:"level,omitempty"` + Message string `json:"message,omitempty"` +} + +// NewLogger creates a new logger with the specified log level. +func NewLogger(level LogLevel) *Logger { + return &Logger{Level: level, IsEnabled: true} +} + +var i *Logger = nil +var w *Logger = nil +var e *Logger = nil +var d *Logger = nil +var f *Logger = nil + +func D() *Logger { + if d != nil { + return d + } + d = NewLogger(Debug) + return d +} + +func I() *Logger { + if i != nil { + return i + } + i = NewLogger(Info) + return i +} + +func W() *Logger { + if w != nil { + return w + } + w = NewLogger(Warn) + return w +} + +func E() *Logger { + if e != nil { + return e + } + e = NewLogger(Error) + return e +} + +func F() *Logger { + if f != nil { + return f + } + f = NewLogger(Fatal) + return f +} + +func (l *Logger) JsonString(data interface{}) string { + s, ok := data.(string) + if ok { + return s + } + result, err := json.Marshal(data) + if err != nil { + log.Printf(err.Error()) + return "" + } + return string(result) +} + +// log logs a message with the given level. +func (l *Logger) log(level LogLevel, format string, args ...interface{}) { + if !l.IsEnabled { + return + } + if l.Level <= level { + entry := LoggerEntry{ + Timestamp: time.Now(), + Level: level, + Message: fmt.Sprintf(format, args...), + } + v, err := json.Marshal(entry) + if err != nil { + fmt.Fprintf(os.Stderr, "Error marshaling log entry: %v\n", err) + return + } + fmt.Println(string(v)) + if level == Fatal { + os.Exit(1) // or using log.Fatalf + } + } +} + +// Info logs a message with info level. +func (l *Logger) Info(format string, args ...interface{}) { + l.log(Info, format, args...) +} + +// Debug logs a message with debug level. +func (l *Logger) Debug(format string, args ...interface{}) { + l.log(Debug, format, args...) +} + +// Warn logs a message with warn level. +func (l *Logger) Warn(format string, args ...interface{}) { + l.log(Warn, format, args...) +} + +// Error logs a message with error level. +func (l *Logger) Error(format string, args ...interface{}) { + l.log(Error, format, args...) +} + +// Fatal logs a message with fatal level and exits the application. +func (l *Logger) Fatal(format string, args ...interface{}) { + l.log(Fatal, format, args...) +} + +// Info logs a message with info level. +func (l *Logger) InfoR(format string, args ...interface{}) *Logger { + l.log(Info, format, args...) + return l +} + +// Debug logs a message with debug level. +func (l *Logger) DebugR(format string, args ...interface{}) *Logger { + l.log(Debug, format, args...) + return l +} + +// Warn logs a message with warn level. +func (l *Logger) WarnR(format string, args ...interface{}) *Logger { + l.log(Warn, format, args...) + return l +} + +// Error logs a message with error level. +func (l *Logger) ErrorR(format string, args ...interface{}) *Logger { + l.log(Error, format, args...) + return l +} + +// Fatal logs a message with fatal level and exits the application. +func (l *Logger) FatalR(format string, args ...interface{}) *Logger { + l.log(Fatal, format, args...) + return l +} + +func (l *Logger) SetEnabled(value bool) *Logger { + l.IsEnabled = value + return l +} diff --git a/pkg/ami/ami_model.go b/pkg/ami/ami_model.go index 68f2cba..4e3859e 100644 --- a/pkg/ami/ami_model.go +++ b/pkg/ami/ami_model.go @@ -329,12 +329,12 @@ type AMIPayloadDb struct { } type AMIOriginateDirection struct { - Telephone string `json:"telephone" binding:"required"` // like customer phone number or no. extension internal from all routes - ChannelProtocol string `json:"channel_protocol" binding:"required"` // protocols include: SIP, H323, IAX... - Extension int `json:"extension" binding:"required"` // like current user using no. extension - AllowDebug bool `json:"allow_debug"` // allow to trace log - Timeout int `json:"timeout"` // set timeout while calling - AllowSysValidator bool `json:"allow_sys_validator"` // allow validate the extension and channel + Telephone string `json:"telephone" binding:"required"` // like customer phone number or no. extension internal from all routes + ChannelProtocol string `json:"channel_protocol" binding:"required"` // protocols include: SIP, H323, IAX... + Extension int `json:"extension" binding:"required"` // like current user using no. extension + DebugMode bool `json:"debug_mode"` // allow to trace log + Timeout int `json:"timeout"` // set timeout while calling + ExtensionExists bool `json:"extension_exists"` // allow validate the extension and channel } type AMIExtensionStatesConf struct { @@ -431,16 +431,13 @@ type AMICdr struct { UserField string `json:"user_field,omitempty"` DateReceivedAt time.Time `json:"date_received_at"` Privilege string `json:"privilege,omitempty"` - // Type of call direction - // `inbound` - // `outbound` - Direction string `json:"direction"` - FlowCall string `json:"flow_call"` - TypeDirection string `json:"type_direction"` - UserExtension string `json:"user_extension,omitempty"` - PhoneNumber string `json:"phone_number,omitempty"` - PlaybackUrl string `json:"playback_url,omitempty"` // the only cdr has status answered - ExtenSplitterSymbol string `json:"-"` // default exten splitter symbol: -, example: SIP/1000-00098fec then split by - + Direction string `json:"direction"` // inbound, outbound + Desc string `json:"desc"` + Type string `json:"type"` + UserExtension string `json:"user_extension,omitempty"` + PhoneNumber string `json:"phone_number,omitempty"` + PlaybackUrl string `json:"playback_url,omitempty"` // the only cdr has status answered + symbol string `json:"-"` // default extension splitter symbol: -, example: SIP/1000-00098fec then split by - } type AMIPayloadChanspy struct { @@ -448,5 +445,5 @@ type AMIPayloadChanspy struct { SourceExten string `json:"source_extension" binding:"required"` CurrentExten string `json:"current_extension" binding:"required"` ChannelProtocol string `json:"channel_protocol" binding:"required"` // protocols include: SIP, H323, IAX... - AllowDebug bool `json:"allow_debug"` // allow to trace log + DebugMode bool `json:"debug_mode"` // allow to trace log } diff --git a/pkg/ami/ami_originate.go b/pkg/ami/ami_originate.go index 8ff6595..bf228a6 100644 --- a/pkg/ami/ami_originate.go +++ b/pkg/ami/ami_originate.go @@ -16,7 +16,7 @@ func NewAMIPayloadOriginate() *AMIPayloadOriginate { func NewAMIOriginateDirection() *AMIOriginateDirection { o := &AMIOriginateDirection{} - o.SetAllowSysValidator(true) + o.SetExtensionExists(true) return o } @@ -150,8 +150,8 @@ func (o *AMIOriginateDirection) SetExtension(value int) *AMIOriginateDirection { return o } -func (o *AMIOriginateDirection) SetAllowDebug(value bool) *AMIOriginateDirection { - o.AllowDebug = value +func (o *AMIOriginateDirection) SetDebugMode(value bool) *AMIOriginateDirection { + o.DebugMode = value return o } @@ -162,8 +162,8 @@ func (o *AMIOriginateDirection) SetTimeout(value int) *AMIOriginateDirection { return o } -func (o *AMIOriginateDirection) SetAllowSysValidator(value bool) *AMIOriginateDirection { - o.AllowSysValidator = value +func (o *AMIOriginateDirection) SetExtensionExists(value bool) *AMIOriginateDirection { + o.ExtensionExists = value return o } @@ -200,7 +200,7 @@ func MakeOutboundCall(ctx context.Context, s AMISocket, d AMIOriginateDirection) o.SetTimeout(30000) // as default } - if d.AllowSysValidator { + if d.ExtensionExists { peer, err := SIPPeerStatusShort(ctx, s, fmt.Sprintf("%v", d.Extension)) if err != nil { return nil, false, err @@ -211,7 +211,7 @@ func MakeOutboundCall(ctx context.Context, s AMISocket, d AMIOriginateDirection) o.SetChannel(peer.GetVal(config.AmiJsonFieldPeer)) } - if d.AllowDebug { + if d.DebugMode { log.Printf("MakeOutboundCall, an outgoing call with originate request body = %v", o.Json()) log.Printf("MakeOutboundCall, an outgoing call with original request body (setter) = %v", d.Json()) } @@ -244,7 +244,7 @@ func MakeInternalCall(ctx context.Context, s AMISocket, d AMIOriginateDirection) o.SetTimeout(30000) // as default } - if d.AllowSysValidator { + if d.ExtensionExists { peer, err := SIPPeerStatusShort(ctx, s, fmt.Sprintf("%v", d.Extension)) if err != nil { return nil, false, err @@ -255,7 +255,7 @@ func MakeInternalCall(ctx context.Context, s AMISocket, d AMIOriginateDirection) o.SetChannel(peer.GetVal(config.AmiJsonFieldPeer)) } - if d.AllowDebug { + if d.DebugMode { log.Printf("MakeInternalCall, an internal call with originate request body = %v", o.Json()) log.Printf("MakeInternalCall, an internal call with original request body (setter) = %v", d.Json()) }