public.gno
13.80 Kb · 407 lines
1package social
2
3import (
4 "strconv"
5
6 "gno.land/p/nt/bptree/v0"
7 "gno.land/p/nt/ufmt/v0"
8 "gno.land/r/sys/users"
9)
10
11type UserAndPostID struct {
12 UserPostAddr address
13 PostID PostID
14}
15
16// Post a message to the caller's main user posts.
17// The caller must already be registered with /r/gnoland/users/v1 Register.
18// Return the "thread ID" of the new post.
19// (This is similar to boards.CreateThread, but no message title)
20func PostMessage(cur realm, body string) PostID {
21 caller := cur.Previous().Address()
22 userPosts := getOrCreateUserPosts(caller, usernameOf(caller))
23 thread := userPosts.AddThread(body)
24 return thread.id
25}
26
27// Post a reply to the user posts of userPostsAddr where threadid is the ID returned by
28// the original call to PostMessage. If postid == threadid then create another top-level
29// post for the threadid, otherwise post a reply to the postid "sub reply".
30// The caller must already be registered with /r/gnoland/users/v1 Register.
31// Return the new post ID.
32// (This is similar to boards.CreateReply.)
33func PostReply(cur realm, userPostsAddr address, threadid, postid PostID, body string) PostID {
34 caller := cur.Previous().Address()
35 userPosts := getUserPosts(userPostsAddr)
36 if userPosts == nil {
37 panic("posts for userPostsAddr do not exist")
38 }
39 thread := userPosts.GetThread(threadid)
40 if thread == nil {
41 panic("threadid in user posts does not exist")
42 }
43 if postid == threadid {
44 reply := thread.AddReply(caller, body)
45 return reply.id
46 } else {
47 post := thread.GetReply(postid)
48 if post == nil {
49 panic("postid does not exist")
50 }
51 reply := post.AddReply(caller, body)
52 return reply.id
53 }
54}
55
56// Repost the message from the user posts of userPostsAddr where threadid is the ID returned by
57// the original call to PostMessage. This must be a top-level thread (not a reply).
58// Return the new post ID.
59// (This is similar to boards.CreateRepost.)
60func RepostThread(cur realm, userPostsAddr address, threadid PostID, comment string) PostID {
61 caller := cur.Previous().Address()
62 if userPostsAddr == caller {
63 panic("Cannot repost a user's own message")
64 }
65
66 dstUserPosts := getOrCreateUserPosts(caller, usernameOf(caller))
67
68 userPosts := getUserPosts(userPostsAddr)
69 if userPosts == nil {
70 panic("posts for userPostsAddr do not exist")
71 }
72 thread := userPosts.GetThread(threadid)
73 if thread == nil {
74 panic("threadid in user posts does not exist")
75 }
76 repost := thread.AddRepostTo(caller, comment, dstUserPosts)
77 return repost.id
78}
79
80// For each address/PostID in addrAndIDs, get the thread post. The Post ID must be
81// for a a top-level thread (not a reply; to get reply posts, use GetThreadPosts).
82// If the Post ID is not found, set the result for that Post ID to {}.
83// The response is a JSON string.
84func GetJsonTopPostsByID(addrAndIDs []UserAndPostID) string {
85 json := "[ "
86 for _, addrAndID := range addrAndIDs {
87 if len(json) > 2 {
88 json += ",\n "
89 }
90
91 userPosts := getUserPosts(addrAndID.UserPostAddr)
92 if userPosts == nil {
93 json += "{}"
94 continue
95 }
96
97 post := userPosts.GetThread(PostID(addrAndID.PostID))
98 if post == nil {
99 json += "{}"
100 continue
101 }
102
103 postJson, err := post.MarshalJSON()
104 if err != nil {
105 panic("can't get post JSON")
106 }
107 json += string(postJson)
108 }
109 json += "]"
110
111 return json
112}
113
114// Get posts in a thread for a user. A thread is the sequence of posts without replies.
115// While each post has an an arbitrary id, it also has an index within the thread starting from 0.
116// Limit the response to posts from startIndex up to (not including) endIndex within the thread.
117// If you just want the total count, set startIndex and endIndex to 0 and see the response "n_threads".
118// If threadID is 0 then return the user's top-level posts. (Like render args "user".)
119// If threadID is X and replyID is 0, then return the posts (without replies) in that thread. (Like render args "user/2".)
120// If threadID is X and replyID is Y, then return the posts in the thread starting with replyID. (Like render args "user/2/5".)
121// The response includes reposts by this user (only if threadID is 0), but not messages of other
122// users that are being followed. (See GetHomePosts.) The response is a JSON string.
123func GetThreadPosts(userPostsAddr address, threadID int, replyID int, startIndex int, endIndex int) string {
124 userPosts := getUserPosts(userPostsAddr)
125 if userPosts == nil {
126 panic("posts for userPostsAddr do not exist")
127 }
128
129 if threadID == 0 {
130 return getPosts(userPosts.threads, startIndex, endIndex)
131 }
132
133 thread := userPosts.GetThread(PostID(threadID))
134 if thread == nil {
135 panic(ufmt.Sprintf("thread does not exist with id %d", threadID))
136 }
137
138 if replyID == 0 {
139 return getPosts(thread.replies, startIndex, endIndex)
140 } else {
141 reply := thread.GetReply(PostID(replyID))
142 if reply == nil {
143 panic(ufmt.Sprintf("reply does not exist with id %d in thread with id %d", replyID, threadID))
144 }
145
146 return getPosts(reply.replies, startIndex, endIndex)
147 }
148}
149
150// Update the home posts by scanning all posts from all followed users and adding the
151// followed posts since the last call to RefreshHomePosts (or since started following the user).
152// Return the new count of home posts. The result is something like "(12 int)".
153func RefreshHomePosts(_ realm, userPostsAddr address) int {
154 userPosts := getUserPosts(userPostsAddr)
155 if userPosts == nil {
156 panic("posts for userPostsAddr do not exist")
157 }
158 userPosts.refreshHomePosts()
159
160 return userPosts.homePosts.Size()
161}
162
163// Get the number of posts which GetHomePosts or GetJsonHomePosts will return.
164// The result is something like "(12 int)".
165// This returns the current count of the home posts (without need to pay gas). To include the
166// latest followed posts, call RefreshHomePosts.
167func GetHomePostsCount(userPostsAddr address) int {
168 return GetHomePosts(userPostsAddr).Size()
169}
170
171// Get home posts for a user, which are the user's top-level posts plus all posts of all
172// users being followed.
173// The response is a map of postID -> *Post. The bptree.BPTree sorts by the post ID which is
174// unique for every post and increases in time.
175// If you just want the total count, use GetHomePostsCount.
176// This returns the current state of the home posts (without need to pay gas). To include the
177// latest followed posts, call RefreshHomePosts.
178func GetHomePosts(userPostsAddr address) *bptree.BPTree {
179 userPosts := getUserPosts(userPostsAddr)
180 if userPosts == nil {
181 panic("posts for userPostsAddr do not exist")
182 }
183 return &userPosts.homePosts
184}
185
186// Get home posts for a user (using GetHomePosts), which are the user's top-level posts plus all
187// posts of all users being followed.
188// Limit the response to posts from startIndex up to (not including) endIndex within the home posts.
189// If you just want the total count, use GetHomePostsCount.
190// The response is a JSON string.
191// This returns the current state of the home posts (without need to pay gas). To include the
192// latest posts, call RefreshHomePosts.
193func GetJsonHomePosts(userPostsAddr address, startIndex int, endIndex int) string {
194 allPosts := GetHomePosts(userPostsAddr)
195 postsJson := ""
196 for i := startIndex; i < endIndex && i < allPosts.Size(); i++ {
197 _, postI := allPosts.GetByIndex(i)
198 if postsJson != "" {
199 postsJson += ",\n "
200 }
201
202 postJson, err := postI.(*Post).MarshalJSON()
203 if err != nil {
204 panic("can't get post JSON")
205 }
206 postsJson += ufmt.Sprintf("{\"index\": %d, \"post\": %s}", int(i), string(postJson))
207 }
208
209 return ufmt.Sprintf("{\"n_posts\": %d, \"posts\": [\n %s]}", allPosts.Size(), postsJson)
210}
211
212// Update the caller to follow the user with followedAddr. See UserPosts.Follow.
213func Follow(cur realm, followedAddr address) PostID {
214 caller := cur.Previous().Address()
215 if followedAddr == caller {
216 panic("you can't follow yourself")
217 }
218
219 // A user can follow someone before doing any posts, so create the UserPosts if needed.
220 userPosts := getOrCreateUserPosts(caller, usernameOf(caller))
221 return userPosts.Follow(followedAddr)
222}
223
224// Update the caller to unfollow the user with followedAddr. See UserPosts.Unfollow.
225func Unfollow(cur realm, followedAddr address) {
226 caller := cur.Previous().Address()
227 userPosts := getUserPosts(caller)
228 if userPosts == nil {
229 // We don't expect this, but just do nothing.
230 return
231 }
232
233 userPosts.Unfollow(followedAddr)
234}
235
236// Add the reaction by the caller to the post of userPostsAddr, where threadid is the ID
237// returned by the original call to PostMessage. If postid == threadid then add the reaction
238// to a top-level post for the threadid, otherwise add the reaction to the postid "sub reply".
239// (This function's arguments are similar to PostReply.)
240// The caller must already be registered with /r/gnoland/users/v1 Register.
241// Return a boolean indicating whether the userAddr was added. See Post.AddReaction.
242func AddReaction(cur realm, userPostsAddr address, threadid, postid PostID, reaction Reaction) bool {
243 caller := cur.Previous().Address()
244 userPosts := getUserPosts(userPostsAddr)
245 if userPosts == nil {
246 panic("posts for userPostsAddr do not exist")
247 }
248 thread := userPosts.GetThread(threadid)
249 if thread == nil {
250 panic("threadid in user posts does not exist")
251 }
252 if postid == threadid {
253 return thread.AddReaction(caller, reaction)
254 } else {
255 post := thread.GetReply(postid)
256 if post == nil {
257 panic("postid does not exist")
258 }
259 return post.AddReaction(caller, reaction)
260 }
261}
262
263// Remove the reaction by the caller to the post of userPostsAddr, where threadid is the ID
264// returned by the original call to PostMessage. If postid == threadid then remove the reaction
265// from a top-level post for the threadid, otherwise remove the reaction from the postid "sub reply".
266// (This function's arguments are similar to PostReply.)
267// The caller must already be registered with /r/gnoland/users/v1 Register.
268// Return a boolean indicating whether the userAddr was removed. See Post.RemoveReaction.
269func RemoveReaction(cur realm, userPostsAddr address, threadid, postid PostID, reaction Reaction) bool {
270 caller := cur.Previous().Address()
271 userPosts := getUserPosts(userPostsAddr)
272 if userPosts == nil {
273 panic("posts for userPostsAddr do not exist")
274 }
275 thread := userPosts.GetThread(threadid)
276 if thread == nil {
277 panic("threadid in user posts does not exist")
278 }
279 if postid == threadid {
280 return thread.RemoveReaction(caller, reaction)
281 } else {
282 post := thread.GetReply(postid)
283 if post == nil {
284 panic("postid does not exist")
285 }
286 return post.RemoveReaction(caller, reaction)
287 }
288}
289
290// Call users.ResolveAddress and return the result as JSON, or "" if not found.
291// (This is a temporary utility until gno.land supports returning structured data directly.)
292func GetJsonUserByAddress(addr address) string {
293 user := users.ResolveAddress(addr)
294 if user == nil {
295 return ""
296 }
297
298 return marshalJsonUser(user)
299}
300
301// Call users.ResolveName and return the result as JSON, or "" if not found.
302// (This is a temporary utility until gno.land supports returning structured data directly.)
303func GetJsonUserByName(name string) string {
304 user, _ := users.ResolveName(name)
305 if user == nil {
306 return ""
307 }
308
309 return marshalJsonUser(user)
310}
311
312// Get the UserPosts info for the user with the given address, including
313// url, n_threads, n_followers and n_following. If the user address is not
314// found, return "". The name of this function has "Info" because it just returns
315// the number of items, not the items themselves. To get the items, see
316// GetJsonFollowers, etc.
317// The response is a JSON string.
318func GetJsonUserPostsInfo(address address) string {
319 userPosts := getUserPosts(address)
320 if userPosts == nil {
321 return ""
322 }
323
324 json, err := userPosts.MarshalJSON()
325 if err != nil {
326 panic("can't get UserPosts JSON")
327 }
328
329 return string(json)
330}
331
332// Get the UserPosts for the user with the given address, and return
333// the list of followers. If the user address is not found, return "".
334// Limit the response to entries from startIndex up to (not including) endIndex.
335// The response is a JSON string.
336func GetJsonFollowers(address address, startIndex int, endIndex int) string {
337 userPosts := getUserPosts(address)
338 if userPosts == nil {
339 return ""
340 }
341
342 json := ufmt.Sprintf("{\"n_followers\": %d, \"followers\": [\n ", userPosts.followers.Size())
343 for i := startIndex; i < endIndex && i < userPosts.followers.Size(); i++ {
344 addr, _ := userPosts.followers.GetByIndex(i)
345
346 if i > startIndex {
347 json += ",\n "
348 }
349 json += ufmt.Sprintf(`{"address": "%s"}`, addr)
350 }
351 json += "]}"
352
353 return json
354}
355
356// Get the UserPosts for the user with the given address, and return
357// the list of other users that this user is following.
358// If the user address is not found, return "".
359// Limit the response to entries from startIndex up to (not including) endIndex.
360// The response is a JSON string.
361func GetJsonFollowing(address address, startIndex int, endIndex int) string {
362 userPosts := getUserPosts(address)
363 if userPosts == nil {
364 return ""
365 }
366
367 json := ufmt.Sprintf("{\"n_following\": %d, \"following\": [\n ", userPosts.following.Size())
368 for i := startIndex; i < endIndex && i < userPosts.following.Size(); i++ {
369 addr, infoI := userPosts.following.GetByIndex(i)
370
371 if i > startIndex {
372 json += ",\n "
373 }
374 startedAt, err := infoI.(*FollowingInfo).startedFollowingAt.MarshalJSON()
375 if err != nil {
376 panic("can't get startedFollowingAt JSON")
377 }
378 json += ufmt.Sprintf(`{"address": "%s", "started_following_at": %s}`,
379 addr, string(startedAt))
380 }
381 json += "]}"
382
383 return json
384}
385
386// Get a list of user names starting from the given prefix. Limit the
387// number of results to maxResults.
388func ListUsersByPrefix(prefix string, maxResults int) []string {
389 return listByteStringKeysByPrefix(&gUserAddressByName, prefix, maxResults)
390}
391
392// Get a list of user names starting from the given prefix. Limit the
393// number of results to maxResults.
394// The response is a JSON string.
395func ListJsonUsersByPrefix(prefix string, maxResults int) string {
396 names := ListUsersByPrefix(prefix, maxResults)
397
398 json := "["
399 for i, name := range names {
400 if i > 0 {
401 json += ", "
402 }
403 json += strconv.Quote(name)
404 }
405 json += "]"
406 return json
407}