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