Skip to content
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

feat: new realm /r/demo/microblog #791

Merged
merged 25 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
198 changes: 198 additions & 0 deletions examples/gno.land/p/demo/microblog/microblog.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package microblog

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

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

var (
ErrNotFound error = errors.New("not found")
StatusNotFound string = "404"
thehowl marked this conversation as resolved.
Show resolved Hide resolved
)

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() {
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) newPage() error {
author := std.GetOrigCaller()
_, found := m.Pages.Get(author.String())
if found {
return errors.New("author already exists")
}
thehowl marked this conversation as resolved.
Show resolved Hide resolved

m.Pages.Set(author.String(), &Page{
Author: author,
CreatedAt: time.Now(),
})
return nil
}

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
if err := m.newPage(); err != nil {
return err
}
}

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) }
thehowl marked this conversation as resolved.
Show resolved Hide resolved

func (p *Page) String() string {
o := ufmt.Sprintf("# %s\n\n", 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 += "## posts\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, p.Posts.Size()), &Post{
thehowl marked this conversation as resolved.
Show resolved Hide resolved
ID: p.Posts.Size(),
Text: text,
CreatedAt: now,
})
return nil
}

func (p *Page) GetPosts() []*Post {
posts := make([]*Post, p.Posts.Size())

p.Posts.Iterate("", "", func(key string, value interface{}) bool {
postParsed := value.(*Post)
posts[postParsed.ID] = postParsed
return false
})

return reverse(posts)
}

func reverse(input []*Post) []*Post {
inputLen := len(input)
output := make([]*Post, inputLen)

for i, n := range input {
j := inputLen - i - 1

output[j] = n
}

return output
}
thehowl marked this conversation as resolved.
Show resolved Hide resolved

// Post lists the specific update

type Post struct {
ID int
CreatedAt time.Time
Text string
}

func (p *Post) String() string {
return "> " + p.Text + " *- " + p.CreatedAt.Format(time.RFC822) + "*"
thehowl marked this conversation as resolved.
Show resolved Hide resolved
}
89 changes: 89 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,89 @@
package microblog

import (
"log"
"std"
"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, 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 := d.Render(author1.String()); rendering != `# g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6

joined 2009-13-02, last updated 2009-13-02

## posts

> goodbyte, web2 *- 13 Feb 09 23:31 UTC*

` {
t.Fatalf("incorrect rendering /: '%s'", rendering)
}

if rendering := d.Render(author2.String()); rendering != `# g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00

joined 2009-13-02, last updated 2009-13-02

## posts

> hi again, web4? *- 13 Feb 09 23:31 UTC*

> hello again, web3 *- 13 Feb 09 23:31 UTC*

> hello, web3 *- 13 Feb 09 23:31 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>
```
34 changes: 34 additions & 0 deletions examples/gno.land/r/demo/microblog/microblog.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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 (
"gno.land/p/demo/microblog"
)

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