From c85c530bc13c1e43af08a8014a680f86b98b3a93 Mon Sep 17 00:00:00 2001 From: AKP Date: Sun, 3 Apr 2022 20:59:21 +0100 Subject: Implement many things Signed-off-by: AKP --- go.mod | 2 +- walrss/internal/core/feeds.go | 76 ++++++++ walrss/internal/core/validation.go | 20 ++ walrss/internal/db/db.go | 7 + walrss/internal/http/edit.go | 50 +++++ walrss/internal/http/http.go | 11 ++ walrss/internal/http/mainpage.go | 6 + walrss/internal/http/new.go | 36 ++++ walrss/internal/http/views/main.qtpl.html | 151 +++++++++++++--- walrss/internal/http/views/main.qtpl.html.go | 261 ++++++++++++++++++++++++--- walrss/internal/http/views/page.qtpl.html | 1 + walrss/internal/http/views/page.qtpl.html.go | 1 + walrss/internal/urls/urls.go | 29 ++- 13 files changed, 591 insertions(+), 60 deletions(-) create mode 100644 walrss/internal/core/feeds.go create mode 100644 walrss/internal/http/new.go diff --git a/go.mod b/go.mod index 6a3149e..90a6f33 100644 --- a/go.mod +++ b/go.mod @@ -23,4 +23,4 @@ require ( golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect -) +) \ No newline at end of file diff --git a/walrss/internal/core/feeds.go b/walrss/internal/core/feeds.go new file mode 100644 index 0000000..c9e4022 --- /dev/null +++ b/walrss/internal/core/feeds.go @@ -0,0 +1,76 @@ +package core + +import ( + "errors" + "github.com/codemicro/walrss/walrss/internal/db" + "github.com/codemicro/walrss/walrss/internal/state" + "github.com/lithammer/shortuuid/v4" + bh "github.com/timshannon/bolthold" +) + +func NewFeed(st *state.State, userID, name, url string) (*db.Feed, error) { + if err := validateFeedName(name); err != nil { + return nil, err + } + + if err := validateURL(url); err != nil { + return nil, err + } + + feed := &db.Feed{ + ID: shortuuid.New(), + URL: url, + Name: name, + UserID: userID, + } + + if err := st.Data.Insert(feed.ID, feed); err != nil { + return nil, err + } + + return feed, nil +} + +func GetFeedsForUser(st *state.State, userID string) ([]*db.Feed, error) { + var feeds []*db.Feed + if err := st.Data.Find(&feeds, bh.Where("UserID").Eq(userID)); err != nil { + return nil, err + } + return feeds, nil +} + +func GetFeed(st *state.State, id string) (*db.Feed, error) { + feed := new(db.Feed) + if err := st.Data.FindOne(feed, bh.Where("ID").Eq(id)); err != nil { + if errors.Is(err, bh.ErrNotFound) { + return nil, ErrNotFound + } + return nil, err + } + return feed, nil +} + +func DeleteFeed(st *state.State, id string) error { + if err := st.Data.Delete(id, new(db.Feed)); err != nil { + return err + } + return nil +} + +func UpdateFeed(st *state.State, feed *db.Feed) error { + if err := validateFeedName(feed.Name); err != nil { + return err + } + + if err := validateURL(feed.URL); err != nil { + return err + } + + if err := st.Data.Update(feed.ID, feed); err != nil { + if errors.Is(err, bh.ErrNotFound) { + return ErrNotFound + } + return err + } + return nil +} diff --git a/walrss/internal/core/validation.go b/walrss/internal/core/validation.go index 501c023..e7c323c 100644 --- a/walrss/internal/core/validation.go +++ b/walrss/internal/core/validation.go @@ -1,7 +1,9 @@ package core import ( + "net/url" "regexp" + "strings" ) var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") @@ -19,3 +21,21 @@ func validatePassword(password string) error { } return nil } + +func validateFeedName(name string) error { + if strings.TrimSpace(name) == "" { + return NewUserError("feed name cannot be blank") + } + return nil +} + +func validateURL(inputURL string) error { + u, err := url.ParseRequestURI(inputURL) + if err != nil { + return NewUserError("invalid URL") + } + if s := strings.ToLower(u.Scheme); !(s == "http" || s == "https") { + return NewUserError("invalid URL request scheme - must be HTTP or HTTPS") + } + return nil +} diff --git a/walrss/internal/db/db.go b/walrss/internal/db/db.go index eb8590a..5139af8 100644 --- a/walrss/internal/db/db.go +++ b/walrss/internal/db/db.go @@ -28,3 +28,10 @@ type User struct { Hour int `boltholdIndex:"Hour"` } } + +type Feed struct { + ID string `boltholdKey:""` + URL string + Name string + UserID string `boldholdIndex:"UserID"` +} diff --git a/walrss/internal/http/edit.go b/walrss/internal/http/edit.go index 0c5bd95..2cfc545 100644 --- a/walrss/internal/http/edit.go +++ b/walrss/internal/http/edit.go @@ -81,3 +81,53 @@ func (s *Server) editTimings(ctx *fiber.Ctx) error { SelectedTime: user.Schedule.Hour, }).RenderScheduleCard()) } + +func (s *Server) editFeedItem(ctx *fiber.Ctx) error { + currentUserID := getCurrentUserID(ctx) + if currentUserID == "" { + return requestFragmentSignIn(ctx, urls.Index) + } + + feedID := ctx.Params("id") + + feed, err := core.GetFeed(s.state, feedID) + if err != nil { + return err + } + + switch ctx.Method() { + case fiber.MethodGet: + return ctx.SendString(views.RenderFeedEditRow(feed.ID, feed.Name, feed.URL)) + case fiber.MethodDelete: + if err := core.DeleteFeed(s.state, feed.ID); err != nil { + return err + } + return nil + case fiber.MethodPut: + feed.Name = ctx.FormValue("name") + feed.URL = ctx.FormValue("url") + + if err := core.UpdateFeed(s.state, feed); err != nil { + return err + } + return ctx.SendString(views.RenderFeedRow(feed.ID, feed.Name, feed.URL)) + } + + panic("unreachable") +} + +func (s *Server) cancelEditFeedItem(ctx *fiber.Ctx) error { + currentUserID := getCurrentUserID(ctx) + if currentUserID == "" { + return requestFragmentSignIn(ctx, urls.Index) + } + + feedID := ctx.Params("id") + + feed, err := core.GetFeed(s.state, feedID) + if err != nil { + return err + } + + return ctx.SendString(views.RenderFeedRow(feed.ID, feed.Name, feed.URL)) +} diff --git a/walrss/internal/http/http.go b/walrss/internal/http/http.go index 603002b..813cb83 100644 --- a/walrss/internal/http/http.go +++ b/walrss/internal/http/http.go @@ -1,6 +1,7 @@ package http import ( + "fmt" "github.com/codemicro/walrss/walrss/internal/core" "github.com/codemicro/walrss/walrss/internal/http/views" "github.com/codemicro/walrss/walrss/internal/state" @@ -28,6 +29,8 @@ func New(st *state.State) (*Server, error) { DisableStartupMessage: !st.Config.Debug, AppName: "Walrss", ErrorHandler: func(ctx *fiber.Ctx, err error) error { + fmt.Println("Got ERROR", err) + code := fiber.StatusInternalServerError msg := "Internal Server Error" @@ -81,6 +84,14 @@ func (s *Server) registerHandlers() { s.app.Put(urls.EditEnabledState, s.editEnabledState) s.app.Put(urls.EditTimings, s.editTimings) + + s.app.Get(urls.EditFeedItem, s.editFeedItem) + s.app.Put(urls.EditFeedItem, s.editFeedItem) + s.app.Delete(urls.EditFeedItem, s.editFeedItem) + s.app.Get(urls.CancelEditFeedItem, s.cancelEditFeedItem) + + s.app.Get(urls.NewFeedItem, s.newFeedItem) + s.app.Post(urls.NewFeedItem, s.newFeedItem) } func (s *Server) Run() error { diff --git a/walrss/internal/http/mainpage.go b/walrss/internal/http/mainpage.go index 6c60753..1d94d2a 100644 --- a/walrss/internal/http/mainpage.go +++ b/walrss/internal/http/mainpage.go @@ -17,9 +17,15 @@ func (s *Server) mainPage(ctx *fiber.Ctx) error { return err } + feeds, err := core.GetFeedsForUser(s.state, currentUserID) + if err != nil { + return err + } + return views.SendPage(ctx, &views.MainPage{ EnableDigests: user.Schedule.Active, SelectedDay: user.Schedule.Day, SelectedTime: user.Schedule.Hour, + Feeds: feeds, }) } diff --git a/walrss/internal/http/new.go b/walrss/internal/http/new.go new file mode 100644 index 0000000..0932123 --- /dev/null +++ b/walrss/internal/http/new.go @@ -0,0 +1,36 @@ +package http + +import ( + "github.com/codemicro/walrss/walrss/internal/core" + "github.com/codemicro/walrss/walrss/internal/http/views" + "github.com/codemicro/walrss/walrss/internal/urls" + "github.com/gofiber/fiber/v2" +) + +func (s *Server) newFeedItem(ctx *fiber.Ctx) error { + + currentUserID := getCurrentUserID(ctx) + if currentUserID == "" { + return requestFragmentSignIn(ctx, urls.Index) + } + + switch ctx.Method() { + case fiber.MethodGet: + return ctx.SendString(views.RenderNewFeedItemRow()) + case fiber.MethodPost: + feed, err := core.NewFeed( + s.state, + currentUserID, + ctx.FormValue("name"), + ctx.FormValue("url"), + ) + + if err != nil { + return err + } + + return ctx.SendString(views.RenderFeedRow(feed.ID, feed.Name, feed.URL)) + } + + panic("unreachable") +} diff --git a/walrss/internal/http/views/main.qtpl.html b/walrss/internal/http/views/main.qtpl.html index 60a19eb..34a92c6 100644 --- a/walrss/internal/http/views/main.qtpl.html +++ b/walrss/internal/http/views/main.qtpl.html @@ -1,11 +1,13 @@ {% import "github.com/codemicro/walrss/walrss/internal/db" %} {% import "github.com/codemicro/walrss/walrss/internal/urls" %} +{% import "github.com/lithammer/shortuuid/v4" %} {% code type MainPage struct { BasePage EnableDigests bool SelectedDay db.SendDay SelectedTime int + Feeds []*db.Feed } %} {% func (p *MainPage) Title() %}{% endfunc %} @@ -31,15 +33,19 @@ new bootstrap.Toast(toast, {delay: delay}).show(); } - function errorHandler() { - toastBody.innerText = "Internal error: action incomplete"; + function errorHandler(text) { + toastBody.innerText = "Error: " + text; toast.classList.remove("bg-success") toast.classList.add("bg-danger") showToast(5000) } - document.body.addEventListener("htmx:sendError", errorHandler); - document.body.addEventListener("htmx:responseError", errorHandler); + document.body.addEventListener("htmx:sendError", function () { + errorHandler("could not communicate with server"); + }); + document.body.addEventListener("htmx:responseError", function (evt) { + errorHandler(evt.detail.xhr.response) + }); document.body.addEventListener("successResponse", function () { toastBody.innerText = "Success!" @@ -62,31 +68,28 @@ - - - - - - + + + + + - - - - - - - - - - - - - - - - - - + + {% for _, feed := range p.Feeds %} + {%= RenderFeedRow(feed.ID, feed.Name, feed.URL) %} + {% endfor %}
#FirstLastHandle
NameURL +
+ +
+
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
@@ -96,6 +99,98 @@ {% endfunc %} +{% func RenderFeedRow(id, name, url string) %} + + {%s name %} + {%s url %} + +
+ + +
+ + +{% endfunc %} + +{% func RenderFeedEditRow(id, name, url string) %} + + + + +
+ + +
+ + +{% endfunc %} + +{% func RenderNewFeedItemRow() %} +{% code id := shortuuid.New() %} + + + + +
+ + +
+ + + + +{% endfunc %} + {% func (p *MainPage) RenderScheduleCard() %}
diff --git a/walrss/internal/http/views/main.qtpl.html.go b/walrss/internal/http/views/main.qtpl.html.go index d18a5ac..55be033 100644 --- a/walrss/internal/http/views/main.qtpl.html.go +++ b/walrss/internal/http/views/main.qtpl.html.go @@ -7,6 +7,8 @@ import "github.com/codemicro/walrss/walrss/internal/db" import "github.com/codemicro/walrss/walrss/internal/urls" +import "github.com/lithammer/shortuuid/v4" + import ( qtio422016 "io" @@ -23,6 +25,7 @@ type MainPage struct { EnableDigests bool SelectedDay db.SendDay SelectedTime int + Feeds []*db.Feed } func (p *MainPage) StreamTitle(qw422016 *qt422016.Writer) { @@ -67,15 +70,19 @@ func (p *MainPage) StreamBody(qw422016 *qt422016.Writer) { new bootstrap.Toast(toast, {delay: delay}).show(); } - function errorHandler() { - toastBody.innerText = "Internal error: action incomplete"; + function errorHandler(text) { + toastBody.innerText = "Error: " + text; toast.classList.remove("bg-success") toast.classList.add("bg-danger") showToast(5000) } - document.body.addEventListener("htmx:sendError", errorHandler); - document.body.addEventListener("htmx:responseError", errorHandler); + document.body.addEventListener("htmx:sendError", function () { + errorHandler("could not communicate with server"); + }); + document.body.addEventListener("htmx:responseError", function (evt) { + errorHandler(evt.detail.xhr.response) + }); document.body.addEventListener("successResponse", function () { toastBody.innerText = "Success!" @@ -100,31 +107,36 @@ func (p *MainPage) StreamBody(qw422016 *qt422016.Writer) { - - - - - - + + + + + - - - - - - - - - - - - - - - - - - + + `) + for _, feed := range p.Feeds { + qw422016.N().S(` + `) + StreamRenderFeedRow(qw422016, feed.ID, feed.Name, feed.URL) + qw422016.N().S(` + `) + } + qw422016.N().S(`
#FirstLastHandle
NameURL +
+ +
+
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
@@ -149,6 +161,199 @@ func (p *MainPage) Body() string { return qs422016 } +func StreamRenderFeedRow(qw422016 *qt422016.Writer, id, name, url string) { + qw422016.N().S(` + + `) + qw422016.E().S(name) + qw422016.N().S(` + `) + qw422016.E().S(url) + qw422016.N().S(` + +
+ + +
+ + +`) +} + +func WriteRenderFeedRow(qq422016 qtio422016.Writer, id, name, url string) { + qw422016 := qt422016.AcquireWriter(qq422016) + StreamRenderFeedRow(qw422016, id, name, url) + qt422016.ReleaseWriter(qw422016) +} + +func RenderFeedRow(id, name, url string) string { + qb422016 := qt422016.AcquireByteBuffer() + WriteRenderFeedRow(qb422016, id, name, url) + qs422016 := string(qb422016.B) + qt422016.ReleaseByteBuffer(qb422016) + return qs422016 +} + +func StreamRenderFeedEditRow(qw422016 *qt422016.Writer, id, name, url string) { + qw422016.N().S(` + + + + +
+ + +
+ + +`) +} + +func WriteRenderFeedEditRow(qq422016 qtio422016.Writer, id, name, url string) { + qw422016 := qt422016.AcquireWriter(qq422016) + StreamRenderFeedEditRow(qw422016, id, name, url) + qt422016.ReleaseWriter(qw422016) +} + +func RenderFeedEditRow(id, name, url string) string { + qb422016 := qt422016.AcquireByteBuffer() + WriteRenderFeedEditRow(qb422016, id, name, url) + qs422016 := string(qb422016.B) + qt422016.ReleaseByteBuffer(qb422016) + return qs422016 +} + +func StreamRenderNewFeedItemRow(qw422016 *qt422016.Writer) { + qw422016.N().S(` +`) + id := shortuuid.New() + + qw422016.N().S(` + + + + +
+ + +
+ + + + +`) +} + +func WriteRenderNewFeedItemRow(qq422016 qtio422016.Writer) { + qw422016 := qt422016.AcquireWriter(qq422016) + StreamRenderNewFeedItemRow(qw422016) + qt422016.ReleaseWriter(qw422016) +} + +func RenderNewFeedItemRow() string { + qb422016 := qt422016.AcquireByteBuffer() + WriteRenderNewFeedItemRow(qb422016) + qs422016 := string(qb422016.B) + qt422016.ReleaseByteBuffer(qb422016) + return qs422016 +} + func (p *MainPage) StreamRenderScheduleCard(qw422016 *qt422016.Writer) { qw422016.N().S(`
diff --git a/walrss/internal/http/views/page.qtpl.html b/walrss/internal/http/views/page.qtpl.html index 9e9c0c2..872b468 100644 --- a/walrss/internal/http/views/page.qtpl.html +++ b/walrss/internal/http/views/page.qtpl.html @@ -15,6 +15,7 @@ Page prints a page implementing Page interface. +