-
Notifications
You must be signed in to change notification settings - Fork 428
feat(example): refactor p/demo/blog #4324
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
7b33d03
59e3372
e299bc2
46ed05b
7fe0711
3a4dd0c
fbcd0f6
f355973
9ec1939
1953263
28d9a0f
1b6eea7
0d742de
643d088
b1ddcb7
24f9013
4677828
8aab0ee
75361bd
58772be
47ac1ac
a0ffa3c
cf96d49
bd0812e
4680440
353f3cb
b1e3ec2
39dd57d
b160937
6db74bb
77bce7d
5e2bf5b
fdd6df7
3eada1c
cf6e17d
6955e90
8d0e9a5
ff47f9d
bc1f921
e5d7f46
8089069
0792006
df4d362
b47dc69
a34e436
f6d34b3
154246b
d7963f7
23b9bae
fc9ef4d
50a9347
f1e71a7
eca83b5
0327adb
46758e6
26e5df8
17803e1
a3481bc
fde1420
647aa82
73d178f
8274fe9
be0ecc2
a3f5ac8
cd2d7ae
42ea4af
894efbd
14807ea
96478b9
dc0ee5c
5d8ed5a
b68504d
d3fe69f
32b6705
6a00b57
e0c4a2f
a7d21c6
938ecb6
407f05f
9c054d6
d3c7a84
03a2646
9382c45
6080cd3
735a61a
f431890
e3ae639
62338c4
5e6b2c3
5f92383
36b833f
1c1dbf3
30b5dd9
2090aa3
ef0a4e7
63bb4d2
4dd8378
6f54b6e
0c65fc8
d7e4d6a
3647ef7
faa65d1
9e371f5
f843ef4
826de5b
7083eaf
57d9a93
dae13d2
0f270cb
39eeb24
93b96d7
2d465d4
d4b120e
9c6db8a
b6eedb0
488687f
4ddd013
9b750a8
3867e4a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,396 @@ | ||
package blog | ||
|
||
import ( | ||
"std" | ||
"strings" | ||
"time" | ||
|
||
"gno.land/p/demo/avl" | ||
"gno.land/p/demo/ownable/exts/authorizable" | ||
"gno.land/p/demo/seqid" | ||
"gno.land/p/demo/ufmt" | ||
"gno.land/p/moul/md" | ||
) | ||
|
||
type Blog struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another thing that would be good is that the user of your library can choose if the blog 1) allows comments 2) allows reactions 3) etc. Check out the commondao There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ef0a4e7 |
||
Authorizable *authorizable.Authorizable | ||
title string | ||
prefix string | ||
PostId seqid.ID | ||
Posts *avl.Tree // id --> *Post | ||
PostsBySlug *avl.Tree // slug --> *Post | ||
PostsByUpdatedAt *avl.Tree // "<id>::updatedAt" --> *Post (To ensure no overlay) | ||
TagsIndex *avl.Tree // tagName --> int | ||
TagsSorted *avl.Tree // "<count>::tag" --> tag (To sort from most/least common tags) | ||
AuthorsIndex *avl.Tree // authorAddress --> int | ||
AuthorsSorted *avl.Tree // "<count>::author" --> author (To sort from most/least common authors) | ||
|
||
CustomHeader *[]HeaderPreset // header.gno | ||
DisableLikes bool | ||
DisableComments bool | ||
UserResolver UserResolver // moderation.gno | ||
} | ||
|
||
func (b Blog) Title() string { | ||
return b.title | ||
} | ||
|
||
func (b Blog) Prefix() string { | ||
return b.prefix | ||
} | ||
|
||
func NewBlog(title string, owner std.Address, opts ...Options) (*Blog, error) { | ||
if title == "" { | ||
return nil, ErrEmptyTitle | ||
} | ||
if err := CheckAddr(owner.String()); err != nil { | ||
return nil, err | ||
} | ||
|
||
pkgPath := std.CurrentRealm().PkgPath() | ||
if pkgPath == "" { | ||
return nil, ErrEmptyPrefix | ||
} | ||
prefix := strings.Split(pkgPath, "gno.land")[1] | ||
if prefix == "" { | ||
return nil, ErrEmptyPrefix | ||
} | ||
|
||
auth := authorizable.NewAuthorizableWithAddress(owner) | ||
blog := &Blog{ | ||
Authorizable: auth, | ||
title: title, | ||
prefix: prefix, | ||
PostId: seqid.ID(0), | ||
Posts: avl.NewTree(), | ||
PostsBySlug: avl.NewTree(), | ||
PostsByUpdatedAt: avl.NewTree(), | ||
TagsIndex: avl.NewTree(), | ||
TagsSorted: avl.NewTree(), | ||
AuthorsIndex: avl.NewTree(), | ||
AuthorsSorted: avl.NewTree(), | ||
DisableLikes: false, | ||
DisableComments: false, | ||
UserResolver: nil, | ||
CustomHeader: nil, | ||
} | ||
|
||
for _, opt := range opts { | ||
opt(blog) | ||
} | ||
return blog, nil | ||
} | ||
|
||
type Options func(*Blog) | ||
|
||
func WithDisableLikes() Options { | ||
return func(b *Blog) { | ||
b.DisableLikes = true | ||
b.SetDisablePostLikes(true) | ||
} | ||
} | ||
|
||
func WithDisableComments() Options { | ||
return func(b *Blog) { | ||
b.DisableComments = true | ||
b.SetDisableComments(true) | ||
} | ||
} | ||
|
||
func WithUserResolver(resolver UserResolver) Options { | ||
return func(b *Blog) { | ||
b.UserResolver = resolver | ||
} | ||
} | ||
|
||
func (b *Blog) AddPost(post *Post) error { | ||
b.Authorizable.AssertPreviousOnAuthList() | ||
|
||
post.id = b.PostId.Next() | ||
if _, err := b.GetPostBySlug(post.Slug()); err == nil { | ||
return ErrPostAlreadyExists | ||
} | ||
if err := CheckAddr(post.Publisher()); err != nil { | ||
return err | ||
} | ||
|
||
post.DisableLikes = b.DisableLikes | ||
post.DisableComments = b.DisableComments | ||
if b.UserResolver != nil { | ||
post.UserResolver = b.UserResolver | ||
post.authors = post.resolveAuthors(post.Authors()) | ||
post.publisher, _ = CheckUser(post.Publisher(), b.UserResolver) | ||
} | ||
|
||
b.addToIndex(post) | ||
b.Posts.Set(post.ID(), post) | ||
b.PostsBySlug.Set(post.Slug(), post) | ||
b.PostsByUpdatedAt.Set(post.ID()+"::"+post.UpdatedAt().String(), post) | ||
return nil | ||
} | ||
|
||
func (b *Blog) UpdatePostById(id string, newPost *Post) error { | ||
b.Authorizable.AssertPreviousOnAuthList() | ||
|
||
post, err := b.GetPostById(id) | ||
if err != nil { | ||
return err | ||
} | ||
postBySlug, _ := b.PostsBySlug.Get(post.Slug()) | ||
postByUpdated, _ := b.PostsByUpdatedAt.Get(post.ID() + "::" + post.UpdatedAt().String()) | ||
post.UpdatePost(newPost) | ||
postBySlug.(*Post).UpdatePost(newPost) | ||
postByUpdated.(*Post).UpdatePost(newPost) | ||
return nil | ||
} | ||
|
||
func (b *Blog) UpdatePostBySlug(slug string, newPost *Post) error { | ||
b.Authorizable.AssertPreviousOnAuthList() | ||
|
||
post, err := b.GetPostBySlug(slug) | ||
if err != nil { | ||
return err | ||
} | ||
postById, _ := b.Posts.Get(post.ID()) | ||
postByUpdated, _ := b.PostsByUpdatedAt.Get(post.ID() + "::" + post.UpdatedAt().String()) | ||
post.UpdatePost(newPost) | ||
postById.(*Post).UpdatePost(newPost) | ||
postByUpdated.(*Post).UpdatePost(newPost) | ||
return nil | ||
} | ||
|
||
func (b *Blog) DeletePostById(id string) error { | ||
b.Authorizable.AssertPreviousOnAuthList() | ||
|
||
post, err := b.GetPostById(id) | ||
if err != nil { | ||
return err | ||
} | ||
return b.DeletePost(post) | ||
} | ||
|
||
func (b *Blog) DeletePostBySlug(slug string) error { | ||
b.Authorizable.AssertPreviousOnAuthList() | ||
|
||
post, err := b.GetPostBySlug(slug) | ||
if err != nil { | ||
return err | ||
} | ||
return b.DeletePost(post) | ||
} | ||
|
||
func (b *Blog) DeletePost(post *Post) error { | ||
_, removed := b.Posts.Remove(post.ID()) | ||
_, removedSlug := b.PostsBySlug.Remove(post.Slug()) | ||
_, removedUpdated := b.PostsByUpdatedAt.Remove(post.ID() + "::" + post.UpdatedAt().String()) | ||
if !removed || !removedSlug || !removedUpdated { | ||
return ErrDeleteFailed | ||
} | ||
return nil | ||
} | ||
|
||
func (b *Blog) LikePostById(id string) error { // toggles between like and unlike | ||
b.Authorizable.AssertPreviousOnAuthList() | ||
|
||
post, err := b.GetPostById(id) | ||
if err != nil { | ||
return err | ||
} | ||
if err := post.LikePost(std.PreviousRealm().Address().String()); err != nil { | ||
post.UnlikePost(std.PreviousRealm().Address().String()) | ||
} | ||
return nil | ||
|
||
} | ||
|
||
func (b *Blog) LikePostBySlug(slug string) error { // toggles between like and unlike | ||
b.Authorizable.AssertPreviousOnAuthList() | ||
|
||
post, err := b.GetPostBySlug(slug) | ||
if err != nil { | ||
return err | ||
} | ||
if err := post.LikePost(std.PreviousRealm().Address().String()); err != nil { | ||
post.UnlikePost(std.PreviousRealm().Address().String()) | ||
} | ||
return nil | ||
} | ||
|
||
func (b Blog) GetPostById(id string) (*Post, error) { | ||
post, found := b.Posts.Get(id) | ||
if !found { | ||
return nil, ErrPostNotFound | ||
} | ||
return post.(*Post), nil | ||
} | ||
|
||
func (b Blog) GetPostBySlug(slug string) (*Post, error) { | ||
post, found := b.PostsBySlug.Get(slug) | ||
if !found { | ||
return nil, ErrPostNotFound | ||
} | ||
return post.(*Post), nil | ||
} | ||
|
||
func (b *Blog) SetDisablePostLikes(disable bool) { | ||
b.Authorizable.AssertPreviousOnAuthList() | ||
|
||
b.DisableLikes = disable | ||
b.Posts.Iterate("", "", func(_ string, value any) bool { | ||
post := value.(*Post) | ||
post.DisableLikes = disable | ||
return false | ||
}) | ||
b.PostsBySlug.Iterate("", "", func(_ string, value any) bool { | ||
post := value.(*Post) | ||
post.DisableLikes = disable | ||
return false | ||
}) | ||
b.PostsByUpdatedAt.Iterate("", "", func(_ string, value any) bool { | ||
post := value.(*Post) | ||
post.DisableLikes = disable | ||
return false | ||
}) | ||
} | ||
|
||
func (b *Blog) SetDisableComments(disable bool) { | ||
b.Authorizable.AssertPreviousOnAuthList() | ||
|
||
b.DisableComments = disable | ||
b.Posts.Iterate("", "", func(_ string, value any) bool { | ||
post := value.(*Post) | ||
post.DisableComments = disable | ||
return false | ||
}) | ||
b.PostsBySlug.Iterate("", "", func(_ string, value any) bool { | ||
post := value.(*Post) | ||
post.DisableComments = disable | ||
return false | ||
}) | ||
b.PostsByUpdatedAt.Iterate("", "", func(_ string, value any) bool { | ||
post := value.(*Post) | ||
post.DisableComments = disable | ||
return false | ||
}) | ||
} | ||
|
||
func (b *Blog) SetUserResolver(resolver UserResolver) { | ||
b.Authorizable.AssertPreviousOnAuthList() | ||
b.UserResolver = resolver | ||
} | ||
|
||
func (b *Blog) SetCustomHeader(presets []HeaderPreset) { | ||
b.Authorizable.AssertPreviousOnAuthList() | ||
b.CustomHeader = &presets | ||
} | ||
|
||
func (b Blog) Mention(role, recipient string) string { | ||
if role == "author" { | ||
return md.Bold(md.Link("@"+recipient, b.Prefix()+":authors/"+recipient)) | ||
} | ||
if role == "commenter" { | ||
return md.Bold(md.Link("@"+recipient, b.Prefix()+":commenters/"+recipient)) | ||
} | ||
if role == "tag" { | ||
return md.Bold(md.Link("#"+recipient, b.Prefix()+":tags/"+recipient)) | ||
} | ||
return "" | ||
} | ||
|
||
func (b *Blog) addToIndex(post *Post) { | ||
for _, tag := range post.Tags() { | ||
oldCount := 0 | ||
if val, found := b.TagsIndex.Get(tag); found { | ||
oldCount = val.(int) | ||
b.TagsSorted.Remove(ufmt.Sprintf("%05d::%s", oldCount, tag)) | ||
} | ||
newCount := oldCount + 1 | ||
b.TagsIndex.Set(tag, newCount) | ||
b.TagsSorted.Set(ufmt.Sprintf("%05d::%s", newCount, tag), newCount) | ||
} | ||
for _, author := range post.Authors() { | ||
oldCount := 0 | ||
if val, found := b.AuthorsIndex.Get(author); found { | ||
oldCount = val.(int) | ||
b.AuthorsSorted.Remove(ufmt.Sprintf("%05d::%s", oldCount, author)) | ||
} | ||
newCount := oldCount + 1 | ||
b.AuthorsIndex.Set(author, newCount) | ||
b.AuthorsSorted.Set(ufmt.Sprintf("%05d::%s", newCount, author), newCount) | ||
} | ||
} | ||
|
||
func (b Blog) filterPostsStartEnd(tree *avl.Tree, start, end *time.Time) *avl.Tree { | ||
filtered := avl.NewTree() | ||
tree.Iterate("", "", func(k string, v interface{}) bool { | ||
post := v.(*Post) | ||
if (start == nil || post.CreatedAt().After(*start)) && | ||
(end == nil || post.CreatedAt().Before(*end)) { | ||
filtered.Set(k, post) | ||
} | ||
return false | ||
}) | ||
return filtered | ||
} | ||
|
||
func (b Blog) filterPostsByField(field, value, sort string) (*avl.Tree, bool) { | ||
recentPosts := avl.NewTree() | ||
alphaPosts := avl.NewTree() | ||
updatedPosts := avl.NewTree() | ||
|
||
switch field { | ||
case "tag", "author": | ||
b.Posts.ReverseIterate("", "", func(k string, v interface{}) bool { | ||
post := v.(*Post) | ||
var match bool | ||
if field == "tag" { | ||
match = hasField(post.Tags(), value) | ||
} else { | ||
match = hasField(post.Authors(), value) | ||
} | ||
if match { | ||
recentPosts.Set(k, post) | ||
alphaPosts.Set(post.Slug(), post) | ||
updatedPosts.Set(post.UpdatedAt().String(), post) | ||
} | ||
return false | ||
}) | ||
|
||
case "commenter": | ||
commenterId := seqid.ID(0) | ||
b.Posts.ReverseIterate("", "", func(k string, v interface{}) bool { | ||
post := v.(*Post) | ||
comments := post.GetCommentsByAuthor(value) | ||
for _, comment := range comments { | ||
keyPrefix := commenterId.Next().String() + "::" | ||
recentPosts.Set(keyPrefix+post.ID(), comment) | ||
alphaPosts.Set(keyPrefix+post.Slug(), comment) | ||
updatedPosts.Set(keyPrefix+post.UpdatedAt().String(), comment) | ||
} | ||
return false | ||
}) | ||
} | ||
|
||
switch sort { | ||
case "alpha": | ||
return alphaPosts, alphaPosts.Size() > 0 | ||
case "update": | ||
return updatedPosts, updatedPosts.Size() > 0 | ||
default: | ||
return recentPosts, recentPosts.Size() > 0 | ||
} | ||
} | ||
|
||
func (b Blog) findPostBySlug(value string) (string, bool) { | ||
var foundKey string | ||
var found bool | ||
b.Posts.Iterate("", "", func(k string, v interface{}) bool { | ||
post := v.(*Post) | ||
if post.Slug() == value { | ||
foundKey = k | ||
found = true | ||
} | ||
return found | ||
}) | ||
return foundKey, found | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you can, try to make this file a bit smaller. Maybe move the render functions into
render.gno
? I know they're on theBlog
receiver but its better for readability.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
eca83b5