187 lines
5.0 KiB
Go
187 lines
5.0 KiB
Go
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
|
|
}
|