package agent import ( "bytes" "context" "errors" "fmt" agentpb "git.ipao.vip/rogee/go-sip/gen/agent" "google.golang.org/protobuf/proto" ) var ErrRecordingClientUnavailable = errors.New("Agent recording delivery requires an active Dispatcher session and Unary client") // RecordingClient belongs to one approved call. It requests upload tokens only // when its caller explicitly asks, and it never retries an OSS PUT or invents a // successful call end or result after an RPC error. type RecordingClient struct { Client agentpb.AgentControlServiceClient Session func(context.Context) (*agentpb.RequestMeta, error) DispatcherID string TenantID int64 SourceEventID string } func (c RecordingClient) requestMeta(ctx context.Context, action string) (*agentpb.RequestMeta, error) { if c.Client == nil || c.Session == nil || c.DispatcherID == "" || c.TenantID <= 0 || c.SourceEventID == "" { return nil, ErrRecordingClientUnavailable } if err := ctx.Err(); err != nil { return nil, err } meta, err := c.Session(ctx) if err != nil { return nil, fmt.Errorf("obtain current Agent session: %w", err) } if meta == nil || meta.GetAgentId() == "" || meta.GetCellId() == "" || meta.GetBootId() == "" || meta.GetDispatcherEpoch() == "" || meta.GetSessionGeneration() == 0 { return nil, ErrRecordingClientUnavailable } copy := proto.Clone(meta).(*agentpb.RequestMeta) copy.OperationId = c.SourceEventID + "/" + action copy.IdempotencyKey = copy.OperationId return copy, nil } func (c RecordingClient) RequestUpload(ctx context.Context, asset *agentpb.AssetDescriptor, uploadID string) (*agentpb.UploadGrant, error) { meta, err := c.requestMeta(ctx, "upload/"+uploadID) if err != nil { return nil, err } if asset == nil || uploadID == "" || asset.GetKind() != agentpb.AssetKind_ASSET_KIND_RECORDING || asset.GetExecutionId() != c.SourceEventID || asset.GetCallId() != c.SourceEventID || asset.GetAssetId() == "" || asset.GetSizeBytes() <= 0 || asset.GetChecksumSha256() == "" { return nil, errors.New("recording token request does not describe the approved call and original asset") } response, err := c.Client.RequestRecordingUpload(ctx, &agentpb.RequestRecordingUploadRequest{ Meta: meta, DispatcherId: c.DispatcherID, TenantId: c.TenantID, SourceEventId: c.SourceEventID, UploadId: uploadID, Asset: proto.Clone(asset).(*agentpb.AssetDescriptor), }) if err != nil { return nil, fmt.Errorf("request original recording upload token: %w", err) } grant := response.GetGrant() if grant == nil || grant.GetUploadId() != uploadID || grant.GetBucket() == "" || grant.GetObjectKey() == "" || grant.GetMaxBytes() != asset.GetSizeBytes() || grant.GetRequiredChecksumSha256() != asset.GetChecksumSha256() { return nil, fmt.Errorf("%w: Dispatcher returned a different recording asset", ErrUploadGrantInvalid) } return grant, nil } func (c RecordingClient) ReportEnded(ctx context.Context) error { meta, err := c.requestMeta(ctx, "ended") if err != nil { return err } response, err := c.Client.ReportCallEnded(ctx, &agentpb.ReportCallEndedRequest{ Meta: meta, DispatcherId: c.DispatcherID, TenantId: c.TenantID, SourceEventId: c.SourceEventID, }) if err != nil { return fmt.Errorf("report confirmed call end: %w", err) } if receipt := response.GetReceipt(); receipt.GetResult() != agentpb.ResultCode_RESULT_CODE_APPLIED || receipt.GetFactId() != c.SourceEventID { return errors.New("Dispatcher did not persist the confirmed call end") } return nil } func (c RecordingClient) ReportFinal(ctx context.Context, payload []byte, upload *agentpb.UploadObservation) (string, error) { meta, err := c.requestMeta(ctx, "result") if err != nil { return "", err } if len(payload) == 0 { return "", errors.New("final call result payload is required") } request := &agentpb.ReportCallResultRequest{ Meta: meta, DispatcherId: c.DispatcherID, TenantId: c.TenantID, SourceEventId: c.SourceEventID, ResultPayloadJson: bytes.Clone(payload), } if upload != nil { request.Upload = proto.Clone(upload).(*agentpb.UploadObservation) } response, err := c.Client.ReportCallResult(ctx, request) if err != nil { return "", fmt.Errorf("persist unique call result: %w", err) } if receipt := response.GetReceipt(); receipt.GetResult() == agentpb.ResultCode_RESULT_CODE_ACCEPTED && receipt.GetFactId() != "" { return receipt.GetFactId(), nil } return "", errors.New("Dispatcher did not persist the unique call result") }