clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/standalone-relay
tinyrelay / internal / storage / query.go
query.go 6.8K
1package storage23import (4 "context"5 "encoding/json"6 "fmt"7 "sort"8 "strings"9 "time"1011 "github.com/FelineStateMachine/tinyrelay/internal/event"12)1314// Access controls which private and callback events a query may return. All15// bypasses recipient filtering but still excludes hidden, pending and expired16// rows.17type Access struct {18 PubKeys []string19 All bool20 // IncludeCallbackRegistrations opts out of the daemon's callback privacy21 // filter. Standalone relays use this to preserve opaque Nostr events.22 IncludeCallbackRegistrations bool23}2425// QueryOptions supplies query time, access principals, an optional result26// limit and a cursor from a prior Query result.27type QueryOptions struct {28 Now int6429 Access Access30 Limit int31 Before *EventCursor32}3334// EventCursor follows the stable newest-first query order.35type EventCursor struct {36 CreatedAt int6437 ID string38}3940// QueryResult contains the matching events and whether the limit hid more41// rows.42type QueryResult struct {43 Events []event.Event44 More bool45}4647type predicate struct {48 conditions []string49 args []any50}5152func (p *predicate) add(condition string, args ...any) {53 p.conditions = append(p.conditions, condition)54 p.args = append(p.args, args...)55}5657func (p *predicate) list(column string, value any) error {58 b, err := json.Marshal(value)59 if err != nil {60 return fmt.Errorf("encode filter list: %w", err)61 }62 p.add(column+" IN (SELECT value FROM json_each(?))", string(b))63 return nil64}6566func where(f event.Filter, opts QueryOptions) (predicate, error) {67 var p predicate68 if opts.Before != nil {69 p.add("(created_at<? OR (created_at=? AND id>?))", opts.Before.CreatedAt, opts.Before.CreatedAt, opts.Before.ID)70 }71 if f.IDs != nil {72 if err := p.list("id", f.IDs); err != nil {73 return p, err74 }75 }76 if f.Authors != nil {77 if err := p.list("pubkey", f.Authors); err != nil {78 return p, err79 }80 }81 if f.Kinds != nil {82 if err := p.list("kind", f.Kinds); err != nil {83 return p, err84 }85 }86 if f.Since != nil {87 p.add("created_at>=?", *f.Since)88 }89 if f.Until != nil {90 p.add("created_at<=?", *f.Until)91 }92 keys := make([]string, 0, len(f.Tags))93 for name := range f.Tags {94 keys = append(keys, name)95 }96 sort.Strings(keys)97 for _, name := range keys {98 values, err := json.Marshal(f.Tags[name])99 if err != nil {100 return p, fmt.Errorf("encode tag filter: %w", err)101 }102 p.add("EXISTS(SELECT 1 FROM tags WHERE tags.event_id=events.id AND name=? AND value IN(SELECT value FROM json_each(?)))", name, string(values))103 }104 terms := event.SearchTerms(f.Search)105 if len(terms) > 0 {106 quoted := make([]string, len(terms))107 for i, term := range terms {108 quoted[i] = `"` + strings.ReplaceAll(term, `"`, `""`) + `"`109 }110 p.add("seq IN (SELECT rowid FROM search WHERE search MATCH ?)", strings.Join(quoted, " AND "))111 }112 p.add("(expires=0 OR expires>?)", opts.Now)113 p.add("NOT EXISTS(SELECT 1 FROM hidden_events WHERE hidden_events.id=events.id)")114 p.add("NOT EXISTS(SELECT 1 FROM pending_events WHERE pending_events.id=events.id)")115 if err := addAccess(&p, opts.Access); err != nil {116 return p, err117 }118 return p, nil119}120121func addAccess(p *predicate, who Access) error {122 keys, err := json.Marshal(who.PubKeys)123 if err != nil {124 return fmt.Errorf("encode read principals: %w", err)125 }126 // Callback registrations never become visible through internal all-access.127 if !who.IncludeCallbackRegistrations {128 p.add("(kind<>"+fmt.Sprint(event.KIND_PUSH_REGISTRATION)+" OR pubkey IN(SELECT value FROM json_each(?)))", string(keys))129 }130 if who.All {131 return nil132 }133 // NIP-59 seals (kind 13) carry no recipient tag; only their author may134 // retrieve them. Gift wraps and other private events are recipient scoped.135 p.add("(kind NOT IN(4,13,1059,21059,24133) OR pubkey IN(SELECT value FROM json_each(?)) OR (kind<>13 AND EXISTS(SELECT 1 FROM tags WHERE event_id=events.id AND name='p' AND value IN(SELECT value FROM json_each(?)))))", string(keys), string(keys))136 return nil137}138139func (s *Store) Query(ctx context.Context, f event.Filter, opts QueryOptions) (result QueryResult, err error) {140 started := time.Now()141 defer func() { s.observe("query", started, err) }()142 p, err := where(f, opts)143 if err != nil {144 return result, err145 }146 query := "SELECT raw FROM events WHERE " + strings.Join(p.conditions, " AND ") + " ORDER BY created_at DESC,id ASC"147 limit := opts.Limit148 if f.Limit != nil && (limit <= 0 || *f.Limit < limit) {149 limit = *f.Limit150 }151 limited := opts.Limit > 0 || f.Limit != nil152 if limited {153 if limit < 0 {154 limit = 0155 }156 query += " LIMIT ?"157 p.args = append(p.args, int64(limit)+1)158 }159 rows, err := s.db.QueryContext(ctx, query, p.args...)160 if err != nil {161 return result, fmt.Errorf("query events: %w", err)162 }163 defer func() {164 if closeErr := rows.Close(); err == nil {165 err = closeErr166 }167 }()168 result.Events = []event.Event{}169 for rows.Next() {170 if limited && len(result.Events) == limit {171 result.More = true172 break173 }174 var raw string175 if err := rows.Scan(&raw); err != nil {176 return result, fmt.Errorf("read event: %w", err)177 }178 var e event.Event179 if err := json.Unmarshal([]byte(raw), &e); err != nil {180 return result, fmt.Errorf("decode stored event: %w", err)181 }182 result.Events = append(result.Events, e)183 }184 return result, rows.Err()185}186187func (s *Store) Count(ctx context.Context, filters []event.Filter, opts QueryOptions) (count int64, err error) {188 started := time.Now()189 defer func() { s.observe("count", started, err) }()190 if len(filters) == 0 {191 return 0, nil192 }193 var ors []string194 var args []any195 for _, f := range filters {196 p, err := where(f, opts)197 if err != nil {198 return 0, err199 }200 ors = append(ors, "("+strings.Join(p.conditions, " AND ")+")")201 args = append(args, p.args...)202 }203 err = s.db.QueryRowContext(ctx, "SELECT count(*) FROM events WHERE "+strings.Join(ors, " OR "), args...).Scan(&count)204 if err != nil {205 return 0, fmt.Errorf("count events: %w", err)206 }207 return count, nil208}209210type CursorEvent struct {211 Sequence int64 `json:"seq"`212 Event event.Event `json:"event"`213}214215func (s *Store) After(ctx context.Context, seq int64, f event.Filter, opts QueryOptions) ([]CursorEvent, error) {216 p, err := where(f, opts)217 if err != nil {218 return nil, err219 }220 p.add("seq>?", seq)221 q := "SELECT seq,raw FROM events WHERE " + strings.Join(p.conditions, " AND ") + " ORDER BY seq"222 if opts.Limit > 0 {223 q += " LIMIT ?"224 p.args = append(p.args, opts.Limit)225 }226 rows, err := s.db.QueryContext(ctx, q, p.args...)227 if err != nil {228 return nil, fmt.Errorf("query event cursor: %w", err)229 }230 defer rows.Close()231 var result []CursorEvent232 for rows.Next() {233 var item CursorEvent234 var raw string235 if err := rows.Scan(&item.Sequence, &raw); err != nil {236 return nil, err237 }238 if err := json.Unmarshal([]byte(raw), &item.Event); err != nil {239 return nil, err240 }241 result = append(result, item)242 }243 return result, rows.Err()244}