clone

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

feat/tinyagent
tinyrelay / internal / storage / history.go
history.go 3.5K
1package storage23import (4 "context"5 "encoding/json"6 "fmt"78 "github.com/FelineStateMachine/tinyrelay/internal/event"9)1011// 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}1920func (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 HistoryRow29 if err := rows.Scan(&row.Kind, &row.D, &row.EventID, &row.CreatedAt, &row.SavedAt); err != nil {30 return nil, err31 }32 result = append(result, row)33 }34 return result, rows.Err()35}3637// RestoreResult contains a prior list event draft and its difference from the38// current list.39type RestoreResult struct {40 Draft Draft `json:"draft"`41 Diff Diff `json:"diff"`42}4344// 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}5152// Diff describes content and tag changes between the retained and current53// versions.54type Diff struct {55 AddedTags [][]string `json:"addedTags"`56 RemovedTags [][]string `json:"removedTags"`57 ContentChanged bool `json:"contentChanged"`58}5960// RestoreOptions identifies the retained list event and the query time used61// for expiration checks.62type RestoreOptions struct {63 Owner string64 EventID string65 Now int6466}6768func (s *Store) RestoreHistory(ctx context.Context, opts RestoreOptions) (RestoreResult, error) {69 var raw string70 var result RestoreResult71 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.Event76 if err := json.Unmarshal([]byte(raw), &old); err != nil {77 return result, err78 }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, err86 }87 var cur event.Event88 if len(current.Events) > 0 {89 cur = current.Events[0]90 }91 at := opts.Now92 if cur.CreatedAt >= at {93 at = cur.CreatedAt + 194 }95 result.Draft = Draft{Kind: old.Kind, CreatedAt: at, Tags: old.Tags, Content: old.Content}96 result.Diff.ContentChanged = cur.Content != old.Content97 result.Diff.AddedTags = tagDifference(old.Tags, cur.Tags)98 result.Diff.RemovedTags = tagDifference(cur.Tags, old.Tags)99 return result, nil100}101102func tagDifference(first, second [][]string) [][]string {103 result := [][]string{}104 for _, a := range first {105 found := false106 for _, b := range second {107 if equalTag(a, b) {108 found = true109 break110 }111 }112 if !found {113 result = append(result, a)114 }115 }116 return result117}118119func equalTag(a, b []string) bool {120 if len(a) != len(b) {121 return false122 }123 for i := range a {124 if a[i] != b[i] {125 return false126 }127 }128 return true129}