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.

profile.gno

4.19 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 := "# gnomi 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}