render.gno
5.36 Kb · 221 lines
1package user
2
3import (
4 "net/url"
5 "strconv"
6 "strings"
7
8 "gno.land/p/g1nqnrt3aldzhu6zzeg75yw97wvavqy7wr77g56q/deploy-test/v2/validate"
9 "gno.land/p/jeronimoalbi/pager"
10 "gno.land/r/g1nqnrt3aldzhu6zzeg75yw97wvavqy7wr77g56q/deploy-test/v2/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}