Schedule and notification
This commit is contained in:
parent
fd01e751e2
commit
e9ce21133a
13
db.go
Normal file
13
db.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
// Standard
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Queryable can take both a Db and a transaction
|
||||||
|
type Queryable interface {
|
||||||
|
Exec(string, ...any) (sql.Result, error)
|
||||||
|
Query(string, ...any) (*sql.Rows, error)
|
||||||
|
QueryRow(string, ...any) *sql.Row
|
||||||
|
}
|
42
main.go
42
main.go
@ -20,6 +20,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const LISTEN_HOST = "0.0.0.0"
|
const LISTEN_HOST = "0.0.0.0"
|
||||||
@ -36,6 +37,7 @@ var (
|
|||||||
static http.Handler
|
static http.Handler
|
||||||
config Config
|
config Config
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
schedulers map[int]Schedule
|
||||||
VERSION string
|
VERSION string
|
||||||
|
|
||||||
//go:embed version sql/*
|
//go:embed version sql/*
|
||||||
@ -63,6 +65,8 @@ func init() { // {{{
|
|||||||
opt.Level = slog.LevelDebug
|
opt.Level = slog.LevelDebug
|
||||||
logger = slog.New(slog.NewJSONHandler(os.Stdout, &opt))
|
logger = slog.New(slog.NewJSONHandler(os.Stdout, &opt))
|
||||||
|
|
||||||
|
schedulers = make(map[int]Schedule, 512)
|
||||||
|
|
||||||
configFilename := os.Getenv("HOME") + "/.config/notes.yaml"
|
configFilename := os.Getenv("HOME") + "/.config/notes.yaml"
|
||||||
flag.IntVar(&flagPort, "port", 1371, "TCP port to listen on")
|
flag.IntVar(&flagPort, "port", 1371, "TCP port to listen on")
|
||||||
flag.BoolVar(&flagVersion, "version", false, "Shows Notes version and exists")
|
flag.BoolVar(&flagVersion, "version", false, "Shows Notes version and exists")
|
||||||
@ -122,45 +126,25 @@ func main() { // {{{
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go scheduleHandler()
|
||||||
|
|
||||||
err = service.Start()
|
err = service.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("webserver", "error", err)
|
logger.Error("webserver", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
} // }}}
|
} // }}}
|
||||||
|
func scheduleHandler() { // {{{
|
||||||
|
// Wait for the approximate minute.
|
||||||
|
wait := 60000 - time.Now().Sub(time.Now().Truncate(time.Minute)).Milliseconds()
|
||||||
|
time.Sleep(time.Millisecond * time.Duration(wait))
|
||||||
|
|
||||||
/*
|
tick := time.NewTicker(time.Minute)
|
||||||
func userPassword(w http.ResponseWriter, r *http.Request) { // {{{
|
for {
|
||||||
var err error
|
<-tick.C
|
||||||
var ok bool
|
|
||||||
var session Session
|
|
||||||
|
|
||||||
if session, _, err = ValidateSession(r, true); err != nil {
|
|
||||||
responseError(w, err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
req := struct {
|
|
||||||
CurrentPassword string
|
|
||||||
NewPassword string
|
|
||||||
}{}
|
|
||||||
if err = parseRequest(r, &req); err != nil {
|
|
||||||
responseError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, err = session.UpdatePassword(req.CurrentPassword, req.NewPassword)
|
|
||||||
if err != nil {
|
|
||||||
responseError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
responseData(w, map[string]interface{}{
|
|
||||||
"OK": true,
|
|
||||||
"CurrentPasswordOK": ok,
|
|
||||||
})
|
|
||||||
} // }}}
|
} // }}}
|
||||||
*/
|
|
||||||
|
|
||||||
func nodeTree(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
func nodeTree(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
||||||
logger.Info("webserver", "request", "/node/tree")
|
logger.Info("webserver", "request", "/node/tree")
|
||||||
|
72
node.go
72
node.go
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
// Standard
|
// Standard
|
||||||
"time"
|
"time"
|
||||||
|
"database/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChecklistItem struct {
|
type ChecklistItem struct {
|
||||||
@ -323,8 +324,69 @@ func CreateNode(userID, parentID int, name string) (node Node, err error) { // {
|
|||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
func UpdateNode(userID, nodeID int, content string, cryptoKeyID int, markdown bool) (err error) { // {{{
|
func UpdateNode(userID, nodeID int, content string, cryptoKeyID int, markdown bool) (err error) { // {{{
|
||||||
|
var scannedSchedules, dbSchedules, add, remove []Schedule
|
||||||
|
scannedSchedules = ScanForSchedules(content)
|
||||||
|
for i := range scannedSchedules {
|
||||||
|
scannedSchedules[i].Node.ID = nodeID
|
||||||
|
scannedSchedules[i].UserID = userID
|
||||||
|
}
|
||||||
|
|
||||||
|
var tsx *sql.Tx
|
||||||
|
tsx, err = service.Db.Conn.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dbSchedules, err = RetrieveSchedules(userID, nodeID)
|
||||||
|
if err != nil {
|
||||||
|
tsx.Rollback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scanned := range scannedSchedules {
|
||||||
|
found := false
|
||||||
|
for _, db := range dbSchedules {
|
||||||
|
if scanned.IsEqual(db) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
add = append(add, scanned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, db := range dbSchedules {
|
||||||
|
found := false
|
||||||
|
for _, scanned := range scannedSchedules {
|
||||||
|
if db.IsEqual(scanned) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
remove = append(remove, db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, event := range remove {
|
||||||
|
err = event.Delete(tsx)
|
||||||
|
if err != nil {
|
||||||
|
tsx.Rollback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, event := range add {
|
||||||
|
err = event.Insert(tsx)
|
||||||
|
if err != nil {
|
||||||
|
tsx.Rollback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if cryptoKeyID > 0 {
|
if cryptoKeyID > 0 {
|
||||||
_, err = service.Db.Conn.Exec(`
|
_, err = tsx.Exec(`
|
||||||
UPDATE node
|
UPDATE node
|
||||||
SET
|
SET
|
||||||
content = '',
|
content = '',
|
||||||
@ -345,7 +407,7 @@ func UpdateNode(userID, nodeID int, content string, cryptoKeyID int, markdown bo
|
|||||||
markdown,
|
markdown,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
_, err = service.Db.Conn.Exec(`
|
_, err = tsx.Exec(`
|
||||||
UPDATE node
|
UPDATE node
|
||||||
SET
|
SET
|
||||||
content = $1,
|
content = $1,
|
||||||
@ -366,6 +428,12 @@ func UpdateNode(userID, nodeID int, content string, cryptoKeyID int, markdown bo
|
|||||||
markdown,
|
markdown,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
tsx.Rollback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tsx.Commit()
|
||||||
|
|
||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
|
140
schedule.go
Normal file
140
schedule.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
// Standard
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
fmt.Printf("")
|
||||||
|
}
|
||||||
|
|
||||||
|
type Schedule struct {
|
||||||
|
ID int
|
||||||
|
UserID int `json:"user_id"`
|
||||||
|
Node Node
|
||||||
|
ScheduleUUID string `db:"schedule_uuid"`
|
||||||
|
Time time.Time
|
||||||
|
Description string
|
||||||
|
Acknowledged bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScanForSchedules(content string) (schedules []Schedule) {
|
||||||
|
schedules = []Schedule{}
|
||||||
|
|
||||||
|
rxp := regexp.MustCompile(`\{\s*([0-9]{4}-[0-9]{2}-[0-9]{2}\s+[0-9]{2}:[0-9]{2}(?::[0-9]{2})?)\s*\,\s*([^\]]+?)\s*\}`)
|
||||||
|
foundSchedules := rxp.FindAllStringSubmatch(content, -1)
|
||||||
|
|
||||||
|
for _, data := range foundSchedules {
|
||||||
|
// Missing seconds
|
||||||
|
if strings.Count(data[1], ":") == 1 {
|
||||||
|
data[1] = data[1] + ":00"
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp, err := time.Parse("2006-01-02 15:04:05", data[1])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule := Schedule{
|
||||||
|
Time: timestamp,
|
||||||
|
Description: data[2],
|
||||||
|
}
|
||||||
|
schedules = append(schedules, schedule)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Schedule) IsEqual(b Schedule) bool {
|
||||||
|
return a.UserID == b.UserID &&
|
||||||
|
a.Node.ID == b.Node.ID &&
|
||||||
|
a.Time.Equal(b.Time) &&
|
||||||
|
a.Description == b.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schedule) Insert(queryable Queryable) error {
|
||||||
|
res := queryable.QueryRow(`
|
||||||
|
INSERT INTO schedule(user_id, node_id, time, description)
|
||||||
|
VALUES($1, $2, $3, $4)
|
||||||
|
RETURNING id
|
||||||
|
`,
|
||||||
|
s.UserID,
|
||||||
|
s.Node.ID,
|
||||||
|
s.Time,
|
||||||
|
s.Description,
|
||||||
|
)
|
||||||
|
|
||||||
|
return res.Scan(&s.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schedule) Delete(queryable Queryable) error {
|
||||||
|
_, err := queryable.Exec(`
|
||||||
|
DELETE FROM schedule
|
||||||
|
WHERE
|
||||||
|
user_id = $1 AND
|
||||||
|
id = $2
|
||||||
|
`,
|
||||||
|
s.UserID,
|
||||||
|
s.ID,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func RetrieveSchedules(userID int, nodeID int) (schedules []Schedule, err error) {
|
||||||
|
schedules = []Schedule{}
|
||||||
|
|
||||||
|
res := service.Db.Conn.QueryRow(`
|
||||||
|
WITH schedule_events AS (
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
json_build_object('id', node_id) AS node,
|
||||||
|
schedule_uuid,
|
||||||
|
time::timestamptz,
|
||||||
|
description,
|
||||||
|
acknowledged
|
||||||
|
FROM schedule
|
||||||
|
WHERE
|
||||||
|
user_id=$1 AND
|
||||||
|
CASE
|
||||||
|
WHEN $2 > 0 THEN node_id = $2
|
||||||
|
ELSE true
|
||||||
|
END
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COALESCE(jsonb_agg(s.*), '[]'::jsonb)
|
||||||
|
FROM schedule_events s
|
||||||
|
`,
|
||||||
|
userID,
|
||||||
|
nodeID,
|
||||||
|
)
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
err = res.Scan(&data)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(data, &schedules)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpiredSchedules() []Schedule {
|
||||||
|
schedules := []Schedule{}
|
||||||
|
res, err := service.Db.Conn.Query(`
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM schedule
|
||||||
|
WHERE
|
||||||
|
time < NOW() AND
|
||||||
|
NOT acknowledged
|
||||||
|
ORDER BY
|
||||||
|
time ASC
|
||||||
|
`)
|
||||||
|
return schedules
|
||||||
|
}
|
14
sql/00015.sql
Normal file
14
sql/00015.sql
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE public.schedule (
|
||||||
|
id SERIAL NOT NULL,
|
||||||
|
user_id INT4 NOT NULL,
|
||||||
|
node_id INT4 NOT NULL,
|
||||||
|
schedule_uuid CHAR(36) DEFAULT GEN_RANDOM_UUID() NOT NULL,
|
||||||
|
"time" TIMESTAMP NOT NULL,
|
||||||
|
description VARCHAR DEFAULT '' NOT NULL,
|
||||||
|
acknowledged BOOL DEFAULT false NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT schedule_pk PRIMARY KEY (id),
|
||||||
|
CONSTRAINT schedule_uuid UNIQUE (schedule_uuid),
|
||||||
|
CONSTRAINT schedule_node_fk FOREIGN KEY (node_id) REFERENCES public.node(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT schedule_user_fk FOREIGN KEY (user_id) REFERENCES "_webservice"."user"(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
1
sql/00016.sql
Normal file
1
sql/00016.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE public.schedule ADD CONSTRAINT schedule_event UNIQUE (user_id, node_id, "time", description);
|
11
sql/00017.sql
Normal file
11
sql/00017.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE public.notification (
|
||||||
|
id SERIAl NOT NULL,
|
||||||
|
user_id INT4 NOT NULL,
|
||||||
|
service VARCHAR DEFAULT 'NTFY' NOT NULL,
|
||||||
|
"configuration" JSONB DEFAULT '{}' NOT NULL,
|
||||||
|
prio INT DEFAULT 0 NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT notification_pk PRIMARY KEY (id),
|
||||||
|
CONSTRAINT notification_unique UNIQUE (user_id,prio),
|
||||||
|
CONSTRAINT notification_user_fk FOREIGN KEY (user_id) REFERENCES "_webservice"."user"(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
Loading…
Reference in New Issue
Block a user