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.

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}