From 5d0a59dc8625b564435015a89e2966697986d6e8 Mon Sep 17 00:00:00 2001 From: Daniel Svitan Date: Sun, 4 May 2025 21:32:00 +0200 Subject: [PATCH] :sparkles: Adds locking and gotify alerts --- go.mod | 1 + go.sum | 2 + main.go | 173 +++++++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 155 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index d622384..ec2f162 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( ) require ( + github.com/joho/godotenv v1.5.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect diff --git a/go.sum b/go.sum index 780bd20..aacf720 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= diff --git a/main.go b/main.go index 3fbf51a..eb72c87 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,19 @@ package main import ( + "bytes" + "encoding/json" "errors" "fmt" + "io" "net/http" + "os" + "strconv" + "strings" "sync" "time" + "github.com/joho/godotenv" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/labstack/gommon/log" @@ -14,20 +21,41 @@ import ( const TimeFormat = "2006-01-02 15:04:05" -// the condition is: distance >= threshold -var value uint16 = 0 -var valueMutex sync.Mutex +var token string -type ReadReq struct { - token string `body:"token"` -} +// the condition is: distance >= threshold (is door open) +var opened bool + +// is door locked +var locked bool = false + +// alert the user when door locked and but open? +var alert bool = false +var gotifyToken string +var gotifyURL string + +var mut sync.Mutex type WriteReq struct { - value uint16 `body:"value"` - token string `body:"token"` + opened bool `body:"opened"` } func main() { + _ = godotenv.Load() + token = os.Getenv("TOKEN") + + alertRaw := strings.ToLower(os.Getenv("USE_ALERTS")) + alert = alertRaw == "true" || alertRaw == "t" || alertRaw == "1" || alertRaw == "y" || alertRaw == "yes" + + gotifyToken = os.Getenv("GOTIFY_TOKEN") + gotifyURL = os.Getenv("GOTIFY_URL") + if alert && gotifyToken == "" { + log.Fatal("GOTIFY_TOKEN can't be empty when alerts are enabled") + } + if alert && gotifyURL == "" { + log.Fatal("GOTIFY_URL can't be empty when alerts are enabled") + } + app := echo.New() app.Logger.SetLevel(log.INFO) @@ -76,29 +104,132 @@ func main() { return c.JSON(http.StatusOK, "Hello, World!") }) - app.GET("/read", func(c echo.Context) error { - var data ReadReq - err := c.Bind(&data) - if err != nil { - return c.NoContent(http.StatusBadRequest) - } + app.GET("/read", authed(func(c echo.Context) error { + mut.Lock() + o := opened + mut.Unlock() - return c.JSON(http.StatusOK, echo.Map{"value": value}) - }) + return c.JSON(http.StatusOK, o) + })) - app.POST("/write", func(c echo.Context) error { + app.POST("/write", authed(func(c echo.Context) error { var data WriteReq err := c.Bind(&data) if err != nil { return c.NoContent(http.StatusBadRequest) } - valueMutex.Lock() - value = data.value - valueMutex.Unlock() + mut.Lock() + if data.opened == opened { + mut.Unlock() + return c.NoContent(http.StatusOK) + } + + opened = data.opened + if locked && alert { + var action string + if opened { + action = "opened" + } else { + action = "closed" + } + + go sendAlert(action) + } + + mut.Unlock() + return c.NoContent(http.StatusOK) + })) + + app.POST("/lock", authed(func(c echo.Context) error { + mut.Lock() + locked = true + mut.Unlock() return c.NoContent(http.StatusOK) - }) + })) + + app.POST("/unlock", authed(func(c echo.Context) error { + mut.Lock() + locked = false + mut.Unlock() + + return c.NoContent(http.StatusOK) + })) + + app.POST("/alerts/pause", authed(func(c echo.Context) error { + pauseForRaw := c.QueryParam("for") + if pauseForRaw != "" { + pauseFor, err := strconv.Atoi(pauseForRaw) + if err != nil || pauseFor <= 0 { + return c.JSON(http.StatusBadRequest, "query param 'for' not valid") + } + + go func() { + time.Sleep(time.Duration(pauseFor) * time.Second) + + mut.Lock() + alert = true + mut.Unlock() + }() + } + + mut.Lock() + alert = false + mut.Unlock() + + return c.NoContent(http.StatusOK) + })) + + app.POST("/alerts/resume", authed(func(c echo.Context) error { + mut.Lock() + alert = false + mut.Unlock() + + return c.NoContent(http.StatusOK) + })) log.Fatal(app.Start(":1323")) } + +func authed(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if c.Request().Header.Get("Authorization") != token { + return c.NoContent(http.StatusUnauthorized) + } + + return next(c) + } +} + +func sendAlert(action string) { + to := fmt.Sprintf("%s/message?token=%s", gotifyURL, gotifyToken) + what := echo.Map{ + "title": fmt.Sprintf("Your door has been %s", action), + "priority": 1, + "message": fmt.Sprintf("Your locked door has been %s at %s", action, time.Now().Format(TimeFormat)), + } + b, err := json.Marshal(&what) + if err != nil { + log.Error(err) + return + } + + res, err := http.Post(to, "application/json", bytes.NewBuffer(b)) + if err != nil { + log.Error(err) + return + } + + body, err := io.ReadAll(res.Body) + if err != nil { + log.Error(err) + return + } + + if res.StatusCode != 200 { + log.Errorf("sendAlert response status code is not 200: %d, body:\n%s", res.StatusCode, body) + } else { + log.Infof("sent alert (%s) to gotify", action) + } +}