Search Apps Documentation Source Content File Folder Download Copy Actions Download

render.gno

5.27 Kb · 221 lines
  1package user
  2
  3import (
  4	"net/url"
  5	"strconv"
  6	"strings"
  7
  8	"gno.land/p/akkadia/v0/validate"
  9	"gno.land/p/jeronimoalbi/pager"
 10	"gno.land/r/akkadia/v0/admin"
 11)
 12
 13// Render handles RESTful routing and returns Markdown responses
 14func Render(iUrl string) string {
 15	assertMigrationStateAvailable()
 16	validate.AssertStringTextLen(iUrl, true, "url")
 17	u, err := url.Parse(iUrl)
 18	if err != nil {
 19		return "404\n"
 20	}
 21
 22	query := u.Query()
 23
 24	switch u.Path {
 25	case "":
 26		return renderHome()
 27	case "users":
 28		return renderUsersList(iUrl)
 29	case "user":
 30		addr := address(query.Get("address"))
 31		return renderUser(iUrl, addr)
 32	case "nickname":
 33		nickname := query.Get("nickname")
 34		return renderNicknameLookup(nickname)
 35	default:
 36		return renderError("Not found")
 37	}
 38}
 39
 40// renderHome renders the home page
 41func renderHome() string {
 42	output := `# AKKADIA User
 43
 44## Introduction
 45
 46User realm manages user profiles for AKKADIA.
 47
 48- **metadata**: Admin-managed opaque user attributes
 49- **properties**: User-managed personal data
 50- **nicknames**: Exact-match unique nickname registry
 51
 52## Stats
 53
 54`
 55	output += "* **Total Users**: " + strconv.Itoa(GetTotalUserSize()) + "\n"
 56	output += `
 57## Quick Links
 58
 59* [Browse Users](./user:users)
 60
 61## User Lookup
 62
 63<gno-form path="user">
 64  <gno-input name="address" placeholder="Enter the address" />
 65</gno-form>
 66
 67## Nickname Lookup
 68
 69<gno-form path="nickname">
 70  <gno-input name="nickname" placeholder="Enter the nickname" />
 71</gno-form>
 72`
 73	return output
 74}
 75
 76// renderUsersList renders a list of all users with pagination
 77func renderUsersList(iUrl string) string {
 78	output := "# Users\n\n"
 79	output += "[← Home](./user:)\n\n"
 80
 81	totalUsers := GetTotalUserSize()
 82	if totalUsers == 0 {
 83		output += "*No users registered yet.*\n"
 84		return output
 85	}
 86
 87	pages, err := pager.New(iUrl, totalUsers, pager.WithPageSize(20))
 88	if err != nil {
 89		return renderError("Invalid page")
 90	}
 91
 92	// Get current page number for back links
 93	currentPage := (pages.Offset() / pages.PageSize()) + 1
 94	users := ListUsers(currentPage, pages.PageSize())
 95
 96	for _, addrStr := range users {
 97		nickname := GetNickname(address(addrStr))
 98		if nickname != "" {
 99			output += "* [" + escapeMarkdown(nickname) + " (" + addrStr + ")](./user:user?address=" + addrStr + "&page=" + strconv.Itoa(currentPage) + ")\n"
100		} else {
101			output += "* [" + addrStr + "](./user:user?address=" + addrStr + "&page=" + strconv.Itoa(currentPage) + ")\n"
102		}
103	}
104
105	if pages.HasPages() {
106		output += "\n" + pager.Picker(pages)
107	}
108
109	return output
110}
111
112// renderUser renders a single user detail
113func renderUser(iUrl string, addr address) string {
114	if !addr.IsValid() {
115		return renderError("Invalid address")
116	}
117
118	u, _ := url.Parse(iUrl)
119	page := normalizeRenderPage(u.Query().Get("page"))
120
121	addrStr := addr.String()
122	if !HasUser(addr) {
123		return renderError("User not found")
124	}
125
126	nickname := GetNickname(addr)
127	metadata := ListUserMetadataByAddresses(addr)
128	users := ListUsersByAddresses(addr)
129
130	output := "# User Detail\n\n"
131	if page != "" {
132		output += "[← Back to List](./user:users?page=" + page + ")\n\n"
133	} else {
134		output += "[← Home](./user:)\n\n"
135	}
136	if nickname != "" {
137		output += "* **Nickname**: " + escapeMarkdown(nickname) + "\n"
138	}
139	output += "* **Address**: " + addrStr + " [[View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/player?address=" + addrStr + ")]\n\n"
140
141	if len(metadata) > 0 && metadata[0] != "" {
142		output += "## Metadata\n\n"
143		output += escapeMarkdown(metadata[0]) + "\n"
144		output += "\n"
145	}
146
147	if len(users) > 0 {
148		output += "## Properties\n\n"
149		for k, v := range users[0] {
150			if k == "address" {
151				continue
152			}
153			output += "* **" + escapeMarkdown(k) + "**: " + escapeMarkdown(v) + "\n"
154		}
155	}
156
157	return output
158}
159
160func normalizeRenderPage(rawPage string) string {
161	if rawPage == "" {
162		return ""
163	}
164	page, err := strconv.Atoi(rawPage)
165	if err != nil || page < 1 {
166		return ""
167	}
168	return strconv.Itoa(page)
169}
170
171// renderNicknameLookup renders nickname search result
172func renderNicknameLookup(nickname string) string {
173	if nickname == "" {
174		return renderError("Nickname is required")
175	}
176
177	if len(nickname) > 20 ||
178		strings.TrimSpace(nickname) == "" ||
179		strings.TrimSpace(nickname) != nickname ||
180		isURLLikeNickname(nickname) ||
181		hasControlChar(nickname) {
182		return renderError("Invalid nickname")
183	}
184
185	if !HasNickname(nickname) {
186		return renderError("User not found")
187	}
188
189	addr := GetAddressByNickname(nickname)
190	return renderUser("", addr)
191}
192
193// renderError renders an error page
194func renderError(message string) string {
195	output := "# Error\n\n"
196	output += "> " + message + "\n\n"
197	output += "[← Back to Home](./user:)\n"
198	return output
199}
200
201func escapeMarkdown(s string) string {
202	s = strings.ReplaceAll(s, "\\", "\\\\")
203	s = strings.ReplaceAll(s, "\n", "\\n")
204	s = strings.ReplaceAll(s, "`", "\\`")
205	s = strings.ReplaceAll(s, "*", "\\*")
206	s = strings.ReplaceAll(s, "_", "\\_")
207	s = strings.ReplaceAll(s, "{", "\\{")
208	s = strings.ReplaceAll(s, "}", "\\}")
209	s = strings.ReplaceAll(s, "[", "\\[")
210	s = strings.ReplaceAll(s, "]", "\\]")
211	s = strings.ReplaceAll(s, "(", "\\(")
212	s = strings.ReplaceAll(s, ")", "\\)")
213	s = strings.ReplaceAll(s, "#", "\\#")
214	s = strings.ReplaceAll(s, "+", "\\+")
215	s = strings.ReplaceAll(s, "-", "\\-")
216	s = strings.ReplaceAll(s, ".", "\\.")
217	s = strings.ReplaceAll(s, "!", "\\!")
218	s = strings.ReplaceAll(s, "|", "\\|")
219	s = strings.ReplaceAll(s, ">", "\\>")
220	return s
221}