query.gno
5.67 Kb · 250 lines
1package gnocial
2
3import (
4 "strings"
5)
6
7// Holding is one creator's tickets in a wallet. It is a copy.
8type Holding struct {
9 Creator address
10 Handle string
11 Amount int
12}
13
14func mustLimit(n int) int {
15 if n < 1 || n > 20 {
16 panic("limit")
17 }
18 return n
19}
20
21// Feed lists newest top-level posts with id < before. before 0 starts at the newest.
22func Feed(before, limit int) []Article {
23 limit = mustLimit(limit)
24 posts := recentPosts(before, limit)
25 out := make([]Article, len(posts))
26 for i, p := range posts {
27 out[i] = *p
28 }
29 return out
30}
31
32// PostsBy lists one member's newest top-level posts with id < before.
33func PostsBy(addr address, before, limit int) []Article {
34 limit = mustLimit(limit)
35 prefix := addr.String() + "/"
36 out := make([]Article, 0, limit)
37 state.byAuthor.ReverseIterate(prefix, prefix+"~", func(key string, value any) bool {
38 id, ok := value.(int)
39 if !ok {
40 panic("author type")
41 }
42 if before > 0 && id >= before {
43 return false
44 }
45 p := postByID(id)
46 if p == nil || p.Deleted {
47 return false
48 }
49 out = append(out, *p)
50 return len(out) >= limit
51 })
52 return out
53}
54
55// Replies lists direct replies of parent with id > after, oldest first.
56func Replies(parent, after, limit int) []Article {
57 limit = mustLimit(limit)
58 if parent <= 0 {
59 panic("id")
60 }
61 prefix := pad(parent) + "/"
62 start := prefix
63 if after > 0 {
64 start = prefix + pad(after) + "\x00"
65 }
66 out := make([]Article, 0, limit)
67 state.replies.Iterate(start, prefix+"~", func(key string, value any) bool {
68 id, ok := value.(int)
69 if !ok {
70 panic("reply type")
71 }
72 p := postByID(id)
73 if p == nil || p.Deleted {
74 return false
75 }
76 out = append(out, *p)
77 return len(out) >= limit
78 })
79 return out
80}
81
82// Handles lists profiles in handle order, strictly after the given handle.
83func Handles(after string, limit int) []Profile {
84 limit = mustLimit(limit)
85 start := ""
86 after = strings.ToLower(strings.TrimSpace(after))
87 if after != "" {
88 start = after + "\x00"
89 }
90 out := make([]Profile, 0, limit)
91 state.byHandle.Iterate(start, "", func(key string, value any) bool {
92 p, ok := value.(*Profile)
93 if !ok {
94 panic("profile type")
95 }
96 out = append(out, *p)
97 return len(out) >= limit
98 })
99 return out
100}
101
102// FollowingList lists profiles `who` follows, in address order, after `after`.
103func FollowingList(who, after address, limit int) []Profile {
104 return edgeList(who, after, limit, ">")
105}
106
107// FollowerList lists profiles that follow `who`, in address order, after `after`.
108func FollowerList(who, after address, limit int) []Profile {
109 return edgeList(who, after, limit, "<")
110}
111
112func edgeList(who, after address, limit int, sep string) []Profile {
113 limit = mustLimit(limit)
114 if !who.IsValid() {
115 panic("bad address")
116 }
117 prefix := who.String() + sep
118 start := prefix
119 if after.IsValid() {
120 start = prefix + after.String() + "\x00"
121 }
122 out := make([]Profile, 0, limit)
123 tree := state.follows
124 if sep == "<" {
125 tree = state.fans
126 }
127 tree.Iterate(start, prefix+"~", func(key string, value any) bool {
128 i := strings.LastIndex(key, sep)
129 if i < 0 {
130 return false
131 }
132 p := profileByAddr(address(key[i+1:]))
133 if p == nil {
134 return false
135 }
136 out = append(out, *p)
137 return len(out) >= limit
138 })
139 return out
140}
141
142// Holdings lists ticket balances of holder, after the given creator address.
143func Holdings(holder, after address, limit int) []Holding {
144 limit = mustLimit(limit)
145 if !holder.IsValid() {
146 panic("bad address")
147 }
148 prefix := holder.String() + "/"
149 start := prefix
150 if after.IsValid() {
151 start = prefix + after.String() + "\x00"
152 }
153 out := make([]Holding, 0, limit)
154 state.shares.Iterate(start, prefix+"~", func(key string, value any) bool {
155 n, ok := value.(int)
156 if !ok {
157 panic("share type")
158 }
159 i := strings.LastIndex(key, "/")
160 if i < 0 || n < 1 {
161 return false
162 }
163 creator := address(key[i+1:])
164 handle := ""
165 if p := profileByAddr(creator); p != nil {
166 handle = p.Handle
167 }
168 out = append(out, Holding{Creator: creator, Handle: handle, Amount: n})
169 return len(out) >= limit
170 })
171 return out
172}
173
174// Hot ranks the newest window the same way Render does. It is not a global index.
175func Hot(limit int) []Article {
176 limit = mustLimit(limit)
177 posts := recentPosts(0, hotWindow)
178 top := make([]scoredPost, 0, limit)
179 for _, p := range posts {
180 score := int64(p.Likes)*1_000_000 + p.Tips
181 top = insertRank(top, scoredPost{p: p, score: score}, limit)
182 }
183 out := make([]Article, len(top))
184 for i, row := range top {
185 out[i] = *row.p
186 }
187 return out
188}
189
190// MemberCount is the number of registered handles.
191func MemberCount() int { return state.byHandle.Size() }
192
193// FeedCount is the number of live top-level posts still in the feed index.
194func FeedCount() int { return state.feed.Size() }
195
196// LatestID is the newest post id, or 0 when nothing has been posted.
197func LatestID() int {
198 if state.nextID <= 1 {
199 return 0
200 }
201 return state.nextID - 1
202}
203
204const hotWindow = 30
205
206type scoredPost struct {
207 p *Article
208 score int64
209}
210
211func insertRank(top []scoredPost, row scoredPost, limit int) []scoredPost {
212 if len(top) == limit && row.score <= top[len(top)-1].score {
213 return top
214 }
215 top = append(top, row)
216 for i := len(top) - 1; i > 0; i-- {
217 if top[i].score <= top[i-1].score {
218 break
219 }
220 top[i], top[i-1] = top[i-1], top[i]
221 }
222 if len(top) > limit {
223 top = top[:limit]
224 }
225 return top
226}
227
228func recentPosts(before, limit int) []*Article {
229 end := ""
230 if before > 0 {
231 end = pad(before)
232 }
233 out := make([]*Article, 0, limit)
234 state.feed.ReverseIterate("", end, func(key string, value any) bool {
235 id, ok := value.(int)
236 if !ok {
237 panic("feed type")
238 }
239 if before > 0 && id >= before {
240 return false
241 }
242 p := postByID(id)
243 if p == nil || p.Deleted || p.ReplyTo != 0 {
244 return false
245 }
246 out = append(out, p)
247 return len(out) >= limit
248 })
249 return out
250}