memba_appstore_reviews_v1.gno
5.51 Kb · 147 lines
1// Package memba_appstore_reviews_v1 is the dedicated on-chain reviews / web-of-trust realm for
2// Memba App Store listings. Subject = an app's package path (e.g. gno.land/r/samcrew/…).
3//
4// It is a THIN wrapper over the shared, audited p/samcrew/reviews engine (extracted from the
5// DoS-hardened memba_reviews_v2: RV-1 O(1) subject summary, RV-3 flaggedIDs GC). This realm
6// owns one Store and supplies only what a package cannot: the caller (unsafe.PreviousRealm),
7// the block height (runtime.ChainHeight), the moderator-multisig gate on takedowns, and a
8// gnoweb Render() view. The ABI mirrors memba_reviews_v2 exactly so the frontend reviews
9// client can target this realm by path with no code change.
10//
11// WHY DEDICATED (not the shared memba_reviews realm): reputation isolation. memba_reviews keeps
12// a global per-author reputation; sharing it would let sockpuppet down-votes on an app review
13// bleed into someone's validator web-of-trust. A separate Store + moderator wall that off.
14package memba_appstore_reviews_v1
15
16import (
17 "strings"
18
19 "chain/runtime"
20 "chain/runtime/unsafe"
21
22 "gno.land/p/nt/ufmt/v0"
23
24 reviews "gno.land/p/samcrew/memba_reviews_core_v1"
25)
26
27// ModeratorAddress is the Memba moderator multisig (samcrew-core-test1 on testnet).
28// CONFIRM before deploy — takedowns (Hide*/Unhide) are gated to exactly this address.
29const ModeratorAddress = "g1x7k4628w93a7wzdhqc06atzx0v50rnshweuxu0"
30
31var store *reviews.Store
32
33func init() { store = reviews.NewStore() }
34
35func caller() address { return unsafe.PreviousRealm().Address() }
36
37func height() int64 { return runtime.ChainHeight() }
38
39func assertModerator() {
40 if caller() != address(ModeratorAddress) {
41 panic("unauthorized: moderator multisig only")
42 }
43}
44
45// ── Writes ───────────────────────────────────────────────────
46
47// PostReview creates or in-place-updates the caller's review for `subject` (one per pair).
48func PostReview(cur realm, subject string, rating int, body string) {
49 store.PostReview(caller(), subject, rating, body, height())
50}
51
52// EditReview updates the caller's existing review. Author only.
53func EditReview(cur realm, reviewID uint64, rating int, body string) {
54 store.EditReview(caller(), reviewID, rating, body, height())
55}
56
57// DeleteReview tombstones the caller's review. Author only.
58func DeleteReview(cur realm, reviewID uint64) {
59 store.DeleteReview(caller(), reviewID)
60}
61
62// PostComment posts a flat reply to a review.
63func PostComment(cur realm, reviewID uint64, body string) {
64 store.PostComment(caller(), reviewID, body, height())
65}
66
67// EditComment updates the caller's comment. Author only.
68func EditComment(cur realm, commentID uint64, body string) {
69 store.EditComment(caller(), commentID, body, height())
70}
71
72// DeleteComment tombstones the caller's comment. Author only.
73func DeleteComment(cur realm, commentID uint64) {
74 store.DeleteComment(caller(), commentID)
75}
76
77// React toggles a like/dislike on a review or comment. Self-reactions rejected.
78func React(cur realm, targetID uint64, kind string) {
79 store.React(caller(), targetID, kind)
80}
81
82// Flag records one community flag per account per target.
83func Flag(cur realm, targetID uint64) {
84 store.Flag(caller(), targetID)
85}
86
87// ── Moderation (multisig only) ───────────────────────────────
88
89// HideReview soft-deletes a review. Moderator multisig only.
90func HideReview(cur realm, id uint64) {
91 assertModerator()
92 store.HideReview(id)
93}
94
95// HideComment soft-deletes a comment. Moderator multisig only.
96func HideComment(cur realm, id uint64) {
97 assertModerator()
98 store.HideComment(id)
99}
100
101// Unhide reverses a hide on a review or comment. Moderator multisig only.
102func Unhide(cur realm, targetID uint64) {
103 assertModerator()
104 store.Unhide(targetID)
105}
106
107// ── Reads (vm/qeval) ─────────────────────────────────────────
108
109// GetReviewsJSON returns a subject's non-hidden reviews (paginated JSON array).
110func GetReviewsJSON(subject string, offset, limit int) string {
111 return store.GetReviewsJSON(subject, offset, limit)
112}
113
114// GetCommentsJSON returns a review's non-hidden comments (paginated JSON array).
115func GetCommentsJSON(reviewID uint64, offset, limit int) string {
116 return store.GetCommentsJSON(reviewID, offset, limit)
117}
118
119// GetSubjectSummaryJSON returns {"count","average","sum"} over a subject's visible reviews (O(1)).
120func GetSubjectSummaryJSON(subject string) string {
121 return store.SubjectSummaryJSON(subject)
122}
123
124// GetReputation returns an address's net likes−dislikes reputation (0 if unknown).
125func GetReputation(addr string) int64 {
126 return store.GetReputation(addr)
127}
128
129// GetFlaggedJSON returns flagged target IDs for the moderator dashboard (paginated).
130func GetFlaggedJSON(offset, limit int) string {
131 return store.GetFlaggedJSON(offset, limit)
132}
133
134// ── Render (gnoweb) ──────────────────────────────────────────
135
136// Render — human gnoweb view. "" → home (total count); "s/<subject>" → subject review list.
137func Render(path string) string {
138 if path == "" {
139 return ufmt.Sprintf("# Memba App Reviews\n\nOn-chain reviews for App Store listings. %d reviews total.\n",
140 store.ReviewCount())
141 }
142 if strings.HasPrefix(path, "s/") {
143 subject := strings.TrimPrefix(path, "s/")
144 return "# Reviews for " + reviews.SanitizeForRender(subject) + "\n\n" + store.RenderSubject(subject)
145 }
146 return "# 404\n"
147}