Skip to content

Commit

Permalink
feat(boards2): add core flagging logic (#3451)
Browse files Browse the repository at this point in the history
Add flagging support for boards2 realm.

Closes #3146
  • Loading branch information
x1unix authored Jan 7, 2025
1 parent b8ac6b0 commit 8844f30
Show file tree
Hide file tree
Showing 11 changed files with 268 additions and 8 deletions.
7 changes: 6 additions & 1 deletion examples/gno.land/r/demo/boards2/board.gno
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
}
Expand Down
47 changes: 47 additions & 0 deletions examples/gno.land/r/demo/boards2/flag.gno
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions examples/gno.land/r/demo/boards2/permission.gno
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
45 changes: 41 additions & 4 deletions examples/gno.land/r/demo/boards2/post.gno
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -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
})
}
Expand Down
21 changes: 21 additions & 0 deletions examples/gno.land/r/demo/boards2/post_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 45 additions & 1 deletion examples/gno.land/r/demo/boards2/public.gno
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -57,21 +72,38 @@ 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
reply = thread.AddReply(caller, body)
} 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()

Expand Down Expand Up @@ -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")
}
}
8 changes: 6 additions & 2 deletions examples/gno.land/r/demo/boards2/render.gno
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -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())
}
Expand Down
23 changes: 23 additions & 0 deletions examples/gno.land/r/demo/boards2/z_2_a_filetest.gno
Original file line number Diff line number Diff line change
@@ -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
26 changes: 26 additions & 0 deletions examples/gno.land/r/demo/boards2/z_2_b_filetest.gno
Original file line number Diff line number Diff line change
@@ -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
26 changes: 26 additions & 0 deletions examples/gno.land/r/demo/boards2/z_2_c_filetest.gno
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions examples/gno.land/r/demo/boards2/z_2_d_filetest.gno
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 8844f30

Please sign in to comment.