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.

linktree.gno

4.88 Kb · 192 lines
  1package linktree
  2
  3import (
  4	"strconv"
  5	"strings"
  6
  7	"chain"
  8
  9	"gno.land/p/nym-moultest123/kit/ui/v0"
 10	"gno.land/p/nt/avl/v0"
 11)
 12
 13// selfPath is this realm's own page. A root-relative link resolves against
 14// the domain, not the realm, so every link back into it names it in full.
 15const selfPath = "/r/nym-moultest123/x/daily/linktree/v1"
 16
 17// link is a single labeled URL on a profile.
 18type link struct {
 19	Label string
 20	URL   string
 21}
 22
 23// profile is one address's personal links page.
 24type profile struct {
 25	Bio   string
 26	Links []link
 27}
 28
 29// profiles maps an address (string key) to its *profile.
 30// avl.Tree keeps iteration deterministic for Render.
 31var profiles avl.Tree
 32
 33// The bounds on what a caller may store. Anyone may create a profile, and the
 34// root page lists every profile while a profile page lists every link.
 35const (
 36	MaxProfiles = 1000
 37	MaxLinks    = 20
 38	MaxBioLen   = 280
 39	MaxLabelLen = 64
 40	MaxURLLen   = 512
 41)
 42
 43// SetBio sets (or replaces) the calling address's bio.
 44func SetBio(cur realm, bio string) {
 45	addr := caller(cur)
 46	if len(bio) > MaxBioLen {
 47		panic("bio too long")
 48	}
 49	p := getOrCreate(addr.String())
 50	p.Bio = bio
 51	chain.Emit("BioSet", "addr", addr.String())
 52}
 53
 54// AddLink appends a labeled URL to the calling address's profile.
 55func AddLink(cur realm, label string, url string) {
 56	addr := caller(cur)
 57	if label == "" {
 58		panic("label must not be empty")
 59	}
 60	if url == "" {
 61		panic("url must not be empty")
 62	}
 63	// The URL becomes a markdown link destination as written, so it is
 64	// checked here rather than escaped there: http(s) only, and nothing that
 65	// could close the destination or start another.
 66	if !safeURL(url) {
 67		panic("url must be http:// or https:// with no spaces, parentheses or angle brackets")
 68	}
 69	if len(label) > MaxLabelLen || len(url) > MaxURLLen {
 70		panic("label or url too long")
 71	}
 72	p := getOrCreate(addr.String())
 73	if len(p.Links) >= MaxLinks {
 74		panic("a profile holds at most " + strconv.Itoa(MaxLinks) + " links")
 75	}
 76	p.Links = append(p.Links, link{Label: label, URL: url})
 77	chain.Emit("LinkAdded", "addr", addr.String(), "label", label)
 78}
 79
 80// RemoveLink deletes the link at index from the calling address's profile.
 81func RemoveLink(cur realm, index int) {
 82	addr := caller(cur)
 83	key := addr.String()
 84	v := profiles.Get(key)
 85	if v == nil {
 86		panic("no profile for caller")
 87	}
 88	p := v.(*profile)
 89	if index < 0 || index >= len(p.Links) {
 90		panic("index out of range")
 91	}
 92	// A fresh slice, not append(s[:i], s[i+1:]...): the in-place form keeps the
 93	// old backing array, so the deposit for a removed link never comes back.
 94	kept := make([]link, 0, len(p.Links)-1)
 95	kept = append(kept, p.Links[:index]...)
 96	kept = append(kept, p.Links[index+1:]...)
 97	p.Links = kept
 98	chain.Emit("LinkRemoved", "addr", key, "index", strconv.Itoa(index))
 99}
100
101// getOrCreate returns the profile for key, creating an empty one if absent.
102func getOrCreate(key string) *profile {
103	if v := profiles.Get(key); v != nil {
104		return v.(*profile)
105	}
106	if profiles.Size() >= MaxProfiles {
107		panic("linktree is full at " + strconv.Itoa(MaxProfiles) + " profiles")
108	}
109	p := &profile{}
110	profiles.Set(key, p)
111	return p
112}
113
114// Render shows all profiles at root, or a single profile at "/<address>".
115func Render(path string) string {
116	addr := strings.Trim(path, "/")
117	if addr == "" {
118		return renderIndex()
119	}
120	return renderProfile(addr)
121}
122
123func renderIndex() string {
124	var b strings.Builder
125	b.WriteString("# Linktree\n\n")
126	if profiles.Size() == 0 {
127		b.WriteString("_No profiles yet. Call `SetBio` or `AddLink` to create one._\n")
128		return b.String()
129	}
130	b.WriteString("Profiles:\n\n")
131	profiles.Iterate("", "", func(key string, _ interface{}) bool {
132		b.WriteString("- [")
133		b.WriteString(key)
134		b.WriteString("](" + selfPath + ":")
135		b.WriteString(key)
136		b.WriteString(")\n")
137		return false
138	})
139	return b.String()
140}
141
142func renderProfile(addr string) string {
143	var b strings.Builder
144	b.WriteString("# ")
145	b.WriteString(ui.Inline(addr))
146	b.WriteString("\n\n")
147
148	v := profiles.Get(addr)
149	if v == nil {
150		b.WriteString("_No profile for this address._\n")
151		return b.String()
152	}
153	p := v.(*profile)
154
155	if p.Bio != "" {
156		b.WriteString(ui.Inline(p.Bio))
157		b.WriteString("\n\n")
158	}
159
160	if len(p.Links) == 0 {
161		b.WriteString("_No links yet._\n")
162		return b.String()
163	}
164
165	b.WriteString("## Links\n\n")
166	for _, l := range p.Links {
167		b.WriteString("- [")
168		b.WriteString(ui.Inline(l.Label))
169		b.WriteString("](")
170		b.WriteString(l.URL)
171		b.WriteString(")\n")
172	}
173	return b.String()
174}
175
176// caller is the address that crossed into this realm, after checking that the
177// realm token is this frame's own.
178func caller(cur realm) address {
179	if !cur.IsCurrent() {
180		panic("linktree: spoofed realm")
181	}
182	return cur.Previous().Address()
183}
184
185// safeURL reports whether u can be written into a markdown link destination
186// unescaped.
187func safeURL(u string) bool {
188	if !strings.HasPrefix(u, "https://") && !strings.HasPrefix(u, "http://") {
189		return false
190	}
191	return !strings.ContainsAny(u, " \t\r\n()<>[]`\\")
192}