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