home.gno
4.09 Kb · 154 lines
1// Package home is zôÖma's profile page on gno.land.
2//
3// gnoweb renders a profile by calling Render("") on the realm at exactly
4// /r/<namespace>/home, so this path carries no /vN suffix.
5//
6// The page is assembled from SLOTS: named markdown fragments, one Set
7// transaction each. The slot named "layout" is the template; every :slug:
8// placeholder in it is replaced by the slot of that name, in a single pass.
9// init seeds every slot from content.gno, so the page is complete the block
10// it goes live and later edits are data, not redeploys.
11//
12// One thing on this page is not written by its owner: visitors can send a
13// heart (<3), one per address, and the page counts them.
14package home
15
16import (
17 "strconv"
18 "strings"
19
20 "chain/runtime"
21
22 "gno.land/p/nt/avl/v0"
23)
24
25// admin is the only address allowed to edit slots.
26const admin = address("g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c")
27
28const (
29 layoutSlug = "layout"
30 maxSlugLen = 64
31 maxRecent = 7
32)
33
34type slot struct {
35 body string
36 rev int
37 updated int64
38}
39
40var (
41 slots = avl.NewTree() // slug -> *slot
42 rev int // total slot writes
43
44 hearts = avl.NewTree() // address string -> int64 height of the heart
45 recent []address // newest first, at most maxRecent
46)
47
48// reservedSlugs are placeholders computed at render time. A slot may not take
49// one of these names, so nothing a slot says can shadow chain state.
50var reservedSlugs = []string{"chainid", "height", "hearts", "hero", "realm", "rev"}
51
52func init() {
53 for _, s := range defaultSlots {
54 rev++
55 slots.Set(s.slug, &slot{body: s.body, rev: rev, updated: runtime.ChainHeight()})
56 }
57 for _, g := range defaultGalleries {
58 galleries.Set(g.name, parseGallery(g.spec))
59 }
60}
61
62func assertAdmin(cur realm) {
63 if cur.Previous().Address() != admin {
64 panic("restricted to admin")
65 }
66}
67
68func validSlug(slug string) bool {
69 if len(slug) == 0 || len(slug) > maxSlugLen {
70 return false
71 }
72 for i := 0; i < len(slug); i++ {
73 c := slug[i]
74 if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-' || c == '_' || c == '.') {
75 return false
76 }
77 }
78 return true
79}
80
81func assertWritableSlug(slug string) {
82 if !validSlug(slug) {
83 panic("invalid slug: want 1-" + strconv.Itoa(maxSlugLen) + " bytes of [a-z0-9._-], got " + strconv.Quote(slug))
84 }
85 for _, r := range reservedSlugs {
86 if slug == r {
87 panic("reserved slug: " + slug)
88 }
89 }
90 if strings.HasPrefix(slug, galleryPrefix) {
91 panic("reserved slug: " + galleryPrefix + "* renders a gallery, use SetGallery")
92 }
93}
94
95// Set creates or replaces one slot.
96func Set(cur realm, slug, body string) {
97 assertAdmin(cur)
98 assertWritableSlug(slug)
99 rev++
100 slots.Set(slug, &slot{body: body, rev: rev, updated: runtime.ChainHeight()})
101}
102
103// Append adds to the end of a slot, creating it when absent, for bodies too
104// large for one transaction.
105func Append(cur realm, slug, body string) {
106 assertAdmin(cur)
107 assertWritableSlug(slug)
108 prev := ""
109 if v := slots.Get(slug); v != nil {
110 prev = v.(*slot).body
111 }
112 rev++
113 slots.Set(slug, &slot{body: prev + body, rev: rev, updated: runtime.ChainHeight()})
114}
115
116// Delete removes a slot. Deleting "layout" falls back to the built-in layout.
117func Delete(cur realm, slug string) {
118 assertAdmin(cur)
119 if _, removed := slots.Remove(slug); !removed {
120 panic("no such slot: " + slug)
121 }
122 rev++
123}
124
125// Get returns a slot body, or "" when the slot does not exist.
126func Get(slug string) string {
127 if v := slots.Get(slug); v != nil {
128 return v.(*slot).body
129 }
130 return ""
131}
132
133// Revision is the total number of slot writes accepted.
134func Revision() int { return rev }
135
136// Heart records one <3 from the caller. Each address can send exactly one.
137func Heart(cur realm) {
138 from := cur.Previous().Address()
139 if hearts.Has(from.String()) {
140 panic("already sent: one <3 per address")
141 }
142 hearts.Set(from.String(), runtime.ChainHeight())
143
144 recent = append([]address{from}, recent...)
145 if len(recent) > maxRecent {
146 recent = recent[:maxRecent]
147 }
148}
149
150// Hearts returns how many distinct addresses sent a <3.
151func Hearts() int { return hearts.Size() }
152
153// HasHeart reports whether addr already sent a <3.
154func HasHeart(addr address) bool { return hearts.Has(addr.String()) }