Files
go-sip/deploys/test/saas-mock/publish.go
T

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
}
}
}