diff --git a/cmd/app.go b/cmd/app.go index d68d98f..a110204 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -5,6 +5,8 @@ import ( "fmt" "os" "strings" + + "github.com/nlopes/slack" ) type App struct { @@ -68,14 +70,17 @@ func (a *App) initialize() error { a.m.setSlackAdminDMId(strings.TrimSpace(slackDMid)) } - if err = a.m.NewSlack(); err != nil { - return err - } - go a.watchMessageChannel() + go a.watchRTMEventChannel() return nil } +func (a *App) MonitorSlackMessages() { + for msg := range a.m.IncomingSlackMessages { + a.m.SendMessage("slack", "main", slack.Message(*msg)) + } +} + func (a *App) watchMessageChannel() { for a.running { msg := <-a.m.messages @@ -93,5 +98,13 @@ func (a *App) watchMessageChannel() { v.State.ProcessMessage(msg) } } - close(a.m.messages) +} + +func (a *App) watchRTMEventChannel() { + for a.running { + msg := <-a.m.OtherRTMEvents + for _, v := range a.plugins { + v.State.ProcessRTMEvent(*msg) + } + } } diff --git a/cmd/model.go b/cmd/model.go index 22dce55..0e82cf3 100644 --- a/cmd/model.go +++ b/cmd/model.go @@ -5,8 +5,8 @@ import ( "fmt" "os" "strings" + "time" - goslack "git.bullercodeworks.com/brian/go-slack" "git.bullercodeworks.com/brian/helperbot" "github.com/br0xen/boltease" "github.com/nlopes/slack" @@ -17,21 +17,29 @@ type BotModel struct { messages chan helperbot.Message - slack *goslack.Slack + slackApiToken string + slackApi *slack.Client + slackRTM *slack.RTM + slackRTMLatency time.Duration + slackIdToFriendly map[string]string + IncomingSlackMessages chan *slack.MessageEvent + OtherRTMEvents chan *slack.RTMEvent - cache map[string][]byte + dataCache map[string][]byte } func NewBotModel() (*BotModel, error) { var err error m := new(BotModel) - m.cache = make(map[string][]byte) + m.dataCache = make(map[string][]byte) m.messages = make(chan helperbot.Message, 100) m.db, err = boltease.Create("helperbot.db", 0600, nil) if err != nil { return nil, err } - + if err = m.NewSlack(); err != nil { + return nil, err + } return m, nil } @@ -58,7 +66,7 @@ func (m *BotModel) GetBytes(path []string) ([]byte, error) { var v []byte var ok bool joinedPath := strings.Join(path, "/") - if v, ok = m.cache[joinedPath]; !ok { + if v, ok = m.dataCache[joinedPath]; !ok { // Value is not cached, try to pull it from the DB if len(path) > 2 { path, key := path[:len(path)-1], path[len(path)-1] @@ -66,7 +74,7 @@ func (m *BotModel) GetBytes(path []string) ([]byte, error) { if err != nil { return nil, err } - m.cache[joinedPath] = v + m.dataCache[joinedPath] = v } } return v, nil @@ -81,7 +89,7 @@ func (m *BotModel) SetBytes(path []string, val []byte) error { return err } // Update the cache - m.cache[joinedPath] = val + m.dataCache[joinedPath] = val return nil } return errors.New("Invalid path") diff --git a/cmd/model_slack.go b/cmd/model_slack.go index 030b8b5..bc67d4c 100644 --- a/cmd/model_slack.go +++ b/cmd/model_slack.go @@ -1,7 +1,10 @@ package main import ( - goslack "git.bullercodeworks.com/brian/go-slack" + "errors" + "fmt" + "time" + "github.com/nlopes/slack" ) @@ -25,20 +28,37 @@ func (m *BotModel) GetSlackAdminDMId() (string, error) { /* End DB Functions */ func (m *BotModel) NewSlack() error { - token, err := m.getSlackToken() + var err error + m.slackApiToken, err = m.getSlackToken() if err != nil { return err } - if m.slack, err = goslack.CreateSlack(token); err != nil { - return err - } - m.slack.StartRTM() + m.IncomingSlackMessages = make(chan *slack.MessageEvent, 50) + m.OtherRTMEvents = make(chan *slack.RTMEvent, 50) + m.slackApi = slack.New(m.slackApiToken) + m.slackIdToFriendly = make(map[string]string) + m.slackRTM = m.slackApi.NewRTM() + m.slackRTMLatency = time.Duration(0) + go m.slackRTM.ManageConnection() + go m.HandleRTMEvents() return nil } -func (a *App) MonitorSlackMessages() { - for msg := range a.m.slack.IncomingMessages { - a.m.SendMessage("slack", "main", slack.Message(*msg)) +func (m *BotModel) HandleRTMEvents() { + for msg := range m.slackRTM.IncomingEvents { + switch ev := msg.Data.(type) { + case *slack.MessageEvent: + m.processMessageEvent(ev) + + case *slack.LatencyReport: + m.OtherRTMEvents <- &msg + + case *slack.RTMError: + fmt.Printf("RTM ERROR: (%d) %s", ev.Code, ev.Msg) + m.OtherRTMEvents <- &msg + + default: // Ignore other events + } } } @@ -47,7 +67,7 @@ func (m *BotModel) SendSlackChannelMessage(msg *slack.Message) error { return m.SendSlackAdminMessage(msg) } // Send message to slack channel - m.slack.PostMessage(msg) + m.PostSlackMessage(msg) return nil } @@ -58,6 +78,124 @@ func (m *BotModel) SendSlackAdminMessage(msg *slack.Message) error { if err != nil { return err } - m.slack.PostMessage(msg) + m.PostSlackMessage(msg) return nil } + +func (m *BotModel) LoadDirectMessages() { + cs, err := m.slackApi.GetIMChannels() + if err != nil { + return + } + for _, v := range cs { + uname, err := m.GetSlackUserName(v.User) + if err != nil { + uname = v.User + } + m.slackIdToFriendly[v.ID] = uname + } +} + +func (m *BotModel) processMessageEvent(ev *slack.MessageEvent) { + m.GetSlackUserName(ev.User) + m.GetSlackIdName(ev.Channel) + m.IncomingSlackMessages <- ev +} + +func (m *BotModel) GetSlackIdName(id string) (string, error) { + switch id[0] { + case 'U': + return m.GetSlackUserName(id) + case 'G': + return m.GetSlackGroupName(id) + case 'C': + return m.GetSlackChannelName(id) + case 'D': + return m.GetSlackIMName(id) + } + return "", errors.New("Unknown ID Type") +} + +func (m *BotModel) GetSlackUserName(id string) (string, error) { + if v, ok := m.slackIdToFriendly[id]; ok { + return v, nil + } + user, err := m.slackApi.GetUserInfo(id) + if err != nil { + return "", err + } + m.slackIdToFriendly[id] = user.Profile.DisplayName + return user.Profile.DisplayName, nil +} + +func (m *BotModel) GetSlackIMName(id string) (string, error) { + if v, ok := m.slackIdToFriendly[id]; ok { + return v, nil + } + c, err := m.slackApi.GetChannelInfo(id) + if err != nil { + return "", err + } + m.slackIdToFriendly[id] = c.Name + return c.Name, nil +} + +func (m *BotModel) GetSlackChannelName(id string) (string, error) { + if v, ok := m.slackIdToFriendly[id]; ok { + return v, nil + } + c, err := m.slackApi.GetChannelInfo(id) + if err != nil { + return "", err + } + m.slackIdToFriendly[id] = c.Name + return c.Name, nil +} + +func (m *BotModel) GetSlackUserIM(id string) (string, error) { + for k, v := range m.slackIdToFriendly { + if v == id { + return k, nil + } + } + _, _, newId, err := m.slackApi.OpenIMChannel(id) + if err != nil { + return "", err + } + m.slackIdToFriendly[id] = newId + return newId, nil +} + +func (m *BotModel) GetSlackGroupName(id string) (string, error) { + if v, ok := m.slackIdToFriendly[id]; ok { + return v, nil + } + g, err := m.slackApi.GetGroupInfo(id) + if err != nil { + return "", err + } + m.slackIdToFriendly[id] = g.Name + return g.Name, nil +} + +func (m *BotModel) PostSlackMessage(msg *slack.Message) { + m.slackRTM.SendMessage(m.slackRTM.NewOutgoingMessage(msg.Text, msg.Channel)) +} + +func (m *BotModel) SendMessageToUser(msg *slack.Message, uid string) error { + dmId, err := m.GetSlackUserIM(uid) + if err != nil { + return err + } + msg.Channel = dmId + m.PostSlackMessage(msg) + return nil +} + +func (m *BotModel) GetSlackUserInfo(uid string) (*slack.User, error) { + return m.slackApi.GetUserInfo(uid) +} + +func (m *BotModel) GetSlackLatency() time.Duration { + return m.slackRTMLatency +} diff --git a/interfaces.go b/interfaces.go index 651bf16..a62b89a 100644 --- a/interfaces.go +++ b/interfaces.go @@ -1,10 +1,15 @@ package helperbot -import "github.com/nlopes/slack" +import ( + "time" + + "github.com/nlopes/slack" +) type Model interface { SendMessage(src, dest string, message slack.Message) GetSlackAdminDMId() (string, error) + GetSlackLatency() time.Duration GetBytes(path []string) ([]byte, error) SetBytes(path []string, val []byte) error GetString(path []string) (string, error) @@ -21,6 +26,7 @@ type PluginState interface { Name() string Initialize(Model) error ProcessMessage(Message) + ProcessRTMEvent(slack.RTMEvent) Run() Exit() } diff --git a/package.sh b/package.sh index b7938cb..405e138 100755 --- a/package.sh +++ b/package.sh @@ -4,5 +4,5 @@ rm helperbot.tgz ./buildplugins.sh cd cmd go build -o helperbot -tar -zcvf ../helperbot.tgz helperbot* plugins +tar -zcvf ../helperbot.tgz helperbot helperbot.service plugins cd .. diff --git a/plugins_src/plugin_aoc.go b/plugins_src/plugin_aoc.go index b7efa6a..8afa77b 100644 --- a/plugins_src/plugin_aoc.go +++ b/plugins_src/plugin_aoc.go @@ -25,7 +25,8 @@ type AoCState struct { sessionCookie string sessionNeedsUpdate bool - aoc *aoc.AoC + aoc *aoc.AoC + lastYear int } var State AoCState @@ -71,7 +72,7 @@ func (s *AoCState) ProcessMessage(m helperbot.Message) { return } switch slackMsg.Channel[0] { - case 'C': + case 'C', 'G': s.ProcessChannelMessage(slackMsg) case 'D': admin, err := s.model.GetSlackAdminDMId() @@ -88,6 +89,8 @@ func (s *AoCState) ProcessMessage(m helperbot.Message) { } } +func (s *AoCState) ProcessRTMEvent(msg slack.RTMEvent) {} + func (s *AoCState) Run() { go s.runLoop() } @@ -113,12 +116,18 @@ func (s *AoCState) NewAoC() error { func (s *AoCState) runLoop() { for { _, err := s.getChannelId() + // This plugin fails without a channel id if err != nil { - // This plugin fails without a channel id return } for _, yr := range s.GetListOfAoCYears() { if !s.sessionNeedsUpdate { + if s.GetLatestYear() != s.lastYear { + // Latest year changed. Grab that board first. + s.lastYear = s.GetLatestYear() + s.AoCBoardCheckAndUpdate(s.lastYear) + time.Sleep(time.Minute) + } s.AoCBoardCheckAndUpdate(yr) time.Sleep(time.Minute) } @@ -385,6 +394,20 @@ func (s *AoCState) AoCBoardNeedsUpdate(yr int) bool { return time.Since(l.LastFetch) > freshDt } +func (s *AoCState) AoCBoardIsNew(yr int) bool { + l, err := s.aoc.GetCachedLeaderboard(yr) + fmt.Println("Checking if board is new") + if err != nil || l == nil { + if err.Error() == "Invalid Year" { + return false + } + fmt.Println("YUP!") + + return true + } + return false +} + func (s *AoCState) RequestBoardId() { reader := bufio.NewReader(os.Stdin) fmt.Print("Advent of Code Board ID: ") diff --git a/plugins_src/plugin_stats.go b/plugins_src/plugin_stats.go new file mode 100644 index 0000000..801b8c2 --- /dev/null +++ b/plugins_src/plugin_stats.go @@ -0,0 +1,89 @@ +package main + +import ( + "C" +) +import ( + "strings" + "time" + + "git.bullercodeworks.com/brian/helperbot" + "github.com/nlopes/slack" +) + +type StatsState struct { + model helperbot.Model + lag time.Duration +} + +var State StatsState + +/* Plugin Interface Functions */ +func (s *StatsState) Name() string { return "slack-stats" } +func (s *StatsState) Initialize(m helperbot.Model) error { + s.model = m + return nil +} + +func (s *StatsState) ProcessMessage(m helperbot.Message) { + if m.GetSource() == "slack" { + slackMsg := m.GetMessage() + if len(slackMsg.Channel) == 0 { + return + } + admin, err := s.model.GetSlackAdminDMId() + if err != nil { + s.SendAdminIdError() + return + } + if slackMsg.Channel == admin { + s.ProcessAdminDirectMessage(slackMsg) + } + } +} + +func (s *StatsState) ProcessRTMEvent(msg slack.RTMEvent) { + switch ev := msg.Data.(type) { + case *slack.LatencyReport: + s.lag = ev.Value + } +} + +func (s *StatsState) Run() {} +func (s *StatsState) Exit() {} + +/* Other Functions */ +func (s *StatsState) ProcessAdminDirectMessage(slackMsg slack.Message) { + msgPts := strings.Fields(slackMsg.Text) + if len(msgPts) < 2 || msgPts[0] != "!stats" { + return + } + switch msgPts[1] { + case "lag", "latency": + s.DoLatencyCommand(slackMsg) + } +} + +func (s *StatsState) DoLatencyCommand(slackMsg slack.Message) { + if s.lag == 0 { + s.SendSlackMessage("Unknown", slackMsg.Channel) + return + } + s.SendSlackMessage(s.lag.String(), slackMsg.Channel) +} + +func (s *StatsState) SendSlackMessage(text, dest string) { + s.model.SendMessage(s.Name(), "slack", s.BuildMessage("message", text, dest)) +} + +func (s *StatsState) SendAdminIdError() { + s.model.SendMessage(s.Name(), "error", s.BuildMessage("error", "Error getting Admin DM Id", "")) +} + +func (s *StatsState) BuildMessage(tp, text, ch string) slack.Message { + ret := slack.Message{} + ret.Type = tp + ret.Text = text + ret.Channel = ch + return ret +}