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

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()) }