moderation.gno
5.18 Kb · 171 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// RedactPublicContent removes one public title (field 1) or body (field 2)
74// version from realm storage. Public history keeps the version's SHA-256 and a
75// content-redact entry naming the moderator. Redacting the current version also
76// replaces it (placeholder title, empty body) under a new field revision. Gno
77// refunds the freed storage to the moderator who frees it.
78func RedactPublicContent(cur realm, id [16]byte, baseRevision uint64, field uint8, fieldRevision uint64, opID [16]byte) {
79 who := enter(cur, false)
80 defer leave()
81 mutateRedactPublicContent(cur, who, id, baseRevision, field, fieldRevision, opID)
82}
83func mutateRedactPublicContent(cur realm, who address, id [16]byte, baseRevision uint64, field uint8, fieldRevision uint64, opID [16]byte) {
84 n := find(id)
85 if !moderator(who) {
86 panic("notes: moderator required")
87 }
88 expected(n, baseRevision)
89 if !public(n.Mode) {
90 panic("notes: public note required")
91 }
92 name := ""
93 switch field {
94 case 1:
95 name = "title"
96 case 2:
97 name = "body"
98 default:
99 panic("notes: bad redaction field")
100 }
101 v, ok := historyBlobs.Get(name + ":" + historyKey(n.ID, fieldRevision))
102 if !ok {
103 panic("notes: history version missing")
104 }
105 b := v.(*historyBlob)
106 if b.Redacted {
107 panic("notes: already redacted")
108 }
109 operation(n, opID)
110 if !n.Deleted && field == 1 && fieldRevision == n.TitleRevision {
111 if n.TitleRevision == ^uint64(0) {
112 panic("notes: title revision exhausted")
113 }
114 n.Title = []byte(RedactedTitle)
115 n.TitleRevision++
116 }
117 if !n.Deleted && field == 2 && fieldRevision == n.BodyRevision {
118 if n.BodyRevision == ^uint64(0) {
119 panic("notes: body revision exhausted")
120 }
121 n.Body = []byte{}
122 n.BodyBytes = 0
123 n.BodyRevision++
124 }
125 b.Bytes, b.Redacted = nil, true
126 bumpHistory(n, who, opID, "content-redact", `,"field":`+q(name)+`,"field_revision":`+u(fieldRevision)+`,"sha256":`+q(b.Hash))
127}
128
129// RedactPublicComment removes a public comment's anchor and body from both the
130// live comment (which becomes hidden and cannot be shown again) and its public
131// history. A purged comment keeps only its history record, which is redacted
132// the same way; baseRevision is then ignored (pass 0).
133func RedactPublicComment(cur realm, id, cid [16]byte, baseRevision uint64, opID [16]byte) {
134 who := enter(cur, false)
135 defer leave()
136 mutateRedactPublicComment(cur, who, id, cid, baseRevision, opID)
137}
138func mutateRedactPublicComment(cur realm, who address, id, cid [16]byte, baseRevision uint64, opID [16]byte) {
139 n := find(id)
140 if !moderator(who) {
141 panic("notes: moderator required")
142 }
143 if !public(n.Mode) {
144 panic("notes: public note required")
145 }
146 key := commentKey(id, cid)
147 v, ok := historyComments.Get(key)
148 if !ok {
149 panic("notes: missing public comment history")
150 }
151 archived := v.(*historyComment)
152 if archived.Redacted {
153 panic("notes: already redacted")
154 }
155 cv, live := comments.Get(key)
156 if !live {
157 if opID == ([16]byte{}) {
158 panic("notes: empty operation id")
159 }
160 archived.Initial.Anchor, archived.Initial.Body, archived.Redacted = nil, nil, true
161 e := historyAppend(n, who, opID, cid, "comment-redact", `{"comment_id":`+q(idText(cid))+`,"purged":true}`)
162 archived.LatestSeq = e.Seq
163 return
164 }
165 c := cv.(*comment)
166 commentExpected(c, baseRevision, opID)
167 archived.Initial.Anchor, archived.Initial.Body, archived.Redacted = nil, nil, true
168 c.Anchor, c.Body, c.Hidden = nil, nil, true
169 commentReceipt(c, who, opID)
170 historyCommentChange(n, c, "comment-redact")
171}