api.gno
5.26 Kb · 191 lines
1package reputation
2
3import (
4 "net/url"
5 "strconv"
6 "strings"
7
8 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/guildhall/jsonw/v0"
9 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/guildhall/trust/v0"
10 "gno.land/p/nt/avl/v0"
11)
12
13const apiMaxLimit = 50
14
15// renderAPI serves JSON for the web app, under Render("api/..."):
16//
17// api/stats totals, parameters, admin
18// api/top?limit= leaderboard
19// api/profile/<address> one contributor
20// api/records?addr=&kind=work|reviews&page=&limit= newest first; all records without addr
21// api/record/<id> one record
22// api/seeds seed reviewers
23func renderAPI(route string, q url.Values) string {
24 parts := strings.Split(route, "/")
25 switch {
26 case route == "api/stats":
27 return jsonw.Obj(
28 "records", jsonw.Int(int64(records.Size())),
29 "contributors", jsonw.Int(int64(leaderboard.Size())),
30 "seeds", jsonw.Int(int64(seeds.Size())),
31 "admin", jsonw.String(Admin().String()),
32 "params", jsonw.Obj(
33 "base", jsonw.Int(params.Base),
34 "trustThreshold", jsonw.Int(params.TrustThreshold),
35 "trustFloorBps", jsonw.Int(params.TrustFloorBps),
36 "selfFundedFactorBps", jsonw.Int(params.SelfFundedFactorBps),
37 ),
38 "writers", jsonw.Strings(keys(&writers)),
39 )
40 case route == "api/top":
41 var items []string
42 for i, s := range Top(apiLimit(q)) {
43 items = append(items, jsonw.Obj(
44 "rank", jsonw.Int(int64(i+1)),
45 "address", jsonw.String(s.Addr.String()),
46 "score", jsonw.Int(s.Score),
47 "level", jsonw.String(trust.Level(s.Score)),
48 "completed", jsonw.Int(int64(CompletedCount(s.Addr))),
49 "reviews", jsonw.Int(int64(ReviewCount(s.Addr))),
50 "seed", jsonw.Bool(IsSeed(s.Addr)),
51 ))
52 }
53 return jsonw.Obj("items", jsonw.Arr(items))
54 case len(parts) == 3 && parts[1] == "profile":
55 return apiProfile(address(parts[2]))
56 case route == "api/records":
57 return apiRecords(q)
58 case len(parts) == 3 && parts[1] == "record":
59 id, err := strconv.ParseUint(parts[2], 10, 64)
60 if err != nil {
61 return apiError("invalid id")
62 }
63 r := getRecord(id)
64 if r == nil {
65 return apiError("not found")
66 }
67 return recordJSON(r)
68 case route == "api/seeds":
69 return jsonw.Obj("items", jsonw.Strings(keys(&seeds)))
70 }
71 return apiError("unknown endpoint")
72}
73
74func apiProfile(addr address) string {
75 if !addr.IsValid() {
76 return apiError("invalid address")
77 }
78 p := getProfile(addr)
79 var earned []string
80 score, first, last := int64(0), jsonw.Null, jsonw.Null
81 if p != nil {
82 score, first, last = p.Score, jsonw.Time(p.First), jsonw.Time(p.Last)
83 if p.earned != nil {
84 p.earned.Iterate("", "", func(k string, v any) bool {
85 earned = append(earned, jsonw.Obj("denom", jsonw.String(k), "amount", jsonw.Int(v.(int64))))
86 return false
87 })
88 }
89 }
90 return jsonw.Obj(
91 "address", jsonw.String(addr.String()),
92 "score", jsonw.Int(score),
93 "level", jsonw.String(trust.Level(score)),
94 "rank", jsonw.Int(int64(Rank(addr))),
95 "seed", jsonw.Bool(IsSeed(addr)),
96 "completed", jsonw.Int(int64(CompletedCount(addr))),
97 "reviews", jsonw.Int(int64(ReviewCount(addr))),
98 "earned", jsonw.Arr(earned),
99 "firstAt", first,
100 "lastAt", last,
101 )
102}
103
104func apiRecords(q url.Values) string {
105 limit := apiLimit(q)
106 page := 1
107 if n, err := strconv.Atoi(q.Get("page")); err == nil && n > 0 {
108 page = n
109 }
110 offset := (page - 1) * limit
111
112 var rs []Record
113 total := 0
114 if a := q.Get("addr"); a != "" {
115 addr := address(a)
116 if !addr.IsValid() {
117 return apiError("invalid address")
118 }
119 if q.Get("kind") == "reviews" {
120 rs = ReviewsOf(addr, offset, limit)
121 total = ReviewCount(addr)
122 } else {
123 rs = RecordsOf(addr, offset, limit)
124 total = CompletedCount(addr)
125 }
126 } else {
127 rs = latest(offset, limit)
128 total = records.Size()
129 }
130 items := make([]string, len(rs))
131 for i := range rs {
132 items[i] = recordJSON(&rs[i])
133 }
134 return jsonw.Obj("items", jsonw.Arr(items), "more", jsonw.Bool(total > page*limit), "total", jsonw.Int(int64(total)))
135}
136
137func recordJSON(r *Record) string {
138 return jsonw.Obj(
139 "id", jsonw.Uint(r.ID),
140 "worker", jsonw.String(r.Worker.String()),
141 "poster", jsonw.String(r.Poster.String()),
142 "reviewers", jsonw.Addresses(r.Reviewers),
143 "reviewerTrustBps", int64s(r.ReviewerTrust),
144 "points", jsonw.Int(r.Points),
145 "shareBps", jsonw.Int(r.ShareBps),
146 "amount", jsonw.Int(r.Amount),
147 "denom", jsonw.String(r.Denom),
148 "title", jsonw.String(r.Title),
149 "link", jsonw.String(r.Link),
150 "ref", jsonw.String(r.Ref),
151 "source", jsonw.String(r.Source),
152 "signedBy", jsonw.String(r.SignedBy.String()),
153 "arbitrated", jsonw.Bool(r.Arbitrated),
154 "height", jsonw.Int(r.Height),
155 "at", jsonw.Time(r.At),
156 "voided", jsonw.Bool(r.Voided),
157 "voidReason", jsonw.String(r.VoidReason),
158 )
159}
160
161func int64s(ns []int64) string {
162 out := make([]string, len(ns))
163 for i, n := range ns {
164 out[i] = jsonw.Int(n)
165 }
166 return jsonw.Arr(out)
167}
168
169func keys(t *avl.Tree) []string {
170 var out []string
171 t.Iterate("", "", func(k string, _ any) bool {
172 out = append(out, k)
173 return false
174 })
175 return out
176}
177
178func apiLimit(q url.Values) int {
179 n, err := strconv.Atoi(q.Get("limit"))
180 if err != nil || n < 1 {
181 return 20
182 }
183 if n > apiMaxLimit {
184 return apiMaxLimit
185 }
186 return n
187}
188
189func apiError(msg string) string {
190 return jsonw.Obj("error", jsonw.String(msg))
191}