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

153 lines
5.8 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 (
ErrCurrentCalleeRejected = errors.New("callee is not on the approved outbound whitelist")
ErrCurrentRuleWait = errors.New("task rules are temporarily not satisfied")
ErrCurrentRuleInvalid = errors.New("task execution rules are invalid or unknown")
)
type CurrentSelectedTrunk struct {
TrunkID string
CallerID string
Callee string
DialedCallee string
Deadline time.Time
MaxCallDurationMS int64
}
type currentTrunk 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"`
CallerProfiles []currentCallerProfile `json:"caller_profiles"`
Schedule callwindow.WeeklySchedule `json:"schedule"`
}
type currentCallerProfile struct {
ID string `json:"caller_profile_id"`
CallerID string `json:"caller_id"`
}
var currentCalleeWhitelist = map[string]struct{}{"15003164745": {}, "15830461047": {}}
// SelectCurrentTrunk 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 SelectCurrentTrunk(snapshot configread.CurrentSnapshot, callee string, at time.Time, trunkOccupancy, loadedRevisions map[string]int64) (CurrentSelectedTrunk, error) {
if _, allowed := currentCalleeWhitelist[callee]; !allowed {
return CurrentSelectedTrunk{}, ErrCurrentCalleeRejected
}
if snapshot.Task.MaxCallDurationMS <= 0 || snapshot.Task.MaxCallDurationMS > math.MaxInt64/int64(time.Millisecond) {
return CurrentSelectedTrunk{}, fmt.Errorf("%w: invalid task call duration", ErrCurrentRuleInvalid)
}
var taskSchedule callwindow.TaskSchedule
if err := json.Unmarshal(snapshot.Task.Schedule, &taskSchedule); err != nil {
return CurrentSelectedTrunk{}, fmt.Errorf("%w: task schedule: %v", ErrCurrentRuleInvalid, err)
}
var trunks []currentTrunk
if err := json.Unmarshal(snapshot.SIP.Trunks, &trunks); err != nil {
return CurrentSelectedTrunk{}, fmt.Errorf("%w: SIP trunks: %v", ErrCurrentRuleInvalid, err)
}
byID := make(map[string]currentTrunk, len(trunks))
for _, trunk := range trunks {
if trunk.TrunkID == "" {
return CurrentSelectedTrunk{}, fmt.Errorf("%w: trunk identity missing", ErrCurrentRuleInvalid)
}
if _, exists := byID[trunk.TrunkID]; exists {
return CurrentSelectedTrunk{}, fmt.Errorf("%w: duplicate trunk identity", ErrCurrentRuleInvalid)
}
byID[trunk.TrunkID] = trunk
}
if len(snapshot.Task.AllowedTrunkIDs) == 0 {
return CurrentSelectedTrunk{}, fmt.Errorf("%w: task has no allowed trunks", ErrCurrentRuleInvalid)
}
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 CurrentSelectedTrunk{}, fmt.Errorf("%w: AI call duration: %v", ErrCurrentRuleInvalid, err)
}
if ai.Conversation.MaxDurationMS != nil {
if *ai.Conversation.MaxDurationMS <= 0 {
return CurrentSelectedTrunk{}, fmt.Errorf("%w: AI call duration must be positive", ErrCurrentRuleInvalid)
}
if *ai.Conversation.MaxDurationMS < maxDurationMS {
maxDurationMS = *ai.Conversation.MaxDurationMS
}
}
}
if maxDurationMS > math.MaxInt64/int64(time.Millisecond) {
return CurrentSelectedTrunk{}, fmt.Errorf("%w: call duration exceeds time range", ErrCurrentRuleInvalid)
}
var invalid error
for _, id := range snapshot.Task.AllowedTrunkIDs {
trunk, found := byID[id]
if !found {
invalid = fmt.Errorf("%w: allowed trunk %q is missing", ErrCurrentRuleInvalid, 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", ErrCurrentRuleInvalid, id)
continue
}
var callerID string
for _, profile := range trunk.CallerProfiles {
if profile.ID == snapshot.Task.CallerProfileID {
callerID = profile.CallerID
break
}
}
if callerID == "" {
invalid = fmt.Errorf("%w: caller profile is missing on trunk %q", ErrCurrentRuleInvalid, 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", ErrCurrentRuleInvalid, id, err)
continue
}
if !trunk.Enabled || 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 CurrentSelectedTrunk{
TrunkID: id, CallerID: callerID, Callee: callee,
DialedCallee: trunk.DialPrefix + callee, Deadline: deadline,
MaxCallDurationMS: maxDurationMS,
}, nil
}
if invalid != nil {
return CurrentSelectedTrunk{}, invalid
}
return CurrentSelectedTrunk{}, ErrCurrentRuleWait
}