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  }