167 lines
6.1 KiB
Go
167 lines
6.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
agentv1 "git.ipao.vip/rogee/go-sip/gen/agent/v1"
|
|
"git.ipao.vip/rogee/go-sip/internal/agent"
|
|
"git.ipao.vip/rogee/go-sip/internal/callruntime"
|
|
"git.ipao.vip/rogee/go-sip/internal/config"
|
|
"git.ipao.vip/rogee/go-sip/internal/rpc"
|
|
)
|
|
|
|
func uploadCallRecordings(ctx context.Context, cfg config.Config, result callruntime.Result) ([]map[string]any, error) {
|
|
if strings.TrimSpace(cfg.DispatcherGRPCEndpoint) == "" {
|
|
return nil, nil
|
|
}
|
|
for name, value := range map[string]string{
|
|
"AGENT_CALL_TENANT_ID": cfg.CallTenantID,
|
|
"AGENT_CALL_TENANT_KEY": cfg.CallTenantKey,
|
|
"AGENT_CALL_TASK_ID": cfg.CallTaskID,
|
|
"AGENT_CALL_TASK_ITEM_ID": cfg.CallTaskItemID,
|
|
} {
|
|
if strings.TrimSpace(value) == "" {
|
|
return nil, fmt.Errorf("%s is required when Dispatcher OSS upload is enabled", name)
|
|
}
|
|
}
|
|
if result.ChannelID == "" {
|
|
return nil, errors.New("call result has no channel ID for upload binding")
|
|
}
|
|
executionID := cfg.CallExecutionID
|
|
if executionID == "" {
|
|
executionID = result.ChannelID
|
|
}
|
|
binding := &agentv1.ExecutionBinding{
|
|
TenantId: cfg.CallTenantID,
|
|
TenantKey: cfg.CallTenantKey,
|
|
ExecutionId: executionID,
|
|
TaskId: cfg.CallTaskID,
|
|
TaskItemId: cfg.CallTaskItemID,
|
|
TaskRevision: 1,
|
|
CallId: result.ChannelID,
|
|
AttemptId: result.ChannelID,
|
|
AgentVersionId: cfg.Version,
|
|
}
|
|
client, err := rpc.DialFromFiles(cfg.DispatcherGRPCEndpoint, cfg.MTLSCAFile, cfg.MTLSCertFile, cfg.MTLSKeyFile, cfg.DispatcherGRPCServerName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dial Dispatcher gRPC service: %w", err)
|
|
}
|
|
defer client.Close()
|
|
uploader := agent.UploadClient{Now: time.Now}
|
|
facts := append(append([]callruntime.RecordingFact(nil), result.InboundRecordings...), result.OutboundRecordings...)
|
|
if len(facts) == 0 {
|
|
return nil, errors.New("call produced no recordings for OSS upload")
|
|
}
|
|
uploaded := make([]map[string]any, 0, len(facts))
|
|
for index, recording := range facts {
|
|
if recording.Path == "" || recording.Bytes <= 0 || recording.SHA256 == "" {
|
|
return nil, fmt.Errorf("recording %d is missing path, size or checksum", index+1)
|
|
}
|
|
assetID := recordingAssetID(recording.Segment, recording.Path, index+1)
|
|
asset := &agentv1.AssetDescriptor{
|
|
Kind: agentv1.AssetKind_ASSET_KIND_RECORDING,
|
|
AssetId: assetID,
|
|
CallId: result.ChannelID,
|
|
ExecutionId: executionID,
|
|
Format: "wav",
|
|
SizeBytes: int64(recording.Bytes),
|
|
ChecksumSha256: recording.SHA256,
|
|
Channels: 1,
|
|
SampleRateHz: 16000,
|
|
}
|
|
uploadID := stableUploadID(binding, asset)
|
|
meta := uploadMeta(cfg, "request", uploadID)
|
|
grantResponse, err := client.RequestUpload(ctx, &agentv1.RequestUploadRequest{Meta: meta, Binding: binding, Asset: asset, UploadId: uploadID})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("request upload %s: %w", uploadID, err)
|
|
}
|
|
if grantResponse == nil || grantResponse.Grant == nil || grantResponse.Receipt == nil || grantResponse.Receipt.Result != agentv1.ResultCode_RESULT_CODE_ACCEPTED {
|
|
return nil, fmt.Errorf("request upload %s was rejected", uploadID)
|
|
}
|
|
parsed, err := url.Parse(grantResponse.Grant.TargetUrl)
|
|
if err != nil || parsed.Host == "" {
|
|
return nil, fmt.Errorf("upload %s returned invalid target URL", uploadID)
|
|
}
|
|
uploader.AllowedHosts = map[string]struct{}{strings.ToLower(parsed.Host): {}}
|
|
uploadResult, err := uploader.UploadFile(ctx, grantResponse.Grant, recording.Path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("upload %s data plane: %w", uploadID, err)
|
|
}
|
|
if uploadResult.SizeBytes != asset.SizeBytes || !strings.EqualFold(uploadResult.SHA256, asset.ChecksumSha256) {
|
|
return nil, fmt.Errorf("upload %s local result does not match asset", uploadID)
|
|
}
|
|
completeResponse, err := client.CompleteUpload(ctx, &agentv1.CompleteUploadRequest{
|
|
Meta: uploadMeta(cfg, "complete", uploadID),
|
|
Binding: binding,
|
|
Asset: asset,
|
|
UploadId: uploadID,
|
|
UploadedSizeBytes: uploadResult.SizeBytes,
|
|
UploadedChecksumSha256: uploadResult.SHA256,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("complete upload %s: %w", uploadID, err)
|
|
}
|
|
if completeResponse == nil || completeResponse.Receipt == nil || completeResponse.Receipt.Result != agentv1.ResultCode_RESULT_CODE_ACCEPTED || completeResponse.OssId == "" {
|
|
return nil, fmt.Errorf("complete upload %s was not verified", uploadID)
|
|
}
|
|
uploaded = append(uploaded, map[string]any{
|
|
"upload_id": uploadID, "asset_id": asset.AssetId, "object_key": grantResponse.Grant.ObjectKey,
|
|
"oss_id": completeResponse.OssId, "bytes": uploadResult.SizeBytes, "sha256": uploadResult.SHA256,
|
|
"status_code": uploadResult.StatusCode,
|
|
})
|
|
}
|
|
return uploaded, nil
|
|
}
|
|
|
|
func recordingAssetID(segment, path string, index int) string {
|
|
raw := segment
|
|
if raw == "" {
|
|
raw = fmt.Sprintf("segment-%d", index)
|
|
}
|
|
raw += "-" + filepath.Base(path)
|
|
var builder strings.Builder
|
|
for _, r := range raw {
|
|
if r == '/' || r == '\\' || r == ' ' || r == '\t' || r == '\n' || r == '\r' {
|
|
builder.WriteByte('-')
|
|
continue
|
|
}
|
|
builder.WriteRune(r)
|
|
}
|
|
assetID := strings.Trim(builder.String(), "-")
|
|
if assetID == "" {
|
|
assetID = fmt.Sprintf("segment-%d", index)
|
|
}
|
|
if len([]byte(assetID)) > 120 {
|
|
digest := sha256.Sum256([]byte(assetID))
|
|
assetID = "recording-" + hex.EncodeToString(digest[:16])
|
|
}
|
|
return assetID
|
|
}
|
|
|
|
func stableUploadID(binding *agentv1.ExecutionBinding, asset *agentv1.AssetDescriptor) string {
|
|
value := binding.ExecutionId + "\x00" + asset.AssetId + "\x00" + asset.ChecksumSha256
|
|
digest := sha256.Sum256([]byte(value))
|
|
return "upload-" + hex.EncodeToString(digest[:16])
|
|
}
|
|
|
|
func uploadMeta(cfg config.Config, phase, uploadID string) *agentv1.RequestMeta {
|
|
operationID := "recording-" + phase + "-" + uploadID
|
|
return &agentv1.RequestMeta{
|
|
ProtocolVersion: "agent.v1",
|
|
RequestId: operationID,
|
|
TraceId: operationID,
|
|
OperationId: operationID,
|
|
IdempotencyKey: operationID,
|
|
AgentId: cfg.AgentID,
|
|
CellId: cfg.CellID,
|
|
}
|
|
}
|