memeland.gno
5.09 Kb ยท 240 lines
1package memeland
2
3import (
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
15const (
16 DATE_CREATED = "DATE_CREATED"
17 UPVOTES = "UPVOTES"
18)
19
20type Post struct {
21 ID string
22 Data string
23 Author std.Address
24 Timestamp time.Time
25 UpvoteTracker *avl.Tree // address > struct{}{}
26}
27
28type Memeland struct {
29 *ownable.Ownable
30 Posts []*Post
31 MemeCounter seqid.ID
32}
33
34func NewMemeland() *Memeland {
35 return &Memeland{
36 Ownable: ownable.New(),
37 Posts: make([]*Post, 0),
38 }
39}
40
41// PostMeme - Adds a new post
42func (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.CurrentRealm().Address(),
54 Timestamp: time.Unix(timestamp, 0),
55 UpvoteTracker: avl.NewTree(),
56 }
57
58 m.Posts = append(m.Posts, newPost)
59 return id
60}
61
62func (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.CurrentRealm().Address().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
80func (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
158func (m *Memeland) RemovePost(id string) string {
159 if id == "" {
160 panic("id cannot be empty")
161 }
162
163 if !m.OwnedByCurrent() {
164 panic(ownable.ErrUnauthorized)
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
178func 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
195func 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.
210func escapeString(s string) string {
211 return strings.ReplaceAll(s, `"`, `\"`)
212}
213
214func (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
225type PostSorter struct {
226 Posts []*Post
227 LessF func(i, j int) bool
228}
229
230func (p PostSorter) Len() int {
231 return len(p.Posts)
232}
233
234func (p PostSorter) Swap(i, j int) {
235 p.Posts[i], p.Posts[j] = p.Posts[j], p.Posts[i]
236}
237
238func (p PostSorter) Less(i, j int) bool {
239 return p.LessF(i, j)
240}