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.

render.gno

10.81 Kb · 326 lines
  1package reputation
  2
  3import (
  4	"net/url"
  5	"strconv"
  6	"strings"
  7
  8	"gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/guildhall/trust/v0"
  9	"gno.land/p/nt/markdown/sanitize/v0"
 10	"gno.land/p/nt/ufmt/v0"
 11)
 12
 13const (
 14	realmURL  = "/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/guildhall/reputation"
 15	pageSize  = 20
 16	boardSize = 25
 17)
 18
 19// Render serves the reputation pages:
 20//
 21//	""            leaderboard and latest records
 22//	u/<address>   contributor profile
 23//	record/<id>   a single record
 24//	records       every record, newest first (?page=N)
 25//	seeds         seed reviewers
 26//	about         scoring rules
 27func Render(path string) string {
 28	route, query := splitQuery(path)
 29	parts := strings.Split(route, "/")
 30	switch {
 31	case parts[0] == "api":
 32		return renderAPI(route, query)
 33	case route == "":
 34		return renderHome()
 35	case len(parts) == 2 && parts[0] == "u":
 36		return renderProfile(address(parts[1]))
 37	case len(parts) == 2 && parts[0] == "record":
 38		id, err := strconv.ParseUint(parts[1], 10, 64)
 39		if err != nil {
 40			return notFound()
 41		}
 42		return renderRecord(id)
 43	case route == "records":
 44		return renderRecords(pageOf(query))
 45	case route == "seeds":
 46		return renderSeeds()
 47	case route == "about":
 48		return renderAbout()
 49	}
 50	return notFound()
 51}
 52
 53func renderHome() string {
 54	var b strings.Builder
 55	b.WriteString("# Guildhall Reputation\n\n")
 56	b.WriteString("Reputation earned from paid, reviewed work. Every point traces back to a record, and every record to the people who vouched for it.\n\n")
 57	b.WriteString(ufmt.Sprintf("**%d** records · **%d** ranked contributors · **%d** seed reviewers · [scoring rules](%s:about) · [all records](%s:records)\n\n",
 58		records.Size(), leaderboard.Size(), seeds.Size(), realmURL, realmURL))
 59
 60	b.WriteString("## Leaderboard\n\n")
 61	top := Top(boardSize)
 62	if len(top) == 0 {
 63		b.WriteString("_No reputation has been earned yet. Complete a [bounty](/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/guildhall/bounties) to be the first._\n\n")
 64	} else {
 65		b.WriteString("| # | Contributor | Score | Level | Work | Reviews |\n|---:|---|---:|---|---:|---:|\n")
 66		for i, s := range top {
 67			b.WriteString(ufmt.Sprintf("| %d | %s | %d | %s | %d | %d |\n",
 68				i+1, userLink(s.Addr), s.Score, trust.Level(s.Score), CompletedCount(s.Addr), ReviewCount(s.Addr)))
 69		}
 70		b.WriteString("\n")
 71	}
 72
 73	b.WriteString("## Latest records\n\n")
 74	b.WriteString(recordTable(latest(0, 10)))
 75	b.WriteString("\n## Use it in your realm\n\n")
 76	b.WriteString("```go\nimport \"gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/guildhall/reputation\"\n\nreputation.AssertScore(cur.Previous().Address(), 300)\n```\n")
 77	return b.String()
 78}
 79
 80func renderProfile(addr address) string {
 81	if !addr.IsValid() {
 82		return notFound()
 83	}
 84	var b strings.Builder
 85	b.WriteString(ufmt.Sprintf("# %s\n\n", addr.String()))
 86	p := getProfile(addr)
 87	if p == nil {
 88		b.WriteString("_No recorded work or reviews yet._\n\n")
 89		b.WriteString(ufmt.Sprintf("[Browse open bounties](/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/guildhall/bounties?status=open) · [Leaderboard](%s)\n", realmURL))
 90		return b.String()
 91	}
 92
 93	rank := "unranked"
 94	if r := Rank(addr); r > 0 {
 95		rank = "#" + strconv.Itoa(r)
 96	}
 97	seed := ""
 98	if IsSeed(addr) {
 99		seed = " · seed reviewer"
100	}
101	b.WriteString(ufmt.Sprintf("**%d** points · %s · %s%s\n\n", p.Score, trust.Level(p.Score), rank, seed))
102	b.WriteString(ufmt.Sprintf("- Completed work: **%d**\n- Reviews given: **%d**\n", len(p.Records), len(p.Reviews)))
103	if p.earned != nil && p.earned.Size() > 0 {
104		var parts []string
105		p.earned.Iterate("", "", func(k string, v any) bool {
106			parts = append(parts, formatAmount(v.(int64), k))
107			return false
108		})
109		b.WriteString("- Earned: " + strings.Join(parts, ", ") + "\n")
110	}
111	if !p.First.IsZero() {
112		b.WriteString("- Active since: " + p.First.Format("2006-01-02") + "\n")
113	}
114	b.WriteString(ufmt.Sprintf("- Bounties: [%s](/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/guildhall/bounties:u/%s)\n\n", "view", addr.String()))
115
116	b.WriteString("## Work\n\n")
117	b.WriteString(recordTable(RecordsOf(addr, 0, pageSize)))
118	b.WriteString("\n## Reviews\n\n")
119	b.WriteString(recordTable(ReviewsOf(addr, 0, pageSize)))
120	return b.String()
121}
122
123func renderRecord(id uint64) string {
124	r := getRecord(id)
125	if r == nil {
126		return notFound()
127	}
128	var b strings.Builder
129	b.WriteString(ufmt.Sprintf("# Record #%d\n\n", r.ID))
130	if r.Voided {
131		b.WriteString("> **Voided:** " + sanitize.InlineText(r.VoidReason) + "\n\n")
132	}
133	b.WriteString("**" + sanitize.InlineText(r.Title) + "**\n\n")
134	b.WriteString("| | |\n|---|---|\n")
135	b.WriteString("| Worker | " + userLink(r.Worker) + " |\n")
136	b.WriteString("| Funded by | " + userLink(r.Poster) + " |\n")
137	b.WriteString("| Paid | " + formatAmount(r.Amount, r.Denom) + " |\n")
138	if r.ShareBps < trust.MaxBps {
139		b.WriteString(ufmt.Sprintf("| Share of milestone | %s |\n", bpsPercent(r.ShareBps)))
140	}
141	b.WriteString(ufmt.Sprintf("| Points | %d |\n", r.Points))
142	if r.Link != "" {
143		b.WriteString("| Work | " + linkOrText(r.Link) + " |\n")
144	}
145	b.WriteString("| Source | " + sourceLink(r) + " |\n")
146	b.WriteString("| Released by | " + userLink(r.SignedBy) + " |\n")
147	b.WriteString(ufmt.Sprintf("| Block | %d · %s |\n", r.Height, r.At.UTC().Format("2006-01-02 15:04 UTC")))
148	if r.Arbitrated {
149		b.WriteString("| Settled by | arbitration |\n")
150	}
151
152	b.WriteString("\n## Vouched for by\n\n| Reviewer | Trust at approval |\n|---|---:|\n")
153	for i, rv := range r.Reviewers {
154		t := int64(0)
155		if i < len(r.ReviewerTrust) {
156			t = r.ReviewerTrust[i]
157		}
158		b.WriteString("| " + userLink(rv) + " | " + bpsPercent(t) + " |\n")
159	}
160	return b.String()
161}
162
163func renderRecords(page int) string {
164	var b strings.Builder
165	b.WriteString("# All records\n\n")
166	b.WriteString(recordTable(latest((page-1)*pageSize, pageSize)))
167	b.WriteString("\n" + pager("records", page, records.Size()))
168	return b.String()
169}
170
171func renderSeeds() string {
172	var b strings.Builder
173	b.WriteString("# Seed reviewers\n\n")
174	b.WriteString("Seeds carry full trust when they approve work. Everyone else earns trust by completing work that trusted people approved. ")
175	b.WriteString(ufmt.Sprintf("Seeds are curated by %s.\n\n", userLink(Admin())))
176	if seeds.Size() == 0 {
177		b.WriteString("_No seeds yet._\n")
178		return b.String()
179	}
180	seeds.Iterate("", "", func(k string, _ any) bool {
181		b.WriteString("- " + userLink(address(k)) + "\n")
182		return false
183	})
184	return b.String()
185}
186
187func renderAbout() string {
188	p := params
189	var b strings.Builder
190	b.WriteString("# How reputation is scored\n\n")
191	b.WriteString("1. Reputation is only created when escrowed funds are released for finished work. Writing records is restricted to allowlisted realms:\n")
192	writers.Iterate("", "", func(k string, _ any) bool {
193		b.WriteString("   - [" + k + "](" + strings.TrimPrefix(k, "gno.land") + ")\n")
194		return false
195	})
196	b.WriteString(ufmt.Sprintf("2. A record is worth up to **%d** points, scaled by the average trust of the reviewers who approved it.\n", p.Base))
197	b.WriteString(ufmt.Sprintf("3. Seed reviewers carry full trust. Anyone else carries trust in proportion to their own score, reaching full trust at **%d** points.\n", p.TrustThreshold))
198	b.WriteString(ufmt.Sprintf("4. Reviewers with no score carry **%s** trust, so a ring of fresh accounts approving each other earns nothing.\n", bpsPercent(p.TrustFloorBps)))
199	b.WriteString(ufmt.Sprintf("5. Work funded by the worker themselves is scaled to **%s**. Nobody can vouch for their own work.\n", bpsPercent(p.SelfFundedFactorBps)))
200	b.WriteString("6. When a dispute ends in a split, the record's points scale with the share the arbiter awarded.\n")
201	b.WriteString("7. Points are fixed when the record is written. The Guildhall admin may void a fraudulent record, which removes its points.\n\n")
202	b.WriteString("## Levels\n\n| Score | Level |\n|---:|---|\n| 0 | Newcomer |\n| 1+ | Apprentice |\n| 300+ | Journeyman |\n| 800+ | Artisan |\n| 2000+ | Master |\n| 5000+ | Grandmaster |\n")
203	return b.String()
204}
205
206func recordTable(rs []Record) string {
207	if len(rs) == 0 {
208		return "_Nothing here yet._\n"
209	}
210	var b strings.Builder
211	b.WriteString("| # | Work | Worker | Paid | Points |\n|---:|---|---|---:|---:|\n")
212	for _, r := range rs {
213		title := sanitize.TableCell(r.Title)
214		if r.Voided {
215			title = "~~" + title + "~~"
216		}
217		b.WriteString(ufmt.Sprintf("| [%d](%s:record/%d) | %s | %s | %s | %d |\n",
218			r.ID, realmURL, r.ID, title, userLink(r.Worker), formatAmount(r.Amount, r.Denom), r.Points))
219	}
220	return b.String()
221}
222
223func latest(offset, limit int) []Record {
224	var out []Record
225	records.ReverseIterateByOffset(offset, limit, func(_ string, v any) bool {
226		out = append(out, copyRecord(v.(*Record)))
227		return false
228	})
229	return out
230}
231
232func userLink(addr address) string {
233	if addr == "" {
234		return "-"
235	}
236	s := addr.String()
237	short := s
238	if len(s) > 14 {
239		short = s[:8] + "…" + s[len(s)-4:]
240	}
241	return "[" + short + "](" + realmURL + ":u/" + s + ")"
242}
243
244func sourceLink(r *Record) string {
245	path := strings.TrimPrefix(r.Source, "gno.land")
246	if r.Source == BountiesPath && r.Ref != "" {
247		id := r.Ref
248		if i := strings.Index(id, "/"); i >= 0 {
249			id = id[:i]
250		}
251		return "[bounty " + sanitize.InlineText(r.Ref) + "](" + path + ":" + url.PathEscape(id) + ")"
252	}
253	return "[" + r.Source + "](" + path + ")"
254}
255
256func linkOrText(s string) string {
257	if u := sanitize.URL(s); u != "" && (strings.HasPrefix(s, "https://") || strings.HasPrefix(s, "/")) {
258		return "[" + sanitize.LinkTitle(s) + "](" + u + ")"
259	}
260	return sanitize.InlineText(s)
261}
262
263func formatAmount(amount int64, denom string) string {
264	if denom == "ugnot" {
265		return decimal(amount, 6) + " GNOT"
266	}
267	return strconv.FormatInt(amount, 10) + " " + sanitize.InlineText(denom)
268}
269
270func decimal(amount int64, decimals int) string {
271	s := strconv.FormatInt(amount, 10)
272	neg := strings.HasPrefix(s, "-")
273	s = strings.TrimPrefix(s, "-")
274	for len(s) <= decimals {
275		s = "0" + s
276	}
277	whole, frac := s[:len(s)-decimals], strings.TrimRight(s[len(s)-decimals:], "0")
278	if neg {
279		whole = "-" + whole
280	}
281	if frac == "" {
282		return whole
283	}
284	return whole + "." + frac
285}
286
287func bpsPercent(bps int64) string {
288	return decimal(bps, 2) + "%"
289}
290
291func pager(route string, page, total int) string {
292	pages := (total + pageSize - 1) / pageSize
293	if pages <= 1 {
294		return ""
295	}
296	var parts []string
297	if page > 1 {
298		parts = append(parts, ufmt.Sprintf("[← newer](%s:%s?page=%d)", realmURL, route, page-1))
299	}
300	parts = append(parts, ufmt.Sprintf("page %d of %d", page, pages))
301	if page < pages {
302		parts = append(parts, ufmt.Sprintf("[older →](%s:%s?page=%d)", realmURL, route, page+1))
303	}
304	return strings.Join(parts, " · ") + "\n"
305}
306
307func splitQuery(path string) (string, url.Values) {
308	route, raw := path, ""
309	if i := strings.Index(path, "?"); i >= 0 {
310		route, raw = path[:i], path[i+1:]
311	}
312	q, _ := url.ParseQuery(raw)
313	return strings.Trim(route, "/"), q
314}
315
316func pageOf(q url.Values) int {
317	p, err := strconv.Atoi(q.Get("page"))
318	if err != nil || p < 1 {
319		return 1
320	}
321	return p
322}
323
324func notFound() string {
325	return "# Not found\n\n[Back to the leaderboard](" + realmURL + ")\n"
326}