Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}