posts.gno

3.51 Kb ยท 141 lines
  1package minisocial
  2
  3import (
  4	"errors"
  5	"std"
  6	"strconv"
  7	"time"
  8
  9	"gno.land/p/demo/avl"
 10	"gno.land/p/demo/avl/pager"
 11	"gno.land/p/demo/seqid"
 12	"gno.land/p/demo/ufmt"
 13	"gno.land/p/moul/md"
 14
 15	"gno.land/r/sys/users"
 16)
 17
 18var (
 19	postID seqid.ID                         // counter for post IDs
 20	posts  = avl.NewTree()                  // seqid.ID.String() > *Post
 21	pag    = pager.NewPager(posts, 5, true) // To help with pagination in rendering
 22
 23	// Errors
 24	ErrEmptyPost           = errors.New("empty post text")
 25	ErrPostNotFound        = errors.New("post not found")
 26	ErrUpdateWindowExpired = errors.New("update window expired")
 27	ErrUnauthorized        = errors.New("you're not authorized to update this post")
 28)
 29
 30// CreatePost creates a new post
 31func CreatePost(text string) error {
 32	if text == "" {
 33		return ErrEmptyPost
 34	}
 35
 36	// Get the next ID
 37	// seqid.IDs are sequentially stored in the AVL tree
 38	// This provides chronological order when iterating
 39	id := postID.Next()
 40
 41	// Set the key:value pair into the AVL tree:
 42	// avl.Tree.Set takes a string for a key, and anything as a value.
 43	// Stringify the key, and set the pointer to a new Post struct
 44	posts.Set(id.String(), &Post{
 45		id:        id,                            // Set the ID, used later for editing or deletion
 46		text:      text,                          // Set the input text
 47		author:    std.PreviousRealm().Address(), // The author of the address is the previous realm, the realm that called this one
 48		createdAt: time.Now(),                    // Capture the time of the transaction, in this case the block timestamp
 49		updatedAt: time.Now(),
 50	})
 51
 52	return nil
 53}
 54
 55// UpdatePost allows the author to update a post
 56// The post can only be updated up to 10 minutes after posting
 57func UpdatePost(id string, text string) error {
 58	// Try to get the post
 59	raw, ok := posts.Get(id)
 60	if !ok {
 61		return ErrPostNotFound
 62	}
 63
 64	// Cast post from AVL tree
 65	post := raw.(*Post)
 66	if std.PreviousRealm().Address() != post.author {
 67		return ErrUnauthorized
 68	}
 69
 70	// Can only update 10 mins after it was posted
 71	if post.updatedAt.After(post.createdAt.Add(time.Minute * 10)) {
 72		return ErrUpdateWindowExpired
 73	}
 74
 75	post.text = text
 76	post.updatedAt = time.Now()
 77
 78	return nil
 79}
 80
 81// DeletePost deletes a post with a specific id
 82// Only the creator of a post can delete the post
 83func DeletePost(id string) error {
 84	// Try to get the post
 85	raw, ok := posts.Get(id)
 86	if !ok {
 87		return ErrPostNotFound
 88	}
 89
 90	// Cast post from AVL tree
 91	post := raw.(*Post)
 92	if std.PreviousRealm().Address() != post.author {
 93		return ErrUnauthorized
 94	}
 95
 96	// Use avl.Tree.Remove
 97	_, removed := posts.Remove(id)
 98	if !removed {
 99		// This shouldn't happen after all checks above
100		// If it does, discard any possible state changes
101		panic("failed to remove post")
102	}
103
104	return nil
105}
106
107// Render renders the main page of threads
108func Render(path string) string {
109	out := md.H1("MiniSocial")
110
111	if posts.Size() == 0 {
112		out += "No posts yet!\n\n"
113		return out
114	}
115
116	// Get the page from the path
117	page := pag.MustGetPageByPath(path)
118
119	// Iterate over items in the page
120	for _, item := range page.Items {
121		post := item.Value.(*Post)
122
123		// Try resolving the address for a username
124		text := post.author.String()
125		user := users.ResolveAddress(post.author)
126		if user != nil {
127			text = user.RenderLink("")
128		}
129
130		out += md.H4(ufmt.Sprintf("Post #%d - %s\n\n", int(post.id), text))
131
132		out += post.String()
133		out += md.HorizontalRule()
134	}
135
136	out += page.Picker(path)
137	out += "\n\n"
138	out += "Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "\n\n"
139
140	return out
141}