Browse Source

Post space opening/closure posting on (lack of) ESP ping

go-version
Lomanic 10 months ago
parent
commit
a2f15a9f68
2 changed files with 242 additions and 7 deletions
  1. 2
    0
      .gitignore
  2. 240
    7
      main.go

+ 2
- 0
.gitignore View File

@@ -0,0 +1,2 @@
1
+/.data/
2
+/.env

+ 240
- 7
main.go View File

@@ -1,16 +1,249 @@
1 1
 package main
2 2
 
3 3
 import (
4
-    "fmt"
5
-    "log"
6
-    "net/http"
4
+	"bytes"
5
+	"encoding/json"
6
+	"fmt"
7
+	"io/ioutil"
8
+	"log"
9
+	"net/http"
10
+	"net/url"
11
+	"os"
12
+	"path/filepath"
13
+	"strconv"
14
+	"strings"
15
+	"time"
7 16
 )
8 17
 
9
-func handler(w http.ResponseWriter, r *http.Request) {
10
-    fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
18
+type Status struct {
19
+	FuzIsOpen      bool      `json:"fuzIsOpen"`
20
+	LastSeenAsOpen bool      `json:"lastSeenAsOpen"`
21
+	LastSeen       time.Time `json:"lastSeen"`
22
+	LastOpened     time.Time `json:"lastOpened"`
23
+	LastClosed     time.Time `json:"lastClosed"`
24
+	ProcessUptime  string    `json:"processUptime"`
25
+}
26
+
27
+type Config struct {
28
+	PORT                 string
29
+	MATRIXROOM           string
30
+	MATRIXOPENINGMESSAGE string
31
+	MATRIXCLOSINGMESSAGE string
32
+	MATRIXACCESSTOKEN    string
33
+	MATRIXUSERNAME       string
34
+	ESPUSERNAME          string
35
+	ESPPASSWORD          string
36
+}
37
+
38
+const (
39
+	dbPath                = "./.data/data.json"
40
+	defaultClosingTimeout = 5 * time.Minute
41
+)
42
+
43
+var (
44
+	status Status
45
+	config = Config{
46
+		PORT: "8080",
47
+	}
48
+	startTime = time.Now()
49
+	imgs      = map[bool]string{
50
+		// https://www.iconfinder.com/icons/1871431/online_open_shop_shopping_sign_icon
51
+		// formerly https://www.flaticon.com/free-icon/open_1234189, maybe try https://flaticons.net/customize.php?dir=Miscellaneous&icon=Open.png without attribution
52
+		true: `<?xml version="1.0" ?><svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:#5bc9e1;}.cls-2{fill:#fd0;}.cls-3{fill:#314967;}</style></defs><title/><g data-name="15 Open Sign" id="_15_Open_Sign"><rect class="cls-1" height="36" rx="6" ry="6" width="96" x="16" y="64"/><circle class="cls-2" cx="64" cy="28" r="6"/><path class="cls-3" d="M92.73,98H22a4,4,0,0,1-4-4V70a4,4,0,0,1,4-4H42.54a2,2,0,0,0,0-4H32.83L59.93,34.89a8,8,0,0,0,8.13,0L81.71,48.54a2,2,0,0,0,2.83-2.83L70.88,32.06A8,8,0,0,0,64,20a2,2,0,0,0,0,4,4,4,0,1,1-3.49,2,2,2,0,0,0-3.49-2,8,8,0,0,0,.09,8L27.17,62H22a8,8,0,0,0-8,8V94a8,8,0,0,0,8,8H92.73A2,2,0,0,0,92.73,98Z"/><path class="cls-3" d="M106,62h-5.17L88.09,49.26a2,2,0,0,0-2.83,2.83L95.17,62H76a2,2,0,0,0,0,4h30a4,4,0,0,1,4,4V94a4,4,0,0,1-4,4H98.51a2,2,0,0,0,0,4H106a8,8,0,0,0,8-8V70A8,8,0,0,0,106,62Z"/><path class="cls-3" d="M70,62H49.22a2,2,0,0,0,0,4H70A2,2,0,0,0,70,62Z"/><path class="cls-3" d="M54.86,73.62a2.2,2.2,0,0,0-2.19,2.19v12.8a2.2,2.2,0,0,0,4.41,0V84.91h1.67a5.64,5.64,0,1,0,0-11.29ZM60,79.27c0,1.49-1.64,1.23-2.93,1.23V78C58.42,78,60,77.78,60,79.27Z"/><path class="cls-3" d="M77,78c2.86,0,2.93-4.41,0-4.41H69.92a2.17,2.17,0,0,0-2.19,2.19v12.8a2.2,2.2,0,0,0,1.25,2v.21h8c2.91,0,2.87-4.41,0-4.41H72.13v-2h4.06a2.2,2.2,0,0,0,0-4.41H72.13V78Z"/><path class="cls-3" d="M96.63,76c0-2.83-4.37-2.9-4.37,0v5.85l-5.19-7.19A2.18,2.18,0,0,0,83.13,76V88.62a2.18,2.18,0,0,0,4.37,0V82.71l5.1,7.08a2.19,2.19,0,0,0,4-1.17Z"/><path class="cls-3" d="M39.84,73.19a8.82,8.82,0,0,0,0,17.62A8.69,8.69,0,0,0,48.22,82C48.22,77.31,44.49,73.19,39.84,73.19Zm0,13.15c-5.3,0-5.26-8.68,0-8.68a4.36,4.36,0,0,1,0,8.68Z"/></g></svg>`,
53
+		// https://www.iconfinder.com/icons/1871435/closed_online_shop_shopping_sign_icon
54
+		// formerly https://www.flaticon.com/free-icon/closed_1234190, maybe try https://flaticons.net/customize.php?dir=Miscellaneous&icon=Closed.png without attribution
55
+		false: `<?xml version="1.0" ?><svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:#f8991d;}.cls-2{fill:#fd0;}.cls-3{fill:#314967;}</style></defs><title/><g data-name="18 Closed Sign" id="_18_Closed_Sign"><rect class="cls-1" height="36" rx="6" ry="6" width="96" x="16" y="64"/><circle class="cls-2" cx="64" cy="28" r="6"/><path class="cls-3" d="M71.85,77.83a4.37,4.37,0,0,1,2.34,1,1.93,1.93,0,1,0,2.08-3.25A7.94,7.94,0,0,0,71.85,74c-3.13,0-5.55,2.06-5.55,4.58,0,2.74,2.64,4.21,5.35,4.58.55.11,2,.45,2.26,1,0,.57-1.38,1-2,1a5.25,5.25,0,0,1-2.79-1.16,1.93,1.93,0,1,0-2.42,3,8.73,8.73,0,0,0,5.23,2c3.12,0,5.88-2,5.88-4.82s-2.88-4.4-5.66-4.78c-1.6-.31-2-.77-2-.77C70.15,78.39,70.8,77.83,71.85,77.83Z"/><path class="cls-3" d="M40,76.34V87a2,2,0,0,0,2,2h5.83a2,2,0,0,0,0-4H44V76.34A2,2,0,0,0,40,76.34Z"/><path class="cls-3" d="M50,81.48A7.16,7.16,0,1,0,57.23,74,7.32,7.32,0,0,0,50,81.48Zm10.26,0c0,2.89-3.21,4.64-5.27,2.43-1.94-2-.71-5.86,2.2-5.86A3.29,3.29,0,0,1,60.3,81.48Z"/><path class="cls-3" d="M34.44,78.82a2,2,0,0,0,2.49-3.21A7.7,7.7,0,0,0,32.15,74a7.6,7.6,0,0,0-7.6,7.48h0c0,6.3,7.44,9.7,12.39,5.86a2,2,0,0,0-2.51-3.2,3.52,3.52,0,1,1,0-5.31Z"/><path class="cls-3" d="M87.69,78.35a2,2,0,0,0,0-4H81.8a2,2,0,0,0-2,2V87a2,2,0,0,0,1,1.75V89h6.83a2,2,0,0,0,0-4H83.81V83.65H87a2,2,0,0,0,0-4h-3.2V78.35Z"/><path class="cls-3" d="M103.09,81.64a7.29,7.29,0,0,0-7.28-7.28H93.69a2,2,0,0,0-2,2V87a2,2,0,0,0,2,2h2.13A7.31,7.31,0,0,0,103.09,81.64ZM95.81,85h-.12V78.35h.12C100.14,78.35,100.18,84.93,95.81,85Z"/><path class="cls-3" d="M92.73,98H22a4,4,0,0,1-4-4V70a4,4,0,0,1,4-4H42.55a2,2,0,0,0,0-4H32.83L59.93,34.89a8,8,0,0,0,8.13,0L81.71,48.54a2,2,0,0,0,2.83-2.83L70.88,32.06A8,8,0,0,0,64,20a2,2,0,0,0,0,4,4,4,0,0,1,2.79,6.86C63.58,34,58.24,30.08,60.51,26a2,2,0,0,0-3.49-2,8,8,0,0,0,.09,8L27.17,62H22a8,8,0,0,0-8,8V94a8,8,0,0,0,8,8H92.73A2,2,0,0,0,92.73,98Z"/><path class="cls-3" d="M106,62h-5.17L88.09,49.26a2,2,0,0,0-2.83,2.83L95.17,62H76a2,2,0,0,0,0,4h30a4,4,0,0,1,4,4V94a4,4,0,0,1-4,4H98.51a2,2,0,0,0,0,4H106a8,8,0,0,0,8-8V70A8,8,0,0,0,106,62Z"/><path class="cls-3" d="M70,62H49.22a2,2,0,0,0,0,4H70A2,2,0,0,0,70,62Z"/></g></svg>`,
56
+	}
57
+	db *os.File
58
+)
59
+
60
+func init() {
61
+	port := os.Getenv("PORT")
62
+	if val, _ := strconv.Atoi(port); val > 0 {
63
+		config.PORT = port
64
+	}
65
+	config.MATRIXROOM = os.Getenv("MATRIXROOM")
66
+	config.MATRIXOPENINGMESSAGE = os.Getenv("MATRIXOPENINGMESSAGE")
67
+	config.MATRIXCLOSINGMESSAGE = os.Getenv("MATRIXCLOSINGMESSAGE")
68
+	config.MATRIXACCESSTOKEN = os.Getenv("MATRIXACCESSTOKEN")
69
+	config.MATRIXUSERNAME = os.Getenv("MATRIXUSERNAME")
70
+	config.ESPUSERNAME = os.Getenv("ESPUSERNAME")
71
+	config.ESPPASSWORD = os.Getenv("ESPPASSWORD")
72
+
73
+	if config.MATRIXROOM == "" {
74
+		panic("MATRIXROOM is empty")
75
+	}
76
+	if config.MATRIXOPENINGMESSAGE == "" {
77
+		panic("MATRIXOPENINGMESSAGE is empty")
78
+	}
79
+	if config.MATRIXCLOSINGMESSAGE == "" {
80
+		panic("MATRIXCLOSINGMESSAGE is empty")
81
+	}
82
+	if config.MATRIXACCESSTOKEN == "" {
83
+		panic("MATRIXACCESSTOKEN is empty")
84
+	}
85
+	if config.MATRIXUSERNAME == "" {
86
+		panic("MATRIXUSERNAME is empty")
87
+	}
88
+	if config.ESPPASSWORD == "" {
89
+		panic("ESPPASSWORD is empty")
90
+	}
91
+
92
+	err := os.MkdirAll(filepath.Dir(dbPath), 0755)
93
+	if err != nil {
94
+		panic(err)
95
+	}
96
+	db, err = os.OpenFile(dbPath, os.O_RDWR|os.O_CREATE, 0600)
97
+	if err != nil && !os.IsNotExist(err) {
98
+		panic(err)
99
+	}
100
+	d := json.NewDecoder(db)
101
+	d.Decode(&status)
102
+	if err != nil {
103
+		fmt.Println("error unmarshalling db:", err)
104
+	}
105
+}
106
+
107
+func updateUptime() {
108
+	for range time.Tick(time.Second) {
109
+		status.ProcessUptime = time.Since(startTime).Truncate(time.Second).String()
110
+	}
111
+}
112
+
113
+func checkClosure() {
114
+	time.Sleep(time.Minute) // give some time for presence button to show up
115
+	for {
116
+		if status.LastSeen.Add(defaultClosingTimeout).Before(time.Now()) && status.LastClosed.Before(status.LastSeen) {
117
+			// the Fuz is newly closed, notify on matrix and write file to survive reboot
118
+			// TODO: matrix msg
119
+			fmt.Println("the Fuz is newly closed, notify on matrix and write file to survive reboot")
120
+			err := sendMatrixMessage(config.MATRIXUSERNAME, config.MATRIXACCESSTOKEN, config.MATRIXROOM, config.MATRIXCLOSINGMESSAGE)
121
+			if err != nil {
122
+				fmt.Println("err:", err)
123
+				time.Sleep(10 * time.Second)
124
+				continue
125
+			}
126
+
127
+			status.LastClosed = time.Now()
128
+			status.FuzIsOpen = false
129
+			db.Truncate(0)
130
+			db.Seek(0, 0)
131
+			e := json.NewEncoder(db)
132
+			e.SetIndent("", "    ")
133
+			e.Encode(status)
134
+		}
135
+		time.Sleep(10 * time.Second)
136
+	}
137
+}
138
+
139
+func rootHandler(w http.ResponseWriter, r *http.Request) {
140
+	if r.URL.Path != "/" {
141
+		http.NotFound(w, r)
142
+		return
143
+	}
144
+	fmt.Fprintf(w, "Beautiful homepage")
145
+}
146
+
147
+func apiHandler(w http.ResponseWriter, r *http.Request) {
148
+	w.Header().Set("Content-Type", "application/json")
149
+	w.Header().Set("Access-Control-Allow-Origin", "*")
150
+	w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
151
+	e := json.NewEncoder(w)
152
+	e.SetIndent("", "    ")
153
+	e.Encode(status)
154
+}
155
+
156
+func imgHandler(w http.ResponseWriter, r *http.Request) {
157
+	w.Header().Set("Content-Type", "image/svg+xml")
158
+	w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate")
159
+	w.Header().Set("Pragma", "no-cache")
160
+	w.Header().Set("Expires", "0")
161
+	fmt.Fprintf(w, imgs[status.FuzIsOpen])
162
+}
163
+
164
+func statusHandler(w http.ResponseWriter, r *http.Request) {
165
+	user, pass, ok := r.BasicAuth()
166
+	fmt.Println("user", user, "pass", pass, "ok", ok)
167
+	if !ok || user != config.ESPUSERNAME || pass != config.ESPPASSWORD {
168
+		w.Header().Set("WWW-Authenticate", `Basic realm="Authentication required"`)
169
+		http.Error(w, "Authentication required", 401)
170
+		return
171
+	}
172
+	q := r.URL.Query()
173
+	status.FuzIsOpen = q.Get("fuzisopen") == "1"
174
+	status.LastSeenAsOpen = q.Get("fuzisopen") == "1"
175
+	status.LastSeen = time.Now()
176
+	fmt.Fprintf(w, "OK")
177
+
178
+	db.Truncate(0)
179
+	db.Seek(0, 0)
180
+	e := json.NewEncoder(db)
181
+	e.SetIndent("", "    ")
182
+	e.Encode(status)
183
+	if status.FuzIsOpen && (status.LastOpened.Equal(status.LastClosed) || status.LastOpened.Before(status.LastClosed)) {
184
+		// the Fuz is newly opened, notify on matrix and write file to survive reboot
185
+		fmt.Println("the Fuz is newly opened, notify on matrix and write file to survive reboot")
186
+		err := sendMatrixMessage(config.MATRIXUSERNAME, config.MATRIXACCESSTOKEN, config.MATRIXROOM, config.MATRIXOPENINGMESSAGE)
187
+		if err != nil {
188
+			fmt.Println("err:", err)
189
+			return
190
+		}
191
+
192
+		status.LastOpened = time.Now()
193
+		db.Truncate(0)
194
+		db.Seek(0, 0)
195
+		e := json.NewEncoder(db)
196
+		e.SetIndent("", "    ")
197
+		e.Encode(status)
198
+	}
199
+}
200
+
201
+func sendMatrixMessage(username, accessToken, room, messageText string) error {
202
+	type Message struct {
203
+		Msgtype string `json:"msgtype"`
204
+		Body    string `json:"body"`
205
+	}
206
+	client := &http.Client{}
207
+	message := Message{
208
+		Msgtype: "m.text",
209
+		Body:    messageText,
210
+	}
211
+	body := new(bytes.Buffer)
212
+	err := json.NewEncoder(body).Encode(message)
213
+	if err != nil {
214
+		return err
215
+	}
216
+	v := url.Values{}
217
+	v.Set("access_token", accessToken)
218
+	v.Set("limit", "1")
219
+	url := url.URL{
220
+		Scheme:   "https",
221
+		Host:     username[strings.Index(username, ":")+1:],
222
+		Path:     fmt.Sprintf("/_matrix/client/r0/rooms/%s/send/m.room.message/%d", room, time.Now().UnixNano()/1000000),
223
+		RawQuery: v.Encode(),
224
+	}
225
+	req, err := http.NewRequest(http.MethodPut, url.String(), body)
226
+	if err != nil {
227
+		return err
228
+	}
229
+	req.Header.Set("Content-Type", "application/json")
230
+	res, err := client.Do(req)
231
+	if err != nil {
232
+		return err
233
+	}
234
+	defer res.Body.Close()
235
+	resBody, _ := ioutil.ReadAll(res.Body)
236
+	fmt.Println(string(resBody))
237
+
238
+	return nil
11 239
 }
12 240
 
13 241
 func main() {
14
-    http.HandleFunc("/", handler)
15
-    log.Fatal(http.ListenAndServe(":8080", nil))
242
+	go updateUptime()
243
+	go checkClosure()
244
+	http.HandleFunc("/", rootHandler)
245
+	http.HandleFunc("/api", apiHandler)
246
+	http.HandleFunc("/img", imgHandler)
247
+	http.HandleFunc("/status", statusHandler)
248
+	log.Fatal(http.ListenAndServe(":"+config.PORT, nil))
16 249
 }

Loading…
Cancel
Save