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

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}