diff --git a/examples/gno.land/r/demo/teritori/social_follow/CMD.md b/examples/gno.land/r/demo/teritori/social_follow/CMD.md new file mode 100644 index 00000000000..02e98e6f3fe --- /dev/null +++ b/examples/gno.land/r/demo/teritori/social_follow/CMD.md @@ -0,0 +1,56 @@ +gnokey maketx addpkg \ + -deposit="1ugnot" \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="51.15.236.215:26657" \ + -chainid="teritori-1" \ + -pkgdir="." \ + -pkgpath="gno.land/r/demo/social_follow_2" \ + mykey2 + +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="51.15.236.215:26657" \ + -chainid="teritori-1" \ + -pkgpath="gno.land/r/demo/social_follow_2" \ + -func="Follow" \ + -args="g1c5y8jpe585uezcvlmgdjmk5jt2glfw88wxa3xq" \ + mykey3 + +gnokey query "vm/qeval" -data='gno.land/r/demo/social_follow_2 +Followers("g1c5y8jpe585uezcvlmgdjmk5jt2glfw88wxa3xq",0,1)' -remote="51.15.236.215:26657" + +gnokey query "vm/qeval" -data='gno.land/r/demo/social_follow_2 +FollowersCount("g1c5y8jpe585uezcvlmgdjmk5jt2glfw88wxa3xq")' -remote="51.15.236.215:26657" + +gnokey query "vm/qeval" -data='gno.land/r/demo/social_follow_2 +Followed("g1j3ylca07vlhklzftrznw7jyquzqf2wtxvjdm4r",0,1)' -remote="51.15.236.215:26657" + +gnokey query "vm/qeval" -data='gno.land/r/demo/social_follow_2 +FollowedCount("g1j3ylca07vlhklzftrznw7jyquzqf2wtxvjdm4r")' -remote="51.15.236.215:26657" + +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="51.15.236.215:26657" \ + -chainid="teritori-1" \ + -pkgpath="gno.land/r/demo/social_follow_2" \ + -func="Unfollow" \ + -args="g1c5y8jpe585uezcvlmgdjmk5jt2glfw88wxa3xq" \ + mykey3 + +gnokey query "vm/qeval" -data='gno.land/r/demo/social_follow_2 +Followers("g1c5y8jpe585uezcvlmgdjmk5jt2glfw88wxa3xq",0,1)' -remote="51.15.236.215:26657" + +gnokey query "vm/qeval" -data='gno.land/r/demo/social_follow_2 +FollowersCount("g1c5y8jpe585uezcvlmgdjmk5jt2glfw88wxa3xq")' -remote="51.15.236.215:26657" + +gnokey query "vm/qeval" -data='gno.land/r/demo/social_follow_2 +Followed("g1j3ylca07vlhklzftrznw7jyquzqf2wtxvjdm4r",0,1)' -remote="51.15.236.215:26657" + +gnokey query "vm/qeval" -data='gno.land/r/demo/social_follow_2 +FollowedCount("g1j3ylca07vlhklzftrznw7jyquzqf2wtxvjdm4r")' -remote="51.15.236.215:26657" \ No newline at end of file diff --git a/examples/gno.land/r/demo/teritori/social_follow/follow.gno b/examples/gno.land/r/demo/teritori/social_follow/follow.gno new file mode 100644 index 00000000000..5544cd7b9ce --- /dev/null +++ b/examples/gno.land/r/demo/teritori/social_follow/follow.gno @@ -0,0 +1,83 @@ +package social_follow + +import ( + "std" + + "gno.land/p/demo/avl" +) + +var addr2User avl.Tree // std.Address -> *User + +func getOrCreateUser(addr std.Address) *User { + userI, ok := addr2User.Get(addr.String()) + if ok { + return userI.(*User) + } + user := &User{ + address: addr, + } + addr2User.Set(addr.String(), user) + return user +} + +func Follow(addr std.Address) { + caller := std.PrevRealm().Addr() + callerUser := getOrCreateUser(caller) + user := getOrCreateUser(addr) + callerUser.Follow(user) +} + +func Unfollow(addr std.Address) { + caller := std.PrevRealm().Addr() + callerUser := getOrCreateUser(caller) + user := getOrCreateUser(addr) + callerUser.Unfollow(user) +} + +func Followers(addr std.Address, page, pageSize int) []std.Address { + userI, ok := addr2User.Get(addr.String()) + if !ok { + return nil + } + user := userI.(*User) + return user.Followers(page, pageSize) +} + +func FollowedCount(addr std.Address) uint { + userI, ok := addr2User.Get(addr.String()) + if !ok { + return 0 + } + user := userI.(*User) + return uint(user.followeds.Size()) +} + +func Followed(addr std.Address, page, pageSize int) []std.Address { + userI, ok := addr2User.Get(addr.String()) + if !ok { + return nil + } + + user := userI.(*User) + + return user.Followed(page, pageSize) +} + +func FollowersCount(addr std.Address) uint { + userI, ok := addr2User.Get(addr.String()) + if !ok { + return 0 + } + user := userI.(*User) + return uint(user.followers.Size()) +} + +func IsFollower(follower std.Address, followed std.Address) bool { + userI, ok := addr2User.Get(followed.String()) + if !ok { + return false + } + user := userI.(*User) + _, ok = user.followers.Get(follower.String()) + return ok +} diff --git a/examples/gno.land/r/demo/teritori/social_follow/follow_test.gno b/examples/gno.land/r/demo/teritori/social_follow/follow_test.gno new file mode 100644 index 00000000000..bdef40a8580 --- /dev/null +++ b/examples/gno.land/r/demo/teritori/social_follow/follow_test.gno @@ -0,0 +1,106 @@ +package social_follow + +import ( + "std" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/testutils" +) + +func TestFollow_Follow(t *testing.T) { + addr2User = avl.Tree{} + main := testutils.TestAddress("main") + std.TestSetOrigCaller(main) + + user := testutils.TestAddress("user") + + Follow(user) + + if c := FollowersCount(user); c != 1 { + t.Fatalf("FollowersCount expected to have 1 has %d", c) + } + + if followers := Followers(user, 0, 1); followers[0].String() != string(main) { + t.Fatalf("Followers expected to have %s has %s", string(main), followers[0].String()) + } + + if followed := Followed(main, 0, 1); followed[0].String() != string(user) { + t.Fatalf("Followed expected to have %s has %s", string(user), followed[0].String()) + } +} + +func TestFollow_UnFollow(t *testing.T) { + addr2User = avl.Tree{} + + user1 := testutils.TestAddress("user1") + user2 := testutils.TestAddress("user2") + std.TestSetOrigCaller(user1) + + Follow(user2) + if IsFollower(user1, user2) != true { + t.Fatalf("expected to be a follower") + } + + Unfollow(user2) + if IsFollower(user1, user2) != false { + t.Fatalf("expected to not be a follower") + } +} + +func TestFollow_FollowedPagination(t *testing.T) { + addr2User = avl.Tree{} + + main := testutils.TestAddress("main") + std.TestSetOrigCaller(main) + for i := 0; i < 10; i++ { + user := testutils.TestAddress("user" + string(i)) + Follow(user) + } + + for i := 0; i < 10; i++ { + if followed := Followed(main, i, 1); len(followed) != 1 { + t.Fatalf("at page %d expected to have 1 has %d", i, len(followed)) + } + } + + if followed := Followed(main, 10, 1); len(followed) != 0 { + t.Fatalf("at page 10 expected to have 0 has %d", len(followed)) + } + + if followed := Followed(main, 0, 10); len(followed) != 10 { + t.Fatalf("at page 0 expected to have 10 has %d", len(followed)) + } + + if followed := Followed(main, 1, 8); len(followed) != 2 { + t.Fatalf("at page 2 expected to have 2 has %d", len(followed)) + } +} + +func TestFollow_FollowersPagination(t *testing.T) { + main := testutils.TestAddress("main") + + for i := 0; i < 10; i++ { + user := testutils.TestAddress("user" + string(i)) + std.TestSetOrigCaller(user) + Follow(main) + } + + for i := 0; i < 10; i++ { + if followers := Followers(main, i, 1); len(followers) != 1 { + t.Fatalf("at page %d expected to have 1 has %d", i, len(followers)) + } + } + + if followers := Followers(main, 10, 1); len(followers) != 0 { + t.Fatalf("at page 10 expected to have 0 has %d", len(followers)) + } + + if followers := Followers(main, 0, 10); len(followers) != 10 { + t.Fatalf("at page 0 expected to have 10 has %d", len(followers)) + } + + if followers := Followers(main, 1, 8); len(followers) != 2 { + t.Fatalf("at page 2 expected to have 2 has %d", len(followers)) + } +} diff --git a/examples/gno.land/r/demo/teritori/social_follow/gno.mod b/examples/gno.land/r/demo/teritori/social_follow/gno.mod new file mode 100644 index 00000000000..abc0c720f8e --- /dev/null +++ b/examples/gno.land/r/demo/teritori/social_follow/gno.mod @@ -0,0 +1,6 @@ +module gno.land/r/demo/teritori/social_follow + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/r/demo/teritori/social_follow/spec.md b/examples/gno.land/r/demo/teritori/social_follow/spec.md new file mode 100644 index 00000000000..99ca8b0da7c --- /dev/null +++ b/examples/gno.land/r/demo/teritori/social_follow/spec.md @@ -0,0 +1,46 @@ +# Social Follow System + +## Overview +The Social Network System is designed to facilitate user interactions within a social network. It allows users to follow and unfollow each other, as well as retrieve information about followers and followed users. + +## Modules + +### 1. User Management +- This module manages user information within the social follow. +- Each user is represented by a `User` struct containing their address and lists of followers and followed users. + +### 2. Following Functionality +- Users can follow and unfollow other users. +- When a user follows another user, they are added to the followed user's list of followers, and the followed user is added to the user's list of followed users. +- When a user unfollows another user, they are removed from the followed user's list of followers, and the followed user is removed from the user's list of followed users. + +## Data Structures + +### User +```go +type User struct { + address std.Address + followers *avl.Tree // std.Address -> *User + followeds *avl.Tree // std.Address -> *User +} +``` + +## Functions + +### User Management +- `Followers(page, pageSize int) []std.Address `: Returns a list of addresses of users following the user. +- `Followed(page, pageSize int) []std.Address `: Returns a list of addresses of users whom the user is following. +- `FollowedCount(addr std.Address) uint`: Returns the number of users being followed by the user with the given address. +- `FollowersCount(addr std.Address) uint`: Returns the number of users following the user with the given address. + +### Following Functionality +- `Follow(user *User)`: Adds the given user to the list of users being followed by the user. +- `Unfollow(user *User)`: Removes the given user from the list of users being followed by the user. + +## Realm Configuration Process +- Users are managed within the social network system using the provided functionality. +- The system utilizes an AVL tree data structure to efficiently store and retrieve user information. + +## Usage +- Users interact with the system by following or unfollowing other users. +- User information is maintained and updated dynamically as users follow and unfollow each other. diff --git a/examples/gno.land/r/demo/teritori/social_follow/user.gno b/examples/gno.land/r/demo/teritori/social_follow/user.gno new file mode 100644 index 00000000000..44bd73a19e6 --- /dev/null +++ b/examples/gno.land/r/demo/teritori/social_follow/user.gno @@ -0,0 +1,61 @@ +package social_follow + +import ( + "std" + + "gno.land/p/demo/avl" +) + +type User struct { + address std.Address + followers avl.Tree // std.Address -> *User + followeds avl.Tree // std.Address -> *User +} + +func (u *User) Address() std.Address { + return u.address +} + +func (u *User) Followers(page, pageSize int) []std.Address { + followers := make([]std.Address, 0, u.followers.Size()) + u.followers.IterateByOffset(page*pageSize, pageSize, func(key string, value interface{}) bool { + follower := value.(*User) + followers = append(followers, follower.address) + return false + }) + return followers +} + +func (u *User) Followed(page, pageSize int) []std.Address { + followeds := make([]std.Address, 0, u.followeds.Size()) + u.followeds.IterateByOffset(page*pageSize, pageSize, func(key string, value interface{}) bool { + followed := value.(*User) + followeds = append(followeds, followed.address) + return false + }) + return followeds +} + +func (u *User) Follow(user *User) { + if u.address == user.address { + panic("can't follow self") + } + if _, ok := u.followeds.Get(user.address.String()); ok { + panic("already follow") + } + u.followeds.Set(user.address.String(), user) + user.followers.Set(u.address.String(), u) +} + +func (u *User) Unfollow(user *User) { + if _, ok := u.followeds.Get(user.address.String()); !ok { + panic("not follow") + } + if _, ok := u.followeds.Remove(user.address.String()); !ok { + panic("can't remove on followed") + } + + if _, ok := user.followers.Remove(u.address.String()); !ok { + panic("can't remove on follower") + } +}