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

5.86 Kb · 225 lines
  1// Package home is a personal home realm. gno.land also shows it as the
  2// profile page at /u/<name>, which always calls Render("") and reads its
  3// links against /u/<name>: every link here is absolute.
  4package home
  5
  6import (
  7	"chain/runtime"
  8	"strconv"
  9	"strings"
 10
 11	"gno.land/p/nt/avl/v0"
 12)
 13
 14const (
 15	homeTab      = "home"
 16	workTab      = "work"
 17	blogTab      = "blog"
 18	guestbookTab = "guestbook"
 19
 20	maxTabs       = 8
 21	maxTabName    = 20
 22	maxPageBytes  = 16 * 1024 // so all pages together stay under 128 KB
 23	maxLineRunes  = 140
 24	maxBadges     = 6
 25	maxSlugBytes  = 60
 26	maxTitleRunes = 100
 27	pageSize      = 20
 28	latestPosts   = 3
 29	seqWidth      = 10
 30
 31	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)
 43
 44type profileData struct {
 45	name    string
 46	tagline string
 47}
 48
 49type badge struct {
 50	label string
 51	value string
 52	url   string
 53	row   bool // starts a new row
 54}
 55
 56type nowLine struct {
 57	text   string
 58	height int64
 59}
 60
 61type entry struct {
 62	msg    string
 63	height int64
 64	hidden bool
 65}
 66
 67// post dates are UTC days of the block time, "2006-01-02".
 68type post struct {
 69	title     string
 70	body      string
 71	published string
 72	edited    string // "" until the first edit
 73	seq       string // key in postOrder
 74}
 75
 76type tipStats struct {
 77	count int
 78	total int64
 79}
 80
 81var (
 82	owner        address
 83	pendingOwner address // set by TransferOwnership, cleared by AcceptOwnership
 84	base         string  // "/r/<name>/home"; every generated link starts with it
 85	profile      profileData
 86	badges       []badge
 87	now          nowLine
 88	tabs         []string          // tab order: home first, guestbook last
 89	pages        map[string]string // markdown per tab
 90	waves        int               // wave count; anyone can wave again
 91	entries      *avl.Tree         // address -> *entry
 92	order        *avl.Tree         // zero-padded sequence -> address
 93	posts        *avl.Tree         // slug -> *post
 94	postOrder    *avl.Tree         // zero-padded sequence -> slug
 95	postSeq      int               // last post sequence; never reused
 96	tips         tipStats
 97	updated      int64 // block height of the last owner write
 98)
 99
100// init records the deployer as owner. Under mainnet's approval flow it runs
101// 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}
107
108// 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 = 0
122	entries = avl.NewTree()
123	order = avl.NewTree()
124	posts = avl.NewTree()
125	postOrder = avl.NewTree()
126	postSeq = 0
127	tips = tipStats{}
128	updated = runtime.ChainHeight()
129}
130
131// callerOf is the account guard: a live cur and a direct call from an
132// account. It must not call AssertOriginCall, which refuses every call from
133// 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}
140
141// assertOwner is the owner guard.
142func assertOwner(cur realm) {
143	if callerOf(cur) != owner {
144		panic(errOwnerOnly)
145	}
146}
147
148// Owner returns the current owner.
149func Owner() address {
150	return owner
151}
152
153// Render shows one tab, or one post at blog/<slug>. An unknown path shows
154// Home and an unknown post shows Blog. The path is only compared against
155// 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 bold
161		}
162		tab = blogTab
163	}
164	if _, ok := pages[tab]; !ok {
165		tab = homeTab
166	}
167	return frame(tab, expand(pages[tab], tab, parsePage(query)))
168}
169
170// 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}
174
175func tabURL(t string) string {
176	if t == homeTab {
177		return base
178	}
179	return base + ":" + t
180}
181
182// tabLabel capitalizes a tab name; names are ASCII by validName.
183func tabLabel(t string) string {
184	return strings.ToUpper(t[:1]) + t[1:]
185}
186
187// 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 as
189// 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 yet
199		}
200		parts = append(parts, pillLink(tabLabel(t), tabURL(t), tabH, 0, s))
201	}
202	return strings.Join(parts, " ")
203}
204
205func footer() string {
206	return "---\n\nOwner " + userLink(owner) +
207		" · updated at block " + strconv.FormatInt(updated, 10) +
208		" · [Source](" + base + "$source)\n"
209}
210
211// 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			continue
217		}
218		n, err := strconv.Atoi(v)
219		if err != nil || n < 1 {
220			return 1
221		}
222		return n
223	}
224	return 1
225}