From 71c93a70faf5dffdcf22fad710636e877b55f4a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lker=20G=2E=20=C3=96zt=C3=BCrk?= Date: Fri, 4 Oct 2024 14:23:51 +0300 Subject: [PATCH 01/52] feat(boardsv2): init (#2900) --- examples/gno.land/p/demo/boardsv2/boardsv2.gno | 1 + examples/gno.land/p/demo/boardsv2/post/avlstorage/avlstorage.gno | 1 + examples/gno.land/p/demo/boardsv2/post/plugin.gno | 1 + examples/gno.land/p/demo/boardsv2/post/post.gno | 1 + examples/gno.land/p/demo/boardsv2/post/view.gno | 1 + examples/gno.land/r/demo/boardsv2/boardsv2.gno | 1 + 6 files changed, 6 insertions(+) create mode 100644 examples/gno.land/p/demo/boardsv2/boardsv2.gno create mode 100644 examples/gno.land/p/demo/boardsv2/post/avlstorage/avlstorage.gno create mode 100644 examples/gno.land/p/demo/boardsv2/post/plugin.gno create mode 100644 examples/gno.land/p/demo/boardsv2/post/post.gno create mode 100644 examples/gno.land/p/demo/boardsv2/post/view.gno create mode 100644 examples/gno.land/r/demo/boardsv2/boardsv2.gno diff --git a/examples/gno.land/p/demo/boardsv2/boardsv2.gno b/examples/gno.land/p/demo/boardsv2/boardsv2.gno new file mode 100644 index 00000000000..4ad3d466272 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/boardsv2.gno @@ -0,0 +1 @@ +package boardsv2 \ No newline at end of file diff --git a/examples/gno.land/p/demo/boardsv2/post/avlstorage/avlstorage.gno b/examples/gno.land/p/demo/boardsv2/post/avlstorage/avlstorage.gno new file mode 100644 index 00000000000..c1f6c226c20 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/post/avlstorage/avlstorage.gno @@ -0,0 +1 @@ +package avlstorage \ No newline at end of file diff --git a/examples/gno.land/p/demo/boardsv2/post/plugin.gno b/examples/gno.land/p/demo/boardsv2/post/plugin.gno new file mode 100644 index 00000000000..54be1f50a86 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/post/plugin.gno @@ -0,0 +1 @@ +package post \ No newline at end of file diff --git a/examples/gno.land/p/demo/boardsv2/post/post.gno b/examples/gno.land/p/demo/boardsv2/post/post.gno new file mode 100644 index 00000000000..54be1f50a86 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/post/post.gno @@ -0,0 +1 @@ +package post \ No newline at end of file diff --git a/examples/gno.land/p/demo/boardsv2/post/view.gno b/examples/gno.land/p/demo/boardsv2/post/view.gno new file mode 100644 index 00000000000..54be1f50a86 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/post/view.gno @@ -0,0 +1 @@ +package post \ No newline at end of file diff --git a/examples/gno.land/r/demo/boardsv2/boardsv2.gno b/examples/gno.land/r/demo/boardsv2/boardsv2.gno new file mode 100644 index 00000000000..4ad3d466272 --- /dev/null +++ b/examples/gno.land/r/demo/boardsv2/boardsv2.gno @@ -0,0 +1 @@ +package boardsv2 \ No newline at end of file From 1a88d583903354a5aff31deb414360fcdece4ea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Mon, 4 Nov 2024 17:50:52 +0100 Subject: [PATCH 02/52] feat(boardsv2): experiment API - WIP (#2902) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Boards v2 package and realm API experimentation
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Co-authored-by: İlker G. Öztürk Co-authored-by: Denys Sedchenko --- .../gno.land/p/demo/boardsv2/boardsv2.gno | 1 - .../gno.land/p/demo/boardsv2/draft0/app.gno | 64 +++++++++ .../gno.land/p/demo/boardsv2/draft0/board.gno | 24 ++++ .../p/demo/boardsv2/draft0/boardsv2.gno | 7 + .../p/demo/boardsv2/draft0/option.gno | 13 ++ .../post/avlstorage/avlstorage.gno | 0 .../p/demo/boardsv2/draft0/post/content.gno | 5 + .../boardsv2/{ => draft0}/post/plugin.gno | 0 .../draft0/post/plugins/content/content.gno | 30 ++++ .../p/demo/boardsv2/draft0/post/post.gno | 90 ++++++++++++ .../p/demo/boardsv2/draft0/post/storage.gno | 4 + .../p/demo/boardsv2/draft0/post/view.gno | 10 ++ .../p/demo/boardsv2/draft0/thread.gno | 30 ++++ .../p/demo/boardsv2/draft1/boards.gno | 69 +++++++++ .../p/demo/boardsv2/draft1/content_board.gno | 30 ++++ .../demo/boardsv2/draft1/content_comment.gno | 27 ++++ .../p/demo/boardsv2/draft1/content_poll.gno | 33 +++++ .../p/demo/boardsv2/draft1/content_post.gno | 29 ++++ .../p/demo/boardsv2/draft1/features.gno | 69 +++++++++ .../draft1/plugin/locking/locking.gno | 50 +++++++ .../draft1/plugin/reputation/options.gno | 15 ++ .../draft1/plugin/reputation/reputation.gno | 77 ++++++++++ .../gno.land/p/demo/boardsv2/draft1/post.gno | 45 ++++++ .../gno.land/p/demo/boardsv2/draft1/store.gno | 9 ++ .../gno.land/p/demo/boardsv2/draft2/app.gno | 91 ++++++++++++ .../gno.land/p/demo/boardsv2/draft2/board.gno | 14 ++ .../p/demo/boardsv2/draft2/comment.gno | 8 ++ .../p/demo/boardsv2/draft2/context.gno | 38 +++++ .../p/demo/boardsv2/draft2/option.gno | 19 +++ .../p/demo/boardsv2/draft2/post/cursor.gno | 8 ++ .../draft2/post/plugin/comment/comment.gno | 34 +++++ .../boardsv2/draft2/post/plugin/lock/lock.gno | 1 + .../boardsv2/draft2/post/plugin/plugin.gno | 7 + .../boardsv2/draft2/post/plugin/poll/poll.gno | 1 + .../post/plugin/reputation/reputation.gno | 1 + .../boardsv2/draft2/post/plugin/text/text.gno | 3 + .../draft2/post/plugin/title/title.gno | 24 ++++ .../p/demo/boardsv2/draft2/post/post.gno | 26 ++++ .../demo/boardsv2/draft2/post/store/store.gno | 1 + .../p/demo/boardsv2/draft2/thread.gno | 18 +++ .../gno.land/p/demo/boardsv2/draft3/app.gno | 133 ++++++++++++++++++ .../gno.land/p/demo/boardsv2/draft3/board.gno | 52 +++++++ .../p/demo/boardsv2/draft3/comment.gno | 8 ++ .../p/demo/boardsv2/draft3/context.gno | 36 +++++ .../p/demo/boardsv2/draft3/options.gno | 31 ++++ .../draft3/post/plugin/comment/comment.gno | 50 +++++++ .../boardsv2/draft3/post/plugin/fork/fork.gno | 47 +++++++ .../draft3/post/plugin/fork/options.gno | 9 ++ .../boardsv2/draft3/post/plugin/lock/lock.gno | 55 ++++++++ .../boardsv2/draft3/post/plugin/plugin.gno | 48 +++++++ .../boardsv2/draft3/post/plugin/poll/poll.gno | 47 +++++++ .../draft3/post/plugin/reputation/options.gno | 15 ++ .../post/plugin/reputation/reputation.gno | 99 +++++++++++++ .../draft3/post/plugin/reputation/store.gno | 58 ++++++++ .../boardsv2/draft3/post/plugin/text/text.gno | 38 +++++ .../draft3/post/plugin/title/title.gno | 35 +++++ .../p/demo/boardsv2/draft3/post/post.gno | 25 ++++ .../p/demo/boardsv2/draft3/post/store.gno | 31 ++++ .../p/demo/boardsv2/draft3/store/cursor.gno | 9 ++ .../p/demo/boardsv2/draft3/store/store.gno | 6 + .../p/demo/boardsv2/draft3/thread.gno | 64 +++++++++ .../gno.land/p/demo/boardsv2/post/post.gno | 1 - .../gno.land/p/demo/boardsv2/post/view.gno | 1 - .../gno.land/r/demo/boardsv2/boardsv2.gno | 45 +++++- .../gno.land/r/demo/boardsv2/draft2/main.gno | 15 ++ .../r/demo/boardsv2/draft3/boards.gno | 68 +++++++++ 66 files changed, 2047 insertions(+), 4 deletions(-) delete mode 100644 examples/gno.land/p/demo/boardsv2/boardsv2.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft0/app.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft0/board.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft0/boardsv2.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft0/option.gno rename examples/gno.land/p/demo/boardsv2/{ => draft0}/post/avlstorage/avlstorage.gno (100%) create mode 100644 examples/gno.land/p/demo/boardsv2/draft0/post/content.gno rename examples/gno.land/p/demo/boardsv2/{ => draft0}/post/plugin.gno (100%) create mode 100644 examples/gno.land/p/demo/boardsv2/draft0/post/plugins/content/content.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft0/post/post.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft0/post/storage.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft0/post/view.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft0/thread.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft1/boards.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft1/content_board.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft1/content_comment.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft1/content_poll.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft1/content_post.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft1/features.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft1/plugin/locking/locking.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/options.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/reputation.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft1/post.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft1/store.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft2/app.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft2/board.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft2/comment.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft2/context.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft2/option.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/cursor.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/plugin/comment/comment.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/plugin/lock/lock.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/plugin/plugin.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/plugin/poll/poll.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/plugin/reputation/reputation.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/plugin/text/text.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/plugin/title/title.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/post.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/store/store.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft2/thread.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/app.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/board.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/comment.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/context.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/options.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/comment/comment.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/fork.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/options.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/lock/lock.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/plugin.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/poll/poll.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/options.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/reputation.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/store.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/text/text.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/title/title.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/post.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/store.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/store/cursor.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/store/store.gno create mode 100644 examples/gno.land/p/demo/boardsv2/draft3/thread.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/post/post.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/post/view.gno create mode 100644 examples/gno.land/r/demo/boardsv2/draft2/main.gno create mode 100644 examples/gno.land/r/demo/boardsv2/draft3/boards.gno diff --git a/examples/gno.land/p/demo/boardsv2/boardsv2.gno b/examples/gno.land/p/demo/boardsv2/boardsv2.gno deleted file mode 100644 index 4ad3d466272..00000000000 --- a/examples/gno.land/p/demo/boardsv2/boardsv2.gno +++ /dev/null @@ -1 +0,0 @@ -package boardsv2 \ No newline at end of file diff --git a/examples/gno.land/p/demo/boardsv2/draft0/app.gno b/examples/gno.land/p/demo/boardsv2/draft0/app.gno new file mode 100644 index 00000000000..28017f895c8 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/app.gno @@ -0,0 +1,64 @@ +package boardsv2 + +import ( + "gno.land/demo/p/boardsv2/post" + contentplugin "gno.land/demo/p/boardsv2/post/plugins/content" +) + +// type Rating struct{} +// +// var ratingIndex = avl.Tree{} +// app.AddBoardHook(func (changeType int, change ChangeSet) { +// if changeType == 0 { +// ratingIndex.Set("...", ) +// } +// }) + +type App struct { + st Storage + boards []Board +} + +func New(s Storage, o ...Option) App { + a := App{ + st: Storage, + } + // Define the rule for a spesific view. + boardsView := view.New(view.Filter{ + Level: 0, // this will give me the list of the boards. + }) + + return a +} + +func (a *App) AddBoard(name, title, description string) (*Board, error) { + p := post.New(contentplugin.TitleBasedContent{ + Title: title, + Description: description, + }) + + // I want to create a query for listing threads under this new board. + threadView := view.New(view.Filter{ + Level: 1, + SlugPrefix: name, + }) + userActivityView := view.New(view.Filter{ + LevelGte: 2, + By: func(content Content) []View { + c.Author // by account address + } + }) + + if err := post.Add(a.st, name, p); err != nil { + nil, err + } + return a.GetBoard(name), nil +} + +func (a *App) GetBoard(name string) (board *Board, found bool) { + +} + +func (a *App) ListBoards() ([]*Board, error) { + +} diff --git a/examples/gno.land/p/demo/boardsv2/draft0/board.gno b/examples/gno.land/p/demo/boardsv2/draft0/board.gno new file mode 100644 index 00000000000..e56895a9b1d --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/board.gno @@ -0,0 +1,24 @@ +package boardsv2 + +type Board struct { +} + +func (b *Board) AddPost() error { + +} + +func (b *Board) GetThread(id string) (post *Post, found bool) { + +} + +func (b *Board) ListThreads(id string) (post *Post, found bool) { + threadView.List() // there should be an iterator, pagination +} + +func (b *Board) Fork() error { + +} + +func (b *Board) Lock() error { + +} diff --git a/examples/gno.land/p/demo/boardsv2/draft0/boardsv2.gno b/examples/gno.land/p/demo/boardsv2/draft0/boardsv2.gno new file mode 100644 index 00000000000..91c308f9576 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/boardsv2.gno @@ -0,0 +1,7 @@ +// boardsv2 is a reddit like abstraction around post/*. +// You might implement other abstractions around post/* to create +// different type of dApps. +// refer to the app.gno file to get started. +package boardsv2 + + diff --git a/examples/gno.land/p/demo/boardsv2/draft0/option.gno b/examples/gno.land/p/demo/boardsv2/draft0/option.gno new file mode 100644 index 00000000000..b8d7b65c643 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/option.gno @@ -0,0 +1,13 @@ +package boardsv2 + +type Option struct{} + +// LinearReputationPolicy allows upvoting or downvoting a post by one +// for each account. +func LinearReputationPolicy() Option {} + +// TokenBasedReputationPolicy allows upvoting or downvoting a post propotional +// to the specified tokens that an account holds. +func TokenBasedReputationPolicy() Option {} + +// TODO: make it configurable how many levels allowed diff --git a/examples/gno.land/p/demo/boardsv2/post/avlstorage/avlstorage.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/avlstorage/avlstorage.gno similarity index 100% rename from examples/gno.land/p/demo/boardsv2/post/avlstorage/avlstorage.gno rename to examples/gno.land/p/demo/boardsv2/draft0/post/avlstorage/avlstorage.gno diff --git a/examples/gno.land/p/demo/boardsv2/draft0/post/content.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/content.gno new file mode 100644 index 00000000000..cbfd45c36fe --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/post/content.gno @@ -0,0 +1,5 @@ +package post + +type Content interface { + Render() string +} diff --git a/examples/gno.land/p/demo/boardsv2/post/plugin.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/plugin.gno similarity index 100% rename from examples/gno.land/p/demo/boardsv2/post/plugin.gno rename to examples/gno.land/p/demo/boardsv2/draft0/post/plugin.gno diff --git a/examples/gno.land/p/demo/boardsv2/draft0/post/plugins/content/content.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/plugins/content/content.gno new file mode 100644 index 00000000000..05043e7e172 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/post/plugins/content/content.gno @@ -0,0 +1,30 @@ +package commentplugin + +type CommentContent struct { + Body string +} + +func (c CommentContent) Render() string { + +} + +type TitleBasedContent struct{} + +type TextContent struct { + Title string + Body string + Tags []string +} + +func (c TextContent) Render() string { + +} + +type PollContent struct { + Question string + Options []string + Votes []struct { + Address std.Adress + Option string + } +} diff --git a/examples/gno.land/p/demo/boardsv2/draft0/post/post.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/post.gno new file mode 100644 index 00000000000..d103579d437 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/post/post.gno @@ -0,0 +1,90 @@ +package post + +import "time" + +/* +alicePost = &Post { content: "foo" } (0x001) +bobFork := &Post { Origial: alicePost (0x001) } + +//1. Check gc behavior in realm for forks + +--- +alicePost := &(*alicePost) (0x002) +alicePost.content = "new content" + +bobFork := &Post { Origial: uintptr(0x001) } +--- +type Post struct { + ID int + Level int +} + +package reddit + +// explore with plugins +// - boardsv2 +// - pkg/general +// - pkg/reddit +var ( + rating avl.Tree +) + +genericPost := Post{} +reddit.UpvotePost(genericPost.ID) +*/ + +// Blog example +// Home +// - post 1 (content: title, body, author, label, timestamp) +// - post 1.1 (body, author) (thread) +// - post 1.1.1 (comment to a thread but also a new thread) +// - post 1.1.1.1 +// - post 1.2 (thread) +// +// - post 2 +// - post 3 +// +// Reddit example +// Home +// - post 1 (title, body) (board) +// - post 1.1 (title, body) (sub-board) +// - post 1.1.1 (title, body, label) +// - post 1.1.1.1 (comment, thread) +type Post struct { + ID string + Content Content // title, body, label, author, other metadata... + Level int + Base *Post + Children []*Post + Forks []*Post + UpdatedAt time.Time + CreatedAt time.Time // the time when created by user or forked. + Creator std.Address +} + +// create plugins for Post type < +// upvoting < implement first plugin +// define public API for plugin, post packages and boardsv2 +// moderation +// +// plugin ideas: +// - visibility +// - upcoting +// - acess control > you shouldn't be able to answer to the boards yo're not invited +// - moedaration (ban certain posts -this could be through a dao in the future) + +func New(s Storage) Post { + +} + +func Create(c Content) *Post { + +} + +func (p *Post) NextIncrementalKey(base string) string { + +} + +// func (p *Post) Append() error { +// +// } diff --git a/examples/gno.land/p/demo/boardsv2/draft0/post/storage.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/storage.gno new file mode 100644 index 00000000000..49a5f7eef32 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/post/storage.gno @@ -0,0 +1,4 @@ +package post + +type Storage interface { +} diff --git a/examples/gno.land/p/demo/boardsv2/draft0/post/view.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/view.gno new file mode 100644 index 00000000000..3921e441039 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/post/view.gno @@ -0,0 +1,10 @@ +package post + +// Two cases to solve +// - Give me a list of boards (board list page) +// - Give me a list of comments, created by a user accross all boards (user activity page, of a user) +type View interface { + Name() string + Size() int + Iterate(start, end string, fn func(key string, v interface{}) bool) bool +} diff --git a/examples/gno.land/p/demo/boardsv2/draft0/thread.gno b/examples/gno.land/p/demo/boardsv2/draft0/thread.gno new file mode 100644 index 00000000000..0ecad4ae41d --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/thread.gno @@ -0,0 +1,30 @@ +package boardsv2 + +import ( + "gno.land/demo/p/boardsv2/post" + replyplugin "gno.land/demo/p/boardsv2/post/plugins/content/reply" +) + +type Thread struct { + post post.Post + st Store +} + +func (p *Thread) Comment(creator std.Address, message string) (id string, err error) { + pp := p.New(replyplugin.MessageContent{ + Message: message, + }) + id := p.post.NextIncrementalKey(creator.String()) // Post.ID/address/1 = "comment ID" + if err := post.Add(p.st, id); err != nil { + return "", err + } + return id, nil +} + +func (p *Thread) Upvote() error { + +} + +func (p *Thread) Downvote() error { + +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/boards.gno b/examples/gno.land/p/demo/boardsv2/draft1/boards.gno new file mode 100644 index 00000000000..f05972c0879 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/boards.gno @@ -0,0 +1,69 @@ +package boardsv2 + +import ( + "strconv" +) + +// TODO: Locking +// - Locking a board means, you can not create new threads, and you can not comment in existing ones +// - Locking a thread means, you can not comment in this thread anymore + +// TODO: Move boards (or App) to `boardsv2` +type Boards struct { + // NOTE: We might want different AVL trees to avoid using level prefixes + posts avl.Tree // string(Post.Level + Post.CreatedAt + slug) -> *Post (post, comment, poll) + locking lockingplugin.Plugin +} + +// TODO: Support pagination Start/End (see pager implementation) +func (b Boards) Iterate(level int, path string, fn func(*Post) bool) bool {} +func (b Boards) ReverseIterate(level int, path string, fn func(*Post) bool) bool {} + +func (b *Boards) Lock(path string) { + post := b.Get(LevelBoard, path) // Otherwise we try LevelPost + if err := b.locking.Lock(post); err != nil { + panic(err) + } +} + +// How to map render paths to actual post instances? +// +// AVL KEYS BY LEVEL PREFIX (start/end) +// Boards => 0_ ... 1_ +// Posts => 1_BOARD/ ... 2_ +// Comments => 2_BOARD/POST/ ... 3_ +// +// HOW TO GUESS PREFIX FROM SLUG +// User enters a SLUG => (one part => 1_BOARD)(more than one part => 1_BOARD/POST) +// How to recognize comments? Should be URL accesible? We could use ":" as separator (not optimal) +// +// LEVEL_BOARD/POST/POST-2/COMMENT/COMMENT-2 (deprecated) +// LEVEL_TIMESTAMP_BOARD/POST/COMMENT +// +// :board/post/comment + +func (b *Boards) Set(p *Post) (updated bool) { + key := newKey(p.Level, p.Slug()) + return b.posts.Set(key, p) +} + +func (b *Boards) Remove(level int, path string) (_ *Post, removed bool) { + key := newKey(level, path) + if v, removed := b.posts.Remove(key); removed { + return v.(*Post), true + } + return nil, false +} + +func (b Boards) Get(level int, path string) (_ *Post, found bool) { + key := newKey(level, path) + if v, found := b.posts.Get(key); found { + return v.(*Post), true + } + return "", false +} + +func newKey(level int, path string) string { + // TODO: Add timestamp to key + return strconv.Itoa(level) + "_" + path +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/content_board.gno b/examples/gno.land/p/demo/boardsv2/draft1/content_board.gno new file mode 100644 index 00000000000..e59af9bc4cf --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/content_board.gno @@ -0,0 +1,30 @@ +package boardsv2 + +// TODO: Move content types to `boardsv2` API + +const ContentTypeBoard = "boards:board" + +var _ Content = (*BoardContent)(nil) + +type BoardContent struct { + Name string + Tags []string +} + +func NewBoard() *Post { + return &Post{ // TODO: Use a contructor to be able to use private fields (use options), NewPost + // ... + Level: LevelBoard, + Content: &BoardContent{ + // ... + }, + } +} + +func (c BoardContent) Type() string { + return ContentTypeBoard +} + +func (c BoardContent) Render() string { + return "" +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/content_comment.gno b/examples/gno.land/p/demo/boardsv2/draft1/content_comment.gno new file mode 100644 index 00000000000..fcaac13c3e8 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/content_comment.gno @@ -0,0 +1,27 @@ +package boardsv2 + +const ContentTypeComment = "boards:comment" + +var _ Content = (*CommentContent)(nil) + +type CommentContent struct { + Body string +} + +func NewComment() *Post { + return &Post{ + // ... + Level: LevelComment, + Content: &CommentContent{ + // ... + }, + } +} + +func (c CommentContent) Type() string { + return ContentTypeComment +} + +func (c CommentContent) Render() string { + return "" +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/content_poll.gno b/examples/gno.land/p/demo/boardsv2/draft1/content_poll.gno new file mode 100644 index 00000000000..1f3a0bc9846 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/content_poll.gno @@ -0,0 +1,33 @@ +package boardsv2 + +const ContentTypePoll = "boards:poll" + +var _ Content = (*PollContent)(nil) + +type PollContent struct { + Question string + Options []string + Votes []struct { + Address std.Adress + Option string + } + Tags []string +} + +func NewPoll( /* ... */ ) *Post { + return &Post{ + // ... + Level: LevelPost, + Content: &PollContent{ + // ... + }, + } +} + +func (c PollContent) Type() string { + return ContentTypePoll +} + +func (c PollContent) Render() string { + return "" +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/content_post.gno b/examples/gno.land/p/demo/boardsv2/draft1/content_post.gno new file mode 100644 index 00000000000..289b5ba0099 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/content_post.gno @@ -0,0 +1,29 @@ +package boardsv2 + +const ContentTypePost = "boards:post" + +var _ Content = (*TextContent)(nil) + +type TextContent struct { + Title string + Body string + Tags []string +} + +func NewPost() *Post { + return &Post{ + // ... + Level: LevelPost, + Content: &TextContent{ + // ... + }, + } +} + +func (c TextContent) Type() string { + return ContentTypePost +} + +func (c TextContent) Render() string { + return "" +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/features.gno b/examples/gno.land/p/demo/boardsv2/draft1/features.gno new file mode 100644 index 00000000000..c5dff81eee4 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/features.gno @@ -0,0 +1,69 @@ +package boardsv2 + +import "errors" + +func AddBoard(s PostStore, slug string /* ... */) (path string, _ error) { + // TODO: Finish implementation + + return slug, nil +} + +// NOTE: Define a pattern to add functionality to posts by type (AddComment, AddThread, AddPoll, Repost, Upvote, ...) +// NOTE: Maybe though functions that assert the right arguments +func AddComment(s PostStore, parentPath string, creator std.Address, message string) (path string, _ error) { + // Try to get parent as a post or a comment, otherwise parent doesn't support comments + p, found := s.Get(LevelPost, parentPath) + if !found { + p, found = s.Get(LevelComment, parentPath) + if !found { + return "", errors.New("parent post or comment not found: " + parentPath) + } + } + + // TODO: + // Call the IsLocked function from the plugin for both the board post and thread post + // of this new comment. And confirm that both of them are false + // if so, then proceed, otherwise can not add new comments because locked. + // level 0 - boards + // level 1 - thread + // level 2 - comment + // level 3 - comment under comment + // level 4 - comment under comment under comment + // ... + + // TODO: + // Consider using reverse iteration while checking IsLocked in parent levels. + // If the keys in the AVL tree has levels as the prefix it should be optimized. If + // timestamp is used it may not be. + + comment := NewComment(p /* ... */) + + // TODO: Finish implementation + s.Set( /* ... */ ) + + path = parentPath + "/" + comment.ID + return path, nil +} + +// NOTE: Arguments could potentially be many, consider variadic + sane defaults (?) +func AddThread(s PostStore, parentPath, slug string, creator std.Address /* ... */) (path string, _ error) { + p, found := b.Get(LevelPost, parentPath) + if !found { + return "", errors.New("parent post not found: " + parentPath) + } + + post := NewPost(p, slug /* ... */) + + // TODO: Finish implementation + + path = parentPath + "/" + post.ID + return path, nil +} + +// ----- Other features ----- +// type VotesStore interface { +// /*...*/ +// } +// +// func Upvote(s VotesStore /* ... */) {} +// func DownVote(s VotesStore /* ... */) {} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/plugin/locking/locking.gno b/examples/gno.land/p/demo/boardsv2/draft1/plugin/locking/locking.gno new file mode 100644 index 00000000000..ba23251a4ef --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/plugin/locking/locking.gno @@ -0,0 +1,50 @@ +package lockingplugin + +import "errors" + +const Name = "boards:locking" + +var ErrInvalidPostType = errors.New("post type is not a board or thread") + +type ( + Plugin struct{} + Storage struct { + IsLocked bool + } +) + +func New() Plugin { + return Plugin{} +} + +func (p Plugin) Name() string { + return Name +} + +func (p *Plugin) Lock(p *Post) error { + if !isBoardOrThread(p) { + return ErrInvalidPostType + } + + p.MustGetPluginStorage(p.Name()).(*Storage).IsLocked = true +} + +func (p *Plugin) Unlock(p *Post) error { + if !isBoardOrThread(p) { + return ErrInvalidPostType + } + + p.MustGetPluginStorage(p.Name()).(*Storage).IsLocked = false +} + +func (p Plugin) IsLocked(p *Post) bool { + if !isBoardOrThread(p) { + return ErrInvalidPostType + } + + return p.MustGetPluginStorage(p.Name()).(*Storage).IsLocked +} + +func isBoardOrThread(p *Post) bool { + return p.Level == LevelBoard || p.Level == LevelPost +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/options.gno b/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/options.gno new file mode 100644 index 00000000000..83287a53580 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/options.gno @@ -0,0 +1,15 @@ +package reputationplugin + +type Option func(*Plugin) + +func UseTokenBasePolicy() Option { + return func(p *Plugin) { + p.Policy = PolicyTokenBase + } +} + +func AllowedPostLevels(levels []int) Option { + return func(p *Plugin) { + p.AllowedPostLevels = levels + } +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/reputation.gno b/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/reputation.gno new file mode 100644 index 00000000000..13fcfa3facd --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/reputation.gno @@ -0,0 +1,77 @@ +package reputationplugin + +import "errors" + +const ( + PolicyLinear = iota + PolicyTokenBase +) + +const Name = "boards:reputation" + +var ErrNotSupported = errors.New("reputation not supported") + +type ( + Plugin struct { + Policy int + AllowedPostLevels []int + } + + Storage struct { + Upvotes uint + Downbotes uint + ListOfWhoVotedWhat avl.Tree // string(std.Address) -> ?? (TODO: define) + } +) + +func NewReputationPlugin(o ...Option) Plugin { + var p Plugin + for _, apply := range o { + apply(&p) + } + return p +} + +func (p Plugin) Name() string { + return Name +} + +func (p Plugin) HasReputationSupport(p *Post) bool { + if len(p.AllowedPostLevels) == 0 { + return true + } + + for _, lvl := range p.AllowedPostLevels { + if p.Level == lvl { + return true + } + } + return false +} + +func (p *Plugin) Votes(p *Post) uint32 { + if !p.HasReputationSupport(p) { + return ErrNotSupported + } + + // TODO: Implement +} + +func (p *Plugin) Upvote(p *Post) error { + if !p.HasReputationSupport(p) { + return ErrNotSupported + } + + // TODO: Modify global state + // TODO: Modify local state + // TODO: Implement + st := p.MustGetPluginStorage(p.Name()).(*Storage) +} + +func (p *Plugin) Downvote(p *Post) error { + if !p.HasReputationSupport(p) { + return ErrNotSupported + } + + // TODO: Implement +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/post.gno b/examples/gno.land/p/demo/boardsv2/draft1/post.gno new file mode 100644 index 00000000000..1498cda066f --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/post.gno @@ -0,0 +1,45 @@ +package boardsv2 + +import ( + "strconv" + "time" +) + +const ( + LevelBoard = iota + LevelPost + LevelComment +) + +type ( + Content interface { + Type() string + Render() string + } + + Post struct { + ID string + Content Content + PluginStorage avl.Tree // string(plugin name) -> interface{}(plugin storage) + Parent *Post + Level int + Base *Post + Children []*Post + Forks []*Post + UpdatedAt time.Time + CreatedAt time.Time + Creator std.Address + } +) + +func (p Post) MustGetPluginStorage(name string) interface{} { + if v, found := p.pluginStorage.Get(name); found { + return v + } + + panic("plugin storage not found: " + name) +} + +func (p Post) NextIncrementalKey(baseKey string) string { + return baseKey + "/" + strconv.Itoa(len(p.Children)) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/store.gno b/examples/gno.land/p/demo/boardsv2/draft1/store.gno new file mode 100644 index 00000000000..f8c98f3e724 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/store.gno @@ -0,0 +1,9 @@ +package boardsv2 + +// NOTE: Maybe we could abstract the location where posts are stored +type PostStore interface { + Set(*Post) (updated bool) + Get(level int, path string) (_ *Post, found bool) // NOTE: Level could be a type alias for better semantics + + // TODO: Add iterator (or define PostIterator interface) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/app.gno b/examples/gno.land/p/demo/boardsv2/draft2/app.gno new file mode 100644 index 00000000000..30125302302 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/app.gno @@ -0,0 +1,91 @@ +package boards + +import ( + "plugin" + + "golang.org/x/mod/sumdb/storage" +) + +const ( + LevelBoard = iota + LevelThread + LevelComment +) + +type App struct { + cntx Context +} + +func New(st storage.PostStorage, o ...Option) App { + // TODO: avl.Tree feels wrong here but maybe get rid of the map anyway + p := map[plugin.Name]plugin.Plugin{ + plugintitle.Name: plugintitle.New(st), // content for boards + plugintext.Name: plugintext.New(st),// content for text based threads + pluginpoll.Name: pluginpoll.New(st),// content for poll based threads + plugincomment.Name: plugincomment.New(st),// content for comments to the threads + } + + c := Context{ + storage: st, + plugins: p, + } + + a := App{ + cntx: c, + } + + return a +} + +func (a App) Board(path string) (Board, error) { + a.c.Get(level, path func(){}) +} + +func (a App) Boards(c post.Cursor) ([]Board, error) { + +} + +func (a App) CreateBoard(c BoardContent) (Board, error) {} + +func (b App) Thread(path string) (Thread, error) { + return ThreadWithComments(path, nil) +} + +// Fork forks either a board or a thread by their path. +func (a App) Fork(path, newPath string) error {} + +// Lock locks either a board or a thread by their path. +// Once a board is locked new threads to the board and comments to the existing +// threads won't be allowed. +// Once a thread is locked new comments to the thread won't be allowed. +func (a App) Lock(path string) error {} + + +// ThreadWithComments returns a thread with its comments with the comment depth +// configured with commentDepth for direct and child comments. +// For ex. +// To get a thread with only 10 direct (parent level) comments use: +// - []int{10} +// To get a thread with 10 direct comments and 3 of their child comments use: +// - []int{10, 3} +// You can define configure this for more levels until you reach to value defined +// by MaxCommentDepth. +// By default the configuration is as follows: +// - []int{20, 3} +func (b App) ThreadWithComments(path string, commentDepth []int) (Thread, error) {} +func (b App) Threads(c post.Cursor) ([]Thread, error) {} +func (b App) CreateTextThread(c ThreadTextContent) (Thread, error) {} +func (b App) CreatePollThread(c ThreadPollContent) (Thread, error) {} + +// parentPath could be a path to thread (root), or path to any of the +// nested comments. +func (b App) Comments(parentPath string, c Cursor) ([]Comment, error) {} +func (b App) CreateComment(path string, c plugincomment.Content) (Comment, error) { + post, err := a.c.Plugin(plugincomment.Name).NewPost(c, LevelComment) + if err != nil { + return Comment{}, err + } + return Comment{Post: post, c: a.c} +} + +func (a App) Render(path string) string {} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/board.gno b/examples/gno.land/p/demo/boardsv2/draft2/board.gno new file mode 100644 index 00000000000..ac9258fa629 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/board.gno @@ -0,0 +1,14 @@ +package boards + +type Board struct { + post.Post + c Context +} + +func (b Board) Content() BoardContent { + return b.c.Plugin(pluginbasiccontent.Name).Content(b.Post) +} + +func (b Board) Render() string { + +} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/comment.gno b/examples/gno.land/p/demo/boardsv2/draft2/comment.gno new file mode 100644 index 00000000000..d66a77d1a67 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/comment.gno @@ -0,0 +1,8 @@ +package boards + +type Comment struct { + post.Post + c Context +} + +func (c Comment) Content() CommentContent {} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/context.gno b/examples/gno.land/p/demo/boardsv2/draft2/context.gno new file mode 100644 index 00000000000..15c31e1db1d --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/context.gno @@ -0,0 +1,38 @@ +package boards + +import "plugin" + +type Context struct { + opts []Option + st post.Storage + plugs map[post.PluginName]plugin.Plugin +} + +func newContext() Context { + +} + +func (c Context) Plugin(n post.PluginName) post.Plugin { + +} + +func (c Context) Set(p *Post) (updated bool) { + key := newKey(p.Level, p.Slug()) + return b.posts.Set(key, p) +} + +func (c Context) Remove(level int, path string) (_ *Post, removed bool) { + key := newKey(level, path) + if v, removed := b.posts.Remove(key); removed { + return v.(*Post), true + } + return nil, false +} + +func (c Context) Get(level int, path string, iterator func()) (_ *Post, found bool) { + key := newKey(level, path) + if v, found := b.posts.Get(key); found { + return v.(*Post), true + } + return "", false +} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/option.gno b/examples/gno.land/p/demo/boardsv2/draft2/option.gno new file mode 100644 index 00000000000..48ab0cc1bff --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/option.gno @@ -0,0 +1,19 @@ +package boards + +type Option struct{} + +// LinearReputationPolicy allows upvoting or downvoting a post by one +// for each account. +func LinearReputationPolicy() Option {} + +// TokenBasedReputationPolicy allows upvoting or downvoting a post propotional +// to the specified tokens that an account holds. +func TokenBasedReputationPolicy() Option {} + +// MaxPostDepth configures the max depth for nested comments. +// 0 -> boards +// 1 -> threads +// 2 -> comments-1 (direct comments to the threads) +// The above are already reserved. +// Setting it to zero will disable comments. +func MaxCommentDepth(d int) Option {} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/cursor.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/cursor.gno new file mode 100644 index 00000000000..44f8ebe85f6 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/cursor.gno @@ -0,0 +1,8 @@ +package post + +type Cursor struct { + FromID string + Count int +} + +func NewCursor(fromID string, count int) Cursor {} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/comment/comment.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/comment/comment.gno new file mode 100644 index 00000000000..2c4d7a53994 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/comment/comment.gno @@ -0,0 +1,34 @@ +package plugincomment + +const Name = "post-comment" + +func New(st Storage) Plugin { +} + +type Plugin struct { + postStorage Storage +} + +type Content struct { // Content of the comment. + Title string + Description string + Tags []string +} + +func (p Plugin) CreateComment(id string, c Content, level int) *post.Post { + pp := &post.Post{ + ID: id, + Level: level, + } + p.EditCommentContent(pp, c) + return pp +} + +func (p Plugin) Content(pst *post.Post) Content { + return pst.PluginStorage[Name].(Content) +} + +func (p Plugin) EditCommentContent(pp *Post, c Content) (updated bool) { + pp.PluginStorage[Name] = c + return p.postStorage.Set(post.ID, pp) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/lock/lock.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/lock/lock.gno new file mode 100644 index 00000000000..5cd488aaabe --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/lock/lock.gno @@ -0,0 +1 @@ +package pluginlock diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/plugin.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/plugin.gno new file mode 100644 index 00000000000..2f1cff7e627 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/plugin.gno @@ -0,0 +1,7 @@ +package plugin + +type Plugin interface { + Type() string +} + +type Name string diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/poll/poll.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/poll/poll.gno new file mode 100644 index 00000000000..db9c0bb40ce --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/poll/poll.gno @@ -0,0 +1 @@ +package pluginpoll diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/reputation/reputation.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/reputation/reputation.gno new file mode 100644 index 00000000000..155fc53850f --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/reputation/reputation.gno @@ -0,0 +1 @@ +package pluginreputation diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/text/text.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/text/text.gno new file mode 100644 index 00000000000..a9f41c39947 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/text/text.gno @@ -0,0 +1,3 @@ +package plugintext + +const Name = "post-text" diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/title/title.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/title/title.gno new file mode 100644 index 00000000000..da2f8e905b5 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/title/title.gno @@ -0,0 +1,24 @@ +package plugintitle + +const Name = "post-title-only" + +func New(st Storage) Plugin { +} + +type Plugin struct { + st Storage +} + +type Content struct { + Title string + Description string + Tags []string +} + +func (p Plugin) Content(post *Post) Content { + return post.Body[Name].(Content) +} + +func (p Plugin) SetContent(post *Post, c Content) { + post.Body[Name] = c +} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/post.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/post.gno new file mode 100644 index 00000000000..04b6b88f48c --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/post.gno @@ -0,0 +1,26 @@ +package boardsv2 + +import ( + "plugin" + "strconv" + "time" +) + +type ( + Post struct { + ID string + PluginStore plugin.Plugin + Parent *Post + Level int + Base *Post + Children []*Post + Forks []*Post + UpdatedAt time.Time + CreatedAt time.Time + Creator std.Address + } +) + +func (p Post) NextIncrementalKey(baseKey string) string { + return baseKey + "/" + strconv.Itoa(len(p.Children)) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/store/store.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/store/store.gno new file mode 100644 index 00000000000..72440ea2a61 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/store/store.gno @@ -0,0 +1 @@ +package store diff --git a/examples/gno.land/p/demo/boardsv2/draft2/thread.gno b/examples/gno.land/p/demo/boardsv2/draft2/thread.gno new file mode 100644 index 00000000000..42e731d4fd2 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/thread.gno @@ -0,0 +1,18 @@ +package boards + +type Thread struct { + post.Post + c Context +} + +func (t Thread) TextContent() ThreadTextContent { + +} + +func (t Thread) PollContent() ThreadPollContent {} +func (t Thread) Type() ContentType {} + +// Comments returns a list of comments sent to the thread. +// The comment slice will be non-nil only when Thread is initiated +// through ThreadWithComments. +func (t Thread) Comments() []Comment {} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/app.gno b/examples/gno.land/p/demo/boardsv2/draft3/app.gno new file mode 100644 index 00000000000..3893e0866f7 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/app.gno @@ -0,0 +1,133 @@ +package boards + +import ( + "gno.land/p/demo/boards/post" + "gno.land/p/demo/boards/post/plugin" + pluginfork "gno.land/p/demo/boards/post/plugin/fork" + pluginpoll "gno.land/p/demo/boards/post/plugin/poll" + pluginreputation "gno.land/p/demo/boards/post/plugin/reputation" + plugintitle "gno.land/p/demo/boards/post/plugin/title" +) + +const ( + LevelBoard = iota + LevelThread + LevelComment +) + +// App is the boards application. +type App struct { + posts post.Store + plugins *plugin.Registry + maxCommentsDepth int + reputationPolicy pluginreputation.Policy +} + +// New creates a new boards application. +func New(st post.Store, options ...Option) App { + app := App{ + posts: st, + maxCommentsDepth: -1, // Infinite number of comments + } + for _, apply := range options { + apply(&app) + } + + app.plugins := plugin.NewRegistry( + plugintitle.New(st), // Plugin for boards + plugintext.New(st), // Plugin for text based threads + pluginpoll.New(st), // Plugin for poll based threads + plugincomment.New(st), // Plugin for comments to the threads + pluginreputation.New( + pluginreputation.UsePolicy(app.reputationPolicy), + pluginreputation.AllowedPostLevels(post.LevelPost, post.LevelComment), + ), + pluginfork.New( + pluginfork.AllowedPostLevels(post.LevelPost), + ), + ) + return app +} + +func (a App) GetBoard(path string) (_ Board, found bool) { + p, found := a.posts.GetByLevel(path, LevelBoard) + if !found { + return Board{}, false + } + return Board{p}, true +} + +func (a App) GetThread(path string) (_ Thread, found bool) { + p, found := a.posts.GetByLevel(path, LevelThread) + if !found { + return Thread{}, false + } + return Thread{p}, true +} + +func (a App) GetComment(path string) (_ Comment, found bool) { + p, found := a.posts.GetByLevel(path, LevelComment) + if !found { + return Comment{}, false + } + return Comment{p}, true +} + +func (a App) CreateBoard(slug, title, description string, tags []string) (Board, error) {} + +// Fork forks either a board or a thread by their path. +func (a App) ForkBoard(b Board, newPath string) error { + // NOTE: Instead of `app.ForkBoard()` we could use `b.Fork(newPath)` instead but that requires Board to have plugin access + // NOTE: This case gets the plugin from the plugin list to fork + p, _ := a.plugins.Get(pluginfork.Name) + return p.Fork(b.Post, newPath) +} + +func (a App) ForkThread(t Thread, newPath string) error { + // TODO: Implement thread fork app support +} + +// Lock locks either a board or a thread by their path. +// Once a board is locked new threads to the board and comments to the existing +// threads won't be allowed. +// Once a thread is locked new comments to the thread won't be allowed. +func (a App) Lock(path string) error {} + +// ---- TODO: Review the following list of app methods ----- // + +func (a App) Boards(c post.Cursor) ([]Board, error) { +} + +func (b App) Thread(path string) (Thread, error) { + return ThreadWithComments(path, nil) +} + +// ThreadWithComments returns a thread with its comments with the comment depth +// configured with commentDepth for direct and child comments. +// For ex. +// To get a thread with only 10 direct (parent level) comments use: +// - []int{10} +// To get a thread with 10 direct comments and 3 of their child comments use: +// - []int{10, 3} +// You can define configure this for more levels until you reach to value defined +// by MaxCommentDepth. +// By default the configuration is as follows: +// - []int{20, 3} +func (b App) ThreadWithComments(path string, commentDepth []int) (Thread, error) {} +func (b App) Threads(c post.Cursor) ([]Thread, error) {} +func (b App) CreateTextThread(c ThreadTextContent) (Thread, error) {} +func (b App) CreatePollThread(c ThreadPollContent) (Thread, error) {} + +// parentPath could be a path to thread (root), or path to any of the +// nested comments. +func (b App) Comments(parentPath string, c Cursor) ([]Comment, error) {} + +func (b App) CreateComment(path string, c plugincomment.Content) (Comment, error) { + post, err := a.c.Plugin(plugincomment.Name).NewPost(c, LevelComment) + if err != nil { + return Comment{}, err + } + return Comment{Post: post, c: a.c} +} + +func (a App) Render(path string) string {} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/board.gno b/examples/gno.land/p/demo/boardsv2/draft3/board.gno new file mode 100644 index 00000000000..b75db758a79 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/board.gno @@ -0,0 +1,52 @@ +package boards + +import ( + "gno.land/p/demo/boards/post" + pluginfork "gno.land/p/demo/boards/post/plugin/fork" + pluginreputation "gno.land/p/demo/boards/post/plugin/reputation" + plugintitle "gno.land/p/demo/boards/post/plugin/title" +) + +type ( + BoardContent plugintitle.Content + + Board struct { + *post.Post + } +) + +func NewBoard(pst *post.Post) Board { + // TODO: Local plugins must be initialized here (same for other plugins) + return Board{pst} +} + +func (b Board) Info() BoardContent { + return BoardContent(b.getContent()) +} + +func (b Board) Update(c BoardContent) { + b.PluginStore[plugintitle.Name] = plugintitle.Content(c) +} + +func (b Board) Upvote() error { + r := b.getReputation() + return r.Upvote(b.Post) +} + +func (b Board) Downvote() error { + r := b.getReputation() + return r.Downvote(b.Post) +} + +func (b Board) Render() string { + c := b.getContent() + return c.Render() +} + +func (b Board) getContent() *plugintitle.Content { + return b.PluginStore[plugintitle.Name].(*plugintitle.Content) +} + +func (b Board) getReputation() *pluginreputation.Reputation { + return b.PluginStore[pluginreputation.Name].(*pluginreputation.Reputation) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/comment.gno b/examples/gno.land/p/demo/boardsv2/draft3/comment.gno new file mode 100644 index 00000000000..d66a77d1a67 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/comment.gno @@ -0,0 +1,8 @@ +package boards + +type Comment struct { + post.Post + c Context +} + +func (c Comment) Content() CommentContent {} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/context.gno b/examples/gno.land/p/demo/boardsv2/draft3/context.gno new file mode 100644 index 00000000000..f3139abccb4 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/context.gno @@ -0,0 +1,36 @@ +package boards + +import "plugin" + +type Context struct { + opts []Option + st post.Store + plugs map[post.PluginName]plugin.Plugin +} + +func newContext() Context { +} + +func (c Context) Plugin(n post.PluginName) post.Plugin { +} + +func (c Context) Set(p *Post) (updated bool) { + key := newKey(p.Level, p.Slug()) + return b.posts.Set(key, p) +} + +func (c Context) Remove(level int, path string) (_ *Post, removed bool) { + key := newKey(level, path) + if v, removed := b.posts.Remove(key); removed { + return v.(*Post), true + } + return nil, false +} + +func (c Context) Get(level int, path string, iterator func()) (_ *Post, found bool) { + key := newKey(level, path) + if v, found := b.posts.Get(key); found { + return v.(*Post), true + } + return "", false +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/options.gno b/examples/gno.land/p/demo/boardsv2/draft3/options.gno new file mode 100644 index 00000000000..906b1130663 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/options.gno @@ -0,0 +1,31 @@ +package boards + +import ( + pluginreputation "gno.land/p/demo/boards/post/plugin/reputation" +) + +// Option configures board applications. +type Options func(*App) + +// LinearReputationPolicy allows upvoting or downvoting a post by one for each account. +func LinearReputationPolicy() Option { + return func(a *App) { + a.reputationPolicy = pluginreputation.PolicyLinear + } +} + +// TokenBasedReputationPolicy allows upvoting or downvoting +// a post propotional to the specified tokens that an account holds. +func TokenBasedReputationPolicy() Option { + return func(a *App) { + a.reputationPolicy = pluginreputation.PolicyTokenBased + } +} + +// MaxCommentsDepth configures the max depth for nested comments. +// Setting it to -1 allows an infinite number of nested comments (default). +func MaxCommentsDepth(d int) Option { + return func(a *App) { + a.maxCommentsDepth = d + } +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/comment/comment.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/comment/comment.gno new file mode 100644 index 00000000000..f7b1eed2e5e --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/comment/comment.gno @@ -0,0 +1,50 @@ +package plugincomment + +const Name = "post-comment" + +type ( + Plugin struct { + posts post.Store + } + + // Content is the comment's content. + Content struct { + Title string + Description string + Tags []string + } +) + +func New(st post.Store) Plugin { + return Plugin{ + posts: st, + } +} + +func (p Plugin) Name() string { + return Name +} + +func (p Plugin) Render() string { + // TODO: Implement render support for comments + return "" +} + +func (p Plugin) CreateComment(id string, c Content, level int) *post.Post { + pst := &post.Post{ + ID: id, + Level: level, + } + p.SetContent(pst, c) + return pst +} + +func (p Plugin) Content(pst *post.Post) (_ *Content, ok bool) { + c, ok := pst.PluginStore[Name].(*Content) + return c, ok +} + +func (p Plugin) SetContent(pst *post.Post, c Content) (updated bool) { + ps.PluginStore[Name] = c + return p.posts.Set(pst) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/fork.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/fork.gno new file mode 100644 index 00000000000..543f18b6e8d --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/fork.gno @@ -0,0 +1,47 @@ +package pluginfork + +import ( + "gno.land/p/demo/boards/post" +) + +const Name = "fork" + +// TODO: Implement fork plugin to support thread forking +type Plugin struct { + AllowedPostLevels []int +} + +func New(o ...Option) Plugin { + var p Plugin + for _, apply := range o { + apply(&p) + } + return p +} + +func (p Plugin) Name() string { + return Name +} + +func (p Plugin) Render() string { + // TODO: Implement render support for text + return "" +} + +func (p Plugin) HasForkSupport(pst *post.Post) bool { + if len(p.AllowedPostLevels) == 0 { + return true + } + + for _, lvl := range p.AllowedPostLevels { + if pst.Level == lvl { + return true + } + } + return false +} + +func (p Plugin) Fork(pst *post.Post, newPath string) error { + // TODO: Implement fork support + return nil +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/options.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/options.gno new file mode 100644 index 00000000000..85fe888bcb1 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/options.gno @@ -0,0 +1,9 @@ +package pluginfork + +type Option func(*Plugin) + +func AllowedPostLevels(levels []int) Option { + return func(p *Plugin) { + p.AllowedPostLevels = levels + } +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/lock/lock.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/lock/lock.gno new file mode 100644 index 00000000000..fe75dd364c5 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/lock/lock.gno @@ -0,0 +1,55 @@ +package pluginlock + +import "errors" + +const Name = "lock" + +var ErrInvalidPostType = errors.New("post type is not a board or thread") + +type ( + Plugin struct{} + Lock struct { + IsLocked bool + } +) + +func New() Plugin { + return Plugin{} +} + +func (p Plugin) Name() string { + return Name +} + +func (p Plugin) Render() string { + return "" +} + +func (p *Plugin) Lock(pst *post.Post) error { + if !isBoardOrThread(pst) { + return ErrInvalidPostType + } + + pst.PluginStore[Name].(*Lock).IsLocked = true +} + +func (p *Plugin) Unlock(pst *post.Post) error { + if !isBoardOrThread(pst) { + return ErrInvalidPostType + } + + pst.PluginStore[Name].(*Lock).IsLocked = false +} + +func (p Plugin) IsLocked(pst *post.Post) bool { + if !isBoardOrThread(pst) { + return ErrInvalidPostType + } + + // TODO: Check parents if current post is not locked + return pst.PluginStore[Name].(*Lock).IsLocked +} + +func isBoardOrThread(pst *post.Post) bool { + return pst.Level == post.LevelBoard || pst.Level == post.LevelPost +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/plugin.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/plugin.gno new file mode 100644 index 00000000000..5a42dc8bd10 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/plugin.gno @@ -0,0 +1,48 @@ +// TODO: Document how plugins work and best practices +package plugin + +import ( + "gno.land/p/demo/avl" +) + +type ( + // NOTE: Consider adding lifecycle methods like `Post` creation, deletion, ... + Plugin interface { + Name() string + Render() string + } + + Registry struct { + plugins avl.Tree // string(name) -> Plugin + } +) + +func NewRegistry(plugins ...Plugin) *Registry { + r := &Registry{} + for _, p := range plugins { + r.plugins.Set(p.Name(), p) + } + return r +} + +func (r Registry) Has(name string) bool { + return r.posts.Has(name) +} + +func (r Registry) Get(name string) (_ Plugin, found bool) { + if v, found := r.plugins.Get(name); found { + return v.(Plugin), true + } + return nil, false +} + +func (r *Registry) Add(p Plugin) { + r.plugins.Set(p.Name(), p) +} + +func (r *Registry) Remove(name string) (_ Plugin, removed bool) { + if v, removed := r.plugins.Remove(name, p); removed { + return v.(Plugin), false + } + return nil, false +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/poll/poll.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/poll/poll.gno new file mode 100644 index 00000000000..a20a4a22dd7 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/poll/poll.gno @@ -0,0 +1,47 @@ +package pluginpoll + +const Name = "post-poll" + +type ( + Plugin struct { + posts post.Store + } + + Poll struct { + Question string + Options []string + Votes []struct { + Address std.Adress + Option string + } + Tags []string + } +) + +func New(st post.Store) Plugin { + return Plugin{ + posts: st, + } +} + +func (p Plugin) Name() string { + return Name +} + +func (p Plugin) Render() string { + return "" +} + +func (p Plugin) CreatePoll(id string, v Poll) *post.Post { + pst := &post.Post{ + ID: id, + Level: LevelPost, + } + p.SetPoll(pst, v) + return pst +} + +func (p Plugin) SetPoll(pst *post.Post, v Poll) (updated bool) { + pst.PluginStore[Name] = v + return p.posts.Set(pst.ID, pst) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/options.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/options.gno new file mode 100644 index 00000000000..a87b45bf1e0 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/options.gno @@ -0,0 +1,15 @@ +package pluginreputation + +type Option func(*Plugin) + +func UsePolicy(v Policy) Option { + return func(p *Plugin) { + p.Policy = v + } +} + +func AllowedPostLevels(levels []int) Option { + return func(p *Plugin) { + p.AllowedPostLevels = levels + } +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/reputation.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/reputation.gno new file mode 100644 index 00000000000..0ec8e1406af --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/reputation.gno @@ -0,0 +1,99 @@ +package pluginreputation + +import ( + "errors" + "std" +) + +// NOTE: Think about implementing a reputation based policy +const ( + PolicyLinear Policy = iota + PolicyTokenBased +) + +const Name = "reputation" + +var ErrNotSupported = errors.New("reputation not supported") + +type ( + Policy int + + Plugin struct { + Store Store + Policy Policy + AllowedPostLevels []int + } + + Reputation struct { + Upvotes uint + Downvotes uint + } +) + +func New(o ...Option) Plugin { + var p Plugin + for _, apply := range o { + apply(&p) + } + return p +} + +func (p Plugin) Name() string { + return Name +} + +func (p Plugin) Render() string { + return "" +} + +func (p Plugin) HasReputationSupport(pst *post.Post) bool { + if len(p.AllowedPostLevels) == 0 { + return true + } + + for _, lvl := range p.AllowedPostLevels { + if pst.Level == lvl { + return true + } + } + return false +} + +func (p Plugin) Votes(pst *post.Post) (upvotes uint64, downvotes uint64) { + if !p.HasReputationSupport(pst) { + return ErrNotSupported + } + + r := pst.PluginStore[Name].(*Reputation) + return r.Upvotes, r.Downvotes +} + +func (p Plugin) Voters(pst *post.Post) []std.Address { + if !p.HasReputationSupport(pst) { + return ErrNotSupported + } + + // TODO: Implement support for tracking voters +} + +func (p *Plugin) Upvote(pst *post.Post) error { + if !p.HasReputationSupport(pst) { + return ErrNotSupported + } + + // TODO: Handle accounts and change downvotes for existing accounts that downvoted + r := pst.PluginStore[Name].(*Reputation) + r.Upvotes++ + p.store.inc(pst.ID) +} + +func (p *Plugin) Downvote(pst *post.Post) error { + if !p.HasReputationSupport(pst) { + return ErrNotSupported + } + + // TODO: Handle accounts and change upvotes for existing accounts that upvoted + r := pst.PluginStore[Name].(*Reputation) + r.Downvotes++ + p.store.dec(pst.ID) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/store.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/store.gno new file mode 100644 index 00000000000..9661210f830 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/store.gno @@ -0,0 +1,58 @@ +package pluginreputation + +import ( + "gno.land/p/demo/seqid" +) + +type ( + VotesIterFn = func(votes uint64, path string) bool + + Store struct { + votes avl.Tree // string(count) -> string(path) + } +) + +func (s Store) Iterate(fn VotesIterFn) bool { + // TODO: Support pagination of votes? + return s.votes.Iterate("", "", func(key string, v interface{}) bool { + count, _ := seqid.FromBinary(key) + return fn(uint64(count), v.(string)) + }) +} + +func (s *Store) inc(path string) uint64 { + var ( + current seqid.ID + v, found = s.votes.Get(path) + ) + if found { + current = v.(seqid.ID) + // TODO: Implement the right solution because this is not right, just showcase + s.votes.Remove(current.Binary()) + } + + current.Next() + s.votes.Set(current.Binary(), path) + return uint64(current) +} + +func (s *Store) dec(path string) uint64 { + var ( + current seqid.ID + v, found = s.votes.Get(path) + ) + if found { + current = v.(seqid.ID) + } + + if current == 0 { + return current + } + + s.votes.Remove(current.Binary()) + current-- + if current != 0 { + s.votes.Set(current.Binary(), current) + } + return uint64(current) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/text/text.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/text/text.gno new file mode 100644 index 00000000000..014956e367b --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/text/text.gno @@ -0,0 +1,38 @@ +// plugintext is a content type for representing a tweet, blog post or a thread like Reddit. +package plugintext + +import ( + "gno.land/p/demo/boards/post" // NOTE: Plugins should be at the same level of post package +) + +const Name = "post-text" + +type ( + Plugin struct{} + Content struct { + Title string + Body string + Tags []string + } +) + +func New() Plugin { + return Plugin{} +} + +func (p Plugin) Name() string { + return Name +} + +func (p Plugin) Render() string { + // TODO: Implement render support for text + return "" +} + +func (p Plugin) Content(pst *post.Post) Content { + return pst.Body[Name].(*Content) +} + +func (p Plugin) SetContent(pst *post.Post, c Content) { + pst.Body[Name] = c +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/title/title.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/title/title.gno new file mode 100644 index 00000000000..506f32001a8 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/title/title.gno @@ -0,0 +1,35 @@ +// plugintext is a content type for representing organizations, categories or sections. +package plugintitle + +const Name = "post-title" + +type ( + Plugin struct{} + + Content struct { + Title string + Description string + Tags []string + } +) + +func New() Plugin { + return Plugin{} +} + +func (p Plugin) Name() string { + return Name +} + +func (p Plugin) Render() string { + // TODO: Implement render support for title + return "" +} + +func (p Plugin) Content(pst *post.Post) Content { + return pst.Body[Name].(*Content) +} + +func (p Plugin) SetContent(pst *post.Post, c Content) { + pst.Body[Name] = c +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/post.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/post.gno new file mode 100644 index 00000000000..229a904fd66 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/post.gno @@ -0,0 +1,25 @@ +package post + +import ( + "strconv" + "time" + + "gno.land/p/demo/boards/post/plugin" +) + +type Post struct { + ID string + PluginStore plugin.Plugin + Parent *Post + Level int + Base *Post + Children []*Post + Forks []*Post + UpdatedAt time.Time + CreatedAt time.Time + Creator std.Address +} + +func (p Post) NextIncrementalKey(baseKey string) string { + return baseKey + "/" + strconv.Itoa(len(p.Children)) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/store.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/store.gno new file mode 100644 index 00000000000..49513f250db --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/store.gno @@ -0,0 +1,31 @@ +package post + +func NewStore() Store { + return Store{} +} + +// TODO: Implement posts store +type Store struct { + posts avl.Tree // string(level + creation timestamp + slug) -> *Post + slugs avl.Tree // string(slug) -> *Post +} + +func (s Store) Get(path string) (_ *Post, found bool) { + if v, found := s.slugs.Get(path); found { + return v.(*Post), true + } + return nil, false +} + +func (s Store) GetByLevel(path string, level int) (_ *Post, found bool) { + v, found := s.slugs.Get(path) + if !found { + return nil, false + } + + p := v.(*Post) + if p.Level != level { + return nil, false + } + return p, true +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/store/cursor.gno b/examples/gno.land/p/demo/boardsv2/draft3/store/cursor.gno new file mode 100644 index 00000000000..c4fc379c28d --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/store/cursor.gno @@ -0,0 +1,9 @@ +package store + +// TODO: Define how cursors should be used alongside stores +type Cursor struct { + FromID string + Count int +} + +func NewCursor(fromID string, count int) Cursor {} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/store/store.gno b/examples/gno.land/p/demo/boardsv2/draft3/store/store.gno new file mode 100644 index 00000000000..5c7fba3db4d --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/store/store.gno @@ -0,0 +1,6 @@ +package store + +// TODO: Define a storage interface and create and avl.Tree wrapper +type Store interface{} + +type AVLTreeStore struct{} // TODO: Use IAVL instead if there is a package implemented diff --git a/examples/gno.land/p/demo/boardsv2/draft3/thread.gno b/examples/gno.land/p/demo/boardsv2/draft3/thread.gno new file mode 100644 index 00000000000..cce35799b6f --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/thread.gno @@ -0,0 +1,64 @@ +package boards + +import ( + "gno.land/p/demo/boards/post" + pluginfork "gno.land/p/demo/boards/post/plugin/fork" + pluginpoll "gno.land/p/demo/boards/post/plugin/poll" + pluginreputation "gno.land/p/demo/boards/post/plugin/reputation" + plugintext "gno.land/p/demo/boards/post/plugin/text" +) + +type ( + ThreadContent plugintext.Content + + // TODO: Should polls be handler within this type? + Thread struct { + *post.Post + } +) + +func (t Thread) Info() ThreadContent { + return ThreadContent(t.getContent()) +} + +func (t Thread) Update(c ThreadContent) { + t.PluginStore[plugintext.Name] = plugintext.Content(c) +} + +func (t Thread) Upvote() error { + r := t.getReputation() + return r.Upvote(t.Post) +} + +func (t Thread) Downvote() error { + r := t.getReputation() + return r.Downvote(t.Post) +} + +func (t Thread) Fork(newPath string) error { + f := t.getFork() + return f.Fork(t.Post) +} + +func (t Thread) Render() string { + c := t.getContent() + return c.Render() +} + +// Comments returns a list of comments sent to the thread. +// The comment slice will be non-nil only when Thread is initiated +// through ThreadWithComments. +// TODO: Add support to get sub-threads (any type) and comments +// func (t Thread) Comments() []Comment {} + +func (t Thread) getContent() *plugintext.Content { + return t.PluginStore[plugintext.Name].(*plugintext.Content) +} + +func (t Thread) getReputation() *pluginreputation.Reputation { + return t.PluginStore[pluginreputation.Name].(*pluginreputation.Reputation) +} + +func (t Thread) getFork() *pluginfork.Fork { + return t.PluginStore[pluginfork.Name].(*pluginfork.Fork) +} diff --git a/examples/gno.land/p/demo/boardsv2/post/post.gno b/examples/gno.land/p/demo/boardsv2/post/post.gno deleted file mode 100644 index 54be1f50a86..00000000000 --- a/examples/gno.land/p/demo/boardsv2/post/post.gno +++ /dev/null @@ -1 +0,0 @@ -package post \ No newline at end of file diff --git a/examples/gno.land/p/demo/boardsv2/post/view.gno b/examples/gno.land/p/demo/boardsv2/post/view.gno deleted file mode 100644 index 54be1f50a86..00000000000 --- a/examples/gno.land/p/demo/boardsv2/post/view.gno +++ /dev/null @@ -1 +0,0 @@ -package post \ No newline at end of file diff --git a/examples/gno.land/r/demo/boardsv2/boardsv2.gno b/examples/gno.land/r/demo/boardsv2/boardsv2.gno index 4ad3d466272..4205c210da5 100644 --- a/examples/gno.land/r/demo/boardsv2/boardsv2.gno +++ b/examples/gno.land/r/demo/boardsv2/boardsv2.gno @@ -1 +1,44 @@ -package boardsv2 \ No newline at end of file +package boardsv2 + +import "gno.land/p/demo/avl" + +// TODO: This goes in the realm +// type Boards struct { +// // TODO: Define how do we want to display and sort boards and posts (upvotes, pinned, ...) +// boards avl.Tree +// Title string +// Description string +// } + +func Render(path string) string { + // TODO: Implement render + return "" +} + +// TODO: Define public API + +func CreateBoard() {} // Maybe +func EditBoard() {} // Maybe +func ForkBoard() {} // Maybe + +func CreatePost() {} +func EditPost() {} +func ForkPost() {} +func DeletePost() {} +func Repost() {} +func Pin() {} +func Invite() {} // Maybe: Could also rely on an allow list +func UpVote() {} +func DownVote() {} + +func Comment() {} // Maybe +func EditComment() {} // Maybe +func DeleteComment() {} // Maybe + +func ToggleCommentsSupport() {} // Maybe +func ToggleThreadsSupport() {} // Maybe +func GetTags() {} // Maybe: List of allowed tags (moderated) + +func AddModerator() {} // Maybe +func RemoveModerator() {} // Maybe +func GetModerators() {} // Maybe diff --git a/examples/gno.land/r/demo/boardsv2/draft2/main.gno b/examples/gno.land/r/demo/boardsv2/draft2/main.gno new file mode 100644 index 00000000000..6f8d203d31a --- /dev/null +++ b/examples/gno.land/r/demo/boardsv2/draft2/main.gno @@ -0,0 +1,15 @@ +package boards + +var postStore = avl.Tree{} // string(level + timestamp + slug) -> *Post + +func newApp() boards.App { // stateless approach for App struct + return boards.New( + postStore, + boards.MaxCommentDepth(10), + boards.LinearReputationPolicy(), + ) +} + +func Boards(c post.Cursor) ([]boards.Board, error) { + return newApp().Boards(c) +} diff --git a/examples/gno.land/r/demo/boardsv2/draft3/boards.gno b/examples/gno.land/r/demo/boardsv2/draft3/boards.gno new file mode 100644 index 00000000000..efc316fa458 --- /dev/null +++ b/examples/gno.land/r/demo/boardsv2/draft3/boards.gno @@ -0,0 +1,68 @@ +package boards + +import ( + "std" + + "gno.land/p/demo/boards" + "gno.land/p/demo/boards/post" +) + +var app = boards.New( + post.NewStore(), + boards.MaxCommentDepth(10), + boards.LinearReputationPolicy(), +) + +func Render(path string) string { + // TODO: Define how to render the tree of boards, posts and comments + return "" +} + +func CreateBoard(slug, title, description string, tags []string) (path string) { + creator := std.GetOrigCaller() + board := app.CreateBoard(slug, title, description, tags, creator) + return board.ID +} + +func Lock(path string) { + post := getBoardOrThread(path) + if post == nil { + panic("path doesn't exist or locking this path not supported") + } + + assertOrigCallerIsCreator(post) + + // NOTE: Explore if it's better to use Post or Board/Thread types + if err := app.Lock(post); err != nil { + panic(err) + } +} + +func Fork(path, newPath string) { + post := getBoardOrThread(path) + if post == nil { + panic("path doesn't exist or forking this path not supported") + } + + // TODO: Use this way + app.ForkBoard(board) + app.ForkThread(thread) + + if err := app.Fork(post, newPath); err != nil { + panic(err) + } +} + +func getBoardOrThread(path string) *post.Post { + p, found := app.GetPost(path) + if found && (p.Level == boards.LevelBoard || p.Level == boards.LevelThread) { + return p + } + return nil +} + +func assertOrigCallerIsCreator(p *post.Post) { + if post.Creator != std.GetOrigCaller() { + panic("original caller is not allowed to perform this action") + } +} From 42d50893b2d5f1c2fcd8d13e06f614dc09bdd01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Tue, 12 Nov 2024 08:45:31 +0100 Subject: [PATCH 03/52] chore: remove `boardsv2` drafts (#3111) We won't continue in that direction right now, the current objective is to finish new features for `boards` which will become `boards2`. --- .../gno.land/p/demo/boardsv2/draft0/app.gno | 64 --------- .../gno.land/p/demo/boardsv2/draft0/board.gno | 24 ---- .../p/demo/boardsv2/draft0/boardsv2.gno | 7 - .../p/demo/boardsv2/draft0/option.gno | 13 -- .../draft0/post/avlstorage/avlstorage.gno | 1 - .../p/demo/boardsv2/draft0/post/content.gno | 5 - .../p/demo/boardsv2/draft0/post/plugin.gno | 1 - .../draft0/post/plugins/content/content.gno | 30 ---- .../p/demo/boardsv2/draft0/post/post.gno | 90 ------------ .../p/demo/boardsv2/draft0/post/storage.gno | 4 - .../p/demo/boardsv2/draft0/post/view.gno | 10 -- .../p/demo/boardsv2/draft0/thread.gno | 30 ---- .../p/demo/boardsv2/draft1/boards.gno | 69 --------- .../p/demo/boardsv2/draft1/content_board.gno | 30 ---- .../demo/boardsv2/draft1/content_comment.gno | 27 ---- .../p/demo/boardsv2/draft1/content_poll.gno | 33 ----- .../p/demo/boardsv2/draft1/content_post.gno | 29 ---- .../p/demo/boardsv2/draft1/features.gno | 69 --------- .../draft1/plugin/locking/locking.gno | 50 ------- .../draft1/plugin/reputation/options.gno | 15 -- .../draft1/plugin/reputation/reputation.gno | 77 ---------- .../gno.land/p/demo/boardsv2/draft1/post.gno | 45 ------ .../gno.land/p/demo/boardsv2/draft1/store.gno | 9 -- .../gno.land/p/demo/boardsv2/draft2/app.gno | 91 ------------ .../gno.land/p/demo/boardsv2/draft2/board.gno | 14 -- .../p/demo/boardsv2/draft2/comment.gno | 8 -- .../p/demo/boardsv2/draft2/context.gno | 38 ----- .../p/demo/boardsv2/draft2/option.gno | 19 --- .../p/demo/boardsv2/draft2/post/cursor.gno | 8 -- .../draft2/post/plugin/comment/comment.gno | 34 ----- .../boardsv2/draft2/post/plugin/lock/lock.gno | 1 - .../boardsv2/draft2/post/plugin/plugin.gno | 7 - .../boardsv2/draft2/post/plugin/poll/poll.gno | 1 - .../post/plugin/reputation/reputation.gno | 1 - .../boardsv2/draft2/post/plugin/text/text.gno | 3 - .../draft2/post/plugin/title/title.gno | 24 ---- .../p/demo/boardsv2/draft2/post/post.gno | 26 ---- .../demo/boardsv2/draft2/post/store/store.gno | 1 - .../p/demo/boardsv2/draft2/thread.gno | 18 --- .../gno.land/p/demo/boardsv2/draft3/app.gno | 133 ------------------ .../gno.land/p/demo/boardsv2/draft3/board.gno | 52 ------- .../p/demo/boardsv2/draft3/comment.gno | 8 -- .../p/demo/boardsv2/draft3/context.gno | 36 ----- .../p/demo/boardsv2/draft3/options.gno | 31 ---- .../draft3/post/plugin/comment/comment.gno | 50 ------- .../boardsv2/draft3/post/plugin/fork/fork.gno | 47 ------- .../draft3/post/plugin/fork/options.gno | 9 -- .../boardsv2/draft3/post/plugin/lock/lock.gno | 55 -------- .../boardsv2/draft3/post/plugin/plugin.gno | 48 ------- .../boardsv2/draft3/post/plugin/poll/poll.gno | 47 ------- .../draft3/post/plugin/reputation/options.gno | 15 -- .../post/plugin/reputation/reputation.gno | 99 ------------- .../draft3/post/plugin/reputation/store.gno | 58 -------- .../boardsv2/draft3/post/plugin/text/text.gno | 38 ----- .../draft3/post/plugin/title/title.gno | 35 ----- .../p/demo/boardsv2/draft3/post/post.gno | 25 ---- .../p/demo/boardsv2/draft3/post/store.gno | 31 ---- .../p/demo/boardsv2/draft3/store/cursor.gno | 9 -- .../p/demo/boardsv2/draft3/store/store.gno | 6 - .../p/demo/boardsv2/draft3/thread.gno | 64 --------- .../gno.land/r/demo/boardsv2/boardsv2.gno | 44 ------ .../gno.land/r/demo/boardsv2/draft2/main.gno | 15 -- .../r/demo/boardsv2/draft3/boards.gno | 68 --------- 63 files changed, 2049 deletions(-) delete mode 100644 examples/gno.land/p/demo/boardsv2/draft0/app.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft0/board.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft0/boardsv2.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft0/option.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft0/post/avlstorage/avlstorage.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft0/post/content.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft0/post/plugin.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft0/post/plugins/content/content.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft0/post/post.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft0/post/storage.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft0/post/view.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft0/thread.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft1/boards.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft1/content_board.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft1/content_comment.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft1/content_poll.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft1/content_post.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft1/features.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft1/plugin/locking/locking.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/options.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/reputation.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft1/post.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft1/store.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft2/app.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft2/board.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft2/comment.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft2/context.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft2/option.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/cursor.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/plugin/comment/comment.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/plugin/lock/lock.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/plugin/plugin.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/plugin/poll/poll.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/plugin/reputation/reputation.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/plugin/text/text.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/plugin/title/title.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/post.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft2/post/store/store.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft2/thread.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/app.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/board.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/comment.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/context.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/options.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/comment/comment.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/fork.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/options.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/lock/lock.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/plugin.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/poll/poll.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/options.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/reputation.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/store.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/text/text.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/plugin/title/title.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/post.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/post/store.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/store/cursor.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/store/store.gno delete mode 100644 examples/gno.land/p/demo/boardsv2/draft3/thread.gno delete mode 100644 examples/gno.land/r/demo/boardsv2/boardsv2.gno delete mode 100644 examples/gno.land/r/demo/boardsv2/draft2/main.gno delete mode 100644 examples/gno.land/r/demo/boardsv2/draft3/boards.gno diff --git a/examples/gno.land/p/demo/boardsv2/draft0/app.gno b/examples/gno.land/p/demo/boardsv2/draft0/app.gno deleted file mode 100644 index 28017f895c8..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft0/app.gno +++ /dev/null @@ -1,64 +0,0 @@ -package boardsv2 - -import ( - "gno.land/demo/p/boardsv2/post" - contentplugin "gno.land/demo/p/boardsv2/post/plugins/content" -) - -// type Rating struct{} -// -// var ratingIndex = avl.Tree{} -// app.AddBoardHook(func (changeType int, change ChangeSet) { -// if changeType == 0 { -// ratingIndex.Set("...", ) -// } -// }) - -type App struct { - st Storage - boards []Board -} - -func New(s Storage, o ...Option) App { - a := App{ - st: Storage, - } - // Define the rule for a spesific view. - boardsView := view.New(view.Filter{ - Level: 0, // this will give me the list of the boards. - }) - - return a -} - -func (a *App) AddBoard(name, title, description string) (*Board, error) { - p := post.New(contentplugin.TitleBasedContent{ - Title: title, - Description: description, - }) - - // I want to create a query for listing threads under this new board. - threadView := view.New(view.Filter{ - Level: 1, - SlugPrefix: name, - }) - userActivityView := view.New(view.Filter{ - LevelGte: 2, - By: func(content Content) []View { - c.Author // by account address - } - }) - - if err := post.Add(a.st, name, p); err != nil { - nil, err - } - return a.GetBoard(name), nil -} - -func (a *App) GetBoard(name string) (board *Board, found bool) { - -} - -func (a *App) ListBoards() ([]*Board, error) { - -} diff --git a/examples/gno.land/p/demo/boardsv2/draft0/board.gno b/examples/gno.land/p/demo/boardsv2/draft0/board.gno deleted file mode 100644 index e56895a9b1d..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft0/board.gno +++ /dev/null @@ -1,24 +0,0 @@ -package boardsv2 - -type Board struct { -} - -func (b *Board) AddPost() error { - -} - -func (b *Board) GetThread(id string) (post *Post, found bool) { - -} - -func (b *Board) ListThreads(id string) (post *Post, found bool) { - threadView.List() // there should be an iterator, pagination -} - -func (b *Board) Fork() error { - -} - -func (b *Board) Lock() error { - -} diff --git a/examples/gno.land/p/demo/boardsv2/draft0/boardsv2.gno b/examples/gno.land/p/demo/boardsv2/draft0/boardsv2.gno deleted file mode 100644 index 91c308f9576..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft0/boardsv2.gno +++ /dev/null @@ -1,7 +0,0 @@ -// boardsv2 is a reddit like abstraction around post/*. -// You might implement other abstractions around post/* to create -// different type of dApps. -// refer to the app.gno file to get started. -package boardsv2 - - diff --git a/examples/gno.land/p/demo/boardsv2/draft0/option.gno b/examples/gno.land/p/demo/boardsv2/draft0/option.gno deleted file mode 100644 index b8d7b65c643..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft0/option.gno +++ /dev/null @@ -1,13 +0,0 @@ -package boardsv2 - -type Option struct{} - -// LinearReputationPolicy allows upvoting or downvoting a post by one -// for each account. -func LinearReputationPolicy() Option {} - -// TokenBasedReputationPolicy allows upvoting or downvoting a post propotional -// to the specified tokens that an account holds. -func TokenBasedReputationPolicy() Option {} - -// TODO: make it configurable how many levels allowed diff --git a/examples/gno.land/p/demo/boardsv2/draft0/post/avlstorage/avlstorage.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/avlstorage/avlstorage.gno deleted file mode 100644 index c1f6c226c20..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft0/post/avlstorage/avlstorage.gno +++ /dev/null @@ -1 +0,0 @@ -package avlstorage \ No newline at end of file diff --git a/examples/gno.land/p/demo/boardsv2/draft0/post/content.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/content.gno deleted file mode 100644 index cbfd45c36fe..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft0/post/content.gno +++ /dev/null @@ -1,5 +0,0 @@ -package post - -type Content interface { - Render() string -} diff --git a/examples/gno.land/p/demo/boardsv2/draft0/post/plugin.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/plugin.gno deleted file mode 100644 index 54be1f50a86..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft0/post/plugin.gno +++ /dev/null @@ -1 +0,0 @@ -package post \ No newline at end of file diff --git a/examples/gno.land/p/demo/boardsv2/draft0/post/plugins/content/content.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/plugins/content/content.gno deleted file mode 100644 index 05043e7e172..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft0/post/plugins/content/content.gno +++ /dev/null @@ -1,30 +0,0 @@ -package commentplugin - -type CommentContent struct { - Body string -} - -func (c CommentContent) Render() string { - -} - -type TitleBasedContent struct{} - -type TextContent struct { - Title string - Body string - Tags []string -} - -func (c TextContent) Render() string { - -} - -type PollContent struct { - Question string - Options []string - Votes []struct { - Address std.Adress - Option string - } -} diff --git a/examples/gno.land/p/demo/boardsv2/draft0/post/post.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/post.gno deleted file mode 100644 index d103579d437..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft0/post/post.gno +++ /dev/null @@ -1,90 +0,0 @@ -package post - -import "time" - -/* -alicePost = &Post { content: "foo" } (0x001) -bobFork := &Post { Origial: alicePost (0x001) } - -//1. Check gc behavior in realm for forks - ---- -alicePost := &(*alicePost) (0x002) -alicePost.content = "new content" - -bobFork := &Post { Origial: uintptr(0x001) } ---- -type Post struct { - ID int - Level int -} - -package reddit - -// explore with plugins -// - boardsv2 -// - pkg/general -// - pkg/reddit -var ( - rating avl.Tree -) - -genericPost := Post{} -reddit.UpvotePost(genericPost.ID) -*/ - -// Blog example -// Home -// - post 1 (content: title, body, author, label, timestamp) -// - post 1.1 (body, author) (thread) -// - post 1.1.1 (comment to a thread but also a new thread) -// - post 1.1.1.1 -// - post 1.2 (thread) -// -// - post 2 -// - post 3 -// -// Reddit example -// Home -// - post 1 (title, body) (board) -// - post 1.1 (title, body) (sub-board) -// - post 1.1.1 (title, body, label) -// - post 1.1.1.1 (comment, thread) -type Post struct { - ID string - Content Content // title, body, label, author, other metadata... - Level int - Base *Post - Children []*Post - Forks []*Post - UpdatedAt time.Time - CreatedAt time.Time // the time when created by user or forked. - Creator std.Address -} - -// create plugins for Post type < -// upvoting < implement first plugin -// define public API for plugin, post packages and boardsv2 -// moderation -// -// plugin ideas: -// - visibility -// - upcoting -// - acess control > you shouldn't be able to answer to the boards yo're not invited -// - moedaration (ban certain posts -this could be through a dao in the future) - -func New(s Storage) Post { - -} - -func Create(c Content) *Post { - -} - -func (p *Post) NextIncrementalKey(base string) string { - -} - -// func (p *Post) Append() error { -// -// } diff --git a/examples/gno.land/p/demo/boardsv2/draft0/post/storage.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/storage.gno deleted file mode 100644 index 49a5f7eef32..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft0/post/storage.gno +++ /dev/null @@ -1,4 +0,0 @@ -package post - -type Storage interface { -} diff --git a/examples/gno.land/p/demo/boardsv2/draft0/post/view.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/view.gno deleted file mode 100644 index 3921e441039..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft0/post/view.gno +++ /dev/null @@ -1,10 +0,0 @@ -package post - -// Two cases to solve -// - Give me a list of boards (board list page) -// - Give me a list of comments, created by a user accross all boards (user activity page, of a user) -type View interface { - Name() string - Size() int - Iterate(start, end string, fn func(key string, v interface{}) bool) bool -} diff --git a/examples/gno.land/p/demo/boardsv2/draft0/thread.gno b/examples/gno.land/p/demo/boardsv2/draft0/thread.gno deleted file mode 100644 index 0ecad4ae41d..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft0/thread.gno +++ /dev/null @@ -1,30 +0,0 @@ -package boardsv2 - -import ( - "gno.land/demo/p/boardsv2/post" - replyplugin "gno.land/demo/p/boardsv2/post/plugins/content/reply" -) - -type Thread struct { - post post.Post - st Store -} - -func (p *Thread) Comment(creator std.Address, message string) (id string, err error) { - pp := p.New(replyplugin.MessageContent{ - Message: message, - }) - id := p.post.NextIncrementalKey(creator.String()) // Post.ID/address/1 = "comment ID" - if err := post.Add(p.st, id); err != nil { - return "", err - } - return id, nil -} - -func (p *Thread) Upvote() error { - -} - -func (p *Thread) Downvote() error { - -} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/boards.gno b/examples/gno.land/p/demo/boardsv2/draft1/boards.gno deleted file mode 100644 index f05972c0879..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft1/boards.gno +++ /dev/null @@ -1,69 +0,0 @@ -package boardsv2 - -import ( - "strconv" -) - -// TODO: Locking -// - Locking a board means, you can not create new threads, and you can not comment in existing ones -// - Locking a thread means, you can not comment in this thread anymore - -// TODO: Move boards (or App) to `boardsv2` -type Boards struct { - // NOTE: We might want different AVL trees to avoid using level prefixes - posts avl.Tree // string(Post.Level + Post.CreatedAt + slug) -> *Post (post, comment, poll) - locking lockingplugin.Plugin -} - -// TODO: Support pagination Start/End (see pager implementation) -func (b Boards) Iterate(level int, path string, fn func(*Post) bool) bool {} -func (b Boards) ReverseIterate(level int, path string, fn func(*Post) bool) bool {} - -func (b *Boards) Lock(path string) { - post := b.Get(LevelBoard, path) // Otherwise we try LevelPost - if err := b.locking.Lock(post); err != nil { - panic(err) - } -} - -// How to map render paths to actual post instances? -// -// AVL KEYS BY LEVEL PREFIX (start/end) -// Boards => 0_ ... 1_ -// Posts => 1_BOARD/ ... 2_ -// Comments => 2_BOARD/POST/ ... 3_ -// -// HOW TO GUESS PREFIX FROM SLUG -// User enters a SLUG => (one part => 1_BOARD)(more than one part => 1_BOARD/POST) -// How to recognize comments? Should be URL accesible? We could use ":" as separator (not optimal) -// -// LEVEL_BOARD/POST/POST-2/COMMENT/COMMENT-2 (deprecated) -// LEVEL_TIMESTAMP_BOARD/POST/COMMENT -// -// :board/post/comment - -func (b *Boards) Set(p *Post) (updated bool) { - key := newKey(p.Level, p.Slug()) - return b.posts.Set(key, p) -} - -func (b *Boards) Remove(level int, path string) (_ *Post, removed bool) { - key := newKey(level, path) - if v, removed := b.posts.Remove(key); removed { - return v.(*Post), true - } - return nil, false -} - -func (b Boards) Get(level int, path string) (_ *Post, found bool) { - key := newKey(level, path) - if v, found := b.posts.Get(key); found { - return v.(*Post), true - } - return "", false -} - -func newKey(level int, path string) string { - // TODO: Add timestamp to key - return strconv.Itoa(level) + "_" + path -} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/content_board.gno b/examples/gno.land/p/demo/boardsv2/draft1/content_board.gno deleted file mode 100644 index e59af9bc4cf..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft1/content_board.gno +++ /dev/null @@ -1,30 +0,0 @@ -package boardsv2 - -// TODO: Move content types to `boardsv2` API - -const ContentTypeBoard = "boards:board" - -var _ Content = (*BoardContent)(nil) - -type BoardContent struct { - Name string - Tags []string -} - -func NewBoard() *Post { - return &Post{ // TODO: Use a contructor to be able to use private fields (use options), NewPost - // ... - Level: LevelBoard, - Content: &BoardContent{ - // ... - }, - } -} - -func (c BoardContent) Type() string { - return ContentTypeBoard -} - -func (c BoardContent) Render() string { - return "" -} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/content_comment.gno b/examples/gno.land/p/demo/boardsv2/draft1/content_comment.gno deleted file mode 100644 index fcaac13c3e8..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft1/content_comment.gno +++ /dev/null @@ -1,27 +0,0 @@ -package boardsv2 - -const ContentTypeComment = "boards:comment" - -var _ Content = (*CommentContent)(nil) - -type CommentContent struct { - Body string -} - -func NewComment() *Post { - return &Post{ - // ... - Level: LevelComment, - Content: &CommentContent{ - // ... - }, - } -} - -func (c CommentContent) Type() string { - return ContentTypeComment -} - -func (c CommentContent) Render() string { - return "" -} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/content_poll.gno b/examples/gno.land/p/demo/boardsv2/draft1/content_poll.gno deleted file mode 100644 index 1f3a0bc9846..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft1/content_poll.gno +++ /dev/null @@ -1,33 +0,0 @@ -package boardsv2 - -const ContentTypePoll = "boards:poll" - -var _ Content = (*PollContent)(nil) - -type PollContent struct { - Question string - Options []string - Votes []struct { - Address std.Adress - Option string - } - Tags []string -} - -func NewPoll( /* ... */ ) *Post { - return &Post{ - // ... - Level: LevelPost, - Content: &PollContent{ - // ... - }, - } -} - -func (c PollContent) Type() string { - return ContentTypePoll -} - -func (c PollContent) Render() string { - return "" -} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/content_post.gno b/examples/gno.land/p/demo/boardsv2/draft1/content_post.gno deleted file mode 100644 index 289b5ba0099..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft1/content_post.gno +++ /dev/null @@ -1,29 +0,0 @@ -package boardsv2 - -const ContentTypePost = "boards:post" - -var _ Content = (*TextContent)(nil) - -type TextContent struct { - Title string - Body string - Tags []string -} - -func NewPost() *Post { - return &Post{ - // ... - Level: LevelPost, - Content: &TextContent{ - // ... - }, - } -} - -func (c TextContent) Type() string { - return ContentTypePost -} - -func (c TextContent) Render() string { - return "" -} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/features.gno b/examples/gno.land/p/demo/boardsv2/draft1/features.gno deleted file mode 100644 index c5dff81eee4..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft1/features.gno +++ /dev/null @@ -1,69 +0,0 @@ -package boardsv2 - -import "errors" - -func AddBoard(s PostStore, slug string /* ... */) (path string, _ error) { - // TODO: Finish implementation - - return slug, nil -} - -// NOTE: Define a pattern to add functionality to posts by type (AddComment, AddThread, AddPoll, Repost, Upvote, ...) -// NOTE: Maybe though functions that assert the right arguments -func AddComment(s PostStore, parentPath string, creator std.Address, message string) (path string, _ error) { - // Try to get parent as a post or a comment, otherwise parent doesn't support comments - p, found := s.Get(LevelPost, parentPath) - if !found { - p, found = s.Get(LevelComment, parentPath) - if !found { - return "", errors.New("parent post or comment not found: " + parentPath) - } - } - - // TODO: - // Call the IsLocked function from the plugin for both the board post and thread post - // of this new comment. And confirm that both of them are false - // if so, then proceed, otherwise can not add new comments because locked. - // level 0 - boards - // level 1 - thread - // level 2 - comment - // level 3 - comment under comment - // level 4 - comment under comment under comment - // ... - - // TODO: - // Consider using reverse iteration while checking IsLocked in parent levels. - // If the keys in the AVL tree has levels as the prefix it should be optimized. If - // timestamp is used it may not be. - - comment := NewComment(p /* ... */) - - // TODO: Finish implementation - s.Set( /* ... */ ) - - path = parentPath + "/" + comment.ID - return path, nil -} - -// NOTE: Arguments could potentially be many, consider variadic + sane defaults (?) -func AddThread(s PostStore, parentPath, slug string, creator std.Address /* ... */) (path string, _ error) { - p, found := b.Get(LevelPost, parentPath) - if !found { - return "", errors.New("parent post not found: " + parentPath) - } - - post := NewPost(p, slug /* ... */) - - // TODO: Finish implementation - - path = parentPath + "/" + post.ID - return path, nil -} - -// ----- Other features ----- -// type VotesStore interface { -// /*...*/ -// } -// -// func Upvote(s VotesStore /* ... */) {} -// func DownVote(s VotesStore /* ... */) {} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/plugin/locking/locking.gno b/examples/gno.land/p/demo/boardsv2/draft1/plugin/locking/locking.gno deleted file mode 100644 index ba23251a4ef..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft1/plugin/locking/locking.gno +++ /dev/null @@ -1,50 +0,0 @@ -package lockingplugin - -import "errors" - -const Name = "boards:locking" - -var ErrInvalidPostType = errors.New("post type is not a board or thread") - -type ( - Plugin struct{} - Storage struct { - IsLocked bool - } -) - -func New() Plugin { - return Plugin{} -} - -func (p Plugin) Name() string { - return Name -} - -func (p *Plugin) Lock(p *Post) error { - if !isBoardOrThread(p) { - return ErrInvalidPostType - } - - p.MustGetPluginStorage(p.Name()).(*Storage).IsLocked = true -} - -func (p *Plugin) Unlock(p *Post) error { - if !isBoardOrThread(p) { - return ErrInvalidPostType - } - - p.MustGetPluginStorage(p.Name()).(*Storage).IsLocked = false -} - -func (p Plugin) IsLocked(p *Post) bool { - if !isBoardOrThread(p) { - return ErrInvalidPostType - } - - return p.MustGetPluginStorage(p.Name()).(*Storage).IsLocked -} - -func isBoardOrThread(p *Post) bool { - return p.Level == LevelBoard || p.Level == LevelPost -} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/options.gno b/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/options.gno deleted file mode 100644 index 83287a53580..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/options.gno +++ /dev/null @@ -1,15 +0,0 @@ -package reputationplugin - -type Option func(*Plugin) - -func UseTokenBasePolicy() Option { - return func(p *Plugin) { - p.Policy = PolicyTokenBase - } -} - -func AllowedPostLevels(levels []int) Option { - return func(p *Plugin) { - p.AllowedPostLevels = levels - } -} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/reputation.gno b/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/reputation.gno deleted file mode 100644 index 13fcfa3facd..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/reputation.gno +++ /dev/null @@ -1,77 +0,0 @@ -package reputationplugin - -import "errors" - -const ( - PolicyLinear = iota - PolicyTokenBase -) - -const Name = "boards:reputation" - -var ErrNotSupported = errors.New("reputation not supported") - -type ( - Plugin struct { - Policy int - AllowedPostLevels []int - } - - Storage struct { - Upvotes uint - Downbotes uint - ListOfWhoVotedWhat avl.Tree // string(std.Address) -> ?? (TODO: define) - } -) - -func NewReputationPlugin(o ...Option) Plugin { - var p Plugin - for _, apply := range o { - apply(&p) - } - return p -} - -func (p Plugin) Name() string { - return Name -} - -func (p Plugin) HasReputationSupport(p *Post) bool { - if len(p.AllowedPostLevels) == 0 { - return true - } - - for _, lvl := range p.AllowedPostLevels { - if p.Level == lvl { - return true - } - } - return false -} - -func (p *Plugin) Votes(p *Post) uint32 { - if !p.HasReputationSupport(p) { - return ErrNotSupported - } - - // TODO: Implement -} - -func (p *Plugin) Upvote(p *Post) error { - if !p.HasReputationSupport(p) { - return ErrNotSupported - } - - // TODO: Modify global state - // TODO: Modify local state - // TODO: Implement - st := p.MustGetPluginStorage(p.Name()).(*Storage) -} - -func (p *Plugin) Downvote(p *Post) error { - if !p.HasReputationSupport(p) { - return ErrNotSupported - } - - // TODO: Implement -} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/post.gno b/examples/gno.land/p/demo/boardsv2/draft1/post.gno deleted file mode 100644 index 1498cda066f..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft1/post.gno +++ /dev/null @@ -1,45 +0,0 @@ -package boardsv2 - -import ( - "strconv" - "time" -) - -const ( - LevelBoard = iota - LevelPost - LevelComment -) - -type ( - Content interface { - Type() string - Render() string - } - - Post struct { - ID string - Content Content - PluginStorage avl.Tree // string(plugin name) -> interface{}(plugin storage) - Parent *Post - Level int - Base *Post - Children []*Post - Forks []*Post - UpdatedAt time.Time - CreatedAt time.Time - Creator std.Address - } -) - -func (p Post) MustGetPluginStorage(name string) interface{} { - if v, found := p.pluginStorage.Get(name); found { - return v - } - - panic("plugin storage not found: " + name) -} - -func (p Post) NextIncrementalKey(baseKey string) string { - return baseKey + "/" + strconv.Itoa(len(p.Children)) -} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/store.gno b/examples/gno.land/p/demo/boardsv2/draft1/store.gno deleted file mode 100644 index f8c98f3e724..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft1/store.gno +++ /dev/null @@ -1,9 +0,0 @@ -package boardsv2 - -// NOTE: Maybe we could abstract the location where posts are stored -type PostStore interface { - Set(*Post) (updated bool) - Get(level int, path string) (_ *Post, found bool) // NOTE: Level could be a type alias for better semantics - - // TODO: Add iterator (or define PostIterator interface) -} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/app.gno b/examples/gno.land/p/demo/boardsv2/draft2/app.gno deleted file mode 100644 index 30125302302..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft2/app.gno +++ /dev/null @@ -1,91 +0,0 @@ -package boards - -import ( - "plugin" - - "golang.org/x/mod/sumdb/storage" -) - -const ( - LevelBoard = iota - LevelThread - LevelComment -) - -type App struct { - cntx Context -} - -func New(st storage.PostStorage, o ...Option) App { - // TODO: avl.Tree feels wrong here but maybe get rid of the map anyway - p := map[plugin.Name]plugin.Plugin{ - plugintitle.Name: plugintitle.New(st), // content for boards - plugintext.Name: plugintext.New(st),// content for text based threads - pluginpoll.Name: pluginpoll.New(st),// content for poll based threads - plugincomment.Name: plugincomment.New(st),// content for comments to the threads - } - - c := Context{ - storage: st, - plugins: p, - } - - a := App{ - cntx: c, - } - - return a -} - -func (a App) Board(path string) (Board, error) { - a.c.Get(level, path func(){}) -} - -func (a App) Boards(c post.Cursor) ([]Board, error) { - -} - -func (a App) CreateBoard(c BoardContent) (Board, error) {} - -func (b App) Thread(path string) (Thread, error) { - return ThreadWithComments(path, nil) -} - -// Fork forks either a board or a thread by their path. -func (a App) Fork(path, newPath string) error {} - -// Lock locks either a board or a thread by their path. -// Once a board is locked new threads to the board and comments to the existing -// threads won't be allowed. -// Once a thread is locked new comments to the thread won't be allowed. -func (a App) Lock(path string) error {} - - -// ThreadWithComments returns a thread with its comments with the comment depth -// configured with commentDepth for direct and child comments. -// For ex. -// To get a thread with only 10 direct (parent level) comments use: -// - []int{10} -// To get a thread with 10 direct comments and 3 of their child comments use: -// - []int{10, 3} -// You can define configure this for more levels until you reach to value defined -// by MaxCommentDepth. -// By default the configuration is as follows: -// - []int{20, 3} -func (b App) ThreadWithComments(path string, commentDepth []int) (Thread, error) {} -func (b App) Threads(c post.Cursor) ([]Thread, error) {} -func (b App) CreateTextThread(c ThreadTextContent) (Thread, error) {} -func (b App) CreatePollThread(c ThreadPollContent) (Thread, error) {} - -// parentPath could be a path to thread (root), or path to any of the -// nested comments. -func (b App) Comments(parentPath string, c Cursor) ([]Comment, error) {} -func (b App) CreateComment(path string, c plugincomment.Content) (Comment, error) { - post, err := a.c.Plugin(plugincomment.Name).NewPost(c, LevelComment) - if err != nil { - return Comment{}, err - } - return Comment{Post: post, c: a.c} -} - -func (a App) Render(path string) string {} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/board.gno b/examples/gno.land/p/demo/boardsv2/draft2/board.gno deleted file mode 100644 index ac9258fa629..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft2/board.gno +++ /dev/null @@ -1,14 +0,0 @@ -package boards - -type Board struct { - post.Post - c Context -} - -func (b Board) Content() BoardContent { - return b.c.Plugin(pluginbasiccontent.Name).Content(b.Post) -} - -func (b Board) Render() string { - -} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/comment.gno b/examples/gno.land/p/demo/boardsv2/draft2/comment.gno deleted file mode 100644 index d66a77d1a67..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft2/comment.gno +++ /dev/null @@ -1,8 +0,0 @@ -package boards - -type Comment struct { - post.Post - c Context -} - -func (c Comment) Content() CommentContent {} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/context.gno b/examples/gno.land/p/demo/boardsv2/draft2/context.gno deleted file mode 100644 index 15c31e1db1d..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft2/context.gno +++ /dev/null @@ -1,38 +0,0 @@ -package boards - -import "plugin" - -type Context struct { - opts []Option - st post.Storage - plugs map[post.PluginName]plugin.Plugin -} - -func newContext() Context { - -} - -func (c Context) Plugin(n post.PluginName) post.Plugin { - -} - -func (c Context) Set(p *Post) (updated bool) { - key := newKey(p.Level, p.Slug()) - return b.posts.Set(key, p) -} - -func (c Context) Remove(level int, path string) (_ *Post, removed bool) { - key := newKey(level, path) - if v, removed := b.posts.Remove(key); removed { - return v.(*Post), true - } - return nil, false -} - -func (c Context) Get(level int, path string, iterator func()) (_ *Post, found bool) { - key := newKey(level, path) - if v, found := b.posts.Get(key); found { - return v.(*Post), true - } - return "", false -} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/option.gno b/examples/gno.land/p/demo/boardsv2/draft2/option.gno deleted file mode 100644 index 48ab0cc1bff..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft2/option.gno +++ /dev/null @@ -1,19 +0,0 @@ -package boards - -type Option struct{} - -// LinearReputationPolicy allows upvoting or downvoting a post by one -// for each account. -func LinearReputationPolicy() Option {} - -// TokenBasedReputationPolicy allows upvoting or downvoting a post propotional -// to the specified tokens that an account holds. -func TokenBasedReputationPolicy() Option {} - -// MaxPostDepth configures the max depth for nested comments. -// 0 -> boards -// 1 -> threads -// 2 -> comments-1 (direct comments to the threads) -// The above are already reserved. -// Setting it to zero will disable comments. -func MaxCommentDepth(d int) Option {} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/cursor.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/cursor.gno deleted file mode 100644 index 44f8ebe85f6..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft2/post/cursor.gno +++ /dev/null @@ -1,8 +0,0 @@ -package post - -type Cursor struct { - FromID string - Count int -} - -func NewCursor(fromID string, count int) Cursor {} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/comment/comment.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/comment/comment.gno deleted file mode 100644 index 2c4d7a53994..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/comment/comment.gno +++ /dev/null @@ -1,34 +0,0 @@ -package plugincomment - -const Name = "post-comment" - -func New(st Storage) Plugin { -} - -type Plugin struct { - postStorage Storage -} - -type Content struct { // Content of the comment. - Title string - Description string - Tags []string -} - -func (p Plugin) CreateComment(id string, c Content, level int) *post.Post { - pp := &post.Post{ - ID: id, - Level: level, - } - p.EditCommentContent(pp, c) - return pp -} - -func (p Plugin) Content(pst *post.Post) Content { - return pst.PluginStorage[Name].(Content) -} - -func (p Plugin) EditCommentContent(pp *Post, c Content) (updated bool) { - pp.PluginStorage[Name] = c - return p.postStorage.Set(post.ID, pp) -} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/lock/lock.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/lock/lock.gno deleted file mode 100644 index 5cd488aaabe..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/lock/lock.gno +++ /dev/null @@ -1 +0,0 @@ -package pluginlock diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/plugin.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/plugin.gno deleted file mode 100644 index 2f1cff7e627..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/plugin.gno +++ /dev/null @@ -1,7 +0,0 @@ -package plugin - -type Plugin interface { - Type() string -} - -type Name string diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/poll/poll.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/poll/poll.gno deleted file mode 100644 index db9c0bb40ce..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/poll/poll.gno +++ /dev/null @@ -1 +0,0 @@ -package pluginpoll diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/reputation/reputation.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/reputation/reputation.gno deleted file mode 100644 index 155fc53850f..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/reputation/reputation.gno +++ /dev/null @@ -1 +0,0 @@ -package pluginreputation diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/text/text.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/text/text.gno deleted file mode 100644 index a9f41c39947..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/text/text.gno +++ /dev/null @@ -1,3 +0,0 @@ -package plugintext - -const Name = "post-text" diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/title/title.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/title/title.gno deleted file mode 100644 index da2f8e905b5..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/title/title.gno +++ /dev/null @@ -1,24 +0,0 @@ -package plugintitle - -const Name = "post-title-only" - -func New(st Storage) Plugin { -} - -type Plugin struct { - st Storage -} - -type Content struct { - Title string - Description string - Tags []string -} - -func (p Plugin) Content(post *Post) Content { - return post.Body[Name].(Content) -} - -func (p Plugin) SetContent(post *Post, c Content) { - post.Body[Name] = c -} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/post.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/post.gno deleted file mode 100644 index 04b6b88f48c..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft2/post/post.gno +++ /dev/null @@ -1,26 +0,0 @@ -package boardsv2 - -import ( - "plugin" - "strconv" - "time" -) - -type ( - Post struct { - ID string - PluginStore plugin.Plugin - Parent *Post - Level int - Base *Post - Children []*Post - Forks []*Post - UpdatedAt time.Time - CreatedAt time.Time - Creator std.Address - } -) - -func (p Post) NextIncrementalKey(baseKey string) string { - return baseKey + "/" + strconv.Itoa(len(p.Children)) -} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/store/store.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/store/store.gno deleted file mode 100644 index 72440ea2a61..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft2/post/store/store.gno +++ /dev/null @@ -1 +0,0 @@ -package store diff --git a/examples/gno.land/p/demo/boardsv2/draft2/thread.gno b/examples/gno.land/p/demo/boardsv2/draft2/thread.gno deleted file mode 100644 index 42e731d4fd2..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft2/thread.gno +++ /dev/null @@ -1,18 +0,0 @@ -package boards - -type Thread struct { - post.Post - c Context -} - -func (t Thread) TextContent() ThreadTextContent { - -} - -func (t Thread) PollContent() ThreadPollContent {} -func (t Thread) Type() ContentType {} - -// Comments returns a list of comments sent to the thread. -// The comment slice will be non-nil only when Thread is initiated -// through ThreadWithComments. -func (t Thread) Comments() []Comment {} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/app.gno b/examples/gno.land/p/demo/boardsv2/draft3/app.gno deleted file mode 100644 index 3893e0866f7..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/app.gno +++ /dev/null @@ -1,133 +0,0 @@ -package boards - -import ( - "gno.land/p/demo/boards/post" - "gno.land/p/demo/boards/post/plugin" - pluginfork "gno.land/p/demo/boards/post/plugin/fork" - pluginpoll "gno.land/p/demo/boards/post/plugin/poll" - pluginreputation "gno.land/p/demo/boards/post/plugin/reputation" - plugintitle "gno.land/p/demo/boards/post/plugin/title" -) - -const ( - LevelBoard = iota - LevelThread - LevelComment -) - -// App is the boards application. -type App struct { - posts post.Store - plugins *plugin.Registry - maxCommentsDepth int - reputationPolicy pluginreputation.Policy -} - -// New creates a new boards application. -func New(st post.Store, options ...Option) App { - app := App{ - posts: st, - maxCommentsDepth: -1, // Infinite number of comments - } - for _, apply := range options { - apply(&app) - } - - app.plugins := plugin.NewRegistry( - plugintitle.New(st), // Plugin for boards - plugintext.New(st), // Plugin for text based threads - pluginpoll.New(st), // Plugin for poll based threads - plugincomment.New(st), // Plugin for comments to the threads - pluginreputation.New( - pluginreputation.UsePolicy(app.reputationPolicy), - pluginreputation.AllowedPostLevels(post.LevelPost, post.LevelComment), - ), - pluginfork.New( - pluginfork.AllowedPostLevels(post.LevelPost), - ), - ) - return app -} - -func (a App) GetBoard(path string) (_ Board, found bool) { - p, found := a.posts.GetByLevel(path, LevelBoard) - if !found { - return Board{}, false - } - return Board{p}, true -} - -func (a App) GetThread(path string) (_ Thread, found bool) { - p, found := a.posts.GetByLevel(path, LevelThread) - if !found { - return Thread{}, false - } - return Thread{p}, true -} - -func (a App) GetComment(path string) (_ Comment, found bool) { - p, found := a.posts.GetByLevel(path, LevelComment) - if !found { - return Comment{}, false - } - return Comment{p}, true -} - -func (a App) CreateBoard(slug, title, description string, tags []string) (Board, error) {} - -// Fork forks either a board or a thread by their path. -func (a App) ForkBoard(b Board, newPath string) error { - // NOTE: Instead of `app.ForkBoard()` we could use `b.Fork(newPath)` instead but that requires Board to have plugin access - // NOTE: This case gets the plugin from the plugin list to fork - p, _ := a.plugins.Get(pluginfork.Name) - return p.Fork(b.Post, newPath) -} - -func (a App) ForkThread(t Thread, newPath string) error { - // TODO: Implement thread fork app support -} - -// Lock locks either a board or a thread by their path. -// Once a board is locked new threads to the board and comments to the existing -// threads won't be allowed. -// Once a thread is locked new comments to the thread won't be allowed. -func (a App) Lock(path string) error {} - -// ---- TODO: Review the following list of app methods ----- // - -func (a App) Boards(c post.Cursor) ([]Board, error) { -} - -func (b App) Thread(path string) (Thread, error) { - return ThreadWithComments(path, nil) -} - -// ThreadWithComments returns a thread with its comments with the comment depth -// configured with commentDepth for direct and child comments. -// For ex. -// To get a thread with only 10 direct (parent level) comments use: -// - []int{10} -// To get a thread with 10 direct comments and 3 of their child comments use: -// - []int{10, 3} -// You can define configure this for more levels until you reach to value defined -// by MaxCommentDepth. -// By default the configuration is as follows: -// - []int{20, 3} -func (b App) ThreadWithComments(path string, commentDepth []int) (Thread, error) {} -func (b App) Threads(c post.Cursor) ([]Thread, error) {} -func (b App) CreateTextThread(c ThreadTextContent) (Thread, error) {} -func (b App) CreatePollThread(c ThreadPollContent) (Thread, error) {} - -// parentPath could be a path to thread (root), or path to any of the -// nested comments. -func (b App) Comments(parentPath string, c Cursor) ([]Comment, error) {} - -func (b App) CreateComment(path string, c plugincomment.Content) (Comment, error) { - post, err := a.c.Plugin(plugincomment.Name).NewPost(c, LevelComment) - if err != nil { - return Comment{}, err - } - return Comment{Post: post, c: a.c} -} - -func (a App) Render(path string) string {} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/board.gno b/examples/gno.land/p/demo/boardsv2/draft3/board.gno deleted file mode 100644 index b75db758a79..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/board.gno +++ /dev/null @@ -1,52 +0,0 @@ -package boards - -import ( - "gno.land/p/demo/boards/post" - pluginfork "gno.land/p/demo/boards/post/plugin/fork" - pluginreputation "gno.land/p/demo/boards/post/plugin/reputation" - plugintitle "gno.land/p/demo/boards/post/plugin/title" -) - -type ( - BoardContent plugintitle.Content - - Board struct { - *post.Post - } -) - -func NewBoard(pst *post.Post) Board { - // TODO: Local plugins must be initialized here (same for other plugins) - return Board{pst} -} - -func (b Board) Info() BoardContent { - return BoardContent(b.getContent()) -} - -func (b Board) Update(c BoardContent) { - b.PluginStore[plugintitle.Name] = plugintitle.Content(c) -} - -func (b Board) Upvote() error { - r := b.getReputation() - return r.Upvote(b.Post) -} - -func (b Board) Downvote() error { - r := b.getReputation() - return r.Downvote(b.Post) -} - -func (b Board) Render() string { - c := b.getContent() - return c.Render() -} - -func (b Board) getContent() *plugintitle.Content { - return b.PluginStore[plugintitle.Name].(*plugintitle.Content) -} - -func (b Board) getReputation() *pluginreputation.Reputation { - return b.PluginStore[pluginreputation.Name].(*pluginreputation.Reputation) -} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/comment.gno b/examples/gno.land/p/demo/boardsv2/draft3/comment.gno deleted file mode 100644 index d66a77d1a67..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/comment.gno +++ /dev/null @@ -1,8 +0,0 @@ -package boards - -type Comment struct { - post.Post - c Context -} - -func (c Comment) Content() CommentContent {} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/context.gno b/examples/gno.land/p/demo/boardsv2/draft3/context.gno deleted file mode 100644 index f3139abccb4..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/context.gno +++ /dev/null @@ -1,36 +0,0 @@ -package boards - -import "plugin" - -type Context struct { - opts []Option - st post.Store - plugs map[post.PluginName]plugin.Plugin -} - -func newContext() Context { -} - -func (c Context) Plugin(n post.PluginName) post.Plugin { -} - -func (c Context) Set(p *Post) (updated bool) { - key := newKey(p.Level, p.Slug()) - return b.posts.Set(key, p) -} - -func (c Context) Remove(level int, path string) (_ *Post, removed bool) { - key := newKey(level, path) - if v, removed := b.posts.Remove(key); removed { - return v.(*Post), true - } - return nil, false -} - -func (c Context) Get(level int, path string, iterator func()) (_ *Post, found bool) { - key := newKey(level, path) - if v, found := b.posts.Get(key); found { - return v.(*Post), true - } - return "", false -} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/options.gno b/examples/gno.land/p/demo/boardsv2/draft3/options.gno deleted file mode 100644 index 906b1130663..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/options.gno +++ /dev/null @@ -1,31 +0,0 @@ -package boards - -import ( - pluginreputation "gno.land/p/demo/boards/post/plugin/reputation" -) - -// Option configures board applications. -type Options func(*App) - -// LinearReputationPolicy allows upvoting or downvoting a post by one for each account. -func LinearReputationPolicy() Option { - return func(a *App) { - a.reputationPolicy = pluginreputation.PolicyLinear - } -} - -// TokenBasedReputationPolicy allows upvoting or downvoting -// a post propotional to the specified tokens that an account holds. -func TokenBasedReputationPolicy() Option { - return func(a *App) { - a.reputationPolicy = pluginreputation.PolicyTokenBased - } -} - -// MaxCommentsDepth configures the max depth for nested comments. -// Setting it to -1 allows an infinite number of nested comments (default). -func MaxCommentsDepth(d int) Option { - return func(a *App) { - a.maxCommentsDepth = d - } -} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/comment/comment.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/comment/comment.gno deleted file mode 100644 index f7b1eed2e5e..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/comment/comment.gno +++ /dev/null @@ -1,50 +0,0 @@ -package plugincomment - -const Name = "post-comment" - -type ( - Plugin struct { - posts post.Store - } - - // Content is the comment's content. - Content struct { - Title string - Description string - Tags []string - } -) - -func New(st post.Store) Plugin { - return Plugin{ - posts: st, - } -} - -func (p Plugin) Name() string { - return Name -} - -func (p Plugin) Render() string { - // TODO: Implement render support for comments - return "" -} - -func (p Plugin) CreateComment(id string, c Content, level int) *post.Post { - pst := &post.Post{ - ID: id, - Level: level, - } - p.SetContent(pst, c) - return pst -} - -func (p Plugin) Content(pst *post.Post) (_ *Content, ok bool) { - c, ok := pst.PluginStore[Name].(*Content) - return c, ok -} - -func (p Plugin) SetContent(pst *post.Post, c Content) (updated bool) { - ps.PluginStore[Name] = c - return p.posts.Set(pst) -} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/fork.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/fork.gno deleted file mode 100644 index 543f18b6e8d..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/fork.gno +++ /dev/null @@ -1,47 +0,0 @@ -package pluginfork - -import ( - "gno.land/p/demo/boards/post" -) - -const Name = "fork" - -// TODO: Implement fork plugin to support thread forking -type Plugin struct { - AllowedPostLevels []int -} - -func New(o ...Option) Plugin { - var p Plugin - for _, apply := range o { - apply(&p) - } - return p -} - -func (p Plugin) Name() string { - return Name -} - -func (p Plugin) Render() string { - // TODO: Implement render support for text - return "" -} - -func (p Plugin) HasForkSupport(pst *post.Post) bool { - if len(p.AllowedPostLevels) == 0 { - return true - } - - for _, lvl := range p.AllowedPostLevels { - if pst.Level == lvl { - return true - } - } - return false -} - -func (p Plugin) Fork(pst *post.Post, newPath string) error { - // TODO: Implement fork support - return nil -} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/options.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/options.gno deleted file mode 100644 index 85fe888bcb1..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/options.gno +++ /dev/null @@ -1,9 +0,0 @@ -package pluginfork - -type Option func(*Plugin) - -func AllowedPostLevels(levels []int) Option { - return func(p *Plugin) { - p.AllowedPostLevels = levels - } -} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/lock/lock.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/lock/lock.gno deleted file mode 100644 index fe75dd364c5..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/lock/lock.gno +++ /dev/null @@ -1,55 +0,0 @@ -package pluginlock - -import "errors" - -const Name = "lock" - -var ErrInvalidPostType = errors.New("post type is not a board or thread") - -type ( - Plugin struct{} - Lock struct { - IsLocked bool - } -) - -func New() Plugin { - return Plugin{} -} - -func (p Plugin) Name() string { - return Name -} - -func (p Plugin) Render() string { - return "" -} - -func (p *Plugin) Lock(pst *post.Post) error { - if !isBoardOrThread(pst) { - return ErrInvalidPostType - } - - pst.PluginStore[Name].(*Lock).IsLocked = true -} - -func (p *Plugin) Unlock(pst *post.Post) error { - if !isBoardOrThread(pst) { - return ErrInvalidPostType - } - - pst.PluginStore[Name].(*Lock).IsLocked = false -} - -func (p Plugin) IsLocked(pst *post.Post) bool { - if !isBoardOrThread(pst) { - return ErrInvalidPostType - } - - // TODO: Check parents if current post is not locked - return pst.PluginStore[Name].(*Lock).IsLocked -} - -func isBoardOrThread(pst *post.Post) bool { - return pst.Level == post.LevelBoard || pst.Level == post.LevelPost -} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/plugin.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/plugin.gno deleted file mode 100644 index 5a42dc8bd10..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/plugin.gno +++ /dev/null @@ -1,48 +0,0 @@ -// TODO: Document how plugins work and best practices -package plugin - -import ( - "gno.land/p/demo/avl" -) - -type ( - // NOTE: Consider adding lifecycle methods like `Post` creation, deletion, ... - Plugin interface { - Name() string - Render() string - } - - Registry struct { - plugins avl.Tree // string(name) -> Plugin - } -) - -func NewRegistry(plugins ...Plugin) *Registry { - r := &Registry{} - for _, p := range plugins { - r.plugins.Set(p.Name(), p) - } - return r -} - -func (r Registry) Has(name string) bool { - return r.posts.Has(name) -} - -func (r Registry) Get(name string) (_ Plugin, found bool) { - if v, found := r.plugins.Get(name); found { - return v.(Plugin), true - } - return nil, false -} - -func (r *Registry) Add(p Plugin) { - r.plugins.Set(p.Name(), p) -} - -func (r *Registry) Remove(name string) (_ Plugin, removed bool) { - if v, removed := r.plugins.Remove(name, p); removed { - return v.(Plugin), false - } - return nil, false -} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/poll/poll.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/poll/poll.gno deleted file mode 100644 index a20a4a22dd7..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/poll/poll.gno +++ /dev/null @@ -1,47 +0,0 @@ -package pluginpoll - -const Name = "post-poll" - -type ( - Plugin struct { - posts post.Store - } - - Poll struct { - Question string - Options []string - Votes []struct { - Address std.Adress - Option string - } - Tags []string - } -) - -func New(st post.Store) Plugin { - return Plugin{ - posts: st, - } -} - -func (p Plugin) Name() string { - return Name -} - -func (p Plugin) Render() string { - return "" -} - -func (p Plugin) CreatePoll(id string, v Poll) *post.Post { - pst := &post.Post{ - ID: id, - Level: LevelPost, - } - p.SetPoll(pst, v) - return pst -} - -func (p Plugin) SetPoll(pst *post.Post, v Poll) (updated bool) { - pst.PluginStore[Name] = v - return p.posts.Set(pst.ID, pst) -} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/options.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/options.gno deleted file mode 100644 index a87b45bf1e0..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/options.gno +++ /dev/null @@ -1,15 +0,0 @@ -package pluginreputation - -type Option func(*Plugin) - -func UsePolicy(v Policy) Option { - return func(p *Plugin) { - p.Policy = v - } -} - -func AllowedPostLevels(levels []int) Option { - return func(p *Plugin) { - p.AllowedPostLevels = levels - } -} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/reputation.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/reputation.gno deleted file mode 100644 index 0ec8e1406af..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/reputation.gno +++ /dev/null @@ -1,99 +0,0 @@ -package pluginreputation - -import ( - "errors" - "std" -) - -// NOTE: Think about implementing a reputation based policy -const ( - PolicyLinear Policy = iota - PolicyTokenBased -) - -const Name = "reputation" - -var ErrNotSupported = errors.New("reputation not supported") - -type ( - Policy int - - Plugin struct { - Store Store - Policy Policy - AllowedPostLevels []int - } - - Reputation struct { - Upvotes uint - Downvotes uint - } -) - -func New(o ...Option) Plugin { - var p Plugin - for _, apply := range o { - apply(&p) - } - return p -} - -func (p Plugin) Name() string { - return Name -} - -func (p Plugin) Render() string { - return "" -} - -func (p Plugin) HasReputationSupport(pst *post.Post) bool { - if len(p.AllowedPostLevels) == 0 { - return true - } - - for _, lvl := range p.AllowedPostLevels { - if pst.Level == lvl { - return true - } - } - return false -} - -func (p Plugin) Votes(pst *post.Post) (upvotes uint64, downvotes uint64) { - if !p.HasReputationSupport(pst) { - return ErrNotSupported - } - - r := pst.PluginStore[Name].(*Reputation) - return r.Upvotes, r.Downvotes -} - -func (p Plugin) Voters(pst *post.Post) []std.Address { - if !p.HasReputationSupport(pst) { - return ErrNotSupported - } - - // TODO: Implement support for tracking voters -} - -func (p *Plugin) Upvote(pst *post.Post) error { - if !p.HasReputationSupport(pst) { - return ErrNotSupported - } - - // TODO: Handle accounts and change downvotes for existing accounts that downvoted - r := pst.PluginStore[Name].(*Reputation) - r.Upvotes++ - p.store.inc(pst.ID) -} - -func (p *Plugin) Downvote(pst *post.Post) error { - if !p.HasReputationSupport(pst) { - return ErrNotSupported - } - - // TODO: Handle accounts and change upvotes for existing accounts that upvoted - r := pst.PluginStore[Name].(*Reputation) - r.Downvotes++ - p.store.dec(pst.ID) -} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/store.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/store.gno deleted file mode 100644 index 9661210f830..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/store.gno +++ /dev/null @@ -1,58 +0,0 @@ -package pluginreputation - -import ( - "gno.land/p/demo/seqid" -) - -type ( - VotesIterFn = func(votes uint64, path string) bool - - Store struct { - votes avl.Tree // string(count) -> string(path) - } -) - -func (s Store) Iterate(fn VotesIterFn) bool { - // TODO: Support pagination of votes? - return s.votes.Iterate("", "", func(key string, v interface{}) bool { - count, _ := seqid.FromBinary(key) - return fn(uint64(count), v.(string)) - }) -} - -func (s *Store) inc(path string) uint64 { - var ( - current seqid.ID - v, found = s.votes.Get(path) - ) - if found { - current = v.(seqid.ID) - // TODO: Implement the right solution because this is not right, just showcase - s.votes.Remove(current.Binary()) - } - - current.Next() - s.votes.Set(current.Binary(), path) - return uint64(current) -} - -func (s *Store) dec(path string) uint64 { - var ( - current seqid.ID - v, found = s.votes.Get(path) - ) - if found { - current = v.(seqid.ID) - } - - if current == 0 { - return current - } - - s.votes.Remove(current.Binary()) - current-- - if current != 0 { - s.votes.Set(current.Binary(), current) - } - return uint64(current) -} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/text/text.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/text/text.gno deleted file mode 100644 index 014956e367b..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/text/text.gno +++ /dev/null @@ -1,38 +0,0 @@ -// plugintext is a content type for representing a tweet, blog post or a thread like Reddit. -package plugintext - -import ( - "gno.land/p/demo/boards/post" // NOTE: Plugins should be at the same level of post package -) - -const Name = "post-text" - -type ( - Plugin struct{} - Content struct { - Title string - Body string - Tags []string - } -) - -func New() Plugin { - return Plugin{} -} - -func (p Plugin) Name() string { - return Name -} - -func (p Plugin) Render() string { - // TODO: Implement render support for text - return "" -} - -func (p Plugin) Content(pst *post.Post) Content { - return pst.Body[Name].(*Content) -} - -func (p Plugin) SetContent(pst *post.Post, c Content) { - pst.Body[Name] = c -} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/title/title.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/title/title.gno deleted file mode 100644 index 506f32001a8..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/title/title.gno +++ /dev/null @@ -1,35 +0,0 @@ -// plugintext is a content type for representing organizations, categories or sections. -package plugintitle - -const Name = "post-title" - -type ( - Plugin struct{} - - Content struct { - Title string - Description string - Tags []string - } -) - -func New() Plugin { - return Plugin{} -} - -func (p Plugin) Name() string { - return Name -} - -func (p Plugin) Render() string { - // TODO: Implement render support for title - return "" -} - -func (p Plugin) Content(pst *post.Post) Content { - return pst.Body[Name].(*Content) -} - -func (p Plugin) SetContent(pst *post.Post, c Content) { - pst.Body[Name] = c -} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/post.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/post.gno deleted file mode 100644 index 229a904fd66..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/post/post.gno +++ /dev/null @@ -1,25 +0,0 @@ -package post - -import ( - "strconv" - "time" - - "gno.land/p/demo/boards/post/plugin" -) - -type Post struct { - ID string - PluginStore plugin.Plugin - Parent *Post - Level int - Base *Post - Children []*Post - Forks []*Post - UpdatedAt time.Time - CreatedAt time.Time - Creator std.Address -} - -func (p Post) NextIncrementalKey(baseKey string) string { - return baseKey + "/" + strconv.Itoa(len(p.Children)) -} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/store.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/store.gno deleted file mode 100644 index 49513f250db..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/post/store.gno +++ /dev/null @@ -1,31 +0,0 @@ -package post - -func NewStore() Store { - return Store{} -} - -// TODO: Implement posts store -type Store struct { - posts avl.Tree // string(level + creation timestamp + slug) -> *Post - slugs avl.Tree // string(slug) -> *Post -} - -func (s Store) Get(path string) (_ *Post, found bool) { - if v, found := s.slugs.Get(path); found { - return v.(*Post), true - } - return nil, false -} - -func (s Store) GetByLevel(path string, level int) (_ *Post, found bool) { - v, found := s.slugs.Get(path) - if !found { - return nil, false - } - - p := v.(*Post) - if p.Level != level { - return nil, false - } - return p, true -} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/store/cursor.gno b/examples/gno.land/p/demo/boardsv2/draft3/store/cursor.gno deleted file mode 100644 index c4fc379c28d..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/store/cursor.gno +++ /dev/null @@ -1,9 +0,0 @@ -package store - -// TODO: Define how cursors should be used alongside stores -type Cursor struct { - FromID string - Count int -} - -func NewCursor(fromID string, count int) Cursor {} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/store/store.gno b/examples/gno.land/p/demo/boardsv2/draft3/store/store.gno deleted file mode 100644 index 5c7fba3db4d..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/store/store.gno +++ /dev/null @@ -1,6 +0,0 @@ -package store - -// TODO: Define a storage interface and create and avl.Tree wrapper -type Store interface{} - -type AVLTreeStore struct{} // TODO: Use IAVL instead if there is a package implemented diff --git a/examples/gno.land/p/demo/boardsv2/draft3/thread.gno b/examples/gno.land/p/demo/boardsv2/draft3/thread.gno deleted file mode 100644 index cce35799b6f..00000000000 --- a/examples/gno.land/p/demo/boardsv2/draft3/thread.gno +++ /dev/null @@ -1,64 +0,0 @@ -package boards - -import ( - "gno.land/p/demo/boards/post" - pluginfork "gno.land/p/demo/boards/post/plugin/fork" - pluginpoll "gno.land/p/demo/boards/post/plugin/poll" - pluginreputation "gno.land/p/demo/boards/post/plugin/reputation" - plugintext "gno.land/p/demo/boards/post/plugin/text" -) - -type ( - ThreadContent plugintext.Content - - // TODO: Should polls be handler within this type? - Thread struct { - *post.Post - } -) - -func (t Thread) Info() ThreadContent { - return ThreadContent(t.getContent()) -} - -func (t Thread) Update(c ThreadContent) { - t.PluginStore[plugintext.Name] = plugintext.Content(c) -} - -func (t Thread) Upvote() error { - r := t.getReputation() - return r.Upvote(t.Post) -} - -func (t Thread) Downvote() error { - r := t.getReputation() - return r.Downvote(t.Post) -} - -func (t Thread) Fork(newPath string) error { - f := t.getFork() - return f.Fork(t.Post) -} - -func (t Thread) Render() string { - c := t.getContent() - return c.Render() -} - -// Comments returns a list of comments sent to the thread. -// The comment slice will be non-nil only when Thread is initiated -// through ThreadWithComments. -// TODO: Add support to get sub-threads (any type) and comments -// func (t Thread) Comments() []Comment {} - -func (t Thread) getContent() *plugintext.Content { - return t.PluginStore[plugintext.Name].(*plugintext.Content) -} - -func (t Thread) getReputation() *pluginreputation.Reputation { - return t.PluginStore[pluginreputation.Name].(*pluginreputation.Reputation) -} - -func (t Thread) getFork() *pluginfork.Fork { - return t.PluginStore[pluginfork.Name].(*pluginfork.Fork) -} diff --git a/examples/gno.land/r/demo/boardsv2/boardsv2.gno b/examples/gno.land/r/demo/boardsv2/boardsv2.gno deleted file mode 100644 index 4205c210da5..00000000000 --- a/examples/gno.land/r/demo/boardsv2/boardsv2.gno +++ /dev/null @@ -1,44 +0,0 @@ -package boardsv2 - -import "gno.land/p/demo/avl" - -// TODO: This goes in the realm -// type Boards struct { -// // TODO: Define how do we want to display and sort boards and posts (upvotes, pinned, ...) -// boards avl.Tree -// Title string -// Description string -// } - -func Render(path string) string { - // TODO: Implement render - return "" -} - -// TODO: Define public API - -func CreateBoard() {} // Maybe -func EditBoard() {} // Maybe -func ForkBoard() {} // Maybe - -func CreatePost() {} -func EditPost() {} -func ForkPost() {} -func DeletePost() {} -func Repost() {} -func Pin() {} -func Invite() {} // Maybe: Could also rely on an allow list -func UpVote() {} -func DownVote() {} - -func Comment() {} // Maybe -func EditComment() {} // Maybe -func DeleteComment() {} // Maybe - -func ToggleCommentsSupport() {} // Maybe -func ToggleThreadsSupport() {} // Maybe -func GetTags() {} // Maybe: List of allowed tags (moderated) - -func AddModerator() {} // Maybe -func RemoveModerator() {} // Maybe -func GetModerators() {} // Maybe diff --git a/examples/gno.land/r/demo/boardsv2/draft2/main.gno b/examples/gno.land/r/demo/boardsv2/draft2/main.gno deleted file mode 100644 index 6f8d203d31a..00000000000 --- a/examples/gno.land/r/demo/boardsv2/draft2/main.gno +++ /dev/null @@ -1,15 +0,0 @@ -package boards - -var postStore = avl.Tree{} // string(level + timestamp + slug) -> *Post - -func newApp() boards.App { // stateless approach for App struct - return boards.New( - postStore, - boards.MaxCommentDepth(10), - boards.LinearReputationPolicy(), - ) -} - -func Boards(c post.Cursor) ([]boards.Board, error) { - return newApp().Boards(c) -} diff --git a/examples/gno.land/r/demo/boardsv2/draft3/boards.gno b/examples/gno.land/r/demo/boardsv2/draft3/boards.gno deleted file mode 100644 index efc316fa458..00000000000 --- a/examples/gno.land/r/demo/boardsv2/draft3/boards.gno +++ /dev/null @@ -1,68 +0,0 @@ -package boards - -import ( - "std" - - "gno.land/p/demo/boards" - "gno.land/p/demo/boards/post" -) - -var app = boards.New( - post.NewStore(), - boards.MaxCommentDepth(10), - boards.LinearReputationPolicy(), -) - -func Render(path string) string { - // TODO: Define how to render the tree of boards, posts and comments - return "" -} - -func CreateBoard(slug, title, description string, tags []string) (path string) { - creator := std.GetOrigCaller() - board := app.CreateBoard(slug, title, description, tags, creator) - return board.ID -} - -func Lock(path string) { - post := getBoardOrThread(path) - if post == nil { - panic("path doesn't exist or locking this path not supported") - } - - assertOrigCallerIsCreator(post) - - // NOTE: Explore if it's better to use Post or Board/Thread types - if err := app.Lock(post); err != nil { - panic(err) - } -} - -func Fork(path, newPath string) { - post := getBoardOrThread(path) - if post == nil { - panic("path doesn't exist or forking this path not supported") - } - - // TODO: Use this way - app.ForkBoard(board) - app.ForkThread(thread) - - if err := app.Fork(post, newPath); err != nil { - panic(err) - } -} - -func getBoardOrThread(path string) *post.Post { - p, found := app.GetPost(path) - if found && (p.Level == boards.LevelBoard || p.Level == boards.LevelThread) { - return p - } - return nil -} - -func assertOrigCallerIsCreator(p *post.Post) { - if post.Creator != std.GetOrigCaller() { - panic("original caller is not allowed to perform this action") - } -} From 1510a6f0328f862a678bd76a29ac17d3ff85ca4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Tue, 12 Nov 2024 09:07:44 +0100 Subject: [PATCH 04/52] chore: copy `boards` realm to `boards2` (#3110) Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> Co-authored-by: Jae Kwon <53785+jaekwon@users.noreply.github.com> Co-authored-by: Jeff Thompson Co-authored-by: Hariom Verma Co-authored-by: Morgan Co-authored-by: Albert Le Batteux Co-authored-by: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Co-authored-by: Blake <104744707+r3v4s@users.noreply.github.com> Co-authored-by: Jeff Thompson Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Co-authored-by: Poroburu Co-authored-by: deelawn Co-authored-by: grepsuzette <350354+grepsuzette@users.noreply.github.com> Co-authored-by: jon roethke --- examples/gno.land/r/demo/boards2/board.gno | 139 +++++++++++ examples/gno.land/r/demo/boards2/boards.gno | 22 ++ examples/gno.land/r/demo/boards2/gno.mod | 7 + examples/gno.land/r/demo/boards2/misc.gno | 95 +++++++ examples/gno.land/r/demo/boards2/post.gno | 263 ++++++++++++++++++++ examples/gno.land/r/demo/boards2/public.gno | 185 ++++++++++++++ examples/gno.land/r/demo/boards2/render.gno | 83 ++++++ examples/gno.land/r/demo/boards2/role.gno | 8 + 8 files changed, 802 insertions(+) create mode 100644 examples/gno.land/r/demo/boards2/board.gno create mode 100644 examples/gno.land/r/demo/boards2/boards.gno create mode 100644 examples/gno.land/r/demo/boards2/gno.mod create mode 100644 examples/gno.land/r/demo/boards2/misc.gno create mode 100644 examples/gno.land/r/demo/boards2/post.gno create mode 100644 examples/gno.land/r/demo/boards2/public.gno create mode 100644 examples/gno.land/r/demo/boards2/render.gno create mode 100644 examples/gno.land/r/demo/boards2/role.gno diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno new file mode 100644 index 00000000000..79b27da84b2 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -0,0 +1,139 @@ +package boards + +import ( + "std" + "strconv" + "time" + + "gno.land/p/demo/avl" + "gno.land/p/moul/txlink" +) + +//---------------------------------------- +// Board + +type BoardID uint64 + +func (bid BoardID) String() string { + return strconv.Itoa(int(bid)) +} + +type Board struct { + id BoardID // only set for public boards. + url string + name string + creator std.Address + threads avl.Tree // Post.id -> *Post + postsCtr uint64 // increments Post.id + createdAt time.Time + 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 { + panic("board already exists") + } + return &Board{ + id: id, + url: url, + name: name, + creator: creator, + threads: avl.Tree{}, + createdAt: time.Now(), + deleted: avl.Tree{}, + } +} + +/* TODO support this once we figure out how to ensure URL correctness. +// A private board is not tracked by gBoards*, +// but must be persisted by the caller's realm. +// Private boards have 0 id and does not ping +// back the remote board on reposts. +func NewPrivateBoard(url string, name string, creator std.Address) *Board { + return newBoard(0, url, name, creator) +} +*/ + +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 + } + return postI.(*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) + 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) + if !removed { + panic("thread does not exist with id " + pid.String()) + } +} + +func (board *Board) HasPermission(addr std.Address, perm Permission) bool { + if board.creator == addr { + switch perm { + case EditPermission: + return true + case DeletePermission: + return true + default: + return false + } + } + 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" + if board.threads.Size() > 0 { + board.threads.Iterate("", "", func(key string, value interface{}) bool { + if str != "" { + str += "----------------------------------------\n" + } + str += value.(*Post).RenderSummary() + "\n" + return false + }) + } + return str +} + +func (board *Board) incGetPostID() PostID { + board.postsCtr++ + 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) GetPostFormURL() string { + return txlink.URL("CreateThread", "bid", board.id.String()) +} diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno new file mode 100644 index 00000000000..5de0555a2f9 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -0,0 +1,22 @@ +package boards + +import ( + "regexp" + + "gno.land/p/demo/avl" +) + +//---------------------------------------- +// Realm (package) state + +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}$`) diff --git a/examples/gno.land/r/demo/boards2/gno.mod b/examples/gno.land/r/demo/boards2/gno.mod new file mode 100644 index 00000000000..1738959bf31 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/gno.mod @@ -0,0 +1,7 @@ +module gno.land/r/demo/boards2 + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/moul/txlink v0.0.0-latest + gno.land/r/demo/users v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/boards2/misc.gno b/examples/gno.land/r/demo/boards2/misc.gno new file mode 100644 index 00000000000..bc561ca7d22 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/misc.gno @@ -0,0 +1,95 @@ +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 new file mode 100644 index 00000000000..95d4b2977ba --- /dev/null +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -0,0 +1,263 @@ +package boards + +import ( + "std" + "strconv" + "time" + + "gno.land/p/demo/avl" + "gno.land/p/moul/txlink" +) + +//---------------------------------------- +// Post + +// NOTE: a PostID is relative to the board. +type PostID uint64 + +func (pid PostID) String() string { + return strconv.Itoa(int(pid)) +} + +// A Post is a "thread" or a "reply" depending on context. +// A thread is a Post of a Board that holds other replies. +type Post struct { + board *Board + id PostID + creator std.Address + title string // optional + body string + replies avl.Tree // Post.id -> *Post + repliesAll avl.Tree // Post.id -> *Post (all replies, for top-level posts) + reposts avl.Tree // Board.id -> Post.id + threadID PostID // original Post.id + parentID PostID // parent Post.id (if reply or repost) + repostBoard BoardID // original Board.id (if repost) + createdAt time.Time + updatedAt time.Time +} + +func newPost(board *Board, id PostID, creator std.Address, title, body string, threadID, parentID PostID, repostBoard BoardID) *Post { + return &Post{ + board: board, + id: id, + creator: creator, + title: title, + body: body, + replies: avl.Tree{}, + repliesAll: avl.Tree{}, + reposts: avl.Tree{}, + threadID: threadID, + parentID: parentID, + repostBoard: repostBoard, + createdAt: time.Now(), + } +} + +func (post *Post) IsThread() bool { + return post.parentID == 0 +} + +func (post *Post) GetPostID() PostID { + return post.id +} + +func (post *Post) AddReply(creator std.Address, body string) *Post { + board := post.board + pid := board.incGetPostID() + pidkey := postIDKey(pid) + reply := newPost(board, pid, creator, "", body, post.threadID, post.id, 0) + post.replies.Set(pidkey, reply) + if post.threadID == post.id { + post.repliesAll.Set(pidkey, reply) + } else { + thread := board.GetThread(post.threadID) + thread.repliesAll.Set(pidkey, reply) + } + return reply +} + +func (post *Post) Update(title string, body string) { + post.title = title + post.body = body + 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 (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) + if !dst.IsPrivate() { + bidkey := boardIDKey(dst.id) + post.reposts.Set(bidkey, pid) + } + return repost +} + +func (thread *Post) DeletePost(pid PostID) { + if thread.id == pid { + panic("should not happen") + } + pidkey := postIDKey(pid) + postI, removed := thread.repliesAll.Remove(pidkey) + if !removed { + panic("post not found in thread") + } + post := postI.(*Post) + if post.parentID != thread.id { + parent := thread.GetReply(post.parentID) + parent.replies.Remove(pidkey) + } else { + thread.replies.Remove(pidkey) + } +} + +func (post *Post) HasPermission(addr std.Address, perm Permission) bool { + if post.creator == addr { + switch perm { + case EditPermission: + return true + case DeletePermission: + return true + default: + return false + } + } + // post notes inherit permissions of the board. + return post.board.HasPermission(addr, perm) +} + +func (post *Post) GetSummary() string { + return summaryOf(post.body, 80) +} + +func (post *Post) GetURL() string { + if post.IsThread() { + return post.board.GetURLFromThreadAndReplyID( + post.id, 0) + } else { + return post.board.GetURLFromThreadAndReplyID( + post.threadID, post.id) + } +} + +func (post *Post) GetReplyFormURL() string { + return txlink.URL("CreateReply", + "bid", post.board.id.String(), + "threadid", post.threadID.String(), + "postid", post.id.String(), + ) +} + +func (post *Post) GetRepostFormURL() string { + return txlink.URL("CreateRepost", + "bid", post.board.id.String(), + "postid", post.id.String(), + ) +} + +func (post *Post) GetDeleteFormURL() string { + return txlink.URL("DeletePost", + "bid", post.board.id.String(), + "threadid", post.threadID.String(), + "postid", post.id.String(), + ) +} + +func (post *Post) RenderSummary() string { + if post.repostBoard != 0 { + dstBoard := getBoard(post.repostBoard) + 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 := "" + if post.title != "" { + str += "## [" + summaryOf(post.title, 80) + "](" + post.GetURL() + ")\n" + str += "\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 +} + +func (post *Post) RenderPost(indent string, levels int) string { + if post == nil { + return "nil post" + } + str := "" + if post.title != "" { + str += indent + "# " + post.title + "\n" + str += 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() + ")]" + if post.IsThread() { + str += " \\[[repost](" + post.GetRepostFormURL() + ")]" + } + str += " \\[[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) + return false + }) + } + } else { + if post.replies.Size() > 0 { + str += indent + "\n" + str += indent + "_[see all " + strconv.Itoa(post.replies.Size()) + " replies](" + post.GetURL() + ")_\n" + } + } + return str +} + +// 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 + if thread.id == parentID { + parent = thread + } else { + parent = thread.GetReply(parentID) + } + str += parent.RenderPost("", 0) + str += "\n" + str += post.RenderPost("> ", 5) + return str +} diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno new file mode 100644 index 00000000000..1d26126fcb2 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -0,0 +1,185 @@ +package boards + +import ( + "std" + "strconv" +) + +//---------------------------------------- +// Public facing functions + +func GetBoardIDFromName(name string) (BoardID, bool) { + boardI, exists := gBoardsByName.Get(name) + if !exists { + return 0, false + } + return boardI.(*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") + } + url := "/r/demo/boards:" + name + board := newBoard(bid, url, name, caller) + bidkey := boardIDKey(bid) + gBoards.Set(bidkey, 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") + } + 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 +} + +func CreateReply(bid BoardID, threadid, postid PostID, 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.GetThread(threadid) + if thread == nil { + panic("thread not exist") + } + if postid == threadid { + reply := thread.AddReply(caller, body) + return reply.id + } else { + post := thread.GetReply(postid) + 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") + } + 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") + } + 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 +} + +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) { + 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) + } +} + +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) { + 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) + } +} diff --git a/examples/gno.land/r/demo/boards2/render.gno b/examples/gno.land/r/demo/boards2/render.gno new file mode 100644 index 00000000000..3709ad02e5d --- /dev/null +++ b/examples/gno.land/r/demo/boards2/render.gno @@ -0,0 +1,83 @@ +package boards + +import ( + "strconv" + "strings" +) + +//---------------------------------------- +// Render functions + +func RenderBoard(bid BoardID) string { + board := getBoard(bid) + if board == nil { + return "missing board" + } + return board.RenderBoard() +} + +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 + } + 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() + } else { + return "unrecognized path " + path + } +} diff --git a/examples/gno.land/r/demo/boards2/role.gno b/examples/gno.land/r/demo/boards2/role.gno new file mode 100644 index 00000000000..64073d64f34 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/role.gno @@ -0,0 +1,8 @@ +package boards + +type Permission string + +const ( + DeletePermission Permission = "role:delete" + EditPermission Permission = "role:edit" +) From 4c7d16b3006e316ea026a42706e44361953932da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Wed, 13 Nov 2024 17:04:06 +0100 Subject: [PATCH 05/52] feat: simplify `boards2` implementation (#3115) Refactors the code copied from `gno.land/r/demo/boards` to simplify it and to have it ready before introducing the new features. --- examples/gno.land/r/demo/boards2/board.gno | 89 +++---- examples/gno.land/r/demo/boards2/boards.gno | 60 +++-- examples/gno.land/r/demo/boards2/format.gno | 65 +++++ examples/gno.land/r/demo/boards2/misc.gno | 95 -------- examples/gno.land/r/demo/boards2/post.gno | 191 ++++++++------- examples/gno.land/r/demo/boards2/public.gno | 254 +++++++++----------- examples/gno.land/r/demo/boards2/render.gno | 158 ++++++------ examples/gno.land/r/demo/boards2/role.gno | 6 +- 8 files changed, 470 insertions(+), 448 deletions(-) create mode 100644 examples/gno.land/r/demo/boards2/format.gno delete mode 100644 examples/gno.land/r/demo/boards2/misc.gno 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" ) From 1f6d61f39615d3e800201135a8aff2b56841a098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Wed, 20 Nov 2024 08:43:17 +0100 Subject: [PATCH 06/52] test(boards2): unit tests for `Board` and `Post` types (#3150) Co-authored-by: x1unix --- examples/gno.land/r/demo/boards2/board.gno | 4 + .../gno.land/r/demo/boards2/board_test.gno | 158 ++++++++ examples/gno.land/r/demo/boards2/post.gno | 121 ++++-- .../gno.land/r/demo/boards2/post_test.gno | 375 ++++++++++++++++++ 4 files changed, 617 insertions(+), 41 deletions(-) create mode 100644 examples/gno.land/r/demo/boards2/board_test.gno create mode 100644 examples/gno.land/r/demo/boards2/post_test.gno diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index 0df39f7ebfb..16f42f786c5 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -64,6 +64,10 @@ func (board *Board) IsPrivate() bool { return board.id == 0 } +func (board *Board) GetID() BoardID { + return board.id +} + // GetURL returns the relative URL of the board. func (board *Board) GetURL() string { return strings.TrimPrefix(std.CurrentRealm().PkgPath(), "gno.land") + ":" + board.name diff --git a/examples/gno.land/r/demo/boards2/board_test.gno b/examples/gno.land/r/demo/boards2/board_test.gno new file mode 100644 index 00000000000..9a6c51e8ea9 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/board_test.gno @@ -0,0 +1,158 @@ +package boards + +import ( + "std" + "strings" + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/moul/txlink" +) + +func TestBoardID_String(t *testing.T) { + input := BoardID(32) + + uassert.Equal(t, "32", input.String()) +} + +func TestBoardID_Key(t *testing.T) { + input := BoardID(128) + want := strings.Repeat("0", 7) + "128" + uassert.Equal(t, want, input.Key()) +} + +func TestBoard_IsPrivate(t *testing.T) { + b := new(Board) + b.id = 0 + uassert.True(t, b.IsPrivate()) + + b.id = 128 + uassert.False(t, b.IsPrivate()) +} + +func TestBoard_GetID(t *testing.T) { + want := int(92) + b := new(Board) + b.id = BoardID(want) + got := int(b.GetID()) + + uassert.Equal(t, got, want) + uassert.NotEqual(t, got, want*want) +} + +func TestBoard_GetURL(t *testing.T) { + pkgPath := strings.TrimPrefix(std.CurrentRealm().PkgPath(), "gno.land") + name := "foobar_test_get_url123" + want := pkgPath + ":" + name + + var addr std.Address + + board := newBoard(1, name, addr) + got := board.GetURL() + uassert.Equal(t, want, got) +} + +func TestBoard_GetThread(t *testing.T) { + var addr std.Address + b := newBoard(1, "test123", addr) + + _, ok := b.GetThread(12345) + uassert.False(t, ok) + + post := b.AddThread(addr, "foo", "bar") + _, ok = b.GetThread(post.GetPostID()) + uassert.True(t, ok) +} + +func TestBoard_DeleteThread(t *testing.T) { + var addr std.Address + b := newBoard(1, "test123", addr) + + post := b.AddThread(addr, "foo", "bar") + id := post.GetPostID() + + b.DeleteThread(id) + + _, ok := b.GetThread(id) + uassert.False(t, ok) +} + +func TestBoard_HasPermission(t *testing.T) { + var ( + alice std.Address = "012345" + bob std.Address = "cafebabe" + ) + + cases := []struct { + label string + creator std.Address + actor std.Address + perm Permission + expect bool + }{ + { + label: "creator should be able to edit board", + expect: true, + creator: alice, + actor: alice, + perm: PermissionEdit, + }, + { + label: "creator should be able to delete board", + expect: true, + creator: alice, + actor: alice, + perm: PermissionDelete, + }, + { + label: "guest shouldn't be able to edit boards", + expect: false, + creator: alice, + actor: bob, + perm: PermissionEdit, + }, + { + label: "guest shouldn't be able to delete boards", + expect: false, + creator: alice, + actor: bob, + perm: PermissionDelete, + }, + } + + for i, c := range cases { + t.Run(c.label, func(t *testing.T) { + b := newBoard(BoardID(i), "test12345", c.creator) + got := b.HasPermission(c.actor, c.perm) + uassert.Equal(t, c.expect, got) + }) + } +} + +var boardUrlPrefix = strings.TrimPrefix(std.CurrentRealm().PkgPath(), "gno.land") + +func TestBoard_GetURLFromThreadID(t *testing.T) { + boardName := "test12345" + b := newBoard(BoardID(11), boardName, "") + want := boardUrlPrefix + ":" + boardName + "/10" + + got := b.GetURLFromThreadID(10) + uassert.Equal(t, want, got) +} + +func TestBoard_GetURLFromReplyID(t *testing.T) { + boardName := "test12345" + b := newBoard(BoardID(11), boardName, "") + want := boardUrlPrefix + ":" + boardName + "/10/20" + + got := b.GetURLFromReplyID(10, 20) + uassert.Equal(t, want, got) +} + +func TestBoard_GetPostFormURL(t *testing.T) { + bid := BoardID(386) + b := newBoard(bid, "foo1234", "") + expect := txlink.URL("CreateThread", "bid", bid.String()) + got := b.GetPostFormURL() + uassert.Equal(t, expect, got) +} diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index 5f9ceae2f5e..a2a19a1b618 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -1,6 +1,7 @@ package boards import ( + "errors" "std" "strconv" "time" @@ -24,35 +25,35 @@ 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. type Post struct { - board *Board - id PostID - creator std.Address - title string // optional - body string - replies avl.Tree // Post.id -> *Post - repliesAll avl.Tree // Post.id -> *Post (all replies, for top-level posts) - reposts avl.Tree // Board.id -> Post.id - threadID PostID // original Post.id - parentID PostID // parent Post.id (if reply or repost) - repostBoard BoardID // original Board.id (if repost) - createdAt time.Time - updatedAt time.Time + board *Board + id PostID + creator std.Address + title string // optional + body string + replies avl.Tree // Post.id -> *Post + repliesAll avl.Tree // Post.id -> *Post (all replies, for top-level posts) + reposts avl.Tree // Board.id -> Post.id + threadID PostID // original Post.id + parentID PostID // parent Post.id (if reply or repost) + repostBoardID BoardID // original Board.id (if repost) + createdAt time.Time + updatedAt time.Time } -func newPost(board *Board, id PostID, creator std.Address, title, body string, threadID, parentID PostID, repostBoard BoardID) *Post { +func newPost(board *Board, id PostID, creator std.Address, title, body string, threadID, parentID PostID, repostBoardID BoardID) *Post { return &Post{ - board: board, - id: id, - creator: creator, - title: title, - body: body, - replies: avl.Tree{}, - repliesAll: avl.Tree{}, - reposts: avl.Tree{}, - threadID: threadID, - parentID: parentID, - repostBoard: repostBoard, - createdAt: time.Now(), + board: board, + id: id, + creator: creator, + title: title, + body: body, + replies: avl.Tree{}, + repliesAll: avl.Tree{}, + reposts: avl.Tree{}, + threadID: threadID, + parentID: parentID, + repostBoardID: repostBoardID, + createdAt: time.Now(), } } @@ -60,10 +61,42 @@ func (post *Post) IsThread() bool { return post.parentID == 0 } +func (post *Post) GetBoard() *Board { + return post.board +} + func (post *Post) GetPostID() PostID { return post.id } +func (post *Post) GetParentID() PostID { + return post.parentID +} + +func (post *Post) GetRepostBoardID() BoardID { + return post.repostBoardID +} + +func (post *Post) GetCreator() std.Address { + return post.creator +} + +func (post *Post) GetTitle() string { + return post.title +} + +func (post *Post) GetBody() string { + return post.body +} + +func (post *Post) GetCreatedAt() time.Time { + return post.createdAt +} + +func (post *Post) GetUpdatedAt() time.Time { + return post.updatedAt +} + func (post *Post) AddReply(creator std.Address, body string) *Post { board := post.board pid := board.incGetPostID() @@ -108,24 +141,30 @@ func (post *Post) AddRepostTo(creator std.Address, title, body string, dst *Boar return repost } -func (thread *Post) DeleteReply(replyID PostID) { - if thread.id == replyID { - panic("should not happen") +func (post *Post) DeleteReply(replyID PostID) error { + if !post.IsThread() { + // TODO: Allow removing replies from parent replies too + panic("cannot delete reply from a non-thread post") + } + + if post.id == replyID { + return errors.New("expected an ID of an inner reply") } key := replyID.Key() - v, removed := thread.repliesAll.Remove(key) + v, removed := post.repliesAll.Remove(key) if !removed { - panic("reply not found in thread") + return errors.New("reply not found in thread") } - post := v.(*Post) - if post.parentID != thread.id { - parent, _ := thread.GetReply(post.parentID) + reply := v.(*Post) + if reply.parentID != post.id { + parent, _ := post.GetReply(reply.parentID) parent.replies.Remove(key) } else { - thread.replies.Remove(key) + post.replies.Remove(key) } + return nil } // TODO: Change HasPermission to use a new authorization interface's `CanDo()` @@ -158,15 +197,15 @@ func (post *Post) GetURL() string { func (post *Post) GetReplyFormURL() string { return txlink.URL("CreateReply", "bid", post.board.id.String(), - "threadid", post.threadID.String(), - "postid", post.id.String(), + "threadID", post.threadID.String(), + "postID", post.id.String(), ) } func (post *Post) GetRepostFormURL() string { return txlink.URL("CreateRepost", "bid", post.board.id.String(), - "postid", post.id.String(), + "postID", post.id.String(), ) } @@ -185,10 +224,10 @@ func (post *Post) GetDeleteFormURL() string { } func (post *Post) RenderSummary() string { - if post.repostBoard != 0 { - dstBoard, found := getBoard(post.repostBoard) + if post.repostBoardID != 0 { + dstBoard, found := getBoard(post.repostBoardID) if !found { - panic("repostBoard does not exist") + panic("repost board does not exist") } thread, found := dstBoard.GetThread(PostID(post.parentID)) diff --git a/examples/gno.land/r/demo/boards2/post_test.gno b/examples/gno.land/r/demo/boards2/post_test.gno new file mode 100644 index 00000000000..f4ec444f0cc --- /dev/null +++ b/examples/gno.land/r/demo/boards2/post_test.gno @@ -0,0 +1,375 @@ +package boards + +import ( + "strings" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" +) + +func TestPostUpdate(t *testing.T) { + board := newBoard(1, "test123", testutils.TestAddress("creator")) + creator := testutils.TestAddress("creator") + post := newPost(board, 1, creator, "Title", "Body", 1, 0, 0) + title := "New Title" + body := "New body" + + post.Update(title, body) + + uassert.Equal(t, title, post.GetTitle()) + uassert.Equal(t, body, post.GetBody()) + uassert.False(t, post.GetUpdatedAt().IsZero()) +} + +func TestPostAddRepostTo(t *testing.T) { + cases := []struct { + name, title, body string + dstBoard *Board + thread *Post + setup func() *Post + err string + }{ + { + name: "repost thread", + title: "Repost Title", + body: "Repost body", + dstBoard: newBoard(42, "dst123", testutils.TestAddress("creatorDstBoard")), + setup: func() *Post { return createTestThread(t) }, + }, + { + name: "invalid repost from reply", + setup: func() *Post { return createTestReply(t) }, + err: "cannot repost non-thread post", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var ( + repost *Post + creator = testutils.TestAddress("repostCreator") + thread = tc.setup() + ) + + createRepost := func() { + repost = thread.AddRepostTo(creator, tc.title, tc.body, tc.dstBoard) + } + + if tc.err != "" { + uassert.PanicsWithMessage(t, tc.err, createRepost) + return + } else { + uassert.NotPanics(t, createRepost) + } + + r, found := tc.dstBoard.GetThread(repost.GetPostID()) + uassert.True(t, found) + uassert.True(t, repost == r) + uassert.Equal(t, tc.title, repost.GetTitle()) + uassert.Equal(t, tc.body, repost.GetBody()) + uassert.Equal(t, uint(thread.GetBoard().GetID()), uint(repost.GetRepostBoardID())) + }) + } +} + +func TestNewThread(t *testing.T) { + creator := testutils.TestAddress("creator") + member := testutils.TestAddress("member") + title := "Test Title" + body := strings.Repeat("A", 82) + boardID := BoardID(1) + threadID := PostID(42) + boardName := "test123" + board := newBoard(boardID, boardName, creator) + url := ufmt.Sprintf( + "/r/demo/boards2:%s/%d", + boardName, + uint(threadID), + ) + replyURL := ufmt.Sprintf( + "/r/demo/boards2$help&func=CreateReply&bid=%d&threadID=%d&postID=%d", + uint(boardID), + uint(threadID), + uint(threadID), + ) + repostURL := ufmt.Sprintf( + "/r/demo/boards2$help&func=CreateRepost&bid=%d&postID=%d", + uint(boardID), + uint(threadID), + ) + deleteURL := ufmt.Sprintf( + "/r/demo/boards2$help&func=DeleteThread&bid=%d&threadID=%d", + uint(boardID), + uint(threadID), + ) + + thread := newPost(board, threadID, creator, title, body, threadID, 0, 0) + + uassert.True(t, thread.IsThread()) + uassert.Equal(t, uint(threadID), uint(thread.GetPostID())) + uassert.False(t, thread.GetCreatedAt().IsZero()) + uassert.True(t, thread.GetUpdatedAt().IsZero()) + uassert.Equal(t, title, thread.GetTitle()) + uassert.Equal(t, body[:77]+"...", thread.GetSummary()) + uassert.Equal(t, url, thread.GetURL()) + uassert.Equal(t, replyURL, thread.GetReplyFormURL()) + uassert.Equal(t, repostURL, thread.GetRepostFormURL()) + uassert.Equal(t, deleteURL, thread.GetDeleteFormURL()) + uassert.True(t, thread.HasPermission(creator, PermissionEdit)) + uassert.True(t, thread.HasPermission(creator, PermissionDelete)) + uassert.False(t, thread.HasPermission(creator, Permission("unknown"))) + uassert.False(t, thread.HasPermission(member, PermissionEdit)) + uassert.False(t, thread.HasPermission(member, PermissionDelete)) + uassert.False(t, thread.HasPermission(member, Permission("unknown"))) +} + +func TestThreadAddReply(t *testing.T) { + replier := testutils.TestAddress("replier") + thread := createTestThread(t) + threadID := uint(thread.GetPostID()) + body := "A reply" + + reply := thread.AddReply(replier, body) + + r, found := thread.GetReply(reply.GetPostID()) + uassert.True(t, found) + uassert.True(t, reply == r) + uassert.Equal(t, threadID+1, uint(reply.GetPostID())) + uassert.Equal(t, reply.GetCreator(), replier) + uassert.Equal(t, reply.GetBody(), body) +} + +func TestThreadGetReply(t *testing.T) { + cases := []struct { + name string + thread *Post + setup func(thread *Post) (replyID PostID) + found bool + }{ + { + name: "found", + thread: createTestThread(t), + setup: func(thread *Post) PostID { + reply := thread.AddReply(testutils.TestAddress("replier"), "") + return reply.GetPostID() + }, + found: true, + }, + { + name: "not found", + thread: createTestThread(t), + setup: func(*Post) PostID { return 42 }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + replyID := tc.setup(tc.thread) + + reply, found := tc.thread.GetReply(replyID) + + uassert.Equal(t, tc.found, found) + if reply != nil { + uassert.Equal(t, uint(replyID), uint(reply.GetPostID())) + } + }) + } +} + +func TestThreadDeleteReply(t *testing.T) { + thread := createTestThread(t) + cases := []struct { + name string + setup func() PostID + err string + }{ + { + name: "ok", + setup: func() PostID { + reply := thread.AddReply(testutils.TestAddress("replier"), "") + return reply.GetPostID() + }, + }, + { + name: "ok nested", + setup: func() PostID { + reply := thread.AddReply(testutils.TestAddress("replier"), "") + return reply.AddReply(testutils.TestAddress("replier2"), "").GetPostID() + }, + }, + { + name: "invalid", + setup: func() PostID { return thread.GetPostID() }, + err: "expected an ID of an inner reply", + }, + { + name: "not found", + setup: func() PostID { return 42 }, + err: "reply not found in thread", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + replyID := tc.setup() + + err := thread.DeleteReply(replyID) + + if tc.err != "" { + uassert.ErrorContains(t, err, tc.err) + return + } + + uassert.NoError(t, err) + _, found := thread.GetReply(replyID) + uassert.False(t, found) + }) + } +} + +func TestThreadRenderSummary(t *testing.T) { + t.Skip("TODO: implement") +} + +func TestThreadRender(t *testing.T) { + t.Skip("TODO: implement") +} + +func TestThreadRenderInner(t *testing.T) { + t.Skip("TODO: implement") +} + +func TestNewReply(t *testing.T) { + creator := testutils.TestAddress("creator") + member := testutils.TestAddress("member") + body := strings.Repeat("A", 82) + boardID := BoardID(1) + threadID := PostID(42) + parentID := PostID(1) + replyID := PostID(2) + boardName := "test123" + board := newBoard(boardID, boardName, creator) + url := ufmt.Sprintf( + "/r/demo/boards2:%s/%d/%d", + boardName, + uint(threadID), + uint(replyID), + ) + replyURL := ufmt.Sprintf( + "/r/demo/boards2$help&func=CreateReply&bid=%d&threadID=%d&postID=%d", + uint(boardID), + uint(threadID), + uint(replyID), + ) + deleteURL := ufmt.Sprintf( + "/r/demo/boards2$help&func=DeleteReply&bid=%d&threadID=%d&replyID=%d", + uint(boardID), + uint(threadID), + uint(replyID), + ) + + reply := newPost(board, replyID, creator, "", body, threadID, parentID, 0) + + uassert.False(t, reply.IsThread()) + uassert.Equal(t, uint(replyID), uint(reply.GetPostID())) + uassert.False(t, reply.GetCreatedAt().IsZero()) + uassert.True(t, reply.GetUpdatedAt().IsZero()) + uassert.Equal(t, body[:77]+"...", reply.GetSummary()) + uassert.Equal(t, url, reply.GetURL()) + uassert.Equal(t, replyURL, reply.GetReplyFormURL()) + uassert.Equal(t, deleteURL, reply.GetDeleteFormURL()) + uassert.True(t, reply.HasPermission(creator, PermissionEdit)) + uassert.True(t, reply.HasPermission(creator, PermissionDelete)) + uassert.False(t, reply.HasPermission(creator, Permission("unknown"))) + uassert.False(t, reply.HasPermission(member, PermissionEdit)) + uassert.False(t, reply.HasPermission(member, PermissionDelete)) + uassert.False(t, reply.HasPermission(member, Permission("unknown"))) +} + +func TestReplyAddReply(t *testing.T) { + replier := testutils.TestAddress("replier") + thread := createTestThread(t) + parentReply := thread.AddReply(testutils.TestAddress("parentReplier"), "") + threadID := uint(thread.GetPostID()) + parentReplyID := uint(parentReply.GetPostID()) + body := "A child reply" + + reply := parentReply.AddReply(replier, body) + + r, found := thread.GetReply(reply.GetPostID()) + uassert.True(t, found) + uassert.True(t, reply == r) + uassert.Equal(t, parentReplyID, uint(reply.GetParentID())) + uassert.Equal(t, parentReplyID+1, uint(reply.GetPostID())) + uassert.Equal(t, reply.GetCreator(), replier) + uassert.Equal(t, reply.GetBody(), body) +} + +func TestReplyGetReply(t *testing.T) { + thread := createTestThread(t) + parentReply := thread.AddReply(testutils.TestAddress("parentReplier"), "") + cases := []struct { + name string + setup func() PostID + found bool + }{ + { + name: "found", + setup: func() PostID { + reply := parentReply.AddReply(testutils.TestAddress("replier"), "") + return reply.GetPostID() + }, + found: true, + }, + { + name: "not found", + setup: func() PostID { return 42 }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + replyID := tc.setup() + + reply, found := thread.GetReply(replyID) + + uassert.Equal(t, tc.found, found) + if reply != nil { + uassert.Equal(t, uint(replyID), uint(reply.GetPostID())) + } + }) + } +} + +func TestReplyDeleteReply(t *testing.T) { + thread := createTestThread(t) + parentReply := thread.AddReply(testutils.TestAddress("replier"), "") + reply := parentReply.AddReply(testutils.TestAddress("replier2"), "") + + // NOTE: Deleting a reply from a parent reply should eventually be suported + uassert.PanicsWithMessage(t, "cannot delete reply from a non-thread post", func() { + parentReply.DeleteReply(reply.GetPostID()) + }) +} + +func TestReplyRender(t *testing.T) { + t.Skip("TODO: implement") +} + +func createTestThread(t *testing.T) *Post { + t.Helper() + + creator := testutils.TestAddress("creator") + board := newBoard(1, "test_board_123", creator) + return board.AddThread(creator, "Title", "Body") +} + +func createTestReply(t *testing.T) *Post { + t.Helper() + + creator := testutils.TestAddress("replier") + thread := createTestThread(t) + return thread.AddReply(creator, "Test message") +} From 7446197ee8d9f70c2b0bc8db045072b8e53ee2de Mon Sep 17 00:00:00 2001 From: Denys Sedchenko Date: Fri, 22 Nov 2024 11:56:44 -0500 Subject: [PATCH 07/52] feat(boards2): update package name to "boards2" (#3184) Use correct package name for boards2 realm. --- examples/gno.land/r/demo/boards2/board.gno | 2 +- examples/gno.land/r/demo/boards2/board_test.gno | 2 +- examples/gno.land/r/demo/boards2/boards.gno | 2 +- examples/gno.land/r/demo/boards2/format.gno | 2 +- examples/gno.land/r/demo/boards2/gno.mod | 4 ++++ examples/gno.land/r/demo/boards2/post.gno | 2 +- examples/gno.land/r/demo/boards2/post_test.gno | 2 +- examples/gno.land/r/demo/boards2/public.gno | 2 +- examples/gno.land/r/demo/boards2/render.gno | 2 +- examples/gno.land/r/demo/boards2/role.gno | 2 +- 10 files changed, 13 insertions(+), 9 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index 16f42f786c5..8942ad8f2df 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -1,4 +1,4 @@ -package boards +package boards2 import ( "regexp" diff --git a/examples/gno.land/r/demo/boards2/board_test.gno b/examples/gno.land/r/demo/boards2/board_test.gno index 9a6c51e8ea9..d4fcc212ed5 100644 --- a/examples/gno.land/r/demo/boards2/board_test.gno +++ b/examples/gno.land/r/demo/boards2/board_test.gno @@ -1,4 +1,4 @@ -package boards +package boards2 import ( "std" diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno index fa487a3914a..f50d779426f 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -1,4 +1,4 @@ -package boards +package boards2 import "gno.land/p/demo/avl" diff --git a/examples/gno.land/r/demo/boards2/format.gno b/examples/gno.land/r/demo/boards2/format.gno index da29918fdae..a316b42cb5b 100644 --- a/examples/gno.land/r/demo/boards2/format.gno +++ b/examples/gno.land/r/demo/boards2/format.gno @@ -1,4 +1,4 @@ -package boards +package boards2 import ( "std" diff --git a/examples/gno.land/r/demo/boards2/gno.mod b/examples/gno.land/r/demo/boards2/gno.mod index 1738959bf31..3872857d2b7 100644 --- a/examples/gno.land/r/demo/boards2/gno.mod +++ b/examples/gno.land/r/demo/boards2/gno.mod @@ -2,6 +2,10 @@ module gno.land/r/demo/boards2 require ( gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/mux v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/uassert v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/moul/txlink v0.0.0-latest gno.land/r/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index a2a19a1b618..38035ddaec3 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -1,4 +1,4 @@ -package boards +package boards2 import ( "errors" diff --git a/examples/gno.land/r/demo/boards2/post_test.gno b/examples/gno.land/r/demo/boards2/post_test.gno index f4ec444f0cc..42529da8483 100644 --- a/examples/gno.land/r/demo/boards2/post_test.gno +++ b/examples/gno.land/r/demo/boards2/post_test.gno @@ -1,4 +1,4 @@ -package boards +package boards2 import ( "strings" diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index cb1a4b5eef6..5cf1f1ae08d 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -1,4 +1,4 @@ -package boards +package boards2 import ( "std" diff --git a/examples/gno.land/r/demo/boards2/render.gno b/examples/gno.land/r/demo/boards2/render.gno index bd9e8676c24..02e7fda9e81 100644 --- a/examples/gno.land/r/demo/boards2/render.gno +++ b/examples/gno.land/r/demo/boards2/render.gno @@ -1,4 +1,4 @@ -package boards +package boards2 import ( "strconv" diff --git a/examples/gno.land/r/demo/boards2/role.gno b/examples/gno.land/r/demo/boards2/role.gno index fdfc2b5d7ef..b33f46c6472 100644 --- a/examples/gno.land/r/demo/boards2/role.gno +++ b/examples/gno.land/r/demo/boards2/role.gno @@ -1,4 +1,4 @@ -package boards +package boards2 // TODO: Rename file to "auth.gno" and define a new interface From 4944a7e4fe5990f895890810dc226b9e2d1b2ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Tue, 26 Nov 2024 10:26:09 +0100 Subject: [PATCH 08/52] feat(boards2): initial permissions support (#3151) Related to #3139 Permissioner interface is defined based on Jae's idea to handle permissioned tasks. --------- Co-authored-by: Jae Kwon <53785+jaekwon@users.noreply.github.com> --- .../p/demo/boards2/admindao/admindao.gno | 46 +++ .../p/demo/boards2/admindao/admindao_test.gno | 88 ++++++ .../gno.land/p/demo/boards2/admindao/gno.mod | 7 + .../p/demo/boards2/admindao/options.gno | 20 ++ examples/gno.land/r/demo/boards2/acl.gno | 100 +++++++ .../gno.land/r/demo/boards2/acl_options.gno | 30 ++ examples/gno.land/r/demo/boards2/acl_test.gno | 271 ++++++++++++++++++ examples/gno.land/r/demo/boards2/board.gno | 26 -- .../gno.land/r/demo/boards2/board_test.gno | 52 ---- examples/gno.land/r/demo/boards2/boards.gno | 23 +- examples/gno.land/r/demo/boards2/gno.mod | 2 + .../gno.land/r/demo/boards2/permission.gno | 36 +++ examples/gno.land/r/demo/boards2/post.gno | 17 +- .../gno.land/r/demo/boards2/post_test.gno | 12 - examples/gno.land/r/demo/boards2/public.gno | 148 +++++++--- examples/gno.land/r/demo/boards2/role.gno | 10 - 16 files changed, 729 insertions(+), 159 deletions(-) create mode 100644 examples/gno.land/p/demo/boards2/admindao/admindao.gno create mode 100644 examples/gno.land/p/demo/boards2/admindao/admindao_test.gno create mode 100644 examples/gno.land/p/demo/boards2/admindao/gno.mod create mode 100644 examples/gno.land/p/demo/boards2/admindao/options.gno create mode 100644 examples/gno.land/r/demo/boards2/acl.gno create mode 100644 examples/gno.land/r/demo/boards2/acl_options.gno create mode 100644 examples/gno.land/r/demo/boards2/acl_test.gno create mode 100644 examples/gno.land/r/demo/boards2/permission.gno delete mode 100644 examples/gno.land/r/demo/boards2/role.gno diff --git a/examples/gno.land/p/demo/boards2/admindao/admindao.gno b/examples/gno.land/p/demo/boards2/admindao/admindao.gno new file mode 100644 index 00000000000..4e1d49e435e --- /dev/null +++ b/examples/gno.land/p/demo/boards2/admindao/admindao.gno @@ -0,0 +1,46 @@ +package admindao + +import ( + "std" + + "gno.land/p/demo/avl" +) + +// TODO: Add support for proposals +// TODO: Add support for events + +// AdminDAO defines a Boards administration DAO. +type AdminDAO struct { + parent *AdminDAO + members *avl.Tree // string(std.Address) -> struct{} +} + +// New creates a new admin DAO. +func New(options ...Option) *AdminDAO { + dao := &AdminDAO{members: avl.NewTree()} + for _, apply := range options { + apply(dao) + } + return dao +} + +// Parent returns the parent DAO. +// Null can be returned when DAO has no parent assigned. +func (dao AdminDAO) Parent() *AdminDAO { + return dao.parent +} + +// Members returns the list of DAO members. +func (dao AdminDAO) Members() []std.Address { + var members []std.Address + dao.members.Iterate("", "", func(key string, _ interface{}) bool { + members = append(members, std.Address(key)) + return false + }) + return members +} + +// IsMember checks if a user is a member of the DAO. +func (dao AdminDAO) IsMember(user std.Address) bool { + return dao.members.Has(user.String()) +} diff --git a/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno b/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno new file mode 100644 index 00000000000..171f5618892 --- /dev/null +++ b/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno @@ -0,0 +1,88 @@ +package admindao + +import ( + "std" + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func TestNew(t *testing.T) { + cases := []struct { + name string + parent *AdminDAO + members []std.Address + }{ + { + name: "with parent", + parent: New(), + members: []std.Address{"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"}, + }, + { + name: "without parent", + members: []std.Address{"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"}, + }, + { + name: "multiple members", + members: []std.Address{ + "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", + "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc", + }, + }, + { + name: "no members", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + options := []Option{WithParent(tc.parent)} + for _, m := range tc.members { + options = append(options, WithMember(m)) + } + + dao := New(options...) + + if tc.parent == nil { + uassert.Equal(t, nil, dao.Parent()) + } else { + uassert.NotEqual(t, nil, dao.Parent()) + } + + urequire.Equal(t, len(tc.members), len(dao.Members()), "dao members") + for i, m := range dao.Members() { + uassert.Equal(t, tc.members[i], m) + } + }) + } +} + +func TestAdminDAOIsMember(t *testing.T) { + cases := []struct { + name string + member std.Address + dao *AdminDAO + want bool + }{ + { + name: "member", + member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", + dao: New(WithMember("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn")), + want: true, + }, + { + name: "not a dao member", + member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", + dao: New(WithMember("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc")), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := tc.dao.IsMember(tc.member) + uassert.Equal(t, got, tc.want) + }) + } +} diff --git a/examples/gno.land/p/demo/boards2/admindao/gno.mod b/examples/gno.land/p/demo/boards2/admindao/gno.mod new file mode 100644 index 00000000000..5067fdbcbcc --- /dev/null +++ b/examples/gno.land/p/demo/boards2/admindao/gno.mod @@ -0,0 +1,7 @@ +module gno.land/p/demo/boards2/admindao + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/uassert v0.0.0-latest + gno.land/p/demo/urequire v0.0.0-latest +) diff --git a/examples/gno.land/p/demo/boards2/admindao/options.gno b/examples/gno.land/p/demo/boards2/admindao/options.gno new file mode 100644 index 00000000000..66ee99f1cba --- /dev/null +++ b/examples/gno.land/p/demo/boards2/admindao/options.gno @@ -0,0 +1,20 @@ +package admindao + +import "std" + +// Option configures the AdminDAO. +type Option func(*AdminDAO) + +// WithParent assigns a parent DAO. +func WithParent(p *AdminDAO) Option { + return func(dao *AdminDAO) { + dao.parent = p + } +} + +// WithMember assigns a member to the DAO. +func WithMember(addr std.Address) Option { + return func(dao *AdminDAO) { + dao.members.Set(addr.String(), struct{}{}) + } +} diff --git a/examples/gno.land/r/demo/boards2/acl.gno b/examples/gno.land/r/demo/boards2/acl.gno new file mode 100644 index 00000000000..631591e1214 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/acl.gno @@ -0,0 +1,100 @@ +package boards2 + +import ( + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/boards2/admindao" +) + +// TODO: Support to deal with permissions for anonymous users? + +const ( + RoleOwner Role = "owner" + RoleAdmin = "admin" + RoleModerator = "moderator" +) + +type ( + // Role defines the type for user roles. + Role string + + // ACL or access control list manages user roles and permissions. + ACL struct { + superRole Role + dao *admindao.AdminDAO + users *avl.Tree // string(std.Address) -> []Role + roles *avl.Tree // string(role) -> []Permission + } +) + +// NewACL create a new access control list. +func NewACL(dao *admindao.AdminDAO, options ...ACLOption) *ACL { + acl := &ACL{ + dao: dao, + roles: avl.NewTree(), + users: avl.NewTree(), + } + for _, apply := range options { + apply(acl) + } + return acl +} + +// Roles returns the list of roles. +func (acl ACL) Roles() []Role { + var roles []Role + acl.roles.Iterate("", "", func(name string, _ interface{}) bool { + roles = append(roles, Role(name)) + return false + }) + return roles +} + +// GetUserRoles returns the list of roles assigned to a user. +func (acl ACL) GetUserRoles(user std.Address) []Role { + v, found := acl.users.Get(user.String()) + if !found { + return nil + } + return v.([]Role) +} + +// HasRole checks if a user has a specific role assigned. +func (acl ACL) HasRole(user std.Address, r Role) bool { + for _, role := range acl.GetUserRoles(user) { + if role == r { + return true + } + } + return false +} + +// HasPermission checks if a user has a specific permission. +func (acl ACL) HasPermission(user std.Address, perm Permission) bool { + // TODO: Should we check that the user belongs to the DAO? + for _, r := range acl.GetUserRoles(user) { + v, found := acl.roles.Get(string(r)) + if !found { + continue + } + + for _, p := range v.([]Permission) { + if p == perm { + return true + } + } + } + return false +} + +// WithPermission calls a callback when a user has a specific permission. +// It panics on error. +func (acl ACL) WithPermission(user std.Address, perm Permission, a Args, cb func(Args)) { + if !acl.HasPermission(user, perm) || !acl.dao.IsMember(user) { + panic("unauthorized") + } + + // TODO: Support DAO proposals that run the callback on proposal execution + cb(a) +} diff --git a/examples/gno.land/r/demo/boards2/acl_options.gno b/examples/gno.land/r/demo/boards2/acl_options.gno new file mode 100644 index 00000000000..6a481470766 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/acl_options.gno @@ -0,0 +1,30 @@ +package boards2 + +import "std" + +// ACLOption configures an ACL. +type ACLOption func(*ACL) + +// WithSuperRole assigns a super role. +// A super role is one that have all ACL permissions. +// These type of role doesn't need to be mapped to any permission. +func WithSuperRole(r Role) ACLOption { + return func(acl *ACL) { + acl.superRole = r + } +} + +// WithUser adds a user to the ACL with optional assigned roles. +func WithUser(user std.Address, roles ...Role) ACLOption { + return func(acl *ACL) { + // TODO: Should we enforce that users are members of the DAO? [acl.dao.IsMember(user)] + acl.users.Set(user.String(), append([]Role(nil), roles...)) + } +} + +// WithRole add a role to the ACL with one or more assigned permissions. +func WithRole(r Role, p Permission, extra ...Permission) ACLOption { + return func(acl *ACL) { + acl.roles.Set(string(r), append([]Permission{p}, extra...)) + } +} diff --git a/examples/gno.land/r/demo/boards2/acl_test.gno b/examples/gno.land/r/demo/boards2/acl_test.gno new file mode 100644 index 00000000000..2b765920a45 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/acl_test.gno @@ -0,0 +1,271 @@ +package boards2 + +import ( + "std" + "testing" + + "gno.land/p/demo/boards2/admindao" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func TestNewACL(t *testing.T) { + roles := []string{"a", "b"} + dao := admindao.New() + + acl := NewACL(dao, WithRole("a", "permission1"), WithRole("b", "permission2")) + + urequire.Equal(t, len(roles), len(acl.Roles()), "roles") + for i, r := range acl.Roles() { + uassert.Equal(t, roles[i], string(r)) + } +} + +func TestACLWithPermission(t *testing.T) { + cases := []struct { + name string + user std.Address + permission Permission + args Args + acl *ACL + err string + called bool + }{ + { + name: "ok", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + permission: "bar", + acl: NewACL( + admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), + WithRole("foo", "bar"), + ), + called: true, + }, + { + name: "ok with arguments", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + permission: "bar", + args: Args{"a", "b"}, + acl: NewACL( + admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), + WithRole("foo", "bar"), + ), + called: true, + }, + { + name: "no permission", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + permission: "bar", + acl: NewACL( + admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + WithRole("foo", "bar"), + ), + err: "unauthorized", + }, + { + name: "is not a DAO member", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + permission: "bar", + acl: NewACL( + admindao.New(), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), + WithRole("foo", "bar"), + ), + err: "unauthorized", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var ( + called bool + args Args + ) + + callback := func(a Args) { + args = a + called = true + } + + testCaseFn := func() { + tc.acl.WithPermission(tc.user, tc.permission, tc.args, callback) + } + + if tc.err != "" { + urequire.PanicsWithMessage(t, tc.err, testCaseFn, "panic") + return + } else { + urequire.NotPanics(t, testCaseFn, "no panic") + } + + urequire.Equal(t, tc.called, called, "callback called") + urequire.Equal(t, len(tc.args), len(args), "args count") + for i, a := range args { + uassert.Equal(t, tc.args[i].(string), a.(string)) + } + }) + } +} + +func TestACLGetUserRoles(t *testing.T) { + cases := []struct { + name string + user std.Address + roles []string + acl *ACL + }{ + { + name: "single role", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + roles: []string{"admin"}, + acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin")), + }, + { + name: "multiple roles", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + roles: []string{"admin", "foo", "bar"}, + acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo", "bar")), + }, + { + name: "without roles", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + }, + { + name: "not a user", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + acl: NewACL(admindao.New()), + }, + { + name: "multiple users", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + roles: []string{"admin"}, + acl: NewACL( + admindao.New(), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin"), + WithUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "admin"), + WithUser("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc", "admin", "bar"), + ), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + roles := tc.acl.GetUserRoles(tc.user) + + urequire.Equal(t, len(tc.roles), len(roles), "user role count") + for i, r := range roles { + uassert.Equal(t, tc.roles[i], string(r)) + } + }) + } +} + +func TestACLHasRole(t *testing.T) { + cases := []struct { + name string + user std.Address + role Role + acl *ACL + want bool + }{ + { + name: "ok", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + role: "admin", + acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin")), + want: true, + }, + { + name: "ok with multiple roles", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + role: "foo", + acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo")), + want: true, + }, + { + name: "user without roles", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + }, + { + name: "has no role", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + role: "bar", + acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo")), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := tc.acl.HasRole(tc.user, tc.role) + uassert.Equal(t, got, tc.want) + }) + } +} + +func TestACLHasPermission(t *testing.T) { + cases := []struct { + name string + user std.Address + permission Permission + acl *ACL + want bool + }{ + { + name: "ok", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + permission: "bar", + acl: NewACL( + admindao.New(), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), + WithRole("foo", "bar"), + ), + want: true, + }, + { + name: "ok with multiple users", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + permission: "bar", + acl: NewACL( + admindao.New(), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), + WithUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "foo"), + WithRole("foo", "bar"), + ), + want: true, + }, + { + name: "ok with multiple roles", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + permission: "other", + acl: NewACL( + admindao.New(), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo", "baz"), + WithRole("foo", "bar"), + WithRole("baz", "other"), + ), + want: true, + }, + { + name: "no permission", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + permission: "other", + acl: NewACL( + admindao.New(), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), + WithRole("foo", "bar"), + ), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := tc.acl.HasPermission(tc.user, tc.permission) + uassert.Equal(t, got, tc.want) + }) + } +} diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index 8942ad8f2df..64cc8730d6b 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -1,7 +1,6 @@ package boards2 import ( - "regexp" "std" "strconv" "strings" @@ -11,8 +10,6 @@ import ( "gno.land/p/moul/txlink" ) -var reBoardName = regexp.MustCompile(`^[a-z]{3}[_a-z0-9]{0,23}[0-9]{3}$`) - type BoardID uint64 func (id BoardID) String() string { @@ -34,8 +31,6 @@ type Board struct { } func newBoard(id BoardID, name string, creator std.Address) *Board { - assertIsBoardName(name) - if gBoardsByName.Has(name) { panic("board already exists") } @@ -97,21 +92,6 @@ 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 { - case PermissionEdit: - return true - case PermissionDelete: - return true - default: - return false - } - } - return false -} - func (board *Board) Render() string { s := "\\[" + newLink("post", board.GetPostFormURL()) + "]\n\n" if board.threads.Size() > 0 { @@ -140,9 +120,3 @@ func (board *Board) GetURLFromReplyID(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/board_test.gno b/examples/gno.land/r/demo/boards2/board_test.gno index d4fcc212ed5..88996928fc3 100644 --- a/examples/gno.land/r/demo/boards2/board_test.gno +++ b/examples/gno.land/r/demo/boards2/board_test.gno @@ -77,58 +77,6 @@ func TestBoard_DeleteThread(t *testing.T) { uassert.False(t, ok) } -func TestBoard_HasPermission(t *testing.T) { - var ( - alice std.Address = "012345" - bob std.Address = "cafebabe" - ) - - cases := []struct { - label string - creator std.Address - actor std.Address - perm Permission - expect bool - }{ - { - label: "creator should be able to edit board", - expect: true, - creator: alice, - actor: alice, - perm: PermissionEdit, - }, - { - label: "creator should be able to delete board", - expect: true, - creator: alice, - actor: alice, - perm: PermissionDelete, - }, - { - label: "guest shouldn't be able to edit boards", - expect: false, - creator: alice, - actor: bob, - perm: PermissionEdit, - }, - { - label: "guest shouldn't be able to delete boards", - expect: false, - creator: alice, - actor: bob, - perm: PermissionDelete, - }, - } - - for i, c := range cases { - t.Run(c.label, func(t *testing.T) { - b := newBoard(BoardID(i), "test12345", c.creator) - got := b.HasPermission(c.actor, c.perm) - uassert.Equal(t, c.expect, got) - }) - } -} - var boardUrlPrefix = strings.TrimPrefix(std.CurrentRealm().PkgPath(), "gno.land") func TestBoard_GetURLFromThreadID(t *testing.T) { diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno index f50d779426f..752f63bc438 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -1,16 +1,37 @@ package boards2 -import "gno.land/p/demo/avl" +import ( + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/boards2/admindao" +) // Default minimum fee in ugnot required for anonymous users const defaultAnonymousFee = 100_000_000 var ( + gAuth Permissioner gLastBoardID BoardID gBoardsByID avl.Tree // string(id) -> *Board gBoardsByName avl.Tree // string(name) -> *Board ) +func init() { + // TODO: Decide how to initialize realm owner (DAO owner member) + admin := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + // TODO: Implement support for assigning a new realm DAO + dao := admindao.New(admindao.WithMember(admin)) + gAuth = NewACL( + dao, + WithSuperRole(RoleOwner), + // TODO: Assign roles and permissions + // WithRole(RoleAdmin, permissions...), + // WithRole(RoleModerator, permissions...), + WithUser(admin, RoleOwner), + ) +} + // incGetBoardID returns a new board ID. func incGetBoardID() BoardID { gLastBoardID++ diff --git a/examples/gno.land/r/demo/boards2/gno.mod b/examples/gno.land/r/demo/boards2/gno.mod index 3872857d2b7..6c8431f592c 100644 --- a/examples/gno.land/r/demo/boards2/gno.mod +++ b/examples/gno.land/r/demo/boards2/gno.mod @@ -2,10 +2,12 @@ module gno.land/r/demo/boards2 require ( gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/boards2/admindao v0.0.0-latest gno.land/p/demo/mux v0.0.0-latest gno.land/p/demo/testutils v0.0.0-latest gno.land/p/demo/uassert v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/demo/urequire v0.0.0-latest gno.land/p/moul/txlink v0.0.0-latest gno.land/r/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/boards2/permission.gno b/examples/gno.land/r/demo/boards2/permission.gno new file mode 100644 index 00000000000..70ce929c38d --- /dev/null +++ b/examples/gno.land/r/demo/boards2/permission.gno @@ -0,0 +1,36 @@ +package boards2 + +import ( + "errors" + "std" +) + +const ( + PermissionBoardCreate Permission = "board:create" + PermissionThreadCreate = "thread:create" + PermissionThreadEdit = "thread:edit" + PermissionThreadDelete = "thread:delete" + PermissionThreadRepost = "thread:repost" + PermissionReplyDelete = "reply:delete" +) + +// ErrUnauzorized indicates that user doesn't have a required permission. +var ErrUnauzorized = errors.New("unauthorized") + +type ( + // Permission defines the type for permissions. + Permission string + + // Args is a list of generic arguments. + Args []interface{} + + // Permissioner define an interface to for permissioned execution. + Permissioner interface { + // HasPermission checks if a user has a specific permission. + HasPermission(std.Address, Permission) bool + + // WithPermission calls a callback when a user has a specific permission. + // It panics on error. + WithPermission(std.Address, Permission, Args, func(Args)) + } +) diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index 38035ddaec3..655e7259512 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -157,6 +157,7 @@ func (post *Post) DeleteReply(replyID PostID) error { return errors.New("reply not found in thread") } + // TODO: Remove child replies too! reply := v.(*Post) if reply.parentID != post.id { parent, _ := post.GetReply(reply.parentID) @@ -167,22 +168,6 @@ func (post *Post) DeleteReply(replyID PostID) error { return nil } -// 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 PermissionEdit: - return true - case PermissionDelete: - return true - default: - return false - } - } - // post notes inherit permissions of the board. - return post.board.HasPermission(addr, perm) -} - func (post *Post) GetSummary() string { return summaryOf(post.body, 80) } diff --git a/examples/gno.land/r/demo/boards2/post_test.gno b/examples/gno.land/r/demo/boards2/post_test.gno index 42529da8483..05888f9e297 100644 --- a/examples/gno.land/r/demo/boards2/post_test.gno +++ b/examples/gno.land/r/demo/boards2/post_test.gno @@ -117,12 +117,6 @@ func TestNewThread(t *testing.T) { uassert.Equal(t, replyURL, thread.GetReplyFormURL()) uassert.Equal(t, repostURL, thread.GetRepostFormURL()) uassert.Equal(t, deleteURL, thread.GetDeleteFormURL()) - uassert.True(t, thread.HasPermission(creator, PermissionEdit)) - uassert.True(t, thread.HasPermission(creator, PermissionDelete)) - uassert.False(t, thread.HasPermission(creator, Permission("unknown"))) - uassert.False(t, thread.HasPermission(member, PermissionEdit)) - uassert.False(t, thread.HasPermission(member, PermissionDelete)) - uassert.False(t, thread.HasPermission(member, Permission("unknown"))) } func TestThreadAddReply(t *testing.T) { @@ -280,12 +274,6 @@ func TestNewReply(t *testing.T) { uassert.Equal(t, url, reply.GetURL()) uassert.Equal(t, replyURL, reply.GetReplyFormURL()) uassert.Equal(t, deleteURL, reply.GetDeleteFormURL()) - uassert.True(t, reply.HasPermission(creator, PermissionEdit)) - uassert.True(t, reply.HasPermission(creator, PermissionDelete)) - uassert.False(t, reply.HasPermission(creator, Permission("unknown"))) - uassert.False(t, reply.HasPermission(member, PermissionEdit)) - uassert.False(t, reply.HasPermission(member, PermissionDelete)) - uassert.False(t, reply.HasPermission(member, Permission("unknown"))) } func TestReplyAddReply(t *testing.T) { diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 5cf1f1ae08d..e3cb3066f3d 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -2,6 +2,7 @@ package boards2 import ( "std" + "strings" "gno.land/r/demo/users" ) @@ -17,21 +18,38 @@ func GetBoardIDFromName(name string) (BoardID, bool) { func CreateBoard(name string) BoardID { assertIsUserCall() - caller := std.GetOrigCaller() - assertIsNotAnonymousCaller(caller) + name = strings.TrimSpace(name) + if name == "" { + panic("board name is empty") + } + + // TODO: Now that registered user requirement is removed must define a way to avoid + // increasing the IDs. Require a fee? + // Or we have to change the way boards are created, which could be async. + caller := std.GetOrigCaller() id := incGetBoardID() - board := newBoard(id, name, caller) - gBoardsByID.Set(id.Key(), board) - gBoardsByName.Set(name, board) - return board.id + args := Args{name, id} + gAuth.WithPermission(caller, PermissionBoardCreate, args, func(a Args) { + // TODO: Do the callback really need the args or we could have the same result directly referencing? + name := a[0].(string) + id := a[1].(BoardID) + board := newBoard(id, name, caller) + gBoardsByID.Set(id.Key(), board) + gBoardsByName.Set(name, board) + }) + return id } -func CreateThread(bid BoardID, title string, body string) PostID { +func CreateThread(bid BoardID, title, body string) PostID { assertIsUserCall() caller := std.GetOrigCaller() - assertAnonymousCallerFeeReceived(caller) + assertHasPermission(caller, PermissionThreadCreate) // TODO: Who can create threads? + assertAnonymousCallerFeeReceived(caller) // TODO: Do we require a fee to anonymous users? + assertBoardExists(bid) + + // TODO: Assert that caller is a board member (when board type is invite only) board := mustGetBoard(bid) thread := board.AddThread(caller, title, body) @@ -42,75 +60,109 @@ func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { assertIsUserCall() caller := std.GetOrigCaller() - assertAnonymousCallerFeeReceived(caller) + assertAnonymousCallerFeeReceived(caller) // TODO: Do we require a fee to anonymous users? + + board := mustGetBoard(bid) + thread := mustGetThread(board, threadID) - var ( - reply *Post - board = mustGetBoard(bid) - thread = mustGetThread(board, threadID) - ) + // TODO: Assert thread is not locked + // TODO: Assert that caller is a board member (when board type is invite only) + var reply *Post if replyID == threadID { + // When the parent reply is the thread just add reply to thread reply = thread.AddReply(caller, body) } else { + // Try to get parent reply and add a new child reply post := mustGetReply(thread, replyID) reply = post.AddReply(caller, body) } return reply.id } -func CreateRepost(bid BoardID, threadID PostID, title string, body string, dstBoardID BoardID) PostID { +func CreateRepost(bid BoardID, threadID PostID, title, body string, dstBoardID BoardID) PostID { assertIsUserCall() caller := std.GetOrigCaller() assertAnonymousCallerFeeReceived(caller) + assertBoardExists(dstBoardID) board := mustGetBoard(bid) if board.IsPrivate() { panic("cannot repost from a private board") } + // TODO: Assert that board allows reposts? + // TODO: Assert that caller is member of both boards (when board types are invite only) + + assertThreadExists(board, threadID) + dst := mustGetBoard(dstBoardID) thread := mustGetThread(board, threadID) repost := thread.AddRepostTo(caller, title, body, dst) return repost.id } -func DeleteThread(bid BoardID, threadID PostID, reason string) { +func DeleteThread(bid BoardID, threadID PostID) { assertIsUserCall() board := mustGetBoard(bid) - thread := mustGetThread(board, threadID) + assertThreadExists(board, threadID) caller := std.GetOrigCaller() - assertUserHasPermission(thread, caller, PermissionDelete) - - board.DeleteThread(threadID) + args := Args{bid, threadID} + gAuth.WithPermission(caller, PermissionThreadDelete, args, func(a Args) { + bid := a[0].(BoardID) + board := mustGetBoard(bid) + + threadID := a[1].(PostID) + board.DeleteThread(threadID) + }) } -func DeleteReply(bid BoardID, threadID, replyID PostID, reason string) { +func DeleteReply(bid BoardID, threadID, replyID PostID) { assertIsUserCall() board := mustGetBoard(bid) thread := mustGetThread(board, threadID) - reply := mustGetReply(thread, replyID) + assertReplyExists(thread, replyID) + + // TODO: Hide reply when the caller is the owner of the reply (remove WithPermission call for now) + // TODO: Support removing reply and children though proposals? (WithPermission) caller := std.GetOrigCaller() - assertUserHasPermission(reply, caller, PermissionDelete) + args := Args{bid, threadID, replyID} + gAuth.WithPermission(caller, PermissionReplyDelete, args, func(a Args) { + bid := a[0].(BoardID) + board := mustGetBoard(bid) - thread.DeleteReply(replyID) + threadID := a[1].(PostID) + thread := mustGetThread(board, threadID) + + replyID := a[2].(PostID) + thread.DeleteReply(replyID) + }) } func EditThread(bid BoardID, threadID PostID, title, body string) { assertIsUserCall() board := mustGetBoard(bid) - thread := mustGetThread(board, threadID) + assertThreadExists(board, threadID) caller := std.GetOrigCaller() - assertUserHasPermission(thread, caller, PermissionEdit) - - thread.Update(title, body) + args := Args{bid, threadID, title, body} + gAuth.WithPermission(caller, PermissionThreadEdit, args, func(a Args) { + bid := a[0].(BoardID) + board := mustGetBoard(bid) + + threadID := a[1].(PostID) + thread := mustGetThread(board, threadID) + + title := a[2].(string) + body := a[3].(string) + thread.Update(title, body) + }) } func EditReply(bid BoardID, threadID, replyID PostID, title, body string) { @@ -118,12 +170,14 @@ func EditReply(bid BoardID, threadID, replyID PostID, title, body string) { board := mustGetBoard(bid) thread := mustGetThread(board, threadID) - post := mustGetReply(thread, replyID) - + reply := mustGetReply(thread, replyID) caller := std.GetOrigCaller() - assertUserHasPermission(post, caller, PermissionEdit) + if caller != reply.GetCreator() { + panic("only the reply creator is allowed to edit it") + } - post.Update(title, body) + // TODO: Should we have a history of previous reply contents? + reply.Update(title, body) } func assertIsUserCall() { @@ -132,20 +186,12 @@ func assertIsUserCall() { } } -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() { 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) { @@ -154,8 +200,26 @@ func assertAnonymousCallerFeeReceived(caller std.Address) { } } -func assertUserHasPermission(post *Post, user std.Address, p Permission) { - if !post.HasPermission(user, p) { +func assertHasPermission(user std.Address, p Permission) { + if !gAuth.HasPermission(user, p) { panic("unauthorized") } } + +func assertBoardExists(id BoardID) { + if _, found := getBoard(id); !found { + panic("board not found: " + id.String()) + } +} + +func assertThreadExists(b *Board, threadID PostID) { + if _, found := b.GetThread(threadID); !found { + panic("thread not found: " + threadID.String()) + } +} + +func assertReplyExists(thread *Post, replyID PostID) { + if _, found := thread.GetReply(replyID); !found { + panic("reply not found: " + replyID.String()) + } +} diff --git a/examples/gno.land/r/demo/boards2/role.gno b/examples/gno.land/r/demo/boards2/role.gno deleted file mode 100644 index b33f46c6472..00000000000 --- a/examples/gno.land/r/demo/boards2/role.gno +++ /dev/null @@ -1,10 +0,0 @@ -package boards2 - -// TODO: Rename file to "auth.gno" and define a new interface - -type Permission string - -const ( - PermissionEdit Permission = "edit" - PermissionDelete Permission = "delete" -) From 4027d071ecc8a4040fe3b11e7d7b0e61328b974e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Fri, 29 Nov 2024 15:36:36 +0100 Subject: [PATCH 09/52] feat(boards2): board creation and permissions (#3211) Related to #3140 --- .../p/demo/boards2/admindao/admindao.gno | 19 + .../p/demo/boards2/admindao/admindao_test.gno | 24 ++ examples/gno.land/r/demo/boards2/acl.gno | 100 ----- .../gno.land/r/demo/boards2/acl_options.gno | 30 -- examples/gno.land/r/demo/boards2/acl_test.gno | 271 ------------ examples/gno.land/r/demo/boards2/board.gno | 4 - examples/gno.land/r/demo/boards2/boards.gno | 26 +- .../gno.land/r/demo/boards2/permission.gno | 26 +- .../r/demo/boards2/permission_default.gno | 169 ++++++++ .../demo/boards2/permission_default_test.gno | 401 ++++++++++++++++++ .../r/demo/boards2/permission_handlers.gno | 38 ++ .../r/demo/boards2/permission_options.gno | 30 ++ examples/gno.land/r/demo/boards2/public.gno | 67 +-- .../r/demo/boards2/z_0_a_filetest.gno | 21 + .../r/demo/boards2/z_0_b_filetest.gno | 20 + .../r/demo/boards2/z_0_c_filetest.gno | 24 ++ .../r/demo/boards2/z_0_d_filetest.gno | 11 + .../r/demo/boards2/z_0_e_filetest.gno | 20 + .../r/demo/boards2/z_0_f_filetest.gno | 20 + .../r/demo/boards2/z_1_a_filetest.gno | 24 ++ .../r/demo/boards2/z_1_b_filetest.gno | 29 ++ .../r/demo/boards2/z_1_c_filetest.gno | 30 ++ 22 files changed, 941 insertions(+), 463 deletions(-) delete mode 100644 examples/gno.land/r/demo/boards2/acl.gno delete mode 100644 examples/gno.land/r/demo/boards2/acl_options.gno delete mode 100644 examples/gno.land/r/demo/boards2/acl_test.gno create mode 100644 examples/gno.land/r/demo/boards2/permission_default.gno create mode 100644 examples/gno.land/r/demo/boards2/permission_default_test.gno create mode 100644 examples/gno.land/r/demo/boards2/permission_handlers.gno create mode 100644 examples/gno.land/r/demo/boards2/permission_options.gno create mode 100644 examples/gno.land/r/demo/boards2/z_0_a_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_0_b_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_0_c_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_0_d_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_0_e_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_0_f_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_1_a_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_1_b_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_1_c_filetest.gno diff --git a/examples/gno.land/p/demo/boards2/admindao/admindao.gno b/examples/gno.land/p/demo/boards2/admindao/admindao.gno index 4e1d49e435e..404bfe78db7 100644 --- a/examples/gno.land/p/demo/boards2/admindao/admindao.gno +++ b/examples/gno.land/p/demo/boards2/admindao/admindao.gno @@ -1,6 +1,7 @@ package admindao import ( + "errors" "std" "gno.land/p/demo/avl" @@ -9,6 +10,9 @@ import ( // TODO: Add support for proposals // TODO: Add support for events +// ErrMemberExists indicates that a member is already part of the DAO. +var ErrMemberExists = errors.New("member already exist") + // AdminDAO defines a Boards administration DAO. type AdminDAO struct { parent *AdminDAO @@ -40,6 +44,21 @@ func (dao AdminDAO) Members() []std.Address { return members } +// AddMember adds a new member to the DAO. +func (dao *AdminDAO) AddMember(user std.Address) error { + if dao.IsMember(user) { + return ErrMemberExists + } + dao.members.Set(user.String(), struct{}{}) + return nil +} + +// RemoveMember removes a member from the DAO. +func (dao *AdminDAO) RemoveMember(user std.Address) (removed bool) { + _, removed = dao.members.Remove(user.String()) + return removed +} + // IsMember checks if a user is a member of the DAO. func (dao AdminDAO) IsMember(user std.Address) bool { return dao.members.Has(user.String()) diff --git a/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno b/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno index 171f5618892..999ecdfcd0a 100644 --- a/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno +++ b/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno @@ -59,6 +59,30 @@ func TestNew(t *testing.T) { } } +func TestAdminDAOAddMember(t *testing.T) { + member := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + dao := New(WithMember("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn")) + + err := dao.AddMember(member) + urequire.NoError(t, err) + uassert.Equal(t, 2, len(dao.Members())) + uassert.True(t, dao.IsMember(member)) + + err = dao.AddMember(member) + uassert.ErrorIs(t, err, ErrMemberExists) +} + +func TestAdminDAORemoveMember(t *testing.T) { + member := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + dao := New(WithMember(member)) + + removed := dao.RemoveMember(member) + urequire.True(t, removed) + + removed = dao.RemoveMember(member) + urequire.False(t, removed) +} + func TestAdminDAOIsMember(t *testing.T) { cases := []struct { name string diff --git a/examples/gno.land/r/demo/boards2/acl.gno b/examples/gno.land/r/demo/boards2/acl.gno deleted file mode 100644 index 631591e1214..00000000000 --- a/examples/gno.land/r/demo/boards2/acl.gno +++ /dev/null @@ -1,100 +0,0 @@ -package boards2 - -import ( - "std" - - "gno.land/p/demo/avl" - "gno.land/p/demo/boards2/admindao" -) - -// TODO: Support to deal with permissions for anonymous users? - -const ( - RoleOwner Role = "owner" - RoleAdmin = "admin" - RoleModerator = "moderator" -) - -type ( - // Role defines the type for user roles. - Role string - - // ACL or access control list manages user roles and permissions. - ACL struct { - superRole Role - dao *admindao.AdminDAO - users *avl.Tree // string(std.Address) -> []Role - roles *avl.Tree // string(role) -> []Permission - } -) - -// NewACL create a new access control list. -func NewACL(dao *admindao.AdminDAO, options ...ACLOption) *ACL { - acl := &ACL{ - dao: dao, - roles: avl.NewTree(), - users: avl.NewTree(), - } - for _, apply := range options { - apply(acl) - } - return acl -} - -// Roles returns the list of roles. -func (acl ACL) Roles() []Role { - var roles []Role - acl.roles.Iterate("", "", func(name string, _ interface{}) bool { - roles = append(roles, Role(name)) - return false - }) - return roles -} - -// GetUserRoles returns the list of roles assigned to a user. -func (acl ACL) GetUserRoles(user std.Address) []Role { - v, found := acl.users.Get(user.String()) - if !found { - return nil - } - return v.([]Role) -} - -// HasRole checks if a user has a specific role assigned. -func (acl ACL) HasRole(user std.Address, r Role) bool { - for _, role := range acl.GetUserRoles(user) { - if role == r { - return true - } - } - return false -} - -// HasPermission checks if a user has a specific permission. -func (acl ACL) HasPermission(user std.Address, perm Permission) bool { - // TODO: Should we check that the user belongs to the DAO? - for _, r := range acl.GetUserRoles(user) { - v, found := acl.roles.Get(string(r)) - if !found { - continue - } - - for _, p := range v.([]Permission) { - if p == perm { - return true - } - } - } - return false -} - -// WithPermission calls a callback when a user has a specific permission. -// It panics on error. -func (acl ACL) WithPermission(user std.Address, perm Permission, a Args, cb func(Args)) { - if !acl.HasPermission(user, perm) || !acl.dao.IsMember(user) { - panic("unauthorized") - } - - // TODO: Support DAO proposals that run the callback on proposal execution - cb(a) -} diff --git a/examples/gno.land/r/demo/boards2/acl_options.gno b/examples/gno.land/r/demo/boards2/acl_options.gno deleted file mode 100644 index 6a481470766..00000000000 --- a/examples/gno.land/r/demo/boards2/acl_options.gno +++ /dev/null @@ -1,30 +0,0 @@ -package boards2 - -import "std" - -// ACLOption configures an ACL. -type ACLOption func(*ACL) - -// WithSuperRole assigns a super role. -// A super role is one that have all ACL permissions. -// These type of role doesn't need to be mapped to any permission. -func WithSuperRole(r Role) ACLOption { - return func(acl *ACL) { - acl.superRole = r - } -} - -// WithUser adds a user to the ACL with optional assigned roles. -func WithUser(user std.Address, roles ...Role) ACLOption { - return func(acl *ACL) { - // TODO: Should we enforce that users are members of the DAO? [acl.dao.IsMember(user)] - acl.users.Set(user.String(), append([]Role(nil), roles...)) - } -} - -// WithRole add a role to the ACL with one or more assigned permissions. -func WithRole(r Role, p Permission, extra ...Permission) ACLOption { - return func(acl *ACL) { - acl.roles.Set(string(r), append([]Permission{p}, extra...)) - } -} diff --git a/examples/gno.land/r/demo/boards2/acl_test.gno b/examples/gno.land/r/demo/boards2/acl_test.gno deleted file mode 100644 index 2b765920a45..00000000000 --- a/examples/gno.land/r/demo/boards2/acl_test.gno +++ /dev/null @@ -1,271 +0,0 @@ -package boards2 - -import ( - "std" - "testing" - - "gno.land/p/demo/boards2/admindao" - "gno.land/p/demo/uassert" - "gno.land/p/demo/urequire" -) - -func TestNewACL(t *testing.T) { - roles := []string{"a", "b"} - dao := admindao.New() - - acl := NewACL(dao, WithRole("a", "permission1"), WithRole("b", "permission2")) - - urequire.Equal(t, len(roles), len(acl.Roles()), "roles") - for i, r := range acl.Roles() { - uassert.Equal(t, roles[i], string(r)) - } -} - -func TestACLWithPermission(t *testing.T) { - cases := []struct { - name string - user std.Address - permission Permission - args Args - acl *ACL - err string - called bool - }{ - { - name: "ok", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - permission: "bar", - acl: NewACL( - admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), - WithRole("foo", "bar"), - ), - called: true, - }, - { - name: "ok with arguments", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - permission: "bar", - args: Args{"a", "b"}, - acl: NewACL( - admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), - WithRole("foo", "bar"), - ), - called: true, - }, - { - name: "no permission", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - permission: "bar", - acl: NewACL( - admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), - WithRole("foo", "bar"), - ), - err: "unauthorized", - }, - { - name: "is not a DAO member", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - permission: "bar", - acl: NewACL( - admindao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), - WithRole("foo", "bar"), - ), - err: "unauthorized", - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - var ( - called bool - args Args - ) - - callback := func(a Args) { - args = a - called = true - } - - testCaseFn := func() { - tc.acl.WithPermission(tc.user, tc.permission, tc.args, callback) - } - - if tc.err != "" { - urequire.PanicsWithMessage(t, tc.err, testCaseFn, "panic") - return - } else { - urequire.NotPanics(t, testCaseFn, "no panic") - } - - urequire.Equal(t, tc.called, called, "callback called") - urequire.Equal(t, len(tc.args), len(args), "args count") - for i, a := range args { - uassert.Equal(t, tc.args[i].(string), a.(string)) - } - }) - } -} - -func TestACLGetUserRoles(t *testing.T) { - cases := []struct { - name string - user std.Address - roles []string - acl *ACL - }{ - { - name: "single role", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - roles: []string{"admin"}, - acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin")), - }, - { - name: "multiple roles", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - roles: []string{"admin", "foo", "bar"}, - acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo", "bar")), - }, - { - name: "without roles", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), - }, - { - name: "not a user", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - acl: NewACL(admindao.New()), - }, - { - name: "multiple users", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - roles: []string{"admin"}, - acl: NewACL( - admindao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin"), - WithUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "admin"), - WithUser("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc", "admin", "bar"), - ), - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - roles := tc.acl.GetUserRoles(tc.user) - - urequire.Equal(t, len(tc.roles), len(roles), "user role count") - for i, r := range roles { - uassert.Equal(t, tc.roles[i], string(r)) - } - }) - } -} - -func TestACLHasRole(t *testing.T) { - cases := []struct { - name string - user std.Address - role Role - acl *ACL - want bool - }{ - { - name: "ok", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - role: "admin", - acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin")), - want: true, - }, - { - name: "ok with multiple roles", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - role: "foo", - acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo")), - want: true, - }, - { - name: "user without roles", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), - }, - { - name: "has no role", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - role: "bar", - acl: NewACL(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo")), - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - got := tc.acl.HasRole(tc.user, tc.role) - uassert.Equal(t, got, tc.want) - }) - } -} - -func TestACLHasPermission(t *testing.T) { - cases := []struct { - name string - user std.Address - permission Permission - acl *ACL - want bool - }{ - { - name: "ok", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - permission: "bar", - acl: NewACL( - admindao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), - WithRole("foo", "bar"), - ), - want: true, - }, - { - name: "ok with multiple users", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - permission: "bar", - acl: NewACL( - admindao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), - WithUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "foo"), - WithRole("foo", "bar"), - ), - want: true, - }, - { - name: "ok with multiple roles", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - permission: "other", - acl: NewACL( - admindao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo", "baz"), - WithRole("foo", "bar"), - WithRole("baz", "other"), - ), - want: true, - }, - { - name: "no permission", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - permission: "other", - acl: NewACL( - admindao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), - WithRole("foo", "bar"), - ), - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - got := tc.acl.HasPermission(tc.user, tc.permission) - uassert.Equal(t, got, tc.want) - }) - } -} diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index 64cc8730d6b..ee1b548aa0f 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -31,10 +31,6 @@ type Board struct { } func newBoard(id BoardID, name string, creator std.Address) *Board { - if gBoardsByName.Has(name) { - panic("board already exists") - } - return &Board{ id: id, name: name, diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno index 752f63bc438..53072e37542 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -1,35 +1,17 @@ package boards2 -import ( - "std" - - "gno.land/p/demo/avl" - "gno.land/p/demo/boards2/admindao" -) - -// Default minimum fee in ugnot required for anonymous users -const defaultAnonymousFee = 100_000_000 +import "gno.land/p/demo/avl" var ( - gAuth Permissioner + gPerm Permissioner // TODO: Support changing the permissioner gLastBoardID BoardID gBoardsByID avl.Tree // string(id) -> *Board gBoardsByName avl.Tree // string(name) -> *Board ) func init() { - // TODO: Decide how to initialize realm owner (DAO owner member) - admin := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 - // TODO: Implement support for assigning a new realm DAO - dao := admindao.New(admindao.WithMember(admin)) - gAuth = NewACL( - dao, - WithSuperRole(RoleOwner), - // TODO: Assign roles and permissions - // WithRole(RoleAdmin, permissions...), - // WithRole(RoleModerator, permissions...), - WithUser(admin, RoleOwner), - ) + // Initialize the default realm permissions + gPerm = createDefaultPermissions() } // incGetBoardID returns a new board ID. diff --git a/examples/gno.land/r/demo/boards2/permission.gno b/examples/gno.land/r/demo/boards2/permission.gno index 70ce929c38d..0fc5dc9515d 100644 --- a/examples/gno.land/r/demo/boards2/permission.gno +++ b/examples/gno.land/r/demo/boards2/permission.gno @@ -1,9 +1,6 @@ package boards2 -import ( - "errors" - "std" -) +import "std" const ( PermissionBoardCreate Permission = "board:create" @@ -12,25 +9,42 @@ const ( PermissionThreadDelete = "thread:delete" PermissionThreadRepost = "thread:repost" PermissionReplyDelete = "reply:delete" + PermissionMemberInvite = "member:invite" + PermissionMemberRemove = "member:remove" ) -// ErrUnauzorized indicates that user doesn't have a required permission. -var ErrUnauzorized = errors.New("unauthorized") +const ( + RoleOwner Role = "owner" + RoleAdmin = "admin" + RoleModerator = "moderator" +) type ( // Permission defines the type for permissions. Permission string + // Role defines the type for user roles. + Role string + // Args is a list of generic arguments. Args []interface{} // Permissioner define an interface to for permissioned execution. Permissioner interface { + // HasRole checks if a user has a specific role assigned. + HasRole(std.Address, Role) bool + // HasPermission checks if a user has a specific permission. HasPermission(std.Address, Permission) bool // WithPermission calls a callback when a user has a specific permission. // It panics on error. WithPermission(std.Address, Permission, Args, func(Args)) + + // AddUser adds a new user to the permissioner. + AddUser(std.Address, ...Role) error + + // RemoveUser removes a user from the permissioner. + RemoveUser(std.Address) (removed bool) } ) diff --git a/examples/gno.land/r/demo/boards2/permission_default.gno b/examples/gno.land/r/demo/boards2/permission_default.gno new file mode 100644 index 00000000000..fc8efee14d2 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/permission_default.gno @@ -0,0 +1,169 @@ +package boards2 + +import ( + "errors" + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/boards2/admindao" +) + +type ( + // PermissionsHandlerFunc defines a function to handle permission callbacks. + // Handlers are called by the `WithPermission()` method to execute callbacks + // when users have the permission assigned. + PermissionsHandlerFunc func(Permissioner, Args, func(Args)) + + // DefaultPermissions manages users, roles and permissions. + DefaultPermissions struct { + superRole Role + dao *admindao.AdminDAO + handlers *avl.Tree // string(permission) -> PermissionsHandlerFunc + users *avl.Tree // string(std.Address) -> []Role + roles *avl.Tree // string(role) -> []Permission + } +) + +// NewDefaultPermissions creates a new permissions type. +// This type is a default implementation to handle users, roles and permissions. +func NewDefaultPermissions(dao *admindao.AdminDAO, options ...DefaultPermissionsOption) *DefaultPermissions { + dp := &DefaultPermissions{ + dao: dao, + handlers: avl.NewTree(), + roles: avl.NewTree(), + users: avl.NewTree(), + } + for _, apply := range options { + apply(dp) + } + return dp +} + +// Roles returns the list of roles. +func (dp DefaultPermissions) Roles() []Role { + var roles []Role + dp.roles.Iterate("", "", func(name string, _ interface{}) bool { + roles = append(roles, Role(name)) + return false + }) + return roles +} + +// RoleExists checks if a role exists. +func (dp DefaultPermissions) RoleExists(r Role) bool { + return dp.roles.Iterate("", "", func(name string, _ interface{}) bool { + return Role(name) == r + }) +} + +// GetUserRoles returns the list of roles assigned to a user. +func (dp DefaultPermissions) GetUserRoles(user std.Address) []Role { + v, found := dp.users.Get(user.String()) + if !found { + return nil + } + return v.([]Role) +} + +// HasRole checks if a user has a specific role assigned. +func (dp DefaultPermissions) HasRole(user std.Address, r Role) bool { + for _, role := range dp.GetUserRoles(user) { + if role == r { + return true + } + } + return false +} + +// HasPermission checks if a user has a specific permission. +func (dp DefaultPermissions) HasPermission(user std.Address, perm Permission) bool { + // TODO: Should we check that the user belongs to the DAO? + for _, r := range dp.GetUserRoles(user) { + if dp.superRole == r { + return true + } + + v, found := dp.roles.Get(string(r)) + if !found { + continue + } + + for _, p := range v.([]Permission) { + if p == perm { + return true + } + } + } + return false +} + +// AddUser adds a new user to permissions. +func (dp *DefaultPermissions) AddUser(user std.Address, roles ...Role) error { + if dp.users.Has(user.String()) { + return errors.New("user already exists") + } + + for _, r := range roles { + if !dp.RoleExists(r) { + return errors.New("invalid role: " + string(r)) + } + } + + if err := dp.dao.AddMember(user); err != nil { + return err + } + + dp.users.Set(user.String(), append([]Role(nil), roles...)) + return nil +} + +// RemoveUser removes a user from permissions. +func (dp *DefaultPermissions) RemoveUser(user std.Address) bool { + _, removed := dp.users.Remove(user.String()) + dp.dao.RemoveMember(user) + return removed +} + +// HandleFunc registers a handler function for a permission. +func (dp *DefaultPermissions) HandleFunc(p Permission, fn PermissionsHandlerFunc) { + dp.handlers.Set(string(p), fn) +} + +// WithPermission calls a callback when a user has a specific permission. +// It panics on error or when a handler panics. +// Callbacks are by default called when there is no handle registered for the permission. +func (dp *DefaultPermissions) WithPermission(user std.Address, perm Permission, args Args, cb func(Args)) { + if !dp.HasPermission(user, perm) || !dp.dao.IsMember(user) { + panic("unauthorized") + } + + h, found := dp.handlers.Get(string(perm)) + if !found { + cb(args) // TODO: Should we fail instead? + return + } + + fn := h.(PermissionsHandlerFunc) + fn(dp, args, cb) +} + +func createDefaultPermissions() *DefaultPermissions { + // TODO: Define and change the default realm owner (or owners) + owner := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + // TODO: DAO should be a different realm or proposal and voting functions should be part of boards realm? + // Permissions and DAO mechanics should be discussed and improved. Add `GetDAO()` to `Permissioner`?? + dao := admindao.New(admindao.WithMember(owner)) + perms := NewDefaultPermissions( + dao, + WithSuperRole(RoleOwner), + WithRole(RoleAdmin, PermissionBoardCreate, PermissionMemberInvite), + // TODO: Finish assigning all roles and permissions + // WithRole(RoleModerator, permissions...), + WithUser(owner, RoleOwner), + ) + + perms.HandleFunc(PermissionBoardCreate, handleBoardCreate) + perms.HandleFunc(PermissionMemberInvite, handleMemberInvite) + + return perms +} diff --git a/examples/gno.land/r/demo/boards2/permission_default_test.gno b/examples/gno.land/r/demo/boards2/permission_default_test.gno new file mode 100644 index 00000000000..bf64018c11a --- /dev/null +++ b/examples/gno.land/r/demo/boards2/permission_default_test.gno @@ -0,0 +1,401 @@ +package boards2 + +import ( + "std" + "testing" + + "gno.land/p/demo/boards2/admindao" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +var _ Permissioner = (*DefaultPermissions)(nil) + +func TestNewDefaultPermissions(t *testing.T) { + roles := []Role{"a", "b"} + dao := admindao.New() + + perms := NewDefaultPermissions(dao, WithRole("a", "permission1"), WithRole("b", "permission2")) + + urequire.Equal(t, len(roles), len(perms.Roles()), "roles") + for i, r := range perms.Roles() { + uassert.Equal(t, string(roles[i]), string(r)) + } + + for _, r := range roles { + uassert.True(t, perms.RoleExists(r)) + } +} + +func TestDefaultPermissionsWithPermission(t *testing.T) { + cases := []struct { + name string + user std.Address + permission Permission + args Args + perms *DefaultPermissions + err string + called bool + }{ + { + name: "ok", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + permission: "bar", + perms: NewDefaultPermissions( + admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), + WithRole("foo", "bar"), + ), + called: true, + }, + { + name: "ok with arguments", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + permission: "bar", + args: Args{"a", "b"}, + perms: NewDefaultPermissions( + admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), + WithRole("foo", "bar"), + ), + called: true, + }, + { + name: "no permission", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + permission: "bar", + perms: NewDefaultPermissions( + admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + WithRole("foo", "bar"), + ), + err: "unauthorized", + }, + { + name: "is not a DAO member", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + permission: "bar", + perms: NewDefaultPermissions( + admindao.New(), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), + WithRole("foo", "bar"), + ), + err: "unauthorized", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var ( + called bool + args Args + ) + + callback := func(a Args) { + args = a + called = true + } + + testCaseFn := func() { + tc.perms.WithPermission(tc.user, tc.permission, tc.args, callback) + } + + if tc.err != "" { + urequire.PanicsWithMessage(t, tc.err, testCaseFn, "panic") + return + } else { + urequire.NotPanics(t, testCaseFn, "no panic") + } + + urequire.Equal(t, tc.called, called, "callback called") + urequire.Equal(t, len(tc.args), len(args), "args count") + for i, a := range args { + uassert.Equal(t, tc.args[i].(string), a.(string)) + } + }) + } +} + +func TestDefaultPermissionsGetUserRoles(t *testing.T) { + cases := []struct { + name string + user std.Address + roles []string + perms *DefaultPermissions + }{ + { + name: "single role", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + roles: []string{"admin"}, + perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin")), + }, + { + name: "multiple roles", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + roles: []string{"admin", "foo", "bar"}, + perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo", "bar")), + }, + { + name: "without roles", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + }, + { + name: "not a user", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + perms: NewDefaultPermissions(admindao.New()), + }, + { + name: "multiple users", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + roles: []string{"admin"}, + perms: NewDefaultPermissions( + admindao.New(), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin"), + WithUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "admin"), + WithUser("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc", "admin", "bar"), + ), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + roles := tc.perms.GetUserRoles(tc.user) + + urequire.Equal(t, len(tc.roles), len(roles), "user role count") + for i, r := range roles { + uassert.Equal(t, tc.roles[i], string(r)) + } + }) + } +} + +func TestDefaultPermissionsHasRole(t *testing.T) { + cases := []struct { + name string + user std.Address + role Role + perms *DefaultPermissions + want bool + }{ + { + name: "ok", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + role: "admin", + perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin")), + want: true, + }, + { + name: "ok with multiple roles", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + role: "foo", + perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo")), + want: true, + }, + { + name: "user without roles", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + }, + { + name: "has no role", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + role: "bar", + perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo")), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := tc.perms.HasRole(tc.user, tc.role) + uassert.Equal(t, got, tc.want) + }) + } +} + +func TestDefaultPermissionsHasPermission(t *testing.T) { + cases := []struct { + name string + user std.Address + permission Permission + perms *DefaultPermissions + want bool + }{ + { + name: "ok", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + permission: "bar", + perms: NewDefaultPermissions( + admindao.New(), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), + WithRole("foo", "bar"), + ), + want: true, + }, + { + name: "ok with multiple users", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + permission: "bar", + perms: NewDefaultPermissions( + admindao.New(), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), + WithUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "foo"), + WithRole("foo", "bar"), + ), + want: true, + }, + { + name: "ok with multiple roles", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + permission: "other", + perms: NewDefaultPermissions( + admindao.New(), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo", "baz"), + WithRole("foo", "bar"), + WithRole("baz", "other"), + ), + want: true, + }, + { + name: "no permission", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + permission: "other", + perms: NewDefaultPermissions( + admindao.New(), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), + WithRole("foo", "bar"), + ), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := tc.perms.HasPermission(tc.user, tc.permission) + uassert.Equal(t, got, tc.want) + }) + } +} + +func TestDefaultPermissionsAddUser(t *testing.T) { + cases := []struct { + name string + user std.Address + roles []Role + setup func() *DefaultPermissions + err string + }{ + { + name: "single user", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + roles: []Role{"a", "b"}, + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(), + WithRole("a", "permission1"), + WithRole("b", "permission2"), + ) + }, + }, + { + name: "multiple users", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + roles: []Role{"a"}, + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(), + WithRole("a", "permission1"), + WithUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "a"), + WithUser("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc"), + ) + }, + }, + { + name: "duplicated user", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + ) + }, + err: "user already exists", + }, + { + name: "duplicated user", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + roles: []Role{"a", "foo"}, + setup: func() *DefaultPermissions { + return NewDefaultPermissions(admindao.New(), WithRole("a", "permission1")) + }, + err: "invalid role: foo", + }, + { + name: "already a DAO member", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + ) + }, + err: "member already exist", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + perm := tc.setup() + + err := perm.AddUser(tc.user, tc.roles...) + + if tc.err != "" { + urequire.True(t, err != nil, "expected an error") + uassert.Equal(t, tc.err, err.Error()) + return + } else { + urequire.NoError(t, err) + } + + roles := perm.GetUserRoles(tc.user) + uassert.Equal(t, len(tc.roles), len(roles)) + for i, r := range roles { + urequire.Equal(t, string(tc.roles[i]), string(r)) + } + }) + } +} + +func TestDefaultPermissionsRemoveUser(t *testing.T) { + cases := []struct { + name string + user std.Address + setup func() *DefaultPermissions + want bool + }{ + { + name: "ok", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + ) + }, + want: true, + }, + { + name: "user not found", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + setup: func() *DefaultPermissions { + return NewDefaultPermissions(admindao.New()) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + perm := tc.setup() + got := perm.RemoveUser(tc.user) + uassert.Equal(t, tc.want, got) + }) + } +} diff --git a/examples/gno.land/r/demo/boards2/permission_handlers.gno b/examples/gno.land/r/demo/boards2/permission_handlers.gno new file mode 100644 index 00000000000..9c6a24b90f0 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/permission_handlers.gno @@ -0,0 +1,38 @@ +package boards2 + +import ( + "std" + + "gno.land/r/demo/users" +) + +func handleBoardCreate(_ Permissioner, args Args, cb func(Args)) { + // TODO: This way of dealing with arguments is delicate, ideally types should be used + name := args[0].(string) + if std.Address(name).IsValid() { + panic("addresses are not allowed as board name") + } + + // When the board name is the name of a registered user + // check that caller is the owner of the name. + caller := std.GetOrigCaller() + user := users.GetUserByName(name) + if user != nil && user.Address != caller { + panic("board name is a user name registered to a different user") + } + + cb(args) +} + +func handleMemberInvite(p Permissioner, args Args, cb func(Args)) { + // Make sure that only owners invite other owners + role := args[1].(Role) + if role == RoleOwner { + caller := std.GetOrigCaller() + if !p.HasRole(caller, RoleOwner) { + panic("only owners are allowed to invite other owners") + } + } + + cb(args) +} diff --git a/examples/gno.land/r/demo/boards2/permission_options.gno b/examples/gno.land/r/demo/boards2/permission_options.gno new file mode 100644 index 00000000000..fdaef050206 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/permission_options.gno @@ -0,0 +1,30 @@ +package boards2 + +import "std" + +// DefaultPermissionsOption configures an DefaultPermissions. +type DefaultPermissionsOption func(*DefaultPermissions) + +// WithSuperRole assigns a super role. +// A super role is one that have all permissions. +// These type of role doesn't need to be mapped to any permission. +func WithSuperRole(r Role) DefaultPermissionsOption { + return func(dp *DefaultPermissions) { + dp.superRole = r + } +} + +// WithUser adds a user to default permissions with optional assigned roles. +func WithUser(user std.Address, roles ...Role) DefaultPermissionsOption { + return func(dp *DefaultPermissions) { + // TODO: Should we enforce that users are members of the DAO? [dp.dao.IsMember(user)] + dp.users.Set(user.String(), append([]Role(nil), roles...)) + } +} + +// WithRole add a role to default permissions with one or more assigned permissions. +func WithRole(r Role, p Permission, extra ...Permission) DefaultPermissionsOption { + return func(dp *DefaultPermissions) { + dp.roles.Set(string(r), append([]Permission{p}, extra...)) + } +} diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index e3cb3066f3d..9820c45499f 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -3,8 +3,6 @@ package boards2 import ( "std" "strings" - - "gno.land/r/demo/users" ) func GetBoardIDFromName(name string) (BoardID, bool) { @@ -23,16 +21,16 @@ func CreateBoard(name string) BoardID { panic("board name is empty") } - // TODO: Now that registered user requirement is removed must define a way to avoid - // increasing the IDs. Require a fee? - // Or we have to change the way boards are created, which could be async. - caller := std.GetOrigCaller() id := incGetBoardID() args := Args{name, id} - gAuth.WithPermission(caller, PermissionBoardCreate, args, func(a Args) { + gPerm.WithPermission(caller, PermissionBoardCreate, args, func(a Args) { // TODO: Do the callback really need the args or we could have the same result directly referencing? name := a[0].(string) + if gBoardsByName.Has(name) { + panic("board already exists") + } + id := a[1].(BoardID) board := newBoard(id, name, caller) gBoardsByID.Set(id.Key(), board) @@ -44,13 +42,11 @@ func CreateBoard(name string) BoardID { func CreateThread(bid BoardID, title, body string) PostID { assertIsUserCall() + // TODO: Assert that caller is a board member (when board type is invite only) caller := std.GetOrigCaller() assertHasPermission(caller, PermissionThreadCreate) // TODO: Who can create threads? - assertAnonymousCallerFeeReceived(caller) // TODO: Do we require a fee to anonymous users? assertBoardExists(bid) - // TODO: Assert that caller is a board member (when board type is invite only) - board := mustGetBoard(bid) thread := board.AddThread(caller, title, body) return thread.id @@ -59,9 +55,8 @@ func CreateThread(bid BoardID, title, body string) PostID { func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { assertIsUserCall() + // TODO: Assert that caller is a board member (when board type is invite only) caller := std.GetOrigCaller() - assertAnonymousCallerFeeReceived(caller) // TODO: Do we require a fee to anonymous users? - board := mustGetBoard(bid) thread := mustGetThread(board, threadID) @@ -83,8 +78,8 @@ func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { func CreateRepost(bid BoardID, threadID PostID, title, body string, dstBoardID BoardID) PostID { assertIsUserCall() + // TODO: Assert that caller is a board member (when board type is invite only) caller := std.GetOrigCaller() - assertAnonymousCallerFeeReceived(caller) assertBoardExists(dstBoardID) board := mustGetBoard(bid) @@ -111,7 +106,7 @@ func DeleteThread(bid BoardID, threadID PostID) { caller := std.GetOrigCaller() args := Args{bid, threadID} - gAuth.WithPermission(caller, PermissionThreadDelete, args, func(a Args) { + gPerm.WithPermission(caller, PermissionThreadDelete, args, func(a Args) { bid := a[0].(BoardID) board := mustGetBoard(bid) @@ -132,7 +127,7 @@ func DeleteReply(bid BoardID, threadID, replyID PostID) { caller := std.GetOrigCaller() args := Args{bid, threadID, replyID} - gAuth.WithPermission(caller, PermissionReplyDelete, args, func(a Args) { + gPerm.WithPermission(caller, PermissionReplyDelete, args, func(a Args) { bid := a[0].(BoardID) board := mustGetBoard(bid) @@ -152,7 +147,7 @@ func EditThread(bid BoardID, threadID PostID, title, body string) { caller := std.GetOrigCaller() args := Args{bid, threadID, title, body} - gAuth.WithPermission(caller, PermissionThreadEdit, args, func(a Args) { + gPerm.WithPermission(caller, PermissionThreadEdit, args, func(a Args) { bid := a[0].(BoardID) board := mustGetBoard(bid) @@ -180,28 +175,40 @@ func EditReply(bid BoardID, threadID, replyID PostID, title, body string) { reply.Update(title, body) } -func assertIsUserCall() { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { - panic("invalid non-user call") - } +func InviteMember(user std.Address, role Role) { + assertIsUserCall() + + caller := std.GetOrigCaller() + args := Args{user, role} + gPerm.WithPermission(caller, PermissionMemberInvite, args, func(a Args) { + user := a[0].(std.Address) + role := a[1].(Role) + if err := gPerm.AddUser(user, role); err != nil { + panic(err) + } + }) } -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") - } +func RemoveMember(user std.Address) { + assertIsUserCall() + + caller := std.GetOrigCaller() + gPerm.WithPermission(caller, PermissionMemberRemove, Args{user}, func(a Args) { + user := a[0].(std.Address) + if !gPerm.RemoveUser(user) { + panic("member not found") + } + }) } -func assertAnonymousCallerFeeReceived(caller std.Address) { - if users.GetUserByAddress(caller) == nil { - assertAnonymousFeeReceived() +func assertIsUserCall() { + if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + panic("invalid non-user call") } } func assertHasPermission(user std.Address, p Permission) { - if !gAuth.HasPermission(user, p) { + if !gPerm.HasPermission(user, p) { panic("unauthorized") } } diff --git a/examples/gno.land/r/demo/boards2/z_0_a_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_a_filetest.gno new file mode 100644 index 00000000000..fc3781a78a8 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_0_a_filetest.gno @@ -0,0 +1,21 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + bid := boards2.CreateBoard("test1") + println("ID =", bid) +} + +// Output: +// ID = 1 diff --git a/examples/gno.land/r/demo/boards2/z_0_b_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_b_filetest.gno new file mode 100644 index 00000000000..08cbcf8ff43 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_0_b_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.CreateBoard("") +} + +// Error: +// board name is empty diff --git a/examples/gno.land/r/demo/boards2/z_0_c_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_c_filetest.gno new file mode 100644 index 00000000000..e9a06b53e55 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_0_c_filetest.gno @@ -0,0 +1,24 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + boardName = "test1" +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.CreateBoard(boardName) +} + +func main() { + boards2.CreateBoard(boardName) +} + +// Error: +// board already exists diff --git a/examples/gno.land/r/demo/boards2/z_0_d_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_d_filetest.gno new file mode 100644 index 00000000000..8992a5cb133 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_0_d_filetest.gno @@ -0,0 +1,11 @@ +// PKGPATH: gno.land/r/demo/boards2_test +package boards2_test + +import "gno.land/r/demo/boards2" + +func main() { + boards2.CreateBoard("foo") +} + +// Error: +// invalid non-user call diff --git a/examples/gno.land/r/demo/boards2/z_0_e_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_e_filetest.gno new file mode 100644 index 00000000000..33f45878b65 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_0_e_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.CreateBoard("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") +} + +// Error: +// addresses are not allowed as board name diff --git a/examples/gno.land/r/demo/boards2/z_0_f_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_f_filetest.gno new file mode 100644 index 00000000000..f17f73bac4c --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_0_f_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.CreateBoard("gnoland") +} + +// Error: +// board name is a user name registered to a different user diff --git a/examples/gno.land/r/demo/boards2/z_1_a_filetest.gno b/examples/gno.land/r/demo/boards2/z_1_a_filetest.gno new file mode 100644 index 00000000000..6497b4203e1 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_1_a_filetest.gno @@ -0,0 +1,24 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 +) + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.InviteMember(admin, boards2.RoleAdmin) + println("ok") +} + +// Output: +// ok diff --git a/examples/gno.land/r/demo/boards2/z_1_b_filetest.gno b/examples/gno.land/r/demo/boards2/z_1_b_filetest.gno new file mode 100644 index 00000000000..eac31e48707 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_1_b_filetest.gno @@ -0,0 +1,29 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + user = std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") +) + +func init() { + // Add an admin user + std.TestSetOrigCaller(owner) + boards2.InviteMember(admin, boards2.RoleAdmin) + + // Next call will be done by the admin user + std.TestSetOrigCaller(admin) +} + +func main() { + boards2.InviteMember(user, boards2.RoleOwner) +} + +// Error: +// only owners are allowed to invite other owners diff --git a/examples/gno.land/r/demo/boards2/z_1_c_filetest.gno b/examples/gno.land/r/demo/boards2/z_1_c_filetest.gno new file mode 100644 index 00000000000..01ca55cb2f2 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_1_c_filetest.gno @@ -0,0 +1,30 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + user = std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") +) + +func init() { + // Add an admin user + std.TestSetOrigCaller(owner) + boards2.InviteMember(admin, boards2.RoleAdmin) + + // Next call will be done by the admin user + std.TestSetOrigCaller(admin) +} + +func main() { + boards2.InviteMember(user, boards2.RoleAdmin) + println("ok") +} + +// Output: +// ok From 75cd4abc85e42d59447ffd9fdc163b5ca4e2b2c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Wed, 18 Dec 2024 16:43:58 +0100 Subject: [PATCH 10/52] fix: resolve devx boards2 branch issues (#3365) --- examples/gno.land/p/demo/boards2/admindao/gno.mod | 6 ------ examples/gno.land/r/demo/boards2/board.gno | 2 +- examples/gno.land/r/demo/boards2/board_test.gno | 2 +- examples/gno.land/r/demo/boards2/gno.mod | 12 ------------ examples/gno.land/r/demo/boards2/post.gno | 8 ++++---- 5 files changed, 6 insertions(+), 24 deletions(-) diff --git a/examples/gno.land/p/demo/boards2/admindao/gno.mod b/examples/gno.land/p/demo/boards2/admindao/gno.mod index 5067fdbcbcc..5d54360ae7a 100644 --- a/examples/gno.land/p/demo/boards2/admindao/gno.mod +++ b/examples/gno.land/p/demo/boards2/admindao/gno.mod @@ -1,7 +1 @@ module gno.land/p/demo/boards2/admindao - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index ee1b548aa0f..088500cb4a5 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -114,5 +114,5 @@ func (board *Board) GetURLFromReplyID(threadID, replyID PostID) string { } func (board *Board) GetPostFormURL() string { - return txlink.URL("CreateThread", "bid", board.id.String()) + return txlink.Call("CreateThread", "bid", board.id.String()) } diff --git a/examples/gno.land/r/demo/boards2/board_test.gno b/examples/gno.land/r/demo/boards2/board_test.gno index 88996928fc3..2f06b93eefa 100644 --- a/examples/gno.land/r/demo/boards2/board_test.gno +++ b/examples/gno.land/r/demo/boards2/board_test.gno @@ -100,7 +100,7 @@ func TestBoard_GetURLFromReplyID(t *testing.T) { func TestBoard_GetPostFormURL(t *testing.T) { bid := BoardID(386) b := newBoard(bid, "foo1234", "") - expect := txlink.URL("CreateThread", "bid", bid.String()) + expect := txlink.Call("CreateThread", "bid", bid.String()) got := b.GetPostFormURL() uassert.Equal(t, expect, got) } diff --git a/examples/gno.land/r/demo/boards2/gno.mod b/examples/gno.land/r/demo/boards2/gno.mod index 6c8431f592c..4337a775e76 100644 --- a/examples/gno.land/r/demo/boards2/gno.mod +++ b/examples/gno.land/r/demo/boards2/gno.mod @@ -1,13 +1 @@ module gno.land/r/demo/boards2 - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/boards2/admindao v0.0.0-latest - gno.land/p/demo/mux v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/p/moul/txlink v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index 655e7259512..7364b985c84 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -180,7 +180,7 @@ func (post *Post) GetURL() string { } func (post *Post) GetReplyFormURL() string { - return txlink.URL("CreateReply", + return txlink.Call("CreateReply", "bid", post.board.id.String(), "threadID", post.threadID.String(), "postID", post.id.String(), @@ -188,7 +188,7 @@ func (post *Post) GetReplyFormURL() string { } func (post *Post) GetRepostFormURL() string { - return txlink.URL("CreateRepost", + return txlink.Call("CreateRepost", "bid", post.board.id.String(), "postID", post.id.String(), ) @@ -196,12 +196,12 @@ func (post *Post) GetRepostFormURL() string { func (post *Post) GetDeleteFormURL() string { if post.IsThread() { - return txlink.URL("DeleteThread", + return txlink.Call("DeleteThread", "bid", post.board.id.String(), "threadID", post.threadID.String(), ) } - return txlink.URL("DeleteReply", + return txlink.Call("DeleteReply", "bid", post.board.id.String(), "threadID", post.threadID.String(), "replyID", post.id.String(), From 261e3167071afc00cc6fe22b45d928d3fc5a810c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Mon, 23 Dec 2024 16:42:32 +0100 Subject: [PATCH 11/52] feat: permissions support for individual boards (#3386) This is an initial implementation to unblock features that depend on board permissions. --- examples/gno.land/r/demo/boards2/board.gno | 22 ++++++++++++ examples/gno.land/r/demo/boards2/boards.gno | 13 +++++-- .../gno.land/r/demo/boards2/permission.gno | 4 +-- .../r/demo/boards2/permission_default.gno | 13 +++---- .../demo/boards2/permission_default_test.gno | 2 +- .../r/demo/boards2/permission_handlers.gno | 4 +-- examples/gno.land/r/demo/boards2/public.gno | 36 ++++++------------- 7 files changed, 52 insertions(+), 42 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index 088500cb4a5..1920cd41225 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -8,6 +8,8 @@ import ( "gno.land/p/demo/avl" "gno.land/p/moul/txlink" + + "gno.land/p/demo/boards2/admindao" ) type BoardID uint64 @@ -28,6 +30,7 @@ type Board struct { postsCtr uint64 // increments Post.id createdAt time.Time deleted avl.Tree // TODO reserved for fast-delete. + perms Permissions } func newBoard(id BoardID, name string, creator std.Address) *Board { @@ -38,6 +41,7 @@ func newBoard(id BoardID, name string, creator std.Address) *Board { threads: avl.Tree{}, createdAt: time.Now(), deleted: avl.Tree{}, + perms: createDefaultBoardPermissions(creator), } } @@ -64,6 +68,10 @@ func (board *Board) GetURL() string { return strings.TrimPrefix(std.CurrentRealm().PkgPath(), "gno.land") + ":" + board.name } +func (board *Board) GetPermissions() Permissions { + return board.perms +} + func (board *Board) GetThread(threadID PostID) (_ *Post, found bool) { v, found := board.threads.Get(threadID.Key()) if !found { @@ -116,3 +124,17 @@ func (board *Board) GetURLFromReplyID(threadID, replyID PostID) string { func (board *Board) GetPostFormURL() string { return txlink.Call("CreateThread", "bid", board.id.String()) } + +// TODO: This is a temporary implementation until the permissions and DAO mecahnics are defined +func createDefaultBoardPermissions(owner std.Address) *DefaultPermissions { + perms := NewDefaultPermissions( + admindao.New(admindao.WithMember(owner)), + WithSuperRole(RoleOwner), + WithRole(RoleAdmin, PermissionMemberInvite), + // TODO: Finish assigning all roles and permissions + // WithRole(RoleModerator, permissions...), + WithUser(owner, RoleOwner), + ) + perms.HandleFunc(PermissionMemberInvite, handleMemberInvite) + return perms +} diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno index 53072e37542..70e02d5dffe 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -1,17 +1,24 @@ package boards2 -import "gno.land/p/demo/avl" +import ( + "std" + + "gno.land/p/demo/avl" +) var ( - gPerm Permissioner // TODO: Support changing the permissioner + gPerm Permissions // TODO: Support assigning a different implementation gLastBoardID BoardID gBoardsByID avl.Tree // string(id) -> *Board gBoardsByName avl.Tree // string(name) -> *Board ) func init() { + // TODO: Define and change the default realm owner (or owners) + owner := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + // Initialize the default realm permissions - gPerm = createDefaultPermissions() + gPerm = createDefaultPermissions(owner) } // incGetBoardID returns a new board ID. diff --git a/examples/gno.land/r/demo/boards2/permission.gno b/examples/gno.land/r/demo/boards2/permission.gno index 0fc5dc9515d..d287b4f8e8b 100644 --- a/examples/gno.land/r/demo/boards2/permission.gno +++ b/examples/gno.land/r/demo/boards2/permission.gno @@ -29,8 +29,8 @@ type ( // Args is a list of generic arguments. Args []interface{} - // Permissioner define an interface to for permissioned execution. - Permissioner interface { + // Permissions define an interface to for permissioned execution. + Permissions interface { // HasRole checks if a user has a specific role assigned. HasRole(std.Address, Role) bool diff --git a/examples/gno.land/r/demo/boards2/permission_default.gno b/examples/gno.land/r/demo/boards2/permission_default.gno index fc8efee14d2..b4558f82bcc 100644 --- a/examples/gno.land/r/demo/boards2/permission_default.gno +++ b/examples/gno.land/r/demo/boards2/permission_default.gno @@ -12,7 +12,7 @@ type ( // PermissionsHandlerFunc defines a function to handle permission callbacks. // Handlers are called by the `WithPermission()` method to execute callbacks // when users have the permission assigned. - PermissionsHandlerFunc func(Permissioner, Args, func(Args)) + PermissionsHandlerFunc func(Permissions, Args, func(Args)) // DefaultPermissions manages users, roles and permissions. DefaultPermissions struct { @@ -147,23 +147,18 @@ func (dp *DefaultPermissions) WithPermission(user std.Address, perm Permission, fn(dp, args, cb) } -func createDefaultPermissions() *DefaultPermissions { - // TODO: Define and change the default realm owner (or owners) - owner := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 +func createDefaultPermissions(owner std.Address) *DefaultPermissions { // TODO: DAO should be a different realm or proposal and voting functions should be part of boards realm? - // Permissions and DAO mechanics should be discussed and improved. Add `GetDAO()` to `Permissioner`?? - dao := admindao.New(admindao.WithMember(owner)) + // Permissions and DAO mechanics should be discussed and improved. Add `GetDAO()` to `Permissions`?? perms := NewDefaultPermissions( - dao, + admindao.New(admindao.WithMember(owner)), WithSuperRole(RoleOwner), WithRole(RoleAdmin, PermissionBoardCreate, PermissionMemberInvite), // TODO: Finish assigning all roles and permissions // WithRole(RoleModerator, permissions...), WithUser(owner, RoleOwner), ) - perms.HandleFunc(PermissionBoardCreate, handleBoardCreate) perms.HandleFunc(PermissionMemberInvite, handleMemberInvite) - return perms } diff --git a/examples/gno.land/r/demo/boards2/permission_default_test.gno b/examples/gno.land/r/demo/boards2/permission_default_test.gno index bf64018c11a..a3c51fb6abe 100644 --- a/examples/gno.land/r/demo/boards2/permission_default_test.gno +++ b/examples/gno.land/r/demo/boards2/permission_default_test.gno @@ -9,7 +9,7 @@ import ( "gno.land/p/demo/urequire" ) -var _ Permissioner = (*DefaultPermissions)(nil) +var _ Permissions = (*DefaultPermissions)(nil) func TestNewDefaultPermissions(t *testing.T) { roles := []Role{"a", "b"} diff --git a/examples/gno.land/r/demo/boards2/permission_handlers.gno b/examples/gno.land/r/demo/boards2/permission_handlers.gno index 9c6a24b90f0..63b351f1d53 100644 --- a/examples/gno.land/r/demo/boards2/permission_handlers.gno +++ b/examples/gno.land/r/demo/boards2/permission_handlers.gno @@ -6,7 +6,7 @@ import ( "gno.land/r/demo/users" ) -func handleBoardCreate(_ Permissioner, args Args, cb func(Args)) { +func handleBoardCreate(_ Permissions, args Args, cb func(Args)) { // TODO: This way of dealing with arguments is delicate, ideally types should be used name := args[0].(string) if std.Address(name).IsValid() { @@ -24,7 +24,7 @@ func handleBoardCreate(_ Permissioner, args Args, cb func(Args)) { cb(args) } -func handleMemberInvite(p Permissioner, args Args, cb func(Args)) { +func handleMemberInvite(p Permissions, args Args, cb func(Args)) { // Make sure that only owners invite other owners role := args[1].(Role) if role == RoleOwner { diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 9820c45499f..083f8daff65 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -24,14 +24,11 @@ func CreateBoard(name string) BoardID { caller := std.GetOrigCaller() id := incGetBoardID() args := Args{name, id} - gPerm.WithPermission(caller, PermissionBoardCreate, args, func(a Args) { - // TODO: Do the callback really need the args or we could have the same result directly referencing? - name := a[0].(string) + gPerm.WithPermission(caller, PermissionBoardCreate, args, func(Args) { if gBoardsByName.Has(name) { panic("board already exists") } - id := a[1].(BoardID) board := newBoard(id, name, caller) gBoardsByID.Set(id.Key(), board) gBoardsByName.Set(name, board) @@ -106,11 +103,8 @@ func DeleteThread(bid BoardID, threadID PostID) { caller := std.GetOrigCaller() args := Args{bid, threadID} - gPerm.WithPermission(caller, PermissionThreadDelete, args, func(a Args) { - bid := a[0].(BoardID) + gPerm.WithPermission(caller, PermissionThreadDelete, args, func(Args) { board := mustGetBoard(bid) - - threadID := a[1].(PostID) board.DeleteThread(threadID) }) } @@ -128,13 +122,8 @@ func DeleteReply(bid BoardID, threadID, replyID PostID) { caller := std.GetOrigCaller() args := Args{bid, threadID, replyID} gPerm.WithPermission(caller, PermissionReplyDelete, args, func(a Args) { - bid := a[0].(BoardID) board := mustGetBoard(bid) - - threadID := a[1].(PostID) thread := mustGetThread(board, threadID) - - replyID := a[2].(PostID) thread.DeleteReply(replyID) }) } @@ -147,15 +136,9 @@ func EditThread(bid BoardID, threadID PostID, title, body string) { caller := std.GetOrigCaller() args := Args{bid, threadID, title, body} - gPerm.WithPermission(caller, PermissionThreadEdit, args, func(a Args) { - bid := a[0].(BoardID) + gPerm.WithPermission(caller, PermissionThreadEdit, args, func(Args) { board := mustGetBoard(bid) - - threadID := a[1].(PostID) thread := mustGetThread(board, threadID) - - title := a[2].(string) - body := a[3].(string) thread.Update(title, body) }) } @@ -180,9 +163,7 @@ func InviteMember(user std.Address, role Role) { caller := std.GetOrigCaller() args := Args{user, role} - gPerm.WithPermission(caller, PermissionMemberInvite, args, func(a Args) { - user := a[0].(std.Address) - role := a[1].(Role) + gPerm.WithPermission(caller, PermissionMemberInvite, args, func(Args) { if err := gPerm.AddUser(user, role); err != nil { panic(err) } @@ -193,8 +174,7 @@ func RemoveMember(user std.Address) { assertIsUserCall() caller := std.GetOrigCaller() - gPerm.WithPermission(caller, PermissionMemberRemove, Args{user}, func(a Args) { - user := a[0].(std.Address) + gPerm.WithPermission(caller, PermissionMemberRemove, Args{user}, func(Args) { if !gPerm.RemoveUser(user) { panic("member not found") } @@ -213,6 +193,12 @@ func assertHasPermission(user std.Address, p Permission) { } } +func assertHasBoardPermission(b *Board, user std.Address, p Permission) { + if !b.perms.HasPermission(user, p) { + panic("unauthorized") + } +} + func assertBoardExists(id BoardID) { if _, found := getBoard(id); !found { panic("board not found: " + id.String()) From 8844f305f2b45b5db5f027115642efa6fb4011f1 Mon Sep 17 00:00:00 2001 From: Denys Sedchenko Date: Tue, 7 Jan 2025 09:40:08 -0500 Subject: [PATCH 12/52] feat(boards2): add core flagging logic (#3451) Add flagging support for boards2 realm. Closes https://github.com/gnolang/gno/issues/3146 --- examples/gno.land/r/demo/boards2/board.gno | 7 ++- examples/gno.land/r/demo/boards2/flag.gno | 47 +++++++++++++++++++ .../gno.land/r/demo/boards2/permission.gno | 2 + examples/gno.land/r/demo/boards2/post.gno | 45 ++++++++++++++++-- .../gno.land/r/demo/boards2/post_test.gno | 21 +++++++++ examples/gno.land/r/demo/boards2/public.gno | 46 +++++++++++++++++- examples/gno.land/r/demo/boards2/render.gno | 8 +++- .../r/demo/boards2/z_2_a_filetest.gno | 23 +++++++++ .../r/demo/boards2/z_2_b_filetest.gno | 26 ++++++++++ .../r/demo/boards2/z_2_c_filetest.gno | 26 ++++++++++ .../r/demo/boards2/z_2_d_filetest.gno | 25 ++++++++++ 11 files changed, 268 insertions(+), 8 deletions(-) create mode 100644 examples/gno.land/r/demo/boards2/flag.gno create mode 100644 examples/gno.land/r/demo/boards2/z_2_a_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_2_b_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_2_c_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_2_d_filetest.gno diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index 1920cd41225..8b667cb291e 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -100,8 +100,13 @@ func (board *Board) Render() string { s := "\\[" + newLink("post", board.GetPostFormURL()) + "]\n\n" if board.threads.Size() > 0 { board.threads.Iterate("", "", func(_ string, v interface{}) bool { + post := v.(*Post) + if post.isHidden { + return false + } + s += "----------------------------------------\n" - s += v.(*Post).RenderSummary() + "\n" + s += post.RenderSummary() + "\n" return false }) } diff --git a/examples/gno.land/r/demo/boards2/flag.gno b/examples/gno.land/r/demo/boards2/flag.gno new file mode 100644 index 00000000000..e5744211ddf --- /dev/null +++ b/examples/gno.land/r/demo/boards2/flag.gno @@ -0,0 +1,47 @@ +package boards2 + +import ( + "std" + "strconv" +) + +const flagThreshold = 1 + +type Flag struct { + User std.Address + Reason string +} + +func NewFlag(creator std.Address, reason string) Flag { + return Flag{ + User: creator, + Reason: reason, + } +} + +type Flaggable interface { + // AddFlag adds a new flag to an item. + // + // Returns false if item was already flagged by user. + AddFlag(flag Flag) bool + + // FlagsCount returns number of times item was flagged. + FlagsCount() int +} + +// flagItem adds a flag to a flaggable item (post, thread, etc). +// +// Returns whether flag count threshold is reached and item can be hidden. +// +// Panics if flag count threshold was already reached. +func flagItem(item Flaggable, flag Flag) bool { + if item.FlagsCount() >= flagThreshold { + panic("item flag count threshold exceeded: " + strconv.Itoa(flagThreshold)) + } + + if !item.AddFlag(flag) { + panic("item has been already flagged by a current user") + } + + return item.FlagsCount() == flagThreshold +} diff --git a/examples/gno.land/r/demo/boards2/permission.gno b/examples/gno.land/r/demo/boards2/permission.gno index d287b4f8e8b..e465e346418 100644 --- a/examples/gno.land/r/demo/boards2/permission.gno +++ b/examples/gno.land/r/demo/boards2/permission.gno @@ -7,8 +7,10 @@ const ( PermissionThreadCreate = "thread:create" PermissionThreadEdit = "thread:edit" PermissionThreadDelete = "thread:delete" + PermissionThreadFlag = "thread:flag" PermissionThreadRepost = "thread:repost" PermissionReplyDelete = "reply:delete" + PermissionReplyFlag = "reply:flag" PermissionMemberInvite = "member:invite" PermissionMemberRemove = "member:remove" ) diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index 7364b985c84..5a79302af22 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -30,12 +30,14 @@ type Post struct { creator std.Address title string // optional body string + isHidden bool replies avl.Tree // Post.id -> *Post repliesAll avl.Tree // Post.id -> *Post (all replies, for top-level posts) reposts avl.Tree // Board.id -> Post.id - threadID PostID // original Post.id - parentID PostID // parent Post.id (if reply or repost) - repostBoardID BoardID // original Board.id (if repost) + flags []Flag + threadID PostID // original Post.id + parentID PostID // parent Post.id (if reply or repost) + repostBoardID BoardID // original Board.id (if repost) createdAt time.Time updatedAt time.Time } @@ -97,6 +99,30 @@ func (post *Post) GetUpdatedAt() time.Time { return post.updatedAt } +func (post *Post) AddFlag(flag Flag) bool { + // TODO: sort flags for fast search in case of big thresholds + for _, v := range post.flags { + if v.User == flag.User { + return false + } + } + + post.flags = append(post.flags, flag) + return true +} + +func (post *Post) FlagsCount() int { + return len(post.flags) +} + +func (post *Post) SetVisible(isVisible bool) { + post.isHidden = !isVisible +} + +func (post *Post) IsHidden() bool { + return post.isHidden +} + func (post *Post) AddReply(creator std.Address, body string) *Post { board := post.board pid := board.incGetPostID() @@ -219,6 +245,11 @@ func (post *Post) RenderSummary() string { if !found { return "reposted post does not exist" } + + if thread.isHidden { + return "reposted post was hidden" + } + return "Repost: " + post.GetSummary() + "\n" + thread.RenderSummary() } @@ -267,8 +298,14 @@ func (post *Post) Render(indent string, levels int) string { if levels > 0 { if post.replies.Size() > 0 { post.replies.Iterate("", "", func(_ string, value interface{}) bool { + reply := value.(*Post) + if reply.isHidden { + // TODO: change this in case of pagination + return false + } + s += indent + "\n" - s += value.(*Post).Render(indent+"> ", levels-1) + s += reply.Render(indent+"> ", levels-1) return false }) } diff --git a/examples/gno.land/r/demo/boards2/post_test.gno b/examples/gno.land/r/demo/boards2/post_test.gno index 05888f9e297..232b4ea75cd 100644 --- a/examples/gno.land/r/demo/boards2/post_test.gno +++ b/examples/gno.land/r/demo/boards2/post_test.gno @@ -23,6 +23,27 @@ func TestPostUpdate(t *testing.T) { uassert.False(t, post.GetUpdatedAt().IsZero()) } +func TestPostAddFlag(t *testing.T) { + addr := testutils.TestAddress("creator") + post := createTestThread(t) + + flag := NewFlag(addr, "foobar") + uassert.True(t, post.AddFlag(flag)) + uassert.False(t, post.AddFlag(flag), "should reject flag from duplicate user") + uassert.Equal(t, post.FlagsCount(), 1) +} + +func TestPostSetVisible(t *testing.T) { + post := createTestThread(t) + uassert.False(t, post.IsHidden(), "post should be visible by default") + + post.SetVisible(false) + uassert.True(t, post.IsHidden(), "post should be hidden") + + post.SetVisible(true) + uassert.False(t, post.IsHidden(), "post should be visible") +} + func TestPostAddRepostTo(t *testing.T) { cases := []struct { name, title, body string diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 083f8daff65..62015f9d690 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -36,6 +36,21 @@ func CreateBoard(name string) BoardID { return id } +func FlagThread(bid BoardID, postID PostID, reason string) { + caller := std.GetOrigCaller() + board := mustGetBoard(bid) + assertHasBoardPermission(board, caller, PermissionThreadFlag) + + t, ok := board.GetThread(postID) + if !ok { + panic("post doesn't exist") + } + + if flagItem(t, NewFlag(caller, reason)) { + t.SetVisible(false) + } +} + func CreateThread(bid BoardID, title, body string) PostID { assertIsUserCall() @@ -57,9 +72,10 @@ func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { board := mustGetBoard(bid) thread := mustGetThread(board, threadID) + assertThreadVisible(thread) + // TODO: Assert thread is not locked // TODO: Assert that caller is a board member (when board type is invite only) - var reply *Post if replyID == threadID { // When the parent reply is the thread just add reply to thread @@ -67,11 +83,27 @@ func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { } else { // Try to get parent reply and add a new child reply post := mustGetReply(thread, replyID) + assertReplyVisible(post) + reply = post.AddReply(caller, body) } return reply.id } +func FlagReply(bid BoardID, threadID, replyID PostID, reason string) { + caller := std.GetOrigCaller() + + board := mustGetBoard(bid) + assertHasBoardPermission(board, caller, PermissionThreadFlag) + + thread := mustGetThread(board, threadID) + reply := mustGetReply(thread, replyID) + + if hide := flagItem(reply, NewFlag(caller, reason)); hide { + reply.SetVisible(false) + } +} + func CreateRepost(bid BoardID, threadID PostID, title, body string, dstBoardID BoardID) PostID { assertIsUserCall() @@ -216,3 +248,15 @@ func assertReplyExists(thread *Post, replyID PostID) { panic("reply not found: " + replyID.String()) } } + +func assertThreadVisible(thread *Post) { + if thread.IsHidden() { + panic("thread with ID: " + thread.GetPostID().String() + " was hidden") + } +} + +func assertReplyVisible(thread *Post) { + if thread.IsHidden() { + panic("reply with ID: " + thread.GetPostID().String() + " was hidden") + } +} diff --git a/examples/gno.land/r/demo/boards2/render.gno b/examples/gno.land/r/demo/boards2/render.gno index 02e7fda9e81..ea575bc4fb1 100644 --- a/examples/gno.land/r/demo/boards2/render.gno +++ b/examples/gno.land/r/demo/boards2/render.gno @@ -59,7 +59,9 @@ func renderThread(res *mux.ResponseWriter, req *mux.Request) { board := v.(*Board) thread, found := board.GetThread(PostID(tID)) if !found { - res.Write("Thread does not exist with ID: " + req.GetVar("thread")) + res.Write("Thread does not exist with ID: " + rawID) + } else if thread.IsHidden() { + res.Write("Thread with ID: " + rawID + " has been flagged as inappropriate") } else { res.Write(thread.Render("", 5)) } @@ -96,7 +98,9 @@ func renderReply(res *mux.ResponseWriter, req *mux.Request) { reply, found := thread.GetReply(PostID(rID)) if !found { - res.Write("Reply does not exist with ID: " + req.GetVar("reply")) + res.Write("Reply does not exist with ID: " + rawID) + } else if reply.IsHidden() { + res.Write("Reply with ID: " + rawID + " was hidden") } else { res.Write(reply.RenderInner()) } diff --git a/examples/gno.land/r/demo/boards2/z_2_a_filetest.gno b/examples/gno.land/r/demo/boards2/z_2_a_filetest.gno new file mode 100644 index 00000000000..d95759e4236 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_2_a_filetest.gno @@ -0,0 +1,23 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + bid := boards2.CreateBoard("test1") + pid := boards2.CreateThread(bid, "thread", "thread") + boards2.FlagThread(bid, pid, "reason") + _ = boards2.CreateReply(bid, pid, pid, "reply") +} + +// Error: +// thread with ID: 1 was hidden diff --git a/examples/gno.land/r/demo/boards2/z_2_b_filetest.gno b/examples/gno.land/r/demo/boards2/z_2_b_filetest.gno new file mode 100644 index 00000000000..a4f4d403c8c --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_2_b_filetest.gno @@ -0,0 +1,26 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + // ensure that nested replies denied if root thread is hidden. + bid := boards2.CreateBoard("test1") + pid := boards2.CreateThread(bid, "thread", "thread") + rid := boards2.CreateReply(bid, pid, pid, "reply1") + + boards2.FlagThread(bid, pid, "reason") + _ = boards2.CreateReply(bid, pid, rid, "reply1.1") +} + +// Error: +// thread with ID: 1 was hidden diff --git a/examples/gno.land/r/demo/boards2/z_2_c_filetest.gno b/examples/gno.land/r/demo/boards2/z_2_c_filetest.gno new file mode 100644 index 00000000000..3a27b4497cd --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_2_c_filetest.gno @@ -0,0 +1,26 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + // ensure that nested replies denied if root thread is hidden. + bid := boards2.CreateBoard("test1") + pid := boards2.CreateThread(bid, "thread", "thread") + rid := boards2.CreateReply(bid, pid, pid, "reply1") + + boards2.FlagReply(bid, pid, rid, "reason") + _ = boards2.CreateReply(bid, pid, rid, "reply1.1") +} + +// Error: +// reply with ID: 2 was hidden diff --git a/examples/gno.land/r/demo/boards2/z_2_d_filetest.gno b/examples/gno.land/r/demo/boards2/z_2_d_filetest.gno new file mode 100644 index 00000000000..dbd809c84cd --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_2_d_filetest.gno @@ -0,0 +1,25 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + // Only single user per flag can't be tested atm, as flagThreshold = 1. + bid := boards2.CreateBoard("test1") + pid := boards2.CreateThread(bid, "thread", "thread") + + boards2.FlagThread(bid, pid, "reason1") + boards2.FlagThread(bid, pid, "reason2") +} + +// Error: +// item flag count threshold exceeded: 1 From 3c41887bafafe5c763d474abe96adddfb64f2f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Tue, 7 Jan 2025 15:52:06 +0100 Subject: [PATCH 13/52] chore(boards2): remove permission handlers (#3449) This approach doesn't make a lot of sense for the current implementation, we can use default permissions methods instead. --- examples/gno.land/r/demo/boards2/board.gno | 4 +- .../r/demo/boards2/permission_default.gno | 84 +++++++++++-------- .../r/demo/boards2/permission_handlers.gno | 38 --------- 3 files changed, 51 insertions(+), 75 deletions(-) delete mode 100644 examples/gno.land/r/demo/boards2/permission_handlers.gno diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index 8b667cb291e..bae551e784e 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -132,7 +132,7 @@ func (board *Board) GetPostFormURL() string { // TODO: This is a temporary implementation until the permissions and DAO mecahnics are defined func createDefaultBoardPermissions(owner std.Address) *DefaultPermissions { - perms := NewDefaultPermissions( + return NewDefaultPermissions( admindao.New(admindao.WithMember(owner)), WithSuperRole(RoleOwner), WithRole(RoleAdmin, PermissionMemberInvite), @@ -140,6 +140,4 @@ func createDefaultBoardPermissions(owner std.Address) *DefaultPermissions { // WithRole(RoleModerator, permissions...), WithUser(owner, RoleOwner), ) - perms.HandleFunc(PermissionMemberInvite, handleMemberInvite) - return perms } diff --git a/examples/gno.land/r/demo/boards2/permission_default.gno b/examples/gno.land/r/demo/boards2/permission_default.gno index b4558f82bcc..53c924669ca 100644 --- a/examples/gno.land/r/demo/boards2/permission_default.gno +++ b/examples/gno.land/r/demo/boards2/permission_default.gno @@ -6,32 +6,25 @@ import ( "gno.land/p/demo/avl" "gno.land/p/demo/boards2/admindao" -) -type ( - // PermissionsHandlerFunc defines a function to handle permission callbacks. - // Handlers are called by the `WithPermission()` method to execute callbacks - // when users have the permission assigned. - PermissionsHandlerFunc func(Permissions, Args, func(Args)) - - // DefaultPermissions manages users, roles and permissions. - DefaultPermissions struct { - superRole Role - dao *admindao.AdminDAO - handlers *avl.Tree // string(permission) -> PermissionsHandlerFunc - users *avl.Tree // string(std.Address) -> []Role - roles *avl.Tree // string(role) -> []Permission - } + "gno.land/r/demo/users" ) +// DefaultPermissions manages users, roles and permissions. +type DefaultPermissions struct { + superRole Role + dao *admindao.AdminDAO + users *avl.Tree // string(std.Address) -> []Role + roles *avl.Tree // string(role) -> []Permission +} + // NewDefaultPermissions creates a new permissions type. // This type is a default implementation to handle users, roles and permissions. func NewDefaultPermissions(dao *admindao.AdminDAO, options ...DefaultPermissionsOption) *DefaultPermissions { dp := &DefaultPermissions{ - dao: dao, - handlers: avl.NewTree(), - roles: avl.NewTree(), - users: avl.NewTree(), + dao: dao, + roles: avl.NewTree(), + users: avl.NewTree(), } for _, apply := range options { apply(dp) @@ -124,11 +117,6 @@ func (dp *DefaultPermissions) RemoveUser(user std.Address) bool { return removed } -// HandleFunc registers a handler function for a permission. -func (dp *DefaultPermissions) HandleFunc(p Permission, fn PermissionsHandlerFunc) { - dp.handlers.Set(string(p), fn) -} - // WithPermission calls a callback when a user has a specific permission. // It panics on error or when a handler panics. // Callbacks are by default called when there is no handle registered for the permission. @@ -137,20 +125,51 @@ func (dp *DefaultPermissions) WithPermission(user std.Address, perm Permission, panic("unauthorized") } - h, found := dp.handlers.Get(string(perm)) - if !found { - cb(args) // TODO: Should we fail instead? - return + switch perm { + case PermissionBoardCreate: + dp.handleBoardCreate(args, cb) + case PermissionMemberInvite: + dp.handleMemberInvite(args, cb) + default: + cb(args) + } +} + +func (DefaultPermissions) handleBoardCreate(args Args, cb func(Args)) { + // TODO: This way of dealing with arguments is delicate, ideally types should be used + name := args[0].(string) + if std.Address(name).IsValid() { + panic("addresses are not allowed as board name") + } + + // When the board name is the name of a registered user + // check that caller is the owner of the name. + caller := std.GetOrigCaller() + user := users.GetUserByName(name) + if user != nil && user.Address != caller { + panic("board name is a user name registered to a different user") + } + + cb(args) +} + +func (dp DefaultPermissions) handleMemberInvite(args Args, cb func(Args)) { + // Make sure that only owners invite other owners + role := args[1].(Role) + if role == RoleOwner { + caller := std.GetOrigCaller() + if !dp.HasRole(caller, RoleOwner) { + panic("only owners are allowed to invite other owners") + } } - fn := h.(PermissionsHandlerFunc) - fn(dp, args, cb) + cb(args) } func createDefaultPermissions(owner std.Address) *DefaultPermissions { // TODO: DAO should be a different realm or proposal and voting functions should be part of boards realm? // Permissions and DAO mechanics should be discussed and improved. Add `GetDAO()` to `Permissions`?? - perms := NewDefaultPermissions( + return NewDefaultPermissions( admindao.New(admindao.WithMember(owner)), WithSuperRole(RoleOwner), WithRole(RoleAdmin, PermissionBoardCreate, PermissionMemberInvite), @@ -158,7 +177,4 @@ func createDefaultPermissions(owner std.Address) *DefaultPermissions { // WithRole(RoleModerator, permissions...), WithUser(owner, RoleOwner), ) - perms.HandleFunc(PermissionBoardCreate, handleBoardCreate) - perms.HandleFunc(PermissionMemberInvite, handleMemberInvite) - return perms } diff --git a/examples/gno.land/r/demo/boards2/permission_handlers.gno b/examples/gno.land/r/demo/boards2/permission_handlers.gno deleted file mode 100644 index 63b351f1d53..00000000000 --- a/examples/gno.land/r/demo/boards2/permission_handlers.gno +++ /dev/null @@ -1,38 +0,0 @@ -package boards2 - -import ( - "std" - - "gno.land/r/demo/users" -) - -func handleBoardCreate(_ Permissions, args Args, cb func(Args)) { - // TODO: This way of dealing with arguments is delicate, ideally types should be used - name := args[0].(string) - if std.Address(name).IsValid() { - panic("addresses are not allowed as board name") - } - - // When the board name is the name of a registered user - // check that caller is the owner of the name. - caller := std.GetOrigCaller() - user := users.GetUserByName(name) - if user != nil && user.Address != caller { - panic("board name is a user name registered to a different user") - } - - cb(args) -} - -func handleMemberInvite(p Permissions, args Args, cb func(Args)) { - // Make sure that only owners invite other owners - role := args[1].(Role) - if role == RoleOwner { - caller := std.GetOrigCaller() - if !p.HasRole(caller, RoleOwner) { - panic("only owners are allowed to invite other owners") - } - } - - cb(args) -} From 40eead05165cc90fe8d4b8f804908a8febcc4d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Thu, 9 Jan 2025 18:00:19 +0100 Subject: [PATCH 14/52] feat(boards2): board renaming (#3462) Resolves #3141 --- examples/gno.land/r/demo/boards2/board.gno | 3 +- examples/gno.land/r/demo/boards2/boards.gno | 9 +++ .../gno.land/r/demo/boards2/permission.gno | 12 +++- .../r/demo/boards2/permission_default.gno | 64 +++++++++++++++---- examples/gno.land/r/demo/boards2/public.gno | 44 +++++++++++-- .../r/demo/boards2/z_3_a_filetest.gno | 32 ++++++++++ .../r/demo/boards2/z_3_b_filetest.gno | 24 +++++++ .../r/demo/boards2/z_3_c_filetest.gno | 24 +++++++ .../r/demo/boards2/z_3_d_filetest.gno | 20 ++++++ .../r/demo/boards2/z_3_e_filetest.gno | 24 +++++++ .../r/demo/boards2/z_3_f_filetest.gno | 43 +++++++++++++ .../r/demo/boards2/z_3_g_filetest.gno | 47 ++++++++++++++ 12 files changed, 327 insertions(+), 19 deletions(-) create mode 100644 examples/gno.land/r/demo/boards2/z_3_a_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_3_b_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_3_c_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_3_d_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_3_e_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_3_f_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_3_g_filetest.gno diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index bae551e784e..d6a5306860c 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -25,6 +25,7 @@ func (id BoardID) Key() string { type Board struct { id BoardID // only set for public boards. name string + aliases []string creator std.Address threads avl.Tree // Post.id -> *Post postsCtr uint64 // increments Post.id @@ -135,7 +136,7 @@ func createDefaultBoardPermissions(owner std.Address) *DefaultPermissions { return NewDefaultPermissions( admindao.New(admindao.WithMember(owner)), WithSuperRole(RoleOwner), - WithRole(RoleAdmin, PermissionMemberInvite), + WithRole(RoleAdmin, PermissionMemberInvite, PermissionBoardRename), // TODO: Finish assigning all roles and permissions // WithRole(RoleModerator, permissions...), WithUser(owner, RoleOwner), diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/demo/boards2/boards.gno index 70e02d5dffe..8a0c57bd9dc 100644 --- a/examples/gno.land/r/demo/boards2/boards.gno +++ b/examples/gno.land/r/demo/boards2/boards.gno @@ -36,6 +36,15 @@ func getBoard(id BoardID) (_ *Board, found bool) { return v.(*Board), true } +// mustGetBoardByName returns a board or panics when it's not found. +func mustGetBoardByName(name string) *Board { + v, found := gBoardsByName.Get(name) + if !found { + panic("board does not exist with name: " + name) + } + return v.(*Board) +} + // mustGetBoard returns a board or panics when it's not found. func mustGetBoard(id BoardID) *Board { board, found := getBoard(id) diff --git a/examples/gno.land/r/demo/boards2/permission.gno b/examples/gno.land/r/demo/boards2/permission.gno index e465e346418..e9a91b7b670 100644 --- a/examples/gno.land/r/demo/boards2/permission.gno +++ b/examples/gno.land/r/demo/boards2/permission.gno @@ -1,9 +1,14 @@ package boards2 -import "std" +import ( + "std" + + "gno.land/p/demo/boards2/admindao" +) const ( PermissionBoardCreate Permission = "board:create" + PermissionBoardRename = "board:rename" PermissionThreadCreate = "thread:create" PermissionThreadEdit = "thread:edit" PermissionThreadDelete = "thread:delete" @@ -19,6 +24,7 @@ const ( RoleOwner Role = "owner" RoleAdmin = "admin" RoleModerator = "moderator" + RoleGuest = "" ) type ( @@ -48,5 +54,9 @@ type ( // RemoveUser removes a user from the permissioner. RemoveUser(std.Address) (removed bool) + + // GetDAO returns the underlying DAO. + // Returned value can be nil if the implementation doesn't have a DAO. + GetDAO() *admindao.AdminDAO // TODO: should return an interface } ) diff --git a/examples/gno.land/r/demo/boards2/permission_default.gno b/examples/gno.land/r/demo/boards2/permission_default.gno index 53c924669ca..a98b2fdb6f3 100644 --- a/examples/gno.land/r/demo/boards2/permission_default.gno +++ b/examples/gno.land/r/demo/boards2/permission_default.gno @@ -35,6 +35,10 @@ func NewDefaultPermissions(dao *admindao.AdminDAO, options ...DefaultPermissions // Roles returns the list of roles. func (dp DefaultPermissions) Roles() []Role { var roles []Role + if dp.superRole != "" { + roles = append(roles, dp.superRole) + } + dp.roles.Iterate("", "", func(name string, _ interface{}) bool { roles = append(roles, Role(name)) return false @@ -44,6 +48,10 @@ func (dp DefaultPermissions) Roles() []Role { // RoleExists checks if a role exists. func (dp DefaultPermissions) RoleExists(r Role) bool { + if dp.superRole != "" && r == dp.superRole { + return true + } + return dp.roles.Iterate("", "", func(name string, _ interface{}) bool { return Role(name) == r }) @@ -117,6 +125,12 @@ func (dp *DefaultPermissions) RemoveUser(user std.Address) bool { return removed } +// GetDAO returns the underlying DAO. +// Returned value can be nil if the implementation doesn't have a DAO. +func (dp DefaultPermissions) GetDAO() *admindao.AdminDAO { + return dp.dao +} + // WithPermission calls a callback when a user has a specific permission. // It panics on error or when a handler panics. // Callbacks are by default called when there is no handle registered for the permission. @@ -128,6 +142,8 @@ func (dp *DefaultPermissions) WithPermission(user std.Address, perm Permission, switch perm { case PermissionBoardCreate: dp.handleBoardCreate(args, cb) + case PermissionBoardRename: + dp.handleBoardRename(args, cb) case PermissionMemberInvite: dp.handleMemberInvite(args, cb) default: @@ -136,26 +152,36 @@ func (dp *DefaultPermissions) WithPermission(user std.Address, perm Permission, } func (DefaultPermissions) handleBoardCreate(args Args, cb func(Args)) { - // TODO: This way of dealing with arguments is delicate, ideally types should be used - name := args[0].(string) - if std.Address(name).IsValid() { - panic("addresses are not allowed as board name") + name, ok := args[0].(string) + if !ok { + panic("expected board name to be a string") } - // When the board name is the name of a registered user - // check that caller is the owner of the name. - caller := std.GetOrigCaller() - user := users.GetUserByName(name) - if user != nil && user.Address != caller { - panic("board name is a user name registered to a different user") + assertBoardNameIsNotAddress(name) + assertBoardNameBelongsToCaller(name) + + cb(args) +} + +func (DefaultPermissions) handleBoardRename(args Args, cb func(Args)) { + newName, ok := args[2].(string) + if !ok { + panic("expected new board name to be a string") } + assertBoardNameIsNotAddress(newName) + assertBoardNameBelongsToCaller(newName) + cb(args) } func (dp DefaultPermissions) handleMemberInvite(args Args, cb func(Args)) { // Make sure that only owners invite other owners - role := args[1].(Role) + role, ok := args[1].(Role) + if !ok { + panic("expected a valid new member role") + } + if role == RoleOwner { caller := std.GetOrigCaller() if !dp.HasRole(caller, RoleOwner) { @@ -178,3 +204,19 @@ func createDefaultPermissions(owner std.Address) *DefaultPermissions { WithUser(owner, RoleOwner), ) } + +func assertBoardNameIsNotAddress(s string) { + if std.Address(s).IsValid() { + panic("addresses are not allowed as board name") + } +} + +func assertBoardNameBelongsToCaller(name string) { + // When the board name is the name of a registered user + // check that caller is the owner of the name. + caller := std.GetOrigCaller() + user := users.GetUserByName(name) + if user != nil && user.Address != caller { + panic("board name is a user name registered to a different user") + } +} diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 62015f9d690..ebc39a45bf0 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -17,17 +17,14 @@ func CreateBoard(name string) BoardID { assertIsUserCall() name = strings.TrimSpace(name) - if name == "" { - panic("board name is empty") - } + assertBoardNameIsNotEmpty(name) + assertBoardNameNotExists(name) caller := std.GetOrigCaller() id := incGetBoardID() args := Args{name, id} gPerm.WithPermission(caller, PermissionBoardCreate, args, func(Args) { - if gBoardsByName.Has(name) { - panic("board already exists") - } + assertBoardNameNotExists(name) board := newBoard(id, name, caller) gBoardsByID.Set(id.Key(), board) @@ -36,6 +33,29 @@ func CreateBoard(name string) BoardID { return id } +func RenameBoard(name, newName string) { + assertIsUserCall() + + newName = strings.TrimSpace(newName) + assertBoardNameIsNotEmpty(newName) + assertBoardNameNotExists(newName) + + board := mustGetBoardByName(name) + bid := board.GetID() + caller := std.GetOrigCaller() + args := Args{bid, name, newName} + board.perms.WithPermission(caller, PermissionBoardRename, args, func(Args) { + assertBoardNameNotExists(newName) + + board := mustGetBoard(bid) + board.aliases = append(board.aliases, board.name) + board.name = newName + + // Index board for the new name keeping previous indexes for older names + gBoardsByName.Set(newName, board) + }) +} + func FlagThread(bid BoardID, postID PostID, reason string) { caller := std.GetOrigCaller() board := mustGetBoard(bid) @@ -237,6 +257,18 @@ func assertBoardExists(id BoardID) { } } +func assertBoardNameIsNotEmpty(name string) { + if name == "" { + panic("board name is empty") + } +} + +func assertBoardNameNotExists(name string) { + if gBoardsByName.Has(name) { + panic("board already exists") + } +} + func assertThreadExists(b *Board, threadID PostID) { if _, found := b.GetThread(threadID); !found { panic("thread not found: " + threadID.String()) diff --git a/examples/gno.land/r/demo/boards2/z_3_a_filetest.gno b/examples/gno.land/r/demo/boards2/z_3_a_filetest.gno new file mode 100644 index 00000000000..0b06c219725 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_3_a_filetest.gno @@ -0,0 +1,32 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + name = "foo" +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.CreateBoard(name) +} + +func main() { + newName := "bar" + _, exists := boards2.GetBoardIDFromName(newName) + println("Exists =", exists) + + boards2.RenameBoard(name, newName) + + bid, _ := boards2.GetBoardIDFromName(newName) + println("ID =", bid) +} + +// Output: +// Exists = false +// ID = 1 diff --git a/examples/gno.land/r/demo/boards2/z_3_b_filetest.gno b/examples/gno.land/r/demo/boards2/z_3_b_filetest.gno new file mode 100644 index 00000000000..02186772956 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_3_b_filetest.gno @@ -0,0 +1,24 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + name = "foo" +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.CreateBoard(name) +} + +func main() { + boards2.RenameBoard(name, "") +} + +// Error: +// board name is empty diff --git a/examples/gno.land/r/demo/boards2/z_3_c_filetest.gno b/examples/gno.land/r/demo/boards2/z_3_c_filetest.gno new file mode 100644 index 00000000000..7567c7872e6 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_3_c_filetest.gno @@ -0,0 +1,24 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + name = "foo" +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.CreateBoard(name) +} + +func main() { + boards2.RenameBoard(name, name) +} + +// Error: +// board already exists diff --git a/examples/gno.land/r/demo/boards2/z_3_d_filetest.gno b/examples/gno.land/r/demo/boards2/z_3_d_filetest.gno new file mode 100644 index 00000000000..343abb76d3c --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_3_d_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.RenameBoard("unexisting", "foo") +} + +// Error: +// board does not exist with name: unexisting diff --git a/examples/gno.land/r/demo/boards2/z_3_e_filetest.gno b/examples/gno.land/r/demo/boards2/z_3_e_filetest.gno new file mode 100644 index 00000000000..4075e8712cd --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_3_e_filetest.gno @@ -0,0 +1,24 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + name = "foo" +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.CreateBoard(name) +} + +func main() { + boards2.RenameBoard(name, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") +} + +// Error: +// addresses are not allowed as board name diff --git a/examples/gno.land/r/demo/boards2/z_3_f_filetest.gno b/examples/gno.land/r/demo/boards2/z_3_f_filetest.gno new file mode 100644 index 00000000000..bd1a3dafe08 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_3_f_filetest.gno @@ -0,0 +1,43 @@ +package main + +// SEND: 200000000ugnot + +import ( + "std" + + "gno.land/r/demo/boards2" + "gno.land/r/demo/users" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + name = "foo" + newName = "barbaz" +) + +func init() { + std.TestSetOrigCaller(owner) + + // Test1 is the boards owner and its address has a user already registered + // so a new member must register a user with the new board name. + boards2.InviteMember(member, boards2.RoleOwner) + std.TestSetOrigCaller(member) + users.Register("", newName, "") + + boards2.CreateBoard(name) +} + +func main() { + _, exists := boards2.GetBoardIDFromName(newName) + println("Exists =", exists) + + boards2.RenameBoard(name, newName) + + bid, _ := boards2.GetBoardIDFromName(newName) + println("ID =", bid) +} + +// Output: +// Exists = false +// ID = 1 diff --git a/examples/gno.land/r/demo/boards2/z_3_g_filetest.gno b/examples/gno.land/r/demo/boards2/z_3_g_filetest.gno new file mode 100644 index 00000000000..30617a49e52 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_3_g_filetest.gno @@ -0,0 +1,47 @@ +package main + +// SEND: 200000000ugnot + +import ( + "std" + + "gno.land/r/demo/boards2" + "gno.land/r/demo/users" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + member2 = std.Address("g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5") + name = "foo" + newName = "barbaz" +) + +func init() { + std.TestSetOrigCaller(owner) + + // Test1 is the boards owner and its address has a user already registered + // so a new member must register a user with the new board name. + boards2.InviteMember(member, boards2.RoleOwner) + std.TestSetOrigCaller(member) + users.Register("", newName, "") + + // Invite a new member that doesn't own the user that matches the new board name + boards2.InviteMember(member2, boards2.RoleOwner) + std.TestSetOrigCaller(member2) + + boards2.CreateBoard(name) +} + +func main() { + _, exists := boards2.GetBoardIDFromName(newName) + println("Exists =", exists) + + boards2.RenameBoard(name, newName) + + bid, _ := boards2.GetBoardIDFromName(newName) + println("ID =", bid) +} + +// Error: +// board name is a user name registered to a different user From 4090aac6f29b3fba66009922780cb04282890934 Mon Sep 17 00:00:00 2001 From: Denys Sedchenko <9203548+x1unix@users.noreply.github.com> Date: Thu, 16 Jan 2025 04:12:00 -0500 Subject: [PATCH 15/52] feat(boardsv2): initialize reposting (#3513) Based on https://github.com/gnolang/gno/pull/3469 Closes: https://github.com/gnolang/gno/issues/3227 --- examples/gno.land/r/demo/boards2/format.gno | 5 + examples/gno.land/r/demo/boards2/post.gno | 119 ++++++++++++------ .../gno.land/r/demo/boards2/post_test.gno | 10 +- examples/gno.land/r/demo/boards2/public.gno | 12 +- 4 files changed, 104 insertions(+), 42 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/format.gno b/examples/gno.land/r/demo/boards2/format.gno index a316b42cb5b..9094139a656 100644 --- a/examples/gno.land/r/demo/boards2/format.gno +++ b/examples/gno.land/r/demo/boards2/format.gno @@ -55,6 +55,11 @@ func newLink(label, uri string) string { return "[" + label + "](" + uri + ")" } +// newButtonLink returns a Makdown link with label wrapped in brackets. +func newButtonLink(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) diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index 5a79302af22..efc0bc2f4ed 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -38,6 +38,7 @@ type Post struct { threadID PostID // original Post.id parentID PostID // parent Post.id (if reply or repost) repostBoardID BoardID // original Board.id (if repost) + repostsCount uint64 createdAt time.Time updatedAt time.Time } @@ -60,7 +61,8 @@ func newPost(board *Board, id PostID, creator std.Address, title, body string, t } func (post *Post) IsThread() bool { - return post.parentID == 0 + // repost threads also have parent ID + return post.parentID == 0 || post.repostBoardID != 0 } func (post *Post) GetBoard() *Board { @@ -153,18 +155,20 @@ func (thread *Post) GetReply(pid PostID) (_ *Post, found bool) { return v.(*Post), true } -func (post *Post) AddRepostTo(creator std.Address, title, body string, dst *Board) *Post { +func (post *Post) AddRepostTo(creator std.Address, repost *Post, dst *Board) { 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) + if post.isHidden { + panic("thread has been flagged as inappropriate") + } + + post.repostsCount++ + dst.threads.Set(repost.id.Key(), repost) if !dst.IsPrivate() { - post.reposts.Set(dst.id.Key(), pid) + post.reposts.Set(dst.id.Key(), repost.id) } - return repost } func (post *Post) DeleteReply(replyID PostID) error { @@ -216,7 +220,7 @@ func (post *Post) GetReplyFormURL() string { func (post *Post) GetRepostFormURL() string { return txlink.Call("CreateRepost", "bid", post.board.id.String(), - "postID", post.id.String(), + "threadID", post.id.String(), ) } @@ -235,24 +239,6 @@ func (post *Post) GetDeleteFormURL() string { } func (post *Post) RenderSummary() string { - if post.repostBoardID != 0 { - dstBoard, found := getBoard(post.repostBoardID) - if !found { - panic("repost board does not exist") - } - - thread, found := dstBoard.GetThread(PostID(post.parentID)) - if !found { - return "reposted post does not exist" - } - - if thread.isHidden { - return "reposted post was hidden" - } - - return "Repost: " + post.GetSummary() + "\n" + thread.RenderSummary() - } - var ( s string postURL = post.GetURL() @@ -263,14 +249,43 @@ func (post *Post) RenderSummary() string { } s += post.GetSummary() + "\n" + + repostBody, _ := post.renderSourcePost("") + s += repostBody + s += "\\- " + newUserLink(post.creator) + "," s += " " + newLink(post.createdAt.Format(dateFormat), postURL) - s += " \\[" + newLink("x", post.GetDeleteFormURL()) + "]" + s += " " + newButtonLink("x", post.GetDeleteFormURL()) s += " (" + strconv.Itoa(post.replies.Size()) + " replies)" s += " (" + strconv.Itoa(post.reposts.Size()) + " reposts)" + "\n" return s } +func (post *Post) renderSourcePost(indent string) (string, *Post) { + if post.repostBoardID == 0 { + return "", nil + } + + indent += "> " + + // TODO: figure out a way to decouple posts from a global storage. + board, ok := getBoard(post.repostBoardID) + if !ok { + return indentBody(indent, "*Source board is not available*\n\n"), nil + } + + srcPost, ok := board.GetThread(post.parentID) + if !ok { + return indentBody(indent, "*Source post is not available*\n\n"), nil + } + + if srcPost.isHidden { + return indentBody(indent, "*Source post has been flagged as inappropriate*\n\n"), nil + } + + return indentBody(indent, srcPost.GetSummary()) + "\n\n", srcPost +} + func (post *Post) Render(indent string, levels int) string { if post == nil { return "nil post" @@ -286,14 +301,34 @@ func (post *Post) Render(indent string, levels int) string { s += indent + "\n" } + srcContent, srcPost := post.renderSourcePost(indent) + + s += srcContent s += indentBody(indent, post.body) + "\n" // TODO: indent body lines. + + if post.IsThread() { + // Split content and controls for threads. + s += "\n" + } + s += indent + "\\- " + newUserLink(post.creator) + ", " s += newLink(post.createdAt.Format(dateFormat), postURL) - s += " \\[" + newLink("reply", post.GetReplyFormURL()) + "]" + + if post.repostsCount > 0 { + s += ", " + strconv.FormatUint(post.repostsCount, 10) + " reposts" + } + + if srcPost != nil { + s += " " + newButtonLink("see source post", srcPost.GetURL()) + } + + s += " " + newButtonLink("reply", post.GetReplyFormURL()) + if post.IsThread() { - s += " \\[" + newLink("repost", post.GetRepostFormURL()) + "]" + s += " " + newButtonLink("repost", post.GetRepostFormURL()) } - s += " \\[" + newLink("x", post.GetDeleteFormURL()) + "]\n" + + s += " " + newButtonLink("x", post.GetDeleteFormURL()) + "\n" if levels > 0 { if post.replies.Size() > 0 { @@ -322,20 +357,28 @@ func (post *Post) RenderInner() string { } 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) + s := "_" + newLink("see thread", post.board.GetURLFromThreadID(threadID)) + "_\n\n" + + // Fully render parent if it's not a repost. + if post.repostBoardID == 0 { + var ( + parent *Post + parentID = post.parentID + ) + + if thread.id == parentID { + parent = thread + } else { + parent, _ = thread.GetReply(parentID) + } + + s += parent.Render("", 0) + "\n" } - 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/post_test.gno b/examples/gno.land/r/demo/boards2/post_test.gno index 232b4ea75cd..c1307ebf8b6 100644 --- a/examples/gno.land/r/demo/boards2/post_test.gno +++ b/examples/gno.land/r/demo/boards2/post_test.gno @@ -75,7 +75,13 @@ func TestPostAddRepostTo(t *testing.T) { ) createRepost := func() { - repost = thread.AddRepostTo(creator, tc.title, tc.body, tc.dstBoard) + var repostId PostID + if tc.dstBoard != nil { + repostId = tc.dstBoard.incGetPostID() + } + + repost = newPost(tc.dstBoard, repostId, creator, tc.title, tc.body, repostId, thread.GetPostID(), thread.GetBoard().GetID()) + thread.AddRepostTo(creator, repost, tc.dstBoard) } if tc.err != "" { @@ -116,7 +122,7 @@ func TestNewThread(t *testing.T) { uint(threadID), ) repostURL := ufmt.Sprintf( - "/r/demo/boards2$help&func=CreateRepost&bid=%d&postID=%d", + "/r/demo/boards2$help&func=CreateRepost&bid=%d&threadID=%d", uint(boardID), uint(threadID), ) diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index ebc39a45bf0..15f53a823cb 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -143,8 +143,16 @@ func CreateRepost(bid BoardID, threadID PostID, title, body string, dstBoardID B dst := mustGetBoard(dstBoardID) thread := mustGetThread(board, threadID) - repost := thread.AddRepostTo(caller, title, body, dst) - return repost.id + + repostId := dst.incGetPostID() + repost := newPost(dst, repostId, caller, title, body, repostId, thread.GetPostID(), thread.GetBoard().GetID()) + + args := Args{board.GetID(), threadID, dst.GetID()} + dst.GetPermissions().WithPermission(caller, PermissionThreadRepost, args, func(_ Args) { + thread.AddRepostTo(caller, repost, dst) + }) + + return repostId } func DeleteThread(bid BoardID, threadID PostID) { From adaafd745113f0bfd27e56b1dfce0df4b5d74aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Thu, 16 Jan 2025 15:22:58 +0100 Subject: [PATCH 16/52] feat(boards2): member role management (#3512) Resolves #3171 --- examples/gno.land/r/demo/boards2/board.gno | 31 ++++- .../gno.land/r/demo/boards2/permission.gno | 7 + .../r/demo/boards2/permission_default.gno | 108 +++++++++++---- .../demo/boards2/permission_default_test.gno | 130 +++++++++++++++--- .../r/demo/boards2/permission_options.gno | 5 +- examples/gno.land/r/demo/boards2/public.gno | 48 +++++-- .../r/demo/boards2/z_1_a_filetest.gno | 3 +- .../r/demo/boards2/z_1_b_filetest.gno | 5 +- .../r/demo/boards2/z_1_c_filetest.gno | 5 +- .../r/demo/boards2/z_3_f_filetest.gno | 3 +- .../r/demo/boards2/z_3_g_filetest.gno | 5 +- .../r/demo/boards2/z_4_a_filetest.gno | 36 +++++ .../r/demo/boards2/z_4_b_filetest.gno | 38 +++++ .../r/demo/boards2/z_4_c_filetest.gno | 29 ++++ .../r/demo/boards2/z_4_d_filetest.gno | 29 ++++ .../r/demo/boards2/z_4_e_filetest.gno | 26 ++++ .../r/demo/boards2/z_4_f_filetest.gno | 25 ++++ .../r/demo/boards2/z_4_g_filetest.gno | 25 ++++ 18 files changed, 491 insertions(+), 67 deletions(-) create mode 100644 examples/gno.land/r/demo/boards2/z_4_a_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_4_b_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_4_c_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_4_d_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_4_e_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_4_f_filetest.gno create mode 100644 examples/gno.land/r/demo/boards2/z_4_g_filetest.gno diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index d6a5306860c..75469943398 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -131,14 +131,35 @@ func (board *Board) GetPostFormURL() string { return txlink.Call("CreateThread", "bid", board.id.String()) } -// TODO: This is a temporary implementation until the permissions and DAO mecahnics are defined func createDefaultBoardPermissions(owner std.Address) *DefaultPermissions { return NewDefaultPermissions( - admindao.New(admindao.WithMember(owner)), + admindao.New(), WithSuperRole(RoleOwner), - WithRole(RoleAdmin, PermissionMemberInvite, PermissionBoardRename), - // TODO: Finish assigning all roles and permissions - // WithRole(RoleModerator, permissions...), + WithRole( + RoleAdmin, + PermissionBoardRename, + PermissionMemberInvite, + PermissionMemberRemove, + PermissionThreadCreate, + PermissionThreadEdit, + PermissionThreadDelete, + PermissionThreadFlag, + PermissionReplyDelete, + PermissionReplyFlag, + PermissionRoleChange, + ), + WithRole( + RoleModerator, + PermissionThreadCreate, + PermissionThreadEdit, + PermissionThreadFlag, + PermissionReplyFlag, + ), + WithRole( + RoleGuest, + PermissionThreadCreate, + PermissionThreadRepost, + ), WithUser(owner, RoleOwner), ) } diff --git a/examples/gno.land/r/demo/boards2/permission.gno b/examples/gno.land/r/demo/boards2/permission.gno index e9a91b7b670..aaae6d463b6 100644 --- a/examples/gno.land/r/demo/boards2/permission.gno +++ b/examples/gno.land/r/demo/boards2/permission.gno @@ -18,6 +18,7 @@ const ( PermissionReplyFlag = "reply:flag" PermissionMemberInvite = "member:invite" PermissionMemberRemove = "member:remove" + PermissionRoleChange = "role:change" ) const ( @@ -52,9 +53,15 @@ type ( // AddUser adds a new user to the permissioner. AddUser(std.Address, ...Role) error + // SetUserRoles sets the roles of a user. + SetUserRoles(std.Address, ...Role) error + // RemoveUser removes a user from the permissioner. RemoveUser(std.Address) (removed bool) + // HasUser checks if a user exists. + HasUser(std.Address) bool + // GetDAO returns the underlying DAO. // Returned value can be nil if the implementation doesn't have a DAO. GetDAO() *admindao.AdminDAO // TODO: should return an interface diff --git a/examples/gno.land/r/demo/boards2/permission_default.gno b/examples/gno.land/r/demo/boards2/permission_default.gno index a98b2fdb6f3..a24bc02f2ec 100644 --- a/examples/gno.land/r/demo/boards2/permission_default.gno +++ b/examples/gno.land/r/demo/boards2/permission_default.gno @@ -32,20 +32,6 @@ func NewDefaultPermissions(dao *admindao.AdminDAO, options ...DefaultPermissions return dp } -// Roles returns the list of roles. -func (dp DefaultPermissions) Roles() []Role { - var roles []Role - if dp.superRole != "" { - roles = append(roles, dp.superRole) - } - - dp.roles.Iterate("", "", func(name string, _ interface{}) bool { - roles = append(roles, Role(name)) - return false - }) - return roles -} - // RoleExists checks if a role exists. func (dp DefaultPermissions) RoleExists(r Role) bool { if dp.superRole != "" && r == dp.superRole { @@ -104,18 +90,18 @@ func (dp *DefaultPermissions) AddUser(user std.Address, roles ...Role) error { return errors.New("user already exists") } - for _, r := range roles { - if !dp.RoleExists(r) { - return errors.New("invalid role: " + string(r)) - } - } - if err := dp.dao.AddMember(user); err != nil { return err } + return dp.setUserRoles(user, roles...) +} - dp.users.Set(user.String(), append([]Role(nil), roles...)) - return nil +// SetUserRoles sets the roles of a user. +func (dp *DefaultPermissions) SetUserRoles(user std.Address, roles ...Role) error { + if !dp.users.Has(user.String()) { + return errors.New("user not found") + } + return dp.setUserRoles(user, roles...) } // RemoveUser removes a user from permissions. @@ -125,6 +111,11 @@ func (dp *DefaultPermissions) RemoveUser(user std.Address) bool { return removed } +// HasUser checks if a user exists. +func (dp DefaultPermissions) HasUser(user std.Address) bool { + return dp.users.Has(user.String()) +} + // GetDAO returns the underlying DAO. // Returned value can be nil if the implementation doesn't have a DAO. func (dp DefaultPermissions) GetDAO() *admindao.AdminDAO { @@ -146,11 +137,24 @@ func (dp *DefaultPermissions) WithPermission(user std.Address, perm Permission, dp.handleBoardRename(args, cb) case PermissionMemberInvite: dp.handleMemberInvite(args, cb) + case PermissionRoleChange: + dp.handleRoleChange(args, cb) default: cb(args) } } +func (dp *DefaultPermissions) setUserRoles(user std.Address, roles ...Role) error { + for _, r := range roles { + if !dp.RoleExists(r) { + return errors.New("invalid role: " + string(r)) + } + } + + dp.users.Set(user.String(), append([]Role(nil), roles...)) + return nil +} + func (DefaultPermissions) handleBoardCreate(args Args, cb func(Args)) { name, ok := args[0].(string) if !ok { @@ -192,15 +196,63 @@ func (dp DefaultPermissions) handleMemberInvite(args Args, cb func(Args)) { cb(args) } +func (dp DefaultPermissions) handleRoleChange(args Args, cb func(Args)) { + // Owners and Admins can change roles. + // Admins should not be able to assign or remove the Owner role from members. + caller := std.GetOrigCaller() + if dp.HasRole(caller, RoleAdmin) { + role, ok := args[2].(Role) + if !ok { + panic("expected a valid member role") + } + + if role == RoleOwner { + panic("admins are not allowed to promote members to Owner") + } else { + member, ok := args[1].(std.Address) + if !ok { + panic("expected a valid member address") + } + + if dp.HasRole(member, RoleOwner) { + panic("admins are not allowed to remove the Owner role") + } + } + } + + cb(args) +} + func createDefaultPermissions(owner std.Address) *DefaultPermissions { - // TODO: DAO should be a different realm or proposal and voting functions should be part of boards realm? - // Permissions and DAO mechanics should be discussed and improved. Add `GetDAO()` to `Permissions`?? return NewDefaultPermissions( - admindao.New(admindao.WithMember(owner)), + admindao.New(), WithSuperRole(RoleOwner), - WithRole(RoleAdmin, PermissionBoardCreate, PermissionMemberInvite), - // TODO: Finish assigning all roles and permissions - // WithRole(RoleModerator, permissions...), + WithRole( + RoleAdmin, + PermissionBoardCreate, + PermissionBoardRename, + PermissionMemberInvite, + PermissionMemberRemove, + PermissionThreadCreate, + PermissionThreadEdit, + PermissionThreadDelete, + PermissionThreadFlag, + PermissionReplyDelete, + PermissionReplyFlag, + PermissionRoleChange, + ), + WithRole( + RoleModerator, + PermissionThreadCreate, + PermissionThreadEdit, + PermissionThreadFlag, + PermissionReplyFlag, + ), + WithRole( + RoleGuest, + PermissionThreadCreate, + PermissionThreadRepost, + ), WithUser(owner, RoleOwner), ) } diff --git a/examples/gno.land/r/demo/boards2/permission_default_test.gno b/examples/gno.land/r/demo/boards2/permission_default_test.gno index a3c51fb6abe..625d56720c0 100644 --- a/examples/gno.land/r/demo/boards2/permission_default_test.gno +++ b/examples/gno.land/r/demo/boards2/permission_default_test.gno @@ -17,11 +17,6 @@ func TestNewDefaultPermissions(t *testing.T) { perms := NewDefaultPermissions(dao, WithRole("a", "permission1"), WithRole("b", "permission2")) - urequire.Equal(t, len(roles), len(perms.Roles()), "roles") - for i, r := range perms.Roles() { - uassert.Equal(t, string(roles[i]), string(r)) - } - for _, r := range roles { uassert.True(t, perms.RoleExists(r)) } @@ -42,7 +37,7 @@ func TestDefaultPermissionsWithPermission(t *testing.T) { user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", perms: NewDefaultPermissions( - admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), WithRole("foo", "bar"), ), @@ -54,7 +49,7 @@ func TestDefaultPermissionsWithPermission(t *testing.T) { permission: "bar", args: Args{"a", "b"}, perms: NewDefaultPermissions( - admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), WithRole("foo", "bar"), ), @@ -65,7 +60,7 @@ func TestDefaultPermissionsWithPermission(t *testing.T) { user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", perms: NewDefaultPermissions( - admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), WithRole("foo", "bar"), ), @@ -75,12 +70,8 @@ func TestDefaultPermissionsWithPermission(t *testing.T) { name: "is not a DAO member", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", - perms: NewDefaultPermissions( - admindao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), - WithRole("foo", "bar"), - ), - err: "unauthorized", + perms: NewDefaultPermissions(admindao.New()), + err: "unauthorized", }, } @@ -314,7 +305,7 @@ func TestDefaultPermissionsAddUser(t *testing.T) { user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), setup: func() *DefaultPermissions { return NewDefaultPermissions( - admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), ) }, @@ -364,6 +355,113 @@ func TestDefaultPermissionsAddUser(t *testing.T) { } } +func TestDefaultPermissionsSetUserRoles(t *testing.T) { + cases := []struct { + name string + user std.Address + roles []Role + setup func() *DefaultPermissions + err string + }{ + { + name: "single role", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + roles: []Role{"b"}, + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(), + WithRole("a", "permission1"), + WithRole("b", "permission2"), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a"), + ) + }, + }, + { + name: "multiple roles", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + roles: []Role{"b", "c"}, + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(), + WithRole("a", "permission1"), + WithRole("b", "permission2"), + WithRole("c", "permission2"), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a"), + ) + }, + }, + { + name: "duplicated role", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + roles: []Role{"a", "c"}, + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(), + WithRole("a", "permission1"), + WithRole("b", "permission2"), + WithRole("c", "permission2"), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a", "c"), + ) + }, + }, + { + name: "remove roles", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(), + WithRole("a", "permission1"), + WithRole("b", "permission2"), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a", "b"), + ) + }, + }, + { + name: "invalid role", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + roles: []Role{"x", "a"}, + setup: func() *DefaultPermissions { + return NewDefaultPermissions( + admindao.New(), + WithRole("a", "permission1"), + WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a"), + ) + }, + err: "invalid role: x", + }, + { + name: "user not found", + user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + setup: func() *DefaultPermissions { + return NewDefaultPermissions(admindao.New()) + }, + err: "user not found", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + perm := tc.setup() + + err := perm.SetUserRoles(tc.user, tc.roles...) + + if tc.err != "" { + urequire.True(t, err != nil, "expected an error") + uassert.Equal(t, tc.err, err.Error()) + return + } else { + urequire.NoError(t, err) + } + + roles := perm.GetUserRoles(tc.user) + uassert.Equal(t, len(tc.roles), len(roles)) + for i, r := range roles { + urequire.Equal(t, string(tc.roles[i]), string(r)) + } + }) + } +} + func TestDefaultPermissionsRemoveUser(t *testing.T) { cases := []struct { name string @@ -376,7 +474,7 @@ func TestDefaultPermissionsRemoveUser(t *testing.T) { user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), setup: func() *DefaultPermissions { return NewDefaultPermissions( - admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), ) }, diff --git a/examples/gno.land/r/demo/boards2/permission_options.gno b/examples/gno.land/r/demo/boards2/permission_options.gno index fdaef050206..08cacf02582 100644 --- a/examples/gno.land/r/demo/boards2/permission_options.gno +++ b/examples/gno.land/r/demo/boards2/permission_options.gno @@ -17,7 +17,10 @@ func WithSuperRole(r Role) DefaultPermissionsOption { // WithUser adds a user to default permissions with optional assigned roles. func WithUser(user std.Address, roles ...Role) DefaultPermissionsOption { return func(dp *DefaultPermissions) { - // TODO: Should we enforce that users are members of the DAO? [dp.dao.IsMember(user)] + if !dp.dao.IsMember(user) { + dp.dao.AddMember(user) + } + dp.users.Set(user.String(), append([]Role(nil), roles...)) } } diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 15f53a823cb..15a72e5e943 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -87,7 +87,7 @@ func CreateThread(bid BoardID, title, body string) PostID { func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { assertIsUserCall() - // TODO: Assert that caller is a board member (when board type is invite only) + // TODO: Assert that caller is a board member when board type is invite only caller := std.GetOrigCaller() board := mustGetBoard(bid) thread := mustGetThread(board, threadID) @@ -176,8 +176,8 @@ func DeleteReply(bid BoardID, threadID, replyID PostID) { thread := mustGetThread(board, threadID) assertReplyExists(thread, replyID) - // TODO: Hide reply when the caller is the owner of the reply (remove WithPermission call for now) - // TODO: Support removing reply and children though proposals? (WithPermission) + // TODO: Hide reply when the caller is the owner of the reply without permission + // TODO: Support removing reply and children though proposals? caller := std.GetOrigCaller() args := Args{bid, threadID, replyID} @@ -194,6 +194,8 @@ func EditThread(bid BoardID, threadID PostID, title, body string) { board := mustGetBoard(bid) assertThreadExists(board, threadID) + // TODO: Thread owners should be able to edit without permission? + caller := std.GetOrigCaller() args := Args{bid, threadID, title, body} gPerm.WithPermission(caller, PermissionThreadEdit, args, func(Args) { @@ -218,29 +220,49 @@ func EditReply(bid BoardID, threadID, replyID PostID, title, body string) { reply.Update(title, body) } -func InviteMember(user std.Address, role Role) { +func InviteMember(bid BoardID, user std.Address, role Role) { assertIsUserCall() + perms := mustGetPermissions(bid) caller := std.GetOrigCaller() args := Args{user, role} - gPerm.WithPermission(caller, PermissionMemberInvite, args, func(Args) { - if err := gPerm.AddUser(user, role); err != nil { + perms.WithPermission(caller, PermissionMemberInvite, args, func(Args) { + if err := perms.AddUser(user, role); err != nil { panic(err) } }) } -func RemoveMember(user std.Address) { +func RemoveMember(bid BoardID, user std.Address) { assertIsUserCall() + perms := mustGetPermissions(bid) caller := std.GetOrigCaller() - gPerm.WithPermission(caller, PermissionMemberRemove, Args{user}, func(Args) { - if !gPerm.RemoveUser(user) { + perms.WithPermission(caller, PermissionMemberRemove, Args{user}, func(Args) { + if !perms.RemoveUser(user) { panic("member not found") } }) } +func HasMemberRole(bid BoardID, member std.Address, role Role) bool { + perms := mustGetPermissions(bid) + return perms.HasRole(member, role) +} + +func ChangeMemberRole(bid BoardID, member std.Address, role Role) { + assertIsUserCall() + + perms := mustGetPermissions(bid) + caller := std.GetOrigCaller() + args := Args{bid, member, role} + perms.WithPermission(caller, PermissionRoleChange, args, func(Args) { + if err := perms.SetUserRoles(member, role); err != nil { + panic(err) + } + }) +} + func assertIsUserCall() { if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { panic("invalid non-user call") @@ -300,3 +322,11 @@ func assertReplyVisible(thread *Post) { panic("reply with ID: " + thread.GetPostID().String() + " was hidden") } } + +func mustGetPermissions(bid BoardID) Permissions { + if bid != 0 { + board := mustGetBoard(bid) + return board.perms + } + return gPerm +} diff --git a/examples/gno.land/r/demo/boards2/z_1_a_filetest.gno b/examples/gno.land/r/demo/boards2/z_1_a_filetest.gno index 6497b4203e1..3218334419d 100644 --- a/examples/gno.land/r/demo/boards2/z_1_a_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_1_a_filetest.gno @@ -9,6 +9,7 @@ import ( const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards ) func init() { @@ -16,7 +17,7 @@ func init() { } func main() { - boards2.InviteMember(admin, boards2.RoleAdmin) + boards2.InviteMember(bid, admin, boards2.RoleAdmin) println("ok") } diff --git a/examples/gno.land/r/demo/boards2/z_1_b_filetest.gno b/examples/gno.land/r/demo/boards2/z_1_b_filetest.gno index eac31e48707..01bdac4cf75 100644 --- a/examples/gno.land/r/demo/boards2/z_1_b_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_1_b_filetest.gno @@ -10,19 +10,20 @@ const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 user = std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards ) func init() { // Add an admin user std.TestSetOrigCaller(owner) - boards2.InviteMember(admin, boards2.RoleAdmin) + boards2.InviteMember(bid, admin, boards2.RoleAdmin) // Next call will be done by the admin user std.TestSetOrigCaller(admin) } func main() { - boards2.InviteMember(user, boards2.RoleOwner) + boards2.InviteMember(bid, user, boards2.RoleOwner) } // Error: diff --git a/examples/gno.land/r/demo/boards2/z_1_c_filetest.gno b/examples/gno.land/r/demo/boards2/z_1_c_filetest.gno index 01ca55cb2f2..3151bf519de 100644 --- a/examples/gno.land/r/demo/boards2/z_1_c_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_1_c_filetest.gno @@ -10,19 +10,20 @@ const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 user = std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards ) func init() { // Add an admin user std.TestSetOrigCaller(owner) - boards2.InviteMember(admin, boards2.RoleAdmin) + boards2.InviteMember(bid, admin, boards2.RoleAdmin) // Next call will be done by the admin user std.TestSetOrigCaller(admin) } func main() { - boards2.InviteMember(user, boards2.RoleAdmin) + boards2.InviteMember(bid, user, boards2.RoleAdmin) println("ok") } diff --git a/examples/gno.land/r/demo/boards2/z_3_f_filetest.gno b/examples/gno.land/r/demo/boards2/z_3_f_filetest.gno index bd1a3dafe08..23f9bc7d3a6 100644 --- a/examples/gno.land/r/demo/boards2/z_3_f_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_3_f_filetest.gno @@ -12,6 +12,7 @@ import ( const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards name = "foo" newName = "barbaz" ) @@ -21,7 +22,7 @@ func init() { // Test1 is the boards owner and its address has a user already registered // so a new member must register a user with the new board name. - boards2.InviteMember(member, boards2.RoleOwner) + boards2.InviteMember(bid, member, boards2.RoleOwner) std.TestSetOrigCaller(member) users.Register("", newName, "") diff --git a/examples/gno.land/r/demo/boards2/z_3_g_filetest.gno b/examples/gno.land/r/demo/boards2/z_3_g_filetest.gno index 30617a49e52..f953f14a1e4 100644 --- a/examples/gno.land/r/demo/boards2/z_3_g_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_3_g_filetest.gno @@ -13,6 +13,7 @@ const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 member2 = std.Address("g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5") + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards name = "foo" newName = "barbaz" ) @@ -22,12 +23,12 @@ func init() { // Test1 is the boards owner and its address has a user already registered // so a new member must register a user with the new board name. - boards2.InviteMember(member, boards2.RoleOwner) + boards2.InviteMember(bid, member, boards2.RoleOwner) std.TestSetOrigCaller(member) users.Register("", newName, "") // Invite a new member that doesn't own the user that matches the new board name - boards2.InviteMember(member2, boards2.RoleOwner) + boards2.InviteMember(bid, member2, boards2.RoleOwner) std.TestSetOrigCaller(member2) boards2.CreateBoard(name) diff --git a/examples/gno.land/r/demo/boards2/z_4_a_filetest.gno b/examples/gno.land/r/demo/boards2/z_4_a_filetest.gno new file mode 100644 index 00000000000..e0a1a65a92e --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_4_a_filetest.gno @@ -0,0 +1,36 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + initialRole = boards2.RoleGuest + newRole = boards2.RoleAdmin + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.InviteMember(bid, member, boards2.RoleGuest) +} + +func main() { + if boards2.HasMemberRole(bid, member, initialRole) { + println("ok") + } + + boards2.ChangeMemberRole(bid, member, newRole) + + if boards2.HasMemberRole(bid, member, newRole) { + println("ok") + } +} + +// Output: +// ok +// ok diff --git a/examples/gno.land/r/demo/boards2/z_4_b_filetest.gno b/examples/gno.land/r/demo/boards2/z_4_b_filetest.gno new file mode 100644 index 00000000000..dd638576459 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_4_b_filetest.gno @@ -0,0 +1,38 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + initialRole = boards2.RoleGuest + newRole = boards2.RoleAdmin +) + +var bid boards2.BoardID + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("foo") // Operate on board DAO members + boards2.InviteMember(bid, member, boards2.RoleGuest) +} + +func main() { + if boards2.HasMemberRole(bid, member, initialRole) { + println("ok") + } + + boards2.ChangeMemberRole(bid, member, newRole) + + if boards2.HasMemberRole(bid, member, newRole) { + println("ok") + } +} + +// Output: +// ok +// ok diff --git a/examples/gno.land/r/demo/boards2/z_4_c_filetest.gno b/examples/gno.land/r/demo/boards2/z_4_c_filetest.gno new file mode 100644 index 00000000000..514233d8e06 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_4_c_filetest.gno @@ -0,0 +1,29 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + owner2 = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + admin = std.Address("g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5") + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.InviteMember(bid, owner2, boards2.RoleOwner) + boards2.InviteMember(bid, admin, boards2.RoleAdmin) + + std.TestSetOrigCaller(admin) +} + +func main() { + boards2.ChangeMemberRole(bid, owner2, boards2.RoleAdmin) +} + +// Error: +// admins are not allowed to remove the Owner role diff --git a/examples/gno.land/r/demo/boards2/z_4_d_filetest.gno b/examples/gno.land/r/demo/boards2/z_4_d_filetest.gno new file mode 100644 index 00000000000..d589fe9e694 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_4_d_filetest.gno @@ -0,0 +1,29 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + admin2 = std.Address("g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5") + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.InviteMember(bid, admin, boards2.RoleAdmin) + boards2.InviteMember(bid, admin2, boards2.RoleAdmin) + + std.TestSetOrigCaller(admin) +} + +func main() { + boards2.ChangeMemberRole(bid, admin2, boards2.RoleOwner) +} + +// Error: +// admins are not allowed to promote members to Owner diff --git a/examples/gno.land/r/demo/boards2/z_4_e_filetest.gno b/examples/gno.land/r/demo/boards2/z_4_e_filetest.gno new file mode 100644 index 00000000000..449cc6c06a1 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_4_e_filetest.gno @@ -0,0 +1,26 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.InviteMember(bid, admin, boards2.RoleAdmin) +} + +func main() { + boards2.ChangeMemberRole(bid, admin, boards2.RoleOwner) // Owner can promote other members to Owner + println("ok") +} + +// Output: +// ok diff --git a/examples/gno.land/r/demo/boards2/z_4_f_filetest.gno b/examples/gno.land/r/demo/boards2/z_4_f_filetest.gno new file mode 100644 index 00000000000..c6622b88c8a --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_4_f_filetest.gno @@ -0,0 +1,25 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.InviteMember(bid, admin, boards2.RoleGuest) +} + +func main() { + boards2.ChangeMemberRole(bid, admin, boards2.Role("foo")) +} + +// Error: +// invalid role: foo diff --git a/examples/gno.land/r/demo/boards2/z_4_g_filetest.gno b/examples/gno.land/r/demo/boards2/z_4_g_filetest.gno new file mode 100644 index 00000000000..24fec3fb9e3 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/z_4_g_filetest.gno @@ -0,0 +1,25 @@ +package main + +import ( + "std" + + "gno.land/r/demo/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.InviteMember(bid, admin, boards2.RoleGuest) +} + +func main() { + boards2.ChangeMemberRole(bid, "invalid address", boards2.RoleModerator) +} + +// Error: +// user not found From e60574fcf735ee47b9c25b97e0c981aa88e2fa5f Mon Sep 17 00:00:00 2001 From: Denys Sedchenko <9203548+x1unix@users.noreply.github.com> Date: Fri, 17 Jan 2025 03:48:52 -0500 Subject: [PATCH 17/52] feat(boardsv2): hide flagged comments (#3536) ## Description This PR adjusts display of flagged comments with flow defined in #3480. Now, flagged comments will be hidden but children replies still be visible. ### Demo ![image](https://github.com/user-attachments/assets/78dae965-bed7-4fd8-a52e-404afa8bf20a) > [!IMPORTANT] > The "comment is hidden" message is actually italic, but [italic font styles are broken](https://github.com/gnolang/gno/issues/3535) in Gnoweb ## Other Changes In addition to that: * Fixed Reply URL arguments. * Added Flag thread/comment URL. * Started adopting `strings.Builder` instead of string concatenation. CC @jeronimoalbi Closes: #3480 --- examples/gno.land/r/demo/boards2/post.gno | 124 ++++++++++++------ .../gno.land/r/demo/boards2/post_test.gno | 4 +- examples/gno.land/r/demo/boards2/render.gno | 10 +- 3 files changed, 95 insertions(+), 43 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index efc0bc2f4ed..d121683c763 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -4,6 +4,7 @@ import ( "errors" "std" "strconv" + "strings" "time" "gno.land/p/demo/avl" @@ -213,7 +214,7 @@ func (post *Post) GetReplyFormURL() string { return txlink.Call("CreateReply", "bid", post.board.id.String(), "threadID", post.threadID.String(), - "postID", post.id.String(), + "replyID", post.id.String(), ) } @@ -238,6 +239,21 @@ func (post *Post) GetDeleteFormURL() string { ) } +func (post *Post) GetFlagFormURL() string { + if post.IsThread() { + return txlink.Call("FlagThread", + "bid", post.board.id.String(), + "postID", post.threadID.String(), + ) + } + + return txlink.Call("FlagReply", + "bid", post.board.id.String(), + "threadID", post.threadID.String(), + "replyID", post.id.String(), + ) +} + func (post *Post) RenderSummary() string { var ( s string @@ -286,69 +302,103 @@ func (post *Post) renderSourcePost(indent string) (string, *Post) { return indentBody(indent, srcPost.GetSummary()) + "\n\n", srcPost } -func (post *Post) Render(indent string, levels int) string { - if post == nil { - return "nil post" - } - - var ( - s string - postURL = post.GetURL() - ) - - if post.title != "" { - s += indent + "# " + post.title + "\n" - s += indent + "\n" +// renderPostContent renders post text content (including repost body). +// Function will dump a predefined message instead of a body if post is hidden. +func (post *Post) renderPostContent(sb *strings.Builder, indent string) { + if post.isHidden { + // Flagged comment should be hidden, but replies still visible (see: #3480) + // Flagged threads will be hidden by render function caller. + sb.WriteString(indentBody(indent, "_Reply is hidden as it has been flagged as inappropriate_")) + sb.WriteString("\n") + return } srcContent, srcPost := post.renderSourcePost(indent) - - s += srcContent - s += indentBody(indent, post.body) + "\n" // TODO: indent body lines. + sb.WriteString(srcContent) + sb.WriteString(indentBody(indent, post.body)) + sb.WriteString("\n") if post.IsThread() { // Split content and controls for threads. - s += "\n" + sb.WriteString("\n") } - s += indent + "\\- " + newUserLink(post.creator) + ", " - s += newLink(post.createdAt.Format(dateFormat), postURL) + postURL := post.GetURL() + + // Buttons & counters + sb.WriteString(indent) + sb.WriteString(`\- `) + sb.WriteString(newUserLink(post.creator)) + sb.WriteString(", ") + sb.WriteString(newLink(post.createdAt.Format(dateFormat), postURL)) if post.repostsCount > 0 { - s += ", " + strconv.FormatUint(post.repostsCount, 10) + " reposts" + sb.WriteString(", ") + sb.WriteString(strconv.FormatUint(post.repostsCount, 10)) + sb.WriteString(" reposts") } if srcPost != nil { - s += " " + newButtonLink("see source post", srcPost.GetURL()) + sb.WriteString(" ") + sb.WriteString(newButtonLink("see source post", srcPost.GetURL())) } - s += " " + newButtonLink("reply", post.GetReplyFormURL()) + sb.WriteString(" ") + sb.WriteString(newButtonLink("reply", post.GetReplyFormURL())) if post.IsThread() { - s += " " + newButtonLink("repost", post.GetRepostFormURL()) + sb.WriteString(" ") + sb.WriteString(newButtonLink("repost", post.GetRepostFormURL())) } - s += " " + newButtonLink("x", post.GetDeleteFormURL()) + "\n" + sb.WriteString(" ") + sb.WriteString(newButtonLink("flag", post.GetFlagFormURL())) + + sb.WriteString(" ") + sb.WriteString(newButtonLink("x", post.GetDeleteFormURL())) + sb.WriteString("\n") +} + +func (post *Post) Render(indent string, levels int) string { + if post == nil { + return "nil post" + } + + // TODO: pass a builder as arg into Render. + sb := &strings.Builder{} + + if post.title != "" { + sb.WriteString(indent) + sb.WriteString("# ") + sb.WriteString(post.title) + sb.WriteString("\n") + sb.WriteString(indent) + sb.WriteString("\n") + } + + post.renderPostContent(sb, indent) + + if post.replies.Size() > 0 { + if levels > 0 { + commentsIndent := indent + "> " - if levels > 0 { - if post.replies.Size() > 0 { post.replies.Iterate("", "", func(_ string, value interface{}) bool { reply := value.(*Post) - if reply.isHidden { - // TODO: change this in case of pagination - return false - } - s += indent + "\n" - s += reply.Render(indent+"> ", levels-1) + sb.WriteString(indent + "\n") + sb.WriteString(reply.Render(commentsIndent, levels-1)) return false }) + } else { + sb.WriteString(indent + "\n") + sb.WriteString(indent) + sb.WriteString("_") + sb.WriteString(newLink("see all "+strconv.Itoa(post.replies.Size())+" replies", post.GetURL())) + sb.WriteString("_\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 + + return sb.String() } func (post *Post) RenderInner() string { diff --git a/examples/gno.land/r/demo/boards2/post_test.gno b/examples/gno.land/r/demo/boards2/post_test.gno index c1307ebf8b6..75e52f43c83 100644 --- a/examples/gno.land/r/demo/boards2/post_test.gno +++ b/examples/gno.land/r/demo/boards2/post_test.gno @@ -116,7 +116,7 @@ func TestNewThread(t *testing.T) { uint(threadID), ) replyURL := ufmt.Sprintf( - "/r/demo/boards2$help&func=CreateReply&bid=%d&threadID=%d&postID=%d", + "/r/demo/boards2$help&func=CreateReply&bid=%d&threadID=%d&replyID=%d", uint(boardID), uint(threadID), uint(threadID), @@ -279,7 +279,7 @@ func TestNewReply(t *testing.T) { uint(replyID), ) replyURL := ufmt.Sprintf( - "/r/demo/boards2$help&func=CreateReply&bid=%d&threadID=%d&postID=%d", + "/r/demo/boards2$help&func=CreateReply&bid=%d&threadID=%d&replyID=%d", uint(boardID), uint(threadID), uint(replyID), diff --git a/examples/gno.land/r/demo/boards2/render.gno b/examples/gno.land/r/demo/boards2/render.gno index ea575bc4fb1..3bde3ec4ee5 100644 --- a/examples/gno.land/r/demo/boards2/render.gno +++ b/examples/gno.land/r/demo/boards2/render.gno @@ -99,9 +99,11 @@ func renderReply(res *mux.ResponseWriter, req *mux.Request) { reply, found := thread.GetReply(PostID(rID)) if !found { res.Write("Reply does not exist with ID: " + rawID) - } else if reply.IsHidden() { - res.Write("Reply with ID: " + rawID + " was hidden") - } else { - res.Write(reply.RenderInner()) + return } + + // Call render even for hidden replies to display children. + // Original comment content will be hidden under the hood. + // See: #3480 + res.Write(reply.RenderInner()) } From 1e24a33a5e44122e0f000b45acc9108283f567eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Wed, 22 Jan 2025 09:45:07 +0100 Subject: [PATCH 18/52] feat(boards2): initial commondao implementation (#3558) Resolves #3345 --- .../p/demo/boards2/admindao/admindao.gno | 65 --- .../p/demo/boards2/admindao/admindao_test.gno | 112 ----- .../gno.land/p/demo/boards2/admindao/gno.mod | 1 - .../gno.land/p/demo/commondao/commondao.gno | 242 ++++++++++ .../p/demo/commondao/commondao_test.gno | 445 ++++++++++++++++++ examples/gno.land/p/demo/commondao/gno.mod | 1 + .../admindao => commondao}/options.gno | 12 +- .../gno.land/p/demo/commondao/proposal.gno | 125 +++++ examples/gno.land/p/demo/commondao/record.gno | 83 ++++ .../gno.land/p/demo/commondao/record_test.gno | 168 +++++++ examples/gno.land/r/demo/boards2/board.gno | 5 +- .../gno.land/r/demo/boards2/permission.gno | 4 +- .../r/demo/boards2/permission_default.gno | 18 +- .../demo/boards2/permission_default_test.gno | 64 +-- .../r/demo/boards2/z_0_a_filetest.gno | 2 +- .../r/demo/boards2/z_0_c_filetest.gno | 2 +- .../r/demo/boards2/z_0_d_filetest.gno | 2 +- .../r/demo/boards2/z_2_a_filetest.gno | 2 +- .../r/demo/boards2/z_2_b_filetest.gno | 2 +- .../r/demo/boards2/z_2_c_filetest.gno | 2 +- .../r/demo/boards2/z_2_d_filetest.gno | 2 +- .../r/demo/boards2/z_3_a_filetest.gno | 4 +- .../r/demo/boards2/z_3_b_filetest.gno | 2 +- .../r/demo/boards2/z_3_c_filetest.gno | 2 +- .../r/demo/boards2/z_3_e_filetest.gno | 2 +- .../r/demo/boards2/z_3_f_filetest.gno | 2 +- .../r/demo/boards2/z_3_g_filetest.gno | 2 +- .../r/demo/boards2/z_4_b_filetest.gno | 2 +- 28 files changed, 1134 insertions(+), 241 deletions(-) delete mode 100644 examples/gno.land/p/demo/boards2/admindao/admindao.gno delete mode 100644 examples/gno.land/p/demo/boards2/admindao/admindao_test.gno delete mode 100644 examples/gno.land/p/demo/boards2/admindao/gno.mod create mode 100644 examples/gno.land/p/demo/commondao/commondao.gno create mode 100644 examples/gno.land/p/demo/commondao/commondao_test.gno create mode 100644 examples/gno.land/p/demo/commondao/gno.mod rename examples/gno.land/p/demo/{boards2/admindao => commondao}/options.gno (53%) create mode 100644 examples/gno.land/p/demo/commondao/proposal.gno create mode 100644 examples/gno.land/p/demo/commondao/record.gno create mode 100644 examples/gno.land/p/demo/commondao/record_test.gno diff --git a/examples/gno.land/p/demo/boards2/admindao/admindao.gno b/examples/gno.land/p/demo/boards2/admindao/admindao.gno deleted file mode 100644 index 404bfe78db7..00000000000 --- a/examples/gno.land/p/demo/boards2/admindao/admindao.gno +++ /dev/null @@ -1,65 +0,0 @@ -package admindao - -import ( - "errors" - "std" - - "gno.land/p/demo/avl" -) - -// TODO: Add support for proposals -// TODO: Add support for events - -// ErrMemberExists indicates that a member is already part of the DAO. -var ErrMemberExists = errors.New("member already exist") - -// AdminDAO defines a Boards administration DAO. -type AdminDAO struct { - parent *AdminDAO - members *avl.Tree // string(std.Address) -> struct{} -} - -// New creates a new admin DAO. -func New(options ...Option) *AdminDAO { - dao := &AdminDAO{members: avl.NewTree()} - for _, apply := range options { - apply(dao) - } - return dao -} - -// Parent returns the parent DAO. -// Null can be returned when DAO has no parent assigned. -func (dao AdminDAO) Parent() *AdminDAO { - return dao.parent -} - -// Members returns the list of DAO members. -func (dao AdminDAO) Members() []std.Address { - var members []std.Address - dao.members.Iterate("", "", func(key string, _ interface{}) bool { - members = append(members, std.Address(key)) - return false - }) - return members -} - -// AddMember adds a new member to the DAO. -func (dao *AdminDAO) AddMember(user std.Address) error { - if dao.IsMember(user) { - return ErrMemberExists - } - dao.members.Set(user.String(), struct{}{}) - return nil -} - -// RemoveMember removes a member from the DAO. -func (dao *AdminDAO) RemoveMember(user std.Address) (removed bool) { - _, removed = dao.members.Remove(user.String()) - return removed -} - -// IsMember checks if a user is a member of the DAO. -func (dao AdminDAO) IsMember(user std.Address) bool { - return dao.members.Has(user.String()) -} diff --git a/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno b/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno deleted file mode 100644 index 999ecdfcd0a..00000000000 --- a/examples/gno.land/p/demo/boards2/admindao/admindao_test.gno +++ /dev/null @@ -1,112 +0,0 @@ -package admindao - -import ( - "std" - "testing" - - "gno.land/p/demo/uassert" - "gno.land/p/demo/urequire" -) - -func TestNew(t *testing.T) { - cases := []struct { - name string - parent *AdminDAO - members []std.Address - }{ - { - name: "with parent", - parent: New(), - members: []std.Address{"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"}, - }, - { - name: "without parent", - members: []std.Address{"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"}, - }, - { - name: "multiple members", - members: []std.Address{ - "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", - "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc", - }, - }, - { - name: "no members", - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - options := []Option{WithParent(tc.parent)} - for _, m := range tc.members { - options = append(options, WithMember(m)) - } - - dao := New(options...) - - if tc.parent == nil { - uassert.Equal(t, nil, dao.Parent()) - } else { - uassert.NotEqual(t, nil, dao.Parent()) - } - - urequire.Equal(t, len(tc.members), len(dao.Members()), "dao members") - for i, m := range dao.Members() { - uassert.Equal(t, tc.members[i], m) - } - }) - } -} - -func TestAdminDAOAddMember(t *testing.T) { - member := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - dao := New(WithMember("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn")) - - err := dao.AddMember(member) - urequire.NoError(t, err) - uassert.Equal(t, 2, len(dao.Members())) - uassert.True(t, dao.IsMember(member)) - - err = dao.AddMember(member) - uassert.ErrorIs(t, err, ErrMemberExists) -} - -func TestAdminDAORemoveMember(t *testing.T) { - member := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - dao := New(WithMember(member)) - - removed := dao.RemoveMember(member) - urequire.True(t, removed) - - removed = dao.RemoveMember(member) - urequire.False(t, removed) -} - -func TestAdminDAOIsMember(t *testing.T) { - cases := []struct { - name string - member std.Address - dao *AdminDAO - want bool - }{ - { - name: "member", - member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", - dao: New(WithMember("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn")), - want: true, - }, - { - name: "not a dao member", - member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", - dao: New(WithMember("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc")), - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - got := tc.dao.IsMember(tc.member) - uassert.Equal(t, got, tc.want) - }) - } -} diff --git a/examples/gno.land/p/demo/boards2/admindao/gno.mod b/examples/gno.land/p/demo/boards2/admindao/gno.mod deleted file mode 100644 index 5d54360ae7a..00000000000 --- a/examples/gno.land/p/demo/boards2/admindao/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/p/demo/boards2/admindao diff --git a/examples/gno.land/p/demo/commondao/commondao.gno b/examples/gno.land/p/demo/commondao/commondao.gno new file mode 100644 index 00000000000..53414624cf0 --- /dev/null +++ b/examples/gno.land/p/demo/commondao/commondao.gno @@ -0,0 +1,242 @@ +package commondao + +import ( + "errors" + "std" + "time" + + "gno.land/p/demo/avl" + "gno.land/p/demo/avl/rotree" + "gno.land/p/demo/seqid" +) + +// DefaultQuorum defines the default quorum required to tally proposal votes. +const DefaultQuorum = 0.34 // 34% + +var ( + ErrInvalidAddress = errors.New("invalid address") + ErrInvalidVoteChoice = errors.New("invalid vote choice") + ErrMemberExists = errors.New("member already exist") + ErrNotMember = errors.New("account is not a member of the DAO") + ErrOverflow = errors.New("next ID overflows uint64") + ErrProposalDefinitionRequired = errors.New("proposal definition is required") + ErrProposalNotFound = errors.New("proposal not found") + ErrStatusIsNotActive = errors.New("proposal status is not active") + ErrVotingDeadlineNotMet = errors.New("voting deadline not met") +) + +type ( + // CommonDAO defines a DAO. + CommonDAO struct { + parent *CommonDAO + members *avl.Tree // string(std.Address) -> struct{} + genID seqid.ID + active *avl.Tree // string(proposal ID) -> *Proposal + finished *avl.Tree // string(proposal ID) -> *Proposal + } + + // Stats contains proposal voting stats. + Stats struct { + YayVotes int + NayVotes int + Abstained int + } +) + +// New creates a new common DAO. +func New(options ...Option) *CommonDAO { + dao := &CommonDAO{ + members: avl.NewTree(), + active: avl.NewTree(), + finished: avl.NewTree(), + } + for _, apply := range options { + apply(dao) + } + return dao +} + +// Parent returns the parent DAO. +// Null can be returned when DAO has no parent assigned. +func (dao CommonDAO) Parent() *CommonDAO { + return dao.parent +} + +// Members returns the list of DAO members. +func (dao CommonDAO) Members() []std.Address { + var members []std.Address + dao.members.Iterate("", "", func(key string, _ interface{}) bool { + members = append(members, std.Address(key)) + return false + }) + return members +} + +// AddMember adds a new member to the DAO. +func (dao *CommonDAO) AddMember(user std.Address) error { + if dao.IsMember(user) { + return ErrMemberExists + } + dao.members.Set(user.String(), struct{}{}) + return nil +} + +// RemoveMember removes a member from the DAO. +func (dao *CommonDAO) RemoveMember(user std.Address) (removed bool) { + _, removed = dao.members.Remove(user.String()) + return removed +} + +// IsMember checks if a user is a member of the DAO. +func (dao CommonDAO) IsMember(user std.Address) bool { + return dao.members.Has(user.String()) +} + +// ActiveProposals returns all active DAO proposals. +func (dao CommonDAO) ActiveProposals() rotree.IReadOnlyTree { + return dao.active +} + +// FinishedProposalsi returns all finished DAO proposals. +func (dao CommonDAO) FinishedProposals() rotree.IReadOnlyTree { + return dao.finished +} + +// Propose creates a new DAO proposal. +func (dao *CommonDAO) Propose(creator std.Address, d ProposalDefinition) (*Proposal, error) { + if d == nil { + return nil, ErrProposalDefinitionRequired + } + + if !creator.IsValid() { + return nil, ErrInvalidAddress + } + + id, ok := dao.genID.TryNext() + if !ok { + return nil, ErrOverflow + } + + p := NewProposal(uint64(id), creator, d) + key := makeProposalKey(p.ID()) + dao.active.Set(key, p) + return p, nil +} + +// GetActiveProposal returns an active proposal. +func (dao CommonDAO) GetActiveProposal(proposalID uint64) (_ *Proposal, found bool) { + key := makeProposalKey(proposalID) + if v, ok := dao.active.Get(key); ok { + return v.(*Proposal), true + } + return nil, false +} + +// GetFinishedProposal returns a finished proposal. +func (dao CommonDAO) GetFinishedProposal(proposalID uint64) (_ *Proposal, found bool) { + key := makeProposalKey(proposalID) + if v, ok := dao.finished.Get(key); ok { + return v.(*Proposal), true + } + return nil, false +} + +// Vote submits a new vote for a proposal. +func (dao *CommonDAO) Vote(member std.Address, proposalID uint64, c VoteChoice) error { + if c != ChoiceYes && c != ChoiceNo && c != ChoiceAbstain { + return ErrInvalidVoteChoice + } + + if !dao.IsMember(member) { + return ErrNotMember + } + + p, found := dao.GetActiveProposal(proposalID) + if !found { + return ErrProposalNotFound + } + return p.record.AddVote(member, c) +} + +func (dao *CommonDAO) Tally(p *Proposal) Stats { + // Initialize stats considering only yes/no votes + record := p.VotingRecord() + votesCount := record.VoteCount(ChoiceYes) + record.VoteCount(ChoiceNo) + membersCount := len(dao.Members()) + stats := Stats{ + YayVotes: record.VoteCount(ChoiceYes), + NayVotes: record.VoteCount(ChoiceNo), + Abstained: membersCount - votesCount, + } + + // Check quorum before tallying + quorum := p.Definition().Quorum() + if quorum <= 0 || quorum > 1 { + quorum = DefaultQuorum + } + + percentage := float64(votesCount) / float64(membersCount) + if percentage < quorum { + p.status = StatusFailed + p.statusReason = "low participation" + return stats + } + + // Tally by majority requiring more than 50% of yes/no votes to win + // TODO: Confirm this way of tallying is enough for now. Keeping it minimal. + choice := record.GetProvableMajorityChoice() + if record.VoteCount(choice) <= int(votesCount/2) { + p.status = StatusFailed + p.statusReason = "majority not met" + } + return stats +} + +// Execute executes a proposal. +func (dao *CommonDAO) Execute(proposalID uint64) error { + p, found := dao.GetActiveProposal(proposalID) + if !found { + return ErrProposalNotFound + } + + if p.Status() != StatusActive { + return ErrStatusIsNotActive + } + + if time.Now().Before(p.VotingDeadline()) { + return ErrVotingDeadlineNotMet + } + + // Validate proposal before executing it + def := p.Definition() + err := def.Validate() + if err != nil { + p.status = StatusFailed + p.statusReason = err.Error() + } else { + // Tally votes and update proposal status + dao.Tally(p) + + // Execute proposal only when the majority vote wins + if p.Status() != StatusFailed { + err = def.Execute() + if err != nil { + p.status = StatusFailed + p.statusReason = err.Error() + } else { + p.status = StatusExecuted + } + } + } + + // Whichever the outcome of the validation, tallying + // and execution consider the proposal finished. + key := makeProposalKey(p.id) + dao.active.Remove(key) + dao.finished.Set(key, p) + return err +} + +func makeProposalKey(id uint64) string { + return seqid.ID(id).String() +} diff --git a/examples/gno.land/p/demo/commondao/commondao_test.gno b/examples/gno.land/p/demo/commondao/commondao_test.gno new file mode 100644 index 00000000000..04b2ed94fa3 --- /dev/null +++ b/examples/gno.land/p/demo/commondao/commondao_test.gno @@ -0,0 +1,445 @@ +package commondao + +import ( + "errors" + "std" + "testing" + "time" + + "gno.land/p/demo/seqid" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func TestNew(t *testing.T) { + cases := []struct { + name string + parent *CommonDAO + members []std.Address + }{ + { + name: "with parent", + parent: New(), + members: []std.Address{"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"}, + }, + { + name: "without parent", + members: []std.Address{"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"}, + }, + { + name: "multiple members", + members: []std.Address{ + "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", + "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc", + }, + }, + { + name: "no members", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + options := []Option{WithParent(tc.parent)} + for _, m := range tc.members { + options = append(options, WithMember(m)) + } + + dao := New(options...) + + if tc.parent == nil { + uassert.Equal(t, nil, dao.Parent()) + } else { + uassert.NotEqual(t, nil, dao.Parent()) + } + + urequire.Equal(t, len(tc.members), len(dao.Members()), "dao members") + for i, m := range dao.Members() { + uassert.Equal(t, tc.members[i], m) + } + }) + } +} + +func TestCommonDAOAddMember(t *testing.T) { + member := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + dao := New(WithMember("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn")) + + err := dao.AddMember(member) + urequire.NoError(t, err) + uassert.Equal(t, 2, len(dao.Members())) + uassert.True(t, dao.IsMember(member)) + + err = dao.AddMember(member) + uassert.ErrorIs(t, err, ErrMemberExists) +} + +func TestCommonDAORemoveMember(t *testing.T) { + member := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + dao := New(WithMember(member)) + + removed := dao.RemoveMember(member) + urequire.True(t, removed) + + removed = dao.RemoveMember(member) + urequire.False(t, removed) +} + +func TestCommonDAOIsMember(t *testing.T) { + cases := []struct { + name string + member std.Address + dao *CommonDAO + want bool + }{ + { + name: "member", + member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", + dao: New(WithMember("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn")), + want: true, + }, + { + name: "not a dao member", + member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", + dao: New(WithMember("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc")), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := tc.dao.IsMember(tc.member) + uassert.Equal(t, got, tc.want) + }) + } +} + +func TestCommonDAOPropose(t *testing.T) { + cases := []struct { + name string + setup func() *CommonDAO + creator std.Address + def ProposalDefinition + err error + }{ + { + name: "ok", + setup: func() *CommonDAO { return New() }, + creator: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", + def: &testPropDef{}, + }, + { + name: "nil definition", + setup: func() *CommonDAO { return New() }, + err: ErrProposalDefinitionRequired, + }, + { + name: "invalid address", + setup: func() *CommonDAO { return New() }, + def: &testPropDef{}, + err: ErrInvalidAddress, + }, + { + name: "proposal ID overflow", + setup: func() *CommonDAO { + dao := New() + dao.genID = seqid.ID(1<<64 - 1) + return dao + }, + creator: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", + def: &testPropDef{}, + err: ErrOverflow, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dao := tc.setup() + + p, err := dao.Propose(tc.creator, tc.def) + + if tc.err != nil { + urequire.ErrorIs(t, err, tc.err) + return + } + + urequire.NoError(t, err) + + _, found := dao.GetActiveProposal(p.ID()) + urequire.True(t, found, "proposal not found") + uassert.Equal(t, p.Creator(), tc.creator) + }) + } +} + +func TestCommonDAOVote(t *testing.T) { + cases := []struct { + name string + setup func() *CommonDAO + member std.Address + choice VoteChoice + proposalID uint64 + err error + }{ + { + name: "ok", + setup: func() *CommonDAO { + member := std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") + dao := New(WithMember(member)) + dao.Propose(member, &testPropDef{}) + return dao + }, + member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", + choice: ChoiceYes, + proposalID: 1, + }, + { + name: "invalid vote choice", + setup: func() *CommonDAO { return New() }, + choice: VoteChoice("invalid"), + err: ErrInvalidVoteChoice, + }, + { + name: "not a member", + setup: func() *CommonDAO { return New() }, + member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", + err: ErrNotMember, + }, + { + name: "proposal not found", + setup: func() *CommonDAO { + return New(WithMember("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn")) + }, + member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", + proposalID: 42, + err: ErrProposalNotFound, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dao := tc.setup() + + err := dao.Vote(tc.member, tc.proposalID, tc.choice) + + if tc.err != nil { + urequire.ErrorIs(t, err, tc.err) + return + } + + urequire.NoError(t, err) + + p, found := dao.GetActiveProposal(tc.proposalID) + urequire.True(t, found, "proposal not found") + + record := p.VotingRecord() + uassert.True(t, record.HasVoted(tc.member)) + uassert.Equal(t, record.VoteCount(tc.choice), 1) + }) + } +} + +func TestCommonDAOTally(t *testing.T) { + cases := []struct { + name string + dao *CommonDAO + votes []Vote + status ProposalStatus + statusReason string + stats Stats + }{ + { + name: "pass", + dao: New( + WithMember("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn"), + WithMember("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc"), + WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + ), + votes: []Vote{ + {Address: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", Choice: ChoiceYes}, + {Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", Choice: ChoiceYes}, + }, + status: StatusActive, + stats: Stats{YayVotes: 2, Abstained: 1}, + }, + { + name: "no votes", + dao: New(), + status: StatusFailed, + statusReason: "low participation", + }, + { + name: "no quorum", + dao: New( + WithMember("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn"), + WithMember("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc"), + WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + ), + votes: []Vote{ + {Address: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", Choice: ChoiceYes}, + }, + status: StatusFailed, + statusReason: "low participation", + stats: Stats{YayVotes: 1, Abstained: 2}, + }, + { + name: "majority not met", + dao: New( + WithMember("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn"), + WithMember("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc"), + WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + ), + votes: []Vote{ + {Address: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", Choice: ChoiceYes}, + {Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", Choice: ChoiceNo}, + }, + status: StatusFailed, + statusReason: "majority not met", + stats: Stats{YayVotes: 1, NayVotes: 1, Abstained: 1}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + p := NewProposal(1, "", testPropDef{}) + for _, v := range tc.votes { + p.record.AddVote(v.Address, v.Choice) + } + + stats := tc.dao.Tally(p) + + uassert.Equal(t, string(p.Status()), string(tc.status)) + uassert.Equal(t, p.StatusReason(), tc.statusReason) + uassert.Equal(t, stats.YayVotes, tc.stats.YayVotes) + uassert.Equal(t, stats.NayVotes, tc.stats.NayVotes) + uassert.Equal(t, stats.Abstained, tc.stats.Abstained) + }) + } +} + +func TestCommonDAOExecute(t *testing.T) { + errValidation := errors.New("validation error") + errExecution := errors.New("execution error") + cases := []struct { + name string + setup func() *CommonDAO + proposalID uint64 + status ProposalStatus + statusReason string + err error + }{ + { + name: "ok", + setup: func() *CommonDAO { + members := []std.Address{ + "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", + "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc", + "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + } + dao := New(WithMember(members[0]), WithMember(members[1]), WithMember(members[2])) + p, _ := dao.Propose(members[0], &testPropDef{}) + p.record.AddVote(members[0], ChoiceYes) + p.record.AddVote(members[1], ChoiceYes) + return dao + }, + status: StatusExecuted, + proposalID: 1, + }, + { + name: "proposal not found", + setup: func() *CommonDAO { return New() }, + proposalID: 1, + err: ErrProposalNotFound, + }, + { + name: "proposal not active", + setup: func() *CommonDAO { + member := std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") + dao := New(WithMember(member)) + p, _ := dao.Propose(member, &testPropDef{}) + p.status = StatusExecuted + return dao + }, + proposalID: 1, + err: ErrStatusIsNotActive, + }, + { + name: "voting deadline not met", + setup: func() *CommonDAO { + member := std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") + dao := New(WithMember(member)) + dao.Propose(member, &testPropDef{votingPeriod: time.Minute * 5}) + return dao + }, + proposalID: 1, + err: ErrVotingDeadlineNotMet, + }, + { + name: "validation error", + setup: func() *CommonDAO { + member := std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") + dao := New(WithMember(member)) + dao.Propose(member, &testPropDef{validationErr: errValidation}) + return dao + }, + proposalID: 1, + status: StatusFailed, + statusReason: errValidation.Error(), + err: errValidation, + }, + { + name: "execution error", + setup: func() *CommonDAO { + members := []std.Address{ + "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", + "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc", + "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + } + dao := New(WithMember(members[0]), WithMember(members[1]), WithMember(members[2])) + p, _ := dao.Propose(members[0], &testPropDef{executionErr: errExecution}) + p.record.AddVote(members[0], ChoiceYes) + p.record.AddVote(members[1], ChoiceYes) + return dao + }, + proposalID: 1, + status: StatusFailed, + statusReason: errExecution.Error(), + err: errExecution, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dao := tc.setup() + + err := dao.Execute(tc.proposalID) + + if tc.err != nil { + urequire.ErrorIs(t, err, tc.err) + return + } + + urequire.NoError(t, err) + + _, found := dao.GetActiveProposal(tc.proposalID) + urequire.False(t, found, "proposal should not be active") + + p, found := dao.GetFinishedProposal(tc.proposalID) + urequire.True(t, found, "proposal not found") + uassert.Equal(t, string(p.Status()), string(tc.status)) + uassert.Equal(t, string(p.StatusReason()), string(tc.statusReason)) + }) + } +} + +type testPropDef struct { + votingPeriod time.Duration + validationErr, executionErr error +} + +func (testPropDef) Title() string { return "" } +func (testPropDef) Body() string { return "" } +func (testPropDef) Quorum() float64 { return 0 } +func (d testPropDef) VotingPeriod() time.Duration { return d.votingPeriod } +func (d testPropDef) Validate() error { return d.validationErr } +func (d testPropDef) Execute() error { return d.executionErr } diff --git a/examples/gno.land/p/demo/commondao/gno.mod b/examples/gno.land/p/demo/commondao/gno.mod new file mode 100644 index 00000000000..94b73a1d8e8 --- /dev/null +++ b/examples/gno.land/p/demo/commondao/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/commondao diff --git a/examples/gno.land/p/demo/boards2/admindao/options.gno b/examples/gno.land/p/demo/commondao/options.gno similarity index 53% rename from examples/gno.land/p/demo/boards2/admindao/options.gno rename to examples/gno.land/p/demo/commondao/options.gno index 66ee99f1cba..87dff3c0f79 100644 --- a/examples/gno.land/p/demo/boards2/admindao/options.gno +++ b/examples/gno.land/p/demo/commondao/options.gno @@ -1,20 +1,20 @@ -package admindao +package commondao import "std" -// Option configures the AdminDAO. -type Option func(*AdminDAO) +// Option configures the CommonDAO. +type Option func(*CommonDAO) // WithParent assigns a parent DAO. -func WithParent(p *AdminDAO) Option { - return func(dao *AdminDAO) { +func WithParent(p *CommonDAO) Option { + return func(dao *CommonDAO) { dao.parent = p } } // WithMember assigns a member to the DAO. func WithMember(addr std.Address) Option { - return func(dao *AdminDAO) { + return func(dao *CommonDAO) { dao.members.Set(addr.String(), struct{}{}) } } diff --git a/examples/gno.land/p/demo/commondao/proposal.gno b/examples/gno.land/p/demo/commondao/proposal.gno new file mode 100644 index 00000000000..2300a0c635f --- /dev/null +++ b/examples/gno.land/p/demo/commondao/proposal.gno @@ -0,0 +1,125 @@ +package commondao + +import ( + "std" + "time" +) + +const ( + StatusActive ProposalStatus = "active" + StatusFailed = "failed" + StatusExecuted = "executed" +) + +const ( + ChoiceAbstain VoteChoice = "" + ChoiceYes = "yes" + ChoiceNo = "no" +) + +type ( + // ProposalStatus defines a type for different proposal states. + ProposalStatus string + + // VoteChoice defines a type for proposal vote choices. + VoteChoice string + + // Proposal defines a DAO proposal. + Proposal struct { + id uint64 + status ProposalStatus + definition ProposalDefinition + creator std.Address + record *VotingRecord + statusReason string + votingDeadline time.Time + createdAt time.Time + } + + // ProposalDefinition defines an interface for custom proposal definitions. + // These definitions define proposal content and behavior, they esentially + // allow the definition for different proposal types. + ProposalDefinition interface { + // Title returns the proposal title. + Title() string + + // Body returns the proposal body. + // It usually contains the proposal description and other elements like proposal parameters. + Body() string + + // VotingPeriod returns the period where votes are allowed after proposal creation. + // No more votes should be allowed once this period is met. It is used to calculate + // the voting deadline from the proposal's creationd date. + VotingPeriod() time.Duration + + // Quorum returns the percentage of members that must vote to be able to pass a proposal. + // This is an optional value. DAOs use a default value when quorum is zero or invalid. + // Its value must be between 0 and 1, being 1 = 100% of member votes. + Quorum() float64 + + // Validate validates that the proposal is valid. + // Validations are optional and allow the validation of the current state before proposal execution. + Validate() error + + // Execute executes the proposal. + // Once proposal are executed they are archived and considered finished. + // Execution allows changing the state after a proposal passes. + Execute() error + } +) + +// NewProposal creates a new DAO proposal. +func NewProposal(id uint64, creator std.Address, d ProposalDefinition) *Proposal { + now := time.Now() + return &Proposal{ + id: id, + status: StatusActive, + definition: d, + creator: creator, + record: &VotingRecord{}, + votingDeadline: now.Add(d.VotingPeriod()), + createdAt: now, + } +} + +// ID returns the unique proposal identifies. +func (p Proposal) ID() uint64 { + return p.id +} + +// Definition returns the proposal definition. +// Proposal definitions define proposal content and behavior. +func (p Proposal) Definition() ProposalDefinition { + return p.definition +} + +// Status returns the current proposal status. +func (p Proposal) Status() ProposalStatus { + return p.status +} + +// Creator returns the address of the account that created the proposal. +func (p Proposal) Creator() std.Address { + return p.creator +} + +// CreatedAt returns the time that proposal was created. +func (p Proposal) CreatedAt() time.Time { + return p.createdAt +} + +// VotingRecord returns a record that contains all the votes submitted for the proposal. +func (p Proposal) VotingRecord() *VotingRecord { + return p.record +} + +// StatusReason returns an optional reason that lead to the current proposal status. +// Reason is mostyl useful when a proposal fails. +func (p Proposal) StatusReason() string { + return p.statusReason +} + +// VotingDeadline returns the deadline after which no more votes should be allowed. +func (p Proposal) VotingDeadline() time.Time { + return p.votingDeadline +} diff --git a/examples/gno.land/p/demo/commondao/record.gno b/examples/gno.land/p/demo/commondao/record.gno new file mode 100644 index 00000000000..2080d7dabd3 --- /dev/null +++ b/examples/gno.land/p/demo/commondao/record.gno @@ -0,0 +1,83 @@ +package commondao + +import ( + "errors" + "std" + + "gno.land/p/demo/avl" +) + +// ErrVoteExists indicates that a user already voted. +var ErrVoteExists = errors.New("user already voted") + +type ( + // Vote defines a single vote. + Vote struct { + Address std.Address + Choice VoteChoice + } + + // VotingRecord stores accounts that voted and vote choices. + VotingRecord struct { + votes avl.Tree // string(address) -> VoteChoice + count avl.Tree // string(choice) -> int + } +) + +// Votes returns the list of all votes. +func (r VotingRecord) Votes() []Vote { + var votes []Vote + r.votes.Iterate("", "", func(k string, v interface{}) bool { + votes = append(votes, Vote{ + Address: std.Address(k), + Choice: v.(VoteChoice), + }) + return false + }) + return votes +} + +// VoteCount returns the number of votes for a single voting choice. +func (r VotingRecord) VoteCount(c VoteChoice) int { + if v, found := r.count.Get(string(c)); found { + return v.(int) + } + return 0 +} + +// HasVoted checks if an account already voted. +func (r VotingRecord) HasVoted(user std.Address) bool { + return r.votes.Has(user.String()) +} + +// AddVote adds a vote. +// Users are allowd to vote only once. +func (r *VotingRecord) AddVote(user std.Address, c VoteChoice) error { + if r.HasVoted(user) { + return ErrVoteExists + } + + r.votes.Set(user.String(), c) + r.count.Set(string(c), r.VoteCount(c)+1) + return nil +} + +// GetProvableMajorityChoice returns the choice voted by the majority. +// The result is only valid if there is a majority. +// Caller must validate that the returned choice represents a majority. +func (r VotingRecord) GetProvableMajorityChoice() VoteChoice { + var ( + choice VoteChoice + currentCount int + ) + + r.count.Iterate("", "", func(k string, v interface{}) bool { + count := v.(int) + if currentCount < count { + choice = VoteChoice(k) + currentCount = count + } + return false + }) + return choice +} diff --git a/examples/gno.land/p/demo/commondao/record_test.gno b/examples/gno.land/p/demo/commondao/record_test.gno new file mode 100644 index 00000000000..3c58922f085 --- /dev/null +++ b/examples/gno.land/p/demo/commondao/record_test.gno @@ -0,0 +1,168 @@ +package commondao + +import ( + "std" + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func TestVotingRecordDefaults(t *testing.T) { + var ( + record VotingRecord + user = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + ) + + uassert.Equal(t, record.Votes(), nil) + uassert.Equal(t, record.VoteCount(ChoiceYes), 0) + uassert.Equal(t, record.VoteCount(ChoiceNo), 0) + uassert.Equal(t, record.VoteCount(ChoiceAbstain), 0) + uassert.False(t, record.HasVoted(user)) +} + +func TestVotingRecordAddVote(t *testing.T) { + cases := []struct { + name string + setup func(*VotingRecord) + votes []Vote + yesCount, noCount, abstainCount int + err error + }{ + { + name: "single vote", + votes: []Vote{ + { + Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + Choice: ChoiceYes, + }, + }, + yesCount: 1, + }, + { + name: "multiple votes", + votes: []Vote{ + { + Address: "g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt", + Choice: ChoiceNo, + }, + { + Address: "g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk", + Choice: ChoiceYes, + }, + { + Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + Choice: ChoiceNo, + }, + { + Address: "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", + Choice: ChoiceAbstain, + }, + }, + yesCount: 1, + noCount: 2, + abstainCount: 1, + }, + { + name: "vote exists", + votes: []Vote{ + { + Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + Choice: ChoiceYes, + }, + }, + setup: func(r *VotingRecord) { + r.AddVote("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", ChoiceAbstain) + }, + err: ErrVoteExists, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var ( + err error + record VotingRecord + ) + + if tc.setup != nil { + tc.setup(&record) + } + + for _, v := range tc.votes { + err = record.AddVote(v.Address, v.Choice) + if err != nil { + break + } + } + + if tc.err != nil { + urequire.ErrorIs(t, err, tc.err) + return + } + + urequire.NoError(t, err) + urequire.Equal(t, len(record.Votes()), len(tc.votes), "unexpected number of votes") + for i, v := range record.Votes() { + uassert.Equal(t, v.Address, tc.votes[i].Address) + uassert.Equal(t, string(v.Choice), string(tc.votes[i].Choice)) + uassert.True(t, record.HasVoted(v.Address)) + } + + uassert.Equal(t, record.VoteCount(ChoiceYes), tc.yesCount) + uassert.Equal(t, record.VoteCount(ChoiceNo), tc.noCount) + uassert.Equal(t, record.VoteCount(ChoiceAbstain), tc.abstainCount) + }) + } +} + +func TestVotingRecordGetProvableMajorityChoice(t *testing.T) { + cases := []struct { + name string + setup func(*VotingRecord) + choice VoteChoice + }{ + { + name: "no votes", + choice: ChoiceAbstain, + }, + { + name: "one vote", + setup: func(r *VotingRecord) { + r.AddVote("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", ChoiceYes) + }, + choice: ChoiceYes, + }, + { + name: "majority", + setup: func(r *VotingRecord) { + r.AddVote("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", ChoiceYes) + r.AddVote("g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk", ChoiceYes) + r.AddVote("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", ChoiceNo) + }, + choice: ChoiceYes, + }, + { + name: "invalid because no majority", + setup: func(r *VotingRecord) { + r.AddVote("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", ChoiceYes) + r.AddVote("g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk", ChoiceNo) + }, + choice: ChoiceNo, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var record VotingRecord + + if tc.setup != nil { + tc.setup(&record) + } + + choice := record.GetProvableMajorityChoice() + + uassert.Equal(t, string(choice), string(tc.choice)) + }) + } +} diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index 75469943398..dfca8f2d761 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -7,9 +7,8 @@ import ( "time" "gno.land/p/demo/avl" + "gno.land/p/demo/commondao" "gno.land/p/moul/txlink" - - "gno.land/p/demo/boards2/admindao" ) type BoardID uint64 @@ -133,7 +132,7 @@ func (board *Board) GetPostFormURL() string { func createDefaultBoardPermissions(owner std.Address) *DefaultPermissions { return NewDefaultPermissions( - admindao.New(), + commondao.New(), WithSuperRole(RoleOwner), WithRole( RoleAdmin, diff --git a/examples/gno.land/r/demo/boards2/permission.gno b/examples/gno.land/r/demo/boards2/permission.gno index aaae6d463b6..13fc14276e7 100644 --- a/examples/gno.land/r/demo/boards2/permission.gno +++ b/examples/gno.land/r/demo/boards2/permission.gno @@ -3,7 +3,7 @@ package boards2 import ( "std" - "gno.land/p/demo/boards2/admindao" + "gno.land/p/demo/commondao" ) const ( @@ -64,6 +64,6 @@ type ( // GetDAO returns the underlying DAO. // Returned value can be nil if the implementation doesn't have a DAO. - GetDAO() *admindao.AdminDAO // TODO: should return an interface + GetDAO() *commondao.CommonDAO // TODO: should return an interface } ) diff --git a/examples/gno.land/r/demo/boards2/permission_default.gno b/examples/gno.land/r/demo/boards2/permission_default.gno index a24bc02f2ec..2eae5806e99 100644 --- a/examples/gno.land/r/demo/boards2/permission_default.gno +++ b/examples/gno.land/r/demo/boards2/permission_default.gno @@ -5,7 +5,7 @@ import ( "std" "gno.land/p/demo/avl" - "gno.land/p/demo/boards2/admindao" + "gno.land/p/demo/commondao" "gno.land/r/demo/users" ) @@ -13,14 +13,14 @@ import ( // DefaultPermissions manages users, roles and permissions. type DefaultPermissions struct { superRole Role - dao *admindao.AdminDAO + dao *commondao.CommonDAO users *avl.Tree // string(std.Address) -> []Role roles *avl.Tree // string(role) -> []Permission } // NewDefaultPermissions creates a new permissions type. // This type is a default implementation to handle users, roles and permissions. -func NewDefaultPermissions(dao *admindao.AdminDAO, options ...DefaultPermissionsOption) *DefaultPermissions { +func NewDefaultPermissions(dao *commondao.CommonDAO, options ...DefaultPermissionsOption) *DefaultPermissions { dp := &DefaultPermissions{ dao: dao, roles: avl.NewTree(), @@ -118,7 +118,7 @@ func (dp DefaultPermissions) HasUser(user std.Address) bool { // GetDAO returns the underlying DAO. // Returned value can be nil if the implementation doesn't have a DAO. -func (dp DefaultPermissions) GetDAO() *admindao.AdminDAO { +func (dp DefaultPermissions) GetDAO() *commondao.CommonDAO { return dp.dao } @@ -161,6 +161,7 @@ func (DefaultPermissions) handleBoardCreate(args Args, cb func(Args)) { panic("expected board name to be a string") } + assertValidBoardNameLength(name) assertBoardNameIsNotAddress(name) assertBoardNameBelongsToCaller(name) @@ -173,6 +174,7 @@ func (DefaultPermissions) handleBoardRename(args Args, cb func(Args)) { panic("expected new board name to be a string") } + assertValidBoardNameLength(newName) assertBoardNameIsNotAddress(newName) assertBoardNameBelongsToCaller(newName) @@ -225,7 +227,7 @@ func (dp DefaultPermissions) handleRoleChange(args Args, cb func(Args)) { func createDefaultPermissions(owner std.Address) *DefaultPermissions { return NewDefaultPermissions( - admindao.New(), + commondao.New(), WithSuperRole(RoleOwner), WithRole( RoleAdmin, @@ -263,6 +265,12 @@ func assertBoardNameIsNotAddress(s string) { } } +func assertValidBoardNameLength(name string) { + if len(name) < 6 { + panic("the minimum allowed board name length is 6 characters") + } +} + func assertBoardNameBelongsToCaller(name string) { // When the board name is the name of a registered user // check that caller is the owner of the name. diff --git a/examples/gno.land/r/demo/boards2/permission_default_test.gno b/examples/gno.land/r/demo/boards2/permission_default_test.gno index 625d56720c0..76980bd552c 100644 --- a/examples/gno.land/r/demo/boards2/permission_default_test.gno +++ b/examples/gno.land/r/demo/boards2/permission_default_test.gno @@ -4,7 +4,7 @@ import ( "std" "testing" - "gno.land/p/demo/boards2/admindao" + "gno.land/p/demo/commondao" "gno.land/p/demo/uassert" "gno.land/p/demo/urequire" ) @@ -13,7 +13,7 @@ var _ Permissions = (*DefaultPermissions)(nil) func TestNewDefaultPermissions(t *testing.T) { roles := []Role{"a", "b"} - dao := admindao.New() + dao := commondao.New() perms := NewDefaultPermissions(dao, WithRole("a", "permission1"), WithRole("b", "permission2")) @@ -37,7 +37,7 @@ func TestDefaultPermissionsWithPermission(t *testing.T) { user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", perms: NewDefaultPermissions( - admindao.New(), + commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), WithRole("foo", "bar"), ), @@ -49,7 +49,7 @@ func TestDefaultPermissionsWithPermission(t *testing.T) { permission: "bar", args: Args{"a", "b"}, perms: NewDefaultPermissions( - admindao.New(), + commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), WithRole("foo", "bar"), ), @@ -60,7 +60,7 @@ func TestDefaultPermissionsWithPermission(t *testing.T) { user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", perms: NewDefaultPermissions( - admindao.New(), + commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), WithRole("foo", "bar"), ), @@ -70,7 +70,7 @@ func TestDefaultPermissionsWithPermission(t *testing.T) { name: "is not a DAO member", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", - perms: NewDefaultPermissions(admindao.New()), + perms: NewDefaultPermissions(commondao.New()), err: "unauthorized", }, } @@ -118,30 +118,30 @@ func TestDefaultPermissionsGetUserRoles(t *testing.T) { name: "single role", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", roles: []string{"admin"}, - perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin")), + perms: NewDefaultPermissions(commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin")), }, { name: "multiple roles", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", roles: []string{"admin", "foo", "bar"}, - perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo", "bar")), + perms: NewDefaultPermissions(commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo", "bar")), }, { name: "without roles", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + perms: NewDefaultPermissions(commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), }, { name: "not a user", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - perms: NewDefaultPermissions(admindao.New()), + perms: NewDefaultPermissions(commondao.New()), }, { name: "multiple users", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", roles: []string{"admin"}, perms: NewDefaultPermissions( - admindao.New(), + commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin"), WithUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "admin"), WithUser("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc", "admin", "bar"), @@ -173,26 +173,26 @@ func TestDefaultPermissionsHasRole(t *testing.T) { name: "ok", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", role: "admin", - perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin")), + perms: NewDefaultPermissions(commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin")), want: true, }, { name: "ok with multiple roles", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", role: "foo", - perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo")), + perms: NewDefaultPermissions(commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo")), want: true, }, { name: "user without roles", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + perms: NewDefaultPermissions(commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), }, { name: "has no role", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", role: "bar", - perms: NewDefaultPermissions(admindao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo")), + perms: NewDefaultPermissions(commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo")), }, } @@ -217,7 +217,7 @@ func TestDefaultPermissionsHasPermission(t *testing.T) { user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", perms: NewDefaultPermissions( - admindao.New(), + commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), WithRole("foo", "bar"), ), @@ -228,7 +228,7 @@ func TestDefaultPermissionsHasPermission(t *testing.T) { user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", perms: NewDefaultPermissions( - admindao.New(), + commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), WithUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "foo"), WithRole("foo", "bar"), @@ -240,7 +240,7 @@ func TestDefaultPermissionsHasPermission(t *testing.T) { user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "other", perms: NewDefaultPermissions( - admindao.New(), + commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo", "baz"), WithRole("foo", "bar"), WithRole("baz", "other"), @@ -252,7 +252,7 @@ func TestDefaultPermissionsHasPermission(t *testing.T) { user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "other", perms: NewDefaultPermissions( - admindao.New(), + commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), WithRole("foo", "bar"), ), @@ -281,7 +281,7 @@ func TestDefaultPermissionsAddUser(t *testing.T) { roles: []Role{"a", "b"}, setup: func() *DefaultPermissions { return NewDefaultPermissions( - admindao.New(), + commondao.New(), WithRole("a", "permission1"), WithRole("b", "permission2"), ) @@ -293,7 +293,7 @@ func TestDefaultPermissionsAddUser(t *testing.T) { roles: []Role{"a"}, setup: func() *DefaultPermissions { return NewDefaultPermissions( - admindao.New(), + commondao.New(), WithRole("a", "permission1"), WithUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "a"), WithUser("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc"), @@ -305,7 +305,7 @@ func TestDefaultPermissionsAddUser(t *testing.T) { user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), setup: func() *DefaultPermissions { return NewDefaultPermissions( - admindao.New(), + commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), ) }, @@ -316,7 +316,7 @@ func TestDefaultPermissionsAddUser(t *testing.T) { user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), roles: []Role{"a", "foo"}, setup: func() *DefaultPermissions { - return NewDefaultPermissions(admindao.New(), WithRole("a", "permission1")) + return NewDefaultPermissions(commondao.New(), WithRole("a", "permission1")) }, err: "invalid role: foo", }, @@ -325,7 +325,7 @@ func TestDefaultPermissionsAddUser(t *testing.T) { user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), setup: func() *DefaultPermissions { return NewDefaultPermissions( - admindao.New(admindao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + commondao.New(commondao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), ) }, err: "member already exist", @@ -369,7 +369,7 @@ func TestDefaultPermissionsSetUserRoles(t *testing.T) { roles: []Role{"b"}, setup: func() *DefaultPermissions { return NewDefaultPermissions( - admindao.New(), + commondao.New(), WithRole("a", "permission1"), WithRole("b", "permission2"), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a"), @@ -382,7 +382,7 @@ func TestDefaultPermissionsSetUserRoles(t *testing.T) { roles: []Role{"b", "c"}, setup: func() *DefaultPermissions { return NewDefaultPermissions( - admindao.New(), + commondao.New(), WithRole("a", "permission1"), WithRole("b", "permission2"), WithRole("c", "permission2"), @@ -396,7 +396,7 @@ func TestDefaultPermissionsSetUserRoles(t *testing.T) { roles: []Role{"a", "c"}, setup: func() *DefaultPermissions { return NewDefaultPermissions( - admindao.New(), + commondao.New(), WithRole("a", "permission1"), WithRole("b", "permission2"), WithRole("c", "permission2"), @@ -409,7 +409,7 @@ func TestDefaultPermissionsSetUserRoles(t *testing.T) { user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), setup: func() *DefaultPermissions { return NewDefaultPermissions( - admindao.New(), + commondao.New(), WithRole("a", "permission1"), WithRole("b", "permission2"), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a", "b"), @@ -422,7 +422,7 @@ func TestDefaultPermissionsSetUserRoles(t *testing.T) { roles: []Role{"x", "a"}, setup: func() *DefaultPermissions { return NewDefaultPermissions( - admindao.New(), + commondao.New(), WithRole("a", "permission1"), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a"), ) @@ -433,7 +433,7 @@ func TestDefaultPermissionsSetUserRoles(t *testing.T) { name: "user not found", user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), setup: func() *DefaultPermissions { - return NewDefaultPermissions(admindao.New()) + return NewDefaultPermissions(commondao.New()) }, err: "user not found", }, @@ -474,7 +474,7 @@ func TestDefaultPermissionsRemoveUser(t *testing.T) { user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), setup: func() *DefaultPermissions { return NewDefaultPermissions( - admindao.New(), + commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), ) }, @@ -484,7 +484,7 @@ func TestDefaultPermissionsRemoveUser(t *testing.T) { name: "user not found", user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), setup: func() *DefaultPermissions { - return NewDefaultPermissions(admindao.New()) + return NewDefaultPermissions(commondao.New()) }, }, } diff --git a/examples/gno.land/r/demo/boards2/z_0_a_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_a_filetest.gno index fc3781a78a8..0079c7fb404 100644 --- a/examples/gno.land/r/demo/boards2/z_0_a_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_0_a_filetest.gno @@ -13,7 +13,7 @@ func init() { } func main() { - bid := boards2.CreateBoard("test1") + bid := boards2.CreateBoard("test123") println("ID =", bid) } diff --git a/examples/gno.land/r/demo/boards2/z_0_c_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_c_filetest.gno index e9a06b53e55..087b56fac2b 100644 --- a/examples/gno.land/r/demo/boards2/z_0_c_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_0_c_filetest.gno @@ -8,7 +8,7 @@ import ( const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 - boardName = "test1" + boardName = "test123" ) func init() { diff --git a/examples/gno.land/r/demo/boards2/z_0_d_filetest.gno b/examples/gno.land/r/demo/boards2/z_0_d_filetest.gno index 8992a5cb133..176e3ae881c 100644 --- a/examples/gno.land/r/demo/boards2/z_0_d_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_0_d_filetest.gno @@ -4,7 +4,7 @@ package boards2_test import "gno.land/r/demo/boards2" func main() { - boards2.CreateBoard("foo") + boards2.CreateBoard("foo123") } // Error: diff --git a/examples/gno.land/r/demo/boards2/z_2_a_filetest.gno b/examples/gno.land/r/demo/boards2/z_2_a_filetest.gno index d95759e4236..228b4c35dfd 100644 --- a/examples/gno.land/r/demo/boards2/z_2_a_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_2_a_filetest.gno @@ -13,7 +13,7 @@ func init() { } func main() { - bid := boards2.CreateBoard("test1") + bid := boards2.CreateBoard("test123") pid := boards2.CreateThread(bid, "thread", "thread") boards2.FlagThread(bid, pid, "reason") _ = boards2.CreateReply(bid, pid, pid, "reply") diff --git a/examples/gno.land/r/demo/boards2/z_2_b_filetest.gno b/examples/gno.land/r/demo/boards2/z_2_b_filetest.gno index a4f4d403c8c..908ecb03e33 100644 --- a/examples/gno.land/r/demo/boards2/z_2_b_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_2_b_filetest.gno @@ -14,7 +14,7 @@ func init() { func main() { // ensure that nested replies denied if root thread is hidden. - bid := boards2.CreateBoard("test1") + bid := boards2.CreateBoard("test123") pid := boards2.CreateThread(bid, "thread", "thread") rid := boards2.CreateReply(bid, pid, pid, "reply1") diff --git a/examples/gno.land/r/demo/boards2/z_2_c_filetest.gno b/examples/gno.land/r/demo/boards2/z_2_c_filetest.gno index 3a27b4497cd..911e44ffe62 100644 --- a/examples/gno.land/r/demo/boards2/z_2_c_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_2_c_filetest.gno @@ -14,7 +14,7 @@ func init() { func main() { // ensure that nested replies denied if root thread is hidden. - bid := boards2.CreateBoard("test1") + bid := boards2.CreateBoard("test123") pid := boards2.CreateThread(bid, "thread", "thread") rid := boards2.CreateReply(bid, pid, pid, "reply1") diff --git a/examples/gno.land/r/demo/boards2/z_2_d_filetest.gno b/examples/gno.land/r/demo/boards2/z_2_d_filetest.gno index dbd809c84cd..9f140ba25b5 100644 --- a/examples/gno.land/r/demo/boards2/z_2_d_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_2_d_filetest.gno @@ -14,7 +14,7 @@ func init() { func main() { // Only single user per flag can't be tested atm, as flagThreshold = 1. - bid := boards2.CreateBoard("test1") + bid := boards2.CreateBoard("test123") pid := boards2.CreateThread(bid, "thread", "thread") boards2.FlagThread(bid, pid, "reason1") diff --git a/examples/gno.land/r/demo/boards2/z_3_a_filetest.gno b/examples/gno.land/r/demo/boards2/z_3_a_filetest.gno index 0b06c219725..48af8039a5a 100644 --- a/examples/gno.land/r/demo/boards2/z_3_a_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_3_a_filetest.gno @@ -8,7 +8,7 @@ import ( const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 - name = "foo" + name = "foo123" ) func init() { @@ -17,7 +17,7 @@ func init() { } func main() { - newName := "bar" + newName := "bar123" _, exists := boards2.GetBoardIDFromName(newName) println("Exists =", exists) diff --git a/examples/gno.land/r/demo/boards2/z_3_b_filetest.gno b/examples/gno.land/r/demo/boards2/z_3_b_filetest.gno index 02186772956..9dbe90c60b6 100644 --- a/examples/gno.land/r/demo/boards2/z_3_b_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_3_b_filetest.gno @@ -8,7 +8,7 @@ import ( const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 - name = "foo" + name = "foo123" ) func init() { diff --git a/examples/gno.land/r/demo/boards2/z_3_c_filetest.gno b/examples/gno.land/r/demo/boards2/z_3_c_filetest.gno index 7567c7872e6..0489cb63fa2 100644 --- a/examples/gno.land/r/demo/boards2/z_3_c_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_3_c_filetest.gno @@ -8,7 +8,7 @@ import ( const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 - name = "foo" + name = "foo123" ) func init() { diff --git a/examples/gno.land/r/demo/boards2/z_3_e_filetest.gno b/examples/gno.land/r/demo/boards2/z_3_e_filetest.gno index 4075e8712cd..6911d843479 100644 --- a/examples/gno.land/r/demo/boards2/z_3_e_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_3_e_filetest.gno @@ -8,7 +8,7 @@ import ( const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 - name = "foo" + name = "foo123" ) func init() { diff --git a/examples/gno.land/r/demo/boards2/z_3_f_filetest.gno b/examples/gno.land/r/demo/boards2/z_3_f_filetest.gno index 23f9bc7d3a6..4d243479430 100644 --- a/examples/gno.land/r/demo/boards2/z_3_f_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_3_f_filetest.gno @@ -13,7 +13,7 @@ const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards - name = "foo" + name = "foo123" newName = "barbaz" ) diff --git a/examples/gno.land/r/demo/boards2/z_3_g_filetest.gno b/examples/gno.land/r/demo/boards2/z_3_g_filetest.gno index f953f14a1e4..0327ed7b07c 100644 --- a/examples/gno.land/r/demo/boards2/z_3_g_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_3_g_filetest.gno @@ -14,7 +14,7 @@ const ( member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 member2 = std.Address("g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5") bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards - name = "foo" + name = "foo123" newName = "barbaz" ) diff --git a/examples/gno.land/r/demo/boards2/z_4_b_filetest.gno b/examples/gno.land/r/demo/boards2/z_4_b_filetest.gno index dd638576459..a226e19d60a 100644 --- a/examples/gno.land/r/demo/boards2/z_4_b_filetest.gno +++ b/examples/gno.land/r/demo/boards2/z_4_b_filetest.gno @@ -17,7 +17,7 @@ var bid boards2.BoardID func init() { std.TestSetOrigCaller(owner) - bid = boards2.CreateBoard("foo") // Operate on board DAO members + bid = boards2.CreateBoard("foo123") // Operate on board DAO members boards2.InviteMember(bid, member, boards2.RoleGuest) } From 2f99874bc13663c232f937219151ac7535fef526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Mon, 27 Jan 2025 09:18:00 +0100 Subject: [PATCH 19/52] feat(boards2): change public delete reply function to soft delete replies (#3606) Replies are soft deleted by replacing their body with a deleted message to keep sub comments Related to https://github.com/gnolang/gno/pull/3583#pullrequestreview-2569368733 --- examples/gno.land/r/demo/boards2/post.gno | 4 ++++ .../gno.land/r/demo/boards2/post_test.gno | 5 +++++ examples/gno.land/r/demo/boards2/public.gno | 21 +++++++++++-------- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index d121683c763..e4c8fabe375 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -148,6 +148,10 @@ func (post *Post) Update(title string, body string) { post.updatedAt = time.Now() } +func (post *Post) HasReplies() bool { + return post.replies.Size() > 0 +} + func (thread *Post) GetReply(pid PostID) (_ *Post, found bool) { v, found := thread.repliesAll.Get(pid.Key()) if !found { diff --git a/examples/gno.land/r/demo/boards2/post_test.gno b/examples/gno.land/r/demo/boards2/post_test.gno index 75e52f43c83..9d7367fc6ca 100644 --- a/examples/gno.land/r/demo/boards2/post_test.gno +++ b/examples/gno.land/r/demo/boards2/post_test.gno @@ -140,6 +140,7 @@ func TestNewThread(t *testing.T) { uassert.True(t, thread.GetUpdatedAt().IsZero()) uassert.Equal(t, title, thread.GetTitle()) uassert.Equal(t, body[:77]+"...", thread.GetSummary()) + uassert.False(t, thread.HasReplies()) uassert.Equal(t, url, thread.GetURL()) uassert.Equal(t, replyURL, thread.GetReplyFormURL()) uassert.Equal(t, repostURL, thread.GetRepostFormURL()) @@ -160,6 +161,7 @@ func TestThreadAddReply(t *testing.T) { uassert.Equal(t, threadID+1, uint(reply.GetPostID())) uassert.Equal(t, reply.GetCreator(), replier) uassert.Equal(t, reply.GetBody(), body) + uassert.True(t, thread.HasReplies()) } func TestThreadGetReply(t *testing.T) { @@ -297,6 +299,7 @@ func TestNewReply(t *testing.T) { uassert.Equal(t, uint(replyID), uint(reply.GetPostID())) uassert.False(t, reply.GetCreatedAt().IsZero()) uassert.True(t, reply.GetUpdatedAt().IsZero()) + uassert.False(t, reply.HasReplies()) uassert.Equal(t, body[:77]+"...", reply.GetSummary()) uassert.Equal(t, url, reply.GetURL()) uassert.Equal(t, replyURL, reply.GetReplyFormURL()) @@ -320,6 +323,8 @@ func TestReplyAddReply(t *testing.T) { uassert.Equal(t, parentReplyID+1, uint(reply.GetPostID())) uassert.Equal(t, reply.GetCreator(), replier) uassert.Equal(t, reply.GetBody(), body) + uassert.False(t, reply.HasReplies()) + uassert.True(t, parentReply.HasReplies()) } func TestReplyGetReply(t *testing.T) { diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 15a72e5e943..82c5fb78877 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -174,18 +174,21 @@ func DeleteReply(bid BoardID, threadID, replyID PostID) { board := mustGetBoard(bid) thread := mustGetThread(board, threadID) - assertReplyExists(thread, replyID) - - // TODO: Hide reply when the caller is the owner of the reply without permission - // TODO: Support removing reply and children though proposals? + reply := mustGetReply(thread, replyID) + assertReplyVisible(reply) caller := std.GetOrigCaller() - args := Args{bid, threadID, replyID} - gPerm.WithPermission(caller, PermissionReplyDelete, args, func(a Args) { - board := mustGetBoard(bid) - thread := mustGetThread(board, threadID) + if caller != reply.GetCreator() { + assertHasBoardPermission(board, caller, PermissionReplyDelete) + } + + // Soft delete reply by changing its body when it contains + // sub-replies, otherwise hard delete it. + if reply.HasReplies() { + reply.Update(reply.GetTitle(), "this reply has been deleted") + } else { thread.DeleteReply(replyID) - }) + } } func EditThread(bid BoardID, threadID PostID, title, body string) { From dc5a753311c9d13e0879a452fcd8794c2472dff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Mon, 27 Jan 2025 09:24:43 +0100 Subject: [PATCH 20/52] feat(boards2): improve public realm functions (#3583) The PR simplifies the implementation of a couple public boards2 realm functions and change them to be up to date with the latest permission handling changes. These changes also allow a creator of a thread to edit it without permission, or creators of a reply/comment to delete the reply. Also creators of a thread are allowed to delete it for now. This behavior might change in future PRs. --- examples/gno.land/r/demo/boards2/board.gno | 5 ++ .../gno.land/r/demo/boards2/permission.gno | 1 + .../r/demo/boards2/permission_default.gno | 4 +- examples/gno.land/r/demo/boards2/post.gno | 2 +- examples/gno.land/r/demo/boards2/public.gno | 67 +++++++------------ 5 files changed, 33 insertions(+), 46 deletions(-) diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index dfca8f2d761..e70214a1ad0 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -142,7 +142,9 @@ func createDefaultBoardPermissions(owner std.Address) *DefaultPermissions { PermissionThreadCreate, PermissionThreadEdit, PermissionThreadDelete, + PermissionThreadRepost, PermissionThreadFlag, + PermissionReplyCreate, PermissionReplyDelete, PermissionReplyFlag, PermissionRoleChange, @@ -151,13 +153,16 @@ func createDefaultBoardPermissions(owner std.Address) *DefaultPermissions { RoleModerator, PermissionThreadCreate, PermissionThreadEdit, + PermissionThreadRepost, PermissionThreadFlag, + PermissionReplyCreate, PermissionReplyFlag, ), WithRole( RoleGuest, PermissionThreadCreate, PermissionThreadRepost, + PermissionReplyCreate, ), WithUser(owner, RoleOwner), ) diff --git a/examples/gno.land/r/demo/boards2/permission.gno b/examples/gno.land/r/demo/boards2/permission.gno index 13fc14276e7..7c83416a37e 100644 --- a/examples/gno.land/r/demo/boards2/permission.gno +++ b/examples/gno.land/r/demo/boards2/permission.gno @@ -14,6 +14,7 @@ const ( PermissionThreadDelete = "thread:delete" PermissionThreadFlag = "thread:flag" PermissionThreadRepost = "thread:repost" + PermissionReplyCreate = "reply:create" PermissionReplyDelete = "reply:delete" PermissionReplyFlag = "reply:flag" PermissionMemberInvite = "member:invite" diff --git a/examples/gno.land/r/demo/boards2/permission_default.gno b/examples/gno.land/r/demo/boards2/permission_default.gno index 2eae5806e99..83eb027321c 100644 --- a/examples/gno.land/r/demo/boards2/permission_default.gno +++ b/examples/gno.land/r/demo/boards2/permission_default.gno @@ -64,7 +64,6 @@ func (dp DefaultPermissions) HasRole(user std.Address, r Role) bool { // HasPermission checks if a user has a specific permission. func (dp DefaultPermissions) HasPermission(user std.Address, perm Permission) bool { - // TODO: Should we check that the user belongs to the DAO? for _, r := range dp.GetUserRoles(user) { if dp.superRole == r { return true @@ -239,6 +238,7 @@ func createDefaultPermissions(owner std.Address) *DefaultPermissions { PermissionThreadEdit, PermissionThreadDelete, PermissionThreadFlag, + PermissionReplyCreate, PermissionReplyDelete, PermissionReplyFlag, PermissionRoleChange, @@ -248,12 +248,14 @@ func createDefaultPermissions(owner std.Address) *DefaultPermissions { PermissionThreadCreate, PermissionThreadEdit, PermissionThreadFlag, + PermissionReplyCreate, PermissionReplyFlag, ), WithRole( RoleGuest, PermissionThreadCreate, PermissionThreadRepost, + PermissionReplyCreate, ), WithUser(owner, RoleOwner), ) diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index e4c8fabe375..3789b214f75 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -192,7 +192,7 @@ func (post *Post) DeleteReply(replyID PostID) error { return errors.New("reply not found in thread") } - // TODO: Remove child replies too! + // TODO: Shouldn't reply be hidden instead of deleted? Maybe replace reply by a deleted message. reply := v.(*Post) if reply.parentID != post.id { parent, _ := post.GetReply(reply.parentID) diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/demo/boards2/public.gno index 82c5fb78877..47f2ac9b294 100644 --- a/examples/gno.land/r/demo/boards2/public.gno +++ b/examples/gno.land/r/demo/boards2/public.gno @@ -66,7 +66,8 @@ func FlagThread(bid BoardID, postID PostID, reason string) { panic("post doesn't exist") } - if flagItem(t, NewFlag(caller, reason)) { + hide := flagItem(t, NewFlag(caller, reason)) + if hide { t.SetVisible(false) } } @@ -74,12 +75,10 @@ func FlagThread(bid BoardID, postID PostID, reason string) { func CreateThread(bid BoardID, title, body string) PostID { assertIsUserCall() - // TODO: Assert that caller is a board member (when board type is invite only) caller := std.GetOrigCaller() - assertHasPermission(caller, PermissionThreadCreate) // TODO: Who can create threads? - assertBoardExists(bid) - board := mustGetBoard(bid) + assertHasBoardPermission(board, caller, PermissionThreadCreate) + thread := board.AddThread(caller, title, body) return thread.id } @@ -87,15 +86,13 @@ func CreateThread(bid BoardID, title, body string) PostID { func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { assertIsUserCall() - // TODO: Assert that caller is a board member when board type is invite only caller := std.GetOrigCaller() board := mustGetBoard(bid) - thread := mustGetThread(board, threadID) + assertHasBoardPermission(board, caller, PermissionReplyCreate) + thread := mustGetThread(board, threadID) assertThreadVisible(thread) - // TODO: Assert thread is not locked - // TODO: Assert that caller is a board member (when board type is invite only) var reply *Post if replyID == threadID { // When the parent reply is the thread just add reply to thread @@ -112,14 +109,14 @@ func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { func FlagReply(bid BoardID, threadID, replyID PostID, reason string) { caller := std.GetOrigCaller() - board := mustGetBoard(bid) assertHasBoardPermission(board, caller, PermissionThreadFlag) thread := mustGetThread(board, threadID) reply := mustGetReply(thread, replyID) - if hide := flagItem(reply, NewFlag(caller, reason)); hide { + hide := flagItem(reply, NewFlag(caller, reason)) + if hide { reply.SetVisible(false) } } @@ -127,46 +124,33 @@ func FlagReply(bid BoardID, threadID, replyID PostID, reason string) { func CreateRepost(bid BoardID, threadID PostID, title, body string, dstBoardID BoardID) PostID { assertIsUserCall() - // TODO: Assert that caller is a board member (when board type is invite only) caller := std.GetOrigCaller() - assertBoardExists(dstBoardID) + dst := mustGetBoard(dstBoardID) + assertHasBoardPermission(dst, caller, PermissionThreadRepost) board := mustGetBoard(bid) if board.IsPrivate() { panic("cannot repost from a private board") } - // TODO: Assert that board allows reposts? - // TODO: Assert that caller is member of both boards (when board types are invite only) - - assertThreadExists(board, threadID) - - dst := mustGetBoard(dstBoardID) thread := mustGetThread(board, threadID) - repostId := dst.incGetPostID() repost := newPost(dst, repostId, caller, title, body, repostId, thread.GetPostID(), thread.GetBoard().GetID()) - - args := Args{board.GetID(), threadID, dst.GetID()} - dst.GetPermissions().WithPermission(caller, PermissionThreadRepost, args, func(_ Args) { - thread.AddRepostTo(caller, repost, dst) - }) - + thread.AddRepostTo(caller, repost, dst) return repostId } func DeleteThread(bid BoardID, threadID PostID) { assertIsUserCall() + caller := std.GetOrigCaller() board := mustGetBoard(bid) - assertThreadExists(board, threadID) + thread := mustGetThread(board, threadID) + if caller != thread.GetCreator() { + assertHasBoardPermission(board, caller, PermissionThreadDelete) + } - caller := std.GetOrigCaller() - args := Args{bid, threadID} - gPerm.WithPermission(caller, PermissionThreadDelete, args, func(Args) { - board := mustGetBoard(bid) - board.DeleteThread(threadID) - }) + board.DeleteThread(threadID) } func DeleteReply(bid BoardID, threadID, replyID PostID) { @@ -195,17 +179,13 @@ func EditThread(bid BoardID, threadID PostID, title, body string) { assertIsUserCall() board := mustGetBoard(bid) - assertThreadExists(board, threadID) - - // TODO: Thread owners should be able to edit without permission? - + thread := mustGetThread(board, threadID) caller := std.GetOrigCaller() - args := Args{bid, threadID, title, body} - gPerm.WithPermission(caller, PermissionThreadEdit, args, func(Args) { - board := mustGetBoard(bid) - thread := mustGetThread(board, threadID) - thread.Update(title, body) - }) + if caller != thread.GetCreator() { + assertHasBoardPermission(board, caller, PermissionThreadEdit) + } + + thread.Update(title, body) } func EditReply(bid BoardID, threadID, replyID PostID, title, body string) { @@ -219,7 +199,6 @@ func EditReply(bid BoardID, threadID, replyID PostID, title, body string) { panic("only the reply creator is allowed to edit it") } - // TODO: Should we have a history of previous reply contents? reply.Update(title, body) } From 48a804203a8fbc092476f85349cd36c46f5c1924 Mon Sep 17 00:00:00 2001 From: Denys Sedchenko <9203548+x1unix@users.noreply.github.com> Date: Mon, 27 Jan 2025 04:13:31 -0500 Subject: [PATCH 21/52] feat(boards2): pagination (#3586) Implement pagination for boards, threads and replies. Closes #3200, #3539 CC @jeronimoalbi @salmad3 --- examples/gno.land/r/demo/boards2/board.gno | 49 ++++++++++++++----- .../gno.land/r/demo/boards2/pagination.gno | 48 ++++++++++++++++++ examples/gno.land/r/demo/boards2/post.gno | 46 ++++++++++------- examples/gno.land/r/demo/boards2/render.gno | 30 +++++++++--- 4 files changed, 135 insertions(+), 38 deletions(-) create mode 100644 examples/gno.land/r/demo/boards2/pagination.gno diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/demo/boards2/board.gno index e70214a1ad0..884864359b9 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/demo/boards2/board.gno @@ -1,6 +1,7 @@ package boards2 import ( + "net/url" "std" "strconv" "strings" @@ -65,6 +66,13 @@ func (board *Board) GetID() BoardID { // GetURL returns the relative URL of the board. func (board *Board) GetURL() string { + return strings.TrimPrefix(std.CurrentRealm().PkgPath(), "gno.land") + ":" + url.PathEscape(board.name) +} + +// GetURL returns relative board path relative. +// +// Note: returned result is not escaped. Use GetURL to get URL-encoded path. +func (board *Board) GetPath() string { return strings.TrimPrefix(std.CurrentRealm().PkgPath(), "gno.land") + ":" + board.name } @@ -96,21 +104,38 @@ func (board *Board) DeleteThread(pid PostID) { } } -func (board *Board) Render() string { - s := "\\[" + newLink("post", board.GetPostFormURL()) + "]\n\n" - if board.threads.Size() > 0 { - board.threads.Iterate("", "", func(_ string, v interface{}) bool { - post := v.(*Post) - if post.isHidden { - return false - } +// Render renders a board into Markdown. +// +// Pager is used for pagination if it's not nil. +func (board *Board) Render(p *PaginationOpts) string { + sb := &strings.Builder{} + + sb.WriteString(newButtonLink("post", board.GetPostFormURL())) + sb.WriteString("\n\n") + + if board.threads.Size() == 0 { + sb.WriteString("*This board doesn't have any threads.*") + return sb.String() + } - s += "----------------------------------------\n" - s += post.RenderSummary() + "\n" + page := p.Iterate(&board.threads, func(_ string, v interface{}) bool { + p := v.(*Post) + if p.isHidden { return false - }) + } + + sb.WriteString("----------------------------------------\n") + sb.WriteString(p.RenderSummary()) + sb.WriteString("\n") + return false + }) + + if page != nil { + sb.WriteString("\n---\n") + sb.WriteString(page.Picker()) } - return s + + return sb.String() } func (board *Board) incGetPostID() PostID { diff --git a/examples/gno.land/r/demo/boards2/pagination.gno b/examples/gno.land/r/demo/boards2/pagination.gno new file mode 100644 index 00000000000..bd8ec4579a9 --- /dev/null +++ b/examples/gno.land/r/demo/boards2/pagination.gno @@ -0,0 +1,48 @@ +package boards2 + +import ( + "gno.land/p/demo/avl" + "gno.land/p/demo/avl/pager" +) + +type PaginationOpts struct { + pager pager.Pager + pageNumber int +} + +// Iterate loops over an a page. +// +// Loops though all avl tree contents if PaginationOpts is nil. +func (opts *PaginationOpts) Iterate(tree *avl.Tree, cb func(k string, val interface{}) bool) *pager.Page { + if opts == nil { + tree.Iterate("", "", cb) + return nil + } + + opts.pager.Tree = tree + page := opts.pager.GetPage(opts.pageNumber) + for _, item := range page.Items { + if cb(item.Key, item.Value) { + break + } + } + + return page +} + +func mustGetPagination(rawPath string, pageSize int) *PaginationOpts { + p := pager.Pager{ + PageQueryParam: "page", + DefaultPageSize: pageSize, + } + + pageNumber, _, err := p.ParseQuery(rawPath) + if err != nil { + panic(err) + } + + return &PaginationOpts{ + pager: p, + pageNumber: pageNumber, + } +} diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/demo/boards2/post.gno index 3789b214f75..05883810157 100644 --- a/examples/gno.land/r/demo/boards2/post.gno +++ b/examples/gno.land/r/demo/boards2/post.gno @@ -363,7 +363,7 @@ func (post *Post) renderPostContent(sb *strings.Builder, indent string) { sb.WriteString("\n") } -func (post *Post) Render(indent string, levels int) string { +func (post *Post) Render(p *PaginationOpts, indent string, levels int) string { if post == nil { return "nil post" } @@ -382,24 +382,32 @@ func (post *Post) Render(indent string, levels int) string { post.renderPostContent(sb, indent) - if post.replies.Size() > 0 { - if levels > 0 { - commentsIndent := indent + "> " + if post.replies.Size() == 0 { + return sb.String() + } - post.replies.Iterate("", "", func(_ string, value interface{}) bool { - reply := value.(*Post) + if levels == 0 { + sb.WriteString(indent + "\n") + sb.WriteString(indent) + sb.WriteString("_") + sb.WriteString(newLink("see all "+strconv.Itoa(post.replies.Size())+" replies", post.GetURL())) + sb.WriteString("_\n") + return sb.String() + } - sb.WriteString(indent + "\n") - sb.WriteString(reply.Render(commentsIndent, levels-1)) - return false - }) - } else { - sb.WriteString(indent + "\n") - sb.WriteString(indent) - sb.WriteString("_") - sb.WriteString(newLink("see all "+strconv.Itoa(post.replies.Size())+" replies", post.GetURL())) - sb.WriteString("_\n") - } + commentsIndent := indent + "> " + page := p.Iterate(&post.replies, func(_ string, value interface{}) bool { + reply := value.(*Post) + + sb.WriteString(indent) + sb.WriteString("\n") + sb.WriteString(reply.Render(nil, commentsIndent, levels-1)) + return false + }) + + if page != nil { + sb.WriteString("\n---\n") + sb.WriteString(page.Picker()) } return sb.String() @@ -430,9 +438,9 @@ func (post *Post) RenderInner() string { parent, _ = thread.GetReply(parentID) } - s += parent.Render("", 0) + "\n" + s += parent.Render(nil, "", 0) + "\n" } - s += post.Render("> ", 5) + s += post.Render(nil, "> ", 5) return s } diff --git a/examples/gno.land/r/demo/boards2/render.gno b/examples/gno.land/r/demo/boards2/render.gno index 3bde3ec4ee5..653d5338927 100644 --- a/examples/gno.land/r/demo/boards2/render.gno +++ b/examples/gno.land/r/demo/boards2/render.gno @@ -6,6 +6,12 @@ import ( "gno.land/p/demo/mux" ) +const ( + boardsPageSize = 20 + threadsPageSize = 30 + repliesPageSize = 30 +) + func Render(path string) string { router := mux.NewRouter() router.HandleFunc("", renderBoardsList) @@ -20,14 +26,21 @@ func Render(path string) string { return router.Render(path) } -func renderBoardsList(res *mux.ResponseWriter, _ *mux.Request) { +func renderBoardsList(res *mux.ResponseWriter, req *mux.Request) { res.Write("These are all the boards of this realm:\n\n") - gBoardsByID.Iterate("", "", func(_ string, value interface{}) bool { + p := mustGetPagination(req.RawPath, boardsPageSize) + page := p.Iterate(&gBoardsByID, func(_ string, value interface{}) bool { board := value.(*Board) + path := board.GetPath() url := board.GetURL() - res.Write(" * " + newLink(url, url) + "\n") + res.Write(" * " + newLink(path, url) + "\n") return false }) + + if page != nil { + res.Write("\n---\n") + res.Write(page.Picker()) + } } func renderBoard(res *mux.ResponseWriter, req *mux.Request) { @@ -35,10 +48,12 @@ func renderBoard(res *mux.ResponseWriter, req *mux.Request) { v, found := gBoardsByName.Get(name) if !found { res.Write("Board does not exist: " + name) - } else { - board := v.(*Board) - res.Write(board.Render()) + return } + + board := v.(*Board) + p := mustGetPagination(req.RawPath, threadsPageSize) + res.Write(board.Render(p)) } func renderThread(res *mux.ResponseWriter, req *mux.Request) { @@ -63,7 +78,8 @@ func renderThread(res *mux.ResponseWriter, req *mux.Request) { } else if thread.IsHidden() { res.Write("Thread with ID: " + rawID + " has been flagged as inappropriate") } else { - res.Write(thread.Render("", 5)) + p := mustGetPagination(req.RawPath, repliesPageSize) + res.Write(thread.Render(p, "", 5)) } } From 65920358449b426287816d95d35d27b0335b465f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Tue, 28 Jan 2025 09:32:56 +0100 Subject: [PATCH 22/52] feat(boards2): add proposal definition tally support to `commondao` package (#3616) --- .../gno.land/p/demo/commondao/commondao.gno | 59 ++---- .../p/demo/commondao/commondao_test.gno | 46 +++-- .../gno.land/p/demo/commondao/proposal.gno | 34 +++- .../p/demo/commondao/proposal_test.gno | 42 ++++ examples/gno.land/p/demo/commondao/record.gno | 101 +++++++++- .../gno.land/p/demo/commondao/record_test.gno | 180 ++++++++++++++++++ 6 files changed, 394 insertions(+), 68 deletions(-) create mode 100644 examples/gno.land/p/demo/commondao/proposal_test.gno diff --git a/examples/gno.land/p/demo/commondao/commondao.gno b/examples/gno.land/p/demo/commondao/commondao.gno index 53414624cf0..ac57846c0d3 100644 --- a/examples/gno.land/p/demo/commondao/commondao.gno +++ b/examples/gno.land/p/demo/commondao/commondao.gno @@ -10,19 +10,14 @@ import ( "gno.land/p/demo/seqid" ) -// DefaultQuorum defines the default quorum required to tally proposal votes. -const DefaultQuorum = 0.34 // 34% - var ( - ErrInvalidAddress = errors.New("invalid address") - ErrInvalidVoteChoice = errors.New("invalid vote choice") - ErrMemberExists = errors.New("member already exist") - ErrNotMember = errors.New("account is not a member of the DAO") - ErrOverflow = errors.New("next ID overflows uint64") - ErrProposalDefinitionRequired = errors.New("proposal definition is required") - ErrProposalNotFound = errors.New("proposal not found") - ErrStatusIsNotActive = errors.New("proposal status is not active") - ErrVotingDeadlineNotMet = errors.New("voting deadline not met") + ErrInvalidVoteChoice = errors.New("invalid vote choice") + ErrMemberExists = errors.New("member already exist") + ErrNotMember = errors.New("account is not a member of the DAO") + ErrOverflow = errors.New("next ID overflows uint64") + ErrProposalNotFound = errors.New("proposal not found") + ErrStatusIsNotActive = errors.New("proposal status is not active") + ErrVotingDeadlineNotMet = errors.New("voting deadline not met") ) type ( @@ -104,20 +99,16 @@ func (dao CommonDAO) FinishedProposals() rotree.IReadOnlyTree { // Propose creates a new DAO proposal. func (dao *CommonDAO) Propose(creator std.Address, d ProposalDefinition) (*Proposal, error) { - if d == nil { - return nil, ErrProposalDefinitionRequired - } - - if !creator.IsValid() { - return nil, ErrInvalidAddress - } - id, ok := dao.genID.TryNext() if !ok { return nil, ErrOverflow } - p := NewProposal(uint64(id), creator, d) + p, err := NewProposal(uint64(id), creator, d) + if err != nil { + return nil, err + } + key := makeProposalKey(p.ID()) dao.active.Set(key, p) return p, nil @@ -161,34 +152,26 @@ func (dao *CommonDAO) Vote(member std.Address, proposalID uint64, c VoteChoice) func (dao *CommonDAO) Tally(p *Proposal) Stats { // Initialize stats considering only yes/no votes record := p.VotingRecord() - votesCount := record.VoteCount(ChoiceYes) + record.VoteCount(ChoiceNo) - membersCount := len(dao.Members()) stats := Stats{ - YayVotes: record.VoteCount(ChoiceYes), - NayVotes: record.VoteCount(ChoiceNo), - Abstained: membersCount - votesCount, - } - - // Check quorum before tallying - quorum := p.Definition().Quorum() - if quorum <= 0 || quorum > 1 { - quorum = DefaultQuorum + YayVotes: record.VoteCount(ChoiceYes), + NayVotes: record.VoteCount(ChoiceNo), } + votesCount := stats.YayVotes + stats.NayVotes + membersCount := len(dao.Members()) + stats.Abstained = membersCount - votesCount percentage := float64(votesCount) / float64(membersCount) - if percentage < quorum { + if percentage < p.Quorum() { p.status = StatusFailed p.statusReason = "low participation" return stats } - // Tally by majority requiring more than 50% of yes/no votes to win - // TODO: Confirm this way of tallying is enough for now. Keeping it minimal. - choice := record.GetProvableMajorityChoice() - if record.VoteCount(choice) <= int(votesCount/2) { + if !p.Definition().Tally(record, membersCount) { p.status = StatusFailed - p.statusReason = "majority not met" + p.statusReason = "no consensus" } + return stats } diff --git a/examples/gno.land/p/demo/commondao/commondao_test.gno b/examples/gno.land/p/demo/commondao/commondao_test.gno index 04b2ed94fa3..b118841eb96 100644 --- a/examples/gno.land/p/demo/commondao/commondao_test.gno +++ b/examples/gno.land/p/demo/commondao/commondao_test.gno @@ -126,7 +126,7 @@ func TestCommonDAOPropose(t *testing.T) { name: "ok", setup: func() *CommonDAO { return New() }, creator: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", - def: &testPropDef{}, + def: testPropDef{}, }, { name: "nil definition", @@ -134,10 +134,10 @@ func TestCommonDAOPropose(t *testing.T) { err: ErrProposalDefinitionRequired, }, { - name: "invalid address", + name: "invalid creator address", setup: func() *CommonDAO { return New() }, - def: &testPropDef{}, - err: ErrInvalidAddress, + def: testPropDef{}, + err: ErrInvalidCreatorAddress, }, { name: "proposal ID overflow", @@ -147,7 +147,7 @@ func TestCommonDAOPropose(t *testing.T) { return dao }, creator: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", - def: &testPropDef{}, + def: testPropDef{}, err: ErrOverflow, }, } @@ -186,7 +186,7 @@ func TestCommonDAOVote(t *testing.T) { setup: func() *CommonDAO { member := std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") dao := New(WithMember(member)) - dao.Propose(member, &testPropDef{}) + dao.Propose(member, testPropDef{}) return dao }, member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", @@ -283,7 +283,7 @@ func TestCommonDAOTally(t *testing.T) { stats: Stats{YayVotes: 1, Abstained: 2}, }, { - name: "majority not met", + name: "no consensus", dao: New( WithMember("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn"), WithMember("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc"), @@ -294,14 +294,14 @@ func TestCommonDAOTally(t *testing.T) { {Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", Choice: ChoiceNo}, }, status: StatusFailed, - statusReason: "majority not met", + statusReason: "no consensus", stats: Stats{YayVotes: 1, NayVotes: 1, Abstained: 1}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - p := NewProposal(1, "", testPropDef{}) + p, _ := NewProposal(1, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", majorityPropDef{}) for _, v := range tc.votes { p.record.AddVote(v.Address, v.Choice) } @@ -337,7 +337,7 @@ func TestCommonDAOExecute(t *testing.T) { "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", } dao := New(WithMember(members[0]), WithMember(members[1]), WithMember(members[2])) - p, _ := dao.Propose(members[0], &testPropDef{}) + p, _ := dao.Propose(members[0], testPropDef{tallyResult: true}) p.record.AddVote(members[0], ChoiceYes) p.record.AddVote(members[1], ChoiceYes) return dao @@ -356,7 +356,7 @@ func TestCommonDAOExecute(t *testing.T) { setup: func() *CommonDAO { member := std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") dao := New(WithMember(member)) - p, _ := dao.Propose(member, &testPropDef{}) + p, _ := dao.Propose(member, testPropDef{}) p.status = StatusExecuted return dao }, @@ -368,7 +368,7 @@ func TestCommonDAOExecute(t *testing.T) { setup: func() *CommonDAO { member := std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") dao := New(WithMember(member)) - dao.Propose(member, &testPropDef{votingPeriod: time.Minute * 5}) + dao.Propose(member, testPropDef{votingPeriod: time.Minute * 5}) return dao }, proposalID: 1, @@ -379,7 +379,7 @@ func TestCommonDAOExecute(t *testing.T) { setup: func() *CommonDAO { member := std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") dao := New(WithMember(member)) - dao.Propose(member, &testPropDef{validationErr: errValidation}) + dao.Propose(member, testPropDef{validationErr: errValidation}) return dao }, proposalID: 1, @@ -396,7 +396,10 @@ func TestCommonDAOExecute(t *testing.T) { "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", } dao := New(WithMember(members[0]), WithMember(members[1]), WithMember(members[2])) - p, _ := dao.Propose(members[0], &testPropDef{executionErr: errExecution}) + p, _ := dao.Propose(members[0], testPropDef{ + tallyResult: true, + executionErr: errExecution, + }) p.record.AddVote(members[0], ChoiceYes) p.record.AddVote(members[1], ChoiceYes) return dao @@ -432,14 +435,9 @@ func TestCommonDAOExecute(t *testing.T) { } } -type testPropDef struct { - votingPeriod time.Duration - validationErr, executionErr error -} +type majorityPropDef struct{ testPropDef } -func (testPropDef) Title() string { return "" } -func (testPropDef) Body() string { return "" } -func (testPropDef) Quorum() float64 { return 0 } -func (d testPropDef) VotingPeriod() time.Duration { return d.votingPeriod } -func (d testPropDef) Validate() error { return d.validationErr } -func (d testPropDef) Execute() error { return d.executionErr } +func (majorityPropDef) Tally(r ReadOnlyVotingRecord, membersCount int) bool { + _, success := SelectChoiceByAbsoluteMajority(r, membersCount) + return success +} diff --git a/examples/gno.land/p/demo/commondao/proposal.gno b/examples/gno.land/p/demo/commondao/proposal.gno index 2300a0c635f..ec431f7d77d 100644 --- a/examples/gno.land/p/demo/commondao/proposal.gno +++ b/examples/gno.land/p/demo/commondao/proposal.gno @@ -1,10 +1,14 @@ package commondao import ( + "errors" "std" "time" ) +// DefaultQuorum defines the default quorum required to tally proposal votes. +const DefaultQuorum = 0.34 // 34% + const ( StatusActive ProposalStatus = "active" StatusFailed = "failed" @@ -17,6 +21,11 @@ const ( ChoiceNo = "no" ) +var ( + ErrInvalidCreatorAddress = errors.New("invalid proposal creator address") + ErrProposalDefinitionRequired = errors.New("proposal definition is required") +) + type ( // ProposalStatus defines a type for different proposal states. ProposalStatus string @@ -61,6 +70,10 @@ type ( // Validations are optional and allow the validation of the current state before proposal execution. Validate() error + // Tally counts the number of votes and verifies that there is consensus. + // Tally fails when none of the vote choices wins over the others. + Tally(r ReadOnlyVotingRecord, memberCount int) (success bool) + // Execute executes the proposal. // Once proposal are executed they are archived and considered finished. // Execution allows changing the state after a proposal passes. @@ -69,7 +82,15 @@ type ( ) // NewProposal creates a new DAO proposal. -func NewProposal(id uint64, creator std.Address, d ProposalDefinition) *Proposal { +func NewProposal(id uint64, creator std.Address, d ProposalDefinition) (*Proposal, error) { + if d == nil { + return nil, ErrProposalDefinitionRequired + } + + if !creator.IsValid() { + return nil, ErrInvalidCreatorAddress + } + now := time.Now() return &Proposal{ id: id, @@ -79,7 +100,7 @@ func NewProposal(id uint64, creator std.Address, d ProposalDefinition) *Proposal record: &VotingRecord{}, votingDeadline: now.Add(d.VotingPeriod()), createdAt: now, - } + }, nil } // ID returns the unique proposal identifies. @@ -93,6 +114,15 @@ func (p Proposal) Definition() ProposalDefinition { return p.definition } +// Quorum returns the percentage of members that must vote to be able to pass a proposal. +func (p Proposal) Quorum() float64 { + quorum := p.definition.Quorum() + if quorum <= 0 || quorum > 1 { + quorum = DefaultQuorum + } + return quorum +} + // Status returns the current proposal status. func (p Proposal) Status() ProposalStatus { return p.status diff --git a/examples/gno.land/p/demo/commondao/proposal_test.gno b/examples/gno.land/p/demo/commondao/proposal_test.gno new file mode 100644 index 00000000000..656afa7b120 --- /dev/null +++ b/examples/gno.land/p/demo/commondao/proposal_test.gno @@ -0,0 +1,42 @@ +package commondao + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/uassert" +) + +func TestProposalDefaults(t *testing.T) { + id := uint64(1) + creator := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + votingPeriod := time.Minute * 10 + + p, err := NewProposal(id, creator, testPropDef{votingPeriod: votingPeriod}) + + uassert.NoError(t, err) + uassert.Equal(t, p.ID(), id) + uassert.NotEqual(t, p.Definition(), nil) + uassert.Equal(t, p.Quorum(), DefaultQuorum) + uassert.True(t, p.Status() == StatusActive) + uassert.Equal(t, p.Creator(), creator) + uassert.False(t, p.CreatedAt().IsZero()) + uassert.NotEqual(t, p.VotingRecord(), nil) + uassert.Empty(t, p.StatusReason()) + uassert.True(t, p.VotingDeadline() == p.CreatedAt().Add(votingPeriod)) +} + +type testPropDef struct { + votingPeriod time.Duration + tallyResult bool + validationErr, executionErr error +} + +func (testPropDef) Title() string { return "" } +func (testPropDef) Body() string { return "" } +func (testPropDef) Quorum() float64 { return 0 } +func (d testPropDef) VotingPeriod() time.Duration { return d.votingPeriod } +func (d testPropDef) Validate() error { return d.validationErr } +func (d testPropDef) Tally(ReadOnlyVotingRecord, int) bool { return d.tallyResult } +func (d testPropDef) Execute() error { return d.executionErr } diff --git a/examples/gno.land/p/demo/commondao/record.gno b/examples/gno.land/p/demo/commondao/record.gno index 2080d7dabd3..d1e9d5a092b 100644 --- a/examples/gno.land/p/demo/commondao/record.gno +++ b/examples/gno.land/p/demo/commondao/record.gno @@ -17,13 +17,43 @@ type ( Choice VoteChoice } - // VotingRecord stores accounts that voted and vote choices. - VotingRecord struct { - votes avl.Tree // string(address) -> VoteChoice - count avl.Tree // string(choice) -> int + // ReadOnlyVotingRecord defines an interface for read only voting records. + ReadOnlyVotingRecord interface { + // VoteChoices returns the voting choices that has been voted. + VoteChoices() []VoteChoice + + // Votes returns the list of all votes. + Votes() []Vote + + // VoteCount returns the number of votes for a single voting choice. + VoteCount(VoteChoice) int + + // HasVoted checks if an account already voted. + HasVoted(std.Address) bool + + // GetProvableMajorityChoice returns the choice voted by the majority. + // The result is only valid if there is a majority. + // Caller must validate that the returned choice represents a majority. + GetProvableMajorityChoice() VoteChoice } ) +// VotingRecord stores accounts that voted and vote choices. +type VotingRecord struct { + votes avl.Tree // string(address) -> VoteChoice + count avl.Tree // string(choice) -> int +} + +// VoteChoices returns the voting choices that has been voted. +func (r VotingRecord) VoteChoices() []VoteChoice { + var choices []VoteChoice + r.count.Iterate("", "", func(k string, v interface{}) bool { + choices = append(choices, VoteChoice(k)) + return false + }) + return choices +} + // Votes returns the list of all votes. func (r VotingRecord) Votes() []Vote { var votes []Vote @@ -81,3 +111,66 @@ func (r VotingRecord) GetProvableMajorityChoice() VoteChoice { }) return choice } + +// SelectChoiceByAbsoluteMajority select the vote choice by absolute majority. +// Vote choice is a majority when chosen by more than half of the votes. +// Absolute majority considers abstentions when counting votes. +func SelectChoiceByAbsoluteMajority(r ReadOnlyVotingRecord, memberCount int) (VoteChoice, bool) { + choice := r.GetProvableMajorityChoice() + if r.VoteCount(choice) > int(memberCount/2) { + return choice, true + } + return "", false +} + +// SelectChoiceBySuperMajority select the vote choice by super majority using a 2/3s threshold. +// Abstentions are not considered when calculating the super majority choice. +func SelectChoiceBySuperMajority(r ReadOnlyVotingRecord) (VoteChoice, bool) { + var count int + for _, v := range r.Votes() { + if v.Choice != ChoiceAbstain { + count++ + } + } + + if count < 3 { + return "", false + } + + choice := r.GetProvableMajorityChoice() + if r.VoteCount(choice) >= int((2*count)/3) { + return choice, true + } + return "", false +} + +// SelectChoiceByPlurality selects the vote choice by plurality. +// The choice will be considered a majority if it has votes and if there is no other +// choice with the same number of votes. A tie won't be considered majority. +func SelectChoiceByPlurality(r ReadOnlyVotingRecord) (VoteChoice, bool) { + var ( + choice VoteChoice + currentCount int + isMajority bool + ) + + for _, c := range r.VoteChoices() { + if c == ChoiceAbstain { + continue + } + + count := r.VoteCount(c) + if currentCount < count { + choice = c + currentCount = count + isMajority = true + } else if currentCount == count { + isMajority = false + } + } + + if isMajority { + return choice, true + } + return "", false +} diff --git a/examples/gno.land/p/demo/commondao/record_test.gno b/examples/gno.land/p/demo/commondao/record_test.gno index 3c58922f085..2e4a234ce4c 100644 --- a/examples/gno.land/p/demo/commondao/record_test.gno +++ b/examples/gno.land/p/demo/commondao/record_test.gno @@ -166,3 +166,183 @@ func TestVotingRecordGetProvableMajorityChoice(t *testing.T) { }) } } + +func TestSelectChoiceByAbsoluteMajority(t *testing.T) { + memberCount := 3 + cases := []struct { + name string + setup func(*VotingRecord) + choice VoteChoice + success bool + }{ + { + name: "no votes", + choice: "", + success: false, + }, + { + name: "majority", + setup: func(r *VotingRecord) { + r.AddVote("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", ChoiceYes) + r.AddVote("g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk", ChoiceYes) + r.AddVote("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", ChoiceNo) + }, + choice: ChoiceYes, + success: true, + }, + { + name: "no majority", + setup: func(r *VotingRecord) { + r.AddVote("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", ChoiceYes) + r.AddVote("g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk", ChoiceNo) + }, + choice: "", + success: false, + }, + { + name: "majority with abstain vote", + setup: func(r *VotingRecord) { + r.AddVote("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", ChoiceYes) + r.AddVote("g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk", ChoiceYes) + r.AddVote("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", ChoiceAbstain) + }, + choice: ChoiceYes, + success: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var record VotingRecord + + if tc.setup != nil { + tc.setup(&record) + } + + choice, success := SelectChoiceByAbsoluteMajority(record, memberCount) + + uassert.Equal(t, string(tc.choice), string(choice), "choice") + uassert.Equal(t, tc.success, success, "success") + }) + } +} + +func TestSelectChoiceBySuperMajority(t *testing.T) { + cases := []struct { + name string + setup func(*VotingRecord) + choice VoteChoice + success bool + }{ + { + name: "no votes", + choice: "", + success: false, + }, + { + name: "majority", + setup: func(r *VotingRecord) { + r.AddVote("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", ChoiceYes) + r.AddVote("g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk", ChoiceYes) + r.AddVote("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", ChoiceNo) + }, + choice: ChoiceYes, + success: true, + }, + { + name: "no majority", + setup: func(r *VotingRecord) { + r.AddVote("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", ChoiceYes) + r.AddVote("g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk", ChoiceNo) + }, + choice: "", + success: false, + }, + { + name: "majority with abstain vote", + setup: func(r *VotingRecord) { + r.AddVote("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", ChoiceYes) + r.AddVote("g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk", ChoiceYes) + r.AddVote("g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5", ChoiceNo) + r.AddVote("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", ChoiceAbstain) + }, + choice: ChoiceYes, + success: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var record VotingRecord + + if tc.setup != nil { + tc.setup(&record) + } + + choice, success := SelectChoiceBySuperMajority(record) + + uassert.Equal(t, string(tc.choice), string(choice), "choice") + uassert.Equal(t, tc.success, success, "success") + }) + } +} + +func TestSelectChoiceByPlurality(t *testing.T) { + cases := []struct { + name string + setup func(*VotingRecord) + choice VoteChoice + success bool + }{ + { + name: "no votes", + choice: ChoiceAbstain, + success: false, + }, + { + name: "plurality", + setup: func(r *VotingRecord) { + r.AddVote("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", ChoiceYes) + r.AddVote("g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk", ChoiceYes) + r.AddVote("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", ChoiceNo) + }, + choice: ChoiceYes, + success: true, + }, + { + name: "no plurality", + setup: func(r *VotingRecord) { + r.AddVote("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", ChoiceYes) + r.AddVote("g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk", ChoiceNo) + }, + choice: "", + success: false, + }, + { + name: "plurality with abstain vote", + setup: func(r *VotingRecord) { + r.AddVote("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", ChoiceYes) + r.AddVote("g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk", ChoiceYes) + r.AddVote("g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5", ChoiceNo) + r.AddVote("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", ChoiceAbstain) + }, + choice: ChoiceYes, + success: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var record VotingRecord + + if tc.setup != nil { + tc.setup(&record) + } + + choice, success := SelectChoiceByPlurality(record) + + uassert.Equal(t, string(tc.choice), string(choice), "choice") + uassert.Equal(t, tc.success, success, "success") + }) + } +} From 5f3a9f6899113cf6ab4fa759e206fceca732360c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Tue, 28 Jan 2025 12:26:27 +0100 Subject: [PATCH 23/52] chore(boards2): move `commondao` & `boards2` to "nt" namespace (#3618) Follow up to discussion https://github.com/gnolang/gno/pull/3616#issuecomment-2616338118 --- examples/gno.land/p/demo/commondao/gno.mod | 1 - .../p/{demo => nt}/commondao/commondao.gno | 0 .../p/{demo => nt}/commondao/commondao_test.gno | 0 examples/gno.land/p/nt/commondao/gno.mod | 1 + .../gno.land/p/{demo => nt}/commondao/options.gno | 0 .../gno.land/p/{demo => nt}/commondao/proposal.gno | 0 .../p/{demo => nt}/commondao/proposal_test.gno | 0 .../gno.land/p/{demo => nt}/commondao/record.gno | 0 .../p/{demo => nt}/commondao/record_test.gno | 0 examples/gno.land/r/demo/boards2/gno.mod | 1 - examples/gno.land/r/{demo => nt}/boards2/board.gno | 2 +- .../gno.land/r/{demo => nt}/boards2/board_test.gno | 0 .../gno.land/r/{demo => nt}/boards2/boards.gno | 0 examples/gno.land/r/{demo => nt}/boards2/flag.gno | 0 .../gno.land/r/{demo => nt}/boards2/format.gno | 0 examples/gno.land/r/nt/boards2/gno.mod | 1 + .../gno.land/r/{demo => nt}/boards2/pagination.gno | 0 .../gno.land/r/{demo => nt}/boards2/permission.gno | 2 +- .../r/{demo => nt}/boards2/permission_default.gno | 2 +- .../boards2/permission_default_test.gno | 2 +- .../r/{demo => nt}/boards2/permission_options.gno | 0 examples/gno.land/r/{demo => nt}/boards2/post.gno | 0 .../gno.land/r/{demo => nt}/boards2/post_test.gno | 14 +++++++------- .../gno.land/r/{demo => nt}/boards2/public.gno | 0 .../gno.land/r/{demo => nt}/boards2/render.gno | 0 .../r/{demo => nt}/boards2/z_0_a_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_0_b_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_0_c_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_0_d_filetest.gno | 4 ++-- .../r/{demo => nt}/boards2/z_0_e_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_0_f_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_1_a_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_1_b_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_1_c_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_2_a_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_2_b_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_2_c_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_2_d_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_3_a_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_3_b_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_3_c_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_3_d_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_3_e_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_3_f_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_3_g_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_4_a_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_4_b_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_4_c_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_4_d_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_4_e_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_4_f_filetest.gno | 2 +- .../r/{demo => nt}/boards2/z_4_g_filetest.gno | 2 +- 52 files changed, 41 insertions(+), 41 deletions(-) delete mode 100644 examples/gno.land/p/demo/commondao/gno.mod rename examples/gno.land/p/{demo => nt}/commondao/commondao.gno (100%) rename examples/gno.land/p/{demo => nt}/commondao/commondao_test.gno (100%) create mode 100644 examples/gno.land/p/nt/commondao/gno.mod rename examples/gno.land/p/{demo => nt}/commondao/options.gno (100%) rename examples/gno.land/p/{demo => nt}/commondao/proposal.gno (100%) rename examples/gno.land/p/{demo => nt}/commondao/proposal_test.gno (100%) rename examples/gno.land/p/{demo => nt}/commondao/record.gno (100%) rename examples/gno.land/p/{demo => nt}/commondao/record_test.gno (100%) delete mode 100644 examples/gno.land/r/demo/boards2/gno.mod rename examples/gno.land/r/{demo => nt}/boards2/board.gno (99%) rename examples/gno.land/r/{demo => nt}/boards2/board_test.gno (100%) rename examples/gno.land/r/{demo => nt}/boards2/boards.gno (100%) rename examples/gno.land/r/{demo => nt}/boards2/flag.gno (100%) rename examples/gno.land/r/{demo => nt}/boards2/format.gno (100%) create mode 100644 examples/gno.land/r/nt/boards2/gno.mod rename examples/gno.land/r/{demo => nt}/boards2/pagination.gno (100%) rename examples/gno.land/r/{demo => nt}/boards2/permission.gno (98%) rename examples/gno.land/r/{demo => nt}/boards2/permission_default.gno (99%) rename examples/gno.land/r/{demo => nt}/boards2/permission_default_test.gno (99%) rename examples/gno.land/r/{demo => nt}/boards2/permission_options.gno (100%) rename examples/gno.land/r/{demo => nt}/boards2/post.gno (100%) rename examples/gno.land/r/{demo => nt}/boards2/post_test.gno (96%) rename examples/gno.land/r/{demo => nt}/boards2/public.gno (100%) rename examples/gno.land/r/{demo => nt}/boards2/render.gno (100%) rename examples/gno.land/r/{demo => nt}/boards2/z_0_a_filetest.gno (90%) rename examples/gno.land/r/{demo => nt}/boards2/z_0_b_filetest.gno (89%) rename examples/gno.land/r/{demo => nt}/boards2/z_0_c_filetest.gno (91%) rename examples/gno.land/r/{demo => nt}/boards2/z_0_d_filetest.gno (58%) rename examples/gno.land/r/{demo => nt}/boards2/z_0_e_filetest.gno (91%) rename examples/gno.land/r/{demo => nt}/boards2/z_0_f_filetest.gno (91%) rename examples/gno.land/r/{demo => nt}/boards2/z_1_a_filetest.gno (94%) rename examples/gno.land/r/{demo => nt}/boards2/z_1_b_filetest.gno (96%) rename examples/gno.land/r/{demo => nt}/boards2/z_1_c_filetest.gno (96%) rename examples/gno.land/r/{demo => nt}/boards2/z_2_a_filetest.gno (93%) rename examples/gno.land/r/{demo => nt}/boards2/z_2_b_filetest.gno (95%) rename examples/gno.land/r/{demo => nt}/boards2/z_2_c_filetest.gno (95%) rename examples/gno.land/r/{demo => nt}/boards2/z_2_d_filetest.gno (94%) rename examples/gno.land/r/{demo => nt}/boards2/z_3_a_filetest.gno (94%) rename examples/gno.land/r/{demo => nt}/boards2/z_3_b_filetest.gno (91%) rename examples/gno.land/r/{demo => nt}/boards2/z_3_c_filetest.gno (91%) rename examples/gno.land/r/{demo => nt}/boards2/z_3_d_filetest.gno (91%) rename examples/gno.land/r/{demo => nt}/boards2/z_3_e_filetest.gno (92%) rename examples/gno.land/r/{demo => nt}/boards2/z_3_f_filetest.gno (97%) rename examples/gno.land/r/{demo => nt}/boards2/z_3_g_filetest.gno (97%) rename examples/gno.land/r/{demo => nt}/boards2/z_4_a_filetest.gno (96%) rename examples/gno.land/r/{demo => nt}/boards2/z_4_b_filetest.gno (96%) rename examples/gno.land/r/{demo => nt}/boards2/z_4_c_filetest.gno (96%) rename examples/gno.land/r/{demo => nt}/boards2/z_4_d_filetest.gno (96%) rename examples/gno.land/r/{demo => nt}/boards2/z_4_e_filetest.gno (95%) rename examples/gno.land/r/{demo => nt}/boards2/z_4_f_filetest.gno (95%) rename examples/gno.land/r/{demo => nt}/boards2/z_4_g_filetest.gno (95%) diff --git a/examples/gno.land/p/demo/commondao/gno.mod b/examples/gno.land/p/demo/commondao/gno.mod deleted file mode 100644 index 94b73a1d8e8..00000000000 --- a/examples/gno.land/p/demo/commondao/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/p/demo/commondao diff --git a/examples/gno.land/p/demo/commondao/commondao.gno b/examples/gno.land/p/nt/commondao/commondao.gno similarity index 100% rename from examples/gno.land/p/demo/commondao/commondao.gno rename to examples/gno.land/p/nt/commondao/commondao.gno diff --git a/examples/gno.land/p/demo/commondao/commondao_test.gno b/examples/gno.land/p/nt/commondao/commondao_test.gno similarity index 100% rename from examples/gno.land/p/demo/commondao/commondao_test.gno rename to examples/gno.land/p/nt/commondao/commondao_test.gno diff --git a/examples/gno.land/p/nt/commondao/gno.mod b/examples/gno.land/p/nt/commondao/gno.mod new file mode 100644 index 00000000000..2d995d21ccb --- /dev/null +++ b/examples/gno.land/p/nt/commondao/gno.mod @@ -0,0 +1 @@ +module gno.land/p/nt/commondao diff --git a/examples/gno.land/p/demo/commondao/options.gno b/examples/gno.land/p/nt/commondao/options.gno similarity index 100% rename from examples/gno.land/p/demo/commondao/options.gno rename to examples/gno.land/p/nt/commondao/options.gno diff --git a/examples/gno.land/p/demo/commondao/proposal.gno b/examples/gno.land/p/nt/commondao/proposal.gno similarity index 100% rename from examples/gno.land/p/demo/commondao/proposal.gno rename to examples/gno.land/p/nt/commondao/proposal.gno diff --git a/examples/gno.land/p/demo/commondao/proposal_test.gno b/examples/gno.land/p/nt/commondao/proposal_test.gno similarity index 100% rename from examples/gno.land/p/demo/commondao/proposal_test.gno rename to examples/gno.land/p/nt/commondao/proposal_test.gno diff --git a/examples/gno.land/p/demo/commondao/record.gno b/examples/gno.land/p/nt/commondao/record.gno similarity index 100% rename from examples/gno.land/p/demo/commondao/record.gno rename to examples/gno.land/p/nt/commondao/record.gno diff --git a/examples/gno.land/p/demo/commondao/record_test.gno b/examples/gno.land/p/nt/commondao/record_test.gno similarity index 100% rename from examples/gno.land/p/demo/commondao/record_test.gno rename to examples/gno.land/p/nt/commondao/record_test.gno diff --git a/examples/gno.land/r/demo/boards2/gno.mod b/examples/gno.land/r/demo/boards2/gno.mod deleted file mode 100644 index 4337a775e76..00000000000 --- a/examples/gno.land/r/demo/boards2/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/r/demo/boards2 diff --git a/examples/gno.land/r/demo/boards2/board.gno b/examples/gno.land/r/nt/boards2/board.gno similarity index 99% rename from examples/gno.land/r/demo/boards2/board.gno rename to examples/gno.land/r/nt/boards2/board.gno index 884864359b9..1f7b8a761d1 100644 --- a/examples/gno.land/r/demo/boards2/board.gno +++ b/examples/gno.land/r/nt/boards2/board.gno @@ -8,8 +8,8 @@ import ( "time" "gno.land/p/demo/avl" - "gno.land/p/demo/commondao" "gno.land/p/moul/txlink" + "gno.land/p/nt/commondao" ) type BoardID uint64 diff --git a/examples/gno.land/r/demo/boards2/board_test.gno b/examples/gno.land/r/nt/boards2/board_test.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/board_test.gno rename to examples/gno.land/r/nt/boards2/board_test.gno diff --git a/examples/gno.land/r/demo/boards2/boards.gno b/examples/gno.land/r/nt/boards2/boards.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/boards.gno rename to examples/gno.land/r/nt/boards2/boards.gno diff --git a/examples/gno.land/r/demo/boards2/flag.gno b/examples/gno.land/r/nt/boards2/flag.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/flag.gno rename to examples/gno.land/r/nt/boards2/flag.gno diff --git a/examples/gno.land/r/demo/boards2/format.gno b/examples/gno.land/r/nt/boards2/format.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/format.gno rename to examples/gno.land/r/nt/boards2/format.gno diff --git a/examples/gno.land/r/nt/boards2/gno.mod b/examples/gno.land/r/nt/boards2/gno.mod new file mode 100644 index 00000000000..08d15a3e133 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/gno.mod @@ -0,0 +1 @@ +module gno.land/r/nt/boards2 diff --git a/examples/gno.land/r/demo/boards2/pagination.gno b/examples/gno.land/r/nt/boards2/pagination.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/pagination.gno rename to examples/gno.land/r/nt/boards2/pagination.gno diff --git a/examples/gno.land/r/demo/boards2/permission.gno b/examples/gno.land/r/nt/boards2/permission.gno similarity index 98% rename from examples/gno.land/r/demo/boards2/permission.gno rename to examples/gno.land/r/nt/boards2/permission.gno index 7c83416a37e..1c74c231e1f 100644 --- a/examples/gno.land/r/demo/boards2/permission.gno +++ b/examples/gno.land/r/nt/boards2/permission.gno @@ -3,7 +3,7 @@ package boards2 import ( "std" - "gno.land/p/demo/commondao" + "gno.land/p/nt/commondao" ) const ( diff --git a/examples/gno.land/r/demo/boards2/permission_default.gno b/examples/gno.land/r/nt/boards2/permission_default.gno similarity index 99% rename from examples/gno.land/r/demo/boards2/permission_default.gno rename to examples/gno.land/r/nt/boards2/permission_default.gno index 83eb027321c..d5fb7963f73 100644 --- a/examples/gno.land/r/demo/boards2/permission_default.gno +++ b/examples/gno.land/r/nt/boards2/permission_default.gno @@ -5,7 +5,7 @@ import ( "std" "gno.land/p/demo/avl" - "gno.land/p/demo/commondao" + "gno.land/p/nt/commondao" "gno.land/r/demo/users" ) diff --git a/examples/gno.land/r/demo/boards2/permission_default_test.gno b/examples/gno.land/r/nt/boards2/permission_default_test.gno similarity index 99% rename from examples/gno.land/r/demo/boards2/permission_default_test.gno rename to examples/gno.land/r/nt/boards2/permission_default_test.gno index 76980bd552c..3e95e8c207c 100644 --- a/examples/gno.land/r/demo/boards2/permission_default_test.gno +++ b/examples/gno.land/r/nt/boards2/permission_default_test.gno @@ -4,9 +4,9 @@ import ( "std" "testing" - "gno.land/p/demo/commondao" "gno.land/p/demo/uassert" "gno.land/p/demo/urequire" + "gno.land/p/nt/commondao" ) var _ Permissions = (*DefaultPermissions)(nil) diff --git a/examples/gno.land/r/demo/boards2/permission_options.gno b/examples/gno.land/r/nt/boards2/permission_options.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/permission_options.gno rename to examples/gno.land/r/nt/boards2/permission_options.gno diff --git a/examples/gno.land/r/demo/boards2/post.gno b/examples/gno.land/r/nt/boards2/post.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/post.gno rename to examples/gno.land/r/nt/boards2/post.gno diff --git a/examples/gno.land/r/demo/boards2/post_test.gno b/examples/gno.land/r/nt/boards2/post_test.gno similarity index 96% rename from examples/gno.land/r/demo/boards2/post_test.gno rename to examples/gno.land/r/nt/boards2/post_test.gno index 9d7367fc6ca..cd69025c593 100644 --- a/examples/gno.land/r/demo/boards2/post_test.gno +++ b/examples/gno.land/r/nt/boards2/post_test.gno @@ -111,23 +111,23 @@ func TestNewThread(t *testing.T) { boardName := "test123" board := newBoard(boardID, boardName, creator) url := ufmt.Sprintf( - "/r/demo/boards2:%s/%d", + "/r/nt/boards2:%s/%d", boardName, uint(threadID), ) replyURL := ufmt.Sprintf( - "/r/demo/boards2$help&func=CreateReply&bid=%d&threadID=%d&replyID=%d", + "/r/nt/boards2$help&func=CreateReply&bid=%d&threadID=%d&replyID=%d", uint(boardID), uint(threadID), uint(threadID), ) repostURL := ufmt.Sprintf( - "/r/demo/boards2$help&func=CreateRepost&bid=%d&threadID=%d", + "/r/nt/boards2$help&func=CreateRepost&bid=%d&threadID=%d", uint(boardID), uint(threadID), ) deleteURL := ufmt.Sprintf( - "/r/demo/boards2$help&func=DeleteThread&bid=%d&threadID=%d", + "/r/nt/boards2$help&func=DeleteThread&bid=%d&threadID=%d", uint(boardID), uint(threadID), ) @@ -275,19 +275,19 @@ func TestNewReply(t *testing.T) { boardName := "test123" board := newBoard(boardID, boardName, creator) url := ufmt.Sprintf( - "/r/demo/boards2:%s/%d/%d", + "/r/nt/boards2:%s/%d/%d", boardName, uint(threadID), uint(replyID), ) replyURL := ufmt.Sprintf( - "/r/demo/boards2$help&func=CreateReply&bid=%d&threadID=%d&replyID=%d", + "/r/nt/boards2$help&func=CreateReply&bid=%d&threadID=%d&replyID=%d", uint(boardID), uint(threadID), uint(replyID), ) deleteURL := ufmt.Sprintf( - "/r/demo/boards2$help&func=DeleteReply&bid=%d&threadID=%d&replyID=%d", + "/r/nt/boards2$help&func=DeleteReply&bid=%d&threadID=%d&replyID=%d", uint(boardID), uint(threadID), uint(replyID), diff --git a/examples/gno.land/r/demo/boards2/public.gno b/examples/gno.land/r/nt/boards2/public.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/public.gno rename to examples/gno.land/r/nt/boards2/public.gno diff --git a/examples/gno.land/r/demo/boards2/render.gno b/examples/gno.land/r/nt/boards2/render.gno similarity index 100% rename from examples/gno.land/r/demo/boards2/render.gno rename to examples/gno.land/r/nt/boards2/render.gno diff --git a/examples/gno.land/r/demo/boards2/z_0_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_0_a_filetest.gno similarity index 90% rename from examples/gno.land/r/demo/boards2/z_0_a_filetest.gno rename to examples/gno.land/r/nt/boards2/z_0_a_filetest.gno index 0079c7fb404..39fcaf264d3 100644 --- a/examples/gno.land/r/demo/boards2/z_0_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_0_a_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/demo/boards2/z_0_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_0_b_filetest.gno similarity index 89% rename from examples/gno.land/r/demo/boards2/z_0_b_filetest.gno rename to examples/gno.land/r/nt/boards2/z_0_b_filetest.gno index 08cbcf8ff43..689c76f81f1 100644 --- a/examples/gno.land/r/demo/boards2/z_0_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_0_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/demo/boards2/z_0_c_filetest.gno b/examples/gno.land/r/nt/boards2/z_0_c_filetest.gno similarity index 91% rename from examples/gno.land/r/demo/boards2/z_0_c_filetest.gno rename to examples/gno.land/r/nt/boards2/z_0_c_filetest.gno index 087b56fac2b..ce6672be9e3 100644 --- a/examples/gno.land/r/demo/boards2/z_0_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_0_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const ( diff --git a/examples/gno.land/r/demo/boards2/z_0_d_filetest.gno b/examples/gno.land/r/nt/boards2/z_0_d_filetest.gno similarity index 58% rename from examples/gno.land/r/demo/boards2/z_0_d_filetest.gno rename to examples/gno.land/r/nt/boards2/z_0_d_filetest.gno index 176e3ae881c..27d8ae6e0dd 100644 --- a/examples/gno.land/r/demo/boards2/z_0_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_0_d_filetest.gno @@ -1,7 +1,7 @@ -// PKGPATH: gno.land/r/demo/boards2_test +// PKGPATH: gno.land/r/nt/boards2_test package boards2_test -import "gno.land/r/demo/boards2" +import "gno.land/r/nt/boards2" func main() { boards2.CreateBoard("foo123") diff --git a/examples/gno.land/r/demo/boards2/z_0_e_filetest.gno b/examples/gno.land/r/nt/boards2/z_0_e_filetest.gno similarity index 91% rename from examples/gno.land/r/demo/boards2/z_0_e_filetest.gno rename to examples/gno.land/r/nt/boards2/z_0_e_filetest.gno index 33f45878b65..face81f51cf 100644 --- a/examples/gno.land/r/demo/boards2/z_0_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_0_e_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/demo/boards2/z_0_f_filetest.gno b/examples/gno.land/r/nt/boards2/z_0_f_filetest.gno similarity index 91% rename from examples/gno.land/r/demo/boards2/z_0_f_filetest.gno rename to examples/gno.land/r/nt/boards2/z_0_f_filetest.gno index f17f73bac4c..9f7871be625 100644 --- a/examples/gno.land/r/demo/boards2/z_0_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_0_f_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/demo/boards2/z_1_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_1_a_filetest.gno similarity index 94% rename from examples/gno.land/r/demo/boards2/z_1_a_filetest.gno rename to examples/gno.land/r/nt/boards2/z_1_a_filetest.gno index 3218334419d..67b677cf667 100644 --- a/examples/gno.land/r/demo/boards2/z_1_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_1_a_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const ( diff --git a/examples/gno.land/r/demo/boards2/z_1_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_1_b_filetest.gno similarity index 96% rename from examples/gno.land/r/demo/boards2/z_1_b_filetest.gno rename to examples/gno.land/r/nt/boards2/z_1_b_filetest.gno index 01bdac4cf75..187f33f47b7 100644 --- a/examples/gno.land/r/demo/boards2/z_1_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_1_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const ( diff --git a/examples/gno.land/r/demo/boards2/z_1_c_filetest.gno b/examples/gno.land/r/nt/boards2/z_1_c_filetest.gno similarity index 96% rename from examples/gno.land/r/demo/boards2/z_1_c_filetest.gno rename to examples/gno.land/r/nt/boards2/z_1_c_filetest.gno index 3151bf519de..01e140af9dd 100644 --- a/examples/gno.land/r/demo/boards2/z_1_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_1_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const ( diff --git a/examples/gno.land/r/demo/boards2/z_2_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_2_a_filetest.gno similarity index 93% rename from examples/gno.land/r/demo/boards2/z_2_a_filetest.gno rename to examples/gno.land/r/nt/boards2/z_2_a_filetest.gno index 228b4c35dfd..c4fd1da0c87 100644 --- a/examples/gno.land/r/demo/boards2/z_2_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_2_a_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/demo/boards2/z_2_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_2_b_filetest.gno similarity index 95% rename from examples/gno.land/r/demo/boards2/z_2_b_filetest.gno rename to examples/gno.land/r/nt/boards2/z_2_b_filetest.gno index 908ecb03e33..f215c0c84eb 100644 --- a/examples/gno.land/r/demo/boards2/z_2_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_2_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/demo/boards2/z_2_c_filetest.gno b/examples/gno.land/r/nt/boards2/z_2_c_filetest.gno similarity index 95% rename from examples/gno.land/r/demo/boards2/z_2_c_filetest.gno rename to examples/gno.land/r/nt/boards2/z_2_c_filetest.gno index 911e44ffe62..98ffbc7f624 100644 --- a/examples/gno.land/r/demo/boards2/z_2_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_2_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/demo/boards2/z_2_d_filetest.gno b/examples/gno.land/r/nt/boards2/z_2_d_filetest.gno similarity index 94% rename from examples/gno.land/r/demo/boards2/z_2_d_filetest.gno rename to examples/gno.land/r/nt/boards2/z_2_d_filetest.gno index 9f140ba25b5..9a95fd869b7 100644 --- a/examples/gno.land/r/demo/boards2/z_2_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_2_d_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/demo/boards2/z_3_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_3_a_filetest.gno similarity index 94% rename from examples/gno.land/r/demo/boards2/z_3_a_filetest.gno rename to examples/gno.land/r/nt/boards2/z_3_a_filetest.gno index 48af8039a5a..88812194645 100644 --- a/examples/gno.land/r/demo/boards2/z_3_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_3_a_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const ( diff --git a/examples/gno.land/r/demo/boards2/z_3_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_3_b_filetest.gno similarity index 91% rename from examples/gno.land/r/demo/boards2/z_3_b_filetest.gno rename to examples/gno.land/r/nt/boards2/z_3_b_filetest.gno index 9dbe90c60b6..851fbf303af 100644 --- a/examples/gno.land/r/demo/boards2/z_3_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_3_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const ( diff --git a/examples/gno.land/r/demo/boards2/z_3_c_filetest.gno b/examples/gno.land/r/nt/boards2/z_3_c_filetest.gno similarity index 91% rename from examples/gno.land/r/demo/boards2/z_3_c_filetest.gno rename to examples/gno.land/r/nt/boards2/z_3_c_filetest.gno index 0489cb63fa2..05b27fd3719 100644 --- a/examples/gno.land/r/demo/boards2/z_3_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_3_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const ( diff --git a/examples/gno.land/r/demo/boards2/z_3_d_filetest.gno b/examples/gno.land/r/nt/boards2/z_3_d_filetest.gno similarity index 91% rename from examples/gno.land/r/demo/boards2/z_3_d_filetest.gno rename to examples/gno.land/r/nt/boards2/z_3_d_filetest.gno index 343abb76d3c..c2d60d6b8ca 100644 --- a/examples/gno.land/r/demo/boards2/z_3_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_3_d_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/demo/boards2/z_3_e_filetest.gno b/examples/gno.land/r/nt/boards2/z_3_e_filetest.gno similarity index 92% rename from examples/gno.land/r/demo/boards2/z_3_e_filetest.gno rename to examples/gno.land/r/nt/boards2/z_3_e_filetest.gno index 6911d843479..ad76d398789 100644 --- a/examples/gno.land/r/demo/boards2/z_3_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_3_e_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const ( diff --git a/examples/gno.land/r/demo/boards2/z_3_f_filetest.gno b/examples/gno.land/r/nt/boards2/z_3_f_filetest.gno similarity index 97% rename from examples/gno.land/r/demo/boards2/z_3_f_filetest.gno rename to examples/gno.land/r/nt/boards2/z_3_f_filetest.gno index 4d243479430..f910c630d37 100644 --- a/examples/gno.land/r/demo/boards2/z_3_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_3_f_filetest.gno @@ -5,8 +5,8 @@ package main import ( "std" - "gno.land/r/demo/boards2" "gno.land/r/demo/users" + "gno.land/r/nt/boards2" ) const ( diff --git a/examples/gno.land/r/demo/boards2/z_3_g_filetest.gno b/examples/gno.land/r/nt/boards2/z_3_g_filetest.gno similarity index 97% rename from examples/gno.land/r/demo/boards2/z_3_g_filetest.gno rename to examples/gno.land/r/nt/boards2/z_3_g_filetest.gno index 0327ed7b07c..f4ee3c34741 100644 --- a/examples/gno.land/r/demo/boards2/z_3_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_3_g_filetest.gno @@ -5,8 +5,8 @@ package main import ( "std" - "gno.land/r/demo/boards2" "gno.land/r/demo/users" + "gno.land/r/nt/boards2" ) const ( diff --git a/examples/gno.land/r/demo/boards2/z_4_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_4_a_filetest.gno similarity index 96% rename from examples/gno.land/r/demo/boards2/z_4_a_filetest.gno rename to examples/gno.land/r/nt/boards2/z_4_a_filetest.gno index e0a1a65a92e..4eba0caa6e9 100644 --- a/examples/gno.land/r/demo/boards2/z_4_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_4_a_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const ( diff --git a/examples/gno.land/r/demo/boards2/z_4_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_4_b_filetest.gno similarity index 96% rename from examples/gno.land/r/demo/boards2/z_4_b_filetest.gno rename to examples/gno.land/r/nt/boards2/z_4_b_filetest.gno index a226e19d60a..70760b217f3 100644 --- a/examples/gno.land/r/demo/boards2/z_4_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_4_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const ( diff --git a/examples/gno.land/r/demo/boards2/z_4_c_filetest.gno b/examples/gno.land/r/nt/boards2/z_4_c_filetest.gno similarity index 96% rename from examples/gno.land/r/demo/boards2/z_4_c_filetest.gno rename to examples/gno.land/r/nt/boards2/z_4_c_filetest.gno index 514233d8e06..6c8573a494b 100644 --- a/examples/gno.land/r/demo/boards2/z_4_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_4_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const ( diff --git a/examples/gno.land/r/demo/boards2/z_4_d_filetest.gno b/examples/gno.land/r/nt/boards2/z_4_d_filetest.gno similarity index 96% rename from examples/gno.land/r/demo/boards2/z_4_d_filetest.gno rename to examples/gno.land/r/nt/boards2/z_4_d_filetest.gno index d589fe9e694..713e14cee1e 100644 --- a/examples/gno.land/r/demo/boards2/z_4_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_4_d_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const ( diff --git a/examples/gno.land/r/demo/boards2/z_4_e_filetest.gno b/examples/gno.land/r/nt/boards2/z_4_e_filetest.gno similarity index 95% rename from examples/gno.land/r/demo/boards2/z_4_e_filetest.gno rename to examples/gno.land/r/nt/boards2/z_4_e_filetest.gno index 449cc6c06a1..dcce443582a 100644 --- a/examples/gno.land/r/demo/boards2/z_4_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_4_e_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const ( diff --git a/examples/gno.land/r/demo/boards2/z_4_f_filetest.gno b/examples/gno.land/r/nt/boards2/z_4_f_filetest.gno similarity index 95% rename from examples/gno.land/r/demo/boards2/z_4_f_filetest.gno rename to examples/gno.land/r/nt/boards2/z_4_f_filetest.gno index c6622b88c8a..3ca8769a025 100644 --- a/examples/gno.land/r/demo/boards2/z_4_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_4_f_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const ( diff --git a/examples/gno.land/r/demo/boards2/z_4_g_filetest.gno b/examples/gno.land/r/nt/boards2/z_4_g_filetest.gno similarity index 95% rename from examples/gno.land/r/demo/boards2/z_4_g_filetest.gno rename to examples/gno.land/r/nt/boards2/z_4_g_filetest.gno index 24fec3fb9e3..6dd6acab2f7 100644 --- a/examples/gno.land/r/demo/boards2/z_4_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_4_g_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/demo/boards2" + "gno.land/r/nt/boards2" ) const ( From 69960ec6a7b60b07df2f07bcf5faafbf5a2bbc88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Tue, 28 Jan 2025 17:54:23 +0100 Subject: [PATCH 24/52] test(boards2): add missing filetests for board creation (#3622) Add a missing filetests for `CreateBoard()` function. Related to #3623 This covers all tests for the function: - Creation success - Fail w/ empty board name - Fail w/ existing board name - Fail w/ non user call - Fail w/ an address as board name - Fail because name is registered and not owned in `users` realm - Fail w/ short name - Creation success with owned name registered in `users` realm - Fail for non realm DAO member --- .../gno.land/r/nt/boards2/z_0_g_filetest.gno | 20 +++++++++++ .../gno.land/r/nt/boards2/z_0_h_filetest.gno | 34 +++++++++++++++++++ .../gno.land/r/nt/boards2/z_0_i_filetest.gno | 20 +++++++++++ 3 files changed, 74 insertions(+) create mode 100644 examples/gno.land/r/nt/boards2/z_0_g_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_0_h_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_0_i_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/z_0_g_filetest.gno b/examples/gno.land/r/nt/boards2/z_0_g_filetest.gno new file mode 100644 index 00000000000..c835a5877ac --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_0_g_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.CreateBoard("short") +} + +// Error: +// the minimum allowed board name length is 6 characters diff --git a/examples/gno.land/r/nt/boards2/z_0_h_filetest.gno b/examples/gno.land/r/nt/boards2/z_0_h_filetest.gno new file mode 100644 index 00000000000..89da0a08dac --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_0_h_filetest.gno @@ -0,0 +1,34 @@ +package main + +// SEND: 200000000ugnot + +import ( + "std" + + "gno.land/r/demo/users" + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + name = "test123" +) + +func init() { + std.TestSetOrigCaller(owner) + + // Test1 is the boards owner and its address has a user already registered + // so a new member must register a user with the new board name. + boards2.InviteMember(0, member, boards2.RoleOwner) // Operate on realm DAO members instead of individual boards + std.TestSetOrigCaller(member) + users.Register("", name, "") +} + +func main() { + bid := boards2.CreateBoard(name) + println("ID =", bid) +} + +// Output: +// ID = 1 diff --git a/examples/gno.land/r/nt/boards2/z_0_i_filetest.gno b/examples/gno.land/r/nt/boards2/z_0_i_filetest.gno new file mode 100644 index 00000000000..c54a7e8c1d0 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_0_i_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.CreateBoard("test123") +} + +// Error: +// unauthorized From 33fb8891a00ac35eae59e4d6b84703724c300ae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Tue, 28 Jan 2025 17:54:46 +0100 Subject: [PATCH 25/52] test(boards2): add missing filetests for board rename (#3625) Add a missing filetests for `RenameBoard()` function. Related to #3623. This covers all tests for the function: - Rename success by default board owner - Fail w/ empty board name - Fail w/ existing board name - Fail when renaming unexisting board - Fail w/ an address as board name - Rename success by another board owner - Fail because name is registered and not owned in `users` realm - Fail w/ short name - Rename success with owned name registered in `users` realm - Fail for non board DAO member --- .../gno.land/r/nt/boards2/z_3_a_filetest.gno | 21 +++++---- .../gno.land/r/nt/boards2/z_3_f_filetest.gno | 20 ++++----- .../gno.land/r/nt/boards2/z_3_g_filetest.gno | 15 +++---- .../gno.land/r/nt/boards2/z_3_h_filetest.gno | 24 +++++++++++ .../gno.land/r/nt/boards2/z_3_i_filetest.gno | 43 +++++++++++++++++++ .../gno.land/r/nt/boards2/z_3_j_filetest.gno | 27 ++++++++++++ 6 files changed, 118 insertions(+), 32 deletions(-) create mode 100644 examples/gno.land/r/nt/boards2/z_3_h_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_3_i_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_3_j_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/z_3_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_3_a_filetest.gno index 88812194645..ecfd3e33e06 100644 --- a/examples/gno.land/r/nt/boards2/z_3_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_3_a_filetest.gno @@ -7,26 +7,25 @@ import ( ) const ( - owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 - name = "foo123" + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + name = "foo123" + newName = "bar123" ) +var bid boards2.BoardID // Operate on board DAO + func init() { std.TestSetOrigCaller(owner) - boards2.CreateBoard(name) + bid = boards2.CreateBoard(name) } func main() { - newName := "bar123" - _, exists := boards2.GetBoardIDFromName(newName) - println("Exists =", exists) - boards2.RenameBoard(name, newName) - bid, _ := boards2.GetBoardIDFromName(newName) - println("ID =", bid) + // Ensure board is renamed by the default board owner + bid2, _ := boards2.GetBoardIDFromName(newName) + println("IDs match =", bid == bid2) } // Output: -// Exists = false -// ID = 1 +// IDs match = true diff --git a/examples/gno.land/r/nt/boards2/z_3_f_filetest.gno b/examples/gno.land/r/nt/boards2/z_3_f_filetest.gno index f910c630d37..b4f9d07a04b 100644 --- a/examples/gno.land/r/nt/boards2/z_3_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_3_f_filetest.gno @@ -12,33 +12,31 @@ import ( const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 - bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards name = "foo123" newName = "barbaz" ) +var bid boards2.BoardID // Operate on board DAO + func init() { std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard(name) + boards2.InviteMember(bid, member, boards2.RoleOwner) + // Test1 is the boards owner and its address has a user already registered // so a new member must register a user with the new board name. - boards2.InviteMember(bid, member, boards2.RoleOwner) std.TestSetOrigCaller(member) users.Register("", newName, "") - - boards2.CreateBoard(name) } func main() { - _, exists := boards2.GetBoardIDFromName(newName) - println("Exists =", exists) - boards2.RenameBoard(name, newName) - bid, _ := boards2.GetBoardIDFromName(newName) - println("ID =", bid) + // Ensure board is renamed by another board owner + bid2, _ := boards2.GetBoardIDFromName(newName) + println("IDs match =", bid == bid2) } // Output: -// Exists = false -// ID = 1 +// IDs match = true diff --git a/examples/gno.land/r/nt/boards2/z_3_g_filetest.gno b/examples/gno.land/r/nt/boards2/z_3_g_filetest.gno index f4ee3c34741..0c127148b81 100644 --- a/examples/gno.land/r/nt/boards2/z_3_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_3_g_filetest.gno @@ -13,35 +13,30 @@ const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 member2 = std.Address("g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5") - bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards name = "foo123" newName = "barbaz" ) +var bid boards2.BoardID // Operate on board DAO + func init() { std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard(name) + boards2.InviteMember(bid, member, boards2.RoleOwner) + // Test1 is the boards owner and its address has a user already registered // so a new member must register a user with the new board name. - boards2.InviteMember(bid, member, boards2.RoleOwner) std.TestSetOrigCaller(member) users.Register("", newName, "") // Invite a new member that doesn't own the user that matches the new board name boards2.InviteMember(bid, member2, boards2.RoleOwner) std.TestSetOrigCaller(member2) - - boards2.CreateBoard(name) } func main() { - _, exists := boards2.GetBoardIDFromName(newName) - println("Exists =", exists) - boards2.RenameBoard(name, newName) - - bid, _ := boards2.GetBoardIDFromName(newName) - println("ID =", bid) } // Error: diff --git a/examples/gno.land/r/nt/boards2/z_3_h_filetest.gno b/examples/gno.land/r/nt/boards2/z_3_h_filetest.gno new file mode 100644 index 00000000000..36d66e682ca --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_3_h_filetest.gno @@ -0,0 +1,24 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + name = "foo123" +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.CreateBoard(name) +} + +func main() { + boards2.RenameBoard(name, "short") +} + +// Error: +// the minimum allowed board name length is 6 characters diff --git a/examples/gno.land/r/nt/boards2/z_3_i_filetest.gno b/examples/gno.land/r/nt/boards2/z_3_i_filetest.gno new file mode 100644 index 00000000000..0c127148b81 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_3_i_filetest.gno @@ -0,0 +1,43 @@ +package main + +// SEND: 200000000ugnot + +import ( + "std" + + "gno.land/r/demo/users" + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + member2 = std.Address("g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5") + name = "foo123" + newName = "barbaz" +) + +var bid boards2.BoardID // Operate on board DAO + +func init() { + std.TestSetOrigCaller(owner) + + bid = boards2.CreateBoard(name) + boards2.InviteMember(bid, member, boards2.RoleOwner) + + // Test1 is the boards owner and its address has a user already registered + // so a new member must register a user with the new board name. + std.TestSetOrigCaller(member) + users.Register("", newName, "") + + // Invite a new member that doesn't own the user that matches the new board name + boards2.InviteMember(bid, member2, boards2.RoleOwner) + std.TestSetOrigCaller(member2) +} + +func main() { + boards2.RenameBoard(name, newName) +} + +// Error: +// board name is a user name registered to a different user diff --git a/examples/gno.land/r/nt/boards2/z_3_j_filetest.gno b/examples/gno.land/r/nt/boards2/z_3_j_filetest.gno new file mode 100644 index 00000000000..309b029e593 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_3_j_filetest.gno @@ -0,0 +1,27 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + name = "foo123" +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.CreateBoard(name) + + std.TestSetOrigCaller(user) +} + +func main() { + boards2.RenameBoard(name, "barbaz") +} + +// Error: +// unauthorized From 90a3c4ef15baa43fa2a59999b2d15b9e19532c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Thu, 30 Jan 2025 09:37:57 +0100 Subject: [PATCH 26/52] test(boards2): add missing filetests for member invite (#3632) Add missing filetests for `InviteMember()` function. Related to #3623 This covers all tests for the function: - Invite to realm success - Fail when admin invites a new realm owner - Success when admin invites a new realm admin - Fail w/ invalid invite role - Invite to board success - Fail w/ existing user - Fail for non member (unauthorized) --- .../gno.land/r/nt/boards2/z_1_a_filetest.gno | 13 +++++--- .../gno.land/r/nt/boards2/z_1_b_filetest.gno | 6 ++-- .../gno.land/r/nt/boards2/z_1_c_filetest.gno | 15 +++++---- .../gno.land/r/nt/boards2/z_1_d_filetest.gno | 23 +++++++++++++ .../gno.land/r/nt/boards2/z_1_e_filetest.gno | 32 +++++++++++++++++++ .../gno.land/r/nt/boards2/z_1_f_filetest.gno | 26 +++++++++++++++ .../gno.land/r/nt/boards2/z_1_g_filetest.gno | 20 ++++++++++++ 7 files changed, 121 insertions(+), 14 deletions(-) create mode 100644 examples/gno.land/r/nt/boards2/z_1_d_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_1_e_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_1_f_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_1_g_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/z_1_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_1_a_filetest.gno index 67b677cf667..d8ab2e8ae84 100644 --- a/examples/gno.land/r/nt/boards2/z_1_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_1_a_filetest.gno @@ -8,8 +8,9 @@ import ( const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 - admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 - bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm DAO instead of individual boards + role = boards2.RoleOwner ) func init() { @@ -17,9 +18,11 @@ func init() { } func main() { - boards2.InviteMember(bid, admin, boards2.RoleAdmin) - println("ok") + boards2.InviteMember(bid, user, role) + + // Check that user is invited + println(boards2.HasMemberRole(bid, user, role)) } // Output: -// ok +// true diff --git a/examples/gno.land/r/nt/boards2/z_1_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_1_b_filetest.gno index 187f33f47b7..ba41b803f75 100644 --- a/examples/gno.land/r/nt/boards2/z_1_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_1_b_filetest.gno @@ -10,15 +10,15 @@ const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 user = std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") - bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards + bid = boards2.BoardID(0) // Operate on realm DAO instead of individual boards ) func init() { - // Add an admin user + // Add an admin member std.TestSetOrigCaller(owner) boards2.InviteMember(bid, admin, boards2.RoleAdmin) - // Next call will be done by the admin user + // Next call will be done by the admin member std.TestSetOrigCaller(admin) } diff --git a/examples/gno.land/r/nt/boards2/z_1_c_filetest.gno b/examples/gno.land/r/nt/boards2/z_1_c_filetest.gno index 01e140af9dd..6beaf0774a6 100644 --- a/examples/gno.land/r/nt/boards2/z_1_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_1_c_filetest.gno @@ -10,22 +10,25 @@ const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 user = std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") - bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards + bid = boards2.BoardID(0) // Operate on realm DAO instead of individual boards + role = boards2.RoleAdmin ) func init() { - // Add an admin user + // Add an admin member std.TestSetOrigCaller(owner) boards2.InviteMember(bid, admin, boards2.RoleAdmin) - // Next call will be done by the admin user + // Next call will be done by the admin member std.TestSetOrigCaller(admin) } func main() { - boards2.InviteMember(bid, user, boards2.RoleAdmin) - println("ok") + boards2.InviteMember(bid, user, role) + + // Check that user is invited + println(boards2.HasMemberRole(bid, user, role)) } // Output: -// ok +// true diff --git a/examples/gno.land/r/nt/boards2/z_1_d_filetest.gno b/examples/gno.land/r/nt/boards2/z_1_d_filetest.gno new file mode 100644 index 00000000000..f2a30517286 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_1_d_filetest.gno @@ -0,0 +1,23 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 +) + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.InviteMember(0, user, boards2.Role("foobar")) // Operate on realm DAO instead of individual boards +} + +// Error: +// invalid role: foobar diff --git a/examples/gno.land/r/nt/boards2/z_1_e_filetest.gno b/examples/gno.land/r/nt/boards2/z_1_e_filetest.gno new file mode 100644 index 00000000000..01e3caa1e87 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_1_e_filetest.gno @@ -0,0 +1,32 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + role = boards2.RoleOwner +) + +var bid boards2.BoardID + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("foo123") // Operate on board DAO members +} + +func main() { + boards2.InviteMember(bid, user, role) + + // Check that user is invited + println(boards2.HasMemberRole(0, user, role)) // Operate on realm DAO + println(boards2.HasMemberRole(bid, user, role)) +} + +// Output: +// false +// true diff --git a/examples/gno.land/r/nt/boards2/z_1_f_filetest.gno b/examples/gno.land/r/nt/boards2/z_1_f_filetest.gno new file mode 100644 index 00000000000..7e56d5ad64f --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_1_f_filetest.gno @@ -0,0 +1,26 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm DAO instead of individual boards + role = boards2.RoleOwner +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.InviteMember(bid, user, role) +} + +func main() { + boards2.InviteMember(bid, user, role) +} + +// Error: +// user already exists diff --git a/examples/gno.land/r/nt/boards2/z_1_g_filetest.gno b/examples/gno.land/r/nt/boards2/z_1_g_filetest.gno new file mode 100644 index 00000000000..e4fbd6f4d76 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_1_g_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.InviteMember(0, "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", boards2.RoleGuest) +} + +// Error: +// unauthorized From dc173c2ec184c8862c4ba5f3c698432843f86568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Thu, 30 Jan 2025 09:38:23 +0100 Subject: [PATCH 27/52] test(boards2): add missing filetests for member removal (#3633) Add missing filetests for `RemoveMember()` function. Related to #3623 This covers all tests for the function: - Success removing a realm member - Fail for non member (unauthorized) - Fail w/ unexisting user --- examples/gno.land/r/nt/boards2/public.gno | 5 ++++ .../gno.land/r/nt/boards2/z_5_a_filetest.gno | 28 +++++++++++++++++++ .../gno.land/r/nt/boards2/z_5_b_filetest.gno | 20 +++++++++++++ .../gno.land/r/nt/boards2/z_5_c_filetest.gno | 20 +++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 examples/gno.land/r/nt/boards2/z_5_a_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_5_b_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_5_c_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/public.gno b/examples/gno.land/r/nt/boards2/public.gno index 47f2ac9b294..7a7563a5145 100644 --- a/examples/gno.land/r/nt/boards2/public.gno +++ b/examples/gno.land/r/nt/boards2/public.gno @@ -227,6 +227,11 @@ func RemoveMember(bid BoardID, user std.Address) { }) } +func IsMember(bid BoardID, member std.Address) bool { + perms := mustGetPermissions(bid) + return perms.HasUser(member) +} + func HasMemberRole(bid BoardID, member std.Address, role Role) bool { perms := mustGetPermissions(bid) return perms.HasRole(member, role) diff --git a/examples/gno.land/r/nt/boards2/z_5_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_5_a_filetest.gno new file mode 100644 index 00000000000..77e2ca2dff3 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_5_a_filetest.gno @@ -0,0 +1,28 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm DAO instead of individual boards +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.InviteMember(bid, user, boards2.RoleGuest) +} + +func main() { + boards2.RemoveMember(bid, user) + + // Check that user is not a member + println(boards2.IsMember(bid, user)) +} + +// Output: +// false diff --git a/examples/gno.land/r/nt/boards2/z_5_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_5_b_filetest.gno new file mode 100644 index 00000000000..64024f1c2f6 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_5_b_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.RemoveMember(0, "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") // Operate on realm DAO instead of individual boards +} + +// Error: +// unauthorized diff --git a/examples/gno.land/r/nt/boards2/z_5_c_filetest.gno b/examples/gno.land/r/nt/boards2/z_5_c_filetest.gno new file mode 100644 index 00000000000..d75506ec1f5 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_5_c_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.RemoveMember(0, "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // Operate on realm DAO instead of individual boards +} + +// Error: +// member not found From 63007abda3970b49569589f3c87d283705c484db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Thu, 30 Jan 2025 09:38:45 +0100 Subject: [PATCH 28/52] test(boards2): add missing filetests for member role change (#3636) Add missing filetests for `ChangeMemberRole()` function. Related to #3623 This covers all tests for the function: - Successful role change for realm member - Successful role change for board member - Fail for an admin removing an owner role - Fail for an admin changing a role to owner - Success changing a role to owner by another owner - Fail when role doesn't exist - Fail when user is not found - Fail for non member (unauthorized) --- .../gno.land/r/nt/boards2/z_4_a_filetest.gno | 21 ++++++----------- .../gno.land/r/nt/boards2/z_4_b_filetest.gno | 23 +++++++------------ .../gno.land/r/nt/boards2/z_4_c_filetest.gno | 2 +- .../gno.land/r/nt/boards2/z_4_d_filetest.gno | 2 +- .../gno.land/r/nt/boards2/z_4_e_filetest.gno | 17 ++++++++------ .../gno.land/r/nt/boards2/z_4_h_filetest.gno | 20 ++++++++++++++++ 6 files changed, 47 insertions(+), 38 deletions(-) create mode 100644 examples/gno.land/r/nt/boards2/z_4_h_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/z_4_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_4_a_filetest.gno index 4eba0caa6e9..5cc97b5da0b 100644 --- a/examples/gno.land/r/nt/boards2/z_4_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_4_a_filetest.gno @@ -7,11 +7,10 @@ import ( ) const ( - owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 - member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 - initialRole = boards2.RoleGuest - newRole = boards2.RoleAdmin - bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + newRole = boards2.RoleAdmin + bid = boards2.BoardID(0) // Operate on realm DAO instead of individual boards ) func init() { @@ -20,17 +19,11 @@ func init() { } func main() { - if boards2.HasMemberRole(bid, member, initialRole) { - println("ok") - } - boards2.ChangeMemberRole(bid, member, newRole) - if boards2.HasMemberRole(bid, member, newRole) { - println("ok") - } + // Ensure that new role has been changed + println(boards2.HasMemberRole(bid, member, newRole)) } // Output: -// ok -// ok +// true diff --git a/examples/gno.land/r/nt/boards2/z_4_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_4_b_filetest.gno index 70760b217f3..9114392a137 100644 --- a/examples/gno.land/r/nt/boards2/z_4_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_4_b_filetest.gno @@ -7,32 +7,25 @@ import ( ) const ( - owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 - member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 - initialRole = boards2.RoleGuest - newRole = boards2.RoleAdmin + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + newRole = boards2.RoleAdmin ) -var bid boards2.BoardID +var bid boards2.BoardID // Operate on board DAO func init() { std.TestSetOrigCaller(owner) - bid = boards2.CreateBoard("foo123") // Operate on board DAO members + bid = boards2.CreateBoard("foo123") boards2.InviteMember(bid, member, boards2.RoleGuest) } func main() { - if boards2.HasMemberRole(bid, member, initialRole) { - println("ok") - } - boards2.ChangeMemberRole(bid, member, newRole) - if boards2.HasMemberRole(bid, member, newRole) { - println("ok") - } + // Ensure that new role has been changed + println(boards2.HasMemberRole(bid, member, newRole)) } // Output: -// ok -// ok +// true diff --git a/examples/gno.land/r/nt/boards2/z_4_c_filetest.gno b/examples/gno.land/r/nt/boards2/z_4_c_filetest.gno index 6c8573a494b..a71ce1a21b8 100644 --- a/examples/gno.land/r/nt/boards2/z_4_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_4_c_filetest.gno @@ -10,7 +10,7 @@ const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 owner2 = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 admin = std.Address("g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5") - bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards + bid = boards2.BoardID(0) // Operate on realm DAO instead of individual boards ) func init() { diff --git a/examples/gno.land/r/nt/boards2/z_4_d_filetest.gno b/examples/gno.land/r/nt/boards2/z_4_d_filetest.gno index 713e14cee1e..6c2678873b6 100644 --- a/examples/gno.land/r/nt/boards2/z_4_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_4_d_filetest.gno @@ -10,7 +10,7 @@ const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 admin2 = std.Address("g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5") - bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards + bid = boards2.BoardID(0) // Operate on realm DAO instead of individual boards ) func init() { diff --git a/examples/gno.land/r/nt/boards2/z_4_e_filetest.gno b/examples/gno.land/r/nt/boards2/z_4_e_filetest.gno index dcce443582a..80d33faaafa 100644 --- a/examples/gno.land/r/nt/boards2/z_4_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_4_e_filetest.gno @@ -7,20 +7,23 @@ import ( ) const ( - owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 - admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 - bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards + newRole = boards2.RoleOwner ) func init() { std.TestSetOrigCaller(owner) - boards2.InviteMember(bid, admin, boards2.RoleAdmin) + boards2.InviteMember(bid, member, boards2.RoleAdmin) } func main() { - boards2.ChangeMemberRole(bid, admin, boards2.RoleOwner) // Owner can promote other members to Owner - println("ok") + boards2.ChangeMemberRole(bid, member, newRole) // Owner can promote other members to Owner + + // Ensure that new role has been changed to owner + println(boards2.HasMemberRole(bid, member, newRole)) } // Output: -// ok +// true diff --git a/examples/gno.land/r/nt/boards2/z_4_h_filetest.gno b/examples/gno.land/r/nt/boards2/z_4_h_filetest.gno new file mode 100644 index 00000000000..0d9bc8e5303 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_4_h_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.ChangeMemberRole(0, "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", boards2.RoleGuest) +} + +// Error: +// unauthorized From bab11530e802dc0296027546b502910e0c2b3de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Thu, 30 Jan 2025 09:39:04 +0100 Subject: [PATCH 29/52] test(boards2): add missing filetests for member check (#3638) Add missing filetests for`HasMemberRole()` and `IsMember()` functions. Related to #3623 This covers all tests for the function: - Successfully check valid member - Successfully check non member --- .../gno.land/r/nt/boards2/z_6_a_filetest.gno | 30 +++++++++++++++++++ .../gno.land/r/nt/boards2/z_6_b_filetest.gno | 27 +++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 examples/gno.land/r/nt/boards2/z_6_a_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_6_b_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/z_6_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_6_a_filetest.gno new file mode 100644 index 00000000000..18c89c7d896 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_6_a_filetest.gno @@ -0,0 +1,30 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm DAO instead of individual boards + role = boards2.RoleGuest +) + +func init() { + std.TestSetOrigCaller(owner) + boards2.InviteMember(bid, member, role) +} + +func main() { + println(boards2.HasMemberRole(bid, member, role)) + println(boards2.HasMemberRole(bid, member, "invalid")) + println(boards2.IsMember(bid, member)) +} + +// Output: +// true +// false +// true diff --git a/examples/gno.land/r/nt/boards2/z_6_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_6_b_filetest.gno new file mode 100644 index 00000000000..8344d467a31 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_6_b_filetest.gno @@ -0,0 +1,27 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm DAO instead of individual boards + role = boards2.RoleGuest +) + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + println(boards2.HasMemberRole(bid, user, role)) + println(boards2.IsMember(bid, user)) +} + +// Output: +// false +// false From db1dd43618cef065b955e916894469d0535c2ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Thu, 30 Jan 2025 17:15:22 +0100 Subject: [PATCH 30/52] test(boards2): add missing filetests for thread creation (#3644) Add missing filetests for `CreateThread()` function. Related to #3623 This covers all tests for the function: - Successfully create a thread - Fail because board is not found - Fail because user has no permission to create a thread - Fail w/ empty title - Fail w/ empty body --- examples/gno.land/r/nt/boards2/public.gno | 32 +++++++++++++-- .../gno.land/r/nt/boards2/z_0_b_filetest.gno | 2 +- .../gno.land/r/nt/boards2/z_3_b_filetest.gno | 2 +- .../gno.land/r/nt/boards2/z_8_a_filetest.gno | 39 +++++++++++++++++++ .../gno.land/r/nt/boards2/z_8_b_filetest.gno | 20 ++++++++++ .../gno.land/r/nt/boards2/z_8_c_filetest.gno | 28 +++++++++++++ .../gno.land/r/nt/boards2/z_8_d_filetest.gno | 23 +++++++++++ .../gno.land/r/nt/boards2/z_8_e_filetest.gno | 23 +++++++++++ 8 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 examples/gno.land/r/nt/boards2/z_8_a_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_8_b_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_8_c_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_8_d_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_8_e_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/public.gno b/examples/gno.land/r/nt/boards2/public.gno index 7a7563a5145..fff727e0560 100644 --- a/examples/gno.land/r/nt/boards2/public.gno +++ b/examples/gno.land/r/nt/boards2/public.gno @@ -17,7 +17,7 @@ func CreateBoard(name string) BoardID { assertIsUserCall() name = strings.TrimSpace(name) - assertBoardNameIsNotEmpty(name) + assertNameIsNotEmpty(name) assertBoardNameNotExists(name) caller := std.GetOrigCaller() @@ -37,7 +37,7 @@ func RenameBoard(name, newName string) { assertIsUserCall() newName = strings.TrimSpace(newName) - assertBoardNameIsNotEmpty(newName) + assertNameIsNotEmpty(newName) assertBoardNameNotExists(newName) board := mustGetBoardByName(name) @@ -75,6 +75,12 @@ func FlagThread(bid BoardID, postID PostID, reason string) { func CreateThread(bid BoardID, title, body string) PostID { assertIsUserCall() + title = strings.TrimSpace(title) + assertTitleIsNotEmpty(title) + + body = strings.TrimSpace(body) + assertBodyIsNotEmpty(body) + caller := std.GetOrigCaller() board := mustGetBoard(bid) assertHasBoardPermission(board, caller, PermissionThreadCreate) @@ -178,6 +184,12 @@ func DeleteReply(bid BoardID, threadID, replyID PostID) { func EditThread(bid BoardID, threadID PostID, title, body string) { assertIsUserCall() + title = strings.TrimSpace(title) + assertTitleIsNotEmpty(title) + + body = strings.TrimSpace(body) + assertBodyIsNotEmpty(body) + board := mustGetBoard(bid) thread := mustGetThread(board, threadID) caller := std.GetOrigCaller() @@ -274,9 +286,21 @@ func assertBoardExists(id BoardID) { } } -func assertBoardNameIsNotEmpty(name string) { +func assertNameIsNotEmpty(name string) { if name == "" { - panic("board name is empty") + panic("name is empty") + } +} + +func assertTitleIsNotEmpty(title string) { + if title == "" { + panic("title is empty") + } +} + +func assertBodyIsNotEmpty(body string) { + if body == "" { + panic("body is empty") } } diff --git a/examples/gno.land/r/nt/boards2/z_0_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_0_b_filetest.gno index 689c76f81f1..3ae985acc3c 100644 --- a/examples/gno.land/r/nt/boards2/z_0_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_0_b_filetest.gno @@ -17,4 +17,4 @@ func main() { } // Error: -// board name is empty +// name is empty diff --git a/examples/gno.land/r/nt/boards2/z_3_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_3_b_filetest.gno index 851fbf303af..a5f521147ea 100644 --- a/examples/gno.land/r/nt/boards2/z_3_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_3_b_filetest.gno @@ -21,4 +21,4 @@ func main() { } // Error: -// board name is empty +// name is empty diff --git a/examples/gno.land/r/nt/boards2/z_8_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_8_a_filetest.gno new file mode 100644 index 00000000000..8a46386b25a --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_8_a_filetest.gno @@ -0,0 +1,39 @@ +package main + +import ( + "std" + "strings" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + title = "Test Thread" + body = "Test body" + path = "test-board/1" +) + +var bid boards2.BoardID + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") +} + +func main() { + pid := boards2.CreateThread(bid, title, body) + + // Ensure that returned ID is right + println(pid == 1) + + // Render content must contains thread's title and body + content := boards2.Render(path) + println(strings.HasPrefix(content, "# "+title)) + println(strings.Contains(content, body)) +} + +// Output: +// true +// true +// true diff --git a/examples/gno.land/r/nt/boards2/z_8_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_8_b_filetest.gno new file mode 100644 index 00000000000..500ed277f99 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_8_b_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.CreateThread(404, "Foo", "bar") +} + +// Error: +// board does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/z_8_c_filetest.gno b/examples/gno.land/r/nt/boards2/z_8_c_filetest.gno new file mode 100644 index 00000000000..4c2d51141df --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_8_c_filetest.gno @@ -0,0 +1,28 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 +) + +var bid boards2.BoardID + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") + + std.TestSetOrigCaller(user) +} + +func main() { + boards2.CreateThread(bid, "Foo", "bar") +} + +// Error: +// unauthorized diff --git a/examples/gno.land/r/nt/boards2/z_8_d_filetest.gno b/examples/gno.land/r/nt/boards2/z_8_d_filetest.gno new file mode 100644 index 00000000000..e5347474ae5 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_8_d_filetest.gno @@ -0,0 +1,23 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var bid boards2.BoardID + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") +} + +func main() { + boards2.CreateThread(bid, "", "bar") +} + +// Error: +// title is empty diff --git a/examples/gno.land/r/nt/boards2/z_8_e_filetest.gno b/examples/gno.land/r/nt/boards2/z_8_e_filetest.gno new file mode 100644 index 00000000000..97bc013a3d6 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_8_e_filetest.gno @@ -0,0 +1,23 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var bid boards2.BoardID + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") +} + +func main() { + boards2.CreateThread(bid, "Foo", "") +} + +// Error: +// body is empty From 33da5eda4e6153eafc769174f7c29361e190dce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Fri, 31 Jan 2025 09:16:05 +0100 Subject: [PATCH 31/52] test(boards2): add missing filetests for thread edit (#3649) Add missing filetests for `EditThread()` function. Related to #3623 This covers all tests for the function: - Successfully edit a thread - Fail w/ empty title - Fail w/ empty body - Fail because board is not found - Fail because thread is not found - Fail because user has no permission to edit a thread - Successfully edit a thread using a user with permission to delete --- .../gno.land/r/nt/boards2/z_11_a_filetest.gno | 39 ++++++++++++++++ .../gno.land/r/nt/boards2/z_11_b_filetest.gno | 27 ++++++++++++ .../gno.land/r/nt/boards2/z_11_c_filetest.gno | 27 ++++++++++++ .../gno.land/r/nt/boards2/z_11_d_filetest.gno | 20 +++++++++ .../gno.land/r/nt/boards2/z_11_e_filetest.gno | 23 ++++++++++ .../gno.land/r/nt/boards2/z_11_f_filetest.gno | 32 ++++++++++++++ .../gno.land/r/nt/boards2/z_11_g_filetest.gno | 44 +++++++++++++++++++ 7 files changed, 212 insertions(+) create mode 100644 examples/gno.land/r/nt/boards2/z_11_a_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_11_b_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_11_c_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_11_d_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_11_e_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_11_f_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_11_g_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/z_11_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_11_a_filetest.gno new file mode 100644 index 00000000000..795a4172096 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_11_a_filetest.gno @@ -0,0 +1,39 @@ +package main + +import ( + "std" + "strings" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + title = "Test Thread" + body = "Test body" + path = "test-board/1" +) + +var ( + bid boards2.BoardID + pid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + pid = boards2.CreateThread(bid, "Foo", "bar") +} + +func main() { + boards2.EditThread(bid, pid, title, body) + + // Render content must contains thread's title and body + content := boards2.Render(path) + println(strings.HasPrefix(content, "# "+title)) + println(strings.Contains(content, body)) +} + +// Output: +// true +// true diff --git a/examples/gno.land/r/nt/boards2/z_11_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_11_b_filetest.gno new file mode 100644 index 00000000000..2c4268afc15 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_11_b_filetest.gno @@ -0,0 +1,27 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + bid boards2.BoardID + pid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + pid = boards2.CreateThread(bid, "Foo", "bar") +} + +func main() { + boards2.EditThread(bid, pid, "", "bar") +} + +// Error: +// title is empty diff --git a/examples/gno.land/r/nt/boards2/z_11_c_filetest.gno b/examples/gno.land/r/nt/boards2/z_11_c_filetest.gno new file mode 100644 index 00000000000..357c0ac4f94 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_11_c_filetest.gno @@ -0,0 +1,27 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + bid boards2.BoardID + pid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + pid = boards2.CreateThread(bid, "Foo", "bar") +} + +func main() { + boards2.EditThread(bid, pid, "Foo", "") +} + +// Error: +// body is empty diff --git a/examples/gno.land/r/nt/boards2/z_11_d_filetest.gno b/examples/gno.land/r/nt/boards2/z_11_d_filetest.gno new file mode 100644 index 00000000000..4587b4c64ab --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_11_d_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.EditThread(404, 1, "Foo", "bar") +} + +// Error: +// board does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/z_11_e_filetest.gno b/examples/gno.land/r/nt/boards2/z_11_e_filetest.gno new file mode 100644 index 00000000000..f0be5da2bb8 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_11_e_filetest.gno @@ -0,0 +1,23 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var bid boards2.BoardID + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") +} + +func main() { + boards2.EditThread(bid, 404, "Foo", "") +} + +// Error: +// body is empty diff --git a/examples/gno.land/r/nt/boards2/z_11_f_filetest.gno b/examples/gno.land/r/nt/boards2/z_11_f_filetest.gno new file mode 100644 index 00000000000..d2ff805fa08 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_11_f_filetest.gno @@ -0,0 +1,32 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 +) + +var ( + bid boards2.BoardID + pid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + pid = boards2.CreateThread(bid, "Foo", "bar") + + std.TestSetOrigCaller(user) +} + +func main() { + boards2.EditThread(bid, pid, "Foo", "bar") +} + +// Error: +// unauthorized diff --git a/examples/gno.land/r/nt/boards2/z_11_g_filetest.gno b/examples/gno.land/r/nt/boards2/z_11_g_filetest.gno new file mode 100644 index 00000000000..9c2ccd1b4a9 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_11_g_filetest.gno @@ -0,0 +1,44 @@ +package main + +import ( + "std" + "strings" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + title = "Test Thread" + body = "Test body" + path = "test-board/1" +) + +var ( + bid boards2.BoardID + pid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + pid = boards2.CreateThread(bid, "Foo", "bar") + + // Invite a member using a role with permission to edit threads + boards2.InviteMember(bid, admin, boards2.RoleAdmin) + std.TestSetOrigCaller(admin) +} + +func main() { + boards2.EditThread(bid, pid, title, body) + + // Render content must contains thread's title and body + content := boards2.Render(path) + println(strings.HasPrefix(content, "# "+title)) + println(strings.Contains(content, body)) +} + +// Output: +// true +// true From d7dcd9206aaed3e8dd375031fa1ccd2ac0263353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Fri, 31 Jan 2025 09:16:36 +0100 Subject: [PATCH 32/52] test(boards2): add missing filetests for thread flagging (#3646) Add missing filetests for `FlagThread()` function. Related to #3623 This covers all tests for the function: - Successfully flag a thread - Fail because board is not found - Fail because user has no permission to flag a thread - Fail because thread is not found - Fail because default flag threshold of 1 is exceeded - Successfully flag a thread using a user with permission to flag --- examples/gno.land/r/nt/boards2/flag.gno | 1 + .../gno.land/r/nt/boards2/z_10_a_filetest.gno | 30 +++++++++++++++ .../gno.land/r/nt/boards2/z_10_b_filetest.gno | 20 ++++++++++ .../gno.land/r/nt/boards2/z_10_c_filetest.gno | 32 ++++++++++++++++ .../gno.land/r/nt/boards2/z_10_d_filetest.gno | 23 ++++++++++++ .../gno.land/r/nt/boards2/z_10_e_filetest.gno | 28 ++++++++++++++ .../gno.land/r/nt/boards2/z_10_f_filetest.gno | 37 +++++++++++++++++++ 7 files changed, 171 insertions(+) create mode 100644 examples/gno.land/r/nt/boards2/z_10_a_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_10_b_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_10_c_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_10_d_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_10_e_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_10_f_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/flag.gno b/examples/gno.land/r/nt/boards2/flag.gno index e5744211ddf..902f6263b32 100644 --- a/examples/gno.land/r/nt/boards2/flag.gno +++ b/examples/gno.land/r/nt/boards2/flag.gno @@ -5,6 +5,7 @@ import ( "strconv" ) +// TODO: We should allow changing the threshold, also support value per board const flagThreshold = 1 type Flag struct { diff --git a/examples/gno.land/r/nt/boards2/z_10_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_10_a_filetest.gno new file mode 100644 index 00000000000..8060621c1af --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_10_a_filetest.gno @@ -0,0 +1,30 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + bid boards2.BoardID + pid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + pid = boards2.CreateThread(bid, "Foo", "bar") +} + +func main() { + boards2.FlagThread(bid, pid, "") + + // Ensure that original thread content not visible + println(boards2.Render("test-board/1")) +} + +// Output: +// Thread with ID: 1 has been flagged as inappropriate diff --git a/examples/gno.land/r/nt/boards2/z_10_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_10_b_filetest.gno new file mode 100644 index 00000000000..1c0bdca47c6 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_10_b_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.FlagThread(404, 1, "") +} + +// Error: +// board does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/z_10_c_filetest.gno b/examples/gno.land/r/nt/boards2/z_10_c_filetest.gno new file mode 100644 index 00000000000..aa3ca0d3c37 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_10_c_filetest.gno @@ -0,0 +1,32 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 +) + +var ( + bid boards2.BoardID + pid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + pid = boards2.CreateThread(bid, "Foo", "bar") + + std.TestSetOrigCaller(user) +} + +func main() { + boards2.FlagThread(bid, pid, "") +} + +// Error: +// unauthorized diff --git a/examples/gno.land/r/nt/boards2/z_10_d_filetest.gno b/examples/gno.land/r/nt/boards2/z_10_d_filetest.gno new file mode 100644 index 00000000000..0216c58837c --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_10_d_filetest.gno @@ -0,0 +1,23 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var bid boards2.BoardID + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") +} + +func main() { + boards2.FlagThread(bid, 404, "") +} + +// Error: +// post doesn't exist diff --git a/examples/gno.land/r/nt/boards2/z_10_e_filetest.gno b/examples/gno.land/r/nt/boards2/z_10_e_filetest.gno new file mode 100644 index 00000000000..3e872a4e152 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_10_e_filetest.gno @@ -0,0 +1,28 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + bid boards2.BoardID + pid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + pid = boards2.CreateThread(bid, "Foo", "bar") + boards2.FlagThread(bid, pid, "") +} + +func main() { + boards2.FlagThread(bid, pid, "") +} + +// Error: +// item flag count threshold exceeded: 1 diff --git a/examples/gno.land/r/nt/boards2/z_10_f_filetest.gno b/examples/gno.land/r/nt/boards2/z_10_f_filetest.gno new file mode 100644 index 00000000000..b04f4a267ed --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_10_f_filetest.gno @@ -0,0 +1,37 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + moderator = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 +) + +var ( + bid boards2.BoardID + pid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + pid = boards2.CreateThread(bid, "Foo", "bar") + + // Invite a member using a role with permission to flag threads + boards2.InviteMember(bid, moderator, boards2.RoleModerator) + std.TestSetOrigCaller(moderator) +} + +func main() { + boards2.FlagThread(bid, pid, "") + + // Ensure that original thread content not visible + println(boards2.Render("test-board/1")) +} + +// Output: +// Thread with ID: 1 has been flagged as inappropriate From d7740a48e0e634fb09c8f28329d832e6701337bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Fri, 31 Jan 2025 09:18:47 +0100 Subject: [PATCH 33/52] test(boards2): add missing filetests for thread deletion (#3645) Add missing filetests for `DeleteThread()` function. Related to #3623 This covers all tests for the function: - Successfully delete a thread - Fail because board is not found - Fail because thread is not found - Fail because user has no permission to delete a thread - Successfully delete a thread using a user with permission to delete --- examples/gno.land/r/nt/boards2/public.gno | 1 + .../gno.land/r/nt/boards2/z_9_a_filetest.gno | 34 +++++++++++++++++ .../gno.land/r/nt/boards2/z_9_b_filetest.gno | 20 ++++++++++ .../gno.land/r/nt/boards2/z_9_c_filetest.gno | 23 ++++++++++++ .../gno.land/r/nt/boards2/z_9_d_filetest.gno | 33 +++++++++++++++++ .../gno.land/r/nt/boards2/z_9_e_filetest.gno | 37 +++++++++++++++++++ 6 files changed, 148 insertions(+) create mode 100644 examples/gno.land/r/nt/boards2/z_9_a_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_9_b_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_9_c_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_9_d_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_9_e_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/public.gno b/examples/gno.land/r/nt/boards2/public.gno index fff727e0560..f6f3f7f0429 100644 --- a/examples/gno.land/r/nt/boards2/public.gno +++ b/examples/gno.land/r/nt/boards2/public.gno @@ -156,6 +156,7 @@ func DeleteThread(bid BoardID, threadID PostID) { assertHasBoardPermission(board, caller, PermissionThreadDelete) } + // TODO: Discuss how to deal with thread deletion board.DeleteThread(threadID) } diff --git a/examples/gno.land/r/nt/boards2/z_9_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_9_a_filetest.gno new file mode 100644 index 00000000000..6d1bf46dda7 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_9_a_filetest.gno @@ -0,0 +1,34 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + title = "Test Thread" + body = "Test body" +) + +var ( + bid boards2.BoardID + pid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + pid = boards2.CreateThread(bid, title, body) +} + +func main() { + boards2.DeleteThread(bid, pid) + + // Ensure thread doesn't exist + println(boards2.Render("test-board/1")) +} + +// Output: +// Thread does not exist with ID: 1 diff --git a/examples/gno.land/r/nt/boards2/z_9_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_9_b_filetest.gno new file mode 100644 index 00000000000..70ae9fcaf6d --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_9_b_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.DeleteThread(404, 1) +} + +// Error: +// board does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/z_9_c_filetest.gno b/examples/gno.land/r/nt/boards2/z_9_c_filetest.gno new file mode 100644 index 00000000000..ff33634820c --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_9_c_filetest.gno @@ -0,0 +1,23 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var bid boards2.BoardID + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") +} + +func main() { + boards2.DeleteThread(bid, 404) +} + +// Error: +// thread does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/z_9_d_filetest.gno b/examples/gno.land/r/nt/boards2/z_9_d_filetest.gno new file mode 100644 index 00000000000..d677c801622 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_9_d_filetest.gno @@ -0,0 +1,33 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 +) + +var ( + bid boards2.BoardID + pid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + pid = boards2.CreateThread(bid, "Foo", "bar") + + // Call using a user that has not permission to delete threads + std.TestSetOrigCaller(user) +} + +func main() { + boards2.DeleteThread(bid, pid) +} + +// Error: +// unauthorized diff --git a/examples/gno.land/r/nt/boards2/z_9_e_filetest.gno b/examples/gno.land/r/nt/boards2/z_9_e_filetest.gno new file mode 100644 index 00000000000..d63a0bd2bfb --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_9_e_filetest.gno @@ -0,0 +1,37 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 +) + +var ( + bid boards2.BoardID + pid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + pid = boards2.CreateThread(bid, "Foo", "bar") + + // Invite a member using a role with permission to delete threads + boards2.InviteMember(bid, member, boards2.RoleAdmin) + std.TestSetOrigCaller(member) +} + +func main() { + boards2.DeleteThread(bid, pid) + + // Ensure thread doesn't exist + println(boards2.Render("test-board/1")) +} + +// Output: +// Thread does not exist with ID: 1 From 5f67719663648224b602900f23e9967a92585402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Fri, 31 Jan 2025 09:20:30 +0100 Subject: [PATCH 34/52] test(boards2): add missing filetests for board ID getter by name (#3642) Add missing filetests for `GetBoardIDFromName()` function. Related to #3623 This covers all tests for the function: - Successfully get ID by name - Fail because name is not found --- examples/gno.land/r/nt/boards2/public.gno | 6 ---- .../gno.land/r/nt/boards2/z_7_a_filetest.gno | 29 +++++++++++++++++++ .../gno.land/r/nt/boards2/z_7_b_filetest.gno | 23 +++++++++++++++ 3 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 examples/gno.land/r/nt/boards2/z_7_a_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_7_b_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/public.gno b/examples/gno.land/r/nt/boards2/public.gno index f6f3f7f0429..06505b4a2fc 100644 --- a/examples/gno.land/r/nt/boards2/public.gno +++ b/examples/gno.land/r/nt/boards2/public.gno @@ -269,12 +269,6 @@ func assertIsUserCall() { } } -func assertHasPermission(user std.Address, p Permission) { - if !gPerm.HasPermission(user, p) { - panic("unauthorized") - } -} - func assertHasBoardPermission(b *Board, user std.Address, p Permission) { if !b.perms.HasPermission(user, p) { panic("unauthorized") diff --git a/examples/gno.land/r/nt/boards2/z_7_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_7_a_filetest.gno new file mode 100644 index 00000000000..688aaa67244 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_7_a_filetest.gno @@ -0,0 +1,29 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + name = "test123" +) + +var bid boards2.BoardID + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard(name) +} + +func main() { + bid2, found := boards2.GetBoardIDFromName(name) + println(found) + println(bid2 == bid) +} + +// Output: +// true +// true diff --git a/examples/gno.land/r/nt/boards2/z_7_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_7_b_filetest.gno new file mode 100644 index 00000000000..8e8ce18cb87 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_7_b_filetest.gno @@ -0,0 +1,23 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + bid, found := boards2.GetBoardIDFromName("foobar") + println(found) + println(bid == 0) +} + +// Output: +// false +// true From b983f8f2797a49234f9ae9b66f7a46f56129b931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Fri, 31 Jan 2025 15:52:24 +0100 Subject: [PATCH 35/52] test(boards2): add missing filetests for reply creation (#3653) Add missing filetests for `CreateReply()` function. Related to #3623 This covers all tests for the function: - Successfully create a reply - Fail because board is not found - Fail because thread is not found - Fail because parent reply is not found - Fail because thread is hidden - Fail creating sub-reply because thread is hidden - Fail creating sub-reply because parent reply is hidden - Fail because user has no permission to create a reply - Fail w/ empty body - Successfully create a sub-reply --- examples/gno.land/r/nt/boards2/public.gno | 5 ++- .../gno.land/r/nt/boards2/z_2_a_filetest.gno | 31 ++++++++++---- .../gno.land/r/nt/boards2/z_2_b_filetest.gno | 10 +---- .../gno.land/r/nt/boards2/z_2_c_filetest.gno | 13 +++--- .../gno.land/r/nt/boards2/z_2_d_filetest.gno | 16 ++++---- .../gno.land/r/nt/boards2/z_2_e_filetest.gno | 30 ++++++++++++++ .../gno.land/r/nt/boards2/z_2_f_filetest.gno | 31 ++++++++++++++ .../gno.land/r/nt/boards2/z_2_g_filetest.gno | 31 ++++++++++++++ .../gno.land/r/nt/boards2/z_2_h_filetest.gno | 32 +++++++++++++++ .../gno.land/r/nt/boards2/z_2_i_filetest.gno | 27 ++++++++++++ .../gno.land/r/nt/boards2/z_2_j_filetest.gno | 41 +++++++++++++++++++ 11 files changed, 236 insertions(+), 31 deletions(-) create mode 100644 examples/gno.land/r/nt/boards2/z_2_e_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_2_f_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_2_g_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_2_h_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_2_i_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_2_j_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/public.gno b/examples/gno.land/r/nt/boards2/public.gno index 06505b4a2fc..cc2e36279f5 100644 --- a/examples/gno.land/r/nt/boards2/public.gno +++ b/examples/gno.land/r/nt/boards2/public.gno @@ -92,6 +92,9 @@ func CreateThread(bid BoardID, title, body string) PostID { func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { assertIsUserCall() + body = strings.TrimSpace(body) + assertBodyIsNotEmpty(body) + caller := std.GetOrigCaller() board := mustGetBoard(bid) assertHasBoardPermission(board, caller, PermissionReplyCreate) @@ -100,7 +103,7 @@ func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { assertThreadVisible(thread) var reply *Post - if replyID == threadID { + if replyID == 0 { // When the parent reply is the thread just add reply to thread reply = thread.AddReply(caller, body) } else { diff --git a/examples/gno.land/r/nt/boards2/z_2_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_2_a_filetest.gno index c4fd1da0c87..f9e332d2cca 100644 --- a/examples/gno.land/r/nt/boards2/z_2_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_2_a_filetest.gno @@ -2,22 +2,39 @@ package main import ( "std" + "strings" "gno.land/r/nt/boards2" ) -const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + path = "test-board/1/2" + comment = "Test comment" +) + +var ( + bid boards2.BoardID + tid boards2.PostID +) func init() { std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") } func main() { - bid := boards2.CreateBoard("test123") - pid := boards2.CreateThread(bid, "thread", "thread") - boards2.FlagThread(bid, pid, "reason") - _ = boards2.CreateReply(bid, pid, pid, "reply") + rid := boards2.CreateReply(bid, tid, 0, comment) + + // Ensure that returned ID is right + println(rid == 2) + + // Render content must contain the reply + content := boards2.Render(path) + println(strings.Contains(content, "\n> "+comment+"\n")) } -// Error: -// thread with ID: 1 was hidden +// Output: +// true +// true diff --git a/examples/gno.land/r/nt/boards2/z_2_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_2_b_filetest.gno index f215c0c84eb..88b2233dff3 100644 --- a/examples/gno.land/r/nt/boards2/z_2_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_2_b_filetest.gno @@ -13,14 +13,8 @@ func init() { } func main() { - // ensure that nested replies denied if root thread is hidden. - bid := boards2.CreateBoard("test123") - pid := boards2.CreateThread(bid, "thread", "thread") - rid := boards2.CreateReply(bid, pid, pid, "reply1") - - boards2.FlagThread(bid, pid, "reason") - _ = boards2.CreateReply(bid, pid, rid, "reply1.1") + boards2.CreateReply(404, 1, 0, "comment") } // Error: -// thread with ID: 1 was hidden +// board does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/z_2_c_filetest.gno b/examples/gno.land/r/nt/boards2/z_2_c_filetest.gno index 98ffbc7f624..4e0779194ab 100644 --- a/examples/gno.land/r/nt/boards2/z_2_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_2_c_filetest.gno @@ -8,19 +8,16 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 +var bid boards2.BoardID + func init() { std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") } func main() { - // ensure that nested replies denied if root thread is hidden. - bid := boards2.CreateBoard("test123") - pid := boards2.CreateThread(bid, "thread", "thread") - rid := boards2.CreateReply(bid, pid, pid, "reply1") - - boards2.FlagReply(bid, pid, rid, "reason") - _ = boards2.CreateReply(bid, pid, rid, "reply1.1") + boards2.CreateReply(bid, 404, 0, "comment") } // Error: -// reply with ID: 2 was hidden +// thread does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/z_2_d_filetest.gno b/examples/gno.land/r/nt/boards2/z_2_d_filetest.gno index 9a95fd869b7..66edfb3d6f2 100644 --- a/examples/gno.land/r/nt/boards2/z_2_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/z_2_d_filetest.gno @@ -8,18 +8,20 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 +var ( + bid boards2.BoardID + tid boards2.PostID +) + func init() { std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") } func main() { - // Only single user per flag can't be tested atm, as flagThreshold = 1. - bid := boards2.CreateBoard("test123") - pid := boards2.CreateThread(bid, "thread", "thread") - - boards2.FlagThread(bid, pid, "reason1") - boards2.FlagThread(bid, pid, "reason2") + boards2.CreateReply(bid, tid, 404, "comment") } // Error: -// item flag count threshold exceeded: 1 +// reply does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/z_2_e_filetest.gno b/examples/gno.land/r/nt/boards2/z_2_e_filetest.gno new file mode 100644 index 00000000000..8c231b552cd --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_2_e_filetest.gno @@ -0,0 +1,30 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + bid boards2.BoardID + tid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") + tid = boards2.CreateThread(bid, "Foo", "bar") + + // Hide thread by flagging it so reply can't be submitted + boards2.FlagThread(bid, tid, "reason") +} + +func main() { + boards2.CreateReply(bid, tid, 0, "Test reply") +} + +// Error: +// thread with ID: 1 was hidden diff --git a/examples/gno.land/r/nt/boards2/z_2_f_filetest.gno b/examples/gno.land/r/nt/boards2/z_2_f_filetest.gno new file mode 100644 index 00000000000..0b595a84cb6 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_2_f_filetest.gno @@ -0,0 +1,31 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + bid boards2.BoardID + tid, rid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") + tid = boards2.CreateThread(bid, "Foo", "bar") + rid = boards2.CreateReply(bid, tid, 0, "reply1") + + // Hide thread by flagging it so reply of a reply can't be submitted + boards2.FlagThread(bid, tid, "reason") +} + +func main() { + boards2.CreateReply(bid, tid, rid, "reply1.1") +} + +// Error: +// thread with ID: 1 was hidden diff --git a/examples/gno.land/r/nt/boards2/z_2_g_filetest.gno b/examples/gno.land/r/nt/boards2/z_2_g_filetest.gno new file mode 100644 index 00000000000..6f69a023a0b --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_2_g_filetest.gno @@ -0,0 +1,31 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + bid boards2.BoardID + tid, rid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") + tid = boards2.CreateThread(bid, "thread", "thread") + rid = boards2.CreateReply(bid, tid, 0, "reply1") + + // Hide reply by flagging it so sub reply can't be submitted + boards2.FlagReply(bid, tid, rid, "reason") +} + +func main() { + boards2.CreateReply(bid, tid, rid, "reply1.1") +} + +// Error: +// reply with ID: 2 was hidden diff --git a/examples/gno.land/r/nt/boards2/z_2_h_filetest.gno b/examples/gno.land/r/nt/boards2/z_2_h_filetest.gno new file mode 100644 index 00000000000..e34d8d167ef --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_2_h_filetest.gno @@ -0,0 +1,32 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 +) + +var ( + bid boards2.BoardID + tid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") + tid = boards2.CreateThread(bid, "Foo", "bar") + + std.TestSetOrigCaller(user) +} + +func main() { + boards2.CreateReply(bid, tid, 0, "Test reply") +} + +// Error: +// unauthorized diff --git a/examples/gno.land/r/nt/boards2/z_2_i_filetest.gno b/examples/gno.land/r/nt/boards2/z_2_i_filetest.gno new file mode 100644 index 00000000000..aa43805f7fd --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_2_i_filetest.gno @@ -0,0 +1,27 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + bid boards2.BoardID + tid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") +} + +func main() { + boards2.CreateReply(bid, tid, 0, "") +} + +// Error: +// body is empty diff --git a/examples/gno.land/r/nt/boards2/z_2_j_filetest.gno b/examples/gno.land/r/nt/boards2/z_2_j_filetest.gno new file mode 100644 index 00000000000..3449fb1c7ba --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_2_j_filetest.gno @@ -0,0 +1,41 @@ +package main + +import ( + "std" + "strings" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + path = "test-board/1/2" + comment = "Second comment" +) + +var ( + bid boards2.BoardID + tid, rid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") + rid = boards2.CreateReply(bid, tid, 0, "First comment") +} + +func main() { + rid2 := boards2.CreateReply(bid, tid, rid, comment) + + // Ensure that returned ID is right + println(rid2 == 3) + + // Render content must contain the sub-reply + content := boards2.Render(path) + println(strings.Contains(content, "\n> > "+comment+"\n")) +} + +// Output: +// true +// true From 22035537d5a44a346cdc172a0f7911e11a7fccc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Mon, 3 Feb 2025 09:22:23 +0100 Subject: [PATCH 36/52] test(boards2): add missing filetests for reply edit (#3655) Add missing filetests for `EditReply()` function. Related to #3623 This covers all tests for the function: - Successfully edit of a reply - Successfully edit of a sub-reply - Fail because board is not found - Fail because thread is not found - Fail because parent reply is not found - Fail when edited by a user that is not the creator of the reply - Fail when reply is hidden because is flagged - Fail w/ empty body --- examples/gno.land/r/nt/boards2/public.gno | 12 ++++-- .../gno.land/r/nt/boards2/z_12_a_filetest.gno | 37 +++++++++++++++++ .../gno.land/r/nt/boards2/z_12_b_filetest.gno | 40 +++++++++++++++++++ .../gno.land/r/nt/boards2/z_12_c_filetest.gno | 20 ++++++++++ .../gno.land/r/nt/boards2/z_12_d_filetest.gno | 23 +++++++++++ .../gno.land/r/nt/boards2/z_12_e_filetest.gno | 27 +++++++++++++ .../gno.land/r/nt/boards2/z_12_f_filetest.gno | 32 +++++++++++++++ .../gno.land/r/nt/boards2/z_12_g_filetest.gno | 34 ++++++++++++++++ .../gno.land/r/nt/boards2/z_12_h_filetest.gno | 28 +++++++++++++ 9 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 examples/gno.land/r/nt/boards2/z_12_a_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_12_b_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_12_c_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_12_d_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_12_e_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_12_f_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_12_g_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_12_h_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/public.gno b/examples/gno.land/r/nt/boards2/public.gno index cc2e36279f5..dadccf60029 100644 --- a/examples/gno.land/r/nt/boards2/public.gno +++ b/examples/gno.land/r/nt/boards2/public.gno @@ -204,18 +204,22 @@ func EditThread(bid BoardID, threadID PostID, title, body string) { thread.Update(title, body) } -func EditReply(bid BoardID, threadID, replyID PostID, title, body string) { +func EditReply(bid BoardID, threadID, replyID PostID, body string) { assertIsUserCall() + body = strings.TrimSpace(body) + assertBodyIsNotEmpty(body) + board := mustGetBoard(bid) thread := mustGetThread(board, threadID) reply := mustGetReply(thread, replyID) - caller := std.GetOrigCaller() - if caller != reply.GetCreator() { + assertReplyVisible(reply) + + if std.GetOrigCaller() != reply.GetCreator() { panic("only the reply creator is allowed to edit it") } - reply.Update(title, body) + reply.Update("", body) } func InviteMember(bid BoardID, user std.Address, role Role) { diff --git a/examples/gno.land/r/nt/boards2/z_12_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_12_a_filetest.gno new file mode 100644 index 00000000000..9650bf29bb0 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_12_a_filetest.gno @@ -0,0 +1,37 @@ +package main + +import ( + "std" + "strings" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + body = "Test reply" + path = "test-board/1/2" +) + +var ( + bid boards2.BoardID + tid, rid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") + rid = boards2.CreateReply(bid, tid, 0, "body") +} + +func main() { + boards2.EditReply(bid, tid, rid, body) + + // Render content must contain the modified reply + content := boards2.Render(path) + println(strings.Contains(content, "\n> "+body+"\n")) +} + +// Output: +// true diff --git a/examples/gno.land/r/nt/boards2/z_12_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_12_b_filetest.gno new file mode 100644 index 00000000000..406d658c731 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_12_b_filetest.gno @@ -0,0 +1,40 @@ +package main + +import ( + "std" + "strings" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + body = "Test reply" + path = "test-board/1/2" +) + +var ( + bid boards2.BoardID + tid, rid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") + + // Create a reply and a sub reply + parentRID := boards2.CreateReply(bid, tid, 0, "Parent") + rid = boards2.CreateReply(bid, tid, parentRID, "Child") +} + +func main() { + boards2.EditReply(bid, tid, rid, body) + + // Render content must contain the modified reply + content := boards2.Render(path) + println(strings.Contains(content, "\n> > "+body+"\n")) +} + +// Output: +// true diff --git a/examples/gno.land/r/nt/boards2/z_12_c_filetest.gno b/examples/gno.land/r/nt/boards2/z_12_c_filetest.gno new file mode 100644 index 00000000000..56501415bb6 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_12_c_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.EditReply(404, 1, 0, "body") +} + +// Error: +// board does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/z_12_d_filetest.gno b/examples/gno.land/r/nt/boards2/z_12_d_filetest.gno new file mode 100644 index 00000000000..373168e6a74 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_12_d_filetest.gno @@ -0,0 +1,23 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var bid boards2.BoardID + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") +} + +func main() { + boards2.EditReply(bid, 404, 0, "body") +} + +// Error: +// thread does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/z_12_e_filetest.gno b/examples/gno.land/r/nt/boards2/z_12_e_filetest.gno new file mode 100644 index 00000000000..f43d272de8f --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_12_e_filetest.gno @@ -0,0 +1,27 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + bid boards2.BoardID + tid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") +} + +func main() { + boards2.EditReply(bid, tid, 404, "body") +} + +// Error: +// reply does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/z_12_f_filetest.gno b/examples/gno.land/r/nt/boards2/z_12_f_filetest.gno new file mode 100644 index 00000000000..aa8778785a4 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_12_f_filetest.gno @@ -0,0 +1,32 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 +) + +var ( + bid boards2.BoardID + tid, rid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") + rid = boards2.CreateReply(bid, tid, 0, "body") + std.TestSetOrigCaller(user) +} + +func main() { + boards2.EditReply(bid, tid, rid, "new body") +} + +// Error: +// only the reply creator is allowed to edit it diff --git a/examples/gno.land/r/nt/boards2/z_12_g_filetest.gno b/examples/gno.land/r/nt/boards2/z_12_g_filetest.gno new file mode 100644 index 00000000000..e3506b8c1a1 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_12_g_filetest.gno @@ -0,0 +1,34 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 +) + +var ( + bid boards2.BoardID + tid, rid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") + rid = boards2.CreateReply(bid, tid, 0, "body") + + // Flag the reply so it's hidden + boards2.FlagReply(bid, tid, rid, "reason") +} + +func main() { + boards2.EditReply(bid, tid, rid, "body") +} + +// Error: +// reply with ID: 2 was hidden diff --git a/examples/gno.land/r/nt/boards2/z_12_h_filetest.gno b/examples/gno.land/r/nt/boards2/z_12_h_filetest.gno new file mode 100644 index 00000000000..2fbd84be1b7 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_12_h_filetest.gno @@ -0,0 +1,28 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + bid boards2.BoardID + tid, rid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") + rid = boards2.CreateReply(bid, tid, 0, "body") +} + +func main() { + boards2.EditReply(bid, tid, rid, "") +} + +// Error: +// body is empty From 277b49d1264af3e4bfc8e128148f02a9956851f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Mon, 3 Feb 2025 09:22:34 +0100 Subject: [PATCH 37/52] test(boards2): add missing filetests for reply flagging (#3656) Add missing filetests for `FlagReply()` function. Related to #3623 This covers all tests for the function: - Successfully flag a reply - Fail because board is not found - Fail because thread is not found - Fail because reply is not found - Fail because default flag threshold of 1 is exceeded - Fail because user has no permission to flag a reply - Successfully flag a thread using a user with permission to flag --- .../gno.land/r/nt/boards2/z_13_a_filetest.gno | 33 +++++++++++++++ .../gno.land/r/nt/boards2/z_13_b_filetest.gno | 20 ++++++++++ .../gno.land/r/nt/boards2/z_13_c_filetest.gno | 23 +++++++++++ .../gno.land/r/nt/boards2/z_13_d_filetest.gno | 27 +++++++++++++ .../gno.land/r/nt/boards2/z_13_e_filetest.gno | 29 ++++++++++++++ .../gno.land/r/nt/boards2/z_13_f_filetest.gno | 33 +++++++++++++++ .../gno.land/r/nt/boards2/z_13_g_filetest.gno | 40 +++++++++++++++++++ 7 files changed, 205 insertions(+) create mode 100644 examples/gno.land/r/nt/boards2/z_13_a_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_13_b_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_13_c_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_13_d_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_13_e_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_13_f_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_13_g_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/z_13_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_13_a_filetest.gno new file mode 100644 index 00000000000..40165599c51 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_13_a_filetest.gno @@ -0,0 +1,33 @@ +package main + +import ( + "std" + "strings" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + bid boards2.BoardID + rid, tid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") + rid = boards2.CreateReply(bid, tid, 0, "body") +} + +func main() { + boards2.FlagReply(bid, tid, rid, "") + + // Render content must contain a message about the hidden reply + content := boards2.Render("test-board/1/2") + println(strings.Contains(content, "\n> _Reply is hidden as it has been flagged as inappropriate_\n")) +} + +// Output: +// true diff --git a/examples/gno.land/r/nt/boards2/z_13_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_13_b_filetest.gno new file mode 100644 index 00000000000..5bef42b97e3 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_13_b_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.FlagReply(404, 1, 1, "") +} + +// Error: +// board does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/z_13_c_filetest.gno b/examples/gno.land/r/nt/boards2/z_13_c_filetest.gno new file mode 100644 index 00000000000..6221efffb5e --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_13_c_filetest.gno @@ -0,0 +1,23 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var bid boards2.BoardID + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") +} + +func main() { + boards2.FlagReply(bid, 404, 1, "") +} + +// Error: +// thread does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/z_13_d_filetest.gno b/examples/gno.land/r/nt/boards2/z_13_d_filetest.gno new file mode 100644 index 00000000000..a6557d7daab --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_13_d_filetest.gno @@ -0,0 +1,27 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + bid boards2.BoardID + tid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") +} + +func main() { + boards2.FlagReply(bid, tid, 404, "") +} + +// Error: +// reply does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/z_13_e_filetest.gno b/examples/gno.land/r/nt/boards2/z_13_e_filetest.gno new file mode 100644 index 00000000000..437a34a3768 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_13_e_filetest.gno @@ -0,0 +1,29 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + bid boards2.BoardID + rid, tid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") + rid = boards2.CreateReply(bid, tid, 0, "body") + boards2.FlagReply(bid, tid, rid, "") +} + +func main() { + boards2.FlagReply(bid, tid, rid, "") +} + +// Error: +// item flag count threshold exceeded: 1 diff --git a/examples/gno.land/r/nt/boards2/z_13_f_filetest.gno b/examples/gno.land/r/nt/boards2/z_13_f_filetest.gno new file mode 100644 index 00000000000..c658bc960a6 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_13_f_filetest.gno @@ -0,0 +1,33 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 +) + +var ( + bid boards2.BoardID + rid, tid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") + rid = boards2.CreateReply(bid, tid, 0, "body") + + std.TestSetOrigCaller(user) +} + +func main() { + boards2.FlagReply(bid, tid, rid, "") +} + +// Error: +// unauthorized diff --git a/examples/gno.land/r/nt/boards2/z_13_g_filetest.gno b/examples/gno.land/r/nt/boards2/z_13_g_filetest.gno new file mode 100644 index 00000000000..3f7fbe11217 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_13_g_filetest.gno @@ -0,0 +1,40 @@ +package main + +import ( + "std" + "strings" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + moderator = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 +) + +var ( + bid boards2.BoardID + rid, tid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") + rid = boards2.CreateReply(bid, tid, 0, "body") + + // Invite a member using a role with permission to flag replies + boards2.InviteMember(bid, moderator, boards2.RoleModerator) + std.TestSetOrigCaller(moderator) +} + +func main() { + boards2.FlagReply(bid, tid, rid, "") + + // Render content must contain a message about the hidden reply + content := boards2.Render("test-board/1/2") + println(strings.Contains(content, "\n> _Reply is hidden as it has been flagged as inappropriate_\n")) +} + +// Output: +// true From d2b489268409ef48713010a45317fb31458e9286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Mon, 3 Feb 2025 09:22:44 +0100 Subject: [PATCH 38/52] test(boards2): add missing filetests for reply deletion (#3657) Add missing filetests for `DeleteReply()` function. Related to #3623 This covers all tests for the function: - Successfully delete a reply - Fail because board is not found - Fail because thread is not found - Fail because reply is not found - Successfully delete a sub-reply - Fail because user has no permission to delete a reply - Successfully delete a reply using a user with permission to delete --- examples/gno.land/r/nt/boards2/public.gno | 2 +- .../gno.land/r/nt/boards2/z_14_a_filetest.gno | 31 +++++++++++++++ .../gno.land/r/nt/boards2/z_14_b_filetest.gno | 20 ++++++++++ .../gno.land/r/nt/boards2/z_14_c_filetest.gno | 23 +++++++++++ .../gno.land/r/nt/boards2/z_14_d_filetest.gno | 27 +++++++++++++ .../gno.land/r/nt/boards2/z_14_e_filetest.gno | 37 ++++++++++++++++++ .../gno.land/r/nt/boards2/z_14_f_filetest.gno | 34 +++++++++++++++++ .../gno.land/r/nt/boards2/z_14_g_filetest.gno | 38 +++++++++++++++++++ 8 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 examples/gno.land/r/nt/boards2/z_14_a_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_14_b_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_14_c_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_14_d_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_14_e_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_14_f_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_14_g_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/public.gno b/examples/gno.land/r/nt/boards2/public.gno index dadccf60029..264fd6a2a39 100644 --- a/examples/gno.land/r/nt/boards2/public.gno +++ b/examples/gno.land/r/nt/boards2/public.gno @@ -179,7 +179,7 @@ func DeleteReply(bid BoardID, threadID, replyID PostID) { // Soft delete reply by changing its body when it contains // sub-replies, otherwise hard delete it. if reply.HasReplies() { - reply.Update(reply.GetTitle(), "this reply has been deleted") + reply.Update(reply.GetTitle(), "This reply has been deleted") } else { thread.DeleteReply(replyID) } diff --git a/examples/gno.land/r/nt/boards2/z_14_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_14_a_filetest.gno new file mode 100644 index 00000000000..9dcdb80b5b4 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_14_a_filetest.gno @@ -0,0 +1,31 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + bid boards2.BoardID + tid, rid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") + rid = boards2.CreateReply(bid, tid, 0, "body") +} + +func main() { + boards2.DeleteReply(bid, tid, rid) + + // Ensure reply doesn't exist + println(boards2.Render("test-board/1/2")) +} + +// Output: +// Reply does not exist with ID: 2 diff --git a/examples/gno.land/r/nt/boards2/z_14_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_14_b_filetest.gno new file mode 100644 index 00000000000..f415646846a --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_14_b_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.DeleteReply(404, 1, 1) +} + +// Error: +// board does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/z_14_c_filetest.gno b/examples/gno.land/r/nt/boards2/z_14_c_filetest.gno new file mode 100644 index 00000000000..90b0b57776b --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_14_c_filetest.gno @@ -0,0 +1,23 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var bid boards2.BoardID + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") +} + +func main() { + boards2.DeleteReply(bid, 404, 1) +} + +// Error: +// thread does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/z_14_d_filetest.gno b/examples/gno.land/r/nt/boards2/z_14_d_filetest.gno new file mode 100644 index 00000000000..bb1f299ce1b --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_14_d_filetest.gno @@ -0,0 +1,27 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + bid boards2.BoardID + tid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") +} + +func main() { + boards2.DeleteReply(bid, tid, 404) +} + +// Error: +// reply does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/z_14_e_filetest.gno b/examples/gno.land/r/nt/boards2/z_14_e_filetest.gno new file mode 100644 index 00000000000..36ad1601fb8 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_14_e_filetest.gno @@ -0,0 +1,37 @@ +package main + +import ( + "std" + "strings" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + bid boards2.BoardID + tid, rid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") + + rid = boards2.CreateReply(bid, tid, 0, "Parent") + boards2.CreateReply(bid, tid, rid, "Child reply") +} + +func main() { + boards2.DeleteReply(bid, tid, rid) + + // Render content must contain the releted message instead of reply's body + content := boards2.Render("test-board/1/2") + println(strings.Contains(content, "\n> This reply has been deleted\n")) + println(strings.Contains(content, "\n> > Child reply\n")) +} + +// Output: +// true +// true diff --git a/examples/gno.land/r/nt/boards2/z_14_f_filetest.gno b/examples/gno.land/r/nt/boards2/z_14_f_filetest.gno new file mode 100644 index 00000000000..117c21c35db --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_14_f_filetest.gno @@ -0,0 +1,34 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 +) + +var ( + bid boards2.BoardID + tid, rid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") + rid = boards2.CreateReply(bid, tid, 0, "body") + + // Call using a user that has not permission to delete replies + std.TestSetOrigCaller(user) +} + +func main() { + boards2.DeleteReply(bid, tid, rid) +} + +// Error: +// unauthorized diff --git a/examples/gno.land/r/nt/boards2/z_14_g_filetest.gno b/examples/gno.land/r/nt/boards2/z_14_g_filetest.gno new file mode 100644 index 00000000000..a18c0e065ef --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_14_g_filetest.gno @@ -0,0 +1,38 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 +) + +var ( + bid boards2.BoardID + tid, rid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + tid = boards2.CreateThread(bid, "Foo", "bar") + rid = boards2.CreateReply(bid, tid, 0, "body") + + // Invite a member using a role with permission to delete replies + boards2.InviteMember(bid, member, boards2.RoleAdmin) + std.TestSetOrigCaller(member) +} + +func main() { + boards2.DeleteReply(bid, tid, rid) + + // Ensure reply doesn't exist + println(boards2.Render("test-board/1/2")) +} + +// Output: +// Reply does not exist with ID: 2 From a7c56e24e0c9bd14f0da2a849a26359f62956a6f Mon Sep 17 00:00:00 2001 From: Denys Sedchenko <9203548+x1unix@users.noreply.github.com> Date: Tue, 4 Feb 2025 03:26:33 -0500 Subject: [PATCH 39/52] feat(boards2): repost tests (#3659) * Add `CreateRepost` tests: * sequential repost * repost flagged Related to: https://github.com/gnolang/gno/issues/3623 CC @salmad3 @jeronimoalbi --- .../gno.land/r/nt/boards2/z_15_a_filetest.gno | 32 +++++++++++++++ .../gno.land/r/nt/boards2/z_15_b_filetest.gno | 29 +++++++++++++ .../gno.land/r/nt/boards2/z_15_c_filetest.gno | 34 +++++++++++++++ .../gno.land/r/nt/boards2/z_15_d_filetest.gno | 41 +++++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 examples/gno.land/r/nt/boards2/z_15_a_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_15_b_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_15_c_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/z_15_d_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/z_15_a_filetest.gno b/examples/gno.land/r/nt/boards2/z_15_a_filetest.gno new file mode 100644 index 00000000000..61d15cc3911 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_15_a_filetest.gno @@ -0,0 +1,32 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + srcBid boards2.BoardID + dstBid boards2.BoardID + srcTid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + srcBid = boards2.CreateBoard("src-board") + dstBid = boards2.CreateBoard("dst-board") + + srcTid = boards2.CreateThread(srcBid, "Foo", "bar") + boards2.FlagThread(srcBid, srcTid, "idk") +} + +func main() { + // Repost should fail if source thread is flagged + _ = boards2.CreateRepost(srcBid, srcTid, "foo", "bar", dstBid) +} + +// Error: +// thread has been flagged as inappropriate diff --git a/examples/gno.land/r/nt/boards2/z_15_b_filetest.gno b/examples/gno.land/r/nt/boards2/z_15_b_filetest.gno new file mode 100644 index 00000000000..2a0bd2107dd --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_15_b_filetest.gno @@ -0,0 +1,29 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + srcBid boards2.BoardID + dstBid boards2.BoardID + srcTid boards2.PostID = 1024 +) + +func init() { + std.TestSetOrigCaller(owner) + srcBid = boards2.CreateBoard("src-board") + dstBid = boards2.CreateBoard("dst-board") +} + +func main() { + // Repost should fail if source thread doesn't exist + _ = boards2.CreateRepost(srcBid, srcTid, "foo", "bar", dstBid) +} + +// Error: +// thread does not exist with ID: 1024 diff --git a/examples/gno.land/r/nt/boards2/z_15_c_filetest.gno b/examples/gno.land/r/nt/boards2/z_15_c_filetest.gno new file mode 100644 index 00000000000..6c8834c680a --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_15_c_filetest.gno @@ -0,0 +1,34 @@ +package main + +import ( + "std" + + "gno.land/r/nt/boards2" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 +) + +var ( + srcBid boards2.BoardID + dstBid boards2.BoardID + srcTid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + srcBid = boards2.CreateBoard("src-board") + dstBid = boards2.CreateBoard("dst-board") + + srcTid = boards2.CreateThread(srcBid, "Foo", "bar") + std.TestSetOrigCaller(user) +} + +func main() { + _ = boards2.CreateRepost(srcBid, srcTid, "foo", "bar", dstBid) +} + +// Error: +// unauthorized diff --git a/examples/gno.land/r/nt/boards2/z_15_d_filetest.gno b/examples/gno.land/r/nt/boards2/z_15_d_filetest.gno new file mode 100644 index 00000000000..c5362e486eb --- /dev/null +++ b/examples/gno.land/r/nt/boards2/z_15_d_filetest.gno @@ -0,0 +1,41 @@ +package main + +import ( + "std" + "strings" + + "gno.land/p/demo/ufmt" + "gno.land/r/nt/boards2" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + srcBid boards2.BoardID + dstBid boards2.BoardID + srcTid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + srcBid = boards2.CreateBoard("src-board") + dstBid = boards2.CreateBoard("dst-board") + + srcTid = boards2.CreateThread(srcBid, "original title", "original text") +} + +func main() { + // Success case + tid := boards2.CreateRepost(srcBid, srcTid, "repost title", "repost text", dstBid) + p := ufmt.Sprintf("dst-board/%s", tid) + out := boards2.Render(p) + + println(strings.Contains(out, "original text")) + println(strings.Contains(out, "repost title")) + println(strings.Contains(out, "repost text")) +} + +// Output: +// true +// true +// true From 7a94b18335c3b1b7a7e5b0e223770c4cbaa0f356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Tue, 4 Feb 2025 15:58:18 +0100 Subject: [PATCH 40/52] chore(boards2): move all files into `v1` folder (#3680) --- examples/gno.land/r/nt/boards2/gno.mod | 1 - examples/gno.land/r/nt/boards2/{ => v1}/board.gno | 0 .../gno.land/r/nt/boards2/{ => v1}/board_test.gno | 0 examples/gno.land/r/nt/boards2/{ => v1}/boards.gno | 0 examples/gno.land/r/nt/boards2/{ => v1}/flag.gno | 0 examples/gno.land/r/nt/boards2/{ => v1}/format.gno | 0 examples/gno.land/r/nt/boards2/v1/gno.mod | 1 + .../gno.land/r/nt/boards2/{ => v1}/pagination.gno | 0 .../gno.land/r/nt/boards2/{ => v1}/permission.gno | 0 .../r/nt/boards2/{ => v1}/permission_default.gno | 0 .../boards2/{ => v1}/permission_default_test.gno | 0 .../r/nt/boards2/{ => v1}/permission_options.gno | 0 examples/gno.land/r/nt/boards2/{ => v1}/post.gno | 0 .../gno.land/r/nt/boards2/{ => v1}/post_test.gno | 14 +++++++------- examples/gno.land/r/nt/boards2/{ => v1}/public.gno | 0 examples/gno.land/r/nt/boards2/{ => v1}/render.gno | 0 .../r/nt/boards2/{ => v1}/z_0_a_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_0_b_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_0_c_filetest.gno | 2 +- .../gno.land/r/nt/boards2/v1/z_0_d_filetest.gno | 13 +++++++++++++ .../r/nt/boards2/{ => v1}/z_0_e_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_0_f_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_0_g_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_0_h_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_0_i_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_10_a_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_10_b_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_10_c_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_10_d_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_10_e_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_10_f_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_11_a_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_11_b_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_11_c_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_11_d_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_11_e_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_11_f_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_11_g_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_12_a_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_12_b_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_12_c_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_12_d_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_12_e_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_12_f_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_12_g_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_12_h_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_13_a_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_13_b_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_13_c_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_13_d_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_13_e_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_13_f_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_13_g_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_14_a_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_14_b_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_14_c_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_14_d_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_14_e_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_14_f_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_14_g_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_15_a_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_15_b_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_15_c_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_15_d_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_1_a_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_1_b_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_1_c_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_1_d_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_1_e_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_1_f_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_1_g_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_2_a_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_2_b_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_2_c_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_2_d_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_2_e_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_2_f_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_2_g_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_2_h_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_2_i_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_2_j_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_3_a_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_3_b_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_3_c_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_3_d_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_3_e_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_3_f_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_3_g_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_3_h_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_3_i_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_3_j_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_4_a_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_4_b_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_4_c_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_4_d_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_4_e_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_4_f_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_4_g_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_4_h_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_5_a_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_5_b_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_5_c_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_6_a_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_6_b_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_7_a_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_7_b_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_8_a_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_8_b_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_8_c_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_8_d_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_8_e_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_9_a_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_9_b_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_9_c_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_9_d_filetest.gno | 2 +- .../r/nt/boards2/{ => v1}/z_9_e_filetest.gno | 2 +- examples/gno.land/r/nt/boards2/z_0_d_filetest.gno | 11 ----------- 117 files changed, 120 insertions(+), 118 deletions(-) delete mode 100644 examples/gno.land/r/nt/boards2/gno.mod rename examples/gno.land/r/nt/boards2/{ => v1}/board.gno (100%) rename examples/gno.land/r/nt/boards2/{ => v1}/board_test.gno (100%) rename examples/gno.land/r/nt/boards2/{ => v1}/boards.gno (100%) rename examples/gno.land/r/nt/boards2/{ => v1}/flag.gno (100%) rename examples/gno.land/r/nt/boards2/{ => v1}/format.gno (100%) create mode 100644 examples/gno.land/r/nt/boards2/v1/gno.mod rename examples/gno.land/r/nt/boards2/{ => v1}/pagination.gno (100%) rename examples/gno.land/r/nt/boards2/{ => v1}/permission.gno (100%) rename examples/gno.land/r/nt/boards2/{ => v1}/permission_default.gno (100%) rename examples/gno.land/r/nt/boards2/{ => v1}/permission_default_test.gno (100%) rename examples/gno.land/r/nt/boards2/{ => v1}/permission_options.gno (100%) rename examples/gno.land/r/nt/boards2/{ => v1}/post.gno (100%) rename examples/gno.land/r/nt/boards2/{ => v1}/post_test.gno (96%) rename examples/gno.land/r/nt/boards2/{ => v1}/public.gno (100%) rename examples/gno.land/r/nt/boards2/{ => v1}/render.gno (100%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_0_a_filetest.gno (87%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_0_b_filetest.gno (86%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_0_c_filetest.gno (89%) create mode 100644 examples/gno.land/r/nt/boards2/v1/z_0_d_filetest.gno rename examples/gno.land/r/nt/boards2/{ => v1}/z_0_e_filetest.gno (89%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_0_f_filetest.gno (88%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_0_g_filetest.gno (88%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_0_h_filetest.gno (95%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_0_i_filetest.gno (86%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_10_a_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_10_b_filetest.gno (87%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_10_c_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_10_d_filetest.gno (89%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_10_e_filetest.gno (92%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_10_f_filetest.gno (95%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_11_a_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_11_b_filetest.gno (91%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_11_c_filetest.gno (91%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_11_d_filetest.gno (88%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_11_e_filetest.gno (89%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_11_f_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_11_g_filetest.gno (96%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_12_a_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_12_b_filetest.gno (95%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_12_c_filetest.gno (88%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_12_d_filetest.gno (90%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_12_e_filetest.gno (91%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_12_f_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_12_g_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_12_h_filetest.gno (92%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_13_a_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_13_b_filetest.gno (87%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_13_c_filetest.gno (90%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_13_d_filetest.gno (91%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_13_e_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_13_f_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_13_g_filetest.gno (96%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_14_a_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_14_b_filetest.gno (87%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_14_c_filetest.gno (90%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_14_d_filetest.gno (91%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_14_e_filetest.gno (95%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_14_f_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_14_g_filetest.gno (95%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_15_a_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_15_b_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_15_c_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_15_d_filetest.gno (95%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_1_a_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_1_b_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_1_c_filetest.gno (95%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_1_d_filetest.gno (91%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_1_e_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_1_f_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_1_g_filetest.gno (89%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_2_a_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_2_b_filetest.gno (88%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_2_c_filetest.gno (90%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_2_d_filetest.gno (91%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_2_e_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_2_f_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_2_g_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_2_h_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_2_i_filetest.gno (91%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_2_j_filetest.gno (95%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_3_a_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_3_b_filetest.gno (88%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_3_c_filetest.gno (89%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_3_d_filetest.gno (88%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_3_e_filetest.gno (90%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_3_f_filetest.gno (96%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_3_g_filetest.gno (96%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_3_h_filetest.gno (90%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_3_i_filetest.gno (96%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_3_j_filetest.gno (91%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_4_a_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_4_b_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_4_c_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_4_d_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_4_e_filetest.gno (95%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_4_f_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_4_g_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_4_h_filetest.gno (89%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_5_a_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_5_b_filetest.gno (90%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_5_c_filetest.gno (90%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_6_a_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_6_b_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_7_a_filetest.gno (91%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_7_b_filetest.gno (89%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_8_a_filetest.gno (94%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_8_b_filetest.gno (88%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_8_c_filetest.gno (92%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_8_d_filetest.gno (89%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_8_e_filetest.gno (89%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_9_a_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_9_b_filetest.gno (87%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_9_c_filetest.gno (90%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_9_d_filetest.gno (93%) rename examples/gno.land/r/nt/boards2/{ => v1}/z_9_e_filetest.gno (95%) delete mode 100644 examples/gno.land/r/nt/boards2/z_0_d_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/gno.mod b/examples/gno.land/r/nt/boards2/gno.mod deleted file mode 100644 index 08d15a3e133..00000000000 --- a/examples/gno.land/r/nt/boards2/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/r/nt/boards2 diff --git a/examples/gno.land/r/nt/boards2/board.gno b/examples/gno.land/r/nt/boards2/v1/board.gno similarity index 100% rename from examples/gno.land/r/nt/boards2/board.gno rename to examples/gno.land/r/nt/boards2/v1/board.gno diff --git a/examples/gno.land/r/nt/boards2/board_test.gno b/examples/gno.land/r/nt/boards2/v1/board_test.gno similarity index 100% rename from examples/gno.land/r/nt/boards2/board_test.gno rename to examples/gno.land/r/nt/boards2/v1/board_test.gno diff --git a/examples/gno.land/r/nt/boards2/boards.gno b/examples/gno.land/r/nt/boards2/v1/boards.gno similarity index 100% rename from examples/gno.land/r/nt/boards2/boards.gno rename to examples/gno.land/r/nt/boards2/v1/boards.gno diff --git a/examples/gno.land/r/nt/boards2/flag.gno b/examples/gno.land/r/nt/boards2/v1/flag.gno similarity index 100% rename from examples/gno.land/r/nt/boards2/flag.gno rename to examples/gno.land/r/nt/boards2/v1/flag.gno diff --git a/examples/gno.land/r/nt/boards2/format.gno b/examples/gno.land/r/nt/boards2/v1/format.gno similarity index 100% rename from examples/gno.land/r/nt/boards2/format.gno rename to examples/gno.land/r/nt/boards2/v1/format.gno diff --git a/examples/gno.land/r/nt/boards2/v1/gno.mod b/examples/gno.land/r/nt/boards2/v1/gno.mod new file mode 100644 index 00000000000..fd4698c13b0 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/v1/gno.mod @@ -0,0 +1 @@ +module gno.land/r/nt/boards2/v1 diff --git a/examples/gno.land/r/nt/boards2/pagination.gno b/examples/gno.land/r/nt/boards2/v1/pagination.gno similarity index 100% rename from examples/gno.land/r/nt/boards2/pagination.gno rename to examples/gno.land/r/nt/boards2/v1/pagination.gno diff --git a/examples/gno.land/r/nt/boards2/permission.gno b/examples/gno.land/r/nt/boards2/v1/permission.gno similarity index 100% rename from examples/gno.land/r/nt/boards2/permission.gno rename to examples/gno.land/r/nt/boards2/v1/permission.gno diff --git a/examples/gno.land/r/nt/boards2/permission_default.gno b/examples/gno.land/r/nt/boards2/v1/permission_default.gno similarity index 100% rename from examples/gno.land/r/nt/boards2/permission_default.gno rename to examples/gno.land/r/nt/boards2/v1/permission_default.gno diff --git a/examples/gno.land/r/nt/boards2/permission_default_test.gno b/examples/gno.land/r/nt/boards2/v1/permission_default_test.gno similarity index 100% rename from examples/gno.land/r/nt/boards2/permission_default_test.gno rename to examples/gno.land/r/nt/boards2/v1/permission_default_test.gno diff --git a/examples/gno.land/r/nt/boards2/permission_options.gno b/examples/gno.land/r/nt/boards2/v1/permission_options.gno similarity index 100% rename from examples/gno.land/r/nt/boards2/permission_options.gno rename to examples/gno.land/r/nt/boards2/v1/permission_options.gno diff --git a/examples/gno.land/r/nt/boards2/post.gno b/examples/gno.land/r/nt/boards2/v1/post.gno similarity index 100% rename from examples/gno.land/r/nt/boards2/post.gno rename to examples/gno.land/r/nt/boards2/v1/post.gno diff --git a/examples/gno.land/r/nt/boards2/post_test.gno b/examples/gno.land/r/nt/boards2/v1/post_test.gno similarity index 96% rename from examples/gno.land/r/nt/boards2/post_test.gno rename to examples/gno.land/r/nt/boards2/v1/post_test.gno index cd69025c593..4db4e048100 100644 --- a/examples/gno.land/r/nt/boards2/post_test.gno +++ b/examples/gno.land/r/nt/boards2/v1/post_test.gno @@ -111,23 +111,23 @@ func TestNewThread(t *testing.T) { boardName := "test123" board := newBoard(boardID, boardName, creator) url := ufmt.Sprintf( - "/r/nt/boards2:%s/%d", + "/r/nt/boards2/v1:%s/%d", boardName, uint(threadID), ) replyURL := ufmt.Sprintf( - "/r/nt/boards2$help&func=CreateReply&bid=%d&threadID=%d&replyID=%d", + "/r/nt/boards2/v1$help&func=CreateReply&bid=%d&threadID=%d&replyID=%d", uint(boardID), uint(threadID), uint(threadID), ) repostURL := ufmt.Sprintf( - "/r/nt/boards2$help&func=CreateRepost&bid=%d&threadID=%d", + "/r/nt/boards2/v1$help&func=CreateRepost&bid=%d&threadID=%d", uint(boardID), uint(threadID), ) deleteURL := ufmt.Sprintf( - "/r/nt/boards2$help&func=DeleteThread&bid=%d&threadID=%d", + "/r/nt/boards2/v1$help&func=DeleteThread&bid=%d&threadID=%d", uint(boardID), uint(threadID), ) @@ -275,19 +275,19 @@ func TestNewReply(t *testing.T) { boardName := "test123" board := newBoard(boardID, boardName, creator) url := ufmt.Sprintf( - "/r/nt/boards2:%s/%d/%d", + "/r/nt/boards2/v1:%s/%d/%d", boardName, uint(threadID), uint(replyID), ) replyURL := ufmt.Sprintf( - "/r/nt/boards2$help&func=CreateReply&bid=%d&threadID=%d&replyID=%d", + "/r/nt/boards2/v1$help&func=CreateReply&bid=%d&threadID=%d&replyID=%d", uint(boardID), uint(threadID), uint(replyID), ) deleteURL := ufmt.Sprintf( - "/r/nt/boards2$help&func=DeleteReply&bid=%d&threadID=%d&replyID=%d", + "/r/nt/boards2/v1$help&func=DeleteReply&bid=%d&threadID=%d&replyID=%d", uint(boardID), uint(threadID), uint(replyID), diff --git a/examples/gno.land/r/nt/boards2/public.gno b/examples/gno.land/r/nt/boards2/v1/public.gno similarity index 100% rename from examples/gno.land/r/nt/boards2/public.gno rename to examples/gno.land/r/nt/boards2/v1/public.gno diff --git a/examples/gno.land/r/nt/boards2/render.gno b/examples/gno.land/r/nt/boards2/v1/render.gno similarity index 100% rename from examples/gno.land/r/nt/boards2/render.gno rename to examples/gno.land/r/nt/boards2/v1/render.gno diff --git a/examples/gno.land/r/nt/boards2/z_0_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_a_filetest.gno similarity index 87% rename from examples/gno.land/r/nt/boards2/z_0_a_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_0_a_filetest.gno index 39fcaf264d3..e0e5f7b68d5 100644 --- a/examples/gno.land/r/nt/boards2/z_0_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_0_a_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_0_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_b_filetest.gno similarity index 86% rename from examples/gno.land/r/nt/boards2/z_0_b_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_0_b_filetest.gno index 3ae985acc3c..1d9d93ea68d 100644 --- a/examples/gno.land/r/nt/boards2/z_0_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_0_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_0_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_c_filetest.gno similarity index 89% rename from examples/gno.land/r/nt/boards2/z_0_c_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_0_c_filetest.gno index ce6672be9e3..aa746c01615 100644 --- a/examples/gno.land/r/nt/boards2/z_0_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_0_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/v1/z_0_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_d_filetest.gno new file mode 100644 index 00000000000..df119385f33 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/v1/z_0_d_filetest.gno @@ -0,0 +1,13 @@ +// PKGPATH: gno.land/r/nt/boards2/v1_test +package v1_test + +import ( + boards2 "gno.land/r/nt/boards2/v1" +) + +func main() { + boards2.CreateBoard("foo123") +} + +// Error: +// invalid non-user call diff --git a/examples/gno.land/r/nt/boards2/z_0_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_e_filetest.gno similarity index 89% rename from examples/gno.land/r/nt/boards2/z_0_e_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_0_e_filetest.gno index face81f51cf..7fc2fdbcaec 100644 --- a/examples/gno.land/r/nt/boards2/z_0_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_0_e_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_0_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_f_filetest.gno similarity index 88% rename from examples/gno.land/r/nt/boards2/z_0_f_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_0_f_filetest.gno index 9f7871be625..f457e4c389f 100644 --- a/examples/gno.land/r/nt/boards2/z_0_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_0_f_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_0_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_g_filetest.gno similarity index 88% rename from examples/gno.land/r/nt/boards2/z_0_g_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_0_g_filetest.gno index c835a5877ac..d313e9b33d0 100644 --- a/examples/gno.land/r/nt/boards2/z_0_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_0_g_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_0_h_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_h_filetest.gno similarity index 95% rename from examples/gno.land/r/nt/boards2/z_0_h_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_0_h_filetest.gno index 89da0a08dac..27e68939996 100644 --- a/examples/gno.land/r/nt/boards2/z_0_h_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_0_h_filetest.gno @@ -6,7 +6,7 @@ import ( "std" "gno.land/r/demo/users" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_0_i_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_i_filetest.gno similarity index 86% rename from examples/gno.land/r/nt/boards2/z_0_i_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_0_i_filetest.gno index c54a7e8c1d0..be624e800e9 100644 --- a/examples/gno.land/r/nt/boards2/z_0_i_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_0_i_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 diff --git a/examples/gno.land/r/nt/boards2/z_10_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_10_a_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_10_a_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_10_a_filetest.gno index 8060621c1af..618b63c0bd1 100644 --- a/examples/gno.land/r/nt/boards2/z_10_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_10_a_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_10_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_10_b_filetest.gno similarity index 87% rename from examples/gno.land/r/nt/boards2/z_10_b_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_10_b_filetest.gno index 1c0bdca47c6..68fe6867a33 100644 --- a/examples/gno.land/r/nt/boards2/z_10_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_10_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_10_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_10_c_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_10_c_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_10_c_filetest.gno index aa3ca0d3c37..d0bd55478d7 100644 --- a/examples/gno.land/r/nt/boards2/z_10_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_10_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_10_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_10_d_filetest.gno similarity index 89% rename from examples/gno.land/r/nt/boards2/z_10_d_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_10_d_filetest.gno index 0216c58837c..dc0c7c37936 100644 --- a/examples/gno.land/r/nt/boards2/z_10_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_10_d_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_10_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_10_e_filetest.gno similarity index 92% rename from examples/gno.land/r/nt/boards2/z_10_e_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_10_e_filetest.gno index 3e872a4e152..695680b5953 100644 --- a/examples/gno.land/r/nt/boards2/z_10_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_10_e_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_10_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_10_f_filetest.gno similarity index 95% rename from examples/gno.land/r/nt/boards2/z_10_f_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_10_f_filetest.gno index b04f4a267ed..dba8ec6de95 100644 --- a/examples/gno.land/r/nt/boards2/z_10_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_10_f_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_11_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_11_a_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_11_a_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_11_a_filetest.gno index 795a4172096..84bc0d35160 100644 --- a/examples/gno.land/r/nt/boards2/z_11_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_11_a_filetest.gno @@ -4,7 +4,7 @@ import ( "std" "strings" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_11_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_11_b_filetest.gno similarity index 91% rename from examples/gno.land/r/nt/boards2/z_11_b_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_11_b_filetest.gno index 2c4268afc15..841e251bb1e 100644 --- a/examples/gno.land/r/nt/boards2/z_11_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_11_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_11_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_11_c_filetest.gno similarity index 91% rename from examples/gno.land/r/nt/boards2/z_11_c_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_11_c_filetest.gno index 357c0ac4f94..cb620a6ea90 100644 --- a/examples/gno.land/r/nt/boards2/z_11_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_11_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_11_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_11_d_filetest.gno similarity index 88% rename from examples/gno.land/r/nt/boards2/z_11_d_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_11_d_filetest.gno index 4587b4c64ab..4542911ebec 100644 --- a/examples/gno.land/r/nt/boards2/z_11_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_11_d_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_11_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_11_e_filetest.gno similarity index 89% rename from examples/gno.land/r/nt/boards2/z_11_e_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_11_e_filetest.gno index f0be5da2bb8..55f48e9792a 100644 --- a/examples/gno.land/r/nt/boards2/z_11_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_11_e_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_11_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_11_f_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_11_f_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_11_f_filetest.gno index d2ff805fa08..f6dfd814d2b 100644 --- a/examples/gno.land/r/nt/boards2/z_11_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_11_f_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_11_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_11_g_filetest.gno similarity index 96% rename from examples/gno.land/r/nt/boards2/z_11_g_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_11_g_filetest.gno index 9c2ccd1b4a9..9625a71c8a5 100644 --- a/examples/gno.land/r/nt/boards2/z_11_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_11_g_filetest.gno @@ -4,7 +4,7 @@ import ( "std" "strings" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_12_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_12_a_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_12_a_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_12_a_filetest.gno index 9650bf29bb0..656425e76cc 100644 --- a/examples/gno.land/r/nt/boards2/z_12_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_12_a_filetest.gno @@ -4,7 +4,7 @@ import ( "std" "strings" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_12_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_12_b_filetest.gno similarity index 95% rename from examples/gno.land/r/nt/boards2/z_12_b_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_12_b_filetest.gno index 406d658c731..b1b3efefe71 100644 --- a/examples/gno.land/r/nt/boards2/z_12_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_12_b_filetest.gno @@ -4,7 +4,7 @@ import ( "std" "strings" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_12_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_12_c_filetest.gno similarity index 88% rename from examples/gno.land/r/nt/boards2/z_12_c_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_12_c_filetest.gno index 56501415bb6..bc6916d2ee5 100644 --- a/examples/gno.land/r/nt/boards2/z_12_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_12_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_12_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_12_d_filetest.gno similarity index 90% rename from examples/gno.land/r/nt/boards2/z_12_d_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_12_d_filetest.gno index 373168e6a74..29df7bf4de3 100644 --- a/examples/gno.land/r/nt/boards2/z_12_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_12_d_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_12_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_12_e_filetest.gno similarity index 91% rename from examples/gno.land/r/nt/boards2/z_12_e_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_12_e_filetest.gno index f43d272de8f..0d9bca1fc97 100644 --- a/examples/gno.land/r/nt/boards2/z_12_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_12_e_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_12_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_12_f_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_12_f_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_12_f_filetest.gno index aa8778785a4..5f128041da7 100644 --- a/examples/gno.land/r/nt/boards2/z_12_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_12_f_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_12_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_12_g_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_12_g_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_12_g_filetest.gno index e3506b8c1a1..71c8264cd4e 100644 --- a/examples/gno.land/r/nt/boards2/z_12_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_12_g_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_12_h_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_12_h_filetest.gno similarity index 92% rename from examples/gno.land/r/nt/boards2/z_12_h_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_12_h_filetest.gno index 2fbd84be1b7..ce63bea7b31 100644 --- a/examples/gno.land/r/nt/boards2/z_12_h_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_12_h_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_13_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_13_a_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_13_a_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_13_a_filetest.gno index 40165599c51..0aeeaaae84b 100644 --- a/examples/gno.land/r/nt/boards2/z_13_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_13_a_filetest.gno @@ -4,7 +4,7 @@ import ( "std" "strings" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_13_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_13_b_filetest.gno similarity index 87% rename from examples/gno.land/r/nt/boards2/z_13_b_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_13_b_filetest.gno index 5bef42b97e3..8b2144a5c77 100644 --- a/examples/gno.land/r/nt/boards2/z_13_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_13_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_13_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_13_c_filetest.gno similarity index 90% rename from examples/gno.land/r/nt/boards2/z_13_c_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_13_c_filetest.gno index 6221efffb5e..32afd159bba 100644 --- a/examples/gno.land/r/nt/boards2/z_13_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_13_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_13_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_13_d_filetest.gno similarity index 91% rename from examples/gno.land/r/nt/boards2/z_13_d_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_13_d_filetest.gno index a6557d7daab..039e164a1e2 100644 --- a/examples/gno.land/r/nt/boards2/z_13_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_13_d_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_13_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_13_e_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_13_e_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_13_e_filetest.gno index 437a34a3768..156ba6b7068 100644 --- a/examples/gno.land/r/nt/boards2/z_13_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_13_e_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_13_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_13_f_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_13_f_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_13_f_filetest.gno index c658bc960a6..a401e42237e 100644 --- a/examples/gno.land/r/nt/boards2/z_13_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_13_f_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_13_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_13_g_filetest.gno similarity index 96% rename from examples/gno.land/r/nt/boards2/z_13_g_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_13_g_filetest.gno index 3f7fbe11217..1d24134a2b7 100644 --- a/examples/gno.land/r/nt/boards2/z_13_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_13_g_filetest.gno @@ -4,7 +4,7 @@ import ( "std" "strings" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_14_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_14_a_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_14_a_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_14_a_filetest.gno index 9dcdb80b5b4..104acce005a 100644 --- a/examples/gno.land/r/nt/boards2/z_14_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_14_a_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_14_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_14_b_filetest.gno similarity index 87% rename from examples/gno.land/r/nt/boards2/z_14_b_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_14_b_filetest.gno index f415646846a..e8efd728257 100644 --- a/examples/gno.land/r/nt/boards2/z_14_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_14_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_14_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_14_c_filetest.gno similarity index 90% rename from examples/gno.land/r/nt/boards2/z_14_c_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_14_c_filetest.gno index 90b0b57776b..8def87b12a8 100644 --- a/examples/gno.land/r/nt/boards2/z_14_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_14_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_14_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_14_d_filetest.gno similarity index 91% rename from examples/gno.land/r/nt/boards2/z_14_d_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_14_d_filetest.gno index bb1f299ce1b..61c2187dac5 100644 --- a/examples/gno.land/r/nt/boards2/z_14_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_14_d_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_14_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_14_e_filetest.gno similarity index 95% rename from examples/gno.land/r/nt/boards2/z_14_e_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_14_e_filetest.gno index 36ad1601fb8..9849e68fc13 100644 --- a/examples/gno.land/r/nt/boards2/z_14_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_14_e_filetest.gno @@ -4,7 +4,7 @@ import ( "std" "strings" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_14_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_14_f_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_14_f_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_14_f_filetest.gno index 117c21c35db..eb9909fd147 100644 --- a/examples/gno.land/r/nt/boards2/z_14_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_14_f_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_14_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_14_g_filetest.gno similarity index 95% rename from examples/gno.land/r/nt/boards2/z_14_g_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_14_g_filetest.gno index a18c0e065ef..e44c72b07f2 100644 --- a/examples/gno.land/r/nt/boards2/z_14_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_14_g_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_15_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_15_a_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_15_a_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_15_a_filetest.gno index 61d15cc3911..d8946c9b4be 100644 --- a/examples/gno.land/r/nt/boards2/z_15_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_15_a_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_15_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_15_b_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_15_b_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_15_b_filetest.gno index 2a0bd2107dd..58b9fa60408 100644 --- a/examples/gno.land/r/nt/boards2/z_15_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_15_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_15_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_15_c_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_15_c_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_15_c_filetest.gno index 6c8834c680a..99b1dee6059 100644 --- a/examples/gno.land/r/nt/boards2/z_15_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_15_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_15_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_15_d_filetest.gno similarity index 95% rename from examples/gno.land/r/nt/boards2/z_15_d_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_15_d_filetest.gno index c5362e486eb..bab704c8bbd 100644 --- a/examples/gno.land/r/nt/boards2/z_15_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_15_d_filetest.gno @@ -5,7 +5,7 @@ import ( "strings" "gno.land/p/demo/ufmt" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_1_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_1_a_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_1_a_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_1_a_filetest.gno index d8ab2e8ae84..68e0bd7f8fb 100644 --- a/examples/gno.land/r/nt/boards2/z_1_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_1_a_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_1_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_1_b_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_1_b_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_1_b_filetest.gno index ba41b803f75..efb3787cb91 100644 --- a/examples/gno.land/r/nt/boards2/z_1_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_1_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_1_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_1_c_filetest.gno similarity index 95% rename from examples/gno.land/r/nt/boards2/z_1_c_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_1_c_filetest.gno index 6beaf0774a6..4a4a687356a 100644 --- a/examples/gno.land/r/nt/boards2/z_1_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_1_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_1_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_1_d_filetest.gno similarity index 91% rename from examples/gno.land/r/nt/boards2/z_1_d_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_1_d_filetest.gno index f2a30517286..73c5750cc36 100644 --- a/examples/gno.land/r/nt/boards2/z_1_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_1_d_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_1_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_1_e_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_1_e_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_1_e_filetest.gno index 01e3caa1e87..074a053facd 100644 --- a/examples/gno.land/r/nt/boards2/z_1_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_1_e_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_1_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_1_f_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_1_f_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_1_f_filetest.gno index 7e56d5ad64f..0e2de99c1f7 100644 --- a/examples/gno.land/r/nt/boards2/z_1_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_1_f_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_1_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_1_g_filetest.gno similarity index 89% rename from examples/gno.land/r/nt/boards2/z_1_g_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_1_g_filetest.gno index e4fbd6f4d76..220c886cfd9 100644 --- a/examples/gno.land/r/nt/boards2/z_1_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_1_g_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 diff --git a/examples/gno.land/r/nt/boards2/z_2_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_a_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_2_a_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_2_a_filetest.gno index f9e332d2cca..67d541da339 100644 --- a/examples/gno.land/r/nt/boards2/z_2_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_a_filetest.gno @@ -4,7 +4,7 @@ import ( "std" "strings" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_2_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_b_filetest.gno similarity index 88% rename from examples/gno.land/r/nt/boards2/z_2_b_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_2_b_filetest.gno index 88b2233dff3..15fd82460c2 100644 --- a/examples/gno.land/r/nt/boards2/z_2_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_2_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_c_filetest.gno similarity index 90% rename from examples/gno.land/r/nt/boards2/z_2_c_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_2_c_filetest.gno index 4e0779194ab..e22baf8c86c 100644 --- a/examples/gno.land/r/nt/boards2/z_2_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_2_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_d_filetest.gno similarity index 91% rename from examples/gno.land/r/nt/boards2/z_2_d_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_2_d_filetest.gno index 66edfb3d6f2..6ca1d0d54ae 100644 --- a/examples/gno.land/r/nt/boards2/z_2_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_d_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_2_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_e_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_2_e_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_2_e_filetest.gno index 8c231b552cd..b3050f25387 100644 --- a/examples/gno.land/r/nt/boards2/z_2_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_e_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_2_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_f_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_2_f_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_2_f_filetest.gno index 0b595a84cb6..fe0e27fe6ca 100644 --- a/examples/gno.land/r/nt/boards2/z_2_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_f_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_2_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_g_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_2_g_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_2_g_filetest.gno index 6f69a023a0b..cb70150c881 100644 --- a/examples/gno.land/r/nt/boards2/z_2_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_g_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_2_h_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_h_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_2_h_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_2_h_filetest.gno index e34d8d167ef..0af9369d8b4 100644 --- a/examples/gno.land/r/nt/boards2/z_2_h_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_h_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_2_i_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_i_filetest.gno similarity index 91% rename from examples/gno.land/r/nt/boards2/z_2_i_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_2_i_filetest.gno index aa43805f7fd..f5025273c74 100644 --- a/examples/gno.land/r/nt/boards2/z_2_i_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_i_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_2_j_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_j_filetest.gno similarity index 95% rename from examples/gno.land/r/nt/boards2/z_2_j_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_2_j_filetest.gno index 3449fb1c7ba..f397fb1c843 100644 --- a/examples/gno.land/r/nt/boards2/z_2_j_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_j_filetest.gno @@ -4,7 +4,7 @@ import ( "std" "strings" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_3_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_a_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_3_a_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_3_a_filetest.gno index ecfd3e33e06..d1a4918e3fc 100644 --- a/examples/gno.land/r/nt/boards2/z_3_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_a_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_3_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_b_filetest.gno similarity index 88% rename from examples/gno.land/r/nt/boards2/z_3_b_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_3_b_filetest.gno index a5f521147ea..07ec8f6af49 100644 --- a/examples/gno.land/r/nt/boards2/z_3_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_3_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_c_filetest.gno similarity index 89% rename from examples/gno.land/r/nt/boards2/z_3_c_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_3_c_filetest.gno index 05b27fd3719..997fc18f87c 100644 --- a/examples/gno.land/r/nt/boards2/z_3_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_3_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_d_filetest.gno similarity index 88% rename from examples/gno.land/r/nt/boards2/z_3_d_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_3_d_filetest.gno index c2d60d6b8ca..d4935afff71 100644 --- a/examples/gno.land/r/nt/boards2/z_3_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_d_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_3_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_e_filetest.gno similarity index 90% rename from examples/gno.land/r/nt/boards2/z_3_e_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_3_e_filetest.gno index ad76d398789..71cf8d38864 100644 --- a/examples/gno.land/r/nt/boards2/z_3_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_e_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_3_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_f_filetest.gno similarity index 96% rename from examples/gno.land/r/nt/boards2/z_3_f_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_3_f_filetest.gno index b4f9d07a04b..c12e360c12f 100644 --- a/examples/gno.land/r/nt/boards2/z_3_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_f_filetest.gno @@ -6,7 +6,7 @@ import ( "std" "gno.land/r/demo/users" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_3_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_g_filetest.gno similarity index 96% rename from examples/gno.land/r/nt/boards2/z_3_g_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_3_g_filetest.gno index 0c127148b81..d183b450a62 100644 --- a/examples/gno.land/r/nt/boards2/z_3_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_g_filetest.gno @@ -6,7 +6,7 @@ import ( "std" "gno.land/r/demo/users" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_3_h_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_h_filetest.gno similarity index 90% rename from examples/gno.land/r/nt/boards2/z_3_h_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_3_h_filetest.gno index 36d66e682ca..50327ff33c0 100644 --- a/examples/gno.land/r/nt/boards2/z_3_h_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_h_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_3_i_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_i_filetest.gno similarity index 96% rename from examples/gno.land/r/nt/boards2/z_3_i_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_3_i_filetest.gno index 0c127148b81..d183b450a62 100644 --- a/examples/gno.land/r/nt/boards2/z_3_i_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_i_filetest.gno @@ -6,7 +6,7 @@ import ( "std" "gno.land/r/demo/users" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_3_j_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_j_filetest.gno similarity index 91% rename from examples/gno.land/r/nt/boards2/z_3_j_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_3_j_filetest.gno index 309b029e593..f6eacba7191 100644 --- a/examples/gno.land/r/nt/boards2/z_3_j_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_j_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_4_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_a_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_4_a_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_4_a_filetest.gno index 5cc97b5da0b..7ced83bddb4 100644 --- a/examples/gno.land/r/nt/boards2/z_4_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_a_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_4_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_b_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_4_b_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_4_b_filetest.gno index 9114392a137..d99a4c77e2a 100644 --- a/examples/gno.land/r/nt/boards2/z_4_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_4_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_c_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_4_c_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_4_c_filetest.gno index a71ce1a21b8..ec0f9d741b9 100644 --- a/examples/gno.land/r/nt/boards2/z_4_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_4_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_d_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_4_d_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_4_d_filetest.gno index 6c2678873b6..142db53be5f 100644 --- a/examples/gno.land/r/nt/boards2/z_4_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_d_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_4_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_e_filetest.gno similarity index 95% rename from examples/gno.land/r/nt/boards2/z_4_e_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_4_e_filetest.gno index 80d33faaafa..6c0e42e1512 100644 --- a/examples/gno.land/r/nt/boards2/z_4_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_e_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_4_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_f_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_4_f_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_4_f_filetest.gno index 3ca8769a025..517634d45a4 100644 --- a/examples/gno.land/r/nt/boards2/z_4_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_f_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_4_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_g_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_4_g_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_4_g_filetest.gno index 6dd6acab2f7..3491f5af16d 100644 --- a/examples/gno.land/r/nt/boards2/z_4_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_g_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_4_h_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_h_filetest.gno similarity index 89% rename from examples/gno.land/r/nt/boards2/z_4_h_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_4_h_filetest.gno index 0d9bc8e5303..c60e46b003a 100644 --- a/examples/gno.land/r/nt/boards2/z_4_h_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_h_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 diff --git a/examples/gno.land/r/nt/boards2/z_5_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_5_a_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_5_a_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_5_a_filetest.gno index 77e2ca2dff3..9835326fb8b 100644 --- a/examples/gno.land/r/nt/boards2/z_5_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_5_a_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_5_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_5_b_filetest.gno similarity index 90% rename from examples/gno.land/r/nt/boards2/z_5_b_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_5_b_filetest.gno index 64024f1c2f6..a17c575aaac 100644 --- a/examples/gno.land/r/nt/boards2/z_5_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_5_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 diff --git a/examples/gno.land/r/nt/boards2/z_5_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_5_c_filetest.gno similarity index 90% rename from examples/gno.land/r/nt/boards2/z_5_c_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_5_c_filetest.gno index d75506ec1f5..704f630fc03 100644 --- a/examples/gno.land/r/nt/boards2/z_5_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_5_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_6_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_6_a_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_6_a_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_6_a_filetest.gno index 18c89c7d896..34eac8c34a0 100644 --- a/examples/gno.land/r/nt/boards2/z_6_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_6_a_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_6_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_6_b_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_6_b_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_6_b_filetest.gno index 8344d467a31..26d10c9001e 100644 --- a/examples/gno.land/r/nt/boards2/z_6_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_6_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_7_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_7_a_filetest.gno similarity index 91% rename from examples/gno.land/r/nt/boards2/z_7_a_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_7_a_filetest.gno index 688aaa67244..9a4ece4cf73 100644 --- a/examples/gno.land/r/nt/boards2/z_7_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_7_a_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_7_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_7_b_filetest.gno similarity index 89% rename from examples/gno.land/r/nt/boards2/z_7_b_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_7_b_filetest.gno index 8e8ce18cb87..26cf509a919 100644 --- a/examples/gno.land/r/nt/boards2/z_7_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_7_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_8_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_8_a_filetest.gno similarity index 94% rename from examples/gno.land/r/nt/boards2/z_8_a_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_8_a_filetest.gno index 8a46386b25a..4e0f50fea3b 100644 --- a/examples/gno.land/r/nt/boards2/z_8_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_8_a_filetest.gno @@ -4,7 +4,7 @@ import ( "std" "strings" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_8_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_8_b_filetest.gno similarity index 88% rename from examples/gno.land/r/nt/boards2/z_8_b_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_8_b_filetest.gno index 500ed277f99..18d63bcc992 100644 --- a/examples/gno.land/r/nt/boards2/z_8_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_8_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_8_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_8_c_filetest.gno similarity index 92% rename from examples/gno.land/r/nt/boards2/z_8_c_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_8_c_filetest.gno index 4c2d51141df..37dec2c5d52 100644 --- a/examples/gno.land/r/nt/boards2/z_8_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_8_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_8_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_8_d_filetest.gno similarity index 89% rename from examples/gno.land/r/nt/boards2/z_8_d_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_8_d_filetest.gno index e5347474ae5..d97b61c111c 100644 --- a/examples/gno.land/r/nt/boards2/z_8_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_8_d_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_8_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_8_e_filetest.gno similarity index 89% rename from examples/gno.land/r/nt/boards2/z_8_e_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_8_e_filetest.gno index 97bc013a3d6..41f12ddbf98 100644 --- a/examples/gno.land/r/nt/boards2/z_8_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_8_e_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_9_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_9_a_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_9_a_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_9_a_filetest.gno index 6d1bf46dda7..19f6b5d1a47 100644 --- a/examples/gno.land/r/nt/boards2/z_9_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_9_a_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_9_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_9_b_filetest.gno similarity index 87% rename from examples/gno.land/r/nt/boards2/z_9_b_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_9_b_filetest.gno index 70ae9fcaf6d..1ff0f5439d2 100644 --- a/examples/gno.land/r/nt/boards2/z_9_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_9_b_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_9_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_9_c_filetest.gno similarity index 90% rename from examples/gno.land/r/nt/boards2/z_9_c_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_9_c_filetest.gno index ff33634820c..e7fd465f9d9 100644 --- a/examples/gno.land/r/nt/boards2/z_9_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_9_c_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 diff --git a/examples/gno.land/r/nt/boards2/z_9_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_9_d_filetest.gno similarity index 93% rename from examples/gno.land/r/nt/boards2/z_9_d_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_9_d_filetest.gno index d677c801622..6143406aafc 100644 --- a/examples/gno.land/r/nt/boards2/z_9_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_9_d_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_9_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_9_e_filetest.gno similarity index 95% rename from examples/gno.land/r/nt/boards2/z_9_e_filetest.gno rename to examples/gno.land/r/nt/boards2/v1/z_9_e_filetest.gno index d63a0bd2bfb..d33213c3915 100644 --- a/examples/gno.land/r/nt/boards2/z_9_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_9_e_filetest.gno @@ -3,7 +3,7 @@ package main import ( "std" - "gno.land/r/nt/boards2" + boards2 "gno.land/r/nt/boards2/v1" ) const ( diff --git a/examples/gno.land/r/nt/boards2/z_0_d_filetest.gno b/examples/gno.land/r/nt/boards2/z_0_d_filetest.gno deleted file mode 100644 index 27d8ae6e0dd..00000000000 --- a/examples/gno.land/r/nt/boards2/z_0_d_filetest.gno +++ /dev/null @@ -1,11 +0,0 @@ -// PKGPATH: gno.land/r/nt/boards2_test -package boards2_test - -import "gno.land/r/nt/boards2" - -func main() { - boards2.CreateBoard("foo123") -} - -// Error: -// invalid non-user call From a9c821a0d66eaa4f03241ca35ad6d3f672a96080 Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Mon, 10 Feb 2025 10:55:51 +0100 Subject: [PATCH 41/52] chore: fix issues after latest `master` merge --- examples/gno.land/r/nt/boards2/v1/post_test.gno | 10 +++++----- examples/gno.land/r/nt/boards2/v1/public.gno | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/gno.land/r/nt/boards2/v1/post_test.gno b/examples/gno.land/r/nt/boards2/v1/post_test.gno index 4db4e048100..8eb437325a9 100644 --- a/examples/gno.land/r/nt/boards2/v1/post_test.gno +++ b/examples/gno.land/r/nt/boards2/v1/post_test.gno @@ -116,7 +116,7 @@ func TestNewThread(t *testing.T) { uint(threadID), ) replyURL := ufmt.Sprintf( - "/r/nt/boards2/v1$help&func=CreateReply&bid=%d&threadID=%d&replyID=%d", + "/r/nt/boards2/v1$help&func=CreateReply&bid=%d&replyID=%d&threadID=%d", uint(boardID), uint(threadID), uint(threadID), @@ -281,16 +281,16 @@ func TestNewReply(t *testing.T) { uint(replyID), ) replyURL := ufmt.Sprintf( - "/r/nt/boards2/v1$help&func=CreateReply&bid=%d&threadID=%d&replyID=%d", + "/r/nt/boards2/v1$help&func=CreateReply&bid=%d&replyID=%d&threadID=%d", uint(boardID), - uint(threadID), uint(replyID), + uint(threadID), ) deleteURL := ufmt.Sprintf( - "/r/nt/boards2/v1$help&func=DeleteReply&bid=%d&threadID=%d&replyID=%d", + "/r/nt/boards2/v1$help&func=DeleteReply&bid=%d&replyID=%d&threadID=%d", uint(boardID), - uint(threadID), uint(replyID), + uint(threadID), ) reply := newPost(board, replyID, creator, "", body, threadID, parentID, 0) diff --git a/examples/gno.land/r/nt/boards2/v1/public.gno b/examples/gno.land/r/nt/boards2/v1/public.gno index 264fd6a2a39..2c951551934 100644 --- a/examples/gno.land/r/nt/boards2/v1/public.gno +++ b/examples/gno.land/r/nt/boards2/v1/public.gno @@ -271,7 +271,7 @@ func ChangeMemberRole(bid BoardID, member std.Address, role Role) { } func assertIsUserCall() { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + if !std.PrevRealm().IsUser() { panic("invalid non-user call") } } From c94851814e35e9dec11fdba1a14c0fbaf41e327d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Thu, 13 Feb 2025 14:22:09 +0100 Subject: [PATCH 42/52] feat(boards2): change flagging threshold to be configurable per board (#3724) This enables each board to use a flagging threshold different than the default of 1. Threshold specifies how many flags are required for a thread or comment to be hidden. --- examples/gno.land/r/nt/boards2/v1/board.gno | 1 + examples/gno.land/r/nt/boards2/v1/flag.gno | 23 ++++++++++---- .../gno.land/r/nt/boards2/v1/permission.gno | 27 +++++++++-------- examples/gno.land/r/nt/boards2/v1/public.gno | 26 ++++++++++++++-- .../r/nt/boards2/v1/z_10_g_filetest.gno | 30 +++++++++++++++++++ .../r/nt/boards2/v1/z_16_a_filetest.gno | 26 ++++++++++++++++ .../r/nt/boards2/v1/z_16_b_filetest.gno | 22 ++++++++++++++ .../r/nt/boards2/v1/z_16_c_filetest.gno | 20 +++++++++++++ 8 files changed, 154 insertions(+), 21 deletions(-) create mode 100644 examples/gno.land/r/nt/boards2/v1/z_10_g_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/v1/z_16_a_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/v1/z_16_b_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/v1/z_16_c_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/v1/board.gno b/examples/gno.land/r/nt/boards2/v1/board.gno index 1f7b8a761d1..472437733b2 100644 --- a/examples/gno.land/r/nt/boards2/v1/board.gno +++ b/examples/gno.land/r/nt/boards2/v1/board.gno @@ -162,6 +162,7 @@ func createDefaultBoardPermissions(owner std.Address) *DefaultPermissions { WithRole( RoleAdmin, PermissionBoardRename, + PermissionBoardFlaggingUpdate, PermissionMemberInvite, PermissionMemberRemove, PermissionThreadCreate, diff --git a/examples/gno.land/r/nt/boards2/v1/flag.gno b/examples/gno.land/r/nt/boards2/v1/flag.gno index 902f6263b32..f004145873e 100644 --- a/examples/gno.land/r/nt/boards2/v1/flag.gno +++ b/examples/gno.land/r/nt/boards2/v1/flag.gno @@ -3,10 +3,14 @@ package boards2 import ( "std" "strconv" + + "gno.land/p/demo/avl" ) -// TODO: We should allow changing the threshold, also support value per board -const flagThreshold = 1 +// DefaultFlaggingThreshold defines the default number of flags that hides flaggable items. +const DefaultFlaggingThreshold = 1 + +var gFlaggingThresholds avl.Tree // string(board ID) -> int type Flag struct { User std.Address @@ -35,14 +39,21 @@ type Flaggable interface { // Returns whether flag count threshold is reached and item can be hidden. // // Panics if flag count threshold was already reached. -func flagItem(item Flaggable, flag Flag) bool { - if item.FlagsCount() >= flagThreshold { - panic("item flag count threshold exceeded: " + strconv.Itoa(flagThreshold)) +func flagItem(item Flaggable, flag Flag, threshold int) bool { + if item.FlagsCount() >= threshold { + panic("item flag count threshold exceeded: " + strconv.Itoa(threshold)) } if !item.AddFlag(flag) { panic("item has been already flagged by a current user") } - return item.FlagsCount() == flagThreshold + return item.FlagsCount() == threshold +} + +func getFlaggingThreshold(bid BoardID) int { + if v, ok := gFlaggingThresholds.Get(bid.String()); ok { + return v.(int) + } + return DefaultFlaggingThreshold } diff --git a/examples/gno.land/r/nt/boards2/v1/permission.gno b/examples/gno.land/r/nt/boards2/v1/permission.gno index 1c74c231e1f..f70a4475ecd 100644 --- a/examples/gno.land/r/nt/boards2/v1/permission.gno +++ b/examples/gno.land/r/nt/boards2/v1/permission.gno @@ -7,19 +7,20 @@ import ( ) const ( - PermissionBoardCreate Permission = "board:create" - PermissionBoardRename = "board:rename" - PermissionThreadCreate = "thread:create" - PermissionThreadEdit = "thread:edit" - PermissionThreadDelete = "thread:delete" - PermissionThreadFlag = "thread:flag" - PermissionThreadRepost = "thread:repost" - PermissionReplyCreate = "reply:create" - PermissionReplyDelete = "reply:delete" - PermissionReplyFlag = "reply:flag" - PermissionMemberInvite = "member:invite" - PermissionMemberRemove = "member:remove" - PermissionRoleChange = "role:change" + PermissionBoardCreate Permission = "board:create" + PermissionBoardRename = "board:rename" + PermissionBoardFlaggingUpdate = "board:flagging-update" + PermissionThreadCreate = "thread:create" + PermissionThreadEdit = "thread:edit" + PermissionThreadDelete = "thread:delete" + PermissionThreadFlag = "thread:flag" + PermissionThreadRepost = "thread:repost" + PermissionReplyCreate = "reply:create" + PermissionReplyDelete = "reply:delete" + PermissionReplyFlag = "reply:flag" + PermissionMemberInvite = "member:invite" + PermissionMemberRemove = "member:remove" + PermissionRoleChange = "role:change" ) const ( diff --git a/examples/gno.land/r/nt/boards2/v1/public.gno b/examples/gno.land/r/nt/boards2/v1/public.gno index 2c951551934..6f8e03daba7 100644 --- a/examples/gno.land/r/nt/boards2/v1/public.gno +++ b/examples/gno.land/r/nt/boards2/v1/public.gno @@ -56,6 +56,26 @@ func RenameBoard(name, newName string) { }) } +func SetFlaggingThreshold(bid BoardID, threshold int) { + if threshold < 1 { + panic("invalid flagging threshold") + } + + board := mustGetBoard(bid) + caller := std.GetOrigCaller() + args := Args{bid, threshold} + board.perms.WithPermission(caller, PermissionBoardFlaggingUpdate, args, func(Args) { + assertBoardExists(bid) + + gFlaggingThresholds.Set(bid.String(), threshold) + }) +} + +func GetFlaggingThreshold(bid BoardID) int { + assertBoardExists(bid) + return getFlaggingThreshold(bid) +} + func FlagThread(bid BoardID, postID PostID, reason string) { caller := std.GetOrigCaller() board := mustGetBoard(bid) @@ -66,7 +86,8 @@ func FlagThread(bid BoardID, postID PostID, reason string) { panic("post doesn't exist") } - hide := flagItem(t, NewFlag(caller, reason)) + threshold := getFlaggingThreshold(bid) + hide := flagItem(t, NewFlag(caller, reason), threshold) if hide { t.SetVisible(false) } @@ -124,7 +145,8 @@ func FlagReply(bid BoardID, threadID, replyID PostID, reason string) { thread := mustGetThread(board, threadID) reply := mustGetReply(thread, replyID) - hide := flagItem(reply, NewFlag(caller, reason)) + threshold := getFlaggingThreshold(bid) + hide := flagItem(reply, NewFlag(caller, reason), threshold) if hide { reply.SetVisible(false) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_10_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_10_g_filetest.gno new file mode 100644 index 00000000000..f11f29ef65b --- /dev/null +++ b/examples/gno.land/r/nt/boards2/v1/z_10_g_filetest.gno @@ -0,0 +1,30 @@ +package main + +import ( + "std" + + boards2 "gno.land/r/nt/boards2/v1" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + bid boards2.BoardID + pid boards2.PostID +) + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") + pid = boards2.CreateThread(bid, "Foo", "bar") + + boards2.SetFlaggingThreshold(bid, 2) + boards2.FlagThread(bid, pid, "") +} + +func main() { + boards2.FlagThread(bid, pid, "") +} + +// Error: +// item has been already flagged by a current user diff --git a/examples/gno.land/r/nt/boards2/v1/z_16_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_16_a_filetest.gno new file mode 100644 index 00000000000..f56a9e2b589 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/v1/z_16_a_filetest.gno @@ -0,0 +1,26 @@ +package main + +import ( + "std" + + boards2 "gno.land/r/nt/boards2/v1" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var bid boards2.BoardID + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test-board") +} + +func main() { + boards2.SetFlaggingThreshold(bid, 4) + + // Ensure that flagging threshold changed + println(boards2.GetFlaggingThreshold(bid)) +} + +// Output: +// 4 diff --git a/examples/gno.land/r/nt/boards2/v1/z_16_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_16_b_filetest.gno new file mode 100644 index 00000000000..2856abbcba7 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/v1/z_16_b_filetest.gno @@ -0,0 +1,22 @@ +package main + +import ( + "std" + + boards2 "gno.land/r/nt/boards2/v1" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +const bid boards2.BoardID = 404 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.SetFlaggingThreshold(bid, 1) +} + +// Error: +// board does not exist with ID: 404 diff --git a/examples/gno.land/r/nt/boards2/v1/z_16_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_16_c_filetest.gno new file mode 100644 index 00000000000..d0e0751c8f8 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/v1/z_16_c_filetest.gno @@ -0,0 +1,20 @@ +package main + +import ( + "std" + + boards2 "gno.land/r/nt/boards2/v1" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +func init() { + std.TestSetOrigCaller(owner) +} + +func main() { + boards2.SetFlaggingThreshold(1, 0) +} + +// Error: +// invalid flagging threshold From 828a084f055ad7ccbcfc465ad15466877c00e1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Sat, 15 Feb 2025 12:24:58 +0100 Subject: [PATCH 43/52] chore(boards2): remove legacy user call asserts (#3756) Removing these asserts allows realms to interact with the boards which is convenient when implementing custom permissions/DAO to support custom features. An example case could be a realm that publishes tutorials which could implement the permissions and have a feature where after a tutorial is published would create a thread within a tutorials board to deal with discussions or questions related to it. --- examples/gno.land/r/nt/boards2/v1/public.gno | 30 ------------------- .../r/nt/boards2/v1/z_0_d_filetest.gno | 13 -------- 2 files changed, 43 deletions(-) delete mode 100644 examples/gno.land/r/nt/boards2/v1/z_0_d_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/v1/public.gno b/examples/gno.land/r/nt/boards2/v1/public.gno index 6f8e03daba7..1700cc34b45 100644 --- a/examples/gno.land/r/nt/boards2/v1/public.gno +++ b/examples/gno.land/r/nt/boards2/v1/public.gno @@ -14,8 +14,6 @@ func GetBoardIDFromName(name string) (BoardID, bool) { } func CreateBoard(name string) BoardID { - assertIsUserCall() - name = strings.TrimSpace(name) assertNameIsNotEmpty(name) assertBoardNameNotExists(name) @@ -34,8 +32,6 @@ func CreateBoard(name string) BoardID { } func RenameBoard(name, newName string) { - assertIsUserCall() - newName = strings.TrimSpace(newName) assertNameIsNotEmpty(newName) assertBoardNameNotExists(newName) @@ -94,8 +90,6 @@ func FlagThread(bid BoardID, postID PostID, reason string) { } func CreateThread(bid BoardID, title, body string) PostID { - assertIsUserCall() - title = strings.TrimSpace(title) assertTitleIsNotEmpty(title) @@ -111,8 +105,6 @@ func CreateThread(bid BoardID, title, body string) PostID { } func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { - assertIsUserCall() - body = strings.TrimSpace(body) assertBodyIsNotEmpty(body) @@ -153,8 +145,6 @@ func FlagReply(bid BoardID, threadID, replyID PostID, reason string) { } func CreateRepost(bid BoardID, threadID PostID, title, body string, dstBoardID BoardID) PostID { - assertIsUserCall() - caller := std.GetOrigCaller() dst := mustGetBoard(dstBoardID) assertHasBoardPermission(dst, caller, PermissionThreadRepost) @@ -172,8 +162,6 @@ func CreateRepost(bid BoardID, threadID PostID, title, body string, dstBoardID B } func DeleteThread(bid BoardID, threadID PostID) { - assertIsUserCall() - caller := std.GetOrigCaller() board := mustGetBoard(bid) thread := mustGetThread(board, threadID) @@ -186,8 +174,6 @@ func DeleteThread(bid BoardID, threadID PostID) { } func DeleteReply(bid BoardID, threadID, replyID PostID) { - assertIsUserCall() - board := mustGetBoard(bid) thread := mustGetThread(board, threadID) reply := mustGetReply(thread, replyID) @@ -208,8 +194,6 @@ func DeleteReply(bid BoardID, threadID, replyID PostID) { } func EditThread(bid BoardID, threadID PostID, title, body string) { - assertIsUserCall() - title = strings.TrimSpace(title) assertTitleIsNotEmpty(title) @@ -227,8 +211,6 @@ func EditThread(bid BoardID, threadID PostID, title, body string) { } func EditReply(bid BoardID, threadID, replyID PostID, body string) { - assertIsUserCall() - body = strings.TrimSpace(body) assertBodyIsNotEmpty(body) @@ -245,8 +227,6 @@ func EditReply(bid BoardID, threadID, replyID PostID, body string) { } func InviteMember(bid BoardID, user std.Address, role Role) { - assertIsUserCall() - perms := mustGetPermissions(bid) caller := std.GetOrigCaller() args := Args{user, role} @@ -258,8 +238,6 @@ func InviteMember(bid BoardID, user std.Address, role Role) { } func RemoveMember(bid BoardID, user std.Address) { - assertIsUserCall() - perms := mustGetPermissions(bid) caller := std.GetOrigCaller() perms.WithPermission(caller, PermissionMemberRemove, Args{user}, func(Args) { @@ -280,8 +258,6 @@ func HasMemberRole(bid BoardID, member std.Address, role Role) bool { } func ChangeMemberRole(bid BoardID, member std.Address, role Role) { - assertIsUserCall() - perms := mustGetPermissions(bid) caller := std.GetOrigCaller() args := Args{bid, member, role} @@ -292,12 +268,6 @@ func ChangeMemberRole(bid BoardID, member std.Address, role Role) { }) } -func assertIsUserCall() { - if !std.PrevRealm().IsUser() { - panic("invalid non-user call") - } -} - func assertHasBoardPermission(b *Board, user std.Address, p Permission) { if !b.perms.HasPermission(user, p) { panic("unauthorized") diff --git a/examples/gno.land/r/nt/boards2/v1/z_0_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_d_filetest.gno deleted file mode 100644 index df119385f33..00000000000 --- a/examples/gno.land/r/nt/boards2/v1/z_0_d_filetest.gno +++ /dev/null @@ -1,13 +0,0 @@ -// PKGPATH: gno.land/r/nt/boards2/v1_test -package v1_test - -import ( - boards2 "gno.land/r/nt/boards2/v1" -) - -func main() { - boards2.CreateBoard("foo123") -} - -// Error: -// invalid non-user call From 98002bbd7629731c61deeaaf4c45772d01d77ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Sat, 15 Feb 2025 12:30:05 +0100 Subject: [PATCH 44/52] chore(boards2): add initial documentation to public realm functions (#3754) PR also adds a couple of other small cleanup changes: - Use `boardID` instead of `bid` in public realm functions - Remove private boards legacy code - Simplify flag instantiation --- examples/gno.land/r/nt/boards2/v1/board.gno | 16 +- .../gno.land/r/nt/boards2/v1/board_test.gno | 11 +- examples/gno.land/r/nt/boards2/v1/flag.gno | 7 - .../r/nt/boards2/v1/permission_default.gno | 2 + examples/gno.land/r/nt/boards2/v1/post.gno | 18 +-- .../gno.land/r/nt/boards2/v1/post_test.gno | 21 ++- examples/gno.land/r/nt/boards2/v1/public.gno | 153 ++++++++++++------ 7 files changed, 131 insertions(+), 97 deletions(-) diff --git a/examples/gno.land/r/nt/boards2/v1/board.gno b/examples/gno.land/r/nt/boards2/v1/board.gno index 472437733b2..da8cbd741ba 100644 --- a/examples/gno.land/r/nt/boards2/v1/board.gno +++ b/examples/gno.land/r/nt/boards2/v1/board.gno @@ -46,20 +46,6 @@ func newBoard(id BoardID, name string, creator std.Address) *Board { } } -/* TODO support this once we figure out how to ensure URL correctness. -// A private board is not tracked by gBoards*, -// but must be persisted by the caller's realm. -// Private boards have 0 id and does not ping -// back the remote board on reposts. -func NewPrivateBoard(url string, name string, creator std.Address) *Board { - return newBoard(0, url, name, creator) -} -*/ - -func (board *Board) IsPrivate() bool { - return board.id == 0 -} - func (board *Board) GetID() BoardID { return board.id } @@ -152,7 +138,7 @@ func (board *Board) GetURLFromReplyID(threadID, replyID PostID) string { } func (board *Board) GetPostFormURL() string { - return txlink.Call("CreateThread", "bid", board.id.String()) + return txlink.Call("CreateThread", "boardID", board.id.String()) } func createDefaultBoardPermissions(owner std.Address) *DefaultPermissions { diff --git a/examples/gno.land/r/nt/boards2/v1/board_test.gno b/examples/gno.land/r/nt/boards2/v1/board_test.gno index 2f06b93eefa..f77b55ecc7f 100644 --- a/examples/gno.land/r/nt/boards2/v1/board_test.gno +++ b/examples/gno.land/r/nt/boards2/v1/board_test.gno @@ -21,15 +21,6 @@ func TestBoardID_Key(t *testing.T) { uassert.Equal(t, want, input.Key()) } -func TestBoard_IsPrivate(t *testing.T) { - b := new(Board) - b.id = 0 - uassert.True(t, b.IsPrivate()) - - b.id = 128 - uassert.False(t, b.IsPrivate()) -} - func TestBoard_GetID(t *testing.T) { want := int(92) b := new(Board) @@ -100,7 +91,7 @@ func TestBoard_GetURLFromReplyID(t *testing.T) { func TestBoard_GetPostFormURL(t *testing.T) { bid := BoardID(386) b := newBoard(bid, "foo1234", "") - expect := txlink.Call("CreateThread", "bid", bid.String()) + expect := txlink.Call("CreateThread", "boardID", bid.String()) got := b.GetPostFormURL() uassert.Equal(t, expect, got) } diff --git a/examples/gno.land/r/nt/boards2/v1/flag.gno b/examples/gno.land/r/nt/boards2/v1/flag.gno index f004145873e..4befac42a94 100644 --- a/examples/gno.land/r/nt/boards2/v1/flag.gno +++ b/examples/gno.land/r/nt/boards2/v1/flag.gno @@ -17,13 +17,6 @@ type Flag struct { Reason string } -func NewFlag(creator std.Address, reason string) Flag { - return Flag{ - User: creator, - Reason: reason, - } -} - type Flaggable interface { // AddFlag adds a new flag to an item. // diff --git a/examples/gno.land/r/nt/boards2/v1/permission_default.gno b/examples/gno.land/r/nt/boards2/v1/permission_default.gno index d5fb7963f73..06699364537 100644 --- a/examples/gno.land/r/nt/boards2/v1/permission_default.gno +++ b/examples/gno.land/r/nt/boards2/v1/permission_default.gno @@ -10,6 +10,8 @@ import ( "gno.land/r/demo/users" ) +// TODO: Move to a package or make it private? + // DefaultPermissions manages users, roles and permissions. type DefaultPermissions struct { superRole Role diff --git a/examples/gno.land/r/nt/boards2/v1/post.gno b/examples/gno.land/r/nt/boards2/v1/post.gno index 05883810157..8de64d9165c 100644 --- a/examples/gno.land/r/nt/boards2/v1/post.gno +++ b/examples/gno.land/r/nt/boards2/v1/post.gno @@ -171,9 +171,7 @@ func (post *Post) AddRepostTo(creator std.Address, repost *Post, dst *Board) { post.repostsCount++ dst.threads.Set(repost.id.Key(), repost) - if !dst.IsPrivate() { - post.reposts.Set(dst.id.Key(), repost.id) - } + post.reposts.Set(dst.id.Key(), repost.id) } func (post *Post) DeleteReply(replyID PostID) error { @@ -216,7 +214,7 @@ func (post *Post) GetURL() string { func (post *Post) GetReplyFormURL() string { return txlink.Call("CreateReply", - "bid", post.board.id.String(), + "boardID", post.board.id.String(), "threadID", post.threadID.String(), "replyID", post.id.String(), ) @@ -224,7 +222,7 @@ func (post *Post) GetReplyFormURL() string { func (post *Post) GetRepostFormURL() string { return txlink.Call("CreateRepost", - "bid", post.board.id.String(), + "boardID", post.board.id.String(), "threadID", post.id.String(), ) } @@ -232,12 +230,12 @@ func (post *Post) GetRepostFormURL() string { func (post *Post) GetDeleteFormURL() string { if post.IsThread() { return txlink.Call("DeleteThread", - "bid", post.board.id.String(), + "boardID", post.board.id.String(), "threadID", post.threadID.String(), ) } return txlink.Call("DeleteReply", - "bid", post.board.id.String(), + "boardID", post.board.id.String(), "threadID", post.threadID.String(), "replyID", post.id.String(), ) @@ -246,13 +244,13 @@ func (post *Post) GetDeleteFormURL() string { func (post *Post) GetFlagFormURL() string { if post.IsThread() { return txlink.Call("FlagThread", - "bid", post.board.id.String(), - "postID", post.threadID.String(), + "boardID", post.board.id.String(), + "threadID", post.threadID.String(), ) } return txlink.Call("FlagReply", - "bid", post.board.id.String(), + "boardID", post.board.id.String(), "threadID", post.threadID.String(), "replyID", post.id.String(), ) diff --git a/examples/gno.land/r/nt/boards2/v1/post_test.gno b/examples/gno.land/r/nt/boards2/v1/post_test.gno index 8eb437325a9..fc6070a1db7 100644 --- a/examples/gno.land/r/nt/boards2/v1/post_test.gno +++ b/examples/gno.land/r/nt/boards2/v1/post_test.gno @@ -27,7 +27,10 @@ func TestPostAddFlag(t *testing.T) { addr := testutils.TestAddress("creator") post := createTestThread(t) - flag := NewFlag(addr, "foobar") + flag := Flag{ + User: addr, + Reason: "foobar", + } uassert.True(t, post.AddFlag(flag)) uassert.False(t, post.AddFlag(flag), "should reject flag from duplicate user") uassert.Equal(t, post.FlagsCount(), 1) @@ -116,18 +119,23 @@ func TestNewThread(t *testing.T) { uint(threadID), ) replyURL := ufmt.Sprintf( - "/r/nt/boards2/v1$help&func=CreateReply&bid=%d&replyID=%d&threadID=%d", + "/r/nt/boards2/v1$help&func=CreateReply&boardID=%d&replyID=%d&threadID=%d", uint(boardID), uint(threadID), uint(threadID), ) repostURL := ufmt.Sprintf( - "/r/nt/boards2/v1$help&func=CreateRepost&bid=%d&threadID=%d", + "/r/nt/boards2/v1$help&func=CreateRepost&boardID=%d&threadID=%d", uint(boardID), uint(threadID), ) deleteURL := ufmt.Sprintf( - "/r/nt/boards2/v1$help&func=DeleteThread&bid=%d&threadID=%d", + "/r/nt/boards2/v1$help&func=DeleteThread&boardID=%d&threadID=%d", + uint(boardID), + uint(threadID), + ) + flagURL := ufmt.Sprintf( + "/r/nt/boards2/v1$help&func=FlagThread&boardID=%d&threadID=%d", uint(boardID), uint(threadID), ) @@ -145,6 +153,7 @@ func TestNewThread(t *testing.T) { uassert.Equal(t, replyURL, thread.GetReplyFormURL()) uassert.Equal(t, repostURL, thread.GetRepostFormURL()) uassert.Equal(t, deleteURL, thread.GetDeleteFormURL()) + uassert.Equal(t, flagURL, thread.GetFlagFormURL()) } func TestThreadAddReply(t *testing.T) { @@ -281,13 +290,13 @@ func TestNewReply(t *testing.T) { uint(replyID), ) replyURL := ufmt.Sprintf( - "/r/nt/boards2/v1$help&func=CreateReply&bid=%d&replyID=%d&threadID=%d", + "/r/nt/boards2/v1$help&func=CreateReply&boardID=%d&replyID=%d&threadID=%d", uint(boardID), uint(replyID), uint(threadID), ) deleteURL := ufmt.Sprintf( - "/r/nt/boards2/v1$help&func=DeleteReply&bid=%d&replyID=%d&threadID=%d", + "/r/nt/boards2/v1$help&func=DeleteReply&boardID=%d&replyID=%d&threadID=%d", uint(boardID), uint(replyID), uint(threadID), diff --git a/examples/gno.land/r/nt/boards2/v1/public.gno b/examples/gno.land/r/nt/boards2/v1/public.gno index 1700cc34b45..b642cbefe30 100644 --- a/examples/gno.land/r/nt/boards2/v1/public.gno +++ b/examples/gno.land/r/nt/boards2/v1/public.gno @@ -5,7 +5,8 @@ import ( "strings" ) -func GetBoardIDFromName(name string) (BoardID, bool) { +// GetBoardIDFromName searches a board by name and returns it's ID. +func GetBoardIDFromName(name string) (_ BoardID, found bool) { v, found := gBoardsByName.Get(name) if !found { return 0, false @@ -13,6 +14,7 @@ func GetBoardIDFromName(name string) (BoardID, bool) { return v.(*Board).id, true } +// CreateBoard creates a new board. func CreateBoard(name string) BoardID { name = strings.TrimSpace(name) assertNameIsNotEmpty(name) @@ -31,6 +33,10 @@ func CreateBoard(name string) BoardID { return id } +// RenameBoard changes the name of an existing board. +// +// A history of previous board names is kept when boards are renamed. +// Because of that boards are also accesible using previous name(s). func RenameBoard(name, newName string) { newName = strings.TrimSpace(newName) assertNameIsNotEmpty(newName) @@ -52,44 +58,56 @@ func RenameBoard(name, newName string) { }) } -func SetFlaggingThreshold(bid BoardID, threshold int) { +// SetFlaggingThreshold sets the number of flags required to hide a thread or comment. +// +// Threshold is only applicable within the board where it's setted. +func SetFlaggingThreshold(boardID BoardID, threshold int) { if threshold < 1 { panic("invalid flagging threshold") } - board := mustGetBoard(bid) + board := mustGetBoard(boardID) caller := std.GetOrigCaller() - args := Args{bid, threshold} + args := Args{boardID, threshold} board.perms.WithPermission(caller, PermissionBoardFlaggingUpdate, args, func(Args) { - assertBoardExists(bid) + assertBoardExists(boardID) - gFlaggingThresholds.Set(bid.String(), threshold) + gFlaggingThresholds.Set(boardID.String(), threshold) }) } -func GetFlaggingThreshold(bid BoardID) int { - assertBoardExists(bid) - return getFlaggingThreshold(bid) +// GetFlaggingThreshold returns the number of flags required to hide a thread or comment within a board. +func GetFlaggingThreshold(boardID BoardID) int { + assertBoardExists(boardID) + return getFlaggingThreshold(boardID) } -func FlagThread(bid BoardID, postID PostID, reason string) { +// FlagThread adds a new flag to a thread. +// +// Flagging requires special permissions and hides the thread when +// the number of flags reaches a pre-defined flagging threshold. +func FlagThread(boardID BoardID, threadID PostID, reason string) { caller := std.GetOrigCaller() - board := mustGetBoard(bid) + board := mustGetBoard(boardID) assertHasBoardPermission(board, caller, PermissionThreadFlag) - t, ok := board.GetThread(postID) + t, ok := board.GetThread(threadID) if !ok { panic("post doesn't exist") } - threshold := getFlaggingThreshold(bid) - hide := flagItem(t, NewFlag(caller, reason), threshold) + f := Flag{ + User: caller, + Reason: reason, + } + hide := flagItem(t, f, getFlaggingThreshold(boardID)) if hide { t.SetVisible(false) } } -func CreateThread(bid BoardID, title, body string) PostID { +// CreateThread creates a new thread within a board. +func CreateThread(boardID BoardID, title, body string) PostID { title = strings.TrimSpace(title) assertTitleIsNotEmpty(title) @@ -97,19 +115,22 @@ func CreateThread(bid BoardID, title, body string) PostID { assertBodyIsNotEmpty(body) caller := std.GetOrigCaller() - board := mustGetBoard(bid) + board := mustGetBoard(boardID) assertHasBoardPermission(board, caller, PermissionThreadCreate) thread := board.AddThread(caller, title, body) return thread.id } -func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { +// CreateReply creates a new comment or reply within a thread. +// +// The value of `replyID` is only required when creating a reply of another reply. +func CreateReply(boardID BoardID, threadID, replyID PostID, body string) PostID { body = strings.TrimSpace(body) assertBodyIsNotEmpty(body) caller := std.GetOrigCaller() - board := mustGetBoard(bid) + board := mustGetBoard(boardID) assertHasBoardPermission(board, caller, PermissionReplyCreate) thread := mustGetThread(board, threadID) @@ -129,31 +150,35 @@ func CreateReply(bid BoardID, threadID, replyID PostID, body string) PostID { return reply.id } -func FlagReply(bid BoardID, threadID, replyID PostID, reason string) { +// FlagReply adds a new flag to a comment or reply. +// +// Flagging requires special permissions and hides the comment or reply +// when the number of flags reaches a pre-defined flagging threshold. +func FlagReply(boardID BoardID, threadID, replyID PostID, reason string) { caller := std.GetOrigCaller() - board := mustGetBoard(bid) + board := mustGetBoard(boardID) assertHasBoardPermission(board, caller, PermissionThreadFlag) thread := mustGetThread(board, threadID) reply := mustGetReply(thread, replyID) - threshold := getFlaggingThreshold(bid) - hide := flagItem(reply, NewFlag(caller, reason), threshold) + f := Flag{ + User: caller, + Reason: reason, + } + hide := flagItem(reply, f, getFlaggingThreshold(boardID)) if hide { reply.SetVisible(false) } } -func CreateRepost(bid BoardID, threadID PostID, title, body string, dstBoardID BoardID) PostID { +// CreateRepost reposts a thread into another board. +func CreateRepost(boardID BoardID, threadID PostID, title, body string, dstBoardID BoardID) PostID { caller := std.GetOrigCaller() dst := mustGetBoard(dstBoardID) assertHasBoardPermission(dst, caller, PermissionThreadRepost) - board := mustGetBoard(bid) - if board.IsPrivate() { - panic("cannot repost from a private board") - } - + board := mustGetBoard(boardID) thread := mustGetThread(board, threadID) repostId := dst.incGetPostID() repost := newPost(dst, repostId, caller, title, body, repostId, thread.GetPostID(), thread.GetBoard().GetID()) @@ -161,20 +186,28 @@ func CreateRepost(bid BoardID, threadID PostID, title, body string, dstBoardID B return repostId } -func DeleteThread(bid BoardID, threadID PostID) { +// DeleteThread deletes a thread from a board. +// +// Threads can be deleted by the users who created them or otherwise by users with special permissions. +func DeleteThread(boardID BoardID, threadID PostID) { caller := std.GetOrigCaller() - board := mustGetBoard(bid) + board := mustGetBoard(boardID) thread := mustGetThread(board, threadID) if caller != thread.GetCreator() { assertHasBoardPermission(board, caller, PermissionThreadDelete) } - // TODO: Discuss how to deal with thread deletion + // TODO: Discuss how to deal with thread deletion (should we hide instead?) board.DeleteThread(threadID) } -func DeleteReply(bid BoardID, threadID, replyID PostID) { - board := mustGetBoard(bid) +// DeleteReply deletes a reply from a thread. +// +// Replies can be deleted by the users who created them or otherwise by users with special permissions. +// Soft deletion is used when the deleted reply contains sub replies, in which case the reply content +// is replaced by a text informing that reply has been deleted to avoid deleting sub-replies. +func DeleteReply(boardID BoardID, threadID, replyID PostID) { + board := mustGetBoard(boardID) thread := mustGetThread(board, threadID) reply := mustGetReply(thread, replyID) assertReplyVisible(reply) @@ -193,14 +226,17 @@ func DeleteReply(bid BoardID, threadID, replyID PostID) { } } -func EditThread(bid BoardID, threadID PostID, title, body string) { +// EditThread updates the title and body of thread. +// +// Threads can be updated by the users who created them or otherwise by users with special permissions. +func EditThread(boardID BoardID, threadID PostID, title, body string) { title = strings.TrimSpace(title) assertTitleIsNotEmpty(title) body = strings.TrimSpace(body) assertBodyIsNotEmpty(body) - board := mustGetBoard(bid) + board := mustGetBoard(boardID) thread := mustGetThread(board, threadID) caller := std.GetOrigCaller() if caller != thread.GetCreator() { @@ -210,11 +246,14 @@ func EditThread(bid BoardID, threadID PostID, title, body string) { thread.Update(title, body) } -func EditReply(bid BoardID, threadID, replyID PostID, body string) { +// EditReply updates the body of comment or reply. +// +// Replies can be updated only by the users who created them. +func EditReply(boardID BoardID, threadID, replyID PostID, body string) { body = strings.TrimSpace(body) assertBodyIsNotEmpty(body) - board := mustGetBoard(bid) + board := mustGetBoard(boardID) thread := mustGetThread(board, threadID) reply := mustGetReply(thread, replyID) assertReplyVisible(reply) @@ -226,8 +265,12 @@ func EditReply(bid BoardID, threadID, replyID PostID, body string) { reply.Update("", body) } -func InviteMember(bid BoardID, user std.Address, role Role) { - perms := mustGetPermissions(bid) +// InviteMember adds a member to the realm or to a boards. +// +// A role can optionally be specified to be assigned to the new member. +// Board ID is only required when inviting a member to a board. +func InviteMember(boardID BoardID, user std.Address, role Role) { + perms := mustGetPermissions(boardID) caller := std.GetOrigCaller() args := Args{user, role} perms.WithPermission(caller, PermissionMemberInvite, args, func(Args) { @@ -237,8 +280,11 @@ func InviteMember(bid BoardID, user std.Address, role Role) { }) } -func RemoveMember(bid BoardID, user std.Address) { - perms := mustGetPermissions(bid) +// RemoveMember removes a member from the realm or a boards. +// +// Board ID is only required when removing a member from board. +func RemoveMember(boardID BoardID, user std.Address) { + perms := mustGetPermissions(boardID) caller := std.GetOrigCaller() perms.WithPermission(caller, PermissionMemberRemove, Args{user}, func(Args) { if !perms.RemoveUser(user) { @@ -247,20 +293,29 @@ func RemoveMember(bid BoardID, user std.Address) { }) } -func IsMember(bid BoardID, member std.Address) bool { - perms := mustGetPermissions(bid) - return perms.HasUser(member) +// IsMember checks if an user is a member of the realm or a board. +// +// Board ID is only required when checking if a user is a member of a board. +func IsMember(boardID BoardID, user std.Address) bool { + perms := mustGetPermissions(boardID) + return perms.HasUser(user) } -func HasMemberRole(bid BoardID, member std.Address, role Role) bool { - perms := mustGetPermissions(bid) +// HasMemberRole checks if a realm or board member has a specific role assigned. +// +// Board ID is only required when checking a member of a board. +func HasMemberRole(boardID BoardID, member std.Address, role Role) bool { + perms := mustGetPermissions(boardID) return perms.HasRole(member, role) } -func ChangeMemberRole(bid BoardID, member std.Address, role Role) { - perms := mustGetPermissions(bid) +// ChangeMemberRole changes the role of a realm or board member. +// +// Board ID is only required when changing the role for a member of a board. +func ChangeMemberRole(boardID BoardID, member std.Address, role Role) { + perms := mustGetPermissions(boardID) caller := std.GetOrigCaller() - args := Args{bid, member, role} + args := Args{boardID, member, role} perms.WithPermission(caller, PermissionRoleChange, args, func(Args) { if err := perms.SetUserRoles(member, role); err != nil { panic(err) From c1e595916236d6a81aa6343af31a2936c123664e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Sat, 15 Feb 2025 12:30:59 +0100 Subject: [PATCH 45/52] chore(boards2): simplify boards realm permissions (#3717) Realm permissions should have minimal permissions. Boards should be treated as independent communities. At this point it makes sense to only have two roles for the boards realm permissions, owners and admins, where admins can only create boards. Owners are the ones that can manage members in addition to be able to create new boards. Realm permissions could have more roles and permissions in the future when new features are introduced. --- .../r/nt/boards2/v1/permission_default.gno | 30 +------------------ .../r/nt/boards2/v1/z_1_b_filetest.gno | 7 +++-- .../r/nt/boards2/v1/z_1_c_filetest.gno | 7 +++-- .../r/nt/boards2/v1/z_4_a_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_4_c_filetest.gno | 5 +++- .../r/nt/boards2/v1/z_4_d_filetest.gno | 5 +++- .../r/nt/boards2/v1/z_4_f_filetest.gno | 5 +++- .../r/nt/boards2/v1/z_4_g_filetest.gno | 5 +++- .../r/nt/boards2/v1/z_5_a_filetest.gno | 5 +++- .../r/nt/boards2/v1/z_6_a_filetest.gno | 5 +++- 10 files changed, 37 insertions(+), 41 deletions(-) diff --git a/examples/gno.land/r/nt/boards2/v1/permission_default.gno b/examples/gno.land/r/nt/boards2/v1/permission_default.gno index 06699364537..b6b36f16693 100644 --- a/examples/gno.land/r/nt/boards2/v1/permission_default.gno +++ b/examples/gno.land/r/nt/boards2/v1/permission_default.gno @@ -230,35 +230,7 @@ func createDefaultPermissions(owner std.Address) *DefaultPermissions { return NewDefaultPermissions( commondao.New(), WithSuperRole(RoleOwner), - WithRole( - RoleAdmin, - PermissionBoardCreate, - PermissionBoardRename, - PermissionMemberInvite, - PermissionMemberRemove, - PermissionThreadCreate, - PermissionThreadEdit, - PermissionThreadDelete, - PermissionThreadFlag, - PermissionReplyCreate, - PermissionReplyDelete, - PermissionReplyFlag, - PermissionRoleChange, - ), - WithRole( - RoleModerator, - PermissionThreadCreate, - PermissionThreadEdit, - PermissionThreadFlag, - PermissionReplyCreate, - PermissionReplyFlag, - ), - WithRole( - RoleGuest, - PermissionThreadCreate, - PermissionThreadRepost, - PermissionReplyCreate, - ), + WithRole(RoleAdmin, PermissionBoardCreate), WithUser(owner, RoleOwner), ) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_1_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_1_b_filetest.gno index efb3787cb91..34465983f96 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_1_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_1_b_filetest.gno @@ -10,12 +10,15 @@ const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 user = std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") - bid = boards2.BoardID(0) // Operate on realm DAO instead of individual boards ) +var bid boards2.BoardID // Operate on board DAO + func init() { - // Add an admin member std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") + + // Add an admin member boards2.InviteMember(bid, admin, boards2.RoleAdmin) // Next call will be done by the admin member diff --git a/examples/gno.land/r/nt/boards2/v1/z_1_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_1_c_filetest.gno index 4a4a687356a..75cea64ca1a 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_1_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_1_c_filetest.gno @@ -10,13 +10,16 @@ const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 user = std.Address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn") - bid = boards2.BoardID(0) // Operate on realm DAO instead of individual boards role = boards2.RoleAdmin ) +var bid boards2.BoardID // Operate on board DAO + func init() { - // Add an admin member std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") + + // Add an admin member boards2.InviteMember(bid, admin, boards2.RoleAdmin) // Next call will be done by the admin member diff --git a/examples/gno.land/r/nt/boards2/v1/z_4_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_a_filetest.gno index 7ced83bddb4..c4321d8bc30 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_4_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_a_filetest.gno @@ -9,13 +9,13 @@ import ( const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 - newRole = boards2.RoleAdmin + newRole = boards2.RoleOwner bid = boards2.BoardID(0) // Operate on realm DAO instead of individual boards ) func init() { std.TestSetOrigCaller(owner) - boards2.InviteMember(bid, member, boards2.RoleGuest) + boards2.InviteMember(bid, member, boards2.RoleAdmin) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_4_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_c_filetest.gno index ec0f9d741b9..a9d05bb3bc6 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_4_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_c_filetest.gno @@ -10,11 +10,14 @@ const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 owner2 = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 admin = std.Address("g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5") - bid = boards2.BoardID(0) // Operate on realm DAO instead of individual boards ) +var bid boards2.BoardID // Operate on board DAO + func init() { std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") + boards2.InviteMember(bid, owner2, boards2.RoleOwner) boards2.InviteMember(bid, admin, boards2.RoleAdmin) diff --git a/examples/gno.land/r/nt/boards2/v1/z_4_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_d_filetest.gno index 142db53be5f..6adcefc9a3d 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_4_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_d_filetest.gno @@ -10,11 +10,14 @@ const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 admin2 = std.Address("g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5") - bid = boards2.BoardID(0) // Operate on realm DAO instead of individual boards ) +var bid boards2.BoardID // Operate on board DAO + func init() { std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") + boards2.InviteMember(bid, admin, boards2.RoleAdmin) boards2.InviteMember(bid, admin2, boards2.RoleAdmin) diff --git a/examples/gno.land/r/nt/boards2/v1/z_4_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_f_filetest.gno index 517634d45a4..39764bf8442 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_4_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_f_filetest.gno @@ -9,11 +9,14 @@ import ( const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 - bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards ) +var bid boards2.BoardID // Operate on board DAO + func init() { std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") + boards2.InviteMember(bid, admin, boards2.RoleGuest) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_4_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_g_filetest.gno index 3491f5af16d..686a4e3d796 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_4_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_g_filetest.gno @@ -9,11 +9,14 @@ import ( const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 - bid = boards2.BoardID(0) // Operate on realm DAO members instead of individual boards ) +var bid boards2.BoardID // Operate on board DAO + func init() { std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") + boards2.InviteMember(bid, admin, boards2.RoleGuest) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_5_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_5_a_filetest.gno index 9835326fb8b..2672ddd1934 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_5_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_5_a_filetest.gno @@ -9,11 +9,14 @@ import ( const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 - bid = boards2.BoardID(0) // Operate on realm DAO instead of individual boards ) +var bid boards2.BoardID // Operate on board DAO + func init() { std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") + boards2.InviteMember(bid, user, boards2.RoleGuest) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_6_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_6_a_filetest.gno index 34eac8c34a0..3783b4519e4 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_6_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_6_a_filetest.gno @@ -9,12 +9,15 @@ import ( const ( owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 member = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 - bid = boards2.BoardID(0) // Operate on realm DAO instead of individual boards role = boards2.RoleGuest ) +var bid boards2.BoardID // Operate on board DAO + func init() { std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") + boards2.InviteMember(bid, member, role) } From fed38e6d838435bc474da107ff0ce09d17db904a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Mon, 17 Feb 2025 11:21:30 +0100 Subject: [PATCH 46/52] feat(boards2): add support to freeze individual boards (#3755) Depends on #3754 --- examples/gno.land/r/nt/boards2/v1/board.gno | 11 +++ .../gno.land/r/nt/boards2/v1/permission.gno | 1 + examples/gno.land/r/nt/boards2/v1/public.gno | 83 +++++++++++++++++-- .../r/nt/boards2/v1/z_17_a_filetest.gno | 24 ++++++ .../r/nt/boards2/v1/z_17_b_filetest.gno | 24 ++++++ 5 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 examples/gno.land/r/nt/boards2/v1/z_17_a_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/v1/z_17_b_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/v1/board.gno b/examples/gno.land/r/nt/boards2/v1/board.gno index da8cbd741ba..7df374b53c0 100644 --- a/examples/gno.land/r/nt/boards2/v1/board.gno +++ b/examples/gno.land/r/nt/boards2/v1/board.gno @@ -32,6 +32,7 @@ type Board struct { createdAt time.Time deleted avl.Tree // TODO reserved for fast-delete. perms Permissions + readOnly bool } func newBoard(id BoardID, name string, creator std.Address) *Board { @@ -66,6 +67,16 @@ func (board *Board) GetPermissions() Permissions { return board.perms } +// SetReadOnly updates board's read-only status. +func (board *Board) SetReadOnly(readOnly bool) { + board.readOnly = readOnly +} + +// IsReadOnly checks if the board is a read-only board. +func (board *Board) IsReadOnly() bool { + return board.readOnly +} + func (board *Board) GetThread(threadID PostID) (_ *Post, found bool) { v, found := board.threads.Get(threadID.Key()) if !found { diff --git a/examples/gno.land/r/nt/boards2/v1/permission.gno b/examples/gno.land/r/nt/boards2/v1/permission.gno index f70a4475ecd..e707b1f6e47 100644 --- a/examples/gno.land/r/nt/boards2/v1/permission.gno +++ b/examples/gno.land/r/nt/boards2/v1/permission.gno @@ -10,6 +10,7 @@ const ( PermissionBoardCreate Permission = "board:create" PermissionBoardRename = "board:rename" PermissionBoardFlaggingUpdate = "board:flagging-update" + PermissionBoardFreeze = "board:freeze" PermissionThreadCreate = "thread:create" PermissionThreadEdit = "thread:edit" PermissionThreadDelete = "thread:delete" diff --git a/examples/gno.land/r/nt/boards2/v1/public.gno b/examples/gno.land/r/nt/boards2/v1/public.gno index b642cbefe30..eaa4ef78293 100644 --- a/examples/gno.land/r/nt/boards2/v1/public.gno +++ b/examples/gno.land/r/nt/boards2/v1/public.gno @@ -43,6 +43,8 @@ func RenameBoard(name, newName string) { assertBoardNameNotExists(newName) board := mustGetBoardByName(name) + assertBoardIsNotFrozen(board) + bid := board.GetID() caller := std.GetOrigCaller() args := Args{bid, name, newName} @@ -58,6 +60,27 @@ func RenameBoard(name, newName string) { }) } +// FreezeBoard freezes a board so no more threads and comments can be created or modified. +func FreezeBoard(boardID BoardID) { + board := mustGetBoard(boardID) + assertBoardIsNotFrozen(board) + + caller := std.GetOrigCaller() + args := Args{boardID} + board.perms.WithPermission(caller, PermissionBoardFreeze, args, func(Args) { + board := mustGetBoard(boardID) + assertBoardIsNotFrozen(board) + + board.SetReadOnly(true) + }) +} + +// IsBoardFrozen checks if a board has been frozen. +func IsBoardFrozen(boardID BoardID) bool { + board := mustGetBoard(boardID) + return board.IsReadOnly() +} + // SetFlaggingThreshold sets the number of flags required to hide a thread or comment. // // Threshold is only applicable within the board where it's setted. @@ -67,6 +90,8 @@ func SetFlaggingThreshold(boardID BoardID, threshold int) { } board := mustGetBoard(boardID) + assertBoardIsNotFrozen(board) + caller := std.GetOrigCaller() args := Args{boardID, threshold} board.perms.WithPermission(caller, PermissionBoardFlaggingUpdate, args, func(Args) { @@ -87,8 +112,10 @@ func GetFlaggingThreshold(boardID BoardID) int { // Flagging requires special permissions and hides the thread when // the number of flags reaches a pre-defined flagging threshold. func FlagThread(boardID BoardID, threadID PostID, reason string) { - caller := std.GetOrigCaller() board := mustGetBoard(boardID) + assertBoardIsNotFrozen(board) + + caller := std.GetOrigCaller() assertHasBoardPermission(board, caller, PermissionThreadFlag) t, ok := board.GetThread(threadID) @@ -114,8 +141,10 @@ func CreateThread(boardID BoardID, title, body string) PostID { body = strings.TrimSpace(body) assertBodyIsNotEmpty(body) - caller := std.GetOrigCaller() board := mustGetBoard(boardID) + assertBoardIsNotFrozen(board) + + caller := std.GetOrigCaller() assertHasBoardPermission(board, caller, PermissionThreadCreate) thread := board.AddThread(caller, title, body) @@ -129,8 +158,10 @@ func CreateReply(boardID BoardID, threadID, replyID PostID, body string) PostID body = strings.TrimSpace(body) assertBodyIsNotEmpty(body) - caller := std.GetOrigCaller() board := mustGetBoard(boardID) + assertBoardIsNotFrozen(board) + + caller := std.GetOrigCaller() assertHasBoardPermission(board, caller, PermissionReplyCreate) thread := mustGetThread(board, threadID) @@ -155,8 +186,10 @@ func CreateReply(boardID BoardID, threadID, replyID PostID, body string) PostID // Flagging requires special permissions and hides the comment or reply // when the number of flags reaches a pre-defined flagging threshold. func FlagReply(boardID BoardID, threadID, replyID PostID, reason string) { - caller := std.GetOrigCaller() board := mustGetBoard(boardID) + assertBoardIsNotFrozen(board) + + caller := std.GetOrigCaller() assertHasBoardPermission(board, caller, PermissionThreadFlag) thread := mustGetThread(board, threadID) @@ -176,6 +209,7 @@ func FlagReply(boardID BoardID, threadID, replyID PostID, reason string) { func CreateRepost(boardID BoardID, threadID PostID, title, body string, dstBoardID BoardID) PostID { caller := std.GetOrigCaller() dst := mustGetBoard(dstBoardID) + assertBoardIsNotFrozen(dst) assertHasBoardPermission(dst, caller, PermissionThreadRepost) board := mustGetBoard(boardID) @@ -190,8 +224,10 @@ func CreateRepost(boardID BoardID, threadID PostID, title, body string, dstBoard // // Threads can be deleted by the users who created them or otherwise by users with special permissions. func DeleteThread(boardID BoardID, threadID PostID) { - caller := std.GetOrigCaller() board := mustGetBoard(boardID) + assertBoardIsNotFrozen(board) + + caller := std.GetOrigCaller() thread := mustGetThread(board, threadID) if caller != thread.GetCreator() { assertHasBoardPermission(board, caller, PermissionThreadDelete) @@ -208,6 +244,8 @@ func DeleteThread(boardID BoardID, threadID PostID) { // is replaced by a text informing that reply has been deleted to avoid deleting sub-replies. func DeleteReply(boardID BoardID, threadID, replyID PostID) { board := mustGetBoard(boardID) + assertBoardIsNotFrozen(board) + thread := mustGetThread(board, threadID) reply := mustGetReply(thread, replyID) assertReplyVisible(reply) @@ -237,6 +275,8 @@ func EditThread(boardID BoardID, threadID PostID, title, body string) { assertBodyIsNotEmpty(body) board := mustGetBoard(boardID) + assertBoardIsNotFrozen(board) + thread := mustGetThread(board, threadID) caller := std.GetOrigCaller() if caller != thread.GetCreator() { @@ -254,6 +294,8 @@ func EditReply(boardID BoardID, threadID, replyID PostID, body string) { assertBodyIsNotEmpty(body) board := mustGetBoard(boardID) + assertBoardIsNotFrozen(board) + thread := mustGetThread(board, threadID) reply := mustGetReply(thread, replyID) assertReplyVisible(reply) @@ -270,6 +312,11 @@ func EditReply(boardID BoardID, threadID, replyID PostID, body string) { // A role can optionally be specified to be assigned to the new member. // Board ID is only required when inviting a member to a board. func InviteMember(boardID BoardID, user std.Address, role Role) { + if boardID != 0 { + board := mustGetBoard(boardID) + assertBoardIsNotFrozen(board) + } + perms := mustGetPermissions(boardID) caller := std.GetOrigCaller() args := Args{user, role} @@ -284,6 +331,11 @@ func InviteMember(boardID BoardID, user std.Address, role Role) { // // Board ID is only required when removing a member from board. func RemoveMember(boardID BoardID, user std.Address) { + if boardID != 0 { + board := mustGetBoard(boardID) + assertBoardIsNotFrozen(board) + } + perms := mustGetPermissions(boardID) caller := std.GetOrigCaller() perms.WithPermission(caller, PermissionMemberRemove, Args{user}, func(Args) { @@ -297,6 +349,11 @@ func RemoveMember(boardID BoardID, user std.Address) { // // Board ID is only required when checking if a user is a member of a board. func IsMember(boardID BoardID, user std.Address) bool { + if boardID != 0 { + board := mustGetBoard(boardID) + assertBoardIsNotFrozen(board) + } + perms := mustGetPermissions(boardID) return perms.HasUser(user) } @@ -305,6 +362,11 @@ func IsMember(boardID BoardID, user std.Address) bool { // // Board ID is only required when checking a member of a board. func HasMemberRole(boardID BoardID, member std.Address, role Role) bool { + if boardID != 0 { + board := mustGetBoard(boardID) + assertBoardIsNotFrozen(board) + } + perms := mustGetPermissions(boardID) return perms.HasRole(member, role) } @@ -313,6 +375,11 @@ func HasMemberRole(boardID BoardID, member std.Address, role Role) bool { // // Board ID is only required when changing the role for a member of a board. func ChangeMemberRole(boardID BoardID, member std.Address, role Role) { + if boardID != 0 { + board := mustGetBoard(boardID) + assertBoardIsNotFrozen(board) + } + perms := mustGetPermissions(boardID) caller := std.GetOrigCaller() args := Args{boardID, member, role} @@ -335,6 +402,12 @@ func assertBoardExists(id BoardID) { } } +func assertBoardIsNotFrozen(b *Board) { + if b.IsReadOnly() { + panic("board is frozen") + } +} + func assertNameIsNotEmpty(name string) { if name == "" { panic("name is empty") diff --git a/examples/gno.land/r/nt/boards2/v1/z_17_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_17_a_filetest.gno new file mode 100644 index 00000000000..6ab87e9087f --- /dev/null +++ b/examples/gno.land/r/nt/boards2/v1/z_17_a_filetest.gno @@ -0,0 +1,24 @@ +package main + +import ( + "std" + + boards2 "gno.land/r/nt/boards2/v1" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var bid boards2.BoardID + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") +} + +func main() { + boards2.FreezeBoard(bid) + println(boards2.IsBoardFrozen(bid)) +} + +// Output: +// true diff --git a/examples/gno.land/r/nt/boards2/v1/z_17_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_17_b_filetest.gno new file mode 100644 index 00000000000..986012edf67 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/v1/z_17_b_filetest.gno @@ -0,0 +1,24 @@ +package main + +import ( + "std" + + boards2 "gno.land/r/nt/boards2/v1" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var bid boards2.BoardID + +func init() { + std.TestSetOrigCaller(owner) + bid = boards2.CreateBoard("test123") + boards2.FreezeBoard(bid) +} + +func main() { + boards2.FreezeBoard(bid) +} + +// Error: +// board is frozen From 72c140725695fc677a7e36372c8c46caf2bdf6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Tue, 18 Feb 2025 20:04:45 +0100 Subject: [PATCH 47/52] chore(boards2): simplify default permissions implementation (#3770) The default permissions implementation is an implementation of the `Permissions` interfaces that is used by default when creating new boards and also used as the default realm permissions. The original idea is to be able to switch the implementation for a custom one allowing boards to customize the board interaction rules, or to be able to switch the realm implementation for a different one that uses another DAO with different board creation rules for example. This is an idea that still requires further discussions and refinement. This PR removes the default permission creation options which at this point makes no sense given that we don't have a boards2 package and default permissions are only used within the realm. --- examples/gno.land/r/nt/boards2/v1/board.gno | 69 ++-- .../r/nt/boards2/v1/permission_options.gno | 33 -- .../v1/{permission.gno => permissions.gno} | 0 ...on_default.gno => permissions_default.gno} | 37 +- ..._test.gno => permissions_default_test.gno} | 332 ++++++++++-------- 5 files changed, 241 insertions(+), 230 deletions(-) delete mode 100644 examples/gno.land/r/nt/boards2/v1/permission_options.gno rename examples/gno.land/r/nt/boards2/v1/{permission.gno => permissions.gno} (100%) rename examples/gno.land/r/nt/boards2/v1/{permission_default.gno => permissions_default.gno} (88%) rename examples/gno.land/r/nt/boards2/v1/{permission_default_test.gno => permissions_default_test.gno} (51%) diff --git a/examples/gno.land/r/nt/boards2/v1/board.gno b/examples/gno.land/r/nt/boards2/v1/board.gno index 7df374b53c0..071f26de61e 100644 --- a/examples/gno.land/r/nt/boards2/v1/board.gno +++ b/examples/gno.land/r/nt/boards2/v1/board.gno @@ -153,40 +153,39 @@ func (board *Board) GetPostFormURL() string { } func createDefaultBoardPermissions(owner std.Address) *DefaultPermissions { - return NewDefaultPermissions( - commondao.New(), - WithSuperRole(RoleOwner), - WithRole( - RoleAdmin, - PermissionBoardRename, - PermissionBoardFlaggingUpdate, - PermissionMemberInvite, - PermissionMemberRemove, - PermissionThreadCreate, - PermissionThreadEdit, - PermissionThreadDelete, - PermissionThreadRepost, - PermissionThreadFlag, - PermissionReplyCreate, - PermissionReplyDelete, - PermissionReplyFlag, - PermissionRoleChange, - ), - WithRole( - RoleModerator, - PermissionThreadCreate, - PermissionThreadEdit, - PermissionThreadRepost, - PermissionThreadFlag, - PermissionReplyCreate, - PermissionReplyFlag, - ), - WithRole( - RoleGuest, - PermissionThreadCreate, - PermissionThreadRepost, - PermissionReplyCreate, - ), - WithUser(owner, RoleOwner), + perms := NewDefaultPermissions(commondao.New()) + perms.SetSuperRole(RoleOwner) + perms.AddRole( + RoleAdmin, + PermissionBoardRename, + PermissionBoardFlaggingUpdate, + PermissionMemberInvite, + PermissionMemberRemove, + PermissionThreadCreate, + PermissionThreadEdit, + PermissionThreadDelete, + PermissionThreadRepost, + PermissionThreadFlag, + PermissionReplyCreate, + PermissionReplyDelete, + PermissionReplyFlag, + PermissionRoleChange, ) + perms.AddRole( + RoleModerator, + PermissionThreadCreate, + PermissionThreadEdit, + PermissionThreadRepost, + PermissionThreadFlag, + PermissionReplyCreate, + PermissionReplyFlag, + ) + perms.AddRole( + RoleGuest, + PermissionThreadCreate, + PermissionThreadRepost, + PermissionReplyCreate, + ) + perms.AddUser(owner, RoleOwner) + return perms } diff --git a/examples/gno.land/r/nt/boards2/v1/permission_options.gno b/examples/gno.land/r/nt/boards2/v1/permission_options.gno deleted file mode 100644 index 08cacf02582..00000000000 --- a/examples/gno.land/r/nt/boards2/v1/permission_options.gno +++ /dev/null @@ -1,33 +0,0 @@ -package boards2 - -import "std" - -// DefaultPermissionsOption configures an DefaultPermissions. -type DefaultPermissionsOption func(*DefaultPermissions) - -// WithSuperRole assigns a super role. -// A super role is one that have all permissions. -// These type of role doesn't need to be mapped to any permission. -func WithSuperRole(r Role) DefaultPermissionsOption { - return func(dp *DefaultPermissions) { - dp.superRole = r - } -} - -// WithUser adds a user to default permissions with optional assigned roles. -func WithUser(user std.Address, roles ...Role) DefaultPermissionsOption { - return func(dp *DefaultPermissions) { - if !dp.dao.IsMember(user) { - dp.dao.AddMember(user) - } - - dp.users.Set(user.String(), append([]Role(nil), roles...)) - } -} - -// WithRole add a role to default permissions with one or more assigned permissions. -func WithRole(r Role, p Permission, extra ...Permission) DefaultPermissionsOption { - return func(dp *DefaultPermissions) { - dp.roles.Set(string(r), append([]Permission{p}, extra...)) - } -} diff --git a/examples/gno.land/r/nt/boards2/v1/permission.gno b/examples/gno.land/r/nt/boards2/v1/permissions.gno similarity index 100% rename from examples/gno.land/r/nt/boards2/v1/permission.gno rename to examples/gno.land/r/nt/boards2/v1/permissions.gno diff --git a/examples/gno.land/r/nt/boards2/v1/permission_default.gno b/examples/gno.land/r/nt/boards2/v1/permissions_default.gno similarity index 88% rename from examples/gno.land/r/nt/boards2/v1/permission_default.gno rename to examples/gno.land/r/nt/boards2/v1/permissions_default.gno index b6b36f16693..1a374ff1c59 100644 --- a/examples/gno.land/r/nt/boards2/v1/permission_default.gno +++ b/examples/gno.land/r/nt/boards2/v1/permissions_default.gno @@ -10,8 +10,6 @@ import ( "gno.land/r/demo/users" ) -// TODO: Move to a package or make it private? - // DefaultPermissions manages users, roles and permissions. type DefaultPermissions struct { superRole Role @@ -22,16 +20,28 @@ type DefaultPermissions struct { // NewDefaultPermissions creates a new permissions type. // This type is a default implementation to handle users, roles and permissions. -func NewDefaultPermissions(dao *commondao.CommonDAO, options ...DefaultPermissionsOption) *DefaultPermissions { - dp := &DefaultPermissions{ +func NewDefaultPermissions(dao *commondao.CommonDAO) *DefaultPermissions { + if dao == nil { + panic("default permissions require a DAO") + } + + return &DefaultPermissions{ dao: dao, roles: avl.NewTree(), users: avl.NewTree(), } - for _, apply := range options { - apply(dp) - } - return dp +} + +// SetSuperRole assigns a super role. +// A super role is one that have all permissions. +// These type of role doesn't need to be mapped to any permission. +func (dp *DefaultPermissions) SetSuperRole(r Role) { + dp.superRole = r +} + +// AddRole add a role with one or more assigned permissions. +func (dp *DefaultPermissions) AddRole(r Role, p Permission, extra ...Permission) { + dp.roles.Set(string(r), append([]Permission{p}, extra...)) } // RoleExists checks if a role exists. @@ -227,12 +237,11 @@ func (dp DefaultPermissions) handleRoleChange(args Args, cb func(Args)) { } func createDefaultPermissions(owner std.Address) *DefaultPermissions { - return NewDefaultPermissions( - commondao.New(), - WithSuperRole(RoleOwner), - WithRole(RoleAdmin, PermissionBoardCreate), - WithUser(owner, RoleOwner), - ) + perms := NewDefaultPermissions(commondao.New()) + perms.SetSuperRole(RoleOwner) + perms.AddRole(RoleAdmin, PermissionBoardCreate) + perms.AddUser(owner, RoleOwner) + return perms } func assertBoardNameIsNotAddress(s string) { diff --git a/examples/gno.land/r/nt/boards2/v1/permission_default_test.gno b/examples/gno.land/r/nt/boards2/v1/permissions_default_test.gno similarity index 51% rename from examples/gno.land/r/nt/boards2/v1/permission_default_test.gno rename to examples/gno.land/r/nt/boards2/v1/permissions_default_test.gno index 3e95e8c207c..2f504b84f0a 100644 --- a/examples/gno.land/r/nt/boards2/v1/permission_default_test.gno +++ b/examples/gno.land/r/nt/boards2/v1/permissions_default_test.gno @@ -11,24 +11,13 @@ import ( var _ Permissions = (*DefaultPermissions)(nil) -func TestNewDefaultPermissions(t *testing.T) { - roles := []Role{"a", "b"} - dao := commondao.New() - - perms := NewDefaultPermissions(dao, WithRole("a", "permission1"), WithRole("b", "permission2")) - - for _, r := range roles { - uassert.True(t, perms.RoleExists(r)) - } -} - func TestDefaultPermissionsWithPermission(t *testing.T) { cases := []struct { name string user std.Address permission Permission args Args - perms *DefaultPermissions + setup func() *DefaultPermissions err string called bool }{ @@ -36,11 +25,12 @@ func TestDefaultPermissionsWithPermission(t *testing.T) { name: "ok", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", - perms: NewDefaultPermissions( - commondao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), - WithRole("foo", "bar"), - ), + setup: func() *DefaultPermissions { + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("foo", "bar") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo") + return perms + }, called: true, }, { @@ -48,30 +38,34 @@ func TestDefaultPermissionsWithPermission(t *testing.T) { user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", args: Args{"a", "b"}, - perms: NewDefaultPermissions( - commondao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), - WithRole("foo", "bar"), - ), + setup: func() *DefaultPermissions { + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("foo", "bar") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo") + return perms + }, called: true, }, { name: "no permission", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", - perms: NewDefaultPermissions( - commondao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), - WithRole("foo", "bar"), - ), + setup: func() *DefaultPermissions { + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("foo", "bar") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + return perms + }, err: "unauthorized", }, { name: "is not a DAO member", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", - perms: NewDefaultPermissions(commondao.New()), - err: "unauthorized", + setup: func() *DefaultPermissions { + return NewDefaultPermissions(commondao.New()) + }, + err: "unauthorized", }, } @@ -82,13 +76,14 @@ func TestDefaultPermissionsWithPermission(t *testing.T) { args Args ) + perms := tc.setup() callback := func(a Args) { args = a called = true } testCaseFn := func() { - tc.perms.WithPermission(tc.user, tc.permission, tc.args, callback) + perms.WithPermission(tc.user, tc.permission, tc.args, callback) } if tc.err != "" { @@ -112,46 +107,68 @@ func TestDefaultPermissionsGetUserRoles(t *testing.T) { name string user std.Address roles []string - perms *DefaultPermissions + setup func() *DefaultPermissions }{ { name: "single role", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", roles: []string{"admin"}, - perms: NewDefaultPermissions(commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin")), + setup: func() *DefaultPermissions { + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("admin", "x") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin") + return perms + }, }, { name: "multiple roles", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", roles: []string{"admin", "foo", "bar"}, - perms: NewDefaultPermissions(commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo", "bar")), + setup: func() *DefaultPermissions { + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("admin", "x") + perms.AddRole("foo", "x") + perms.AddRole("bar", "x") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo", "bar") + return perms + }, }, { - name: "without roles", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - perms: NewDefaultPermissions(commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + name: "without roles", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + setup: func() *DefaultPermissions { + perms := NewDefaultPermissions(commondao.New()) + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + return perms + }, }, { - name: "not a user", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - perms: NewDefaultPermissions(commondao.New()), + name: "not a user", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + setup: func() *DefaultPermissions { + return NewDefaultPermissions(commondao.New()) + }, }, { name: "multiple users", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", roles: []string{"admin"}, - perms: NewDefaultPermissions( - commondao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin"), - WithUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "admin"), - WithUser("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc", "admin", "bar"), - ), + setup: func() *DefaultPermissions { + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("admin", "x") + perms.AddRole("bar", "x") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin") + perms.AddUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "admin") + perms.AddUser("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc", "admin", "bar") + return perms + }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - roles := tc.perms.GetUserRoles(tc.user) + perms := tc.setup() + roles := perms.GetUserRoles(tc.user) urequire.Equal(t, len(tc.roles), len(roles), "user role count") for i, r := range roles { @@ -166,39 +183,60 @@ func TestDefaultPermissionsHasRole(t *testing.T) { name string user std.Address role Role - perms *DefaultPermissions + setup func() *DefaultPermissions want bool }{ { - name: "ok", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - role: "admin", - perms: NewDefaultPermissions(commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin")), - want: true, + name: "ok", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + role: "admin", + setup: func() *DefaultPermissions { + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("admin", "x") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin") + return perms + }, + want: true, }, { - name: "ok with multiple roles", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - role: "foo", - perms: NewDefaultPermissions(commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo")), - want: true, + name: "ok with multiple roles", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + role: "foo", + setup: func() *DefaultPermissions { + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("admin", "x") + perms.AddRole("foo", "x") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "admin", "foo") + return perms + }, + want: true, }, { - name: "user without roles", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - perms: NewDefaultPermissions(commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")), + name: "user without roles", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + setup: func() *DefaultPermissions { + perms := NewDefaultPermissions(commondao.New()) + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + return perms + }, }, { - name: "has no role", - user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - role: "bar", - perms: NewDefaultPermissions(commondao.New(), WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo")), + name: "has no role", + user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + role: "bar", + setup: func() *DefaultPermissions { + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("foo", "x") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo") + return perms + }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - got := tc.perms.HasRole(tc.user, tc.role) + perms := tc.setup() + got := perms.HasRole(tc.user, tc.role) uassert.Equal(t, got, tc.want) }) } @@ -209,59 +247,64 @@ func TestDefaultPermissionsHasPermission(t *testing.T) { name string user std.Address permission Permission - perms *DefaultPermissions + setup func() *DefaultPermissions want bool }{ { name: "ok", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", - perms: NewDefaultPermissions( - commondao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), - WithRole("foo", "bar"), - ), + setup: func() *DefaultPermissions { + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("foo", "bar") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo") + return perms + }, want: true, }, { name: "ok with multiple users", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "bar", - perms: NewDefaultPermissions( - commondao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), - WithUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "foo"), - WithRole("foo", "bar"), - ), + setup: func() *DefaultPermissions { + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("foo", "bar") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo") + perms.AddUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "foo") + return perms + }, want: true, }, { name: "ok with multiple roles", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "other", - perms: NewDefaultPermissions( - commondao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo", "baz"), - WithRole("foo", "bar"), - WithRole("baz", "other"), - ), + setup: func() *DefaultPermissions { + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("foo", "bar") + perms.AddRole("baz", "other") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo", "baz") + return perms + }, want: true, }, { name: "no permission", user: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", permission: "other", - perms: NewDefaultPermissions( - commondao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo"), - WithRole("foo", "bar"), - ), + setup: func() *DefaultPermissions { + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("foo", "bar") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "foo") + return perms + }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - got := tc.perms.HasPermission(tc.user, tc.permission) + perms := tc.setup() + got := perms.HasPermission(tc.user, tc.permission) uassert.Equal(t, got, tc.want) }) } @@ -280,11 +323,10 @@ func TestDefaultPermissionsAddUser(t *testing.T) { user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), roles: []Role{"a", "b"}, setup: func() *DefaultPermissions { - return NewDefaultPermissions( - commondao.New(), - WithRole("a", "permission1"), - WithRole("b", "permission2"), - ) + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("a", "permission1") + perms.AddRole("b", "permission2") + return perms }, }, { @@ -292,22 +334,20 @@ func TestDefaultPermissionsAddUser(t *testing.T) { user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), roles: []Role{"a"}, setup: func() *DefaultPermissions { - return NewDefaultPermissions( - commondao.New(), - WithRole("a", "permission1"), - WithUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "a"), - WithUser("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc"), - ) + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("a", "permission1") + perms.AddUser("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", "a") + perms.AddUser("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc") + return perms }, }, { name: "duplicated user", user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), setup: func() *DefaultPermissions { - return NewDefaultPermissions( - commondao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), - ) + perms := NewDefaultPermissions(commondao.New()) + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + return perms }, err: "user already exists", }, @@ -316,7 +356,9 @@ func TestDefaultPermissionsAddUser(t *testing.T) { user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), roles: []Role{"a", "foo"}, setup: func() *DefaultPermissions { - return NewDefaultPermissions(commondao.New(), WithRole("a", "permission1")) + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("a", "permission1") + return perms }, err: "invalid role: foo", }, @@ -334,9 +376,9 @@ func TestDefaultPermissionsAddUser(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - perm := tc.setup() + perms := tc.setup() - err := perm.AddUser(tc.user, tc.roles...) + err := perms.AddUser(tc.user, tc.roles...) if tc.err != "" { urequire.True(t, err != nil, "expected an error") @@ -346,7 +388,7 @@ func TestDefaultPermissionsAddUser(t *testing.T) { urequire.NoError(t, err) } - roles := perm.GetUserRoles(tc.user) + roles := perms.GetUserRoles(tc.user) uassert.Equal(t, len(tc.roles), len(roles)) for i, r := range roles { urequire.Equal(t, string(tc.roles[i]), string(r)) @@ -368,12 +410,11 @@ func TestDefaultPermissionsSetUserRoles(t *testing.T) { user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), roles: []Role{"b"}, setup: func() *DefaultPermissions { - return NewDefaultPermissions( - commondao.New(), - WithRole("a", "permission1"), - WithRole("b", "permission2"), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a"), - ) + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("a", "permission1") + perms.AddRole("b", "permission2") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a") + return perms }, }, { @@ -381,13 +422,12 @@ func TestDefaultPermissionsSetUserRoles(t *testing.T) { user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), roles: []Role{"b", "c"}, setup: func() *DefaultPermissions { - return NewDefaultPermissions( - commondao.New(), - WithRole("a", "permission1"), - WithRole("b", "permission2"), - WithRole("c", "permission2"), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a"), - ) + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("a", "permission1") + perms.AddRole("b", "permission2") + perms.AddRole("c", "permission2") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a") + return perms }, }, { @@ -395,25 +435,23 @@ func TestDefaultPermissionsSetUserRoles(t *testing.T) { user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), roles: []Role{"a", "c"}, setup: func() *DefaultPermissions { - return NewDefaultPermissions( - commondao.New(), - WithRole("a", "permission1"), - WithRole("b", "permission2"), - WithRole("c", "permission2"), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a", "c"), - ) + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("a", "permission1") + perms.AddRole("b", "permission2") + perms.AddRole("c", "permission2") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a", "c") + return perms }, }, { name: "remove roles", user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), setup: func() *DefaultPermissions { - return NewDefaultPermissions( - commondao.New(), - WithRole("a", "permission1"), - WithRole("b", "permission2"), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a", "b"), - ) + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("a", "permission1") + perms.AddRole("b", "permission2") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a", "b") + return perms }, }, { @@ -421,11 +459,10 @@ func TestDefaultPermissionsSetUserRoles(t *testing.T) { user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), roles: []Role{"x", "a"}, setup: func() *DefaultPermissions { - return NewDefaultPermissions( - commondao.New(), - WithRole("a", "permission1"), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a"), - ) + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("a", "permission1") + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", "a") + return perms }, err: "invalid role: x", }, @@ -441,9 +478,9 @@ func TestDefaultPermissionsSetUserRoles(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - perm := tc.setup() + perms := tc.setup() - err := perm.SetUserRoles(tc.user, tc.roles...) + err := perms.SetUserRoles(tc.user, tc.roles...) if tc.err != "" { urequire.True(t, err != nil, "expected an error") @@ -453,7 +490,7 @@ func TestDefaultPermissionsSetUserRoles(t *testing.T) { urequire.NoError(t, err) } - roles := perm.GetUserRoles(tc.user) + roles := perms.GetUserRoles(tc.user) uassert.Equal(t, len(tc.roles), len(roles)) for i, r := range roles { urequire.Equal(t, string(tc.roles[i]), string(r)) @@ -473,10 +510,9 @@ func TestDefaultPermissionsRemoveUser(t *testing.T) { name: "ok", user: std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), setup: func() *DefaultPermissions { - return NewDefaultPermissions( - commondao.New(), - WithUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), - ) + perms := NewDefaultPermissions(commondao.New()) + perms.AddUser("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + return perms }, want: true, }, @@ -491,8 +527,8 @@ func TestDefaultPermissionsRemoveUser(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - perm := tc.setup() - got := perm.RemoveUser(tc.user) + perms := tc.setup() + got := perms.RemoveUser(tc.user) uassert.Equal(t, tc.want, got) }) } From 17377ea2acc679036bf277b189c68ff75327630f Mon Sep 17 00:00:00 2001 From: jeronimoalbi Date: Thu, 20 Feb 2025 19:36:25 +0100 Subject: [PATCH 48/52] chore: remove `GetDAO` from `Permissions` interface THis was discussed at some point but it turns out is not going to be used by Boards2. --- examples/gno.land/r/nt/boards2/v1/permissions.gno | 10 +--------- .../gno.land/r/nt/boards2/v1/permissions_default.gno | 6 ------ 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/examples/gno.land/r/nt/boards2/v1/permissions.gno b/examples/gno.land/r/nt/boards2/v1/permissions.gno index e707b1f6e47..0e48d3f3004 100644 --- a/examples/gno.land/r/nt/boards2/v1/permissions.gno +++ b/examples/gno.land/r/nt/boards2/v1/permissions.gno @@ -1,10 +1,6 @@ package boards2 -import ( - "std" - - "gno.land/p/nt/commondao" -) +import "std" const ( PermissionBoardCreate Permission = "board:create" @@ -64,9 +60,5 @@ type ( // HasUser checks if a user exists. HasUser(std.Address) bool - - // GetDAO returns the underlying DAO. - // Returned value can be nil if the implementation doesn't have a DAO. - GetDAO() *commondao.CommonDAO // TODO: should return an interface } ) diff --git a/examples/gno.land/r/nt/boards2/v1/permissions_default.gno b/examples/gno.land/r/nt/boards2/v1/permissions_default.gno index 1a374ff1c59..0c7b305f942 100644 --- a/examples/gno.land/r/nt/boards2/v1/permissions_default.gno +++ b/examples/gno.land/r/nt/boards2/v1/permissions_default.gno @@ -127,12 +127,6 @@ func (dp DefaultPermissions) HasUser(user std.Address) bool { return dp.users.Has(user.String()) } -// GetDAO returns the underlying DAO. -// Returned value can be nil if the implementation doesn't have a DAO. -func (dp DefaultPermissions) GetDAO() *commondao.CommonDAO { - return dp.dao -} - // WithPermission calls a callback when a user has a specific permission. // It panics on error or when a handler panics. // Callbacks are by default called when there is no handle registered for the permission. From df84cbeee0186901801671348ea56250c3c5134d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Fri, 21 Feb 2025 09:06:54 +0100 Subject: [PATCH 49/52] fix(boards2): change `boards2` realm to work with breaking `master` changes (#3795) Issue #3374 describes the breaking changes --- .../r/nt/boards2/v1/permissions_default.gno | 6 ++-- examples/gno.land/r/nt/boards2/v1/public.gno | 32 +++++++++---------- .../r/nt/boards2/v1/z_0_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_0_b_filetest.gno | 2 +- .../r/nt/boards2/v1/z_0_c_filetest.gno | 2 +- .../r/nt/boards2/v1/z_0_e_filetest.gno | 2 +- .../r/nt/boards2/v1/z_0_f_filetest.gno | 2 +- .../r/nt/boards2/v1/z_0_g_filetest.gno | 2 +- .../r/nt/boards2/v1/z_0_h_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_0_i_filetest.gno | 2 +- .../r/nt/boards2/v1/z_10_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_10_b_filetest.gno | 2 +- .../r/nt/boards2/v1/z_10_c_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_10_d_filetest.gno | 2 +- .../r/nt/boards2/v1/z_10_e_filetest.gno | 2 +- .../r/nt/boards2/v1/z_10_f_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_10_g_filetest.gno | 2 +- .../r/nt/boards2/v1/z_11_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_11_b_filetest.gno | 2 +- .../r/nt/boards2/v1/z_11_c_filetest.gno | 2 +- .../r/nt/boards2/v1/z_11_d_filetest.gno | 2 +- .../r/nt/boards2/v1/z_11_e_filetest.gno | 2 +- .../r/nt/boards2/v1/z_11_f_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_11_g_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_12_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_12_b_filetest.gno | 2 +- .../r/nt/boards2/v1/z_12_c_filetest.gno | 2 +- .../r/nt/boards2/v1/z_12_d_filetest.gno | 2 +- .../r/nt/boards2/v1/z_12_e_filetest.gno | 2 +- .../r/nt/boards2/v1/z_12_f_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_12_g_filetest.gno | 2 +- .../r/nt/boards2/v1/z_12_h_filetest.gno | 2 +- .../r/nt/boards2/v1/z_13_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_13_b_filetest.gno | 2 +- .../r/nt/boards2/v1/z_13_c_filetest.gno | 2 +- .../r/nt/boards2/v1/z_13_d_filetest.gno | 2 +- .../r/nt/boards2/v1/z_13_e_filetest.gno | 2 +- .../r/nt/boards2/v1/z_13_f_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_13_g_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_14_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_14_b_filetest.gno | 2 +- .../r/nt/boards2/v1/z_14_c_filetest.gno | 2 +- .../r/nt/boards2/v1/z_14_d_filetest.gno | 2 +- .../r/nt/boards2/v1/z_14_e_filetest.gno | 2 +- .../r/nt/boards2/v1/z_14_f_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_14_g_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_15_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_15_b_filetest.gno | 2 +- .../r/nt/boards2/v1/z_15_c_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_15_d_filetest.gno | 2 +- .../r/nt/boards2/v1/z_16_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_16_b_filetest.gno | 2 +- .../r/nt/boards2/v1/z_16_c_filetest.gno | 2 +- .../r/nt/boards2/v1/z_17_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_17_b_filetest.gno | 2 +- .../r/nt/boards2/v1/z_1_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_1_b_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_1_c_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_1_d_filetest.gno | 2 +- .../r/nt/boards2/v1/z_1_e_filetest.gno | 2 +- .../r/nt/boards2/v1/z_1_f_filetest.gno | 2 +- .../r/nt/boards2/v1/z_1_g_filetest.gno | 2 +- .../r/nt/boards2/v1/z_2_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_2_b_filetest.gno | 2 +- .../r/nt/boards2/v1/z_2_c_filetest.gno | 2 +- .../r/nt/boards2/v1/z_2_d_filetest.gno | 2 +- .../r/nt/boards2/v1/z_2_e_filetest.gno | 2 +- .../r/nt/boards2/v1/z_2_f_filetest.gno | 2 +- .../r/nt/boards2/v1/z_2_g_filetest.gno | 2 +- .../r/nt/boards2/v1/z_2_h_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_2_i_filetest.gno | 2 +- .../r/nt/boards2/v1/z_2_j_filetest.gno | 2 +- .../r/nt/boards2/v1/z_3_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_3_b_filetest.gno | 2 +- .../r/nt/boards2/v1/z_3_c_filetest.gno | 2 +- .../r/nt/boards2/v1/z_3_d_filetest.gno | 2 +- .../r/nt/boards2/v1/z_3_e_filetest.gno | 2 +- .../r/nt/boards2/v1/z_3_f_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_3_g_filetest.gno | 6 ++-- .../r/nt/boards2/v1/z_3_h_filetest.gno | 2 +- .../r/nt/boards2/v1/z_3_i_filetest.gno | 6 ++-- .../r/nt/boards2/v1/z_3_j_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_4_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_4_b_filetest.gno | 2 +- .../r/nt/boards2/v1/z_4_c_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_4_d_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_4_e_filetest.gno | 2 +- .../r/nt/boards2/v1/z_4_f_filetest.gno | 2 +- .../r/nt/boards2/v1/z_4_g_filetest.gno | 2 +- .../r/nt/boards2/v1/z_4_h_filetest.gno | 2 +- .../r/nt/boards2/v1/z_5_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_5_b_filetest.gno | 2 +- .../r/nt/boards2/v1/z_5_c_filetest.gno | 2 +- .../r/nt/boards2/v1/z_6_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_6_b_filetest.gno | 2 +- .../r/nt/boards2/v1/z_7_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_7_b_filetest.gno | 2 +- .../r/nt/boards2/v1/z_8_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_8_b_filetest.gno | 2 +- .../r/nt/boards2/v1/z_8_c_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_8_d_filetest.gno | 2 +- .../r/nt/boards2/v1/z_8_e_filetest.gno | 2 +- .../r/nt/boards2/v1/z_9_a_filetest.gno | 2 +- .../r/nt/boards2/v1/z_9_b_filetest.gno | 2 +- .../r/nt/boards2/v1/z_9_c_filetest.gno | 2 +- .../r/nt/boards2/v1/z_9_d_filetest.gno | 4 +-- .../r/nt/boards2/v1/z_9_e_filetest.gno | 4 +-- 107 files changed, 149 insertions(+), 149 deletions(-) diff --git a/examples/gno.land/r/nt/boards2/v1/permissions_default.gno b/examples/gno.land/r/nt/boards2/v1/permissions_default.gno index 0c7b305f942..931bd751748 100644 --- a/examples/gno.land/r/nt/boards2/v1/permissions_default.gno +++ b/examples/gno.land/r/nt/boards2/v1/permissions_default.gno @@ -194,7 +194,7 @@ func (dp DefaultPermissions) handleMemberInvite(args Args, cb func(Args)) { } if role == RoleOwner { - caller := std.GetOrigCaller() + caller := std.OriginCaller() if !dp.HasRole(caller, RoleOwner) { panic("only owners are allowed to invite other owners") } @@ -206,7 +206,7 @@ func (dp DefaultPermissions) handleMemberInvite(args Args, cb func(Args)) { func (dp DefaultPermissions) handleRoleChange(args Args, cb func(Args)) { // Owners and Admins can change roles. // Admins should not be able to assign or remove the Owner role from members. - caller := std.GetOrigCaller() + caller := std.OriginCaller() if dp.HasRole(caller, RoleAdmin) { role, ok := args[2].(Role) if !ok { @@ -253,7 +253,7 @@ func assertValidBoardNameLength(name string) { func assertBoardNameBelongsToCaller(name string) { // When the board name is the name of a registered user // check that caller is the owner of the name. - caller := std.GetOrigCaller() + caller := std.OriginCaller() user := users.GetUserByName(name) if user != nil && user.Address != caller { panic("board name is a user name registered to a different user") diff --git a/examples/gno.land/r/nt/boards2/v1/public.gno b/examples/gno.land/r/nt/boards2/v1/public.gno index eaa4ef78293..f7d801b49c6 100644 --- a/examples/gno.land/r/nt/boards2/v1/public.gno +++ b/examples/gno.land/r/nt/boards2/v1/public.gno @@ -20,7 +20,7 @@ func CreateBoard(name string) BoardID { assertNameIsNotEmpty(name) assertBoardNameNotExists(name) - caller := std.GetOrigCaller() + caller := std.OriginCaller() id := incGetBoardID() args := Args{name, id} gPerm.WithPermission(caller, PermissionBoardCreate, args, func(Args) { @@ -46,7 +46,7 @@ func RenameBoard(name, newName string) { assertBoardIsNotFrozen(board) bid := board.GetID() - caller := std.GetOrigCaller() + caller := std.OriginCaller() args := Args{bid, name, newName} board.perms.WithPermission(caller, PermissionBoardRename, args, func(Args) { assertBoardNameNotExists(newName) @@ -65,7 +65,7 @@ func FreezeBoard(boardID BoardID) { board := mustGetBoard(boardID) assertBoardIsNotFrozen(board) - caller := std.GetOrigCaller() + caller := std.OriginCaller() args := Args{boardID} board.perms.WithPermission(caller, PermissionBoardFreeze, args, func(Args) { board := mustGetBoard(boardID) @@ -92,7 +92,7 @@ func SetFlaggingThreshold(boardID BoardID, threshold int) { board := mustGetBoard(boardID) assertBoardIsNotFrozen(board) - caller := std.GetOrigCaller() + caller := std.OriginCaller() args := Args{boardID, threshold} board.perms.WithPermission(caller, PermissionBoardFlaggingUpdate, args, func(Args) { assertBoardExists(boardID) @@ -115,7 +115,7 @@ func FlagThread(boardID BoardID, threadID PostID, reason string) { board := mustGetBoard(boardID) assertBoardIsNotFrozen(board) - caller := std.GetOrigCaller() + caller := std.OriginCaller() assertHasBoardPermission(board, caller, PermissionThreadFlag) t, ok := board.GetThread(threadID) @@ -144,7 +144,7 @@ func CreateThread(boardID BoardID, title, body string) PostID { board := mustGetBoard(boardID) assertBoardIsNotFrozen(board) - caller := std.GetOrigCaller() + caller := std.OriginCaller() assertHasBoardPermission(board, caller, PermissionThreadCreate) thread := board.AddThread(caller, title, body) @@ -161,7 +161,7 @@ func CreateReply(boardID BoardID, threadID, replyID PostID, body string) PostID board := mustGetBoard(boardID) assertBoardIsNotFrozen(board) - caller := std.GetOrigCaller() + caller := std.OriginCaller() assertHasBoardPermission(board, caller, PermissionReplyCreate) thread := mustGetThread(board, threadID) @@ -189,7 +189,7 @@ func FlagReply(boardID BoardID, threadID, replyID PostID, reason string) { board := mustGetBoard(boardID) assertBoardIsNotFrozen(board) - caller := std.GetOrigCaller() + caller := std.OriginCaller() assertHasBoardPermission(board, caller, PermissionThreadFlag) thread := mustGetThread(board, threadID) @@ -207,7 +207,7 @@ func FlagReply(boardID BoardID, threadID, replyID PostID, reason string) { // CreateRepost reposts a thread into another board. func CreateRepost(boardID BoardID, threadID PostID, title, body string, dstBoardID BoardID) PostID { - caller := std.GetOrigCaller() + caller := std.OriginCaller() dst := mustGetBoard(dstBoardID) assertBoardIsNotFrozen(dst) assertHasBoardPermission(dst, caller, PermissionThreadRepost) @@ -227,7 +227,7 @@ func DeleteThread(boardID BoardID, threadID PostID) { board := mustGetBoard(boardID) assertBoardIsNotFrozen(board) - caller := std.GetOrigCaller() + caller := std.OriginCaller() thread := mustGetThread(board, threadID) if caller != thread.GetCreator() { assertHasBoardPermission(board, caller, PermissionThreadDelete) @@ -250,7 +250,7 @@ func DeleteReply(boardID BoardID, threadID, replyID PostID) { reply := mustGetReply(thread, replyID) assertReplyVisible(reply) - caller := std.GetOrigCaller() + caller := std.OriginCaller() if caller != reply.GetCreator() { assertHasBoardPermission(board, caller, PermissionReplyDelete) } @@ -278,7 +278,7 @@ func EditThread(boardID BoardID, threadID PostID, title, body string) { assertBoardIsNotFrozen(board) thread := mustGetThread(board, threadID) - caller := std.GetOrigCaller() + caller := std.OriginCaller() if caller != thread.GetCreator() { assertHasBoardPermission(board, caller, PermissionThreadEdit) } @@ -300,7 +300,7 @@ func EditReply(boardID BoardID, threadID, replyID PostID, body string) { reply := mustGetReply(thread, replyID) assertReplyVisible(reply) - if std.GetOrigCaller() != reply.GetCreator() { + if std.OriginCaller() != reply.GetCreator() { panic("only the reply creator is allowed to edit it") } @@ -318,7 +318,7 @@ func InviteMember(boardID BoardID, user std.Address, role Role) { } perms := mustGetPermissions(boardID) - caller := std.GetOrigCaller() + caller := std.OriginCaller() args := Args{user, role} perms.WithPermission(caller, PermissionMemberInvite, args, func(Args) { if err := perms.AddUser(user, role); err != nil { @@ -337,7 +337,7 @@ func RemoveMember(boardID BoardID, user std.Address) { } perms := mustGetPermissions(boardID) - caller := std.GetOrigCaller() + caller := std.OriginCaller() perms.WithPermission(caller, PermissionMemberRemove, Args{user}, func(Args) { if !perms.RemoveUser(user) { panic("member not found") @@ -381,7 +381,7 @@ func ChangeMemberRole(boardID BoardID, member std.Address, role Role) { } perms := mustGetPermissions(boardID) - caller := std.GetOrigCaller() + caller := std.OriginCaller() args := Args{boardID, member, role} perms.WithPermission(caller, PermissionRoleChange, args, func(Args) { if err := perms.SetUserRoles(member, role); err != nil { diff --git a/examples/gno.land/r/nt/boards2/v1/z_0_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_a_filetest.gno index e0e5f7b68d5..106ce1c4fe3 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_0_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_0_a_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_0_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_b_filetest.gno index 1d9d93ea68d..b2f9c674492 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_0_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_0_b_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_0_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_c_filetest.gno index aa746c01615..4be9c4be8be 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_0_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_0_c_filetest.gno @@ -12,7 +12,7 @@ const ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) boards2.CreateBoard(boardName) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_0_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_e_filetest.gno index 7fc2fdbcaec..98992a17ef8 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_0_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_0_e_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_0_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_f_filetest.gno index f457e4c389f..c08abd08d87 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_0_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_0_f_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_0_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_g_filetest.gno index d313e9b33d0..658b3cef215 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_0_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_0_g_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_0_h_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_h_filetest.gno index 27e68939996..82856e8677c 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_0_h_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_0_h_filetest.gno @@ -16,12 +16,12 @@ const ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) // Test1 is the boards owner and its address has a user already registered // so a new member must register a user with the new board name. boards2.InviteMember(0, member, boards2.RoleOwner) // Operate on realm DAO members instead of individual boards - std.TestSetOrigCaller(member) + std.TestSetOriginCaller(member) users.Register("", name, "") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_0_i_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_0_i_filetest.gno index be624e800e9..aac2127c552 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_0_i_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_0_i_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_10_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_10_a_filetest.gno index 618b63c0bd1..f699f7866e9 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_10_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_10_a_filetest.gno @@ -14,7 +14,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") pid = boards2.CreateThread(bid, "Foo", "bar") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_10_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_10_b_filetest.gno index 68fe6867a33..905bf3cc824 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_10_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_10_b_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_10_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_10_c_filetest.gno index d0bd55478d7..96aa028a8b8 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_10_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_10_c_filetest.gno @@ -17,11 +17,11 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") pid = boards2.CreateThread(bid, "Foo", "bar") - std.TestSetOrigCaller(user) + std.TestSetOriginCaller(user) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_10_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_10_d_filetest.gno index dc0c7c37936..b3545957105 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_10_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_10_d_filetest.gno @@ -11,7 +11,7 @@ const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 var bid boards2.BoardID func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_10_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_10_e_filetest.gno index 695680b5953..efe836a4a3b 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_10_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_10_e_filetest.gno @@ -14,7 +14,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") pid = boards2.CreateThread(bid, "Foo", "bar") boards2.FlagThread(bid, pid, "") diff --git a/examples/gno.land/r/nt/boards2/v1/z_10_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_10_f_filetest.gno index dba8ec6de95..3a730a9713b 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_10_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_10_f_filetest.gno @@ -17,13 +17,13 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") pid = boards2.CreateThread(bid, "Foo", "bar") // Invite a member using a role with permission to flag threads boards2.InviteMember(bid, moderator, boards2.RoleModerator) - std.TestSetOrigCaller(moderator) + std.TestSetOriginCaller(moderator) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_10_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_10_g_filetest.gno index f11f29ef65b..bb0c0aceb67 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_10_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_10_g_filetest.gno @@ -14,7 +14,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") pid = boards2.CreateThread(bid, "Foo", "bar") diff --git a/examples/gno.land/r/nt/boards2/v1/z_11_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_11_a_filetest.gno index 84bc0d35160..b2c7cdb693e 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_11_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_11_a_filetest.gno @@ -20,7 +20,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") pid = boards2.CreateThread(bid, "Foo", "bar") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_11_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_11_b_filetest.gno index 841e251bb1e..b30c15d1da1 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_11_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_11_b_filetest.gno @@ -14,7 +14,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") pid = boards2.CreateThread(bid, "Foo", "bar") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_11_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_11_c_filetest.gno index cb620a6ea90..570a6b19e4e 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_11_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_11_c_filetest.gno @@ -14,7 +14,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") pid = boards2.CreateThread(bid, "Foo", "bar") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_11_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_11_d_filetest.gno index 4542911ebec..6ad751348d0 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_11_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_11_d_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_11_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_11_e_filetest.gno index 55f48e9792a..294c7d7c282 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_11_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_11_e_filetest.gno @@ -11,7 +11,7 @@ const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 var bid boards2.BoardID func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_11_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_11_f_filetest.gno index f6dfd814d2b..e2b24da8295 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_11_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_11_f_filetest.gno @@ -17,11 +17,11 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") pid = boards2.CreateThread(bid, "Foo", "bar") - std.TestSetOrigCaller(user) + std.TestSetOriginCaller(user) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_11_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_11_g_filetest.gno index 9625a71c8a5..159ba2ec79f 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_11_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_11_g_filetest.gno @@ -21,13 +21,13 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") pid = boards2.CreateThread(bid, "Foo", "bar") // Invite a member using a role with permission to edit threads boards2.InviteMember(bid, admin, boards2.RoleAdmin) - std.TestSetOrigCaller(admin) + std.TestSetOriginCaller(admin) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_12_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_12_a_filetest.gno index 656425e76cc..b1f5798889d 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_12_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_12_a_filetest.gno @@ -19,7 +19,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") rid = boards2.CreateReply(bid, tid, 0, "body") diff --git a/examples/gno.land/r/nt/boards2/v1/z_12_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_12_b_filetest.gno index b1b3efefe71..7b9d0ee0cb7 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_12_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_12_b_filetest.gno @@ -19,7 +19,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") diff --git a/examples/gno.land/r/nt/boards2/v1/z_12_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_12_c_filetest.gno index bc6916d2ee5..e70c55d5413 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_12_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_12_c_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_12_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_12_d_filetest.gno index 29df7bf4de3..bd99705359f 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_12_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_12_d_filetest.gno @@ -11,7 +11,7 @@ const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 var bid boards2.BoardID func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_12_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_12_e_filetest.gno index 0d9bca1fc97..06aa752773c 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_12_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_12_e_filetest.gno @@ -14,7 +14,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_12_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_12_f_filetest.gno index 5f128041da7..824e7505344 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_12_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_12_f_filetest.gno @@ -17,11 +17,11 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") rid = boards2.CreateReply(bid, tid, 0, "body") - std.TestSetOrigCaller(user) + std.TestSetOriginCaller(user) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_12_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_12_g_filetest.gno index 71c8264cd4e..f35984e2c84 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_12_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_12_g_filetest.gno @@ -17,7 +17,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") rid = boards2.CreateReply(bid, tid, 0, "body") diff --git a/examples/gno.land/r/nt/boards2/v1/z_12_h_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_12_h_filetest.gno index ce63bea7b31..f36e50d7ba6 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_12_h_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_12_h_filetest.gno @@ -14,7 +14,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") rid = boards2.CreateReply(bid, tid, 0, "body") diff --git a/examples/gno.land/r/nt/boards2/v1/z_13_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_13_a_filetest.gno index 0aeeaaae84b..420d722c862 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_13_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_13_a_filetest.gno @@ -15,7 +15,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") rid = boards2.CreateReply(bid, tid, 0, "body") diff --git a/examples/gno.land/r/nt/boards2/v1/z_13_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_13_b_filetest.gno index 8b2144a5c77..a11c57af144 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_13_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_13_b_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_13_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_13_c_filetest.gno index 32afd159bba..2065e3c87de 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_13_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_13_c_filetest.gno @@ -11,7 +11,7 @@ const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 var bid boards2.BoardID func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_13_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_13_d_filetest.gno index 039e164a1e2..d16a89e3998 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_13_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_13_d_filetest.gno @@ -14,7 +14,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_13_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_13_e_filetest.gno index 156ba6b7068..56d58618e37 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_13_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_13_e_filetest.gno @@ -14,7 +14,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") rid = boards2.CreateReply(bid, tid, 0, "body") diff --git a/examples/gno.land/r/nt/boards2/v1/z_13_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_13_f_filetest.gno index a401e42237e..387898783a5 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_13_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_13_f_filetest.gno @@ -17,12 +17,12 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") rid = boards2.CreateReply(bid, tid, 0, "body") - std.TestSetOrigCaller(user) + std.TestSetOriginCaller(user) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_13_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_13_g_filetest.gno index 1d24134a2b7..3f0c762107f 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_13_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_13_g_filetest.gno @@ -18,14 +18,14 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") rid = boards2.CreateReply(bid, tid, 0, "body") // Invite a member using a role with permission to flag replies boards2.InviteMember(bid, moderator, boards2.RoleModerator) - std.TestSetOrigCaller(moderator) + std.TestSetOriginCaller(moderator) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_14_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_14_a_filetest.gno index 104acce005a..d167f2776ed 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_14_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_14_a_filetest.gno @@ -14,7 +14,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") rid = boards2.CreateReply(bid, tid, 0, "body") diff --git a/examples/gno.land/r/nt/boards2/v1/z_14_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_14_b_filetest.gno index e8efd728257..6373a604fc1 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_14_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_14_b_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_14_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_14_c_filetest.gno index 8def87b12a8..82045269705 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_14_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_14_c_filetest.gno @@ -11,7 +11,7 @@ const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 var bid boards2.BoardID func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_14_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_14_d_filetest.gno index 61c2187dac5..cdde26d4f7f 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_14_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_14_d_filetest.gno @@ -14,7 +14,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_14_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_14_e_filetest.gno index 9849e68fc13..c1314f65429 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_14_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_14_e_filetest.gno @@ -15,7 +15,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") diff --git a/examples/gno.land/r/nt/boards2/v1/z_14_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_14_f_filetest.gno index eb9909fd147..1cbafc26ced 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_14_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_14_f_filetest.gno @@ -17,13 +17,13 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") rid = boards2.CreateReply(bid, tid, 0, "body") // Call using a user that has not permission to delete replies - std.TestSetOrigCaller(user) + std.TestSetOriginCaller(user) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_14_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_14_g_filetest.gno index e44c72b07f2..5e913e02994 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_14_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_14_g_filetest.gno @@ -17,14 +17,14 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") rid = boards2.CreateReply(bid, tid, 0, "body") // Invite a member using a role with permission to delete replies boards2.InviteMember(bid, member, boards2.RoleAdmin) - std.TestSetOrigCaller(member) + std.TestSetOriginCaller(member) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_15_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_15_a_filetest.gno index d8946c9b4be..dfb70648364 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_15_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_15_a_filetest.gno @@ -15,7 +15,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) srcBid = boards2.CreateBoard("src-board") dstBid = boards2.CreateBoard("dst-board") diff --git a/examples/gno.land/r/nt/boards2/v1/z_15_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_15_b_filetest.gno index 58b9fa60408..3846cd70d93 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_15_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_15_b_filetest.gno @@ -15,7 +15,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) srcBid = boards2.CreateBoard("src-board") dstBid = boards2.CreateBoard("dst-board") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_15_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_15_c_filetest.gno index 99b1dee6059..daab5017321 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_15_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_15_c_filetest.gno @@ -18,12 +18,12 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) srcBid = boards2.CreateBoard("src-board") dstBid = boards2.CreateBoard("dst-board") srcTid = boards2.CreateThread(srcBid, "Foo", "bar") - std.TestSetOrigCaller(user) + std.TestSetOriginCaller(user) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_15_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_15_d_filetest.gno index bab704c8bbd..850632c5763 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_15_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_15_d_filetest.gno @@ -17,7 +17,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) srcBid = boards2.CreateBoard("src-board") dstBid = boards2.CreateBoard("dst-board") diff --git a/examples/gno.land/r/nt/boards2/v1/z_16_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_16_a_filetest.gno index f56a9e2b589..8e151d16e0c 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_16_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_16_a_filetest.gno @@ -11,7 +11,7 @@ const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 var bid boards2.BoardID func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_16_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_16_b_filetest.gno index 2856abbcba7..14593cb8310 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_16_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_16_b_filetest.gno @@ -11,7 +11,7 @@ const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 const bid boards2.BoardID = 404 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_16_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_16_c_filetest.gno index d0e0751c8f8..b827d964953 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_16_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_16_c_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_17_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_17_a_filetest.gno index 6ab87e9087f..f8aad471ea9 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_17_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_17_a_filetest.gno @@ -11,7 +11,7 @@ const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 var bid boards2.BoardID func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_17_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_17_b_filetest.gno index 986012edf67..c2ac61ace64 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_17_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_17_b_filetest.gno @@ -11,7 +11,7 @@ const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 var bid boards2.BoardID func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") boards2.FreezeBoard(bid) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_1_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_1_a_filetest.gno index 68e0bd7f8fb..54c84f8c186 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_1_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_1_a_filetest.gno @@ -14,7 +14,7 @@ const ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_1_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_1_b_filetest.gno index 34465983f96..1f53c6b6f6e 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_1_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_1_b_filetest.gno @@ -15,14 +15,14 @@ const ( var bid boards2.BoardID // Operate on board DAO func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") // Add an admin member boards2.InviteMember(bid, admin, boards2.RoleAdmin) // Next call will be done by the admin member - std.TestSetOrigCaller(admin) + std.TestSetOriginCaller(admin) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_1_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_1_c_filetest.gno index 75cea64ca1a..cdb9b2d9bed 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_1_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_1_c_filetest.gno @@ -16,14 +16,14 @@ const ( var bid boards2.BoardID // Operate on board DAO func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") // Add an admin member boards2.InviteMember(bid, admin, boards2.RoleAdmin) // Next call will be done by the admin member - std.TestSetOrigCaller(admin) + std.TestSetOriginCaller(admin) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_1_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_1_d_filetest.gno index 73c5750cc36..d4588dc9fad 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_1_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_1_d_filetest.gno @@ -12,7 +12,7 @@ const ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_1_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_1_e_filetest.gno index 074a053facd..49dc4f82d89 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_1_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_1_e_filetest.gno @@ -15,7 +15,7 @@ const ( var bid boards2.BoardID func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("foo123") // Operate on board DAO members } diff --git a/examples/gno.land/r/nt/boards2/v1/z_1_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_1_f_filetest.gno index 0e2de99c1f7..e3cdfc0535a 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_1_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_1_f_filetest.gno @@ -14,7 +14,7 @@ const ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) boards2.InviteMember(bid, user, role) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_1_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_1_g_filetest.gno index 220c886cfd9..97f1b780738 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_1_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_1_g_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_2_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_a_filetest.gno index 67d541da339..7195e29eb26 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_2_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_a_filetest.gno @@ -19,7 +19,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_2_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_b_filetest.gno index 15fd82460c2..7126223358c 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_2_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_b_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_2_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_c_filetest.gno index e22baf8c86c..ae36bc63140 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_2_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_c_filetest.gno @@ -11,7 +11,7 @@ const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 var bid boards2.BoardID func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_2_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_d_filetest.gno index 6ca1d0d54ae..44e634f0794 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_2_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_d_filetest.gno @@ -14,7 +14,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_2_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_e_filetest.gno index b3050f25387..02c93888f9e 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_2_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_e_filetest.gno @@ -14,7 +14,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") tid = boards2.CreateThread(bid, "Foo", "bar") diff --git a/examples/gno.land/r/nt/boards2/v1/z_2_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_f_filetest.gno index fe0e27fe6ca..d98acf7a235 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_2_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_f_filetest.gno @@ -14,7 +14,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") tid = boards2.CreateThread(bid, "Foo", "bar") rid = boards2.CreateReply(bid, tid, 0, "reply1") diff --git a/examples/gno.land/r/nt/boards2/v1/z_2_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_g_filetest.gno index cb70150c881..bc7cf601d15 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_2_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_g_filetest.gno @@ -14,7 +14,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") tid = boards2.CreateThread(bid, "thread", "thread") rid = boards2.CreateReply(bid, tid, 0, "reply1") diff --git a/examples/gno.land/r/nt/boards2/v1/z_2_h_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_h_filetest.gno index 0af9369d8b4..a94caf1dd23 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_2_h_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_h_filetest.gno @@ -17,11 +17,11 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") tid = boards2.CreateThread(bid, "Foo", "bar") - std.TestSetOrigCaller(user) + std.TestSetOriginCaller(user) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_2_i_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_i_filetest.gno index f5025273c74..5bc871355ce 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_2_i_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_i_filetest.gno @@ -14,7 +14,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_2_j_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_2_j_filetest.gno index f397fb1c843..4b0585c3688 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_2_j_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_2_j_filetest.gno @@ -19,7 +19,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") tid = boards2.CreateThread(bid, "Foo", "bar") rid = boards2.CreateReply(bid, tid, 0, "First comment") diff --git a/examples/gno.land/r/nt/boards2/v1/z_3_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_a_filetest.gno index d1a4918e3fc..c41125affc3 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_3_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_a_filetest.gno @@ -15,7 +15,7 @@ const ( var bid boards2.BoardID // Operate on board DAO func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard(name) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_3_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_b_filetest.gno index 07ec8f6af49..a23b4c54936 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_3_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_b_filetest.gno @@ -12,7 +12,7 @@ const ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) boards2.CreateBoard(name) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_3_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_c_filetest.gno index 997fc18f87c..0b6722a46de 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_3_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_c_filetest.gno @@ -12,7 +12,7 @@ const ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) boards2.CreateBoard(name) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_3_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_d_filetest.gno index d4935afff71..5e78150657e 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_3_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_d_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_3_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_e_filetest.gno index 71cf8d38864..b407326ee51 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_3_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_e_filetest.gno @@ -12,7 +12,7 @@ const ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) boards2.CreateBoard(name) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_3_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_f_filetest.gno index c12e360c12f..b074504acd2 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_3_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_f_filetest.gno @@ -19,14 +19,14 @@ const ( var bid boards2.BoardID // Operate on board DAO func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard(name) boards2.InviteMember(bid, member, boards2.RoleOwner) // Test1 is the boards owner and its address has a user already registered // so a new member must register a user with the new board name. - std.TestSetOrigCaller(member) + std.TestSetOriginCaller(member) users.Register("", newName, "") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_3_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_g_filetest.gno index d183b450a62..f3d1ba69ead 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_3_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_g_filetest.gno @@ -20,19 +20,19 @@ const ( var bid boards2.BoardID // Operate on board DAO func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard(name) boards2.InviteMember(bid, member, boards2.RoleOwner) // Test1 is the boards owner and its address has a user already registered // so a new member must register a user with the new board name. - std.TestSetOrigCaller(member) + std.TestSetOriginCaller(member) users.Register("", newName, "") // Invite a new member that doesn't own the user that matches the new board name boards2.InviteMember(bid, member2, boards2.RoleOwner) - std.TestSetOrigCaller(member2) + std.TestSetOriginCaller(member2) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_3_h_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_h_filetest.gno index 50327ff33c0..7ccd271b401 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_3_h_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_h_filetest.gno @@ -12,7 +12,7 @@ const ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) boards2.CreateBoard(name) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_3_i_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_i_filetest.gno index d183b450a62..f3d1ba69ead 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_3_i_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_i_filetest.gno @@ -20,19 +20,19 @@ const ( var bid boards2.BoardID // Operate on board DAO func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard(name) boards2.InviteMember(bid, member, boards2.RoleOwner) // Test1 is the boards owner and its address has a user already registered // so a new member must register a user with the new board name. - std.TestSetOrigCaller(member) + std.TestSetOriginCaller(member) users.Register("", newName, "") // Invite a new member that doesn't own the user that matches the new board name boards2.InviteMember(bid, member2, boards2.RoleOwner) - std.TestSetOrigCaller(member2) + std.TestSetOriginCaller(member2) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_3_j_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_3_j_filetest.gno index f6eacba7191..e2598f53572 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_3_j_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_3_j_filetest.gno @@ -13,10 +13,10 @@ const ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) boards2.CreateBoard(name) - std.TestSetOrigCaller(user) + std.TestSetOriginCaller(user) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_4_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_a_filetest.gno index c4321d8bc30..671a012a8bf 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_4_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_a_filetest.gno @@ -14,7 +14,7 @@ const ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) boards2.InviteMember(bid, member, boards2.RoleAdmin) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_4_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_b_filetest.gno index d99a4c77e2a..ff910620ca5 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_4_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_b_filetest.gno @@ -15,7 +15,7 @@ const ( var bid boards2.BoardID // Operate on board DAO func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("foo123") boards2.InviteMember(bid, member, boards2.RoleGuest) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_4_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_c_filetest.gno index a9d05bb3bc6..c4f174cd8e2 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_4_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_c_filetest.gno @@ -15,13 +15,13 @@ const ( var bid boards2.BoardID // Operate on board DAO func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") boards2.InviteMember(bid, owner2, boards2.RoleOwner) boards2.InviteMember(bid, admin, boards2.RoleAdmin) - std.TestSetOrigCaller(admin) + std.TestSetOriginCaller(admin) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_4_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_d_filetest.gno index 6adcefc9a3d..d0834af98d1 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_4_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_d_filetest.gno @@ -15,13 +15,13 @@ const ( var bid boards2.BoardID // Operate on board DAO func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") boards2.InviteMember(bid, admin, boards2.RoleAdmin) boards2.InviteMember(bid, admin2, boards2.RoleAdmin) - std.TestSetOrigCaller(admin) + std.TestSetOriginCaller(admin) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_4_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_e_filetest.gno index 6c0e42e1512..94c9e572eb9 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_4_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_e_filetest.gno @@ -14,7 +14,7 @@ const ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) boards2.InviteMember(bid, member, boards2.RoleAdmin) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_4_f_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_f_filetest.gno index 39764bf8442..17e67cd6ef3 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_4_f_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_f_filetest.gno @@ -14,7 +14,7 @@ const ( var bid boards2.BoardID // Operate on board DAO func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") boards2.InviteMember(bid, admin, boards2.RoleGuest) diff --git a/examples/gno.land/r/nt/boards2/v1/z_4_g_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_g_filetest.gno index 686a4e3d796..1ed67f84f6d 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_4_g_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_g_filetest.gno @@ -14,7 +14,7 @@ const ( var bid boards2.BoardID // Operate on board DAO func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") boards2.InviteMember(bid, admin, boards2.RoleGuest) diff --git a/examples/gno.land/r/nt/boards2/v1/z_4_h_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_4_h_filetest.gno index c60e46b003a..aa7d0be5d27 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_4_h_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_4_h_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_5_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_5_a_filetest.gno index 2672ddd1934..761fb359209 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_5_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_5_a_filetest.gno @@ -14,7 +14,7 @@ const ( var bid boards2.BoardID // Operate on board DAO func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") boards2.InviteMember(bid, user, boards2.RoleGuest) diff --git a/examples/gno.land/r/nt/boards2/v1/z_5_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_5_b_filetest.gno index a17c575aaac..4b1a1b0f57e 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_5_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_5_b_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_5_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_5_c_filetest.gno index 704f630fc03..87c82f57494 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_5_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_5_c_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_6_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_6_a_filetest.gno index 3783b4519e4..258c679abf1 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_6_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_6_a_filetest.gno @@ -15,7 +15,7 @@ const ( var bid boards2.BoardID // Operate on board DAO func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") boards2.InviteMember(bid, member, role) diff --git a/examples/gno.land/r/nt/boards2/v1/z_6_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_6_b_filetest.gno index 26d10c9001e..e179de32839 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_6_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_6_b_filetest.gno @@ -14,7 +14,7 @@ const ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_7_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_7_a_filetest.gno index 9a4ece4cf73..d738e1e4591 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_7_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_7_a_filetest.gno @@ -14,7 +14,7 @@ const ( var bid boards2.BoardID func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard(name) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_7_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_7_b_filetest.gno index 26cf509a919..970abeab621 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_7_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_7_b_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_8_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_8_a_filetest.gno index 4e0f50fea3b..31adb1431ca 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_8_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_8_a_filetest.gno @@ -17,7 +17,7 @@ const ( var bid boards2.BoardID func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_8_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_8_b_filetest.gno index 18d63bcc992..6ba9ff167de 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_8_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_8_b_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_8_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_8_c_filetest.gno index 37dec2c5d52..610c4422f2d 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_8_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_8_c_filetest.gno @@ -14,10 +14,10 @@ const ( var bid boards2.BoardID func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") - std.TestSetOrigCaller(user) + std.TestSetOriginCaller(user) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_8_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_8_d_filetest.gno index d97b61c111c..615b8eaf2c0 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_8_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_8_d_filetest.gno @@ -11,7 +11,7 @@ const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 var bid boards2.BoardID func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_8_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_8_e_filetest.gno index 41f12ddbf98..339efa5bed1 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_8_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_8_e_filetest.gno @@ -11,7 +11,7 @@ const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 var bid boards2.BoardID func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test123") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_9_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_9_a_filetest.gno index 19f6b5d1a47..4f8cf6c3fdb 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_9_a_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_9_a_filetest.gno @@ -18,7 +18,7 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") pid = boards2.CreateThread(bid, title, body) } diff --git a/examples/gno.land/r/nt/boards2/v1/z_9_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_9_b_filetest.gno index 1ff0f5439d2..9243ed05ee3 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_9_b_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_9_b_filetest.gno @@ -9,7 +9,7 @@ import ( const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_9_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_9_c_filetest.gno index e7fd465f9d9..a979ae2785e 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_9_c_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_9_c_filetest.gno @@ -11,7 +11,7 @@ const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 var bid boards2.BoardID func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") } diff --git a/examples/gno.land/r/nt/boards2/v1/z_9_d_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_9_d_filetest.gno index 6143406aafc..a0021b7e302 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_9_d_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_9_d_filetest.gno @@ -17,12 +17,12 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") pid = boards2.CreateThread(bid, "Foo", "bar") // Call using a user that has not permission to delete threads - std.TestSetOrigCaller(user) + std.TestSetOriginCaller(user) } func main() { diff --git a/examples/gno.land/r/nt/boards2/v1/z_9_e_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_9_e_filetest.gno index d33213c3915..3a84b4fb817 100644 --- a/examples/gno.land/r/nt/boards2/v1/z_9_e_filetest.gno +++ b/examples/gno.land/r/nt/boards2/v1/z_9_e_filetest.gno @@ -17,13 +17,13 @@ var ( ) func init() { - std.TestSetOrigCaller(owner) + std.TestSetOriginCaller(owner) bid = boards2.CreateBoard("test-board") pid = boards2.CreateThread(bid, "Foo", "bar") // Invite a member using a role with permission to delete threads boards2.InviteMember(bid, member, boards2.RoleAdmin) - std.TestSetOrigCaller(member) + std.TestSetOriginCaller(member) } func main() { From 47347a5ca9401b8a8e964762db336ea5e68952e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Mon, 24 Feb 2025 19:26:50 +0100 Subject: [PATCH 50/52] feat(boards2): improve menus and add missing actions (#3809) PR add missing options that link to Gnoweb help forms to allow easy generation of `gnokey` commands. It also introduces new menus using a simplistic Markdown to simulate sub-menus to avoid extensive menu entries that require specific permissions to execute. Board List Menu: Screenshot 2025-02-23 at 18 40 44 Board Menu: Screenshot 2025-02-23 at 18 40 57 Screenshot 2025-02-23 at 18 41 05 --- examples/gno.land/r/nt/boards2/v1/board.gno | 29 ++++++- .../gno.land/r/nt/boards2/v1/pagination.gno | 5 +- examples/gno.land/r/nt/boards2/v1/post.gno | 43 ++++++++-- .../gno.land/r/nt/boards2/v1/post_test.gno | 9 ++- examples/gno.land/r/nt/boards2/v1/render.gno | 81 +++++++++++++++++++ 5 files changed, 155 insertions(+), 12 deletions(-) diff --git a/examples/gno.land/r/nt/boards2/v1/board.gno b/examples/gno.land/r/nt/boards2/v1/board.gno index 071f26de61e..acb9cb3b050 100644 --- a/examples/gno.land/r/nt/boards2/v1/board.gno +++ b/examples/gno.land/r/nt/boards2/v1/board.gno @@ -105,10 +105,7 @@ func (board *Board) DeleteThread(pid PostID) { // // Pager is used for pagination if it's not nil. func (board *Board) Render(p *PaginationOpts) string { - sb := &strings.Builder{} - - sb.WriteString(newButtonLink("post", board.GetPostFormURL())) - sb.WriteString("\n\n") + var sb strings.Builder if board.threads.Size() == 0 { sb.WriteString("*This board doesn't have any threads.*") @@ -148,6 +145,30 @@ func (board *Board) GetURLFromReplyID(threadID, replyID PostID) string { return board.GetURL() + "/" + threadID.String() + "/" + replyID.String() } +func (board *Board) GetRenameFormURL() string { + return txlink.Call("RenameBoard", "name", board.name) +} + +func (board *Board) GetFreezeFormURL() string { + return txlink.Call("FreezeBoard", "boardID", board.id.String()) +} + +func (board *Board) GetFlaggingThresholdFormURL() string { + return txlink.Call("SetFlaggingThreshold", "boardID", board.id.String()) +} + +func (board *Board) GetInviteMemberFormURL() string { + return txlink.Call("InviteMember", "boardID", board.id.String()) +} + +func (board *Board) GetRemoveMemberFormURL() string { + return txlink.Call("RemoveMember", "boardID", board.id.String()) +} + +func (board *Board) GetChangeMemberRoleFormURL() string { + return txlink.Call("ChangeMemberRole", "boardID", board.id.String()) +} + func (board *Board) GetPostFormURL() string { return txlink.Call("CreateThread", "boardID", board.id.String()) } diff --git a/examples/gno.land/r/nt/boards2/v1/pagination.gno b/examples/gno.land/r/nt/boards2/v1/pagination.gno index bd8ec4579a9..edb47ca5d5b 100644 --- a/examples/gno.land/r/nt/boards2/v1/pagination.gno +++ b/examples/gno.land/r/nt/boards2/v1/pagination.gno @@ -27,7 +27,10 @@ func (opts *PaginationOpts) Iterate(tree *avl.Tree, cb func(k string, val interf } } - return page + if page.TotalPages > 1 { + return page + } + return nil } func mustGetPagination(rawPath string, pageSize int) *PaginationOpts { diff --git a/examples/gno.land/r/nt/boards2/v1/post.gno b/examples/gno.land/r/nt/boards2/v1/post.gno index 8de64d9165c..2341dbfe147 100644 --- a/examples/gno.land/r/nt/boards2/v1/post.gno +++ b/examples/gno.land/r/nt/boards2/v1/post.gno @@ -213,6 +213,13 @@ func (post *Post) GetURL() string { } func (post *Post) GetReplyFormURL() string { + if post.IsThread() { + return txlink.Call("CreateReply", + "boardID", post.board.id.String(), + "threadID", post.threadID.String(), + "replyID", "0", + ) + } return txlink.Call("CreateReply", "boardID", post.board.id.String(), "threadID", post.threadID.String(), @@ -241,6 +248,24 @@ func (post *Post) GetDeleteFormURL() string { ) } +func (post *Post) GetEditFormURL() string { + if post.IsThread() { + return txlink.Call("EditThread", + "boardID", post.board.id.String(), + "threadID", post.threadID.String(), + "title", post.GetTitle(), + "body", post.GetBody(), + ) + } + + return txlink.Call("EditReply", + "boardID", post.board.id.String(), + "threadID", post.threadID.String(), + "replyID", post.id.String(), + "body", post.GetBody(), + ) +} + func (post *Post) GetFlagFormURL() string { if post.IsThread() { return txlink.Call("FlagThread", @@ -325,14 +350,15 @@ func (post *Post) renderPostContent(sb *strings.Builder, indent string) { sb.WriteString("\n") } - postURL := post.GetURL() - // Buttons & counters sb.WriteString(indent) - sb.WriteString(`\- `) + if !post.IsThread() { + sb.WriteString(" \n" + indent) + } + sb.WriteString(newUserLink(post.creator)) sb.WriteString(", ") - sb.WriteString(newLink(post.createdAt.Format(dateFormat), postURL)) + sb.WriteString(post.createdAt.Format(dateFormat)) if post.repostsCount > 0 { sb.WriteString(", ") @@ -340,6 +366,8 @@ func (post *Post) renderPostContent(sb *strings.Builder, indent string) { sb.WriteString(" reposts") } + sb.WriteString(" - ") + if srcPost != nil { sb.WriteString(" ") sb.WriteString(newButtonLink("see source post", srcPost.GetURL())) @@ -353,6 +381,9 @@ func (post *Post) renderPostContent(sb *strings.Builder, indent string) { sb.WriteString(newButtonLink("repost", post.GetRepostFormURL())) } + sb.WriteString(" ") + sb.WriteString(newButtonLink("edit", post.GetEditFormURL())) + sb.WriteString(" ") sb.WriteString(newButtonLink("flag", post.GetFlagFormURL())) @@ -367,7 +398,7 @@ func (post *Post) Render(p *PaginationOpts, indent string, levels int) string { } // TODO: pass a builder as arg into Render. - sb := &strings.Builder{} + var sb strings.Builder if post.title != "" { sb.WriteString(indent) @@ -378,7 +409,7 @@ func (post *Post) Render(p *PaginationOpts, indent string, levels int) string { sb.WriteString("\n") } - post.renderPostContent(sb, indent) + post.renderPostContent(&sb, indent) if post.replies.Size() == 0 { return sb.String() diff --git a/examples/gno.land/r/nt/boards2/v1/post_test.gno b/examples/gno.land/r/nt/boards2/v1/post_test.gno index fc6070a1db7..3540099a805 100644 --- a/examples/gno.land/r/nt/boards2/v1/post_test.gno +++ b/examples/gno.land/r/nt/boards2/v1/post_test.gno @@ -119,10 +119,16 @@ func TestNewThread(t *testing.T) { uint(threadID), ) replyURL := ufmt.Sprintf( - "/r/nt/boards2/v1$help&func=CreateReply&boardID=%d&replyID=%d&threadID=%d", + "/r/nt/boards2/v1$help&func=CreateReply&boardID=%d&replyID=0&threadID=%d", uint(boardID), uint(threadID), + ) + editURL := ufmt.Sprintf( + "/r/nt/boards2/v1$help&func=EditThread&boardID=%d&body=%s&threadID=%d&title=%s", + uint(boardID), + body, uint(threadID), + strings.ReplaceAll(title, " ", "+"), ) repostURL := ufmt.Sprintf( "/r/nt/boards2/v1$help&func=CreateRepost&boardID=%d&threadID=%d", @@ -151,6 +157,7 @@ func TestNewThread(t *testing.T) { uassert.False(t, thread.HasReplies()) uassert.Equal(t, url, thread.GetURL()) uassert.Equal(t, replyURL, thread.GetReplyFormURL()) + uassert.Equal(t, editURL, thread.GetEditFormURL()) uassert.Equal(t, repostURL, thread.GetRepostFormURL()) uassert.Equal(t, deleteURL, thread.GetDeleteFormURL()) uassert.Equal(t, flagURL, thread.GetFlagFormURL()) diff --git a/examples/gno.land/r/nt/boards2/v1/render.gno b/examples/gno.land/r/nt/boards2/v1/render.gno index 653d5338927..52c7153555f 100644 --- a/examples/gno.land/r/nt/boards2/v1/render.gno +++ b/examples/gno.land/r/nt/boards2/v1/render.gno @@ -1,9 +1,12 @@ package boards2 import ( + "net/url" "strconv" + "strings" "gno.land/p/demo/mux" + "gno.land/p/moul/txlink" ) const ( @@ -12,6 +15,11 @@ const ( repliesPageSize = 30 ) +const ( + menuAdmin = "admin" + menuMembership = "membership" +) + func Render(path string) string { router := mux.NewRouter() router.HandleFunc("", renderBoardsList) @@ -27,6 +35,8 @@ func Render(path string) string { } func renderBoardsList(res *mux.ResponseWriter, req *mux.Request) { + renderBoardListMenu(res, req) + res.Write("These are all the boards of this realm:\n\n") p := mustGetPagination(req.RawPath, boardsPageSize) page := p.Iterate(&gBoardsByID, func(_ string, value interface{}) bool { @@ -43,6 +53,26 @@ func renderBoardsList(res *mux.ResponseWriter, req *mux.Request) { } } +func renderBoardListMenu(res *mux.ResponseWriter, req *mux.Request) { + res.Write(newButtonLink("create board", txlink.Call("CreateBoard")) + " - ") + + menu := getCurrentSubmenu(req.RawPath) + if menu == menuMembership { + res.Write("**membership**") + } else { + res.Write(newButtonLink("membership", submenuURL(menuMembership))) + } + + res.Write("\n\n") + + if menu == menuMembership { + res.Write("↳") + res.Write(newButtonLink("invite", txlink.Call("InviteMember", "boardID", "0")) + " ") + res.Write(newButtonLink("remove member", txlink.Call("RemoveMember", "boardID", "0")) + " ") + res.Write(newButtonLink("change member role", txlink.Call("ChangeMemberRole", "boardID", "0")) + "\n\n") + } +} + func renderBoard(res *mux.ResponseWriter, req *mux.Request) { name := req.GetVar("board") v, found := gBoardsByName.Get(name) @@ -52,10 +82,46 @@ func renderBoard(res *mux.ResponseWriter, req *mux.Request) { } board := v.(*Board) + renderBoardMenu(board, res, req) + p := mustGetPagination(req.RawPath, threadsPageSize) res.Write(board.Render(p)) } +func renderBoardMenu(board *Board, res *mux.ResponseWriter, req *mux.Request) { + res.Write(newButtonLink("post", board.GetPostFormURL()) + " - ") + + menu := getCurrentSubmenu(req.RawPath) + if menu == menuMembership { + res.Write("**membership** - ") + } else { + res.Write(newButtonLink("membership", submenuURL(menuMembership)) + " - ") + } + + if menu == menuAdmin { + res.Write("**admin**") + } else { + res.Write(newButtonLink("admin", submenuURL(menuAdmin))) + } + + res.Write("\n\n") + + if menu != "" { + res.Write("↳") + } + + switch menu { + case menuAdmin: + res.Write(newButtonLink("rename board", board.GetRenameFormURL()) + " ") + res.Write(newButtonLink("freeze", board.GetFreezeFormURL()) + " ") + res.Write(newButtonLink("change flagging threshold", board.GetFlaggingThresholdFormURL()) + "\n\n") + case menuMembership: + res.Write(newButtonLink("invite", board.GetInviteMemberFormURL()) + " ") + res.Write(newButtonLink("remove member", board.GetRemoveMemberFormURL()) + " ") + res.Write(newButtonLink("change member role", board.GetChangeMemberRoleFormURL()) + "\n\n") + } +} + func renderThread(res *mux.ResponseWriter, req *mux.Request) { name := req.GetVar("board") v, found := gBoardsByName.Get(name) @@ -123,3 +189,18 @@ func renderReply(res *mux.ResponseWriter, req *mux.Request) { // See: #3480 res.Write(reply.RenderInner()) } + +func submenuURL(name string) string { + // TODO: Submenu URL works because no other GET arguments are being used + return "?submenu=" + name +} + +func getCurrentSubmenu(rawURL string) string { + _, rawQuery, found := strings.Cut(rawURL, "?") + if !found { + return "" + } + + query, _ := url.ParseQuery(rawQuery) + return query.Get("submenu") +} From d4ee2c9c0a1fbf9230e831e818547bc493104076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Mon, 24 Feb 2025 19:27:16 +0100 Subject: [PATCH 51/52] feat(boards2): allow changing permissions implementation (#3718) Permissions interface allows assigning different permissions implementation to the boards2 realms and to individual boards. This interface may use a DAO to deal with members and governance. It would be convenient to be able to switch the permissions interface to allow each board to have a custom implementation that enforces custom rules. Apart from individual boards it's also convenient to be able to assign a custom implementation for boards realm permissions which for example might want to use a different DAO. --- examples/gno.land/r/nt/boards2/v1/board.gno | 4 +-- .../gno.land/r/nt/boards2/v1/board_test.gno | 30 ++++++++++------ examples/gno.land/r/nt/boards2/v1/boards.gno | 6 ++-- .../gno.land/r/nt/boards2/v1/permissions.gno | 1 + .../gno.land/r/nt/boards2/v1/post_test.gno | 18 +++++++--- examples/gno.land/r/nt/boards2/v1/public.gno | 32 +++++++++++++++-- .../r/nt/boards2/v1/z_18_a_filetest.gno | 33 ++++++++++++++++++ .../r/nt/boards2/v1/z_18_b_filetest.gno | 30 ++++++++++++++++ .../r/nt/boards2/v1/z_18_c_filetest.gno | 34 +++++++++++++++++++ 9 files changed, 165 insertions(+), 23 deletions(-) create mode 100644 examples/gno.land/r/nt/boards2/v1/z_18_a_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/v1/z_18_b_filetest.gno create mode 100644 examples/gno.land/r/nt/boards2/v1/z_18_c_filetest.gno diff --git a/examples/gno.land/r/nt/boards2/v1/board.gno b/examples/gno.land/r/nt/boards2/v1/board.gno index acb9cb3b050..cad3b8e3e01 100644 --- a/examples/gno.land/r/nt/boards2/v1/board.gno +++ b/examples/gno.land/r/nt/boards2/v1/board.gno @@ -35,7 +35,7 @@ type Board struct { readOnly bool } -func newBoard(id BoardID, name string, creator std.Address) *Board { +func newBoard(id BoardID, name string, creator std.Address, p Permissions) *Board { return &Board{ id: id, name: name, @@ -43,7 +43,7 @@ func newBoard(id BoardID, name string, creator std.Address) *Board { threads: avl.Tree{}, createdAt: time.Now(), deleted: avl.Tree{}, - perms: createDefaultBoardPermissions(creator), + perms: p, } } diff --git a/examples/gno.land/r/nt/boards2/v1/board_test.gno b/examples/gno.land/r/nt/boards2/v1/board_test.gno index f77b55ecc7f..a7738517c0b 100644 --- a/examples/gno.land/r/nt/boards2/v1/board_test.gno +++ b/examples/gno.land/r/nt/boards2/v1/board_test.gno @@ -5,6 +5,7 @@ import ( "strings" "testing" + "gno.land/p/demo/testutils" "gno.land/p/demo/uassert" "gno.land/p/moul/txlink" ) @@ -36,16 +37,17 @@ func TestBoard_GetURL(t *testing.T) { name := "foobar_test_get_url123" want := pkgPath + ":" + name - var addr std.Address - - board := newBoard(1, name, addr) + addr := testutils.TestAddress("creator") + perms := createDefaultBoardPermissions(addr) + board := newBoard(1, name, addr, perms) got := board.GetURL() uassert.Equal(t, want, got) } func TestBoard_GetThread(t *testing.T) { - var addr std.Address - b := newBoard(1, "test123", addr) + addr := testutils.TestAddress("creator") + perms := createDefaultBoardPermissions(addr) + b := newBoard(1, "test123", addr, perms) _, ok := b.GetThread(12345) uassert.False(t, ok) @@ -56,8 +58,9 @@ func TestBoard_GetThread(t *testing.T) { } func TestBoard_DeleteThread(t *testing.T) { - var addr std.Address - b := newBoard(1, "test123", addr) + addr := testutils.TestAddress("creator") + perms := createDefaultBoardPermissions(addr) + b := newBoard(1, "test123", addr, perms) post := b.AddThread(addr, "foo", "bar") id := post.GetPostID() @@ -72,7 +75,9 @@ var boardUrlPrefix = strings.TrimPrefix(std.CurrentRealm().PkgPath(), "gno.land" func TestBoard_GetURLFromThreadID(t *testing.T) { boardName := "test12345" - b := newBoard(BoardID(11), boardName, "") + addr := testutils.TestAddress("creator") + perms := createDefaultBoardPermissions(addr) + b := newBoard(BoardID(11), boardName, addr, perms) want := boardUrlPrefix + ":" + boardName + "/10" got := b.GetURLFromThreadID(10) @@ -81,7 +86,9 @@ func TestBoard_GetURLFromThreadID(t *testing.T) { func TestBoard_GetURLFromReplyID(t *testing.T) { boardName := "test12345" - b := newBoard(BoardID(11), boardName, "") + addr := testutils.TestAddress("creator") + perms := createDefaultBoardPermissions(addr) + b := newBoard(BoardID(11), boardName, addr, perms) want := boardUrlPrefix + ":" + boardName + "/10/20" got := b.GetURLFromReplyID(10, 20) @@ -90,8 +97,11 @@ func TestBoard_GetURLFromReplyID(t *testing.T) { func TestBoard_GetPostFormURL(t *testing.T) { bid := BoardID(386) - b := newBoard(bid, "foo1234", "") + addr := testutils.TestAddress("creator") + perms := createDefaultBoardPermissions(addr) + b := newBoard(bid, "foo1234", addr, perms) expect := txlink.Call("CreateThread", "boardID", bid.String()) + got := b.GetPostFormURL() uassert.Equal(t, expect, got) } diff --git a/examples/gno.land/r/nt/boards2/v1/boards.gno b/examples/gno.land/r/nt/boards2/v1/boards.gno index 8a0c57bd9dc..a1c737c4b91 100644 --- a/examples/gno.land/r/nt/boards2/v1/boards.gno +++ b/examples/gno.land/r/nt/boards2/v1/boards.gno @@ -7,7 +7,7 @@ import ( ) var ( - gPerm Permissions // TODO: Support assigning a different implementation + gPerms Permissions gLastBoardID BoardID gBoardsByID avl.Tree // string(id) -> *Board gBoardsByName avl.Tree // string(name) -> *Board @@ -17,8 +17,8 @@ func init() { // TODO: Define and change the default realm owner (or owners) owner := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 - // Initialize the default realm permissions - gPerm = createDefaultPermissions(owner) + // Initialize default realm permissions + gPerms = createDefaultPermissions(owner) } // incGetBoardID returns a new board ID. diff --git a/examples/gno.land/r/nt/boards2/v1/permissions.gno b/examples/gno.land/r/nt/boards2/v1/permissions.gno index 0e48d3f3004..235ac2bd513 100644 --- a/examples/gno.land/r/nt/boards2/v1/permissions.gno +++ b/examples/gno.land/r/nt/boards2/v1/permissions.gno @@ -18,6 +18,7 @@ const ( PermissionMemberInvite = "member:invite" PermissionMemberRemove = "member:remove" PermissionRoleChange = "role:change" + PermissionPermissionsUpdate = "permissions:update" ) const ( diff --git a/examples/gno.land/r/nt/boards2/v1/post_test.gno b/examples/gno.land/r/nt/boards2/v1/post_test.gno index 3540099a805..1f4926a7436 100644 --- a/examples/gno.land/r/nt/boards2/v1/post_test.gno +++ b/examples/gno.land/r/nt/boards2/v1/post_test.gno @@ -10,7 +10,9 @@ import ( ) func TestPostUpdate(t *testing.T) { - board := newBoard(1, "test123", testutils.TestAddress("creator")) + addr := testutils.TestAddress("creator") + perms := createDefaultBoardPermissions(addr) + board := newBoard(1, "test123", addr, perms) creator := testutils.TestAddress("creator") post := newPost(board, 1, creator, "Title", "Body", 1, 0, 0) title := "New Title" @@ -48,6 +50,9 @@ func TestPostSetVisible(t *testing.T) { } func TestPostAddRepostTo(t *testing.T) { + // TODO: Improve this unit test + addr := testutils.TestAddress("creatorDstBoard") + perms := createDefaultBoardPermissions(addr) cases := []struct { name, title, body string dstBoard *Board @@ -59,7 +64,7 @@ func TestPostAddRepostTo(t *testing.T) { name: "repost thread", title: "Repost Title", body: "Repost body", - dstBoard: newBoard(42, "dst123", testutils.TestAddress("creatorDstBoard")), + dstBoard: newBoard(42, "dst123", addr, perms), setup: func() *Post { return createTestThread(t) }, }, { @@ -112,7 +117,8 @@ func TestNewThread(t *testing.T) { boardID := BoardID(1) threadID := PostID(42) boardName := "test123" - board := newBoard(boardID, boardName, creator) + perms := createDefaultBoardPermissions(creator) + board := newBoard(boardID, boardName, creator, perms) url := ufmt.Sprintf( "/r/nt/boards2/v1:%s/%d", boardName, @@ -289,7 +295,8 @@ func TestNewReply(t *testing.T) { parentID := PostID(1) replyID := PostID(2) boardName := "test123" - board := newBoard(boardID, boardName, creator) + perms := createDefaultBoardPermissions(creator) + board := newBoard(boardID, boardName, creator, perms) url := ufmt.Sprintf( "/r/nt/boards2/v1:%s/%d/%d", boardName, @@ -398,7 +405,8 @@ func createTestThread(t *testing.T) *Post { t.Helper() creator := testutils.TestAddress("creator") - board := newBoard(1, "test_board_123", creator) + perms := createDefaultBoardPermissions(creator) + board := newBoard(1, "test_board_123", creator, perms) return board.AddThread(creator, "Title", "Body") } diff --git a/examples/gno.land/r/nt/boards2/v1/public.gno b/examples/gno.land/r/nt/boards2/v1/public.gno index f7d801b49c6..c42aa0e7a6e 100644 --- a/examples/gno.land/r/nt/boards2/v1/public.gno +++ b/examples/gno.land/r/nt/boards2/v1/public.gno @@ -5,6 +5,31 @@ import ( "strings" ) +// SetPermissions sets a permissions implementation for boards2 realm or a board. +func SetPermissions(bid BoardID, p Permissions) { + if p == nil { + panic("permissions is required") + } + + if bid != 0 { + assertBoardExists(bid) + } + + caller := std.OriginCaller() + args := Args{bid} + gPerms.WithPermission(caller, PermissionPermissionsUpdate, args, func(Args) { + // When board ID is zero it means that realm permissions are being updated + if bid == 0 { + gPerms = p + return + } + + // Otherwise update the permissions of a single board + board := mustGetBoard(bid) + board.perms = p + }) +} + // GetBoardIDFromName searches a board by name and returns it's ID. func GetBoardIDFromName(name string) (_ BoardID, found bool) { v, found := gBoardsByName.Get(name) @@ -23,10 +48,11 @@ func CreateBoard(name string) BoardID { caller := std.OriginCaller() id := incGetBoardID() args := Args{name, id} - gPerm.WithPermission(caller, PermissionBoardCreate, args, func(Args) { + gPerms.WithPermission(caller, PermissionBoardCreate, args, func(Args) { assertBoardNameNotExists(name) - board := newBoard(id, name, caller) + perms := createDefaultBoardPermissions(caller) + board := newBoard(id, name, caller, perms) gBoardsByID.Set(id.Key(), board) gBoardsByName.Set(name, board) }) @@ -461,5 +487,5 @@ func mustGetPermissions(bid BoardID) Permissions { board := mustGetBoard(bid) return board.perms } - return gPerm + return gPerms } diff --git a/examples/gno.land/r/nt/boards2/v1/z_18_a_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_18_a_filetest.gno new file mode 100644 index 00000000000..4c16b2ea07f --- /dev/null +++ b/examples/gno.land/r/nt/boards2/v1/z_18_a_filetest.gno @@ -0,0 +1,33 @@ +package main + +import ( + "std" + + "gno.land/p/nt/commondao" + + boards2 "gno.land/r/nt/boards2/v1" +) + +const ( + owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + bid = boards2.BoardID(0) // Operate on realm instead of individual boards +) + +var perms boards2.Permissions + +func init() { + // Create a new permissions instance without users + perms = boards2.NewDefaultPermissions(commondao.New()) + + std.TestSetOriginCaller(owner) +} + +func main() { + boards2.SetPermissions(bid, perms) + + // Owner that setted new permissions is not a member of the new permissions + println(boards2.IsMember(bid, owner)) +} + +// Output: +// false diff --git a/examples/gno.land/r/nt/boards2/v1/z_18_b_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_18_b_filetest.gno new file mode 100644 index 00000000000..3d65b049b5b --- /dev/null +++ b/examples/gno.land/r/nt/boards2/v1/z_18_b_filetest.gno @@ -0,0 +1,30 @@ +package main + +import ( + "std" + + "gno.land/p/nt/commondao" + + boards2 "gno.land/r/nt/boards2/v1" +) + +const ( + user = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // @test2 + bid = boards2.BoardID(0) // Operate on realm instead of individual boards +) + +var perms boards2.Permissions + +func init() { + // Create a new permissions instance + perms = boards2.NewDefaultPermissions(commondao.New()) + + std.TestSetOriginCaller(user) +} + +func main() { + boards2.SetPermissions(bid, perms) +} + +// Error: +// unauthorized diff --git a/examples/gno.land/r/nt/boards2/v1/z_18_c_filetest.gno b/examples/gno.land/r/nt/boards2/v1/z_18_c_filetest.gno new file mode 100644 index 00000000000..84c4cad0fe3 --- /dev/null +++ b/examples/gno.land/r/nt/boards2/v1/z_18_c_filetest.gno @@ -0,0 +1,34 @@ +package main + +import ( + "std" + + "gno.land/p/nt/commondao" + + boards2 "gno.land/r/nt/boards2/v1" +) + +const owner = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // @test1 + +var ( + perms boards2.Permissions + bid boards2.BoardID +) + +func init() { + // Create a new permissions instance without users + perms = boards2.NewDefaultPermissions(commondao.New()) + + std.TestSetOriginCaller(owner) + bid = boards2.CreateBoard("foobar") +} + +func main() { + boards2.SetPermissions(bid, perms) + + // Owner that setted new board permissions is not a member of the new permissions + println(boards2.IsMember(bid, owner)) +} + +// Output: +// false From 129cccb80f162d83ba4e2fa73be1afc1fbe3d36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Tue, 25 Feb 2025 20:01:08 +0100 Subject: [PATCH 52/52] feat(boards2): add views to list board members (#3803) PR adds two new views to list realm or board members: - `/r/nt/boards2/v1:members` - `/r/nt/boards2/v1:BOARD_NAME/members` Views list the member address and assigned role. --- examples/gno.land/r/nt/boards2/v1/board.gno | 16 +++- .../gno.land/r/nt/boards2/v1/permissions.gno | 15 ++++ .../r/nt/boards2/v1/permissions_default.gno | 24 ++++-- .../boards2/v1/permissions_default_test.gno | 75 +++++++++++++++++++ examples/gno.land/r/nt/boards2/v1/render.gno | 54 +++++++++++++ 5 files changed, 174 insertions(+), 10 deletions(-) diff --git a/examples/gno.land/r/nt/boards2/v1/board.gno b/examples/gno.land/r/nt/boards2/v1/board.gno index cad3b8e3e01..85692fa3c91 100644 --- a/examples/gno.land/r/nt/boards2/v1/board.gno +++ b/examples/gno.land/r/nt/boards2/v1/board.gno @@ -51,6 +51,11 @@ func (board *Board) GetID() BoardID { return board.id } +// GetName returns the name of the board. +func (board *Board) GetName() string { + return board.name +} + // GetURL returns the relative URL of the board. func (board *Board) GetURL() string { return strings.TrimPrefix(std.CurrentRealm().PkgPath(), "gno.land") + ":" + url.PathEscape(board.name) @@ -105,13 +110,12 @@ func (board *Board) DeleteThread(pid PostID) { // // Pager is used for pagination if it's not nil. func (board *Board) Render(p *PaginationOpts) string { - var sb strings.Builder - if board.threads.Size() == 0 { - sb.WriteString("*This board doesn't have any threads.*") - return sb.String() + return "*This board doesn't have any threads.*" } + var sb strings.Builder + page := p.Iterate(&board.threads, func(_ string, v interface{}) bool { p := v.(*Post) if p.isHidden { @@ -173,6 +177,10 @@ func (board *Board) GetPostFormURL() string { return txlink.Call("CreateThread", "boardID", board.id.String()) } +func (board *Board) GetMembersURL() string { + return board.GetURL() + "/members" +} + func createDefaultBoardPermissions(owner std.Address) *DefaultPermissions { perms := NewDefaultPermissions(commondao.New()) perms.SetSuperRole(RoleOwner) diff --git a/examples/gno.land/r/nt/boards2/v1/permissions.gno b/examples/gno.land/r/nt/boards2/v1/permissions.gno index 235ac2bd513..2d6de0ec3f2 100644 --- a/examples/gno.land/r/nt/boards2/v1/permissions.gno +++ b/examples/gno.land/r/nt/boards2/v1/permissions.gno @@ -38,6 +38,15 @@ type ( // Args is a list of generic arguments. Args []interface{} + // User contains user info. + User struct { + Address std.Address + Roles []Role + } + + // UsersIterFn defines a function type to iterate users. + UsersIterFn func(User) bool + // Permissions define an interface to for permissioned execution. Permissions interface { // HasRole checks if a user has a specific role assigned. @@ -61,5 +70,11 @@ type ( // HasUser checks if a user exists. HasUser(std.Address) bool + + // UsersCount returns the total number of users the permissioner contains. + UsersCount() int + + // IterateUsers iterates permissions' users. + IterateUsers(start, count int, fn UsersIterFn) bool } ) diff --git a/examples/gno.land/r/nt/boards2/v1/permissions_default.gno b/examples/gno.land/r/nt/boards2/v1/permissions_default.gno index 931bd751748..9379b43e07f 100644 --- a/examples/gno.land/r/nt/boards2/v1/permissions_default.gno +++ b/examples/gno.land/r/nt/boards2/v1/permissions_default.gno @@ -127,6 +127,21 @@ func (dp DefaultPermissions) HasUser(user std.Address) bool { return dp.users.Has(user.String()) } +// UsersCount returns the total number of users the permissioner contains. +func (dp DefaultPermissions) UsersCount() int { + return dp.users.Size() +} + +// IterateUsers iterates permissions' users. +func (dp DefaultPermissions) IterateUsers(start, count int, fn UsersIterFn) bool { + return dp.users.IterateByOffset(start, count, func(k string, v interface{}) bool { + return fn(User{ + Address: std.Address(k), + Roles: v.([]Role), + }) + }) +} + // WithPermission calls a callback when a user has a specific permission. // It panics on error or when a handler panics. // Callbacks are by default called when there is no handle registered for the permission. @@ -194,8 +209,7 @@ func (dp DefaultPermissions) handleMemberInvite(args Args, cb func(Args)) { } if role == RoleOwner { - caller := std.OriginCaller() - if !dp.HasRole(caller, RoleOwner) { + if !dp.HasRole(std.OriginCaller(), RoleOwner) { panic("only owners are allowed to invite other owners") } } @@ -206,8 +220,7 @@ func (dp DefaultPermissions) handleMemberInvite(args Args, cb func(Args)) { func (dp DefaultPermissions) handleRoleChange(args Args, cb func(Args)) { // Owners and Admins can change roles. // Admins should not be able to assign or remove the Owner role from members. - caller := std.OriginCaller() - if dp.HasRole(caller, RoleAdmin) { + if dp.HasRole(std.OriginCaller(), RoleAdmin) { role, ok := args[2].(Role) if !ok { panic("expected a valid member role") @@ -253,9 +266,8 @@ func assertValidBoardNameLength(name string) { func assertBoardNameBelongsToCaller(name string) { // When the board name is the name of a registered user // check that caller is the owner of the name. - caller := std.OriginCaller() user := users.GetUserByName(name) - if user != nil && user.Address != caller { + if user != nil && user.Address != std.OriginCaller() { panic("board name is a user name registered to a different user") } } diff --git a/examples/gno.land/r/nt/boards2/v1/permissions_default_test.gno b/examples/gno.land/r/nt/boards2/v1/permissions_default_test.gno index 2f504b84f0a..079de274e8d 100644 --- a/examples/gno.land/r/nt/boards2/v1/permissions_default_test.gno +++ b/examples/gno.land/r/nt/boards2/v1/permissions_default_test.gno @@ -533,3 +533,78 @@ func TestDefaultPermissionsRemoveUser(t *testing.T) { }) } } + +func TestDefaultPermissionsIterateUsers(t *testing.T) { + users := []User{ + { + Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + Roles: []Role{"foo"}, + }, + { + Address: "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj", + Roles: []Role{"foo", "bar"}, + }, + { + Address: "g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5", + Roles: []Role{"bar"}, + }, + } + + perms := NewDefaultPermissions(commondao.New()) + perms.AddRole("foo", "perm1") + perms.AddRole("bar", "perm2") + for _, u := range users { + perms.AddUser(u.Address, u.Roles...) + } + + cases := []struct { + name string + start, count, want int + }{ + { + name: "exceed users count", + count: 50, + want: 3, + }, + { + name: "exact users count", + count: 3, + want: 3, + }, + { + name: "two users", + start: 1, + count: 2, + want: 2, + }, + { + name: "one user", + start: 1, + count: 1, + want: 1, + }, + { + name: "no iteration", + start: 50, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var i int + perms.IterateUsers(0, len(users), func(u User) bool { + urequire.True(t, i < len(users), "expect iterator to respect number of users") + uassert.Equal(t, users[i].Address, u.Address) + + urequire.Equal(t, len(users[i].Roles), len(u.Roles), "expect number of roles to match") + for j, r := range u.Roles { + uassert.Equal(t, string(users[i].Roles[j]), string(u.Roles[j])) + } + + i++ + }) + + uassert.Equal(t, i, len(users), "expect iterator to iterate all users") + }) + } +} diff --git a/examples/gno.land/r/nt/boards2/v1/render.gno b/examples/gno.land/r/nt/boards2/v1/render.gno index 52c7153555f..59fb2f97406 100644 --- a/examples/gno.land/r/nt/boards2/v1/render.gno +++ b/examples/gno.land/r/nt/boards2/v1/render.gno @@ -2,10 +2,12 @@ package boards2 import ( "net/url" + "std" "strconv" "strings" "gno.land/p/demo/mux" + "gno.land/p/jeronimoalbi/pager" "gno.land/p/moul/txlink" ) @@ -23,7 +25,9 @@ const ( func Render(path string) string { router := mux.NewRouter() router.HandleFunc("", renderBoardsList) + router.HandleFunc("members", renderMembers) router.HandleFunc("{board}", renderBoard) + router.HandleFunc("{board}/members", renderMembers) router.HandleFunc("{board}/{thread}", renderThread) router.HandleFunc("{board}/{thread}/{reply}", renderReply) @@ -66,8 +70,11 @@ func renderBoardListMenu(res *mux.ResponseWriter, req *mux.Request) { res.Write("\n\n") if menu == menuMembership { + path := strings.TrimPrefix(std.CurrentRealm().PkgPath(), "gno.land") + res.Write("↳") res.Write(newButtonLink("invite", txlink.Call("InviteMember", "boardID", "0")) + " ") + res.Write(newButtonLink("members", path+":members") + " ") res.Write(newButtonLink("remove member", txlink.Call("RemoveMember", "boardID", "0")) + " ") res.Write(newButtonLink("change member role", txlink.Call("ChangeMemberRole", "boardID", "0")) + "\n\n") } @@ -117,6 +124,7 @@ func renderBoardMenu(board *Board, res *mux.ResponseWriter, req *mux.Request) { res.Write(newButtonLink("change flagging threshold", board.GetFlaggingThresholdFormURL()) + "\n\n") case menuMembership: res.Write(newButtonLink("invite", board.GetInviteMemberFormURL()) + " ") + res.Write(newButtonLink("members", board.GetPath()+"/members") + " ") res.Write(newButtonLink("remove member", board.GetRemoveMemberFormURL()) + " ") res.Write(newButtonLink("change member role", board.GetChangeMemberRoleFormURL()) + "\n\n") } @@ -190,6 +198,52 @@ func renderReply(res *mux.ResponseWriter, req *mux.Request) { res.Write(reply.RenderInner()) } +func renderMembers(res *mux.ResponseWriter, req *mux.Request) { + perms := gPerms + name := req.GetVar("board") + if name != "" { + v, found := gBoardsByName.Get(name) + if !found { + res.Write("Board does not exist: " + name) + return + } + + board := v.(*Board) + perms = board.perms + + res.Write("# Board Members: " + board.GetName() + "\n\n") + } else { + res.Write("# Boards Members\n\n") + } + + p, err := pager.New(req.RawPath, perms.UsersCount()) + if err != nil { + res.Write(err.Error()) + return + } + + perms.IterateUsers(p.Offset(), p.PageSize(), func(u User) bool { + res.Write("- " + u.Address.String() + " " + rolesToString(u.Roles) + "\n") + return false + }) + + if p.HasPages() { + res.Write("\n\n" + pager.Picker(p)) + } +} + +func rolesToString(roles []Role) string { + if len(roles) == 0 { + return "" + } + + names := make([]string, len(roles)) + for i, r := range roles { + names[i] = string(r) + } + return strings.Join(names, ", ") +} + func submenuURL(name string) string { // TODO: Submenu URL works because no other GET arguments are being used return "?submenu=" + name