Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
119 commits
Select commit Hold shift + click to select a range
7b33d03
feat: copy initial demo package of blog
louonezime May 27, 2025
59e3372
feat(blog): keep main structs to settle the structure of all types
louonezime May 28, 2025
e299bc2
feat(blog): basic start of data structures to start off
louonezime May 28, 2025
46ed05b
merge: pull request #12 from louonezime/scratch-blog to refactor-demo…
louonezime May 28, 2025
7fe0711
feat(blog): add base rendering for preview of posts
louonezime May 30, 2025
3a4dd0c
feat(realm-blog): realm is started to test out p/blog package
louonezime May 30, 2025
fbcd0f6
merge: pull request #15 from louonezime/scratch-blog to refactor-demo…
louonezime May 30, 2025
f355973
feat(blog): add router and error handling to posting
louonezime Jun 2, 2025
9ec1939
fix: merge conflict
louonezime Jun 2, 2025
1953263
merge: conflict of realm is pasted on the branch
louonezime Jun 2, 2025
28d9a0f
Merge branch 'tester-blog' into refactor-demo-blog
louonezime Jun 2, 2025
1b6eea7
feat(blog): add paging and post rendering
louonezime Jun 2, 2025
0d742de
feat(blog): comments are started and router is expanded
louonezime Jun 2, 2025
643d088
style: apply mod tidy
louonezime Jun 2, 2025
b1ddcb7
Merge branch 'master' into refactor-demo-blog
louonezime Jun 2, 2025
24f9013
test(blog-realm): temp blog realm is up to date
louonezime Jun 2, 2025
4677828
fix(blog): remove displayMode struct var to make use of query
louonezime Jun 2, 2025
8aab0ee
feat(blog): comments are all handled and sorting
louonezime Jun 3, 2025
75361bd
fix(blog): make use of slug instead of ID and add more errors
louonezime Jun 3, 2025
58772be
Merge branch 'master' into refactor-demo-blog
louonezime Jun 3, 2025
47ac1ac
feat(blog): gnodoc is added
louonezime Jun 3, 2025
a0ffa3c
style(blog): apply fmt
louonezime Jun 3, 2025
cf96d49
chore(blog): add error handling
louonezime Jun 3, 2025
bd0812e
feat(blog): more error handling and adding the commenter page
louonezime Jun 3, 2025
4680440
Merge branch 'master' into refactor-demo-blog
louonezime Jun 3, 2025
353f3cb
Merge branch 'master' into refactor-demo-blog
louonezime Jun 4, 2025
b1e3ec2
feat(blog): more sorting options that is toggled between ascending an…
louonezime Jun 4, 2025
39dd57d
feat(blog): litsing of all tags is configured
louonezime Jun 4, 2025
b160937
feat(blog): listing of all authors is now available
louonezime Jun 4, 2025
6db74bb
fix(blog): fix listings pages
louonezime Jun 4, 2025
77bce7d
style(blog): organise code in blog.gno
louonezime Jun 4, 2025
5e2bf5b
feat(blog): more sorting options for filtered routes
louonezime Jun 4, 2025
fdd6df7
fix(blog): replies are implemented
louonezime Jun 4, 2025
3eada1c
refactor(blog): rendering of listings are more organised
louonezime Jun 5, 2025
cf6e17d
feat(blog): sorting for most common is done
louonezime Jun 5, 2025
6955e90
chore(blog): remove unecessary values
louonezime Jun 5, 2025
8d0e9a5
fix(blog): remove comments
louonezime Jun 6, 2025
ff47f9d
chore(blog): parsequery is gathered in a struct to make it more acces…
louonezime Jun 6, 2025
bc1f921
feat(blog): start parsing start and end timing for filter
louonezime Jun 6, 2025
e5d7f46
Feat(blog): commenters are now clickable
louonezime Jun 10, 2025
8089069
feat(blog): add links and make sure replies are considered for :comme…
louonezime Jun 10, 2025
0792006
Merge branch 'master' into refactor-demo-blog
louonezime Jun 10, 2025
df4d362
feat(blog): enable customisation of footers and enable/disable likes
louonezime Jun 10, 2025
b47dc69
refactor(blog): make listing and filtered listing cleaner
louonezime Jun 10, 2025
a34e436
style(blog): clean up sorting
louonezime Jun 11, 2025
f6d34b3
style(blog): apply fmt
louonezime Jun 11, 2025
154246b
fix(blog): remove tmp realm to test
louonezime Jun 11, 2025
d7963f7
style(blog): adjust comments
louonezime Jun 11, 2025
23b9bae
fix(blog): apply make tidy
louonezime Jun 11, 2025
fc9ef4d
fix(blog): time range links are fixed to config for filters
louonezime Jun 11, 2025
50a9347
Merge branch 'master' into refactor-demo-blog
louonezime Jun 16, 2025
f1e71a7
feat(blog): start moderation to test
louonezime Jun 16, 2025
eca83b5
style(blog): move all rendering functions to render.gno to clean up b…
louonezime Jun 16, 2025
0327adb
chore(blog): iterate to next postid when creating post
louonezime Jun 16, 2025
46758e6
fix(blog): remove debugging
louonezime Jun 16, 2025
26e5df8
fix(blog): moderation for resolving users is fixed
louonezime Jun 18, 2025
17803e1
refactor(moderation): added moderation that's streamlined accross the…
louonezime Jun 18, 2025
a3481bc
fix(blog): test sample is fixed
louonezime Jun 18, 2025
fde1420
fix(blog): paths are fixed and tests are now more debugged
louonezime Jun 18, 2025
647aa82
Merge branch 'master' into refactor-demo-blog
louonezime Jun 18, 2025
73d178f
chore(blog): clean up use of resolver and fix bugs
louonezime Jun 18, 2025
8274fe9
chore(blog): add comments and organise code
louonezime Jun 18, 2025
be0ecc2
fix(blog): fix linting
louonezime Jun 18, 2025
a3f5ac8
fix(blog): comment errors from reply test
louonezime Jun 19, 2025
cd2d7ae
fix(blog): make sure the comments have relative IDs and replies too
louonezime Jun 19, 2025
42ea4af
chore(blog): remove debug and adjust comment size for deletions
louonezime Jun 19, 2025
894efbd
fix(blog): tests from adding comments/replies are good now
louonezime Jun 19, 2025
14807ea
test(blog): add test files with scenario + query functions
louonezime Jun 19, 2025
96478b9
feat(blog): add base of customising of header
louonezime Jun 23, 2025
dc0ee5c
feat(blog realm): r/lou/blog
louonezime Jun 25, 2025
5d8ed5a
chore(blog): headers are cleaned up
louonezime Jun 25, 2025
b68504d
chore(blog): remove comments
louonezime Jun 25, 2025
d3fe69f
chore: cleanup realm blog
louonezime Jun 25, 2025
32b6705
Merge branch 'master' into refactor-demo-blog
louonezime Jun 25, 2025
6a00b57
fix(blog): moderation makes use of ownable/exts/authorizable
louonezime Jun 26, 2025
e0c4a2f
feat(blog): make the blog have its exported functions
louonezime Jun 26, 2025
a7d21c6
Merge branch 'master' into refactor-demo-blog
louonezime Jun 26, 2025
938ecb6
chore(blog): cleanup files to show all tests
louonezime Jun 26, 2025
407f05f
fix(blog): realm is initialised immediately
louonezime Jun 26, 2025
9c054d6
fix(blog): don't wait for myblog to be created
louonezime Jun 26, 2025
d3c7a84
fix(blog): make updatePost a Post method
louonezime Jun 26, 2025
03a2646
Merge branch 'master' into refactor-demo-blog
louonezime Jun 26, 2025
9382c45
fix(blog): add error handling to realm
louonezime Jun 26, 2025
6080cd3
Merge branch 'master' into refactor-demo-blog
louonezime Jul 7, 2025
735a61a
chore: remove debug from authorizable
louonezime Jul 7, 2025
f431890
fix(realm): make add post use origin caller
louonezime Jul 7, 2025
e3ae639
Merge branch 'master' into refactor-demo-blog
louonezime Jul 11, 2025
62338c4
Merge branch 'master' into refactor-demo-blog
louonezime Jul 15, 2025
5e6b2c3
fix(blog): small fixes
louonezime Jul 15, 2025
5f92383
fix(blog): ignore Set return value of updated
louonezime Jul 15, 2025
36b833f
feat(blog): likes are functionable
louonezime Jul 15, 2025
1c1dbf3
fix(blog): remove Set function bool value as it always returns false
louonezime Jul 15, 2025
30b5dd9
fix(blog): remove all instances of Set error handling
louonezime Jul 15, 2025
2090aa3
Merge branch 'master' into refactor-demo-blog
louonezime Jul 15, 2025
ef0a4e7
fix(blog): refactor with options in blog.gno
louonezime Jul 16, 2025
63bb4d2
style(blog): simple fmt
louonezime Jul 16, 2025
4dd8378
feat(blog): make use of PkgPath() for the creation of a newblog
louonezime Jul 16, 2025
6f54b6e
Merge branch 'master' into refactor-demo-blog
louonezime Jul 16, 2025
0c65fc8
fix(blog): remove old unused file
louonezime Jul 16, 2025
d7e4d6a
Merge branch 'master' into refactor-demo-blog
leohhhn Jul 16, 2025
3647ef7
refactor(blog): realm toggles likes instead and makes use of helpers …
louonezime Jul 16, 2025
faa65d1
fix(blog): time default for ranges is adapted to time.Now()
louonezime Jul 16, 2025
9e371f5
Merge branch 'master' into refactor-demo-blog
louonezime Jul 16, 2025
f843ef4
Merge branch 'master' into refactor-demo-blog
louonezime Jul 17, 2025
826de5b
fix(blog): time.Now() instead of uninitialised
louonezime Jul 17, 2025
7083eaf
fix(blog): uniqueness of LastUpdated tree for no overlap
louonezime Jul 18, 2025
57d9a93
doc(blog): add more comments for header.gno files
louonezime Jul 18, 2025
dae13d2
Merge branch 'master' into refactor-demo-blog
louonezime Jul 23, 2025
0f270cb
fix(blog): case of pkgpath empty when checked
louonezime Jul 23, 2025
39eeb24
fix: pushed wrong file previously
louonezime Jul 23, 2025
93b96d7
fix(blog): handle escapedchars in slug
louonezime Jul 28, 2025
2d465d4
fix(blog): slugs are twice-escaped instead of single to avoid : or / …
louonezime Jul 29, 2025
d4b120e
style: remove comment
louonezime Jul 29, 2025
9c6db8a
chore: fixup error handling on some aspects
louonezime Jul 30, 2025
b6eedb0
test(blog): basic blog filetest are added
louonezime Jul 31, 2025
488687f
Merge branch 'master' into refactor-demo-blog
leohhhn Aug 14, 2025
4ddd013
Merge branch 'master' into refactor-demo-blog
leohhhn Aug 20, 2025
9b750a8
feat(tests): add missing tests
louonezime Sep 1, 2025
3867e4a
Merge branch 'master' into refactor-demo-blog
louonezime Sep 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
396 changes: 396 additions & 0 deletions examples/gno.land/p/lou/blog/blog.gno
Copy link
Contributor

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 the Blog receiver but its better for readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The 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 Options pattern and try to use it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ef0a4e7
is this what you meant by this?

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
}
Loading
Loading