| 1 | package storage |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | "sort" |
| 8 | "strings" |
| 9 | "time" |
| 10 | |
| 11 | "github.com/FelineStateMachine/tinyrelay/internal/event" |
| 12 | ) |
| 13 | |
| 14 | // Access controls which private and callback events a query may return. All |
| 15 | // bypasses recipient filtering but still excludes hidden, pending and expired |
| 16 | // rows. |
| 17 | type Access struct { |
| 18 | PubKeys []string |
| 19 | All bool |
| 20 | } |
| 21 | |
| 22 | // QueryOptions supplies query time, access principals, an optional result |
| 23 | // limit and a cursor from a prior Query result. |
| 24 | type QueryOptions struct { |
| 25 | Now int64 |
| 26 | Access Access |
| 27 | Limit int |
| 28 | Before *EventCursor |
| 29 | } |
| 30 | |
| 31 | // EventCursor follows the stable newest-first query order. |
| 32 | type EventCursor struct { |
| 33 | CreatedAt int64 |
| 34 | ID string |
| 35 | } |
| 36 | |
| 37 | // QueryResult contains the matching events and whether the limit hid more |
| 38 | // rows. |
| 39 | type QueryResult struct { |
| 40 | Events []event.Event |
| 41 | More bool |
| 42 | } |
| 43 | |
| 44 | type predicate struct { |
| 45 | conditions []string |
| 46 | args []any |
| 47 | } |
| 48 | |
| 49 | func (p *predicate) add(condition string, args ...any) { |
| 50 | p.conditions = append(p.conditions, condition) |
| 51 | p.args = append(p.args, args...) |
| 52 | } |
| 53 | |
| 54 | func (p *predicate) list(column string, value any) error { |
| 55 | b, err := json.Marshal(value) |
| 56 | if err != nil { |
| 57 | return fmt.Errorf("encode filter list: %w" , err) |
| 58 | } |
| 59 | p.add(column+" IN (SELECT value FROM json_each(?))" , string(b)) |
| 60 | return nil |
| 61 | } |
| 62 | |
| 63 | func where(f event.Filter, opts QueryOptions) (predicate, error) { |
| 64 | var p predicate |
| 65 | if opts.Before != nil { |
| 66 | p.add("(created_at<? OR (created_at=? AND id>?))" , opts.Before.CreatedAt, opts.Before.CreatedAt, opts.Before.ID) |
| 67 | } |
| 68 | if f.IDs != nil { |
| 69 | if err := p.list("id" , f.IDs); err != nil { |
| 70 | return p, err |
| 71 | } |
| 72 | } |
| 73 | if f.Authors != nil { |
| 74 | if err := p.list("pubkey" , f.Authors); err != nil { |
| 75 | return p, err |
| 76 | } |
| 77 | } |
| 78 | if f.Kinds != nil { |
| 79 | if err := p.list("kind" , f.Kinds); err != nil { |
| 80 | return p, err |
| 81 | } |
| 82 | } |
| 83 | if f.Since != nil { |
| 84 | p.add("created_at>=?" , *f.Since) |
| 85 | } |
| 86 | if f.Until != nil { |
| 87 | p.add("created_at<=?" , *f.Until) |
| 88 | } |
| 89 | keys := make([]string, 0, len(f.Tags)) |
| 90 | for name := range f.Tags { |
| 91 | keys = append(keys, name) |
| 92 | } |
| 93 | sort.Strings(keys) |
| 94 | for _, name := range keys { |
| 95 | values, err := json.Marshal(f.Tags[name]) |
| 96 | if err != nil { |
| 97 | return p, fmt.Errorf("encode tag filter: %w" , err) |
| 98 | } |
| 99 | 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)) |
| 100 | } |
| 101 | terms := event.SearchTerms(f.Search) |
| 102 | if len(terms) > 0 { |
| 103 | quoted := make([]string, len(terms)) |
| 104 | for i, term := range terms { |
| 105 | quoted[i] = `"` + strings.ReplaceAll(term, `"` , `""` ) + `"` |
| 106 | } |
| 107 | p.add("seq IN (SELECT rowid FROM search WHERE search MATCH ?)" , strings.Join(quoted, " AND " )) |
| 108 | } |
| 109 | p.add("(expires=0 OR expires>?)" , opts.Now) |
| 110 | p.add("NOT EXISTS(SELECT 1 FROM hidden_events WHERE hidden_events.id=events.id)" ) |
| 111 | p.add("NOT EXISTS(SELECT 1 FROM pending_events WHERE pending_events.id=events.id)" ) |
| 112 | if err := addAccess(&p, opts.Access); err != nil { |
| 113 | return p, err |
| 114 | } |
| 115 | return p, nil |
| 116 | } |
| 117 | |
| 118 | func addAccess(p *predicate, who Access) error { |
| 119 | keys, err := json.Marshal(who.PubKeys) |
| 120 | if err != nil { |
| 121 | return fmt.Errorf("encode read principals: %w" , err) |
| 122 | } |
| 123 | // Callback registrations never become visible through internal all-access. |
| 124 | p.add("(kind<>" +fmt.Sprint(event.KIND_PUSH_REGISTRATION)+" OR pubkey IN(SELECT value FROM json_each(?)))" , string(keys)) |
| 125 | if who.All { |
| 126 | return nil |
| 127 | } |
| 128 | // NIP-59 seals (kind 13) carry no recipient tag; only their author may |
| 129 | // retrieve them. Gift wraps and other private events are recipient scoped. |
| 130 | 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)) |
| 131 | return nil |
| 132 | } |
| 133 | |
| 134 | func (s *Store) Query(ctx context.Context, f event.Filter, opts QueryOptions) (result QueryResult, err error) { |
| 135 | started := time.Now() |
| 136 | defer func() { s.observe("query" , started, err) }() |
| 137 | p, err := where(f, opts) |
| 138 | if err != nil { |
| 139 | return result, err |
| 140 | } |
| 141 | query := "SELECT raw FROM events WHERE " + strings.Join(p.conditions, " AND " ) + " ORDER BY created_at DESC,id ASC" |
| 142 | limit := opts.Limit |
| 143 | if f.Limit != nil && (limit <= 0 || *f.Limit < limit) { |
| 144 | limit = *f.Limit |
| 145 | } |
| 146 | limited := opts.Limit > 0 || f.Limit != nil |
| 147 | if limited { |
| 148 | if limit < 0 { |
| 149 | limit = 0 |
| 150 | } |
| 151 | query += " LIMIT ?" |
| 152 | p.args = append(p.args, int64(limit)+1) |
| 153 | } |
| 154 | rows, err := s.db.QueryContext(ctx, query, p.args...) |
| 155 | if err != nil { |
| 156 | return result, fmt.Errorf("query events: %w" , err) |
| 157 | } |
| 158 | defer func() { |
| 159 | if closeErr := rows.Close(); err == nil { |
| 160 | err = closeErr |
| 161 | } |
| 162 | }() |
| 163 | result.Events = []event.Event{} |
| 164 | for rows.Next() { |
| 165 | if limited && len(result.Events) == limit { |
| 166 | result.More = true |
| 167 | break |
| 168 | } |
| 169 | var raw string |
| 170 | if err := rows.Scan(&raw); err != nil { |
| 171 | return result, fmt.Errorf("read event: %w" , err) |
| 172 | } |
| 173 | var e event.Event |
| 174 | if err := json.Unmarshal([]byte(raw), &e); err != nil { |
| 175 | return result, fmt.Errorf("decode stored event: %w" , err) |
| 176 | } |
| 177 | result.Events = append(result.Events, e) |
| 178 | } |
| 179 | return result, rows.Err() |
| 180 | } |
| 181 | |
| 182 | func (s *Store) Count(ctx context.Context, filters []event.Filter, opts QueryOptions) (count int64, err error) { |
| 183 | started := time.Now() |
| 184 | defer func() { s.observe("count" , started, err) }() |
| 185 | if len(filters) == 0 { |
| 186 | return 0, nil |
| 187 | } |
| 188 | var ors []string |
| 189 | var args []any |
| 190 | for _, f := range filters { |
| 191 | p, err := where(f, opts) |
| 192 | if err != nil { |
| 193 | return 0, err |
| 194 | } |
| 195 | ors = append(ors, "(" +strings.Join(p.conditions, " AND " )+")" ) |
| 196 | args = append(args, p.args...) |
| 197 | } |
| 198 | err = s.db.QueryRowContext(ctx, "SELECT count(*) FROM events WHERE " +strings.Join(ors, " OR " ), args...).Scan(&count) |
| 199 | if err != nil { |
| 200 | return 0, fmt.Errorf("count events: %w" , err) |
| 201 | } |
| 202 | return count, nil |
| 203 | } |
| 204 | |
| 205 | type CursorEvent struct { |
| 206 | Sequence int64 `json:"seq"` |
| 207 | Event event.Event `json:"event"` |
| 208 | } |
| 209 | |
| 210 | func (s *Store) After(ctx context.Context, seq int64, f event.Filter, opts QueryOptions) ([]CursorEvent, error) { |
| 211 | p, err := where(f, opts) |
| 212 | if err != nil { |
| 213 | return nil, err |
| 214 | } |
| 215 | p.add("seq>?" , seq) |
| 216 | q := "SELECT seq,raw FROM events WHERE " + strings.Join(p.conditions, " AND " ) + " ORDER BY seq" |
| 217 | if opts.Limit > 0 { |
| 218 | q += " LIMIT ?" |
| 219 | p.args = append(p.args, opts.Limit) |
| 220 | } |
| 221 | rows, err := s.db.QueryContext(ctx, q, p.args...) |
| 222 | if err != nil { |
| 223 | return nil, fmt.Errorf("query event cursor: %w" , err) |
| 224 | } |
| 225 | defer rows.Close() |
| 226 | var result []CursorEvent |
| 227 | for rows.Next() { |
| 228 | var item CursorEvent |
| 229 | var raw string |
| 230 | if err := rows.Scan(&item.Sequence, &raw); err != nil { |
| 231 | return nil, err |
| 232 | } |
| 233 | if err := json.Unmarshal([]byte(raw), &item.Event); err != nil { |
| 234 | return nil, err |
| 235 | } |
| 236 | result = append(result, item) |
| 237 | } |
| 238 | return result, rows.Err() |
| 239 | } |