| 1 | package storage |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "database/sql" |
| 6 | "encoding/json" |
| 7 | "fmt" |
| 8 | ) |
| 9 | |
| 10 | // RetentionRule removes non-identity events older than Days. A negative Kind |
| 11 | // applies the catch-all rule to kinds eligible for retention. |
| 12 | type RetentionRule struct { |
| 13 | Kind int |
| 14 | Days int |
| 15 | } |
| 16 | |
| 17 | // MemberRetention removes eligible events and list history for one author |
| 18 | // older than Days. |
| 19 | type MemberRetention struct { |
| 20 | PubKey string |
| 21 | Days int |
| 22 | } |
| 23 | |
| 24 | // RetentionOptions supplies the retention time, relay identity and rules. |
| 25 | type RetentionOptions struct { |
| 26 | Now int64 |
| 27 | Identity string |
| 28 | Rules []RetentionRule |
| 29 | Members []MemberRetention |
| 30 | } |
| 31 | |
| 32 | // SweepRetention enforces user-selected content retention, independent of host |
| 33 | // capacity. The catch-all deliberately retains replaceable lists and mail. |
| 34 | func (s *Store) SweepRetention(ctx context.Context, opts RetentionOptions) (int64, error) { |
| 35 | var removed int64 |
| 36 | err := s.WithTx(ctx, func(tx *sql.Tx) error { |
| 37 | specific := []int{} |
| 38 | for _, rule := range opts.Rules { |
| 39 | if rule.Kind >= 0 { |
| 40 | specific = append(specific, rule.Kind) |
| 41 | } |
| 42 | } |
| 43 | encoded, err := json.Marshal(specific) |
| 44 | if err != nil { |
| 45 | return err |
| 46 | } |
| 47 | for _, rule := range opts.Rules { |
| 48 | if rule.Days <= 0 { |
| 49 | continue |
| 50 | } |
| 51 | q := "DELETE FROM events WHERE created_at<? AND pubkey<>?" |
| 52 | args := []any{opts.Now - int64(rule.Days)*86400, opts.Identity} |
| 53 | if rule.Kind < 0 { |
| 54 | q += " AND " + keepableKinds + " AND kind<>1059 AND kind NOT IN (SELECT value FROM json_each(?))" |
| 55 | args = append(args, string(encoded)) |
| 56 | } else { |
| 57 | q += " AND kind=?" |
| 58 | args = append(args, rule.Kind) |
| 59 | } |
| 60 | n, err := deleteCount(ctx, tx, q, args...) |
| 61 | if err != nil { |
| 62 | return err |
| 63 | } |
| 64 | removed += n |
| 65 | } |
| 66 | for _, member := range opts.Members { |
| 67 | if member.Days <= 0 { |
| 68 | continue |
| 69 | } |
| 70 | before := opts.Now - int64(member.Days)*86400 |
| 71 | n, err := deleteCount(ctx, tx, "DELETE FROM events WHERE pubkey=? AND created_at<? AND " +keepableKinds, member.PubKey, before) |
| 72 | if err != nil { |
| 73 | return err |
| 74 | } |
| 75 | removed += n |
| 76 | if _, err := tx.ExecContext(ctx, "DELETE FROM list_history WHERE owner=? AND created_at<?" , member.PubKey, before); err != nil { |
| 77 | return err |
| 78 | } |
| 79 | } |
| 80 | return nil |
| 81 | }) |
| 82 | return removed, err |
| 83 | } |
| 84 | |
| 85 | const keepableKinds = `kind NOT IN(0,3,10002,9735,13534,8000,8001,9000,9001,33534,39000,39001,39002,39003,39005) |
| 86 | AND kind NOT BETWEEN 10000 AND 19999 AND kind NOT BETWEEN 30000 AND 39999` |
| 87 | |
| 88 | func deleteCount(ctx context.Context, tx *sql.Tx, query string, args ...any) (int64, error) { |
| 89 | result, err := tx.ExecContext(ctx, query, args...) |
| 90 | if err != nil { |
| 91 | return 0, fmt.Errorf("sweep retention: %w" , err) |
| 92 | } |
| 93 | return result.RowsAffected() |
| 94 | } |
| 95 | |
| 96 | // KindStats reports count, size and time bounds for one stored event kind. |
| 97 | type KindStats struct { |
| 98 | Kind int `json:"kind"` |
| 99 | N int64 `json:"n"` |
| 100 | Bytes int64 `json:"bytes"` |
| 101 | Oldest int64 `json:"oldest"` |
| 102 | Newest int64 `json:"newest"` |
| 103 | } |
| 104 | |
| 105 | func (s *Store) KindStats(ctx context.Context) ([]KindStats, error) { |
| 106 | rows, err := s.db.QueryContext(ctx, "SELECT kind,count(*),sum(length(raw)),min(created_at),max(created_at) FROM events WHERE kind<>30390 GROUP BY kind ORDER BY sum(length(raw)) DESC" ) |
| 107 | if err != nil { |
| 108 | return nil, err |
| 109 | } |
| 110 | defer rows.Close() |
| 111 | result := []KindStats{} |
| 112 | for rows.Next() { |
| 113 | var row KindStats |
| 114 | if err := rows.Scan(&row.Kind, &row.N, &row.Bytes, &row.Oldest, &row.Newest); err != nil { |
| 115 | return nil, err |
| 116 | } |
| 117 | result = append(result, row) |
| 118 | } |
| 119 | return result, rows.Err() |
| 120 | } |