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.

urlshort.gno

5.45 Kb · 203 lines
  1// Package urlshort is a simple on-chain URL alias registry.
  2//
  3// Callers register an `alias -> url` mapping they own. An alias can only be
  4// claimed once; the owner may later update the target URL, but nobody else can
  5// overwrite an alias they do not own. A per-alias click counter is bumped every
  6// time the alias is resolved through Render("/<alias>").
  7package urlshort
  8
  9import (
 10	"strings"
 11
 12	"chain"
 13
 14	"gno.land/p/nym-moultest123/kit/ui/v0"
 15	"gno.land/p/nym-moultest123/md/v0"
 16	"gno.land/p/nt/avl/v0"
 17)
 18
 19// selfPath is this realm's own page. A root-relative link resolves against
 20// the domain, not the realm, so every link back into it names it in full.
 21const selfPath = "/r/nym-moultest123/x/daily/urlshort/v1"
 22
 23// entry is the persisted record for a single alias.
 24type entry struct {
 25	url    string
 26	owner  address
 27	note   string
 28	clicks int
 29}
 30
 31// aliases maps alias (string) -> *entry, kept in an avl.Tree for deterministic
 32// iteration order in Render.
 33var aliases avl.Tree
 34
 35// isAlphanumeric reports whether s is non-empty and made only of [0-9A-Za-z].
 36func isAlphanumeric(s string) bool {
 37	if s == "" {
 38		return false
 39	}
 40	for _, c := range s {
 41		switch {
 42		case c >= '0' && c <= '9':
 43		case c >= 'a' && c <= 'z':
 44		case c >= 'A' && c <= 'Z':
 45		default:
 46			return false
 47		}
 48	}
 49	return true
 50}
 51
 52// Shorten registers `alias -> url` owned by the caller.
 53//
 54// Rules:
 55//   - alias must be non-empty and alphanumeric;
 56//   - url must be non-empty;
 57//   - if the alias is unclaimed, it is created owned by the caller;
 58//   - if it is already claimed by the caller, the url/note are updated;
 59//   - if it is claimed by someone else, the call aborts.
 60func Shorten(cur realm, alias string, url string, note string) {
 61	if !cur.IsCurrent() {
 62		panic("urlshort: spoofed realm")
 63	}
 64	if !isAlphanumeric(alias) {
 65		panic("alias must be non-empty and alphanumeric")
 66	}
 67	if url == "" {
 68		panic("url must be non-empty")
 69	}
 70	// The URL is rendered as an <autolink>, so it is checked here rather than
 71	// escaped there: http(s) only, and nothing that could close the brackets.
 72	if !safeURL(url) {
 73		panic("url must be http:// or https:// with no spaces or angle brackets")
 74	}
 75
 76	caller := cur.Previous().Address()
 77
 78	if v := aliases.Get(alias); v != nil {
 79		e := v.(*entry)
 80		if e.owner != caller {
 81			panic("alias already taken by another owner")
 82		}
 83		e.url = url
 84		e.note = note
 85		chain.Emit("AliasUpdated", "alias", alias, "owner", caller.String())
 86		return
 87	}
 88
 89	e := &entry{url: url, owner: caller, note: note, clicks: 0}
 90	aliases.Set(alias, e)
 91	chain.Emit("AliasCreated", "alias", alias, "owner", caller.String())
 92}
 93
 94// Remove deletes an alias owned by the caller. Aborts if the alias does not
 95// exist or the caller is not the owner.
 96func Remove(cur realm, alias string) {
 97	if !cur.IsCurrent() {
 98		panic("urlshort: spoofed realm")
 99	}
100	v := aliases.Get(alias)
101	if v == nil {
102		panic("alias not found")
103	}
104	e := v.(*entry)
105	caller := cur.Previous().Address()
106	if e.owner != caller {
107		panic("only the owner can remove an alias")
108	}
109	aliases.Remove(alias)
110	chain.Emit("AliasRemoved", "alias", alias, "owner", caller.String())
111}
112
113// Lookup returns the target url for an alias and whether it exists. It is a
114// read-only helper and does NOT increment the click counter.
115func Lookup(alias string) (string, bool) {
116	v := aliases.Get(alias)
117	if v == nil {
118		return "", false
119	}
120	return v.(*entry).url, true
121}
122
123// Render renders Markdown. The root path lists every alias; "/<alias>" shows a
124// single alias detail and bumps its click counter.
125func Render(path string) string {
126	if path == "" || path == "/" {
127		return renderIndex()
128	}
129
130	alias := strings.TrimPrefix(path, "/")
131	v := aliases.Get(alias)
132	if v == nil {
133		return "# URL Shortener\n\nNo alias named " + md.InlineCode(alias) + ".\n\n[← all aliases](" + selfPath + ")\n"
134	}
135
136	e := v.(*entry)
137	e.clicks++
138
139	var b strings.Builder
140	b.WriteString("# 🔗 " + alias + "\n\n")
141	b.WriteString("**Target:** <" + e.url + ">\n\n")
142	b.WriteString("**Owner:** " + e.owner.String() + "\n\n")
143	if e.note != "" {
144		b.WriteString("**Note:** " + ui.Inline(e.note) + "\n\n")
145	}
146	b.WriteString("**Clicks:** " + itoa(e.clicks) + "\n\n")
147	b.WriteString("[← all aliases](" + selfPath + ")\n")
148	return b.String()
149}
150
151func renderIndex() string {
152	var b strings.Builder
153	b.WriteString("# 🔗 URL Shortener\n\n")
154
155	if aliases.Size() == 0 {
156		b.WriteString("_No aliases registered yet._\n\n")
157		b.WriteString("Call `Shorten(alias, url, note)` to register one.\n")
158		return b.String()
159	}
160
161	b.WriteString("| Alias | Target | Owner | Clicks |\n")
162	b.WriteString("|-------|--------|-------|--------|\n")
163	aliases.Iterate("", "", func(key string, value interface{}) bool {
164		e := value.(*entry)
165		b.WriteString("| [" + key + "](" + selfPath + ":" + key + ") | <" + e.url + "> | " +
166			ui.Addr(e.owner) + " | " + itoa(e.clicks) + " |\n")
167		return false
168	})
169	b.WriteString("\nTotal aliases: " + itoa(aliases.Size()) + "\n")
170	return b.String()
171}
172
173// itoa converts a non-negative int to its decimal string without importing
174// strconv (kept minimal & deterministic).
175func itoa(n int) string {
176	if n == 0 {
177		return "0"
178	}
179	neg := n < 0
180	if neg {
181		n = -n
182	}
183	var buf [20]byte
184	i := len(buf)
185	for n > 0 {
186		i--
187		buf[i] = byte('0' + n%10)
188		n /= 10
189	}
190	if neg {
191		i--
192		buf[i] = '-'
193	}
194	return string(buf[i:])
195}
196
197// safeURL reports whether u can be written into a markdown autolink unescaped.
198func safeURL(u string) bool {
199	if !strings.HasPrefix(u, "https://") && !strings.HasPrefix(u, "http://") {
200		return false
201	}
202	return !strings.ContainsAny(u, " \t\r\n<>`\\")
203}