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}$`) var mockCallee = regexp.MustCompile(`^[0-9]{1,32}$`) // 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) { if !mockCallID.MatchString(eventID) || !mockCallID.MatchString(taskID) || !mockCallee.MatchString(callee) { return "", nil, errors.New("one-shot command identity or numeric callee 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 // The one-shot host capture arm is bound to one exact trunk before the MQ event is sent. if err := json.Unmarshal(body, &task); err != nil || task.DispatcherID != data.dispatcherID || task.TenantID != data.tenantID || task.TaskID != taskID || task.Status != "running" || len(task.AllowedTrunks) != 1 || task.AllowedTrunks[0].Concurrency == 0 { 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 } } }