github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/examples/gno.land/p/demo/memeland/memeland.gno (about) 1 package memeland 2 3 import ( 4 "sort" 5 "std" 6 "strconv" 7 "strings" 8 "time" 9 10 "gno.land/p/demo/avl" 11 "gno.land/p/demo/ownable" 12 "gno.land/p/demo/seqid" 13 ) 14 15 const ( 16 DATE_CREATED = "DATE_CREATED" 17 UPVOTES = "UPVOTES" 18 ) 19 20 type Post struct { 21 ID string 22 Data string 23 Author std.Address 24 Timestamp time.Time 25 UpvoteTracker *avl.Tree // address > struct{}{} 26 } 27 28 type Memeland struct { 29 *ownable.Ownable 30 Posts []*Post 31 MemeCounter seqid.ID 32 } 33 34 func NewMemeland() *Memeland { 35 return &Memeland{ 36 Ownable: ownable.New(), 37 Posts: make([]*Post, 0), 38 } 39 } 40 41 // PostMeme - Adds a new post 42 func (m *Memeland) PostMeme(data string, timestamp int64) string { 43 if data == "" || timestamp <= 0 { 44 panic("timestamp or data cannot be empty") 45 } 46 47 // Generate ID 48 id := m.MemeCounter.Next().String() 49 50 newPost := &Post{ 51 ID: id, 52 Data: data, 53 Author: std.PrevRealm().Addr(), 54 Timestamp: time.Unix(timestamp, 0), 55 UpvoteTracker: avl.NewTree(), 56 } 57 58 m.Posts = append(m.Posts, newPost) 59 return id 60 } 61 62 func (m *Memeland) Upvote(id string) string { 63 post := m.getPost(id) 64 if post == nil { 65 panic("post with specified ID does not exist") 66 } 67 68 caller := std.PrevRealm().Addr().String() 69 70 if _, exists := post.UpvoteTracker.Get(caller); exists { 71 panic("user has already upvoted this post") 72 } 73 74 post.UpvoteTracker.Set(caller, struct{}{}) 75 76 return "upvote successful" 77 } 78 79 // GetPostsInRange returns a JSON string of posts within the given timestamp range, supporting pagination 80 func (m *Memeland) GetPostsInRange(startTimestamp, endTimestamp int64, page, pageSize int, sortBy string) string { 81 if len(m.Posts) == 0 { 82 return "[]" 83 } 84 85 if page < 1 { 86 panic("page number cannot be less than 1") 87 } 88 89 // No empty pages 90 if pageSize < 1 { 91 panic("page size cannot be less than 1") 92 } 93 94 // No pages larger than 10 95 if pageSize > 10 { 96 panic("page size cannot be larger than 10") 97 } 98 99 // Need to pass in a sort parameter 100 if sortBy == "" { 101 panic("sort order cannot be empty") 102 } 103 104 var filteredPosts []*Post 105 106 start := time.Unix(startTimestamp, 0) 107 end := time.Unix(endTimestamp, 0) 108 109 // Filtering posts 110 for _, p := range m.Posts { 111 if !p.Timestamp.Before(start) && !p.Timestamp.After(end) { 112 filteredPosts = append(filteredPosts, p) 113 } 114 } 115 116 switch sortBy { 117 // Sort by upvote descending 118 case UPVOTES: 119 dateSorter := PostSorter{ 120 Posts: filteredPosts, 121 LessF: func(i, j int) bool { 122 return filteredPosts[i].UpvoteTracker.Size() > filteredPosts[j].UpvoteTracker.Size() 123 }, 124 } 125 sort.Sort(dateSorter) 126 case DATE_CREATED: 127 // Sort by timestamp, beginning with newest 128 dateSorter := PostSorter{ 129 Posts: filteredPosts, 130 LessF: func(i, j int) bool { 131 return filteredPosts[i].Timestamp.After(filteredPosts[j].Timestamp) 132 }, 133 } 134 sort.Sort(dateSorter) 135 default: 136 panic("sort order can only be \"UPVOTES\" or \"DATE_CREATED\"") 137 } 138 139 // Pagination 140 startIndex := (page - 1) * pageSize 141 endIndex := startIndex + pageSize 142 143 // If page does not contain any posts 144 if startIndex >= len(filteredPosts) { 145 return "[]" 146 } 147 148 // If page contains fewer posts than the page size 149 if endIndex > len(filteredPosts) { 150 endIndex = len(filteredPosts) 151 } 152 153 // Return JSON representation of paginated and sorted posts 154 return PostsToJSONString(filteredPosts[startIndex:endIndex]) 155 } 156 157 // RemovePost allows the owner to remove a post with a specific ID 158 func (m *Memeland) RemovePost(id string) string { 159 if id == "" { 160 panic("id cannot be empty") 161 } 162 163 if err := m.CallerIsOwner(); err != nil { 164 panic(err) 165 } 166 167 for i, post := range m.Posts { 168 if post.ID == id { 169 m.Posts = append(m.Posts[:i], m.Posts[i+1:]...) 170 return id 171 } 172 } 173 174 panic("post with specified id does not exist") 175 } 176 177 // PostsToJSONString converts a slice of Post structs into a JSON string 178 func PostsToJSONString(posts []*Post) string { 179 var sb strings.Builder 180 sb.WriteString("[") 181 182 for i, post := range posts { 183 if i > 0 { 184 sb.WriteString(",") 185 } 186 187 sb.WriteString(PostToJSONString(post)) 188 } 189 sb.WriteString("]") 190 191 return sb.String() 192 } 193 194 // PostToJSONString returns a Post formatted as a JSON string 195 func PostToJSONString(post *Post) string { 196 var sb strings.Builder 197 198 sb.WriteString("{") 199 sb.WriteString(`"id":"` + post.ID + `",`) 200 sb.WriteString(`"data":"` + escapeString(post.Data) + `",`) 201 sb.WriteString(`"author":"` + escapeString(post.Author.String()) + `",`) 202 sb.WriteString(`"timestamp":"` + strconv.Itoa(int(post.Timestamp.Unix())) + `",`) 203 sb.WriteString(`"upvotes":` + strconv.Itoa(post.UpvoteTracker.Size())) 204 sb.WriteString("}") 205 206 return sb.String() 207 } 208 209 // escapeString escapes quotes in a string for JSON compatibility. 210 func escapeString(s string) string { 211 return strings.ReplaceAll(s, `"`, `\"`) 212 } 213 214 func (m *Memeland) getPost(id string) *Post { 215 for _, p := range m.Posts { 216 if p.ID == id { 217 return p 218 } 219 } 220 221 return nil 222 } 223 224 // PostSorter is a flexible sorter for the *Post slice 225 type PostSorter struct { 226 Posts []*Post 227 LessF func(i, j int) bool 228 } 229 230 func (p PostSorter) Len() int { 231 return len(p.Posts) 232 } 233 234 func (p PostSorter) Swap(i, j int) { 235 p.Posts[i], p.Posts[j] = p.Posts[j], p.Posts[i] 236 } 237 238 func (p PostSorter) Less(i, j int) bool { 239 return p.LessF(i, j) 240 }