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}