forked from gnolang/gno
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: new realm /r/demo/microblog (gnolang#791)
* 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
Showing
4 changed files
with
345 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) + "*" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |