From bcba9f8a11dc221dda730de526026f1341130b52 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Tue, 17 Sep 2024 16:28:12 +0200 Subject: [PATCH] Poo recorder almost ported to go, needs to have perodically backup --- src/cmd/serve.go | 35 ++- src/components/homeassistant/homeassistant.go | 54 +++++ src/components/pooRecorder.go | 156 ------------- src/components/pooRecorder/pooRecorder.go | 210 ++++++++++++++++++ src/go.mod | 18 +- src/go.sum | 67 +++++- src/pooRecorder.db | Bin 0 -> 12288 bytes .../homeassistantutil/homeassistantutil.go | 96 ++++++++ 8 files changed, 469 insertions(+), 167 deletions(-) create mode 100644 src/components/homeassistant/homeassistant.go delete mode 100644 src/components/pooRecorder.go create mode 100644 src/components/pooRecorder/pooRecorder.go create mode 100644 src/pooRecorder.db create mode 100644 src/util/homeassistantutil/homeassistantutil.go diff --git a/src/cmd/serve.go b/src/cmd/serve.go index 2b2845a..4cd5c88 100644 --- a/src/cmd/serve.go +++ b/src/cmd/serve.go @@ -13,14 +13,17 @@ import ( "syscall" "time" + "github.com/go-co-op/gocron/v2" "github.com/gorilla/mux" "github.com/spf13/cobra" "github.com/spf13/viper" - pooRecorder "github.com/t-liu93/home-automation-backend/components" + "github.com/t-liu93/home-automation-backend/components/homeassistant" + "github.com/t-liu93/home-automation-backend/components/pooRecorder" "github.com/t-liu93/home-automation-backend/util/notion" ) var port string +var scheduler gocron.Scheduler // serveCmd represents the serve command var serveCmd = &cobra.Command{ @@ -41,11 +44,12 @@ func initUtil() { func initComponent() { // init pooRecorder - pooRecorder.Init() + pooRecorder.Init(&scheduler) } func serve(cmd *cobra.Command, args []string) { - slog.Info("Starting server...") + slog.Info("Starting server..") + viper.SetConfigName("config") // name of config file (without extension) viper.SetConfigType("yaml") viper.AddConfigPath("$HOME/.config/home-automation") @@ -56,23 +60,46 @@ func serve(cmd *cobra.Command, args []string) { os.Exit(1) } viper.WatchConfig() + viper.SetDefault("logLevel", "info") + logLevelCfg := viper.GetString("logLevel") + switch logLevelCfg { + case "debug": + slog.SetLogLoggerLevel(slog.LevelDebug) + case "info": + slog.SetLogLoggerLevel(slog.LevelInfo) + case "warn": + slog.SetLogLoggerLevel(slog.LevelWarn) + case "error": + slog.SetLogLoggerLevel(slog.LevelError) + } + if viper.InConfig("port") { port = viper.GetString("port") } else { slog.Error("Port not found in config file, exiting..") os.Exit(1) } - + scheduler, err = gocron.NewScheduler() + defer scheduler.Shutdown() + if err != nil { + slog.Error(fmt.Sprintf("Cannot create scheduler, %s, exiting..", err)) + os.Exit(1) + } initUtil() initComponent() + scheduler.Start() // routing router := mux.NewRouter() router.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }).Methods("GET") + + router.HandleFunc("/poo/latest", pooRecorder.HandleNotifyLatestPoo).Methods("GET") router.HandleFunc("/poo/record", pooRecorder.HandleRecordPoo).Methods("POST") + router.HandleFunc("/homeassistant/publish", homeassistant.HandleHaMessage).Methods("POST") + srv := &http.Server{ Addr: ":" + port, Handler: router, diff --git a/src/components/homeassistant/homeassistant.go b/src/components/homeassistant/homeassistant.go new file mode 100644 index 0000000..356bc05 --- /dev/null +++ b/src/components/homeassistant/homeassistant.go @@ -0,0 +1,54 @@ +package homeassistant + +import ( + "encoding/json" + "fmt" + "log/slog" + "net/http" + "time" + + "github.com/spf13/viper" +) + +type haMessage struct { + Target string `json:"target"` + Action string `json:"action"` + Content string `json:"content"` +} + +func HandleHaMessage(w http.ResponseWriter, r *http.Request) { + var message haMessage + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() + err := decoder.Decode(&message) + if err != nil { + slog.Warn(fmt.Sprintln("HandleHaMessage Error decoding request body", err)) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + switch message.Target { + case "poo_recorder": + handlePooRecorderMsg(message) + } + +} + +func handlePooRecorderMsg(message haMessage) { + switch message.Action { + case "get_latest": + handleGetLatestPoo() + } + +} + +func handleGetLatestPoo() { + client := &http.Client{ + Timeout: time.Second * 1, + } + port := viper.GetString("port") + _, err := client.Get("http://localhost:" + port + "/poo/latest") + if err != nil { + slog.Warn(fmt.Sprintln("handleGetLatestPoo Error sending request to poo recorder", err)) + } +} diff --git a/src/components/pooRecorder.go b/src/components/pooRecorder.go deleted file mode 100644 index 28aac87..0000000 --- a/src/components/pooRecorder.go +++ /dev/null @@ -1,156 +0,0 @@ -package pooRecorder - -import ( - "bytes" - "database/sql" - "encoding/json" - "fmt" - "net/http" - "os" - "time" - - "github.com/spf13/viper" - "github.com/t-liu93/home-automation-backend/util/notion" - "golang.org/x/exp/slog" -) - -type recordDetail struct { - Status string `json:"status"` - Latitude string `json:"latitude"` - Longitude string `json:"longitude"` -} - -type pooStatusHttpSensorAttributes struct { - LastPoo string `json:"last_poo"` -} - -type pooStatusHttpSensor struct { - EntityId string `json:"entity_id"` - State string `json:"state"` - Attributes pooStatusHttpSensorAttributes `json:"attributes"` -} - -var ( - db *sql.DB -) - -func publishPooStatus(pooStatus pooStatusHttpSensor) { - if viper.InConfig("pooRecorder.homeassistantIp") && - viper.InConfig("pooRecorder.homeassistantPort") && - viper.InConfig("pooRecorder.homeassistantToken") { - homeAssistantIp := viper.GetString("pooRecorder.homeassistantIp") - homeAssistantPort := viper.GetString("pooRecorder.homeassistantPort") - url := fmt.Sprintf("http://%s:%s/api/states/%s", homeAssistantIp, homeAssistantPort, pooStatus.EntityId) - payload, err := json.Marshal(pooStatus) - if err != nil { - slog.Warn(fmt.Sprintln("HandleRecordPoo Error marshalling poo status", err)) - return - } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) - if err != nil { - slog.Warn(fmt.Sprintln("HandleRecordPoo Error creating request", err)) - return - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+viper.GetString("pooRecorder.homeassistantToken")) - client := &http.Client{ - Timeout: time.Second * 1, - } - resp, err := client.Do(req) - if err != nil { - slog.Warn(fmt.Sprintln("HandleRecordPoo Error sending request", err)) - } - if resp.StatusCode != http.StatusOK { - slog.Warn(fmt.Sprintln("HandleRecordPoo Unexpected response status", resp.StatusCode)) - } - defer resp.Body.Close() - } else { - slog.Warn("HandleRecordPoo Home Assistant IP, port, or token not found in config file") - } -} - -func storeStatus(record recordDetail, timestamp time.Time) { - tableId := viper.GetString("pooRecorder.tableId") - recordDate := timestamp.Format("2006-01-02") - recordTime := timestamp.Format("15:04") - slog.Info(fmt.Sprintln("Recording poo", record.Status, "at", record.Latitude, record.Longitude)) - go func() { - header, err := notion.GetTableRows(tableId, 1, "") - if err != nil { - slog.Warn(fmt.Sprintln("HandleRecordPoo Failed to get table header", err)) - return - } - if len(header) == 0 { - slog.Warn("HandleRecordPoo Table header not found") - return - } - headerId := header[0].GetID() - err = notion.WriteTableRow([]string{recordDate, recordTime, record.Status, record.Latitude + "," + record.Longitude}, tableId, headerId.String()) - if err != nil { - slog.Warn(fmt.Sprintln("HandleRecordPoo Failed to write table row", err)) - } - }() - -} - -func migrateDb() { - -} - -func initDb() { - if !viper.InConfig("pooRecorder.dbPath") { - slog.Info("HandleRecordPoo dbPath not found in config file, using default: pooRecorder.db") - viper.SetDefault("pooRecorder.dbPath", "pooRecorder.db") - } - - dbPath := viper.GetString("pooRecorder.dbPath") - err := error(nil) - db, err = sql.Open("sqlite3", dbPath) - if err != nil { - slog.Error(fmt.Sprintln("PooRecorderInit Error opening database", err)) - os.Exit(1) - } - err = db.Ping() - if err != nil { - slog.Error(fmt.Sprintln("PooRecorderInit Error pinging database", err)) - os.Exit(1) - } - _, err = db.Exec(`CREATE TABLE IF NOT EXISTS poo_records ( - timestamp TEXT PRIMARY KEY, - status TEXT, - latitude TEXT, - longitude TEXT)`) - -} - -func Init() { - initDb() -} - -func HandleRecordPoo(w http.ResponseWriter, r *http.Request) { - var record recordDetail - if !viper.InConfig("pooRecorder.tableId") { - slog.Warn("HandleRecordPoo Table ID not found in config file") - http.Error(w, "Table ID not found in config file", http.StatusInternalServerError) - return - } - decorder := json.NewDecoder(r.Body) - decorder.DisallowUnknownFields() - err := decorder.Decode(&record) - if err != nil { - slog.Warn(fmt.Sprintln("HandleRecordPoo Error decoding request body", err)) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - now := time.Now() - storeStatus(record, now) - timeAttribute := now.Format("Mon | 2006-01-02 | 15:04") - pooStatus := pooStatusHttpSensor{ - EntityId: "sensor.test_poo_sensor", - State: record.Status, - Attributes: pooStatusHttpSensorAttributes{ - LastPoo: timeAttribute, - }, - } - publishPooStatus(pooStatus) -} diff --git a/src/components/pooRecorder/pooRecorder.go b/src/components/pooRecorder/pooRecorder.go new file mode 100644 index 0000000..d98edf0 --- /dev/null +++ b/src/components/pooRecorder/pooRecorder.go @@ -0,0 +1,210 @@ +package pooRecorder + +import ( + "database/sql" + "encoding/json" + "fmt" + "net/http" + "os" + "time" + + "log/slog" + + "github.com/go-co-op/gocron/v2" + "github.com/spf13/viper" + "github.com/t-liu93/home-automation-backend/util/homeassistantutil" + "github.com/t-liu93/home-automation-backend/util/notion" + _ "modernc.org/sqlite" +) + +var ( + db *sql.DB + scheduler *gocron.Scheduler +) + +type recordDetail struct { + Status string `json:"status"` + Latitude string `json:"latitude"` + Longitude string `json:"longitude"` +} + +type pooStatusSensorAttributes struct { + LastPoo string `json:"last_poo"` +} + +type pooStatusWebhookBody struct { + Status string `json:"status"` +} + +type pooStatusDbEntry struct { + Timestamp string + Status string + Latitude float64 + Longitude float64 +} + +func Init(mainScheduler *gocron.Scheduler) { + initDb() + initScheduler(mainScheduler) +} + +func HandleRecordPoo(w http.ResponseWriter, r *http.Request) { + var record recordDetail + if !viper.InConfig("pooRecorder.tableId") { + slog.Warn("HandleRecordPoo Table ID not found in config file") + http.Error(w, "Table ID not found in config file", http.StatusInternalServerError) + return + } + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() + err := decoder.Decode(&record) + if err != nil { + slog.Warn(fmt.Sprintln("HandleRecordPoo Error decoding request body", err)) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + now := time.Now() + err = storeStatus(record, now) + if err != nil { + slog.Warn(fmt.Sprintln("HandleRecordPoo Error storing status", err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + pooStatus := homeassistantutil.HttpSensor{ + EntityId: "sensor.test_poo_sensor", + State: record.Status, + Attributes: pooStatusSensorAttributes{ + LastPoo: now.Format("Mon | 2006-01-02 | 15:04"), + }, + } + homeassistantutil.PublishSensor(pooStatus) + if viper.InConfig("pooRecorder.webhookId") { + homeassistantutil.TriggerWebhook(viper.GetString("pooRecorder.webhookId"), pooStatusWebhookBody{Status: record.Status}) + } else { + slog.Warn("HandleRecordPoo Webhook ID not found in config file") + } +} + +func HandleNotifyLatestPoo(w http.ResponseWriter, r *http.Request) { + var latest pooStatusDbEntry + err := db.QueryRow(`SELECT timestamp, status, latitude, longitude FROM poo_records ORDER BY timestamp DESC LIMIT 1`).Scan(&latest.Timestamp, &latest.Status, &latest.Latitude, &latest.Longitude) + if err != nil { + slog.Warn(fmt.Sprintln("HandleGetLatestPoo Error getting latest poo", err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + recordTime, err := time.Parse("2006-01-02T15:04Z07:00", latest.Timestamp) + if err != nil { + slog.Warn(fmt.Sprintln("HandleGetLatestPoo Error parsing timestamp", err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + recordTime = recordTime.Local() + pooStatus := homeassistantutil.HttpSensor{ + EntityId: "sensor.test_poo_sensor", + State: latest.Status, + Attributes: pooStatusSensorAttributes{ + LastPoo: recordTime.Format("Mon | 2006-01-02 | 15:04"), + }, + } + homeassistantutil.PublishSensor(pooStatus) + slog.Debug(fmt.Sprintln("HandleGetLatestPoo Latest poo", pooStatus.State, "at", pooStatus.Attributes.(pooStatusSensorAttributes).LastPoo)) +} + +func initDb() { + if !viper.InConfig("pooRecorder.dbPath") { + slog.Info("HandleRecordPoo dbPath not found in config file, using default: pooRecorder.db") + viper.SetDefault("pooRecorder.dbPath", "pooRecorder.db") + } + + dbPath := viper.GetString("pooRecorder.dbPath") + err := error(nil) + db, err = sql.Open("sqlite", dbPath) + if err != nil { + slog.Error(fmt.Sprintln("PooRecorderInit Error opening database", err)) + os.Exit(1) + } + err = db.Ping() + if err != nil { + slog.Error(fmt.Sprintln("PooRecorderInit Error pinging database", err)) + os.Exit(1) + } + migrateDb() +} + +func migrateDb() { + var userVersion int + err := db.QueryRow("PRAGMA user_version").Scan(&userVersion) + if err != nil { + slog.Error(fmt.Sprintln("PooRecorderInit Error getting db user version", err)) + os.Exit(1) + } + if userVersion == 0 { + migrateDb0To1(&userVersion) + } +} + +func migrateDb0To1(userVersion *int) { + // this is actually create new db + slog.Info("Creating database version 1..") + _, err := db.Exec(`CREATE TABLE IF NOT EXISTS poo_records ( + timestamp TEXT NOT NULL, + status TEXT NOT NULL, + latitude REAL NOT NULL, + longitude REAL NOT NULL, + PRIMARY KEY (timestamp))`) + if err != nil { + slog.Error(fmt.Sprintln("PooRecorderInit Error creating table", err)) + os.Exit(1) + } + _, err = db.Exec(`PRAGMA user_version = 1`) + if err != nil { + slog.Error(fmt.Sprintln("PooRecorderInit Error setting user version to 1", err)) + os.Exit(1) + } + *userVersion = 1 +} + +func initScheduler(mainScheduler *gocron.Scheduler) { + scheduler = mainScheduler + _, err := (*scheduler).NewJob(gocron.CronJob("0 5 * * *", false), gocron.NewTask( + per, + )) + if err != nil { + slog.Error(fmt.Sprintln("PooRecorderInit Error creating scheduled task", err)) + os.Exit(1) + } +} + +func per() { + slog.Info("PooRecorderInit Running scheduled task ") +} + +func storeStatus(record recordDetail, timestamp time.Time) error { + tableId := viper.GetString("pooRecorder.tableId") + recordDate := timestamp.Format("2006-01-02") + recordTime := timestamp.Format("15:04") + slog.Debug(fmt.Sprintln("Recording poo", record.Status, "at", record.Latitude, record.Longitude)) + _, err := db.Exec(`INSERT OR IGNORE INTO poo_records (timestamp, status, latitude, longitude) VALUES (?, ?, ?, ?)`, + timestamp.UTC().Format("2006-01-02T15:04Z07:00"), record.Status, record.Latitude, record.Longitude) + if err != nil { + return err + } + go func() { + header, err := notion.GetTableRows(tableId, 1, "") + if err != nil { + slog.Warn(fmt.Sprintln("HandleRecordPoo Failed to get table header", err)) + return + } + if len(header) == 0 { + slog.Warn("HandleRecordPoo Table header not found") + return + } + headerId := header[0].GetID() + err = notion.WriteTableRow([]string{recordDate, recordTime, record.Status, record.Latitude + "," + record.Longitude}, tableId, headerId.String()) + if err != nil { + slog.Warn(fmt.Sprintln("HandleRecordPoo Failed to write table row", err)) + } + }() + return nil +} diff --git a/src/go.mod b/src/go.mod index 3f0b255..96c3d17 100644 --- a/src/go.mod +++ b/src/go.mod @@ -3,20 +3,30 @@ module github.com/t-liu93/home-automation-backend go 1.23.0 require ( + github.com/go-co-op/gocron/v2 v2.11.0 github.com/gorilla/mux v1.8.1 github.com/jomei/notionapi v1.13.2 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 golang.org/x/term v0.24.0 + modernc.org/sqlite v1.33.1 ) require ( + github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jonboulle/clockwork v0.4.0 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -26,9 +36,15 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + modernc.org/libc v1.55.3 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect ) diff --git a/src/go.sum b/src/go.sum index 1a7a9f2..31842a7 100644 --- a/src/go.sum +++ b/src/go.sum @@ -3,33 +3,53 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/go-co-op/gocron/v2 v2.11.0 h1:IOowNA6SzwdRFnD4/Ol3Kj6G2xKfsoiiGq2Jhhm9bvE= +github.com/go-co-op/gocron/v2 v2.11.0/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jomei/notionapi v1.13.2 h1:YpHKNpkoTMlUfWTlVIodOmQDgRKjfwmtSNVa6/6yC9E= github.com/jomei/notionapi v1.13.2/go.mod h1:BqzP6JBddpBnXvMSIxiR5dCoCjKngmz5QNl1ONDlDoM= +github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -63,21 +83,56 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= +modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= +modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/src/pooRecorder.db b/src/pooRecorder.db new file mode 100644 index 0000000000000000000000000000000000000000..a219bbe03ee1dd8c763fce25e68c8c65ee6d67c3 GIT binary patch literal 12288 zcmeI0PiWIn9LJM(UDu|0Z~wZk4k0iGn`|yg({}3+I;CK-&TUq3L8#Tnt zu54S%t?JHZ(NxqCeGR8F>(Ss(fgaFZTB0gBBKzby*&qfHD6}7X7P=mq zmJX$l(i7>LG|3QbXaEhM0W^RH&;S}h18CsCFrY_$vlD*5uIk2&IzOW&^O}*;ljirm zy;sV4t5~r&?xwSErd~Wcv78R?``)ETmh@4GiMulqh%wxm5rEO=+?g=MaOO4)F){b& z3^^D*6;s`rL4eWc-I)OgqooWE(+@Dbxlw@8Ij#xBaQ2Q2F$s4EA&B9usRS{c{T756 z&OQh@7&R5=?1R4JOd_vqDOGoMaLV6cQE~vz9)H;jBv!#BkQd z+s$Zm+_`xmhO^H^h;jcuLH3UwmA?q|8~se*)7SJVy-(Na3SFQRl#*ZMEBQp;l9%K$ zsgnZ9k{CHj{PGWmU_%3F01co4G=K)s02)98XaEhMfq%n5*gGq>-DXZ%AJ5yG-p+K5636LgC8A#)1 z2uKZ60@9cn>`t3CkifCDRpsdGPMdWMzcXbOi(Vj2m_8tln>}6WpSpq