package storage // The User Data Layer facade (/api/udl/*). Since v2.0.0 a user's data are // ordinary claims signed by the user's key — plaintext for public-social keys // (like, userRating, profile:*), ciphertext for private ones (seek, watched, // …). There is no separate store: these helpers read the claims of one signer // through the cache's claim_by_pid index and the truth claim table. import ( "database/sql" "errors" "sort" "github.com/metazla/meta-core/internal/claim" "github.com/metazla/meta-core/internal/identity" ) // UDLUserStats summarises one signer's live claims. type UDLUserStats struct { Records int `json:"records"` Cids int `json:"cids"` Keys int `json:"keys"` } // UDLGetClaim returns the stored claim of uid on (cid, key), tombstone // included (a client needs its ts to write a newer one). ok is false when the // signer never voted on it. func (c *Client) UDLGetClaim(uid, cid, key string) (claim.Claim, bool, error) { if _, err := c.truthStore(); err != nil { return claim.Claim{}, false, err } ctx, cancel := opCtx() defer cancel() cl := claim.Claim{CID: cid, UID: uid, Key: key} var raw any var ts int64 err := c.truth.DB().QueryRowContext(ctx, `SELECT c.value, c.ts, c.sig FROM claim c JOIN principal p ON p.pid = c.pid WHERE c.cid = ? AND c.key = ? AND p.uid = ?`, cid, key, uid).Scan(&raw, &ts, &cl.Sig) if errors.Is(err, sql.ErrNoRows) { return claim.Claim{}, false, nil } if err != nil { return claim.Claim{}, false, err } cl.Value, cl.TS = scanValue(raw), uint64(ts) return cl, true, nil } // UDLClaimsForUser returns uid's live claims, optionally narrowed to one cid // and/or one key, ordered by cid then key. func (c *Client) UDLClaimsForUser(uid, cid, key string) ([]claim.Claim, error) { refs, err := c.LiveClaimsBySigner(uid) if err != nil { return nil, err } var out []claim.Claim for _, r := range refs { if (cid != "" && r[0] != cid) || (key != "" && r[1] != key) { continue } cl, ok, err := c.UDLGetClaim(uid, r[0], r[1]) if err != nil { return nil, err } if ok && !cl.Value.IsNull() { out = append(out, cl) } } return out, nil } // UDLAllForUser pages uid's live claims by cid: claims of cids strictly after // `after`, stopping at a cid boundary once limit claims are collected. // nextCid is "" when the walk is complete. func (c *Client) UDLAllForUser(uid, after string, limit int) ([]claim.Claim, string, error) { all, err := c.UDLClaimsForUser(uid, "", "") if err != nil { return nil, "", err } var out []claim.Claim for _, cl := range all { if after != "" && cl.CID <= after { continue } if len(out) >= limit && cl.CID != out[len(out)-1].CID { return out, out[len(out)-1].CID, nil } out = append(out, cl) } return out, "", nil } // UDLCountUserCids counts the distinct cids uid holds live claims on. func (c *Client) UDLCountUserCids(uid string) (int, error) { refs, err := c.LiveClaimsBySigner(uid) if err != nil { return 0, err } seen := map[string]bool{} for _, r := range refs { seen[r[0]] = true } return len(seen), nil } // UDLCidUsers lists the signers holding live claims on cid. func (c *Client) UDLCidUsers(cid string) ([]string, error) { claims, err := c.ClaimsForCIDs([]string{cid}, "") if err != nil { return nil, err } seen := map[string]bool{} out := []string{} for _, cl := range claims { if !seen[cl.UID] { seen[cl.UID] = true out = append(out, cl.UID) } } sort.Strings(out) return out, nil } // UDLPublicValues returns the plaintext values of every live claim on // (cid, key) — the input of /api/udl/cid/{cid}/aggregate. func (c *Client) UDLPublicValues(cid, key string) ([]string, error) { claims, err := c.ClaimsForCIDs([]string{cid}, key) if err != nil { return nil, err } out := []string{} for _, cl := range claims { if cl.Value.Kind == claim.KindText { out = append(out, cl.Value.Text) } } return out, nil } // UDLAllUserStats summarises every signer that holds live claims, keyed by // uid. Callers that want only human accounts intersect it with the account // roster. func (c *Client) UDLAllUserStats() (map[string]UDLUserStats, error) { s, err := c.kv() if err != nil { return nil, err } if _, err := c.truthStore(); err != nil { return nil, err } ctx, cancel := opCtx() defer cancel() rows, err := s.DB().QueryContext(ctx, `SELECT pid, count(*), count(DISTINCT cid), count(DISTINCT key) FROM claim_by_pid GROUP BY pid`) if err != nil { return nil, err } type agg struct { pid int64 st UDLUserStats } var aggs []agg for rows.Next() { var a agg if err := rows.Scan(&a.pid, &a.st.Records, &a.st.Cids, &a.st.Keys); err != nil { rows.Close() return nil, err } aggs = append(aggs, a) } if err := rows.Close(); err != nil { return nil, err } out := make(map[string]UDLUserStats, len(aggs)) for _, a := range aggs { var uid string if err := c.truth.DB().QueryRowContext(ctx, `SELECT uid FROM principal WHERE pid = ?`, a.pid).Scan(&uid); err != nil { continue } if identity.ValidUID(uid) { out[uid] = a.st } } return out, nil }