home.gno
3.51 Kb · 145 lines
1// Package home is samcrew's page on gno.land, served at /u/samcrew.
2//
3// The page is assembled from SLOTS (named markdown fragments, one Set each);
4// the slot named "layout" is the template and every :slug: in it is filled in
5// a single pass. The crew roster is not a slot: see crew.gno.
6//
7// The admin starts as the samcrew namespace multisig and moves with a
8// two-step ProposeAdmin / AcceptAdmin, so it can later be handed to the DAO.
9package home
10
11import (
12 "strconv"
13 "strings"
14
15 "chain/runtime"
16
17 "gno.land/p/nt/avl/v0"
18)
19
20const (
21 layoutSlug = "layout"
22 maxSlugLen = 64
23)
24
25type slot struct {
26 body string
27 rev int
28 updated int64
29}
30
31var (
32 admin = address("g136j0m08pkm2lwwde9dmlx8uee26llent9s5cpf") // samcrew namespace, 2-of-3
33 pendingAdmin address
34
35 slots = avl.NewTree() // slug -> *slot
36 rev int
37)
38
39var reservedSlugs = []string{"chainid", "crew", "height", "hero", "realm", "rev"}
40
41func init() {
42 h := runtime.ChainHeight()
43 for _, s := range defaultSlots() {
44 rev++
45 slots.Set(s.slug, &slot{body: s.body, rev: rev, updated: h})
46 }
47 for _, g := range defaultGalleries() {
48 galleries.Set(g.name, parseGallery(g.spec))
49 }
50 for _, m := range defaultCrew() {
51 addMember(m.id, m.handle, m.role, m.team, m.url)
52 }
53 for _, p := range defaultProfiles() {
54 m := mustMember(p.id)
55 m.addr, m.home, m.line = p.addr, p.home, p.line
56 byAddr.Set(p.addr.String(), p.id)
57 }
58}
59
60// ---------------------------------------------------------------------------
61// Admin
62
63func assertAdmin(cur realm) {
64 if cur.Previous().Address() != admin {
65 panic("restricted to admin")
66 }
67}
68
69// Admin returns the current admin address.
70func Admin() address { return admin }
71
72// ProposeAdmin starts a handover. Nothing changes until the new address calls
73// AcceptAdmin, so a typo cannot lock the page.
74func ProposeAdmin(cur realm, next address) {
75 assertAdmin(cur)
76 if !next.IsValid() {
77 panic("invalid address")
78 }
79 pendingAdmin = next
80}
81
82// AcceptAdmin completes a handover started by ProposeAdmin.
83func AcceptAdmin(cur realm) {
84 caller := cur.Previous().Address()
85 if pendingAdmin == "" || caller != pendingAdmin {
86 panic("restricted to the proposed admin")
87 }
88 admin = caller
89 pendingAdmin = ""
90}
91
92// ---------------------------------------------------------------------------
93// Slots
94
95func validSlug(slug string) bool {
96 if len(slug) == 0 || len(slug) > maxSlugLen {
97 return false
98 }
99 for i := 0; i < len(slug); i++ {
100 c := slug[i]
101 if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-' || c == '_' || c == '.') {
102 return false
103 }
104 }
105 return true
106}
107
108func assertWritableSlug(slug string) {
109 if !validSlug(slug) {
110 panic("invalid slug: want 1-" + strconv.Itoa(maxSlugLen) + " bytes of [a-z0-9._-], got " + strconv.Quote(slug))
111 }
112 for _, r := range reservedSlugs {
113 if slug == r {
114 panic("reserved slug: " + slug)
115 }
116 }
117 if strings.HasPrefix(slug, galleryPrefix) {
118 panic("reserved slug: " + galleryPrefix + "* renders a gallery, use SetGallery")
119 }
120}
121
122// Set creates or replaces one slot.
123func Set(cur realm, slug, body string) {
124 assertAdmin(cur)
125 assertWritableSlug(slug)
126 rev++
127 slots.Set(slug, &slot{body: body, rev: rev, updated: runtime.ChainHeight()})
128}
129
130// Delete removes a slot. Deleting "layout" falls back to the built-in layout.
131func Delete(cur realm, slug string) {
132 assertAdmin(cur)
133 if _, removed := slots.Remove(slug); !removed {
134 panic("no such slot: " + slug)
135 }
136 rev++
137}
138
139// Get returns a slot body, or "" when the slot does not exist.
140func Get(slug string) string {
141 if v := slots.Get(slug); v != nil {
142 return v.(*slot).body
143 }
144 return ""
145}