diff --git a/mattermost/plugin.json b/mattermost/plugin.json index de87152..9d291d5 100644 --- a/mattermost/plugin.json +++ b/mattermost/plugin.json @@ -5,20 +5,20 @@ "executable": "bathroom-linux-amd64" }, "webapp": { - "bundle_path": "main.js" + "bundle_path": "main.js" }, "settings_schema": { - "settings": [ - {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, - {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, - {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, - {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, - {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, - {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, - {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, - {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, - {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, - {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} - ] + "settings": [ + {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, + {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, + {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, + {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, + {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, + {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, + {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, + {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, + {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, + {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} + ] } } diff --git a/mattermost/plugin.json b/mattermost/plugin.json index de87152..9d291d5 100644 --- a/mattermost/plugin.json +++ b/mattermost/plugin.json @@ -5,20 +5,20 @@ "executable": "bathroom-linux-amd64" }, "webapp": { - "bundle_path": "main.js" + "bundle_path": "main.js" }, "settings_schema": { - "settings": [ - {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, - {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, - {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, - {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, - {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, - {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, - {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, - {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, - {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, - {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} - ] + "settings": [ + {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, + {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, + {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, + {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, + {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, + {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, + {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, + {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, + {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, + {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} + ] } } diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go deleted file mode 100644 index 44629cc..0000000 --- a/mattermost/server/bathroom.go +++ /dev/null @@ -1,829 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/model" - "github.com/pkg/errors" - "sync" - "reflect" - _ "github.com/google/go-cmp/cmp" - "os" - _ "github.com/fatih/structs" - "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" - "strings" - "regexp" - "strconv" - "time" - "github.com/fsnotify/fsnotify" - "io/ioutil" - "html" - "encoding/json" - "encoding/pem" - "crypto/rsa" - "crypto/x509" - "crypto/rand" - "crypto/sha1" - "path" - "encoding/base64" - _ "math" -) - -const POST_STATUS_TO_ADMIN = false -const DO_LOGGING = false - -var splitWhitespaceOrComma *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -var doorWatchFile *regexp.Regexp = regexp.MustCompile(`(^|/)door(\d+)$`) - -const ( - Unknown = uint(iota) - Open - Closed -) - -func statusName(status uint) (string, error) { - switch status { - case Unknown: - return "unknown", nil - case Open: - return "open", nil - case Closed: - return "closed", nil - } - - return "", errors.New(fmt.Sprintf("Invalid status %d", status)) -} - -type UserOptions struct { - ShowWidget bool `json:"show_widget"` -} - -func defaultUserOptions() *UserOptions { - return &UserOptions { - ShowWidget: true, - } -} - -type DoorRequest struct { - ip string - time int64 - verify string - status uint -} - -type Door struct { - id uint8 - status uint - pubKey *rsa.PublicKey - lastRequest *DoorRequest -} - -type Config struct { - NumDoors string - numDoors uint8 - - PingInterval string - pingInterval int - - WatchPath string - - AdminUsers string - adminUsers []*model.User - - KeyPath string - - DoorNames string - doorNames []string - - InfoIcon string - UnknownIcons string - OpenIcons string - ClosedIcons string - - unknownIcons []string - openIcons []string - closedIcons []string - - DoorOrder string - doorOrder []uint8 - - settingsJson map[string]interface{} -} - -type BathroomMonitorPlugin struct { - plugin.MattermostPlugin - - config *Config - configLock sync.RWMutex - configChanged chan struct{} - configUpdates int - - doorLock sync.RWMutex - doors map[uint8]*Door - - bot *string -} - -func (p *BathroomMonitorPlugin) getKeyFile(id uint8) (*rsa.PublicKey, error) { - keyFile, err := ioutil.ReadFile(path.Join(p.config.KeyPath, fmt.Sprintf("public%d.pem", id))) - if err != nil { - return nil, err - } - - data, _ := pem.Decode(keyFile) - if data == nil { - return nil, errors.New("No PEM formatted data in file") - } - - key, err := x509.ParsePKIXPublicKey(data.Bytes) - if err != nil { - return nil, err - } - - switch pub := key.(type) { - case *rsa.PublicKey: - return pub, nil - } - - return nil, errors.New(fmt.Sprintf("Keyfile wasn't a public RSA PKIX key, it was %T", key)) -} - -func (p *BathroomMonitorPlugin) initDoors() { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - p.doors = make(map[uint8]*Door) - for i := uint8(0); i < p.config.numDoors; i++ { - id := i + 1 - var pub *rsa.PublicKey = nil - if !USE_FSNOTIFY { - var err error - pub, err = p.getKeyFile(id) - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Unable to load key file for id %d", id)).Error()) - pub = nil - } - } - p.doors[i] = &Door { - id: id, - status: Unknown, - pubKey: pub, - } - } -} - -func (p *BathroomMonitorPlugin) log(log string) { - if DO_LOGGING { - p.API.LogInfo(log) - } -} - -func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error { - if id < 1 || id > p.config.numDoors { - return errors.New(fmt.Sprintf("Invalid door id %d", id)) - } - - if status < Unknown || status > Closed { - return errors.New(fmt.Sprintf("Invalid door status %d", status)) - } - - if p.doors == nil || len(p.doors) < int(p.config.numDoors) { - return errors.New("Doors not inited yet") - } - - if p.doors[id - 1].status != status { - p.doors[id - 1].status = status - if report { - statusStr, _ := statusName(status) - _ = statusStr - if POST_STATUS_TO_ADMIN { - p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr)) - } - p.sendUpdate() - go p.resendUpdate(10 * time.Second) - } - } else { - p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status)) - } - - return nil -} - -func (p *BathroomMonitorPlugin) sendUpdate() { - p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{}) -} - -func (p *BathroomMonitorPlugin) resendUpdate(wait time.Duration) { - timer := time.NewTimer(wait) - <-timer.C - p.sendUpdate() -} - - -func (p *BathroomMonitorPlugin) init() *BathroomMonitorPlugin { - p.config = &Config { - NumDoors: "1", - numDoors: 1, - - WatchPath: "./", - - KeyPath: "./", - - AdminUsers: "", - } - - p.configChanged = make(chan struct{}) - - return p; -} - -func (p *BathroomMonitorPlugin) validateRequestDoorId(r *http.Request) (uint8, error) { - p.configLock.Lock() - numDoors := p.config.numDoors - p.configLock.Unlock() - - - doorStr, ok := r.Form["door_id"] - if !ok || len(doorStr) == 0 { - return 0, errors.New("Please send door id") - } - - doorId, err := strconv.ParseUint(doorStr[0], 10, 8) - if err != nil { - return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error())) - } - doorId8 := uint8(doorId) - - if doorId8 < 1 || doorId8 > numDoors { - return 0, errors.New("Invalid door id") - } - - return doorId8, nil -} - -func SliceIndex(limit int, predicate func(i int) bool) int { - for i := 0; i < limit; i++ { - if predicate(i) { - return i - } - } - return -1 -} - -func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { - auth, authOk := r.Header["Mattermost-User-Id"] - var authUser string = "" - if authOk && len(auth) > 0 { - authUser = auth[0] - } - p.log(fmt.Sprintf("Requested path: %s %s AUTH: %s (%t)", r.URL.Path, c.IpAddress, authUser, authOk)) - if r.URL.Path == "/settings" { - p.configLock.Lock() - defer p.configLock.Unlock() - - retMap := make(map[string]interface{}) - for k, v := range p.config.settingsJson { - retMap[k] = v - } - - userOptions := defaultUserOptions() - - if authOk { - p.Helpers.KVGetJSON("settings_" + authUser, userOptions) - } - - retMap["user"] = userOptions - - bytes, err := json.Marshal(retMap) - if err == nil { - fmt.Fprint(w, string(bytes)) - } else { - p.API.LogError(fmt.Sprintf("Unable to json Marshal settings %s", err.Error())) - } - - return - } - if r.URL.Path == "/status" { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - var write map[string]string = make(map[string]string) - for d, s := range p.doors { - statusStr, _ := statusName(s.status) - write[fmt.Sprintf("%d", d + 1)] = statusStr - } - if output, err := json.Marshal(write); err == nil { - fmt.Fprint(w, string(output)) - } else { - fmt.Fprint(w, html.EscapeString(err.Error())) - } - return - } - if r.URL.Path == "/admin-update-status" && authOk { - r.ParseForm() - p.configLock.Lock() - p.doorLock.Lock() - defer p.doorLock.Unlock() - defer p.configLock.Unlock() - - if SliceIndex(len(p.config.adminUsers), func(i int) bool { return p.config.adminUsers[i].Id == authUser }) != -1 { - for n, vs := range r.Form { - matches := doorWatchFile.FindStringSubmatch(n) - if matches != nil && len(matches) >= 3 { - doorId64, err := strconv.ParseUint(matches[2], 10, 8) - doorId := uint8(doorId64) - if err == nil && doorId >= 1 && doorId <= p.config.numDoors && len(vs) > 0 { - status64, err := strconv.ParseUint(vs[0], 10, 8) - status := uint(status64) - if err == nil && status >= Unknown && status <= Closed { - p.setDoorStatus(uint8(doorId), status, true) - } else { - fmt.Fprintf(w, "Not a valid status: %d", status) - } - } else { - fmt.Fprintf(w, "Not a valid door: %# v, %d, %# v", matches, doorId, pretty.Formatter(vs)) - } - } else { - fmt.Fprintf(w, "Not a valid door key: %s", n) - } - } - } else { - fmt.Fprintf(w, "Not authorized: %s", authUser) - } - return - } - if r.URL.Path == "/status-update" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - statusStr, ok := r.Form["status"] - if !ok || len(statusStr) == 0 { - fmt.Fprint(w, "Please send door status") - return - } - - status, err := strconv.ParseUint(statusStr[0], 10, 8) - if err != nil { - fmt.Fprintf(w, "Couldn't parse status: %s", err.Error()) - return - } - - if status != 0 && status != 1 { - fmt.Fprint(w, "Invalid status") - return - } - - statusVal := Open - if status == 1 { - statusVal = Closed - } - - p.doorLock.Lock() - defer p.doorLock.Unlock() - - if p.doors[doorId8 - 1].pubKey == nil { - fmt.Fprintf(w, "No public key found for door %d", doorId8) - return - } - - p.log("Getting random bytes") - var verifyBytes [50]byte - rand.Read(verifyBytes[:]) - p.log("Encoding random bytes") - - verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:]) - - p.log("Encryping random bytes") - encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{}) - if err != nil { - fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error()) - return - } - - p.log("Encoding encrypted bytes") - encryptedB64 := base64.StdEncoding.EncodeToString(encrypted) - - - req := &DoorRequest { - ip:c.IpAddress, - time:time.Now().Unix(), - verify:verifyB64, - status: statusVal, - } - - p.doors[doorId8 - 1].lastRequest = req - - p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify)) - - fmt.Fprint(w, encryptedB64) - - - return - } - if r.URL.Path == "/verify-status" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - verifyB64, ok := r.Form["verify"] - if !ok || len(verifyB64) <= 0 { - fmt.Fprint(w, "Please send the verification code") - return - } - - p.log("Verify locking") - p.doorLock.Lock() - defer p.doorLock.Unlock() - p.log("Verify done locking") - - if p.doors[doorId8 - 1].lastRequest == nil { - fmt.Fprint(w, "Invalid request") - return - } - - req := p.doors[doorId8 - 1].lastRequest - if req.ip != c.IpAddress { - fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr) - return - } - - diff := time.Now().Unix() - req.time - if diff < 0 || diff > 10 { - fmt.Fprint(w, "Request expired") - return - } - - - if req.verify != verifyB64[0] { - p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0])) - fmt.Fprint(w, "Unauthorized request") - return - } - - - p.doors[doorId8 - 1].lastRequest = nil - - p.log("Changing") - err = p.setDoorStatus(doorId8, req.status, true) - p.log("Changed") - if err != nil { - fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error()) - } - - return - - } - http.NotFound(w, r) -} - -func (p *BathroomMonitorPlugin) confChangedEvent() { - select { - case p.configChanged <- struct{}{}: - default: - } -} - -func (p *BathroomMonitorPlugin) postAdminChannel(text string) { - p.API.LogError(text) - - if p.bot != nil { - for _, u := range(p.config.adminUsers) { - channel, err := p.API.GetDirectChannel(*p.bot, u.Id); - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't get direct channel to user %s", u.Username)).Error()) - continue - } - p.API.CreatePost(&model.Post{UserId: *p.bot, ChannelId:channel.Id, Message:text, MessageSource:text}) - } - } -} - -func (p *BathroomMonitorPlugin) OnConfigurationChange() error { - p.configLock.Lock() - defer p.configLock.Unlock() - - var newConfig *Config = new(Config); - if err := p.API.LoadPluginConfiguration(newConfig); err != nil { - newErr := errors.Wrap(err, fmt.Sprintf("%d: Failed to load configuration", p.configUpdates)) - p.postAdminChannel(newErr.Error()) - return newErr - } - - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { - p.API.LogInfo("Passed same config, or empty?") - return nil; - } - - var configErr error = nil - - - if USE_FSNOTIFY { - if info, err := os.Stat(newConfig.WatchPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) - } - } else { - if info, err := os.Stat(newConfig.KeyPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.KeyPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid key path")) - } - } - - - numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) - if err != nil { - newConfig.NumDoors = "1" - newConfig.numDoors = 1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) - } else { - newConfig.numDoors = uint8(numDoors) - } - - if strings.Trim(newConfig.PingInterval, " \n\t") == "" { - newConfig.pingInterval = -1 - } else { - pingInterval, err := strconv.ParseInt(newConfig.PingInterval, 10, 32) - if err != nil { - newConfig.PingInterval = "" - newConfig.pingInterval = -1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid ping interval")) - } else { - newConfig.pingInterval = int(pingInterval) - } - } - - newConfig.adminUsers = make([]*model.User, 0, 4) - split := splitWhitespaceOrComma.Split(newConfig.AdminUsers, -1) - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - u, _ := p.API.GetUserByUsername(trimmed) - if u != nil { - newConfig.adminUsers = append(newConfig.adminUsers, u) - } - } - } - - newConfig.doorNames = make([]string, 0, 4) - split = strings.Split(newConfig.DoorNames, "|") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.doorNames = append(newConfig.doorNames, trimmed) - } - } - - newConfig.unknownIcons = make([]string, 0, 4) - split = strings.Split(newConfig.UnknownIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.unknownIcons = append(newConfig.unknownIcons, trimmed) - } - } - - newConfig.openIcons = make([]string, 0, 4) - split = strings.Split(newConfig.OpenIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.openIcons = append(newConfig.openIcons, trimmed) - } - } - - newConfig.closedIcons = make([]string, 0, 4) - split = strings.Split(newConfig.ClosedIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.closedIcons = append(newConfig.closedIcons, trimmed) - } - } - - doorOrder := make([]uint8, 0, 4) - split = strings.Split(newConfig.DoorOrder, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - id, err := strconv.ParseUint(trimmed, 10, 8) - if err != nil { - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid door order")) - doorOrder = nil - break - } - doorOrder = append(doorOrder, uint8(id)) - } - } - newConfig.doorOrder = doorOrder - - min := func(a uint8, b uint8) uint8 { - if a < b { - return a - } - return b - } - - doorsJson := make(map[string]string) - for i := uint8(0); i < min(uint8(len(newConfig.doorNames)), newConfig.numDoors); i++ { - doorsJson[fmt.Sprintf("%d", i + 1)] = newConfig.doorNames[i] - } - - doorOrderJson := make([]int, len(newConfig.doorOrder)) - for i, o := range newConfig.doorOrder { - doorOrderJson[i] = int(o) - } - newConfig.settingsJson = map[string]interface{} { - "info_icon": newConfig.InfoIcon, - "unknown_icons": newConfig.unknownIcons, - "open_icons": newConfig.openIcons, - "closed_icons": newConfig.closedIcons, - "doors": doorsJson, - "door_order": doorOrderJson, - } - - p.config = newConfig - p.configUpdates++ - - p.initDoors() - - p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath)) - - if configErr != nil { - p.postAdminChannel(configErr.Error()) - } - - p.confChangedEvent() - - return configErr -} - -func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { - for { - p.configLock.Lock() - numDoors := p.config.numDoors - watchPath := p.config.WatchPath - p.configLock.Unlock() - - _ = numDoors - - watcher, err := fsnotify.NewWatcher() - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't make watcher").Error()) - time.Sleep(60) - continue - } - - err = watcher.Add(watchPath) - - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't watch WatchPath").Error()) - } - - run := true - for ;run; { - select { - case event, ok := <- watcher.Events: - if (!ok) { - p.API.LogError(errors.Wrap(err, "Couldn't get fsnotify event").Error()) - time.Sleep(60) - run = false - } else { - match := doorWatchFile.FindStringSubmatch(event.Name) - if match == nil { - continue - } - - id64, err := strconv.ParseUint(match[2], 10, 8) - if err != nil { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - continue - } - - id := uint8(id64) - if id < 1 || id > numDoors { - continue - } - - if event.Op & fsnotify.Write != fsnotify.Write && event.Op & fsnotify.Create != fsnotify.Create { - continue - } - - statusBytes, err := ioutil.ReadFile(event.Name) - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - - status := strings.Trim(string(statusBytes), " \n\t") - if len(status) <= 0 { - continue - } - - statusInt, err := strconv.ParseUint(status, 10, 32) - if err != nil { - p.API.LogError(errors.Wrap(err, "Status invalid " + status).Error()) - continue - } - - newStatus := Open - if statusInt == 1 { - newStatus = Closed - } - - p.doorLock.Lock() - err = p.setDoorStatus(id, newStatus, true) - p.doorLock.Unlock() - - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't set door status %d %d", id, statusInt)).Error()) - continue - } - } - case <- p.configChanged: - p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates)) - run = false - } - } - - watcher.Close() - } -} - -func (p *BathroomMonitorPlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { - userOptions := defaultUserOptions() - _ = userOptions - - return nil, nil -} - -func (p *BathroomMonitorPlugin) OnActivate() error { - p.API.RegisterCommand(&model.Command { - Trigger: "bathrooms", - AutoComplete: true, - AutoCompleteDesc: "Turns the bathroom widget \"on\" or \"off\" (\"status\" or no option prints bathroom statuses)", - AutoCompleteHint: "(on|off|status)", - }) - if p.bot == nil { - botUser, err := p.Helpers.EnsureBot(&model.Bot{Username:"bathroom-bot", DisplayName:"Bathroom Bot", Description:"Tracks Bathroom Status"}); - if err == nil { - p.bot = &botUser - } else { - p.bot = nil - } - } - if USE_FSNOTIFY { - go fileNotifyBasedLoop(p) - } else { - } - - go p.pingLoop(); - - return nil -} - -func (p *BathroomMonitorPlugin) pingLoop() { - for { - - var tickChan <-chan time.Time = nil - var ticker *time.Ticker = nil - - p.configLock.Lock() - p.log(fmt.Sprintf("Setting pingLoop with interval %d", p.config.pingInterval)) - if p.config.pingInterval > 0 { - ticker = time.NewTicker(time.Duration(p.config.pingInterval) * time.Second) - tickChan = ticker.C - - } - p.configLock.Unlock() - - run := true - for ;run; { - select { - case <- tickChan: - p.log(fmt.Sprintf("Sending ping")) - p.API.PublishWebSocketEvent("ping", map[string]interface{}{}, &model.WebsocketBroadcast{}) - case <- p.configChanged: - run = false - } - } - - if ticker != nil { - ticker.Stop() - } - } -} - -func main() { - plugin.ClientMain((&BathroomMonitorPlugin{}).init()) -} diff --git a/mattermost/plugin.json b/mattermost/plugin.json index de87152..9d291d5 100644 --- a/mattermost/plugin.json +++ b/mattermost/plugin.json @@ -5,20 +5,20 @@ "executable": "bathroom-linux-amd64" }, "webapp": { - "bundle_path": "main.js" + "bundle_path": "main.js" }, "settings_schema": { - "settings": [ - {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, - {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, - {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, - {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, - {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, - {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, - {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, - {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, - {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, - {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} - ] + "settings": [ + {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, + {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, + {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, + {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, + {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, + {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, + {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, + {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, + {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, + {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} + ] } } diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go deleted file mode 100644 index 44629cc..0000000 --- a/mattermost/server/bathroom.go +++ /dev/null @@ -1,829 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/model" - "github.com/pkg/errors" - "sync" - "reflect" - _ "github.com/google/go-cmp/cmp" - "os" - _ "github.com/fatih/structs" - "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" - "strings" - "regexp" - "strconv" - "time" - "github.com/fsnotify/fsnotify" - "io/ioutil" - "html" - "encoding/json" - "encoding/pem" - "crypto/rsa" - "crypto/x509" - "crypto/rand" - "crypto/sha1" - "path" - "encoding/base64" - _ "math" -) - -const POST_STATUS_TO_ADMIN = false -const DO_LOGGING = false - -var splitWhitespaceOrComma *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -var doorWatchFile *regexp.Regexp = regexp.MustCompile(`(^|/)door(\d+)$`) - -const ( - Unknown = uint(iota) - Open - Closed -) - -func statusName(status uint) (string, error) { - switch status { - case Unknown: - return "unknown", nil - case Open: - return "open", nil - case Closed: - return "closed", nil - } - - return "", errors.New(fmt.Sprintf("Invalid status %d", status)) -} - -type UserOptions struct { - ShowWidget bool `json:"show_widget"` -} - -func defaultUserOptions() *UserOptions { - return &UserOptions { - ShowWidget: true, - } -} - -type DoorRequest struct { - ip string - time int64 - verify string - status uint -} - -type Door struct { - id uint8 - status uint - pubKey *rsa.PublicKey - lastRequest *DoorRequest -} - -type Config struct { - NumDoors string - numDoors uint8 - - PingInterval string - pingInterval int - - WatchPath string - - AdminUsers string - adminUsers []*model.User - - KeyPath string - - DoorNames string - doorNames []string - - InfoIcon string - UnknownIcons string - OpenIcons string - ClosedIcons string - - unknownIcons []string - openIcons []string - closedIcons []string - - DoorOrder string - doorOrder []uint8 - - settingsJson map[string]interface{} -} - -type BathroomMonitorPlugin struct { - plugin.MattermostPlugin - - config *Config - configLock sync.RWMutex - configChanged chan struct{} - configUpdates int - - doorLock sync.RWMutex - doors map[uint8]*Door - - bot *string -} - -func (p *BathroomMonitorPlugin) getKeyFile(id uint8) (*rsa.PublicKey, error) { - keyFile, err := ioutil.ReadFile(path.Join(p.config.KeyPath, fmt.Sprintf("public%d.pem", id))) - if err != nil { - return nil, err - } - - data, _ := pem.Decode(keyFile) - if data == nil { - return nil, errors.New("No PEM formatted data in file") - } - - key, err := x509.ParsePKIXPublicKey(data.Bytes) - if err != nil { - return nil, err - } - - switch pub := key.(type) { - case *rsa.PublicKey: - return pub, nil - } - - return nil, errors.New(fmt.Sprintf("Keyfile wasn't a public RSA PKIX key, it was %T", key)) -} - -func (p *BathroomMonitorPlugin) initDoors() { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - p.doors = make(map[uint8]*Door) - for i := uint8(0); i < p.config.numDoors; i++ { - id := i + 1 - var pub *rsa.PublicKey = nil - if !USE_FSNOTIFY { - var err error - pub, err = p.getKeyFile(id) - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Unable to load key file for id %d", id)).Error()) - pub = nil - } - } - p.doors[i] = &Door { - id: id, - status: Unknown, - pubKey: pub, - } - } -} - -func (p *BathroomMonitorPlugin) log(log string) { - if DO_LOGGING { - p.API.LogInfo(log) - } -} - -func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error { - if id < 1 || id > p.config.numDoors { - return errors.New(fmt.Sprintf("Invalid door id %d", id)) - } - - if status < Unknown || status > Closed { - return errors.New(fmt.Sprintf("Invalid door status %d", status)) - } - - if p.doors == nil || len(p.doors) < int(p.config.numDoors) { - return errors.New("Doors not inited yet") - } - - if p.doors[id - 1].status != status { - p.doors[id - 1].status = status - if report { - statusStr, _ := statusName(status) - _ = statusStr - if POST_STATUS_TO_ADMIN { - p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr)) - } - p.sendUpdate() - go p.resendUpdate(10 * time.Second) - } - } else { - p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status)) - } - - return nil -} - -func (p *BathroomMonitorPlugin) sendUpdate() { - p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{}) -} - -func (p *BathroomMonitorPlugin) resendUpdate(wait time.Duration) { - timer := time.NewTimer(wait) - <-timer.C - p.sendUpdate() -} - - -func (p *BathroomMonitorPlugin) init() *BathroomMonitorPlugin { - p.config = &Config { - NumDoors: "1", - numDoors: 1, - - WatchPath: "./", - - KeyPath: "./", - - AdminUsers: "", - } - - p.configChanged = make(chan struct{}) - - return p; -} - -func (p *BathroomMonitorPlugin) validateRequestDoorId(r *http.Request) (uint8, error) { - p.configLock.Lock() - numDoors := p.config.numDoors - p.configLock.Unlock() - - - doorStr, ok := r.Form["door_id"] - if !ok || len(doorStr) == 0 { - return 0, errors.New("Please send door id") - } - - doorId, err := strconv.ParseUint(doorStr[0], 10, 8) - if err != nil { - return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error())) - } - doorId8 := uint8(doorId) - - if doorId8 < 1 || doorId8 > numDoors { - return 0, errors.New("Invalid door id") - } - - return doorId8, nil -} - -func SliceIndex(limit int, predicate func(i int) bool) int { - for i := 0; i < limit; i++ { - if predicate(i) { - return i - } - } - return -1 -} - -func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { - auth, authOk := r.Header["Mattermost-User-Id"] - var authUser string = "" - if authOk && len(auth) > 0 { - authUser = auth[0] - } - p.log(fmt.Sprintf("Requested path: %s %s AUTH: %s (%t)", r.URL.Path, c.IpAddress, authUser, authOk)) - if r.URL.Path == "/settings" { - p.configLock.Lock() - defer p.configLock.Unlock() - - retMap := make(map[string]interface{}) - for k, v := range p.config.settingsJson { - retMap[k] = v - } - - userOptions := defaultUserOptions() - - if authOk { - p.Helpers.KVGetJSON("settings_" + authUser, userOptions) - } - - retMap["user"] = userOptions - - bytes, err := json.Marshal(retMap) - if err == nil { - fmt.Fprint(w, string(bytes)) - } else { - p.API.LogError(fmt.Sprintf("Unable to json Marshal settings %s", err.Error())) - } - - return - } - if r.URL.Path == "/status" { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - var write map[string]string = make(map[string]string) - for d, s := range p.doors { - statusStr, _ := statusName(s.status) - write[fmt.Sprintf("%d", d + 1)] = statusStr - } - if output, err := json.Marshal(write); err == nil { - fmt.Fprint(w, string(output)) - } else { - fmt.Fprint(w, html.EscapeString(err.Error())) - } - return - } - if r.URL.Path == "/admin-update-status" && authOk { - r.ParseForm() - p.configLock.Lock() - p.doorLock.Lock() - defer p.doorLock.Unlock() - defer p.configLock.Unlock() - - if SliceIndex(len(p.config.adminUsers), func(i int) bool { return p.config.adminUsers[i].Id == authUser }) != -1 { - for n, vs := range r.Form { - matches := doorWatchFile.FindStringSubmatch(n) - if matches != nil && len(matches) >= 3 { - doorId64, err := strconv.ParseUint(matches[2], 10, 8) - doorId := uint8(doorId64) - if err == nil && doorId >= 1 && doorId <= p.config.numDoors && len(vs) > 0 { - status64, err := strconv.ParseUint(vs[0], 10, 8) - status := uint(status64) - if err == nil && status >= Unknown && status <= Closed { - p.setDoorStatus(uint8(doorId), status, true) - } else { - fmt.Fprintf(w, "Not a valid status: %d", status) - } - } else { - fmt.Fprintf(w, "Not a valid door: %# v, %d, %# v", matches, doorId, pretty.Formatter(vs)) - } - } else { - fmt.Fprintf(w, "Not a valid door key: %s", n) - } - } - } else { - fmt.Fprintf(w, "Not authorized: %s", authUser) - } - return - } - if r.URL.Path == "/status-update" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - statusStr, ok := r.Form["status"] - if !ok || len(statusStr) == 0 { - fmt.Fprint(w, "Please send door status") - return - } - - status, err := strconv.ParseUint(statusStr[0], 10, 8) - if err != nil { - fmt.Fprintf(w, "Couldn't parse status: %s", err.Error()) - return - } - - if status != 0 && status != 1 { - fmt.Fprint(w, "Invalid status") - return - } - - statusVal := Open - if status == 1 { - statusVal = Closed - } - - p.doorLock.Lock() - defer p.doorLock.Unlock() - - if p.doors[doorId8 - 1].pubKey == nil { - fmt.Fprintf(w, "No public key found for door %d", doorId8) - return - } - - p.log("Getting random bytes") - var verifyBytes [50]byte - rand.Read(verifyBytes[:]) - p.log("Encoding random bytes") - - verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:]) - - p.log("Encryping random bytes") - encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{}) - if err != nil { - fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error()) - return - } - - p.log("Encoding encrypted bytes") - encryptedB64 := base64.StdEncoding.EncodeToString(encrypted) - - - req := &DoorRequest { - ip:c.IpAddress, - time:time.Now().Unix(), - verify:verifyB64, - status: statusVal, - } - - p.doors[doorId8 - 1].lastRequest = req - - p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify)) - - fmt.Fprint(w, encryptedB64) - - - return - } - if r.URL.Path == "/verify-status" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - verifyB64, ok := r.Form["verify"] - if !ok || len(verifyB64) <= 0 { - fmt.Fprint(w, "Please send the verification code") - return - } - - p.log("Verify locking") - p.doorLock.Lock() - defer p.doorLock.Unlock() - p.log("Verify done locking") - - if p.doors[doorId8 - 1].lastRequest == nil { - fmt.Fprint(w, "Invalid request") - return - } - - req := p.doors[doorId8 - 1].lastRequest - if req.ip != c.IpAddress { - fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr) - return - } - - diff := time.Now().Unix() - req.time - if diff < 0 || diff > 10 { - fmt.Fprint(w, "Request expired") - return - } - - - if req.verify != verifyB64[0] { - p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0])) - fmt.Fprint(w, "Unauthorized request") - return - } - - - p.doors[doorId8 - 1].lastRequest = nil - - p.log("Changing") - err = p.setDoorStatus(doorId8, req.status, true) - p.log("Changed") - if err != nil { - fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error()) - } - - return - - } - http.NotFound(w, r) -} - -func (p *BathroomMonitorPlugin) confChangedEvent() { - select { - case p.configChanged <- struct{}{}: - default: - } -} - -func (p *BathroomMonitorPlugin) postAdminChannel(text string) { - p.API.LogError(text) - - if p.bot != nil { - for _, u := range(p.config.adminUsers) { - channel, err := p.API.GetDirectChannel(*p.bot, u.Id); - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't get direct channel to user %s", u.Username)).Error()) - continue - } - p.API.CreatePost(&model.Post{UserId: *p.bot, ChannelId:channel.Id, Message:text, MessageSource:text}) - } - } -} - -func (p *BathroomMonitorPlugin) OnConfigurationChange() error { - p.configLock.Lock() - defer p.configLock.Unlock() - - var newConfig *Config = new(Config); - if err := p.API.LoadPluginConfiguration(newConfig); err != nil { - newErr := errors.Wrap(err, fmt.Sprintf("%d: Failed to load configuration", p.configUpdates)) - p.postAdminChannel(newErr.Error()) - return newErr - } - - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { - p.API.LogInfo("Passed same config, or empty?") - return nil; - } - - var configErr error = nil - - - if USE_FSNOTIFY { - if info, err := os.Stat(newConfig.WatchPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) - } - } else { - if info, err := os.Stat(newConfig.KeyPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.KeyPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid key path")) - } - } - - - numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) - if err != nil { - newConfig.NumDoors = "1" - newConfig.numDoors = 1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) - } else { - newConfig.numDoors = uint8(numDoors) - } - - if strings.Trim(newConfig.PingInterval, " \n\t") == "" { - newConfig.pingInterval = -1 - } else { - pingInterval, err := strconv.ParseInt(newConfig.PingInterval, 10, 32) - if err != nil { - newConfig.PingInterval = "" - newConfig.pingInterval = -1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid ping interval")) - } else { - newConfig.pingInterval = int(pingInterval) - } - } - - newConfig.adminUsers = make([]*model.User, 0, 4) - split := splitWhitespaceOrComma.Split(newConfig.AdminUsers, -1) - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - u, _ := p.API.GetUserByUsername(trimmed) - if u != nil { - newConfig.adminUsers = append(newConfig.adminUsers, u) - } - } - } - - newConfig.doorNames = make([]string, 0, 4) - split = strings.Split(newConfig.DoorNames, "|") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.doorNames = append(newConfig.doorNames, trimmed) - } - } - - newConfig.unknownIcons = make([]string, 0, 4) - split = strings.Split(newConfig.UnknownIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.unknownIcons = append(newConfig.unknownIcons, trimmed) - } - } - - newConfig.openIcons = make([]string, 0, 4) - split = strings.Split(newConfig.OpenIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.openIcons = append(newConfig.openIcons, trimmed) - } - } - - newConfig.closedIcons = make([]string, 0, 4) - split = strings.Split(newConfig.ClosedIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.closedIcons = append(newConfig.closedIcons, trimmed) - } - } - - doorOrder := make([]uint8, 0, 4) - split = strings.Split(newConfig.DoorOrder, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - id, err := strconv.ParseUint(trimmed, 10, 8) - if err != nil { - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid door order")) - doorOrder = nil - break - } - doorOrder = append(doorOrder, uint8(id)) - } - } - newConfig.doorOrder = doorOrder - - min := func(a uint8, b uint8) uint8 { - if a < b { - return a - } - return b - } - - doorsJson := make(map[string]string) - for i := uint8(0); i < min(uint8(len(newConfig.doorNames)), newConfig.numDoors); i++ { - doorsJson[fmt.Sprintf("%d", i + 1)] = newConfig.doorNames[i] - } - - doorOrderJson := make([]int, len(newConfig.doorOrder)) - for i, o := range newConfig.doorOrder { - doorOrderJson[i] = int(o) - } - newConfig.settingsJson = map[string]interface{} { - "info_icon": newConfig.InfoIcon, - "unknown_icons": newConfig.unknownIcons, - "open_icons": newConfig.openIcons, - "closed_icons": newConfig.closedIcons, - "doors": doorsJson, - "door_order": doorOrderJson, - } - - p.config = newConfig - p.configUpdates++ - - p.initDoors() - - p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath)) - - if configErr != nil { - p.postAdminChannel(configErr.Error()) - } - - p.confChangedEvent() - - return configErr -} - -func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { - for { - p.configLock.Lock() - numDoors := p.config.numDoors - watchPath := p.config.WatchPath - p.configLock.Unlock() - - _ = numDoors - - watcher, err := fsnotify.NewWatcher() - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't make watcher").Error()) - time.Sleep(60) - continue - } - - err = watcher.Add(watchPath) - - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't watch WatchPath").Error()) - } - - run := true - for ;run; { - select { - case event, ok := <- watcher.Events: - if (!ok) { - p.API.LogError(errors.Wrap(err, "Couldn't get fsnotify event").Error()) - time.Sleep(60) - run = false - } else { - match := doorWatchFile.FindStringSubmatch(event.Name) - if match == nil { - continue - } - - id64, err := strconv.ParseUint(match[2], 10, 8) - if err != nil { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - continue - } - - id := uint8(id64) - if id < 1 || id > numDoors { - continue - } - - if event.Op & fsnotify.Write != fsnotify.Write && event.Op & fsnotify.Create != fsnotify.Create { - continue - } - - statusBytes, err := ioutil.ReadFile(event.Name) - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - - status := strings.Trim(string(statusBytes), " \n\t") - if len(status) <= 0 { - continue - } - - statusInt, err := strconv.ParseUint(status, 10, 32) - if err != nil { - p.API.LogError(errors.Wrap(err, "Status invalid " + status).Error()) - continue - } - - newStatus := Open - if statusInt == 1 { - newStatus = Closed - } - - p.doorLock.Lock() - err = p.setDoorStatus(id, newStatus, true) - p.doorLock.Unlock() - - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't set door status %d %d", id, statusInt)).Error()) - continue - } - } - case <- p.configChanged: - p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates)) - run = false - } - } - - watcher.Close() - } -} - -func (p *BathroomMonitorPlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { - userOptions := defaultUserOptions() - _ = userOptions - - return nil, nil -} - -func (p *BathroomMonitorPlugin) OnActivate() error { - p.API.RegisterCommand(&model.Command { - Trigger: "bathrooms", - AutoComplete: true, - AutoCompleteDesc: "Turns the bathroom widget \"on\" or \"off\" (\"status\" or no option prints bathroom statuses)", - AutoCompleteHint: "(on|off|status)", - }) - if p.bot == nil { - botUser, err := p.Helpers.EnsureBot(&model.Bot{Username:"bathroom-bot", DisplayName:"Bathroom Bot", Description:"Tracks Bathroom Status"}); - if err == nil { - p.bot = &botUser - } else { - p.bot = nil - } - } - if USE_FSNOTIFY { - go fileNotifyBasedLoop(p) - } else { - } - - go p.pingLoop(); - - return nil -} - -func (p *BathroomMonitorPlugin) pingLoop() { - for { - - var tickChan <-chan time.Time = nil - var ticker *time.Ticker = nil - - p.configLock.Lock() - p.log(fmt.Sprintf("Setting pingLoop with interval %d", p.config.pingInterval)) - if p.config.pingInterval > 0 { - ticker = time.NewTicker(time.Duration(p.config.pingInterval) * time.Second) - tickChan = ticker.C - - } - p.configLock.Unlock() - - run := true - for ;run; { - select { - case <- tickChan: - p.log(fmt.Sprintf("Sending ping")) - p.API.PublishWebSocketEvent("ping", map[string]interface{}{}, &model.WebsocketBroadcast{}) - case <- p.configChanged: - run = false - } - } - - if ticker != nil { - ticker.Stop() - } - } -} - -func main() { - plugin.ClientMain((&BathroomMonitorPlugin{}).init()) -} diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh index c2c1a0e..1623a54 100755 --- a/mattermost/server/build.sh +++ b/mattermost/server/build.sh @@ -1,4 +1,7 @@ #! /usr/bin/env bash -filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json -GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES +printf -v FORMATTED_TAGS '%s,' "$@" +FORMATTED_TAGS=${FORMATTED_TAGS%,} + +gcc -P -E $( [[ -n "$DEFS" ]] && echo -D$DEFS ) - - < ../plugin_templ.json > ../plugin.json +GOOS=linux GOARCH=amd64 go build $( [[ -n "$FORMATTED_TAGS" ]] && echo -tags $FORMATTED_TAGS ) -o bathroom-linux-amd64 bathroom-plugin/main diff --git a/mattermost/plugin.json b/mattermost/plugin.json index de87152..9d291d5 100644 --- a/mattermost/plugin.json +++ b/mattermost/plugin.json @@ -5,20 +5,20 @@ "executable": "bathroom-linux-amd64" }, "webapp": { - "bundle_path": "main.js" + "bundle_path": "main.js" }, "settings_schema": { - "settings": [ - {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, - {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, - {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, - {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, - {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, - {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, - {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, - {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, - {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, - {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} - ] + "settings": [ + {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, + {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, + {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, + {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, + {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, + {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, + {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, + {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, + {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, + {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} + ] } } diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go deleted file mode 100644 index 44629cc..0000000 --- a/mattermost/server/bathroom.go +++ /dev/null @@ -1,829 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/model" - "github.com/pkg/errors" - "sync" - "reflect" - _ "github.com/google/go-cmp/cmp" - "os" - _ "github.com/fatih/structs" - "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" - "strings" - "regexp" - "strconv" - "time" - "github.com/fsnotify/fsnotify" - "io/ioutil" - "html" - "encoding/json" - "encoding/pem" - "crypto/rsa" - "crypto/x509" - "crypto/rand" - "crypto/sha1" - "path" - "encoding/base64" - _ "math" -) - -const POST_STATUS_TO_ADMIN = false -const DO_LOGGING = false - -var splitWhitespaceOrComma *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -var doorWatchFile *regexp.Regexp = regexp.MustCompile(`(^|/)door(\d+)$`) - -const ( - Unknown = uint(iota) - Open - Closed -) - -func statusName(status uint) (string, error) { - switch status { - case Unknown: - return "unknown", nil - case Open: - return "open", nil - case Closed: - return "closed", nil - } - - return "", errors.New(fmt.Sprintf("Invalid status %d", status)) -} - -type UserOptions struct { - ShowWidget bool `json:"show_widget"` -} - -func defaultUserOptions() *UserOptions { - return &UserOptions { - ShowWidget: true, - } -} - -type DoorRequest struct { - ip string - time int64 - verify string - status uint -} - -type Door struct { - id uint8 - status uint - pubKey *rsa.PublicKey - lastRequest *DoorRequest -} - -type Config struct { - NumDoors string - numDoors uint8 - - PingInterval string - pingInterval int - - WatchPath string - - AdminUsers string - adminUsers []*model.User - - KeyPath string - - DoorNames string - doorNames []string - - InfoIcon string - UnknownIcons string - OpenIcons string - ClosedIcons string - - unknownIcons []string - openIcons []string - closedIcons []string - - DoorOrder string - doorOrder []uint8 - - settingsJson map[string]interface{} -} - -type BathroomMonitorPlugin struct { - plugin.MattermostPlugin - - config *Config - configLock sync.RWMutex - configChanged chan struct{} - configUpdates int - - doorLock sync.RWMutex - doors map[uint8]*Door - - bot *string -} - -func (p *BathroomMonitorPlugin) getKeyFile(id uint8) (*rsa.PublicKey, error) { - keyFile, err := ioutil.ReadFile(path.Join(p.config.KeyPath, fmt.Sprintf("public%d.pem", id))) - if err != nil { - return nil, err - } - - data, _ := pem.Decode(keyFile) - if data == nil { - return nil, errors.New("No PEM formatted data in file") - } - - key, err := x509.ParsePKIXPublicKey(data.Bytes) - if err != nil { - return nil, err - } - - switch pub := key.(type) { - case *rsa.PublicKey: - return pub, nil - } - - return nil, errors.New(fmt.Sprintf("Keyfile wasn't a public RSA PKIX key, it was %T", key)) -} - -func (p *BathroomMonitorPlugin) initDoors() { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - p.doors = make(map[uint8]*Door) - for i := uint8(0); i < p.config.numDoors; i++ { - id := i + 1 - var pub *rsa.PublicKey = nil - if !USE_FSNOTIFY { - var err error - pub, err = p.getKeyFile(id) - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Unable to load key file for id %d", id)).Error()) - pub = nil - } - } - p.doors[i] = &Door { - id: id, - status: Unknown, - pubKey: pub, - } - } -} - -func (p *BathroomMonitorPlugin) log(log string) { - if DO_LOGGING { - p.API.LogInfo(log) - } -} - -func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error { - if id < 1 || id > p.config.numDoors { - return errors.New(fmt.Sprintf("Invalid door id %d", id)) - } - - if status < Unknown || status > Closed { - return errors.New(fmt.Sprintf("Invalid door status %d", status)) - } - - if p.doors == nil || len(p.doors) < int(p.config.numDoors) { - return errors.New("Doors not inited yet") - } - - if p.doors[id - 1].status != status { - p.doors[id - 1].status = status - if report { - statusStr, _ := statusName(status) - _ = statusStr - if POST_STATUS_TO_ADMIN { - p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr)) - } - p.sendUpdate() - go p.resendUpdate(10 * time.Second) - } - } else { - p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status)) - } - - return nil -} - -func (p *BathroomMonitorPlugin) sendUpdate() { - p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{}) -} - -func (p *BathroomMonitorPlugin) resendUpdate(wait time.Duration) { - timer := time.NewTimer(wait) - <-timer.C - p.sendUpdate() -} - - -func (p *BathroomMonitorPlugin) init() *BathroomMonitorPlugin { - p.config = &Config { - NumDoors: "1", - numDoors: 1, - - WatchPath: "./", - - KeyPath: "./", - - AdminUsers: "", - } - - p.configChanged = make(chan struct{}) - - return p; -} - -func (p *BathroomMonitorPlugin) validateRequestDoorId(r *http.Request) (uint8, error) { - p.configLock.Lock() - numDoors := p.config.numDoors - p.configLock.Unlock() - - - doorStr, ok := r.Form["door_id"] - if !ok || len(doorStr) == 0 { - return 0, errors.New("Please send door id") - } - - doorId, err := strconv.ParseUint(doorStr[0], 10, 8) - if err != nil { - return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error())) - } - doorId8 := uint8(doorId) - - if doorId8 < 1 || doorId8 > numDoors { - return 0, errors.New("Invalid door id") - } - - return doorId8, nil -} - -func SliceIndex(limit int, predicate func(i int) bool) int { - for i := 0; i < limit; i++ { - if predicate(i) { - return i - } - } - return -1 -} - -func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { - auth, authOk := r.Header["Mattermost-User-Id"] - var authUser string = "" - if authOk && len(auth) > 0 { - authUser = auth[0] - } - p.log(fmt.Sprintf("Requested path: %s %s AUTH: %s (%t)", r.URL.Path, c.IpAddress, authUser, authOk)) - if r.URL.Path == "/settings" { - p.configLock.Lock() - defer p.configLock.Unlock() - - retMap := make(map[string]interface{}) - for k, v := range p.config.settingsJson { - retMap[k] = v - } - - userOptions := defaultUserOptions() - - if authOk { - p.Helpers.KVGetJSON("settings_" + authUser, userOptions) - } - - retMap["user"] = userOptions - - bytes, err := json.Marshal(retMap) - if err == nil { - fmt.Fprint(w, string(bytes)) - } else { - p.API.LogError(fmt.Sprintf("Unable to json Marshal settings %s", err.Error())) - } - - return - } - if r.URL.Path == "/status" { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - var write map[string]string = make(map[string]string) - for d, s := range p.doors { - statusStr, _ := statusName(s.status) - write[fmt.Sprintf("%d", d + 1)] = statusStr - } - if output, err := json.Marshal(write); err == nil { - fmt.Fprint(w, string(output)) - } else { - fmt.Fprint(w, html.EscapeString(err.Error())) - } - return - } - if r.URL.Path == "/admin-update-status" && authOk { - r.ParseForm() - p.configLock.Lock() - p.doorLock.Lock() - defer p.doorLock.Unlock() - defer p.configLock.Unlock() - - if SliceIndex(len(p.config.adminUsers), func(i int) bool { return p.config.adminUsers[i].Id == authUser }) != -1 { - for n, vs := range r.Form { - matches := doorWatchFile.FindStringSubmatch(n) - if matches != nil && len(matches) >= 3 { - doorId64, err := strconv.ParseUint(matches[2], 10, 8) - doorId := uint8(doorId64) - if err == nil && doorId >= 1 && doorId <= p.config.numDoors && len(vs) > 0 { - status64, err := strconv.ParseUint(vs[0], 10, 8) - status := uint(status64) - if err == nil && status >= Unknown && status <= Closed { - p.setDoorStatus(uint8(doorId), status, true) - } else { - fmt.Fprintf(w, "Not a valid status: %d", status) - } - } else { - fmt.Fprintf(w, "Not a valid door: %# v, %d, %# v", matches, doorId, pretty.Formatter(vs)) - } - } else { - fmt.Fprintf(w, "Not a valid door key: %s", n) - } - } - } else { - fmt.Fprintf(w, "Not authorized: %s", authUser) - } - return - } - if r.URL.Path == "/status-update" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - statusStr, ok := r.Form["status"] - if !ok || len(statusStr) == 0 { - fmt.Fprint(w, "Please send door status") - return - } - - status, err := strconv.ParseUint(statusStr[0], 10, 8) - if err != nil { - fmt.Fprintf(w, "Couldn't parse status: %s", err.Error()) - return - } - - if status != 0 && status != 1 { - fmt.Fprint(w, "Invalid status") - return - } - - statusVal := Open - if status == 1 { - statusVal = Closed - } - - p.doorLock.Lock() - defer p.doorLock.Unlock() - - if p.doors[doorId8 - 1].pubKey == nil { - fmt.Fprintf(w, "No public key found for door %d", doorId8) - return - } - - p.log("Getting random bytes") - var verifyBytes [50]byte - rand.Read(verifyBytes[:]) - p.log("Encoding random bytes") - - verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:]) - - p.log("Encryping random bytes") - encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{}) - if err != nil { - fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error()) - return - } - - p.log("Encoding encrypted bytes") - encryptedB64 := base64.StdEncoding.EncodeToString(encrypted) - - - req := &DoorRequest { - ip:c.IpAddress, - time:time.Now().Unix(), - verify:verifyB64, - status: statusVal, - } - - p.doors[doorId8 - 1].lastRequest = req - - p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify)) - - fmt.Fprint(w, encryptedB64) - - - return - } - if r.URL.Path == "/verify-status" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - verifyB64, ok := r.Form["verify"] - if !ok || len(verifyB64) <= 0 { - fmt.Fprint(w, "Please send the verification code") - return - } - - p.log("Verify locking") - p.doorLock.Lock() - defer p.doorLock.Unlock() - p.log("Verify done locking") - - if p.doors[doorId8 - 1].lastRequest == nil { - fmt.Fprint(w, "Invalid request") - return - } - - req := p.doors[doorId8 - 1].lastRequest - if req.ip != c.IpAddress { - fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr) - return - } - - diff := time.Now().Unix() - req.time - if diff < 0 || diff > 10 { - fmt.Fprint(w, "Request expired") - return - } - - - if req.verify != verifyB64[0] { - p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0])) - fmt.Fprint(w, "Unauthorized request") - return - } - - - p.doors[doorId8 - 1].lastRequest = nil - - p.log("Changing") - err = p.setDoorStatus(doorId8, req.status, true) - p.log("Changed") - if err != nil { - fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error()) - } - - return - - } - http.NotFound(w, r) -} - -func (p *BathroomMonitorPlugin) confChangedEvent() { - select { - case p.configChanged <- struct{}{}: - default: - } -} - -func (p *BathroomMonitorPlugin) postAdminChannel(text string) { - p.API.LogError(text) - - if p.bot != nil { - for _, u := range(p.config.adminUsers) { - channel, err := p.API.GetDirectChannel(*p.bot, u.Id); - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't get direct channel to user %s", u.Username)).Error()) - continue - } - p.API.CreatePost(&model.Post{UserId: *p.bot, ChannelId:channel.Id, Message:text, MessageSource:text}) - } - } -} - -func (p *BathroomMonitorPlugin) OnConfigurationChange() error { - p.configLock.Lock() - defer p.configLock.Unlock() - - var newConfig *Config = new(Config); - if err := p.API.LoadPluginConfiguration(newConfig); err != nil { - newErr := errors.Wrap(err, fmt.Sprintf("%d: Failed to load configuration", p.configUpdates)) - p.postAdminChannel(newErr.Error()) - return newErr - } - - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { - p.API.LogInfo("Passed same config, or empty?") - return nil; - } - - var configErr error = nil - - - if USE_FSNOTIFY { - if info, err := os.Stat(newConfig.WatchPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) - } - } else { - if info, err := os.Stat(newConfig.KeyPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.KeyPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid key path")) - } - } - - - numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) - if err != nil { - newConfig.NumDoors = "1" - newConfig.numDoors = 1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) - } else { - newConfig.numDoors = uint8(numDoors) - } - - if strings.Trim(newConfig.PingInterval, " \n\t") == "" { - newConfig.pingInterval = -1 - } else { - pingInterval, err := strconv.ParseInt(newConfig.PingInterval, 10, 32) - if err != nil { - newConfig.PingInterval = "" - newConfig.pingInterval = -1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid ping interval")) - } else { - newConfig.pingInterval = int(pingInterval) - } - } - - newConfig.adminUsers = make([]*model.User, 0, 4) - split := splitWhitespaceOrComma.Split(newConfig.AdminUsers, -1) - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - u, _ := p.API.GetUserByUsername(trimmed) - if u != nil { - newConfig.adminUsers = append(newConfig.adminUsers, u) - } - } - } - - newConfig.doorNames = make([]string, 0, 4) - split = strings.Split(newConfig.DoorNames, "|") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.doorNames = append(newConfig.doorNames, trimmed) - } - } - - newConfig.unknownIcons = make([]string, 0, 4) - split = strings.Split(newConfig.UnknownIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.unknownIcons = append(newConfig.unknownIcons, trimmed) - } - } - - newConfig.openIcons = make([]string, 0, 4) - split = strings.Split(newConfig.OpenIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.openIcons = append(newConfig.openIcons, trimmed) - } - } - - newConfig.closedIcons = make([]string, 0, 4) - split = strings.Split(newConfig.ClosedIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.closedIcons = append(newConfig.closedIcons, trimmed) - } - } - - doorOrder := make([]uint8, 0, 4) - split = strings.Split(newConfig.DoorOrder, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - id, err := strconv.ParseUint(trimmed, 10, 8) - if err != nil { - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid door order")) - doorOrder = nil - break - } - doorOrder = append(doorOrder, uint8(id)) - } - } - newConfig.doorOrder = doorOrder - - min := func(a uint8, b uint8) uint8 { - if a < b { - return a - } - return b - } - - doorsJson := make(map[string]string) - for i := uint8(0); i < min(uint8(len(newConfig.doorNames)), newConfig.numDoors); i++ { - doorsJson[fmt.Sprintf("%d", i + 1)] = newConfig.doorNames[i] - } - - doorOrderJson := make([]int, len(newConfig.doorOrder)) - for i, o := range newConfig.doorOrder { - doorOrderJson[i] = int(o) - } - newConfig.settingsJson = map[string]interface{} { - "info_icon": newConfig.InfoIcon, - "unknown_icons": newConfig.unknownIcons, - "open_icons": newConfig.openIcons, - "closed_icons": newConfig.closedIcons, - "doors": doorsJson, - "door_order": doorOrderJson, - } - - p.config = newConfig - p.configUpdates++ - - p.initDoors() - - p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath)) - - if configErr != nil { - p.postAdminChannel(configErr.Error()) - } - - p.confChangedEvent() - - return configErr -} - -func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { - for { - p.configLock.Lock() - numDoors := p.config.numDoors - watchPath := p.config.WatchPath - p.configLock.Unlock() - - _ = numDoors - - watcher, err := fsnotify.NewWatcher() - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't make watcher").Error()) - time.Sleep(60) - continue - } - - err = watcher.Add(watchPath) - - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't watch WatchPath").Error()) - } - - run := true - for ;run; { - select { - case event, ok := <- watcher.Events: - if (!ok) { - p.API.LogError(errors.Wrap(err, "Couldn't get fsnotify event").Error()) - time.Sleep(60) - run = false - } else { - match := doorWatchFile.FindStringSubmatch(event.Name) - if match == nil { - continue - } - - id64, err := strconv.ParseUint(match[2], 10, 8) - if err != nil { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - continue - } - - id := uint8(id64) - if id < 1 || id > numDoors { - continue - } - - if event.Op & fsnotify.Write != fsnotify.Write && event.Op & fsnotify.Create != fsnotify.Create { - continue - } - - statusBytes, err := ioutil.ReadFile(event.Name) - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - - status := strings.Trim(string(statusBytes), " \n\t") - if len(status) <= 0 { - continue - } - - statusInt, err := strconv.ParseUint(status, 10, 32) - if err != nil { - p.API.LogError(errors.Wrap(err, "Status invalid " + status).Error()) - continue - } - - newStatus := Open - if statusInt == 1 { - newStatus = Closed - } - - p.doorLock.Lock() - err = p.setDoorStatus(id, newStatus, true) - p.doorLock.Unlock() - - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't set door status %d %d", id, statusInt)).Error()) - continue - } - } - case <- p.configChanged: - p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates)) - run = false - } - } - - watcher.Close() - } -} - -func (p *BathroomMonitorPlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { - userOptions := defaultUserOptions() - _ = userOptions - - return nil, nil -} - -func (p *BathroomMonitorPlugin) OnActivate() error { - p.API.RegisterCommand(&model.Command { - Trigger: "bathrooms", - AutoComplete: true, - AutoCompleteDesc: "Turns the bathroom widget \"on\" or \"off\" (\"status\" or no option prints bathroom statuses)", - AutoCompleteHint: "(on|off|status)", - }) - if p.bot == nil { - botUser, err := p.Helpers.EnsureBot(&model.Bot{Username:"bathroom-bot", DisplayName:"Bathroom Bot", Description:"Tracks Bathroom Status"}); - if err == nil { - p.bot = &botUser - } else { - p.bot = nil - } - } - if USE_FSNOTIFY { - go fileNotifyBasedLoop(p) - } else { - } - - go p.pingLoop(); - - return nil -} - -func (p *BathroomMonitorPlugin) pingLoop() { - for { - - var tickChan <-chan time.Time = nil - var ticker *time.Ticker = nil - - p.configLock.Lock() - p.log(fmt.Sprintf("Setting pingLoop with interval %d", p.config.pingInterval)) - if p.config.pingInterval > 0 { - ticker = time.NewTicker(time.Duration(p.config.pingInterval) * time.Second) - tickChan = ticker.C - - } - p.configLock.Unlock() - - run := true - for ;run; { - select { - case <- tickChan: - p.log(fmt.Sprintf("Sending ping")) - p.API.PublishWebSocketEvent("ping", map[string]interface{}{}, &model.WebsocketBroadcast{}) - case <- p.configChanged: - run = false - } - } - - if ticker != nil { - ticker.Stop() - } - } -} - -func main() { - plugin.ClientMain((&BathroomMonitorPlugin{}).init()) -} diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh index c2c1a0e..1623a54 100755 --- a/mattermost/server/build.sh +++ b/mattermost/server/build.sh @@ -1,4 +1,7 @@ #! /usr/bin/env bash -filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json -GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES +printf -v FORMATTED_TAGS '%s,' "$@" +FORMATTED_TAGS=${FORMATTED_TAGS%,} + +gcc -P -E $( [[ -n "$DEFS" ]] && echo -D$DEFS ) - - < ../plugin_templ.json > ../plugin.json +GOOS=linux GOARCH=amd64 go build $( [[ -n "$FORMATTED_TAGS" ]] && echo -tags $FORMATTED_TAGS ) -o bathroom-linux-amd64 bathroom-plugin/main diff --git a/mattermost/server/build_web.sh b/mattermost/server/build_web.sh deleted file mode 100755 index 7ccb125..0000000 --- a/mattermost/server/build_web.sh +++ /dev/null @@ -1,3 +0,0 @@ -#! /usr/bin/env bash - -FILES=config_web.go ./build.sh diff --git a/mattermost/plugin.json b/mattermost/plugin.json index de87152..9d291d5 100644 --- a/mattermost/plugin.json +++ b/mattermost/plugin.json @@ -5,20 +5,20 @@ "executable": "bathroom-linux-amd64" }, "webapp": { - "bundle_path": "main.js" + "bundle_path": "main.js" }, "settings_schema": { - "settings": [ - {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, - {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, - {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, - {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, - {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, - {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, - {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, - {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, - {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, - {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} - ] + "settings": [ + {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, + {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, + {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, + {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, + {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, + {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, + {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, + {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, + {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, + {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} + ] } } diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go deleted file mode 100644 index 44629cc..0000000 --- a/mattermost/server/bathroom.go +++ /dev/null @@ -1,829 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/model" - "github.com/pkg/errors" - "sync" - "reflect" - _ "github.com/google/go-cmp/cmp" - "os" - _ "github.com/fatih/structs" - "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" - "strings" - "regexp" - "strconv" - "time" - "github.com/fsnotify/fsnotify" - "io/ioutil" - "html" - "encoding/json" - "encoding/pem" - "crypto/rsa" - "crypto/x509" - "crypto/rand" - "crypto/sha1" - "path" - "encoding/base64" - _ "math" -) - -const POST_STATUS_TO_ADMIN = false -const DO_LOGGING = false - -var splitWhitespaceOrComma *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -var doorWatchFile *regexp.Regexp = regexp.MustCompile(`(^|/)door(\d+)$`) - -const ( - Unknown = uint(iota) - Open - Closed -) - -func statusName(status uint) (string, error) { - switch status { - case Unknown: - return "unknown", nil - case Open: - return "open", nil - case Closed: - return "closed", nil - } - - return "", errors.New(fmt.Sprintf("Invalid status %d", status)) -} - -type UserOptions struct { - ShowWidget bool `json:"show_widget"` -} - -func defaultUserOptions() *UserOptions { - return &UserOptions { - ShowWidget: true, - } -} - -type DoorRequest struct { - ip string - time int64 - verify string - status uint -} - -type Door struct { - id uint8 - status uint - pubKey *rsa.PublicKey - lastRequest *DoorRequest -} - -type Config struct { - NumDoors string - numDoors uint8 - - PingInterval string - pingInterval int - - WatchPath string - - AdminUsers string - adminUsers []*model.User - - KeyPath string - - DoorNames string - doorNames []string - - InfoIcon string - UnknownIcons string - OpenIcons string - ClosedIcons string - - unknownIcons []string - openIcons []string - closedIcons []string - - DoorOrder string - doorOrder []uint8 - - settingsJson map[string]interface{} -} - -type BathroomMonitorPlugin struct { - plugin.MattermostPlugin - - config *Config - configLock sync.RWMutex - configChanged chan struct{} - configUpdates int - - doorLock sync.RWMutex - doors map[uint8]*Door - - bot *string -} - -func (p *BathroomMonitorPlugin) getKeyFile(id uint8) (*rsa.PublicKey, error) { - keyFile, err := ioutil.ReadFile(path.Join(p.config.KeyPath, fmt.Sprintf("public%d.pem", id))) - if err != nil { - return nil, err - } - - data, _ := pem.Decode(keyFile) - if data == nil { - return nil, errors.New("No PEM formatted data in file") - } - - key, err := x509.ParsePKIXPublicKey(data.Bytes) - if err != nil { - return nil, err - } - - switch pub := key.(type) { - case *rsa.PublicKey: - return pub, nil - } - - return nil, errors.New(fmt.Sprintf("Keyfile wasn't a public RSA PKIX key, it was %T", key)) -} - -func (p *BathroomMonitorPlugin) initDoors() { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - p.doors = make(map[uint8]*Door) - for i := uint8(0); i < p.config.numDoors; i++ { - id := i + 1 - var pub *rsa.PublicKey = nil - if !USE_FSNOTIFY { - var err error - pub, err = p.getKeyFile(id) - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Unable to load key file for id %d", id)).Error()) - pub = nil - } - } - p.doors[i] = &Door { - id: id, - status: Unknown, - pubKey: pub, - } - } -} - -func (p *BathroomMonitorPlugin) log(log string) { - if DO_LOGGING { - p.API.LogInfo(log) - } -} - -func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error { - if id < 1 || id > p.config.numDoors { - return errors.New(fmt.Sprintf("Invalid door id %d", id)) - } - - if status < Unknown || status > Closed { - return errors.New(fmt.Sprintf("Invalid door status %d", status)) - } - - if p.doors == nil || len(p.doors) < int(p.config.numDoors) { - return errors.New("Doors not inited yet") - } - - if p.doors[id - 1].status != status { - p.doors[id - 1].status = status - if report { - statusStr, _ := statusName(status) - _ = statusStr - if POST_STATUS_TO_ADMIN { - p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr)) - } - p.sendUpdate() - go p.resendUpdate(10 * time.Second) - } - } else { - p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status)) - } - - return nil -} - -func (p *BathroomMonitorPlugin) sendUpdate() { - p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{}) -} - -func (p *BathroomMonitorPlugin) resendUpdate(wait time.Duration) { - timer := time.NewTimer(wait) - <-timer.C - p.sendUpdate() -} - - -func (p *BathroomMonitorPlugin) init() *BathroomMonitorPlugin { - p.config = &Config { - NumDoors: "1", - numDoors: 1, - - WatchPath: "./", - - KeyPath: "./", - - AdminUsers: "", - } - - p.configChanged = make(chan struct{}) - - return p; -} - -func (p *BathroomMonitorPlugin) validateRequestDoorId(r *http.Request) (uint8, error) { - p.configLock.Lock() - numDoors := p.config.numDoors - p.configLock.Unlock() - - - doorStr, ok := r.Form["door_id"] - if !ok || len(doorStr) == 0 { - return 0, errors.New("Please send door id") - } - - doorId, err := strconv.ParseUint(doorStr[0], 10, 8) - if err != nil { - return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error())) - } - doorId8 := uint8(doorId) - - if doorId8 < 1 || doorId8 > numDoors { - return 0, errors.New("Invalid door id") - } - - return doorId8, nil -} - -func SliceIndex(limit int, predicate func(i int) bool) int { - for i := 0; i < limit; i++ { - if predicate(i) { - return i - } - } - return -1 -} - -func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { - auth, authOk := r.Header["Mattermost-User-Id"] - var authUser string = "" - if authOk && len(auth) > 0 { - authUser = auth[0] - } - p.log(fmt.Sprintf("Requested path: %s %s AUTH: %s (%t)", r.URL.Path, c.IpAddress, authUser, authOk)) - if r.URL.Path == "/settings" { - p.configLock.Lock() - defer p.configLock.Unlock() - - retMap := make(map[string]interface{}) - for k, v := range p.config.settingsJson { - retMap[k] = v - } - - userOptions := defaultUserOptions() - - if authOk { - p.Helpers.KVGetJSON("settings_" + authUser, userOptions) - } - - retMap["user"] = userOptions - - bytes, err := json.Marshal(retMap) - if err == nil { - fmt.Fprint(w, string(bytes)) - } else { - p.API.LogError(fmt.Sprintf("Unable to json Marshal settings %s", err.Error())) - } - - return - } - if r.URL.Path == "/status" { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - var write map[string]string = make(map[string]string) - for d, s := range p.doors { - statusStr, _ := statusName(s.status) - write[fmt.Sprintf("%d", d + 1)] = statusStr - } - if output, err := json.Marshal(write); err == nil { - fmt.Fprint(w, string(output)) - } else { - fmt.Fprint(w, html.EscapeString(err.Error())) - } - return - } - if r.URL.Path == "/admin-update-status" && authOk { - r.ParseForm() - p.configLock.Lock() - p.doorLock.Lock() - defer p.doorLock.Unlock() - defer p.configLock.Unlock() - - if SliceIndex(len(p.config.adminUsers), func(i int) bool { return p.config.adminUsers[i].Id == authUser }) != -1 { - for n, vs := range r.Form { - matches := doorWatchFile.FindStringSubmatch(n) - if matches != nil && len(matches) >= 3 { - doorId64, err := strconv.ParseUint(matches[2], 10, 8) - doorId := uint8(doorId64) - if err == nil && doorId >= 1 && doorId <= p.config.numDoors && len(vs) > 0 { - status64, err := strconv.ParseUint(vs[0], 10, 8) - status := uint(status64) - if err == nil && status >= Unknown && status <= Closed { - p.setDoorStatus(uint8(doorId), status, true) - } else { - fmt.Fprintf(w, "Not a valid status: %d", status) - } - } else { - fmt.Fprintf(w, "Not a valid door: %# v, %d, %# v", matches, doorId, pretty.Formatter(vs)) - } - } else { - fmt.Fprintf(w, "Not a valid door key: %s", n) - } - } - } else { - fmt.Fprintf(w, "Not authorized: %s", authUser) - } - return - } - if r.URL.Path == "/status-update" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - statusStr, ok := r.Form["status"] - if !ok || len(statusStr) == 0 { - fmt.Fprint(w, "Please send door status") - return - } - - status, err := strconv.ParseUint(statusStr[0], 10, 8) - if err != nil { - fmt.Fprintf(w, "Couldn't parse status: %s", err.Error()) - return - } - - if status != 0 && status != 1 { - fmt.Fprint(w, "Invalid status") - return - } - - statusVal := Open - if status == 1 { - statusVal = Closed - } - - p.doorLock.Lock() - defer p.doorLock.Unlock() - - if p.doors[doorId8 - 1].pubKey == nil { - fmt.Fprintf(w, "No public key found for door %d", doorId8) - return - } - - p.log("Getting random bytes") - var verifyBytes [50]byte - rand.Read(verifyBytes[:]) - p.log("Encoding random bytes") - - verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:]) - - p.log("Encryping random bytes") - encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{}) - if err != nil { - fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error()) - return - } - - p.log("Encoding encrypted bytes") - encryptedB64 := base64.StdEncoding.EncodeToString(encrypted) - - - req := &DoorRequest { - ip:c.IpAddress, - time:time.Now().Unix(), - verify:verifyB64, - status: statusVal, - } - - p.doors[doorId8 - 1].lastRequest = req - - p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify)) - - fmt.Fprint(w, encryptedB64) - - - return - } - if r.URL.Path == "/verify-status" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - verifyB64, ok := r.Form["verify"] - if !ok || len(verifyB64) <= 0 { - fmt.Fprint(w, "Please send the verification code") - return - } - - p.log("Verify locking") - p.doorLock.Lock() - defer p.doorLock.Unlock() - p.log("Verify done locking") - - if p.doors[doorId8 - 1].lastRequest == nil { - fmt.Fprint(w, "Invalid request") - return - } - - req := p.doors[doorId8 - 1].lastRequest - if req.ip != c.IpAddress { - fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr) - return - } - - diff := time.Now().Unix() - req.time - if diff < 0 || diff > 10 { - fmt.Fprint(w, "Request expired") - return - } - - - if req.verify != verifyB64[0] { - p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0])) - fmt.Fprint(w, "Unauthorized request") - return - } - - - p.doors[doorId8 - 1].lastRequest = nil - - p.log("Changing") - err = p.setDoorStatus(doorId8, req.status, true) - p.log("Changed") - if err != nil { - fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error()) - } - - return - - } - http.NotFound(w, r) -} - -func (p *BathroomMonitorPlugin) confChangedEvent() { - select { - case p.configChanged <- struct{}{}: - default: - } -} - -func (p *BathroomMonitorPlugin) postAdminChannel(text string) { - p.API.LogError(text) - - if p.bot != nil { - for _, u := range(p.config.adminUsers) { - channel, err := p.API.GetDirectChannel(*p.bot, u.Id); - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't get direct channel to user %s", u.Username)).Error()) - continue - } - p.API.CreatePost(&model.Post{UserId: *p.bot, ChannelId:channel.Id, Message:text, MessageSource:text}) - } - } -} - -func (p *BathroomMonitorPlugin) OnConfigurationChange() error { - p.configLock.Lock() - defer p.configLock.Unlock() - - var newConfig *Config = new(Config); - if err := p.API.LoadPluginConfiguration(newConfig); err != nil { - newErr := errors.Wrap(err, fmt.Sprintf("%d: Failed to load configuration", p.configUpdates)) - p.postAdminChannel(newErr.Error()) - return newErr - } - - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { - p.API.LogInfo("Passed same config, or empty?") - return nil; - } - - var configErr error = nil - - - if USE_FSNOTIFY { - if info, err := os.Stat(newConfig.WatchPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) - } - } else { - if info, err := os.Stat(newConfig.KeyPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.KeyPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid key path")) - } - } - - - numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) - if err != nil { - newConfig.NumDoors = "1" - newConfig.numDoors = 1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) - } else { - newConfig.numDoors = uint8(numDoors) - } - - if strings.Trim(newConfig.PingInterval, " \n\t") == "" { - newConfig.pingInterval = -1 - } else { - pingInterval, err := strconv.ParseInt(newConfig.PingInterval, 10, 32) - if err != nil { - newConfig.PingInterval = "" - newConfig.pingInterval = -1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid ping interval")) - } else { - newConfig.pingInterval = int(pingInterval) - } - } - - newConfig.adminUsers = make([]*model.User, 0, 4) - split := splitWhitespaceOrComma.Split(newConfig.AdminUsers, -1) - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - u, _ := p.API.GetUserByUsername(trimmed) - if u != nil { - newConfig.adminUsers = append(newConfig.adminUsers, u) - } - } - } - - newConfig.doorNames = make([]string, 0, 4) - split = strings.Split(newConfig.DoorNames, "|") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.doorNames = append(newConfig.doorNames, trimmed) - } - } - - newConfig.unknownIcons = make([]string, 0, 4) - split = strings.Split(newConfig.UnknownIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.unknownIcons = append(newConfig.unknownIcons, trimmed) - } - } - - newConfig.openIcons = make([]string, 0, 4) - split = strings.Split(newConfig.OpenIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.openIcons = append(newConfig.openIcons, trimmed) - } - } - - newConfig.closedIcons = make([]string, 0, 4) - split = strings.Split(newConfig.ClosedIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.closedIcons = append(newConfig.closedIcons, trimmed) - } - } - - doorOrder := make([]uint8, 0, 4) - split = strings.Split(newConfig.DoorOrder, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - id, err := strconv.ParseUint(trimmed, 10, 8) - if err != nil { - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid door order")) - doorOrder = nil - break - } - doorOrder = append(doorOrder, uint8(id)) - } - } - newConfig.doorOrder = doorOrder - - min := func(a uint8, b uint8) uint8 { - if a < b { - return a - } - return b - } - - doorsJson := make(map[string]string) - for i := uint8(0); i < min(uint8(len(newConfig.doorNames)), newConfig.numDoors); i++ { - doorsJson[fmt.Sprintf("%d", i + 1)] = newConfig.doorNames[i] - } - - doorOrderJson := make([]int, len(newConfig.doorOrder)) - for i, o := range newConfig.doorOrder { - doorOrderJson[i] = int(o) - } - newConfig.settingsJson = map[string]interface{} { - "info_icon": newConfig.InfoIcon, - "unknown_icons": newConfig.unknownIcons, - "open_icons": newConfig.openIcons, - "closed_icons": newConfig.closedIcons, - "doors": doorsJson, - "door_order": doorOrderJson, - } - - p.config = newConfig - p.configUpdates++ - - p.initDoors() - - p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath)) - - if configErr != nil { - p.postAdminChannel(configErr.Error()) - } - - p.confChangedEvent() - - return configErr -} - -func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { - for { - p.configLock.Lock() - numDoors := p.config.numDoors - watchPath := p.config.WatchPath - p.configLock.Unlock() - - _ = numDoors - - watcher, err := fsnotify.NewWatcher() - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't make watcher").Error()) - time.Sleep(60) - continue - } - - err = watcher.Add(watchPath) - - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't watch WatchPath").Error()) - } - - run := true - for ;run; { - select { - case event, ok := <- watcher.Events: - if (!ok) { - p.API.LogError(errors.Wrap(err, "Couldn't get fsnotify event").Error()) - time.Sleep(60) - run = false - } else { - match := doorWatchFile.FindStringSubmatch(event.Name) - if match == nil { - continue - } - - id64, err := strconv.ParseUint(match[2], 10, 8) - if err != nil { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - continue - } - - id := uint8(id64) - if id < 1 || id > numDoors { - continue - } - - if event.Op & fsnotify.Write != fsnotify.Write && event.Op & fsnotify.Create != fsnotify.Create { - continue - } - - statusBytes, err := ioutil.ReadFile(event.Name) - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - - status := strings.Trim(string(statusBytes), " \n\t") - if len(status) <= 0 { - continue - } - - statusInt, err := strconv.ParseUint(status, 10, 32) - if err != nil { - p.API.LogError(errors.Wrap(err, "Status invalid " + status).Error()) - continue - } - - newStatus := Open - if statusInt == 1 { - newStatus = Closed - } - - p.doorLock.Lock() - err = p.setDoorStatus(id, newStatus, true) - p.doorLock.Unlock() - - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't set door status %d %d", id, statusInt)).Error()) - continue - } - } - case <- p.configChanged: - p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates)) - run = false - } - } - - watcher.Close() - } -} - -func (p *BathroomMonitorPlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { - userOptions := defaultUserOptions() - _ = userOptions - - return nil, nil -} - -func (p *BathroomMonitorPlugin) OnActivate() error { - p.API.RegisterCommand(&model.Command { - Trigger: "bathrooms", - AutoComplete: true, - AutoCompleteDesc: "Turns the bathroom widget \"on\" or \"off\" (\"status\" or no option prints bathroom statuses)", - AutoCompleteHint: "(on|off|status)", - }) - if p.bot == nil { - botUser, err := p.Helpers.EnsureBot(&model.Bot{Username:"bathroom-bot", DisplayName:"Bathroom Bot", Description:"Tracks Bathroom Status"}); - if err == nil { - p.bot = &botUser - } else { - p.bot = nil - } - } - if USE_FSNOTIFY { - go fileNotifyBasedLoop(p) - } else { - } - - go p.pingLoop(); - - return nil -} - -func (p *BathroomMonitorPlugin) pingLoop() { - for { - - var tickChan <-chan time.Time = nil - var ticker *time.Ticker = nil - - p.configLock.Lock() - p.log(fmt.Sprintf("Setting pingLoop with interval %d", p.config.pingInterval)) - if p.config.pingInterval > 0 { - ticker = time.NewTicker(time.Duration(p.config.pingInterval) * time.Second) - tickChan = ticker.C - - } - p.configLock.Unlock() - - run := true - for ;run; { - select { - case <- tickChan: - p.log(fmt.Sprintf("Sending ping")) - p.API.PublishWebSocketEvent("ping", map[string]interface{}{}, &model.WebsocketBroadcast{}) - case <- p.configChanged: - run = false - } - } - - if ticker != nil { - ticker.Stop() - } - } -} - -func main() { - plugin.ClientMain((&BathroomMonitorPlugin{}).init()) -} diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh index c2c1a0e..1623a54 100755 --- a/mattermost/server/build.sh +++ b/mattermost/server/build.sh @@ -1,4 +1,7 @@ #! /usr/bin/env bash -filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json -GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES +printf -v FORMATTED_TAGS '%s,' "$@" +FORMATTED_TAGS=${FORMATTED_TAGS%,} + +gcc -P -E $( [[ -n "$DEFS" ]] && echo -D$DEFS ) - - < ../plugin_templ.json > ../plugin.json +GOOS=linux GOARCH=amd64 go build $( [[ -n "$FORMATTED_TAGS" ]] && echo -tags $FORMATTED_TAGS ) -o bathroom-linux-amd64 bathroom-plugin/main diff --git a/mattermost/server/build_web.sh b/mattermost/server/build_web.sh deleted file mode 100755 index 7ccb125..0000000 --- a/mattermost/server/build_web.sh +++ /dev/null @@ -1,3 +0,0 @@ -#! /usr/bin/env bash - -FILES=config_web.go ./build.sh diff --git a/mattermost/server/config_fs.go b/mattermost/server/config_fs.go deleted file mode 100644 index 6384df5..0000000 --- a/mattermost/server/config_fs.go +++ /dev/null @@ -1,3 +0,0 @@ -package main - -const USE_FSNOTIFY = true diff --git a/mattermost/plugin.json b/mattermost/plugin.json index de87152..9d291d5 100644 --- a/mattermost/plugin.json +++ b/mattermost/plugin.json @@ -5,20 +5,20 @@ "executable": "bathroom-linux-amd64" }, "webapp": { - "bundle_path": "main.js" + "bundle_path": "main.js" }, "settings_schema": { - "settings": [ - {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, - {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, - {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, - {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, - {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, - {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, - {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, - {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, - {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, - {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} - ] + "settings": [ + {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, + {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, + {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, + {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, + {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, + {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, + {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, + {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, + {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, + {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} + ] } } diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go deleted file mode 100644 index 44629cc..0000000 --- a/mattermost/server/bathroom.go +++ /dev/null @@ -1,829 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/model" - "github.com/pkg/errors" - "sync" - "reflect" - _ "github.com/google/go-cmp/cmp" - "os" - _ "github.com/fatih/structs" - "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" - "strings" - "regexp" - "strconv" - "time" - "github.com/fsnotify/fsnotify" - "io/ioutil" - "html" - "encoding/json" - "encoding/pem" - "crypto/rsa" - "crypto/x509" - "crypto/rand" - "crypto/sha1" - "path" - "encoding/base64" - _ "math" -) - -const POST_STATUS_TO_ADMIN = false -const DO_LOGGING = false - -var splitWhitespaceOrComma *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -var doorWatchFile *regexp.Regexp = regexp.MustCompile(`(^|/)door(\d+)$`) - -const ( - Unknown = uint(iota) - Open - Closed -) - -func statusName(status uint) (string, error) { - switch status { - case Unknown: - return "unknown", nil - case Open: - return "open", nil - case Closed: - return "closed", nil - } - - return "", errors.New(fmt.Sprintf("Invalid status %d", status)) -} - -type UserOptions struct { - ShowWidget bool `json:"show_widget"` -} - -func defaultUserOptions() *UserOptions { - return &UserOptions { - ShowWidget: true, - } -} - -type DoorRequest struct { - ip string - time int64 - verify string - status uint -} - -type Door struct { - id uint8 - status uint - pubKey *rsa.PublicKey - lastRequest *DoorRequest -} - -type Config struct { - NumDoors string - numDoors uint8 - - PingInterval string - pingInterval int - - WatchPath string - - AdminUsers string - adminUsers []*model.User - - KeyPath string - - DoorNames string - doorNames []string - - InfoIcon string - UnknownIcons string - OpenIcons string - ClosedIcons string - - unknownIcons []string - openIcons []string - closedIcons []string - - DoorOrder string - doorOrder []uint8 - - settingsJson map[string]interface{} -} - -type BathroomMonitorPlugin struct { - plugin.MattermostPlugin - - config *Config - configLock sync.RWMutex - configChanged chan struct{} - configUpdates int - - doorLock sync.RWMutex - doors map[uint8]*Door - - bot *string -} - -func (p *BathroomMonitorPlugin) getKeyFile(id uint8) (*rsa.PublicKey, error) { - keyFile, err := ioutil.ReadFile(path.Join(p.config.KeyPath, fmt.Sprintf("public%d.pem", id))) - if err != nil { - return nil, err - } - - data, _ := pem.Decode(keyFile) - if data == nil { - return nil, errors.New("No PEM formatted data in file") - } - - key, err := x509.ParsePKIXPublicKey(data.Bytes) - if err != nil { - return nil, err - } - - switch pub := key.(type) { - case *rsa.PublicKey: - return pub, nil - } - - return nil, errors.New(fmt.Sprintf("Keyfile wasn't a public RSA PKIX key, it was %T", key)) -} - -func (p *BathroomMonitorPlugin) initDoors() { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - p.doors = make(map[uint8]*Door) - for i := uint8(0); i < p.config.numDoors; i++ { - id := i + 1 - var pub *rsa.PublicKey = nil - if !USE_FSNOTIFY { - var err error - pub, err = p.getKeyFile(id) - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Unable to load key file for id %d", id)).Error()) - pub = nil - } - } - p.doors[i] = &Door { - id: id, - status: Unknown, - pubKey: pub, - } - } -} - -func (p *BathroomMonitorPlugin) log(log string) { - if DO_LOGGING { - p.API.LogInfo(log) - } -} - -func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error { - if id < 1 || id > p.config.numDoors { - return errors.New(fmt.Sprintf("Invalid door id %d", id)) - } - - if status < Unknown || status > Closed { - return errors.New(fmt.Sprintf("Invalid door status %d", status)) - } - - if p.doors == nil || len(p.doors) < int(p.config.numDoors) { - return errors.New("Doors not inited yet") - } - - if p.doors[id - 1].status != status { - p.doors[id - 1].status = status - if report { - statusStr, _ := statusName(status) - _ = statusStr - if POST_STATUS_TO_ADMIN { - p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr)) - } - p.sendUpdate() - go p.resendUpdate(10 * time.Second) - } - } else { - p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status)) - } - - return nil -} - -func (p *BathroomMonitorPlugin) sendUpdate() { - p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{}) -} - -func (p *BathroomMonitorPlugin) resendUpdate(wait time.Duration) { - timer := time.NewTimer(wait) - <-timer.C - p.sendUpdate() -} - - -func (p *BathroomMonitorPlugin) init() *BathroomMonitorPlugin { - p.config = &Config { - NumDoors: "1", - numDoors: 1, - - WatchPath: "./", - - KeyPath: "./", - - AdminUsers: "", - } - - p.configChanged = make(chan struct{}) - - return p; -} - -func (p *BathroomMonitorPlugin) validateRequestDoorId(r *http.Request) (uint8, error) { - p.configLock.Lock() - numDoors := p.config.numDoors - p.configLock.Unlock() - - - doorStr, ok := r.Form["door_id"] - if !ok || len(doorStr) == 0 { - return 0, errors.New("Please send door id") - } - - doorId, err := strconv.ParseUint(doorStr[0], 10, 8) - if err != nil { - return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error())) - } - doorId8 := uint8(doorId) - - if doorId8 < 1 || doorId8 > numDoors { - return 0, errors.New("Invalid door id") - } - - return doorId8, nil -} - -func SliceIndex(limit int, predicate func(i int) bool) int { - for i := 0; i < limit; i++ { - if predicate(i) { - return i - } - } - return -1 -} - -func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { - auth, authOk := r.Header["Mattermost-User-Id"] - var authUser string = "" - if authOk && len(auth) > 0 { - authUser = auth[0] - } - p.log(fmt.Sprintf("Requested path: %s %s AUTH: %s (%t)", r.URL.Path, c.IpAddress, authUser, authOk)) - if r.URL.Path == "/settings" { - p.configLock.Lock() - defer p.configLock.Unlock() - - retMap := make(map[string]interface{}) - for k, v := range p.config.settingsJson { - retMap[k] = v - } - - userOptions := defaultUserOptions() - - if authOk { - p.Helpers.KVGetJSON("settings_" + authUser, userOptions) - } - - retMap["user"] = userOptions - - bytes, err := json.Marshal(retMap) - if err == nil { - fmt.Fprint(w, string(bytes)) - } else { - p.API.LogError(fmt.Sprintf("Unable to json Marshal settings %s", err.Error())) - } - - return - } - if r.URL.Path == "/status" { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - var write map[string]string = make(map[string]string) - for d, s := range p.doors { - statusStr, _ := statusName(s.status) - write[fmt.Sprintf("%d", d + 1)] = statusStr - } - if output, err := json.Marshal(write); err == nil { - fmt.Fprint(w, string(output)) - } else { - fmt.Fprint(w, html.EscapeString(err.Error())) - } - return - } - if r.URL.Path == "/admin-update-status" && authOk { - r.ParseForm() - p.configLock.Lock() - p.doorLock.Lock() - defer p.doorLock.Unlock() - defer p.configLock.Unlock() - - if SliceIndex(len(p.config.adminUsers), func(i int) bool { return p.config.adminUsers[i].Id == authUser }) != -1 { - for n, vs := range r.Form { - matches := doorWatchFile.FindStringSubmatch(n) - if matches != nil && len(matches) >= 3 { - doorId64, err := strconv.ParseUint(matches[2], 10, 8) - doorId := uint8(doorId64) - if err == nil && doorId >= 1 && doorId <= p.config.numDoors && len(vs) > 0 { - status64, err := strconv.ParseUint(vs[0], 10, 8) - status := uint(status64) - if err == nil && status >= Unknown && status <= Closed { - p.setDoorStatus(uint8(doorId), status, true) - } else { - fmt.Fprintf(w, "Not a valid status: %d", status) - } - } else { - fmt.Fprintf(w, "Not a valid door: %# v, %d, %# v", matches, doorId, pretty.Formatter(vs)) - } - } else { - fmt.Fprintf(w, "Not a valid door key: %s", n) - } - } - } else { - fmt.Fprintf(w, "Not authorized: %s", authUser) - } - return - } - if r.URL.Path == "/status-update" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - statusStr, ok := r.Form["status"] - if !ok || len(statusStr) == 0 { - fmt.Fprint(w, "Please send door status") - return - } - - status, err := strconv.ParseUint(statusStr[0], 10, 8) - if err != nil { - fmt.Fprintf(w, "Couldn't parse status: %s", err.Error()) - return - } - - if status != 0 && status != 1 { - fmt.Fprint(w, "Invalid status") - return - } - - statusVal := Open - if status == 1 { - statusVal = Closed - } - - p.doorLock.Lock() - defer p.doorLock.Unlock() - - if p.doors[doorId8 - 1].pubKey == nil { - fmt.Fprintf(w, "No public key found for door %d", doorId8) - return - } - - p.log("Getting random bytes") - var verifyBytes [50]byte - rand.Read(verifyBytes[:]) - p.log("Encoding random bytes") - - verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:]) - - p.log("Encryping random bytes") - encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{}) - if err != nil { - fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error()) - return - } - - p.log("Encoding encrypted bytes") - encryptedB64 := base64.StdEncoding.EncodeToString(encrypted) - - - req := &DoorRequest { - ip:c.IpAddress, - time:time.Now().Unix(), - verify:verifyB64, - status: statusVal, - } - - p.doors[doorId8 - 1].lastRequest = req - - p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify)) - - fmt.Fprint(w, encryptedB64) - - - return - } - if r.URL.Path == "/verify-status" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - verifyB64, ok := r.Form["verify"] - if !ok || len(verifyB64) <= 0 { - fmt.Fprint(w, "Please send the verification code") - return - } - - p.log("Verify locking") - p.doorLock.Lock() - defer p.doorLock.Unlock() - p.log("Verify done locking") - - if p.doors[doorId8 - 1].lastRequest == nil { - fmt.Fprint(w, "Invalid request") - return - } - - req := p.doors[doorId8 - 1].lastRequest - if req.ip != c.IpAddress { - fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr) - return - } - - diff := time.Now().Unix() - req.time - if diff < 0 || diff > 10 { - fmt.Fprint(w, "Request expired") - return - } - - - if req.verify != verifyB64[0] { - p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0])) - fmt.Fprint(w, "Unauthorized request") - return - } - - - p.doors[doorId8 - 1].lastRequest = nil - - p.log("Changing") - err = p.setDoorStatus(doorId8, req.status, true) - p.log("Changed") - if err != nil { - fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error()) - } - - return - - } - http.NotFound(w, r) -} - -func (p *BathroomMonitorPlugin) confChangedEvent() { - select { - case p.configChanged <- struct{}{}: - default: - } -} - -func (p *BathroomMonitorPlugin) postAdminChannel(text string) { - p.API.LogError(text) - - if p.bot != nil { - for _, u := range(p.config.adminUsers) { - channel, err := p.API.GetDirectChannel(*p.bot, u.Id); - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't get direct channel to user %s", u.Username)).Error()) - continue - } - p.API.CreatePost(&model.Post{UserId: *p.bot, ChannelId:channel.Id, Message:text, MessageSource:text}) - } - } -} - -func (p *BathroomMonitorPlugin) OnConfigurationChange() error { - p.configLock.Lock() - defer p.configLock.Unlock() - - var newConfig *Config = new(Config); - if err := p.API.LoadPluginConfiguration(newConfig); err != nil { - newErr := errors.Wrap(err, fmt.Sprintf("%d: Failed to load configuration", p.configUpdates)) - p.postAdminChannel(newErr.Error()) - return newErr - } - - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { - p.API.LogInfo("Passed same config, or empty?") - return nil; - } - - var configErr error = nil - - - if USE_FSNOTIFY { - if info, err := os.Stat(newConfig.WatchPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) - } - } else { - if info, err := os.Stat(newConfig.KeyPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.KeyPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid key path")) - } - } - - - numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) - if err != nil { - newConfig.NumDoors = "1" - newConfig.numDoors = 1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) - } else { - newConfig.numDoors = uint8(numDoors) - } - - if strings.Trim(newConfig.PingInterval, " \n\t") == "" { - newConfig.pingInterval = -1 - } else { - pingInterval, err := strconv.ParseInt(newConfig.PingInterval, 10, 32) - if err != nil { - newConfig.PingInterval = "" - newConfig.pingInterval = -1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid ping interval")) - } else { - newConfig.pingInterval = int(pingInterval) - } - } - - newConfig.adminUsers = make([]*model.User, 0, 4) - split := splitWhitespaceOrComma.Split(newConfig.AdminUsers, -1) - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - u, _ := p.API.GetUserByUsername(trimmed) - if u != nil { - newConfig.adminUsers = append(newConfig.adminUsers, u) - } - } - } - - newConfig.doorNames = make([]string, 0, 4) - split = strings.Split(newConfig.DoorNames, "|") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.doorNames = append(newConfig.doorNames, trimmed) - } - } - - newConfig.unknownIcons = make([]string, 0, 4) - split = strings.Split(newConfig.UnknownIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.unknownIcons = append(newConfig.unknownIcons, trimmed) - } - } - - newConfig.openIcons = make([]string, 0, 4) - split = strings.Split(newConfig.OpenIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.openIcons = append(newConfig.openIcons, trimmed) - } - } - - newConfig.closedIcons = make([]string, 0, 4) - split = strings.Split(newConfig.ClosedIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.closedIcons = append(newConfig.closedIcons, trimmed) - } - } - - doorOrder := make([]uint8, 0, 4) - split = strings.Split(newConfig.DoorOrder, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - id, err := strconv.ParseUint(trimmed, 10, 8) - if err != nil { - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid door order")) - doorOrder = nil - break - } - doorOrder = append(doorOrder, uint8(id)) - } - } - newConfig.doorOrder = doorOrder - - min := func(a uint8, b uint8) uint8 { - if a < b { - return a - } - return b - } - - doorsJson := make(map[string]string) - for i := uint8(0); i < min(uint8(len(newConfig.doorNames)), newConfig.numDoors); i++ { - doorsJson[fmt.Sprintf("%d", i + 1)] = newConfig.doorNames[i] - } - - doorOrderJson := make([]int, len(newConfig.doorOrder)) - for i, o := range newConfig.doorOrder { - doorOrderJson[i] = int(o) - } - newConfig.settingsJson = map[string]interface{} { - "info_icon": newConfig.InfoIcon, - "unknown_icons": newConfig.unknownIcons, - "open_icons": newConfig.openIcons, - "closed_icons": newConfig.closedIcons, - "doors": doorsJson, - "door_order": doorOrderJson, - } - - p.config = newConfig - p.configUpdates++ - - p.initDoors() - - p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath)) - - if configErr != nil { - p.postAdminChannel(configErr.Error()) - } - - p.confChangedEvent() - - return configErr -} - -func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { - for { - p.configLock.Lock() - numDoors := p.config.numDoors - watchPath := p.config.WatchPath - p.configLock.Unlock() - - _ = numDoors - - watcher, err := fsnotify.NewWatcher() - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't make watcher").Error()) - time.Sleep(60) - continue - } - - err = watcher.Add(watchPath) - - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't watch WatchPath").Error()) - } - - run := true - for ;run; { - select { - case event, ok := <- watcher.Events: - if (!ok) { - p.API.LogError(errors.Wrap(err, "Couldn't get fsnotify event").Error()) - time.Sleep(60) - run = false - } else { - match := doorWatchFile.FindStringSubmatch(event.Name) - if match == nil { - continue - } - - id64, err := strconv.ParseUint(match[2], 10, 8) - if err != nil { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - continue - } - - id := uint8(id64) - if id < 1 || id > numDoors { - continue - } - - if event.Op & fsnotify.Write != fsnotify.Write && event.Op & fsnotify.Create != fsnotify.Create { - continue - } - - statusBytes, err := ioutil.ReadFile(event.Name) - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - - status := strings.Trim(string(statusBytes), " \n\t") - if len(status) <= 0 { - continue - } - - statusInt, err := strconv.ParseUint(status, 10, 32) - if err != nil { - p.API.LogError(errors.Wrap(err, "Status invalid " + status).Error()) - continue - } - - newStatus := Open - if statusInt == 1 { - newStatus = Closed - } - - p.doorLock.Lock() - err = p.setDoorStatus(id, newStatus, true) - p.doorLock.Unlock() - - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't set door status %d %d", id, statusInt)).Error()) - continue - } - } - case <- p.configChanged: - p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates)) - run = false - } - } - - watcher.Close() - } -} - -func (p *BathroomMonitorPlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { - userOptions := defaultUserOptions() - _ = userOptions - - return nil, nil -} - -func (p *BathroomMonitorPlugin) OnActivate() error { - p.API.RegisterCommand(&model.Command { - Trigger: "bathrooms", - AutoComplete: true, - AutoCompleteDesc: "Turns the bathroom widget \"on\" or \"off\" (\"status\" or no option prints bathroom statuses)", - AutoCompleteHint: "(on|off|status)", - }) - if p.bot == nil { - botUser, err := p.Helpers.EnsureBot(&model.Bot{Username:"bathroom-bot", DisplayName:"Bathroom Bot", Description:"Tracks Bathroom Status"}); - if err == nil { - p.bot = &botUser - } else { - p.bot = nil - } - } - if USE_FSNOTIFY { - go fileNotifyBasedLoop(p) - } else { - } - - go p.pingLoop(); - - return nil -} - -func (p *BathroomMonitorPlugin) pingLoop() { - for { - - var tickChan <-chan time.Time = nil - var ticker *time.Ticker = nil - - p.configLock.Lock() - p.log(fmt.Sprintf("Setting pingLoop with interval %d", p.config.pingInterval)) - if p.config.pingInterval > 0 { - ticker = time.NewTicker(time.Duration(p.config.pingInterval) * time.Second) - tickChan = ticker.C - - } - p.configLock.Unlock() - - run := true - for ;run; { - select { - case <- tickChan: - p.log(fmt.Sprintf("Sending ping")) - p.API.PublishWebSocketEvent("ping", map[string]interface{}{}, &model.WebsocketBroadcast{}) - case <- p.configChanged: - run = false - } - } - - if ticker != nil { - ticker.Stop() - } - } -} - -func main() { - plugin.ClientMain((&BathroomMonitorPlugin{}).init()) -} diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh index c2c1a0e..1623a54 100755 --- a/mattermost/server/build.sh +++ b/mattermost/server/build.sh @@ -1,4 +1,7 @@ #! /usr/bin/env bash -filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json -GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES +printf -v FORMATTED_TAGS '%s,' "$@" +FORMATTED_TAGS=${FORMATTED_TAGS%,} + +gcc -P -E $( [[ -n "$DEFS" ]] && echo -D$DEFS ) - - < ../plugin_templ.json > ../plugin.json +GOOS=linux GOARCH=amd64 go build $( [[ -n "$FORMATTED_TAGS" ]] && echo -tags $FORMATTED_TAGS ) -o bathroom-linux-amd64 bathroom-plugin/main diff --git a/mattermost/server/build_web.sh b/mattermost/server/build_web.sh deleted file mode 100755 index 7ccb125..0000000 --- a/mattermost/server/build_web.sh +++ /dev/null @@ -1,3 +0,0 @@ -#! /usr/bin/env bash - -FILES=config_web.go ./build.sh diff --git a/mattermost/server/config_fs.go b/mattermost/server/config_fs.go deleted file mode 100644 index 6384df5..0000000 --- a/mattermost/server/config_fs.go +++ /dev/null @@ -1,3 +0,0 @@ -package main - -const USE_FSNOTIFY = true diff --git a/mattermost/server/config_web.go b/mattermost/server/config_web.go deleted file mode 100644 index c72b5c8..0000000 --- a/mattermost/server/config_web.go +++ /dev/null @@ -1,3 +0,0 @@ -package main - -const USE_FSNOTIFY = false diff --git a/mattermost/plugin.json b/mattermost/plugin.json index de87152..9d291d5 100644 --- a/mattermost/plugin.json +++ b/mattermost/plugin.json @@ -5,20 +5,20 @@ "executable": "bathroom-linux-amd64" }, "webapp": { - "bundle_path": "main.js" + "bundle_path": "main.js" }, "settings_schema": { - "settings": [ - {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, - {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, - {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, - {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, - {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, - {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, - {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, - {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, - {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, - {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} - ] + "settings": [ + {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, + {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, + {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, + {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, + {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, + {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, + {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, + {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, + {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, + {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} + ] } } diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go deleted file mode 100644 index 44629cc..0000000 --- a/mattermost/server/bathroom.go +++ /dev/null @@ -1,829 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/model" - "github.com/pkg/errors" - "sync" - "reflect" - _ "github.com/google/go-cmp/cmp" - "os" - _ "github.com/fatih/structs" - "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" - "strings" - "regexp" - "strconv" - "time" - "github.com/fsnotify/fsnotify" - "io/ioutil" - "html" - "encoding/json" - "encoding/pem" - "crypto/rsa" - "crypto/x509" - "crypto/rand" - "crypto/sha1" - "path" - "encoding/base64" - _ "math" -) - -const POST_STATUS_TO_ADMIN = false -const DO_LOGGING = false - -var splitWhitespaceOrComma *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -var doorWatchFile *regexp.Regexp = regexp.MustCompile(`(^|/)door(\d+)$`) - -const ( - Unknown = uint(iota) - Open - Closed -) - -func statusName(status uint) (string, error) { - switch status { - case Unknown: - return "unknown", nil - case Open: - return "open", nil - case Closed: - return "closed", nil - } - - return "", errors.New(fmt.Sprintf("Invalid status %d", status)) -} - -type UserOptions struct { - ShowWidget bool `json:"show_widget"` -} - -func defaultUserOptions() *UserOptions { - return &UserOptions { - ShowWidget: true, - } -} - -type DoorRequest struct { - ip string - time int64 - verify string - status uint -} - -type Door struct { - id uint8 - status uint - pubKey *rsa.PublicKey - lastRequest *DoorRequest -} - -type Config struct { - NumDoors string - numDoors uint8 - - PingInterval string - pingInterval int - - WatchPath string - - AdminUsers string - adminUsers []*model.User - - KeyPath string - - DoorNames string - doorNames []string - - InfoIcon string - UnknownIcons string - OpenIcons string - ClosedIcons string - - unknownIcons []string - openIcons []string - closedIcons []string - - DoorOrder string - doorOrder []uint8 - - settingsJson map[string]interface{} -} - -type BathroomMonitorPlugin struct { - plugin.MattermostPlugin - - config *Config - configLock sync.RWMutex - configChanged chan struct{} - configUpdates int - - doorLock sync.RWMutex - doors map[uint8]*Door - - bot *string -} - -func (p *BathroomMonitorPlugin) getKeyFile(id uint8) (*rsa.PublicKey, error) { - keyFile, err := ioutil.ReadFile(path.Join(p.config.KeyPath, fmt.Sprintf("public%d.pem", id))) - if err != nil { - return nil, err - } - - data, _ := pem.Decode(keyFile) - if data == nil { - return nil, errors.New("No PEM formatted data in file") - } - - key, err := x509.ParsePKIXPublicKey(data.Bytes) - if err != nil { - return nil, err - } - - switch pub := key.(type) { - case *rsa.PublicKey: - return pub, nil - } - - return nil, errors.New(fmt.Sprintf("Keyfile wasn't a public RSA PKIX key, it was %T", key)) -} - -func (p *BathroomMonitorPlugin) initDoors() { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - p.doors = make(map[uint8]*Door) - for i := uint8(0); i < p.config.numDoors; i++ { - id := i + 1 - var pub *rsa.PublicKey = nil - if !USE_FSNOTIFY { - var err error - pub, err = p.getKeyFile(id) - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Unable to load key file for id %d", id)).Error()) - pub = nil - } - } - p.doors[i] = &Door { - id: id, - status: Unknown, - pubKey: pub, - } - } -} - -func (p *BathroomMonitorPlugin) log(log string) { - if DO_LOGGING { - p.API.LogInfo(log) - } -} - -func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error { - if id < 1 || id > p.config.numDoors { - return errors.New(fmt.Sprintf("Invalid door id %d", id)) - } - - if status < Unknown || status > Closed { - return errors.New(fmt.Sprintf("Invalid door status %d", status)) - } - - if p.doors == nil || len(p.doors) < int(p.config.numDoors) { - return errors.New("Doors not inited yet") - } - - if p.doors[id - 1].status != status { - p.doors[id - 1].status = status - if report { - statusStr, _ := statusName(status) - _ = statusStr - if POST_STATUS_TO_ADMIN { - p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr)) - } - p.sendUpdate() - go p.resendUpdate(10 * time.Second) - } - } else { - p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status)) - } - - return nil -} - -func (p *BathroomMonitorPlugin) sendUpdate() { - p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{}) -} - -func (p *BathroomMonitorPlugin) resendUpdate(wait time.Duration) { - timer := time.NewTimer(wait) - <-timer.C - p.sendUpdate() -} - - -func (p *BathroomMonitorPlugin) init() *BathroomMonitorPlugin { - p.config = &Config { - NumDoors: "1", - numDoors: 1, - - WatchPath: "./", - - KeyPath: "./", - - AdminUsers: "", - } - - p.configChanged = make(chan struct{}) - - return p; -} - -func (p *BathroomMonitorPlugin) validateRequestDoorId(r *http.Request) (uint8, error) { - p.configLock.Lock() - numDoors := p.config.numDoors - p.configLock.Unlock() - - - doorStr, ok := r.Form["door_id"] - if !ok || len(doorStr) == 0 { - return 0, errors.New("Please send door id") - } - - doorId, err := strconv.ParseUint(doorStr[0], 10, 8) - if err != nil { - return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error())) - } - doorId8 := uint8(doorId) - - if doorId8 < 1 || doorId8 > numDoors { - return 0, errors.New("Invalid door id") - } - - return doorId8, nil -} - -func SliceIndex(limit int, predicate func(i int) bool) int { - for i := 0; i < limit; i++ { - if predicate(i) { - return i - } - } - return -1 -} - -func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { - auth, authOk := r.Header["Mattermost-User-Id"] - var authUser string = "" - if authOk && len(auth) > 0 { - authUser = auth[0] - } - p.log(fmt.Sprintf("Requested path: %s %s AUTH: %s (%t)", r.URL.Path, c.IpAddress, authUser, authOk)) - if r.URL.Path == "/settings" { - p.configLock.Lock() - defer p.configLock.Unlock() - - retMap := make(map[string]interface{}) - for k, v := range p.config.settingsJson { - retMap[k] = v - } - - userOptions := defaultUserOptions() - - if authOk { - p.Helpers.KVGetJSON("settings_" + authUser, userOptions) - } - - retMap["user"] = userOptions - - bytes, err := json.Marshal(retMap) - if err == nil { - fmt.Fprint(w, string(bytes)) - } else { - p.API.LogError(fmt.Sprintf("Unable to json Marshal settings %s", err.Error())) - } - - return - } - if r.URL.Path == "/status" { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - var write map[string]string = make(map[string]string) - for d, s := range p.doors { - statusStr, _ := statusName(s.status) - write[fmt.Sprintf("%d", d + 1)] = statusStr - } - if output, err := json.Marshal(write); err == nil { - fmt.Fprint(w, string(output)) - } else { - fmt.Fprint(w, html.EscapeString(err.Error())) - } - return - } - if r.URL.Path == "/admin-update-status" && authOk { - r.ParseForm() - p.configLock.Lock() - p.doorLock.Lock() - defer p.doorLock.Unlock() - defer p.configLock.Unlock() - - if SliceIndex(len(p.config.adminUsers), func(i int) bool { return p.config.adminUsers[i].Id == authUser }) != -1 { - for n, vs := range r.Form { - matches := doorWatchFile.FindStringSubmatch(n) - if matches != nil && len(matches) >= 3 { - doorId64, err := strconv.ParseUint(matches[2], 10, 8) - doorId := uint8(doorId64) - if err == nil && doorId >= 1 && doorId <= p.config.numDoors && len(vs) > 0 { - status64, err := strconv.ParseUint(vs[0], 10, 8) - status := uint(status64) - if err == nil && status >= Unknown && status <= Closed { - p.setDoorStatus(uint8(doorId), status, true) - } else { - fmt.Fprintf(w, "Not a valid status: %d", status) - } - } else { - fmt.Fprintf(w, "Not a valid door: %# v, %d, %# v", matches, doorId, pretty.Formatter(vs)) - } - } else { - fmt.Fprintf(w, "Not a valid door key: %s", n) - } - } - } else { - fmt.Fprintf(w, "Not authorized: %s", authUser) - } - return - } - if r.URL.Path == "/status-update" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - statusStr, ok := r.Form["status"] - if !ok || len(statusStr) == 0 { - fmt.Fprint(w, "Please send door status") - return - } - - status, err := strconv.ParseUint(statusStr[0], 10, 8) - if err != nil { - fmt.Fprintf(w, "Couldn't parse status: %s", err.Error()) - return - } - - if status != 0 && status != 1 { - fmt.Fprint(w, "Invalid status") - return - } - - statusVal := Open - if status == 1 { - statusVal = Closed - } - - p.doorLock.Lock() - defer p.doorLock.Unlock() - - if p.doors[doorId8 - 1].pubKey == nil { - fmt.Fprintf(w, "No public key found for door %d", doorId8) - return - } - - p.log("Getting random bytes") - var verifyBytes [50]byte - rand.Read(verifyBytes[:]) - p.log("Encoding random bytes") - - verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:]) - - p.log("Encryping random bytes") - encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{}) - if err != nil { - fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error()) - return - } - - p.log("Encoding encrypted bytes") - encryptedB64 := base64.StdEncoding.EncodeToString(encrypted) - - - req := &DoorRequest { - ip:c.IpAddress, - time:time.Now().Unix(), - verify:verifyB64, - status: statusVal, - } - - p.doors[doorId8 - 1].lastRequest = req - - p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify)) - - fmt.Fprint(w, encryptedB64) - - - return - } - if r.URL.Path == "/verify-status" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - verifyB64, ok := r.Form["verify"] - if !ok || len(verifyB64) <= 0 { - fmt.Fprint(w, "Please send the verification code") - return - } - - p.log("Verify locking") - p.doorLock.Lock() - defer p.doorLock.Unlock() - p.log("Verify done locking") - - if p.doors[doorId8 - 1].lastRequest == nil { - fmt.Fprint(w, "Invalid request") - return - } - - req := p.doors[doorId8 - 1].lastRequest - if req.ip != c.IpAddress { - fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr) - return - } - - diff := time.Now().Unix() - req.time - if diff < 0 || diff > 10 { - fmt.Fprint(w, "Request expired") - return - } - - - if req.verify != verifyB64[0] { - p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0])) - fmt.Fprint(w, "Unauthorized request") - return - } - - - p.doors[doorId8 - 1].lastRequest = nil - - p.log("Changing") - err = p.setDoorStatus(doorId8, req.status, true) - p.log("Changed") - if err != nil { - fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error()) - } - - return - - } - http.NotFound(w, r) -} - -func (p *BathroomMonitorPlugin) confChangedEvent() { - select { - case p.configChanged <- struct{}{}: - default: - } -} - -func (p *BathroomMonitorPlugin) postAdminChannel(text string) { - p.API.LogError(text) - - if p.bot != nil { - for _, u := range(p.config.adminUsers) { - channel, err := p.API.GetDirectChannel(*p.bot, u.Id); - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't get direct channel to user %s", u.Username)).Error()) - continue - } - p.API.CreatePost(&model.Post{UserId: *p.bot, ChannelId:channel.Id, Message:text, MessageSource:text}) - } - } -} - -func (p *BathroomMonitorPlugin) OnConfigurationChange() error { - p.configLock.Lock() - defer p.configLock.Unlock() - - var newConfig *Config = new(Config); - if err := p.API.LoadPluginConfiguration(newConfig); err != nil { - newErr := errors.Wrap(err, fmt.Sprintf("%d: Failed to load configuration", p.configUpdates)) - p.postAdminChannel(newErr.Error()) - return newErr - } - - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { - p.API.LogInfo("Passed same config, or empty?") - return nil; - } - - var configErr error = nil - - - if USE_FSNOTIFY { - if info, err := os.Stat(newConfig.WatchPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) - } - } else { - if info, err := os.Stat(newConfig.KeyPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.KeyPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid key path")) - } - } - - - numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) - if err != nil { - newConfig.NumDoors = "1" - newConfig.numDoors = 1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) - } else { - newConfig.numDoors = uint8(numDoors) - } - - if strings.Trim(newConfig.PingInterval, " \n\t") == "" { - newConfig.pingInterval = -1 - } else { - pingInterval, err := strconv.ParseInt(newConfig.PingInterval, 10, 32) - if err != nil { - newConfig.PingInterval = "" - newConfig.pingInterval = -1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid ping interval")) - } else { - newConfig.pingInterval = int(pingInterval) - } - } - - newConfig.adminUsers = make([]*model.User, 0, 4) - split := splitWhitespaceOrComma.Split(newConfig.AdminUsers, -1) - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - u, _ := p.API.GetUserByUsername(trimmed) - if u != nil { - newConfig.adminUsers = append(newConfig.adminUsers, u) - } - } - } - - newConfig.doorNames = make([]string, 0, 4) - split = strings.Split(newConfig.DoorNames, "|") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.doorNames = append(newConfig.doorNames, trimmed) - } - } - - newConfig.unknownIcons = make([]string, 0, 4) - split = strings.Split(newConfig.UnknownIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.unknownIcons = append(newConfig.unknownIcons, trimmed) - } - } - - newConfig.openIcons = make([]string, 0, 4) - split = strings.Split(newConfig.OpenIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.openIcons = append(newConfig.openIcons, trimmed) - } - } - - newConfig.closedIcons = make([]string, 0, 4) - split = strings.Split(newConfig.ClosedIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.closedIcons = append(newConfig.closedIcons, trimmed) - } - } - - doorOrder := make([]uint8, 0, 4) - split = strings.Split(newConfig.DoorOrder, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - id, err := strconv.ParseUint(trimmed, 10, 8) - if err != nil { - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid door order")) - doorOrder = nil - break - } - doorOrder = append(doorOrder, uint8(id)) - } - } - newConfig.doorOrder = doorOrder - - min := func(a uint8, b uint8) uint8 { - if a < b { - return a - } - return b - } - - doorsJson := make(map[string]string) - for i := uint8(0); i < min(uint8(len(newConfig.doorNames)), newConfig.numDoors); i++ { - doorsJson[fmt.Sprintf("%d", i + 1)] = newConfig.doorNames[i] - } - - doorOrderJson := make([]int, len(newConfig.doorOrder)) - for i, o := range newConfig.doorOrder { - doorOrderJson[i] = int(o) - } - newConfig.settingsJson = map[string]interface{} { - "info_icon": newConfig.InfoIcon, - "unknown_icons": newConfig.unknownIcons, - "open_icons": newConfig.openIcons, - "closed_icons": newConfig.closedIcons, - "doors": doorsJson, - "door_order": doorOrderJson, - } - - p.config = newConfig - p.configUpdates++ - - p.initDoors() - - p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath)) - - if configErr != nil { - p.postAdminChannel(configErr.Error()) - } - - p.confChangedEvent() - - return configErr -} - -func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { - for { - p.configLock.Lock() - numDoors := p.config.numDoors - watchPath := p.config.WatchPath - p.configLock.Unlock() - - _ = numDoors - - watcher, err := fsnotify.NewWatcher() - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't make watcher").Error()) - time.Sleep(60) - continue - } - - err = watcher.Add(watchPath) - - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't watch WatchPath").Error()) - } - - run := true - for ;run; { - select { - case event, ok := <- watcher.Events: - if (!ok) { - p.API.LogError(errors.Wrap(err, "Couldn't get fsnotify event").Error()) - time.Sleep(60) - run = false - } else { - match := doorWatchFile.FindStringSubmatch(event.Name) - if match == nil { - continue - } - - id64, err := strconv.ParseUint(match[2], 10, 8) - if err != nil { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - continue - } - - id := uint8(id64) - if id < 1 || id > numDoors { - continue - } - - if event.Op & fsnotify.Write != fsnotify.Write && event.Op & fsnotify.Create != fsnotify.Create { - continue - } - - statusBytes, err := ioutil.ReadFile(event.Name) - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - - status := strings.Trim(string(statusBytes), " \n\t") - if len(status) <= 0 { - continue - } - - statusInt, err := strconv.ParseUint(status, 10, 32) - if err != nil { - p.API.LogError(errors.Wrap(err, "Status invalid " + status).Error()) - continue - } - - newStatus := Open - if statusInt == 1 { - newStatus = Closed - } - - p.doorLock.Lock() - err = p.setDoorStatus(id, newStatus, true) - p.doorLock.Unlock() - - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't set door status %d %d", id, statusInt)).Error()) - continue - } - } - case <- p.configChanged: - p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates)) - run = false - } - } - - watcher.Close() - } -} - -func (p *BathroomMonitorPlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { - userOptions := defaultUserOptions() - _ = userOptions - - return nil, nil -} - -func (p *BathroomMonitorPlugin) OnActivate() error { - p.API.RegisterCommand(&model.Command { - Trigger: "bathrooms", - AutoComplete: true, - AutoCompleteDesc: "Turns the bathroom widget \"on\" or \"off\" (\"status\" or no option prints bathroom statuses)", - AutoCompleteHint: "(on|off|status)", - }) - if p.bot == nil { - botUser, err := p.Helpers.EnsureBot(&model.Bot{Username:"bathroom-bot", DisplayName:"Bathroom Bot", Description:"Tracks Bathroom Status"}); - if err == nil { - p.bot = &botUser - } else { - p.bot = nil - } - } - if USE_FSNOTIFY { - go fileNotifyBasedLoop(p) - } else { - } - - go p.pingLoop(); - - return nil -} - -func (p *BathroomMonitorPlugin) pingLoop() { - for { - - var tickChan <-chan time.Time = nil - var ticker *time.Ticker = nil - - p.configLock.Lock() - p.log(fmt.Sprintf("Setting pingLoop with interval %d", p.config.pingInterval)) - if p.config.pingInterval > 0 { - ticker = time.NewTicker(time.Duration(p.config.pingInterval) * time.Second) - tickChan = ticker.C - - } - p.configLock.Unlock() - - run := true - for ;run; { - select { - case <- tickChan: - p.log(fmt.Sprintf("Sending ping")) - p.API.PublishWebSocketEvent("ping", map[string]interface{}{}, &model.WebsocketBroadcast{}) - case <- p.configChanged: - run = false - } - } - - if ticker != nil { - ticker.Stop() - } - } -} - -func main() { - plugin.ClientMain((&BathroomMonitorPlugin{}).init()) -} diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh index c2c1a0e..1623a54 100755 --- a/mattermost/server/build.sh +++ b/mattermost/server/build.sh @@ -1,4 +1,7 @@ #! /usr/bin/env bash -filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json -GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES +printf -v FORMATTED_TAGS '%s,' "$@" +FORMATTED_TAGS=${FORMATTED_TAGS%,} + +gcc -P -E $( [[ -n "$DEFS" ]] && echo -D$DEFS ) - - < ../plugin_templ.json > ../plugin.json +GOOS=linux GOARCH=amd64 go build $( [[ -n "$FORMATTED_TAGS" ]] && echo -tags $FORMATTED_TAGS ) -o bathroom-linux-amd64 bathroom-plugin/main diff --git a/mattermost/server/build_web.sh b/mattermost/server/build_web.sh deleted file mode 100755 index 7ccb125..0000000 --- a/mattermost/server/build_web.sh +++ /dev/null @@ -1,3 +0,0 @@ -#! /usr/bin/env bash - -FILES=config_web.go ./build.sh diff --git a/mattermost/server/config_fs.go b/mattermost/server/config_fs.go deleted file mode 100644 index 6384df5..0000000 --- a/mattermost/server/config_fs.go +++ /dev/null @@ -1,3 +0,0 @@ -package main - -const USE_FSNOTIFY = true diff --git a/mattermost/server/config_web.go b/mattermost/server/config_web.go deleted file mode 100644 index c72b5c8..0000000 --- a/mattermost/server/config_web.go +++ /dev/null @@ -1,3 +0,0 @@ -package main - -const USE_FSNOTIFY = false diff --git a/mattermost/server/go.mod b/mattermost/server/go.mod new file mode 100644 index 0000000..779de8d --- /dev/null +++ b/mattermost/server/go.mod @@ -0,0 +1,54 @@ +module bathroom-plugin + +go 1.23.3 + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.6.2 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect + github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect + github.com/mattermost/logr/v2 v2.0.21 // indirect + github.com/mattermost/mattermost/server/public v0.1.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/pborman/uuid v1.2.1 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/tinylib/msgp v1.2.4 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/wiggin77/merror v1.0.5 // indirect + github.com/wiggin77/srslog v1.0.1 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f // indirect + google.golang.org/grpc v1.68.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/mattermost/plugin.json b/mattermost/plugin.json index de87152..9d291d5 100644 --- a/mattermost/plugin.json +++ b/mattermost/plugin.json @@ -5,20 +5,20 @@ "executable": "bathroom-linux-amd64" }, "webapp": { - "bundle_path": "main.js" + "bundle_path": "main.js" }, "settings_schema": { - "settings": [ - {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, - {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, - {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, - {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, - {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, - {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, - {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, - {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, - {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, - {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} - ] + "settings": [ + {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, + {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, + {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, + {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, + {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, + {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, + {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, + {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, + {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, + {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} + ] } } diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go deleted file mode 100644 index 44629cc..0000000 --- a/mattermost/server/bathroom.go +++ /dev/null @@ -1,829 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/model" - "github.com/pkg/errors" - "sync" - "reflect" - _ "github.com/google/go-cmp/cmp" - "os" - _ "github.com/fatih/structs" - "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" - "strings" - "regexp" - "strconv" - "time" - "github.com/fsnotify/fsnotify" - "io/ioutil" - "html" - "encoding/json" - "encoding/pem" - "crypto/rsa" - "crypto/x509" - "crypto/rand" - "crypto/sha1" - "path" - "encoding/base64" - _ "math" -) - -const POST_STATUS_TO_ADMIN = false -const DO_LOGGING = false - -var splitWhitespaceOrComma *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -var doorWatchFile *regexp.Regexp = regexp.MustCompile(`(^|/)door(\d+)$`) - -const ( - Unknown = uint(iota) - Open - Closed -) - -func statusName(status uint) (string, error) { - switch status { - case Unknown: - return "unknown", nil - case Open: - return "open", nil - case Closed: - return "closed", nil - } - - return "", errors.New(fmt.Sprintf("Invalid status %d", status)) -} - -type UserOptions struct { - ShowWidget bool `json:"show_widget"` -} - -func defaultUserOptions() *UserOptions { - return &UserOptions { - ShowWidget: true, - } -} - -type DoorRequest struct { - ip string - time int64 - verify string - status uint -} - -type Door struct { - id uint8 - status uint - pubKey *rsa.PublicKey - lastRequest *DoorRequest -} - -type Config struct { - NumDoors string - numDoors uint8 - - PingInterval string - pingInterval int - - WatchPath string - - AdminUsers string - adminUsers []*model.User - - KeyPath string - - DoorNames string - doorNames []string - - InfoIcon string - UnknownIcons string - OpenIcons string - ClosedIcons string - - unknownIcons []string - openIcons []string - closedIcons []string - - DoorOrder string - doorOrder []uint8 - - settingsJson map[string]interface{} -} - -type BathroomMonitorPlugin struct { - plugin.MattermostPlugin - - config *Config - configLock sync.RWMutex - configChanged chan struct{} - configUpdates int - - doorLock sync.RWMutex - doors map[uint8]*Door - - bot *string -} - -func (p *BathroomMonitorPlugin) getKeyFile(id uint8) (*rsa.PublicKey, error) { - keyFile, err := ioutil.ReadFile(path.Join(p.config.KeyPath, fmt.Sprintf("public%d.pem", id))) - if err != nil { - return nil, err - } - - data, _ := pem.Decode(keyFile) - if data == nil { - return nil, errors.New("No PEM formatted data in file") - } - - key, err := x509.ParsePKIXPublicKey(data.Bytes) - if err != nil { - return nil, err - } - - switch pub := key.(type) { - case *rsa.PublicKey: - return pub, nil - } - - return nil, errors.New(fmt.Sprintf("Keyfile wasn't a public RSA PKIX key, it was %T", key)) -} - -func (p *BathroomMonitorPlugin) initDoors() { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - p.doors = make(map[uint8]*Door) - for i := uint8(0); i < p.config.numDoors; i++ { - id := i + 1 - var pub *rsa.PublicKey = nil - if !USE_FSNOTIFY { - var err error - pub, err = p.getKeyFile(id) - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Unable to load key file for id %d", id)).Error()) - pub = nil - } - } - p.doors[i] = &Door { - id: id, - status: Unknown, - pubKey: pub, - } - } -} - -func (p *BathroomMonitorPlugin) log(log string) { - if DO_LOGGING { - p.API.LogInfo(log) - } -} - -func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error { - if id < 1 || id > p.config.numDoors { - return errors.New(fmt.Sprintf("Invalid door id %d", id)) - } - - if status < Unknown || status > Closed { - return errors.New(fmt.Sprintf("Invalid door status %d", status)) - } - - if p.doors == nil || len(p.doors) < int(p.config.numDoors) { - return errors.New("Doors not inited yet") - } - - if p.doors[id - 1].status != status { - p.doors[id - 1].status = status - if report { - statusStr, _ := statusName(status) - _ = statusStr - if POST_STATUS_TO_ADMIN { - p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr)) - } - p.sendUpdate() - go p.resendUpdate(10 * time.Second) - } - } else { - p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status)) - } - - return nil -} - -func (p *BathroomMonitorPlugin) sendUpdate() { - p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{}) -} - -func (p *BathroomMonitorPlugin) resendUpdate(wait time.Duration) { - timer := time.NewTimer(wait) - <-timer.C - p.sendUpdate() -} - - -func (p *BathroomMonitorPlugin) init() *BathroomMonitorPlugin { - p.config = &Config { - NumDoors: "1", - numDoors: 1, - - WatchPath: "./", - - KeyPath: "./", - - AdminUsers: "", - } - - p.configChanged = make(chan struct{}) - - return p; -} - -func (p *BathroomMonitorPlugin) validateRequestDoorId(r *http.Request) (uint8, error) { - p.configLock.Lock() - numDoors := p.config.numDoors - p.configLock.Unlock() - - - doorStr, ok := r.Form["door_id"] - if !ok || len(doorStr) == 0 { - return 0, errors.New("Please send door id") - } - - doorId, err := strconv.ParseUint(doorStr[0], 10, 8) - if err != nil { - return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error())) - } - doorId8 := uint8(doorId) - - if doorId8 < 1 || doorId8 > numDoors { - return 0, errors.New("Invalid door id") - } - - return doorId8, nil -} - -func SliceIndex(limit int, predicate func(i int) bool) int { - for i := 0; i < limit; i++ { - if predicate(i) { - return i - } - } - return -1 -} - -func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { - auth, authOk := r.Header["Mattermost-User-Id"] - var authUser string = "" - if authOk && len(auth) > 0 { - authUser = auth[0] - } - p.log(fmt.Sprintf("Requested path: %s %s AUTH: %s (%t)", r.URL.Path, c.IpAddress, authUser, authOk)) - if r.URL.Path == "/settings" { - p.configLock.Lock() - defer p.configLock.Unlock() - - retMap := make(map[string]interface{}) - for k, v := range p.config.settingsJson { - retMap[k] = v - } - - userOptions := defaultUserOptions() - - if authOk { - p.Helpers.KVGetJSON("settings_" + authUser, userOptions) - } - - retMap["user"] = userOptions - - bytes, err := json.Marshal(retMap) - if err == nil { - fmt.Fprint(w, string(bytes)) - } else { - p.API.LogError(fmt.Sprintf("Unable to json Marshal settings %s", err.Error())) - } - - return - } - if r.URL.Path == "/status" { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - var write map[string]string = make(map[string]string) - for d, s := range p.doors { - statusStr, _ := statusName(s.status) - write[fmt.Sprintf("%d", d + 1)] = statusStr - } - if output, err := json.Marshal(write); err == nil { - fmt.Fprint(w, string(output)) - } else { - fmt.Fprint(w, html.EscapeString(err.Error())) - } - return - } - if r.URL.Path == "/admin-update-status" && authOk { - r.ParseForm() - p.configLock.Lock() - p.doorLock.Lock() - defer p.doorLock.Unlock() - defer p.configLock.Unlock() - - if SliceIndex(len(p.config.adminUsers), func(i int) bool { return p.config.adminUsers[i].Id == authUser }) != -1 { - for n, vs := range r.Form { - matches := doorWatchFile.FindStringSubmatch(n) - if matches != nil && len(matches) >= 3 { - doorId64, err := strconv.ParseUint(matches[2], 10, 8) - doorId := uint8(doorId64) - if err == nil && doorId >= 1 && doorId <= p.config.numDoors && len(vs) > 0 { - status64, err := strconv.ParseUint(vs[0], 10, 8) - status := uint(status64) - if err == nil && status >= Unknown && status <= Closed { - p.setDoorStatus(uint8(doorId), status, true) - } else { - fmt.Fprintf(w, "Not a valid status: %d", status) - } - } else { - fmt.Fprintf(w, "Not a valid door: %# v, %d, %# v", matches, doorId, pretty.Formatter(vs)) - } - } else { - fmt.Fprintf(w, "Not a valid door key: %s", n) - } - } - } else { - fmt.Fprintf(w, "Not authorized: %s", authUser) - } - return - } - if r.URL.Path == "/status-update" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - statusStr, ok := r.Form["status"] - if !ok || len(statusStr) == 0 { - fmt.Fprint(w, "Please send door status") - return - } - - status, err := strconv.ParseUint(statusStr[0], 10, 8) - if err != nil { - fmt.Fprintf(w, "Couldn't parse status: %s", err.Error()) - return - } - - if status != 0 && status != 1 { - fmt.Fprint(w, "Invalid status") - return - } - - statusVal := Open - if status == 1 { - statusVal = Closed - } - - p.doorLock.Lock() - defer p.doorLock.Unlock() - - if p.doors[doorId8 - 1].pubKey == nil { - fmt.Fprintf(w, "No public key found for door %d", doorId8) - return - } - - p.log("Getting random bytes") - var verifyBytes [50]byte - rand.Read(verifyBytes[:]) - p.log("Encoding random bytes") - - verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:]) - - p.log("Encryping random bytes") - encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{}) - if err != nil { - fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error()) - return - } - - p.log("Encoding encrypted bytes") - encryptedB64 := base64.StdEncoding.EncodeToString(encrypted) - - - req := &DoorRequest { - ip:c.IpAddress, - time:time.Now().Unix(), - verify:verifyB64, - status: statusVal, - } - - p.doors[doorId8 - 1].lastRequest = req - - p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify)) - - fmt.Fprint(w, encryptedB64) - - - return - } - if r.URL.Path == "/verify-status" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - verifyB64, ok := r.Form["verify"] - if !ok || len(verifyB64) <= 0 { - fmt.Fprint(w, "Please send the verification code") - return - } - - p.log("Verify locking") - p.doorLock.Lock() - defer p.doorLock.Unlock() - p.log("Verify done locking") - - if p.doors[doorId8 - 1].lastRequest == nil { - fmt.Fprint(w, "Invalid request") - return - } - - req := p.doors[doorId8 - 1].lastRequest - if req.ip != c.IpAddress { - fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr) - return - } - - diff := time.Now().Unix() - req.time - if diff < 0 || diff > 10 { - fmt.Fprint(w, "Request expired") - return - } - - - if req.verify != verifyB64[0] { - p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0])) - fmt.Fprint(w, "Unauthorized request") - return - } - - - p.doors[doorId8 - 1].lastRequest = nil - - p.log("Changing") - err = p.setDoorStatus(doorId8, req.status, true) - p.log("Changed") - if err != nil { - fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error()) - } - - return - - } - http.NotFound(w, r) -} - -func (p *BathroomMonitorPlugin) confChangedEvent() { - select { - case p.configChanged <- struct{}{}: - default: - } -} - -func (p *BathroomMonitorPlugin) postAdminChannel(text string) { - p.API.LogError(text) - - if p.bot != nil { - for _, u := range(p.config.adminUsers) { - channel, err := p.API.GetDirectChannel(*p.bot, u.Id); - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't get direct channel to user %s", u.Username)).Error()) - continue - } - p.API.CreatePost(&model.Post{UserId: *p.bot, ChannelId:channel.Id, Message:text, MessageSource:text}) - } - } -} - -func (p *BathroomMonitorPlugin) OnConfigurationChange() error { - p.configLock.Lock() - defer p.configLock.Unlock() - - var newConfig *Config = new(Config); - if err := p.API.LoadPluginConfiguration(newConfig); err != nil { - newErr := errors.Wrap(err, fmt.Sprintf("%d: Failed to load configuration", p.configUpdates)) - p.postAdminChannel(newErr.Error()) - return newErr - } - - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { - p.API.LogInfo("Passed same config, or empty?") - return nil; - } - - var configErr error = nil - - - if USE_FSNOTIFY { - if info, err := os.Stat(newConfig.WatchPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) - } - } else { - if info, err := os.Stat(newConfig.KeyPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.KeyPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid key path")) - } - } - - - numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) - if err != nil { - newConfig.NumDoors = "1" - newConfig.numDoors = 1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) - } else { - newConfig.numDoors = uint8(numDoors) - } - - if strings.Trim(newConfig.PingInterval, " \n\t") == "" { - newConfig.pingInterval = -1 - } else { - pingInterval, err := strconv.ParseInt(newConfig.PingInterval, 10, 32) - if err != nil { - newConfig.PingInterval = "" - newConfig.pingInterval = -1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid ping interval")) - } else { - newConfig.pingInterval = int(pingInterval) - } - } - - newConfig.adminUsers = make([]*model.User, 0, 4) - split := splitWhitespaceOrComma.Split(newConfig.AdminUsers, -1) - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - u, _ := p.API.GetUserByUsername(trimmed) - if u != nil { - newConfig.adminUsers = append(newConfig.adminUsers, u) - } - } - } - - newConfig.doorNames = make([]string, 0, 4) - split = strings.Split(newConfig.DoorNames, "|") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.doorNames = append(newConfig.doorNames, trimmed) - } - } - - newConfig.unknownIcons = make([]string, 0, 4) - split = strings.Split(newConfig.UnknownIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.unknownIcons = append(newConfig.unknownIcons, trimmed) - } - } - - newConfig.openIcons = make([]string, 0, 4) - split = strings.Split(newConfig.OpenIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.openIcons = append(newConfig.openIcons, trimmed) - } - } - - newConfig.closedIcons = make([]string, 0, 4) - split = strings.Split(newConfig.ClosedIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.closedIcons = append(newConfig.closedIcons, trimmed) - } - } - - doorOrder := make([]uint8, 0, 4) - split = strings.Split(newConfig.DoorOrder, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - id, err := strconv.ParseUint(trimmed, 10, 8) - if err != nil { - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid door order")) - doorOrder = nil - break - } - doorOrder = append(doorOrder, uint8(id)) - } - } - newConfig.doorOrder = doorOrder - - min := func(a uint8, b uint8) uint8 { - if a < b { - return a - } - return b - } - - doorsJson := make(map[string]string) - for i := uint8(0); i < min(uint8(len(newConfig.doorNames)), newConfig.numDoors); i++ { - doorsJson[fmt.Sprintf("%d", i + 1)] = newConfig.doorNames[i] - } - - doorOrderJson := make([]int, len(newConfig.doorOrder)) - for i, o := range newConfig.doorOrder { - doorOrderJson[i] = int(o) - } - newConfig.settingsJson = map[string]interface{} { - "info_icon": newConfig.InfoIcon, - "unknown_icons": newConfig.unknownIcons, - "open_icons": newConfig.openIcons, - "closed_icons": newConfig.closedIcons, - "doors": doorsJson, - "door_order": doorOrderJson, - } - - p.config = newConfig - p.configUpdates++ - - p.initDoors() - - p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath)) - - if configErr != nil { - p.postAdminChannel(configErr.Error()) - } - - p.confChangedEvent() - - return configErr -} - -func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { - for { - p.configLock.Lock() - numDoors := p.config.numDoors - watchPath := p.config.WatchPath - p.configLock.Unlock() - - _ = numDoors - - watcher, err := fsnotify.NewWatcher() - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't make watcher").Error()) - time.Sleep(60) - continue - } - - err = watcher.Add(watchPath) - - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't watch WatchPath").Error()) - } - - run := true - for ;run; { - select { - case event, ok := <- watcher.Events: - if (!ok) { - p.API.LogError(errors.Wrap(err, "Couldn't get fsnotify event").Error()) - time.Sleep(60) - run = false - } else { - match := doorWatchFile.FindStringSubmatch(event.Name) - if match == nil { - continue - } - - id64, err := strconv.ParseUint(match[2], 10, 8) - if err != nil { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - continue - } - - id := uint8(id64) - if id < 1 || id > numDoors { - continue - } - - if event.Op & fsnotify.Write != fsnotify.Write && event.Op & fsnotify.Create != fsnotify.Create { - continue - } - - statusBytes, err := ioutil.ReadFile(event.Name) - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - - status := strings.Trim(string(statusBytes), " \n\t") - if len(status) <= 0 { - continue - } - - statusInt, err := strconv.ParseUint(status, 10, 32) - if err != nil { - p.API.LogError(errors.Wrap(err, "Status invalid " + status).Error()) - continue - } - - newStatus := Open - if statusInt == 1 { - newStatus = Closed - } - - p.doorLock.Lock() - err = p.setDoorStatus(id, newStatus, true) - p.doorLock.Unlock() - - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't set door status %d %d", id, statusInt)).Error()) - continue - } - } - case <- p.configChanged: - p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates)) - run = false - } - } - - watcher.Close() - } -} - -func (p *BathroomMonitorPlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { - userOptions := defaultUserOptions() - _ = userOptions - - return nil, nil -} - -func (p *BathroomMonitorPlugin) OnActivate() error { - p.API.RegisterCommand(&model.Command { - Trigger: "bathrooms", - AutoComplete: true, - AutoCompleteDesc: "Turns the bathroom widget \"on\" or \"off\" (\"status\" or no option prints bathroom statuses)", - AutoCompleteHint: "(on|off|status)", - }) - if p.bot == nil { - botUser, err := p.Helpers.EnsureBot(&model.Bot{Username:"bathroom-bot", DisplayName:"Bathroom Bot", Description:"Tracks Bathroom Status"}); - if err == nil { - p.bot = &botUser - } else { - p.bot = nil - } - } - if USE_FSNOTIFY { - go fileNotifyBasedLoop(p) - } else { - } - - go p.pingLoop(); - - return nil -} - -func (p *BathroomMonitorPlugin) pingLoop() { - for { - - var tickChan <-chan time.Time = nil - var ticker *time.Ticker = nil - - p.configLock.Lock() - p.log(fmt.Sprintf("Setting pingLoop with interval %d", p.config.pingInterval)) - if p.config.pingInterval > 0 { - ticker = time.NewTicker(time.Duration(p.config.pingInterval) * time.Second) - tickChan = ticker.C - - } - p.configLock.Unlock() - - run := true - for ;run; { - select { - case <- tickChan: - p.log(fmt.Sprintf("Sending ping")) - p.API.PublishWebSocketEvent("ping", map[string]interface{}{}, &model.WebsocketBroadcast{}) - case <- p.configChanged: - run = false - } - } - - if ticker != nil { - ticker.Stop() - } - } -} - -func main() { - plugin.ClientMain((&BathroomMonitorPlugin{}).init()) -} diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh index c2c1a0e..1623a54 100755 --- a/mattermost/server/build.sh +++ b/mattermost/server/build.sh @@ -1,4 +1,7 @@ #! /usr/bin/env bash -filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json -GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES +printf -v FORMATTED_TAGS '%s,' "$@" +FORMATTED_TAGS=${FORMATTED_TAGS%,} + +gcc -P -E $( [[ -n "$DEFS" ]] && echo -D$DEFS ) - - < ../plugin_templ.json > ../plugin.json +GOOS=linux GOARCH=amd64 go build $( [[ -n "$FORMATTED_TAGS" ]] && echo -tags $FORMATTED_TAGS ) -o bathroom-linux-amd64 bathroom-plugin/main diff --git a/mattermost/server/build_web.sh b/mattermost/server/build_web.sh deleted file mode 100755 index 7ccb125..0000000 --- a/mattermost/server/build_web.sh +++ /dev/null @@ -1,3 +0,0 @@ -#! /usr/bin/env bash - -FILES=config_web.go ./build.sh diff --git a/mattermost/server/config_fs.go b/mattermost/server/config_fs.go deleted file mode 100644 index 6384df5..0000000 --- a/mattermost/server/config_fs.go +++ /dev/null @@ -1,3 +0,0 @@ -package main - -const USE_FSNOTIFY = true diff --git a/mattermost/server/config_web.go b/mattermost/server/config_web.go deleted file mode 100644 index c72b5c8..0000000 --- a/mattermost/server/config_web.go +++ /dev/null @@ -1,3 +0,0 @@ -package main - -const USE_FSNOTIFY = false diff --git a/mattermost/server/go.mod b/mattermost/server/go.mod new file mode 100644 index 0000000..779de8d --- /dev/null +++ b/mattermost/server/go.mod @@ -0,0 +1,54 @@ +module bathroom-plugin + +go 1.23.3 + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.6.2 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect + github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect + github.com/mattermost/logr/v2 v2.0.21 // indirect + github.com/mattermost/mattermost/server/public v0.1.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/pborman/uuid v1.2.1 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/tinylib/msgp v1.2.4 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/wiggin77/merror v1.0.5 // indirect + github.com/wiggin77/srslog v1.0.1 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f // indirect + google.golang.org/grpc v1.68.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/mattermost/server/go.sum b/mattermost/server/go.sum new file mode 100755 index 0000000..2fe6112 --- /dev/null +++ b/mattermost/server/go.sum @@ -0,0 +1,291 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a h1:etIrTD8BQqzColk9nKRusM9um5+1q0iOEJLqfBMIK64= +github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a/go.mod h1:emQhSYTXqB0xxjLITTw4EaWZ+8IIQYw+kx9GqNUKdLg= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= +github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= +github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8= +github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34= +github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb2BTCsOdamENjjWCI6qmfHLbk6OZI= +github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956/go.mod h1:SRl30Lb7/QoYyohYeVBuqYvvmXSZJxZgiV3Zf6VbxjI= +github.com/mattermost/logr/v2 v2.0.21 h1:CMHsP+nrbRlEC4g7BwOk1GAnMtHkniFhlSQPXy52be4= +github.com/mattermost/logr/v2 v2.0.21/go.mod h1:kZkB/zqKL9e+RY5gB3vGpsyenC+TpuiOenjMkvJJbzc= +github.com/mattermost/mattermost v10.2.0+incompatible h1:Jo/tr/282u/ixQPUbCV6BcEVgSxxcxUeIgWMpT+3zbQ= +github.com/mattermost/mattermost v10.2.0+incompatible/go.mod h1:JUnq0RiZa+eUafKS5M47SVRdPIJyIITxTcCFvoPlFlk= +github.com/mattermost/mattermost-server v10.2.0+incompatible h1:RhG3b+xCdGUnHyWVJxYWuBzDdgBrJp1LHPJjBEmu48I= +github.com/mattermost/mattermost-server v10.2.0+incompatible/go.mod h1:5L6MjAec+XXQwMIt791Ganu45GKsSiM+I0tLR9wUj8Y= +github.com/mattermost/mattermost/server/public v0.1.9 h1:l/OKPRVuFeqL0yqRVC/JpveG5sLNKcT9llxqMkO9e+s= +github.com/mattermost/mattermost/server/public v0.1.9/go.mod h1:SkTKbMul91Rq0v2dIxe8mqzUOY+3KwlwwLmAlxDfGCk= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tinylib/msgp v1.2.4 h1:yLFeUGostXXSGW5vxfT5dXG/qzkn4schv2I7at5+hVU= +github.com/tinylib/msgp v1.2.4/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wiggin77/merror v1.0.5 h1:P+lzicsn4vPMycAf2mFf7Zk6G9eco5N+jB1qJ2XW3ME= +github.com/wiggin77/merror v1.0.5/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0= +github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8= +github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f h1:C1QccEa9kUwvMgEUORqQD9S17QesQijxjZ84sO82mfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/mattermost/plugin.json b/mattermost/plugin.json index de87152..9d291d5 100644 --- a/mattermost/plugin.json +++ b/mattermost/plugin.json @@ -5,20 +5,20 @@ "executable": "bathroom-linux-amd64" }, "webapp": { - "bundle_path": "main.js" + "bundle_path": "main.js" }, "settings_schema": { - "settings": [ - {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, - {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, - {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, - {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, - {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, - {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, - {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, - {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, - {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, - {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} - ] + "settings": [ + {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, + {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, + {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, + {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, + {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, + {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, + {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, + {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, + {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, + {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} + ] } } diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go deleted file mode 100644 index 44629cc..0000000 --- a/mattermost/server/bathroom.go +++ /dev/null @@ -1,829 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/model" - "github.com/pkg/errors" - "sync" - "reflect" - _ "github.com/google/go-cmp/cmp" - "os" - _ "github.com/fatih/structs" - "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" - "strings" - "regexp" - "strconv" - "time" - "github.com/fsnotify/fsnotify" - "io/ioutil" - "html" - "encoding/json" - "encoding/pem" - "crypto/rsa" - "crypto/x509" - "crypto/rand" - "crypto/sha1" - "path" - "encoding/base64" - _ "math" -) - -const POST_STATUS_TO_ADMIN = false -const DO_LOGGING = false - -var splitWhitespaceOrComma *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -var doorWatchFile *regexp.Regexp = regexp.MustCompile(`(^|/)door(\d+)$`) - -const ( - Unknown = uint(iota) - Open - Closed -) - -func statusName(status uint) (string, error) { - switch status { - case Unknown: - return "unknown", nil - case Open: - return "open", nil - case Closed: - return "closed", nil - } - - return "", errors.New(fmt.Sprintf("Invalid status %d", status)) -} - -type UserOptions struct { - ShowWidget bool `json:"show_widget"` -} - -func defaultUserOptions() *UserOptions { - return &UserOptions { - ShowWidget: true, - } -} - -type DoorRequest struct { - ip string - time int64 - verify string - status uint -} - -type Door struct { - id uint8 - status uint - pubKey *rsa.PublicKey - lastRequest *DoorRequest -} - -type Config struct { - NumDoors string - numDoors uint8 - - PingInterval string - pingInterval int - - WatchPath string - - AdminUsers string - adminUsers []*model.User - - KeyPath string - - DoorNames string - doorNames []string - - InfoIcon string - UnknownIcons string - OpenIcons string - ClosedIcons string - - unknownIcons []string - openIcons []string - closedIcons []string - - DoorOrder string - doorOrder []uint8 - - settingsJson map[string]interface{} -} - -type BathroomMonitorPlugin struct { - plugin.MattermostPlugin - - config *Config - configLock sync.RWMutex - configChanged chan struct{} - configUpdates int - - doorLock sync.RWMutex - doors map[uint8]*Door - - bot *string -} - -func (p *BathroomMonitorPlugin) getKeyFile(id uint8) (*rsa.PublicKey, error) { - keyFile, err := ioutil.ReadFile(path.Join(p.config.KeyPath, fmt.Sprintf("public%d.pem", id))) - if err != nil { - return nil, err - } - - data, _ := pem.Decode(keyFile) - if data == nil { - return nil, errors.New("No PEM formatted data in file") - } - - key, err := x509.ParsePKIXPublicKey(data.Bytes) - if err != nil { - return nil, err - } - - switch pub := key.(type) { - case *rsa.PublicKey: - return pub, nil - } - - return nil, errors.New(fmt.Sprintf("Keyfile wasn't a public RSA PKIX key, it was %T", key)) -} - -func (p *BathroomMonitorPlugin) initDoors() { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - p.doors = make(map[uint8]*Door) - for i := uint8(0); i < p.config.numDoors; i++ { - id := i + 1 - var pub *rsa.PublicKey = nil - if !USE_FSNOTIFY { - var err error - pub, err = p.getKeyFile(id) - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Unable to load key file for id %d", id)).Error()) - pub = nil - } - } - p.doors[i] = &Door { - id: id, - status: Unknown, - pubKey: pub, - } - } -} - -func (p *BathroomMonitorPlugin) log(log string) { - if DO_LOGGING { - p.API.LogInfo(log) - } -} - -func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error { - if id < 1 || id > p.config.numDoors { - return errors.New(fmt.Sprintf("Invalid door id %d", id)) - } - - if status < Unknown || status > Closed { - return errors.New(fmt.Sprintf("Invalid door status %d", status)) - } - - if p.doors == nil || len(p.doors) < int(p.config.numDoors) { - return errors.New("Doors not inited yet") - } - - if p.doors[id - 1].status != status { - p.doors[id - 1].status = status - if report { - statusStr, _ := statusName(status) - _ = statusStr - if POST_STATUS_TO_ADMIN { - p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr)) - } - p.sendUpdate() - go p.resendUpdate(10 * time.Second) - } - } else { - p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status)) - } - - return nil -} - -func (p *BathroomMonitorPlugin) sendUpdate() { - p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{}) -} - -func (p *BathroomMonitorPlugin) resendUpdate(wait time.Duration) { - timer := time.NewTimer(wait) - <-timer.C - p.sendUpdate() -} - - -func (p *BathroomMonitorPlugin) init() *BathroomMonitorPlugin { - p.config = &Config { - NumDoors: "1", - numDoors: 1, - - WatchPath: "./", - - KeyPath: "./", - - AdminUsers: "", - } - - p.configChanged = make(chan struct{}) - - return p; -} - -func (p *BathroomMonitorPlugin) validateRequestDoorId(r *http.Request) (uint8, error) { - p.configLock.Lock() - numDoors := p.config.numDoors - p.configLock.Unlock() - - - doorStr, ok := r.Form["door_id"] - if !ok || len(doorStr) == 0 { - return 0, errors.New("Please send door id") - } - - doorId, err := strconv.ParseUint(doorStr[0], 10, 8) - if err != nil { - return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error())) - } - doorId8 := uint8(doorId) - - if doorId8 < 1 || doorId8 > numDoors { - return 0, errors.New("Invalid door id") - } - - return doorId8, nil -} - -func SliceIndex(limit int, predicate func(i int) bool) int { - for i := 0; i < limit; i++ { - if predicate(i) { - return i - } - } - return -1 -} - -func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { - auth, authOk := r.Header["Mattermost-User-Id"] - var authUser string = "" - if authOk && len(auth) > 0 { - authUser = auth[0] - } - p.log(fmt.Sprintf("Requested path: %s %s AUTH: %s (%t)", r.URL.Path, c.IpAddress, authUser, authOk)) - if r.URL.Path == "/settings" { - p.configLock.Lock() - defer p.configLock.Unlock() - - retMap := make(map[string]interface{}) - for k, v := range p.config.settingsJson { - retMap[k] = v - } - - userOptions := defaultUserOptions() - - if authOk { - p.Helpers.KVGetJSON("settings_" + authUser, userOptions) - } - - retMap["user"] = userOptions - - bytes, err := json.Marshal(retMap) - if err == nil { - fmt.Fprint(w, string(bytes)) - } else { - p.API.LogError(fmt.Sprintf("Unable to json Marshal settings %s", err.Error())) - } - - return - } - if r.URL.Path == "/status" { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - var write map[string]string = make(map[string]string) - for d, s := range p.doors { - statusStr, _ := statusName(s.status) - write[fmt.Sprintf("%d", d + 1)] = statusStr - } - if output, err := json.Marshal(write); err == nil { - fmt.Fprint(w, string(output)) - } else { - fmt.Fprint(w, html.EscapeString(err.Error())) - } - return - } - if r.URL.Path == "/admin-update-status" && authOk { - r.ParseForm() - p.configLock.Lock() - p.doorLock.Lock() - defer p.doorLock.Unlock() - defer p.configLock.Unlock() - - if SliceIndex(len(p.config.adminUsers), func(i int) bool { return p.config.adminUsers[i].Id == authUser }) != -1 { - for n, vs := range r.Form { - matches := doorWatchFile.FindStringSubmatch(n) - if matches != nil && len(matches) >= 3 { - doorId64, err := strconv.ParseUint(matches[2], 10, 8) - doorId := uint8(doorId64) - if err == nil && doorId >= 1 && doorId <= p.config.numDoors && len(vs) > 0 { - status64, err := strconv.ParseUint(vs[0], 10, 8) - status := uint(status64) - if err == nil && status >= Unknown && status <= Closed { - p.setDoorStatus(uint8(doorId), status, true) - } else { - fmt.Fprintf(w, "Not a valid status: %d", status) - } - } else { - fmt.Fprintf(w, "Not a valid door: %# v, %d, %# v", matches, doorId, pretty.Formatter(vs)) - } - } else { - fmt.Fprintf(w, "Not a valid door key: %s", n) - } - } - } else { - fmt.Fprintf(w, "Not authorized: %s", authUser) - } - return - } - if r.URL.Path == "/status-update" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - statusStr, ok := r.Form["status"] - if !ok || len(statusStr) == 0 { - fmt.Fprint(w, "Please send door status") - return - } - - status, err := strconv.ParseUint(statusStr[0], 10, 8) - if err != nil { - fmt.Fprintf(w, "Couldn't parse status: %s", err.Error()) - return - } - - if status != 0 && status != 1 { - fmt.Fprint(w, "Invalid status") - return - } - - statusVal := Open - if status == 1 { - statusVal = Closed - } - - p.doorLock.Lock() - defer p.doorLock.Unlock() - - if p.doors[doorId8 - 1].pubKey == nil { - fmt.Fprintf(w, "No public key found for door %d", doorId8) - return - } - - p.log("Getting random bytes") - var verifyBytes [50]byte - rand.Read(verifyBytes[:]) - p.log("Encoding random bytes") - - verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:]) - - p.log("Encryping random bytes") - encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{}) - if err != nil { - fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error()) - return - } - - p.log("Encoding encrypted bytes") - encryptedB64 := base64.StdEncoding.EncodeToString(encrypted) - - - req := &DoorRequest { - ip:c.IpAddress, - time:time.Now().Unix(), - verify:verifyB64, - status: statusVal, - } - - p.doors[doorId8 - 1].lastRequest = req - - p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify)) - - fmt.Fprint(w, encryptedB64) - - - return - } - if r.URL.Path == "/verify-status" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - verifyB64, ok := r.Form["verify"] - if !ok || len(verifyB64) <= 0 { - fmt.Fprint(w, "Please send the verification code") - return - } - - p.log("Verify locking") - p.doorLock.Lock() - defer p.doorLock.Unlock() - p.log("Verify done locking") - - if p.doors[doorId8 - 1].lastRequest == nil { - fmt.Fprint(w, "Invalid request") - return - } - - req := p.doors[doorId8 - 1].lastRequest - if req.ip != c.IpAddress { - fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr) - return - } - - diff := time.Now().Unix() - req.time - if diff < 0 || diff > 10 { - fmt.Fprint(w, "Request expired") - return - } - - - if req.verify != verifyB64[0] { - p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0])) - fmt.Fprint(w, "Unauthorized request") - return - } - - - p.doors[doorId8 - 1].lastRequest = nil - - p.log("Changing") - err = p.setDoorStatus(doorId8, req.status, true) - p.log("Changed") - if err != nil { - fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error()) - } - - return - - } - http.NotFound(w, r) -} - -func (p *BathroomMonitorPlugin) confChangedEvent() { - select { - case p.configChanged <- struct{}{}: - default: - } -} - -func (p *BathroomMonitorPlugin) postAdminChannel(text string) { - p.API.LogError(text) - - if p.bot != nil { - for _, u := range(p.config.adminUsers) { - channel, err := p.API.GetDirectChannel(*p.bot, u.Id); - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't get direct channel to user %s", u.Username)).Error()) - continue - } - p.API.CreatePost(&model.Post{UserId: *p.bot, ChannelId:channel.Id, Message:text, MessageSource:text}) - } - } -} - -func (p *BathroomMonitorPlugin) OnConfigurationChange() error { - p.configLock.Lock() - defer p.configLock.Unlock() - - var newConfig *Config = new(Config); - if err := p.API.LoadPluginConfiguration(newConfig); err != nil { - newErr := errors.Wrap(err, fmt.Sprintf("%d: Failed to load configuration", p.configUpdates)) - p.postAdminChannel(newErr.Error()) - return newErr - } - - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { - p.API.LogInfo("Passed same config, or empty?") - return nil; - } - - var configErr error = nil - - - if USE_FSNOTIFY { - if info, err := os.Stat(newConfig.WatchPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) - } - } else { - if info, err := os.Stat(newConfig.KeyPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.KeyPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid key path")) - } - } - - - numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) - if err != nil { - newConfig.NumDoors = "1" - newConfig.numDoors = 1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) - } else { - newConfig.numDoors = uint8(numDoors) - } - - if strings.Trim(newConfig.PingInterval, " \n\t") == "" { - newConfig.pingInterval = -1 - } else { - pingInterval, err := strconv.ParseInt(newConfig.PingInterval, 10, 32) - if err != nil { - newConfig.PingInterval = "" - newConfig.pingInterval = -1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid ping interval")) - } else { - newConfig.pingInterval = int(pingInterval) - } - } - - newConfig.adminUsers = make([]*model.User, 0, 4) - split := splitWhitespaceOrComma.Split(newConfig.AdminUsers, -1) - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - u, _ := p.API.GetUserByUsername(trimmed) - if u != nil { - newConfig.adminUsers = append(newConfig.adminUsers, u) - } - } - } - - newConfig.doorNames = make([]string, 0, 4) - split = strings.Split(newConfig.DoorNames, "|") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.doorNames = append(newConfig.doorNames, trimmed) - } - } - - newConfig.unknownIcons = make([]string, 0, 4) - split = strings.Split(newConfig.UnknownIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.unknownIcons = append(newConfig.unknownIcons, trimmed) - } - } - - newConfig.openIcons = make([]string, 0, 4) - split = strings.Split(newConfig.OpenIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.openIcons = append(newConfig.openIcons, trimmed) - } - } - - newConfig.closedIcons = make([]string, 0, 4) - split = strings.Split(newConfig.ClosedIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.closedIcons = append(newConfig.closedIcons, trimmed) - } - } - - doorOrder := make([]uint8, 0, 4) - split = strings.Split(newConfig.DoorOrder, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - id, err := strconv.ParseUint(trimmed, 10, 8) - if err != nil { - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid door order")) - doorOrder = nil - break - } - doorOrder = append(doorOrder, uint8(id)) - } - } - newConfig.doorOrder = doorOrder - - min := func(a uint8, b uint8) uint8 { - if a < b { - return a - } - return b - } - - doorsJson := make(map[string]string) - for i := uint8(0); i < min(uint8(len(newConfig.doorNames)), newConfig.numDoors); i++ { - doorsJson[fmt.Sprintf("%d", i + 1)] = newConfig.doorNames[i] - } - - doorOrderJson := make([]int, len(newConfig.doorOrder)) - for i, o := range newConfig.doorOrder { - doorOrderJson[i] = int(o) - } - newConfig.settingsJson = map[string]interface{} { - "info_icon": newConfig.InfoIcon, - "unknown_icons": newConfig.unknownIcons, - "open_icons": newConfig.openIcons, - "closed_icons": newConfig.closedIcons, - "doors": doorsJson, - "door_order": doorOrderJson, - } - - p.config = newConfig - p.configUpdates++ - - p.initDoors() - - p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath)) - - if configErr != nil { - p.postAdminChannel(configErr.Error()) - } - - p.confChangedEvent() - - return configErr -} - -func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { - for { - p.configLock.Lock() - numDoors := p.config.numDoors - watchPath := p.config.WatchPath - p.configLock.Unlock() - - _ = numDoors - - watcher, err := fsnotify.NewWatcher() - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't make watcher").Error()) - time.Sleep(60) - continue - } - - err = watcher.Add(watchPath) - - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't watch WatchPath").Error()) - } - - run := true - for ;run; { - select { - case event, ok := <- watcher.Events: - if (!ok) { - p.API.LogError(errors.Wrap(err, "Couldn't get fsnotify event").Error()) - time.Sleep(60) - run = false - } else { - match := doorWatchFile.FindStringSubmatch(event.Name) - if match == nil { - continue - } - - id64, err := strconv.ParseUint(match[2], 10, 8) - if err != nil { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - continue - } - - id := uint8(id64) - if id < 1 || id > numDoors { - continue - } - - if event.Op & fsnotify.Write != fsnotify.Write && event.Op & fsnotify.Create != fsnotify.Create { - continue - } - - statusBytes, err := ioutil.ReadFile(event.Name) - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - - status := strings.Trim(string(statusBytes), " \n\t") - if len(status) <= 0 { - continue - } - - statusInt, err := strconv.ParseUint(status, 10, 32) - if err != nil { - p.API.LogError(errors.Wrap(err, "Status invalid " + status).Error()) - continue - } - - newStatus := Open - if statusInt == 1 { - newStatus = Closed - } - - p.doorLock.Lock() - err = p.setDoorStatus(id, newStatus, true) - p.doorLock.Unlock() - - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't set door status %d %d", id, statusInt)).Error()) - continue - } - } - case <- p.configChanged: - p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates)) - run = false - } - } - - watcher.Close() - } -} - -func (p *BathroomMonitorPlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { - userOptions := defaultUserOptions() - _ = userOptions - - return nil, nil -} - -func (p *BathroomMonitorPlugin) OnActivate() error { - p.API.RegisterCommand(&model.Command { - Trigger: "bathrooms", - AutoComplete: true, - AutoCompleteDesc: "Turns the bathroom widget \"on\" or \"off\" (\"status\" or no option prints bathroom statuses)", - AutoCompleteHint: "(on|off|status)", - }) - if p.bot == nil { - botUser, err := p.Helpers.EnsureBot(&model.Bot{Username:"bathroom-bot", DisplayName:"Bathroom Bot", Description:"Tracks Bathroom Status"}); - if err == nil { - p.bot = &botUser - } else { - p.bot = nil - } - } - if USE_FSNOTIFY { - go fileNotifyBasedLoop(p) - } else { - } - - go p.pingLoop(); - - return nil -} - -func (p *BathroomMonitorPlugin) pingLoop() { - for { - - var tickChan <-chan time.Time = nil - var ticker *time.Ticker = nil - - p.configLock.Lock() - p.log(fmt.Sprintf("Setting pingLoop with interval %d", p.config.pingInterval)) - if p.config.pingInterval > 0 { - ticker = time.NewTicker(time.Duration(p.config.pingInterval) * time.Second) - tickChan = ticker.C - - } - p.configLock.Unlock() - - run := true - for ;run; { - select { - case <- tickChan: - p.log(fmt.Sprintf("Sending ping")) - p.API.PublishWebSocketEvent("ping", map[string]interface{}{}, &model.WebsocketBroadcast{}) - case <- p.configChanged: - run = false - } - } - - if ticker != nil { - ticker.Stop() - } - } -} - -func main() { - plugin.ClientMain((&BathroomMonitorPlugin{}).init()) -} diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh index c2c1a0e..1623a54 100755 --- a/mattermost/server/build.sh +++ b/mattermost/server/build.sh @@ -1,4 +1,7 @@ #! /usr/bin/env bash -filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json -GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES +printf -v FORMATTED_TAGS '%s,' "$@" +FORMATTED_TAGS=${FORMATTED_TAGS%,} + +gcc -P -E $( [[ -n "$DEFS" ]] && echo -D$DEFS ) - - < ../plugin_templ.json > ../plugin.json +GOOS=linux GOARCH=amd64 go build $( [[ -n "$FORMATTED_TAGS" ]] && echo -tags $FORMATTED_TAGS ) -o bathroom-linux-amd64 bathroom-plugin/main diff --git a/mattermost/server/build_web.sh b/mattermost/server/build_web.sh deleted file mode 100755 index 7ccb125..0000000 --- a/mattermost/server/build_web.sh +++ /dev/null @@ -1,3 +0,0 @@ -#! /usr/bin/env bash - -FILES=config_web.go ./build.sh diff --git a/mattermost/server/config_fs.go b/mattermost/server/config_fs.go deleted file mode 100644 index 6384df5..0000000 --- a/mattermost/server/config_fs.go +++ /dev/null @@ -1,3 +0,0 @@ -package main - -const USE_FSNOTIFY = true diff --git a/mattermost/server/config_web.go b/mattermost/server/config_web.go deleted file mode 100644 index c72b5c8..0000000 --- a/mattermost/server/config_web.go +++ /dev/null @@ -1,3 +0,0 @@ -package main - -const USE_FSNOTIFY = false diff --git a/mattermost/server/go.mod b/mattermost/server/go.mod new file mode 100644 index 0000000..779de8d --- /dev/null +++ b/mattermost/server/go.mod @@ -0,0 +1,54 @@ +module bathroom-plugin + +go 1.23.3 + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.6.2 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect + github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect + github.com/mattermost/logr/v2 v2.0.21 // indirect + github.com/mattermost/mattermost/server/public v0.1.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/pborman/uuid v1.2.1 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/tinylib/msgp v1.2.4 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/wiggin77/merror v1.0.5 // indirect + github.com/wiggin77/srslog v1.0.1 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f // indirect + google.golang.org/grpc v1.68.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/mattermost/server/go.sum b/mattermost/server/go.sum new file mode 100755 index 0000000..2fe6112 --- /dev/null +++ b/mattermost/server/go.sum @@ -0,0 +1,291 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a h1:etIrTD8BQqzColk9nKRusM9um5+1q0iOEJLqfBMIK64= +github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a/go.mod h1:emQhSYTXqB0xxjLITTw4EaWZ+8IIQYw+kx9GqNUKdLg= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= +github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= +github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8= +github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34= +github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb2BTCsOdamENjjWCI6qmfHLbk6OZI= +github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956/go.mod h1:SRl30Lb7/QoYyohYeVBuqYvvmXSZJxZgiV3Zf6VbxjI= +github.com/mattermost/logr/v2 v2.0.21 h1:CMHsP+nrbRlEC4g7BwOk1GAnMtHkniFhlSQPXy52be4= +github.com/mattermost/logr/v2 v2.0.21/go.mod h1:kZkB/zqKL9e+RY5gB3vGpsyenC+TpuiOenjMkvJJbzc= +github.com/mattermost/mattermost v10.2.0+incompatible h1:Jo/tr/282u/ixQPUbCV6BcEVgSxxcxUeIgWMpT+3zbQ= +github.com/mattermost/mattermost v10.2.0+incompatible/go.mod h1:JUnq0RiZa+eUafKS5M47SVRdPIJyIITxTcCFvoPlFlk= +github.com/mattermost/mattermost-server v10.2.0+incompatible h1:RhG3b+xCdGUnHyWVJxYWuBzDdgBrJp1LHPJjBEmu48I= +github.com/mattermost/mattermost-server v10.2.0+incompatible/go.mod h1:5L6MjAec+XXQwMIt791Ganu45GKsSiM+I0tLR9wUj8Y= +github.com/mattermost/mattermost/server/public v0.1.9 h1:l/OKPRVuFeqL0yqRVC/JpveG5sLNKcT9llxqMkO9e+s= +github.com/mattermost/mattermost/server/public v0.1.9/go.mod h1:SkTKbMul91Rq0v2dIxe8mqzUOY+3KwlwwLmAlxDfGCk= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tinylib/msgp v1.2.4 h1:yLFeUGostXXSGW5vxfT5dXG/qzkn4schv2I7at5+hVU= +github.com/tinylib/msgp v1.2.4/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wiggin77/merror v1.0.5 h1:P+lzicsn4vPMycAf2mFf7Zk6G9eco5N+jB1qJ2XW3ME= +github.com/wiggin77/merror v1.0.5/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0= +github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8= +github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f h1:C1QccEa9kUwvMgEUORqQD9S17QesQijxjZ84sO82mfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/mattermost/server/main/bathroom.go b/mattermost/server/main/bathroom.go new file mode 100644 index 0000000..feb4837 --- /dev/null +++ b/mattermost/server/main/bathroom.go @@ -0,0 +1,829 @@ +package main + +import ( + "fmt" + "net/http" + "github.com/mattermost/mattermost/server/public/plugin" + "github.com/mattermost/mattermost/server/public/model" + "github.com/pkg/errors" + "sync" + "reflect" + _ "github.com/google/go-cmp/cmp" + "os" + _ "github.com/fatih/structs" + "github.com/hashicorp/go-multierror" + "github.com/kr/pretty" + "strings" + "regexp" + "strconv" + "time" + "github.com/fsnotify/fsnotify" + "io/ioutil" + "html" + "encoding/json" + "encoding/pem" + "crypto/rsa" + "crypto/x509" + "crypto/rand" + "crypto/sha1" + "path" + "encoding/base64" + _ "math" +) + +const POST_STATUS_TO_ADMIN = false +const DO_LOGGING = false + +var splitWhitespaceOrComma *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) +var doorWatchFile *regexp.Regexp = regexp.MustCompile(`(^|/)door(\d+)$`) + +const ( + Unknown = uint(iota) + Open + Closed +) + +func statusName(status uint) (string, error) { + switch status { + case Unknown: + return "unknown", nil + case Open: + return "open", nil + case Closed: + return "closed", nil + } + + return "", errors.New(fmt.Sprintf("Invalid status %d", status)) +} + +type UserOptions struct { + ShowWidget bool `json:"show_widget"` +} + +func defaultUserOptions() *UserOptions { + return &UserOptions { + ShowWidget: true, + } +} + +type DoorRequest struct { + ip string + time int64 + verify string + status uint +} + +type Door struct { + id uint8 + status uint + pubKey *rsa.PublicKey + lastRequest *DoorRequest +} + +type Config struct { + NumDoors string + numDoors uint8 + + PingInterval string + pingInterval int + + WatchPath string + + AdminUsers string + adminUsers []*model.User + + KeyPath string + + DoorNames string + doorNames []string + + InfoIcon string + UnknownIcons string + OpenIcons string + ClosedIcons string + + unknownIcons []string + openIcons []string + closedIcons []string + + DoorOrder string + doorOrder []uint8 + + settingsJson map[string]interface{} +} + +type BathroomMonitorPlugin struct { + plugin.MattermostPlugin + + config *Config + configLock sync.RWMutex + configChanged chan struct{} + configUpdates int + + doorLock sync.RWMutex + doors map[uint8]*Door + + bot *string +} + +func (p *BathroomMonitorPlugin) getKeyFile(id uint8) (*rsa.PublicKey, error) { + keyFile, err := ioutil.ReadFile(path.Join(p.config.KeyPath, fmt.Sprintf("public%d.pem", id))) + if err != nil { + return nil, err + } + + data, _ := pem.Decode(keyFile) + if data == nil { + return nil, errors.New("No PEM formatted data in file") + } + + key, err := x509.ParsePKIXPublicKey(data.Bytes) + if err != nil { + return nil, err + } + + switch pub := key.(type) { + case *rsa.PublicKey: + return pub, nil + } + + return nil, errors.New(fmt.Sprintf("Keyfile wasn't a public RSA PKIX key, it was %T", key)) +} + +func (p *BathroomMonitorPlugin) initDoors() { + p.doorLock.Lock() + defer p.doorLock.Unlock() + + p.doors = make(map[uint8]*Door) + for i := uint8(0); i < p.config.numDoors; i++ { + id := i + 1 + var pub *rsa.PublicKey = nil + if !USE_FSNOTIFY { + var err error + pub, err = p.getKeyFile(id) + if err != nil { + p.API.LogError(errors.Wrap(err, fmt.Sprintf("Unable to load key file for id %d", id)).Error()) + pub = nil + } + } + p.doors[i] = &Door { + id: id, + status: Unknown, + pubKey: pub, + } + } +} + +func (p *BathroomMonitorPlugin) log(log string) { + if DO_LOGGING { + p.API.LogInfo(log) + } +} + +func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error { + if id < 1 || id > p.config.numDoors { + return errors.New(fmt.Sprintf("Invalid door id %d", id)) + } + + if status < Unknown || status > Closed { + return errors.New(fmt.Sprintf("Invalid door status %d", status)) + } + + if p.doors == nil || len(p.doors) < int(p.config.numDoors) { + return errors.New("Doors not inited yet") + } + + if p.doors[id - 1].status != status { + p.doors[id - 1].status = status + if report { + statusStr, _ := statusName(status) + _ = statusStr + if POST_STATUS_TO_ADMIN { + p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr)) + } + p.sendUpdate() + go p.resendUpdate(10 * time.Second) + } + } else { + p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status)) + } + + return nil +} + +func (p *BathroomMonitorPlugin) sendUpdate() { + p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{}) +} + +func (p *BathroomMonitorPlugin) resendUpdate(wait time.Duration) { + timer := time.NewTimer(wait) + <-timer.C + p.sendUpdate() +} + + +func (p *BathroomMonitorPlugin) init() *BathroomMonitorPlugin { + p.config = &Config { + NumDoors: "1", + numDoors: 1, + + WatchPath: "./", + + KeyPath: "./", + + AdminUsers: "", + } + + p.configChanged = make(chan struct{}) + + return p; +} + +func (p *BathroomMonitorPlugin) validateRequestDoorId(r *http.Request) (uint8, error) { + p.configLock.Lock() + numDoors := p.config.numDoors + p.configLock.Unlock() + + + doorStr, ok := r.Form["door_id"] + if !ok || len(doorStr) == 0 { + return 0, errors.New("Please send door id") + } + + doorId, err := strconv.ParseUint(doorStr[0], 10, 8) + if err != nil { + return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error())) + } + doorId8 := uint8(doorId) + + if doorId8 < 1 || doorId8 > numDoors { + return 0, errors.New("Invalid door id") + } + + return doorId8, nil +} + +func SliceIndex(limit int, predicate func(i int) bool) int { + for i := 0; i < limit; i++ { + if predicate(i) { + return i + } + } + return -1 +} + +func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { + auth, authOk := r.Header["Mattermost-User-Id"] + var authUser string = "" + if authOk && len(auth) > 0 { + authUser = auth[0] + } + p.log(fmt.Sprintf("Requested path: %s %s AUTH: %s (%t)", r.URL.Path, c.IpAddress, authUser, authOk)) + if r.URL.Path == "/settings" { + p.configLock.Lock() + defer p.configLock.Unlock() + + retMap := make(map[string]interface{}) + for k, v := range p.config.settingsJson { + retMap[k] = v + } + + userOptions := defaultUserOptions() + + if authOk { + p.Helpers.KVGetJSON("settings_" + authUser, userOptions) + } + + retMap["user"] = userOptions + + bytes, err := json.Marshal(retMap) + if err == nil { + fmt.Fprint(w, string(bytes)) + } else { + p.API.LogError(fmt.Sprintf("Unable to json Marshal settings %s", err.Error())) + } + + return + } + if r.URL.Path == "/status" { + p.doorLock.Lock() + defer p.doorLock.Unlock() + + var write map[string]string = make(map[string]string) + for d, s := range p.doors { + statusStr, _ := statusName(s.status) + write[fmt.Sprintf("%d", d + 1)] = statusStr + } + if output, err := json.Marshal(write); err == nil { + fmt.Fprint(w, string(output)) + } else { + fmt.Fprint(w, html.EscapeString(err.Error())) + } + return + } + if r.URL.Path == "/admin-update-status" && authOk { + r.ParseForm() + p.configLock.Lock() + p.doorLock.Lock() + defer p.doorLock.Unlock() + defer p.configLock.Unlock() + + if SliceIndex(len(p.config.adminUsers), func(i int) bool { return p.config.adminUsers[i].Id == authUser }) != -1 { + for n, vs := range r.Form { + matches := doorWatchFile.FindStringSubmatch(n) + if matches != nil && len(matches) >= 3 { + doorId64, err := strconv.ParseUint(matches[2], 10, 8) + doorId := uint8(doorId64) + if err == nil && doorId >= 1 && doorId <= p.config.numDoors && len(vs) > 0 { + status64, err := strconv.ParseUint(vs[0], 10, 8) + status := uint(status64) + if err == nil && status >= Unknown && status <= Closed { + p.setDoorStatus(uint8(doorId), status, true) + } else { + fmt.Fprintf(w, "Not a valid status: %d", status) + } + } else { + fmt.Fprintf(w, "Not a valid door: %# v, %d, %# v", matches, doorId, pretty.Formatter(vs)) + } + } else { + fmt.Fprintf(w, "Not a valid door key: %s", n) + } + } + } else { + fmt.Fprintf(w, "Not authorized: %s", authUser) + } + return + } + if r.URL.Path == "/status-update" { + r.ParseForm() + p.log("Contacted by " + r.RemoteAddr) + + doorId8, err := p.validateRequestDoorId(r) + if err != nil { + fmt.Fprint(w, err.Error()) + return + } + + statusStr, ok := r.Form["status"] + if !ok || len(statusStr) == 0 { + fmt.Fprint(w, "Please send door status") + return + } + + status, err := strconv.ParseUint(statusStr[0], 10, 8) + if err != nil { + fmt.Fprintf(w, "Couldn't parse status: %s", err.Error()) + return + } + + if status != 0 && status != 1 { + fmt.Fprint(w, "Invalid status") + return + } + + statusVal := Open + if status == 1 { + statusVal = Closed + } + + p.doorLock.Lock() + defer p.doorLock.Unlock() + + if p.doors[doorId8 - 1].pubKey == nil { + fmt.Fprintf(w, "No public key found for door %d", doorId8) + return + } + + p.log("Getting random bytes") + var verifyBytes [50]byte + rand.Read(verifyBytes[:]) + p.log("Encoding random bytes") + + verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:]) + + p.log("Encryping random bytes") + encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{}) + if err != nil { + fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error()) + return + } + + p.log("Encoding encrypted bytes") + encryptedB64 := base64.StdEncoding.EncodeToString(encrypted) + + + req := &DoorRequest { + ip:c.IpAddress, + time:time.Now().Unix(), + verify:verifyB64, + status: statusVal, + } + + p.doors[doorId8 - 1].lastRequest = req + + p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify)) + + fmt.Fprint(w, encryptedB64) + + + return + } + if r.URL.Path == "/verify-status" { + r.ParseForm() + p.log("Contacted by " + r.RemoteAddr) + doorId8, err := p.validateRequestDoorId(r) + if err != nil { + fmt.Fprint(w, err.Error()) + return + } + + verifyB64, ok := r.Form["verify"] + if !ok || len(verifyB64) <= 0 { + fmt.Fprint(w, "Please send the verification code") + return + } + + p.log("Verify locking") + p.doorLock.Lock() + defer p.doorLock.Unlock() + p.log("Verify done locking") + + if p.doors[doorId8 - 1].lastRequest == nil { + fmt.Fprint(w, "Invalid request") + return + } + + req := p.doors[doorId8 - 1].lastRequest + if req.ip != c.IpAddress { + fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr) + return + } + + diff := time.Now().Unix() - req.time + if diff < 0 || diff > 10 { + fmt.Fprint(w, "Request expired") + return + } + + + if req.verify != verifyB64[0] { + p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0])) + fmt.Fprint(w, "Unauthorized request") + return + } + + + p.doors[doorId8 - 1].lastRequest = nil + + p.log("Changing") + err = p.setDoorStatus(doorId8, req.status, true) + p.log("Changed") + if err != nil { + fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error()) + } + + return + + } + http.NotFound(w, r) +} + +func (p *BathroomMonitorPlugin) confChangedEvent() { + select { + case p.configChanged <- struct{}{}: + default: + } +} + +func (p *BathroomMonitorPlugin) postAdminChannel(text string) { + p.API.LogError(text) + + if p.bot != nil { + for _, u := range(p.config.adminUsers) { + channel, err := p.API.GetDirectChannel(*p.bot, u.Id); + if err != nil { + p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't get direct channel to user %s", u.Username)).Error()) + continue + } + p.API.CreatePost(&model.Post{UserId: *p.bot, ChannelId:channel.Id, Message:text, MessageSource:text}) + } + } +} + +func (p *BathroomMonitorPlugin) OnConfigurationChange() error { + p.configLock.Lock() + defer p.configLock.Unlock() + + var newConfig *Config = new(Config); + if err := p.API.LoadPluginConfiguration(newConfig); err != nil { + newErr := errors.Wrap(err, fmt.Sprintf("%d: Failed to load configuration", p.configUpdates)) + p.postAdminChannel(newErr.Error()) + return newErr + } + + if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { + p.API.LogInfo("Passed same config, or empty?") + return nil; + } + + var configErr error = nil + + + if USE_FSNOTIFY { + if info, err := os.Stat(newConfig.WatchPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { + newConfig.WatchPath = "./" + configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) + } + } else { + if info, err := os.Stat(newConfig.KeyPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { + newConfig.KeyPath = "./" + configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid key path")) + } + } + + + numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) + if err != nil { + newConfig.NumDoors = "1" + newConfig.numDoors = 1 + configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) + } else { + newConfig.numDoors = uint8(numDoors) + } + + if strings.Trim(newConfig.PingInterval, " \n\t") == "" { + newConfig.pingInterval = -1 + } else { + pingInterval, err := strconv.ParseInt(newConfig.PingInterval, 10, 32) + if err != nil { + newConfig.PingInterval = "" + newConfig.pingInterval = -1 + configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid ping interval")) + } else { + newConfig.pingInterval = int(pingInterval) + } + } + + newConfig.adminUsers = make([]*model.User, 0, 4) + split := splitWhitespaceOrComma.Split(newConfig.AdminUsers, -1) + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + u, _ := p.API.GetUserByUsername(trimmed) + if u != nil { + newConfig.adminUsers = append(newConfig.adminUsers, u) + } + } + } + + newConfig.doorNames = make([]string, 0, 4) + split = strings.Split(newConfig.DoorNames, "|") + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + newConfig.doorNames = append(newConfig.doorNames, trimmed) + } + } + + newConfig.unknownIcons = make([]string, 0, 4) + split = strings.Split(newConfig.UnknownIcons, ",") + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + newConfig.unknownIcons = append(newConfig.unknownIcons, trimmed) + } + } + + newConfig.openIcons = make([]string, 0, 4) + split = strings.Split(newConfig.OpenIcons, ",") + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + newConfig.openIcons = append(newConfig.openIcons, trimmed) + } + } + + newConfig.closedIcons = make([]string, 0, 4) + split = strings.Split(newConfig.ClosedIcons, ",") + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + newConfig.closedIcons = append(newConfig.closedIcons, trimmed) + } + } + + doorOrder := make([]uint8, 0, 4) + split = strings.Split(newConfig.DoorOrder, ",") + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + id, err := strconv.ParseUint(trimmed, 10, 8) + if err != nil { + configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid door order")) + doorOrder = nil + break + } + doorOrder = append(doorOrder, uint8(id)) + } + } + newConfig.doorOrder = doorOrder + + min := func(a uint8, b uint8) uint8 { + if a < b { + return a + } + return b + } + + doorsJson := make(map[string]string) + for i := uint8(0); i < min(uint8(len(newConfig.doorNames)), newConfig.numDoors); i++ { + doorsJson[fmt.Sprintf("%d", i + 1)] = newConfig.doorNames[i] + } + + doorOrderJson := make([]int, len(newConfig.doorOrder)) + for i, o := range newConfig.doorOrder { + doorOrderJson[i] = int(o) + } + newConfig.settingsJson = map[string]interface{} { + "info_icon": newConfig.InfoIcon, + "unknown_icons": newConfig.unknownIcons, + "open_icons": newConfig.openIcons, + "closed_icons": newConfig.closedIcons, + "doors": doorsJson, + "door_order": doorOrderJson, + } + + p.config = newConfig + p.configUpdates++ + + p.initDoors() + + p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath)) + + if configErr != nil { + p.postAdminChannel(configErr.Error()) + } + + p.confChangedEvent() + + return configErr +} + +func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { + for { + p.configLock.Lock() + numDoors := p.config.numDoors + watchPath := p.config.WatchPath + p.configLock.Unlock() + + _ = numDoors + + watcher, err := fsnotify.NewWatcher() + if err != nil { + p.API.LogError(errors.Wrap(err, "Couldn't make watcher").Error()) + time.Sleep(60) + continue + } + + err = watcher.Add(watchPath) + + if err != nil { + p.API.LogError(errors.Wrap(err, "Couldn't watch WatchPath").Error()) + } + + run := true + for ;run; { + select { + case event, ok := <- watcher.Events: + if (!ok) { + p.API.LogError(errors.Wrap(err, "Couldn't get fsnotify event").Error()) + time.Sleep(60) + run = false + } else { + match := doorWatchFile.FindStringSubmatch(event.Name) + if match == nil { + continue + } + + id64, err := strconv.ParseUint(match[2], 10, 8) + if err != nil { + p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) + continue + } + + id := uint8(id64) + if id < 1 || id > numDoors { + continue + } + + if event.Op & fsnotify.Write != fsnotify.Write && event.Op & fsnotify.Create != fsnotify.Create { + continue + } + + statusBytes, err := ioutil.ReadFile(event.Name) + if err != nil { + p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) + } + + status := strings.Trim(string(statusBytes), " \n\t") + if len(status) <= 0 { + continue + } + + statusInt, err := strconv.ParseUint(status, 10, 32) + if err != nil { + p.API.LogError(errors.Wrap(err, "Status invalid " + status).Error()) + continue + } + + newStatus := Open + if statusInt == 1 { + newStatus = Closed + } + + p.doorLock.Lock() + err = p.setDoorStatus(id, newStatus, true) + p.doorLock.Unlock() + + if err != nil { + p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't set door status %d %d", id, statusInt)).Error()) + continue + } + } + case <- p.configChanged: + p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates)) + run = false + } + } + + watcher.Close() + } +} + +func (p *BathroomMonitorPlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { + userOptions := defaultUserOptions() + _ = userOptions + + return nil, nil +} + +func (p *BathroomMonitorPlugin) OnActivate() error { + p.API.RegisterCommand(&model.Command { + Trigger: "bathrooms", + AutoComplete: true, + AutoCompleteDesc: "Turns the bathroom widget \"on\" or \"off\" (\"status\" or no option prints bathroom statuses)", + AutoCompleteHint: "(on|off|status)", + }) + if p.bot == nil { + botUser, err := p.Helpers.EnsureBot(&model.Bot{Username:"bathroom-bot", DisplayName:"Bathroom Bot", Description:"Tracks Bathroom Status"}); + if err == nil { + p.bot = &botUser + } else { + p.bot = nil + } + } + if USE_FSNOTIFY { + go fileNotifyBasedLoop(p) + } else { + } + + go p.pingLoop(); + + return nil +} + +func (p *BathroomMonitorPlugin) pingLoop() { + for { + + var tickChan <-chan time.Time = nil + var ticker *time.Ticker = nil + + p.configLock.Lock() + p.log(fmt.Sprintf("Setting pingLoop with interval %d", p.config.pingInterval)) + if p.config.pingInterval > 0 { + ticker = time.NewTicker(time.Duration(p.config.pingInterval) * time.Second) + tickChan = ticker.C + + } + p.configLock.Unlock() + + run := true + for ;run; { + select { + case <- tickChan: + p.log(fmt.Sprintf("Sending ping")) + p.API.PublishWebSocketEvent("ping", map[string]interface{}{}, &model.WebsocketBroadcast{}) + case <- p.configChanged: + run = false + } + } + + if ticker != nil { + ticker.Stop() + } + } +} + +func main() { + plugin.ClientMain((&BathroomMonitorPlugin{}).init()) +} diff --git a/mattermost/plugin.json b/mattermost/plugin.json index de87152..9d291d5 100644 --- a/mattermost/plugin.json +++ b/mattermost/plugin.json @@ -5,20 +5,20 @@ "executable": "bathroom-linux-amd64" }, "webapp": { - "bundle_path": "main.js" + "bundle_path": "main.js" }, "settings_schema": { - "settings": [ - {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, - {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, - {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, - {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, - {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, - {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, - {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, - {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, - {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, - {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} - ] + "settings": [ + {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, + {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, + {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, + {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, + {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, + {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, + {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, + {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, + {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, + {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} + ] } } diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go deleted file mode 100644 index 44629cc..0000000 --- a/mattermost/server/bathroom.go +++ /dev/null @@ -1,829 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/model" - "github.com/pkg/errors" - "sync" - "reflect" - _ "github.com/google/go-cmp/cmp" - "os" - _ "github.com/fatih/structs" - "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" - "strings" - "regexp" - "strconv" - "time" - "github.com/fsnotify/fsnotify" - "io/ioutil" - "html" - "encoding/json" - "encoding/pem" - "crypto/rsa" - "crypto/x509" - "crypto/rand" - "crypto/sha1" - "path" - "encoding/base64" - _ "math" -) - -const POST_STATUS_TO_ADMIN = false -const DO_LOGGING = false - -var splitWhitespaceOrComma *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -var doorWatchFile *regexp.Regexp = regexp.MustCompile(`(^|/)door(\d+)$`) - -const ( - Unknown = uint(iota) - Open - Closed -) - -func statusName(status uint) (string, error) { - switch status { - case Unknown: - return "unknown", nil - case Open: - return "open", nil - case Closed: - return "closed", nil - } - - return "", errors.New(fmt.Sprintf("Invalid status %d", status)) -} - -type UserOptions struct { - ShowWidget bool `json:"show_widget"` -} - -func defaultUserOptions() *UserOptions { - return &UserOptions { - ShowWidget: true, - } -} - -type DoorRequest struct { - ip string - time int64 - verify string - status uint -} - -type Door struct { - id uint8 - status uint - pubKey *rsa.PublicKey - lastRequest *DoorRequest -} - -type Config struct { - NumDoors string - numDoors uint8 - - PingInterval string - pingInterval int - - WatchPath string - - AdminUsers string - adminUsers []*model.User - - KeyPath string - - DoorNames string - doorNames []string - - InfoIcon string - UnknownIcons string - OpenIcons string - ClosedIcons string - - unknownIcons []string - openIcons []string - closedIcons []string - - DoorOrder string - doorOrder []uint8 - - settingsJson map[string]interface{} -} - -type BathroomMonitorPlugin struct { - plugin.MattermostPlugin - - config *Config - configLock sync.RWMutex - configChanged chan struct{} - configUpdates int - - doorLock sync.RWMutex - doors map[uint8]*Door - - bot *string -} - -func (p *BathroomMonitorPlugin) getKeyFile(id uint8) (*rsa.PublicKey, error) { - keyFile, err := ioutil.ReadFile(path.Join(p.config.KeyPath, fmt.Sprintf("public%d.pem", id))) - if err != nil { - return nil, err - } - - data, _ := pem.Decode(keyFile) - if data == nil { - return nil, errors.New("No PEM formatted data in file") - } - - key, err := x509.ParsePKIXPublicKey(data.Bytes) - if err != nil { - return nil, err - } - - switch pub := key.(type) { - case *rsa.PublicKey: - return pub, nil - } - - return nil, errors.New(fmt.Sprintf("Keyfile wasn't a public RSA PKIX key, it was %T", key)) -} - -func (p *BathroomMonitorPlugin) initDoors() { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - p.doors = make(map[uint8]*Door) - for i := uint8(0); i < p.config.numDoors; i++ { - id := i + 1 - var pub *rsa.PublicKey = nil - if !USE_FSNOTIFY { - var err error - pub, err = p.getKeyFile(id) - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Unable to load key file for id %d", id)).Error()) - pub = nil - } - } - p.doors[i] = &Door { - id: id, - status: Unknown, - pubKey: pub, - } - } -} - -func (p *BathroomMonitorPlugin) log(log string) { - if DO_LOGGING { - p.API.LogInfo(log) - } -} - -func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error { - if id < 1 || id > p.config.numDoors { - return errors.New(fmt.Sprintf("Invalid door id %d", id)) - } - - if status < Unknown || status > Closed { - return errors.New(fmt.Sprintf("Invalid door status %d", status)) - } - - if p.doors == nil || len(p.doors) < int(p.config.numDoors) { - return errors.New("Doors not inited yet") - } - - if p.doors[id - 1].status != status { - p.doors[id - 1].status = status - if report { - statusStr, _ := statusName(status) - _ = statusStr - if POST_STATUS_TO_ADMIN { - p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr)) - } - p.sendUpdate() - go p.resendUpdate(10 * time.Second) - } - } else { - p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status)) - } - - return nil -} - -func (p *BathroomMonitorPlugin) sendUpdate() { - p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{}) -} - -func (p *BathroomMonitorPlugin) resendUpdate(wait time.Duration) { - timer := time.NewTimer(wait) - <-timer.C - p.sendUpdate() -} - - -func (p *BathroomMonitorPlugin) init() *BathroomMonitorPlugin { - p.config = &Config { - NumDoors: "1", - numDoors: 1, - - WatchPath: "./", - - KeyPath: "./", - - AdminUsers: "", - } - - p.configChanged = make(chan struct{}) - - return p; -} - -func (p *BathroomMonitorPlugin) validateRequestDoorId(r *http.Request) (uint8, error) { - p.configLock.Lock() - numDoors := p.config.numDoors - p.configLock.Unlock() - - - doorStr, ok := r.Form["door_id"] - if !ok || len(doorStr) == 0 { - return 0, errors.New("Please send door id") - } - - doorId, err := strconv.ParseUint(doorStr[0], 10, 8) - if err != nil { - return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error())) - } - doorId8 := uint8(doorId) - - if doorId8 < 1 || doorId8 > numDoors { - return 0, errors.New("Invalid door id") - } - - return doorId8, nil -} - -func SliceIndex(limit int, predicate func(i int) bool) int { - for i := 0; i < limit; i++ { - if predicate(i) { - return i - } - } - return -1 -} - -func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { - auth, authOk := r.Header["Mattermost-User-Id"] - var authUser string = "" - if authOk && len(auth) > 0 { - authUser = auth[0] - } - p.log(fmt.Sprintf("Requested path: %s %s AUTH: %s (%t)", r.URL.Path, c.IpAddress, authUser, authOk)) - if r.URL.Path == "/settings" { - p.configLock.Lock() - defer p.configLock.Unlock() - - retMap := make(map[string]interface{}) - for k, v := range p.config.settingsJson { - retMap[k] = v - } - - userOptions := defaultUserOptions() - - if authOk { - p.Helpers.KVGetJSON("settings_" + authUser, userOptions) - } - - retMap["user"] = userOptions - - bytes, err := json.Marshal(retMap) - if err == nil { - fmt.Fprint(w, string(bytes)) - } else { - p.API.LogError(fmt.Sprintf("Unable to json Marshal settings %s", err.Error())) - } - - return - } - if r.URL.Path == "/status" { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - var write map[string]string = make(map[string]string) - for d, s := range p.doors { - statusStr, _ := statusName(s.status) - write[fmt.Sprintf("%d", d + 1)] = statusStr - } - if output, err := json.Marshal(write); err == nil { - fmt.Fprint(w, string(output)) - } else { - fmt.Fprint(w, html.EscapeString(err.Error())) - } - return - } - if r.URL.Path == "/admin-update-status" && authOk { - r.ParseForm() - p.configLock.Lock() - p.doorLock.Lock() - defer p.doorLock.Unlock() - defer p.configLock.Unlock() - - if SliceIndex(len(p.config.adminUsers), func(i int) bool { return p.config.adminUsers[i].Id == authUser }) != -1 { - for n, vs := range r.Form { - matches := doorWatchFile.FindStringSubmatch(n) - if matches != nil && len(matches) >= 3 { - doorId64, err := strconv.ParseUint(matches[2], 10, 8) - doorId := uint8(doorId64) - if err == nil && doorId >= 1 && doorId <= p.config.numDoors && len(vs) > 0 { - status64, err := strconv.ParseUint(vs[0], 10, 8) - status := uint(status64) - if err == nil && status >= Unknown && status <= Closed { - p.setDoorStatus(uint8(doorId), status, true) - } else { - fmt.Fprintf(w, "Not a valid status: %d", status) - } - } else { - fmt.Fprintf(w, "Not a valid door: %# v, %d, %# v", matches, doorId, pretty.Formatter(vs)) - } - } else { - fmt.Fprintf(w, "Not a valid door key: %s", n) - } - } - } else { - fmt.Fprintf(w, "Not authorized: %s", authUser) - } - return - } - if r.URL.Path == "/status-update" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - statusStr, ok := r.Form["status"] - if !ok || len(statusStr) == 0 { - fmt.Fprint(w, "Please send door status") - return - } - - status, err := strconv.ParseUint(statusStr[0], 10, 8) - if err != nil { - fmt.Fprintf(w, "Couldn't parse status: %s", err.Error()) - return - } - - if status != 0 && status != 1 { - fmt.Fprint(w, "Invalid status") - return - } - - statusVal := Open - if status == 1 { - statusVal = Closed - } - - p.doorLock.Lock() - defer p.doorLock.Unlock() - - if p.doors[doorId8 - 1].pubKey == nil { - fmt.Fprintf(w, "No public key found for door %d", doorId8) - return - } - - p.log("Getting random bytes") - var verifyBytes [50]byte - rand.Read(verifyBytes[:]) - p.log("Encoding random bytes") - - verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:]) - - p.log("Encryping random bytes") - encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{}) - if err != nil { - fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error()) - return - } - - p.log("Encoding encrypted bytes") - encryptedB64 := base64.StdEncoding.EncodeToString(encrypted) - - - req := &DoorRequest { - ip:c.IpAddress, - time:time.Now().Unix(), - verify:verifyB64, - status: statusVal, - } - - p.doors[doorId8 - 1].lastRequest = req - - p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify)) - - fmt.Fprint(w, encryptedB64) - - - return - } - if r.URL.Path == "/verify-status" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - verifyB64, ok := r.Form["verify"] - if !ok || len(verifyB64) <= 0 { - fmt.Fprint(w, "Please send the verification code") - return - } - - p.log("Verify locking") - p.doorLock.Lock() - defer p.doorLock.Unlock() - p.log("Verify done locking") - - if p.doors[doorId8 - 1].lastRequest == nil { - fmt.Fprint(w, "Invalid request") - return - } - - req := p.doors[doorId8 - 1].lastRequest - if req.ip != c.IpAddress { - fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr) - return - } - - diff := time.Now().Unix() - req.time - if diff < 0 || diff > 10 { - fmt.Fprint(w, "Request expired") - return - } - - - if req.verify != verifyB64[0] { - p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0])) - fmt.Fprint(w, "Unauthorized request") - return - } - - - p.doors[doorId8 - 1].lastRequest = nil - - p.log("Changing") - err = p.setDoorStatus(doorId8, req.status, true) - p.log("Changed") - if err != nil { - fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error()) - } - - return - - } - http.NotFound(w, r) -} - -func (p *BathroomMonitorPlugin) confChangedEvent() { - select { - case p.configChanged <- struct{}{}: - default: - } -} - -func (p *BathroomMonitorPlugin) postAdminChannel(text string) { - p.API.LogError(text) - - if p.bot != nil { - for _, u := range(p.config.adminUsers) { - channel, err := p.API.GetDirectChannel(*p.bot, u.Id); - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't get direct channel to user %s", u.Username)).Error()) - continue - } - p.API.CreatePost(&model.Post{UserId: *p.bot, ChannelId:channel.Id, Message:text, MessageSource:text}) - } - } -} - -func (p *BathroomMonitorPlugin) OnConfigurationChange() error { - p.configLock.Lock() - defer p.configLock.Unlock() - - var newConfig *Config = new(Config); - if err := p.API.LoadPluginConfiguration(newConfig); err != nil { - newErr := errors.Wrap(err, fmt.Sprintf("%d: Failed to load configuration", p.configUpdates)) - p.postAdminChannel(newErr.Error()) - return newErr - } - - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { - p.API.LogInfo("Passed same config, or empty?") - return nil; - } - - var configErr error = nil - - - if USE_FSNOTIFY { - if info, err := os.Stat(newConfig.WatchPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) - } - } else { - if info, err := os.Stat(newConfig.KeyPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.KeyPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid key path")) - } - } - - - numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) - if err != nil { - newConfig.NumDoors = "1" - newConfig.numDoors = 1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) - } else { - newConfig.numDoors = uint8(numDoors) - } - - if strings.Trim(newConfig.PingInterval, " \n\t") == "" { - newConfig.pingInterval = -1 - } else { - pingInterval, err := strconv.ParseInt(newConfig.PingInterval, 10, 32) - if err != nil { - newConfig.PingInterval = "" - newConfig.pingInterval = -1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid ping interval")) - } else { - newConfig.pingInterval = int(pingInterval) - } - } - - newConfig.adminUsers = make([]*model.User, 0, 4) - split := splitWhitespaceOrComma.Split(newConfig.AdminUsers, -1) - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - u, _ := p.API.GetUserByUsername(trimmed) - if u != nil { - newConfig.adminUsers = append(newConfig.adminUsers, u) - } - } - } - - newConfig.doorNames = make([]string, 0, 4) - split = strings.Split(newConfig.DoorNames, "|") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.doorNames = append(newConfig.doorNames, trimmed) - } - } - - newConfig.unknownIcons = make([]string, 0, 4) - split = strings.Split(newConfig.UnknownIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.unknownIcons = append(newConfig.unknownIcons, trimmed) - } - } - - newConfig.openIcons = make([]string, 0, 4) - split = strings.Split(newConfig.OpenIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.openIcons = append(newConfig.openIcons, trimmed) - } - } - - newConfig.closedIcons = make([]string, 0, 4) - split = strings.Split(newConfig.ClosedIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.closedIcons = append(newConfig.closedIcons, trimmed) - } - } - - doorOrder := make([]uint8, 0, 4) - split = strings.Split(newConfig.DoorOrder, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - id, err := strconv.ParseUint(trimmed, 10, 8) - if err != nil { - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid door order")) - doorOrder = nil - break - } - doorOrder = append(doorOrder, uint8(id)) - } - } - newConfig.doorOrder = doorOrder - - min := func(a uint8, b uint8) uint8 { - if a < b { - return a - } - return b - } - - doorsJson := make(map[string]string) - for i := uint8(0); i < min(uint8(len(newConfig.doorNames)), newConfig.numDoors); i++ { - doorsJson[fmt.Sprintf("%d", i + 1)] = newConfig.doorNames[i] - } - - doorOrderJson := make([]int, len(newConfig.doorOrder)) - for i, o := range newConfig.doorOrder { - doorOrderJson[i] = int(o) - } - newConfig.settingsJson = map[string]interface{} { - "info_icon": newConfig.InfoIcon, - "unknown_icons": newConfig.unknownIcons, - "open_icons": newConfig.openIcons, - "closed_icons": newConfig.closedIcons, - "doors": doorsJson, - "door_order": doorOrderJson, - } - - p.config = newConfig - p.configUpdates++ - - p.initDoors() - - p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath)) - - if configErr != nil { - p.postAdminChannel(configErr.Error()) - } - - p.confChangedEvent() - - return configErr -} - -func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { - for { - p.configLock.Lock() - numDoors := p.config.numDoors - watchPath := p.config.WatchPath - p.configLock.Unlock() - - _ = numDoors - - watcher, err := fsnotify.NewWatcher() - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't make watcher").Error()) - time.Sleep(60) - continue - } - - err = watcher.Add(watchPath) - - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't watch WatchPath").Error()) - } - - run := true - for ;run; { - select { - case event, ok := <- watcher.Events: - if (!ok) { - p.API.LogError(errors.Wrap(err, "Couldn't get fsnotify event").Error()) - time.Sleep(60) - run = false - } else { - match := doorWatchFile.FindStringSubmatch(event.Name) - if match == nil { - continue - } - - id64, err := strconv.ParseUint(match[2], 10, 8) - if err != nil { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - continue - } - - id := uint8(id64) - if id < 1 || id > numDoors { - continue - } - - if event.Op & fsnotify.Write != fsnotify.Write && event.Op & fsnotify.Create != fsnotify.Create { - continue - } - - statusBytes, err := ioutil.ReadFile(event.Name) - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - - status := strings.Trim(string(statusBytes), " \n\t") - if len(status) <= 0 { - continue - } - - statusInt, err := strconv.ParseUint(status, 10, 32) - if err != nil { - p.API.LogError(errors.Wrap(err, "Status invalid " + status).Error()) - continue - } - - newStatus := Open - if statusInt == 1 { - newStatus = Closed - } - - p.doorLock.Lock() - err = p.setDoorStatus(id, newStatus, true) - p.doorLock.Unlock() - - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't set door status %d %d", id, statusInt)).Error()) - continue - } - } - case <- p.configChanged: - p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates)) - run = false - } - } - - watcher.Close() - } -} - -func (p *BathroomMonitorPlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { - userOptions := defaultUserOptions() - _ = userOptions - - return nil, nil -} - -func (p *BathroomMonitorPlugin) OnActivate() error { - p.API.RegisterCommand(&model.Command { - Trigger: "bathrooms", - AutoComplete: true, - AutoCompleteDesc: "Turns the bathroom widget \"on\" or \"off\" (\"status\" or no option prints bathroom statuses)", - AutoCompleteHint: "(on|off|status)", - }) - if p.bot == nil { - botUser, err := p.Helpers.EnsureBot(&model.Bot{Username:"bathroom-bot", DisplayName:"Bathroom Bot", Description:"Tracks Bathroom Status"}); - if err == nil { - p.bot = &botUser - } else { - p.bot = nil - } - } - if USE_FSNOTIFY { - go fileNotifyBasedLoop(p) - } else { - } - - go p.pingLoop(); - - return nil -} - -func (p *BathroomMonitorPlugin) pingLoop() { - for { - - var tickChan <-chan time.Time = nil - var ticker *time.Ticker = nil - - p.configLock.Lock() - p.log(fmt.Sprintf("Setting pingLoop with interval %d", p.config.pingInterval)) - if p.config.pingInterval > 0 { - ticker = time.NewTicker(time.Duration(p.config.pingInterval) * time.Second) - tickChan = ticker.C - - } - p.configLock.Unlock() - - run := true - for ;run; { - select { - case <- tickChan: - p.log(fmt.Sprintf("Sending ping")) - p.API.PublishWebSocketEvent("ping", map[string]interface{}{}, &model.WebsocketBroadcast{}) - case <- p.configChanged: - run = false - } - } - - if ticker != nil { - ticker.Stop() - } - } -} - -func main() { - plugin.ClientMain((&BathroomMonitorPlugin{}).init()) -} diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh index c2c1a0e..1623a54 100755 --- a/mattermost/server/build.sh +++ b/mattermost/server/build.sh @@ -1,4 +1,7 @@ #! /usr/bin/env bash -filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json -GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES +printf -v FORMATTED_TAGS '%s,' "$@" +FORMATTED_TAGS=${FORMATTED_TAGS%,} + +gcc -P -E $( [[ -n "$DEFS" ]] && echo -D$DEFS ) - - < ../plugin_templ.json > ../plugin.json +GOOS=linux GOARCH=amd64 go build $( [[ -n "$FORMATTED_TAGS" ]] && echo -tags $FORMATTED_TAGS ) -o bathroom-linux-amd64 bathroom-plugin/main diff --git a/mattermost/server/build_web.sh b/mattermost/server/build_web.sh deleted file mode 100755 index 7ccb125..0000000 --- a/mattermost/server/build_web.sh +++ /dev/null @@ -1,3 +0,0 @@ -#! /usr/bin/env bash - -FILES=config_web.go ./build.sh diff --git a/mattermost/server/config_fs.go b/mattermost/server/config_fs.go deleted file mode 100644 index 6384df5..0000000 --- a/mattermost/server/config_fs.go +++ /dev/null @@ -1,3 +0,0 @@ -package main - -const USE_FSNOTIFY = true diff --git a/mattermost/server/config_web.go b/mattermost/server/config_web.go deleted file mode 100644 index c72b5c8..0000000 --- a/mattermost/server/config_web.go +++ /dev/null @@ -1,3 +0,0 @@ -package main - -const USE_FSNOTIFY = false diff --git a/mattermost/server/go.mod b/mattermost/server/go.mod new file mode 100644 index 0000000..779de8d --- /dev/null +++ b/mattermost/server/go.mod @@ -0,0 +1,54 @@ +module bathroom-plugin + +go 1.23.3 + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.6.2 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect + github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect + github.com/mattermost/logr/v2 v2.0.21 // indirect + github.com/mattermost/mattermost/server/public v0.1.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/pborman/uuid v1.2.1 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/tinylib/msgp v1.2.4 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/wiggin77/merror v1.0.5 // indirect + github.com/wiggin77/srslog v1.0.1 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f // indirect + google.golang.org/grpc v1.68.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/mattermost/server/go.sum b/mattermost/server/go.sum new file mode 100755 index 0000000..2fe6112 --- /dev/null +++ b/mattermost/server/go.sum @@ -0,0 +1,291 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a h1:etIrTD8BQqzColk9nKRusM9um5+1q0iOEJLqfBMIK64= +github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a/go.mod h1:emQhSYTXqB0xxjLITTw4EaWZ+8IIQYw+kx9GqNUKdLg= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= +github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= +github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8= +github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34= +github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb2BTCsOdamENjjWCI6qmfHLbk6OZI= +github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956/go.mod h1:SRl30Lb7/QoYyohYeVBuqYvvmXSZJxZgiV3Zf6VbxjI= +github.com/mattermost/logr/v2 v2.0.21 h1:CMHsP+nrbRlEC4g7BwOk1GAnMtHkniFhlSQPXy52be4= +github.com/mattermost/logr/v2 v2.0.21/go.mod h1:kZkB/zqKL9e+RY5gB3vGpsyenC+TpuiOenjMkvJJbzc= +github.com/mattermost/mattermost v10.2.0+incompatible h1:Jo/tr/282u/ixQPUbCV6BcEVgSxxcxUeIgWMpT+3zbQ= +github.com/mattermost/mattermost v10.2.0+incompatible/go.mod h1:JUnq0RiZa+eUafKS5M47SVRdPIJyIITxTcCFvoPlFlk= +github.com/mattermost/mattermost-server v10.2.0+incompatible h1:RhG3b+xCdGUnHyWVJxYWuBzDdgBrJp1LHPJjBEmu48I= +github.com/mattermost/mattermost-server v10.2.0+incompatible/go.mod h1:5L6MjAec+XXQwMIt791Ganu45GKsSiM+I0tLR9wUj8Y= +github.com/mattermost/mattermost/server/public v0.1.9 h1:l/OKPRVuFeqL0yqRVC/JpveG5sLNKcT9llxqMkO9e+s= +github.com/mattermost/mattermost/server/public v0.1.9/go.mod h1:SkTKbMul91Rq0v2dIxe8mqzUOY+3KwlwwLmAlxDfGCk= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tinylib/msgp v1.2.4 h1:yLFeUGostXXSGW5vxfT5dXG/qzkn4schv2I7at5+hVU= +github.com/tinylib/msgp v1.2.4/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wiggin77/merror v1.0.5 h1:P+lzicsn4vPMycAf2mFf7Zk6G9eco5N+jB1qJ2XW3ME= +github.com/wiggin77/merror v1.0.5/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0= +github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8= +github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f h1:C1QccEa9kUwvMgEUORqQD9S17QesQijxjZ84sO82mfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/mattermost/server/main/bathroom.go b/mattermost/server/main/bathroom.go new file mode 100644 index 0000000..feb4837 --- /dev/null +++ b/mattermost/server/main/bathroom.go @@ -0,0 +1,829 @@ +package main + +import ( + "fmt" + "net/http" + "github.com/mattermost/mattermost/server/public/plugin" + "github.com/mattermost/mattermost/server/public/model" + "github.com/pkg/errors" + "sync" + "reflect" + _ "github.com/google/go-cmp/cmp" + "os" + _ "github.com/fatih/structs" + "github.com/hashicorp/go-multierror" + "github.com/kr/pretty" + "strings" + "regexp" + "strconv" + "time" + "github.com/fsnotify/fsnotify" + "io/ioutil" + "html" + "encoding/json" + "encoding/pem" + "crypto/rsa" + "crypto/x509" + "crypto/rand" + "crypto/sha1" + "path" + "encoding/base64" + _ "math" +) + +const POST_STATUS_TO_ADMIN = false +const DO_LOGGING = false + +var splitWhitespaceOrComma *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) +var doorWatchFile *regexp.Regexp = regexp.MustCompile(`(^|/)door(\d+)$`) + +const ( + Unknown = uint(iota) + Open + Closed +) + +func statusName(status uint) (string, error) { + switch status { + case Unknown: + return "unknown", nil + case Open: + return "open", nil + case Closed: + return "closed", nil + } + + return "", errors.New(fmt.Sprintf("Invalid status %d", status)) +} + +type UserOptions struct { + ShowWidget bool `json:"show_widget"` +} + +func defaultUserOptions() *UserOptions { + return &UserOptions { + ShowWidget: true, + } +} + +type DoorRequest struct { + ip string + time int64 + verify string + status uint +} + +type Door struct { + id uint8 + status uint + pubKey *rsa.PublicKey + lastRequest *DoorRequest +} + +type Config struct { + NumDoors string + numDoors uint8 + + PingInterval string + pingInterval int + + WatchPath string + + AdminUsers string + adminUsers []*model.User + + KeyPath string + + DoorNames string + doorNames []string + + InfoIcon string + UnknownIcons string + OpenIcons string + ClosedIcons string + + unknownIcons []string + openIcons []string + closedIcons []string + + DoorOrder string + doorOrder []uint8 + + settingsJson map[string]interface{} +} + +type BathroomMonitorPlugin struct { + plugin.MattermostPlugin + + config *Config + configLock sync.RWMutex + configChanged chan struct{} + configUpdates int + + doorLock sync.RWMutex + doors map[uint8]*Door + + bot *string +} + +func (p *BathroomMonitorPlugin) getKeyFile(id uint8) (*rsa.PublicKey, error) { + keyFile, err := ioutil.ReadFile(path.Join(p.config.KeyPath, fmt.Sprintf("public%d.pem", id))) + if err != nil { + return nil, err + } + + data, _ := pem.Decode(keyFile) + if data == nil { + return nil, errors.New("No PEM formatted data in file") + } + + key, err := x509.ParsePKIXPublicKey(data.Bytes) + if err != nil { + return nil, err + } + + switch pub := key.(type) { + case *rsa.PublicKey: + return pub, nil + } + + return nil, errors.New(fmt.Sprintf("Keyfile wasn't a public RSA PKIX key, it was %T", key)) +} + +func (p *BathroomMonitorPlugin) initDoors() { + p.doorLock.Lock() + defer p.doorLock.Unlock() + + p.doors = make(map[uint8]*Door) + for i := uint8(0); i < p.config.numDoors; i++ { + id := i + 1 + var pub *rsa.PublicKey = nil + if !USE_FSNOTIFY { + var err error + pub, err = p.getKeyFile(id) + if err != nil { + p.API.LogError(errors.Wrap(err, fmt.Sprintf("Unable to load key file for id %d", id)).Error()) + pub = nil + } + } + p.doors[i] = &Door { + id: id, + status: Unknown, + pubKey: pub, + } + } +} + +func (p *BathroomMonitorPlugin) log(log string) { + if DO_LOGGING { + p.API.LogInfo(log) + } +} + +func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error { + if id < 1 || id > p.config.numDoors { + return errors.New(fmt.Sprintf("Invalid door id %d", id)) + } + + if status < Unknown || status > Closed { + return errors.New(fmt.Sprintf("Invalid door status %d", status)) + } + + if p.doors == nil || len(p.doors) < int(p.config.numDoors) { + return errors.New("Doors not inited yet") + } + + if p.doors[id - 1].status != status { + p.doors[id - 1].status = status + if report { + statusStr, _ := statusName(status) + _ = statusStr + if POST_STATUS_TO_ADMIN { + p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr)) + } + p.sendUpdate() + go p.resendUpdate(10 * time.Second) + } + } else { + p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status)) + } + + return nil +} + +func (p *BathroomMonitorPlugin) sendUpdate() { + p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{}) +} + +func (p *BathroomMonitorPlugin) resendUpdate(wait time.Duration) { + timer := time.NewTimer(wait) + <-timer.C + p.sendUpdate() +} + + +func (p *BathroomMonitorPlugin) init() *BathroomMonitorPlugin { + p.config = &Config { + NumDoors: "1", + numDoors: 1, + + WatchPath: "./", + + KeyPath: "./", + + AdminUsers: "", + } + + p.configChanged = make(chan struct{}) + + return p; +} + +func (p *BathroomMonitorPlugin) validateRequestDoorId(r *http.Request) (uint8, error) { + p.configLock.Lock() + numDoors := p.config.numDoors + p.configLock.Unlock() + + + doorStr, ok := r.Form["door_id"] + if !ok || len(doorStr) == 0 { + return 0, errors.New("Please send door id") + } + + doorId, err := strconv.ParseUint(doorStr[0], 10, 8) + if err != nil { + return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error())) + } + doorId8 := uint8(doorId) + + if doorId8 < 1 || doorId8 > numDoors { + return 0, errors.New("Invalid door id") + } + + return doorId8, nil +} + +func SliceIndex(limit int, predicate func(i int) bool) int { + for i := 0; i < limit; i++ { + if predicate(i) { + return i + } + } + return -1 +} + +func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { + auth, authOk := r.Header["Mattermost-User-Id"] + var authUser string = "" + if authOk && len(auth) > 0 { + authUser = auth[0] + } + p.log(fmt.Sprintf("Requested path: %s %s AUTH: %s (%t)", r.URL.Path, c.IpAddress, authUser, authOk)) + if r.URL.Path == "/settings" { + p.configLock.Lock() + defer p.configLock.Unlock() + + retMap := make(map[string]interface{}) + for k, v := range p.config.settingsJson { + retMap[k] = v + } + + userOptions := defaultUserOptions() + + if authOk { + p.Helpers.KVGetJSON("settings_" + authUser, userOptions) + } + + retMap["user"] = userOptions + + bytes, err := json.Marshal(retMap) + if err == nil { + fmt.Fprint(w, string(bytes)) + } else { + p.API.LogError(fmt.Sprintf("Unable to json Marshal settings %s", err.Error())) + } + + return + } + if r.URL.Path == "/status" { + p.doorLock.Lock() + defer p.doorLock.Unlock() + + var write map[string]string = make(map[string]string) + for d, s := range p.doors { + statusStr, _ := statusName(s.status) + write[fmt.Sprintf("%d", d + 1)] = statusStr + } + if output, err := json.Marshal(write); err == nil { + fmt.Fprint(w, string(output)) + } else { + fmt.Fprint(w, html.EscapeString(err.Error())) + } + return + } + if r.URL.Path == "/admin-update-status" && authOk { + r.ParseForm() + p.configLock.Lock() + p.doorLock.Lock() + defer p.doorLock.Unlock() + defer p.configLock.Unlock() + + if SliceIndex(len(p.config.adminUsers), func(i int) bool { return p.config.adminUsers[i].Id == authUser }) != -1 { + for n, vs := range r.Form { + matches := doorWatchFile.FindStringSubmatch(n) + if matches != nil && len(matches) >= 3 { + doorId64, err := strconv.ParseUint(matches[2], 10, 8) + doorId := uint8(doorId64) + if err == nil && doorId >= 1 && doorId <= p.config.numDoors && len(vs) > 0 { + status64, err := strconv.ParseUint(vs[0], 10, 8) + status := uint(status64) + if err == nil && status >= Unknown && status <= Closed { + p.setDoorStatus(uint8(doorId), status, true) + } else { + fmt.Fprintf(w, "Not a valid status: %d", status) + } + } else { + fmt.Fprintf(w, "Not a valid door: %# v, %d, %# v", matches, doorId, pretty.Formatter(vs)) + } + } else { + fmt.Fprintf(w, "Not a valid door key: %s", n) + } + } + } else { + fmt.Fprintf(w, "Not authorized: %s", authUser) + } + return + } + if r.URL.Path == "/status-update" { + r.ParseForm() + p.log("Contacted by " + r.RemoteAddr) + + doorId8, err := p.validateRequestDoorId(r) + if err != nil { + fmt.Fprint(w, err.Error()) + return + } + + statusStr, ok := r.Form["status"] + if !ok || len(statusStr) == 0 { + fmt.Fprint(w, "Please send door status") + return + } + + status, err := strconv.ParseUint(statusStr[0], 10, 8) + if err != nil { + fmt.Fprintf(w, "Couldn't parse status: %s", err.Error()) + return + } + + if status != 0 && status != 1 { + fmt.Fprint(w, "Invalid status") + return + } + + statusVal := Open + if status == 1 { + statusVal = Closed + } + + p.doorLock.Lock() + defer p.doorLock.Unlock() + + if p.doors[doorId8 - 1].pubKey == nil { + fmt.Fprintf(w, "No public key found for door %d", doorId8) + return + } + + p.log("Getting random bytes") + var verifyBytes [50]byte + rand.Read(verifyBytes[:]) + p.log("Encoding random bytes") + + verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:]) + + p.log("Encryping random bytes") + encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{}) + if err != nil { + fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error()) + return + } + + p.log("Encoding encrypted bytes") + encryptedB64 := base64.StdEncoding.EncodeToString(encrypted) + + + req := &DoorRequest { + ip:c.IpAddress, + time:time.Now().Unix(), + verify:verifyB64, + status: statusVal, + } + + p.doors[doorId8 - 1].lastRequest = req + + p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify)) + + fmt.Fprint(w, encryptedB64) + + + return + } + if r.URL.Path == "/verify-status" { + r.ParseForm() + p.log("Contacted by " + r.RemoteAddr) + doorId8, err := p.validateRequestDoorId(r) + if err != nil { + fmt.Fprint(w, err.Error()) + return + } + + verifyB64, ok := r.Form["verify"] + if !ok || len(verifyB64) <= 0 { + fmt.Fprint(w, "Please send the verification code") + return + } + + p.log("Verify locking") + p.doorLock.Lock() + defer p.doorLock.Unlock() + p.log("Verify done locking") + + if p.doors[doorId8 - 1].lastRequest == nil { + fmt.Fprint(w, "Invalid request") + return + } + + req := p.doors[doorId8 - 1].lastRequest + if req.ip != c.IpAddress { + fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr) + return + } + + diff := time.Now().Unix() - req.time + if diff < 0 || diff > 10 { + fmt.Fprint(w, "Request expired") + return + } + + + if req.verify != verifyB64[0] { + p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0])) + fmt.Fprint(w, "Unauthorized request") + return + } + + + p.doors[doorId8 - 1].lastRequest = nil + + p.log("Changing") + err = p.setDoorStatus(doorId8, req.status, true) + p.log("Changed") + if err != nil { + fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error()) + } + + return + + } + http.NotFound(w, r) +} + +func (p *BathroomMonitorPlugin) confChangedEvent() { + select { + case p.configChanged <- struct{}{}: + default: + } +} + +func (p *BathroomMonitorPlugin) postAdminChannel(text string) { + p.API.LogError(text) + + if p.bot != nil { + for _, u := range(p.config.adminUsers) { + channel, err := p.API.GetDirectChannel(*p.bot, u.Id); + if err != nil { + p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't get direct channel to user %s", u.Username)).Error()) + continue + } + p.API.CreatePost(&model.Post{UserId: *p.bot, ChannelId:channel.Id, Message:text, MessageSource:text}) + } + } +} + +func (p *BathroomMonitorPlugin) OnConfigurationChange() error { + p.configLock.Lock() + defer p.configLock.Unlock() + + var newConfig *Config = new(Config); + if err := p.API.LoadPluginConfiguration(newConfig); err != nil { + newErr := errors.Wrap(err, fmt.Sprintf("%d: Failed to load configuration", p.configUpdates)) + p.postAdminChannel(newErr.Error()) + return newErr + } + + if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { + p.API.LogInfo("Passed same config, or empty?") + return nil; + } + + var configErr error = nil + + + if USE_FSNOTIFY { + if info, err := os.Stat(newConfig.WatchPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { + newConfig.WatchPath = "./" + configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) + } + } else { + if info, err := os.Stat(newConfig.KeyPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { + newConfig.KeyPath = "./" + configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid key path")) + } + } + + + numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) + if err != nil { + newConfig.NumDoors = "1" + newConfig.numDoors = 1 + configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) + } else { + newConfig.numDoors = uint8(numDoors) + } + + if strings.Trim(newConfig.PingInterval, " \n\t") == "" { + newConfig.pingInterval = -1 + } else { + pingInterval, err := strconv.ParseInt(newConfig.PingInterval, 10, 32) + if err != nil { + newConfig.PingInterval = "" + newConfig.pingInterval = -1 + configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid ping interval")) + } else { + newConfig.pingInterval = int(pingInterval) + } + } + + newConfig.adminUsers = make([]*model.User, 0, 4) + split := splitWhitespaceOrComma.Split(newConfig.AdminUsers, -1) + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + u, _ := p.API.GetUserByUsername(trimmed) + if u != nil { + newConfig.adminUsers = append(newConfig.adminUsers, u) + } + } + } + + newConfig.doorNames = make([]string, 0, 4) + split = strings.Split(newConfig.DoorNames, "|") + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + newConfig.doorNames = append(newConfig.doorNames, trimmed) + } + } + + newConfig.unknownIcons = make([]string, 0, 4) + split = strings.Split(newConfig.UnknownIcons, ",") + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + newConfig.unknownIcons = append(newConfig.unknownIcons, trimmed) + } + } + + newConfig.openIcons = make([]string, 0, 4) + split = strings.Split(newConfig.OpenIcons, ",") + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + newConfig.openIcons = append(newConfig.openIcons, trimmed) + } + } + + newConfig.closedIcons = make([]string, 0, 4) + split = strings.Split(newConfig.ClosedIcons, ",") + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + newConfig.closedIcons = append(newConfig.closedIcons, trimmed) + } + } + + doorOrder := make([]uint8, 0, 4) + split = strings.Split(newConfig.DoorOrder, ",") + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + id, err := strconv.ParseUint(trimmed, 10, 8) + if err != nil { + configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid door order")) + doorOrder = nil + break + } + doorOrder = append(doorOrder, uint8(id)) + } + } + newConfig.doorOrder = doorOrder + + min := func(a uint8, b uint8) uint8 { + if a < b { + return a + } + return b + } + + doorsJson := make(map[string]string) + for i := uint8(0); i < min(uint8(len(newConfig.doorNames)), newConfig.numDoors); i++ { + doorsJson[fmt.Sprintf("%d", i + 1)] = newConfig.doorNames[i] + } + + doorOrderJson := make([]int, len(newConfig.doorOrder)) + for i, o := range newConfig.doorOrder { + doorOrderJson[i] = int(o) + } + newConfig.settingsJson = map[string]interface{} { + "info_icon": newConfig.InfoIcon, + "unknown_icons": newConfig.unknownIcons, + "open_icons": newConfig.openIcons, + "closed_icons": newConfig.closedIcons, + "doors": doorsJson, + "door_order": doorOrderJson, + } + + p.config = newConfig + p.configUpdates++ + + p.initDoors() + + p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath)) + + if configErr != nil { + p.postAdminChannel(configErr.Error()) + } + + p.confChangedEvent() + + return configErr +} + +func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { + for { + p.configLock.Lock() + numDoors := p.config.numDoors + watchPath := p.config.WatchPath + p.configLock.Unlock() + + _ = numDoors + + watcher, err := fsnotify.NewWatcher() + if err != nil { + p.API.LogError(errors.Wrap(err, "Couldn't make watcher").Error()) + time.Sleep(60) + continue + } + + err = watcher.Add(watchPath) + + if err != nil { + p.API.LogError(errors.Wrap(err, "Couldn't watch WatchPath").Error()) + } + + run := true + for ;run; { + select { + case event, ok := <- watcher.Events: + if (!ok) { + p.API.LogError(errors.Wrap(err, "Couldn't get fsnotify event").Error()) + time.Sleep(60) + run = false + } else { + match := doorWatchFile.FindStringSubmatch(event.Name) + if match == nil { + continue + } + + id64, err := strconv.ParseUint(match[2], 10, 8) + if err != nil { + p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) + continue + } + + id := uint8(id64) + if id < 1 || id > numDoors { + continue + } + + if event.Op & fsnotify.Write != fsnotify.Write && event.Op & fsnotify.Create != fsnotify.Create { + continue + } + + statusBytes, err := ioutil.ReadFile(event.Name) + if err != nil { + p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) + } + + status := strings.Trim(string(statusBytes), " \n\t") + if len(status) <= 0 { + continue + } + + statusInt, err := strconv.ParseUint(status, 10, 32) + if err != nil { + p.API.LogError(errors.Wrap(err, "Status invalid " + status).Error()) + continue + } + + newStatus := Open + if statusInt == 1 { + newStatus = Closed + } + + p.doorLock.Lock() + err = p.setDoorStatus(id, newStatus, true) + p.doorLock.Unlock() + + if err != nil { + p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't set door status %d %d", id, statusInt)).Error()) + continue + } + } + case <- p.configChanged: + p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates)) + run = false + } + } + + watcher.Close() + } +} + +func (p *BathroomMonitorPlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { + userOptions := defaultUserOptions() + _ = userOptions + + return nil, nil +} + +func (p *BathroomMonitorPlugin) OnActivate() error { + p.API.RegisterCommand(&model.Command { + Trigger: "bathrooms", + AutoComplete: true, + AutoCompleteDesc: "Turns the bathroom widget \"on\" or \"off\" (\"status\" or no option prints bathroom statuses)", + AutoCompleteHint: "(on|off|status)", + }) + if p.bot == nil { + botUser, err := p.Helpers.EnsureBot(&model.Bot{Username:"bathroom-bot", DisplayName:"Bathroom Bot", Description:"Tracks Bathroom Status"}); + if err == nil { + p.bot = &botUser + } else { + p.bot = nil + } + } + if USE_FSNOTIFY { + go fileNotifyBasedLoop(p) + } else { + } + + go p.pingLoop(); + + return nil +} + +func (p *BathroomMonitorPlugin) pingLoop() { + for { + + var tickChan <-chan time.Time = nil + var ticker *time.Ticker = nil + + p.configLock.Lock() + p.log(fmt.Sprintf("Setting pingLoop with interval %d", p.config.pingInterval)) + if p.config.pingInterval > 0 { + ticker = time.NewTicker(time.Duration(p.config.pingInterval) * time.Second) + tickChan = ticker.C + + } + p.configLock.Unlock() + + run := true + for ;run; { + select { + case <- tickChan: + p.log(fmt.Sprintf("Sending ping")) + p.API.PublishWebSocketEvent("ping", map[string]interface{}{}, &model.WebsocketBroadcast{}) + case <- p.configChanged: + run = false + } + } + + if ticker != nil { + ticker.Stop() + } + } +} + +func main() { + plugin.ClientMain((&BathroomMonitorPlugin{}).init()) +} diff --git a/mattermost/server/main/config_fs.go b/mattermost/server/main/config_fs.go new file mode 100644 index 0000000..c20f92a --- /dev/null +++ b/mattermost/server/main/config_fs.go @@ -0,0 +1,5 @@ +//go:build fsnotify + +package main + +const USE_FSNOTIFY = true diff --git a/mattermost/plugin.json b/mattermost/plugin.json index de87152..9d291d5 100644 --- a/mattermost/plugin.json +++ b/mattermost/plugin.json @@ -5,20 +5,20 @@ "executable": "bathroom-linux-amd64" }, "webapp": { - "bundle_path": "main.js" + "bundle_path": "main.js" }, "settings_schema": { - "settings": [ - {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, - {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, - {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, - {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, - {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, - {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, - {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, - {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, - {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, - {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} - ] + "settings": [ + {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, + {"key":"KeyPath", "display_name":"Public key path", "type":"text", "default":"./", "help_text":"Path for public keys to server Pi requests"}, + {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"}, + {"key":"PingInterval", "display_name":"Ping interval", "type":"text", "default":"", "help_text":"Send a ping every X seconds to keep websockets alive through nginx (leave blank for none)"}, + {"key":"DoorNames", "display_name":"Door names", "type":"text", "default":"", "help_text":"|-separated list of names of the office doors"}, + {"key":"InfoIcons", "display_name":"Info icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display at the left before all the door icons, 1 per door (or specify 1 for all)"}, + {"key":"UnknownIcons", "display_name":"Unknown icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors without their status set yet, 1 per door (or specify 1 for all)"}, + {"key":"OpenIcons", "display_name":"Open icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are open, 1 per door (or specify 1 for all)"}, + {"key":"ClosedIcons", "display_name":"Closed icon url", "type":"text", "default":"", "help_text":"(Relative) URL of the icon to display for doors that are closed, 1 per door (or specify 1 for all)"}, + {"key":"DoorOrder", "display_name":"Door order", "type":"text", "default":"", "help_text":"Order of the door IDs for display in the widget (comma separated)"} + ] } } diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go deleted file mode 100644 index 44629cc..0000000 --- a/mattermost/server/bathroom.go +++ /dev/null @@ -1,829 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/model" - "github.com/pkg/errors" - "sync" - "reflect" - _ "github.com/google/go-cmp/cmp" - "os" - _ "github.com/fatih/structs" - "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" - "strings" - "regexp" - "strconv" - "time" - "github.com/fsnotify/fsnotify" - "io/ioutil" - "html" - "encoding/json" - "encoding/pem" - "crypto/rsa" - "crypto/x509" - "crypto/rand" - "crypto/sha1" - "path" - "encoding/base64" - _ "math" -) - -const POST_STATUS_TO_ADMIN = false -const DO_LOGGING = false - -var splitWhitespaceOrComma *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -var doorWatchFile *regexp.Regexp = regexp.MustCompile(`(^|/)door(\d+)$`) - -const ( - Unknown = uint(iota) - Open - Closed -) - -func statusName(status uint) (string, error) { - switch status { - case Unknown: - return "unknown", nil - case Open: - return "open", nil - case Closed: - return "closed", nil - } - - return "", errors.New(fmt.Sprintf("Invalid status %d", status)) -} - -type UserOptions struct { - ShowWidget bool `json:"show_widget"` -} - -func defaultUserOptions() *UserOptions { - return &UserOptions { - ShowWidget: true, - } -} - -type DoorRequest struct { - ip string - time int64 - verify string - status uint -} - -type Door struct { - id uint8 - status uint - pubKey *rsa.PublicKey - lastRequest *DoorRequest -} - -type Config struct { - NumDoors string - numDoors uint8 - - PingInterval string - pingInterval int - - WatchPath string - - AdminUsers string - adminUsers []*model.User - - KeyPath string - - DoorNames string - doorNames []string - - InfoIcon string - UnknownIcons string - OpenIcons string - ClosedIcons string - - unknownIcons []string - openIcons []string - closedIcons []string - - DoorOrder string - doorOrder []uint8 - - settingsJson map[string]interface{} -} - -type BathroomMonitorPlugin struct { - plugin.MattermostPlugin - - config *Config - configLock sync.RWMutex - configChanged chan struct{} - configUpdates int - - doorLock sync.RWMutex - doors map[uint8]*Door - - bot *string -} - -func (p *BathroomMonitorPlugin) getKeyFile(id uint8) (*rsa.PublicKey, error) { - keyFile, err := ioutil.ReadFile(path.Join(p.config.KeyPath, fmt.Sprintf("public%d.pem", id))) - if err != nil { - return nil, err - } - - data, _ := pem.Decode(keyFile) - if data == nil { - return nil, errors.New("No PEM formatted data in file") - } - - key, err := x509.ParsePKIXPublicKey(data.Bytes) - if err != nil { - return nil, err - } - - switch pub := key.(type) { - case *rsa.PublicKey: - return pub, nil - } - - return nil, errors.New(fmt.Sprintf("Keyfile wasn't a public RSA PKIX key, it was %T", key)) -} - -func (p *BathroomMonitorPlugin) initDoors() { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - p.doors = make(map[uint8]*Door) - for i := uint8(0); i < p.config.numDoors; i++ { - id := i + 1 - var pub *rsa.PublicKey = nil - if !USE_FSNOTIFY { - var err error - pub, err = p.getKeyFile(id) - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Unable to load key file for id %d", id)).Error()) - pub = nil - } - } - p.doors[i] = &Door { - id: id, - status: Unknown, - pubKey: pub, - } - } -} - -func (p *BathroomMonitorPlugin) log(log string) { - if DO_LOGGING { - p.API.LogInfo(log) - } -} - -func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error { - if id < 1 || id > p.config.numDoors { - return errors.New(fmt.Sprintf("Invalid door id %d", id)) - } - - if status < Unknown || status > Closed { - return errors.New(fmt.Sprintf("Invalid door status %d", status)) - } - - if p.doors == nil || len(p.doors) < int(p.config.numDoors) { - return errors.New("Doors not inited yet") - } - - if p.doors[id - 1].status != status { - p.doors[id - 1].status = status - if report { - statusStr, _ := statusName(status) - _ = statusStr - if POST_STATUS_TO_ADMIN { - p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr)) - } - p.sendUpdate() - go p.resendUpdate(10 * time.Second) - } - } else { - p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status)) - } - - return nil -} - -func (p *BathroomMonitorPlugin) sendUpdate() { - p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{}) -} - -func (p *BathroomMonitorPlugin) resendUpdate(wait time.Duration) { - timer := time.NewTimer(wait) - <-timer.C - p.sendUpdate() -} - - -func (p *BathroomMonitorPlugin) init() *BathroomMonitorPlugin { - p.config = &Config { - NumDoors: "1", - numDoors: 1, - - WatchPath: "./", - - KeyPath: "./", - - AdminUsers: "", - } - - p.configChanged = make(chan struct{}) - - return p; -} - -func (p *BathroomMonitorPlugin) validateRequestDoorId(r *http.Request) (uint8, error) { - p.configLock.Lock() - numDoors := p.config.numDoors - p.configLock.Unlock() - - - doorStr, ok := r.Form["door_id"] - if !ok || len(doorStr) == 0 { - return 0, errors.New("Please send door id") - } - - doorId, err := strconv.ParseUint(doorStr[0], 10, 8) - if err != nil { - return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error())) - } - doorId8 := uint8(doorId) - - if doorId8 < 1 || doorId8 > numDoors { - return 0, errors.New("Invalid door id") - } - - return doorId8, nil -} - -func SliceIndex(limit int, predicate func(i int) bool) int { - for i := 0; i < limit; i++ { - if predicate(i) { - return i - } - } - return -1 -} - -func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { - auth, authOk := r.Header["Mattermost-User-Id"] - var authUser string = "" - if authOk && len(auth) > 0 { - authUser = auth[0] - } - p.log(fmt.Sprintf("Requested path: %s %s AUTH: %s (%t)", r.URL.Path, c.IpAddress, authUser, authOk)) - if r.URL.Path == "/settings" { - p.configLock.Lock() - defer p.configLock.Unlock() - - retMap := make(map[string]interface{}) - for k, v := range p.config.settingsJson { - retMap[k] = v - } - - userOptions := defaultUserOptions() - - if authOk { - p.Helpers.KVGetJSON("settings_" + authUser, userOptions) - } - - retMap["user"] = userOptions - - bytes, err := json.Marshal(retMap) - if err == nil { - fmt.Fprint(w, string(bytes)) - } else { - p.API.LogError(fmt.Sprintf("Unable to json Marshal settings %s", err.Error())) - } - - return - } - if r.URL.Path == "/status" { - p.doorLock.Lock() - defer p.doorLock.Unlock() - - var write map[string]string = make(map[string]string) - for d, s := range p.doors { - statusStr, _ := statusName(s.status) - write[fmt.Sprintf("%d", d + 1)] = statusStr - } - if output, err := json.Marshal(write); err == nil { - fmt.Fprint(w, string(output)) - } else { - fmt.Fprint(w, html.EscapeString(err.Error())) - } - return - } - if r.URL.Path == "/admin-update-status" && authOk { - r.ParseForm() - p.configLock.Lock() - p.doorLock.Lock() - defer p.doorLock.Unlock() - defer p.configLock.Unlock() - - if SliceIndex(len(p.config.adminUsers), func(i int) bool { return p.config.adminUsers[i].Id == authUser }) != -1 { - for n, vs := range r.Form { - matches := doorWatchFile.FindStringSubmatch(n) - if matches != nil && len(matches) >= 3 { - doorId64, err := strconv.ParseUint(matches[2], 10, 8) - doorId := uint8(doorId64) - if err == nil && doorId >= 1 && doorId <= p.config.numDoors && len(vs) > 0 { - status64, err := strconv.ParseUint(vs[0], 10, 8) - status := uint(status64) - if err == nil && status >= Unknown && status <= Closed { - p.setDoorStatus(uint8(doorId), status, true) - } else { - fmt.Fprintf(w, "Not a valid status: %d", status) - } - } else { - fmt.Fprintf(w, "Not a valid door: %# v, %d, %# v", matches, doorId, pretty.Formatter(vs)) - } - } else { - fmt.Fprintf(w, "Not a valid door key: %s", n) - } - } - } else { - fmt.Fprintf(w, "Not authorized: %s", authUser) - } - return - } - if r.URL.Path == "/status-update" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - statusStr, ok := r.Form["status"] - if !ok || len(statusStr) == 0 { - fmt.Fprint(w, "Please send door status") - return - } - - status, err := strconv.ParseUint(statusStr[0], 10, 8) - if err != nil { - fmt.Fprintf(w, "Couldn't parse status: %s", err.Error()) - return - } - - if status != 0 && status != 1 { - fmt.Fprint(w, "Invalid status") - return - } - - statusVal := Open - if status == 1 { - statusVal = Closed - } - - p.doorLock.Lock() - defer p.doorLock.Unlock() - - if p.doors[doorId8 - 1].pubKey == nil { - fmt.Fprintf(w, "No public key found for door %d", doorId8) - return - } - - p.log("Getting random bytes") - var verifyBytes [50]byte - rand.Read(verifyBytes[:]) - p.log("Encoding random bytes") - - verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:]) - - p.log("Encryping random bytes") - encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{}) - if err != nil { - fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error()) - return - } - - p.log("Encoding encrypted bytes") - encryptedB64 := base64.StdEncoding.EncodeToString(encrypted) - - - req := &DoorRequest { - ip:c.IpAddress, - time:time.Now().Unix(), - verify:verifyB64, - status: statusVal, - } - - p.doors[doorId8 - 1].lastRequest = req - - p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify)) - - fmt.Fprint(w, encryptedB64) - - - return - } - if r.URL.Path == "/verify-status" { - r.ParseForm() - p.log("Contacted by " + r.RemoteAddr) - doorId8, err := p.validateRequestDoorId(r) - if err != nil { - fmt.Fprint(w, err.Error()) - return - } - - verifyB64, ok := r.Form["verify"] - if !ok || len(verifyB64) <= 0 { - fmt.Fprint(w, "Please send the verification code") - return - } - - p.log("Verify locking") - p.doorLock.Lock() - defer p.doorLock.Unlock() - p.log("Verify done locking") - - if p.doors[doorId8 - 1].lastRequest == nil { - fmt.Fprint(w, "Invalid request") - return - } - - req := p.doors[doorId8 - 1].lastRequest - if req.ip != c.IpAddress { - fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr) - return - } - - diff := time.Now().Unix() - req.time - if diff < 0 || diff > 10 { - fmt.Fprint(w, "Request expired") - return - } - - - if req.verify != verifyB64[0] { - p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0])) - fmt.Fprint(w, "Unauthorized request") - return - } - - - p.doors[doorId8 - 1].lastRequest = nil - - p.log("Changing") - err = p.setDoorStatus(doorId8, req.status, true) - p.log("Changed") - if err != nil { - fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error()) - } - - return - - } - http.NotFound(w, r) -} - -func (p *BathroomMonitorPlugin) confChangedEvent() { - select { - case p.configChanged <- struct{}{}: - default: - } -} - -func (p *BathroomMonitorPlugin) postAdminChannel(text string) { - p.API.LogError(text) - - if p.bot != nil { - for _, u := range(p.config.adminUsers) { - channel, err := p.API.GetDirectChannel(*p.bot, u.Id); - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't get direct channel to user %s", u.Username)).Error()) - continue - } - p.API.CreatePost(&model.Post{UserId: *p.bot, ChannelId:channel.Id, Message:text, MessageSource:text}) - } - } -} - -func (p *BathroomMonitorPlugin) OnConfigurationChange() error { - p.configLock.Lock() - defer p.configLock.Unlock() - - var newConfig *Config = new(Config); - if err := p.API.LoadPluginConfiguration(newConfig); err != nil { - newErr := errors.Wrap(err, fmt.Sprintf("%d: Failed to load configuration", p.configUpdates)) - p.postAdminChannel(newErr.Error()) - return newErr - } - - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { - p.API.LogInfo("Passed same config, or empty?") - return nil; - } - - var configErr error = nil - - - if USE_FSNOTIFY { - if info, err := os.Stat(newConfig.WatchPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) - } - } else { - if info, err := os.Stat(newConfig.KeyPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { - newConfig.KeyPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid key path")) - } - } - - - numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) - if err != nil { - newConfig.NumDoors = "1" - newConfig.numDoors = 1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) - } else { - newConfig.numDoors = uint8(numDoors) - } - - if strings.Trim(newConfig.PingInterval, " \n\t") == "" { - newConfig.pingInterval = -1 - } else { - pingInterval, err := strconv.ParseInt(newConfig.PingInterval, 10, 32) - if err != nil { - newConfig.PingInterval = "" - newConfig.pingInterval = -1 - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid ping interval")) - } else { - newConfig.pingInterval = int(pingInterval) - } - } - - newConfig.adminUsers = make([]*model.User, 0, 4) - split := splitWhitespaceOrComma.Split(newConfig.AdminUsers, -1) - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - u, _ := p.API.GetUserByUsername(trimmed) - if u != nil { - newConfig.adminUsers = append(newConfig.adminUsers, u) - } - } - } - - newConfig.doorNames = make([]string, 0, 4) - split = strings.Split(newConfig.DoorNames, "|") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.doorNames = append(newConfig.doorNames, trimmed) - } - } - - newConfig.unknownIcons = make([]string, 0, 4) - split = strings.Split(newConfig.UnknownIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.unknownIcons = append(newConfig.unknownIcons, trimmed) - } - } - - newConfig.openIcons = make([]string, 0, 4) - split = strings.Split(newConfig.OpenIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.openIcons = append(newConfig.openIcons, trimmed) - } - } - - newConfig.closedIcons = make([]string, 0, 4) - split = strings.Split(newConfig.ClosedIcons, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - newConfig.closedIcons = append(newConfig.closedIcons, trimmed) - } - } - - doorOrder := make([]uint8, 0, 4) - split = strings.Split(newConfig.DoorOrder, ",") - for _, un := range split { - trimmed := strings.Trim(un, ", \t\n") - if trimmed != "" { - id, err := strconv.ParseUint(trimmed, 10, 8) - if err != nil { - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid door order")) - doorOrder = nil - break - } - doorOrder = append(doorOrder, uint8(id)) - } - } - newConfig.doorOrder = doorOrder - - min := func(a uint8, b uint8) uint8 { - if a < b { - return a - } - return b - } - - doorsJson := make(map[string]string) - for i := uint8(0); i < min(uint8(len(newConfig.doorNames)), newConfig.numDoors); i++ { - doorsJson[fmt.Sprintf("%d", i + 1)] = newConfig.doorNames[i] - } - - doorOrderJson := make([]int, len(newConfig.doorOrder)) - for i, o := range newConfig.doorOrder { - doorOrderJson[i] = int(o) - } - newConfig.settingsJson = map[string]interface{} { - "info_icon": newConfig.InfoIcon, - "unknown_icons": newConfig.unknownIcons, - "open_icons": newConfig.openIcons, - "closed_icons": newConfig.closedIcons, - "doors": doorsJson, - "door_order": doorOrderJson, - } - - p.config = newConfig - p.configUpdates++ - - p.initDoors() - - p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath)) - - if configErr != nil { - p.postAdminChannel(configErr.Error()) - } - - p.confChangedEvent() - - return configErr -} - -func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { - for { - p.configLock.Lock() - numDoors := p.config.numDoors - watchPath := p.config.WatchPath - p.configLock.Unlock() - - _ = numDoors - - watcher, err := fsnotify.NewWatcher() - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't make watcher").Error()) - time.Sleep(60) - continue - } - - err = watcher.Add(watchPath) - - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't watch WatchPath").Error()) - } - - run := true - for ;run; { - select { - case event, ok := <- watcher.Events: - if (!ok) { - p.API.LogError(errors.Wrap(err, "Couldn't get fsnotify event").Error()) - time.Sleep(60) - run = false - } else { - match := doorWatchFile.FindStringSubmatch(event.Name) - if match == nil { - continue - } - - id64, err := strconv.ParseUint(match[2], 10, 8) - if err != nil { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - continue - } - - id := uint8(id64) - if id < 1 || id > numDoors { - continue - } - - if event.Op & fsnotify.Write != fsnotify.Write && event.Op & fsnotify.Create != fsnotify.Create { - continue - } - - statusBytes, err := ioutil.ReadFile(event.Name) - if err != nil { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - - status := strings.Trim(string(statusBytes), " \n\t") - if len(status) <= 0 { - continue - } - - statusInt, err := strconv.ParseUint(status, 10, 32) - if err != nil { - p.API.LogError(errors.Wrap(err, "Status invalid " + status).Error()) - continue - } - - newStatus := Open - if statusInt == 1 { - newStatus = Closed - } - - p.doorLock.Lock() - err = p.setDoorStatus(id, newStatus, true) - p.doorLock.Unlock() - - if err != nil { - p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't set door status %d %d", id, statusInt)).Error()) - continue - } - } - case <- p.configChanged: - p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates)) - run = false - } - } - - watcher.Close() - } -} - -func (p *BathroomMonitorPlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { - userOptions := defaultUserOptions() - _ = userOptions - - return nil, nil -} - -func (p *BathroomMonitorPlugin) OnActivate() error { - p.API.RegisterCommand(&model.Command { - Trigger: "bathrooms", - AutoComplete: true, - AutoCompleteDesc: "Turns the bathroom widget \"on\" or \"off\" (\"status\" or no option prints bathroom statuses)", - AutoCompleteHint: "(on|off|status)", - }) - if p.bot == nil { - botUser, err := p.Helpers.EnsureBot(&model.Bot{Username:"bathroom-bot", DisplayName:"Bathroom Bot", Description:"Tracks Bathroom Status"}); - if err == nil { - p.bot = &botUser - } else { - p.bot = nil - } - } - if USE_FSNOTIFY { - go fileNotifyBasedLoop(p) - } else { - } - - go p.pingLoop(); - - return nil -} - -func (p *BathroomMonitorPlugin) pingLoop() { - for { - - var tickChan <-chan time.Time = nil - var ticker *time.Ticker = nil - - p.configLock.Lock() - p.log(fmt.Sprintf("Setting pingLoop with interval %d", p.config.pingInterval)) - if p.config.pingInterval > 0 { - ticker = time.NewTicker(time.Duration(p.config.pingInterval) * time.Second) - tickChan = ticker.C - - } - p.configLock.Unlock() - - run := true - for ;run; { - select { - case <- tickChan: - p.log(fmt.Sprintf("Sending ping")) - p.API.PublishWebSocketEvent("ping", map[string]interface{}{}, &model.WebsocketBroadcast{}) - case <- p.configChanged: - run = false - } - } - - if ticker != nil { - ticker.Stop() - } - } -} - -func main() { - plugin.ClientMain((&BathroomMonitorPlugin{}).init()) -} diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh index c2c1a0e..1623a54 100755 --- a/mattermost/server/build.sh +++ b/mattermost/server/build.sh @@ -1,4 +1,7 @@ #! /usr/bin/env bash -filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) ../plugin_templ.json > ../plugin.json -GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES +printf -v FORMATTED_TAGS '%s,' "$@" +FORMATTED_TAGS=${FORMATTED_TAGS%,} + +gcc -P -E $( [[ -n "$DEFS" ]] && echo -D$DEFS ) - - < ../plugin_templ.json > ../plugin.json +GOOS=linux GOARCH=amd64 go build $( [[ -n "$FORMATTED_TAGS" ]] && echo -tags $FORMATTED_TAGS ) -o bathroom-linux-amd64 bathroom-plugin/main diff --git a/mattermost/server/build_web.sh b/mattermost/server/build_web.sh deleted file mode 100755 index 7ccb125..0000000 --- a/mattermost/server/build_web.sh +++ /dev/null @@ -1,3 +0,0 @@ -#! /usr/bin/env bash - -FILES=config_web.go ./build.sh diff --git a/mattermost/server/config_fs.go b/mattermost/server/config_fs.go deleted file mode 100644 index 6384df5..0000000 --- a/mattermost/server/config_fs.go +++ /dev/null @@ -1,3 +0,0 @@ -package main - -const USE_FSNOTIFY = true diff --git a/mattermost/server/config_web.go b/mattermost/server/config_web.go deleted file mode 100644 index c72b5c8..0000000 --- a/mattermost/server/config_web.go +++ /dev/null @@ -1,3 +0,0 @@ -package main - -const USE_FSNOTIFY = false diff --git a/mattermost/server/go.mod b/mattermost/server/go.mod new file mode 100644 index 0000000..779de8d --- /dev/null +++ b/mattermost/server/go.mod @@ -0,0 +1,54 @@ +module bathroom-plugin + +go 1.23.3 + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.6.2 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect + github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect + github.com/mattermost/logr/v2 v2.0.21 // indirect + github.com/mattermost/mattermost/server/public v0.1.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/pborman/uuid v1.2.1 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/tinylib/msgp v1.2.4 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/wiggin77/merror v1.0.5 // indirect + github.com/wiggin77/srslog v1.0.1 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f // indirect + google.golang.org/grpc v1.68.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/mattermost/server/go.sum b/mattermost/server/go.sum new file mode 100755 index 0000000..2fe6112 --- /dev/null +++ b/mattermost/server/go.sum @@ -0,0 +1,291 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a h1:etIrTD8BQqzColk9nKRusM9um5+1q0iOEJLqfBMIK64= +github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a/go.mod h1:emQhSYTXqB0xxjLITTw4EaWZ+8IIQYw+kx9GqNUKdLg= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= +github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= +github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8= +github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34= +github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb2BTCsOdamENjjWCI6qmfHLbk6OZI= +github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956/go.mod h1:SRl30Lb7/QoYyohYeVBuqYvvmXSZJxZgiV3Zf6VbxjI= +github.com/mattermost/logr/v2 v2.0.21 h1:CMHsP+nrbRlEC4g7BwOk1GAnMtHkniFhlSQPXy52be4= +github.com/mattermost/logr/v2 v2.0.21/go.mod h1:kZkB/zqKL9e+RY5gB3vGpsyenC+TpuiOenjMkvJJbzc= +github.com/mattermost/mattermost v10.2.0+incompatible h1:Jo/tr/282u/ixQPUbCV6BcEVgSxxcxUeIgWMpT+3zbQ= +github.com/mattermost/mattermost v10.2.0+incompatible/go.mod h1:JUnq0RiZa+eUafKS5M47SVRdPIJyIITxTcCFvoPlFlk= +github.com/mattermost/mattermost-server v10.2.0+incompatible h1:RhG3b+xCdGUnHyWVJxYWuBzDdgBrJp1LHPJjBEmu48I= +github.com/mattermost/mattermost-server v10.2.0+incompatible/go.mod h1:5L6MjAec+XXQwMIt791Ganu45GKsSiM+I0tLR9wUj8Y= +github.com/mattermost/mattermost/server/public v0.1.9 h1:l/OKPRVuFeqL0yqRVC/JpveG5sLNKcT9llxqMkO9e+s= +github.com/mattermost/mattermost/server/public v0.1.9/go.mod h1:SkTKbMul91Rq0v2dIxe8mqzUOY+3KwlwwLmAlxDfGCk= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tinylib/msgp v1.2.4 h1:yLFeUGostXXSGW5vxfT5dXG/qzkn4schv2I7at5+hVU= +github.com/tinylib/msgp v1.2.4/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wiggin77/merror v1.0.5 h1:P+lzicsn4vPMycAf2mFf7Zk6G9eco5N+jB1qJ2XW3ME= +github.com/wiggin77/merror v1.0.5/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0= +github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8= +github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f h1:C1QccEa9kUwvMgEUORqQD9S17QesQijxjZ84sO82mfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/mattermost/server/main/bathroom.go b/mattermost/server/main/bathroom.go new file mode 100644 index 0000000..feb4837 --- /dev/null +++ b/mattermost/server/main/bathroom.go @@ -0,0 +1,829 @@ +package main + +import ( + "fmt" + "net/http" + "github.com/mattermost/mattermost/server/public/plugin" + "github.com/mattermost/mattermost/server/public/model" + "github.com/pkg/errors" + "sync" + "reflect" + _ "github.com/google/go-cmp/cmp" + "os" + _ "github.com/fatih/structs" + "github.com/hashicorp/go-multierror" + "github.com/kr/pretty" + "strings" + "regexp" + "strconv" + "time" + "github.com/fsnotify/fsnotify" + "io/ioutil" + "html" + "encoding/json" + "encoding/pem" + "crypto/rsa" + "crypto/x509" + "crypto/rand" + "crypto/sha1" + "path" + "encoding/base64" + _ "math" +) + +const POST_STATUS_TO_ADMIN = false +const DO_LOGGING = false + +var splitWhitespaceOrComma *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) +var doorWatchFile *regexp.Regexp = regexp.MustCompile(`(^|/)door(\d+)$`) + +const ( + Unknown = uint(iota) + Open + Closed +) + +func statusName(status uint) (string, error) { + switch status { + case Unknown: + return "unknown", nil + case Open: + return "open", nil + case Closed: + return "closed", nil + } + + return "", errors.New(fmt.Sprintf("Invalid status %d", status)) +} + +type UserOptions struct { + ShowWidget bool `json:"show_widget"` +} + +func defaultUserOptions() *UserOptions { + return &UserOptions { + ShowWidget: true, + } +} + +type DoorRequest struct { + ip string + time int64 + verify string + status uint +} + +type Door struct { + id uint8 + status uint + pubKey *rsa.PublicKey + lastRequest *DoorRequest +} + +type Config struct { + NumDoors string + numDoors uint8 + + PingInterval string + pingInterval int + + WatchPath string + + AdminUsers string + adminUsers []*model.User + + KeyPath string + + DoorNames string + doorNames []string + + InfoIcon string + UnknownIcons string + OpenIcons string + ClosedIcons string + + unknownIcons []string + openIcons []string + closedIcons []string + + DoorOrder string + doorOrder []uint8 + + settingsJson map[string]interface{} +} + +type BathroomMonitorPlugin struct { + plugin.MattermostPlugin + + config *Config + configLock sync.RWMutex + configChanged chan struct{} + configUpdates int + + doorLock sync.RWMutex + doors map[uint8]*Door + + bot *string +} + +func (p *BathroomMonitorPlugin) getKeyFile(id uint8) (*rsa.PublicKey, error) { + keyFile, err := ioutil.ReadFile(path.Join(p.config.KeyPath, fmt.Sprintf("public%d.pem", id))) + if err != nil { + return nil, err + } + + data, _ := pem.Decode(keyFile) + if data == nil { + return nil, errors.New("No PEM formatted data in file") + } + + key, err := x509.ParsePKIXPublicKey(data.Bytes) + if err != nil { + return nil, err + } + + switch pub := key.(type) { + case *rsa.PublicKey: + return pub, nil + } + + return nil, errors.New(fmt.Sprintf("Keyfile wasn't a public RSA PKIX key, it was %T", key)) +} + +func (p *BathroomMonitorPlugin) initDoors() { + p.doorLock.Lock() + defer p.doorLock.Unlock() + + p.doors = make(map[uint8]*Door) + for i := uint8(0); i < p.config.numDoors; i++ { + id := i + 1 + var pub *rsa.PublicKey = nil + if !USE_FSNOTIFY { + var err error + pub, err = p.getKeyFile(id) + if err != nil { + p.API.LogError(errors.Wrap(err, fmt.Sprintf("Unable to load key file for id %d", id)).Error()) + pub = nil + } + } + p.doors[i] = &Door { + id: id, + status: Unknown, + pubKey: pub, + } + } +} + +func (p *BathroomMonitorPlugin) log(log string) { + if DO_LOGGING { + p.API.LogInfo(log) + } +} + +func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status uint, report bool) error { + if id < 1 || id > p.config.numDoors { + return errors.New(fmt.Sprintf("Invalid door id %d", id)) + } + + if status < Unknown || status > Closed { + return errors.New(fmt.Sprintf("Invalid door status %d", status)) + } + + if p.doors == nil || len(p.doors) < int(p.config.numDoors) { + return errors.New("Doors not inited yet") + } + + if p.doors[id - 1].status != status { + p.doors[id - 1].status = status + if report { + statusStr, _ := statusName(status) + _ = statusStr + if POST_STATUS_TO_ADMIN { + p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr)) + } + p.sendUpdate() + go p.resendUpdate(10 * time.Second) + } + } else { + p.log(fmt.Sprintf("Asked to change status from %d to %d ?", p.doors[id - 1].status, status)) + } + + return nil +} + +func (p *BathroomMonitorPlugin) sendUpdate() { + p.API.PublishWebSocketEvent("updated", map[string]interface{}{}, &model.WebsocketBroadcast{}) +} + +func (p *BathroomMonitorPlugin) resendUpdate(wait time.Duration) { + timer := time.NewTimer(wait) + <-timer.C + p.sendUpdate() +} + + +func (p *BathroomMonitorPlugin) init() *BathroomMonitorPlugin { + p.config = &Config { + NumDoors: "1", + numDoors: 1, + + WatchPath: "./", + + KeyPath: "./", + + AdminUsers: "", + } + + p.configChanged = make(chan struct{}) + + return p; +} + +func (p *BathroomMonitorPlugin) validateRequestDoorId(r *http.Request) (uint8, error) { + p.configLock.Lock() + numDoors := p.config.numDoors + p.configLock.Unlock() + + + doorStr, ok := r.Form["door_id"] + if !ok || len(doorStr) == 0 { + return 0, errors.New("Please send door id") + } + + doorId, err := strconv.ParseUint(doorStr[0], 10, 8) + if err != nil { + return 0, errors.New(fmt.Sprintf("Couldn't parse door_id: %s", err.Error())) + } + doorId8 := uint8(doorId) + + if doorId8 < 1 || doorId8 > numDoors { + return 0, errors.New("Invalid door id") + } + + return doorId8, nil +} + +func SliceIndex(limit int, predicate func(i int) bool) int { + for i := 0; i < limit; i++ { + if predicate(i) { + return i + } + } + return -1 +} + +func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { + auth, authOk := r.Header["Mattermost-User-Id"] + var authUser string = "" + if authOk && len(auth) > 0 { + authUser = auth[0] + } + p.log(fmt.Sprintf("Requested path: %s %s AUTH: %s (%t)", r.URL.Path, c.IpAddress, authUser, authOk)) + if r.URL.Path == "/settings" { + p.configLock.Lock() + defer p.configLock.Unlock() + + retMap := make(map[string]interface{}) + for k, v := range p.config.settingsJson { + retMap[k] = v + } + + userOptions := defaultUserOptions() + + if authOk { + p.Helpers.KVGetJSON("settings_" + authUser, userOptions) + } + + retMap["user"] = userOptions + + bytes, err := json.Marshal(retMap) + if err == nil { + fmt.Fprint(w, string(bytes)) + } else { + p.API.LogError(fmt.Sprintf("Unable to json Marshal settings %s", err.Error())) + } + + return + } + if r.URL.Path == "/status" { + p.doorLock.Lock() + defer p.doorLock.Unlock() + + var write map[string]string = make(map[string]string) + for d, s := range p.doors { + statusStr, _ := statusName(s.status) + write[fmt.Sprintf("%d", d + 1)] = statusStr + } + if output, err := json.Marshal(write); err == nil { + fmt.Fprint(w, string(output)) + } else { + fmt.Fprint(w, html.EscapeString(err.Error())) + } + return + } + if r.URL.Path == "/admin-update-status" && authOk { + r.ParseForm() + p.configLock.Lock() + p.doorLock.Lock() + defer p.doorLock.Unlock() + defer p.configLock.Unlock() + + if SliceIndex(len(p.config.adminUsers), func(i int) bool { return p.config.adminUsers[i].Id == authUser }) != -1 { + for n, vs := range r.Form { + matches := doorWatchFile.FindStringSubmatch(n) + if matches != nil && len(matches) >= 3 { + doorId64, err := strconv.ParseUint(matches[2], 10, 8) + doorId := uint8(doorId64) + if err == nil && doorId >= 1 && doorId <= p.config.numDoors && len(vs) > 0 { + status64, err := strconv.ParseUint(vs[0], 10, 8) + status := uint(status64) + if err == nil && status >= Unknown && status <= Closed { + p.setDoorStatus(uint8(doorId), status, true) + } else { + fmt.Fprintf(w, "Not a valid status: %d", status) + } + } else { + fmt.Fprintf(w, "Not a valid door: %# v, %d, %# v", matches, doorId, pretty.Formatter(vs)) + } + } else { + fmt.Fprintf(w, "Not a valid door key: %s", n) + } + } + } else { + fmt.Fprintf(w, "Not authorized: %s", authUser) + } + return + } + if r.URL.Path == "/status-update" { + r.ParseForm() + p.log("Contacted by " + r.RemoteAddr) + + doorId8, err := p.validateRequestDoorId(r) + if err != nil { + fmt.Fprint(w, err.Error()) + return + } + + statusStr, ok := r.Form["status"] + if !ok || len(statusStr) == 0 { + fmt.Fprint(w, "Please send door status") + return + } + + status, err := strconv.ParseUint(statusStr[0], 10, 8) + if err != nil { + fmt.Fprintf(w, "Couldn't parse status: %s", err.Error()) + return + } + + if status != 0 && status != 1 { + fmt.Fprint(w, "Invalid status") + return + } + + statusVal := Open + if status == 1 { + statusVal = Closed + } + + p.doorLock.Lock() + defer p.doorLock.Unlock() + + if p.doors[doorId8 - 1].pubKey == nil { + fmt.Fprintf(w, "No public key found for door %d", doorId8) + return + } + + p.log("Getting random bytes") + var verifyBytes [50]byte + rand.Read(verifyBytes[:]) + p.log("Encoding random bytes") + + verifyB64 := base64.StdEncoding.EncodeToString(verifyBytes[:]) + + p.log("Encryping random bytes") + encrypted, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, p.doors[doorId8 - 1].pubKey, verifyBytes[:], []byte{}) + if err != nil { + fmt.Fprintf(w, "Couldn't encrypt verification: %s", err.Error()) + return + } + + p.log("Encoding encrypted bytes") + encryptedB64 := base64.StdEncoding.EncodeToString(encrypted) + + + req := &DoorRequest { + ip:c.IpAddress, + time:time.Now().Unix(), + verify:verifyB64, + status: statusVal, + } + + p.doors[doorId8 - 1].lastRequest = req + + p.log(fmt.Sprintf("NEW REQUEST SAVED: %s %d %s", req.ip, req.time, req.verify)) + + fmt.Fprint(w, encryptedB64) + + + return + } + if r.URL.Path == "/verify-status" { + r.ParseForm() + p.log("Contacted by " + r.RemoteAddr) + doorId8, err := p.validateRequestDoorId(r) + if err != nil { + fmt.Fprint(w, err.Error()) + return + } + + verifyB64, ok := r.Form["verify"] + if !ok || len(verifyB64) <= 0 { + fmt.Fprint(w, "Please send the verification code") + return + } + + p.log("Verify locking") + p.doorLock.Lock() + defer p.doorLock.Unlock() + p.log("Verify done locking") + + if p.doors[doorId8 - 1].lastRequest == nil { + fmt.Fprint(w, "Invalid request") + return + } + + req := p.doors[doorId8 - 1].lastRequest + if req.ip != c.IpAddress { + fmt.Fprintf(w, "Not your request %s %s", req.ip, r.RemoteAddr) + return + } + + diff := time.Now().Unix() - req.time + if diff < 0 || diff > 10 { + fmt.Fprint(w, "Request expired") + return + } + + + if req.verify != verifyB64[0] { + p.log(fmt.Sprintf("Failed verification %s %s", req.verify, verifyB64[0])) + fmt.Fprint(w, "Unauthorized request") + return + } + + + p.doors[doorId8 - 1].lastRequest = nil + + p.log("Changing") + err = p.setDoorStatus(doorId8, req.status, true) + p.log("Changed") + if err != nil { + fmt.Fprintf(w, "Couldn't set status %d %d: %s", doorId8, req.status, err.Error()) + } + + return + + } + http.NotFound(w, r) +} + +func (p *BathroomMonitorPlugin) confChangedEvent() { + select { + case p.configChanged <- struct{}{}: + default: + } +} + +func (p *BathroomMonitorPlugin) postAdminChannel(text string) { + p.API.LogError(text) + + if p.bot != nil { + for _, u := range(p.config.adminUsers) { + channel, err := p.API.GetDirectChannel(*p.bot, u.Id); + if err != nil { + p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't get direct channel to user %s", u.Username)).Error()) + continue + } + p.API.CreatePost(&model.Post{UserId: *p.bot, ChannelId:channel.Id, Message:text, MessageSource:text}) + } + } +} + +func (p *BathroomMonitorPlugin) OnConfigurationChange() error { + p.configLock.Lock() + defer p.configLock.Unlock() + + var newConfig *Config = new(Config); + if err := p.API.LoadPluginConfiguration(newConfig); err != nil { + newErr := errors.Wrap(err, fmt.Sprintf("%d: Failed to load configuration", p.configUpdates)) + p.postAdminChannel(newErr.Error()) + return newErr + } + + if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { + p.API.LogInfo("Passed same config, or empty?") + return nil; + } + + var configErr error = nil + + + if USE_FSNOTIFY { + if info, err := os.Stat(newConfig.WatchPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { + newConfig.WatchPath = "./" + configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) + } + } else { + if info, err := os.Stat(newConfig.KeyPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { + newConfig.KeyPath = "./" + configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid key path")) + } + } + + + numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) + if err != nil { + newConfig.NumDoors = "1" + newConfig.numDoors = 1 + configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) + } else { + newConfig.numDoors = uint8(numDoors) + } + + if strings.Trim(newConfig.PingInterval, " \n\t") == "" { + newConfig.pingInterval = -1 + } else { + pingInterval, err := strconv.ParseInt(newConfig.PingInterval, 10, 32) + if err != nil { + newConfig.PingInterval = "" + newConfig.pingInterval = -1 + configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid ping interval")) + } else { + newConfig.pingInterval = int(pingInterval) + } + } + + newConfig.adminUsers = make([]*model.User, 0, 4) + split := splitWhitespaceOrComma.Split(newConfig.AdminUsers, -1) + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + u, _ := p.API.GetUserByUsername(trimmed) + if u != nil { + newConfig.adminUsers = append(newConfig.adminUsers, u) + } + } + } + + newConfig.doorNames = make([]string, 0, 4) + split = strings.Split(newConfig.DoorNames, "|") + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + newConfig.doorNames = append(newConfig.doorNames, trimmed) + } + } + + newConfig.unknownIcons = make([]string, 0, 4) + split = strings.Split(newConfig.UnknownIcons, ",") + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + newConfig.unknownIcons = append(newConfig.unknownIcons, trimmed) + } + } + + newConfig.openIcons = make([]string, 0, 4) + split = strings.Split(newConfig.OpenIcons, ",") + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + newConfig.openIcons = append(newConfig.openIcons, trimmed) + } + } + + newConfig.closedIcons = make([]string, 0, 4) + split = strings.Split(newConfig.ClosedIcons, ",") + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + newConfig.closedIcons = append(newConfig.closedIcons, trimmed) + } + } + + doorOrder := make([]uint8, 0, 4) + split = strings.Split(newConfig.DoorOrder, ",") + for _, un := range split { + trimmed := strings.Trim(un, ", \t\n") + if trimmed != "" { + id, err := strconv.ParseUint(trimmed, 10, 8) + if err != nil { + configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid door order")) + doorOrder = nil + break + } + doorOrder = append(doorOrder, uint8(id)) + } + } + newConfig.doorOrder = doorOrder + + min := func(a uint8, b uint8) uint8 { + if a < b { + return a + } + return b + } + + doorsJson := make(map[string]string) + for i := uint8(0); i < min(uint8(len(newConfig.doorNames)), newConfig.numDoors); i++ { + doorsJson[fmt.Sprintf("%d", i + 1)] = newConfig.doorNames[i] + } + + doorOrderJson := make([]int, len(newConfig.doorOrder)) + for i, o := range newConfig.doorOrder { + doorOrderJson[i] = int(o) + } + newConfig.settingsJson = map[string]interface{} { + "info_icon": newConfig.InfoIcon, + "unknown_icons": newConfig.unknownIcons, + "open_icons": newConfig.openIcons, + "closed_icons": newConfig.closedIcons, + "doors": doorsJson, + "door_order": doorOrderJson, + } + + p.config = newConfig + p.configUpdates++ + + p.initDoors() + + p.log(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath)) + + if configErr != nil { + p.postAdminChannel(configErr.Error()) + } + + p.confChangedEvent() + + return configErr +} + +func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { + for { + p.configLock.Lock() + numDoors := p.config.numDoors + watchPath := p.config.WatchPath + p.configLock.Unlock() + + _ = numDoors + + watcher, err := fsnotify.NewWatcher() + if err != nil { + p.API.LogError(errors.Wrap(err, "Couldn't make watcher").Error()) + time.Sleep(60) + continue + } + + err = watcher.Add(watchPath) + + if err != nil { + p.API.LogError(errors.Wrap(err, "Couldn't watch WatchPath").Error()) + } + + run := true + for ;run; { + select { + case event, ok := <- watcher.Events: + if (!ok) { + p.API.LogError(errors.Wrap(err, "Couldn't get fsnotify event").Error()) + time.Sleep(60) + run = false + } else { + match := doorWatchFile.FindStringSubmatch(event.Name) + if match == nil { + continue + } + + id64, err := strconv.ParseUint(match[2], 10, 8) + if err != nil { + p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) + continue + } + + id := uint8(id64) + if id < 1 || id > numDoors { + continue + } + + if event.Op & fsnotify.Write != fsnotify.Write && event.Op & fsnotify.Create != fsnotify.Create { + continue + } + + statusBytes, err := ioutil.ReadFile(event.Name) + if err != nil { + p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) + } + + status := strings.Trim(string(statusBytes), " \n\t") + if len(status) <= 0 { + continue + } + + statusInt, err := strconv.ParseUint(status, 10, 32) + if err != nil { + p.API.LogError(errors.Wrap(err, "Status invalid " + status).Error()) + continue + } + + newStatus := Open + if statusInt == 1 { + newStatus = Closed + } + + p.doorLock.Lock() + err = p.setDoorStatus(id, newStatus, true) + p.doorLock.Unlock() + + if err != nil { + p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't set door status %d %d", id, statusInt)).Error()) + continue + } + } + case <- p.configChanged: + p.log(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates)) + run = false + } + } + + watcher.Close() + } +} + +func (p *BathroomMonitorPlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { + userOptions := defaultUserOptions() + _ = userOptions + + return nil, nil +} + +func (p *BathroomMonitorPlugin) OnActivate() error { + p.API.RegisterCommand(&model.Command { + Trigger: "bathrooms", + AutoComplete: true, + AutoCompleteDesc: "Turns the bathroom widget \"on\" or \"off\" (\"status\" or no option prints bathroom statuses)", + AutoCompleteHint: "(on|off|status)", + }) + if p.bot == nil { + botUser, err := p.Helpers.EnsureBot(&model.Bot{Username:"bathroom-bot", DisplayName:"Bathroom Bot", Description:"Tracks Bathroom Status"}); + if err == nil { + p.bot = &botUser + } else { + p.bot = nil + } + } + if USE_FSNOTIFY { + go fileNotifyBasedLoop(p) + } else { + } + + go p.pingLoop(); + + return nil +} + +func (p *BathroomMonitorPlugin) pingLoop() { + for { + + var tickChan <-chan time.Time = nil + var ticker *time.Ticker = nil + + p.configLock.Lock() + p.log(fmt.Sprintf("Setting pingLoop with interval %d", p.config.pingInterval)) + if p.config.pingInterval > 0 { + ticker = time.NewTicker(time.Duration(p.config.pingInterval) * time.Second) + tickChan = ticker.C + + } + p.configLock.Unlock() + + run := true + for ;run; { + select { + case <- tickChan: + p.log(fmt.Sprintf("Sending ping")) + p.API.PublishWebSocketEvent("ping", map[string]interface{}{}, &model.WebsocketBroadcast{}) + case <- p.configChanged: + run = false + } + } + + if ticker != nil { + ticker.Stop() + } + } +} + +func main() { + plugin.ClientMain((&BathroomMonitorPlugin{}).init()) +} diff --git a/mattermost/server/main/config_fs.go b/mattermost/server/main/config_fs.go new file mode 100644 index 0000000..c20f92a --- /dev/null +++ b/mattermost/server/main/config_fs.go @@ -0,0 +1,5 @@ +//go:build fsnotify + +package main + +const USE_FSNOTIFY = true diff --git a/mattermost/server/main/config_web.go b/mattermost/server/main/config_web.go new file mode 100644 index 0000000..e300a39 --- /dev/null +++ b/mattermost/server/main/config_web.go @@ -0,0 +1,5 @@ +//go:build !fsnotify + +package main + +const USE_FSNOTIFY = false