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

profile.gno

4.21 Kb · 190 lines
  1// Package profile stores lightweight user profiles for gnomemepad.
  2// Independent of pad versions — upgrade this realm without redeploying markets.
  3//
  4//	SetProfile(displayName, bio, uri) — EOA only, per caller address
  5//	GetProfile(addr) → name|bio|uri|updatedHeight
  6package profile
  7
  8import (
  9	"chain"
 10	"chain/runtime"
 11	"strconv"
 12	"strings"
 13
 14	"gno.land/p/nt/avl/v0"
 15)
 16
 17const (
 18	maxNameLen = 48
 19	maxBioLen  = 280
 20	maxURILen  = 200
 21)
 22
 23// Profile is one user's public card (all fields exported for LaunchInfo-style reads).
 24type Profile struct {
 25	Name    string
 26	Bio     string
 27	URI     string
 28	Updated int64 // block height
 29}
 30
 31var (
 32	// byAddr: address string -> *Profile
 33	byAddr avl.Tree
 34	inited bool
 35)
 36
 37// Init is optional (no-op state). Kept for deploy symmetry with hub/pad.
 38func Init(cur realm) {
 39	if inited {
 40		panic("profile: already initialized")
 41	}
 42	if !cur.Previous().IsUserCall() {
 43		panic("profile: EOA only")
 44	}
 45	inited = true
 46	byAddr = avl.Tree{}
 47	chain.Emit("Init", "by", cur.Previous().Address().String())
 48}
 49
 50func ensure() {
 51	// Lazy: allow SetProfile before Init for simpler UX; Init still recommended.
 52	if !inited {
 53		byAddr = avl.Tree{}
 54		inited = true
 55	}
 56}
 57
 58func sanitizeText(s string, max int) string {
 59	s = strings.TrimSpace(s)
 60	if len(s) > max {
 61		s = s[:max]
 62	}
 63	// Strip control chars
 64	out := ""
 65	for _, c := range s {
 66		if c >= 0x20 && c != 0x7f {
 67			out += string(c)
 68		}
 69	}
 70	return out
 71}
 72
 73// SetProfile upserts the caller's profile.
 74func SetProfile(cur realm, name, bio, uri string) {
 75	if !cur.Previous().IsUserCall() {
 76		panic("profile: EOA only")
 77	}
 78	ensure()
 79	name = sanitizeText(name, maxNameLen)
 80	bio = sanitizeText(bio, maxBioLen)
 81	uri = sanitizeText(uri, maxURILen)
 82	if name == "" {
 83		panic("profile: name required")
 84	}
 85	if uri != "" && !(strings.HasPrefix(uri, "https://") || strings.HasPrefix(uri, "http://") || strings.HasPrefix(uri, "ipfs://")) {
 86		panic("profile: uri must be http(s) or ipfs")
 87	}
 88	addr := cur.Previous().Address()
 89	p := &Profile{
 90		Name:    name,
 91		Bio:     bio,
 92		URI:     uri,
 93		Updated: runtime.ChainHeight(),
 94	}
 95	byAddr.Set(addr.String(), p)
 96	chain.Emit("SetProfile", "addr", addr.String(), "name", name)
 97}
 98
 99// ClearProfile removes the caller's profile.
100func ClearProfile(cur realm) {
101	if !cur.Previous().IsUserCall() {
102		panic("profile: EOA only")
103	}
104	ensure()
105	addr := cur.Previous().Address().String()
106	if byAddr.Has(addr) {
107		byAddr.Remove(addr)
108		chain.Emit("ClearProfile", "addr", addr)
109	}
110}
111
112// GetProfile returns pipe-delimited: name|bio|uri|updatedHeight
113// Empty string if none.
114func GetProfile(addr string) string {
115	ensure()
116	addr = strings.TrimSpace(addr)
117	if addr == "" {
118		return ""
119	}
120	v := byAddr.Get(addr)
121	if v == nil {
122		return ""
123	}
124	p, ok := v.(*Profile)
125	if !ok || p == nil {
126		return ""
127	}
128	return p.Name + "|" + p.Bio + "|" + p.URI + "|" + strconv.FormatInt(p.Updated, 10)
129}
130
131// HasProfile reports whether addr has a profile.
132func HasProfile(addr string) bool {
133	ensure()
134	return byAddr.Has(strings.TrimSpace(addr))
135}
136
137// ProfileCount returns number of profiles.
138func ProfileCount() int {
139	ensure()
140	return byAddr.Size()
141}
142
143func sanitizeMD(s string) string {
144	s = strings.ReplaceAll(s, "`", "'")
145	s = strings.ReplaceAll(s, "<", "")
146	s = strings.ReplaceAll(s, ">", "")
147	return s
148}
149
150// Render home or user/{addr}
151func Render(path string) string {
152	path = strings.Trim(path, "/")
153	ensure()
154	if path == "" {
155		out := "# gnomemepad profiles\n\n"
156		out += "- Profiles: **" + strconv.Itoa(byAddr.Size()) + "**\n\n"
157		out += "Call `SetProfile(name, bio, uri)` from your wallet.\n"
158		return out
159	}
160	if strings.HasPrefix(path, "user/") {
161		addr := strings.TrimPrefix(path, "user/")
162		raw := GetProfile(addr)
163		if raw == "" {
164			return "> [!WARNING]\n> No profile for `" + sanitizeMD(addr) + "`\n"
165		}
166		parts := strings.SplitN(raw, "|", 4)
167		name, bio, uri := parts[0], "", ""
168		if len(parts) > 1 {
169			bio = parts[1]
170		}
171		if len(parts) > 2 {
172			uri = parts[2]
173		}
174		out := "# " + sanitizeMD(name) + "\n\n"
175		out += "- Address: `" + sanitizeMD(addr) + "`\n"
176		if bio != "" {
177			out += "- Bio: " + sanitizeMD(bio) + "\n"
178		}
179		if uri != "" {
180			out += "- URI: " + sanitizeMD(uri) + "\n"
181		}
182		return out
183	}
184	return "> [!WARNING]\n> Path not found\n"
185}
186
187func resetForTest() {
188	byAddr = avl.Tree{}
189	inited = false
190}