Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
golang 1.24.4
golang 1.25.9
25 changes: 17 additions & 8 deletions realm/post.gno
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"gno.land/p/moul/txlink/v0"
"gno.land/p/nt/bptree/v0"
"gno.land/p/nt/markdown/sanitize/v0"
"gno.land/p/nt/ufmt/v0"
)

Expand Down Expand Up @@ -71,7 +72,7 @@ func (post *Post) GetPostID() PostID {
return post.id
}

func (post *Post) AddReply(creator address, body string) *Post {
func (post *Post) addReply(creator address, body string) *Post {
userPosts := post.userPosts
pid := userPosts.incGetPostID()
pidkey := postIDKey(pid)
Expand All @@ -86,7 +87,7 @@ func (post *Post) AddReply(creator address, body string) *Post {
return reply
}

func (post *Post) AddRepostTo(creator address, comment string, dst *UserPosts) *Post {
func (post *Post) addRepostTo(creator address, comment string, dst *UserPosts) *Post {
if !post.IsThread() {
panic("cannot repost non-thread post")
}
Expand Down Expand Up @@ -116,7 +117,7 @@ func (post *Post) GetReply(pid PostID) *Post {
// If userAddr is already added, do nothing.
// If the userAddr is the post's creator, do nothing. (Don't react to one's own posts.)
// Return a boolean indicating whether the userAddr was added (false if it was already added).
func (post *Post) AddReaction(userAddr address, reaction Reaction) bool {
func (post *Post) addReaction(userAddr address, reaction Reaction) bool {
validateReaction(reaction)

if userAddr == post.creator {
Expand All @@ -136,7 +137,7 @@ func (post *Post) AddReaction(userAddr address, reaction Reaction) bool {
// Remove the userAddr from the posts.reactions for reaction.
// If userAddr is already removed, do nothing.
// Return a boolean indicating whether the userAddr was found and removed.
func (post *Post) RemoveReaction(userAddr address, reaction Reaction) bool {
func (post *Post) removeReaction(userAddr address, reaction Reaction) bool {
validateReaction(reaction)

if !post.reactions.Has(reactionKey(reaction)) {
Expand Down Expand Up @@ -170,7 +171,8 @@ func (post *Post) GetSummary() string {
}

func (post *Post) GetURL() string {
if post.IsThread() {
// Not IsThread(), which is false for a repost even though it is top-level.
if post.threadID == post.id {
return post.userPosts.GetURLFromThreadAndReplyID(
post.id, 0)
} else {
Expand Down Expand Up @@ -211,10 +213,10 @@ func (post *Post) RenderSummary() string {
if thread == nil {
return "reposted post does not exist"
}
return "Repost: " + post.GetSummary() + "\n\n" + thread.RenderSummary()
return "Repost: " + sanitize.InlineText(post.GetSummary()) + "\n\n" + thread.RenderSummary()
}
str := ""
str += post.GetSummary() + "\n"
str += sanitize.InlineText(post.GetSummary()) + "\n"
str += "\\- " + displayAddressMD(post.creator) + ","
str += " [" + post.createdAt.Format("2006-01-02 3:04pm MST") + "](" + post.GetURL() + ")"
str += " (" + strconv.Itoa(post.GetReactionCount(Gnod)) + " gnods)"
Expand All @@ -228,7 +230,7 @@ func (post *Post) RenderPost(indent string, levels int) string {
return "nil post"
}
str := ""
str += indentBody(indent, post.body) + "\n" // TODO: indent body lines.
str += indentBody(indent, sanitize.Block(post.body)) + "\n" // TODO: indent body lines.
str += indent + "\\- " + displayAddressMD(post.creator) + ", "
str += "[" + post.createdAt.Format("2006-01-02 3:04pm (MST)") + "](" + post.GetURL() + ")"
str += " - (" + strconv.Itoa(post.GetReactionCount(Gnod)) + " gnods) - "
Expand All @@ -238,6 +240,9 @@ func (post *Post) RenderPost(indent string, levels int) string {
str += " \\[[repost](" + post.GetRepostFormURL() + ")]"
}
str += "\n"
// TODO: This renders every reply, recursively for `levels` levels, with no pagination, and
// post bodies have no length limit. Should paginate them, and cap the body length.
// See renderThread in r/gnoland/boards2/v0/render_thread.gno .
if levels > 0 {
if post.replies.Size() > 0 {
post.replies.ReverseIterate("", "", func(key string, value interface{}) bool {
Expand Down Expand Up @@ -294,6 +299,10 @@ func (post *Post) MarshalJSON() ([]byte, error) {
if post.repostUser != "" {
json.WriteString(ufmt.Sprintf(`, "repost_user": %s`, strconv.Quote(post.repostUser.String())))
}
// TODO: strconv.Quote is not a JSON encoder. For control characters it emits Go escapes
// like \x01, \a and \v which are invalid JSON, so JSON.parse rejects the whole response.
// Since anyone can reply to any thread, one such reply breaks that thread in the mobile
// app. This goes away when we switch to vm/qeval_json.
json.WriteString(ufmt.Sprintf(`, "body": %s}`, strconv.Quote(post.body)))

return json.Bytes(), nil
Expand Down
66 changes: 43 additions & 23 deletions realm/public.gno
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ type UserAndPostID struct {
// Return the "thread ID" of the new post.
// (This is similar to boards.CreateThread, but no message title)
func PostMessage(cur realm, body string) PostID {
if !cur.IsCurrent() {
panic("spoofed realm")
}
caller := cur.Previous().Address()
userPosts := getOrCreateUserPosts(caller, usernameOf(caller))
thread := userPosts.AddThread(body)
thread := userPosts.addThread(body)
return thread.id
}

Expand All @@ -31,6 +34,9 @@ func PostMessage(cur realm, body string) PostID {
// Return the new post ID.
// (This is similar to boards.CreateReply.)
func PostReply(cur realm, userPostsAddr address, threadid, postid PostID, body string) PostID {
if !cur.IsCurrent() {
panic("spoofed realm")
}
caller := cur.Previous().Address()
userPosts := getUserPosts(userPostsAddr)
if userPosts == nil {
Expand All @@ -41,14 +47,14 @@ func PostReply(cur realm, userPostsAddr address, threadid, postid PostID, body s
panic("threadid in user posts does not exist")
}
if postid == threadid {
reply := thread.AddReply(caller, body)
reply := thread.addReply(caller, body)
return reply.id
} else {
post := thread.GetReply(postid)
if post == nil {
panic("postid does not exist")
}
reply := post.AddReply(caller, body)
reply := post.addReply(caller, body)
return reply.id
}
}
Expand All @@ -58,6 +64,9 @@ func PostReply(cur realm, userPostsAddr address, threadid, postid PostID, body s
// Return the new post ID.
// (This is similar to boards.CreateRepost.)
func RepostThread(cur realm, userPostsAddr address, threadid PostID, comment string) PostID {
if !cur.IsCurrent() {
panic("spoofed realm")
}
caller := cur.Previous().Address()
if userPostsAddr == caller {
panic("Cannot repost a user's own message")
Expand All @@ -73,7 +82,7 @@ func RepostThread(cur realm, userPostsAddr address, threadid PostID, comment str
if thread == nil {
panic("threadid in user posts does not exist")
}
repost := thread.AddRepostTo(caller, comment, dstUserPosts)
repost := thread.addRepostTo(caller, comment, dstUserPosts)
return repost.id
}

Expand Down Expand Up @@ -119,7 +128,7 @@ func GetJsonTopPostsByID(addrAndIDs []UserAndPostID) string {
// If threadID is X and replyID is 0, then return the posts (without replies) in that thread. (Like render args "user/2".)
// If threadID is X and replyID is Y, then return the posts in the thread starting with replyID. (Like render args "user/2/5".)
// The response includes reposts by this user (only if threadID is 0), but not messages of other
// users that are being followed. (See GetHomePosts.) The response is a JSON string.
// users that are being followed. (See GetJsonHomePosts.) The response is a JSON string.
func GetThreadPosts(userPostsAddr address, threadID int, replyID int, startIndex int, endIndex int) string {
userPosts := getUserPosts(userPostsAddr)
if userPosts == nil {
Expand Down Expand Up @@ -160,38 +169,37 @@ func RefreshHomePosts(_ realm, userPostsAddr address) int {
return userPosts.homePosts.Size()
}

// Get the number of posts which GetHomePosts or GetJsonHomePosts will return.
// Get the number of posts which GetJsonHomePosts will return.
// The result is something like "(12 int)".
// This returns the current count of the home posts (without need to pay gas). To include the
// latest followed posts, call RefreshHomePosts.
func GetHomePostsCount(userPostsAddr address) int {
return GetHomePosts(userPostsAddr).Size()
return getHomePosts(userPostsAddr).Size()
}

// Get home posts for a user, which are the user's top-level posts plus all posts of all
// users being followed.
// The response is a map of postID -> *Post. The bptree.BPTree sorts by the post ID which is
// unique for every post and increases in time.
// If you just want the total count, use GetHomePostsCount.
// This returns the current state of the home posts (without need to pay gas). To include the
// latest followed posts, call RefreshHomePosts.
func GetHomePosts(userPostsAddr address) *bptree.BPTree {
// This must not be exported. Another realm holding this pointer could mutate the tree (and
// the posts in it) with this realm's authority.
func getHomePosts(userPostsAddr address) *bptree.BPTree {
userPosts := getUserPosts(userPostsAddr)
if userPosts == nil {
panic("posts for userPostsAddr do not exist")
}
return &userPosts.homePosts
}

// Get home posts for a user (using GetHomePosts), which are the user's top-level posts plus all
// Get home posts for a user, which are the user's top-level posts plus all
// posts of all users being followed.
// Limit the response to posts from startIndex up to (not including) endIndex within the home posts.
// If you just want the total count, use GetHomePostsCount.
// The response is a JSON string.
// This returns the current state of the home posts (without need to pay gas). To include the
// latest posts, call RefreshHomePosts.
func GetJsonHomePosts(userPostsAddr address, startIndex int, endIndex int) string {
allPosts := GetHomePosts(userPostsAddr)
allPosts := getHomePosts(userPostsAddr)
postsJson := ""
for i := startIndex; i < endIndex && i < allPosts.Size(); i++ {
_, postI := allPosts.GetByIndex(i)
Expand All @@ -209,37 +217,46 @@ func GetJsonHomePosts(userPostsAddr address, startIndex int, endIndex int) strin
return ufmt.Sprintf("{\"n_posts\": %d, \"posts\": [\n %s]}", allPosts.Size(), postsJson)
}

// Update the caller to follow the user with followedAddr. See UserPosts.Follow.
// Update the caller to follow the user with followedAddr. See UserPosts.follow.
func Follow(cur realm, followedAddr address) PostID {
if !cur.IsCurrent() {
panic("spoofed realm")
}
caller := cur.Previous().Address()
if followedAddr == caller {
panic("you can't follow yourself")
}

// A user can follow someone before doing any posts, so create the UserPosts if needed.
userPosts := getOrCreateUserPosts(caller, usernameOf(caller))
return userPosts.Follow(followedAddr)
return userPosts.follow(followedAddr)
}

// Update the caller to unfollow the user with followedAddr. See UserPosts.Unfollow.
// Update the caller to unfollow the user with followedAddr. See UserPosts.unfollow.
func Unfollow(cur realm, followedAddr address) {
if !cur.IsCurrent() {
panic("spoofed realm")
}
caller := cur.Previous().Address()
userPosts := getUserPosts(caller)
if userPosts == nil {
// We don't expect this, but just do nothing.
return
}

userPosts.Unfollow(followedAddr)
userPosts.unfollow(followedAddr)
}

// Add the reaction by the caller to the post of userPostsAddr, where threadid is the ID
// returned by the original call to PostMessage. If postid == threadid then add the reaction
// to a top-level post for the threadid, otherwise add the reaction to the postid "sub reply".
// (This function's arguments are similar to PostReply.)
// The caller must already be registered with /r/gnoland/users/v1 Register.
// Return a boolean indicating whether the userAddr was added. See Post.AddReaction.
// Return a boolean indicating whether the userAddr was added. See Post.addReaction.
func AddReaction(cur realm, userPostsAddr address, threadid, postid PostID, reaction Reaction) bool {
if !cur.IsCurrent() {
panic("spoofed realm")
}
caller := cur.Previous().Address()
userPosts := getUserPosts(userPostsAddr)
if userPosts == nil {
Expand All @@ -250,13 +267,13 @@ func AddReaction(cur realm, userPostsAddr address, threadid, postid PostID, reac
panic("threadid in user posts does not exist")
}
if postid == threadid {
return thread.AddReaction(caller, reaction)
return thread.addReaction(caller, reaction)
} else {
post := thread.GetReply(postid)
if post == nil {
panic("postid does not exist")
}
return post.AddReaction(caller, reaction)
return post.addReaction(caller, reaction)
}
}

Expand All @@ -265,8 +282,11 @@ func AddReaction(cur realm, userPostsAddr address, threadid, postid PostID, reac
// from a top-level post for the threadid, otherwise remove the reaction from the postid "sub reply".
// (This function's arguments are similar to PostReply.)
// The caller must already be registered with /r/gnoland/users/v1 Register.
// Return a boolean indicating whether the userAddr was removed. See Post.RemoveReaction.
// Return a boolean indicating whether the userAddr was removed. See Post.removeReaction.
func RemoveReaction(cur realm, userPostsAddr address, threadid, postid PostID, reaction Reaction) bool {
if !cur.IsCurrent() {
panic("spoofed realm")
}
caller := cur.Previous().Address()
userPosts := getUserPosts(userPostsAddr)
if userPosts == nil {
Expand All @@ -277,13 +297,13 @@ func RemoveReaction(cur realm, userPostsAddr address, threadid, postid PostID, r
panic("threadid in user posts does not exist")
}
if postid == threadid {
return thread.RemoveReaction(caller, reaction)
return thread.removeReaction(caller, reaction)
} else {
post := thread.GetReply(postid)
if post == nil {
panic("postid does not exist")
}
return post.RemoveReaction(caller, reaction)
return post.removeReaction(caller, reaction)
}
}

Expand Down
21 changes: 11 additions & 10 deletions realm/render.gno
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"strconv"
"strings"

"gno.land/p/nt/markdown/sanitize/v0"
"gno.land/r/sys/users"
)

Expand Down Expand Up @@ -37,15 +38,15 @@ func Render(path string) string {
userAddr := resolveAddr(path)
userPosts := getUserPosts(userAddr)
if userPosts == nil {
return "No posts by: " + path
return "No posts by: " + sanitize.InlineText(path)
}

return userPosts.RenderUserPosts(false)
} else if len(parts) == 2 {
userAddr := resolveAddr(parts[0])
userPosts := getUserPosts(userAddr)
if userPosts == nil {
return "No posts by: " + parts[0]
return "No posts by: " + sanitize.InlineText(parts[0])
}

if parts[1] == "home" {
Expand All @@ -61,11 +62,11 @@ func Render(path string) string {
// /r/berty/social:USER_NAME_OR_ADDR/THREAD_ID
pid, err := strconv.Atoi(parts[1])
if err != nil {
return "invalid thread id: " + parts[1]
return "invalid thread id: " + sanitize.InlineText(parts[1])
}
thread := userPosts.GetThread(PostID(pid))
if thread == nil {
return "thread does not exist with id: " + parts[1]
return "thread does not exist with id: " + sanitize.InlineText(parts[1])
}
return thread.RenderPost("", 5)
}
Expand All @@ -74,26 +75,26 @@ func Render(path string) string {
userAddr := resolveAddr(parts[0])
userPosts := getUserPosts(userAddr)
if userPosts == nil {
return "No posts by: " + parts[0]
return "No posts by: " + sanitize.InlineText(parts[0])
}
pid, err := strconv.Atoi(parts[1])
if err != nil {
return "invalid thread id: " + parts[1]
return "invalid thread id: " + sanitize.InlineText(parts[1])
}
thread := userPosts.GetThread(PostID(pid))
if thread == nil {
return "thread does not exist with id: " + parts[1]
return "thread does not exist with id: " + sanitize.InlineText(parts[1])
}
rid, err := strconv.Atoi(parts[2])
if err != nil {
return "invalid reply id: " + parts[2]
return "invalid reply id: " + sanitize.InlineText(parts[2])
}
reply := thread.GetReply(PostID(rid))
if reply == nil {
return "reply does not exist with id: " + parts[2]
return "reply does not exist with id: " + sanitize.InlineText(parts[2])
}
return reply.RenderInner()
} else {
return "unrecognized path: " + path
return "unrecognized path: " + sanitize.InlineText(path)
}
}
Loading
Loading