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