107 lines
4.1 KiB
Go
107 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"time"
|
|
|
|
"git.ipao.vip/rogee/go-sip/internal/configread"
|
|
"git.ipao.vip/rogee/go-sip/internal/contract"
|
|
"git.ipao.vip/rogee/go-sip/internal/mq"
|
|
amqp "github.com/rabbitmq/amqp091-go"
|
|
)
|
|
|
|
var mockCallID = regexp.MustCompile(`^[A-Za-z0-9._-]{1,100}$`)
|
|
|
|
// buildExecute deliberately carries only the two approved call inputs. Trunk,
|
|
// caller, AI and duration remain immutable properties of the SaaS task read.
|
|
func buildExecute(data dataset, eventID, taskID, callee string, now time.Time) (string, []byte, error) {
|
|
shanghai, err := time.LoadLocation("Asia/Shanghai")
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
hour := now.In(shanghai).Hour()
|
|
if !mockCallID.MatchString(eventID) || !mockCallID.MatchString(taskID) ||
|
|
(callee != "15003164745" && callee != "15830461047") || hour < 9 || hour >= 20 {
|
|
return "", nil, errors.New("one-shot command identity, whitelist or real call window rejected")
|
|
}
|
|
body, ok := data.tasks[taskID]
|
|
if !ok {
|
|
return "", nil, errors.New("one-shot command task is absent from the approved SaaS dataset")
|
|
}
|
|
var task configread.Task
|
|
if err := json.Unmarshal(body, &task); err != nil || task.DispatcherID != data.dispatcherID || task.TenantID != data.tenantID || task.TaskID != taskID || task.Status != "running" || len(task.AllowedTrunkIDs) != 1 {
|
|
return "", nil, errors.New("one-shot command requires a running task pinned to exactly one approved trunk")
|
|
}
|
|
command, err := json.Marshal(struct {
|
|
EventID string `json:"event_id"`
|
|
EventType string `json:"event_type"`
|
|
DispatcherID string `json:"dispatcher_id"`
|
|
TenantID int64 `json:"tenant_id"`
|
|
IssuedAt string `json:"issued_at"`
|
|
Payload struct {
|
|
TaskID string `json:"task_id"`
|
|
Callee string `json:"callee"`
|
|
} `json:"payload"`
|
|
}{EventID: eventID, EventType: "call.execute", DispatcherID: data.dispatcherID, TenantID: data.tenantID, IssuedAt: now.UTC().Format(time.RFC3339Nano), Payload: struct {
|
|
TaskID string `json:"task_id"`
|
|
Callee string `json:"callee"`
|
|
}{TaskID: taskID, Callee: callee}})
|
|
if err != nil || contract.ValidateCurrent("mq", command) != nil {
|
|
return "", nil, errors.New("one-shot command violates current MQ contract")
|
|
}
|
|
return "d." + data.dispatcherID + ".task." + taskID + ".in", command, nil
|
|
}
|
|
|
|
// publishExecute publishes exactly once with mandatory routing and publisher
|
|
// confirmation. An ambiguous response is NOT retried or called SaaS receipt.
|
|
func publishExecute(ctx context.Context, brokerURL string, data dataset, eventID, taskID, callee string) error {
|
|
return publishExecuteAt(ctx, brokerURL, data, eventID, taskID, callee, time.Now())
|
|
}
|
|
|
|
func publishExecuteAt(ctx context.Context, brokerURL string, data dataset, eventID, taskID, callee string, now time.Time) error {
|
|
if err := requireDedicatedVhost(brokerURL); err != nil {
|
|
return err
|
|
}
|
|
routingKey, body, err := buildExecute(data, eventID, taskID, callee, now)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
conn, err := amqp.Dial(brokerURL)
|
|
if err != nil {
|
|
return fmt.Errorf("connect one-shot test broker: %T", err)
|
|
}
|
|
defer conn.Close()
|
|
ch, err := conn.Channel()
|
|
if err != nil {
|
|
return fmt.Errorf("open one-shot publish channel: %T", err)
|
|
}
|
|
defer ch.Close()
|
|
if err := ch.Confirm(false); err != nil {
|
|
return fmt.Errorf("enable one-shot publisher confirmation: %T", err)
|
|
}
|
|
returned := ch.NotifyReturn(make(chan amqp.Return, 1))
|
|
confirmed := ch.NotifyPublish(make(chan amqp.Confirmation, 1))
|
|
if err := ch.PublishWithContext(ctx, mq.CommandsExchange, routingKey, true, false,
|
|
amqp.Publishing{ContentType: "application/json", DeliveryMode: amqp.Persistent, Body: body}); err != nil {
|
|
return fmt.Errorf("one-shot publish outcome unknown: %T", err)
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return errors.New("one-shot publish confirmation unknown; do not republish")
|
|
case confirmation, ok := <-confirmed:
|
|
if !ok || !confirmation.Ack {
|
|
return errors.New("one-shot command was not publisher-confirmed; do not republish")
|
|
}
|
|
select {
|
|
case <-returned:
|
|
return errors.New("one-shot command was unroutable")
|
|
default:
|
|
return nil // broker accepted the command, NOT SaaS application receipt
|
|
}
|
|
}
|
|
}
|