diff --git a/main.go b/main.go index 40365d1..bfc06d1 100644 --- a/main.go +++ b/main.go @@ -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: ``, } - 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 blink! (%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 STOP!", 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 not blinking", ev.Sender)) + } + default: + matrix.SendFormattedText(config.MATRIXROOM, fmt.Sprintf("%s: command is `blink (status|on|off)`", ev.Sender), fmt.Sprintf("%s: command is blink (status|on|off)", 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)