From e9ce21133a4b4bd60243d2c7498fb5b4952161a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Thu, 28 Mar 2024 21:49:48 +0100 Subject: [PATCH 1/4] Schedule and notification --- db.go | 13 +++++ main.go | 42 +++++---------- node.go | 72 +++++++++++++++++++++++++- schedule.go | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++ sql/00015.sql | 14 +++++ sql/00016.sql | 1 + sql/00017.sql | 11 ++++ 7 files changed, 262 insertions(+), 31 deletions(-) create mode 100644 db.go create mode 100644 schedule.go create mode 100644 sql/00015.sql create mode 100644 sql/00016.sql create mode 100644 sql/00017.sql diff --git a/db.go b/db.go new file mode 100644 index 0000000..2046724 --- /dev/null +++ b/db.go @@ -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 +} diff --git a/main.go b/main.go index eebe749..c78017a 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ import ( "path/filepath" "strconv" "strings" + "time" ) const LISTEN_HOST = "0.0.0.0" @@ -36,6 +37,7 @@ var ( static http.Handler config Config logger *slog.Logger + schedulers map[int]Schedule VERSION string //go:embed version sql/* @@ -63,6 +65,8 @@ func init() { // {{{ opt.Level = slog.LevelDebug logger = slog.New(slog.NewJSONHandler(os.Stdout, &opt)) + schedulers = make(map[int]Schedule, 512) + configFilename := os.Getenv("HOME") + "/.config/notes.yaml" flag.IntVar(&flagPort, "port", 1371, "TCP port to listen on") flag.BoolVar(&flagVersion, "version", false, "Shows Notes version and exists") @@ -122,45 +126,25 @@ func main() { // {{{ os.Exit(0) } + go scheduleHandler() + err = service.Start() if err != nil { logger.Error("webserver", "error", err) 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)) -/* -func userPassword(w http.ResponseWriter, r *http.Request) { // {{{ - var err error - var ok bool - var session Session + tick := time.NewTicker(time.Minute) + for { + <-tick.C - 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) { // {{{ logger.Info("webserver", "request", "/node/tree") diff --git a/node.go b/node.go index c65b4b4..1868122 100644 --- a/node.go +++ b/node.go @@ -6,6 +6,7 @@ import ( // Standard "time" + "database/sql" ) type ChecklistItem struct { @@ -323,8 +324,69 @@ func CreateNode(userID, parentID int, name string) (node Node, err error) { // { return } // }}} 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 { - _, err = service.Db.Conn.Exec(` + _, err = tsx.Exec(` UPDATE node SET content = '', @@ -345,7 +407,7 @@ func UpdateNode(userID, nodeID int, content string, cryptoKeyID int, markdown bo markdown, ) } else { - _, err = service.Db.Conn.Exec(` + _, err = tsx.Exec(` UPDATE node SET content = $1, @@ -366,6 +428,12 @@ func UpdateNode(userID, nodeID int, content string, cryptoKeyID int, markdown bo markdown, ) } + if err != nil { + tsx.Rollback() + return + } + + err = tsx.Commit() return } // }}} diff --git a/schedule.go b/schedule.go new file mode 100644 index 0000000..d2c7aec --- /dev/null +++ b/schedule.go @@ -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 +} diff --git a/sql/00015.sql b/sql/00015.sql new file mode 100644 index 0000000..c39cc4f --- /dev/null +++ b/sql/00015.sql @@ -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 +); diff --git a/sql/00016.sql b/sql/00016.sql new file mode 100644 index 0000000..8a98bf1 --- /dev/null +++ b/sql/00016.sql @@ -0,0 +1 @@ +ALTER TABLE public.schedule ADD CONSTRAINT schedule_event UNIQUE (user_id, node_id, "time", description); diff --git a/sql/00017.sql b/sql/00017.sql new file mode 100644 index 0000000..52009bf --- /dev/null +++ b/sql/00017.sql @@ -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 +); From c2555a1d3544c363b7a1015da7bcee9c3d5540e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Fri, 29 Mar 2024 08:06:23 +0100 Subject: [PATCH 2/4] Added files --- notification/ntfy.go | 26 ++++++++++++++++++++++++++ notification/pkg.go | 5 +++++ schedule.go | 2 ++ 3 files changed, 33 insertions(+) create mode 100644 notification/ntfy.go create mode 100644 notification/pkg.go diff --git a/notification/ntfy.go b/notification/ntfy.go new file mode 100644 index 0000000..d6fd2f2 --- /dev/null +++ b/notification/ntfy.go @@ -0,0 +1,26 @@ +package notification + +import ( + // Standard + "bytes" + "encoding/json" + "net/http" +) + +type NTFY struct { + URL string +} + +func NewNTFY(config []byte) (instance NTFY, err error) { + err = json.Unmarshal(config, &instance) + if err != nil { + return + } + return instance, nil +} + +func (ntfy NTFY) Send(msg []byte) (err error) { + http.NewRequest("POST", ntfy.URL, bytes.NewReader(msg)) + + return +} diff --git a/notification/pkg.go b/notification/pkg.go new file mode 100644 index 0000000..194d88a --- /dev/null +++ b/notification/pkg.go @@ -0,0 +1,5 @@ +package notification + +type Notification interface { + Send([]byte) error +} diff --git a/schedule.go b/schedule.go index d2c7aec..cfb0406 100644 --- a/schedule.go +++ b/schedule.go @@ -126,6 +126,7 @@ func RetrieveSchedules(userID int, nodeID int) (schedules []Schedule, err error) func ExpiredSchedules() []Schedule { schedules := []Schedule{} + /* res, err := service.Db.Conn.Query(` SELECT * @@ -136,5 +137,6 @@ func ExpiredSchedules() []Schedule { ORDER BY time ASC `) + */ return schedules } From afcadc8ae110c609af036fce430d75e0783998c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Fri, 29 Mar 2024 20:24:53 +0100 Subject: [PATCH 3/4] Implemented rudimentary notification with NTFY and acknowledgement --- config.go | 1 + go.mod | 1 + go.sum | 2 + main.go | 93 +++++++++++++++++++++++++++----- notification/factory.go | 15 ++++++ notification/ntfy.go | 54 +++++++++++++++++-- notification/pkg.go | 58 +++++++++++++++++++- notification_manager.go | 75 ++++++++++++++++++++++++++ schedule.go | 116 +++++++++++++++++++++++----------------- 9 files changed, 346 insertions(+), 69 deletions(-) create mode 100644 notification/factory.go create mode 100644 notification_manager.go diff --git a/config.go b/config.go index 56fd2ff..4860517 100644 --- a/config.go +++ b/config.go @@ -28,6 +28,7 @@ type Config struct { Static string Upload string } + NotificationBaseURL string } Session struct { diff --git a/go.mod b/go.mod index a0791a8..59b3275 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21.0 require ( git.gibonuddevalla.se/go/webservice v0.2.2 + git.gibonuddevalla.se/go/wrappederror v0.3.3 github.com/google/uuid v1.5.0 github.com/gorilla/websocket v1.5.0 github.com/jmoiron/sqlx v1.3.5 diff --git a/go.sum b/go.sum index bbaf4e1..6466326 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ git.gibonuddevalla.se/go/dbschema v1.3.0 h1:HzFMR29tWfy/ibIjltTbIMI4inVktj/rh8bE git.gibonuddevalla.se/go/dbschema v1.3.0/go.mod h1:BNw3q/574nXbGoeWyK+tLhRfggVkw2j2aXZzrBKC3ig= git.gibonuddevalla.se/go/webservice v0.2.2 h1:pmfeLa7c9pSPbuu6TuzcJ6yuVwdMLJ8SSPm1IkusThk= git.gibonuddevalla.se/go/webservice v0.2.2/go.mod h1:3uBS6nLbK9qbuGzDls8MZD5Xr9ORY1Srbj6v06BIhws= +git.gibonuddevalla.se/go/wrappederror v0.3.3 h1:pdIy3/daSY3zMmUr9PXW6ffIt8iYonOv64mgJBpKz+0= +git.gibonuddevalla.se/go/wrappederror v0.3.3/go.mod h1:j4w320Hk1wvhOPjUaK4GgLvmtnjUUM5yVu6JFO1OCSc= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= diff --git a/main.go b/main.go index c78017a..b331f2a 100644 --- a/main.go +++ b/main.go @@ -3,14 +3,17 @@ package main import ( // External "git.gibonuddevalla.se/go/webservice" + "git.gibonuddevalla.se/go/wrappederror" // Internal "git.gibonuddevalla.se/go/webservice/session" + "notes/notification" // Standard "crypto/md5" "embed" "encoding/hex" + "encoding/json" "flag" "fmt" "io" @@ -32,13 +35,14 @@ var ( flagCheckLocal bool flagConfig string - service *webservice.Service - connectionManager ConnectionManager - static http.Handler - config Config - logger *slog.Logger - schedulers map[int]Schedule - VERSION string + service *webservice.Service + connectionManager ConnectionManager + notificationManager notification.Manager + static http.Handler + config Config + logger *slog.Logger + schedulers map[int]Schedule + VERSION string //go:embed version sql/* embeddedSQL embed.FS @@ -47,7 +51,7 @@ var ( staticFS embed.FS ) -func sqlProvider(dbname string, version int) (sql []byte, found bool) { +func sqlProvider(dbname string, version int) (sql []byte, found bool) { // {{{ var err error sql, err = embeddedSQL.ReadFile(fmt.Sprintf("sql/%05d.sql", version)) if err != nil { @@ -55,7 +59,27 @@ func sqlProvider(dbname string, version int) (sql []byte, found bool) { } found = true return -} +} // }}} +func logCallback(e WrappedError.Error) { // {{{ + now := time.Now() + out := struct { + Year int + Month int + Day int + Time string + Error any + }{now.Year(), int(now.Month()), now.Day(), now.Format("15:04:05"), e} + + j, _ := json.Marshal(out) + file, err := os.OpenFile("/tmp/notes.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) + if err != nil { + logger.Error("log", "error", err) + return + } + file.Write(j) + file.Write([]byte("\n")) + file.Close() +} // }}} func init() { // {{{ version, _ := embeddedSQL.ReadFile("version") @@ -76,6 +100,9 @@ func init() { // {{{ flag.Parse() } // }}} func main() { // {{{ + WrappedError.Init() + WrappedError.SetLogCallback(logCallback) + var err error if flagVersion { @@ -119,6 +146,7 @@ func main() { // {{{ service.Register("/key/retrieve", true, true, keyRetrieve) service.Register("/key/create", true, true, keyCreate) service.Register("/key/counter", true, true, keyCounter) + service.Register("/notification/ack", false, false, notificationAcknowledge) service.Register("/", false, false, service.StaticHandler) if flagCreateUser { @@ -128,6 +156,17 @@ func main() { // {{{ go scheduleHandler() + if err = service.InitDatabaseConnection(); err != nil { + logger.Error("application", "error", err) + os.Exit(1) + } + + err = InitNotificationManager() + if err != nil { + logger.Error("application", "error", err) + os.Exit(1) + } + err = service.Start() if err != nil { logger.Error("webserver", "error", err) @@ -136,13 +175,25 @@ func main() { // {{{ } // }}} 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)) - + //wait := 60000 - time.Now().Sub(time.Now().Truncate(time.Minute)).Milliseconds() + //time.Sleep(time.Millisecond * time.Duration(wait)) tick := time.NewTicker(time.Minute) + tick = time.NewTicker(time.Second*5) for { <-tick.C - + for _, event := range ExpiredSchedules() { + notificationManager.Send( + event.UserID, + event.ScheduleUUID, + []byte( + fmt.Sprintf( + "%s\n%s", + event.Time.Format("2006-01-02 15:04"), + event.Description, + ), + ), + ) + } } } // }}} @@ -710,4 +761,20 @@ func keyCounter(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{ }) } // }}} + +func notificationAcknowledge(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{ + logger.Info("webserver", "request", "/notification/ack") + var err error + + + err = AcknowledgeNotification(r.URL.Query().Get("uuid")) + if err != nil { + responseError(w, err) + return + } + + responseData(w, map[string]interface{}{ + "OK": true, + }) +} // }}} // vim: foldmethod=marker diff --git a/notification/factory.go b/notification/factory.go new file mode 100644 index 0000000..cc4d145 --- /dev/null +++ b/notification/factory.go @@ -0,0 +1,15 @@ +package notification + +import ( + // External + werr "git.gibonuddevalla.se/go/wrappederror" +) + +func ServiceFactory(t string, config []byte, prio int, ackURL string) (Service, error) { + switch t { + case "NTFY": + return NewNTFY(config, prio, ackURL) + } + + return nil, werr.New("Unknown notification service, '%s'", t).WithCode("002-0000") +} diff --git a/notification/ntfy.go b/notification/ntfy.go index d6fd2f2..3f1b2ef 100644 --- a/notification/ntfy.go +++ b/notification/ntfy.go @@ -1,26 +1,70 @@ package notification import ( + // External + werr "git.gibonuddevalla.se/go/wrappederror" + // Standard "bytes" "encoding/json" + "fmt" + "io" "net/http" ) type NTFY struct { - URL string + URL string + Prio int + AcknowledgeURL string } -func NewNTFY(config []byte) (instance NTFY, err error) { +func NewNTFY(config []byte, prio int, ackURL string) (instance NTFY, err error) { err = json.Unmarshal(config, &instance) if err != nil { - return + err = werr.Wrap(err).WithCode("002-0001").WithData(config) + return } + instance.Prio = prio + instance.AcknowledgeURL = ackURL return instance, nil } -func (ntfy NTFY) Send(msg []byte) (err error) { - http.NewRequest("POST", ntfy.URL, bytes.NewReader(msg)) +func (ntfy NTFY) GetPrio() int { + return ntfy.Prio +} + +func (ntfy NTFY) Send(uuid string, msg []byte) (err error) { + var req *http.Request + var res *http.Response + req, err = http.NewRequest("POST", ntfy.URL, bytes.NewReader(msg)) + if err != nil { + err = werr.Wrap(err).WithCode("002-0002").WithData(ntfy.URL) + return + } + + ackURL := fmt.Sprintf("http, OK, %s/notification/ack?uuid=%s", ntfy.AcknowledgeURL, uuid) + req.Header.Add("X-Actions", ackURL) + + res, err = http.DefaultClient.Do(req) + if err != nil { + err = werr.Wrap(err).WithCode("002-0003") + return + } + + body, _ := io.ReadAll(res.Body) + if res.StatusCode != 200 { + err = werr.New("Invalid NTFY response").WithCode("002-0004").WithData(body) + return + } + + ntfyResp := struct { + ID string + }{} + err = json.Unmarshal(body, &ntfyResp) + if err != nil { + err = werr.Wrap(err).WithCode("002-0005").WithData(body) + return + } return } diff --git a/notification/pkg.go b/notification/pkg.go index 194d88a..5732f31 100644 --- a/notification/pkg.go +++ b/notification/pkg.go @@ -1,5 +1,59 @@ package notification -type Notification interface { - Send([]byte) error +import ( + // External + werr "git.gibonuddevalla.se/go/wrappederror" + + // Standard + _ "fmt" + "slices" +) + +type Service interface { + GetPrio() int + Send(string, []byte) error +} + +type Manager struct { + services map[int][]Service +} + +func NewManager() (nm Manager) { + nm.services = make(map[int][]Service, 32) + return +} + +func (nm *Manager) AddService(userID int, service Service) { + var services []Service + var found bool + if services, found = nm.services[userID]; !found { + services = []Service{} + } + + services = append(services, service) + slices.SortFunc(services, func(a, b Service) int { + if a.GetPrio() < b.GetPrio() { + return -1 + } + if a.GetPrio() > b.GetPrio() { + return 1 + } + return 0 + }) + nm.services[userID] = services +} + +func (nm *Manager) Send(userID int, uuid string, msg []byte) (err error) { + services, found := nm.services[userID] + if !found { + return werr.New("No notification services defined for user ID %d", userID).WithCode("002-0008") + } + + for _, service := range services { + if err = service.Send(uuid, msg); err == nil { + break + } + } + + return } diff --git a/notification_manager.go b/notification_manager.go new file mode 100644 index 0000000..7022af9 --- /dev/null +++ b/notification_manager.go @@ -0,0 +1,75 @@ +package main + +import ( + // External + werr "git.gibonuddevalla.se/go/wrappederror" + + // Internal + "notes/notification" + + // Standard + "database/sql" + "encoding/json" +) + +type DbNotificationService struct { + ID int + UserID int `json:"user_id"` + Service string + Configuration string + Prio int +} + +func InitNotificationManager() (err error) {// {{{ + var dbServices []DbNotificationService + var row *sql.Row + + row = service.Db.Conn.QueryRow(` + WITH services AS ( + SELECT + id, + user_id, + prio, + service, + configuration::varchar + FROM notification n + ORDER BY + user_id ASC, + prio ASC + ) + SELECT jsonb_agg(s.*) + FROM services s + `, + ) + var dbData []byte + err = row.Scan(&dbData) + if err != nil { + err = werr.Wrap(err).WithCode("002-0006") + return + } + + err = json.Unmarshal(dbData, &dbServices) + if err != nil { + err = werr.Wrap(err).WithCode("002-0007") + return + } + + notificationManager = notification.NewManager() + var service notification.Service + for _, dbService := range dbServices { + service, err = notification.ServiceFactory( + dbService.Service, + []byte(dbService.Configuration), + dbService.Prio, + config.Application.NotificationBaseURL, + ) + notificationManager.AddService(dbService.UserID, service) + } + + return +}// }}} + +func AcknowledgeNotification(uuid string) (err error) {// {{{ + _, err = service.Db.Conn.Exec(`UPDATE schedule SET acknowledged=true WHERE schedule_uuid=$1`, uuid) + return +}// }}} diff --git a/schedule.go b/schedule.go index cfb0406..8b0f9b6 100644 --- a/schedule.go +++ b/schedule.go @@ -1,6 +1,9 @@ package main import ( + // External + werr "git.gibonuddevalla.se/go/wrappederror" + // Standard "encoding/json" "fmt" @@ -15,7 +18,7 @@ func init() { type Schedule struct { ID int - UserID int `json:"user_id"` + UserID int `json:"user_id" db:"user_id"` Node Node ScheduleUUID string `db:"schedule_uuid"` Time time.Time @@ -23,7 +26,7 @@ type Schedule struct { Acknowledged bool } -func ScanForSchedules(content string) (schedules []Schedule) { +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*\}`) @@ -48,44 +51,8 @@ func ScanForSchedules(content string) (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) { +}// }}} +func RetrieveSchedules(userID int, nodeID int) (schedules []Schedule, err error) {// {{{ schedules = []Schedule{} res := service.Db.Conn.QueryRow(` @@ -122,14 +89,52 @@ func RetrieveSchedules(userID int, nodeID int) (schedules []Schedule, err error) err = json.Unmarshal(data, &schedules) return -} +}// }}} -func ExpiredSchedules() []Schedule { - schedules := []Schedule{} - /* - res, err := service.Db.Conn.Query(` +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 ExpiredSchedules() (schedules []Schedule) {// {{{ + schedules = []Schedule{} + + res, err := service.Db.Conn.Queryx(` SELECT - * + id, + user_id, + node_id, + schedule_uuid, + time, + description FROM schedule WHERE time < NOW() AND @@ -137,6 +142,19 @@ func ExpiredSchedules() []Schedule { ORDER BY time ASC `) - */ - return schedules -} + if err != nil { + err = werr.Wrap(err).WithCode("002-0009") + return + } + defer res.Close() + + for res.Next() { + s := Schedule{} + if err = res.Scan(&s.ID, &s.UserID, &s.Node.ID, &s.ScheduleUUID, &s.Time, &s.Description); err != nil { + werr.Wrap(err).WithCode("002-000a") + continue + } + schedules = append(schedules, s) + } + return +}// }}} From a6c94ac7ca8144db894d83d29fd1aa2ba0341a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Fri, 29 Mar 2024 20:25:59 +0100 Subject: [PATCH 4/4] Bumped to v22 --- version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version b/version index 1689437..53d1c14 100644 --- a/version +++ b/version @@ -1 +1 @@ -v21 +v22