modrender.gno
12.79 Kb · 414 lines
1package kourt
2import (
3 "math/overflow"
4 "strconv"
5 "strings"
6 bptree "gno.land/p/nt/bptree/v0"
7 sanitize "gno.land/p/nt/markdown/sanitize/v0"
8)
9var (
10 burnIdx = bptree.NewBPTree32()
11 createdIdx = bptree.NewBPTree32()
12 courtSeq uint64
13)
14func beInv(v int64) string {
15 if v < 0 {
16 v = 0
17 }
18 return beClaimKey(^uint64(v))
19}
20func reindexBurn(c *Court, spent int64) {
21 sum, ok := overflow.Add64(c.burnedGNOT, spent)
22 if !ok {
23 return
24 }
25 if c.burnedGNOT > 0 || c.burnKey != "" {
26 burnIdx.Remove(c.burnKey)
27 }
28 c.burnedGNOT = sum
29 c.burnKey = beInv(c.burnedGNOT) + c.id
30 burnIdx.Set(c.burnKey, c.id)
31 recordBurn(c)
32}
33func indexNewCourt(c *Court) {
34 courtSeq++
35 c.createdSeq = courtSeq
36 createdIdx.Set(beClaimKey(c.createdSeq), c.id)
37 c.burnKey = beInv(0) + c.id
38 burnIdx.Set(c.burnKey, c.id)
39}
40func listedPage(order string, offset, limit int) []string {
41 out := []string{}
42 if limit <= 0 {
43 return out
44 }
45 skipped := 0
46 take := func(_ string, v any) bool {
47 slug, ok := v.(string)
48 if !ok || !Exists(slug) {
49 return false
50 }
51 c := mustCourt(slug)
52 if c.tier == tierHidden {
53 return false
54 }
55 if skipped < offset {
56 skipped++
57 return false
58 }
59 out = append(out, slug)
60 return len(out) >= limit
61 }
62 if order == "new" {
63 createdIdx.ReverseIterate("", "", take)
64 } else {
65 burnIdx.Iterate("", "", take)
66 }
67 return out
68}
69func ListedCourtsBy(order string, offset, limit int) []string {
70 return listedPage(order, offset, limit)
71}
72func CourtBurnedGNOT(courtSlug string) int64 { return mustCourt(courtSlug).burnedGNOT }
73func claimTitleFor(c *Court, cs *claimState) string {
74 if c.mod != nil {
75 if clm := lookupClaimMod(c.mod, cs.id); clm != nil {
76 if clm.purged {
77 return "[removed · " + sanitize.InlineText(clm.tombstone) + "]"
78 }
79 if clm.global {
80 return "[text withheld]"
81 }
82 }
83 }
84 return sanitize.InlineText(cs.title)
85}
86func claimBodyVisible(c *Court, cs *claimState) string {
87 if cs.body == "" {
88 return ""
89 }
90 if c.mod != nil {
91 if clm := lookupClaimMod(c.mod, cs.id); clm != nil {
92 if clm.purged || clm.global {
93 return ""
94 }
95 }
96 }
97 return cs.body
98}
99func claimMediaVisible(c *Court, cs *claimState) []mediaItem {
100 if len(cs.media) == 0 {
101 return nil
102 }
103 if c.mod != nil {
104 if clm := lookupClaimMod(c.mod, cs.id); clm != nil {
105 if clm.purged || clm.global {
106 return nil
107 }
108 }
109 }
110 return cs.media
111}
112func claimBodyFor(c *Court, cs *claimState) string {
113 body := claimBodyVisible(c, cs)
114 if body == "" {
115 return ""
116 }
117 return sanitize.Block(body)
118}
119func claimBodyQuoted(c *Court, cs *claimState) string {
120 body := claimBodyVisible(c, cs)
121 if body == "" {
122 return ""
123 }
124 return sanitize.Blockquote(body)
125}
126const (
127 platformName = "Kourt"
128 platformTicker = "KOURT"
129)
130func qualifiedSymbol(c *Court) string {
131 return platformTicker + ":" + sanitize.InlineText(c.coin.Symbol())
132}
133func courtNameFor(c *Court) string {
134 if c.mod != nil && c.mod.courtPurged {
135 return "[removed · " + sanitize.InlineText(c.mod.courtTombstone) + "]"
136 }
137 return sanitize.InlineText(c.name)
138}
139func courtDescFor(c *Court) string {
140 if c.desc == "" {
141 return ""
142 }
143 if c.mod != nil && c.mod.courtPurged {
144 return ""
145 }
146 return sanitize.InlineText(c.desc)
147}
148func hideBanner(c *Court, cs *claimState) string {
149 if c.mod == nil {
150 return ""
151 }
152 clm := lookupClaimMod(c.mod, cs.id)
153 if clm == nil {
154 return ""
155 }
156 switch {
157 case clm.purged:
158 return "> **Removed on legal grounds** (" + sanitize.InlineText(clm.tombstone) +
159 "). The claim's lifecycle is unaffected: stakes, verdict and refunds proceed.\n\n"
160 case clm.global:
161 return "> **Text withheld pending review.** The claim's id, its state, its deadlines and every balance are still shown, so you can always act on a position.\n\n"
162 case clm.meta:
163 return "> **Hidden by the review court.** Listed nowhere, reachable here; the lifecycle is unaffected.\n\n"
164 case clm.court:
165 return "> **Hidden by this court's moderators.** Listed nowhere, reachable here; the lifecycle is unaffected.\n\n"
166 }
167 return ""
168}
169const (
170 stripHeading = "Still flaggable"
171 stripBlurb = "Answered claims that can still be flagged as spam — nearest deadline first. A claim leaves this list when its rewards are opened, not when it settles, so a settled claim stays here while it can still be flagged. This list ignores moderation: a claim hidden from the lists above still appears here."
172)
173func writeStrip(b *strings.Builder, slug string) {
174 c := mustCourt(slug)
175 n := StripSize(slug)
176 if n == 0 {
177 return
178 }
179 b.WriteString("## " + stripHeading + "\n\n" + stripBlurb + "\n\n")
180 ids := StripPage(slug, 0, renderPageSize)
181 for _, id := range ids {
182 cs := mustClaim(c, id)
183 b.WriteString("- [" + claimTitleFor(c, cs) + "](/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + slug + "/" +
184 strconv.FormatUint(id, 10) + ") — settles at block " +
185 strconv.FormatInt(cs.answerHeight+settleDelay, 10) + "\n")
186 }
187 if n > len(ids) {
188 b.WriteString("- _…and " + strconv.Itoa(n-len(ids)) + " more_\n")
189 }
190 b.WriteString("\n")
191}
192func writePending(b *strings.Builder, slug string) {
193 c := mustCourt(slug)
194 n := PendingSize(slug)
195 if n == 0 {
196 return
197 }
198 b.WriteString("## Awaiting an answer\n\nSeeded and appeal claims that no one has answered yet.\n\n")
199 ids := PendingPage(slug, 0, renderPageSize)
200 for _, id := range ids {
201 cs := mustClaim(c, id)
202 tag := ""
203 if cs.seeded {
204 tag = " — seeded by a moderator"
205 }
206 b.WriteString("- [" + claimTitleFor(c, cs) + "](/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + slug + "/" +
207 strconv.FormatUint(id, 10) + ")" + tag + "\n")
208 }
209 if n > len(ids) {
210 b.WriteString("- _…and " + strconv.Itoa(n-len(ids)) + " more_\n")
211 }
212 b.WriteString("\n")
213}
214func writeAppeal(b *strings.Builder, c *Court, cs *claimState) {
215 if c.id != metaSlug {
216 return
217 }
218 verb, court, target, valid := AppealParse(cs.id)
219 if !valid {
220 b.WriteString("> _Not a valid review request — this is an ordinary claim on the review court._\n\n")
221 return
222 }
223 b.WriteString("## Review request\n\n")
224 b.WriteString("- action: **" + sanitize.InlineText(verb) + "** — " +
225 appealVerbGloss(verb) + "\n")
226 b.WriteString("- court: " + sanitize.InlineText(court) + "\n")
227 if verbIsAggressive(verb) {
228 b.WriteString("- this is an **aggressive** act: it can only carry on a " +
229 "contested vote that real opposition turned out for. Silence " +
230 "defeats it.\n")
231 } else {
232 b.WriteString("- this is a **restorative** act: it undoes a restriction, so " +
233 "it can carry undisputed. Silence lets it through.\n")
234 }
235 if verb == verbSetmods {
236 b.WriteString("- installs candidate set #" + strconv.FormatUint(target, 10) + ":\n")
237 if !Exists(court) {
238 b.WriteString(" - _that court does not exist_\n\n")
239 return
240 }
241 tc := mustCourt(court)
242 if tc.mod == nil || tc.mod.candidates == nil ||
243 tc.mod.candidates.Get(beClaimKey(target)) == nil {
244 b.WriteString(" - **this candidate set does not exist yet — do not approve**\n\n")
245 return
246 }
247 cand := tc.mod.candidates.Get(beClaimKey(target)).(*modCandidate)
248 for _, a := range cand.members {
249 b.WriteString(" - " + sanitize.InlineText(a.String()) + "\n")
250 }
251 b.WriteString(" - threshold: " + strconv.Itoa(cand.m) + " of " +
252 strconv.Itoa(len(cand.members)) + " must agree\n")
253 } else if target != 0 {
254 b.WriteString("- claim: " + strconv.FormatUint(target, 10) + "\n")
255 }
256 b.WriteString("\n")
257}
258func modActLine(a modAct, kind string) string {
259 line := "- " + kind + " " + strconv.FormatUint(a.rowID, 10) +
260 " · block " + strconv.FormatInt(a.height, 10) + " · " +
261 sanitize.InlineText(a.code) + " · by " + sanitize.InlineText(a.actor.String())
262 switch {
263 case a.purged:
264 line += " — _[reason removed on legal grounds]_"
265 case a.reason != "":
266 line += " — " + sanitize.InlineText(a.reason)
267 }
268 return line
269}
270func renderModLog(slug string) string {
271 if !Exists(slug) {
272 return "## Not found\nNo court by that slug."
273 }
274 c := mustCourt(slug)
275 b := strings.Builder{}
276 b.WriteString("# Moderation log — " + courtNameFor(c) + "\n\n")
277 b.WriteString("Every moderation act on this court's CLAIMS and COMMENTS is recorded here: who acted, what they did, and when. Moderation controls what is listed; it never moves a coin or changes a verdict.\n\n")
278 b.WriteString("[Who holds these seats, and how to challenge them](/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" +
279 sanitize.InlineText(slug) + "/election)\n\n")
280 b.WriteString("Curation — which claims a moderator filed under which heading — is " +
281 "not in this list. The tree it produced is on the [court page](/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + sanitize.InlineText(slug) + "); the acts themselves are in " +
282 "the chain's event stream.\n\n")
283 if c.mod == nil {
284 b.WriteString("## Moderators\n\n1-of-1 — " + c.admin.String() +
285 " (the court's creator, until an election or an appointment changes it)\n\n")
286 b.WriteString("_No moderation acts yet._\n")
287 return b.String()
288 }
289 cm := c.mod
290 if cm.courtPurged {
291 b.WriteString("> **This court was removed on legal grounds** (" +
292 sanitize.InlineText(cm.courtTombstone) + "). No new claims may be opened; existing claims settle and pay out normally.\n\n")
293 }
294 b.WriteString("## Moderators\n\n")
295 if cm.n == 0 {
296 b.WriteString("_None — the set is empty; the court's holders can elect one._\n\n")
297 } else {
298 b.WriteString("- " + strconv.Itoa(cm.m) + " of " + strconv.Itoa(cm.n) +
299 " must agree for a moderation act\n")
300 if sm := speechM(cm); sm != cm.m {
301 b.WriteString("- " + strconv.Itoa(sm) + " of " + strconv.Itoa(cm.n) +
302 " for a **board** act — hiding a comment, slashing standing, " +
303 "freezing an address — because speech takes a wider agreement " +
304 "than the set's own threshold\n")
305 }
306 if cm.suspended {
307 b.WriteString("- **suspended** — they cannot take new moderation acts. " +
308 "The hides and freezes already in place stand until cleared, and " +
309 "any member may still lift their own\n")
310 }
311 if cm.installedByMeta {
312 b.WriteString("- seated by the review court; may not seed claims until a local election\n")
313 }
314 b.WriteString("\n")
315 }
316 pendingRows := []string{}
317 pendingSeen := 0
318 pendingMore := false
319 cm.pending.Iterate("", "", func(key string, v any) bool {
320 pendingSeen++
321 if a, ok := v.(*approval); ok {
322 deadline := a.openedAt + pendingTTLBlocks
323 if !approvalStale(a) {
324 when := " (block " + strconv.FormatInt(deadline, 10) + ")"
325 if at := deadlineTime(a.openedAtTime, blocksToSecs(pendingTTLBlocks)); at != 0 {
326 when = " " + strconv.FormatInt(at, 10) + when
327 }
328 pendingRows = append(pendingRows,
329 "- "+sanitize.InlineCode(key)+" — "+strconv.Itoa(a.approvers.Size())+
330 " of "+strconv.Itoa(cm.m)+" approved, expires at"+when)
331 }
332 }
333 if len(pendingRows) >= renderPageSize || pendingSeen >= renderPageSize {
334 pendingMore = true
335 return true
336 }
337 return false
338 })
339 if len(pendingRows) > 0 {
340 b.WriteString("## Awaiting approval\n\nProposals this set has started and not yet carried. An approval expires if the set does not reach its threshold in time; the reason is withheld until the act carries.\n\n")
341 for _, r := range pendingRows {
342 b.WriteString(r + "\n")
343 }
344 if pendingMore {
345 b.WriteString("- _…more not shown_\n")
346 }
347 b.WriteString("\n")
348 }
349 rows := []string{}
350 visited := 0
351 truncated := false
352 for i := len(cm.log) - 1; i >= 0 && len(rows) < renderPageSize; i-- {
353 rows = append(rows, modActLine(cm.log[i], "court row"))
354 }
355 if len(rows) >= renderPageSize {
356 truncated = true
357 }
358 cm.claims.ReverseIterate("", "", func(_ string, v any) bool {
359 if len(rows) >= renderPageSize {
360 truncated = true
361 return true
362 }
363 clm, ok := v.(*claimMod)
364 if !ok {
365 return false
366 }
367 visited++
368 for i := len(clm.log) - 1; i >= 0; i-- {
369 rows = append(rows, modActLine(clm.log[i], "row"))
370 if len(rows) >= renderPageSize {
371 truncated = true
372 return true
373 }
374 }
375 if visited >= renderPageSize {
376 truncated = true
377 return true
378 }
379 return false
380 })
381 b.WriteString("## Acts\n\n")
382 if len(rows) == 0 {
383 b.WriteString("_No moderation acts yet._\n")
384 return b.String()
385 }
386 for _, r := range rows {
387 b.WriteString(r + "\n")
388 }
389 if truncated {
390 b.WriteString("\n_Showing the " + strconv.Itoa(len(rows)) +
391 " most recent acts. This court has moderated " + strconv.Itoa(cm.claims.Size()) +
392 " claims; older acts are not shown._\n")
393 }
394 return b.String()
395}
396func appealVerbGloss(verb string) string {
397 switch verb {
398 case verbUnhide:
399 return "un-hides a claim its own court's moderators hid"
400 case verbClear:
401 return "clears a hide the review court itself placed"
402 case verbHide:
403 return "hides a claim across the platform, over its court's head"
404 case verbSuspend:
405 return "disarms a court's moderator set — they can take no further act"
406 case verbUnsuspend:
407 return "re-arms a suspended moderator set, and re-applies the hides that " +
408 "were masked while it was disarmed"
409 case verbSetmods:
410 return "installs a moderator set on that court, replacing whoever holds " +
411 "the seats now"
412 }
413 return "an action this page does not recognise"
414}