| 1 | package storage |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | |
| 8 | "github.com/FelineStateMachine/tinyrelay/internal/event" |
| 9 | ) |
| 10 | |
| 11 | // HistoryRow identifies one retained version of a replaceable list. |
| 12 | type HistoryRow struct { |
| 13 | Kind int `json:"kind"` |
| 14 | D string `json:"d"` |
| 15 | EventID string `json:"event_id"` |
| 16 | CreatedAt int64 `json:"created_at"` |
| 17 | SavedAt int64 `json:"saved_at"` |
| 18 | } |
| 19 | |
| 20 | func (s *Store) ListHistory(ctx context.Context, owner string, now int64) ([]HistoryRow, error) { |
| 21 | rows, err := s.db.QueryContext(ctx, "SELECT kind,d,event_id,created_at,saved_at FROM list_history WHERE owner=? AND(expires=0 OR expires>?) ORDER BY saved_at DESC,event_id" , owner, now) |
| 22 | if err != nil { |
| 23 | return nil, fmt.Errorf("query list history: %w" , err) |
| 24 | } |
| 25 | defer rows.Close() |
| 26 | result := []HistoryRow{} |
| 27 | for rows.Next() { |
| 28 | var row HistoryRow |
| 29 | if err := rows.Scan(&row.Kind, &row.D, &row.EventID, &row.CreatedAt, &row.SavedAt); err != nil { |
| 30 | return nil, err |
| 31 | } |
| 32 | result = append(result, row) |
| 33 | } |
| 34 | return result, rows.Err() |
| 35 | } |
| 36 | |
| 37 | // RestoreResult contains a prior list event draft and its difference from the |
| 38 | // current list. |
| 39 | type RestoreResult struct { |
| 40 | Draft Draft `json:"draft"` |
| 41 | Diff Diff `json:"diff"` |
| 42 | } |
| 43 | |
| 44 | // Draft is the event body needed to publish a restored list version. |
| 45 | type Draft struct { |
| 46 | Kind int `json:"kind"` |
| 47 | CreatedAt int64 `json:"created_at"` |
| 48 | Tags [][]string `json:"tags"` |
| 49 | Content string `json:"content"` |
| 50 | } |
| 51 | |
| 52 | // Diff describes content and tag changes between the retained and current |
| 53 | // versions. |
| 54 | type Diff struct { |
| 55 | AddedTags [][]string `json:"addedTags"` |
| 56 | RemovedTags [][]string `json:"removedTags"` |
| 57 | ContentChanged bool `json:"contentChanged"` |
| 58 | } |
| 59 | |
| 60 | // RestoreOptions identifies the retained list event and the query time used |
| 61 | // for expiration checks. |
| 62 | type RestoreOptions struct { |
| 63 | Owner string |
| 64 | EventID string |
| 65 | Now int64 |
| 66 | } |
| 67 | |
| 68 | func (s *Store) RestoreHistory(ctx context.Context, opts RestoreOptions) (RestoreResult, error) { |
| 69 | var raw string |
| 70 | var result RestoreResult |
| 71 | err := s.db.QueryRowContext(ctx, "SELECT raw FROM list_history WHERE owner=? AND event_id=? AND(expires=0 OR expires>?)" , opts.Owner, opts.EventID, opts.Now).Scan(&raw) |
| 72 | if err != nil { |
| 73 | return result, fmt.Errorf("load owned list history: %w" , err) |
| 74 | } |
| 75 | var old event.Event |
| 76 | if err := json.Unmarshal([]byte(raw), &old); err != nil { |
| 77 | return result, err |
| 78 | } |
| 79 | f := event.Filter{Authors: []string{opts.Owner}, Kinds: []int{old.Kind}} |
| 80 | if event.IsAddressable(old.Kind) { |
| 81 | f.Tags = map[string][]string{"d" : {event.Tag(old, "d" )}} |
| 82 | } |
| 83 | current, err := s.Query(ctx, f, QueryOptions{Now: opts.Now, Access: Access{PubKeys: []string{opts.Owner}}, Limit: 1}) |
| 84 | if err != nil { |
| 85 | return result, err |
| 86 | } |
| 87 | var cur event.Event |
| 88 | if len(current.Events) > 0 { |
| 89 | cur = current.Events[0] |
| 90 | } |
| 91 | at := opts.Now |
| 92 | if cur.CreatedAt >= at { |
| 93 | at = cur.CreatedAt + 1 |
| 94 | } |
| 95 | result.Draft = Draft{Kind: old.Kind, CreatedAt: at, Tags: old.Tags, Content: old.Content} |
| 96 | result.Diff.ContentChanged = cur.Content != old.Content |
| 97 | result.Diff.AddedTags = tagDifference(old.Tags, cur.Tags) |
| 98 | result.Diff.RemovedTags = tagDifference(cur.Tags, old.Tags) |
| 99 | return result, nil |
| 100 | } |
| 101 | |
| 102 | func tagDifference(first, second [][]string) [][]string { |
| 103 | result := [][]string{} |
| 104 | for _, a := range first { |
| 105 | found := false |
| 106 | for _, b := range second { |
| 107 | if equalTag(a, b) { |
| 108 | found = true |
| 109 | break |
| 110 | } |
| 111 | } |
| 112 | if !found { |
| 113 | result = append(result, a) |
| 114 | } |
| 115 | } |
| 116 | return result |
| 117 | } |
| 118 | |
| 119 | func equalTag(a, b []string) bool { |
| 120 | if len(a) != len(b) { |
| 121 | return false |
| 122 | } |
| 123 | for i := range a { |
| 124 | if a[i] != b[i] { |
| 125 | return false |
| 126 | } |
| 127 | } |
| 128 | return true |
| 129 | } |