app.gno
7.44 Kb · 250 lines
1// Package directory is a Remix Lab "community-directory" app generated
2// from a project configuration. Entries are submitted through a fixed form;
3// reviewers accept or reject them, and accepted entries are listed publicly.
4//
5// Every submission is public chain data, including entries that are later
6// rejected. Rejected entries are not shown on the rendered pages.
7//
8// Authority model: every write derives the actor from cur.Previous(), the
9// immediate caller. An intermediary realm calling on a user's behalf acts as
10// itself and gains none of that user's roles. Block state lives in
11// unexported variables and only inert views leave the realm.
12package directory
13
14import (
15 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/core/v1"
16 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/forms/v1"
17 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/membership/v1"
18 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/policies/v1"
19)
20
21const (
22 pkgPath = "gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/directory"
23 appTitle = "Community directory"
24 recipeID = "community-directory"
25 recipeVersion = "1.0.0"
26 pageSize = 25
27)
28
29// titleIndex is the form field shown as each entry's title.
30const titleIndex = 0
31
32// Operation names checked against the policy.
33const (
34 opInvite = "members.invite"
35 opRemove = "members.remove"
36 opSetRole = "members.set_role"
37 opSubmit = "entry.submit"
38 opReview = "entry.review"
39 opPolicy = "policy.set"
40)
41
42var (
43 members *membership.Set
44 policy *policies.Policy
45 entries *forms.Form
46)
47
48// init captures the deployer as the first owner. A realm whose deployer is
49// not a user stays uninitialized and rejects every write; there is no
50// separate initialization call that someone else could front-run.
51func init(cur realm) {
52 deployer := cur.Previous()
53 if deployer.IsUser() && deployer.Address().IsValid() {
54 setup(deployer.Address())
55 }
56}
57
58func setup(owner address) {
59 members = membership.New(membership.Config{
60 JoinMode: membership.JoinInvitation,
61 MaxMembers: 50,
62 }, owner)
63
64 policy = policies.New(members)
65 policy.Define(opInvite, policies.RuleOwner, policies.RuleReviewer)
66 policy.Define(opRemove, policies.RuleOwner, policies.RuleOwner)
67 policy.Define(opSetRole, policies.RuleOwner, policies.RuleOwner)
68 policy.Define(opSubmit, policies.RulePublic, policies.RulePublic)
69 policy.Define(opReview, policies.RuleReviewer, policies.RuleReviewer)
70 policy.Define(opPolicy, policies.RuleOwner, policies.RuleOwner)
71
72 entries = forms.New(forms.Config{
73 MaxSubmissions: 100,
74 MaxPerSubmitter: 3,
75 MaxPayloadBytes: 1024,
76 }, []forms.Field{
77 {Name: "name", Label: "Project name", Kind: forms.KindText, Required: true, MaxBytes: 80},
78 {Name: "link", Label: "Realm or repository", Kind: forms.KindText, Required: true, MaxBytes: 200},
79 {Name: "category", Label: "Category", Kind: forms.KindChoice, Required: true, Choices: []string{"Realm", "Library", "Tool", "Other"}},
80 {Name: "since", Label: "Year started", Kind: forms.KindInteger, Required: false, Min: 2000, Max: 2100},
81 {Name: "summary", Label: "Summary", Kind: forms.KindText, Required: false, MaxBytes: 500, Multiline: true},
82 })
83}
84
85// --- Entries ---
86
87// Submit stores an entry for review and returns its ID. Every answer is
88// public. Leave optional fields empty.
89// - name: required text, at most 80 bytes
90// - link: required text, at most 200 bytes
91// - category: required, one of the field's 4 choices
92// - since: optional integer from 2000 to 2100
93// - summary: optional multi-line text, at most 500 bytes
94func Submit(cur realm, name string, link string, category string, since string, summary string) uint64 {
95 actor := authorize(cur, opSubmit)
96 id, err := entries.Submit(actor, []string{name, link, category, since, summary})
97 must(err)
98 return id
99}
100
101// Accept lists a submitted entry publicly. Review decisions are final.
102func Accept(cur realm, id uint64) {
103 authorize(cur, opReview)
104 must(entries.Review(id, core.SubmissionAccepted))
105}
106
107// Reject declines a submitted entry. Review decisions are final.
108func Reject(cur realm, id uint64) {
109 authorize(cur, opReview)
110 must(entries.Review(id, core.SubmissionRejected))
111}
112
113// --- Membership ---
114
115// Invite lets addr join with role. Inviting a reviewer or owner also
116// requires the members.set_role permission.
117func Invite(cur realm, addr address, role string) {
118 actor := authorize(cur, opInvite)
119 r, err := core.ParseRole(role)
120 must(err)
121 if r > core.RoleMember {
122 must(policy.Authorize(opSetRole, actor))
123 }
124 must(members.Invite(addr, r))
125}
126
127// RevokeInvite cancels a pending invitation. Revoking an invitation for a
128// reviewer or owner also requires the members.set_role permission.
129func RevokeInvite(cur realm, addr address) {
130 actor := authorize(cur, opInvite)
131 if r, ok := members.InviteRole(addr); ok && r > core.RoleMember {
132 must(policy.Authorize(opSetRole, actor))
133 }
134 must(members.RevokeInvite(addr))
135}
136
137// Join accepts the caller's pending invitation.
138func Join(cur realm) {
139 ready()
140 must(members.Join(cur.Previous().Address()))
141}
142
143// Leave removes the caller from the membership.
144func Leave(cur realm) {
145 ready()
146 must(members.Leave(cur.Previous().Address()))
147}
148
149// RemoveMember removes addr.
150func RemoveMember(cur realm, addr address) {
151 authorize(cur, opRemove)
152 must(members.Remove(addr))
153}
154
155// SetRole changes addr's role.
156func SetRole(cur realm, addr address, role string) {
157 authorize(cur, opSetRole)
158 r, err := core.ParseRole(role)
159 must(err)
160 must(members.SetRole(addr, r))
161}
162
163// --- Policy ---
164
165// SetRule changes the rule for an operation, within that operation's floor.
166func SetRule(cur realm, op string, rule string) {
167 authorize(cur, opPolicy)
168 r, err := policies.ParseRule(rule)
169 must(err)
170 must(policy.Set(op, r))
171}
172
173// --- Reads ---
174
175// Entry returns an inert copy of entry id in any status.
176func Entry(id uint64) (core.SubmissionView, bool) {
177 ready()
178 return entries.Submission(id)
179}
180
181// Entries pages through accepted entries in submission order.
182func Entries(offset, limit int) []core.SubmissionView {
183 ready()
184 return entries.Submissions(int(core.SubmissionAccepted), offset, clampLimit(limit))
185}
186
187// Queue pages through entries awaiting review in submission order.
188func Queue(offset, limit int) []core.SubmissionView {
189 ready()
190 return entries.Submissions(int(core.SubmissionSubmitted), offset, clampLimit(limit))
191}
192
193// Fields returns a copy of the form schema.
194func Fields() []forms.Field {
195 ready()
196 return entries.Fields()
197}
198
199// Members pages through members ordered by address.
200func Members(offset, limit int) []core.MemberView {
201 ready()
202 return members.List(offset, clampLimit(limit))
203}
204
205// Invites returns up to limit pending invitations (at most 25) starting at
206// offset, with the role each one grants.
207func Invites(offset, limit int) []core.MemberView {
208 ready()
209 return members.Invites(offset, clampLimit(limit))
210}
211
212// RoleOf returns addr's role name.
213func RoleOf(addr address) string {
214 ready()
215 return members.RoleOf(addr).String()
216}
217
218// Rules lists every operation's current rule.
219func Rules() []policies.RuleView {
220 ready()
221 return policy.List()
222}
223
224// --- Internals ---
225
226func authorize(cur realm, op string) address {
227 ready()
228 actor := cur.Previous().Address()
229 must(policy.Authorize(op, actor))
230 return actor
231}
232
233func ready() {
234 if members == nil {
235 panic(core.Errorf(core.ErrInvalidState, "app is not initialized").Error())
236 }
237}
238
239func clampLimit(limit int) int {
240 if limit < 1 || limit > pageSize {
241 return pageSize
242 }
243 return limit
244}
245
246func must(err error) {
247 if err != nil {
248 panic(err.Error())
249 }
250}