reviews_host.gno
4.75 Kb · 155 lines
1package memba_weighted_host
2
3import "gno.land/p/samcrew/memba_weighted_policy"
4
5// ReviewsItem contains copied moderation-relevant state only. Ordinary reactions
6// do not invalidate moderation; content edits, deletion and visibility do.
7type ReviewsItem struct {
8 ID uint64
9 Review bool
10 ParentID uint64
11 Subject string
12 Author address
13 Rating int
14 BodyHash string
15 CreatedAt int64
16 EditedAt int64
17 Hidden bool
18 Deleted bool
19 Flagged bool
20}
21type ReviewsState struct {
22 Moderator address
23 PendingModerator address
24 Item ReviewsItem
25}
26type reviewsProposal struct {
27 action ReviewsAction
28 before ReviewsState
29}
30
31func attachReviews(h *Host, policy ReviewsPolicy, rlm realm) *Host {
32 policy = NewReviewsPolicy(policy.DAO(), policy.Target(), policy.Successor())
33 if rlm.Address() != policy.DAO() {
34 panic("reviews policy DAO does not own this host")
35 }
36 h.reviewsPolicy = &policy
37 return h
38}
39func NewWithReviews(seats []Seat, policy ReviewsPolicy, rlm realm) *Host {
40 return attachReviews(New(seats, rlm), policy, rlm)
41}
42func NewWithMarketAndReviews(seats []Seat, market MarketPolicy, reviews ReviewsPolicy, rlm realm) *Host {
43 return attachReviews(NewWithMarket(seats, market, rlm), reviews, rlm)
44}
45func reviewsCommitment(action ReviewsAction, before ReviewsState) string {
46 return action.Bytes() + "|before|" + reviewsStateJSON(before)
47}
48func (h *Host) validateReviews(a ReviewsAction, before ReviewsState) {
49 if h.reviewsPolicy == nil {
50 panic("reviews actions are not configured")
51 }
52 a.Bytes()
53 p := h.reviewsPolicy
54 if a.Target() != p.Target() {
55 panic("reviews target does not match policy")
56 }
57 if !before.Moderator.IsValid() || (before.PendingModerator != "" && !before.PendingModerator.IsValid()) {
58 panic("invalid reviews authority pre-state")
59 }
60 if a.ID() == 0 && before.Item != (ReviewsItem{}) {
61 panic("unexpected reviews item pre-state")
62 }
63 if a.Kind() == ReviewsAcceptModerator {
64 if before.Moderator == p.DAO() || before.PendingModerator != p.DAO() {
65 panic("DAO is not pending reviews moderator")
66 }
67 return
68 }
69 if before.Moderator != p.DAO() {
70 panic("DAO is not reviews moderator")
71 }
72 if a.Kind() == ReviewsAbortReturn {
73 if a.Recipient() != p.Successor() || before.PendingModerator != p.Successor() {
74 panic("reviews return does not match policy")
75 }
76 return
77 }
78 if before.PendingModerator != "" {
79 panic("reviews authority transfer is pending")
80 }
81 if a.Kind() == ReviewsReturnModerator {
82 if a.Recipient() != p.Successor() {
83 panic("reviews successor does not match policy")
84 }
85 return
86 }
87 item := before.Item
88 if item.ID != a.ID() || item.ID == 0 {
89 panic("reviews item not found")
90 }
91 // Legacy v1 retains flagged-index entries after deletion. Unhide may
92 // clear only such a flagged tombstone; the adapter preserves Deleted.
93 if item.Deleted && !(a.Kind() == ReviewsUnhide && item.Flagged) {
94 panic("reviews item is deleted")
95 }
96 if !item.Author.IsValid() || len(item.BodyHash) != 64 {
97 panic("invalid reviews item pre-state")
98 }
99 if item.Review {
100 if item.ParentID != 0 || item.Subject == "" || item.Rating < 1 || item.Rating > 5 {
101 panic("invalid review pre-state")
102 }
103 } else if item.ParentID == 0 || item.Subject != "" || item.Rating != 0 {
104 panic("invalid comment pre-state")
105 }
106 switch a.Kind() {
107 case ReviewsHideReview:
108 if !item.Review {
109 panic("reviews item is not a review")
110 }
111 if item.Hidden {
112 panic("reviews hide is a no-op")
113 }
114 case ReviewsHideComment:
115 if item.Review {
116 panic("reviews item is not a comment")
117 }
118 if item.Hidden {
119 panic("reviews hide is a no-op")
120 }
121 case ReviewsUnhide:
122 if !item.Hidden && !item.Flagged {
123 panic("reviews unhide is a no-op")
124 }
125 default:
126 panic("unsupported reviews action")
127 }
128}
129func (h *Host) ProposeReviews(action ReviewsAction, before ReviewsState, rlm realm) uint64 {
130 who := assertCaller(h, rlm)
131 h.validateReviews(action, before)
132 p := &roleProposal{proposer: who, reviews: &reviewsProposal{action: action, before: before}}
133 p.ballot = h.policy.NewProposal(action.Category(), reviewsCommitment(action, before))
134 h.addProposal(p)
135 return uint64(len(h.proposals))
136}
137
138// Only the owning realm's fixed adapter may use this. Keep Host private and
139// let target panics/postcondition failures abort the entire transaction.
140func (h *Host) ConsumeReviews(id uint64, before ReviewsState, rlm realm) ReviewsAction {
141 assertCaller(h, rlm)
142 p := h.proposal(id)
143 if p.reviews == nil {
144 panic("proposal is not a reviews action")
145 }
146 h.validateReviews(p.reviews.action, before)
147 p.ballot.Consume(reviewsCommitment(p.reviews.action, before))
148 members := []memba_weighted_policy.Member{}
149 for _, seat := range h.seats {
150 members = append(members, seat.Member)
151 }
152 h.policy.Reconfigure(members)
153 h.recordInvalidation(InvalidatedByExecution, id, h.reviewsPolicy.Target())
154 return p.reviews.action
155}