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 address5// GetProfile(addr) → name|bio|uri|updatedHeight6package profile78import (9 "chain"10 "chain/runtime"11 "strconv"12 "strings"1314 "gno.land/p/nt/avl/v0"15)1617const (18 maxNameLen = 4819 maxBioLen = 28020 maxURILen = 20021)2223// Profile is one user's public card (all fields exported for LaunchInfo-style reads).24type Profile struct {25 Name string26 Bio string27 URI string28 Updated int64 // block height29}3031var (32 // byAddr: address string -> *Profile33 byAddr avl.Tree34 inited bool35)3637// 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 = true46 byAddr = avl.Tree{}47 chain.Emit("Init", "by", cur.Previous().Address().String())48}4950func ensure() {51 // Lazy: allow SetProfile before Init for simpler UX; Init still recommended.52 if !inited {53 byAddr = avl.Tree{}54 inited = true55 }56}5758func sanitizeText(s string, max int) string {59 s = strings.TrimSpace(s)60 if len(s) > max {61 s = s[:max]62 }63 // Strip control chars64 out := ""65 for _, c := range s {66 if c >= 0x20 && c != 0x7f {67 out += string(c)68 }69 }70 return out71}7273// 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}9899// 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}111112// GetProfile returns pipe-delimited: name|bio|uri|updatedHeight113// 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}130131// HasProfile reports whether addr has a profile.132func HasProfile(addr string) bool {133 ensure()134 return byAddr.Has(strings.TrimSpace(addr))135}136137// ProfileCount returns number of profiles.138func ProfileCount() int {139 ensure()140 return byAddr.Size()141}142143func sanitizeMD(s string) string {144 s = strings.ReplaceAll(s, "`", "'")145 s = strings.ReplaceAll(s, "<", "")146 s = strings.ReplaceAll(s, ">", "")147 return s148}149150// 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 out159 }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 out183 }184 return "> [!WARNING]\n> Path not found\n"185}186187func resetForTest() {188 byAddr = avl.Tree{}189 inited = false190}191ClearProfile(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})
GetProfile(addr string) string
HasProfile(addr string) bool
Init(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})
ProfileCount() int
Render(path string) 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, bio string, uri 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.