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}