Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}