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.

home.gno

6.62 Kb · 198 lines
  1// Package home is the realm behind https://gno.land/u/moul.
  2//
  3// gnoweb builds a user profile page by calling Render("") on the realm at the
  4// exact path /r/<username>/home (gno.land/pkg/gnoweb/handler_http.go,
  5// GetUserView) and embedding the result as the profile body. There is no
  6// version resolution in that lookup, which is why this realm (alone in this
  7// repo) carries no /vN suffix: the bare path IS the interface with gnoweb,
  8// and gno.land/r/nym-moultest123/home/v0 would never be found.
  9//
 10// # Slots
 11//
 12// The page is not hard-coded. It is assembled from SLOTS: named markdown
 13// fragments in an avl tree, each written by its own Set call, so updating one
 14// paragraph is one small transaction instead of a redeploy.
 15//
 16// The layout is itself a slot ("layout"), so the shape of the page (headings,
 17// order, what appears at all) changes without touching the code.
 18// p/moul/dynreplacer fills every :slug: placeholder found in the layout, lazily:
 19// a slot whose placeholder is absent from the layout costs nothing to render.
 20//
 21// Substitution is SINGLE-PASS and non-recursive. A placeholder inside a slot
 22// body is left alone, so no slot can expand into another and no cycle exists.
 23// Two registered placeholders can never be prefixes of one another either
 24// (every one is :slug: and a slug may not contain ':'), so the result does not
 25// depend on registration order.
 26//
 27// A placeholder with no matching slot survives into the output verbatim. That
 28// is deliberate: a missing section should be visible, not silently blank.
 29//
 30// # Versioning
 31//
 32// gnomod.toml declares private = true, which lets the creator re-add the
 33// package at this path. That redeploy re-runs init() and RESETS everything
 34// here, so the markdown under content/ is the source of truth and
 35// tools/gnohome pushes the slots back afterwards. The intended path is that this
 36// never happens: slots cover content, and the layout slot covers presentation.
 37package home
 38
 39import (
 40	"crypto/sha256"
 41	"encoding/hex"
 42	"strconv"
 43	"strings"
 44
 45	"gno.land/p/nt/avl/v0"
 46)
 47
 48// admin is the only address allowed to write. Deliberately a constant and not
 49// transferable ownership: this realm is one person's profile page, and the
 50// same address is the only one the chain lets redeploy it.
 51const admin = address("g1manfred47kzduec920z88wfr64ylksmdcedlf5")
 52
 53// layoutSlug names the slot used as the page template.
 54const layoutSlug = "layout"
 55
 56// realmPath is this realm's own path. Written out rather than read from
 57// unsafe.CurrentRealm() because it is also the interface with gnoweb (see the
 58// package doc): the constant is the thing that must not drift, so it is stated
 59// once and every other use points here.
 60const realmPath = "gno.land/r/nym-moultest123/home"
 61
 62// maxSlugLen bounds a slug so an index row stays readable and a key stays cheap.
 63const maxSlugLen = 64
 64
 65var (
 66	slots = avl.NewTree() // slug -> *slot
 67	rev   int             // total number of writes, bumped by every mutation
 68)
 69
 70type slot struct {
 71	body    string
 72	rev     int   // rev at the time of the last write to this slot
 73	updated int64 // block height of the last write
 74}
 75
 76// reservedSlugs are placeholder names computed at render time from chain state.
 77// A slot may not take one, because it would shadow the computed value and make
 78// the page depend on callback registration order.
 79//
 80// The scan.* half comes from scan.gno's scanPlaceholders and the :reactions:
 81// one from reactions.gno, so adding a placeholder in either file reserves it
 82// here and cannot be forgotten.
 83var reservedSlugs = append(
 84	append(
 85		[]string{"chainid", "height", "owner", "realm", "rev", "slots"},
 86		scanPlaceholders...,
 87	),
 88	reactionsPlaceholders...,
 89)
 90
 91func assertAdmin(cur realm) {
 92	if cur.Previous().Address() != admin {
 93		panic("restricted to admin")
 94	}
 95}
 96
 97// validSlug reports whether slug is a legal slot name: 1..maxSlugLen bytes of
 98// [a-z0-9._-]. The character set excludes ':' so a slug can never break out of
 99// its own :slug: placeholder, and excludes uppercase so a slot has one name.
100func validSlug(slug string) bool {
101	if len(slug) == 0 || len(slug) > maxSlugLen {
102		return false
103	}
104	for i := 0; i < len(slug); i++ {
105		c := slug[i]
106		switch {
107		case c >= 'a' && c <= 'z', c >= '0' && c <= '9':
108		case c == '-', c == '_', c == '.':
109		default:
110			return false
111		}
112	}
113	return true
114}
115
116func assertWritableSlug(slug string) {
117	if !validSlug(slug) {
118		panic("invalid slug: want 1-" + strconv.Itoa(maxSlugLen) + " bytes of [a-z0-9._-], got " + strconv.Quote(slug))
119	}
120	for _, r := range reservedSlugs {
121		if slug == r {
122			panic("reserved slug: " + slug + " is computed at render time")
123		}
124	}
125}
126
127// Set creates or replaces the slot named slug. This is the ordinary update: one
128// slot, one transaction. Passing the empty body keeps the slot but empties it;
129// use Delete to remove it.
130func Set(cur realm, slug, body string) {
131	assertAdmin(cur)
132	assertWritableSlug(slug)
133	rev++
134	slots.Set(slug, &slot{body: body, rev: rev, updated: chainHeight()})
135}
136
137// Append adds to the end of a slot, creating it when absent. It exists for
138// content too large to fit in one transaction: Set the first chunk, Append the
139// rest. Ordinary updates should use Set, which is idempotent.
140func Append(cur realm, slug, body string) {
141	assertAdmin(cur)
142	assertWritableSlug(slug)
143	rev++
144	prev := ""
145	if v := slots.Get(slug); v != nil {
146		prev = v.(*slot).body
147	}
148	slots.Set(slug, &slot{body: prev + body, rev: rev, updated: chainHeight()})
149}
150
151// Delete removes a slot. Deleting the layout slot restores the built-in
152// default layout.
153func Delete(cur realm, slug string) {
154	assertAdmin(cur)
155	if _, removed := slots.Remove(slug); !removed {
156		panic("no such slot: " + slug)
157	}
158	rev++
159}
160
161// Get returns a slot body, or "" when the slot does not exist.
162func Get(slug string) string {
163	v := slots.Get(slug)
164	if v == nil {
165		return ""
166	}
167	return v.(*slot).body
168}
169
170// Revision returns the total number of writes this realm has accepted. It
171// changes on every mutation, so a client can cheaply tell "nothing moved".
172func Revision() int {
173	return rev
174}
175
176// Manifest returns one tab-separated line per slot:
177//
178//	<slug>\t<rev>\t<len>\t<sha256 of body, hex>
179//
180// It is the diff surface tools/gnohome queries with vm/qeval: one read tells it
181// exactly which local file is out of date, without downloading any body.
182func Manifest() string {
183	var b strings.Builder
184	slots.Iterate("", "", func(key string, value any) bool {
185		s := value.(*slot)
186		sum := sha256.Sum256([]byte(s.body))
187		b.WriteString(key)
188		b.WriteString("\t")
189		b.WriteString(strconv.Itoa(s.rev))
190		b.WriteString("\t")
191		b.WriteString(strconv.Itoa(len(s.body)))
192		b.WriteString("\t")
193		b.WriteString(hex.EncodeToString(sum[:]))
194		b.WriteString("\n")
195		return false
196	})
197	return b.String()
198}