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" ) var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) type Config struct { NumDoors string WatchPath string AdminUsers string numDoors uint8 } func (c *Config) configMap() map[string]interface{} { var configMap map[string]interface{} = structs.Map(c) var lowerConfigMap map[string]interface{} = make(map[string]interface{}) for k, v := range(configMap) { lowerConfigMap[strings.ToLower(k)] = fmt.Sprintf("%v", v) } return lowerConfigMap } type BathroomMonitorPlugin struct { plugin.MattermostPlugin config *Config adminUsers []string configLock sync.RWMutex configChanged chan struct{} doors map[uint8]bool configUpdates int lastReport *string } func (p *BathroomMonitorPlugin) initDoors() { p.doors = make(map[uint8]bool) for i := uint8(0); i < p.config.numDoors; i++ { p.doors[i] = false } } func (p *BathroomMonitorPlugin) init() *BathroomMonitorPlugin { p.config = &Config{NumDoors: "1", WatchPath: "./", numDoors: 1, AdminUsers:""} p.configChanged = make(chan struct{}) p.initDoors() return p; } func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/status" { var write map[string]string = make(map[string]string) for d, s := range p.doors { val := "open" if s { val = "closed" } write[fmt.Sprintf("%d", d + 1)] = val } if output, err := json.Marshal(write); err == nil { fmt.Fprint(w, string(output)) } else { fmt.Fprint(w, html.EscapeString(err.Error())) } return } http.NotFound(w, r) } func (p *BathroomMonitorPlugin) confChangedEvent() { select { case p.configChanged <- struct{}{}: default: } } func (p *BathroomMonitorPlugin) checkCorrectConfig() { var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() var goodConfig map[string]interface{} = p.config.configMap() if !cmp.Equal(currentConfig, goodConfig) { p.API.LogInfo(fmt.Sprintf("%d: Saving un-matching config\n%# v\n%# v", p.configUpdates, pretty.Formatter(currentConfig), pretty.Formatter(goodConfig))) p.API.SavePluginConfig(goodConfig) } } func (p *BathroomMonitorPlugin) getLowerCasePluginConfig() map[string]interface{} { var currentConfig map[string]interface{} = p.API.GetPluginConfig() var lowerConfig map[string]interface{} = make(map[string]interface{}) for k, v := range(currentConfig) { lowerConfig[strings.ToLower(k)] = fmt.Sprintf("%v", v) } return lowerConfig } func (p *BathroomMonitorPlugin) postAdminChannel(text string) { p.API.LogError(text) if p.lastReport != nil && *p.lastReport == text { return } p.lastReport = &text var users []*model.User = make([]*model.User, 0, len(p.adminUsers)) for _, un := range(p.adminUsers) { u, _ := p.API.GetUserByUsername(un) if u != nil { users = append(users, u) } } if len(users) > 0 { admin := users[0] bots, _ := p.API.GetBots(&model.BotGetOptions{Page:0, PerPage:1000}) var bathroom_bot *model.Bot = nil if bots != nil { for _, b := range(bots) { if b.Username == "bathroom-bot" { bathroom_bot = b break } } } if bathroom_bot == nil { created_bathroom_bot, err := p.API.CreateBot(&model.Bot{Username:"bathroom-bot", OwnerId:admin.Id, DisplayName:"Bathroom Bot", Description:"Tracks Bathroom Status"}) if err != nil { p.API.LogError(errors.Wrap(err, "Couldn't create bathroom-bot bot").Error()) return } bathroom_bot = created_bathroom_bot } if bathroom_bot == nil { p.API.LogError("Really couldn't create bathroom-bot bot") return } for _, u := range(users) { channel, err := p.API.GetDirectChannel(bathroom_bot.UserId, 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: bathroom_bot.UserId, ChannelId:channel.Id, Message:text, MessageSource:text}) } } else { p.API.LogError("No admin users?") } } func (p *BathroomMonitorPlugin) OnConfigurationChange() error { p.configLock.Lock() defer p.configLock.Unlock() //var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() //if cmp.Equal(currentConfig, p.config.configMap()) { // p.API.LogInfo(fmt.Sprintf("%d: Skipping matching config", p.configUpdates)) // return nil //} //defer p.checkCorrectConfig() 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 } //p.API.LogInfo(fmt.Sprintf("Loaded %t %t", newConfig != nil, p.config != nil)) if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { p.API.LogInfo("Passed same config, or empty?") return nil; } //p.API.LogInfo("Checked dupe") var configErr error = nil if _, err := os.Stat(newConfig.WatchPath); err != nil { newConfig.WatchPath = "./" configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) } //p.API.LogInfo("Checked path") numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) if err != nil { //p.API.LogError("Bad doors! " + err.Error()) newConfig.NumDoors = "1" newConfig.numDoors = 1 configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) } else { //p.API.LogError(fmt.Sprintf("Good doors! %d", numDoors)) newConfig.numDoors = uint8(numDoors) } p.config = newConfig p.adminUsers = make([]string, 0, 4) split := userSplit.Split(newConfig.AdminUsers, -1) for _, un := range split { trimmed := strings.Trim(un, ", \t\n") if trimmed != "" { p.adminUsers = append(p.adminUsers, trimmed) } } p.configUpdates++ p.initDoors() p.API.LogInfo(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 watchLoop(p *BathroomMonitorPlugin) { doorFile, err := regexp.Compile(`(^|/)door(\d+)$`) if err != nil { p.API.LogError(errors.Wrap(err, "Couldn't compile regex").Error()) return } 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 := doorFile.FindStringSubmatch(event.Name) if match != nil { id64, err := strconv.ParseUint(match[2], 10, 8) _ = id64 if err == nil { id := uint8(id64) if id >= 1 && id <= numDoors { if event.Op & fsnotify.Write == fsnotify.Write || event.Op & fsnotify.Create == fsnotify.Create { if statusBytes, err := ioutil.ReadFile(event.Name); err == nil { status := string(statusBytes) if len(status) > 0 { p.postAdminChannel("STATUS: " + event.Name + " = " + status) doorSensor := strings.Trim(status, " \t\n") == "1" p.doors[id - 1] = doorSensor } } else { p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) } } } } else { p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) } } } case <- p.configChanged: p.API.LogInfo(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates)) run = false } } watcher.Close() } } func (p *BathroomMonitorPlugin) OnActivate() error { go watchLoop(p) return nil } func main() { plugin.ClientMain((&BathroomMonitorPlugin{}).init()) }