Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}