sheet.gno
6.60 Kb · 326 lines
1package arenaview
2
3import (
4 "strconv"
5 "strings"
6
7 arena "gno.land/r/g10ragdr3cjqkael8rslpw4y6rlelhev4h0fswcx/arena"
8)
9
10const pageSize = 8
11
12// Sheet is the stable text board the website reads.
13// kind selects the page. a, b, and c are cursors or decimal limits.
14// Gated bodies are left empty. The fund realm still stores them.
15func Sheet(kind, a, b, c string) string {
16 var body string
17 switch kind {
18 case "feed":
19 body = sheetPosts(arena.Feed(atoiOpt(a), atoiLimit(b)), false)
20 case "author":
21 p, ok := arena.ProfileByHandle(a)
22 if !ok {
23 panic("unknown handle")
24 }
25 body = sheetPosts(arena.PostsBy(p.Addr, atoiOpt(b), atoiLimit(c)), false)
26 case "replies":
27 body = sheetPosts(arena.Replies(atoiReq(a), atoiOpt(b), atoiLimit(c)), true)
28 case "post":
29 body = sheetOne(atoiReq(a))
30 case "people":
31 body = sheetProfiles(arena.Handles(a, atoiLimit(b)))
32 case "profile":
33 body = sheetProfile(a)
34 case "account":
35 body = sheetAccount(a)
36 case "hot":
37 body = sheetPosts(arena.Hot(atoiLimit(a)), false)
38 case "followfeed":
39 body = sheetFollowFeed(a, atoiLimit(b))
40 case "graph":
41 body = sheetProfiles(arena.FollowingList(mustAddr(a), optAddr(b), atoiLimit(c)))
42 case "fans":
43 body = sheetProfiles(arena.FollowerList(mustAddr(a), optAddr(b), atoiLimit(c)))
44 case "holdings":
45 body = sheetHoldings(arena.Holdings(mustAddr(a), optAddr(b), atoiLimit(c)))
46 case "meta":
47 body = sheetMeta()
48 case "quotebuy":
49 due, curve, cf, pf, rf := arena.QuoteBuy(a, atoiReq(b))
50 body = join5(due, curve, cf, pf, rf)
51 case "quotesell":
52 proceeds, curve, cf, pf, rf := arena.QuoteSell(a, atoiReq(b))
53 body = join5(proceeds, curve, cf, pf, rf)
54 case "shares":
55 body = strconv.Itoa(arena.SharesOf(mustAddr(a), b))
56 default:
57 panic("kind")
58 }
59 return "v1\n" + body
60}
61
62func sheetPosts(posts []arena.Article, full bool) string {
63 var b strings.Builder
64 for i, p := range posts {
65 if i > 0 {
66 b.WriteString("\n")
67 }
68 if full {
69 b.WriteString(postLine(p))
70 } else {
71 b.WriteString(cardLine(p))
72 }
73 }
74 return b.String()
75}
76
77func sheetOne(id int) string {
78 p, ok := arena.GetPost(id)
79 if !ok {
80 return ""
81 }
82 return postLine(p)
83}
84
85func sheetProfile(handle string) string {
86 p, ok := arena.ProfileByHandle(handle)
87 if !ok {
88 return ""
89 }
90 return profileLine(p)
91}
92
93func sheetAccount(raw string) string {
94 who := optAddr(raw)
95 if !who.IsValid() {
96 return ""
97 }
98 p, ok := arena.ProfileOf(who)
99 if !ok {
100 return ""
101 }
102 return profileLine(p)
103}
104
105func sheetProfiles(rows []arena.Profile) string {
106 var b strings.Builder
107 for i, p := range rows {
108 if i > 0 {
109 b.WriteString("\n")
110 }
111 b.WriteString(p.Handle + "\t" + esc(p.Name) + "\t" + strconv.Itoa(p.Followers) + "\t" + strconv.Itoa(p.Posts))
112 }
113 return b.String()
114}
115
116func sheetHoldings(rows []arena.Holding) string {
117 var b strings.Builder
118 for i, h := range rows {
119 if i > 0 {
120 b.WriteString("\n")
121 }
122 b.WriteString(h.Creator.String() + "\t" + esc(h.Handle) + "\t" + strconv.Itoa(h.Amount))
123 }
124 return b.String()
125}
126
127func sheetFollowFeed(raw string, limit int) string {
128 who := mustAddr(raw)
129 posts := arena.Feed(0, limit)
130 var b strings.Builder
131 n := 0
132 for _, p := range posts {
133 prof, ok := arena.ProfileOf(p.Author)
134 if !ok || !arena.Following(who, prof.Handle) {
135 continue
136 }
137 if n > 0 {
138 b.WriteString("\n")
139 }
140 b.WriteString(cardLine(p))
141 n++
142 }
143 return b.String()
144}
145
146func sheetMeta() string {
147 paused := "0"
148 if arena.Paused() {
149 paused = "1"
150 }
151 return strings.Join([]string{
152 arena.Version(),
153 arena.Successor(),
154 paused,
155 strconv.FormatInt(arena.TotalReserve(), 10),
156 arena.Admin().String(),
157 arena.Treasury().String(),
158 strconv.Itoa(arena.MemberCount()),
159 strconv.Itoa(arena.FeedCount()),
160 strconv.Itoa(arena.LatestID()),
161 }, "\t")
162}
163
164func profileLine(p arena.Profile) string {
165 supply, reserve, holders := arena.SupplyOf(p.Handle)
166 next := "-1"
167 if supply < arena.MaxSupply {
168 due, _, _, _, _ := arena.QuoteBuy(p.Handle, 1)
169 next = strconv.FormatInt(due, 10)
170 }
171 ref := ""
172 if p.Referrer.IsValid() {
173 ref = p.Referrer.String()
174 }
175 return strings.Join([]string{
176 p.Handle,
177 esc(p.Name),
178 esc(p.Bio),
179 p.Addr.String(),
180 ref,
181 strconv.Itoa(p.Posts),
182 strconv.Itoa(p.Followers),
183 strconv.Itoa(p.Following),
184 strconv.FormatInt(p.TipsIn, 10),
185 strconv.FormatInt(p.TipsOut, 10),
186 strconv.Itoa(supply),
187 strconv.FormatInt(reserve, 10),
188 strconv.Itoa(holders),
189 next,
190 }, "\t")
191}
192
193func cardLine(p arena.Article) string {
194 h, name := namesOf(p.Author)
195 return strings.Join([]string{
196 strconv.Itoa(p.ID),
197 esc(h),
198 esc(name),
199 strconv.Itoa(p.Likes),
200 strconv.Itoa(p.Replies),
201 strconv.FormatInt(p.Tips, 10),
202 bit(p.Gated),
203 esc(openPreview(p)),
204 }, "\t")
205}
206
207func postLine(p arena.Article) string {
208 h, name := namesOf(p.Author)
209 body := ""
210 if !p.Gated && !p.Deleted {
211 body = p.Body
212 }
213 return strings.Join([]string{
214 strconv.Itoa(p.ID),
215 p.Author.String(),
216 esc(h),
217 esc(name),
218 strconv.Itoa(p.ReplyTo),
219 strconv.Itoa(p.Likes),
220 strconv.Itoa(p.Replies),
221 strconv.FormatInt(p.Tips, 10),
222 bit(p.Gated),
223 bit(p.Deleted),
224 strconv.FormatInt(p.Height, 10),
225 esc(body),
226 }, "\t")
227}
228
229func namesOf(a address) (handle, name string) {
230 p, ok := arena.ProfileOf(a)
231 if !ok {
232 return a.String(), a.String()
233 }
234 return p.Handle, p.Name
235}
236
237func openPreview(p arena.Article) string {
238 if p.Gated || p.Deleted {
239 return ""
240 }
241 return preview(strings.ReplaceAll(p.Body, "\n", " "), 160)
242}
243
244func bit(v bool) string {
245 if v {
246 return "1"
247 }
248 return "0"
249}
250
251func esc(s string) string {
252 s = strings.ReplaceAll(s, "\\", "\\\\")
253 s = strings.ReplaceAll(s, "\r", "")
254 s = strings.ReplaceAll(s, "\n", "\\n")
255 s = strings.ReplaceAll(s, "\t", "\\t")
256 return s
257}
258
259func join5(a, b, c, d, e int64) string {
260 return strconv.FormatInt(a, 10) + "\t" + strconv.FormatInt(b, 10) + "\t" + strconv.FormatInt(c, 10) + "\t" + strconv.FormatInt(d, 10) + "\t" + strconv.FormatInt(e, 10)
261}
262
263func atoiOpt(s string) int {
264 s = strings.TrimSpace(s)
265 if s == "" {
266 return 0
267 }
268 n, err := strconv.Atoi(s)
269 if err != nil || n < 0 {
270 panic("cursor")
271 }
272 return n
273}
274
275func atoiReq(s string) int {
276 n := atoiOpt(s)
277 if n < 1 {
278 panic("id")
279 }
280 return n
281}
282
283func atoiLimit(s string) int {
284 s = strings.TrimSpace(s)
285 if s == "" {
286 return pageSize
287 }
288 n, err := strconv.Atoi(s)
289 if err != nil {
290 panic("limit")
291 }
292 if n < 1 || n > 20 {
293 panic("limit")
294 }
295 return n
296}
297
298func mustAddr(s string) address {
299 a := optAddr(s)
300 if !a.IsValid() {
301 panic("bad address")
302 }
303 return a
304}
305
306func optAddr(s string) address {
307 s = strings.TrimSpace(s)
308 if s == "" {
309 return address("")
310 }
311 if len(s) > 60 {
312 panic("bad address")
313 }
314 return address(s)
315}
316
317func preview(s string, n int) string {
318 if len(s) <= n {
319 return s
320 }
321 i := n
322 for i > 0 && s[i]&0xC0 == 0x80 {
323 i--
324 }
325 return s[:i] + "…"
326}