From be929058cf31d06be7c6714177889c7f90f15b7f Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Sat, 20 Feb 2021 15:42:45 +0100 Subject: [PATCH] Save notifications to DB, Notificationsadmin --- databaseMigrations.go | 9 ++ go.mod | 9 +- go.sum | 19 ++-- http.go | 9 ++ notifications.go | 146 +++++++++++++++++++++++++++- render.go | 37 +++---- templates/commentsadmin.gohtml | 2 +- templates/notificationsadmin.gohtml | 26 +++++ templates/strings/default.yaml | 1 + 9 files changed, 225 insertions(+), 33 deletions(-) create mode 100644 templates/notificationsadmin.gohtml diff --git a/databaseMigrations.go b/databaseMigrations.go index 48ba813..5445a7c 100644 --- a/databaseMigrations.go +++ b/databaseMigrations.go @@ -130,6 +130,15 @@ func migrateDb() error { return err }, }, + &migrator.Migration{ + Name: "00011", + Func: func(tx *sql.Tx) error { + _, err := tx.Exec(` + create table notifications (id integer primary key autoincrement, time integer not null, text text not null); + `) + return err + }, + }, ), ) if err != nil { diff --git a/go.mod b/go.mod index 1283dfc..6cf005c 100644 --- a/go.mod +++ b/go.mod @@ -43,18 +43,19 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/viper v1.7.1 github.com/tdewolff/minify/v2 v2.9.13 + github.com/tdewolff/parse/v2 v2.5.11 // indirect github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 github.com/vcraescu/go-paginator v1.0.1-0.20201114172518-2cfc59fe05c2 github.com/yuin/goldmark v1.3.2 github.com/yuin/goldmark-emoji v1.0.1 go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.16.0 // indirect - golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect + golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect golang.org/x/mod v0.4.1 // indirect - golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect - golang.org/x/sync v0.0.0-20201207232520-09787c993a3a - golang.org/x/sys v0.0.0-20210217105451-b926d437f341 // indirect + golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 // indirect golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect golang.org/x/text v0.3.5 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index 6313862..0d5f3e6 100644 --- a/go.sum +++ b/go.sum @@ -284,8 +284,9 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tdewolff/minify/v2 v2.9.13 h1:RrwQhgGoYBhKN/ezStGB+crU64wPK1ZE5Jmkl63lif0= github.com/tdewolff/minify/v2 v2.9.13/go.mod h1:faNOp+awAoo+fhFHD+NAkBOaXBAvJI2X2SDERGKnARo= -github.com/tdewolff/parse/v2 v2.5.10 h1:vj35n+ljq8LuYUx436s4qB18wuwP7thrLv+t1syE39M= github.com/tdewolff/parse/v2 v2.5.10/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= +github.com/tdewolff/parse/v2 v2.5.11 h1:Wq0x026IKZh9GPUB5Fp+v5bki/SNmpIkdltcnm6HrO0= +github.com/tdewolff/parse/v2 v2.5.11/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4= github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -324,8 +325,8 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -370,8 +371,8 @@ golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d h1:1aflnvSoWWLI2k/dMUAl5lvU1YO4Mb4hz0gh+1rjcxU= +golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -381,8 +382,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -404,8 +405,8 @@ golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210217105451-b926d437f341 h1:2/QtM1mL37YmcsT8HaDNHDgTqqFVw+zr8UzMiBVLzYU= -golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 h1:SgQ6LNaYJU0JIuEHv9+s6EbhSCwYeAf5Yvj6lpYlqAE= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= diff --git a/http.go b/http.go index 60a21f2..bad7ef9 100644 --- a/http.go +++ b/http.go @@ -144,6 +144,15 @@ func buildHandler() (http.Handler, error) { }) }) + // Notifications + notificationsPath := "/notifications" + r.Route(notificationsPath, func(r chi.Router) { + r.Use(authMiddleware) + handler := notificationsAdmin(notificationsPath) + r.Get("/", handler) + r.Get(paginationPath, handler) + }) + // Posts pp, err := allPostPaths(statusPublished) if err != nil { diff --git a/notifications.go b/notifications.go index fb48192..04cf99d 100644 --- a/notifications.go +++ b/notifications.go @@ -1,15 +1,157 @@ package main import ( + "database/sql" + "fmt" "log" + "net/http" + "reflect" + "strconv" + "time" + + "github.com/go-chi/chi" + "github.com/vcraescu/go-paginator" ) +type notification struct { + ID int + Time int64 + Text string +} + func sendNotification(text string) { - log.Println("Notification:", text) + n := ¬ification{ + Time: time.Now().Unix(), + Text: text, + } + if err := saveNotification(n); err != nil { + log.Println("Failed to save notification:", err.Error()) + } if appConfig.Notifications.Telegram.Enabled { - err := sendTelegramMessage(text, "", appConfig.Notifications.Telegram.BotToken, appConfig.Notifications.Telegram.ChatID) + err := sendTelegramMessage(n.Text, "", appConfig.Notifications.Telegram.BotToken, appConfig.Notifications.Telegram.ChatID) if err != nil { log.Println("Failed to send Telegram notification:", err.Error()) } } } + +func saveNotification(n *notification) error { + if _, err := appDbExec("insert into notifications (time, text) values (@time, @text)", sql.Named("time", n.Time), sql.Named("text", n.Text)); err != nil { + return err + } + return nil +} + +type notificationsRequestConfig struct { + offset, limit int +} + +func buildNotificationsQuery(config *notificationsRequestConfig) (query string, args []interface{}) { + args = []interface{}{} + query = "select id, time, text from notifications order by id desc" + if config.limit != 0 || config.offset != 0 { + query += " limit @limit offset @offset" + args = append(args, sql.Named("limit", config.limit), sql.Named("offset", config.offset)) + } + return +} + +func getNotifications(config *notificationsRequestConfig) ([]*notification, error) { + notifications := []*notification{} + query, args := buildNotificationsQuery(config) + rows, err := appDbQuery(query, args...) + if err != nil { + return nil, err + } + for rows.Next() { + n := ¬ification{} + err = rows.Scan(&n.ID, &n.Time, &n.Text) + if err != nil { + return nil, err + } + notifications = append(notifications, n) + } + return notifications, nil +} + +func countNotifications(config *notificationsRequestConfig) (count int, err error) { + query, params := buildNotificationsQuery(config) + query = "select count(*) from (" + query + ")" + row, err := appDbQueryRow(query, params...) + if err != nil { + return + } + err = row.Scan(&count) + return +} + +type notificationsPaginationAdapter struct { + config *notificationsRequestConfig + nums int64 +} + +func (p *notificationsPaginationAdapter) Nums() (int64, error) { + if p.nums == 0 { + nums, _ := countNotifications(p.config) + p.nums = int64(nums) + } + return p.nums, nil +} + +func (p *notificationsPaginationAdapter) Slice(offset, length int, data interface{}) error { + modifiedConfig := *p.config + modifiedConfig.offset = offset + modifiedConfig.limit = length + + notifications, err := getNotifications(&modifiedConfig) + reflect.ValueOf(data).Elem().Set(reflect.ValueOf(¬ifications).Elem()) + return err +} + +func notificationsAdmin(notificationPath string) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // Adapter + pageNoString := chi.URLParam(r, "page") + pageNo, _ := strconv.Atoi(pageNoString) + p := paginator.New(¬ificationsPaginationAdapter{config: ¬ificationsRequestConfig{}}, 10) + p.SetPage(pageNo) + var notifications []*notification + err := p.Results(¬ifications) + if err != nil { + serveError(w, r, err.Error(), http.StatusInternalServerError) + return + } + // Navigation + var hasPrev, hasNext bool + var prevPage, nextPage int + var prevPath, nextPath string + hasPrev, _ = p.HasPrev() + if hasPrev { + prevPage, _ = p.PrevPage() + } else { + prevPage, _ = p.Page() + } + if prevPage < 2 { + prevPath = notificationPath + } else { + prevPath = fmt.Sprintf("%s/page/%d", notificationPath, prevPage) + } + hasNext, _ = p.HasNext() + if hasNext { + nextPage, _ = p.NextPage() + } else { + nextPage, _ = p.Page() + } + nextPath = fmt.Sprintf("%s/page/%d", notificationPath, nextPage) + // Render + render(w, templateNotificationsAdmin, &renderData{ + Data: map[string]interface{}{ + "Notifications": notifications, + "HasPrev": hasPrev, + "HasNext": hasNext, + "Prev": slashIfEmpty(prevPath), + "Next": slashIfEmpty(nextPath), + }, + }) + } +} diff --git a/render.go b/render.go index c2f2900..f838c2d 100644 --- a/render.go +++ b/render.go @@ -19,24 +19,27 @@ import ( "github.com/goodsign/monday" ) -const templatesDir = "templates" -const templatesExt = ".gohtml" +const ( + templatesDir = "templates" + templatesExt = ".gohtml" -const templateBase = "base" -const templatePost = "post" -const templateError = "error" -const templateIndex = "index" -const templateTaxonomy = "taxonomy" -const templateSearch = "search" -const templateSummary = "summary" -const templatePhotosSummary = "photosummary" -const templateEditor = "editor" -const templateLogin = "login" -const templateStaticHome = "statichome" -const templateBlogStats = "blogstats" -const templateComment = "comment" -const templateCaptcha = "captcha" -const templateCommentsAdmin = "commentsadmin" + templateBase = "base" + templatePost = "post" + templateError = "error" + templateIndex = "index" + templateTaxonomy = "taxonomy" + templateSearch = "search" + templateSummary = "summary" + templatePhotosSummary = "photosummary" + templateEditor = "editor" + templateLogin = "login" + templateStaticHome = "statichome" + templateBlogStats = "blogstats" + templateComment = "comment" + templateCaptcha = "captcha" + templateCommentsAdmin = "commentsadmin" + templateNotificationsAdmin = "notificationsadmin" +) var templates map[string]*template.Template var templateFunctions template.FuncMap diff --git a/templates/commentsadmin.gohtml b/templates/commentsadmin.gohtml index 76493c6..64adabf 100644 --- a/templates/commentsadmin.gohtml +++ b/templates/commentsadmin.gohtml @@ -13,7 +13,7 @@ Target: {{ $comment.Target }}
Name: {{ if $comment.Website }}{{ $comment.Name }}{{ else }}{{ $comment.Name }}{{ end }}

-

+

{{ html $comment.Comment }}

diff --git a/templates/notificationsadmin.gohtml b/templates/notificationsadmin.gohtml new file mode 100644 index 0000000..2edc669 --- /dev/null +++ b/templates/notificationsadmin.gohtml @@ -0,0 +1,26 @@ +{{ define "title" }} + {{ string .Blog.Lang "notifications" }} - {{ .Blog.Title }} +{{ end }} + +{{ define "main" }} +
+

{{ string .Blog.Lang "notifications" }}

+ {{ range $i, $notification := .Data.Notifications }} +

+ ID: {{ $notification.ID }}
+ Time: {{ unixtodate $notification.Time }}
+ Text: {{ $notification.Text }} +

+ {{ end }} + {{ if .Data.HasPrev }} +

{{ string .Blog.Lang "prev" }}

+ {{ end }} + {{ if .Data.HasNext }} +

{{ string .Blog.Lang "next" }}

+ {{ end }} +
+{{ end }} + +{{ define "notificationsadmin" }} + {{ template "base" . }} +{{ end }} \ No newline at end of file diff --git a/templates/strings/default.yaml b/templates/strings/default.yaml index 1a679ef..c2f7a99 100644 --- a/templates/strings/default.yaml +++ b/templates/strings/default.yaml @@ -19,6 +19,7 @@ likeof: "Like of" login: "Login" nameopt: "Name (optional)" next: "Next" +notifications: "Notifications" oldcontent: "⚠️ This entry is already over one year old. It may no longer be up to date. Opinions may have changed." password: "Password" prev: "Previous"