Skip to content

Commit

Permalink
feat: new realm /r/demo/microblog (gnolang#791)
Browse files Browse the repository at this point in the history
* add /r/demo/microblog

* remove unused imports

* fix: remove unused imports and error handling

* sort by last updated

* split out rendering into multiple functions

* better error handling

* NewPage is private

* turn print statements to tests

* fix typo

* move comments to README

* remove unused tests

* add status not found

* refactor to new avl.Node iterator

* revert genesis_balances.txt

* remove redundant newPage

* format time to ensure alphabetical sorting

* time bug formatting fix

* improved ui

* unexport byLastPosted

* use registered users data when available

* remove redundant types
  • Loading branch information
schollz authored and Doozers committed Aug 31, 2023
1 parent 2267b21 commit d6ea818
Show file tree
Hide file tree
Showing 4 changed files with 345 additions and 0 deletions.
182 changes: 182 additions & 0 deletions examples/gno.land/p/demo/microblog/microblog.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package microblog

import (
"errors"
"sort"
"std"
"strings"
"time"

"gno.land/p/demo/avl"
"gno.land/p/demo/ufmt"
"gno.land/r/demo/users"
)

var (
ErrNotFound = errors.New("not found")
StatusNotFound = "404"
)

type Microblog struct {
Title string
Prefix string // i.e. r/gnoland/blog:
Pages avl.Tree // author (string) -> Page
}

func NewMicroblog(title string, prefix string) (m *Microblog) {
return &Microblog{
Title: title,
Prefix: prefix,
Pages: avl.Tree{},
}
}

func (m *Microblog) GetPages() []*Page {
var (
pages = make([]*Page, m.Pages.Size())
index = 0
)

m.Pages.Iterate("", "", func(key string, value interface{}) bool {
pages[index] = value.(*Page)
index++
return false
})

sort.Sort(byLastPosted(pages))

return pages
}

func (m *Microblog) RenderHome() string {
output := ufmt.Sprintf("# %s\n\n", m.Title)
output += "# pages\n\n"

for _, page := range m.GetPages() {
if u := users.GetUserByAddress(page.Author); u != nil {
output += ufmt.Sprintf("- [%s (%s)](%s%s)\n", u.Name(), page.Author.String(), m.Prefix, page.Author.String())
} else {
output += ufmt.Sprintf("- [%s](%s%s)\n", page.Author.String(), m.Prefix, page.Author.String())
}
}

return output
}

func (m *Microblog) RenderUser(user string) string {
silo, found := m.Pages.Get(user)
if !found {
return StatusNotFound
}

return (silo.(*Page)).String()
}

func (m *Microblog) Render(path string) string {
parts := strings.Split(path, "/")

isHome := path == ""
isUser := len(parts) == 1

switch {
case isHome:
return m.RenderHome()

case isUser:
return m.RenderUser(parts[0])
}

return StatusNotFound
}

func (m *Microblog) NewPost(text string) error {
author := std.GetOrigCaller()
_, found := m.Pages.Get(author.String())
if !found {
// make a new page for the new author
m.Pages.Set(author.String(), &Page{
Author: author,
CreatedAt: time.Now(),
})
}

page, err := m.GetPage(author.String())
if err != nil {
return err
}
return page.NewPost(text)
}

func (m *Microblog) GetPage(author string) (*Page, error) {
silo, found := m.Pages.Get(author)
if !found {
return nil, ErrNotFound
}
return silo.(*Page), nil
}

type Page struct {
ID int
Author std.Address
CreatedAt time.Time
LastPosted time.Time
Posts avl.Tree // time -> Post
}

// byLastPosted implements sort.Interface for []Page based on
// the LastPosted field.
type byLastPosted []*Page

func (a byLastPosted) Len() int { return len(a) }
func (a byLastPosted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byLastPosted) Less(i, j int) bool { return a[i].LastPosted.After(a[j].LastPosted) }

func (p *Page) String() string {
o := ""
if u := users.GetUserByAddress(p.Author); u != nil {
o += ufmt.Sprintf("# [%s](/r/demo/users:%s)\n\n", u.Name(), u.Name())
o += ufmt.Sprintf("%s\n\n", u.Profile())
}
o += ufmt.Sprintf("## [%s](/r/demo/microblog:%s)\n\n", p.Author, p.Author)

o += ufmt.Sprintf("joined %s, last updated %s\n\n", p.CreatedAt.Format("2006-02-01"), p.LastPosted.Format("2006-02-01"))
o += "## feed\n\n"
for _, u := range p.GetPosts() {
o += u.String() + "\n\n"
}
return o
}

func (p *Page) NewPost(text string) error {
now := time.Now()
p.LastPosted = now
p.Posts.Set(ufmt.Sprintf("%s%d", now.Format(time.RFC3339), p.Posts.Size()), &Post{
ID: p.Posts.Size(),
Text: text,
CreatedAt: now,
})
return nil
}

func (p *Page) GetPosts() []*Post {
posts := make([]*Post, p.Posts.Size())
i := 0
p.Posts.ReverseIterate("", "", func(key string, value interface{}) bool {
postParsed := value.(*Post)
posts[i] = postParsed
i++
return false
})
return posts
}

// Post lists the specific update
type Post struct {
ID int
CreatedAt time.Time
Text string
}

func (p *Post) String() string {
return "> " + strings.ReplaceAll(p.Text, "\n", "\n>\n>") + "\n>\n> *" + p.CreatedAt.Format(time.RFC1123) + "*"
}
96 changes: 96 additions & 0 deletions examples/gno.land/p/demo/microblog/microblog_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package microblog

import (
"log"
"std"
"strings"
"testing"

"gno.land/p/demo/avl"
"gno.land/p/demo/testutils"
)

func TestMicroblog(t *testing.T) {
const (
title string = "test microblog"
prefix string = "/r/test"
author1 std.Address = testutils.TestAddress("author1")
author2 std.Address = testutils.TestAddress("author2")
)

std.TestSetOrigCaller(author1)

d := NewMicroblog(title, prefix)
if d.Render("/wrongpath") != "404" {
t.Fatalf("rendering not giving 404")
}
if d.Render("") == "404" {
t.Fatalf("rendering / should not give 404")
}
if err := d.NewPost("goodbyte, web2"); err != nil {
t.Fatalf("could not create post")
}
if _, err := d.GetPage(author1.String()); err != nil {
t.Fatalf("silo should exist")
}
if _, err := d.GetPage("no such author"); err == nil {
t.Fatalf("silo should not exist")
}

std.TestSetOrigCaller(author2)

if err := d.NewPost("hello, web3"); err != nil {
t.Fatalf("could not create post")
}
if err := d.NewPost("hello again, web3"); err != nil {
t.Fatalf("could not create post")
}
if err := d.NewPost("hi again,\n web4?"); err != nil {
t.Fatalf("could not create post")
}

println("--- MICROBLOG ---\n\n")
if rendering := d.Render(""); rendering != `# test microblog
# pages
- [g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6](/r/testg1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6)
- [g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00](/r/testg1v96hg6r0wge97h6lta047h6lta047h6lyz7c00)
` {
t.Fatalf("incorrect rendering /: '%s'", rendering)
}

if rendering := strings.TrimSpace(d.Render(author1.String())); rendering != `## [g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6](/r/demo/microblog:g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6)
joined 2009-13-02, last updated 2009-13-02
## feed
> goodbyte, web2
>
> *Fri, 13 Feb 2009 23:31:30 UTC*` {
t.Fatalf("incorrect rendering /: '%s'", rendering)
}

if rendering := strings.TrimSpace(d.Render(author2.String())); rendering != `## [g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00](/r/demo/microblog:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00)
joined 2009-13-02, last updated 2009-13-02
## feed
> hi again,
>
> web4?
>
> *Fri, 13 Feb 2009 23:31:30 UTC*
> hello again, web3
>
> *Fri, 13 Feb 2009 23:31:30 UTC*
> hello, web3
>
> *Fri, 13 Feb 2009 23:31:30 UTC*` {
t.Fatalf("incorrect rendering /: '%s'", rendering)
}
}
24 changes: 24 additions & 0 deletions examples/gno.land/r/demo/microblog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# microblog realm

## Getting started:

(One-time) Add the microblog package:

```
gnokey maketx addpkg --pkgpath "gno.land/p/demo/microblog" --pkgdir "examples/gno.land/p/demo/microblog" \
--deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast --chainid dev --remote localhost:26657 <YOURKEY>
```

(One-time) Add the microblog realm:

```
gnokey maketx addpkg --pkgpath "gno.land/r/demo/microblog" --pkgdir "examples/gno.land/r/demo/microblog" \
--deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast --chainid dev --remote localhost:26657 <YOURKEY>
```

Add a microblog post:

```
gnokey maketx call --pkgpath "gno.land/r/demo/microblog" --func "NewPost" --args "hello, world" \
--gas-fee "1000000ugnot" --gas-wanted "2000000" --broadcast --chainid dev --remote localhost:26657 <YOURKEY>
```
43 changes: 43 additions & 0 deletions examples/gno.land/r/demo/microblog/microblog.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Microblog is a website with shortform posts from users.
// The API is simple - "AddPost" takes markdown and
// adds it to the users site.
// The microblog location is determined by the user address
// /r/demo/microblog:<YOUR-ADDRESS>
package microblog

import (
"std"

"gno.land/p/demo/microblog"
"gno.land/r/demo/users"
)

var (
title = "gno-based microblog"
prefix = "/r/demo/microblog:"
m *microblog.Microblog
)

func init() {
m = microblog.NewMicroblog(title, prefix)
}

// Render calls the microblog renderer
func Render(path string) string {
return m.Render(path)
}

// NewPost takes a single argument (post markdown) and
// adds a post to the address of the caller.
func NewPost(text string) string {
if err := m.NewPost(text); err != nil {
return "unable to add new post"
}
return "added new post"
}

func Register(name, profile string) string {
caller := std.GetOrigCaller() // main
users.Register(caller, name, profile)
return "OK"
}

0 comments on commit d6ea818

Please sign in to comment.