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