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