WIP: Implement remote control of the button from the server

This will not be developed further as the server and the button are not
on the same local network anymore.
This commit is contained in:
Lomanic 2022-10-16 18:59:57 +02:00
parent b5fa938a85
commit 11e9007d86
1 changed files with 147 additions and 3 deletions

150
main.go
View File

@ -4,9 +4,11 @@ import (
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
@ -36,7 +38,8 @@ type Config struct {
const (
dbPath = "./.data/data.json"
defaultClosingTimeout = 5 * time.Minute
defaultClosingTimeout = 1 * time.Minute
espMaxBlinkDuration = 5 * time.Minute
)
var (
@ -53,8 +56,10 @@ var (
// formerly https://www.flaticon.com/free-icon/closed_1234190, maybe try https://flaticons.net/customize.php?dir=Miscellaneous&icon=Closed.png without attribution
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>`,
}
db *os.File
matrix *gomatrix.Client
db *os.File
matrix *gomatrix.Client
espIP string
espBlinkDeadline = time.Now().Add(-espMaxBlinkDuration)
)
func init() {
@ -208,6 +213,21 @@ func statusHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Authentication required", 401)
return
}
// handle being behind proxy https://stackoverflow.com/a/33301173
espIP, _, _ = net.SplitHostPort(r.RemoteAddr)
for _, ifaceIP := range ifacesIPs() {
if espIP == ifaceIP {
for _, header := range []string{"X-Real-Ip", "X-Forwarded-For"} {
if r.Header.Get(header) != "" {
espIP = strings.Split(r.Header.Get(header), ", ")[0]
}
}
}
}
fmt.Printf("espIP %s ", espIP)
q := r.URL.Query()
fuzIsOpen := q.Get("fuzisopen") == "1"
fmt.Printf("button pushed: %v\n", fuzIsOpen)
@ -239,11 +259,62 @@ func statusHandler(w http.ResponseWriter, r *http.Request) {
}
}
// https://github.com/qbit/mcchunkie/blob/53eb25ae3394f326e24782f273584e3e6a4c0e8c/plugins/plugins.go#L52-L66
// NameRE matches the "friendly" name. This is typically used in tab
// completion.
var nameRE = regexp.MustCompile(`@(.+):.+$`)
// ToMe returns true of the message pertains to the bot
func toMe(user, message string) bool {
u := nameRE.ReplaceAllString(user, "$1")
return strings.Contains(message, u)
}
// RemoveName removes the friendly name from a given message
func removeName(user, message string) string {
n := nameRE.ReplaceAllString(user, "$1")
return strings.ReplaceAll(message, n+": ", "")
}
func syncMatrix() {
syncer := matrix.Syncer.(*gomatrix.DefaultSyncer)
syncer.OnEventType("m.room.message", func(ev *gomatrix.Event) {
if ev.Sender != matrix.UserID {
matrix.MarkRead(ev.RoomID, ev.ID)
if body, ok := ev.Body(); ok && toMe(config.MATRIXUSERNAME, body) {
cmd := strings.Fields(removeName(config.MATRIXUSERNAME, body))
if len(cmd) >= 2 {
switch cmd[0] {
case "blink":
switch cmd[1] {
case "on":
if status.FuzIsOpen {
matrix.SendFormattedText(config.MATRIXROOM, fmt.Sprintf("%s: let's make it **blink**! (%s max)", ev.Sender, espMaxBlinkDuration), fmt.Sprintf("%s: let's make it <b>blink</b>! (%s max)", ev.Sender, espMaxBlinkDuration))
espBlinkDeadline = time.Now().Add(espMaxBlinkDuration)
} else {
matrix.SendText(config.MATRIXROOM, fmt.Sprintf("%s: can't do that, space is closed", ev.Sender))
}
case "off":
if status.FuzIsOpen {
matrix.SendFormattedText(config.MATRIXROOM, fmt.Sprintf("%s: OK it's time to **STOP**!", ev.Sender), fmt.Sprintf("%s: OK it's time to <b>STOP</b>!", ev.Sender))
espBlinkDeadline = time.Now()
} else {
matrix.SendText(config.MATRIXROOM, fmt.Sprintf("%s: can't do that, space is closed", ev.Sender))
}
case "status":
if time.Now().Before(espBlinkDeadline) {
matrix.SendText(config.MATRIXROOM, fmt.Sprintf("%s: yes it's still blinking!", ev.Sender))
} else {
matrix.SendFormattedText(config.MATRIXROOM, fmt.Sprintf("%s: it's **not** blinking", ev.Sender), fmt.Sprintf("%s: it's <b>not</b> blinking", ev.Sender))
}
default:
matrix.SendFormattedText(config.MATRIXROOM, fmt.Sprintf("%s: command is `blink (status|on|off)`", ev.Sender), fmt.Sprintf("%s: command is <code>blink (status|on|off)</code>", ev.Sender))
}
return
}
}
matrix.SendText(config.MATRIXROOM, fmt.Sprintf("%s: huh?", ev.Sender))
}
}
})
@ -263,10 +334,83 @@ func syncMatrix() {
}
}
func espBlinkLoop() {
client := &http.Client{}
for {
time.Sleep(time.Second)
if espIP == "" {
continue
}
if time.Now().Before(espBlinkDeadline) {
// http.get request to espIP to have it light up
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s/admin?enablerotatinglight", espIP), nil)
if err != nil {
fmt.Println("error creating request in espBlinkLoop:", err)
continue
}
req.SetBasicAuth(config.ESPUSERNAME, config.ESPPASSWORD)
fmt.Print("admin?enablerotatinglight ")
fmt.Println(client.Do(req))
time.Sleep(7 * time.Second)
// http.get request to espIP to shut down light
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s/admin?disablerotatinglight", espIP), nil)
if err != nil {
fmt.Println("error creating request in espBlinkLoop:", err)
continue
}
req.SetBasicAuth(config.ESPUSERNAME, config.ESPPASSWORD)
fmt.Print("admin?disablerotatinglight ")
fmt.Println(client.Do(req))
time.Sleep(3 * time.Second)
}
}
}
func ifacesIPs() []string { // from https://stackoverflow.com/a/23558495 linked playground
var ips []string
ifaces, err := net.Interfaces()
if err != nil {
return ips
}
for _, iface := range ifaces {
if iface.Flags&net.FlagUp == 0 {
continue // interface down
}
/*if iface.Flags&net.FlagLoopback != 0 {
continue // loopback interface
}*/
addrs, err := iface.Addrs()
if err != nil {
return ips
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil || ip.IsLoopback() {
continue
}
ip = ip.To4()
if ip == nil {
continue // not an ipv4 address
}
ips = append(ips, ip.String())
}
}
return ips
}
func main() {
fmt.Println(ifacesIPs())
go updateUptime()
go checkClosure()
go syncMatrix()
go espBlinkLoop()
http.HandleFunc("/", rootHandler)
http.HandleFunc("/api", apiHandler)
http.HandleFunc("/img", imgHandler)