From 3d0e5928fda0c82761bb02874385d36b2ea14170 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 12 Nov 2024 12:02:41 +0100 Subject: [PATCH 01/21] feat: add `assertIsBoardName()` function Also changed the expression to validate board names to check that the name starts with three letters and ends with three numbers. This has been defined this way to match `users` realm implementation. See: https://github.com/gnolang/gno/issues/2827 In a next iteration we can support vanity names for boards, once vanity names are supported for the `users` realm. --- examples/gno.land/r/demo/boards2/board.gno | 18 +++++++++++++----- examples/gno.land/r/demo/boards2/boards.gno | 11 +---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index 79b27da84b2..a855bf4e002 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -1,6 +1,7 @@ package boards import ( + "regexp" "std" "strconv" "time" @@ -9,8 +10,9 @@ import ( "gno.land/p/moul/txlink" ) -//---------------------------------------- -// Board +var reBoardName = regexp.MustCompile(`^[a-z]{3}[_a-z0-9]{0,23}[0-9]{3}$`) + +// TODO: Move post and board to a package to improve API and reduce realm size? type BoardID uint64 @@ -30,9 +32,7 @@ type Board struct { } func newBoard(id BoardID, url string, name string, creator std.Address) *Board { - if !reName.MatchString(name) { - panic("invalid name: " + name) - } + assertIsBoardName(name) exists := gBoardsByName.Has(name) if exists { panic("board already exists") @@ -58,6 +58,7 @@ func NewPrivateBoard(url string, name string, creator std.Address) *Board { } */ +// TODO: Do we want to support private boards? If so move forward with implementation. func (board *Board) IsPrivate() bool { return board.id == 0 } @@ -89,6 +90,7 @@ func (board *Board) DeleteThread(pid PostID) { } } +// TODO: Change HasPermission to use a new authorization interface's `CanDo()` func (board *Board) HasPermission(addr std.Address, perm Permission) bool { if board.creator == addr { switch perm { @@ -137,3 +139,9 @@ func (board *Board) GetURLFromThreadAndReplyID(threadID, replyID PostID) string func (board *Board) GetPostFormURL() string { return txlink.URL("CreateThread", "bid", board.id.String()) } + +func assertIsBoardName(name string) { + if !reBoardName.MatchString(name) { + panic("invalid board name: " + name) + } +} diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno index 5de0555a2f9..5d228c2251e 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -1,22 +1,13 @@ package boards import ( - "regexp" - "gno.land/p/demo/avl" ) -//---------------------------------------- -// Realm (package) state - +// TODO: Create a Boards or App struct to handle counter and tree indexes var ( gBoards avl.Tree // id -> *Board gBoardsCtr int // increments Board.id gBoardsByName avl.Tree // name -> *Board gDefaultAnonFee = 100000000 // minimum fee required if anonymous ) - -//---------------------------------------- -// Constants - -var reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{2,29}$`) From 45c971a5fa4f9a70c31a0de23761b36613bfdf8e Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 12 Nov 2024 12:41:03 +0100 Subject: [PATCH 02/21] feat: change ID types to have a `Key()` method This change simplifies the code and allow removing some private functions. The changeset also have minor semantic changes to the existing code. --- examples/gno.land/r/demo/boards2/board.gno | 24 ++++----- examples/gno.land/r/demo/boards2/misc.gno | 16 ++---- examples/gno.land/r/demo/boards2/post.gno | 57 ++++++++++----------- examples/gno.land/r/demo/boards2/public.gno | 16 ++++-- 4 files changed, 56 insertions(+), 57 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index a855bf4e002..a11e3222c01 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -16,8 +16,12 @@ var reBoardName = regexp.MustCompile(`^[a-z]{3}[_a-z0-9]{0,23}[0-9]{3}$`) type BoardID uint64 -func (bid BoardID) String() string { - return strconv.Itoa(int(bid)) +func (id BoardID) String() string { + return strconv.Itoa(int(id)) +} + +func (id BoardID) Key() string { + return padZero(uint64(id), 10) } type Board struct { @@ -64,27 +68,24 @@ func (board *Board) IsPrivate() bool { } func (board *Board) GetThread(pid PostID) *Post { - pidkey := postIDKey(pid) - postI, exists := board.threads.Get(pidkey) - if !exists { + v, found := board.threads.Get(pid.Key()) + if !found { return nil } - return postI.(*Post) + return v.(*Post) } func (board *Board) AddThread(creator std.Address, title string, body string) *Post { pid := board.incGetPostID() - pidkey := postIDKey(pid) thread := newPost(board, pid, creator, title, body, pid, 0, 0) - board.threads.Set(pidkey, thread) + board.threads.Set(pid.Key(), thread) return thread } // NOTE: this can be potentially very expensive for threads with many replies. // TODO: implement optional fast-delete where thread is simply moved. func (board *Board) DeleteThread(pid PostID) { - pidkey := postIDKey(pid) - _, removed := board.threads.Remove(pidkey) + _, removed := board.threads.Remove(pid.Key()) if !removed { panic("thread does not exist with id " + pid.String()) } @@ -131,9 +132,8 @@ func (board *Board) incGetPostID() PostID { func (board *Board) GetURLFromThreadAndReplyID(threadID, replyID PostID) string { if replyID == 0 { return board.url + "/" + threadID.String() - } else { - return board.url + "/" + threadID.String() + "/" + replyID.String() } + return board.url + "/" + threadID.String() + "/" + replyID.String() } func (board *Board) GetPostFormURL() string { diff --git a/examples/gno.land/r/demo/boards2/misc.gno b/examples/gno.land/r/demo/boards2/misc.gno index bc561ca7d22..c50aac59a37 100644 --- a/examples/gno.land/r/demo/boards2/misc.gno +++ b/examples/gno.land/r/demo/boards2/misc.gno @@ -8,17 +8,19 @@ import ( "gno.land/r/demo/users" ) +// TODO: Create a "strings.gno" file and move all string related functions there +// TODO: Move getBoard() and incGetBoardID() to "boards.gno" + //---------------------------------------- // private utility methods // XXX ensure these cannot be called from public. func getBoard(bid BoardID) *Board { - bidkey := boardIDKey(bid) - board_, exists := gBoards.Get(bidkey) + v, exists := gBoards.Get(bid.Key()) if !exists { return nil } - board := board_.(*Board) + board := v.(*Board) return board } @@ -44,14 +46,6 @@ func padZero(u64 uint64, length int) string { } } -func boardIDKey(bid BoardID) string { - return padZero(uint64(bid), 10) -} - -func postIDKey(pid PostID) string { - return padZero(uint64(pid), 10) -} - func indentBody(indent string, body string) string { lines := strings.Split(body, "\n") res := "" diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index 95d4b2977ba..7f03f856c86 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -9,18 +9,21 @@ import ( "gno.land/p/moul/txlink" ) -//---------------------------------------- -// Post +// TODO: Move post and board to a package to improve API and reduce realm size? -// NOTE: a PostID is relative to the board. type PostID uint64 -func (pid PostID) String() string { - return strconv.Itoa(int(pid)) +func (id PostID) String() string { + return strconv.Itoa(int(id)) +} + +func (id PostID) Key() string { + return padZero(uint64(id), 10) } // A Post is a "thread" or a "reply" depending on context. // A thread is a Post of a Board that holds other replies. +// TODO: Figure out "repliesAll", "threadID" type Post struct { board *Board id PostID @@ -62,17 +65,19 @@ func (post *Post) GetPostID() PostID { return post.id } +// TODO: Rename to Reply() func (post *Post) AddReply(creator std.Address, body string) *Post { board := post.board pid := board.incGetPostID() - pidkey := postIDKey(pid) + pKey := pid.Key() reply := newPost(board, pid, creator, "", body, post.threadID, post.id, 0) - post.replies.Set(pidkey, reply) + // TODO: Figure out how to remove this redundancy of data "replies==repliesAll" in threads + post.replies.Set(pKey, reply) if post.threadID == post.id { - post.repliesAll.Set(pidkey, reply) + post.repliesAll.Set(pKey, reply) } else { thread := board.GetThread(post.threadID) - thread.repliesAll.Set(pidkey, reply) + thread.repliesAll.Set(pKey, reply) } return reply } @@ -84,13 +89,11 @@ func (post *Post) Update(title string, body string) { } func (thread *Post) GetReply(pid PostID) *Post { - pidkey := postIDKey(pid) - replyI, ok := thread.repliesAll.Get(pidkey) - if !ok { + v, found := thread.repliesAll.Get(pid.Key()) + if !found { return nil - } else { - return replyI.(*Post) } + return v.(*Post) } func (post *Post) AddRepostTo(creator std.Address, title, body string, dst *Board) *Post { @@ -98,12 +101,10 @@ func (post *Post) AddRepostTo(creator std.Address, title, body string, dst *Boar panic("cannot repost non-thread post") } pid := dst.incGetPostID() - pidkey := postIDKey(pid) repost := newPost(dst, pid, creator, title, body, pid, post.id, post.board.id) - dst.threads.Set(pidkey, repost) + dst.threads.Set(pid.Key(), repost) if !dst.IsPrivate() { - bidkey := boardIDKey(dst.id) - post.reposts.Set(bidkey, pid) + post.reposts.Set(dst.id.Key(), pid) } return repost } @@ -112,20 +113,21 @@ func (thread *Post) DeletePost(pid PostID) { if thread.id == pid { panic("should not happen") } - pidkey := postIDKey(pid) - postI, removed := thread.repliesAll.Remove(pidkey) + pKey := pid.Key() + v, removed := thread.repliesAll.Remove(pKey) if !removed { panic("post not found in thread") } - post := postI.(*Post) + post := v.(*Post) if post.parentID != thread.id { parent := thread.GetReply(post.parentID) - parent.replies.Remove(pidkey) + parent.replies.Remove(pKey) } else { - thread.replies.Remove(pidkey) + thread.replies.Remove(pKey) } } +// TODO: Change HasPermission to use a new authorization interface's `CanDo()` func (post *Post) HasPermission(addr std.Address, perm Permission) bool { if post.creator == addr { switch perm { @@ -147,12 +149,9 @@ func (post *Post) GetSummary() string { func (post *Post) GetURL() string { if post.IsThread() { - return post.board.GetURLFromThreadAndReplyID( - post.id, 0) - } else { - return post.board.GetURLFromThreadAndReplyID( - post.threadID, post.id) + return post.board.GetURLFromThreadAndReplyID(post.id, 0) } + return post.board.GetURLFromThreadAndReplyID(post.threadID, post.id) } func (post *Post) GetReplyFormURL() string { @@ -191,7 +190,7 @@ func (post *Post) RenderSummary() string { return "Repost: " + post.GetSummary() + "\n" + thread.RenderSummary() } str := "" - if post.title != "" { + if post.title != "" { // TODO: Add a newLink() function str += "## [" + summaryOf(post.title, 80) + "](" + post.GetURL() + ")\n" str += "\n" } diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 1d26126fcb2..45525118e88 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -8,6 +8,7 @@ import ( //---------------------------------------- // Public facing functions +// TODO: Consider a simpler API initially by removing this type of functions func GetBoardIDFromName(name string) (BoardID, bool) { boardI, exists := gBoardsByName.Get(name) if !exists { @@ -27,12 +28,13 @@ func CreateBoard(name string) BoardID { } url := "/r/demo/boards:" + name board := newBoard(bid, url, name, caller) - bidkey := boardIDKey(bid) - gBoards.Set(bidkey, board) + gBoards.Set(bid.Key(), board) gBoardsByName.Set(name, board) return board.id } +// TODO: This must be an assertion function +// TODO: Should we display realm's amount? Transfer to a pool realm/DAO? func checkAnonFee() bool { sent := std.GetOrigSend() anonFeeCoin := std.NewCoin("ugnot", int64(gDefaultAnonFee)) @@ -60,8 +62,10 @@ func CreateThread(bid BoardID, title string, body string) PostID { return thread.id } +// TODO: Rename "postid" to "commentID", "threadid" to "threadID" +// TODO: Split into CreateComment(threadID) & CreateReply(commentID), check link generation functions first (ex. Post.GetReplyFromURL()) func CreateReply(bid BoardID, threadid, postid PostID, body string) PostID { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { // TODO: Create assertIsUser() function panic("invalid non-user call") } caller := std.GetOrigCaller() @@ -70,11 +74,11 @@ func CreateReply(bid BoardID, threadid, postid PostID, body string) PostID { panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") } } - board := getBoard(bid) + board := getBoard(bid) // TODO: Create a mustGetBoard() function if board == nil { panic("board not exist") } - thread := board.GetThread(threadid) + thread := board.GetThread(threadid) // TODO: Use found flag to encorage returned value validity checks if thread == nil { panic("thread not exist") } @@ -120,6 +124,7 @@ func CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoar return repost.id } +// TODO: Split into DeleteThread() & DeleteComment() func DeletePost(bid BoardID, threadid, postid PostID, reason string) { if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { panic("invalid non-user call") @@ -152,6 +157,7 @@ func DeletePost(bid BoardID, threadid, postid PostID, reason string) { } } +// TODO: Split into EditThread() & EditComment() func EditPost(bid BoardID, threadid, postid PostID, title, body string) { if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { panic("invalid non-user call") From b078c3f33f79b39e28a4f19a7f5798d23464276e Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 12 Nov 2024 13:01:16 +0100 Subject: [PATCH 03/21] chore: add spacing and semantic changes --- examples/gno.land/r/demo/boards2/board.gno | 15 ++--- examples/gno.land/r/demo/boards2/misc.gno | 34 +++++----- examples/gno.land/r/demo/boards2/post.gno | 72 ++++++++++++--------- examples/gno.land/r/demo/boards2/public.gno | 32 +++++++-- examples/gno.land/r/demo/boards2/render.gno | 36 +++++++---- 5 files changed, 112 insertions(+), 77 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index a11e3222c01..20b82254856 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -37,10 +37,12 @@ type Board struct { func newBoard(id BoardID, url string, name string, creator std.Address) *Board { assertIsBoardName(name) + exists := gBoardsByName.Has(name) if exists { panic("board already exists") } + return &Board{ id: id, url: url, @@ -110,18 +112,15 @@ func (board *Board) HasPermission(addr std.Address, perm Permission) bool { // console. This is suitable for demonstration or tests, // but not for prod. func (board *Board) RenderBoard() string { - str := "" - str += "\\[[post](" + board.GetPostFormURL() + ")]\n\n" + s := "\\[[post](" + board.GetPostFormURL() + ")]\n\n" if board.threads.Size() > 0 { - board.threads.Iterate("", "", func(key string, value interface{}) bool { - if str != "" { - str += "----------------------------------------\n" - } - str += value.(*Post).RenderSummary() + "\n" + board.threads.Iterate("", "", func(_ string, v interface{}) bool { + s += "----------------------------------------\n" + s += v.(*Post).RenderSummary() + "\n" return false }) } - return str + return s } func (board *Board) incGetPostID() PostID { diff --git a/examples/gno.land/r/demo/boards2/misc.gno b/examples/gno.land/r/demo/boards2/misc.gno index c50aac59a37..44c31244ff3 100644 --- a/examples/gno.land/r/demo/boards2/misc.gno +++ b/examples/gno.land/r/demo/boards2/misc.gno @@ -20,8 +20,7 @@ func getBoard(bid BoardID) *Board { if !exists { return nil } - board := v.(*Board) - return board + return v.(*Board) } func incGetBoardID() BoardID { @@ -29,26 +28,26 @@ func incGetBoardID() BoardID { return BoardID(gBoardsCtr) } -func padLeft(str string, length int) string { - if len(str) >= length { - return str - } else { - return strings.Repeat(" ", length-len(str)) + str +func padLeft(s string, length int) string { + if len(s) >= length { + return s } + return strings.Repeat(" ", length-len(s)) + s } func padZero(u64 uint64, length int) string { - str := strconv.Itoa(int(u64)) - if len(str) >= length { - return str - } else { - return strings.Repeat("0", length-len(str)) + str + s := strconv.Itoa(int(u64)) + if len(s) >= length { + return s } + return strings.Repeat("0", length-len(s)) + s } func indentBody(indent string, body string) string { - lines := strings.Split(body, "\n") - res := "" + var ( + res string + lines = strings.Split(body, "\n") + ) for i, line := range lines { if i > 0 { res += "\n" @@ -59,8 +58,8 @@ func indentBody(indent string, body string) string { } // NOTE: length must be greater than 3. -func summaryOf(str string, length int) string { - lines := strings.SplitN(str, "\n", 2) +func summaryOf(text string, length int) string { + lines := strings.SplitN(text, "\n", 2) line := lines[0] if len(line) > length { line = line[:(length-3)] + "..." @@ -75,9 +74,8 @@ func displayAddressMD(addr std.Address) string { user := users.GetUserByAddress(addr) if user == nil { return "[" + addr.String() + "](/r/demo/users:" + addr.String() + ")" - } else { - return "[@" + user.Name + "](/r/demo/users:" + user.Name + ")" } + return "[@" + user.Name + "](/r/demo/users:" + user.Name + ")" } func usernameOf(addr std.Address) string { diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index 7f03f856c86..e67c454e4ee 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -100,6 +100,7 @@ func (post *Post) AddRepostTo(creator std.Address, title, body string, dst *Boar if !post.IsThread() { panic("cannot repost non-thread post") } + pid := dst.incGetPostID() repost := newPost(dst, pid, creator, title, body, pid, post.id, post.board.id) dst.threads.Set(pid.Key(), repost) @@ -113,11 +114,13 @@ func (thread *Post) DeletePost(pid PostID) { if thread.id == pid { panic("should not happen") } + pKey := pid.Key() v, removed := thread.repliesAll.Remove(pKey) if !removed { panic("post not found in thread") } + post := v.(*Post) if post.parentID != thread.id { parent := thread.GetReply(post.parentID) @@ -183,58 +186,64 @@ func (post *Post) RenderSummary() string { if dstBoard == nil { panic("repostBoard does not exist") } + thread := dstBoard.GetThread(PostID(post.parentID)) if thread == nil { return "reposted post does not exist" } return "Repost: " + post.GetSummary() + "\n" + thread.RenderSummary() } - str := "" + + var s string if post.title != "" { // TODO: Add a newLink() function - str += "## [" + summaryOf(post.title, 80) + "](" + post.GetURL() + ")\n" - str += "\n" + s += "## [" + summaryOf(post.title, 80) + "](" + post.GetURL() + ")\n" + s += "\n" } - str += post.GetSummary() + "\n" - str += "\\- " + displayAddressMD(post.creator) + "," - str += " [" + post.createdAt.Format("2006-01-02 3:04pm MST") + "](" + post.GetURL() + ")" - str += " \\[[x](" + post.GetDeleteFormURL() + ")]" - str += " (" + strconv.Itoa(post.replies.Size()) + " replies)" - str += " (" + strconv.Itoa(post.reposts.Size()) + " reposts)" + "\n" - return str + s += post.GetSummary() + "\n" + s += "\\- " + displayAddressMD(post.creator) + "," + s += " [" + post.createdAt.Format("2006-01-02 3:04pm MST") + "](" + post.GetURL() + ")" + s += " \\[[x](" + post.GetDeleteFormURL() + ")]" + s += " (" + strconv.Itoa(post.replies.Size()) + " replies)" + s += " (" + strconv.Itoa(post.reposts.Size()) + " reposts)" + "\n" + return s } func (post *Post) RenderPost(indent string, levels int) string { if post == nil { return "nil post" } - str := "" + + var s string if post.title != "" { - str += indent + "# " + post.title + "\n" - str += indent + "\n" + s += indent + "# " + post.title + "\n" + s += indent + "\n" } - str += indentBody(indent, 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 += " \\[[reply](" + post.GetReplyFormURL() + ")]" + + s += indentBody(indent, post.body) + "\n" // TODO: indent body lines. + + s += indent + "\\- " + displayAddressMD(post.creator) + ", " + s += "[" + post.createdAt.Format("2006-01-02 3:04pm (MST)") + "](" + post.GetURL() + ")" + s += " \\[[reply](" + post.GetReplyFormURL() + ")]" if post.IsThread() { - str += " \\[[repost](" + post.GetRepostFormURL() + ")]" + s += " \\[[repost](" + post.GetRepostFormURL() + ")]" } - str += " \\[[x](" + post.GetDeleteFormURL() + ")]\n" + s += " \\[[x](" + post.GetDeleteFormURL() + ")]\n" + if levels > 0 { if post.replies.Size() > 0 { post.replies.Iterate("", "", func(key string, value interface{}) bool { - str += indent + "\n" - str += value.(*Post).RenderPost(indent+"> ", levels-1) + s += indent + "\n" + s += value.(*Post).RenderPost(indent+"> ", levels-1) return false }) } } else { if post.replies.Size() > 0 { - str += indent + "\n" - str += indent + "_[see all " + strconv.Itoa(post.replies.Size()) + " replies](" + post.GetURL() + ")_\n" + s += indent + "\n" + s += indent + "_[see all " + strconv.Itoa(post.replies.Size()) + " replies](" + post.GetURL() + ")_\n" } } - return str + return s } // render reply and link to context thread @@ -242,21 +251,22 @@ func (post *Post) RenderInner() string { if post.IsThread() { panic("unexpected thread") } + threadID := post.threadID // replyID := post.id parentID := post.parentID - str := "" - str += "_[see thread](" + post.board.GetURLFromThreadAndReplyID( - threadID, 0) + ")_\n\n" + s := "_[see thread](" + post.board.GetURLFromThreadAndReplyID(threadID, 0) + ")_\n\n" thread := post.board.GetThread(post.threadID) + var parent *Post if thread.id == parentID { parent = thread } else { parent = thread.GetReply(parentID) } - str += parent.RenderPost("", 0) - str += "\n" - str += post.RenderPost("> ", 5) - return str + + s += parent.RenderPost("", 0) + s += "\n" + s += post.RenderPost("> ", 5) + return s } diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 45525118e88..406aaa6b97c 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -5,27 +5,26 @@ import ( "strconv" ) -//---------------------------------------- -// Public facing functions - // TODO: Consider a simpler API initially by removing this type of functions func GetBoardIDFromName(name string) (BoardID, bool) { - boardI, exists := gBoardsByName.Get(name) - if !exists { + v, found := gBoardsByName.Get(name) + if !found { return 0, false } - return boardI.(*Board).id, true + return v.(*Board).id, true } func CreateBoard(name string) BoardID { if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { panic("invalid non-user call") } - bid := incGetBoardID() + caller := std.GetOrigCaller() if usernameOf(caller) == "" { panic("unauthorized") } + + bid := incGetBoardID() url := "/r/demo/boards:" + name board := newBoard(bid, url, name, caller) gBoards.Set(bid.Key(), board) @@ -48,16 +47,19 @@ func CreateThread(bid BoardID, title string, body string) PostID { if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { panic("invalid non-user call") } + caller := std.GetOrigCaller() if usernameOf(caller) == "" { if !checkAnonFee() { panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") } } + board := getBoard(bid) if board == nil { panic("board not exist") } + thread := board.AddThread(caller, title, body) return thread.id } @@ -68,20 +70,24 @@ func CreateReply(bid BoardID, threadid, postid PostID, body string) PostID { if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { // TODO: Create assertIsUser() function panic("invalid non-user call") } + caller := std.GetOrigCaller() if usernameOf(caller) == "" { if !checkAnonFee() { panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") } } + board := getBoard(bid) // TODO: Create a mustGetBoard() function if board == nil { panic("board not exist") } + thread := board.GetThread(threadid) // TODO: Use found flag to encorage returned value validity checks if thread == nil { panic("thread not exist") } + if postid == threadid { reply := thread.AddReply(caller, body) return reply.id @@ -98,6 +104,7 @@ func CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoar if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { panic("invalid non-user call") } + caller := std.GetOrigCaller() if usernameOf(caller) == "" { // TODO: allow with gDefaultAnonFee payment. @@ -105,21 +112,26 @@ func CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoar panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") } } + board := getBoard(bid) if board == nil { panic("src board not exist") } + if board.IsPrivate() { panic("cannot repost from a private board") } + dst := getBoard(dstBoardID) if dst == nil { panic("dst board not exist") } + thread := board.GetThread(postid) if thread == nil { panic("thread not exist") } + repost := thread.AddRepostTo(caller, title, body, dst) return repost.id } @@ -129,15 +141,18 @@ func DeletePost(bid BoardID, threadid, postid PostID, reason string) { if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { panic("invalid non-user call") } + caller := std.GetOrigCaller() board := getBoard(bid) if board == nil { panic("board not exist") } + thread := board.GetThread(threadid) if thread == nil { panic("thread not exist") } + if postid == threadid { // delete thread if !thread.HasPermission(caller, DeletePermission) { @@ -162,15 +177,18 @@ func EditPost(bid BoardID, threadid, postid PostID, title, body string) { if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { panic("invalid non-user call") } + caller := std.GetOrigCaller() board := getBoard(bid) if board == nil { panic("board not exist") } + thread := board.GetThread(threadid) if thread == nil { panic("thread not exist") } + if postid == threadid { // edit thread if !thread.HasPermission(caller, EditPermission) { diff --git a/examples/gno.land/r/demo/boards2/render.gno b/examples/gno.land/r/demo/boards2/render.gno index 3709ad02e5d..de0576c24b5 100644 --- a/examples/gno.land/r/demo/boards2/render.gno +++ b/examples/gno.land/r/demo/boards2/render.gno @@ -8,6 +8,8 @@ import ( //---------------------------------------- // Render functions +// TODO: Is this function really needed or Render("BOARD_NAME") is enough? +// TODO: Alternatively support ID or BOARD_NAME in Render() func RenderBoard(bid BoardID) string { board := getBoard(bid) if board == nil { @@ -17,36 +19,40 @@ func RenderBoard(bid BoardID) string { } func Render(path string) string { + // TODO: Split into four private render functions for each case if path == "" { - str := "These are all the boards of this realm:\n\n" - gBoards.Iterate("", "", func(key string, value interface{}) bool { + s := "These are all the boards of this realm:\n\n" + gBoards.Iterate("", "", func(_ string, value interface{}) bool { board := value.(*Board) - str += " * [" + board.url + "](" + board.url + ")\n" + s += " * [" + board.url + "](" + board.url + ")\n" return false }) - return str + return s } + parts := strings.Split(path, "/") if len(parts) == 1 { // /r/demo/boards:BOARD_NAME name := parts[0] - boardI, exists := gBoardsByName.Get(name) - if !exists { + v, found := gBoardsByName.Get(name) + if !found { return "board does not exist: " + name } - return boardI.(*Board).RenderBoard() + return v.(*Board).RenderBoard() } else if len(parts) == 2 { // /r/demo/boards:BOARD_NAME/THREAD_ID name := parts[0] - boardI, exists := gBoardsByName.Get(name) - if !exists { + v, found := gBoardsByName.Get(name) + if !found { return "board does not exist: " + name } + pid, err := strconv.Atoi(parts[1]) if err != nil { return "invalid thread id: " + parts[1] } - board := boardI.(*Board) + + board := v.(*Board) thread := board.GetThread(PostID(pid)) if thread == nil { return "thread does not exist with id: " + parts[1] @@ -55,23 +61,27 @@ func Render(path string) string { } else if len(parts) == 3 { // /r/demo/boards:BOARD_NAME/THREAD_ID/REPLY_ID name := parts[0] - boardI, exists := gBoardsByName.Get(name) - if !exists { + v, found := gBoardsByName.Get(name) + if !found { return "board does not exist: " + name } + pid, err := strconv.Atoi(parts[1]) if err != nil { return "invalid thread id: " + parts[1] } - board := boardI.(*Board) + + board := v.(*Board) thread := board.GetThread(PostID(pid)) if thread == nil { return "thread does not exist with id: " + parts[1] } + rid, err := strconv.Atoi(parts[2]) if err != nil { return "invalid reply id: " + parts[2] } + reply := thread.GetReply(PostID(rid)) if reply == nil { return "reply does not exist with id: " + parts[2] From 24546d2eb1a2d17421c1a526e0f21da8419e8183 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 12 Nov 2024 14:55:51 +0100 Subject: [PATCH 04/21] feat: change public realm func to use asserts for anonymous fees Asserts were moved to specific functions which also removes duplication. --- examples/gno.land/r/demo/boards2/boards.gno | 14 ++--- examples/gno.land/r/demo/boards2/misc.gno | 10 +--- examples/gno.land/r/demo/boards2/public.gno | 60 ++++++++++----------- 3 files changed, 37 insertions(+), 47 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno index 5d228c2251e..cee97dd699c 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -1,13 +1,13 @@ package boards -import ( - "gno.land/p/demo/avl" -) +import "gno.land/p/demo/avl" + +// Default minimum fee in ugnot required for anonymous users +const defaultAnonymousFee = 100_000_000 // TODO: Create a Boards or App struct to handle counter and tree indexes var ( - gBoards avl.Tree // id -> *Board - gBoardsCtr int // increments Board.id - gBoardsByName avl.Tree // name -> *Board - gDefaultAnonFee = 100000000 // minimum fee required if anonymous + gBoards avl.Tree // id -> *Board + gBoardsCtr int // increments Board.id + gBoardsByName avl.Tree // name -> *Board ) diff --git a/examples/gno.land/r/demo/boards2/misc.gno b/examples/gno.land/r/demo/boards2/misc.gno index 44c31244ff3..250d0b7aa12 100644 --- a/examples/gno.land/r/demo/boards2/misc.gno +++ b/examples/gno.land/r/demo/boards2/misc.gno @@ -8,7 +8,7 @@ import ( "gno.land/r/demo/users" ) -// TODO: Create a "strings.gno" file and move all string related functions there +// TODO: Create a "formatting.gno" file and move all formatting related functions there // TODO: Move getBoard() and incGetBoardID() to "boards.gno" //---------------------------------------- @@ -77,11 +77,3 @@ func displayAddressMD(addr std.Address) string { } return "[@" + user.Name + "](/r/demo/users:" + user.Name + ")" } - -func usernameOf(addr std.Address) string { - user := users.GetUserByAddress(addr) - if user == nil { - return "" - } - return user.Name -} diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 406aaa6b97c..4cf62d48564 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -2,7 +2,8 @@ package boards import ( "std" - "strconv" + + "gno.land/r/demo/users" ) // TODO: Consider a simpler API initially by removing this type of functions @@ -20,9 +21,7 @@ func CreateBoard(name string) BoardID { } caller := std.GetOrigCaller() - if usernameOf(caller) == "" { - panic("unauthorized") - } + assertIsNotAnonymousCaller(caller) bid := incGetBoardID() url := "/r/demo/boards:" + name @@ -32,28 +31,13 @@ func CreateBoard(name string) BoardID { return board.id } -// TODO: This must be an assertion function -// TODO: Should we display realm's amount? Transfer to a pool realm/DAO? -func checkAnonFee() bool { - sent := std.GetOrigSend() - anonFeeCoin := std.NewCoin("ugnot", int64(gDefaultAnonFee)) - if len(sent) == 1 && sent[0].IsGTE(anonFeeCoin) { - return true - } - return false -} - func CreateThread(bid BoardID, title string, body string) PostID { if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { panic("invalid non-user call") } caller := std.GetOrigCaller() - if usernameOf(caller) == "" { - if !checkAnonFee() { - panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") - } - } + assertAnonymousCallerFeeReceived(caller) board := getBoard(bid) if board == nil { @@ -72,11 +56,7 @@ func CreateReply(bid BoardID, threadid, postid PostID, body string) PostID { } caller := std.GetOrigCaller() - if usernameOf(caller) == "" { - if !checkAnonFee() { - panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") - } - } + assertAnonymousCallerFeeReceived(caller) board := getBoard(bid) // TODO: Create a mustGetBoard() function if board == nil { @@ -106,12 +86,7 @@ func CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoar } caller := std.GetOrigCaller() - if usernameOf(caller) == "" { - // TODO: allow with gDefaultAnonFee payment. - if !checkAnonFee() { - panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") - } - } + assertAnonymousCallerFeeReceived(caller) board := getBoard(bid) if board == nil { @@ -207,3 +182,26 @@ func EditPost(bid BoardID, threadid, postid PostID, title, body string) { post.Update(title, body) } } + +func assertIsNotAnonymousCaller(caller std.Address) { + // Caller is anonymous if doesn't have a registered user name + if users.GetUserByAddress(caller) == nil { + panic("unauthorized") + } +} + +func assertAnonymousFeeReceived() { + // TODO: Should we display realm's amount somewhere or transfer to a pool realm/DAO? + sent := std.GetOrigSend() + fee := std.NewCoin("ugnot", int64(defaultAnonymousFee)) + if len(sent) == 0 || sent[0].IsLT(fee) { + panic("please register a user, otherwise a minimum fee of " + fee.String() + " is required") + } + return +} + +func assertAnonymousCallerFeeReceived(caller std.Address) { + if users.GetUserByAddress(caller) == nil { + assertAnonymousFeeReceived() + } +} From fca2bf5088044836d59438fd1dbef5cefab1a15f Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 12 Nov 2024 15:01:49 +0100 Subject: [PATCH 05/21] feat: change public realm func to use asserts for non user calls --- examples/gno.land/r/demo/boards2/public.gno | 30 +++++++++------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 4cf62d48564..95a3f6dd953 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -16,9 +16,7 @@ func GetBoardIDFromName(name string) (BoardID, bool) { } func CreateBoard(name string) BoardID { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { - panic("invalid non-user call") - } + assertIsUserCall() caller := std.GetOrigCaller() assertIsNotAnonymousCaller(caller) @@ -32,9 +30,7 @@ func CreateBoard(name string) BoardID { } func CreateThread(bid BoardID, title string, body string) PostID { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { - panic("invalid non-user call") - } + assertIsUserCall() caller := std.GetOrigCaller() assertAnonymousCallerFeeReceived(caller) @@ -51,9 +47,7 @@ func CreateThread(bid BoardID, title string, body string) PostID { // TODO: Rename "postid" to "commentID", "threadid" to "threadID" // TODO: Split into CreateComment(threadID) & CreateReply(commentID), check link generation functions first (ex. Post.GetReplyFromURL()) func CreateReply(bid BoardID, threadid, postid PostID, body string) PostID { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { // TODO: Create assertIsUser() function - panic("invalid non-user call") - } + assertIsUserCall() caller := std.GetOrigCaller() assertAnonymousCallerFeeReceived(caller) @@ -81,9 +75,7 @@ func CreateReply(bid BoardID, threadid, postid PostID, body string) PostID { // If dstBoard is private, does not ping back. // If board specified by bid is private, panics. func CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoardID BoardID) PostID { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { - panic("invalid non-user call") - } + assertIsUserCall() caller := std.GetOrigCaller() assertAnonymousCallerFeeReceived(caller) @@ -113,9 +105,7 @@ func CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoar // TODO: Split into DeleteThread() & DeleteComment() func DeletePost(bid BoardID, threadid, postid PostID, reason string) { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { - panic("invalid non-user call") - } + assertIsUserCall() caller := std.GetOrigCaller() board := getBoard(bid) @@ -149,9 +139,7 @@ func DeletePost(bid BoardID, threadid, postid PostID, reason string) { // TODO: Split into EditThread() & EditComment() func EditPost(bid BoardID, threadid, postid PostID, title, body string) { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { - panic("invalid non-user call") - } + assertIsUserCall() caller := std.GetOrigCaller() board := getBoard(bid) @@ -183,6 +171,12 @@ func EditPost(bid BoardID, threadid, postid PostID, title, body string) { } } +func assertIsUserCall() { + if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + panic("invalid non-user call") + } +} + func assertIsNotAnonymousCaller(caller std.Address) { // Caller is anonymous if doesn't have a registered user name if users.GetUserByAddress(caller) == nil { From d06654575f8bb9e6aae52e384a136f3d3af73c6c Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 12 Nov 2024 15:25:19 +0100 Subject: [PATCH 06/21] chore: rename permissions to match Go coding standards --- examples/gno.land/r/demo/boards2/board.gno | 4 ++-- examples/gno.land/r/demo/boards2/post.gno | 5 ++--- examples/gno.land/r/demo/boards2/public.gno | 8 ++++---- examples/gno.land/r/demo/boards2/role.gno | 6 ++++-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index 20b82254856..fe75fd4f984 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -97,9 +97,9 @@ func (board *Board) DeleteThread(pid PostID) { func (board *Board) HasPermission(addr std.Address, perm Permission) bool { if board.creator == addr { switch perm { - case EditPermission: + case PermissionEdit: return true - case DeletePermission: + case PermissionDelete: return true default: return false diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index e67c454e4ee..c0fe2f6387c 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -23,7 +23,6 @@ func (id PostID) Key() string { // A Post is a "thread" or a "reply" depending on context. // A thread is a Post of a Board that holds other replies. -// TODO: Figure out "repliesAll", "threadID" type Post struct { board *Board id PostID @@ -134,9 +133,9 @@ func (thread *Post) DeletePost(pid PostID) { func (post *Post) HasPermission(addr std.Address, perm Permission) bool { if post.creator == addr { switch perm { - case EditPermission: + case PermissionEdit: return true - case DeletePermission: + case PermissionDelete: return true default: return false diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 95a3f6dd953..a2ac4832604 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -120,7 +120,7 @@ func DeletePost(bid BoardID, threadid, postid PostID, reason string) { if postid == threadid { // delete thread - if !thread.HasPermission(caller, DeletePermission) { + if !thread.HasPermission(caller, PermissionDelete) { panic("unauthorized") } board.DeleteThread(threadid) @@ -130,7 +130,7 @@ func DeletePost(bid BoardID, threadid, postid PostID, reason string) { if post == nil { panic("post not exist") } - if !post.HasPermission(caller, DeletePermission) { + if !post.HasPermission(caller, PermissionDelete) { panic("unauthorized") } thread.DeletePost(postid) @@ -154,7 +154,7 @@ func EditPost(bid BoardID, threadid, postid PostID, title, body string) { if postid == threadid { // edit thread - if !thread.HasPermission(caller, EditPermission) { + if !thread.HasPermission(caller, PermissionEdit) { panic("unauthorized") } thread.Update(title, body) @@ -164,7 +164,7 @@ func EditPost(bid BoardID, threadid, postid PostID, title, body string) { if post == nil { panic("post not exist") } - if !post.HasPermission(caller, EditPermission) { + if !post.HasPermission(caller, PermissionEdit) { panic("unauthorized") } post.Update(title, body) diff --git a/examples/gno.land/r/demo/boards2/role.gno b/examples/gno.land/r/demo/boards2/role.gno index 64073d64f34..fdfc2b5d7ef 100644 --- a/examples/gno.land/r/demo/boards2/role.gno +++ b/examples/gno.land/r/demo/boards2/role.gno @@ -1,8 +1,10 @@ package boards +// TODO: Rename file to "auth.gno" and define a new interface + type Permission string const ( - DeletePermission Permission = "role:delete" - EditPermission Permission = "role:edit" + PermissionEdit Permission = "edit" + PermissionDelete Permission = "delete" ) From 83a9393307c26d4cc1211d2e4186e427341a97e4 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 12 Nov 2024 16:19:50 +0100 Subject: [PATCH 07/21] feat: change render to use a router --- examples/gno.land/r/demo/boards2/render.gno | 153 +++++++++++--------- 1 file changed, 81 insertions(+), 72 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/render.gno b/examples/gno.land/r/demo/boards2/render.gno index de0576c24b5..98d22f3bf33 100644 --- a/examples/gno.land/r/demo/boards2/render.gno +++ b/examples/gno.land/r/demo/boards2/render.gno @@ -2,92 +2,101 @@ package boards import ( "strconv" - "strings" + + "gno.land/p/demo/mux" ) -//---------------------------------------- -// Render functions +func Render(path string) string { + router := mux.NewRouter() + router.HandleFunc("", renderBoardsList) + router.HandleFunc("{board}", renderBoard) + router.HandleFunc("{board}/{thread}", renderThread) + router.HandleFunc("{board}/{thread}/{reply}", renderReply) -// TODO: Is this function really needed or Render("BOARD_NAME") is enough? -// TODO: Alternatively support ID or BOARD_NAME in Render() -func RenderBoard(bid BoardID) string { - board := getBoard(bid) - if board == nil { - return "missing board" + router.NotFoundHandler = func(res *mux.ResponseWriter, _ *mux.Request) { + res.Write("Path not found") } - return board.RenderBoard() + + return router.Render(path) } -func Render(path string) string { - // TODO: Split into four private render functions for each case - if path == "" { - s := "These are all the boards of this realm:\n\n" - gBoards.Iterate("", "", func(_ string, value interface{}) bool { - board := value.(*Board) - s += " * [" + board.url + "](" + board.url + ")\n" - return false - }) - return s +func renderBoardsList(res *mux.ResponseWriter, _ *mux.Request) { + res.Write("These are all the boards of this realm:\n\n") + gBoards.Iterate("", "", func(_ string, value interface{}) bool { + board := value.(*Board) + res.Write(" * [" + board.url + "](" + board.url + ")\n") + return false + }) +} + +func renderBoard(res *mux.ResponseWriter, req *mux.Request) { + name := req.GetVar("board") + v, found := gBoardsByName.Get(name) + if !found { + res.Write("Board does not exist: " + name) + } else { + board := v.(*Board) + res.Write(board.RenderBoard()) } +} - parts := strings.Split(path, "/") - if len(parts) == 1 { - // /r/demo/boards:BOARD_NAME - name := parts[0] - v, found := gBoardsByName.Get(name) - if !found { - return "board does not exist: " + name - } - return v.(*Board).RenderBoard() - } else if len(parts) == 2 { - // /r/demo/boards:BOARD_NAME/THREAD_ID - name := parts[0] - v, found := gBoardsByName.Get(name) - if !found { - return "board does not exist: " + name - } +func renderThread(res *mux.ResponseWriter, req *mux.Request) { + name := req.GetVar("board") + v, found := gBoardsByName.Get(name) + if !found { + res.Write("Board does not exist: " + name) + return + } - pid, err := strconv.Atoi(parts[1]) - if err != nil { - return "invalid thread id: " + parts[1] - } + rawID := req.GetVar("thread") + tID, err := strconv.Atoi(rawID) + if err != nil { + res.Write("Invalid thread ID: " + rawID) + return + } - board := v.(*Board) - thread := board.GetThread(PostID(pid)) - if thread == nil { - return "thread does not exist with id: " + parts[1] - } - return thread.RenderPost("", 5) - } else if len(parts) == 3 { - // /r/demo/boards:BOARD_NAME/THREAD_ID/REPLY_ID - name := parts[0] - v, found := gBoardsByName.Get(name) - if !found { - return "board does not exist: " + name - } + board := v.(*Board) + thread := board.GetThread(PostID(tID)) + if thread == nil { + res.Write("Thread does not exist with ID: " + req.GetVar("thread")) + } else { + res.Write(thread.RenderPost("", 5)) + } +} + +func renderReply(res *mux.ResponseWriter, req *mux.Request) { + name := req.GetVar("board") + v, found := gBoardsByName.Get(name) + if !found { + res.Write("Board does not exist: " + name) + return + } - pid, err := strconv.Atoi(parts[1]) - if err != nil { - return "invalid thread id: " + parts[1] - } + rawID := req.GetVar("thread") + tID, err := strconv.Atoi(rawID) + if err != nil { + res.Write("Invalid thread ID: " + rawID) + return + } - board := v.(*Board) - thread := board.GetThread(PostID(pid)) - if thread == nil { - return "thread does not exist with id: " + parts[1] - } + rawID = req.GetVar("reply") + rID, err := strconv.Atoi(rawID) + if err != nil { + res.Write("Invalid reply ID: " + rawID) + return + } - rid, err := strconv.Atoi(parts[2]) - if err != nil { - return "invalid reply id: " + parts[2] - } + board := v.(*Board) + thread := board.GetThread(PostID(tID)) + if thread == nil { + res.Write("Thread does not exist with ID: " + req.GetVar("thread")) + return + } - reply := thread.GetReply(PostID(rid)) - if reply == nil { - return "reply does not exist with id: " + parts[2] - } - return reply.RenderInner() + reply := thread.GetReply(PostID(rID)) + if reply == nil { + res.Write("Reply does not exist with ID: " + req.GetVar("reply")) } else { - return "unrecognized path " + path + res.Write(reply.RenderInner()) } } From e95177f9bd670372767da3ebc1192c62351a6282 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 12 Nov 2024 16:23:16 +0100 Subject: [PATCH 08/21] chore: rename `Board.RenderBoard()` to `Board.Render()` --- examples/gno.land/r/demo/boards2/board.gno | 5 +---- examples/gno.land/r/demo/boards2/render.gno | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index fe75fd4f984..9e433495f38 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -108,10 +108,7 @@ func (board *Board) HasPermission(addr std.Address, perm Permission) bool { return false } -// Renders the board for display suitable as plaintext in -// console. This is suitable for demonstration or tests, -// but not for prod. -func (board *Board) RenderBoard() string { +func (board *Board) Render() string { s := "\\[[post](" + board.GetPostFormURL() + ")]\n\n" if board.threads.Size() > 0 { board.threads.Iterate("", "", func(_ string, v interface{}) bool { diff --git a/examples/gno.land/r/demo/boards2/render.gno b/examples/gno.land/r/demo/boards2/render.gno index 98d22f3bf33..78780d861a5 100644 --- a/examples/gno.land/r/demo/boards2/render.gno +++ b/examples/gno.land/r/demo/boards2/render.gno @@ -36,7 +36,7 @@ func renderBoard(res *mux.ResponseWriter, req *mux.Request) { res.Write("Board does not exist: " + name) } else { board := v.(*Board) - res.Write(board.RenderBoard()) + res.Write(board.Render()) } } From 9b514aa90c2235f02fd07f4481a82498844164f4 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 12 Nov 2024 16:31:42 +0100 Subject: [PATCH 09/21] chore: rename `misc.gno` to `format.gno` Board getter and ID incrementer were moved to `boards.gno` file. --- examples/gno.land/r/demo/boards2/boards.gno | 13 ++++++++++++ .../r/demo/boards2/{misc.gno => format.gno} | 20 ------------------- 2 files changed, 13 insertions(+), 20 deletions(-) rename examples/gno.land/r/demo/boards2/{misc.gno => format.gno} (70%) diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno index cee97dd699c..03db20a2097 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -11,3 +11,16 @@ var ( gBoardsCtr int // increments Board.id gBoardsByName avl.Tree // name -> *Board ) + +func getBoard(bid BoardID) *Board { + v, exists := gBoards.Get(bid.Key()) + if !exists { + return nil + } + return v.(*Board) +} + +func incGetBoardID() BoardID { + gBoardsCtr++ + return BoardID(gBoardsCtr) +} diff --git a/examples/gno.land/r/demo/boards2/misc.gno b/examples/gno.land/r/demo/boards2/format.gno similarity index 70% rename from examples/gno.land/r/demo/boards2/misc.gno rename to examples/gno.land/r/demo/boards2/format.gno index 250d0b7aa12..adc1895e2c5 100644 --- a/examples/gno.land/r/demo/boards2/misc.gno +++ b/examples/gno.land/r/demo/boards2/format.gno @@ -8,26 +8,6 @@ import ( "gno.land/r/demo/users" ) -// TODO: Create a "formatting.gno" file and move all formatting related functions there -// TODO: Move getBoard() and incGetBoardID() to "boards.gno" - -//---------------------------------------- -// private utility methods -// XXX ensure these cannot be called from public. - -func getBoard(bid BoardID) *Board { - v, exists := gBoards.Get(bid.Key()) - if !exists { - return nil - } - return v.(*Board) -} - -func incGetBoardID() BoardID { - gBoardsCtr++ - return BoardID(gBoardsCtr) -} - func padLeft(s string, length int) string { if len(s) >= length { return s From bfce60d89542498eb9204220d778522cf3799cee Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 12 Nov 2024 17:04:30 +0100 Subject: [PATCH 10/21] feat: add `newLink()` function to create Markdown links --- examples/gno.land/r/demo/boards2/board.gno | 2 +- examples/gno.land/r/demo/boards2/format.gno | 12 +++- examples/gno.land/r/demo/boards2/post.gno | 64 +++++++++++---------- examples/gno.land/r/demo/boards2/render.gno | 2 +- 4 files changed, 46 insertions(+), 34 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index 9e433495f38..bab1536436e 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -109,7 +109,7 @@ func (board *Board) HasPermission(addr std.Address, perm Permission) bool { } func (board *Board) Render() string { - s := "\\[[post](" + board.GetPostFormURL() + ")]\n\n" + s := "\\[" + newLink("post", board.GetPostFormURL()) + "]\n\n" if board.threads.Size() > 0 { board.threads.Iterate("", "", func(_ string, v interface{}) bool { s += "----------------------------------------\n" diff --git a/examples/gno.land/r/demo/boards2/format.gno b/examples/gno.land/r/demo/boards2/format.gno index adc1895e2c5..da29918fdae 100644 --- a/examples/gno.land/r/demo/boards2/format.gno +++ b/examples/gno.land/r/demo/boards2/format.gno @@ -50,10 +50,16 @@ func summaryOf(text string, length int) string { return line } -func displayAddressMD(addr std.Address) string { +// newLink returns a Markdown link. +func newLink(label, uri string) string { + return "[" + label + "](" + uri + ")" +} + +// newUserLink returns a Markdown link for an account to the users realm. +func newUserLink(addr std.Address) string { user := users.GetUserByAddress(addr) if user == nil { - return "[" + addr.String() + "](/r/demo/users:" + addr.String() + ")" + return newLink(addr.String(), "/r/demo/users:"+addr.String()) } - return "[@" + user.Name + "](/r/demo/users:" + user.Name + ")" + return newLink("@"+user.Name, "/r/demo/users:"+user.Name) } diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index c0fe2f6387c..c6e88e62981 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -11,6 +11,8 @@ import ( // TODO: Move post and board to a package to improve API and reduce realm size? +const dateFormat = "2006-01-02 3:04pm MST" + type PostID uint64 func (id PostID) String() string { @@ -193,15 +195,19 @@ func (post *Post) RenderSummary() string { return "Repost: " + post.GetSummary() + "\n" + thread.RenderSummary() } - var s string - if post.title != "" { // TODO: Add a newLink() function - s += "## [" + summaryOf(post.title, 80) + "](" + post.GetURL() + ")\n" - s += "\n" + var ( + s string + postURL = post.GetURL() + ) + + if post.title != "" { + s += "## " + newLink(summaryOf(post.title, 80), postURL) + "\n\n" } + s += post.GetSummary() + "\n" - s += "\\- " + displayAddressMD(post.creator) + "," - s += " [" + post.createdAt.Format("2006-01-02 3:04pm MST") + "](" + post.GetURL() + ")" - s += " \\[[x](" + post.GetDeleteFormURL() + ")]" + s += "\\- " + newUserLink(post.creator) + "," + s += " " + newLink(post.createdAt.Format(dateFormat), postURL) + s += " \\[" + newLink("x", post.GetDeleteFormURL()) + "]" s += " (" + strconv.Itoa(post.replies.Size()) + " replies)" s += " (" + strconv.Itoa(post.reposts.Size()) + " reposts)" + "\n" return s @@ -212,60 +218,60 @@ func (post *Post) RenderPost(indent string, levels int) string { return "nil post" } - var s string + var ( + s string + postURL = post.GetURL() + ) + if post.title != "" { s += indent + "# " + post.title + "\n" s += indent + "\n" } s += indentBody(indent, post.body) + "\n" // TODO: indent body lines. - - s += indent + "\\- " + displayAddressMD(post.creator) + ", " - s += "[" + post.createdAt.Format("2006-01-02 3:04pm (MST)") + "](" + post.GetURL() + ")" - s += " \\[[reply](" + post.GetReplyFormURL() + ")]" + s += indent + "\\- " + newUserLink(post.creator) + ", " + s += newLink(post.createdAt.Format(dateFormat), postURL) + s += " \\[" + newLink("reply", post.GetReplyFormURL()) + "]" if post.IsThread() { - s += " \\[[repost](" + post.GetRepostFormURL() + ")]" + s += " \\[" + newLink("repost", post.GetRepostFormURL()) + "]" } - s += " \\[[x](" + post.GetDeleteFormURL() + ")]\n" + s += " \\[" + newLink("x", post.GetDeleteFormURL()) + "]\n" if levels > 0 { if post.replies.Size() > 0 { - post.replies.Iterate("", "", func(key string, value interface{}) bool { + post.replies.Iterate("", "", func(_ string, value interface{}) bool { s += indent + "\n" s += value.(*Post).RenderPost(indent+"> ", levels-1) return false }) } - } else { - if post.replies.Size() > 0 { - s += indent + "\n" - s += indent + "_[see all " + strconv.Itoa(post.replies.Size()) + " replies](" + post.GetURL() + ")_\n" - } + } else if post.replies.Size() > 0 { + s += indent + "\n" + s += indent + "_" + newLink("see all "+strconv.Itoa(post.replies.Size())+" replies", postURL) + "_\n" } return s } -// render reply and link to context thread func (post *Post) RenderInner() string { if post.IsThread() { panic("unexpected thread") } - threadID := post.threadID - // replyID := post.id - parentID := post.parentID - s := "_[see thread](" + post.board.GetURLFromThreadAndReplyID(threadID, 0) + ")_\n\n" - thread := post.board.GetThread(post.threadID) + var ( + parent *Post + parentID = post.parentID + threadID = post.threadID + thread = post.board.GetThread(threadID) + ) - var parent *Post if thread.id == parentID { parent = thread } else { parent = thread.GetReply(parentID) } - s += parent.RenderPost("", 0) - s += "\n" + s := "_" + newLink("see thread", post.board.GetURLFromThreadAndReplyID(threadID, 0)) + "_\n\n" + s += parent.RenderPost("", 0) + "\n" s += post.RenderPost("> ", 5) return s } diff --git a/examples/gno.land/r/demo/boards2/render.gno b/examples/gno.land/r/demo/boards2/render.gno index 78780d861a5..0ccd5e8440f 100644 --- a/examples/gno.land/r/demo/boards2/render.gno +++ b/examples/gno.land/r/demo/boards2/render.gno @@ -24,7 +24,7 @@ func renderBoardsList(res *mux.ResponseWriter, _ *mux.Request) { res.Write("These are all the boards of this realm:\n\n") gBoards.Iterate("", "", func(_ string, value interface{}) bool { board := value.(*Board) - res.Write(" * [" + board.url + "](" + board.url + ")\n") + res.Write(" * " + newLink(board.url, board.url) + "\n") return false }) } From 5a84bdb8619564946f9e9b7c65fe18c0843924d5 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 12 Nov 2024 17:06:32 +0100 Subject: [PATCH 11/21] chore: rename `Post.RenderPost()` to `Post.Render()` --- examples/gno.land/r/demo/boards2/post.gno | 8 ++++---- examples/gno.land/r/demo/boards2/render.gno | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index c6e88e62981..d05dd27ce4f 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -213,7 +213,7 @@ func (post *Post) RenderSummary() string { return s } -func (post *Post) RenderPost(indent string, levels int) string { +func (post *Post) Render(indent string, levels int) string { if post == nil { return "nil post" } @@ -241,7 +241,7 @@ func (post *Post) RenderPost(indent string, levels int) string { if post.replies.Size() > 0 { post.replies.Iterate("", "", func(_ string, value interface{}) bool { s += indent + "\n" - s += value.(*Post).RenderPost(indent+"> ", levels-1) + s += value.(*Post).Render(indent+"> ", levels-1) return false }) } @@ -271,7 +271,7 @@ func (post *Post) RenderInner() string { } s := "_" + newLink("see thread", post.board.GetURLFromThreadAndReplyID(threadID, 0)) + "_\n\n" - s += parent.RenderPost("", 0) + "\n" - s += post.RenderPost("> ", 5) + s += parent.Render("", 0) + "\n" + s += post.Render("> ", 5) return s } diff --git a/examples/gno.land/r/demo/boards2/render.gno b/examples/gno.land/r/demo/boards2/render.gno index 0ccd5e8440f..0b840a7a404 100644 --- a/examples/gno.land/r/demo/boards2/render.gno +++ b/examples/gno.land/r/demo/boards2/render.gno @@ -60,7 +60,7 @@ func renderThread(res *mux.ResponseWriter, req *mux.Request) { if thread == nil { res.Write("Thread does not exist with ID: " + req.GetVar("thread")) } else { - res.Write(thread.RenderPost("", 5)) + res.Write(thread.Render("", 5)) } } From b3cb5db071486699cd0aacac8ef1a67f004ccca6 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 12 Nov 2024 19:56:31 +0100 Subject: [PATCH 12/21] feat: add `mustGetBoard()` function Done to simplify realm code and remove redundancy. --- examples/gno.land/r/demo/boards2/boards.gno | 18 ++++++++--- examples/gno.land/r/demo/boards2/post.gno | 4 +-- examples/gno.land/r/demo/boards2/public.gno | 36 ++++----------------- 3 files changed, 22 insertions(+), 36 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno index 03db20a2097..d5c8e1ead47 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -12,12 +12,22 @@ var ( gBoardsByName avl.Tree // name -> *Board ) -func getBoard(bid BoardID) *Board { - v, exists := gBoards.Get(bid.Key()) +// getBoard returns a boards for a specific ID. +func getBoard(id BoardID) (_ *Board, found bool) { + v, exists := gBoards.Get(id.Key()) if !exists { - return nil + return nil, false } - return v.(*Board) + return v.(*Board), true +} + +// mustGetBoard returns a board or panics when it's not found. +func mustGetBoard(id BoardID) *Board { + board, found := getBoard(id) + if !found { + panic("board does not exist with ID: " + id.String()) + } + return board } func incGetBoardID() BoardID { diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index d05dd27ce4f..cf9f39f1d16 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -183,8 +183,8 @@ func (post *Post) GetDeleteFormURL() string { func (post *Post) RenderSummary() string { if post.repostBoard != 0 { - dstBoard := getBoard(post.repostBoard) - if dstBoard == nil { + dstBoard, found := getBoard(post.repostBoard) + if !found { panic("repostBoard does not exist") } diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index a2ac4832604..bff831f9b4e 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -35,11 +35,7 @@ func CreateThread(bid BoardID, title string, body string) PostID { caller := std.GetOrigCaller() assertAnonymousCallerFeeReceived(caller) - board := getBoard(bid) - if board == nil { - panic("board not exist") - } - + board := mustGetBoard(bid) thread := board.AddThread(caller, title, body) return thread.id } @@ -52,11 +48,7 @@ func CreateReply(bid BoardID, threadid, postid PostID, body string) PostID { caller := std.GetOrigCaller() assertAnonymousCallerFeeReceived(caller) - board := getBoard(bid) // TODO: Create a mustGetBoard() function - if board == nil { - panic("board not exist") - } - + board := mustGetBoard(bid) thread := board.GetThread(threadid) // TODO: Use found flag to encorage returned value validity checks if thread == nil { panic("thread not exist") @@ -80,20 +72,12 @@ func CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoar caller := std.GetOrigCaller() assertAnonymousCallerFeeReceived(caller) - board := getBoard(bid) - if board == nil { - panic("src board not exist") - } - + board := mustGetBoard(bid) if board.IsPrivate() { panic("cannot repost from a private board") } - dst := getBoard(dstBoardID) - if dst == nil { - panic("dst board not exist") - } - + dst := mustGetBoard(dstBoardID) thread := board.GetThread(postid) if thread == nil { panic("thread not exist") @@ -108,11 +92,7 @@ func DeletePost(bid BoardID, threadid, postid PostID, reason string) { assertIsUserCall() caller := std.GetOrigCaller() - board := getBoard(bid) - if board == nil { - panic("board not exist") - } - + board := mustGetBoard(bid) thread := board.GetThread(threadid) if thread == nil { panic("thread not exist") @@ -142,11 +122,7 @@ func EditPost(bid BoardID, threadid, postid PostID, title, body string) { assertIsUserCall() caller := std.GetOrigCaller() - board := getBoard(bid) - if board == nil { - panic("board not exist") - } - + board := mustGetBoard(bid) thread := board.GetThread(threadid) if thread == nil { panic("thread not exist") From 3f1f4de41a4ea6b0e27d7733c486a25ff3311e75 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Tue, 12 Nov 2024 20:14:10 +0100 Subject: [PATCH 13/21] chore: semantic changes --- examples/gno.land/r/demo/boards2/board.gno | 8 ++-- examples/gno.land/r/demo/boards2/post.gno | 14 +++--- examples/gno.land/r/demo/boards2/public.gno | 48 ++++++++++----------- examples/gno.land/r/demo/boards2/render.gno | 8 ++-- 4 files changed, 37 insertions(+), 41 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index bab1536436e..5a1aefa20ab 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -69,12 +69,12 @@ func (board *Board) IsPrivate() bool { return board.id == 0 } -func (board *Board) GetThread(pid PostID) *Post { - v, found := board.threads.Get(pid.Key()) +func (board *Board) GetThread(threadID PostID) (_ *Post, found bool) { + v, found := board.threads.Get(threadID.Key()) if !found { - return nil + return nil, false } - return v.(*Post) + return v.(*Post), true } func (board *Board) AddThread(creator std.Address, title string, body string) *Post { diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index cf9f39f1d16..32987102777 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -77,7 +77,7 @@ func (post *Post) AddReply(creator std.Address, body string) *Post { if post.threadID == post.id { post.repliesAll.Set(pKey, reply) } else { - thread := board.GetThread(post.threadID) + thread, _ := board.GetThread(post.threadID) thread.repliesAll.Set(pKey, reply) } return reply @@ -188,8 +188,8 @@ func (post *Post) RenderSummary() string { panic("repostBoard does not exist") } - thread := dstBoard.GetThread(PostID(post.parentID)) - if thread == nil { + thread, found := dstBoard.GetThread(PostID(post.parentID)) + if !found { return "reposted post does not exist" } return "Repost: " + post.GetSummary() + "\n" + thread.RenderSummary() @@ -258,10 +258,10 @@ func (post *Post) RenderInner() string { } var ( - parent *Post - parentID = post.parentID - threadID = post.threadID - thread = post.board.GetThread(threadID) + parent *Post + parentID = post.parentID + threadID = post.threadID + thread, _ = post.board.GetThread(threadID) // TODO: This seems redundant (post == thread) ) if thread.id == parentID { diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index bff831f9b4e..73d93bc26df 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -40,33 +40,29 @@ func CreateThread(bid BoardID, title string, body string) PostID { return thread.id } -// TODO: Rename "postid" to "commentID", "threadid" to "threadID" -// TODO: Split into CreateComment(threadID) & CreateReply(commentID), check link generation functions first (ex. Post.GetReplyFromURL()) -func CreateReply(bid BoardID, threadid, postid PostID, body string) PostID { +func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { assertIsUserCall() caller := std.GetOrigCaller() assertAnonymousCallerFeeReceived(caller) board := mustGetBoard(bid) - thread := board.GetThread(threadid) // TODO: Use found flag to encorage returned value validity checks - if thread == nil { + thread, found := board.GetThread(threadID) // TODO: Use found flag to encorage returned value validity checks + if !found { panic("thread not exist") } - if postid == threadid { + if replyID == threadID { reply := thread.AddReply(caller, body) return reply.id - } else { - post := thread.GetReply(postid) - reply := post.AddReply(caller, body) - return reply.id } + + post := thread.GetReply(replyID) + reply := post.AddReply(caller, body) + return reply.id } -// If dstBoard is private, does not ping back. -// If board specified by bid is private, panics. -func CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoardID BoardID) PostID { +func CreateRepost(bid BoardID, threadID PostID, title string, body string, dstBoardID BoardID) PostID { assertIsUserCall() caller := std.GetOrigCaller() @@ -78,8 +74,8 @@ func CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoar } dst := mustGetBoard(dstBoardID) - thread := board.GetThread(postid) - if thread == nil { + thread, found := board.GetThread(threadID) + if !found { panic("thread not exist") } @@ -87,23 +83,23 @@ func CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoar return repost.id } -// TODO: Split into DeleteThread() & DeleteComment() -func DeletePost(bid BoardID, threadid, postid PostID, reason string) { +// TODO: Split into DeleteThread() & DeleteReply() +func DeletePost(bid BoardID, threadID, postid PostID, reason string) { assertIsUserCall() caller := std.GetOrigCaller() board := mustGetBoard(bid) - thread := board.GetThread(threadid) - if thread == nil { + thread, found := board.GetThread(threadID) + if !found { panic("thread not exist") } - if postid == threadid { + if postid == threadID { // delete thread if !thread.HasPermission(caller, PermissionDelete) { panic("unauthorized") } - board.DeleteThread(threadid) + board.DeleteThread(threadID) } else { // delete thread's post post := thread.GetReply(postid) @@ -117,18 +113,18 @@ func DeletePost(bid BoardID, threadid, postid PostID, reason string) { } } -// TODO: Split into EditThread() & EditComment() -func EditPost(bid BoardID, threadid, postid PostID, title, body string) { +// TODO: Split into EditThread() & EditReply() +func EditPost(bid BoardID, threadID, postid PostID, title, body string) { assertIsUserCall() caller := std.GetOrigCaller() board := mustGetBoard(bid) - thread := board.GetThread(threadid) - if thread == nil { + thread, found := board.GetThread(threadID) + if !found { panic("thread not exist") } - if postid == threadid { + if postid == threadID { // edit thread if !thread.HasPermission(caller, PermissionEdit) { panic("unauthorized") diff --git a/examples/gno.land/r/demo/boards2/render.gno b/examples/gno.land/r/demo/boards2/render.gno index 0b840a7a404..5eb31c2b7d5 100644 --- a/examples/gno.land/r/demo/boards2/render.gno +++ b/examples/gno.land/r/demo/boards2/render.gno @@ -56,8 +56,8 @@ func renderThread(res *mux.ResponseWriter, req *mux.Request) { } board := v.(*Board) - thread := board.GetThread(PostID(tID)) - if thread == nil { + thread, found := board.GetThread(PostID(tID)) + if !found { res.Write("Thread does not exist with ID: " + req.GetVar("thread")) } else { res.Write(thread.Render("", 5)) @@ -87,8 +87,8 @@ func renderReply(res *mux.ResponseWriter, req *mux.Request) { } board := v.(*Board) - thread := board.GetThread(PostID(tID)) - if thread == nil { + thread, found := board.GetThread(PostID(tID)) + if !found { res.Write("Thread does not exist with ID: " + req.GetVar("thread")) return } From b4d35bf5838b41fd3c26c5e3254630c41e981102 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Wed, 13 Nov 2024 10:55:03 +0100 Subject: [PATCH 14/21] refactor: split `DeletePost` into `DeleteThread` & `DeleteReply` The `DeletePost()` public function was confusing and in case of thread it required an argument to be zero. The generic post concept should not be exposed to users to keep the public realm API easy to understand. --- examples/gno.land/r/demo/boards2/board.gno | 2 - examples/gno.land/r/demo/boards2/post.gno | 19 +++++---- examples/gno.land/r/demo/boards2/public.gno | 47 ++++++++++++--------- 3 files changed, 37 insertions(+), 31 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index 5a1aefa20ab..3e222de1d1a 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -12,8 +12,6 @@ import ( var reBoardName = regexp.MustCompile(`^[a-z]{3}[_a-z0-9]{0,23}[0-9]{3}$`) -// TODO: Move post and board to a package to improve API and reduce realm size? - type BoardID uint64 func (id BoardID) String() string { diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index 32987102777..0e150f3eeff 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -9,8 +9,6 @@ import ( "gno.land/p/moul/txlink" ) -// TODO: Move post and board to a package to improve API and reduce realm size? - const dateFormat = "2006-01-02 3:04pm MST" type PostID uint64 @@ -66,7 +64,6 @@ func (post *Post) GetPostID() PostID { return post.id } -// TODO: Rename to Reply() func (post *Post) AddReply(creator std.Address, body string) *Post { board := post.board pid := board.incGetPostID() @@ -89,7 +86,7 @@ func (post *Post) Update(title string, body string) { post.updatedAt = time.Now() } -func (thread *Post) GetReply(pid PostID) *Post { +func (thread *Post) GetReply(pid PostID) *Post { // TODO: Add found to result to encourage checks v, found := thread.repliesAll.Get(pid.Key()) if !found { return nil @@ -111,7 +108,7 @@ func (post *Post) AddRepostTo(creator std.Address, title, body string, dst *Boar return repost } -func (thread *Post) DeletePost(pid PostID) { +func (thread *Post) DeleteReply(pid PostID) { if thread.id == pid { panic("should not happen") } @@ -174,10 +171,16 @@ func (post *Post) GetRepostFormURL() string { } func (post *Post) GetDeleteFormURL() string { - return txlink.URL("DeletePost", + if post.IsThread() { + return txlink.URL("DeleteThread", + "bid", post.board.id.String(), + "threadID", post.threadID.String(), + ) + } + return txlink.URL("DeleteReply", "bid", post.board.id.String(), - "threadid", post.threadID.String(), - "postid", post.id.String(), + "threadID", post.threadID.String(), + "replyID", post.id.String(), ) } diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 73d93bc26df..839a1f764f3 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -6,7 +6,6 @@ import ( "gno.land/r/demo/users" ) -// TODO: Consider a simpler API initially by removing this type of functions func GetBoardIDFromName(name string) (BoardID, bool) { v, found := gBoardsByName.Get(name) if !found { @@ -47,7 +46,7 @@ func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { assertAnonymousCallerFeeReceived(caller) board := mustGetBoard(bid) - thread, found := board.GetThread(threadID) // TODO: Use found flag to encorage returned value validity checks + thread, found := board.GetThread(threadID) if !found { panic("thread not exist") } @@ -83,34 +82,40 @@ func CreateRepost(bid BoardID, threadID PostID, title string, body string, dstBo return repost.id } -// TODO: Split into DeleteThread() & DeleteReply() -func DeletePost(bid BoardID, threadID, postid PostID, reason string) { +func DeleteThread(bid BoardID, threadID PostID, reason string) { assertIsUserCall() caller := std.GetOrigCaller() board := mustGetBoard(bid) thread, found := board.GetThread(threadID) if !found { - panic("thread not exist") + panic("thread not found") } - if postid == threadID { - // delete thread - if !thread.HasPermission(caller, PermissionDelete) { - panic("unauthorized") - } - board.DeleteThread(threadID) - } else { - // delete thread's post - post := thread.GetReply(postid) - if post == nil { - panic("post not exist") - } - if !post.HasPermission(caller, PermissionDelete) { - panic("unauthorized") - } - thread.DeletePost(postid) + if !thread.HasPermission(caller, PermissionDelete) { + panic("unauthorized") + } + board.DeleteThread(threadID) +} + +func DeleteReply(bid BoardID, threadID, replyID PostID, reason string) { + assertIsUserCall() + + caller := std.GetOrigCaller() + board := mustGetBoard(bid) + thread, found := board.GetThread(threadID) // TODO: Add mustGetThread(board, threadID) + if !found { + panic("thread not found") + } + + reply := thread.GetReply(replyID) // TODO: Add mustGetReply(thread, replyID) + if reply == nil { + panic("reply not found") + } + if !reply.HasPermission(caller, PermissionDelete) { // TODO: Add AssertHasPermission() + panic("unauthorized") } + thread.DeleteReply(replyID) } // TODO: Split into EditThread() & EditReply() From 318d3efeeb78f341ea661d5d6a6355137cb0afd3 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Wed, 13 Nov 2024 11:12:45 +0100 Subject: [PATCH 15/21] refactor: split `EditPost` into `EditThread` & `EditReply` --- examples/gno.land/r/demo/boards2/public.gno | 42 ++++++++++++--------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 839a1f764f3..fe3a256b2a9 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -118,8 +118,23 @@ func DeleteReply(bid BoardID, threadID, replyID PostID, reason string) { thread.DeleteReply(replyID) } -// TODO: Split into EditThread() & EditReply() -func EditPost(bid BoardID, threadID, postid PostID, title, body string) { +func EditThread(bid BoardID, threadID PostID, title, body string) { + assertIsUserCall() + + caller := std.GetOrigCaller() + board := mustGetBoard(bid) + thread, found := board.GetThread(threadID) + if !found { + panic("thread not found") + } + + if !thread.HasPermission(caller, PermissionEdit) { + panic("unauthorized") + } + thread.Update(title, body) +} + +func EditReply(bid BoardID, threadID, replyID PostID, title, body string) { assertIsUserCall() caller := std.GetOrigCaller() @@ -129,23 +144,14 @@ func EditPost(bid BoardID, threadID, postid PostID, title, body string) { panic("thread not exist") } - if postid == threadID { - // edit thread - if !thread.HasPermission(caller, PermissionEdit) { - panic("unauthorized") - } - thread.Update(title, body) - } else { - // edit thread's post - post := thread.GetReply(postid) - if post == nil { - panic("post not exist") - } - if !post.HasPermission(caller, PermissionEdit) { - panic("unauthorized") - } - post.Update(title, body) + post := thread.GetReply(replyID) + if post == nil { + panic("post not found") + } + if !post.HasPermission(caller, PermissionEdit) { + panic("unauthorized") } + post.Update(title, body) } func assertIsUserCall() { From c9b8a3ac663244b2307ab6b05493ef958da6fb99 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Wed, 13 Nov 2024 11:27:09 +0100 Subject: [PATCH 16/21] feat: add `mustGetThread` & `mustGetReply` funcs This allows removing a lot of redundant code. --- examples/gno.land/r/demo/boards2/board.gno | 1 - examples/gno.land/r/demo/boards2/boards.gno | 18 +++++++ examples/gno.land/r/demo/boards2/public.gno | 59 +++++++-------------- 3 files changed, 38 insertions(+), 40 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index 3e222de1d1a..e7e9e4a1de8 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -62,7 +62,6 @@ func NewPrivateBoard(url string, name string, creator std.Address) *Board { } */ -// TODO: Do we want to support private boards? If so move forward with implementation. func (board *Board) IsPrivate() bool { return board.id == 0 } diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno index d5c8e1ead47..e7841bc518c 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -30,6 +30,24 @@ func mustGetBoard(id BoardID) *Board { return board } +// mustGetThread returns a thread or panics when it's not found. +func mustGetThread(board *Board, threadID PostID) *Post { + thread, found := board.GetThread(threadID) + if !found { + panic("thread does not exist with ID: " + threadID.String()) + } + return thread +} + +// mustGetReply returns a reply or panics when it's not found. +func mustGetReply(thread *Post, replyID PostID) *Post { + reply := thread.GetReply(replyID) + if reply == nil { + panic("reply does not exist with ID: " + replyID.String()) + } + return reply +} + func incGetBoardID() BoardID { gBoardsCtr++ return BoardID(gBoardsCtr) diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index fe3a256b2a9..273250313d9 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -45,19 +45,18 @@ func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { caller := std.GetOrigCaller() assertAnonymousCallerFeeReceived(caller) - board := mustGetBoard(bid) - thread, found := board.GetThread(threadID) - if !found { - panic("thread not exist") - } + var ( + reply *Post + board = mustGetBoard(bid) + thread = mustGetThread(board, threadID) + ) if replyID == threadID { - reply := thread.AddReply(caller, body) - return reply.id + reply = thread.AddReply(caller, body) + } else { + post := mustGetReply(thread, replyID) + reply = post.AddReply(caller, body) } - - post := thread.GetReply(replyID) - reply := post.AddReply(caller, body) return reply.id } @@ -73,11 +72,7 @@ func CreateRepost(bid BoardID, threadID PostID, title string, body string, dstBo } dst := mustGetBoard(dstBoardID) - thread, found := board.GetThread(threadID) - if !found { - panic("thread not exist") - } - + thread := mustGetThread(board, threadID) repost := thread.AddRepostTo(caller, title, body, dst) return repost.id } @@ -87,14 +82,12 @@ func DeleteThread(bid BoardID, threadID PostID, reason string) { caller := std.GetOrigCaller() board := mustGetBoard(bid) - thread, found := board.GetThread(threadID) - if !found { - panic("thread not found") - } + thread := mustGetThread(board, threadID) if !thread.HasPermission(caller, PermissionDelete) { panic("unauthorized") } + board.DeleteThread(threadID) } @@ -103,18 +96,13 @@ func DeleteReply(bid BoardID, threadID, replyID PostID, reason string) { caller := std.GetOrigCaller() board := mustGetBoard(bid) - thread, found := board.GetThread(threadID) // TODO: Add mustGetThread(board, threadID) - if !found { - panic("thread not found") - } + thread := mustGetThread(board, threadID) + reply := mustGetReply(thread, replyID) - reply := thread.GetReply(replyID) // TODO: Add mustGetReply(thread, replyID) - if reply == nil { - panic("reply not found") - } if !reply.HasPermission(caller, PermissionDelete) { // TODO: Add AssertHasPermission() panic("unauthorized") } + thread.DeleteReply(replyID) } @@ -123,14 +111,12 @@ func EditThread(bid BoardID, threadID PostID, title, body string) { caller := std.GetOrigCaller() board := mustGetBoard(bid) - thread, found := board.GetThread(threadID) - if !found { - panic("thread not found") - } + thread := mustGetThread(board, threadID) if !thread.HasPermission(caller, PermissionEdit) { panic("unauthorized") } + thread.Update(title, body) } @@ -139,18 +125,13 @@ func EditReply(bid BoardID, threadID, replyID PostID, title, body string) { caller := std.GetOrigCaller() board := mustGetBoard(bid) - thread, found := board.GetThread(threadID) - if !found { - panic("thread not exist") - } + thread := mustGetThread(board, threadID) + post := mustGetReply(thread, replyID) - post := thread.GetReply(replyID) - if post == nil { - panic("post not found") - } if !post.HasPermission(caller, PermissionEdit) { panic("unauthorized") } + post.Update(title, body) } From 44a8a35d908f915c758e9886edd2b52c6b059c8a Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Wed, 13 Nov 2024 11:45:22 +0100 Subject: [PATCH 17/21] feat: add `assertUserHasPermission()` function --- examples/gno.land/r/demo/boards2/public.gno | 31 ++++++++++----------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 273250313d9..9f602d56fbc 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -80,13 +80,11 @@ func CreateRepost(bid BoardID, threadID PostID, title string, body string, dstBo func DeleteThread(bid BoardID, threadID PostID, reason string) { assertIsUserCall() - caller := std.GetOrigCaller() board := mustGetBoard(bid) thread := mustGetThread(board, threadID) - if !thread.HasPermission(caller, PermissionDelete) { - panic("unauthorized") - } + caller := std.GetOrigCaller() + assertUserHasPermission(thread, caller, PermissionDelete) board.DeleteThread(threadID) } @@ -94,14 +92,12 @@ func DeleteThread(bid BoardID, threadID PostID, reason string) { func DeleteReply(bid BoardID, threadID, replyID PostID, reason string) { assertIsUserCall() - caller := std.GetOrigCaller() board := mustGetBoard(bid) thread := mustGetThread(board, threadID) reply := mustGetReply(thread, replyID) - if !reply.HasPermission(caller, PermissionDelete) { // TODO: Add AssertHasPermission() - panic("unauthorized") - } + caller := std.GetOrigCaller() + assertUserHasPermission(reply, caller, PermissionDelete) thread.DeleteReply(replyID) } @@ -109,13 +105,11 @@ func DeleteReply(bid BoardID, threadID, replyID PostID, reason string) { func EditThread(bid BoardID, threadID PostID, title, body string) { assertIsUserCall() - caller := std.GetOrigCaller() board := mustGetBoard(bid) thread := mustGetThread(board, threadID) - if !thread.HasPermission(caller, PermissionEdit) { - panic("unauthorized") - } + caller := std.GetOrigCaller() + assertUserHasPermission(thread, caller, PermissionEdit) thread.Update(title, body) } @@ -123,14 +117,12 @@ func EditThread(bid BoardID, threadID PostID, title, body string) { func EditReply(bid BoardID, threadID, replyID PostID, title, body string) { assertIsUserCall() - caller := std.GetOrigCaller() board := mustGetBoard(bid) thread := mustGetThread(board, threadID) post := mustGetReply(thread, replyID) - if !post.HasPermission(caller, PermissionEdit) { - panic("unauthorized") - } + caller := std.GetOrigCaller() + assertUserHasPermission(post, caller, PermissionEdit) post.Update(title, body) } @@ -149,7 +141,6 @@ func assertIsNotAnonymousCaller(caller std.Address) { } func assertAnonymousFeeReceived() { - // TODO: Should we display realm's amount somewhere or transfer to a pool realm/DAO? sent := std.GetOrigSend() fee := std.NewCoin("ugnot", int64(defaultAnonymousFee)) if len(sent) == 0 || sent[0].IsLT(fee) { @@ -163,3 +154,9 @@ func assertAnonymousCallerFeeReceived(caller std.Address) { assertAnonymousFeeReceived() } } + +func assertUserHasPermission(post *Post, user std.Address, p Permission) { + if !post.HasPermission(user, p) { + panic("unauthorized") + } +} From 69b0bed2aa4be276db77df9119705a3171e79511 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Wed, 13 Nov 2024 11:53:35 +0100 Subject: [PATCH 18/21] refactor: change `Post.GetReply` to also return "found" boolean Returning "found" encourages users to check to avoid invalid memory access and also follows a pattern already stablished in Gno packages which is a good practice. --- examples/gno.land/r/demo/boards2/boards.gno | 4 ++-- examples/gno.land/r/demo/boards2/post.gno | 24 ++++++++++----------- examples/gno.land/r/demo/boards2/render.gno | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno index e7841bc518c..36367f92e2c 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -41,8 +41,8 @@ func mustGetThread(board *Board, threadID PostID) *Post { // mustGetReply returns a reply or panics when it's not found. func mustGetReply(thread *Post, replyID PostID) *Post { - reply := thread.GetReply(replyID) - if reply == nil { + reply, found := thread.GetReply(replyID) + if !found { panic("reply does not exist with ID: " + replyID.String()) } return reply diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index 0e150f3eeff..ac3199b1965 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -86,12 +86,12 @@ func (post *Post) Update(title string, body string) { post.updatedAt = time.Now() } -func (thread *Post) GetReply(pid PostID) *Post { // TODO: Add found to result to encourage checks +func (thread *Post) GetReply(pid PostID) (_ *Post, found bool) { v, found := thread.repliesAll.Get(pid.Key()) if !found { - return nil + return nil, false } - return v.(*Post) + return v.(*Post), true } func (post *Post) AddRepostTo(creator std.Address, title, body string, dst *Board) *Post { @@ -108,23 +108,23 @@ func (post *Post) AddRepostTo(creator std.Address, title, body string, dst *Boar return repost } -func (thread *Post) DeleteReply(pid PostID) { - if thread.id == pid { +func (thread *Post) DeleteReply(replyID PostID) { + if thread.id == replyID { panic("should not happen") } - pKey := pid.Key() - v, removed := thread.repliesAll.Remove(pKey) + key := replyID.Key() + v, removed := thread.repliesAll.Remove(key) if !removed { - panic("post not found in thread") + panic("reply not found in thread") } post := v.(*Post) if post.parentID != thread.id { - parent := thread.GetReply(post.parentID) - parent.replies.Remove(pKey) + parent, _ := thread.GetReply(post.parentID) + parent.replies.Remove(key) } else { - thread.replies.Remove(pKey) + thread.replies.Remove(key) } } @@ -270,7 +270,7 @@ func (post *Post) RenderInner() string { if thread.id == parentID { parent = thread } else { - parent = thread.GetReply(parentID) + parent, _ = thread.GetReply(parentID) } s := "_" + newLink("see thread", post.board.GetURLFromThreadAndReplyID(threadID, 0)) + "_\n\n" diff --git a/examples/gno.land/r/demo/boards2/render.gno b/examples/gno.land/r/demo/boards2/render.gno index 5eb31c2b7d5..0258e266508 100644 --- a/examples/gno.land/r/demo/boards2/render.gno +++ b/examples/gno.land/r/demo/boards2/render.gno @@ -93,8 +93,8 @@ func renderReply(res *mux.ResponseWriter, req *mux.Request) { return } - reply := thread.GetReply(PostID(rID)) - if reply == nil { + reply, found := thread.GetReply(PostID(rID)) + if !found { res.Write("Reply does not exist with ID: " + req.GetVar("reply")) } else { res.Write(reply.RenderInner()) From 5e15250da5a98893096d0662381b1fe4c57f1bf9 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Wed, 13 Nov 2024 12:20:36 +0100 Subject: [PATCH 19/21] refactor: split `Board.GetURLFromThreadAndReplyID()` in two methods --- examples/gno.land/r/demo/boards2/board.gno | 9 +++++---- examples/gno.land/r/demo/boards2/post.gno | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index e7e9e4a1de8..ef4bbd10b18 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -122,10 +122,11 @@ func (board *Board) incGetPostID() PostID { return PostID(board.postsCtr) } -func (board *Board) GetURLFromThreadAndReplyID(threadID, replyID PostID) string { - if replyID == 0 { - return board.url + "/" + threadID.String() - } +func (board *Board) GetURLFromThreadID(threadID PostID) string { + return board.url + "/" + threadID.String() +} + +func (board *Board) GetURLFromReplyID(threadID, replyID PostID) string { return board.url + "/" + threadID.String() + "/" + replyID.String() } diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index ac3199b1965..5f9ceae2f5e 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -150,9 +150,9 @@ func (post *Post) GetSummary() string { func (post *Post) GetURL() string { if post.IsThread() { - return post.board.GetURLFromThreadAndReplyID(post.id, 0) + return post.board.GetURLFromThreadID(post.id) } - return post.board.GetURLFromThreadAndReplyID(post.threadID, post.id) + return post.board.GetURLFromReplyID(post.threadID, post.id) } func (post *Post) GetReplyFormURL() string { @@ -273,7 +273,7 @@ func (post *Post) RenderInner() string { parent, _ = thread.GetReply(parentID) } - s := "_" + newLink("see thread", post.board.GetURLFromThreadAndReplyID(threadID, 0)) + "_\n\n" + s := "_" + newLink("see thread", post.board.GetURLFromThreadID(threadID)) + "_\n\n" s += parent.Render("", 0) + "\n" s += post.Render("> ", 5) return s From 147945d47ffb32a9cbb8c9112ffc0661111c1f50 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Wed, 13 Nov 2024 13:14:40 +0100 Subject: [PATCH 20/21] chore: rename boards state globals for consistency --- examples/gno.land/r/demo/boards2/board.gno | 3 +-- examples/gno.land/r/demo/boards2/boards.gno | 22 ++++++++++----------- examples/gno.land/r/demo/boards2/public.gno | 8 ++++---- examples/gno.land/r/demo/boards2/render.gno | 2 +- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index ef4bbd10b18..e464c1d611b 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -36,8 +36,7 @@ type Board struct { func newBoard(id BoardID, url string, name string, creator std.Address) *Board { assertIsBoardName(name) - exists := gBoardsByName.Has(name) - if exists { + if gBoardsByName.Has(name) { panic("board already exists") } diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno index 36367f92e2c..fa487a3914a 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -5,16 +5,21 @@ import "gno.land/p/demo/avl" // Default minimum fee in ugnot required for anonymous users const defaultAnonymousFee = 100_000_000 -// TODO: Create a Boards or App struct to handle counter and tree indexes var ( - gBoards avl.Tree // id -> *Board - gBoardsCtr int // increments Board.id - gBoardsByName avl.Tree // name -> *Board + gLastBoardID BoardID + gBoardsByID avl.Tree // string(id) -> *Board + gBoardsByName avl.Tree // string(name) -> *Board ) -// getBoard returns a boards for a specific ID. +// incGetBoardID returns a new board ID. +func incGetBoardID() BoardID { + gLastBoardID++ + return gLastBoardID +} + +// getBoard returns a board for a specific ID. func getBoard(id BoardID) (_ *Board, found bool) { - v, exists := gBoards.Get(id.Key()) + v, exists := gBoardsByID.Get(id.Key()) if !exists { return nil, false } @@ -47,8 +52,3 @@ func mustGetReply(thread *Post, replyID PostID) *Post { } return reply } - -func incGetBoardID() BoardID { - gBoardsCtr++ - return BoardID(gBoardsCtr) -} diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 9f602d56fbc..3999401025c 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -20,10 +20,10 @@ func CreateBoard(name string) BoardID { caller := std.GetOrigCaller() assertIsNotAnonymousCaller(caller) - bid := incGetBoardID() - url := "/r/demo/boards:" + name - board := newBoard(bid, url, name, caller) - gBoards.Set(bid.Key(), board) + id := incGetBoardID() + url := "/r/demo/boards:" + name // TODO: Generate URL from current realm + board := newBoard(id, url, name, caller) + gBoardsByID.Set(id.Key(), board) gBoardsByName.Set(name, board) return board.id } diff --git a/examples/gno.land/r/demo/boards2/render.gno b/examples/gno.land/r/demo/boards2/render.gno index 0258e266508..21daee6efa9 100644 --- a/examples/gno.land/r/demo/boards2/render.gno +++ b/examples/gno.land/r/demo/boards2/render.gno @@ -22,7 +22,7 @@ func Render(path string) string { func renderBoardsList(res *mux.ResponseWriter, _ *mux.Request) { res.Write("These are all the boards of this realm:\n\n") - gBoards.Iterate("", "", func(_ string, value interface{}) bool { + gBoardsByID.Iterate("", "", func(_ string, value interface{}) bool { board := value.(*Board) res.Write(" * " + newLink(board.url, board.url) + "\n") return false From 8c02d71d9bf1206e2c335721dc6032e67ee2fa8a Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Wed, 13 Nov 2024 13:35:55 +0100 Subject: [PATCH 21/21] refactor: change board URL to be dynamic THis changeset removes the board url field and uses standard library functions to generate the relative board URL instead of using a hard-coded one. --- examples/gno.land/r/demo/boards2/board.gno | 14 +++++++++----- examples/gno.land/r/demo/boards2/public.gno | 3 +-- examples/gno.land/r/demo/boards2/render.gno | 3 ++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index e464c1d611b..0df39f7ebfb 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -4,6 +4,7 @@ import ( "regexp" "std" "strconv" + "strings" "time" "gno.land/p/demo/avl" @@ -24,7 +25,6 @@ func (id BoardID) Key() string { type Board struct { id BoardID // only set for public boards. - url string name string creator std.Address threads avl.Tree // Post.id -> *Post @@ -33,7 +33,7 @@ type Board struct { deleted avl.Tree // TODO reserved for fast-delete. } -func newBoard(id BoardID, url string, name string, creator std.Address) *Board { +func newBoard(id BoardID, name string, creator std.Address) *Board { assertIsBoardName(name) if gBoardsByName.Has(name) { @@ -42,7 +42,6 @@ func newBoard(id BoardID, url string, name string, creator std.Address) *Board { return &Board{ id: id, - url: url, name: name, creator: creator, threads: avl.Tree{}, @@ -65,6 +64,11 @@ func (board *Board) IsPrivate() bool { return board.id == 0 } +// GetURL returns the relative URL of the board. +func (board *Board) GetURL() string { + return strings.TrimPrefix(std.CurrentRealm().PkgPath(), "gno.land") + ":" + board.name +} + func (board *Board) GetThread(threadID PostID) (_ *Post, found bool) { v, found := board.threads.Get(threadID.Key()) if !found { @@ -122,11 +126,11 @@ func (board *Board) incGetPostID() PostID { } func (board *Board) GetURLFromThreadID(threadID PostID) string { - return board.url + "/" + threadID.String() + return board.GetURL() + "/" + threadID.String() } func (board *Board) GetURLFromReplyID(threadID, replyID PostID) string { - return board.url + "/" + threadID.String() + "/" + replyID.String() + return board.GetURL() + "/" + threadID.String() + "/" + replyID.String() } func (board *Board) GetPostFormURL() string { diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 3999401025c..cb1a4b5eef6 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -21,8 +21,7 @@ func CreateBoard(name string) BoardID { assertIsNotAnonymousCaller(caller) id := incGetBoardID() - url := "/r/demo/boards:" + name // TODO: Generate URL from current realm - board := newBoard(id, url, name, caller) + board := newBoard(id, name, caller) gBoardsByID.Set(id.Key(), board) gBoardsByName.Set(name, board) return board.id diff --git a/examples/gno.land/r/demo/boards2/render.gno b/examples/gno.land/r/demo/boards2/render.gno index 21daee6efa9..bd9e8676c24 100644 --- a/examples/gno.land/r/demo/boards2/render.gno +++ b/examples/gno.land/r/demo/boards2/render.gno @@ -24,7 +24,8 @@ func renderBoardsList(res *mux.ResponseWriter, _ *mux.Request) { res.Write("These are all the boards of this realm:\n\n") gBoardsByID.Iterate("", "", func(_ string, value interface{}) bool { board := value.(*Board) - res.Write(" * " + newLink(board.url, board.url) + "\n") + url := board.GetURL() + res.Write(" * " + newLink(url, url) + "\n") return false }) }