Files
go-sip/internal/dispatcher/policy.go
T

155 lines
5.6 KiB
Go

package dispatcher
import (
"encoding/json"
"errors"
"fmt"
"math"
"time"
"git.ipao.vip/rogee/go-sip/internal/callwindow"
"git.ipao.vip/rogee/go-sip/internal/configread"
)
var (
ErrCalleeRejected = errors.New("SaaS event callee is not a valid original numeric dial route")
ErrRuleWait = errors.New("task rules are temporarily not satisfied")
ErrRuleInvalid = errors.New("task execution rules are invalid or unknown")
)
type SelectedTrunk struct {
TrunkID string
CallerID string
Callee string
DialedCallee string
Deadline time.Time
MaxCallDurationMS int64
}
type trunkConfig struct {
TrunkID string `json:"trunk_id"`
Codec string `json:"codec"`
DialPrefix string `json:"dial_prefix"`
Enabled bool `json:"enabled"`
Transport *string `json:"transport"`
AuthMode *string `json:"auth_mode"`
RegistrationRequired *bool `json:"registration_required"`
MaxConcurrentCalls *int64 `json:"max_concurrent_calls"`
CallerID string `json:"caller_id"`
Schedule callwindow.WeeklySchedule `json:"schedule"`
}
// validRawCallee checks only what the native dial route can safely represent;
// the SaaS call.execute event, not a local list, chooses the business number.
func validRawCallee(callee string) bool {
if len(callee) == 0 || len(callee) > 32 {
return false
}
for i := 0; i < len(callee); i++ {
if callee[i] < '0' || callee[i] > '9' {
return false
}
}
return true
}
// SelectTrunk makes one ordered choice before originate. Its answer is
// frozen with the accepted command; callers never silently reselect on an
// unknown execution or after a failed originate.
func SelectTrunk(snapshot configread.Snapshot, callee string, at time.Time, trunkOccupancy, taskTrunkOccupancy, loadedRevisions map[string]int64) (SelectedTrunk, error) {
if !validRawCallee(callee) {
return SelectedTrunk{}, ErrCalleeRejected
}
if snapshot.Task.MaxCallDurationMS <= 0 || snapshot.Task.MaxCallDurationMS > math.MaxInt64/int64(time.Millisecond) {
return SelectedTrunk{}, fmt.Errorf("%w: invalid task call duration", ErrRuleInvalid)
}
var taskSchedule callwindow.TaskSchedule
if err := json.Unmarshal(snapshot.Task.Schedule, &taskSchedule); err != nil {
return SelectedTrunk{}, fmt.Errorf("%w: task schedule: %v", ErrRuleInvalid, err)
}
var trunks []trunkConfig
if err := json.Unmarshal(snapshot.SIP.Trunks, &trunks); err != nil {
return SelectedTrunk{}, fmt.Errorf("%w: SIP trunks: %v", ErrRuleInvalid, err)
}
byID := make(map[string]trunkConfig, len(trunks))
for _, trunk := range trunks {
if trunk.TrunkID == "" {
return SelectedTrunk{}, fmt.Errorf("%w: trunk identity missing", ErrRuleInvalid)
}
if _, exists := byID[trunk.TrunkID]; exists {
return SelectedTrunk{}, fmt.Errorf("%w: duplicate trunk identity", ErrRuleInvalid)
}
byID[trunk.TrunkID] = trunk
}
limits, err := snapshot.Task.TrunkLimits()
if err != nil {
return SelectedTrunk{}, fmt.Errorf("%w: %v", ErrRuleInvalid, err)
}
maxDurationMS := snapshot.Task.MaxCallDurationMS
if snapshot.Task.Agent.Mode == "full_ai" {
var ai struct {
Conversation struct {
MaxDurationMS *int64 `json:"max_duration_ms"`
} `json:"conversation"`
}
if err := json.Unmarshal(snapshot.Task.Agent.Raw, &ai); err != nil {
return SelectedTrunk{}, fmt.Errorf("%w: AI call duration: %v", ErrRuleInvalid, err)
}
if ai.Conversation.MaxDurationMS != nil {
if *ai.Conversation.MaxDurationMS <= 0 {
return SelectedTrunk{}, fmt.Errorf("%w: AI call duration must be positive", ErrRuleInvalid)
}
if *ai.Conversation.MaxDurationMS < maxDurationMS {
maxDurationMS = *ai.Conversation.MaxDurationMS
}
}
}
if maxDurationMS > math.MaxInt64/int64(time.Millisecond) {
return SelectedTrunk{}, fmt.Errorf("%w: call duration exceeds time range", ErrRuleInvalid)
}
var invalid error
for _, allowed := range snapshot.Task.AllowedTrunks {
id := allowed.TrunkID
trunk, found := byID[id]
if !found {
invalid = fmt.Errorf("%w: allowed trunk %q is missing", ErrRuleInvalid, id)
continue
}
if trunk.Transport == nil || *trunk.Transport == "" || trunk.AuthMode == nil || *trunk.AuthMode == "" || trunk.RegistrationRequired == nil || trunk.MaxConcurrentCalls == nil || *trunk.MaxConcurrentCalls <= 0 || trunk.Codec != "PCMA" {
invalid = fmt.Errorf("%w: SIP trunk %q has unknown required execution fields", ErrRuleInvalid, id)
continue
}
if trunk.CallerID == "" {
invalid = fmt.Errorf("%w: caller ID is missing on trunk %q", ErrRuleInvalid, id)
continue
}
end, err := callwindow.Evaluate(taskSchedule, trunk.Schedule, at)
if err != nil {
if errors.Is(err, callwindow.ErrWindowClosed) {
continue
}
invalid = fmt.Errorf("%w: schedule of trunk %q: %v", ErrRuleInvalid, id, err)
continue
}
if !trunk.Enabled || limits[id] == 0 || taskTrunkOccupancy[id] >= limits[id] || loadedRevisions[id] != snapshot.SIP.Revision || trunkOccupancy[id] >= *trunk.MaxConcurrentCalls {
continue
}
deadline := at.Add(time.Duration(maxDurationMS) * time.Millisecond)
if end.Before(deadline) {
deadline = end
}
if !deadline.After(at) {
continue
}
return SelectedTrunk{
TrunkID: id, CallerID: trunk.CallerID, Callee: callee,
DialedCallee: trunk.DialPrefix + callee, Deadline: deadline,
MaxCallDurationMS: maxDurationMS,
}, nil
}
if invalid != nil {
return SelectedTrunk{}, invalid
}
return SelectedTrunk{}, ErrRuleWait
}