diff --git a/examples/gno.land/p/demo/ownable/errors.gno b/examples/gno.land/p/demo/ownable/errors.gno index ffbf6ab3f6f..89776a6cf12 100644 --- a/examples/gno.land/p/demo/ownable/errors.gno +++ b/examples/gno.land/p/demo/ownable/errors.gno @@ -3,6 +3,6 @@ package ownable import "errors" var ( - ErrUnauthorized = errors.New("unauthorized; caller is not owner") - ErrInvalidAddress = errors.New("new owner address is invalid") + ErrUnauthorized = errors.New("ownable: caller is not owner") + ErrInvalidAddress = errors.New("ownable: new owner address is invalid") ) diff --git a/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable.gno b/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable.gno new file mode 100644 index 00000000000..f9f0ea15dd9 --- /dev/null +++ b/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable.gno @@ -0,0 +1,90 @@ +// Package authorizable is an extension of p/demo/ownable; +// It allows the user to instantiate an Authorizable struct, which extends +// p/demo/ownable with a list of users that are authorized for something. +// By using authorizable, you have a superuser (ownable), as well as another +// authorization level, which can be used for adding moderators or similar to your realm. +package authorizable + +import ( + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" + "gno.land/p/demo/ufmt" +) + +type Authorizable struct { + *ownable.Ownable // owner in ownable is superuser + authorized *avl.Tree // std.Addr > struct{}{} +} + +func NewAuthorizable() *Authorizable { + a := &Authorizable{ + ownable.New(), + avl.NewTree(), + } + + // Add owner to auth list + a.authorized.Set(a.Owner().String(), struct{}{}) + return a +} + +func NewAuthorizableWithAddress(addr std.Address) *Authorizable { + a := &Authorizable{ + ownable.NewWithAddress(addr), + avl.NewTree(), + } + + // Add owner to auth list + a.authorized.Set(a.Owner().String(), struct{}{}) + return a +} + +func (a *Authorizable) AddToAuthList(addr std.Address) error { + if err := a.CallerIsOwner(); err != nil { + return ErrNotSuperuser + } + + if _, exists := a.authorized.Get(addr.String()); exists { + return ErrAlreadyInList + } + + a.authorized.Set(addr.String(), struct{}{}) + + return nil +} + +func (a *Authorizable) DeleteFromAuthList(addr std.Address) error { + if err := a.CallerIsOwner(); err != nil { + return ErrNotSuperuser + } + + if !a.authorized.Has(addr.String()) { + return ErrNotInAuthList + } + + if _, removed := a.authorized.Remove(addr.String()); !removed { + str := ufmt.Sprintf("authorizable: could not remove %s from auth list", addr.String()) + panic(str) + } + + return nil +} + +func (a Authorizable) CallerOnAuthList() error { + caller := std.PrevRealm().Addr() + + if !a.authorized.Has(caller.String()) { + return ErrNotInAuthList + } + + return nil +} + +func (a Authorizable) AssertOnAuthList() { + caller := std.PrevRealm().Addr() + + if !a.authorized.Has(caller.String()) { + panic(ErrNotInAuthList) + } +} diff --git a/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable_test.gno b/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable_test.gno new file mode 100644 index 00000000000..10a5e411bdb --- /dev/null +++ b/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable_test.gno @@ -0,0 +1,116 @@ +package authorizable + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" +) + +var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") + charlie = testutils.TestAddress("charlie") +) + +func TestNewAuthorizable(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) // TODO(bug, issue #2371): should not be needed + + a := NewAuthorizable() + got := a.Owner() + + if alice != got { + t.Fatalf("Expected %s, got: %s", alice, got) + } +} + +func TestNewAuthorizableWithAddress(t *testing.T) { + a := NewAuthorizableWithAddress(alice) + + got := a.Owner() + + if alice != got { + t.Fatalf("Expected %s, got: %s", alice, got) + } +} + +func TestCallerOnAuthList(t *testing.T) { + a := NewAuthorizableWithAddress(alice) + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + if err := a.CallerOnAuthList(); err == ErrNotInAuthList { + t.Fatalf("expected alice to be on the list") + } +} + +func TestNotCallerOnAuthList(t *testing.T) { + a := NewAuthorizableWithAddress(alice) + std.TestSetRealm(std.NewUserRealm(bob)) + std.TestSetOrigCaller(bob) + + if err := a.CallerOnAuthList(); err == nil { + t.Fatalf("expected bob to not be on the list") + } +} + +func TestAddToAuthList(t *testing.T) { + a := NewAuthorizableWithAddress(alice) + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + if err := a.AddToAuthList(bob); err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + std.TestSetRealm(std.NewUserRealm(bob)) + std.TestSetOrigCaller(bob) + + if err := a.AddToAuthList(bob); err == nil { + t.Fatalf("Expected AddToAuth to error while bob called it, but it didn't") + } +} + +func TestDeleteFromList(t *testing.T) { + a := NewAuthorizableWithAddress(alice) + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + if err := a.AddToAuthList(bob); err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if err := a.AddToAuthList(charlie); err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + std.TestSetRealm(std.NewUserRealm(bob)) + std.TestSetOrigCaller(bob) + + // Try an unauthorized deletion + if err := a.DeleteFromAuthList(alice); err == nil { + t.Fatalf("Expected DelFromAuth to error with %v", err) + } + + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + if err := a.DeleteFromAuthList(charlie); err != nil { + t.Fatalf("Expected no error, got %v", err) + } +} + +func TestAssertOnList(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + a := NewAuthorizableWithAddress(alice) + + std.TestSetRealm(std.NewUserRealm(bob)) + std.TestSetOrigCaller(bob) + + uassert.PanicsWithMessage(t, ErrNotInAuthList.Error(), func() { + a.AssertOnAuthList() + }) +} diff --git a/examples/gno.land/p/demo/ownable/exts/authorizable/errors.gno b/examples/gno.land/p/demo/ownable/exts/authorizable/errors.gno new file mode 100644 index 00000000000..4ba5983bccb --- /dev/null +++ b/examples/gno.land/p/demo/ownable/exts/authorizable/errors.gno @@ -0,0 +1,9 @@ +package authorizable + +import "errors" + +var ( + ErrNotInAuthList = errors.New("authorizable: caller is not in authorized list") + ErrNotSuperuser = errors.New("authorizable: caller is not superuser") + ErrAlreadyInList = errors.New("authorizable: address is already in authorized list") +) diff --git a/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod b/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod new file mode 100644 index 00000000000..f36823f3f71 --- /dev/null +++ b/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod @@ -0,0 +1,9 @@ +module gno.land/p/demo/ownable/exts/authorizable + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/ownable v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/uassert v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest +) diff --git a/examples/gno.land/p/demo/ownable/ownable.gno b/examples/gno.land/p/demo/ownable/ownable.gno index 75ebcde0a28..a77b22461a9 100644 --- a/examples/gno.land/p/demo/ownable/ownable.gno +++ b/examples/gno.land/p/demo/ownable/ownable.gno @@ -1,8 +1,6 @@ package ownable -import ( - "std" -) +import "std" const OwnershipTransferEvent = "OwnershipTransfer" @@ -19,7 +17,9 @@ func New() *Ownable { } func NewWithAddress(addr std.Address) *Ownable { - return &Ownable{owner: addr} + return &Ownable{ + owner: addr, + } } // TransferOwnership transfers ownership of the Ownable struct to a new address @@ -40,6 +40,7 @@ func (o *Ownable) TransferOwnership(newOwner std.Address) error { "from", string(prevOwner), "to", string(newOwner), ) + return nil } @@ -64,6 +65,7 @@ func (o *Ownable) DropOwnership() error { return nil } +// Owner returns the owner address from Ownable func (o Ownable) Owner() std.Address { return o.owner } @@ -73,9 +75,11 @@ func (o Ownable) CallerIsOwner() error { if std.PrevRealm().Addr() == o.owner { return nil } + return ErrUnauthorized } +// AssertCallerIsOwner panics if the caller is not the owner func (o Ownable) AssertCallerIsOwner() { if std.PrevRealm().Addr() != o.owner { panic(ErrUnauthorized) diff --git a/examples/gno.land/p/demo/ownable/ownable_test.gno b/examples/gno.land/p/demo/ownable/ownable_test.gno index 6217948d587..a9d97154f45 100644 --- a/examples/gno.land/p/demo/ownable/ownable_test.gno +++ b/examples/gno.land/p/demo/ownable/ownable_test.gno @@ -9,52 +9,60 @@ import ( ) var ( - firstCaller = testutils.TestAddress("first") - secondCaller = testutils.TestAddress("second") + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") ) func TestNew(t *testing.T) { - std.TestSetRealm(std.NewUserRealm(firstCaller)) - std.TestSetOrigCaller(firstCaller) // TODO(bug): should not be needed + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) // TODO(bug): should not be needed o := New() got := o.Owner() - uassert.Equal(t, firstCaller, got) + if alice != got { + t.Fatalf("Expected %s, got: %s", alice, got) + } } func TestNewWithAddress(t *testing.T) { - o := NewWithAddress(firstCaller) + o := NewWithAddress(alice) got := o.Owner() - uassert.Equal(t, firstCaller, got) + if alice != got { + t.Fatalf("Expected %s, got: %s", alice, got) + } } func TestOwner(t *testing.T) { - std.TestSetRealm(std.NewUserRealm(firstCaller)) + std.TestSetRealm(std.NewUserRealm(alice)) o := New() - expected := firstCaller + expected := alice got := o.Owner() uassert.Equal(t, expected, got) } func TestTransferOwnership(t *testing.T) { - std.TestSetRealm(std.NewUserRealm(firstCaller)) + std.TestSetRealm(std.NewUserRealm(alice)) o := New() - err := o.TransferOwnership(secondCaller) - uassert.NoError(t, err, "TransferOwnership failed") + err := o.TransferOwnership(bob) + if err != nil { + t.Fatalf("TransferOwnership failed, %v", err) + } got := o.Owner() - uassert.Equal(t, secondCaller, got) + if bob != got { + t.Fatalf("Expected: %s, got: %s", bob, got) + } } func TestCallerIsOwner(t *testing.T) { - std.TestSetRealm(std.NewUserRealm(firstCaller)) + std.TestSetRealm(std.NewUserRealm(alice)) o := New() - unauthorizedCaller := secondCaller + unauthorizedCaller := bob std.TestSetRealm(std.NewUserRealm(unauthorizedCaller)) std.TestSetOrigCaller(unauthorizedCaller) // TODO(bug): should not be needed @@ -64,7 +72,7 @@ func TestCallerIsOwner(t *testing.T) { } func TestDropOwnership(t *testing.T) { - std.TestSetRealm(std.NewUserRealm(firstCaller)) + std.TestSetRealm(std.NewUserRealm(alice)) o := New() @@ -78,23 +86,25 @@ func TestDropOwnership(t *testing.T) { // Errors func TestErrUnauthorized(t *testing.T) { - std.TestSetRealm(std.NewUserRealm(firstCaller)) - std.TestSetOrigCaller(firstCaller) // TODO(bug): should not be needed + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) // TODO(bug): should not be needed o := New() - std.TestSetRealm(std.NewUserRealm(secondCaller)) - std.TestSetOrigCaller(secondCaller) // TODO(bug): should not be needed + std.TestSetRealm(std.NewUserRealm(bob)) + std.TestSetOrigCaller(bob) // TODO(bug): should not be needed - err := o.TransferOwnership(firstCaller) - uassert.ErrorContains(t, err, ErrUnauthorized.Error()) + err := o.TransferOwnership(alice) + if err != ErrUnauthorized { + t.Fatalf("Should've been ErrUnauthorized, was %v", err) + } err = o.DropOwnership() uassert.ErrorContains(t, err, ErrUnauthorized.Error()) } func TestErrInvalidAddress(t *testing.T) { - std.TestSetRealm(std.NewUserRealm(firstCaller)) + std.TestSetRealm(std.NewUserRealm(alice)) o := New() diff --git a/examples/gno.land/r/gnoland/events/administration.gno b/examples/gno.land/r/gnoland/events/administration.gno new file mode 100644 index 00000000000..02914adee69 --- /dev/null +++ b/examples/gno.land/r/gnoland/events/administration.gno @@ -0,0 +1,26 @@ +package events + +import ( + "std" + + "gno.land/p/demo/ownable/exts/authorizable" +) + +var ( + su = std.Address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5") // @leohhhn + auth = authorizable.NewAuthorizableWithAddress(su) +) + +// GetOwner gets the owner of the events realm +func GetOwner() std.Address { + return auth.Owner() +} + +// AddModerator adds a moderator to the events realm +func AddModerator(mod std.Address) { + auth.AssertCallerIsOwner() + + if err := auth.AddToAuthList(mod); err != nil { + panic(err) + } +} diff --git a/examples/gno.land/r/gnoland/events/errors.gno b/examples/gno.land/r/gnoland/events/errors.gno new file mode 100644 index 00000000000..788115015a4 --- /dev/null +++ b/examples/gno.land/r/gnoland/events/errors.gno @@ -0,0 +1,18 @@ +package events + +import ( + "errors" + "strconv" +) + +var ( + ErrEmptyName = errors.New("event name cannot be empty") + ErrNoSuchID = errors.New("event with specified ID does not exist") + ErrWidgetMinAmt = errors.New("you need to request at least 1 event to render") + ErrWidgetMaxAmt = errors.New("maximum number of events in widget is" + strconv.Itoa(MaxWidgetSize)) + ErrDescriptionTooLong = errors.New("event description is too long") + ErrInvalidStartTime = errors.New("invalid start time format") + ErrInvalidEndTime = errors.New("invalid end time format") + ErrEndBeforeStart = errors.New("end time cannot be before start time") + ErrStartEndTimezonemMismatch = errors.New("start and end timezones are not the same") +) diff --git a/examples/gno.land/r/gnoland/events/events.gno b/examples/gno.land/r/gnoland/events/events.gno index 9c2708a112e..0984edf75a9 100644 --- a/examples/gno.land/r/gnoland/events/events.gno +++ b/examples/gno.land/r/gnoland/events/events.gno @@ -1,240 +1,199 @@ +// Package events allows you to upload data about specific IRL/online events +// It includes dynamic support for updating rendering events based on their +// status, ie if they are upcoming, in progress, or in the past. package events import ( - "gno.land/p/demo/ui" -) - -// XXX: p/demo/ui API is crappy, we need to make it more idiomatic -// XXX: use an updatable block system to update content from a DAO -// XXX: var blocks avl.Tree - -func Render(_ string) string { - dom := ui.DOM{Prefix: "r/gnoland/events:"} - dom.Title = "Gno.land Core Team Attends Industry Events & Meetups" - dom.Classes = []string{"gno-tmpl-section"} + "sort" + "std" + "strings" + "time" - // body - dom.Body.Append(introSection()...) - dom.Body.Append(ui.HR{}) - dom.Body.Append(upcomingEvents()...) - dom.Body.Append(ui.HR{}) - dom.Body.Append(pastEvents()...) - - return dom.String() -} + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" +) -func introSection() ui.Element { - return ui.Element{ - ui.Paragraph("If you’re interested in building web3 with us, catch up with gno.land in person at one of our industry events. We’re looking to connect with developers and like-minded thinkers who can contribute to the growth of our platform."), +type ( + Event struct { + id string + name string // name of event + description string // short description of event + link string // link to auth corresponding web2 page, ie eventbrite/luma or conference page + location string // location of the event + startTime time.Time // given in RFC3339 + endTime time.Time // end time of the event, given in RFC3339 } -} - -func upcomingEvents() ui.Element { - return ui.Element{ - ui.H2("Upcoming Events"), - ui.Text(`
-
- -### GopherCon EU -- Come Meet Us at our Booth -- Berlin, June 17 - 20, 2024 - -[Learn More](https://gophercon.eu/) -
-
- -### GopherCon US -- Come Meet Us at our Booth -- Chicago, July 7 - 10, 2024 - -[Learn More](https://www.gophercon.com/) - -
- -
- -### Nebular Summit -- Join our workshop -- Brussels, July 12 - 13, 2024 + eventsSlice []*Event +) -[Learn More](https://nebular.builders/) -
+var ( + events = make(eventsSlice, 0) // sorted + idCounter seqid.ID +) -
+const ( + maxDescLength = 100 + EventAdded = "EventAdded" + EventDeleted = "EventDeleted" + EventEdited = "EventEdited" +) -
-
+// AddEvent adds auth new event +// Start time & end time need to be specified in RFC3339, ie 2024-08-08T12:00:00+02:00 +func AddEvent(name, description, link, location, startTime, endTime string) (string, error) { + auth.AssertOnAuthList() -
-
`), + if strings.TrimSpace(name) == "" { + return "", ErrEmptyName } -} - -func pastEvents() ui.Element { - return ui.Element{ - ui.H2("Past Events"), - ui.Text(`
- -
- -### Gno @ Golang Serbia - -- **Join the meetup** -- Belgrade, May 23, 2024 - -[Learn more](https://gno.land/r/gnoland/blog:p/gnomes-in-serbia) - -
- -
- -### Intro to Gno Tokyo - -- **Join the meetup** -- Tokyo, April 11, 2024 - -[Learn more](https://gno.land/r/gnoland/blog:p/gno-tokyo) - -
- -
- -### Go to Gno Seoul - -- **Join the workshop** -- Seoul, March 23, 2024 - -[Learn more](https://medium.com/onbloc/go-to-gno-recap-intro-to-the-gno-stack-with-memeland-284a43d7f620) - -
- -
- -### GopherCon US - -- **Come Meet Us at our Booth** -- San Diego, September 26 - 29, 2023 - -[Learn more](https://www.gophercon.com/) - -
-
- -### GopherCon EU - -- **Come Meet Us at our Booth** -- Berlin, July 26 - 29, 2023 - -[Learn more](https://gophercon.eu/) - -
- -
- -### Nebular Summit Gno.land for Developers - -- Paris, July 24 - 25, 2023 -- Manfred Touron - -[Learn more](https://www.nebular.builders/) - -
- -
- -### EthCC - -- **Come Meet Us at our Booth** -- Paris, July 17 - 20, 2023 -- Manfred Touron - -[Learn more](https://www.ethcc.io/) - -
- -
- -### Eth Seoul - -- **The Evolution of Smart Contracts: A Journey into Gno.land** -- Seoul, June 3, 2023 -- Manfred Touron - -[Learn more](https://2023.ethseoul.org/) + if len(description) > maxDescLength { + return "", ufmt.Errorf("%s: provided length is %d, maximum is %d", ErrDescriptionTooLong, len(description), maxDescLength) + } -
-
+ // Parse times + st, et, err := parseTimes(startTime, endTime) + if err != nil { + return "", err + } -### BUIDL Asia + id := idCounter.Next().String() + e := &Event{ + id: id, + name: name, + description: description, + link: link, + location: location, + startTime: st, + endTime: et, + } -- **Proof of Contribution in Gno.land** -- Seoul, June 6, 2023 -- Manfred Touron + events = append(events, e) + sort.Sort(events) -[Learn more](https://www.buidl.asia/) + std.Emit(EventAdded, + "id", + e.id, + ) -
-
+ return id, nil +} -### Game Developer Conference +// DeleteEvent deletes an event with auth given ID +func DeleteEvent(id string) { + auth.AssertOnAuthList() -- **Side Event: Web3 Gaming Apps Powered by Gno** -- San Francisco, Mach 23, 2023 -- Jae Kwon + e, idx, err := GetEventByID(id) + if err != nil { + panic(err) + } -[Watch the talk](https://www.youtube.com/watch?v=IJ0xel8lr4c) + events = append(events[:idx], events[idx+1:]...) -
-
+ std.Emit(EventDeleted, + "id", + e.id, + ) +} -### EthDenver +// EditEvent edits an event with auth given ID +// It only updates values corresponding to non-empty arguments sent with the call +// Note: if you need to update the start time or end time, you need to provide both every time +func EditEvent(id string, name, description, link, location, startTime, endTime string) { + auth.AssertOnAuthList() -- **Side Event: Discover Gno.land** -- Denver, Feb 24 - Mar 5, 2023 -- Jae Kwon + e, _, err := GetEventByID(id) + if err != nil { + panic(err) + } -[Watch the talk](https://www.youtube.com/watch?v=IJ0xel8lr4c) + // Set only valid values + if strings.TrimSpace(name) != "" { + e.name = name + } -
-
+ if strings.TrimSpace(description) != "" { + e.description = description + } -### Istanbul Blockchain Week + if strings.TrimSpace(link) != "" { + e.link = link + } -- Istanbul, Nov 14 - 17, 2022 -- Manfred Touron + if strings.TrimSpace(location) != "" { + e.location = location + } -[Watch the talk](https://www.youtube.com/watch?v=JX0gdWT0Cg4) + if strings.TrimSpace(startTime) != "" || strings.TrimSpace(endTime) != "" { + st, et, err := parseTimes(startTime, endTime) + if err != nil { + panic(err) // need to also revert other state changes + } -
-
+ oldStartTime := e.startTime + e.startTime = st + e.endTime = et -### Web Summit Buckle Up and Build with Cosmos + // If sort order was disrupted, sort again + if oldStartTime != e.startTime { + sort.Sort(events) + } + } -- Lisbon, Nov 1 - 4, 2022 -- Manfred Touron + std.Emit(EventEdited, + "id", + e.id, + ) +} -
-
+func GetEventByID(id string) (*Event, int, error) { + for i, event := range events { + if event.id == id { + return event, i, nil + } + } -### Cosmoverse + return nil, -1, ErrNoSuchID +} -- Medallin, Sept 26 - 28, 2022 -- Manfred Touron +// Len returns the length of the slice +func (m eventsSlice) Len() int { + return len(m) +} -[Watch the talk](https://www.youtube.com/watch?v=6s1zG7hgxMk) +// Less compares the startTime fields of two elements +// In this case, events will be sorted by largest startTime first (upcoming > past) +func (m eventsSlice) Less(i, j int) bool { + return m[i].startTime.After(m[j].startTime) +} -
-
+// Swap swaps two elements in the slice +func (m eventsSlice) Swap(i, j int) { + m[i], m[j] = m[j], m[i] +} -### Berlin Blockchain Week Buckle Up and Build with Cosmos +// parseTimes parses the start and end time for an event and checks for possible errors +func parseTimes(startTime, endTime string) (time.Time, time.Time, error) { + st, err := time.Parse(time.RFC3339, startTime) + if err != nil { + return time.Time{}, time.Time{}, ufmt.Errorf("%s: %s", ErrInvalidStartTime, err.Error()) + } -- Berlin, Sept 11 - 18, 2022 + et, err := time.Parse(time.RFC3339, endTime) + if err != nil { + return time.Time{}, time.Time{}, ufmt.Errorf("%s: %s", ErrInvalidEndTime, err.Error()) + } -[Watch the talk](https://www.youtube.com/watch?v=hCLErPgnavI) + if et.Before(st) { + return time.Time{}, time.Time{}, ErrEndBeforeStart + } -
-
`), + _, stOffset := st.Zone() + _, etOffset := et.Zone() + if stOffset != etOffset { + return time.Time{}, time.Time{}, ErrStartEndTimezonemMismatch } + + return st, et, nil } diff --git a/examples/gno.land/r/gnoland/events/events_filetest.gno b/examples/gno.land/r/gnoland/events/events_filetest.gno deleted file mode 100644 index 46ee273414d..00000000000 --- a/examples/gno.land/r/gnoland/events/events_filetest.gno +++ /dev/null @@ -1,226 +0,0 @@ -package main - -import "gno.land/r/gnoland/events" - -func main() { - println(events.Render("")) -} - -// Output: -//
-// -// # Gno.land Core Team Attends Industry Events & Meetups -// -// -// If you’re interested in building web3 with us, catch up with gno.land in person at one of our industry events. We’re looking to connect with developers and like-minded thinkers who can contribute to the growth of our platform. -// -// -// --- -// -// ## Upcoming Events -// -//
-//
-// -// ### GopherCon EU -// - Come Meet Us at our Booth -// - Berlin, June 17 - 20, 2024 -// -// [Learn More](https://gophercon.eu/) -//
-// -//
-// -// ### GopherCon US -// - Come Meet Us at our Booth -// - Chicago, July 7 - 10, 2024 -// -// [Learn More](https://www.gophercon.com/) -// -//
-// -//
-// -// ### Nebular Summit -// - Join our workshop -// - Brussels, July 12 - 13, 2024 -// -// [Learn More](https://nebular.builders/) -//
-// -//
-// -//
-//
-// -//
-//
-// -// --- -// -// ## Past Events -// -//
-// -//
-// -// ### Gno @ Golang Serbia -// -// - **Join the meetup** -// - Belgrade, May 23, 2024 -// -// [Learn more](https://gno.land/r/gnoland/blog:p/gnomes-in-serbia) -// -//
-// -//
-// -// ### Intro to Gno Tokyo -// -// - **Join the meetup** -// - Tokyo, April 11, 2024 -// -// [Learn more](https://gno.land/r/gnoland/blog:p/gno-tokyo) -// -//
-// -//
-// -// ### Go to Gno Seoul -// -// - **Join the workshop** -// - Seoul, March 23, 2024 -// -// [Learn more](https://medium.com/onbloc/go-to-gno-recap-intro-to-the-gno-stack-with-memeland-284a43d7f620) -// -//
-// -//
-// -// ### GopherCon US -// -// - **Come Meet Us at our Booth** -// - San Diego, September 26 - 29, 2023 -// -// [Learn more](https://www.gophercon.com/) -// -//
-// -//
-// -// ### GopherCon EU -// -// - **Come Meet Us at our Booth** -// - Berlin, July 26 - 29, 2023 -// -// [Learn more](https://gophercon.eu/) -// -//
-// -//
-// -// ### Nebular Summit Gno.land for Developers -// -// - Paris, July 24 - 25, 2023 -// - Manfred Touron -// -// [Learn more](https://www.nebular.builders/) -// -//
-// -//
-// -// ### EthCC -// -// - **Come Meet Us at our Booth** -// - Paris, July 17 - 20, 2023 -// - Manfred Touron -// -// [Learn more](https://www.ethcc.io/) -// -//
-// -//
-// -// ### Eth Seoul -// -// - **The Evolution of Smart Contracts: A Journey into Gno.land** -// - Seoul, June 3, 2023 -// - Manfred Touron -// -// [Learn more](https://2023.ethseoul.org/) -// -//
-//
-// -// ### BUIDL Asia -// -// - **Proof of Contribution in Gno.land** -// - Seoul, June 6, 2023 -// - Manfred Touron -// -// [Learn more](https://www.buidl.asia/) -// -//
-//
-// -// ### Game Developer Conference -// -// - **Side Event: Web3 Gaming Apps Powered by Gno** -// - San Francisco, Mach 23, 2023 -// - Jae Kwon -// -// [Watch the talk](https://www.youtube.com/watch?v=IJ0xel8lr4c) -// -//
-//
-// -// ### EthDenver -// -// - **Side Event: Discover Gno.land** -// - Denver, Feb 24 - Mar 5, 2023 -// - Jae Kwon -// -// [Watch the talk](https://www.youtube.com/watch?v=IJ0xel8lr4c) -// -//
-//
-// -// ### Istanbul Blockchain Week -// -// - Istanbul, Nov 14 - 17, 2022 -// - Manfred Touron -// -// [Watch the talk](https://www.youtube.com/watch?v=JX0gdWT0Cg4) -// -//
-//
-// -// ### Web Summit Buckle Up and Build with Cosmos -// -// - Lisbon, Nov 1 - 4, 2022 -// - Manfred Touron -// -//
-//
-// -// ### Cosmoverse -// -// - Medallin, Sept 26 - 28, 2022 -// - Manfred Touron -// -// [Watch the talk](https://www.youtube.com/watch?v=6s1zG7hgxMk) -// -//
-//
-// -// ### Berlin Blockchain Week Buckle Up and Build with Cosmos -// -// - Berlin, Sept 11 - 18, 2022 -// -// [Watch the talk](https://www.youtube.com/watch?v=hCLErPgnavI) -// -//
-//
-// -//
diff --git a/examples/gno.land/r/gnoland/events/events_test.gno b/examples/gno.land/r/gnoland/events/events_test.gno new file mode 100644 index 00000000000..1e5625e0b2c --- /dev/null +++ b/examples/gno.land/r/gnoland/events/events_test.gno @@ -0,0 +1,161 @@ +package events + +import ( + "std" + "strings" + "testing" + "time" + + "gno.land/p/demo/uassert" +) + +var ( + suRealm = std.NewUserRealm(su) + + now = "2009-02-13T23:31:30Z" // time.Now() is hardcoded to this value in the gno test machine currently + parsedTimeNow, _ = time.Parse(time.RFC3339, now) +) + +func TestAddEvent(t *testing.T) { + std.TestSetOrigCaller(su) + std.TestSetRealm(suRealm) + + e1Start := parsedTimeNow.Add(time.Hour * 24 * 5) + e1End := e1Start.Add(time.Hour * 4) + + AddEvent("Event 1", "this event is upcoming", "gno.land", "gnome land", e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339)) + + got := renderHome(false) + + if !strings.Contains(got, "Event 1") { + t.Fatalf("Expected to find Event 1 in render") + } + + e2Start := parsedTimeNow.Add(-time.Hour * 24 * 5) + e2End := e2Start.Add(time.Hour * 4) + + AddEvent("Event 2", "this event is in the past", "gno.land", "gnome land", e2Start.Format(time.RFC3339), e2End.Format(time.RFC3339)) + + got = renderHome(false) + + upcomingPos := strings.Index(got, "## Upcoming events") + pastPos := strings.Index(got, "## Past events") + + e1Pos := strings.Index(got, "Event 1") + e2Pos := strings.Index(got, "Event 2") + + // expected index ordering: upcoming < e1 < past < e2 + if e1Pos < upcomingPos || e1Pos > pastPos { + t.Fatalf("Expected to find Event 1 in Upcoming events") + } + + if e2Pos < upcomingPos || e2Pos < pastPos || e2Pos < e1Pos { + t.Fatalf("Expected to find Event 2 on auth different pos") + } + + // larger index => smaller startTime (future => past) + if events[0].startTime.Unix() < events[1].startTime.Unix() { + t.Fatalf("expected ordering to be different") + } +} + +func TestAddEventErrors(t *testing.T) { + std.TestSetOrigCaller(su) + std.TestSetRealm(suRealm) + + _, err := AddEvent("", "sample desc", "gno.land", "gnome land", "2009-02-13T23:31:31Z", "2009-02-13T23:33:31Z") + uassert.ErrorIs(t, err, ErrEmptyName) + + _, err = AddEvent("sample name", "sample desc", "gno.land", "gnome land", "", "2009-02-13T23:33:31Z") + uassert.ErrorContains(t, err, ErrInvalidStartTime.Error()) + + _, err = AddEvent("sample name", "sample desc", "gno.land", "gnome land", "2009-02-13T23:31:31Z", "") + uassert.ErrorContains(t, err, ErrInvalidEndTime.Error()) + + _, err = AddEvent("sample name", "sample desc", "gno.land", "gnome land", "2009-02-13T23:31:31Z", "2009-02-13T23:30:31Z") + uassert.ErrorIs(t, err, ErrEndBeforeStart) + + _, err = AddEvent("sample name", "sample desc", "gno.land", "gnome land", "2009-02-13T23:31:31+06:00", "2009-02-13T23:33:31+02:00") + uassert.ErrorIs(t, err, ErrStartEndTimezonemMismatch) + + tooLongDesc := `Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean ma` + _, err = AddEvent("sample name", tooLongDesc, "gno.land", "gnome land", "2009-02-13T23:31:31Z", "2009-02-13T23:33:31Z") + uassert.ErrorContains(t, err, ErrDescriptionTooLong.Error()) +} + +func TestDeleteEvent(t *testing.T) { + events = nil // remove elements from previous tests - see issue #1982 + + e1Start := parsedTimeNow.Add(time.Hour * 24 * 5) + e1End := e1Start.Add(time.Hour * 4) + + id, _ := AddEvent("ToDelete", "description", "gno.land", "gnome land", e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339)) + + got := renderHome(false) + + if !strings.Contains(got, "ToDelete") { + t.Fatalf("Expected to find ToDelete event in render") + } + + DeleteEvent(id) + got = renderHome(false) + + if strings.Contains(got, "ToDelete") { + t.Fatalf("Did not expect to find ToDelete event in render") + } +} + +func TestEditEvent(t *testing.T) { + events = nil // remove elements from previous tests - see issue #1982 + + e1Start := parsedTimeNow.Add(time.Hour * 24 * 5) + e1End := e1Start.Add(time.Hour * 4) + loc := "gnome land" + + id, _ := AddEvent("ToDelete", "description", "gno.land", loc, e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339)) + + newName := "New Name" + newDesc := "Normal description" + newLink := "new Link" + newST := e1Start.Add(time.Hour) + newET := newST.Add(time.Hour) + + EditEvent(id, newName, newDesc, newLink, "", newST.Format(time.RFC3339), newET.Format(time.RFC3339)) + edited, _, _ := GetEventByID(id) + + // Check updated values + uassert.Equal(t, edited.name, newName) + uassert.Equal(t, edited.description, newDesc) + uassert.Equal(t, edited.link, newLink) + uassert.True(t, edited.startTime.Equal(newST)) + uassert.True(t, edited.endTime.Equal(newET)) + + // Check if the old values are the same + uassert.Equal(t, edited.location, loc) +} + +func TestInvalidEdit(t *testing.T) { + events = nil // remove elements from previous tests - see issue #1982 + + uassert.PanicsWithMessage(t, ErrNoSuchID.Error(), func() { + EditEvent("123123", "", "", "", "", "", "") + }) +} + +func TestParseTimes(t *testing.T) { + // times not provided + // end time before start time + // timezone Missmatch + + _, _, err := parseTimes("", "") + uassert.ErrorContains(t, err, ErrInvalidStartTime.Error()) + + _, _, err = parseTimes(now, "") + uassert.ErrorContains(t, err, ErrInvalidEndTime.Error()) + + _, _, err = parseTimes("2009-02-13T23:30:30Z", "2009-02-13T21:30:30Z") + uassert.ErrorContains(t, err, ErrEndBeforeStart.Error()) + + _, _, err = parseTimes("2009-02-10T23:30:30+02:00", "2009-02-13T21:30:33+05:00") + uassert.ErrorContains(t, err, ErrStartEndTimezonemMismatch.Error()) +} diff --git a/examples/gno.land/r/gnoland/events/gno.mod b/examples/gno.land/r/gnoland/events/gno.mod index ec781c7cf10..5a4c6ac56f3 100644 --- a/examples/gno.land/r/gnoland/events/gno.mod +++ b/examples/gno.land/r/gnoland/events/gno.mod @@ -1,3 +1,8 @@ module gno.land/r/gnoland/events -require gno.land/p/demo/ui v0.0.0-latest +require ( + gno.land/p/demo/ownable/exts/authorizable v0.0.0-latest + gno.land/p/demo/seqid v0.0.0-latest + gno.land/p/demo/uassert v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest +) diff --git a/examples/gno.land/r/gnoland/events/rendering.gno b/examples/gno.land/r/gnoland/events/rendering.gno new file mode 100644 index 00000000000..bde32065d27 --- /dev/null +++ b/examples/gno.land/r/gnoland/events/rendering.gno @@ -0,0 +1,145 @@ +package events + +import ( + "bytes" + "time" + + "gno.land/p/demo/ufmt" +) + +const ( + MaxWidgetSize = 5 +) + +// RenderEventWidget shows up to eventsToRender of the latest events to a caller +func RenderEventWidget(eventsToRender int) (string, error) { + numOfEvents := len(events) + if numOfEvents == 0 { + return "No events.", nil + } + + if eventsToRender > MaxWidgetSize { + return "", ErrWidgetMaxAmt + } + + if eventsToRender < 1 { + return "", ErrWidgetMinAmt + } + + if eventsToRender > numOfEvents { + eventsToRender = numOfEvents + } + + output := "" + + for _, event := range events[eventsToRender:] { + output += ufmt.Sprintf("- [%s](%s)\n", event.name, event.link) + } + + return output, nil +} + +// renderHome renders the home page of the events realm +func renderHome(admin bool) string { + output := "# gno.land events\n\n" + + if len(events) == 0 { + output += "No upcoming or past events." + return output + } + + output += "Below is a list of all gno.land events, including in progress, upcoming, and past ones.\n\n" + output += "---\n\n" + + var ( + inProgress = "" + upcoming = "" + past = "" + now = time.Now() + ) + + for _, e := range events { + if now.Before(e.startTime) { + upcoming += e.Render(admin) + } else if now.After(e.endTime) { + past += e.Render(admin) + } else { + inProgress += e.Render(admin) + } + } + + if upcoming != "" { + // Add upcoming events + output += "## Upcoming events\n\n" + output += "
" + + output += upcoming + + output += "
\n\n" + output += "---\n\n" + } + + if inProgress != "" { + output += "## Currently in progress\n\n" + output += "
" + + output += inProgress + + output += "
\n\n" + output += "---\n\n" + } + + if past != "" { + // Add past events + output += "## Past events\n\n" + output += "
" + + output += past + + output += "
\n\n" + } + + return output +} + +// Render returns the markdown representation of a single event instance +func (e Event) Render(admin bool) string { + var buf bytes.Buffer + + buf.WriteString("
\n\n") + buf.WriteString(ufmt.Sprintf("### %s\n\n", e.name)) + buf.WriteString(ufmt.Sprintf("%s\n\n", e.description)) + buf.WriteString(ufmt.Sprintf("**Location:** %s\n\n", e.location)) + + _, offset := e.startTime.Zone() // offset is in seconds + hoursOffset := offset / (60 * 60) + sign := "" + if offset >= 0 { + sign = "+" + } + + buf.WriteString(ufmt.Sprintf("**Starts:** %s UTC%s%d\n\n", e.startTime.Format("02 Jan 2006, 03:04 PM"), sign, hoursOffset)) + buf.WriteString(ufmt.Sprintf("**Ends:** %s UTC%s%d\n\n", e.endTime.Format("02 Jan 2006, 03:04 PM"), sign, hoursOffset)) + + if admin { + buf.WriteString(ufmt.Sprintf("[EDIT](/r/gnoland/events?help&__func=EditEvent&id=%s)\n\n", e.id)) + buf.WriteString(ufmt.Sprintf("[DELETE](/r/gnoland/events?help&__func=DeleteEvent&id=%s)\n\n", e.id)) + } + + if e.link != "" { + buf.WriteString(ufmt.Sprintf("[See more](%s)\n\n", e.link)) + } + + buf.WriteString("
") + + return buf.String() +} + +// Render is the main rendering entry point +func Render(path string) string { + if path == "admin" { + return renderHome(true) + } + + return renderHome(false) +} diff --git a/examples/gno.land/r/gnoland/home/gno.mod b/examples/gno.land/r/gnoland/home/gno.mod index cb2ec58b665..c208ad421c9 100644 --- a/examples/gno.land/r/gnoland/home/gno.mod +++ b/examples/gno.land/r/gnoland/home/gno.mod @@ -5,4 +5,5 @@ require ( gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/ui v0.0.0-latest gno.land/r/gnoland/blog v0.0.0-latest + gno.land/r/gnoland/events v0.0.0-latest ) diff --git a/examples/gno.land/r/gnoland/home/home.gno b/examples/gno.land/r/gnoland/home/home.gno index 62984711d79..e5f24f87135 100644 --- a/examples/gno.land/r/gnoland/home/home.gno +++ b/examples/gno.land/r/gnoland/home/home.gno @@ -7,6 +7,7 @@ import ( "gno.land/p/demo/ufmt" "gno.land/p/demo/ui" blog "gno.land/r/gnoland/blog" + events "gno.land/r/gnoland/events" ) // XXX: p/demo/ui API is crappy, we need to make it more idiomatic @@ -35,7 +36,7 @@ func Render(_ string) string { dom.Body.Append( ui.Columns{3, []ui.Element{ lastBlogposts(4), - upcomingEvents(4), + upcomingEvents(), lastContributions(4), }}, ) @@ -68,7 +69,7 @@ func Render(_ string) string { func lastBlogposts(limit int) ui.Element { posts := blog.RenderLastPostsWidget(limit) return ui.Element{ - ui.H3("Latest Blogposts"), + ui.H3("[Latest Blogposts](/r/gnoland/blog)"), ui.Text(posts), } } @@ -81,11 +82,11 @@ func lastContributions(limit int) ui.Element { } } -func upcomingEvents(limit int) ui.Element { +func upcomingEvents() ui.Element { + out, _ := events.RenderEventWidget(events.MaxWidgetSize) return ui.Element{ - ui.H3("Upcoming Events"), - // TODO: replace with r/gnoland/events - ui.Text("[View upcoming events](/events)"), + ui.H3("[Latest Events](/r/gnoland/events)"), + ui.Text(out), } } diff --git a/examples/gno.land/r/gnoland/home/home_filetest.gno b/examples/gno.land/r/gnoland/home/home_filetest.gno index 2b0a802718f..15f329683f4 100644 --- a/examples/gno.land/r/gnoland/home/home_filetest.gno +++ b/examples/gno.land/r/gnoland/home/home_filetest.gno @@ -68,15 +68,15 @@ func main() { //
//
// -// ### Latest Blogposts +// ### [Latest Blogposts](/r/gnoland/blog) // // No posts. //
//
// -// ### Upcoming Events +// ### [Latest Events](/r/gnoland/events) // -// [View upcoming events](/events) +// No events. //
//
// diff --git a/examples/gno.land/r/gnoland/home/overide_filetest.gno b/examples/gno.land/r/gnoland/home/overide_filetest.gno index 34356b93349..4f21b90a3c2 100644 --- a/examples/gno.land/r/gnoland/home/overide_filetest.gno +++ b/examples/gno.land/r/gnoland/home/overide_filetest.gno @@ -21,4 +21,4 @@ func main() { // Output: // Hello World! -// r: unauthorized; caller is not owner +// r: ownable: caller is not owner