history.gno
7.21 Kb · 223 lines
1package memba_notes_v1
2
3import (
4 "chain/runtime"
5 "crypto/sha256"
6 "encoding/hex"
7 "gno.land/p/samcrew/avl"
8 "strconv"
9 "strings"
10)
11
12// Bytes shares the note's committed array: fields are replaced, never mutated in
13// place, so history stores each version once and a later commit frees nothing
14// another account paid for. A moderator redaction clears Bytes and keeps Hash.
15type historyBlob struct {
16 Bytes []byte
17 Hash string
18 Redacted bool
19}
20type historyVersion struct {
21 State note
22 Seq uint64
23 Title, Body *historyBlob
24 PublicWrites bool
25}
26type historyInfo struct {
27 StartRevision, StartBodyRevision, Head uint64
28 Latest *historyVersion
29}
30type historyEntry struct {
31 Seq, Revision, Proposal uint64
32 Height int64
33 Actor, Executor address
34 Operation, Comment [16]byte
35 Action, GovernanceAction, Detail string
36}
37type historyComment struct {
38 Initial comment
39 AnchorHistory, ParentHistory bool
40 LatestSeq uint64
41 Deleted, Hidden, Resolved bool
42 Redacted bool
43}
44type historyCall struct {
45 Executor address
46 Proposal uint64
47 Operation [16]byte
48 Action string
49}
50
51var historyInfos = avl.NewTree()
52var historyVersions = avl.NewTree()
53var historyBlobs = avl.NewTree()
54var historyEntries = avl.NewTree()
55var historyComments = avl.NewTree()
56
57// Only GovApply sets this while the existing busy guard excludes reentry.
58var historyGov *historyCall
59
60func historyKey(id [16]byte, revision uint64) string {
61 s := strconv.FormatUint(revision, 10)
62 return idText(id) + ":" + strings.Repeat("0", 20-len(s)) + s
63}
64func historyID(text string) [16]byte {
65 id := readID(text)
66 if id == ([16]byte{}) {
67 panic("notes: empty history id")
68 }
69 return id
70}
71func historyGet(id [16]byte) *historyInfo {
72 v, ok := historyInfos.Get(idText(id))
73 if !ok {
74 return nil
75 }
76 return v.(*historyInfo)
77}
78func historyInsert(tree *avl.Tree, key string, value interface{}) {
79 if _, exists := tree.Get(key); exists {
80 panic("notes: history collision")
81 }
82 tree.Set(key, value)
83}
84func historyField(n *note, field string, revision uint64, data []byte) *historyBlob {
85 key := field + ":" + historyKey(n.ID, revision)
86 hash := sha256.Sum256(data)
87 b := &historyBlob{Bytes: data, Hash: hex.EncodeToString(hash[:])}
88 historyInsert(historyBlobs, key, b)
89 return b
90}
91func historyAppend(n *note, who address, op, cid [16]byte, action, detail string) *historyEntry {
92 h := historyGet(n.ID)
93 if h == nil || h.Head == ^uint64(0) {
94 panic("notes: history unavailable/exhausted")
95 }
96 h.Head++
97 e := &historyEntry{Seq: h.Head, Revision: n.Revision, Height: runtime.ChainHeight(), Actor: who, Operation: op, Comment: cid, Action: action, Detail: detail}
98 if historyGov != nil {
99 e.Executor, e.Proposal, e.GovernanceAction = historyGov.Executor, historyGov.Proposal, historyGov.Action
100 e.Operation = historyGov.Operation
101 }
102 historyInsert(historyEntries, historyKey(n.ID, e.Seq), e)
103 return e
104}
105func historyNote(n *note, who address, op [16]byte, action, extra string) {
106 if !public(n.Mode) {
107 return
108 }
109 h := historyGet(n.ID)
110 if h == nil {
111 if action != "create" && action != "publish" {
112 panic("notes: missing public history")
113 }
114 h = &historyInfo{StartRevision: n.Revision, StartBodyRevision: n.BodyRevision}
115 historyInsert(historyInfos, idText(n.ID), h)
116 }
117 previous := "null"
118 mask := 0
119 if h.Latest != nil {
120 previous = u(h.Latest.State.Revision)
121 if n.TitleRevision != h.Latest.State.TitleRevision {
122 mask |= 1
123 }
124 if n.BodyRevision != h.Latest.State.BodyRevision {
125 mask |= 2
126 }
127 }
128 switch action {
129 case "commit":
130 action = "content-commit"
131 extra = `,"field_mask":` + strconv.Itoa(mask)
132 case "comments-mode":
133 action = "comment-mode"
134 case "policy":
135 action = "policy-set"
136 case "listing":
137 action = "owner-listing"
138 case "moderate-listing":
139 action = "listing-moderation"
140 case "delete":
141 action = "note-delete"
142 case "reveal":
143 action = "legacy-epoch-reveal"
144 case "owner-propose":
145 if n.PendingOwner == "" {
146 action = "owner-cancel"
147 }
148 }
149 e := historyAppend(n, who, op, [16]byte{}, action, `{"snapshot_revision":`+u(n.Revision)+`,"previous_state_revision":`+previous+extra+`}`)
150 v := &historyVersion{State: *n, Seq: e.Seq, PublicWrites: publicWritesEnabled(n)}
151 // The snapshot never owns current mutable content, private manifests or readers.
152 v.State.Title, v.State.Body, v.State.Commitment, v.State.Readers = nil, nil, nil, nil
153 v.State.Writers = copiedAddresses(n.Writers)
154 if !n.Deleted {
155 if h.Latest != nil && h.Latest.State.TitleRevision == n.TitleRevision {
156 v.Title = h.Latest.Title
157 } else {
158 v.Title = historyField(n, "title", n.TitleRevision, n.Title)
159 }
160 if h.Latest != nil && h.Latest.State.BodyRevision == n.BodyRevision {
161 v.Body = h.Latest.Body
162 } else {
163 v.Body = historyField(n, "body", n.BodyRevision, n.Body)
164 }
165 }
166 historyInsert(historyVersions, historyKey(n.ID, n.Revision), v)
167 h.Latest = v
168}
169func historyNullableID(id [16]byte) string {
170 if id == ([16]byte{}) {
171 return "null"
172 }
173 return q(idText(id))
174}
175func historyNullableText(s string) string {
176 if s == "" {
177 return "null"
178 }
179 return q(s)
180}
181func historyCommentDetail(c *comment, archived *historyComment) string {
182 anchorRevision := "null"
183 if archived.AnchorHistory {
184 anchorRevision = u(c.BodyRevision)
185 }
186 return `{"comment_id":` + q(idText(c.ID)) + `,"comment_revision":` + u(c.Revision) + `,"content_ref":` + q(idText(c.ID)) + `,"body_revision":` + u(c.BodyRevision) + `,"anchor_history_body_revision":` + anchorRevision + `,"parent":` + historyNullableID(c.Parent) + `,"parent_history_available":` + strconv.FormatBool(archived.ParentHistory) + `,"deleted":` + strconv.FormatBool(c.Deleted) + `,"hidden":` + strconv.FormatBool(c.Hidden) + `,"resolved":` + strconv.FormatBool(c.Resolved) + `}`
187}
188func historyCommentChange(n *note, c *comment, action string) {
189 if !public(n.Mode) || c.Encrypted {
190 return
191 }
192 key := commentKey(n.ID, c.ID)
193 value, exists := historyComments.Get(key)
194 var archived *historyComment
195 if action == "comment-add" {
196 if exists {
197 panic("notes: comment history collision")
198 }
199 _, anchor := historyBlobs.Get("body:" + historyKey(n.ID, c.BodyRevision))
200 _, parent := historyComments.Get(commentKey(n.ID, c.Parent))
201 // Initial shares the comment's arrays for the same single-copy reason.
202 archived = &historyComment{Initial: *c, AnchorHistory: anchor, ParentHistory: parent}
203 historyInsert(historyComments, key, archived)
204 } else {
205 if !exists {
206 panic("notes: missing public comment history")
207 }
208 archived = value.(*historyComment)
209 }
210 e := historyAppend(n, c.Last.Actor, c.Last.Operation, c.ID, action, historyCommentDetail(c, archived))
211 archived.LatestSeq, archived.Deleted, archived.Hidden, archived.Resolved = e.Seq, c.Deleted, c.Hidden, c.Resolved
212}
213func historyCleanup(n *note, who address, counts [5]int) {
214 if !public(n.Mode) {
215 return
216 }
217 total := counts[0] + counts[1] + counts[2] + counts[3] + counts[4]
218 if total == 0 {
219 return
220 }
221 detail := `{"comment_index":` + u(uint64(counts[0])) + `,"comments":` + u(uint64(counts[1])) + `,"cooldowns":` + u(uint64(counts[2])) + `,"epochs":` + u(uint64(counts[3])) + `,"requests":` + u(uint64(counts[4])) + `,"total":` + u(uint64(total)) + `}`
222 historyAppend(n, who, [16]byte{}, [16]byte{}, "cleanup", detail)
223}