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

moderation.gno

5.52 Kb · 175 lines
  1package memba_notes_v1
  2
  3import keys "gno.land/r/samcrew/enckeys_v1"
  4
  5const MaxModerators = 32
  6
  7var moderators []address
  8
  9func moderator(who address) bool { return who == admin || contains(moderators, who) }
 10func AddModerator(cur realm, to address) {
 11	who := enter(cur, false)
 12	defer leave()
 13	administrator(who)
 14	to = keys.CanonicalAddress(to)
 15	if contains(moderators, to) {
 16		panic("notes: moderator exists")
 17	}
 18	if len(moderators) >= MaxModerators {
 19		panic("notes: moderator cap")
 20	}
 21	moderators = append(moderators, to)
 22}
 23func RemoveModerator(cur realm, to address) {
 24	who := enter(cur, false)
 25	defer leave()
 26	administrator(who)
 27	to = keys.CanonicalAddress(to)
 28	for i, a := range moderators {
 29		if a == to {
 30			moderators = append(moderators[:i], moderators[i+1:]...)
 31			return
 32		}
 33	}
 34	panic("notes: moderator missing")
 35}
 36func ModeratorsJSON() string {
 37	out := "["
 38	for i, a := range moderators {
 39		if i > 0 {
 40			out += ","
 41		}
 42		out += q(string(a))
 43	}
 44	return out + "]"
 45}
 46
 47// Directory moderation is separate from owner discoverability. Owners cannot
 48// undo a moderation hide by toggling their own listing preference.
 49func ModerateListing(cur realm, id [16]byte, baseRevision uint64, visible bool, opID [16]byte) {
 50	who := enter(cur, false)
 51	defer leave()
 52	mutateModerateListing(cur, who, id, baseRevision, visible, opID)
 53}
 54func mutateModerateListing(cur realm, who address, id [16]byte, baseRevision uint64, visible bool, opID [16]byte) {
 55	n := live(id)
 56	if !moderator(who) {
 57		panic("notes: moderator required")
 58	}
 59	expected(n, baseRevision)
 60	if !public(n.Mode) {
 61		panic("notes: public note required")
 62	}
 63	operation(n, opID)
 64	unindex(n)
 65	n.ModeratorHidden = !visible
 66	index(n)
 67	bump(n, who, opID, "moderate-listing")
 68}
 69
 70// RedactedTitle replaces a redacted current title, which must stay valid.
 71const RedactedTitle = "Redacted by moderation"
 72
 73// RedactPublicContentAtSeq removes one public title (field 1) or body (field 2)
 74// from realm storage: the one recorded by the note-level history event seq (a
 75// version's recorded_seq). The name carries the addressing so a caller written
 76// for field revisions cannot act on the wrong version. History stores each field
 77// revision once, so every version that kept that title or body shows it
 78// redacted. Public history keeps its SHA-256 and a content-redact entry naming
 79// the moderator and the field revision. Redacting the current version also
 80// replaces it (placeholder title, empty body) under a new field revision. Gno
 81// refunds the freed storage to the moderator who frees it.
 82func RedactPublicContentAtSeq(cur realm, id [16]byte, baseRevision uint64, field uint8, seq uint64, opID [16]byte) {
 83	who := enter(cur, false)
 84	defer leave()
 85	mutateRedactPublicContentAtSeq(cur, who, id, baseRevision, field, seq, opID)
 86}
 87func mutateRedactPublicContentAtSeq(cur realm, who address, id [16]byte, baseRevision uint64, field uint8, seq uint64, opID [16]byte) {
 88	n := find(id)
 89	if !moderator(who) {
 90		panic("notes: moderator required")
 91	}
 92	expected(n, baseRevision)
 93	if !public(n.Mode) {
 94		panic("notes: public note required")
 95	}
 96	if field != 1 && field != 2 {
 97		panic("notes: bad redaction field")
 98	}
 99	v := historyVersionAt(n.ID, seq)
100	if v == nil {
101		panic("notes: history version missing")
102	}
103	name, b, fieldRevision := "title", v.Title, v.TitleRevision
104	if field == 2 {
105		name, b, fieldRevision = "body", v.Body, v.BodyRevision
106	}
107	if b == nil {
108		panic("notes: history version missing")
109	}
110	if b.Redacted {
111		panic("notes: already redacted")
112	}
113	operation(n, opID)
114	if !n.Deleted && field == 1 && fieldRevision == n.TitleRevision {
115		if n.TitleRevision == ^uint64(0) {
116			panic("notes: title revision exhausted")
117		}
118		n.Title = []byte(RedactedTitle)
119		n.TitleRevision++
120	}
121	if !n.Deleted && field == 2 && fieldRevision == n.BodyRevision {
122		if n.BodyRevision == ^uint64(0) {
123			panic("notes: body revision exhausted")
124		}
125		n.Body = []byte{}
126		n.BodyBytes = 0
127		n.BodyRevision++
128	}
129	b.Bytes, b.Redacted = nil, true
130	bumpHistory(n, who, opID, "content-redact", `,"field":`+q(name)+`,"field_revision":`+u(fieldRevision)+`,"sha256":`+q(b.Hash))
131}
132
133// RedactPublicComment removes a public comment's anchor and body from both the
134// live comment (which becomes hidden and cannot be shown again) and its public
135// history. A purged comment keeps only its history record, which is redacted
136// the same way; baseRevision is then ignored (pass 0).
137func RedactPublicComment(cur realm, id, cid [16]byte, baseRevision uint64, opID [16]byte) {
138	who := enter(cur, false)
139	defer leave()
140	mutateRedactPublicComment(cur, who, id, cid, baseRevision, opID)
141}
142func mutateRedactPublicComment(cur realm, who address, id, cid [16]byte, baseRevision uint64, opID [16]byte) {
143	n := find(id)
144	if !moderator(who) {
145		panic("notes: moderator required")
146	}
147	if !public(n.Mode) {
148		panic("notes: public note required")
149	}
150	key := commentKey(id, cid)
151	v, ok := historyComments.Get(key)
152	if !ok {
153		panic("notes: missing public comment history")
154	}
155	archived := v.(*historyComment)
156	if archived.Redacted {
157		panic("notes: already redacted")
158	}
159	cv, live := comments.Get(key)
160	if !live {
161		if opID == ([16]byte{}) {
162			panic("notes: empty operation id")
163		}
164		archived.Anchor, archived.Body, archived.Redacted = nil, nil, true
165		e := historyAppend(n, who, opID, cid, "comment-redact", `{"comment_id":`+q(idText(cid))+`,"purged":true}`)
166		archived.LatestSeq = e.Seq
167		return
168	}
169	c := cv.(*comment)
170	commentExpected(c, baseRevision, opID)
171	archived.Anchor, archived.Body, archived.Redacted = nil, nil, true
172	c.Anchor, c.Body, c.Hidden = nil, nil, true
173	commentReceipt(c, who, opID)
174	historyCommentChange(n, c, "comment-redact")
175}