153 lines
5.8 KiB
Go
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
|
|
}
|