package main import ( "bytes" "encoding/json" "fmt" "io/ioutil" "log" "net/http" "net/url" "os" "path/filepath" "strconv" "strings" "time" ) type Status struct { FuzIsOpen bool `json:"fuzIsOpen"` LastSeenAsOpen bool `json:"lastSeenAsOpen"` LastSeen time.Time `json:"lastSeen"` LastOpened time.Time `json:"lastOpened"` LastClosed time.Time `json:"lastClosed"` ProcessUptime string `json:"processUptime"` } type Config struct { PORT string MATRIXROOM string MATRIXOPENINGMESSAGE string MATRIXCLOSINGMESSAGE string MATRIXACCESSTOKEN string MATRIXUSERNAME string ESPUSERNAME string ESPPASSWORD string } const ( dbPath = "./.data/data.json" defaultClosingTimeout = 5 * time.Minute ) var ( status Status config = Config{ PORT: "8080", } startTime = time.Now() imgs = map[bool]string{ // https://www.iconfinder.com/icons/1871431/online_open_shop_shopping_sign_icon // formerly https://www.flaticon.com/free-icon/open_1234189, maybe try https://flaticons.net/customize.php?dir=Miscellaneous&icon=Open.png without attribution true: ``, // https://www.iconfinder.com/icons/1871435/closed_online_shop_shopping_sign_icon // formerly https://www.flaticon.com/free-icon/closed_1234190, maybe try https://flaticons.net/customize.php?dir=Miscellaneous&icon=Closed.png without attribution false: ``, } db *os.File ) func init() { port := os.Getenv("PORT") if val, _ := strconv.Atoi(port); val > 0 { config.PORT = port } config.MATRIXROOM = os.Getenv("MATRIXROOM") config.MATRIXOPENINGMESSAGE = os.Getenv("MATRIXOPENINGMESSAGE") config.MATRIXCLOSINGMESSAGE = os.Getenv("MATRIXCLOSINGMESSAGE") config.MATRIXACCESSTOKEN = os.Getenv("MATRIXACCESSTOKEN") config.MATRIXUSERNAME = os.Getenv("MATRIXUSERNAME") config.ESPUSERNAME = os.Getenv("ESPUSERNAME") config.ESPPASSWORD = os.Getenv("ESPPASSWORD") if config.MATRIXROOM == "" { panic("MATRIXROOM is empty") } if config.MATRIXOPENINGMESSAGE == "" { panic("MATRIXOPENINGMESSAGE is empty") } if config.MATRIXCLOSINGMESSAGE == "" { panic("MATRIXCLOSINGMESSAGE is empty") } if config.MATRIXACCESSTOKEN == "" { panic("MATRIXACCESSTOKEN is empty") } if config.MATRIXUSERNAME == "" { panic("MATRIXUSERNAME is empty") } if config.ESPPASSWORD == "" { panic("ESPPASSWORD is empty") } err := os.MkdirAll(filepath.Dir(dbPath), 0755) if err != nil { panic(err) } db, err = os.OpenFile(dbPath, os.O_RDWR|os.O_CREATE, 0600) if err != nil && !os.IsNotExist(err) { panic(err) } d := json.NewDecoder(db) d.Decode(&status) if err != nil { fmt.Println("error unmarshalling db:", err) } } func updateUptime() { for range time.Tick(time.Second) { status.ProcessUptime = time.Since(startTime).Truncate(time.Second).String() } } func checkClosure() { time.Sleep(time.Minute) // give some time for presence button to show up for { if status.LastSeen.Add(defaultClosingTimeout).Before(time.Now()) && status.LastClosed.Before(status.LastSeen) { // the Fuz is newly closed, notify on matrix and write file to survive reboot // TODO: matrix msg fmt.Println("the Fuz is newly closed, notify on matrix and write file to survive reboot") err := sendMatrixMessage(config.MATRIXUSERNAME, config.MATRIXACCESSTOKEN, config.MATRIXROOM, config.MATRIXCLOSINGMESSAGE) if err != nil { fmt.Println("err:", err) time.Sleep(10 * time.Second) continue } status.LastClosed = time.Now() status.FuzIsOpen = false db.Truncate(0) db.Seek(0, 0) e := json.NewEncoder(db) e.SetIndent("", " ") e.Encode(status) } time.Sleep(10 * time.Second) } } func rootHandler(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } fmt.Fprintf(w, "Beautiful homepage") } func apiHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") e := json.NewEncoder(w) e.SetIndent("", " ") e.Encode(status) } func imgHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "image/svg+xml") w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate") w.Header().Set("Pragma", "no-cache") w.Header().Set("Expires", "0") fmt.Fprintf(w, imgs[status.FuzIsOpen]) } func statusHandler(w http.ResponseWriter, r *http.Request) { user, pass, ok := r.BasicAuth() fmt.Println("user", user, "pass", pass, "ok", ok) if !ok || user != config.ESPUSERNAME || pass != config.ESPPASSWORD { w.Header().Set("WWW-Authenticate", `Basic realm="Authentication required"`) http.Error(w, "Authentication required", 401) return } q := r.URL.Query() status.FuzIsOpen = q.Get("fuzisopen") == "1" status.LastSeenAsOpen = q.Get("fuzisopen") == "1" status.LastSeen = time.Now() fmt.Fprintf(w, "OK") db.Truncate(0) db.Seek(0, 0) e := json.NewEncoder(db) e.SetIndent("", " ") e.Encode(status) if status.FuzIsOpen && (status.LastOpened.Equal(status.LastClosed) || status.LastOpened.Before(status.LastClosed)) { // the Fuz is newly opened, notify on matrix and write file to survive reboot fmt.Println("the Fuz is newly opened, notify on matrix and write file to survive reboot") err := sendMatrixMessage(config.MATRIXUSERNAME, config.MATRIXACCESSTOKEN, config.MATRIXROOM, config.MATRIXOPENINGMESSAGE) if err != nil { fmt.Println("err:", err) return } status.LastOpened = time.Now() db.Truncate(0) db.Seek(0, 0) e := json.NewEncoder(db) e.SetIndent("", " ") e.Encode(status) } } func sendMatrixMessage(username, accessToken, room, messageText string) error { type Message struct { Msgtype string `json:"msgtype"` Body string `json:"body"` } client := &http.Client{} message := Message{ Msgtype: "m.text", Body: messageText, } body := new(bytes.Buffer) err := json.NewEncoder(body).Encode(message) if err != nil { return err } v := url.Values{} v.Set("access_token", accessToken) v.Set("limit", "1") url := url.URL{ Scheme: "https", Host: username[strings.Index(username, ":")+1:], Path: fmt.Sprintf("/_matrix/client/r0/rooms/%s/send/m.room.message/%d", room, time.Now().UnixNano()/1000000), RawQuery: v.Encode(), } req, err := http.NewRequest(http.MethodPut, url.String(), body) if err != nil { return err } req.Header.Set("Content-Type", "application/json") res, err := client.Do(req) if err != nil { return err } defer res.Body.Close() resBody, _ := ioutil.ReadAll(res.Body) fmt.Println(string(resBody)) return nil } func main() { go updateUptime() go checkClosure() http.HandleFunc("/", rootHandler) http.HandleFunc("/api", apiHandler) http.HandleFunc("/img", imgHandler) http.HandleFunc("/status", statusHandler) log.Fatal(http.ListenAndServe(":"+config.PORT, nil)) }