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}