1// Package home is a personal home realm. gno.land also shows it as the2// profile page at /u/<name>, which always calls Render("") and reads its3// links against /u/<name>: every link here is absolute.4package home56import (7 "chain/runtime"8 "strconv"9 "strings"1011 "gno.land/p/nt/avl/v0"12)1314const (15 homeTab = "home"16 workTab = "work"17 blogTab = "blog"18 guestbookTab = "guestbook"1920 maxTabs = 821 maxTabName = 2022 maxPageBytes = 16 * 1024 // so all pages together stay under 128 KB23 maxLineRunes = 14024 maxBadges = 625 maxSlugBytes = 6026 maxTitleRunes = 10027 pageSize = 2028 latestPosts = 329 seqWidth = 103031 errUnauthorized = "home: unauthorized"32 errMasterKey = "home: needs the master key, not a session key"33 errOwnerOnly = "home: owner only"34 errPendingOnly = "home: pending owner only"35 errInvalidInput = "home: invalid input: "36 errSigned = "home: already signed"37 errNoEntry = "home: no such entry"38 errNoTab = "home: no such tab"39 errNoPost = "home: no such post"40 errTabsRequired = "home: home, blog and guestbook tabs are required"41 errUgnotOnly = "home: send ugnot only"42)4344type profileData struct {45 name string46 tagline string47}4849type badge struct {50 label string51 value string52 url string53 row bool // starts a new row54}5556type nowLine struct {57 text string58 height int6459}6061type entry struct {62 msg string63 height int6464 hidden bool65}6667// post dates are UTC days of the block time, "2006-01-02".68type post struct {69 title string70 body string71 published string72 edited string // "" until the first edit73 seq string // key in postOrder74}7576type tipStats struct {77 count int78 total int6479}8081var (82 owner address83 pendingOwner address // set by TransferOwnership, cleared by AcceptOwnership84 base string // "/r/<name>/home"; every generated link starts with it85 profile profileData86 badges []badge87 now nowLine88 tabs []string // tab order: home first, guestbook last89 pages map[string]string // markdown per tab90 waves int // wave count; anyone can wave again91 entries *avl.Tree // address -> *entry92 order *avl.Tree // zero-padded sequence -> address93 posts *avl.Tree // slug -> *post94 postOrder *avl.Tree // zero-padded sequence -> slug95 postSeq int // last post sequence; never reused96 tips tipStats97 updated int64 // block height of the last owner write98)99100// init records the deployer as owner. Under mainnet's approval flow it runs101// as the submitter, not the approver. No AssertOriginCall: init isn't a call.102func init(cur realm) {103 owner = callerOf(cur)104 base = strings.TrimPrefix(cur.PkgPath(), runtime.ChainDomain())105 setDefaults()106}107108// setDefaults writes the initial state (content.gno holds the personal part).109func setDefaults() {110 profile = defaultProfile()111 badges = defaultBadges()112 now = nowLine{}113 pendingOwner = ""114 tabs = []string{homeTab, workTab, blogTab, guestbookTab}115 pages = map[string]string{116 homeTab: defaultHome(),117 workTab: defaultWork,118 blogTab: defaultBlog,119 guestbookTab: defaultGuestbook,120 }121 waves = 0122 entries = avl.NewTree()123 order = avl.NewTree()124 posts = avl.NewTree()125 postOrder = avl.NewTree()126 postSeq = 0127 tips = tipStats{}128 updated = runtime.ChainHeight()129}130131// callerOf is the account guard: a live cur and a direct call from an132// account. It must not call AssertOriginCall, which refuses every call from133// inside a named helper.134func callerOf(cur realm) address {135 if !cur.IsCurrent() || !cur.Previous().IsUserCall() {136 panic(errUnauthorized)137 }138 return cur.Previous().Address()139}140141// assertOwner is the owner guard.142func assertOwner(cur realm) {143 if callerOf(cur) != owner {144 panic(errOwnerOnly)145 }146}147148// Owner returns the current owner.149func Owner() address {150 return owner151}152153// Render shows one tab, or one post at blog/<slug>. An unknown path shows154// Home and an unknown post shows Blog. The path is only compared against155// tab names or looked up as a slug, and never written into the page.156func Render(path string) string {157 tab, query, _ := strings.Cut(path, "?")158 if slug, ok := strings.CutPrefix(tab, blogTab+"/"); ok {159 if p, ok := posts.Get(slug).(*post); ok {160 return frame("", postMD(p)) // not a tab: no tab is bold161 }162 tab = blogTab163 }164 if _, ok := pages[tab]; !ok {165 tab = homeTab166 }167 return frame(tab, expand(pages[tab], tab, parsePage(query)))168}169170// frame puts the tab bar above a page and the footer below it.171func frame(tab, md string) string {172 return tabBar(tab) + "\n\n" + md + "\n\n" + footer()173}174175func tabURL(t string) string {176 if t == homeTab {177 return base178 }179 return base + ":" + t180}181182// tabLabel capitalizes a tab name; names are ASCII by validName.183func tabLabel(t string) string {184 return strings.ToUpper(t[:1]) + t[1:]185}186187// tabBar draws every tab as a linked pill, the current one in brand green.188// The current tab is linked too, because gnoweb shows an unlinked image as189// a block and the row would break. Blog stays out until there is a post.190func tabBar(current string) string {191 parts := make([]string, 0, len(tabs))192 for _, t := range tabs {193 s := segSoft(tabLabel(t))194 switch {195 case t == current:196 s = segOn(tabLabel(t))197 case t == blogTab && posts.Size() == 0:198 continue // no posts yet199 }200 parts = append(parts, pillLink(tabLabel(t), tabURL(t), tabH, 0, s))201 }202 return strings.Join(parts, " ")203}204205func footer() string {206 return "---\n\nOwner " + userLink(owner) +207 " · updated at block " + strconv.FormatInt(updated, 10) +208 " · [Source](" + base + "$source)\n"209}210211// parsePage reads page=N from the query; anything invalid is page 1.212func parsePage(query string) int {213 for _, kv := range strings.Split(query, "&") {214 k, v, _ := strings.Cut(kv, "=")215 if k != "page" {216 continue217 }218 n, err := strconv.Atoi(v)219 if err != nil || n < 1 {220 return 1221 }222 return n223 }224 return 1225}226AcceptOwnership(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
Hide(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, author string)
Owner() string
RemovePage(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, tab string)
RemovePost(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, slug string)
Render(path string) string
SetBadges(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, spec string)
SetNow(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, text string)
SetPage(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, tab string, md string)
SetPost(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, slug string, title string, body string)
SetProfile(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, tagline string)
Sign(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, msg string)
Tip(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
TransferOwnership(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, newOwner string)
Wave(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.