From 6b210f73bf22e58b9d8cc38f739ed9b158076036 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Mon, 3 Jul 2023 22:10:47 +0700 Subject: [PATCH 01/55] wip: init + testing social feed --- .../gno.land/r/demo/social_feeds/feed.gno | 77 ++++++++++++++++ .../gno.land/r/demo/social_feeds/feeds.gno | 22 +++++ .../r/demo/social_feeds/feeds_test.gno | 45 ++++++++++ examples/gno.land/r/demo/social_feeds/gno.mod | 6 ++ .../gno.land/r/demo/social_feeds/misc.gno | 66 ++++++++++++++ .../gno.land/r/demo/social_feeds/post.gno | 87 +++++++++++++++++++ .../gno.land/r/demo/social_feeds/public.gno | 84 ++++++++++++++++++ 7 files changed, 387 insertions(+) create mode 100644 examples/gno.land/r/demo/social_feeds/feed.gno create mode 100644 examples/gno.land/r/demo/social_feeds/feeds.gno create mode 100644 examples/gno.land/r/demo/social_feeds/feeds_test.gno create mode 100644 examples/gno.land/r/demo/social_feeds/gno.mod create mode 100644 examples/gno.land/r/demo/social_feeds/misc.gno create mode 100644 examples/gno.land/r/demo/social_feeds/post.gno create mode 100644 examples/gno.land/r/demo/social_feeds/public.gno diff --git a/examples/gno.land/r/demo/social_feeds/feed.gno b/examples/gno.land/r/demo/social_feeds/feed.gno new file mode 100644 index 00000000000..bcf5d180a9d --- /dev/null +++ b/examples/gno.land/r/demo/social_feeds/feed.gno @@ -0,0 +1,77 @@ +package social_feeds + +import ( + "std" + "time" + + "gno.land/p/demo/avl" +) + +type FeedID uint64 + +type Feed struct { + id FeedID + url string + name string + creator std.Address + owner std.Address + posts avl.Tree // Post.id -> *Post + createdAt time.Time + deleted avl.Tree + + postsCtr uint64 +} + +func newFeed(fid FeedID, url string, name string, creator std.Address) *Feed { + if !reName.MatchString(name) { + panic("invalid feed name: " + name) + } + + if gFeedsByName.Has(name) { + panic("feed already exists: " + name) + } + + return &Feed{ + id: fid, + url: url, + name: name, + creator: creator, + owner: creator, + posts: avl.Tree{}, + createdAt: time.Now(), + deleted: avl.Tree{}, + + postsCtr: 0, + } +} + +func (feed *Feed) incGetPostID() PostID { + feed.postsCtr++ + return PostID(feed.postsCtr) +} + +func (feed *Feed) GetPost(pid PostID) *Post { + pidkey := postIDKey(pid) + post_, exists := feed.posts.Get(pidkey) + if !exists { + return nil + } + return post_.(*Post) +} + +func (feed *Feed) MustGetPost(pid PostID) *Post { + post := feed.GetPost(pid) + if post == nil { + panic("post does not exist") + } + return post +} + +func (feed *Feed) AddPost(creator std.Address, parentID PostID, category uint64, metadata string) *Post { + pid := feed.incGetPostID() + pidkey := postIDKey(pid) + + post := newPost(feed, pid, creator, parentID, category, metadata) + feed.posts.Set(pidkey, post) + return post +} diff --git a/examples/gno.land/r/demo/social_feeds/feeds.gno b/examples/gno.land/r/demo/social_feeds/feeds.gno new file mode 100644 index 00000000000..ae0f4ed8ce1 --- /dev/null +++ b/examples/gno.land/r/demo/social_feeds/feeds.gno @@ -0,0 +1,22 @@ +package social_feeds + +import ( + "regexp" + + "gno.land/p/demo/avl" +) + +//---------------------------------------- +// Realm (package) state + +var ( + gFeeds avl.Tree // id -> *Feed + gFeedsCtr int // increments Feed.id + gFeedsByName avl.Tree // name -> *Feed + 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/social_feeds/feeds_test.gno b/examples/gno.land/r/demo/social_feeds/feeds_test.gno new file mode 100644 index 00000000000..59c1fe5bef1 --- /dev/null +++ b/examples/gno.land/r/demo/social_feeds/feeds_test.gno @@ -0,0 +1,45 @@ +package social_feeds + +import ( + "fmt" + "testing" + + "gno.land/r/demo/users" +) + +func Test(t *testing.T) { + { + // Create Feed + testing.SetOrigCaller("yo") + feedID := CreateFeed("teritori") + + if feedID != 1 { + t.Fatalf("expected feedID: 1, got %q.", feedID) + } + } + { + // Create Post + postID := CreatePost(1, 0, 0, `{"gifs": [], "files": [], "title": "", "message": "testouille", "hashtags": [], "mentions": [], "createdAt": "2023-03-29T12:19:04.858Z", "updatedAt": "2023-03-29T12:19:04.858Z"}`) + + if postID != 1 { + t.Fatalf("expected postID: 1, got %q.", postID) + } + } + { + // React Post + ReactPost(1, 1, "🥰", true) + + feed := mustGetFeed(1) + post := feed.MustGetPost(1) + + reactionCount_, ok := post.reactions.Get("🥰") + if !ok { + t.Fatalf("expected 🥰 exists") + } + + reactionCount := reactionCount_.(int) + if reactionCount != 1 { + t.Fatalf("expected reactionCount: 1, got %q.", reactionCount) + } + } +} diff --git a/examples/gno.land/r/demo/social_feeds/gno.mod b/examples/gno.land/r/demo/social_feeds/gno.mod new file mode 100644 index 00000000000..5633f9c71b6 --- /dev/null +++ b/examples/gno.land/r/demo/social_feeds/gno.mod @@ -0,0 +1,6 @@ +module gno.land/r/demo/social_feeds + +require ( + "gno.land/p/demo/avl" v0.0.0-latest + "gno.land/r/demo/users" v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/social_feeds/misc.gno b/examples/gno.land/r/demo/social_feeds/misc.gno new file mode 100644 index 00000000000..4ba28cae284 --- /dev/null +++ b/examples/gno.land/r/demo/social_feeds/misc.gno @@ -0,0 +1,66 @@ +package social_feeds + +import ( + "std" + "strconv" + "strings" + + "gno.land/r/demo/users" +) + +func getFeed(fid FeedID) *Feed { + fidkey := feedIDKey(fid) + feed_, exists := gFeeds.Get(fidkey) + if !exists { + return nil + } + feed := feed_.(*Feed) + return feed +} + +func mustGetFeed(fid FeedID) *Feed { + feed := getFeed(fid) + if feed == nil { + panic("Feed does not exist") + } + return feed +} + +func incGetFeedID() FeedID { + gFeedsCtr++ + return FeedID(gFeedsCtr) +} + +func usernameOf(addr std.Address) string { + user := users.GetUserByAddress(addr) + if user == nil { + return "" + } else { + return user.Name() + } +} + +func feedIDKey(fid FeedID) string { + return padZero(uint64(fid), 10) +} + +func postIDKey(pid PostID) string { + return padZero(uint64(pid), 10) +} + +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 + } +} diff --git a/examples/gno.land/r/demo/social_feeds/post.gno b/examples/gno.land/r/demo/social_feeds/post.gno new file mode 100644 index 00000000000..c36b9867822 --- /dev/null +++ b/examples/gno.land/r/demo/social_feeds/post.gno @@ -0,0 +1,87 @@ +package social_feeds + +import ( + "std" + "strconv" + "time" + + "gno.land/p/demo/avl" +) + +type PostID uint64 + +type Reaction struct { + icon string + count uint64 +} + +var Categories []string = []string{ + "Reaction", + "Comment", + "Norman", + "Article", + "Picture", + "Audio", + "Video", +} + +type Post struct { + id PostID + parentID PostID + feedID FeedID + category uint64 + metadata string + reactions avl.Tree + creator std.Address + deleted bool + + createdAt time.Time + updatedAt time.Time + deletedAt time.Time +} + +func newPost(feed *Feed, id PostID, creator std.Address, parentID PostID, category uint64, metadata string) *Post { + return &Post{ + id: id, + parentID: parentID, + feedID: feed.id, + category: category, + metadata: metadata, + reactions: avl.Tree{}, + creator: creator, + createdAt: time.Now(), + } +} + +func (post *Post) Update(category uint64, metadata string) { + post.category = category + post.metadata = metadata + post.updatedAt = time.Now() +} + +func (post *Post) Delete() { + post.deleted = true + post.deletedAt = time.Now() +} + +// Always remove reaction if count = 0 +func (post *Post) React(icon string, up bool) { + count_, ok := post.reactions.Get(icon) + count := 0 + + if ok { + count = count_.(int) + } + + if up { + count++ + } else { + count-- + } + + if count <= 0 { + post.reactions.Remove(icon) + } else { + post.reactions.Set(icon, count) + } +} diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno new file mode 100644 index 00000000000..8c81e333995 --- /dev/null +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -0,0 +1,84 @@ +package social_feeds + +import ( + "std" + "strconv" +) + +func CreateFeed(name string) FeedID { + std.AssertOriginCall() + fid := incGetFeedID() + caller := std.PrevRealm().Addr() + if usernameOf(caller) == "" { + panic("unauthorized") + } + url := "/r/demo/feeds:" + name + feed := newFeed(fid, url, name, caller) + fidkey := feedIDKey(fid) + gFeeds.Set(fidkey, feed) + gFeedsByName.Set(name, feed) + return feed.id +} + +// Anyone can create a post in a existing feed +// Anonymous user has to pay a configurable minimum fee +func CreatePost(fid FeedID, parentID PostID, catetory uint64, metadata string) PostID { + std.AssertOriginCall() + caller := std.PrevRealm().Addr() + + // if usernameOf(caller) == "" { + // if !checkAnonFee() { + // panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") + // } + // } + feed := mustGetFeed(fid) + post := feed.AddPost(caller, parentID, catetory, metadata) + return post.id +} + +// Only post's owner can edit post +func EditPost(fid FeedID, pid PostID, category uint64, metadata string) { + std.AssertOriginCall() + caller := std.PrevRealm().Addr() + feed := mustGetFeed(fid) + post := feed.MustGetPost(pid) + + if caller != post.creator { + panic("you are not creator of this post") + } + + post.Update(category, metadata) +} + +// Only owner can delete the post +func DeletePost(fid FeedID, pid PostID) { + std.AssertOriginCall() + caller := std.PrevRealm().Addr() + feed := mustGetFeed(fid) + post := feed.MustGetPost(pid) + + if caller != post.creator { + panic("you are not creator of this post") + } + + post.Delete() +} + +// Any one can react post +func ReactPost(fid FeedID, pid PostID, icon string, up bool) { + std.AssertOriginCall() + caller := std.PrevRealm().Addr() + feed := mustGetFeed(fid) + post := feed.MustGetPost(pid) + + post.React(icon, up) +} + +func checkAnonFee() bool { + sent := std.GetOrigSend() + anonFeeCoin := std.Coin{"ugnot", int64(gDefaultAnonFee)} + if len(sent) == 1 && sent[0].IsGTE(anonFeeCoin) { + return true + } + return false +} From 7562657c2619c414d9e60052ef58078c4025dad2 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Tue, 4 Jul 2023 21:20:49 +0700 Subject: [PATCH 02/55] feat: add tests --- .../r/demo/social_feeds/feeds_test.gno | 110 ++++++++++++------ .../gno.land/r/demo/social_feeds/public.gno | 10 +- 2 files changed, 81 insertions(+), 39 deletions(-) diff --git a/examples/gno.land/r/demo/social_feeds/feeds_test.gno b/examples/gno.land/r/demo/social_feeds/feeds_test.gno index 59c1fe5bef1..b1305da4e82 100644 --- a/examples/gno.land/r/demo/social_feeds/feeds_test.gno +++ b/examples/gno.land/r/demo/social_feeds/feeds_test.gno @@ -1,45 +1,89 @@ package social_feeds +// SEND: 200000000ugnot + import ( "fmt" + "std" + "strconv" "testing" + "gno.land/p/demo/testutils" + "gno.land/r/demo/boards" "gno.land/r/demo/users" ) func Test(t *testing.T) { - { - // Create Feed - testing.SetOrigCaller("yo") - feedID := CreateFeed("teritori") - - if feedID != 1 { - t.Fatalf("expected feedID: 1, got %q.", feedID) - } - } - { - // Create Post - postID := CreatePost(1, 0, 0, `{"gifs": [], "files": [], "title": "", "message": "testouille", "hashtags": [], "mentions": [], "createdAt": "2023-03-29T12:19:04.858Z", "updatedAt": "2023-03-29T12:19:04.858Z"}`) - - if postID != 1 { - t.Fatalf("expected postID: 1, got %q.", postID) - } - } - { - // React Post - ReactPost(1, 1, "🥰", true) - - feed := mustGetFeed(1) - post := feed.MustGetPost(1) - - reactionCount_, ok := post.reactions.Get("🥰") - if !ok { - t.Fatalf("expected 🥰 exists") - } - - reactionCount := reactionCount_.(int) - if reactionCount != 1 { - t.Fatalf("expected reactionCount: 1, got %q.", reactionCount) - } + // NOTE: Dont know why std.GetOrigCaller in users.Register is always = std.GetCallerAt(1) here + admin := std.GetCallerAt(1) + user := testutils.TestAddress("user") + + std.TestSetOrigCaller(admin) + std.TestSetOrigSend(std.Coins{{"ugnot", 200000000}}, nil) + users.Register("", "social_feeds_admin", "") + + // Create feed with registered user ===================================== + feedName := "teritori" + feedID := CreateFeed(feedName) + createdFeed := mustGetFeed(feedID) + + if feedID != 1 { + t.Fatalf("expected feedID: 1, got %q.", feedID) + } + + if createdFeed.name != feedName { + t.Fatalf("expected feedName: %q, got %q.", feedName, createdFeed.name) + } + + // Create Post ========================================================== + parentID := PostID(0) + catID := uint64(0) + metadata := `{"gifs": [], "files": [], "title": "", "message": "testouille", "hashtags": [], "mentions": [], "createdAt": "2023-03-29T12:19:04.858Z", "updatedAt": "2023-03-29T12:19:04.858Z"}` + postID := CreatePost(feedID, parentID, catID, metadata) + createdPost := createdFeed.MustGetPost(postID) + + if postID != 1 { + t.Fatalf("expected postID: 1, got %q.", postID) + } + + if createdPost.category != catID { + t.Fatalf("expected categoryID: %q, got %q.", catID, createdPost.category) + } + + // React Post ========================================================== + icon := "🥰" + ReactPost(feedID, postID, icon, true) + + // Set reaction + reactionCount_, ok := createdPost.reactions.Get("🥰") + if !ok { + t.Fatalf("expected 🥰 exists") + } + + reactionCount := reactionCount_.(int) + if reactionCount != 1 { + t.Fatalf("expected reactionCount: 1, got %q.", reactionCount) + } + + // Unset reaction + ReactPost(feedID, postID, icon, false) + _, exist := createdPost.reactions.Get("🥰") + if exist { + t.Fatalf("expected 🥰 not exist") + } + + // Create SubPost ========================================================== + parentID = createdPost.id + catID = uint64(1) + metadata = `empty_meta_data` + subPostID := CreatePost(feedID, parentID, catID, metadata) + createdSubPost := createdFeed.MustGetPost(subPostID) + + if createdSubPost.id != 2 { + t.Fatalf("expected postID: 2, got %q.", subPostID) + } + + if createdSubPost.parentID != parentID { + t.Fatalf("expected parentID: %q, got %q.", parentID, createdPost.parentID) } } diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index 8c81e333995..81a71e8e40a 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -10,7 +10,7 @@ func CreateFeed(name string) FeedID { fid := incGetFeedID() caller := std.PrevRealm().Addr() if usernameOf(caller) == "" { - panic("unauthorized") + panic("only registered user can create feed") } url := "/r/demo/feeds:" + name feed := newFeed(fid, url, name, caller) @@ -26,11 +26,9 @@ func CreatePost(fid FeedID, parentID PostID, catetory uint64, metadata string) P std.AssertOriginCall() caller := std.PrevRealm().Addr() - // if usernameOf(caller) == "" { - // if !checkAnonFee() { - // panic("please register, otherwise minimum fee " + strconv.Itoa(gDefaultAnonFee) + " is required if anonymous") - // } - // } + if usernameOf(caller) == "" { + panic("please register user") + } feed := mustGetFeed(fid) post := feed.AddPost(caller, parentID, catetory, metadata) return post.id From 8f67fcb7886c8fc56516d62dd237808739ada4f2 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Wed, 5 Jul 2023 06:46:28 +0700 Subject: [PATCH 03/55] wip: try to output object to strinng --- .../r/demo/social_feeds/feeds_test.gno | 7 +++++ examples/gno.land/r/demo/social_feeds/gno.mod | 2 ++ .../gno.land/r/demo/social_feeds/public.gno | 27 +++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/examples/gno.land/r/demo/social_feeds/feeds_test.gno b/examples/gno.land/r/demo/social_feeds/feeds_test.gno index b1305da4e82..e58fd43ddaf 100644 --- a/examples/gno.land/r/demo/social_feeds/feeds_test.gno +++ b/examples/gno.land/r/demo/social_feeds/feeds_test.gno @@ -86,4 +86,11 @@ func Test(t *testing.T) { if createdSubPost.parentID != parentID { t.Fatalf("expected parentID: %q, got %q.", parentID, createdPost.parentID) } + + // Get Post list + posts := GetPosts(feedID) + panic(posts) + // if len(posts) != 2 { + // t.Fatalf("expected total posts: 2, got %q.", len(posts)) + // } } diff --git a/examples/gno.land/r/demo/social_feeds/gno.mod b/examples/gno.land/r/demo/social_feeds/gno.mod index 5633f9c71b6..f692845bb96 100644 --- a/examples/gno.land/r/demo/social_feeds/gno.mod +++ b/examples/gno.land/r/demo/social_feeds/gno.mod @@ -1,6 +1,8 @@ module gno.land/r/demo/social_feeds require ( + "gno.land/p/demo/ufmt" v0.0.0-latest + "gno.land/p/demo/avl" v0.0.0-latest "gno.land/r/demo/users" v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index 81a71e8e40a..2242610db77 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -3,6 +3,9 @@ package social_feeds import ( "std" "strconv" + "strings" + + "gno.land/p/demo/ufmt" ) func CreateFeed(name string) FeedID { @@ -72,6 +75,30 @@ func ReactPost(fid FeedID, pid PostID, icon string, up bool) { post.React(icon, up) } +// Get Posts list +type PostItem struct { + ID PostID + Category uint64 + Metadata string + Creator std.Address +} + +func GetPosts(fid FeedID) string { + std.AssertOriginCall() + caller := std.PrevRealm().Addr() + feed := mustGetFeed(fid) + + var postList []string + feed.posts.Iterate("", "", func(key string, value interface{}) bool { + post := value.(*Post) + postList = append(postList, ufmt.Sprintf("%d,%d,%s,%s", uint64(post.id), post.category, post.metadata, post.creator)) + return false + }) + + res := strings.Join(postList, "|") + return res +} + func checkAnonFee() bool { sent := std.GetOrigSend() anonFeeCoin := std.Coin{"ugnot", int64(gDefaultAnonFee)} From 3f5d5198ebb74ad658e1071b98673157409385c5 Mon Sep 17 00:00:00 2001 From: "hieu.ha" Date: Wed, 5 Jul 2023 08:59:15 +0700 Subject: [PATCH 04/55] wip: stringify objects --- examples/gno.land/r/demo/social_feeds/post.gno | 2 +- examples/gno.land/r/demo/social_feeds/public.gno | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/gno.land/r/demo/social_feeds/post.gno b/examples/gno.land/r/demo/social_feeds/post.gno index c36b9867822..a805f903f4b 100644 --- a/examples/gno.land/r/demo/social_feeds/post.gno +++ b/examples/gno.land/r/demo/social_feeds/post.gno @@ -18,7 +18,7 @@ type Reaction struct { var Categories []string = []string{ "Reaction", "Comment", - "Norman", + "Normal", "Article", "Picture", "Audio", diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index 2242610db77..82675b2b0f8 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -91,12 +91,11 @@ func GetPosts(fid FeedID) string { var postList []string feed.posts.Iterate("", "", func(key string, value interface{}) bool { post := value.(*Post) - postList = append(postList, ufmt.Sprintf("%d,%d,%s,%s", uint64(post.id), post.category, post.metadata, post.creator)) + postList = append(postList, ufmt.Sprintf("%s\t%s\t%s\t%s", postIDKey(post.id), padZero(post.category, 10), post.creator, post.metadata)) return false }) - res := strings.Join(postList, "|") - return res + return ufmt.Sprintf("[%s]", strings.Join(postList, "\n")) } func checkAnonFee() bool { From 72eb4932237a6e016a615c4ad070078f1edd3a3e Mon Sep 17 00:00:00 2001 From: yo1110 Date: Wed, 5 Jul 2023 23:47:20 +0700 Subject: [PATCH 05/55] feat: user binutils to convert data to buffer --- examples/gno.land/r/demo/social_feeds/post.gno | 14 ++++++++++++++ examples/gno.land/r/demo/social_feeds/public.gno | 13 ++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/examples/gno.land/r/demo/social_feeds/post.gno b/examples/gno.land/r/demo/social_feeds/post.gno index c36b9867822..9538a5514a6 100644 --- a/examples/gno.land/r/demo/social_feeds/post.gno +++ b/examples/gno.land/r/demo/social_feeds/post.gno @@ -1,11 +1,13 @@ package social_feeds import ( + "encoding/binary" "std" "strconv" "time" "gno.land/p/demo/avl" + "gno.land/p/demo/binutils" ) type PostID uint64 @@ -53,6 +55,18 @@ func newPost(feed *Feed, id PostID, creator std.Address, parentID PostID, catego } } +func (post *Post) ToBytes() []byte { + b := []byte{} + b = binary.BigEndian.AppendUint64(b, uint64(post.id)) + b = binary.BigEndian.AppendUint64(b, uint64(post.parentID)) + b = binary.BigEndian.AppendUint64(b, uint64(post.feedID)) + b = binary.BigEndian.AppendUint64(b, post.category) + b = append(b, binutils.EncodeLengthPrefixedStringUint32BE(post.metadata)...) + b = append(b, binutils.EncodeLengthPrefixedStringUint16BE(post.creator.String())...) + b = binary.BigEndian.AppendUint32(b, uint32(post.createdAt.Unix())) + return b +} + func (post *Post) Update(category uint64, metadata string) { post.category = category post.metadata = metadata diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index 2242610db77..97a77c31458 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -1,6 +1,7 @@ package social_feeds import ( + "encoding/base64" "std" "strconv" "strings" @@ -76,13 +77,6 @@ func ReactPost(fid FeedID, pid PostID, icon string, up bool) { } // Get Posts list -type PostItem struct { - ID PostID - Category uint64 - Metadata string - Creator std.Address -} - func GetPosts(fid FeedID) string { std.AssertOriginCall() caller := std.PrevRealm().Addr() @@ -91,11 +85,12 @@ func GetPosts(fid FeedID) string { var postList []string feed.posts.Iterate("", "", func(key string, value interface{}) bool { post := value.(*Post) - postList = append(postList, ufmt.Sprintf("%d,%d,%s,%s", uint64(post.id), post.category, post.metadata, post.creator)) + postList = append(postList, base64.RawURLEncoding.EncodeToString(post.ToBytes())) return false }) - res := strings.Join(postList, "|") + SEPARATOR := "|" + res := strings.Join(postList, SEPARATOR) return res } From 121faabf29ddabc7dcde3dc0d60b6a30a96a4ff2 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Wed, 5 Jul 2023 23:47:40 +0700 Subject: [PATCH 06/55] feat: add binutils --- .../gno.land/p/demo/binutils/binutils.gno | 41 +++++++++++++++++++ examples/gno.land/p/demo/binutils/gno.mod | 1 + 2 files changed, 42 insertions(+) create mode 100644 examples/gno.land/p/demo/binutils/binutils.gno create mode 100644 examples/gno.land/p/demo/binutils/gno.mod diff --git a/examples/gno.land/p/demo/binutils/binutils.gno b/examples/gno.land/p/demo/binutils/binutils.gno new file mode 100644 index 00000000000..adb8b726b70 --- /dev/null +++ b/examples/gno.land/p/demo/binutils/binutils.gno @@ -0,0 +1,41 @@ +package binutils + +import ( + "encoding/binary" + "errors" +) + +var ErrInvalidLengthPrefixedString = errors.New("invalid length-prefixed string") + +func EncodeLengthPrefixedStringUint16BE(s string) []byte { + b := make([]byte, 2+len(s)) + binary.BigEndian.PutUint16(b, uint16(len(s))) + copy(b[2:], s) + return b +} + +func EncodeLengthPrefixedStringUint32BE(s string) []byte { + b := make([]byte, 4+len(s)) + binary.BigEndian.PutUint32(b, uint32(len(s))) + copy(b[4:], s) + return b +} + +func DecodeLengthPrefixedStringUint16BE(b []byte) (string, []byte, error) { + if len(b) < 2 { + return "", nil, ErrInvalidLengthPrefixedString + } + l := binary.BigEndian.Uint16(b) + if len(b) < 2+int(l) { + return "", nil, ErrInvalidLengthPrefixedString + } + return string(b[2 : 2+l]), b[l+2:], nil +} + +func MustDecodeLengthPrefixedStringUint16BE(b []byte) (string, []byte) { + s, r, err := DecodeLengthPrefixedStringUint16BE(b) + if err != nil { + panic(err) + } + return s, r +} diff --git a/examples/gno.land/p/demo/binutils/gno.mod b/examples/gno.land/p/demo/binutils/gno.mod new file mode 100644 index 00000000000..64fe08ae523 --- /dev/null +++ b/examples/gno.land/p/demo/binutils/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/binutils \ No newline at end of file From 87cf40375eb257310843e3d4248d0edfb79355f6 Mon Sep 17 00:00:00 2001 From: "hieu.ha" Date: Thu, 6 Jul 2023 09:06:20 +0700 Subject: [PATCH 07/55] wip: encoding reactions --- .../r/demo/social_feeds/feeds_test.gno | 16 +++++++++-- .../gno.land/r/demo/social_feeds/misc.gno | 5 ++++ .../gno.land/r/demo/social_feeds/post.gno | 27 +++++++++++++++++-- .../gno.land/r/demo/social_feeds/public.gno | 4 +-- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/examples/gno.land/r/demo/social_feeds/feeds_test.gno b/examples/gno.land/r/demo/social_feeds/feeds_test.gno index e58fd43ddaf..9c8feae78f8 100644 --- a/examples/gno.land/r/demo/social_feeds/feeds_test.gno +++ b/examples/gno.land/r/demo/social_feeds/feeds_test.gno @@ -3,11 +3,13 @@ package social_feeds // SEND: 200000000ugnot import ( + "encoding/base64" "fmt" "std" "strconv" "testing" + "gno.land/p/demo/binutils" "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" @@ -87,9 +89,19 @@ func Test(t *testing.T) { t.Fatalf("expected parentID: %q, got %q.", parentID, createdPost.parentID) } + // Re-add reaction to test post list + ReactPost(feedID, postID, "🥰", true) + ReactPost(feedID, postID, "😇", true) + // Get Post list - posts := GetPosts(feedID) - panic(posts) + postsStr := GetPosts(feedID) + postsBytes, err := base64.RawURLEncoding.DecodeString(postsStr) + if err != nil { + panic("uanble to decode post list", err) + } + + panic(postsBytes) + // if len(posts) != 2 { // t.Fatalf("expected total posts: 2, got %q.", len(posts)) // } diff --git a/examples/gno.land/r/demo/social_feeds/misc.gno b/examples/gno.land/r/demo/social_feeds/misc.gno index 4ba28cae284..d9f09aa1a76 100644 --- a/examples/gno.land/r/demo/social_feeds/misc.gno +++ b/examples/gno.land/r/demo/social_feeds/misc.gno @@ -1,6 +1,7 @@ package social_feeds import ( + "encoding/base64" "std" "strconv" "strings" @@ -64,3 +65,7 @@ func padZero(u64 uint64, length int) string { return strings.Repeat("0", length-len(str)) + str } } + +func bytesToString(b []byte) string { + return base64.RawURLEncoding.EncodeToString(b) +} diff --git a/examples/gno.land/r/demo/social_feeds/post.gno b/examples/gno.land/r/demo/social_feeds/post.gno index 88aa22fb6a5..fbf48b04cfc 100644 --- a/examples/gno.land/r/demo/social_feeds/post.gno +++ b/examples/gno.land/r/demo/social_feeds/post.gno @@ -1,13 +1,16 @@ package social_feeds import ( + "encoding/base64" "encoding/binary" "std" "strconv" + "strings" "time" "gno.land/p/demo/avl" "gno.land/p/demo/binutils" + "gno.land/p/demo/ufmt" ) type PostID uint64 @@ -33,7 +36,7 @@ type Post struct { feedID FeedID category uint64 metadata string - reactions avl.Tree + reactions avl.Tree // -> creator std.Address deleted bool @@ -55,7 +58,7 @@ func newPost(feed *Feed, id PostID, creator std.Address, parentID PostID, catego } } -func (post *Post) ToBytes() []byte { +func (post *Post) Bytes() []byte { b := []byte{} b = binary.BigEndian.AppendUint64(b, uint64(post.id)) b = binary.BigEndian.AppendUint64(b, uint64(post.parentID)) @@ -64,9 +67,29 @@ func (post *Post) ToBytes() []byte { b = append(b, binutils.EncodeLengthPrefixedStringUint32BE(post.metadata)...) b = append(b, binutils.EncodeLengthPrefixedStringUint16BE(post.creator.String())...) b = binary.BigEndian.AppendUint32(b, uint32(post.createdAt.Unix())) + + // Encode reactions + var reactionsArr []string + + post.reactions.Iterate("", "", func(key string, value interface{}) bool { + icon := key + count := value.(int) + + reactionsArr = append(reactionsArr, ufmt.Sprintf("%s:%d", icon, count)) + return false + }) + + SEPARATOR := "," + reactionsStr := strings.Join(reactionsArr, SEPARATOR) + b = append(b, binutils.EncodeLengthPrefixedStringUint32BE(reactionsStr)...) + return b } +func (post *Post) String() string { + return bytesToString(post.Bytes()) +} + func (post *Post) Update(category uint64, metadata string) { post.category = category post.metadata = metadata diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index 97a77c31458..c92f7aeb679 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -85,11 +85,11 @@ func GetPosts(fid FeedID) string { var postList []string feed.posts.Iterate("", "", func(key string, value interface{}) bool { post := value.(*Post) - postList = append(postList, base64.RawURLEncoding.EncodeToString(post.ToBytes())) + postList = append(postList, post.String()) return false }) - SEPARATOR := "|" + SEPARATOR := "," res := strings.Join(postList, SEPARATOR) return res } From 7c1f7a202d9788f2f0e52d35516f6c8082d06f7b Mon Sep 17 00:00:00 2001 From: yo1110 Date: Thu, 6 Jul 2023 13:01:11 +0700 Subject: [PATCH 08/55] feat: add GetPost by id --- .../gno.land/r/demo/social_feeds/feeds_test.gno | 17 ++++++++--------- .../gno.land/r/demo/social_feeds/public.gno | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/examples/gno.land/r/demo/social_feeds/feeds_test.gno b/examples/gno.land/r/demo/social_feeds/feeds_test.gno index 9c8feae78f8..b14ab122bba 100644 --- a/examples/gno.land/r/demo/social_feeds/feeds_test.gno +++ b/examples/gno.land/r/demo/social_feeds/feeds_test.gno @@ -94,15 +94,14 @@ func Test(t *testing.T) { ReactPost(feedID, postID, "😇", true) // Get Post list - postsStr := GetPosts(feedID) - postsBytes, err := base64.RawURLEncoding.DecodeString(postsStr) - if err != nil { - panic("uanble to decode post list", err) - } + // postsStr := GetPosts(feedID) + // postsBytes, err := base64.RawURLEncoding.DecodeString(postsStr) + // if err != nil { + // panic("uanble to decode post list") + // } - panic(postsBytes) + // Get Post + postStr := GetPost(feedID, postID) - // if len(posts) != 2 { - // t.Fatalf("expected total posts: 2, got %q.", len(posts)) - // } + panic(postStr) } diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index c92f7aeb679..ff9ded9b0f2 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -94,6 +94,21 @@ func GetPosts(fid FeedID) string { return res } +// Get Post +func GetPost(fid FeedID, pid PostID) string { + std.AssertOriginCall() + caller := std.PrevRealm().Addr() + feed := mustGetFeed(fid) + + data, ok := feed.posts.Get(postIDKey(pid)) + if !ok { + panic("Unable to get post") + } + + post := data.(*Post) + return post.String() +} + func checkAnonFee() bool { sent := std.GetOrigSend() anonFeeCoin := std.Coin{"ugnot", int64(gDefaultAnonFee)} From fc909b0b60404bedbbb66e88a771410b610480a1 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Mon, 10 Jul 2023 08:29:18 +0700 Subject: [PATCH 09/55] wip: handle subposts --- Makefile | 26 +++++++++++++++++++ .../gno.land/r/demo/social_feeds/feed.gno | 9 ++++++- .../r/demo/social_feeds/feeds_test.gno | 15 +++++++++++ .../gno.land/r/demo/social_feeds/post.gno | 20 ++++++++++++-- .../gno.land/r/demo/social_feeds/public.gno | 19 ++++++++++++++ 5 files changed, 86 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 84f86d28bb3..766a774c71c 100644 --- a/Makefile +++ b/Makefile @@ -44,3 +44,29 @@ fmt: .PHONY: lint lint: golangci-lint run --config .github/golangci.yml + +.PHONE: reset +reset: + cd /Users/yo/Projects/teritori/gno/gno.land && make install && rm -fr testdir && gnoland + +.PHONE: faucet +faucet: + cd /Users/yo/Projects/teritori/gno/gno.land && gnofaucet serve test1 --chain-id dev --send 500000000ugnot + +.PHONE: init +init: +# cd /Users/yo/Projects/teritori/gno/gno.land && make install && rm -fr testdir && gnoland +# cd /Users/yo/Projects/teritori/gno/gno.land && gnofaucet serve test1 --chain-id dev --send 500000000ugnot + + curl --location --request POST 'http://localhost:5050' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'toaddr=g1ckn395mpttp0vupgtratyufdaakgh8jgkmr3ym' + + gnokey maketx call -gas-fee="1ugnot" -gas-wanted="5000000" -broadcast="true" -pkgpath="gno.land/r/demo/users" -func="Register" -args="" -args="yo_account" -args="" -send="200000000ugnot" yo + + gnokey maketx call -gas-fee="1ugnot" -gas-wanted="5000000" -broadcast="true" -pkgpath="gno.land/r/demo/users" -func="Register" -args="" -args="test1_account" -args="" -send="200000000ugnot" test1 + + gnokey maketx call -pkgpath "gno.land/r/demo/social_feeds" -func "CreateFeed" -gas-fee 1000000ugnot -gas-wanted 2000000 -send "" -broadcast -chainid "dev" -args "teritori" -remote "127.0.0.1:26657" test1 + + gnokey maketx call -pkgpath "gno.land/r/demo/social_feeds" -func "CreatePost" -gas-fee 1000000ugnot -gas-wanted 2000000 -send "" -broadcast -chainid "dev" -args "1" -args "0" -args "0" -args '{"gifs": [], "files": [], "title": "", "message": "this is 2 reply inside t sdv ds d ds he thread", "hashtags": [], "mentions": [], "createdAt": "2023-04-20T09:39:45.522Z", "updatedAt": "2023-04-20T09:39:45.522Z"}' -remote "127.0.0.1:26657" test1 + diff --git a/examples/gno.land/r/demo/social_feeds/feed.gno b/examples/gno.land/r/demo/social_feeds/feed.gno index bcf5d180a9d..618cec84f6e 100644 --- a/examples/gno.land/r/demo/social_feeds/feed.gno +++ b/examples/gno.land/r/demo/social_feeds/feed.gno @@ -72,6 +72,13 @@ func (feed *Feed) AddPost(creator std.Address, parentID PostID, category uint64, pidkey := postIDKey(pid) post := newPost(feed, pid, creator, parentID, category, metadata) - feed.posts.Set(pidkey, post) + + if parentID != 0 { + parent := feed.MustGetPost(parentID) + parent.subposts.Set(pidkey, post) + } else { + feed.posts.Set(pidkey, post) + } + return post } diff --git a/examples/gno.land/r/demo/social_feeds/feeds_test.gno b/examples/gno.land/r/demo/social_feeds/feeds_test.gno index b14ab122bba..7ea05530e00 100644 --- a/examples/gno.land/r/demo/social_feeds/feeds_test.gno +++ b/examples/gno.land/r/demo/social_feeds/feeds_test.gno @@ -7,6 +7,7 @@ import ( "fmt" "std" "strconv" + "strings" "testing" "gno.land/p/demo/binutils" @@ -79,6 +80,7 @@ func Test(t *testing.T) { catID = uint64(1) metadata = `empty_meta_data` subPostID := CreatePost(feedID, parentID, catID, metadata) + subPostI2 := CreatePost(feedID, parentID, catID, metadata) createdSubPost := createdFeed.MustGetPost(subPostID) if createdSubPost.id != 2 { @@ -89,6 +91,19 @@ func Test(t *testing.T) { t.Fatalf("expected parentID: %q, got %q.", parentID, createdPost.parentID) } + // Check subposts size + if createdPost.subposts.Size() != 2 { + t.Fatalf("expected subposts count: 1, got %q.", createdPost.subposts.Size()) + } + + // Get subposts + subposts := GetSubposts(feedID, postID) + subpostsSplitted := strings.Split(subposts, ",") + + if len(subpostsSplitted) != 2 { + t.Fatalf("expected encoded subposts: 2, got %q.", subpostsSplitted) + } + // Re-add reaction to test post list ReactPost(feedID, postID, "🥰", true) ReactPost(feedID, postID, "😇", true) diff --git a/examples/gno.land/r/demo/social_feeds/post.gno b/examples/gno.land/r/demo/social_feeds/post.gno index fbf48b04cfc..f7e02bd5960 100644 --- a/examples/gno.land/r/demo/social_feeds/post.gno +++ b/examples/gno.land/r/demo/social_feeds/post.gno @@ -36,7 +36,8 @@ type Post struct { feedID FeedID category uint64 metadata string - reactions avl.Tree // -> + reactions avl.Tree // icon -> count + subposts avl.Tree // Post.id -> *Post creator std.Address deleted bool @@ -70,7 +71,6 @@ func (post *Post) Bytes() []byte { // Encode reactions var reactionsArr []string - post.reactions.Iterate("", "", func(key string, value interface{}) bool { icon := key count := value.(int) @@ -83,6 +83,22 @@ func (post *Post) Bytes() []byte { reactionsStr := strings.Join(reactionsArr, SEPARATOR) b = append(b, binutils.EncodeLengthPrefixedStringUint32BE(reactionsStr)...) + // Encode subposts ids + var subpostIDArr []string + post.subposts.Iterate("", "", func(key string, value interface{}) bool { + subpostID := key.(string) + subpost := value.(*Post) + + if subpost.parentID == post.id { + subpostIDArr = append(subpostIDArr, ufmt.Sprintf("%d", uint64(subpost.id))) + } + + return false + }) + + subpostIDsStr := strings.Join(subpostIDArr, SEPARATOR) + b = append(b, binutils.EncodeLengthPrefixedStringUint32BE(subpostIDsStr)...) + return b } diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index ff9ded9b0f2..717ededb802 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -94,6 +94,25 @@ func GetPosts(fid FeedID) string { return res } +// Get Subposts list +func GetSubposts(fid FeedID, pid PostID) string { + std.AssertOriginCall() + caller := std.PrevRealm().Addr() + feed := mustGetFeed(fid) + post := feed.MustGetPost(pid) + + var subpostList []string + post.subposts.Iterate("", "", func(key string, value interface{}) bool { + subpost := value.(*Post) + subpostList = append(subpostList, subpost.String()) + return false + }) + + SEPARATOR := "," + res := strings.Join(subpostList, SEPARATOR) + return res +} + // Get Post func GetPost(fid FeedID, pid PostID) string { std.AssertOriginCall() From e2a6a60c6fa14561e6d4bb6c67c152872e65895a Mon Sep 17 00:00:00 2001 From: "hieu.ha" Date: Mon, 10 Jul 2023 13:13:21 +0700 Subject: [PATCH 10/55] feat: add pagination for query posts/comments --- .../gno.land/r/demo/social_feeds/feed.gno | 8 +-- .../r/demo/social_feeds/feeds_test.gno | 68 ++++++++++++++----- .../gno.land/r/demo/social_feeds/post.gno | 8 +-- .../gno.land/r/demo/social_feeds/public.gno | 30 ++++++-- 4 files changed, 82 insertions(+), 32 deletions(-) diff --git a/examples/gno.land/r/demo/social_feeds/feed.gno b/examples/gno.land/r/demo/social_feeds/feed.gno index 618cec84f6e..529a1215808 100644 --- a/examples/gno.land/r/demo/social_feeds/feed.gno +++ b/examples/gno.land/r/demo/social_feeds/feed.gno @@ -72,13 +72,11 @@ func (feed *Feed) AddPost(creator std.Address, parentID PostID, category uint64, pidkey := postIDKey(pid) post := newPost(feed, pid, creator, parentID, category, metadata) - + feed.posts.Set(pidkey, post) + if parentID != 0 { parent := feed.MustGetPost(parentID) - parent.subposts.Set(pidkey, post) - } else { - feed.posts.Set(pidkey, post) + parent.comments.Set(pidkey, post) } - return post } diff --git a/examples/gno.land/r/demo/social_feeds/feeds_test.gno b/examples/gno.land/r/demo/social_feeds/feeds_test.gno index 7ea05530e00..7c9b0df8d3e 100644 --- a/examples/gno.land/r/demo/social_feeds/feeds_test.gno +++ b/examples/gno.land/r/demo/social_feeds/feeds_test.gno @@ -80,7 +80,7 @@ func Test(t *testing.T) { catID = uint64(1) metadata = `empty_meta_data` subPostID := CreatePost(feedID, parentID, catID, metadata) - subPostI2 := CreatePost(feedID, parentID, catID, metadata) + subPostID2 := CreatePost(feedID, parentID, catID, metadata) createdSubPost := createdFeed.MustGetPost(subPostID) if createdSubPost.id != 2 { @@ -91,32 +91,68 @@ func Test(t *testing.T) { t.Fatalf("expected parentID: %q, got %q.", parentID, createdPost.parentID) } - // Check subposts size - if createdPost.subposts.Size() != 2 { - t.Fatalf("expected subposts count: 1, got %q.", createdPost.subposts.Size()) + // Check comments size + if createdPost.comments.Size() != 2 { + t.Fatalf("expected comments count: 1, got %q.", createdPost.comments.Size()) } - // Get subposts - subposts := GetSubposts(feedID, postID) - subpostsSplitted := strings.Split(subposts, ",") + // Get comments + comments := GetComments(feedID, postID, 1, 5) + commentsSplitted := strings.Split(comments, ",") - if len(subpostsSplitted) != 2 { - t.Fatalf("expected encoded subposts: 2, got %q.", subpostsSplitted) + if len(commentsSplitted) != 2 { + t.Fatalf("expected encoded comments: 2, got %q.", commentsSplitted) + } + + // Delete 1 comment + DeletePost(1, subPostID2) + comments = GetComments(feedID, postID, 1, 5) + commentsSplitted = strings.Split(comments, ",") + if len(commentsSplitted) != 1 { + t.Fatalf("expected encoded comments: 1, got %q.", commentsSplitted) } // Re-add reaction to test post list - ReactPost(feedID, postID, "🥰", true) - ReactPost(feedID, postID, "😇", true) + ReactPost(1, postID, "🥰", true) + ReactPost(1, postID, "😇", true) + + // Get Post: 2 post, 2 subposts + newPostID := CreatePost(1, PostID(0), uint64(0), "metadata") + var postsStr string + var postsCount int + + postsStr = GetPosts(1, uint64(0), PostID(1), 1) + postsCount = len(strings.Split(postsStr, ",")) + if postsCount != 1 { + t.Fatalf("expected posts count: 1, got %q.", postsCount) + } + + postsStr = GetPosts(1, uint64(0), PostID(1), 2) + postsCount = len(strings.Split(postsStr, ",")) + if postsCount != 2 { + t.Fatalf("expected posts count: 2, got %q.", postsCount) + } + + postsStr = GetPosts(1, uint64(0), PostID(1), 3) + postsCount = len(strings.Split(postsStr, ",")) + if postsCount != 2 { + t.Fatalf("expected posts count: 2, got %q.", postsCount) + } + + // Delete one + DeletePost(1, newPostID) + + postsStr = GetPosts(1, uint64(0), PostID(1), 4) + postsCount = len(strings.Split(postsStr, ",")) + if postsCount != 1 { + t.Fatalf("expected posts count: 1, got %q.", postsCount) + } + // Test tree // Get Post list // postsStr := GetPosts(feedID) // postsBytes, err := base64.RawURLEncoding.DecodeString(postsStr) // if err != nil { // panic("uanble to decode post list") // } - - // Get Post - postStr := GetPost(feedID, postID) - - panic(postStr) } diff --git a/examples/gno.land/r/demo/social_feeds/post.gno b/examples/gno.land/r/demo/social_feeds/post.gno index f7e02bd5960..d2667f064ad 100644 --- a/examples/gno.land/r/demo/social_feeds/post.gno +++ b/examples/gno.land/r/demo/social_feeds/post.gno @@ -37,7 +37,7 @@ type Post struct { category uint64 metadata string reactions avl.Tree // icon -> count - subposts avl.Tree // Post.id -> *Post + comments avl.Tree // Post.id -> *Post creator std.Address deleted bool @@ -83,10 +83,10 @@ func (post *Post) Bytes() []byte { reactionsStr := strings.Join(reactionsArr, SEPARATOR) b = append(b, binutils.EncodeLengthPrefixedStringUint32BE(reactionsStr)...) - // Encode subposts ids + // Encode comments ids var subpostIDArr []string - post.subposts.Iterate("", "", func(key string, value interface{}) bool { - subpostID := key.(string) + post.comments.Iterate("", "", func(key string, value interface{}) bool { + subpostID := key subpost := value.(*Post) if subpost.parentID == post.id { diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index 717ededb802..6a28dfd1b20 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -77,15 +77,23 @@ func ReactPost(fid FeedID, pid PostID, icon string, up bool) { } // Get Posts list -func GetPosts(fid FeedID) string { +func GetPosts(fid FeedID, category uint64, start PostID, limit uint8) string { std.AssertOriginCall() caller := std.PrevRealm().Addr() feed := mustGetFeed(fid) var postList []string - feed.posts.Iterate("", "", func(key string, value interface{}) bool { + feed.posts.Iterate(postIDKey(start), "", func(key string, value interface{}) bool { post := value.(*Post) - postList = append(postList, post.String()) + + if post.parentID == uint64(0) && post.category == category && post.deleted == false { + postList = append(postList, post.String()) + } + + if len(postList) == limit { + return true + } + return false }) @@ -94,17 +102,25 @@ func GetPosts(fid FeedID) string { return res } -// Get Subposts list -func GetSubposts(fid FeedID, pid PostID) string { +// Get comments list +func GetComments(fid FeedID, pid PostID, start PostID, limit uint8) string { std.AssertOriginCall() caller := std.PrevRealm().Addr() feed := mustGetFeed(fid) post := feed.MustGetPost(pid) var subpostList []string - post.subposts.Iterate("", "", func(key string, value interface{}) bool { + post.comments.Iterate(postIDKey(start), "", func(key string, value interface{}) bool { subpost := value.(*Post) - subpostList = append(subpostList, subpost.String()) + + if subpost.parentID == pid && subpost.deleted == false { + subpostList = append(subpostList, subpost.String()) + } + + if len(subpostList) == limit { + return true + } + return false }) From 736143e9df2da66625e0e7c1b22fae81a3249b56 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Mon, 10 Jul 2023 23:25:34 +0700 Subject: [PATCH 11/55] feat: handle pginnations for posts --- .../r/demo/social_feeds/feeds_test.gno | 10 ++++----- .../gno.land/r/demo/social_feeds/public.gno | 21 ++++++++++++------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/examples/gno.land/r/demo/social_feeds/feeds_test.gno b/examples/gno.land/r/demo/social_feeds/feeds_test.gno index 7c9b0df8d3e..a635c17bcfe 100644 --- a/examples/gno.land/r/demo/social_feeds/feeds_test.gno +++ b/examples/gno.land/r/demo/social_feeds/feeds_test.gno @@ -97,7 +97,7 @@ func Test(t *testing.T) { } // Get comments - comments := GetComments(feedID, postID, 1, 5) + comments := GetComments(feedID, postID) commentsSplitted := strings.Split(comments, ",") if len(commentsSplitted) != 2 { @@ -106,7 +106,7 @@ func Test(t *testing.T) { // Delete 1 comment DeletePost(1, subPostID2) - comments = GetComments(feedID, postID, 1, 5) + comments = GetComments(feedID, postID) commentsSplitted = strings.Split(comments, ",") if len(commentsSplitted) != 1 { t.Fatalf("expected encoded comments: 1, got %q.", commentsSplitted) @@ -121,19 +121,19 @@ func Test(t *testing.T) { var postsStr string var postsCount int - postsStr = GetPosts(1, uint64(0), PostID(1), 1) + postsStr = GetPosts(1, uint64(0), 0, 1) postsCount = len(strings.Split(postsStr, ",")) if postsCount != 1 { t.Fatalf("expected posts count: 1, got %q.", postsCount) } - postsStr = GetPosts(1, uint64(0), PostID(1), 2) + postsStr = GetPosts(1, uint64(0), 0, 2) postsCount = len(strings.Split(postsStr, ",")) if postsCount != 2 { t.Fatalf("expected posts count: 2, got %q.", postsCount) } - postsStr = GetPosts(1, uint64(0), PostID(1), 3) + postsStr = GetPosts(1, uint64(0), 0, 3) postsCount = len(strings.Split(postsStr, ",")) if postsCount != 2 { t.Fatalf("expected posts count: 2, got %q.", postsCount) diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index 6a28dfd1b20..c5474a65127 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -77,16 +77,24 @@ func ReactPost(fid FeedID, pid PostID, icon string, up bool) { } // Get Posts list -func GetPosts(fid FeedID, category uint64, start PostID, limit uint8) string { +func GetPosts(fid FeedID, category uint64, offset uint64, limit uint8) string { std.AssertOriginCall() caller := std.PrevRealm().Addr() feed := mustGetFeed(fid) var postList []string - feed.posts.Iterate(postIDKey(start), "", func(key string, value interface{}) bool { + var skipped uint64 + + feed.posts.Iterate("", "", func(key string, value interface{}) bool { post := value.(*Post) + // NOTE: this search mechanism is not efficient, only for demo purpose if post.parentID == uint64(0) && post.category == category && post.deleted == false { + if skipped < offset { + skipped++ + return false + } + postList = append(postList, post.String()) } @@ -103,24 +111,21 @@ func GetPosts(fid FeedID, category uint64, start PostID, limit uint8) string { } // Get comments list -func GetComments(fid FeedID, pid PostID, start PostID, limit uint8) string { +// NOTE: Does not support limit/offset for now, just return all the comments +func GetComments(fid FeedID, pid PostID) string { std.AssertOriginCall() caller := std.PrevRealm().Addr() feed := mustGetFeed(fid) post := feed.MustGetPost(pid) var subpostList []string - post.comments.Iterate(postIDKey(start), "", func(key string, value interface{}) bool { + post.comments.Iterate("", "", func(key string, value interface{}) bool { subpost := value.(*Post) if subpost.parentID == pid && subpost.deleted == false { subpostList = append(subpostList, subpost.String()) } - if len(subpostList) == limit { - return true - } - return false }) From c683cd39f4b29b20accaf7a52996869cd3322f2b Mon Sep 17 00:00:00 2001 From: yo1110 Date: Mon, 10 Jul 2023 23:30:46 +0700 Subject: [PATCH 12/55] chore: remove uneeded files --- Makefile | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/Makefile b/Makefile index 766a774c71c..84f86d28bb3 100644 --- a/Makefile +++ b/Makefile @@ -44,29 +44,3 @@ fmt: .PHONY: lint lint: golangci-lint run --config .github/golangci.yml - -.PHONE: reset -reset: - cd /Users/yo/Projects/teritori/gno/gno.land && make install && rm -fr testdir && gnoland - -.PHONE: faucet -faucet: - cd /Users/yo/Projects/teritori/gno/gno.land && gnofaucet serve test1 --chain-id dev --send 500000000ugnot - -.PHONE: init -init: -# cd /Users/yo/Projects/teritori/gno/gno.land && make install && rm -fr testdir && gnoland -# cd /Users/yo/Projects/teritori/gno/gno.land && gnofaucet serve test1 --chain-id dev --send 500000000ugnot - - curl --location --request POST 'http://localhost:5050' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'toaddr=g1ckn395mpttp0vupgtratyufdaakgh8jgkmr3ym' - - gnokey maketx call -gas-fee="1ugnot" -gas-wanted="5000000" -broadcast="true" -pkgpath="gno.land/r/demo/users" -func="Register" -args="" -args="yo_account" -args="" -send="200000000ugnot" yo - - gnokey maketx call -gas-fee="1ugnot" -gas-wanted="5000000" -broadcast="true" -pkgpath="gno.land/r/demo/users" -func="Register" -args="" -args="test1_account" -args="" -send="200000000ugnot" test1 - - gnokey maketx call -pkgpath "gno.land/r/demo/social_feeds" -func "CreateFeed" -gas-fee 1000000ugnot -gas-wanted 2000000 -send "" -broadcast -chainid "dev" -args "teritori" -remote "127.0.0.1:26657" test1 - - gnokey maketx call -pkgpath "gno.land/r/demo/social_feeds" -func "CreatePost" -gas-fee 1000000ugnot -gas-wanted 2000000 -send "" -broadcast -chainid "dev" -args "1" -args "0" -args "0" -args '{"gifs": [], "files": [], "title": "", "message": "this is 2 reply inside t sdv ds d ds he thread", "hashtags": [], "mentions": [], "createdAt": "2023-04-20T09:39:45.522Z", "updatedAt": "2023-04-20T09:39:45.522Z"}' -remote "127.0.0.1:26657" test1 - From 18d7e3bb491069bc7a55b1b8a9636572257d1868 Mon Sep 17 00:00:00 2001 From: hthieu1110 Date: Thu, 3 Aug 2023 10:06:17 +0700 Subject: [PATCH 13/55] fix: remove unneeded AssertOrig --- examples/gno.land/r/demo/social_feeds/public.gno | 8 -------- 1 file changed, 8 deletions(-) diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index c5474a65127..fd8a87a4d34 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -10,7 +10,6 @@ import ( ) func CreateFeed(name string) FeedID { - std.AssertOriginCall() fid := incGetFeedID() caller := std.PrevRealm().Addr() if usernameOf(caller) == "" { @@ -27,7 +26,6 @@ func CreateFeed(name string) FeedID { // Anyone can create a post in a existing feed // Anonymous user has to pay a configurable minimum fee func CreatePost(fid FeedID, parentID PostID, catetory uint64, metadata string) PostID { - std.AssertOriginCall() caller := std.PrevRealm().Addr() if usernameOf(caller) == "" { @@ -40,7 +38,6 @@ func CreatePost(fid FeedID, parentID PostID, catetory uint64, metadata string) P // Only post's owner can edit post func EditPost(fid FeedID, pid PostID, category uint64, metadata string) { - std.AssertOriginCall() caller := std.PrevRealm().Addr() feed := mustGetFeed(fid) post := feed.MustGetPost(pid) @@ -54,7 +51,6 @@ func EditPost(fid FeedID, pid PostID, category uint64, metadata string) { // Only owner can delete the post func DeletePost(fid FeedID, pid PostID) { - std.AssertOriginCall() caller := std.PrevRealm().Addr() feed := mustGetFeed(fid) post := feed.MustGetPost(pid) @@ -68,7 +64,6 @@ func DeletePost(fid FeedID, pid PostID) { // Any one can react post func ReactPost(fid FeedID, pid PostID, icon string, up bool) { - std.AssertOriginCall() caller := std.PrevRealm().Addr() feed := mustGetFeed(fid) post := feed.MustGetPost(pid) @@ -78,7 +73,6 @@ func ReactPost(fid FeedID, pid PostID, icon string, up bool) { // Get Posts list func GetPosts(fid FeedID, category uint64, offset uint64, limit uint8) string { - std.AssertOriginCall() caller := std.PrevRealm().Addr() feed := mustGetFeed(fid) @@ -113,7 +107,6 @@ func GetPosts(fid FeedID, category uint64, offset uint64, limit uint8) string { // Get comments list // NOTE: Does not support limit/offset for now, just return all the comments func GetComments(fid FeedID, pid PostID) string { - std.AssertOriginCall() caller := std.PrevRealm().Addr() feed := mustGetFeed(fid) post := feed.MustGetPost(pid) @@ -136,7 +129,6 @@ func GetComments(fid FeedID, pid PostID) string { // Get Post func GetPost(fid FeedID, pid PostID) string { - std.AssertOriginCall() caller := std.PrevRealm().Addr() feed := mustGetFeed(fid) From b48e66d179059119cd47f9d27c22bad845d68161 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Sat, 5 Aug 2023 18:11:50 +0700 Subject: [PATCH 14/55] feat: add render + remove check for origCaller --- examples/Makefile | 27 ++++++ .../gno.land/r/demo/social_feeds/feed.gno | 33 +++++++ .../gno.land/r/demo/social_feeds/misc.gno | 4 + .../gno.land/r/demo/social_feeds/post.gno | 8 ++ .../gno.land/r/demo/social_feeds/public.gno | 27 ++---- .../gno.land/r/demo/social_feeds/render.gno | 86 +++++++++++++++++++ 6 files changed, 167 insertions(+), 18 deletions(-) create mode 100644 examples/gno.land/r/demo/social_feeds/render.gno diff --git a/examples/Makefile b/examples/Makefile index 5075df198ac..bddb79ba28d 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -37,3 +37,30 @@ clean: GOFMT_FLAGS ?= -w fmt: go run -modfile ../misc/devdeps/go.mod mvdan.cc/gofumpt $(GOFMT_FLAGS) `find . -name "*.gno"` + +.PHONY: create.feed +create.feed: + gnokey maketx call \ + -pkgpath "gno.land/r/demo/social_feeds" \ + -func "CreateFeed" \ + -gas-fee 1000000ugnot \ + -gas-wanted 2000000 \ + -send "" \ + -broadcast \ + -args "teritori" \ + test1 + +.PHONE: create.post +create.post: + gnokey maketx call \ + -pkgpath "gno.land/r/demo/social_feeds" \ + -func "CreatePost" \ + -gas-fee 1000000ugnot \ + -gas-wanted 2000000 \ + -send "" \ + -broadcast \ + -args "1" \ + -args "0" \ + -args "2" \ + -args '{"gifs": [], "files": [], "title": "", "message": "Hello world !", "hashtags": [], "mentions": [], "createdAt": "2023-08-03T01:39:45.522Z", "updatedAt": "2023-08-03T01:39:45.522Z"}' \ + test1 \ No newline at end of file diff --git a/examples/gno.land/r/demo/social_feeds/feed.gno b/examples/gno.land/r/demo/social_feeds/feed.gno index 529a1215808..468a26663b4 100644 --- a/examples/gno.land/r/demo/social_feeds/feed.gno +++ b/examples/gno.land/r/demo/social_feeds/feed.gno @@ -2,6 +2,7 @@ package social_feeds import ( "std" + "strconv" "time" "gno.land/p/demo/avl" @@ -9,6 +10,10 @@ import ( type FeedID uint64 +func (fid FeedID) String() string { + return strconv.Itoa(int(fid)) +} + type Feed struct { id FeedID url string @@ -80,3 +85,31 @@ func (feed *Feed) AddPost(creator std.Address, parentID PostID, category uint64, } return post } + +func (feed *Feed) GetPostFormURL() string { + return "/r/demo/social_feeds?help&__func=CreatePost" + + "&fid=" + feed.id.String() + + "&body.type=textarea" +} + +func (feed *Feed) Render() string { + str := "" + str += "There are " + intToString(feed.posts.Size()) + " post(s) \n\n" + + if feed.posts.Size() > 0 { + feed.posts.Iterate("", "", func(key string, value interface{}) bool { + if str != "" { + str += "----------------------------------------\n" + } + + post := value.(*Post) + postUrl := "/r/demo/social_feeds:" + feed.name + "/" + post.id.String() + + str += " * [PostID: " + post.id.String() + + " - " + intToString(post.reactions.Size()) + " reactions " + + " - " + intToString(post.comments.Size()) + " comments](" + postUrl + ")" + return false + }) + } + return str +} diff --git a/examples/gno.land/r/demo/social_feeds/misc.gno b/examples/gno.land/r/demo/social_feeds/misc.gno index d9f09aa1a76..c8ff1199757 100644 --- a/examples/gno.land/r/demo/social_feeds/misc.gno +++ b/examples/gno.land/r/demo/social_feeds/misc.gno @@ -69,3 +69,7 @@ func padZero(u64 uint64, length int) string { func bytesToString(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) } + +func intToString(val int) string { + return strconv.Itoa(val) +} diff --git a/examples/gno.land/r/demo/social_feeds/post.gno b/examples/gno.land/r/demo/social_feeds/post.gno index d2667f064ad..78e7caac611 100644 --- a/examples/gno.land/r/demo/social_feeds/post.gno +++ b/examples/gno.land/r/demo/social_feeds/post.gno @@ -15,6 +15,10 @@ import ( type PostID uint64 +func (pid PostID) String() string { + return strconv.Itoa(int(pid)) +} + type Reaction struct { icon string count uint64 @@ -138,3 +142,7 @@ func (post *Post) React(icon string, up bool) { post.reactions.Set(icon, count) } } + +func (post *Post) Render() string { + return post.metadata +} diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index fd8a87a4d34..c5e510c2085 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -9,13 +9,17 @@ import ( "gno.land/p/demo/ufmt" ) +// Only registered user can create a new feed +// For the flexibility when testing, allow all user to create feed func CreateFeed(name string) FeedID { fid := incGetFeedID() caller := std.PrevRealm().Addr() - if usernameOf(caller) == "" { - panic("only registered user can create feed") - } - url := "/r/demo/feeds:" + name + + // if usernameOf(caller) == "" { + // panic("only registered user can create feed") + // } + + url := "/r/demo/social_feeds:" + name feed := newFeed(fid, url, name, caller) fidkey := feedIDKey(fid) gFeeds.Set(fidkey, feed) @@ -23,14 +27,10 @@ func CreateFeed(name string) FeedID { return feed.id } -// Anyone can create a post in a existing feed -// Anonymous user has to pay a configurable minimum fee +// Anyone can create a post in a existing feed, allow un-registered users also func CreatePost(fid FeedID, parentID PostID, catetory uint64, metadata string) PostID { caller := std.PrevRealm().Addr() - if usernameOf(caller) == "" { - panic("please register user") - } feed := mustGetFeed(fid) post := feed.AddPost(caller, parentID, catetory, metadata) return post.id @@ -140,12 +140,3 @@ func GetPost(fid FeedID, pid PostID) string { post := data.(*Post) return post.String() } - -func checkAnonFee() bool { - sent := std.GetOrigSend() - anonFeeCoin := std.Coin{"ugnot", int64(gDefaultAnonFee)} - if len(sent) == 1 && sent[0].IsGTE(anonFeeCoin) { - return true - } - return false -} diff --git a/examples/gno.land/r/demo/social_feeds/render.gno b/examples/gno.land/r/demo/social_feeds/render.gno new file mode 100644 index 00000000000..4388d3ce2aa --- /dev/null +++ b/examples/gno.land/r/demo/social_feeds/render.gno @@ -0,0 +1,86 @@ +package social_feeds + +import ( + "strconv" + "strings" +) + +func renderFeed(parts []string) string { + // /r/demo/social_feeds:FEED_NAME + name := parts[0] + feedI, exists := gFeedsByName.Get(name) + if !exists { + return "feed does not exist: " + name + } + return feedI.(*Feed).Render() +} + +func renderPost(parts []string) string { + // /r/demo/boards:FEED_NAME/POST_ID + name := parts[0] + feedI, exists := gFeedsByName.Get(name) + if !exists { + return "feed does not exist: " + name + } + pid, err := strconv.Atoi(parts[1]) + if err != nil { + return "invalid thread id: " + parts[1] + } + feed := feedI.(*Feed) + post := feed.MustGetPost(PostID(pid)) + return post.Render() +} + +func renderFeedsList() string { + str := "There are " + intToString(gFeeds.Size()) + " available feeds:\n\n" + gFeeds.Iterate("", "", func(key string, value interface{}) bool { + feed := value.(*Feed) + str += " * [" + feed.url + " (FeedID: " + feed.id.String() + ")](" + feed.url + ")\n" + return false + }) + return str +} + +func Render(path string) string { + if path == "" { + return renderFeedsList() + } + + parts := strings.Split(path, "/") + + if len(parts) == 1 { + // /r/demo/social_feeds:FEED_NAME + return renderFeed(parts) + } else if len(parts) == 2 { + // /r/demo/boards:FEED_NAME/POST_ID + return renderPost(parts) + } + // } else if len(parts) == 3 { + // // /r/demo/boards:BOARD_NAME/THREAD_ID/REPLY_ID + // name := parts[0] + // feedI, 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] + // } + // feed := feedI.(*Feed) + // thread := feed.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 + // } +} From 6fb95d7e2628eca694827190216da3b2f0adf1e1 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Sat, 5 Aug 2023 18:15:51 +0700 Subject: [PATCH 15/55] feat: add extra binutils --- examples/gno.land/p/demo/binutils/binutils.gno | 7 ------- examples/gno.land/r/demo/social_feeds/post.gno | 6 +++--- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/examples/gno.land/p/demo/binutils/binutils.gno b/examples/gno.land/p/demo/binutils/binutils.gno index adb8b726b70..bc76dd3d3b1 100644 --- a/examples/gno.land/p/demo/binutils/binutils.gno +++ b/examples/gno.land/p/demo/binutils/binutils.gno @@ -14,13 +14,6 @@ func EncodeLengthPrefixedStringUint16BE(s string) []byte { return b } -func EncodeLengthPrefixedStringUint32BE(s string) []byte { - b := make([]byte, 4+len(s)) - binary.BigEndian.PutUint32(b, uint32(len(s))) - copy(b[4:], s) - return b -} - func DecodeLengthPrefixedStringUint16BE(b []byte) (string, []byte, error) { if len(b) < 2 { return "", nil, ErrInvalidLengthPrefixedString diff --git a/examples/gno.land/r/demo/social_feeds/post.gno b/examples/gno.land/r/demo/social_feeds/post.gno index 78e7caac611..3469f440bcd 100644 --- a/examples/gno.land/r/demo/social_feeds/post.gno +++ b/examples/gno.land/r/demo/social_feeds/post.gno @@ -69,7 +69,7 @@ func (post *Post) Bytes() []byte { b = binary.BigEndian.AppendUint64(b, uint64(post.parentID)) b = binary.BigEndian.AppendUint64(b, uint64(post.feedID)) b = binary.BigEndian.AppendUint64(b, post.category) - b = append(b, binutils.EncodeLengthPrefixedStringUint32BE(post.metadata)...) + b = append(b, EncodeLengthPrefixedStringUint32BE(post.metadata)...) b = append(b, binutils.EncodeLengthPrefixedStringUint16BE(post.creator.String())...) b = binary.BigEndian.AppendUint32(b, uint32(post.createdAt.Unix())) @@ -85,7 +85,7 @@ func (post *Post) Bytes() []byte { SEPARATOR := "," reactionsStr := strings.Join(reactionsArr, SEPARATOR) - b = append(b, binutils.EncodeLengthPrefixedStringUint32BE(reactionsStr)...) + b = append(b, EncodeLengthPrefixedStringUint32BE(reactionsStr)...) // Encode comments ids var subpostIDArr []string @@ -101,7 +101,7 @@ func (post *Post) Bytes() []byte { }) subpostIDsStr := strings.Join(subpostIDArr, SEPARATOR) - b = append(b, binutils.EncodeLengthPrefixedStringUint32BE(subpostIDsStr)...) + b = append(b, EncodeLengthPrefixedStringUint32BE(subpostIDsStr)...) return b } From dc88135cce0e39be6f32547d7ba59e281f2bf061 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Sat, 5 Aug 2023 18:16:10 +0700 Subject: [PATCH 16/55] feat: add extra binutils --- .../gno.land/r/demo/social_feeds/binutils_extra.gno | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 examples/gno.land/r/demo/social_feeds/binutils_extra.gno diff --git a/examples/gno.land/r/demo/social_feeds/binutils_extra.gno b/examples/gno.land/r/demo/social_feeds/binutils_extra.gno new file mode 100644 index 00000000000..5d15c8f5468 --- /dev/null +++ b/examples/gno.land/r/demo/social_feeds/binutils_extra.gno @@ -0,0 +1,13 @@ +package social_feeds + +import ( + "encoding/binary" + "errors" +) + +func EncodeLengthPrefixedStringUint32BE(s string) []byte { + b := make([]byte, 4+len(s)) + binary.BigEndian.PutUint32(b, uint32(len(s))) + copy(b[4:], s) + return b +} From a440d7f92963ae156c3192512bcb49307becefb1 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Sat, 5 Aug 2023 18:52:57 +0700 Subject: [PATCH 17/55] chore: update module name --- examples/Makefile | 15 +++++++++++++-- .../r/demo/social_feeds/binutils_extra.gno | 1 - examples/gno.land/r/demo/social_feeds/feed.gno | 6 ------ examples/gno.land/r/demo/social_feeds/gno.mod | 2 +- examples/gno.land/r/demo/social_feeds/public.gno | 7 +------ examples/gno.land/r/demo/social_feeds/render.gno | 6 +++--- 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/examples/Makefile b/examples/Makefile index bddb79ba28d..774d08e23d2 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -38,10 +38,21 @@ GOFMT_FLAGS ?= -w fmt: go run -modfile ../misc/devdeps/go.mod mvdan.cc/gofumpt $(GOFMT_FLAGS) `find . -name "*.gno"` +.PHONY: create.pkg +create.pkg: + gnokey maketx addpkg \ + -deposit="1ugnot" \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -pkgdir="." \ + -pkgpath="gno.land/r/demo/social_feeds_v3" \ + test1 + .PHONY: create.feed create.feed: gnokey maketx call \ - -pkgpath "gno.land/r/demo/social_feeds" \ + -pkgpath "gno.land/r/demo/social_feeds_v3" \ -func "CreateFeed" \ -gas-fee 1000000ugnot \ -gas-wanted 2000000 \ @@ -53,7 +64,7 @@ create.feed: .PHONE: create.post create.post: gnokey maketx call \ - -pkgpath "gno.land/r/demo/social_feeds" \ + -pkgpath "gno.land/r/demo/social_feeds_v3" \ -func "CreatePost" \ -gas-fee 1000000ugnot \ -gas-wanted 2000000 \ diff --git a/examples/gno.land/r/demo/social_feeds/binutils_extra.gno b/examples/gno.land/r/demo/social_feeds/binutils_extra.gno index 5d15c8f5468..623ab932c1d 100644 --- a/examples/gno.land/r/demo/social_feeds/binutils_extra.gno +++ b/examples/gno.land/r/demo/social_feeds/binutils_extra.gno @@ -2,7 +2,6 @@ package social_feeds import ( "encoding/binary" - "errors" ) func EncodeLengthPrefixedStringUint32BE(s string) []byte { diff --git a/examples/gno.land/r/demo/social_feeds/feed.gno b/examples/gno.land/r/demo/social_feeds/feed.gno index 468a26663b4..8a5fbb4d1b7 100644 --- a/examples/gno.land/r/demo/social_feeds/feed.gno +++ b/examples/gno.land/r/demo/social_feeds/feed.gno @@ -86,12 +86,6 @@ func (feed *Feed) AddPost(creator std.Address, parentID PostID, category uint64, return post } -func (feed *Feed) GetPostFormURL() string { - return "/r/demo/social_feeds?help&__func=CreatePost" + - "&fid=" + feed.id.String() + - "&body.type=textarea" -} - func (feed *Feed) Render() string { str := "" str += "There are " + intToString(feed.posts.Size()) + " post(s) \n\n" diff --git a/examples/gno.land/r/demo/social_feeds/gno.mod b/examples/gno.land/r/demo/social_feeds/gno.mod index f692845bb96..1c29f4b5da2 100644 --- a/examples/gno.land/r/demo/social_feeds/gno.mod +++ b/examples/gno.land/r/demo/social_feeds/gno.mod @@ -1,4 +1,4 @@ -module gno.land/r/demo/social_feeds +module gno.land/r/demo/social_feeds_v3 require ( "gno.land/p/demo/ufmt" v0.0.0-latest diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index c5e510c2085..cf200334f33 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -14,12 +14,7 @@ import ( func CreateFeed(name string) FeedID { fid := incGetFeedID() caller := std.PrevRealm().Addr() - - // if usernameOf(caller) == "" { - // panic("only registered user can create feed") - // } - - url := "/r/demo/social_feeds:" + name + url := "/r/demo/social_feeds_v3:" + name feed := newFeed(fid, url, name, caller) fidkey := feedIDKey(fid) gFeeds.Set(fidkey, feed) diff --git a/examples/gno.land/r/demo/social_feeds/render.gno b/examples/gno.land/r/demo/social_feeds/render.gno index 4388d3ce2aa..da190c5b1e6 100644 --- a/examples/gno.land/r/demo/social_feeds/render.gno +++ b/examples/gno.land/r/demo/social_feeds/render.gno @@ -6,7 +6,7 @@ import ( ) func renderFeed(parts []string) string { - // /r/demo/social_feeds:FEED_NAME + // /r/demo/social_feeds_v3:FEED_NAME name := parts[0] feedI, exists := gFeedsByName.Get(name) if !exists { @@ -49,10 +49,10 @@ func Render(path string) string { parts := strings.Split(path, "/") if len(parts) == 1 { - // /r/demo/social_feeds:FEED_NAME + // /r/demo/social_feeds_v3:FEED_NAME return renderFeed(parts) } else if len(parts) == 2 { - // /r/demo/boards:FEED_NAME/POST_ID + // /r/demo/social_feeds_v3:FEED_NAME/POST_ID return renderPost(parts) } // } else if len(parts) == 3 { From 5adb1a93d189d62af350b2ed31959b6926e0b082 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Sat, 5 Aug 2023 18:53:06 +0700 Subject: [PATCH 18/55] chore: update module name --- examples/gno.land/r/demo/social_feeds/gno.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/r/demo/social_feeds/gno.mod b/examples/gno.land/r/demo/social_feeds/gno.mod index 1c29f4b5da2..f692845bb96 100644 --- a/examples/gno.land/r/demo/social_feeds/gno.mod +++ b/examples/gno.land/r/demo/social_feeds/gno.mod @@ -1,4 +1,4 @@ -module gno.land/r/demo/social_feeds_v3 +module gno.land/r/demo/social_feeds require ( "gno.land/p/demo/ufmt" v0.0.0-latest From 20481bae11cc33082d2159f32561bec5405eb61d Mon Sep 17 00:00:00 2001 From: yo1110 Date: Tue, 8 Aug 2023 23:41:27 +0700 Subject: [PATCH 19/55] feat: handle all categories --- .../gno.land/r/demo/social_feeds/feed.gno | 2 +- .../r/demo/social_feeds/feeds_test.gno | 67 ++++++++++++++++--- .../gno.land/r/demo/social_feeds/public.gno | 16 ++++- .../gno.land/r/demo/social_feeds/render.gno | 28 -------- 4 files changed, 70 insertions(+), 43 deletions(-) diff --git a/examples/gno.land/r/demo/social_feeds/feed.gno b/examples/gno.land/r/demo/social_feeds/feed.gno index 8a5fbb4d1b7..5d33d49959b 100644 --- a/examples/gno.land/r/demo/social_feeds/feed.gno +++ b/examples/gno.land/r/demo/social_feeds/feed.gno @@ -97,7 +97,7 @@ func (feed *Feed) Render() string { } post := value.(*Post) - postUrl := "/r/demo/social_feeds:" + feed.name + "/" + post.id.String() + postUrl := "/r/demo/social_feeds_v3:" + feed.name + "/" + post.id.String() str += " * [PostID: " + post.id.String() + " - " + intToString(post.reactions.Size()) + " reactions " + diff --git a/examples/gno.land/r/demo/social_feeds/feeds_test.gno b/examples/gno.land/r/demo/social_feeds/feeds_test.gno index a635c17bcfe..bda8d788441 100644 --- a/examples/gno.land/r/demo/social_feeds/feeds_test.gno +++ b/examples/gno.land/r/demo/social_feeds/feeds_test.gno @@ -121,19 +121,19 @@ func Test(t *testing.T) { var postsStr string var postsCount int - postsStr = GetPosts(1, uint64(0), 0, 1) + postsStr = GetPosts(1, []uint64{0}, 0, 1) postsCount = len(strings.Split(postsStr, ",")) if postsCount != 1 { t.Fatalf("expected posts count: 1, got %q.", postsCount) } - postsStr = GetPosts(1, uint64(0), 0, 2) + postsStr = GetPosts(1, []uint64{0}, 0, 2) postsCount = len(strings.Split(postsStr, ",")) if postsCount != 2 { t.Fatalf("expected posts count: 2, got %q.", postsCount) } - postsStr = GetPosts(1, uint64(0), 0, 3) + postsStr = GetPosts(1, []uint64{0}, 0, 3) postsCount = len(strings.Split(postsStr, ",")) if postsCount != 2 { t.Fatalf("expected posts count: 2, got %q.", postsCount) @@ -142,17 +142,62 @@ func Test(t *testing.T) { // Delete one DeletePost(1, newPostID) - postsStr = GetPosts(1, uint64(0), PostID(1), 4) + postsStr = GetPosts(1, []uint64{0}, PostID(1), 4) postsCount = len(strings.Split(postsStr, ",")) if postsCount != 1 { t.Fatalf("expected posts count: 1, got %q.", postsCount) } - // Test tree - // Get Post list - // postsStr := GetPosts(feedID) - // postsBytes, err := base64.RawURLEncoding.DecodeString(postsStr) - // if err != nil { - // panic("uanble to decode post list") - // } + // Create Post in another category + otherFeedId := CreateFeed("other_feed") + + otherCatId := uint64(2) + rootParentID := PostID(0) + otherPostID := CreatePost(otherFeedId, rootParentID, otherCatId, metadata) + otherPostsStr := GetPosts(1, []uint64{7}, 0, 4) + + otherPost := createdFeed.MustGetPost(PostID(otherPostID)) + + otherPostsCount := 0 + if otherPostsStr != "" { + otherPostsCount := len(strings.Split(otherPostsStr, ",")) + } + + if otherPostsCount != 0 { + t.Fatalf("expected total posts with category 7: 0, got %q.", postID) + } + + // Query with exact category should return result + otherPostsStr = GetPosts(otherFeedId, []uint64{2}, 0, 4) + otherPostsCount = 0 + if otherPostsStr != "" { + otherPostsCount = len(strings.Split(otherPostsStr, ",")) + } + + if otherPostsCount != 1 { + t.Fatalf("expected total posts with category 2: 1, got %q.", otherPostsCount) + } + + // Query multi categories should return result + CreatePost(otherFeedId, rootParentID, uint64(8), metadata) + otherPostsStr = GetPosts(otherFeedId, []uint64{2, 7, 8}, 0, 4) + otherPostsCount = 0 + if otherPostsStr != "" { + otherPostsCount = len(strings.Split(otherPostsStr, ",")) + } + + if otherPostsCount != 2 { + t.Fatalf("expected total posts with category 2, 8: 2, got %q.", otherPostsCount) + } + + // Empty requested cateogries should return all + otherPostsStr = GetPosts(otherFeedId, []uint64{}, 0, 4) + otherPostsCount = 0 + if otherPostsStr != "" { + otherPostsCount = len(strings.Split(otherPostsStr, ",")) + } + + if otherPostsCount != 2 { + t.Fatalf("expected total posts with all categories: 2, got %q.", otherPostsCount) + } } diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index cf200334f33..ef291d25203 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -2,11 +2,12 @@ package social_feeds import ( "encoding/base64" + "fmt" "std" "strconv" "strings" - "gno.land/p/demo/ufmt" + "gno.land/p/demo/avl" ) // Only registered user can create a new feed @@ -67,18 +68,27 @@ func ReactPost(fid FeedID, pid PostID, icon string, up bool) { } // Get Posts list -func GetPosts(fid FeedID, category uint64, offset uint64, limit uint8) string { +func GetPosts(fid FeedID, categories []uint64, offset uint64, limit uint8) string { caller := std.PrevRealm().Addr() feed := mustGetFeed(fid) var postList []string var skipped uint64 + // Create an avlTree for optimizing the check + requestedCategories := avl.NewTree() + for _, category := range categories { + catStr := strconv.FormatUint(category, 10) + requestedCategories.Set(catStr, true) + } + feed.posts.Iterate("", "", func(key string, value interface{}) bool { post := value.(*Post) + postCatStr := strconv.FormatUint(post.category, 10) + // NOTE: this search mechanism is not efficient, only for demo purpose - if post.parentID == uint64(0) && post.category == category && post.deleted == false { + if post.parentID == uint64(0) && (requestedCategories.Size() == 0 || requestedCategories.Has(postCatStr)) && post.deleted == false { if skipped < offset { skipped++ return false diff --git a/examples/gno.land/r/demo/social_feeds/render.gno b/examples/gno.land/r/demo/social_feeds/render.gno index da190c5b1e6..45dd832643a 100644 --- a/examples/gno.land/r/demo/social_feeds/render.gno +++ b/examples/gno.land/r/demo/social_feeds/render.gno @@ -55,32 +55,4 @@ func Render(path string) string { // /r/demo/social_feeds_v3:FEED_NAME/POST_ID return renderPost(parts) } - // } else if len(parts) == 3 { - // // /r/demo/boards:BOARD_NAME/THREAD_ID/REPLY_ID - // name := parts[0] - // feedI, 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] - // } - // feed := feedI.(*Feed) - // thread := feed.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 - // } } From 030a5fb41907591a8ee3eb8ac374dab350803d73 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Tue, 8 Aug 2023 23:42:32 +0700 Subject: [PATCH 20/55] feat: update social_feeds_v4 --- examples/Makefile | 6 +++++- examples/gno.land/r/demo/social_feeds/feed.gno | 2 +- examples/gno.land/r/demo/social_feeds/public.gno | 2 +- examples/gno.land/r/demo/social_feeds/render.gno | 6 +++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/examples/Makefile b/examples/Makefile index 774d08e23d2..c69f90eefda 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -47,6 +47,8 @@ create.pkg: -broadcast="true" \ -pkgdir="." \ -pkgpath="gno.land/r/demo/social_feeds_v3" \ + -chainid "teritori-1" \ + -remote "https://testnet.gno.teritori.com:26657" \ test1 .PHONY: create.feed @@ -55,10 +57,12 @@ create.feed: -pkgpath "gno.land/r/demo/social_feeds_v3" \ -func "CreateFeed" \ -gas-fee 1000000ugnot \ - -gas-wanted 2000000 \ + -gas-wanted 3000000 \ -send "" \ -broadcast \ -args "teritori" \ + -chainid "teritori-1" \ + -remote "https://testnet.gno.teritori.com:26657" \ test1 .PHONE: create.post diff --git a/examples/gno.land/r/demo/social_feeds/feed.gno b/examples/gno.land/r/demo/social_feeds/feed.gno index 5d33d49959b..67112c30644 100644 --- a/examples/gno.land/r/demo/social_feeds/feed.gno +++ b/examples/gno.land/r/demo/social_feeds/feed.gno @@ -97,7 +97,7 @@ func (feed *Feed) Render() string { } post := value.(*Post) - postUrl := "/r/demo/social_feeds_v3:" + feed.name + "/" + post.id.String() + postUrl := "/r/demo/social_feeds_v4:" + feed.name + "/" + post.id.String() str += " * [PostID: " + post.id.String() + " - " + intToString(post.reactions.Size()) + " reactions " + diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index ef291d25203..9e6821fc40c 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -15,7 +15,7 @@ import ( func CreateFeed(name string) FeedID { fid := incGetFeedID() caller := std.PrevRealm().Addr() - url := "/r/demo/social_feeds_v3:" + name + url := "/r/demo/social_feeds_v4:" + name feed := newFeed(fid, url, name, caller) fidkey := feedIDKey(fid) gFeeds.Set(fidkey, feed) diff --git a/examples/gno.land/r/demo/social_feeds/render.gno b/examples/gno.land/r/demo/social_feeds/render.gno index 45dd832643a..d1bae50e91a 100644 --- a/examples/gno.land/r/demo/social_feeds/render.gno +++ b/examples/gno.land/r/demo/social_feeds/render.gno @@ -6,7 +6,7 @@ import ( ) func renderFeed(parts []string) string { - // /r/demo/social_feeds_v3:FEED_NAME + // /r/demo/social_feeds_v4:FEED_NAME name := parts[0] feedI, exists := gFeedsByName.Get(name) if !exists { @@ -49,10 +49,10 @@ func Render(path string) string { parts := strings.Split(path, "/") if len(parts) == 1 { - // /r/demo/social_feeds_v3:FEED_NAME + // /r/demo/social_feeds_v4:FEED_NAME return renderFeed(parts) } else if len(parts) == 2 { - // /r/demo/social_feeds_v3:FEED_NAME/POST_ID + // /r/demo/social_feeds_v4:FEED_NAME/POST_ID return renderPost(parts) } } From 5f05ad73db02b44a75d826c48f1833896723faed Mon Sep 17 00:00:00 2001 From: yo1110 Date: Sat, 12 Aug 2023 08:44:29 +0700 Subject: [PATCH 21/55] chore: optimize test code --- examples/Makefile | 2 +- .../r/demo/social_feeds/feeds_test.gno | 243 ++++++++++-------- .../gno.land/r/demo/social_feeds/public.gno | 1 - 3 files changed, 139 insertions(+), 107 deletions(-) diff --git a/examples/Makefile b/examples/Makefile index c69f90eefda..1a39d3288db 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -54,7 +54,7 @@ create.pkg: .PHONY: create.feed create.feed: gnokey maketx call \ - -pkgpath "gno.land/r/demo/social_feeds_v3" \ + -pkgpath "gno.land/r/demo/social_feeds_v4" \ -func "CreateFeed" \ -gas-fee 1000000ugnot \ -gas-wanted 3000000 \ diff --git a/examples/gno.land/r/demo/social_feeds/feeds_test.gno b/examples/gno.land/r/demo/social_feeds/feeds_test.gno index bda8d788441..cbeb14c5416 100644 --- a/examples/gno.land/r/demo/social_feeds/feeds_test.gno +++ b/examples/gno.land/r/demo/social_feeds/feeds_test.gno @@ -4,7 +4,6 @@ package social_feeds import ( "encoding/base64" - "fmt" "std" "strconv" "strings" @@ -16,49 +15,61 @@ import ( "gno.land/r/demo/users" ) -func Test(t *testing.T) { - // NOTE: Dont know why std.GetOrigCaller in users.Register is always = std.GetCallerAt(1) here - admin := std.GetCallerAt(1) - user := testutils.TestAddress("user") +var ( + rootPostID = PostID(0) + postID1 = PostID(1) + feedID1 = FeedID(1) + cat1 = uint64(1) + cat2 = uint64(2) +) - std.TestSetOrigCaller(admin) - std.TestSetOrigSend(std.Coins{{"ugnot", 200000000}}, nil) - users.Register("", "social_feeds_admin", "") +func getFeed1() *Feed { + return mustGetFeed(feedID1) +} + +func getPost1() *Post { + feed1 := getFeed1() + post1 := feed1.MustGetPost(postID1) + return post1 +} - // Create feed with registered user ===================================== - feedName := "teritori" - feedID := CreateFeed(feedName) - createdFeed := mustGetFeed(feedID) +func testCreateFeed(t *testing.T) { + feedID := CreateFeed("teritori") + feed := mustGetFeed(feedID) if feedID != 1 { t.Fatalf("expected feedID: 1, got %q.", feedID) } - if createdFeed.name != feedName { - t.Fatalf("expected feedName: %q, got %q.", feedName, createdFeed.name) + if feed.name != "teritori" { + t.Fatalf("expected feedName: teritori, got %q.", feed.name) } +} - // Create Post ========================================================== - parentID := PostID(0) - catID := uint64(0) +func testCreatePost(t *testing.T) { metadata := `{"gifs": [], "files": [], "title": "", "message": "testouille", "hashtags": [], "mentions": [], "createdAt": "2023-03-29T12:19:04.858Z", "updatedAt": "2023-03-29T12:19:04.858Z"}` - postID := CreatePost(feedID, parentID, catID, metadata) - createdPost := createdFeed.MustGetPost(postID) + postID := CreatePost(feedID1, rootPostID, cat1, metadata) + feed := mustGetFeed(feedID1) + post := feed.MustGetPost(postID) if postID != 1 { t.Fatalf("expected postID: 1, got %q.", postID) } - if createdPost.category != catID { - t.Fatalf("expected categoryID: %q, got %q.", catID, createdPost.category) + if post.category != cat1 { + t.Fatalf("expected categoryID: %q, got %q.", cat1, post.category) } +} + +func testReactPost(t *testing.T) { + feed := getFeed1() + post := getPost1() - // React Post ========================================================== icon := "🥰" - ReactPost(feedID, postID, icon, true) + ReactPost(feed.id, post.id, icon, true) // Set reaction - reactionCount_, ok := createdPost.reactions.Get("🥰") + reactionCount_, ok := post.reactions.Get("🥰") if !ok { t.Fatalf("expected 🥰 exists") } @@ -69,35 +80,38 @@ func Test(t *testing.T) { } // Unset reaction - ReactPost(feedID, postID, icon, false) - _, exist := createdPost.reactions.Get("🥰") + ReactPost(feed.id, post.id, icon, false) + _, exist := post.reactions.Get("🥰") if exist { t.Fatalf("expected 🥰 not exist") } +} + +func testCreateAndDeleteComment(t *testing.T) { + feed1 := getFeed1() + post1 := getPost1() - // Create SubPost ========================================================== - parentID = createdPost.id - catID = uint64(1) - metadata = `empty_meta_data` - subPostID := CreatePost(feedID, parentID, catID, metadata) - subPostID2 := CreatePost(feedID, parentID, catID, metadata) - createdSubPost := createdFeed.MustGetPost(subPostID) + metadata := `empty_meta_data` - if createdSubPost.id != 2 { - t.Fatalf("expected postID: 2, got %q.", subPostID) + commentID1 := CreatePost(feed1.id, post1.id, cat1, metadata) + commentID2 := CreatePost(feed1.id, post1.id, cat1, metadata) + comment2 := feed1.MustGetPost(commentID2) + + if comment2.id != 3 { // 1 post + 2 comments = 3 + t.Fatalf("expected comment postID: 3, got %q.", comment2.id) } - if createdSubPost.parentID != parentID { - t.Fatalf("expected parentID: %q, got %q.", parentID, createdPost.parentID) + if comment2.parentID != post1.id { + t.Fatalf("expected comment parentID: %q, got %q.", post1.id, comment2.parentID) } // Check comments size - if createdPost.comments.Size() != 2 { - t.Fatalf("expected comments count: 1, got %q.", createdPost.comments.Size()) + if post1.comments.Size() != 2 { + t.Fatalf("expected comments count: 2, got %q.", post1.comments.Size()) } // Get comments - comments := GetComments(feedID, postID) + comments := GetComments(feed1.id, post1.id) commentsSplitted := strings.Split(comments, ",") if len(commentsSplitted) != 2 { @@ -105,99 +119,118 @@ func Test(t *testing.T) { } // Delete 1 comment - DeletePost(1, subPostID2) - comments = GetComments(feedID, postID) + DeletePost(feed1.id, comment2.id) + comments = GetComments(feed1.id, post1.id) commentsSplitted = strings.Split(comments, ",") if len(commentsSplitted) != 1 { t.Fatalf("expected encoded comments: 1, got %q.", commentsSplitted) } +} - // Re-add reaction to test post list - ReactPost(1, postID, "🥰", true) - ReactPost(1, postID, "😇", true) - - // Get Post: 2 post, 2 subposts - newPostID := CreatePost(1, PostID(0), uint64(0), "metadata") - var postsStr string - var postsCount int +func countPosts(feedID FeedID, categories []uint64, limit uint8) int { + offset := uint64(0) - postsStr = GetPosts(1, []uint64{0}, 0, 1) - postsCount = len(strings.Split(postsStr, ",")) - if postsCount != 1 { - t.Fatalf("expected posts count: 1, got %q.", postsCount) + postsStr := GetPosts(feedID, categories, offset, limit) + if postsStr == "" { + return 0 } - postsStr = GetPosts(1, []uint64{0}, 0, 2) - postsCount = len(strings.Split(postsStr, ",")) - if postsCount != 2 { - t.Fatalf("expected posts count: 2, got %q.", postsCount) - } + postsCount := len(strings.Split(postsStr, ",")) + return postsCount +} - postsStr = GetPosts(1, []uint64{0}, 0, 3) - postsCount = len(strings.Split(postsStr, ",")) - if postsCount != 2 { - t.Fatalf("expected posts count: 2, got %q.", postsCount) - } +func testFilterByCategories(t *testing.T) { + // // Re-add reaction to test post list + // ReactPost(1, postID, "🥰", true) + // ReactPost(1, postID, "😇", true) - // Delete one - DeletePost(1, newPostID) + filter_cat1 := []uint64{1} + filter_cat1_2 := []uint64{1, 2} + filter_cat9 := []uint64{9} + filter_cat1_2_9 := []uint64{1, 2, 9} + filter_all := []uint64{} - postsStr = GetPosts(1, []uint64{0}, PostID(1), 4) - postsCount = len(strings.Split(postsStr, ",")) - if postsCount != 1 { - t.Fatalf("expected posts count: 1, got %q.", postsCount) - } + feedID2 := CreateFeed("teritori2") + feed2 := mustGetFeed(feedID2) + + // Create 2 posts on root with cat1 + postID1 := CreatePost(feed2.id, rootPostID, cat1, "metadata") + postID2 := CreatePost(feed2.id, rootPostID, cat1, "metadata") - // Create Post in another category - otherFeedId := CreateFeed("other_feed") + // Create 1 posts on root with cat2 + postID3 := CreatePost(feed2.id, rootPostID, cat2, "metadata") - otherCatId := uint64(2) - rootParentID := PostID(0) - otherPostID := CreatePost(otherFeedId, rootParentID, otherCatId, metadata) - otherPostsStr := GetPosts(1, []uint64{7}, 0, 4) + // Create comments on post 1 + commentPostID1 := CreatePost(feed2.id, postID1, cat1, "metadata") - otherPost := createdFeed.MustGetPost(PostID(otherPostID)) + // cat1: Should return max = limit + if count := countPosts(feed2.id, filter_cat1, 1); count != 1 { + t.Fatalf("expected posts count: 1, got %q.", count) + } - otherPostsCount := 0 - if otherPostsStr != "" { - otherPostsCount := len(strings.Split(otherPostsStr, ",")) + // cat1: Should return max = total + if count := countPosts(feed2.id, filter_cat1, 10); count != 2 { + t.Fatalf("expected posts count: 2, got %q.", count) } - if otherPostsCount != 0 { - t.Fatalf("expected total posts with category 7: 0, got %q.", postID) + // cat 1 + 2: Should return max = limit + if count := countPosts(feed2.id, filter_cat1_2, 2); count != 2 { + t.Fatalf("expected posts count: 2, got %q.", count) } - // Query with exact category should return result - otherPostsStr = GetPosts(otherFeedId, []uint64{2}, 0, 4) - otherPostsCount = 0 - if otherPostsStr != "" { - otherPostsCount = len(strings.Split(otherPostsStr, ",")) + // cat 1 + 2: Should return max = total on both + if count := countPosts(feed2.id, filter_cat1_2, 10); count != 3 { + t.Fatalf("expected posts count: 3, got %q.", count) } - if otherPostsCount != 1 { - t.Fatalf("expected total posts with category 2: 1, got %q.", otherPostsCount) + // cat 1, 2, 9: Should return total of 1, 2 + if count := countPosts(feed2.id, filter_cat1_2_9, 10); count != 3 { + t.Fatalf("expected posts count: 3, got %q.", count) } - // Query multi categories should return result - CreatePost(otherFeedId, rootParentID, uint64(8), metadata) - otherPostsStr = GetPosts(otherFeedId, []uint64{2, 7, 8}, 0, 4) - otherPostsCount = 0 - if otherPostsStr != "" { - otherPostsCount = len(strings.Split(otherPostsStr, ",")) + // cat 9: Should return 0 + if count := countPosts(feed2.id, filter_cat9, 10); count != 0 { + t.Fatalf("expected posts count: 0, got %q.", count) } - if otherPostsCount != 2 { - t.Fatalf("expected total posts with category 2, 8: 2, got %q.", otherPostsCount) + // cat all: should return all + if count := countPosts(feed2.id, filter_all, 10); count != 3 { + t.Fatalf("expected posts count: 3, got %q.", count) } - // Empty requested cateogries should return all - otherPostsStr = GetPosts(otherFeedId, []uint64{}, 0, 4) - otherPostsCount = 0 - if otherPostsStr != "" { - otherPostsCount = len(strings.Split(otherPostsStr, ",")) + // add comments should not impact the results + CreatePost(feed2.id, postID1, cat1, "metadata") + CreatePost(feed2.id, postID2, cat1, "metadata") + + if count := countPosts(feed2.id, filter_all, 10); count != 3 { + t.Fatalf("expected posts count: 3, got %q.", count) } - if otherPostsCount != 2 { - t.Fatalf("expected total posts with all categories: 2, got %q.", otherPostsCount) + // delete a post should affect the result + DeletePost(feed2.id, postID1) + + if count := countPosts(feed2.id, filter_all, 10); count != 2 { + t.Fatalf("expected posts count: 2, got %q.", count) } } + +func Test(t *testing.T) { + // Setup ================================================================ + // NOTE: Dont know why std.GetOrigCaller in users.Register is always = std.GetCallerAt(1) here + admin := std.GetCallerAt(1) + user := testutils.TestAddress("user") + + std.TestSetOrigCaller(admin) + std.TestSetOrigSend(std.Coins{{"ugnot", 200000000}}, nil) + users.Register("", "social_feeds_admin", "") + + testCreateFeed(t) + + testCreatePost(t) + + testReactPost(t) + + testCreateAndDeleteComment(t) + + testFilterByCategories(t) +} diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index 9e6821fc40c..94296547558 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -2,7 +2,6 @@ package social_feeds import ( "encoding/base64" - "fmt" "std" "strconv" "strings" From 0f93a727ff2f7540c8fba0166d989c027224d096 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Sun, 13 Aug 2023 00:25:31 +0700 Subject: [PATCH 22/55] Add TipPost + tests --- .../r/demo/social_feeds/feeds_test.gno | 66 ++++++++++++++++++- .../gno.land/r/demo/social_feeds/post.gno | 12 ++++ .../gno.land/r/demo/social_feeds/public.gno | 8 +++ 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/r/demo/social_feeds/feeds_test.gno b/examples/gno.land/r/demo/social_feeds/feeds_test.gno index cbeb14c5416..96600fa28e9 100644 --- a/examples/gno.land/r/demo/social_feeds/feeds_test.gno +++ b/examples/gno.land/r/demo/social_feeds/feeds_test.gno @@ -21,6 +21,7 @@ var ( feedID1 = FeedID(1) cat1 = uint64(1) cat2 = uint64(2) + user = testutils.TestAddress("user") ) func getFeed1() *Feed { @@ -214,14 +215,73 @@ func testFilterByCategories(t *testing.T) { } } +func testTipPost(t *testing.T) { + creator := testutils.TestAddress("creator") + std.TestIssueCoins(creator, std.Coins{{"ugnot", 100_000_000}}) + + // NOTE: Dont know why the address should be this to be able to call banker (= std.GetCallerAt(1)) + tipper := std.Address("g17rgsdnfxzza0sdfsdma37sdwxagsz378833ca4") + + banker := std.GetBanker(std.BankerTypeReadonly) + + // Check Original coins of creator/tipper + if coins := banker.GetCoins(creator); coins[0].Amount != 100_000_000 { + t.Fatalf("expected creator coin count: 100_000_000, got %d.", coins[0].Amount) + } + + if coins := banker.GetCoins(tipper); coins[0].Amount != 200_000_000 { + t.Fatalf("expected tipper coin count: 200_000_000, got %d.", coins[0].Amount) + } + + // Creator creates feed, post + std.TestSetOrigCaller(creator) + + feedID3 := CreateFeed("teritori3") + feed3 := mustGetFeed(feedID3) + + postID1 := CreatePost(feed3.id, rootPostID, cat1, "metadata") + post1 := feed3.MustGetPost(postID1) + + // Tiper tips the ppst + std.TestSetOrigCaller(tipper) + + coins := banker.GetCoins(tipper) + + tipperCoins := banker.GetCoins(tipper) + + TipPost(feed3.id, post1.id, 1_000_000) + + // Coin must be deducted from tipper + if coins := banker.GetCoins(tipper); coins[0].Amount != 199_000_000 { + t.Fatalf("expected tipper coin after tipping: 199_000_000, got %d.", coins[0].Amount) + } + + // Coin must be increased for creator + if coins := banker.GetCoins(creator); coins[0].Amount != 101_000_000 { + t.Fatalf("expected creator coin after beging tipped: 101_000_000, got %d.", coins[0].Amount) + } + + // Total tip amount should increased + if post1.tipAmount != 1_000_000 { + t.Fatalf("expected total tipAmount: 1_000_000, got %d.", post1.tipAmount) + } + + // Add more tip should update this total + TipPost(feed3.id, post1.id, 2_000_000) + + if post1.tipAmount != 3_000_000 { + t.Fatalf("expected total tipAmount: 3_000_000, got %d.", post1.tipAmount) + } +} + func Test(t *testing.T) { // Setup ================================================================ // NOTE: Dont know why std.GetOrigCaller in users.Register is always = std.GetCallerAt(1) here admin := std.GetCallerAt(1) - user := testutils.TestAddress("user") std.TestSetOrigCaller(admin) - std.TestSetOrigSend(std.Coins{{"ugnot", 200000000}}, nil) + std.TestSetOrigSend(std.Coins{{"ugnot", 200_000_000}}, nil) + users.Register("", "social_feeds_admin", "") testCreateFeed(t) @@ -233,4 +293,6 @@ func Test(t *testing.T) { testCreateAndDeleteComment(t) testFilterByCategories(t) + + testTipPost(t) } diff --git a/examples/gno.land/r/demo/social_feeds/post.gno b/examples/gno.land/r/demo/social_feeds/post.gno index 3469f440bcd..1f15bf83907 100644 --- a/examples/gno.land/r/demo/social_feeds/post.gno +++ b/examples/gno.land/r/demo/social_feeds/post.gno @@ -43,6 +43,7 @@ type Post struct { reactions avl.Tree // icon -> count comments avl.Tree // Post.id -> *Post creator std.Address + tipAmount int64 deleted bool createdAt time.Time @@ -121,6 +122,17 @@ func (post *Post) Delete() { post.deletedAt = time.Now() } +func (post *Post) Tip(from std.Address, to std.Address, amount int64) { + banker := std.GetBanker(std.BankerTypeOrigSend) + // banker := std.GetBanker(std.BankerTypeRealmSend) + + coinsToSend := std.Coins{std.Coin{Denom: "ugnot", Amount: amount}} + banker.SendCoins(from, to, coinsToSend) + + // Update tip amount + post.tipAmount += amount +} + // Always remove reaction if count = 0 func (post *Post) React(icon string, up bool) { count_, ok := post.reactions.Get(icon) diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index 94296547558..7dcff198f3d 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -66,6 +66,14 @@ func ReactPost(fid FeedID, pid PostID, icon string, up bool) { post.React(icon, up) } +func TipPost(fid FeedID, pid PostID, amount int64) { + caller := std.PrevRealm().Addr() + feed := mustGetFeed(fid) + post := feed.MustGetPost(pid) + + post.Tip(caller, post.creator, amount) +} + // Get Posts list func GetPosts(fid FeedID, categories []uint64, offset uint64, limit uint8) string { caller := std.PrevRealm().Addr() From 480ebcc439ae10f4a19d8c9050d7579b55165b37 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Sun, 13 Aug 2023 00:46:12 +0700 Subject: [PATCH 23/55] Add filter by user + test --- .../r/demo/social_feeds/feeds_test.gno | 47 ++++++++++++++++++- .../gno.land/r/demo/social_feeds/public.gno | 12 ++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/examples/gno.land/r/demo/social_feeds/feeds_test.gno b/examples/gno.land/r/demo/social_feeds/feeds_test.gno index 96600fa28e9..167c077ad37 100644 --- a/examples/gno.land/r/demo/social_feeds/feeds_test.gno +++ b/examples/gno.land/r/demo/social_feeds/feeds_test.gno @@ -131,7 +131,20 @@ func testCreateAndDeleteComment(t *testing.T) { func countPosts(feedID FeedID, categories []uint64, limit uint8) int { offset := uint64(0) - postsStr := GetPosts(feedID, categories, offset, limit) + postsStr := GetPosts(feedID, "", categories, offset, limit) + if postsStr == "" { + return 0 + } + + postsCount := len(strings.Split(postsStr, ",")) + return postsCount +} + +func countPostsByUser(feedID FeedID, user string) int { + offset := uint64(0) + limit := uint8(10) + + postsStr := GetPosts(feedID, user, []uint64{}, offset, limit) if postsStr == "" { return 0 } @@ -274,6 +287,36 @@ func testTipPost(t *testing.T) { } } +func testFilterUser(t *testing.T) { + user1 := testutils.TestAddress("user1") + user2 := testutils.TestAddress("user2") + + // User1 create 2 posts + std.TestSetOrigCaller(user1) + + feedID4 := CreateFeed("teritori4") + feed4 := mustGetFeed(feedID4) + + CreatePost(feed4.id, rootPostID, cat1, "metadata1") + CreatePost(feed4.id, rootPostID, cat1, "metadata2") + + // User2 create 1 post + std.TestSetOrigCaller(user2) + CreatePost(feed4.id, rootPostID, cat1, "metadata3") + + if count := countPostsByUser(feed4.id, user1.String()); count != 2 { + t.Fatalf("expected total posts by user1: 2, got %d.", count) + } + + if count := countPostsByUser(feed4.id, user2.String()); count != 1 { + t.Fatalf("expected total posts by user2: 1, got %d.", count) + } + + if count := countPostsByUser(feed4.id, ""); count != 3 { + t.Fatalf("expected total posts: 3, got %d.", count) + } +} + func Test(t *testing.T) { // Setup ================================================================ // NOTE: Dont know why std.GetOrigCaller in users.Register is always = std.GetCallerAt(1) here @@ -295,4 +338,6 @@ func Test(t *testing.T) { testFilterByCategories(t) testTipPost(t) + + testFilterUser(t) } diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index 7dcff198f3d..628bc52a25a 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -75,7 +75,7 @@ func TipPost(fid FeedID, pid PostID, amount int64) { } // Get Posts list -func GetPosts(fid FeedID, categories []uint64, offset uint64, limit uint8) string { +func GetPosts(fid FeedID, user string, categories []uint64, offset uint64, limit uint8) string { caller := std.PrevRealm().Addr() feed := mustGetFeed(fid) @@ -95,7 +95,15 @@ func GetPosts(fid FeedID, categories []uint64, offset uint64, limit uint8) strin postCatStr := strconv.FormatUint(post.category, 10) // NOTE: this search mechanism is not efficient, only for demo purpose - if post.parentID == uint64(0) && (requestedCategories.Size() == 0 || requestedCategories.Has(postCatStr)) && post.deleted == false { + if post.parentID == uint64(0) && post.deleted == false { + if requestedCategories.Size() > 0 && !requestedCategories.Has(postCatStr) { + return false + } + + if user != "" && std.Address(user) != post.creator { + return false + } + if skipped < offset { skipped++ return false From e681af46b512383e324117e0c28ba48954e9b856 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Sun, 13 Aug 2023 23:45:10 +0700 Subject: [PATCH 24/55] feat: add migration to social_feeds --- .../gno.land/r/demo/social_feeds/feed.gno | 32 ++++++++- .../r/demo/social_feeds/feeds_test.gno | 68 +++++++++++++++++++ .../gno.land/r/demo/social_feeds/post.gno | 33 +++++++++ .../gno.land/r/demo/social_feeds/public.gno | 15 ++++ 4 files changed, 146 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/r/demo/social_feeds/feed.gno b/examples/gno.land/r/demo/social_feeds/feed.gno index 67112c30644..00823ef3fd8 100644 --- a/examples/gno.land/r/demo/social_feeds/feed.gno +++ b/examples/gno.land/r/demo/social_feeds/feed.gno @@ -93,7 +93,7 @@ func (feed *Feed) Render() string { if feed.posts.Size() > 0 { feed.posts.Iterate("", "", func(key string, value interface{}) bool { if str != "" { - str += "----------------------------------------\n" + str += "\n" } post := value.(*Post) @@ -101,9 +101,37 @@ func (feed *Feed) Render() string { str += " * [PostID: " + post.id.String() + " - " + intToString(post.reactions.Size()) + " reactions " + - " - " + intToString(post.comments.Size()) + " comments](" + postUrl + ")" + " - " + intToString(post.comments.Size()) + " comments](" + postUrl + ")" + + "\n" return false }) } return str } + +func (feed *Feed) MigrateFrom(srcFeed *Feed) { + if feed.id == FeedID(0) { + feed.id = srcFeed.id + } + feed.url = srcFeed.url + feed.name = srcFeed.name + feed.creator = srcFeed.creator + feed.owner = srcFeed.owner + feed.createdAt = srcFeed.createdAt + + srcFeed.posts.Iterate("", "", func(postKey string, value interface{}) bool { + post := value.(*Post) + targetPost := &Post{} + targetPost.MigrateFrom(post) + + feed.posts.Set(postKey, targetPost) + return false + }) + + gFeedsCtr += 1 + fidkey := feedIDKey(feed.id) + gFeeds.Set(fidkey, feed) + gFeedsByName.Set(feed.name, feed) + + feed.postsCtr = srcFeed.postsCtr +} diff --git a/examples/gno.land/r/demo/social_feeds/feeds_test.gno b/examples/gno.land/r/demo/social_feeds/feeds_test.gno index 167c077ad37..4b886bc66f9 100644 --- a/examples/gno.land/r/demo/social_feeds/feeds_test.gno +++ b/examples/gno.land/r/demo/social_feeds/feeds_test.gno @@ -4,11 +4,13 @@ package social_feeds import ( "encoding/base64" + "fmt" "std" "strconv" "strings" "testing" + "gno.land/p/demo/avl" "gno.land/p/demo/binutils" "gno.land/p/demo/testutils" "gno.land/r/demo/boards" @@ -317,6 +319,70 @@ func testFilterUser(t *testing.T) { } } +func testMigrate(t *testing.T) { + feedID7 := CreateFeed("teritori7") + feed7 := mustGetFeed(feedID7) + + // Create Posts + CreatePost(feed7.id, rootPostID, cat1, "metadata1") + postID := CreatePost(feed7.id, rootPostID, cat1, "metadata2") + postFrom7 := feed7.MustGetPost(postID) + + // Creat Comments + CreatePost(feed7.id, postID, cat1, "metadata3") + CreatePost(feed7.id, postID, cat1, "metadata4") + CreatePost(feed7.id, postID, cat1, "metadata4") + + // React Posts + ReactPost(feed7.id, postID, "🥰", true) + + // Tip Posts + // NOTE: Dont know why the address should be this to be able to call banker (= std.GetCallerAt(1)) + tipper := std.Address("g17rgsdnfxzza0sdfsdma37sdwxagsz378833ca4") + std.TestSetOrigCaller(tipper) + TipPost(feed7.id, postID, 1_000_000) + + // Clone to Feed2 + feed8 := &Feed{id: 8} + feed8.MigrateFrom(feed7) + feed8.name = "NewFeed" + + // Total posts + if feed8.posts.Size() != 5 { + t.Fatalf("total posts must be 5") + } + + // Total posts from root + if count := countPosts(feed8.id, []uint64{}, 10); count != 2 { + t.Fatalf("total root posts must be 2") + } + + post := feed8.MustGetPost(postID) + if post.tipAmount != 1_000_000 { + t.Fatalf("tip must be 1_000_000") + } + + if post.comments.Size() != 3 { + t.Fatalf("total comments must be 3") + } + + if post.reactions.Size() != 1 { + t.Fatalf("total reactions must be 1") + } + + if postFrom7.createdAt != post.createdAt { + t.Fatalf("createdAt should be the same") + } + + if postFrom7.creator != post.creator { + t.Fatalf("creator should be the same") + } + + if feed8.postsCtr != 5 { + t.Fatalf("postsCtr must be 5, get %d", feed8.postsCtr) + } +} + func Test(t *testing.T) { // Setup ================================================================ // NOTE: Dont know why std.GetOrigCaller in users.Register is always = std.GetCallerAt(1) here @@ -340,4 +406,6 @@ func Test(t *testing.T) { testTipPost(t) testFilterUser(t) + + testMigrate(t) } diff --git a/examples/gno.land/r/demo/social_feeds/post.gno b/examples/gno.land/r/demo/social_feeds/post.gno index 1f15bf83907..bdf37c91007 100644 --- a/examples/gno.land/r/demo/social_feeds/post.gno +++ b/examples/gno.land/r/demo/social_feeds/post.gno @@ -158,3 +158,36 @@ func (post *Post) React(icon string, up bool) { func (post *Post) Render() string { return post.metadata } + +func (post *Post) MigrateFrom(srcPost *Post) { + // Copy post to new target + post.feedID = srcPost.feedID + post.id = srcPost.id + post.parentID = srcPost.parentID + post.category = srcPost.category + post.metadata = srcPost.metadata + post.creator = srcPost.creator + post.tipAmount = srcPost.tipAmount + post.deleted = srcPost.deleted + post.createdAt = srcPost.createdAt + post.updatedAt = srcPost.updatedAt + post.deletedAt = srcPost.deletedAt + + // Copy reactions + srcPost.reactions.Iterate("", "", func(icon string, value interface{}) bool { + count := value.(int) + post.reactions.Set(icon, count) + return false + }) + + // Copy comments + srcPost.comments.Iterate("", "", func(pkey string, value interface{}) bool { + srcComment := value.(*Post) + + targetComment := &Post{} + targetComment.MigrateFrom(srcComment) + + post.comments.Set(pkey, targetComment) + return false + }) +} diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index 628bc52a25a..60404ed78a6 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -7,6 +7,8 @@ import ( "strings" "gno.land/p/demo/avl" + "gno.land/r/demo/social_feeds" + // Import realm to migrate data ) // Only registered user can create a new feed @@ -160,3 +162,16 @@ func GetPost(fid FeedID, pid PostID) string { post := data.(*Post) return post.String() } + +// TODO: allow only creator to call +func GetFeedByID(fid FeedID) *Feed { + return mustGetFeed(fid) +} + +func MigrateFromPreviousFeed() { + feed := &Feed{} + + fid := social_feeds.FeedID(1) + srcFeed := social_feeds.GetFeedByID(fid) + feed.MigrateFrom(srcFeed.(*Feed)) +} From 03e03a9cb26452ef92fbfea2a7a7fc4993f8d9e2 Mon Sep 17 00:00:00 2001 From: hthieu1110 Date: Mon, 14 Aug 2023 16:09:39 +0700 Subject: [PATCH 25/55] wip: add flag --- .../gno.land/r/demo/social_feeds/feed.gno | 13 +++++-- .../r/demo/social_feeds/feeds_test.gno | 3 ++ .../gno.land/r/demo/social_feeds/public.gno | 34 ++++++++++++++++--- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/examples/gno.land/r/demo/social_feeds/feed.gno b/examples/gno.land/r/demo/social_feeds/feed.gno index 00823ef3fd8..5f3d00d0f4c 100644 --- a/examples/gno.land/r/demo/social_feeds/feed.gno +++ b/examples/gno.land/r/demo/social_feeds/feed.gno @@ -6,6 +6,7 @@ import ( "time" "gno.land/p/demo/avl" + "gno.land/p/demo/flags_index" ) type FeedID uint64 @@ -24,6 +25,8 @@ type Feed struct { createdAt time.Time deleted avl.Tree + flags *flags_index.FlagsIndex + postsCtr uint64 } @@ -45,8 +48,8 @@ func newFeed(fid FeedID, url string, name string, creator std.Address) *Feed { posts: avl.Tree{}, createdAt: time.Now(), deleted: avl.Tree{}, - - postsCtr: 0, + flags: flags_index.NewFlagsIndex(), + postsCtr: 0, } } @@ -86,6 +89,12 @@ func (feed *Feed) AddPost(creator std.Address, parentID PostID, category uint64, return post } +func (feed *Feed) FlagPost(flagBy std.Address, pid PostID) { + flagID := getFlagID(feed.id, pid) + + feed.flags.Flag(flagID, flagBy.String()) +} + func (feed *Feed) Render() string { str := "" str += "There are " + intToString(feed.posts.Size()) + " post(s) \n\n" diff --git a/examples/gno.land/r/demo/social_feeds/feeds_test.gno b/examples/gno.land/r/demo/social_feeds/feeds_test.gno index 4b886bc66f9..f522f3c0f92 100644 --- a/examples/gno.land/r/demo/social_feeds/feeds_test.gno +++ b/examples/gno.land/r/demo/social_feeds/feeds_test.gno @@ -393,6 +393,9 @@ func Test(t *testing.T) { users.Register("", "social_feeds_admin", "") + path := std.CurrentRealm() + panic(path.PkgPath()) + testCreateFeed(t) testCreatePost(t) diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index 60404ed78a6..5f39dfa8068 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -7,8 +7,7 @@ import ( "strings" "gno.land/p/demo/avl" - "gno.land/r/demo/social_feeds" - // Import realm to migrate data + "gno.land/p/demo/ufmt" ) // Only registered user can create a new feed @@ -163,6 +162,31 @@ func GetPost(fid FeedID, pid PostID) string { return post.String() } +func FlagPost(fid FeedID, pid PostID) { + caller := std.PrevRealm().Addr() + feed := mustGetFeed(fid) + + feed.FlagPost(caller, pid) +} + +func GetFlags(fid FeedID, limit uint64, offset uint64) string { + feed := mustGetFeed(fid) + + type FlagCount struct { + FlagID FlagID + Count uint64 + } + + flags := feed.flags.GetFlags(limit, offset) + + var res []string + for _, flag := range flags { + res = append(res, ufmt.Sprintf("%s:%d", flag.FlagID, flag.Count)) + } + + return res.Join("|") +} + // TODO: allow only creator to call func GetFeedByID(fid FeedID) *Feed { return mustGetFeed(fid) @@ -171,7 +195,7 @@ func GetFeedByID(fid FeedID) *Feed { func MigrateFromPreviousFeed() { feed := &Feed{} - fid := social_feeds.FeedID(1) - srcFeed := social_feeds.GetFeedByID(fid) - feed.MigrateFrom(srcFeed.(*Feed)) + // fid := social_feeds.FeedID(1) + // srcFeed := social_feeds.GetFeedByID(fid) + // feed.MigrateFrom(srcFeed.(*Feed)) } From c662f344374d0ed0c3a4baff1f6f17ae6b82aa13 Mon Sep 17 00:00:00 2001 From: hthieu1110 Date: Mon, 14 Aug 2023 16:09:53 +0700 Subject: [PATCH 26/55] wip: add flags --- .../gno.land/r/demo/social_feeds/flags.gno | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 examples/gno.land/r/demo/social_feeds/flags.gno diff --git a/examples/gno.land/r/demo/social_feeds/flags.gno b/examples/gno.land/r/demo/social_feeds/flags.gno new file mode 100644 index 00000000000..12a78f61536 --- /dev/null +++ b/examples/gno.land/r/demo/social_feeds/flags.gno @@ -0,0 +1,30 @@ +package social_feeds + +import ( + "strconv" + "strings" + + "gno.land/p/demo/flags_index" +) + +var SEPARATOR = "/" + +func getFlagID(fid FeedID, pid PostID) flags_index.FlagID { + return flags_index.FlagID(fid.String() + SEPARATOR + pid.String()) +} + +func parseFlagID(flagID flags_index.FlagID) (FeedID, PostID) { + parts := strings.Split(string(flagID), SEPARATOR) + if len(parts) != 2 { + panic("invalid flag ID '" + string(flagID) + "'") + } + fid, err := strconv.Atoi(parts[0]) + if err != nil || fid == 0 { + panic("invalid feed ID in flag ID '" + parts[0] + "'") + } + pid, err := strconv.Atoi(parts[1]) + if err != nil || pid == 0 { + panic("invalid post ID in flag ID '" + parts[1] + "'") + } + return PostID(fid), PostID(pid) +} From 2e93a46974b74f2e2273aad015abcd3e548db2e4 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Tue, 15 Aug 2023 13:46:08 +0700 Subject: [PATCH 27/55] wip --- examples/gno.land/r/demo/social_feeds/feed.gno | 9 +++++++-- .../gno.land/r/demo/social_feeds/feeds_test.gno | 3 --- examples/gno.land/r/demo/social_feeds/flags.gno | 2 +- examples/gno.land/r/demo/social_feeds/post.gno | 15 +++++++++++---- examples/gno.land/r/demo/social_feeds/public.gno | 5 +++-- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/examples/gno.land/r/demo/social_feeds/feed.gno b/examples/gno.land/r/demo/social_feeds/feed.gno index 5f3d00d0f4c..d2c63da06b8 100644 --- a/examples/gno.land/r/demo/social_feeds/feed.gno +++ b/examples/gno.land/r/demo/social_feeds/feed.gno @@ -7,6 +7,7 @@ import ( "gno.land/p/demo/avl" "gno.land/p/demo/flags_index" + "gno.land/p/demo/ufmt" ) type FeedID uint64 @@ -108,9 +109,13 @@ func (feed *Feed) Render() string { post := value.(*Post) postUrl := "/r/demo/social_feeds_v4:" + feed.name + "/" + post.id.String() - str += " * [PostID: " + post.id.String() + + str += " * [" + + "PostID: " + post.id.String() + " - " + intToString(post.reactions.Size()) + " reactions " + - " - " + intToString(post.comments.Size()) + " comments](" + postUrl + ")" + + " - " + intToString(post.comments.Size()) + " comments" + + " - " + ufmt.Sprintf("%d", post.tipAmount) + " tip amount" + + "]" + + "(" + postUrl + ")" + "\n" return false }) diff --git a/examples/gno.land/r/demo/social_feeds/feeds_test.gno b/examples/gno.land/r/demo/social_feeds/feeds_test.gno index f522f3c0f92..4b886bc66f9 100644 --- a/examples/gno.land/r/demo/social_feeds/feeds_test.gno +++ b/examples/gno.land/r/demo/social_feeds/feeds_test.gno @@ -393,9 +393,6 @@ func Test(t *testing.T) { users.Register("", "social_feeds_admin", "") - path := std.CurrentRealm() - panic(path.PkgPath()) - testCreateFeed(t) testCreatePost(t) diff --git a/examples/gno.land/r/demo/social_feeds/flags.gno b/examples/gno.land/r/demo/social_feeds/flags.gno index 12a78f61536..510a62a29ab 100644 --- a/examples/gno.land/r/demo/social_feeds/flags.gno +++ b/examples/gno.land/r/demo/social_feeds/flags.gno @@ -26,5 +26,5 @@ func parseFlagID(flagID flags_index.FlagID) (FeedID, PostID) { if err != nil || pid == 0 { panic("invalid post ID in flag ID '" + parts[1] + "'") } - return PostID(fid), PostID(pid) + return FeedID(fid), PostID(pid) } diff --git a/examples/gno.land/r/demo/social_feeds/post.gno b/examples/gno.land/r/demo/social_feeds/post.gno index bdf37c91007..84f18a0365c 100644 --- a/examples/gno.land/r/demo/social_feeds/post.gno +++ b/examples/gno.land/r/demo/social_feeds/post.gno @@ -43,7 +43,7 @@ type Post struct { reactions avl.Tree // icon -> count comments avl.Tree // Post.id -> *Post creator std.Address - tipAmount int64 + tipAmount uint64 deleted bool createdAt time.Time @@ -104,6 +104,9 @@ func (post *Post) Bytes() []byte { subpostIDsStr := strings.Join(subpostIDArr, SEPARATOR) b = append(b, EncodeLengthPrefixedStringUint32BE(subpostIDsStr)...) + // Add tipAmount + b = binary.BigEndian.AppendUint64(b, post.tipAmount) + return b } @@ -123,14 +126,18 @@ func (post *Post) Delete() { } func (post *Post) Tip(from std.Address, to std.Address, amount int64) { - banker := std.GetBanker(std.BankerTypeOrigSend) - // banker := std.GetBanker(std.BankerTypeRealmSend) + if amount <= 0 { + panic("amount is not valid") + } + + // banker := std.GetBanker(std.BankerTypeOrigSend) + banker := std.GetBanker(std.BankerTypeRealmSend) coinsToSend := std.Coins{std.Coin{Denom: "ugnot", Amount: amount}} banker.SendCoins(from, to, coinsToSend) // Update tip amount - post.tipAmount += amount + post.tipAmount += uint64(amount) } // Always remove reaction if count = 0 diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index 5f39dfa8068..3ee5012ad51 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -7,6 +7,7 @@ import ( "strings" "gno.land/p/demo/avl" + "gno.land/p/demo/flags_index" "gno.land/p/demo/ufmt" ) @@ -173,7 +174,7 @@ func GetFlags(fid FeedID, limit uint64, offset uint64) string { feed := mustGetFeed(fid) type FlagCount struct { - FlagID FlagID + FlagID flags_index.FlagID Count uint64 } @@ -184,7 +185,7 @@ func GetFlags(fid FeedID, limit uint64, offset uint64) string { res = append(res, ufmt.Sprintf("%s:%d", flag.FlagID, flag.Count)) } - return res.Join("|") + return strings.Join(res, "|") } // TODO: allow only creator to call From 208face7eb6dd7df81644f7c69b8eb7fbbe128b2 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Tue, 15 Aug 2023 13:46:22 +0700 Subject: [PATCH 28/55] wip: add missinng files --- .../p/demo/flags_index/flags_index.gno | 162 ++++++++++++++++++ examples/gno.land/p/demo/flags_index/gno.mod | 5 + examples/gno.land/r/demo/social_feeds/CMD.md | 41 +++++ 3 files changed, 208 insertions(+) create mode 100644 examples/gno.land/p/demo/flags_index/flags_index.gno create mode 100644 examples/gno.land/p/demo/flags_index/gno.mod create mode 100644 examples/gno.land/r/demo/social_feeds/CMD.md diff --git a/examples/gno.land/p/demo/flags_index/flags_index.gno b/examples/gno.land/p/demo/flags_index/flags_index.gno new file mode 100644 index 00000000000..72286b21b6a --- /dev/null +++ b/examples/gno.land/p/demo/flags_index/flags_index.gno @@ -0,0 +1,162 @@ +package flags_index + +import ( + "strconv" + + "gno.land/p/demo/avl" +) + +type FlagID string + +type FlagCount struct { + FlagID FlagID + Count uint64 +} + +type FlagsIndex struct { + flagsCounts []*FlagCount // sorted by count descending; TODO: optimize using big brain datastructure + flagsCountsByID *avl.Tree // key: flagID -> FlagCount + flagsByFlaggerID *avl.Tree // key: flaggerID -> *avl.Tree key: flagID -> struct{} +} + +func NewFlagsIndex() *FlagsIndex { + return &FlagsIndex{ + flagsCountsByID: avl.NewTree(), + flagsByFlaggerID: avl.NewTree(), + } +} + +func (fi *FlagsIndex) HasFlagged(flagID FlagID, flaggerID string) bool { + if flagsByFlagID, ok := fi.flagsByFlaggerID.Get(flaggerID); ok { + if flagsByFlagID.(*avl.Tree).Has(string(flagID)) { + return true + } + } + return false +} + +func (fi *FlagsIndex) GetFlagCount(flagID FlagID) uint64 { + if flagCount, ok := fi.flagsCountsByID.Get(string(flagID)); ok { + return flagCount.(*FlagCount).Count + } + return 0 +} + +func (fi *FlagsIndex) GetFlags(limit uint64, offset uint64) []*FlagCount { + if limit == 0 { + return nil + } + if offset >= uint64(len(fi.flagsCounts)) { + return nil + } + if offset+limit > uint64(len(fi.flagsCounts)) { + limit = uint64(len(fi.flagsCounts)) - offset + } + return fi.flagsCounts[offset : offset+limit] +} + +func (fi *FlagsIndex) Flag(flagID FlagID, flaggerID string) { + // update flagsByFlaggerID + var flagsByFlagID *avl.Tree + if existingFlagsByFlagID, ok := fi.flagsByFlaggerID.Get(flaggerID); ok { + flagsByFlagID = existingFlagsByFlagID.(*avl.Tree) + if flagsByFlagID.(*avl.Tree).Has(string(flagID)) { + panic("already flagged") + } + } else { + newFlagsByFlagID := avl.NewTree() + fi.flagsByFlaggerID.Set(flaggerID, newFlagsByFlagID) + flagsByFlagID = newFlagsByFlagID + } + flagsByFlagID.Set(string(flagID), struct{}{}) + + // update flagsCountsByID and flagsCounts + iFlagCount, ok := fi.flagsCountsByID.Get(string(flagID)) + if !ok { + flagCount := &FlagCount{FlagID: flagID, Count: 1} + fi.flagsCountsByID.Set(string(flagID), flagCount) + fi.flagsCounts = append(fi.flagsCounts, flagCount) // this is valid because 1 will always be the lowest count and we want the newest flags to be last + } else { + flagCount := iFlagCount.(*FlagCount) + flagCount.Count++ + // move flagCount to correct position in flagsCounts + for i := len(fi.flagsCounts) - 1; i > 0; i-- { + if fi.flagsCounts[i].Count > fi.flagsCounts[i-1].Count { + fi.flagsCounts[i], fi.flagsCounts[i-1] = fi.flagsCounts[i-1], fi.flagsCounts[i] + } else { + break + } + } + } +} + +func (fi *FlagsIndex) ClearFlagCount(flagID FlagID) { + // find flagCount in byID + if !fi.flagsCountsByID.Has(string(flagID)) { + panic("flag ID not found") + } + + // remove from byID + fi.flagsCountsByID.Remove(string(flagID)) + + // remove from byCount, we need to recreate the slice since splicing is broken + newByCount := []*FlagCount{} + for i := range fi.flagsCounts { + if fi.flagsCounts[i].FlagID == flagID { + continue + } + newByCount = append(newByCount, fi.flagsCounts[i]) + } + fi.flagsCounts = newByCount + + // update flagsByFlaggerID + var empty []string + fi.flagsByFlaggerID.Iterate("", "", func(key string, value interface{}) bool { + t := value.(*avl.Tree) + t.Remove(string(flagID)) + if t.Size() == 0 { + empty = append(empty, key) + } + return false + }) + for _, key := range empty { + fi.flagsByFlaggerID.Remove(key) + } +} + +func (fi *FlagsIndex) Dump() string { + str := "" + + str += "## flagsCounts:\n" + for i := range fi.flagsCounts { + str += "- " + if fi.flagsCounts[i] == nil { + str += "nil (" + strconv.Itoa(i) + ")\n" + continue + } + str += string(fi.flagsCounts[i].FlagID) + " " + strconv.FormatUint(fi.flagsCounts[i].Count, 10) + "\n" + } + + str += "\n## flagsCountsByID:\n" + fi.flagsCountsByID.Iterate("", "", func(key string, value interface{}) bool { + str += "- " + if value == nil { + str += "nil (" + key + ")\n" + return false + } + str += key + ": " + string(value.(*FlagCount).FlagID) + " " + strconv.FormatUint(value.(*FlagCount).Count, 10) + "\n" + return false + }) + + str += "\n## flagsByFlaggerID:\n" + fi.flagsByFlaggerID.Iterate("", "", func(key string, value interface{}) bool { + str += "- " + key + ":\n" + value.(*avl.Tree).Iterate("", "", func(key string, value interface{}) bool { + str += " - " + key + "\n" + return false + }) + return false + }) + + return str +} diff --git a/examples/gno.land/p/demo/flags_index/gno.mod b/examples/gno.land/p/demo/flags_index/gno.mod new file mode 100644 index 00000000000..3da6281f480 --- /dev/null +++ b/examples/gno.land/p/demo/flags_index/gno.mod @@ -0,0 +1,5 @@ +module gno.land/p/demo/flags_index + +require ( + "gno.land/p/demo/avl" v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/r/demo/social_feeds/CMD.md b/examples/gno.land/r/demo/social_feeds/CMD.md new file mode 100644 index 00000000000..0a151bad304 --- /dev/null +++ b/examples/gno.land/r/demo/social_feeds/CMD.md @@ -0,0 +1,41 @@ +gnokey maketx call \ + -pkgpath "gno.land/r/demo/social_feeds" \ + -func "CreateFeed" \ + -gas-fee 1000000ugnot \ + -gas-wanted 3000000 \ + -send "" \ + -broadcast \ + -args "teritori" \ + test1 + +gnokey maketx call \ + -pkgpath "gno.land/r/demo/social_feeds" \ + -func "CreatePost" \ + -gas-fee 1000000ugnot \ + -gas-wanted 2000000 \ + -send "" \ + -broadcast \ + -args "1" \ + -args "0" \ + -args "2" \ + -args '{"gifs": [], "files": [], "title": "", "message": "Hello world 2 !", "hashtags": [], "mentions": [], "createdAt": "2023-08-03T01:39:45.522Z", "updatedAt": "2023-08-03T01:39:45.522Z"}' \ + test1 + +gnokey maketx addpkg \ + -deposit="1ugnot" \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -pkgdir="." \ + -pkgpath="gno.land/r/demo/social_feeds_v2" \ + test1 + +gnokey maketx call \ + -pkgpath "gno.land/r/demo/social_feeds" \ + -func "MigrateFromPreviousFeed" \ + -gas-fee 1000000ugnot \ + -gas-wanted 2000000 \ + -send "" \ + -broadcast \ + test1 + From f9ffa3cbffd4cd8118f2b9b7d2825c770d95adbb Mon Sep 17 00:00:00 2001 From: hthieu1110 Date: Tue, 15 Aug 2023 13:51:40 +0700 Subject: [PATCH 29/55] fix: test bank send --- examples/gno.land/r/demo/social_feeds/post.gno | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/gno.land/r/demo/social_feeds/post.gno b/examples/gno.land/r/demo/social_feeds/post.gno index 84f18a0365c..4329f7f2d53 100644 --- a/examples/gno.land/r/demo/social_feeds/post.gno +++ b/examples/gno.land/r/demo/social_feeds/post.gno @@ -134,7 +134,10 @@ func (post *Post) Tip(from std.Address, to std.Address, amount int64) { banker := std.GetBanker(std.BankerTypeRealmSend) coinsToSend := std.Coins{std.Coin{Denom: "ugnot", Amount: amount}} - banker.SendCoins(from, to, coinsToSend) + + banker := std.GetBanker(std.BankerTypeRealmSend) + pkgaddr := std.GetOrigPkgAddr() + banker.SendCoins(pkgaddr, to, coinsToSend) // Update tip amount post.tipAmount += uint64(amount) From d282920028b094f55326f21b8184fb3df5a91868 Mon Sep 17 00:00:00 2001 From: yo1110 Date: Wed, 16 Aug 2023 20:05:29 +0700 Subject: [PATCH 30/55] chore: change branch --- examples/gno.land/r/demo/social_feeds/post.gno | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/gno.land/r/demo/social_feeds/post.gno b/examples/gno.land/r/demo/social_feeds/post.gno index 4329f7f2d53..1b7e8ab265b 100644 --- a/examples/gno.land/r/demo/social_feeds/post.gno +++ b/examples/gno.land/r/demo/social_feeds/post.gno @@ -132,10 +132,8 @@ func (post *Post) Tip(from std.Address, to std.Address, amount int64) { // banker := std.GetBanker(std.BankerTypeOrigSend) banker := std.GetBanker(std.BankerTypeRealmSend) - coinsToSend := std.Coins{std.Coin{Denom: "ugnot", Amount: amount}} - banker := std.GetBanker(std.BankerTypeRealmSend) pkgaddr := std.GetOrigPkgAddr() banker.SendCoins(pkgaddr, to, coinsToSend) From cefd249dc37f5c7e6e6ac36c9df801426b41f12e Mon Sep 17 00:00:00 2001 From: yo1110 Date: Thu, 17 Aug 2023 08:53:56 +0700 Subject: [PATCH 31/55] wip: handle flag posts --- examples/gno.land/r/demo/social_feeds/CMD.md | 15 +++++ .../gno.land/r/demo/social_feeds/feed.gno | 58 ++++++++++++++++++- .../r/demo/social_feeds/feeds_test.gno | 43 +++++++++++++- .../gno.land/r/demo/social_feeds/post.gno | 10 +++- .../gno.land/r/demo/social_feeds/public.gno | 31 +++++++++- 5 files changed, 150 insertions(+), 7 deletions(-) diff --git a/examples/gno.land/r/demo/social_feeds/CMD.md b/examples/gno.land/r/demo/social_feeds/CMD.md index 0a151bad304..baacebe50f0 100644 --- a/examples/gno.land/r/demo/social_feeds/CMD.md +++ b/examples/gno.land/r/demo/social_feeds/CMD.md @@ -21,6 +21,21 @@ gnokey maketx call \ -args '{"gifs": [], "files": [], "title": "", "message": "Hello world 2 !", "hashtags": [], "mentions": [], "createdAt": "2023-08-03T01:39:45.522Z", "updatedAt": "2023-08-03T01:39:45.522Z"}' \ test1 +gnokey maketx call \ + -pkgpath "gno.land/r/demo/social_feeds" \ + -func "HidePostForMe" \ + -gas-fee 1000000ugnot \ + -gas-wanted 3000000 \ + -send "" \ + -broadcast \ + -args "1" \ + -args "1" \ + test1 + +// Query posts +gnokey query vm/qeval --data 'gno.land/r/demo/social_feeds +GetPosts(1, "", []uint64{}, 0, 10)' + gnokey maketx addpkg \ -deposit="1ugnot" \ -gas-fee="1ugnot" \ diff --git a/examples/gno.land/r/demo/social_feeds/feed.gno b/examples/gno.land/r/demo/social_feeds/feed.gno index d2c63da06b8..3ab682281a4 100644 --- a/examples/gno.land/r/demo/social_feeds/feed.gno +++ b/examples/gno.land/r/demo/social_feeds/feed.gno @@ -22,11 +22,12 @@ type Feed struct { name string creator std.Address owner std.Address - posts avl.Tree // Post.id -> *Post + posts avl.Tree // pidkey -> *Post createdAt time.Time deleted avl.Tree - flags *flags_index.FlagsIndex + flags *flags_index.FlagsIndex + hiddenPostsByUser avl.Tree // std.Address => *avl.Tree (postID => bool) postsCtr uint64 } @@ -96,6 +97,39 @@ func (feed *Feed) FlagPost(flagBy std.Address, pid PostID) { feed.flags.Flag(flagID, flagBy.String()) } +func (feed *Feed) HidePostForUser(caller std.Address, pid PostID) { + userAddr := caller.String() + + value, exists := feed.hiddenPostsByUser.Get(userAddr) + var hiddenPosts *avl.Tree + if exists { + hiddenPosts = value.(*avl.Tree) + } else { + hiddenPosts = avl.NewTree() + feed.hiddenPostsByUser.Set(userAddr, hiddenPosts) + } + + if hiddenPosts.Has(pid.String()) { + panic("PostID is already hidden: " + pid.String()) + } + + hiddenPosts.Set(pid.String(), true) +} + +func (feed *Feed) UnHidePostForUser(userAddress std.Address, pid PostID) { + value, exists := feed.hiddenPostsByUser.Get(userAddress.String()) + var hiddenPosts *avl.Tree + if exists { + hiddenPosts = value.(*avl.Tree) + _, removed := hiddenPosts.Remove(pid.String()) + if !removed { + panic("Post is not hidden: " + pid.String()) + } + } else { + panic("User has not hidden post: " + pid.String()) + } +} + func (feed *Feed) Render() string { str := "" str += "There are " + intToString(feed.posts.Size()) + " post(s) \n\n" @@ -107,7 +141,7 @@ func (feed *Feed) Render() string { } post := value.(*Post) - postUrl := "/r/demo/social_feeds_v4:" + feed.name + "/" + post.id.String() + postUrl := "/r/demo/social_feeds:" + feed.name + "/" + post.id.String() str += " * [" + "PostID: " + post.id.String() + @@ -120,6 +154,24 @@ func (feed *Feed) Render() string { return false }) } + + str += "---------------------------------------\n" + if feed.hiddenPostsByUser.Size() > 0 { + str += "Hidden posts by users:\n\n" + + feed.hiddenPostsByUser.Iterate("", "", func(userAddr string, value interface{}) bool { + hiddenPosts := value.(*avl.Tree) + str += "\nUser address: " + userAddr + "\n" + + hiddenPosts.Iterate("", "", func(pid string, value interface{}) bool { + str += "- PostID: " + pid + "\n" + return false + }) + + return false + }) + } + return str } diff --git a/examples/gno.land/r/demo/social_feeds/feeds_test.gno b/examples/gno.land/r/demo/social_feeds/feeds_test.gno index 4b886bc66f9..37006ef30b1 100644 --- a/examples/gno.land/r/demo/social_feeds/feeds_test.gno +++ b/examples/gno.land/r/demo/social_feeds/feeds_test.gno @@ -4,7 +4,6 @@ package social_feeds import ( "encoding/base64" - "fmt" "std" "strconv" "strings" @@ -24,6 +23,7 @@ var ( cat1 = uint64(1) cat2 = uint64(2) user = testutils.TestAddress("user") + filter_all = []uint64{} ) func getFeed1() *Feed { @@ -164,7 +164,6 @@ func testFilterByCategories(t *testing.T) { filter_cat1_2 := []uint64{1, 2} filter_cat9 := []uint64{9} filter_cat1_2_9 := []uint64{1, 2, 9} - filter_all := []uint64{} feedID2 := CreateFeed("teritori2") feed2 := mustGetFeed(feedID2) @@ -383,6 +382,44 @@ func testMigrate(t *testing.T) { } } +func testHidePostForMe(t *testing.T) { + user := std.Address("user") + std.TestSetOrigCaller(user) + + feedID8 := CreateFeed("teritor8") + feed8 := mustGetFeed(feedID8) + + postIDToHide := CreatePost(feed8.id, rootPostID, cat1, "metadata1") + postID := CreatePost(feed8.id, rootPostID, cat1, "metadata1") + + if count := countPosts(feed8.id, filter_all, 10); count != 2 { + t.Fatalf("expected posts count: 2, got %q.", count) + } + + // Hide a post for me + HidePostForMe(feed8.id, postIDToHide) + + if count := countPosts(feed8.id, filter_all, 10); count != 1 { + t.Fatalf("expected posts count after hidding: 1, got %q.", count) + } + + // Query from another user should return full list + another := std.Address("another") + std.TestSetOrigCaller(another) + + if count := countPosts(feed8.id, filter_all, 10); count != 2 { + t.Fatalf("expected posts count from another: 2, got %q.", count) + } + + // UnHide a post for me + std.TestSetOrigCaller(user) + UnHidePostForMe(feed8.id, postIDToHide) + + if count := countPosts(feed8.id, filter_all, 10); count != 2 { + t.Fatalf("expected posts count after unhidding: 2, got %q.", count) + } +} + func Test(t *testing.T) { // Setup ================================================================ // NOTE: Dont know why std.GetOrigCaller in users.Register is always = std.GetCallerAt(1) here @@ -408,4 +445,6 @@ func Test(t *testing.T) { testFilterUser(t) testMigrate(t) + + testHidePostForMe(t) } diff --git a/examples/gno.land/r/demo/social_feeds/post.gno b/examples/gno.land/r/demo/social_feeds/post.gno index 1b7e8ab265b..f46283f5c94 100644 --- a/examples/gno.land/r/demo/social_feeds/post.gno +++ b/examples/gno.land/r/demo/social_feeds/post.gno @@ -125,12 +125,20 @@ func (post *Post) Delete() { post.deletedAt = time.Now() } +// TODO: fix this, dont base on amount, we should base on value sent by user +/* + sent := std.GetOrigSend() + anonFeeCoin := std.Coin{"ugnot", int64(gDefaultAnonFee)} + if len(sent) == 1 && sent[0].IsGTE(anonFeeCoin) { + return true + } + return false +*/ func (post *Post) Tip(from std.Address, to std.Address, amount int64) { if amount <= 0 { panic("amount is not valid") } - // banker := std.GetBanker(std.BankerTypeOrigSend) banker := std.GetBanker(std.BankerTypeRealmSend) coinsToSend := std.Coins{std.Coin{Denom: "ugnot", Amount: amount}} diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index 3ee5012ad51..4389995602a 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -78,9 +78,14 @@ func TipPost(fid FeedID, pid PostID, amount int64) { // Get Posts list func GetPosts(fid FeedID, user string, categories []uint64, offset uint64, limit uint8) string { - caller := std.PrevRealm().Addr() + // caller := std.PrevRealm().Addr() + // NOTE: dont know why we cannot get caller by std.PrevRealm().Addr() + // and seems that this line works + caller := std.GetCallerAt(2) feed := mustGetFeed(fid) + panic(caller.String()) + var postList []string var skipped uint64 @@ -106,6 +111,16 @@ func GetPosts(fid FeedID, user string, categories []uint64, offset uint64, limit return false } + // Check if post is in hidden list + value, exists := feed.hiddenPostsByUser.Get(caller.String()) + if exists { + hiddenPosts := value.(*avl.Tree) + // If post.id exists in hiddenPosts tree => that post is hidden + if hiddenPosts.Has(post.id.String()) { + return false + } + } + if skipped < offset { skipped++ return false @@ -170,6 +185,20 @@ func FlagPost(fid FeedID, pid PostID) { feed.FlagPost(caller, pid) } +func HidePostForMe(fid FeedID, pid PostID) { + caller := std.PrevRealm().Addr() + feed := mustGetFeed(fid) + + feed.HidePostForUser(caller, pid) +} + +func UnHidePostForMe(fid FeedID, pid PostID) { + caller := std.PrevRealm().Addr() + feed := mustGetFeed(fid) + + feed.UnHidePostForUser(caller, pid) +} + func GetFlags(fid FeedID, limit uint64, offset uint64) string { feed := mustGetFeed(fid) From cecc1fd8a1fdad17f5f22d7f1d44dc6282760611 Mon Sep 17 00:00:00 2001 From: hthieu1110 Date: Fri, 18 Aug 2023 15:43:38 +0700 Subject: [PATCH 32/55] wip: handle flag post + temporary fix filter hidden posts due to GetPrevRealm empty --- examples/gno.land/r/demo/social_feeds/CMD.md | 11 ++++ .../gno.land/r/demo/social_feeds/feed.gno | 12 +++- .../r/demo/social_feeds/feeds_test.gno | 52 ++++++++++++----- .../gno.land/r/demo/social_feeds/post.gno | 15 ++--- .../gno.land/r/demo/social_feeds/public.gno | 58 +++++++++++++++++-- 5 files changed, 118 insertions(+), 30 deletions(-) diff --git a/examples/gno.land/r/demo/social_feeds/CMD.md b/examples/gno.land/r/demo/social_feeds/CMD.md index 0a151bad304..e522a537c67 100644 --- a/examples/gno.land/r/demo/social_feeds/CMD.md +++ b/examples/gno.land/r/demo/social_feeds/CMD.md @@ -21,6 +21,17 @@ gnokey maketx call \ -args '{"gifs": [], "files": [], "title": "", "message": "Hello world 2 !", "hashtags": [], "mentions": [], "createdAt": "2023-08-03T01:39:45.522Z", "updatedAt": "2023-08-03T01:39:45.522Z"}' \ test1 +gnokey maketx call \ + -pkgpath "gno.land/r/demo/social_feeds" \ + -func "TipPost" \ + -gas-fee 1000000ugnot \ + -gas-wanted 3000000 \ + -send "1000000ugnot" \ + -broadcast \ + -args "1" \ + -args "1" \ + test1 + gnokey maketx addpkg \ -deposit="1ugnot" \ -gas-fee="1ugnot" \ diff --git a/examples/gno.land/r/demo/social_feeds/feed.gno b/examples/gno.land/r/demo/social_feeds/feed.gno index d2c63da06b8..fdaddae653b 100644 --- a/examples/gno.land/r/demo/social_feeds/feed.gno +++ b/examples/gno.land/r/demo/social_feeds/feed.gno @@ -3,6 +3,7 @@ package social_feeds import ( "std" "strconv" + "strings" "time" "gno.land/p/demo/avl" @@ -93,10 +94,16 @@ func (feed *Feed) AddPost(creator std.Address, parentID PostID, category uint64, func (feed *Feed) FlagPost(flagBy std.Address, pid PostID) { flagID := getFlagID(feed.id, pid) + if feed.flags.HasFlagged(flagID, flagBy.String()) { + panic("already flagged") + } + feed.flags.Flag(flagID, flagBy.String()) } func (feed *Feed) Render() string { + pkgpath := std.CurrentRealmPath() + str := "" str += "There are " + intToString(feed.posts.Size()) + " post(s) \n\n" @@ -107,7 +114,7 @@ func (feed *Feed) Render() string { } post := value.(*Post) - postUrl := "/r/demo/social_feeds_v4:" + feed.name + "/" + post.id.String() + postUrl := strings.Replace(pkgpath, "gno.land", "", -1) + ":" + feed.name + "/" + post.id.String() str += " * [" + "PostID: " + post.id.String() + @@ -119,6 +126,9 @@ func (feed *Feed) Render() string { "\n" return false }) + + str += "------------------------- \n\n" + str += feed.flags.Dump() } return str } diff --git a/examples/gno.land/r/demo/social_feeds/feeds_test.gno b/examples/gno.land/r/demo/social_feeds/feeds_test.gno index 4b886bc66f9..af856039b98 100644 --- a/examples/gno.land/r/demo/social_feeds/feeds_test.gno +++ b/examples/gno.land/r/demo/social_feeds/feeds_test.gno @@ -235,7 +235,8 @@ func testTipPost(t *testing.T) { std.TestIssueCoins(creator, std.Coins{{"ugnot", 100_000_000}}) // NOTE: Dont know why the address should be this to be able to call banker (= std.GetCallerAt(1)) - tipper := std.Address("g17rgsdnfxzza0sdfsdma37sdwxagsz378833ca4") + tipper := testutils.TestAddress("tipper") + std.TestIssueCoins(tipper, std.Coins{{"ugnot", 50_000_000}}) banker := std.GetBanker(std.BankerTypeReadonly) @@ -244,8 +245,8 @@ func testTipPost(t *testing.T) { t.Fatalf("expected creator coin count: 100_000_000, got %d.", coins[0].Amount) } - if coins := banker.GetCoins(tipper); coins[0].Amount != 200_000_000 { - t.Fatalf("expected tipper coin count: 200_000_000, got %d.", coins[0].Amount) + if coins := banker.GetCoins(tipper); coins[0].Amount != 50_000_000 { + t.Fatalf("expected tipper coin count: 50_000_000, got %d.", coins[0].Amount) } // Creator creates feed, post @@ -259,17 +260,8 @@ func testTipPost(t *testing.T) { // Tiper tips the ppst std.TestSetOrigCaller(tipper) - - coins := banker.GetCoins(tipper) - - tipperCoins := banker.GetCoins(tipper) - - TipPost(feed3.id, post1.id, 1_000_000) - - // Coin must be deducted from tipper - if coins := banker.GetCoins(tipper); coins[0].Amount != 199_000_000 { - t.Fatalf("expected tipper coin after tipping: 199_000_000, got %d.", coins[0].Amount) - } + std.TestSetOrigSend(std.Coins{{"ugnot", 1_000_000}}, nil) + TipPost(feed3.id, post1.id) // Coin must be increased for creator if coins := banker.GetCoins(creator); coins[0].Amount != 101_000_000 { @@ -282,13 +274,38 @@ func testTipPost(t *testing.T) { } // Add more tip should update this total - TipPost(feed3.id, post1.id, 2_000_000) + std.TestSetOrigSend(std.Coins{{"ugnot", 2_000_000}}, nil) + TipPost(feed3.id, post1.id) if post1.tipAmount != 3_000_000 { t.Fatalf("expected total tipAmount: 3_000_000, got %d.", post1.tipAmount) } } +func testFlagPost(t *testing.T) { + flagger := testutils.TestAddress("flagger") + + feedID9 := CreateFeed("teritori9") + feed9 := mustGetFeed(feedID9) + + CreatePost(feed9.id, rootPostID, cat1, "metadata1") + pid := CreatePost(feed9.id, rootPostID, cat1, "metadata1") + + // Flag post + std.TestSetOrigCaller(flagger) + FlagPost(feed9.id, pid) + + // Another user flags + another := testutils.TestAddress("another") + std.TestSetOrigCaller(another) + FlagPost(feed9.id, pid) + + flaggedPostsStr := GetFlaggedPosts(feed9.id, 0, 10) + if flaggedPostsCount := len(strings.Split(flaggedPostsStr, ",")); flaggedPostsCount != 1 { + t.Fatalf("expected flagged posts: 1, got %d.", flaggedPostsCount) + } +} + func testFilterUser(t *testing.T) { user1 := testutils.TestAddress("user1") user2 := testutils.TestAddress("user2") @@ -340,7 +357,8 @@ func testMigrate(t *testing.T) { // NOTE: Dont know why the address should be this to be able to call banker (= std.GetCallerAt(1)) tipper := std.Address("g17rgsdnfxzza0sdfsdma37sdwxagsz378833ca4") std.TestSetOrigCaller(tipper) - TipPost(feed7.id, postID, 1_000_000) + std.TestSetOrigSend(std.Coins{{"ugnot", 1_000_000}}, nil) + TipPost(feed7.id, postID) // Clone to Feed2 feed8 := &Feed{id: 8} @@ -407,5 +425,7 @@ func Test(t *testing.T) { testFilterUser(t) + testFlagPost(t) + testMigrate(t) } diff --git a/examples/gno.land/r/demo/social_feeds/post.gno b/examples/gno.land/r/demo/social_feeds/post.gno index 4329f7f2d53..7584874bd54 100644 --- a/examples/gno.land/r/demo/social_feeds/post.gno +++ b/examples/gno.land/r/demo/social_feeds/post.gno @@ -125,18 +125,15 @@ func (post *Post) Delete() { post.deletedAt = time.Now() } -func (post *Post) Tip(from std.Address, to std.Address, amount int64) { - if amount <= 0 { - panic("amount is not valid") - } - - // banker := std.GetBanker(std.BankerTypeOrigSend) - banker := std.GetBanker(std.BankerTypeRealmSend) +func (post *Post) Tip(from std.Address, to std.Address) { + receivedCoins := std.GetOrigSend() + amount := receivedCoins[0].Amount + banker := std.GetBanker(std.BankerTypeOrigSend) + // banker := std.GetBanker(std.BankerTypeRealmSend) coinsToSend := std.Coins{std.Coin{Denom: "ugnot", Amount: amount}} - - banker := std.GetBanker(std.BankerTypeRealmSend) pkgaddr := std.GetOrigPkgAddr() + banker.SendCoins(pkgaddr, to, coinsToSend) // Update tip amount diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index 3ee5012ad51..8af2b52baed 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -14,9 +14,11 @@ import ( // Only registered user can create a new feed // For the flexibility when testing, allow all user to create feed func CreateFeed(name string) FeedID { + pkgpath := std.CurrentRealmPath() + fid := incGetFeedID() caller := std.PrevRealm().Addr() - url := "/r/demo/social_feeds_v4:" + name + url := strings.Replace(pkgpath, "gno.land", "", -1) + ":" + name feed := newFeed(fid, url, name, caller) fidkey := feedIDKey(fid) gFeeds.Set(fidkey, feed) @@ -68,18 +70,60 @@ func ReactPost(fid FeedID, pid PostID, icon string, up bool) { post.React(icon, up) } -func TipPost(fid FeedID, pid PostID, amount int64) { +func TipPost(fid FeedID, pid PostID) { caller := std.PrevRealm().Addr() feed := mustGetFeed(fid) post := feed.MustGetPost(pid) - post.Tip(caller, post.creator, amount) + post.Tip(caller, post.creator) } -// Get Posts list +// Get a list of flagged posts +// NOTE: We can support multi feeds in the future but for now we will have only 1 feed +// Return stringified list in format: postStr-count,postStr-count +func GetFlaggedPosts(fid FeedID, offset uint64, limit uint8) string { + feed := mustGetFeed(fid) + + // Already sorted by count descending + flags := feed.flags.GetFlags(uint64(limit), offset) + + var postList []string + for _, flagCount := range flags { + flagID := flagCount.FlagID + count := flagCount.Count + + feedID, postID := parseFlagID(flagID) + if feedID != feed.id { + continue + } + + post := feed.GetPost(postID) + postList = append(postList, ufmt.Sprintf("%s", post)) + } + + SEPARATOR := "," + res := strings.Join(postList, SEPARATOR) + return res +} + +// NOTE: due to bug of std.PrevRealm().Addr() return "" when query so we user this proxy function temporary +// in waiting of correct behaviour of std.PrevRealm().Addr() func GetPosts(fid FeedID, user string, categories []uint64, offset uint64, limit uint8) string { caller := std.PrevRealm().Addr() + + return GetPostsWithCaller(fid, caller.String(), user, categories, offset, limit) +} + +func GetPostsWithCaller(fid FeedID, callerAddrStr string, user string, categories []uint64, offset uint64, limit uint8) string { + // BUG: normally std.PrevRealm().Addr() should return a value instead of empty + // Fix is in progress on Gno side feed := mustGetFeed(fid) + caller := std.Address(callerAddrStr) + + // Return flagged posts + if len(categories) == 1 && categories[0] == uint64(9) { + return GetFlaggedPosts(fid, offset, limit) + } var postList []string var skipped uint64 @@ -106,6 +150,12 @@ func GetPosts(fid FeedID, user string, categories []uint64, offset uint64, limit return false } + // Filter hidden post + flagID := getFlagID(feed.id, post.id) + if feed.flags.HasFlagged(flagID, callerAddrStr) { + return false + } + if skipped < offset { skipped++ return false From 56df907b05d4c989e70254daf5679a689e82afbe Mon Sep 17 00:00:00 2001 From: hthieu1110 Date: Mon, 21 Aug 2023 09:31:27 +0700 Subject: [PATCH 33/55] chore: add daodao pkg --- .../p/demo/daodao/core_v3/dao_core.gno | 70 ++++ .../gno.land/p/demo/daodao/core_v3/gno.mod | 6 + .../daodao/interfaces_v3/dao_interfaces.gno | 172 ++++++++++ .../daodao/interfaces_v3/dao_messages.gno | 66 ++++ .../p/demo/daodao/interfaces_v3/gno.mod | 1 + .../daodao/interfaces_v3/proposal_test.gno | 59 ++++ .../p/demo/daodao/interfaces_v3/threshold.gno | 36 +++ .../p/demo/daodao/jsonutil_v2/jsonutil.gno | 180 +++++++++++ .../p/demo/daodao/jsonutil_v2/tables.gno | 216 +++++++++++++ .../dao_proposal_single.gno | 305 ++++++++++++++++++ .../p/demo/daodao/proposal_single_v4/gno.mod | 7 + .../proposal_single_v4/update_settings.gno | 114 +++++++ .../p/demo/daodao/voting_group_v5/gno.mod | 7 + .../daodao/voting_group_v5/voting_group.gno | 42 +++ .../voting_group_v5/voting_group_test.gno | 19 ++ .../r/demo/dao_registry_v5/dao_registry.gno | 177 ++++++++++ .../dao_registry_v5/dao_registry_test.gno | 54 ++++ .../gno.land/r/demo/dao_registry_v5/gno.mod | 1 + .../r/demo/dao_registry_v5/tables.gno | 216 +++++++++++++ examples/gno.land/r/demo/groups_v6/gno.mod | 1 + examples/gno.land/r/demo/groups_v6/group.gno | 115 +++++++ examples/gno.land/r/demo/groups_v6/groups.gno | 21 ++ examples/gno.land/r/demo/groups_v6/member.gno | 40 +++ .../gno.land/r/demo/groups_v6/messages.gno | 138 ++++++++ examples/gno.land/r/demo/groups_v6/misc.gno | 96 ++++++ examples/gno.land/r/demo/groups_v6/public.gno | 116 +++++++ examples/gno.land/r/demo/groups_v6/render.gno | 40 +++ examples/gno.land/r/demo/groups_v6/role.gno | 8 + 28 files changed, 2323 insertions(+) create mode 100644 examples/gno.land/p/demo/daodao/core_v3/dao_core.gno create mode 100644 examples/gno.land/p/demo/daodao/core_v3/gno.mod create mode 100644 examples/gno.land/p/demo/daodao/interfaces_v3/dao_interfaces.gno create mode 100644 examples/gno.land/p/demo/daodao/interfaces_v3/dao_messages.gno create mode 100644 examples/gno.land/p/demo/daodao/interfaces_v3/gno.mod create mode 100644 examples/gno.land/p/demo/daodao/interfaces_v3/proposal_test.gno create mode 100644 examples/gno.land/p/demo/daodao/interfaces_v3/threshold.gno create mode 100644 examples/gno.land/p/demo/daodao/jsonutil_v2/jsonutil.gno create mode 100644 examples/gno.land/p/demo/daodao/jsonutil_v2/tables.gno create mode 100644 examples/gno.land/p/demo/daodao/proposal_single_v4/dao_proposal_single.gno create mode 100644 examples/gno.land/p/demo/daodao/proposal_single_v4/gno.mod create mode 100644 examples/gno.land/p/demo/daodao/proposal_single_v4/update_settings.gno create mode 100644 examples/gno.land/p/demo/daodao/voting_group_v5/gno.mod create mode 100644 examples/gno.land/p/demo/daodao/voting_group_v5/voting_group.gno create mode 100644 examples/gno.land/p/demo/daodao/voting_group_v5/voting_group_test.gno create mode 100644 examples/gno.land/r/demo/dao_registry_v5/dao_registry.gno create mode 100644 examples/gno.land/r/demo/dao_registry_v5/dao_registry_test.gno create mode 100644 examples/gno.land/r/demo/dao_registry_v5/gno.mod create mode 100644 examples/gno.land/r/demo/dao_registry_v5/tables.gno create mode 100644 examples/gno.land/r/demo/groups_v6/gno.mod create mode 100644 examples/gno.land/r/demo/groups_v6/group.gno create mode 100644 examples/gno.land/r/demo/groups_v6/groups.gno create mode 100644 examples/gno.land/r/demo/groups_v6/member.gno create mode 100644 examples/gno.land/r/demo/groups_v6/messages.gno create mode 100644 examples/gno.land/r/demo/groups_v6/misc.gno create mode 100644 examples/gno.land/r/demo/groups_v6/public.gno create mode 100644 examples/gno.land/r/demo/groups_v6/render.gno create mode 100644 examples/gno.land/r/demo/groups_v6/role.gno diff --git a/examples/gno.land/p/demo/daodao/core_v3/dao_core.gno b/examples/gno.land/p/demo/daodao/core_v3/dao_core.gno new file mode 100644 index 00000000000..4a72bbfd7e7 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/core_v3/dao_core.gno @@ -0,0 +1,70 @@ +package core + +import ( + "std" + "strings" + + dao_interfaces "gno.land/p/demo/daodao/interfaces_v3" + "gno.land/p/demo/markdown_utils" +) + +// TODO: add wrapper message handler to handle multiple proposal modules messages + +type IDAOCore interface { + AddProposalModule(proposalMod dao_interfaces.IProposalModule) + + VotingModule() dao_interfaces.IVotingModule + ProposalModules() []dao_interfaces.IProposalModule + + Render(path string) string +} + +type daoCore struct { + IDAOCore + + votingModule dao_interfaces.IVotingModule + proposalModules []dao_interfaces.IProposalModule +} + +func NewDAOCore( + votingModule dao_interfaces.IVotingModule, + proposalModules []dao_interfaces.IProposalModule, +) IDAOCore { + return &daoCore{ + votingModule: votingModule, + proposalModules: proposalModules, + } +} + +func (d *daoCore) VotingModule() dao_interfaces.IVotingModule { + return d.votingModule +} + +func (d *daoCore) ProposalModules() []dao_interfaces.IProposalModule { + return d.proposalModules +} + +func (d *daoCore) AddProposalModule(proposalMod dao_interfaces.IProposalModule) { + d.proposalModules = append(d.proposalModules, proposalMod) +} + +func (d *daoCore) Render(path string) string { + s := "# DAO Core\n" + s += "This is a port of [DA0-DA0 contracts](https://github.com/DA0-DA0/dao-contracts)\n" + s += markdown_utils.Indent(d.votingModule.Render(path)) + "\n" + for _, propMod := range d.proposalModules { + s += markdown_utils.Indent(propMod.Render(path)) + "\n" + } + return s +} + +func GetProposalModule(core IDAOCore, moduleIndex int) dao_interfaces.IProposalModule { + if moduleIndex < 0 { + panic("Module index must be >= 0") + } + mods := core.ProposalModules() + if moduleIndex >= len(mods) { + panic("invalid module index") + } + return mods[moduleIndex] +} diff --git a/examples/gno.land/p/demo/daodao/core_v3/gno.mod b/examples/gno.land/p/demo/daodao/core_v3/gno.mod new file mode 100644 index 00000000000..810ec7e040c --- /dev/null +++ b/examples/gno.land/p/demo/daodao/core_v3/gno.mod @@ -0,0 +1,6 @@ +module gno.land/p/demo/daodao/core_v3 + +require ( + "gno.land/p/demo/daodao/interfaces_v3" v0.0.0-latest + "gno.land/p/demo/markdown_utils" v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/p/demo/daodao/interfaces_v3/dao_interfaces.gno b/examples/gno.land/p/demo/daodao/interfaces_v3/dao_interfaces.gno new file mode 100644 index 00000000000..7ffab4650a8 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces_v3/dao_interfaces.gno @@ -0,0 +1,172 @@ +package dao_interfaces + +import ( + "std" + "strconv" + + "gno.land/p/demo/avl" + "gno.land/p/demo/jsonutil_v2" +) + +type IVotingModule interface { + VotingPower(addr std.Address) uint64 + TotalPower() uint64 + Render(path string) string +} + +type Ballot struct { + Power uint64 + Vote Vote + Rationale string +} + +func (b Ballot) ToJSON() string { + return jsonutil.FormatObject([]jsonutil.KeyValue{ + {Key: "power", Value: b.Power}, + {Key: "vote", Value: b.Vote}, + {Key: "rationale", Value: b.Rationale}, + }) +} + +type Votes struct { + Yes uint64 + No uint64 + Abstain uint64 +} + +func (v *Votes) Add(vote Vote, power uint64) { + switch vote { + case VoteYes: + v.Yes += power + case VoteNo: + v.No += power + case VoteAbstain: + v.Abstain += power + default: + panic("unknown vote kind") + } +} + +func (v *Votes) Remove(vote Vote, power uint64) { + switch vote { + case VoteYes: + v.Yes -= power + case VoteNo: + v.No -= power + case VoteAbstain: + v.Abstain -= power + default: + panic("unknown vote kind") + } +} + +func (v *Votes) Total() uint64 { + return v.Yes + v.No + v.Abstain +} + +func (v Votes) ToJSON() string { + return jsonutil.FormatObject([]jsonutil.KeyValue{ + {Key: "yes", Value: v.Yes}, + {Key: "no", Value: v.No}, + {Key: "abstain", Value: v.Abstain}, + }) +} + +type Proposal struct { + ID int + Title string + Description string + Proposer std.Address + Messages []ExecutableMessage + Ballots *avl.Tree // dev + // Ballots *avl.MutTree // test3 + Votes Votes + Status ProposalStatus +} + +var _ jsonutil.JSONAble = (*Proposal)(nil) + +func (p Proposal) ToJSON() string { + return jsonutil.FormatObject([]jsonutil.KeyValue{ + {Key: "id", Value: p.ID}, + {Key: "title", Value: p.Title}, + {Key: "description", Value: p.Description}, + {Key: "proposer", Value: p.Proposer}, + {Key: "messages", Value: jsonutil.FormatSlice(p.Messages), Raw: true}, + {Key: "ballots", Value: p.Ballots}, + {Key: "votes", Value: p.Votes}, + {Key: "status", Value: p.Status}, + }) +} + +type ProposalStatus int + +const ( + ProposalStatusOpen ProposalStatus = iota + ProposalStatusPassed + ProposalStatusExecuted +) + +func (p ProposalStatus) ToJSON() string { + return jsonutil.FormatString(p.String()) +} + +func (p ProposalStatus) String() string { + switch p { + case ProposalStatusOpen: + return "Open" + case ProposalStatusPassed: + return "Passed" + case ProposalStatusExecuted: + return "Executed" + default: + return "Unknown(" + strconv.Itoa(int(p)) + ")" + } +} + +type Vote int + +const ( + VoteYes Vote = iota + VoteNo + VoteAbstain +) + +func (v Vote) ToJSON() string { + return jsonutil.FormatString(v.String()) +} + +func (v Vote) String() string { + switch v { + case VoteYes: + return "Yes" + case VoteNo: + return "No" + case VoteAbstain: + return "Abstain" + default: + return "Unknown(" + strconv.Itoa(int(v)) + ")" + } +} + +type IProposalModule interface { + Propose( + title string, + description string, + actions []ExecutableMessage, + ) + Vote(proposalId int, vote Vote, rationale string) + Execute(proposalId int) + Threshold() Threshold + + Proposals() []Proposal + GetBallot(proposalId int, addr std.Address) Ballot + + Render(path string) string +} + +type ExecutableMessage interface { + String() string + Binary() []byte + Type() string +} diff --git a/examples/gno.land/p/demo/daodao/interfaces_v3/dao_messages.gno b/examples/gno.land/p/demo/daodao/interfaces_v3/dao_messages.gno new file mode 100644 index 00000000000..8ab887157dd --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces_v3/dao_messages.gno @@ -0,0 +1,66 @@ +package dao_interfaces + +import ( + "encoding/base64" + "encoding/binary" + "strings" + + "gno.land/p/demo/avl" +) + +type MessageHandler interface { + Execute(message ExecutableMessage) + FromBinary(b []byte) ExecutableMessage + Type() string +} + +type MessagesRegistry struct { + handlers *avl.Tree +} + +func NewMessagesRegistry() *MessagesRegistry { + return &MessagesRegistry{handlers: avl.NewTree()} +} + +func (r *MessagesRegistry) Register(handler MessageHandler) { + r.handlers.Set(handler.Type(), handler) +} + +func (r *MessagesRegistry) FromBinary(b []byte) ExecutableMessage { + if len(b) < 2 { + panic("invalid ExecutableMessage: invalid length") + } + l := binary.BigEndian.Uint16(b[:2]) + if len(b) < int(l+2) { + panic("invalid ExecutableMessage: invalid length") + } + t := string(b[2 : l+2]) + + h, ok := r.handlers.Get(t) + if !ok { + panic("invalid ExecutableMessage: invalid message type") + } + return h.(MessageHandler).FromBinary(b) +} + +func (r *MessagesRegistry) FromBase64String(s string) ExecutableMessage { + b, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + panic("invalid ExecutableMessage: invalid base64 string") + } + return r.FromBinary(b) +} + +func (r *MessagesRegistry) Execute(msg ExecutableMessage) { + h, ok := r.handlers.Get(msg.Type()) + if !ok { + panic("invalid ExecutableMessage: invalid message type") + } + return h.(MessageHandler).Execute(msg) +} + +func (r *MessagesRegistry) ExecuteMessages(msgs []ExecutableMessage) { + for _, msg := range msgs { + r.Execute(msg) + } +} diff --git a/examples/gno.land/p/demo/daodao/interfaces_v3/gno.mod b/examples/gno.land/p/demo/daodao/interfaces_v3/gno.mod new file mode 100644 index 00000000000..1d1fcc9450e --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces_v3/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/daodao/interfaces_v3 diff --git a/examples/gno.land/p/demo/daodao/interfaces_v3/proposal_test.gno b/examples/gno.land/p/demo/daodao/interfaces_v3/proposal_test.gno new file mode 100644 index 00000000000..efc31fa1a08 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces_v3/proposal_test.gno @@ -0,0 +1,59 @@ +package dao_interfaces + +import ( + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/jsonutil_v2" +) + +type NoopMessage struct{} + +var _ ExecutableMessage = (*NoopMessage)(nil) + +func (m NoopMessage) String() string { + return "noop" +} + +func (m NoopMessage) Binary() []byte { + return nil +} + +func (m NoopMessage) Type() string { + return "noop-type" +} + +func (m NoopMessage) ToJSON() string { + return jsonutil.FormatString(m.String()) +} + +func TestProposalJSON(t *testing.T) { + props := []Proposal{ + { + ID: 0, + Title: "Prop #0", + Description: "Wolol0\n\t\r", + Proposer: "0x1234567890", + Votes: Votes{ + Yes: 7, + No: 21, + Abstain: 42, + }, + Ballots: avl.NewTree(), + }, + { + ID: 1, + Title: "Prop #1", + Description: `Wolol1\"`, + Proposer: "0x1234567890", + Status: ProposalStatusExecuted, + Messages: []ExecutableMessage{NoopMessage{}, NoopMessage{}, NoopMessage{}}, + }, + } + props[0].Ballots.Set("0x1234567890", Ballot{Power: 1, Vote: VoteYes, Rationale: "test"}) + str := jsonutil.FormatSlice(props) + expected := `[{"id":0,"title":"Prop #0","description":"Wolol0\n\t\r","proposer":"0x1234567890","messages":[],"ballots":{"0x1234567890":{"power":1,"vote":"Yes","rationale":"test"}},"votes":{"yes":7,"no":21,"abstain":42},"status":"Open"},{"id":1,"title":"Prop #1","description":"Wolol1\\\"","proposer":"0x1234567890","messages":["noop","noop","noop"],"ballots":{},"votes":{"yes":0,"no":0,"abstain":0},"status":"Executed"}]` + if expected != str { + t.Fatalf("JSON does not match, expected %s, got %s", expected, str) + } +} diff --git a/examples/gno.land/p/demo/daodao/interfaces_v3/threshold.gno b/examples/gno.land/p/demo/daodao/interfaces_v3/threshold.gno new file mode 100644 index 00000000000..7b34ba7b4d2 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces_v3/threshold.gno @@ -0,0 +1,36 @@ +package dao_interfaces + +import ( + "strconv" +) + +type Percent uint16 // 4 decimals fixed point + +type PercentageThreshold struct { + Percent *Percent +} + +func (p *PercentageThreshold) String() string { + if p == nil || p.Percent == nil { + return "nil" + } + return p.Percent.String() +} + +type ThresholdQuorum struct { + Threshold PercentageThreshold + Quorum PercentageThreshold +} + +type Threshold struct { + ThresholdQuorum *ThresholdQuorum +} + +func (p Percent) String() string { + s := strconv.FormatUint(uint64(p)/100, 10) + decPart := uint64(p) % 100 + if decPart != 0 { + s += "." + strconv.FormatUint(decPart, 10) + } + return s + "%" +} diff --git a/examples/gno.land/p/demo/daodao/jsonutil_v2/jsonutil.gno b/examples/gno.land/p/demo/daodao/jsonutil_v2/jsonutil.gno new file mode 100644 index 00000000000..51fdc369b0e --- /dev/null +++ b/examples/gno.land/p/demo/daodao/jsonutil_v2/jsonutil.gno @@ -0,0 +1,180 @@ +package jsonutil + +// This package strives to have the same behavior as json.Marshal but has no support for nested slices and returns strings + +import ( + "std" + "strconv" + "strings" + "unicode/utf8" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ufmt" +) + +type JSONAble interface { + ToJSON() string +} + +type KeyValue struct { + Key string + Value interface{} + Raw bool +} + +// does not work for slices, use FormatSlice instead +func FormatAny(p interface{}) string { + switch p.(type) { + case std.Address: + return FormatString(string(p.(std.Address))) + case *avl.Tree: + return FormatAVLTree(p.(*avl.Tree)) + case avl.Tree: + return FormatAVLTree(&p.(avl.Tree)) + case JSONAble: + return p.(JSONAble).ToJSON() + case string: + return FormatString(p.(string)) + case uint64: + return FormatUint64(p.(uint64)) + case uint32: + return FormatUint64(uint64(p.(uint32))) + case uint: + return FormatUint64(uint64(p.(uint))) + case int64: + return FormatInt64(p.(int64)) + case int32: + return FormatInt64(int64(p.(int32))) + case int: + return FormatInt64(int64(p.(int))) + case bool: + return FormatBool(p.(bool)) + default: + return "null" + } +} + +// Ported from https://cs.opensource.google/go/go/+/refs/tags/go1.20.6:src/encoding/json/encode.go +func FormatString(s string) string { + const escapeHTML = true + e := `"` // e.WriteByte('"') + start := 0 + for i := 0; i < len(s); { + if b := s[i]; b < utf8.RuneSelf { + if htmlSafeSet[b] || (!escapeHTML && safeSet[b]) { + i++ + continue + } + if start < i { + e += s[start:i] // e.WriteString(s[start:i]) + } + e += "\\" // e.WriteByte('\\') + switch b { + case '\\', '"': + e += string(b) // e.WriteByte(b) + case '\n': + e += "n" // e.WriteByte('n') + case '\r': + e += "r" // e.WriteByte('r') + case '\t': + e += "t" // e.WriteByte('t') + default: + // This encodes bytes < 0x20 except for \t, \n and \r. + // If escapeHTML is set, it also escapes <, >, and & + // because they can lead to security holes when + // user-controlled strings are rendered into JSON + // and served to some browsers. + e += `u00` // e.WriteString(`u00`) + e += string(hex[b>>4]) // e.WriteByte(hex[b>>4]) + e += string(hex[b&0xF]) // e.WriteByte(hex[b&0xF]) + } + i++ + start = i + continue + } + c, size := utf8.DecodeRuneInString(s[i:]) + if c == utf8.RuneError && size == 1 { + if start < i { + e += s[start:i] // e.WriteString(s[start:i]) + } + e += `\ufffd` // e.WriteString(`\ufffd`) + i += size + start = i + continue + } + // U+2028 is LINE SEPARATOR. + // U+2029 is PARAGRAPH SEPARATOR. + // They are both technically valid characters in JSON strings, + // but don't work in JSONP, which has to be evaluated as JavaScript, + // and can lead to security holes there. It is valid JSON to + // escape them, so we do so unconditionally. + // See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion. + if c == '\u2028' || c == '\u2029' { + if start < i { + e += s[start:i] // e.WriteString(s[start:i]) + } + e += `\u202` // e.WriteString(`\u202`) + e += string(hex[c&0xF]) // e.WriteByte(hex[c&0xF]) + i += size + start = i + continue + } + i += size + } + if start < len(s) { + e += s[start:] // e.WriteString(s[start:]) + } + e += `"` // e.WriteByte('"') + return e +} + +func FormatUint64(i uint64) string { + return strconv.FormatUint(i, 10) +} + +func FormatInt64(i int64) string { + return strconv.FormatInt(i, 10) +} + +func FormatSlice(s []interface{}) string { + elems := make([]string, len(s)) + for i, elem := range s { + elems[i] = FormatAny(elem) + } + return "[" + strings.Join(elems, ",") + "]" +} + +func FormatObject(kv []KeyValue) string { + elems := make([]string, len(kv)) + i := 0 + for _, elem := range kv { + var val string + if elem.Raw { + val = elem.Value.(string) + } else { + val = FormatAny(elem.Value) + } + elems[i] = FormatString(elem.Key) + ":" + val + i++ + } + return "{" + strings.Join(elems, ",") + "}" +} + +func FormatBool(b bool) string { + if b { + return "true" + } + return "false" +} + +func FormatAVLTree(t *avl.Tree) string { + if t == nil { + return "{}" + } + kv := make([]KeyValue, 0, t.Size()) + t.Iterate("", "", func(key string, value interface{}) bool { + kv = append(kv, KeyValue{key, value, false}) + return false + }) + return FormatObject(kv) +} diff --git a/examples/gno.land/p/demo/daodao/jsonutil_v2/tables.gno b/examples/gno.land/p/demo/daodao/jsonutil_v2/tables.gno new file mode 100644 index 00000000000..e761c1faa2f --- /dev/null +++ b/examples/gno.land/p/demo/daodao/jsonutil_v2/tables.gno @@ -0,0 +1,216 @@ +package jsonutil + +import "unicode/utf8" + +var hex = "0123456789abcdef" + +// safeSet holds the value true if the ASCII character with the given array +// position can be represented inside a JSON string without any further +// escaping. +// +// All values are true except for the ASCII control characters (0-31), the +// double quote ("), and the backslash character ("\"). +var safeSet = [utf8.RuneSelf]bool{ + ' ': true, + '!': true, + '"': false, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '(': true, + ')': true, + '*': true, + '+': true, + ',': true, + '-': true, + '.': true, + '/': true, + '0': true, + '1': true, + '2': true, + '3': true, + '4': true, + '5': true, + '6': true, + '7': true, + '8': true, + '9': true, + ':': true, + ';': true, + '<': true, + '=': true, + '>': true, + '?': true, + '@': true, + 'A': true, + 'B': true, + 'C': true, + 'D': true, + 'E': true, + 'F': true, + 'G': true, + 'H': true, + 'I': true, + 'J': true, + 'K': true, + 'L': true, + 'M': true, + 'N': true, + 'O': true, + 'P': true, + 'Q': true, + 'R': true, + 'S': true, + 'T': true, + 'U': true, + 'V': true, + 'W': true, + 'X': true, + 'Y': true, + 'Z': true, + '[': true, + '\\': false, + ']': true, + '^': true, + '_': true, + '`': true, + 'a': true, + 'b': true, + 'c': true, + 'd': true, + 'e': true, + 'f': true, + 'g': true, + 'h': true, + 'i': true, + 'j': true, + 'k': true, + 'l': true, + 'm': true, + 'n': true, + 'o': true, + 'p': true, + 'q': true, + 'r': true, + 's': true, + 't': true, + 'u': true, + 'v': true, + 'w': true, + 'x': true, + 'y': true, + 'z': true, + '{': true, + '|': true, + '}': true, + '~': true, + '\u007f': true, +} + +// htmlSafeSet holds the value true if the ASCII character with the given +// array position can be safely represented inside a JSON string, embedded +// inside of HTML