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))
}