diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index 79b27da84b2..0df39f7ebfb 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -1,26 +1,30 @@ package boards import ( + "regexp" "std" "strconv" + "strings" "time" "gno.land/p/demo/avl" "gno.land/p/moul/txlink" ) -//---------------------------------------- -// Board +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 { id BoardID // only set for public boards. - url string name string creator std.Address threads avl.Tree // Post.id -> *Post @@ -29,17 +33,15 @@ type Board struct { deleted avl.Tree // TODO reserved for fast-delete. } -func newBoard(id BoardID, url string, name string, creator std.Address) *Board { - if !reName.MatchString(name) { - panic("invalid name: " + name) - } - exists := gBoardsByName.Has(name) - if exists { +func newBoard(id BoardID, name string, creator std.Address) *Board { + assertIsBoardName(name) + + if gBoardsByName.Has(name) { panic("board already exists") } + return &Board{ id: id, - url: url, name: name, creator: creator, threads: avl.Tree{}, @@ -62,39 +64,42 @@ func (board *Board) IsPrivate() bool { return board.id == 0 } -func (board *Board) GetThread(pid PostID) *Post { - pidkey := postIDKey(pid) - postI, exists := board.threads.Get(pidkey) - if !exists { - return nil +// 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 { + return nil, false } - return postI.(*Post) + return v.(*Post), true } 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()) } } +// 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 { - case EditPermission: + case PermissionEdit: return true - case DeletePermission: + case PermissionDelete: return true default: return false @@ -103,22 +108,16 @@ 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 { - str := "" - str += "\\[[post](" + board.GetPostFormURL() + ")]\n\n" +func (board *Board) Render() string { + s := "\\[" + newLink("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 { @@ -126,14 +125,20 @@ 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() - } else { - return board.url + "/" + threadID.String() + "/" + replyID.String() - } +func (board *Board) GetURLFromThreadID(threadID PostID) string { + return board.GetURL() + "/" + threadID.String() +} + +func (board *Board) GetURLFromReplyID(threadID, replyID PostID) string { + return board.GetURL() + "/" + threadID.String() + "/" + replyID.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..fa487a3914a 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -1,22 +1,54 @@ package boards -import ( - "regexp" +import "gno.land/p/demo/avl" - "gno.land/p/demo/avl" -) - -//---------------------------------------- -// Realm (package) state +// Default minimum fee in ugnot required for anonymous users +const defaultAnonymousFee = 100_000_000 var ( - gBoards avl.Tree // id -> *Board - gBoardsCtr int // increments Board.id - gBoardsByName avl.Tree // name -> *Board - gDefaultAnonFee = 100000000 // minimum fee required if anonymous + gLastBoardID BoardID + gBoardsByID avl.Tree // string(id) -> *Board + gBoardsByName avl.Tree // string(name) -> *Board ) -//---------------------------------------- -// Constants +// 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 := gBoardsByID.Get(id.Key()) + if !exists { + return nil, false + } + 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 +} + +// 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 +} -var reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{2,29}$`) +// mustGetReply returns a reply or panics when it's not found. +func mustGetReply(thread *Post, replyID PostID) *Post { + 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/format.gno b/examples/gno.land/r/demo/boards2/format.gno new file mode 100644 index 00000000000..da29918fdae --- /dev/null +++ b/examples/gno.land/r/demo/boards2/format.gno @@ -0,0 +1,65 @@ +package boards + +import ( + "std" + "strconv" + "strings" + + "gno.land/r/demo/users" +) + +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 { + 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 { + var ( + res string + lines = strings.Split(body, "\n") + ) + for i, line := range lines { + if i > 0 { + res += "\n" + } + res += indent + line + } + return res +} + +// NOTE: length must be greater than 3. +func summaryOf(text string, length int) string { + lines := strings.SplitN(text, "\n", 2) + line := lines[0] + if len(line) > length { + line = line[:(length-3)] + "..." + } else if len(lines) > 1 { + // len(line) <= 80 + line = line + "..." + } + return line +} + +// 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 newLink(addr.String(), "/r/demo/users:"+addr.String()) + } + return newLink("@"+user.Name, "/r/demo/users:"+user.Name) +} diff --git a/examples/gno.land/r/demo/boards2/misc.gno b/examples/gno.land/r/demo/boards2/misc.gno deleted file mode 100644 index bc561ca7d22..00000000000 --- a/examples/gno.land/r/demo/boards2/misc.gno +++ /dev/null @@ -1,95 +0,0 @@ -package boards - -import ( - "std" - "strconv" - "strings" - - "gno.land/r/demo/users" -) - -//---------------------------------------- -// private utility methods -// XXX ensure these cannot be called from public. - -func getBoard(bid BoardID) *Board { - bidkey := boardIDKey(bid) - board_, exists := gBoards.Get(bidkey) - if !exists { - return nil - } - board := board_.(*Board) - return board -} - -func incGetBoardID() BoardID { - gBoardsCtr++ - 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 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 - } -} - -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 := "" - for i, line := range lines { - if i > 0 { - res += "\n" - } - res += indent + line - } - return res -} - -// NOTE: length must be greater than 3. -func summaryOf(str string, length int) string { - lines := strings.SplitN(str, "\n", 2) - line := lines[0] - if len(line) > length { - line = line[:(length-3)] + "..." - } else if len(lines) > 1 { - // len(line) <= 80 - line = line + "..." - } - return line -} - -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 + ")" - } -} - -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/post.gno b/examples/gno.land/r/demo/boards2/post.gno index 95d4b2977ba..5f9ceae2f5e 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -9,14 +9,16 @@ import ( "gno.land/p/moul/txlink" ) -//---------------------------------------- -// Post +const dateFormat = "2006-01-02 3:04pm MST" -// 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. @@ -65,14 +67,15 @@ func (post *Post) GetPostID() PostID { 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, _ := board.GetThread(post.threadID) + thread.repliesAll.Set(pKey, reply) } return reply } @@ -83,55 +86,55 @@ func (post *Post) Update(title string, body string) { post.updatedAt = time.Now() } -func (thread *Post) GetReply(pid PostID) *Post { - pidkey := postIDKey(pid) - replyI, ok := thread.repliesAll.Get(pidkey) - if !ok { - return nil - } else { - return replyI.(*Post) +func (thread *Post) GetReply(pid PostID) (_ *Post, found bool) { + v, found := thread.repliesAll.Get(pid.Key()) + if !found { + return nil, false } + return v.(*Post), true } func (post *Post) AddRepostTo(creator std.Address, title, body string, dst *Board) *Post { if !post.IsThread() { 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 } -func (thread *Post) DeletePost(pid PostID) { - if thread.id == pid { +func (thread *Post) DeleteReply(replyID PostID) { + if thread.id == replyID { panic("should not happen") } - pidkey := postIDKey(pid) - postI, removed := thread.repliesAll.Remove(pidkey) + + key := replyID.Key() + v, removed := thread.repliesAll.Remove(key) if !removed { - panic("post not found in thread") + panic("reply 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, _ := thread.GetReply(post.parentID) + parent.replies.Remove(key) } else { - thread.replies.Remove(pidkey) + thread.replies.Remove(key) } } +// 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 { - case EditPermission: + case PermissionEdit: return true - case DeletePermission: + case PermissionDelete: return true default: return false @@ -147,12 +150,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.GetURLFromThreadID(post.id) } + return post.board.GetURLFromReplyID(post.threadID, post.id) } func (post *Post) GetReplyFormURL() string { @@ -171,93 +171,110 @@ 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(), ) } 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") } - 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() } - str := "" + + var ( + s string + postURL = post.GetURL() + ) + if post.title != "" { - str += "## [" + summaryOf(post.title, 80) + "](" + post.GetURL() + ")\n" - str += "\n" + s += "## " + newLink(summaryOf(post.title, 80), postURL) + "\n\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 += "\\- " + 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 } -func (post *Post) RenderPost(indent string, levels int) string { +func (post *Post) Render(indent string, levels int) string { if post == nil { return "nil post" } - str := "" + + var ( + s string + postURL = post.GetURL() + ) + 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 + "\\- " + newUserLink(post.creator) + ", " + s += newLink(post.createdAt.Format(dateFormat), postURL) + s += " \\[" + newLink("reply", post.GetReplyFormURL()) + "]" if post.IsThread() { - str += " \\[[repost](" + post.GetRepostFormURL() + ")]" + s += " \\[" + newLink("repost", post.GetRepostFormURL()) + "]" } - str += " \\[[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 { - str += indent + "\n" - str += value.(*Post).RenderPost(indent+"> ", levels-1) + post.replies.Iterate("", "", func(_ string, value interface{}) bool { + s += indent + "\n" + s += value.(*Post).Render(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" - } + } else if post.replies.Size() > 0 { + s += indent + "\n" + s += indent + "_" + newLink("see all "+strconv.Itoa(post.replies.Size())+" replies", postURL) + "_\n" } - return str + 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 - str := "" - str += "_[see thread](" + post.board.GetURLFromThreadAndReplyID( - threadID, 0) + ")_\n\n" - thread := post.board.GetThread(post.threadID) - var parent *Post + + var ( + parent *Post + parentID = post.parentID + threadID = post.threadID + thread, _ = post.board.GetThread(threadID) // TODO: This seems redundant (post == thread) + ) + if thread.id == parentID { parent = thread } else { - parent = thread.GetReply(parentID) + parent, _ = thread.GetReply(parentID) } - str += parent.RenderPost("", 0) - str += "\n" - str += post.RenderPost("> ", 5) - return str + + s := "_" + newLink("see thread", post.board.GetURLFromThreadID(threadID)) + "_\n\n" + s += parent.Render("", 0) + "\n" + s += post.Render("> ", 5) + return s } diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 1d26126fcb2..cb1a4b5eef6 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -2,184 +2,160 @@ package boards import ( "std" - "strconv" -) -//---------------------------------------- -// Public facing functions + "gno.land/r/demo/users" +) 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() + assertIsUserCall() + caller := std.GetOrigCaller() - if usernameOf(caller) == "" { - panic("unauthorized") - } - url := "/r/demo/boards:" + name - board := newBoard(bid, url, name, caller) - bidkey := boardIDKey(bid) - gBoards.Set(bidkey, board) + assertIsNotAnonymousCaller(caller) + + id := incGetBoardID() + board := newBoard(id, name, caller) + gBoardsByID.Set(id.Key(), board) gBoardsByName.Set(name, board) return board.id } -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") - } + assertIsUserCall() + 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") - } + assertAnonymousCallerFeeReceived(caller) + + board := mustGetBoard(bid) thread := board.AddThread(caller, title, body) return thread.id } -func CreateReply(bid BoardID, threadid, postid PostID, body string) PostID { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { - panic("invalid non-user call") - } +func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { + assertIsUserCall() + 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.GetThread(threadid) - if thread == nil { - panic("thread not exist") - } - if postid == threadid { - reply := thread.AddReply(caller, body) - return reply.id + assertAnonymousCallerFeeReceived(caller) + + var ( + reply *Post + board = mustGetBoard(bid) + thread = mustGetThread(board, threadID) + ) + + if replyID == threadID { + reply = thread.AddReply(caller, body) } else { - post := thread.GetReply(postid) - reply := post.AddReply(caller, body) - return reply.id + post := mustGetReply(thread, 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 { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { - panic("invalid non-user call") - } +func CreateRepost(bid BoardID, threadID PostID, title string, body string, dstBoardID BoardID) PostID { + assertIsUserCall() + 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") - } - } - board := getBoard(bid) - if board == nil { - panic("src board not exist") - } + assertAnonymousCallerFeeReceived(caller) + + board := mustGetBoard(bid) 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") - } + + dst := mustGetBoard(dstBoardID) + thread := mustGetThread(board, threadID) repost := thread.AddRepostTo(caller, title, body, dst) return repost.id } -func DeletePost(bid BoardID, threadid, postid PostID, reason string) { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { - panic("invalid non-user call") - } +func DeleteThread(bid BoardID, threadID PostID, reason string) { + assertIsUserCall() + + board := mustGetBoard(bid) + thread := mustGetThread(board, threadID) + 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) { - 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, DeletePermission) { - panic("unauthorized") - } - thread.DeletePost(postid) - } + assertUserHasPermission(thread, caller, PermissionDelete) + + board.DeleteThread(threadID) +} + +func DeleteReply(bid BoardID, threadID, replyID PostID, reason string) { + assertIsUserCall() + + board := mustGetBoard(bid) + thread := mustGetThread(board, threadID) + reply := mustGetReply(thread, replyID) + + caller := std.GetOrigCaller() + assertUserHasPermission(reply, caller, PermissionDelete) + + thread.DeleteReply(replyID) +} + +func EditThread(bid BoardID, threadID PostID, title, body string) { + assertIsUserCall() + + board := mustGetBoard(bid) + thread := mustGetThread(board, threadID) + + caller := std.GetOrigCaller() + assertUserHasPermission(thread, caller, PermissionEdit) + + thread.Update(title, body) } -func EditPost(bid BoardID, threadid, postid PostID, title, body string) { +func EditReply(bid BoardID, threadID, replyID PostID, title, body string) { + assertIsUserCall() + + board := mustGetBoard(bid) + thread := mustGetThread(board, threadID) + post := mustGetReply(thread, replyID) + + caller := std.GetOrigCaller() + assertUserHasPermission(post, caller, PermissionEdit) + + post.Update(title, body) +} + +func assertIsUserCall() { if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { panic("invalid non-user call") } - caller := std.GetOrigCaller() - board := getBoard(bid) - if board == nil { - panic("board not exist") +} + +func assertIsNotAnonymousCaller(caller std.Address) { + // Caller is anonymous if doesn't have a registered user name + if users.GetUserByAddress(caller) == nil { + panic("unauthorized") } - thread := board.GetThread(threadid) - if thread == nil { - panic("thread not exist") +} + +func assertAnonymousFeeReceived() { + 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") } - if postid == threadid { - // edit thread - if !thread.HasPermission(caller, EditPermission) { - 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, EditPermission) { - panic("unauthorized") - } - post.Update(title, body) + return +} + +func assertAnonymousCallerFeeReceived(caller std.Address) { + if users.GetUserByAddress(caller) == nil { + assertAnonymousFeeReceived() + } +} + +func assertUserHasPermission(post *Post, user std.Address, p Permission) { + if !post.HasPermission(user, p) { + panic("unauthorized") } } diff --git a/examples/gno.land/r/demo/boards2/render.gno b/examples/gno.land/r/demo/boards2/render.gno index 3709ad02e5d..bd9e8676c24 100644 --- a/examples/gno.land/r/demo/boards2/render.gno +++ b/examples/gno.land/r/demo/boards2/render.gno @@ -2,82 +2,102 @@ 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) -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 { - if path == "" { - str := "These are all the boards of this realm:\n\n" - gBoards.Iterate("", "", func(key string, value interface{}) bool { - board := value.(*Board) - str += " * [" + board.url + "](" + board.url + ")\n" - return false - }) - return str +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) + url := board.GetURL() + res.Write(" * " + newLink(url, 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.Render()) } - parts := strings.Split(path, "/") - if len(parts) == 1 { - // /r/demo/boards:BOARD_NAME - name := parts[0] - boardI, exists := gBoardsByName.Get(name) - if !exists { - return "board does not exist: " + name - } - return boardI.(*Board).RenderBoard() - } else if len(parts) == 2 { - // /r/demo/boards:BOARD_NAME/THREAD_ID - name := parts[0] - boardI, exists := gBoardsByName.Get(name) - if !exists { - return "board does not exist: " + name - } - pid, err := strconv.Atoi(parts[1]) - if err != nil { - return "invalid thread id: " + parts[1] - } - board := boardI.(*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] - boardI, exists := gBoardsByName.Get(name) - if !exists { - return "board does not exist: " + name - } - pid, err := strconv.Atoi(parts[1]) - if err != nil { - return "invalid thread id: " + parts[1] - } - board := boardI.(*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] - } - return reply.RenderInner() +} + +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 + } + + rawID := req.GetVar("thread") + tID, err := strconv.Atoi(rawID) + if err != nil { + res.Write("Invalid thread ID: " + rawID) + return + } + + board := v.(*Board) + 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)) + } +} + +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 + } + + rawID := req.GetVar("thread") + tID, err := strconv.Atoi(rawID) + if err != nil { + res.Write("Invalid thread ID: " + rawID) + return + } + + rawID = req.GetVar("reply") + rID, err := strconv.Atoi(rawID) + if err != nil { + res.Write("Invalid reply ID: " + rawID) + return + } + + board := v.(*Board) + thread, found := board.GetThread(PostID(tID)) + if !found { + res.Write("Thread does not exist with ID: " + req.GetVar("thread")) + return + } + + reply, found := thread.GetReply(PostID(rID)) + if !found { + res.Write("Reply does not exist with ID: " + req.GetVar("reply")) } else { - return "unrecognized path " + path + res.Write(reply.RenderInner()) } } 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" )