Files

117 lines
3.9 KiB
Go
Raw Permalink Normal View History

package callmemory
import (
"context"
"data-terminal-cloud/internal/identity"
"encoding/base64"
"time"
"github.com/jackc/pgx/v5"
)
type CallRecordsInput struct {
ElderID string `json:"elder_id"`
SessionID string `json:"business_session_id,omitempty"`
Cursor string `json:"cursor,omitempty"`
Limit int `json:"limit,omitempty"`
}
type recordCursor struct {
Time time.Time `json:"time"`
ID string `json:"id"`
}
func (s *Service) callRecords(ctx context.Context, tx pgx.Tx, p identity.Principal, op string, raw []byte) (any, error) {
var in CallRecordsInput
if e := StrictJSON(raw, &in); e != nil {
return nil, e
}
if in.Limit == 0 {
in.Limit = 20
}
if in.Limit < 1 || in.Limit > 100 || len(in.Cursor) > 1024 {
return nil, fault(400, "INVALID_PAGINATION")
}
a, e := ResolveAccess(ctx, tx, p, in.ElderID, false)
if e != nil {
return nil, e
}
// Transcripts have not been classified per sentence. A general read grant
// alone must not expose potentially sensitive conversation text.
if !a.Sensitive {
return nil, fault(403, "TRANSCRIPT_ACCESS_DENIED")
}
c := recordCursor{}
if in.Cursor != "" {
b, err := base64.RawURLEncoding.DecodeString(in.Cursor)
if err != nil || StrictJSON(b, &c) != nil || c.Time.IsZero() || c.ID == "" {
return nil, fault(400, "INVALID_CURSOR")
}
}
var rows pgx.Rows
if op == "calls/query" {
if in.SessionID != "" || (c.ID != "" && !validUUID(c.ID)) {
return nil, fault(400, "INVALID_CALL_ID")
}
rows, e = tx.Query(ctx, `SELECT id::text,created_at,ended_at,call_state,transcript_state,
GREATEST(0,COALESCE(EXTRACT(EPOCH FROM (ended_at-created_at))*1000,0))::bigint
FROM call_session WHERE subject_id=$1::uuid AND tenant_id=$2::uuid
AND ($3='' OR (created_at,id)<($4,NULLIF($3,'')::uuid))
ORDER BY created_at DESC,id DESC LIMIT $5`, a.Subject, a.Tenant, c.ID, c.Time, in.Limit+1)
} else {
if !validUUID(in.SessionID) {
return nil, fault(400, "INVALID_CALL_ID")
}
var exists bool
e = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM call_session WHERE id=$1::uuid AND subject_id=$2::uuid AND tenant_id=$3::uuid)`, in.SessionID, a.Subject, a.Tenant).Scan(&exists)
if e != nil {
return nil, e
}
if !exists {
return nil, fault(404, "CALL_NOT_FOUND")
}
rows, e = tx.Query(ctx, `SELECT u.utterance_id,u.source_at,u.speaker,u.text_content,u.interrupted,u.playback_complete
FROM call_utterance u JOIN call_session c ON c.id=u.session_id
WHERE c.id=$1::uuid AND c.subject_id=$2::uuid AND c.tenant_id=$3::uuid
AND NOT EXISTS(SELECT 1 FROM privacy_request pr WHERE pr.subject_id=c.subject_id AND pr.request_type='DELETE')
AND ($4='' OR (u.source_at,u.utterance_id)>($5,$4))
ORDER BY u.source_at,u.utterance_id LIMIT $6`, in.SessionID, a.Subject, a.Tenant, c.ID, c.Time, in.Limit+1)
}
if e != nil {
return nil, e
}
defer rows.Close()
items := []map[string]any{}
next := ""
last := recordCursor{}
for rows.Next() {
if len(items) == in.Limit {
next = base64.RawURLEncoding.EncodeToString(jsonBytes(last))
break
}
var id string
var at time.Time
item := map[string]any{}
if op == "calls/query" {
var ended *time.Time
var state, transcript string
var duration int64
e = rows.Scan(&id, &at, &ended, &state, &transcript, &duration)
item = map[string]any{"id": id, "created_at": at, "ended_at": ended, "status": state, "transcript_state": transcript, "duration_ms": duration, "source": "call_memory"}
} else {
var speaker, content string
var interrupted, complete bool
e = rows.Scan(&id, &at, &speaker, &content, &interrupted, &complete)
item = map[string]any{"id": id, "created_at": at, "speaker": speaker, "final_text": content, "interrupted": interrupted, "playback_complete": complete}
}
if e != nil {
return nil, e
}
last = recordCursor{at, id}
items = append(items, item)
}
if e = rows.Err(); e != nil {
return nil, e
}
return map[string]any{"items": items, "next_cursor": next}, nil
}