tinyrelayfile
tinyrelay/internal/storage/history.go/

internal/storage/history.go raw
1package storage
2
3import (
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.
12type 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
20func (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.
39type 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.
45type 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.
54type 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.
62type RestoreOptions struct {
63 Owner string
64 EventID string
65 Now int64
66}
67
68func (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
102func 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
119func 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}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs