moderation.gno
19.85 Kb · 788 lines
1package kourtv3
2import (
3 "chain"
4 "strconv"
5 "strings"
6 bptree "gno.land/p/nt/bptree/v0"
7)
8const (
9 maxReasonLen = 200
10 maxModSetSize = 32
11 reSetWindowBlocks = int64(241_920)
12 modActEvent = "ModAct"
13 globalActEvent = "GlobalAct"
14 purgeEvent = "Purge"
15)
16type daoState struct {
17 admin address
18 members *bptree.BPTree
19 n int
20 purgeM int
21 pending *bptree.BPTree
22}
23var globalDAO *daoState
24const (
25 daoBootstrapN = 1
26 daoBootstrapM = 1
27)
28func ensureGlobalDAO() *daoState {
29 if globalDAO != nil {
30 return globalDAO
31 }
32 if directoryAdmin == "" {
33 panic("kourtv3: no court exists yet; the global DAO has no admin")
34 }
35 d := &daoState{
36 admin: directoryAdmin,
37 members: bptree.NewBPTree32(),
38 n: daoBootstrapN,
39 purgeM: daoBootstrapM,
40 pending: bptree.NewBPTree32(),
41 }
42 d.members.Set(directoryAdmin.String(), true)
43 globalDAO = d
44 return d
45}
46func isGlobalMod(who address) bool {
47 d := ensureGlobalDAO()
48 return d.members.Has(who.String())
49}
50func AddGlobalMod(cur realm, who address) {
51 if !cur.IsCurrent() {
52 panic(errStaleRealm)
53 }
54 d := ensureGlobalDAO()
55 if cur.Previous().Address() != d.admin {
56 panic("kourtv3: only the global DAO admin manages membership")
57 }
58 seatGlobalMember(d, who)
59}
60func seatGlobalMember(d *daoState, who address) {
61 if !who.IsValid() {
62 panic("kourtv3: not a valid address")
63 }
64 if d.members.Has(who.String()) {
65 return
66 }
67 d.members.Set(who.String(), true)
68 d.n++
69 clearPendingOnMembershipChange(&d.pending)
70}
71func RemoveGlobalMod(cur realm, who address) {
72 if !cur.IsCurrent() {
73 panic(errStaleRealm)
74 }
75 d := ensureGlobalDAO()
76 if cur.Previous().Address() != d.admin {
77 panic("kourtv3: only the global DAO admin manages membership")
78 }
79 if who == d.admin {
80 panic("kourtv3: the admin cannot be removed; transfer admin first")
81 }
82 if d.members.Has(who.String()) {
83 d.members.Remove(who.String())
84 d.n--
85 if d.purgeM > d.n {
86 d.purgeM = d.n
87 }
88 clearPendingOnMembershipChange(&d.pending)
89 }
90}
91func SetPurgeThreshold(cur realm, m int) {
92 if !cur.IsCurrent() {
93 panic(errStaleRealm)
94 }
95 d := ensureGlobalDAO()
96 if cur.Previous().Address() != d.admin {
97 panic("kourtv3: only the global DAO admin sets the purge threshold")
98 }
99 if m < 1 || m > d.n {
100 panic("kourtv3: the purge threshold must be in 1..n")
101 }
102 if m != d.purgeM {
103 clearPendingOnMembershipChange(&d.pending)
104 }
105 d.purgeM = m
106}
107func TransferGlobalAdmin(cur realm, to address) {
108 if !cur.IsCurrent() {
109 panic(errStaleRealm)
110 }
111 d := ensureGlobalDAO()
112 if cur.Previous().Address() != d.admin {
113 panic("kourtv3: only the global DAO admin transfers the seat")
114 }
115 seatGlobalMember(d, to)
116 d.admin = to
117}
118type courtMod struct {
119 members *bptree.BPTree
120 n int
121 m int
122 suspended bool
123 suspendedAt int64
124 suspendActByGlobal int64
125 suspendedSetID string
126 suspendedM int
127 setActHeight int64
128 lastElectionAt int64
129 installedByMeta bool
130 courtPurged bool
131 courtTombstone string
132 claims *bptree.BPTree
133 pending *bptree.BPTree
134 boardSanctionEpoch int64
135 log []modAct
136 logSeq uint64
137 folders *bptree.BPTree
138 folderSeq uint64
139 folderProps *bptree.BPTree
140 folderPropSeq uint64
141 candidates *bptree.BPTree
142 candSeq uint64
143 election *election
144 electionSeq uint64
145 electionCooldownUntil int64
146 electionCooldownUntilTime int64
147 creatorUnseated bool
148}
149type claimMod struct {
150 court bool
151 meta bool
152 global bool
153 purged bool
154 tombstone string
155 boardFrozenUntil int64
156 boardFrozenUntilTime int64
157 boardFrozenEpoch int64
158 boardFrozenGapUntil int64
159 boardFrozenGapUntilTime int64
160 boardPurgeAll bool
161 globalClearedAt int64
162 globalClearedAtTime int64
163 executedAt int64
164 executedAtTime int64
165 logSeq uint64
166 log []modAct
167}
168type modAct struct {
169 rowID uint64
170 actor address
171 code string
172 reason string
173 height int64
174 purged bool
175}
176func reSetWindowOpen(clearedAtTime, clearedAt, now int64) bool {
177 passed, known := pastDeadline(clearedAtTime, blocksToSecs(reSetWindowBlocks))
178 return (known && !passed) || (!known && now < clearedAt+reSetWindowBlocks)
179}
180const pendingTTLBlocks = decideWindowBlocks
181type approval struct {
182 approvers *bptree.BPTree
183 reason string
184 openedAt int64
185 openedAtTime int64
186}
187func approvalStale(a *approval) bool {
188 passed, known := pastDeadline(a.openedAtTime, blocksToSecs(pendingTTLBlocks))
189 return (known && passed) || (!known && heightNow() >= a.openedAt+pendingTTLBlocks)
190}
191func ensureMod(c *Court) *courtMod {
192 if c.mod != nil {
193 return c.mod
194 }
195 cm := &courtMod{
196 members: bptree.NewBPTree32(),
197 n: 1,
198 m: 1,
199 claims: bptree.NewBPTree32(),
200 pending: bptree.NewBPTree32(),
201 folders: bptree.NewBPTree32(),
202 }
203 cm.members.Set(c.admin.String(), true)
204 c.mod = cm
205 return cm
206}
207func AppointMods(cur realm, courtSlug string, m int, mods ...address) {
208 if !cur.IsCurrent() {
209 panic(errStaleRealm)
210 }
211 c := mustCourt(courtSlug)
212 if cur.Previous().Address() != c.admin {
213 panic("kourtv3: only the court creator appoints moderators (until an election)")
214 }
215 if c.mod != nil && c.mod.creatorUnseated {
216 panic("kourtv3: an election has installed a set; the creator's appointment power is spent")
217 }
218 if c.mod != nil && c.mod.suspended {
219 panic("kourtv3: this moderator set is suspended; only an election or a meta verdict may reinstate it")
220 }
221 canon := canonicalMembers(mods)
222 if len(canon) == 0 {
223 panic("kourtv3: a moderator set may not be empty")
224 }
225 if m < 1 || m > len(canon) {
226 panic("kourtv3: the m-of-n threshold must be in 1..n")
227 }
228 cm := ensureMod(c)
229 fresh := bptree.NewBPTree32()
230 n := 0
231 for _, a := range canon {
232 fresh.Set(a.String(), true)
233 n++
234 }
235 cm.members = fresh
236 cm.n = n
237 cm.m = m
238 cm.setActHeight = heightNow()
239 cm.installedByMeta = false
240 clearPendingOnMembershipChange(&cm.pending)
241}
242func requireMod(cm *courtMod, who address) {
243 if !cm.members.Has(who.String()) {
244 panic("kourtv3: not a moderator of this court")
245 }
246}
247func clearPendingOnMembershipChange(pending **bptree.BPTree) {
248 *pending = bptree.NewBPTree32()
249}
250func requireActiveMod(cm *courtMod, who address) {
251 requireMod(cm, who)
252 if cm.suspended {
253 panic("kourtv3: this moderator set is suspended; only clearing its own bits is allowed")
254 }
255}
256func isActiveMod(cm *courtMod, who address) bool {
257 return !cm.suspended && cm.members.Has(who.String())
258}
259func requireEdgeRemover(c *Court, author, who address) {
260 if author == who {
261 return
262 }
263 cm := ensureMod(c)
264 if !cm.members.Has(who.String()) {
265 panic("kourtv3: only the edge's author or a court moderator may remove it")
266 }
267 requireActiveMod(cm, who)
268}
269func mustCategoryCode(code string) {
270 if len(code) == 0 || runeLen(code) > maxReasonLen {
271 panic("kourtv3: purge needs a statutory category code")
272 }
273}
274func checkReason(reason string) {
275 if runeLen(reason) > maxReasonLen {
276 panic("kourtv3: a moderation reason is at most 200 characters")
277 }
278}
279func ensureClaimMod(c *Court, cm *courtMod, claimID uint64) *claimMod {
280 _ = mustClaim(c, claimID)
281 k := beClaimKey(claimID)
282 if v := cm.claims.Get(k); v != nil {
283 return v.(*claimMod)
284 }
285 clm := &claimMod{}
286 cm.claims.Set(k, clm)
287 return clm
288}
289func lookupClaimMod(cm *courtMod, claimID uint64) *claimMod {
290 if v := cm.claims.Get(beClaimKey(claimID)); v != nil {
291 return v.(*claimMod)
292 }
293 return nil
294}
295func (clm *claimMod) appendLog(actor address, code, reason string) uint64 {
296 clm.logSeq++
297 clm.log = append(clm.log, modAct{
298 rowID: clm.logSeq,
299 actor: actor,
300 code: code,
301 reason: reason,
302 height: heightNow(),
303 })
304 return clm.logSeq
305}
306func (cm *courtMod) appendCourtLog(actor address, code, reason string) uint64 {
307 cm.logSeq++
308 cm.log = append(cm.log, modAct{
309 rowID: cm.logSeq,
310 actor: actor,
311 code: code,
312 reason: reason,
313 height: heightNow(),
314 })
315 return cm.logSeq
316}
317func approveAction(pending *bptree.BPTree, key string, who address, reason string, m int) (bool, string) {
318 now := heightNow()
319 var a *approval
320 if v := pending.Get(key); v != nil {
321 a = v.(*approval)
322 if approvalStale(a) {
323 a = nil
324 }
325 }
326 if a == nil {
327 a = &approval{approvers: bptree.NewBPTree32(), reason: reason,
328 openedAt: now, openedAtTime: nowTime()}
329 }
330 if a.approvers.Has(who.String()) {
331 return false, ""
332 }
333 a.approvers.Set(who.String(), true)
334 if a.approvers.Size() >= m {
335 pending.Remove(key)
336 return true, a.reason
337 }
338 pending.Set(key, a)
339 return false, ""
340}
341func pendingOpenedAt(pending *bptree.BPTree, key string) int64 {
342 v := pending.Get(key)
343 if v == nil {
344 return 0
345 }
346 a := v.(*approval)
347 if approvalStale(a) {
348 return 0
349 }
350 return a.openedAt
351}
352func HideItem(cur realm, courtSlug string, claimID uint64, reason string) {
353 if !cur.IsCurrent() {
354 panic(errStaleRealm)
355 }
356 who := cur.Previous().Address()
357 c := mustCourt(courtSlug)
358 cm := ensureMod(c)
359 requireActiveMod(cm, who)
360 checkReason(reason)
361 _ = mustClaim(c, claimID)
362 if clm := lookupClaimMod(cm, claimID); clm != nil && clm.executedAt != 0 {
363 if passed, known := pastDeadline(clm.executedAtTime, blocksToSecs(c.params.votingBlocks)); (known && !passed) ||
364 (!known && heightNow() < clm.executedAt+c.params.votingBlocks) {
365 panic("kourtv3: a review verdict just cleared this; the set may not re-hide yet")
366 }
367 }
368 fire, r := approveAction(cm.pending, "hide:"+strconv.FormatUint(claimID, 10), who, reason, cm.m)
369 if !fire {
370 return
371 }
372 clm := ensureClaimMod(c, cm, claimID)
373 clm.court = true
374 clm.appendLog(who, "hide", r)
375 emitModAct(c.id, claimID, "hide", who)
376}
377func UnhideItem(cur realm, courtSlug string, claimID uint64, reason string) {
378 if !cur.IsCurrent() {
379 panic(errStaleRealm)
380 }
381 who := cur.Previous().Address()
382 c := mustCourt(courtSlug)
383 cm := ensureMod(c)
384 requireMod(cm, who)
385 checkReason(reason)
386 clm := lookupClaimMod(cm, claimID)
387 if clm == nil || !clm.court {
388 panic("kourtv3: this court is not hiding that claim")
389 }
390 fire, r := approveAction(cm.pending, "unhide:"+strconv.FormatUint(claimID, 10), who, reason, cm.m)
391 if !fire {
392 return
393 }
394 clm.court = false
395 clm.appendLog(who, "unhide", r)
396 emitModAct(c.id, claimID, "unhide", who)
397}
398func GlobalHide(cur realm, courtSlug string, claimID uint64, categoryCode string) {
399 if !cur.IsCurrent() {
400 panic(errStaleRealm)
401 }
402 who := cur.Previous().Address()
403 if !isGlobalMod(who) {
404 panic("kourtv3: only a global DAO member may global-hide")
405 }
406 checkReason(categoryCode)
407 c := mustCourt(courtSlug)
408 cm := ensureMod(c)
409 _ = mustClaim(c, claimID)
410 now := heightNow()
411 if prev := lookupClaimMod(cm, claimID); prev != nil && !prev.global &&
412 prev.globalClearedAt != 0 && reSetWindowOpen(prev.globalClearedAtTime, prev.globalClearedAt, now) {
413 d := ensureGlobalDAO()
414 fire, _ := approveAction(d.pending, "reset:"+c.id+":"+strconv.FormatUint(claimID, 10), who, categoryCode, d.purgeM)
415 if !fire {
416 return
417 }
418 }
419 clm := ensureClaimMod(c, cm, claimID)
420 clm.global = true
421 clm.appendLog(who, "global-hide:"+categoryCode, "")
422 emitGlobalAct(c.id, claimID, "global-hide", who)
423}
424func GlobalClear(cur realm, courtSlug string, claimID uint64) {
425 if !cur.IsCurrent() {
426 panic(errStaleRealm)
427 }
428 who := cur.Previous().Address()
429 if !isGlobalMod(who) {
430 panic("kourtv3: only a global DAO member may clear a global hide")
431 }
432 c := mustCourt(courtSlug)
433 cm := ensureMod(c)
434 clm := lookupClaimMod(cm, claimID)
435 if clm == nil || !clm.global {
436 panic("kourtv3: no global hide on that claim")
437 }
438 clm.global = false
439 clm.globalClearedAt = heightNow()
440 clm.globalClearedAtTime = nowTime()
441 clm.appendLog(who, "global-clear", "")
442 emitGlobalAct(c.id, claimID, "global-clear", who)
443}
444func ClearAnyBit(cur realm, courtSlug string, claimID uint64) {
445 if !cur.IsCurrent() {
446 panic(errStaleRealm)
447 }
448 who := cur.Previous().Address()
449 if !isGlobalMod(who) {
450 panic("kourtv3: only a global DAO member may clear bits (recovery)")
451 }
452 c := mustCourt(courtSlug)
453 cm := ensureMod(c)
454 clm := lookupClaimMod(cm, claimID)
455 if clm == nil {
456 panic("kourtv3: nothing to clear on that claim")
457 }
458 clm.court = false
459 clm.meta = false
460 clm.appendLog(who, "clear-any", "")
461 emitGlobalAct(c.id, claimID, "clear-any", who)
462}
463func ClearCourtSuspension(cur realm, courtSlug string) {
464 if !cur.IsCurrent() {
465 panic(errStaleRealm)
466 }
467 who := cur.Previous().Address()
468 if !isGlobalMod(who) {
469 panic("kourtv3: only a global DAO member may clear a suspension (recovery)")
470 }
471 c := mustCourt(courtSlug)
472 cm := ensureMod(c)
473 if !cm.suspended {
474 panic("kourtv3: that court's set is not suspended")
475 }
476 cm.suspended = false
477 cm.suspendActByGlobal = heightNow()
478 emitGlobalAct(c.id, 0, "clear-suspension", who)
479}
480func PurgeClaim(cur realm, courtSlug string, claimID uint64, categoryCode string) {
481 if !cur.IsCurrent() {
482 panic(errStaleRealm)
483 }
484 who := cur.Previous().Address()
485 d := ensureGlobalDAO()
486 if !d.members.Has(who.String()) {
487 panic("kourtv3: only a global DAO member may purge")
488 }
489 mustCategoryCode(categoryCode)
490 c := mustCourt(courtSlug)
491 cm := ensureMod(c)
492 _ = mustClaim(c, claimID)
493 fire, code := approveAction(d.pending, "purge:"+c.id+":"+strconv.FormatUint(claimID, 10), who, categoryCode, d.purgeM)
494 if !fire {
495 return
496 }
497 clm := ensureClaimMod(c, cm, claimID)
498 clm.purged = true
499 clm.tombstone = code
500 if c.id == metaSlug {
501 poisonMetaParse(claimID)
502 }
503 clm.appendLog(who, "purge:"+code, "")
504 emitPurge(c.id, claimID, code, who)
505}
506func GlobalSuspendSet(cur realm, courtSlug string) {
507 if !cur.IsCurrent() {
508 panic(errStaleRealm)
509 }
510 who := cur.Previous().Address()
511 d := ensureGlobalDAO()
512 if !d.members.Has(who.String()) {
513 panic("kourtv3: only a global DAO member may suspend a moderator set")
514 }
515 c := mustCourt(courtSlug)
516 cm := ensureMod(c)
517 if cm.suspended {
518 panic("kourtv3: that set is already suspended")
519 }
520 fire, _ := approveAction(d.pending, "gsuspend:"+c.id, who, "", d.purgeM)
521 if !fire {
522 return
523 }
524 suspendSet(c, who)
525 cm.suspendActByGlobal = heightNow()
526 emitGlobalAct(c.id, 0, "global-suspend-set", who)
527}
528func ResetModSet(cur realm, courtSlug string) {
529 if !cur.IsCurrent() {
530 panic(errStaleRealm)
531 }
532 who := cur.Previous().Address()
533 d := ensureGlobalDAO()
534 if !d.members.Has(who.String()) {
535 panic("kourtv3: only a global DAO member may reset a moderator set")
536 }
537 c := mustCourt(courtSlug)
538 cm := ensureMod(c)
539 fire, _ := approveAction(d.pending, "resetmods:"+c.id, who, "", d.purgeM)
540 if !fire {
541 return
542 }
543 cm.members = bptree.NewBPTree32()
544 cm.n = 0
545 cm.m = 1
546 clearPendingOnMembershipChange(&cm.pending)
547 cm.suspended = false
548 cm.boardSanctionEpoch++
549 cm.setActHeight = heightNow()
550 emitGlobalAct(c.id, 0, "reset-mod-set", who)
551}
552func PurgeCourt(cur realm, courtSlug string, categoryCode string) {
553 if !cur.IsCurrent() {
554 panic(errStaleRealm)
555 }
556 who := cur.Previous().Address()
557 d := ensureGlobalDAO()
558 if !d.members.Has(who.String()) {
559 panic("kourtv3: only a global DAO member may purge")
560 }
561 mustCategoryCode(categoryCode)
562 c := mustCourt(courtSlug)
563 cm := ensureMod(c)
564 fire, code := approveAction(d.pending, "purgecourt:"+c.id, who, categoryCode, d.purgeM)
565 if !fire {
566 return
567 }
568 cm.courtPurged = true
569 cm.courtTombstone = code
570 emitPurge(c.id, 0, code, who)
571}
572func courtIsPurged(c *Court) bool {
573 return c.mod != nil && c.mod.courtPurged
574}
575func CourtPurged(courtSlug string) bool {
576 return courtIsPurged(mustCourt(courtSlug))
577}
578func setMetaBit(c *Court, claimID uint64, by address) {
579 cm := ensureMod(c)
580 clm := ensureClaimMod(c, cm, claimID)
581 clm.meta = true
582 clm.executedAt = heightNow()
583 clm.executedAtTime = nowTime()
584 clm.appendLog(by, "meta-hide", "")
585 emitModAct(c.id, claimID, "meta-hide", by)
586}
587func clearMetaBit(c *Court, claimID uint64, by address) {
588 cm := ensureMod(c)
589 clm := lookupClaimMod(cm, claimID)
590 if clm == nil || !clm.meta {
591 return
592 }
593 clm.meta = false
594 clm.appendLog(by, "meta-clear", "")
595 emitModAct(c.id, claimID, "meta-clear", by)
596}
597func clearCourtBitByMeta(c *Court, claimID uint64, by address) {
598 cm := ensureMod(c)
599 clm := lookupClaimMod(cm, claimID)
600 if clm == nil || !clm.court {
601 return
602 }
603 clm.court = false
604 clm.executedAt = heightNow()
605 clm.executedAtTime = nowTime()
606 clm.appendLog(by, "meta-unhide", "")
607 emitModAct(c.id, claimID, "meta-unhide", by)
608}
609func suspendSet(c *Court, by address) {
610 cm := ensureMod(c)
611 cm.suspended = true
612 cm.suspendedAt = heightNow()
613 cm.suspendedSetID = currentSetID(cm)
614 cm.suspendedM = cm.m
615 emitModAct(c.id, 0, "suspend", by)
616}
617func currentSetID(cm *courtMod) string {
618 var b strings.Builder
619 cm.members.Iterate("", "", func(k string, _ any) bool {
620 b.WriteString(k)
621 b.WriteByte(',')
622 return false
623 })
624 return b.String()
625}
626func unsuspendSet(c *Court, by address) {
627 cm := ensureMod(c)
628 cm.suspended = false
629 emitModAct(c.id, 0, "unsuspend", by)
630}
631func HiddenFromListing(courtSlug string, claimID uint64) bool {
632 c := mustCourt(courtSlug)
633 if c.mod == nil {
634 return false
635 }
636 clm := lookupClaimMod(c.mod, claimID)
637 if clm == nil {
638 return false
639 }
640 return clm.court || clm.meta || clm.global
641}
642func TextRedacted(courtSlug string, claimID uint64) bool {
643 c := mustCourt(courtSlug)
644 if c.mod == nil {
645 return false
646 }
647 clm := lookupClaimMod(c.mod, claimID)
648 if clm == nil {
649 return false
650 }
651 return clm.global || clm.purged
652}
653func ClaimPurged(courtSlug string, claimID uint64) bool {
654 c := mustCourt(courtSlug)
655 if c.mod == nil {
656 return false
657 }
658 clm := lookupClaimMod(c.mod, claimID)
659 return clm != nil && clm.purged
660}
661func PurgeModLogRow(cur realm, courtSlug string, claimID, rowID uint64, categoryCode string) {
662 if !cur.IsCurrent() {
663 panic(errStaleRealm)
664 }
665 who := cur.Previous().Address()
666 d := ensureGlobalDAO()
667 if !d.members.Has(who.String()) {
668 panic("kourtv3: only a global DAO member may purge")
669 }
670 mustCategoryCode(categoryCode)
671 c := mustCourt(courtSlug)
672 cm := ensureMod(c)
673 clm := lookupClaimMod(cm, claimID)
674 if clm == nil {
675 panic("kourtv3: that claim has no moderation log")
676 }
677 idx := -1
678 for i := range clm.log {
679 if clm.log[i].rowID == rowID {
680 idx = i
681 break
682 }
683 }
684 if idx < 0 {
685 panic("kourtv3: no such moderation-log row")
686 }
687 if clm.log[idx].purged {
688 return
689 }
690 key := "purgerow:" + c.id + ":" + strconv.FormatUint(claimID, 10) + ":" +
691 strconv.FormatUint(rowID, 10)
692 fire, code := approveAction(d.pending, key, who, categoryCode, d.purgeM)
693 if !fire {
694 return
695 }
696 clm.log[idx].purged = true
697 clm.log[idx].reason = ""
698 clm.appendLog(who, "purgerow:"+code, "")
699 emitPurge(c.id, claimID, code, who)
700}
701func PendingApproval(courtSlug, actionKey string) (approvals int, expiresAt, expiresAtHeight int64) {
702 var pending *bptree.BPTree
703 if courtSlug == "" {
704 if globalDAO == nil {
705 return 0, 0, 0
706 }
707 pending = globalDAO.pending
708 } else {
709 c := mustCourt(courtSlug)
710 if c.mod == nil {
711 return 0, 0, 0
712 }
713 pending = c.mod.pending
714 }
715 v := pending.Get(actionKey)
716 if v == nil {
717 return 0, 0, 0
718 }
719 a := v.(*approval)
720 if approvalStale(a) {
721 return 0, 0, 0
722 }
723 return a.approvers.Size(),
724 deadlineTime(a.openedAtTime, blocksToSecs(pendingTTLBlocks)),
725 a.openedAt + pendingTTLBlocks
726}
727func CourtSuspended(courtSlug string) bool {
728 c := mustCourt(courtSlug)
729 return c.mod != nil && c.mod.suspended
730}
731func IsCourtMod(courtSlug string, who address) bool {
732 c := mustCourt(courtSlug)
733 if c.mod == nil {
734 return who == c.admin
735 }
736 return c.mod.members.Has(who.String())
737}
738func ModThreshold(courtSlug string) (m, n int) {
739 c := mustCourt(courtSlug)
740 if c.mod == nil {
741 return 1, 1
742 }
743 return c.mod.m, c.mod.n
744}
745func GlobalModCount() int {
746 if globalDAO == nil {
747 return 0
748 }
749 return globalDAO.n
750}
751func ModLogLen(courtSlug string, claimID uint64) int {
752 c := mustCourt(courtSlug)
753 if c.mod == nil {
754 return 0
755 }
756 clm := lookupClaimMod(c.mod, claimID)
757 if clm == nil {
758 return 0
759 }
760 return len(clm.log)
761}
762func emitModAct(slug string, claimID uint64, code string, actor address) {
763 chain.Emit(modActEvent,
764 "court", slug,
765 "claim", strconv.FormatUint(claimID, 10),
766 "act", code,
767 "by", actor.String(),
768 "height", eventHeight(),
769 )
770}
771func emitGlobalAct(slug string, claimID uint64, code string, actor address) {
772 chain.Emit(globalActEvent,
773 "court", slug,
774 "claim", strconv.FormatUint(claimID, 10),
775 "act", code,
776 "by", actor.String(),
777 "height", eventHeight(),
778 )
779}
780func emitPurge(slug string, claimID uint64, categoryCode string, actor address) {
781 chain.Emit(purgeEvent,
782 "court", slug,
783 "claim", strconv.FormatUint(claimID, 10),
784 "code", categoryCode,
785 "by", actor.String(),
786 "height", eventHeight(),
787 )
788}