Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(boards2): add core flagging logic #3451

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading