Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}