package plugins import ( "bufio" "errors" "fmt" "os" "sort" "strconv" "strings" "time" aoc "git.bullercodeworks.com/brian/go-adventofcode" "git.bullercodeworks.com/brian/helperbot/models" "github.com/slack-go/slack" ) /* Plugin State */ type AoCState struct { model *models.BotModel boardId string sessionCookie string sessionNeedsUpdate bool lastUpdate time.Time aoc *aoc.AoC lastYear int } /* Plugin Interface Functions */ func (s *AoCState) Name() string { return "advent-of-code" } func (s *AoCState) Initialize(m *models.BotModel) error { // Initialize AoC stuff var err error var boardId, aocSession, aocChannelId string s.model = m boardId, err = s.getAoCBoardId() boardId = strings.TrimSpace(boardId) if err != nil || boardId == "" { s.RequestBoardId() } aocSession, err = s.getAoCSessionCookie() aocSession = strings.TrimSpace(aocSession) if err != nil || aocSession == "" { s.RequestSessionCookie() } else { s.sessionCookie = aocSession } aocChannelId, err = s.getChannelId() aocChannelId = strings.TrimSpace(aocChannelId) if err != nil || aocChannelId == "" { s.RequestChannelId() } if err = s.NewAoC(); err != nil { return err } return nil } func (s *AoCState) ProcessMessage(m models.BotMessage) bool { if m.Source == models.MsgSrcSlack { if len(m.Target) == 0 { return false } switch m.Target[0] { case 'C', 'G': return s.ProcessChannelMessage(m) case 'D': admin := s.model.GetSlackAdminDMId() if m.Target == admin { return s.ProcessAdminDirectMessage(m) } else { return s.ProcessDirectMessage(m) } } } return false } func (s *AoCState) ProcessRTMEvent(msg *slack.RTMEvent) bool { return false } func (s *AoCState) Run() { go s.runLoop() } func (s *AoCState) Exit() {} /* Other Functions */ func (s *AoCState) NewAoC() error { board, err := s.getAoCBoardId() if err != nil { return err } sess, err := s.getAoCSessionCookie() if err != nil { return err } if s.aoc, err = aoc.NewAoC(board, sess); err != nil { return err } return nil } func (s *AoCState) runLoop() { _, err := s.getChannelId() // This plugin fails without a channel id if err != nil { return } for s.model.Running { _, err := s.getChannelId() // This plugin fails without a channel id if err != nil { return } if !s.sessionNeedsUpdate { if s.GetLatestYear() != s.lastYear { // Latest year changed. Update it. s.lastYear = s.GetLatestYear() admin := s.model.GetSlackAdminDMId() if admin == "" { s.SendAdminIdError() return } s.SendSlackMessage(fmt.Sprintf(":christmas_tree: AoC Set latest leaderboard to %d", s.lastYear), admin) } s.AoCBoardCheckAndUpdate(s.lastYear) s.lastUpdate = time.Now() } time.Sleep(time.Minute * 15) } s.model.SendMessage(models.NewBotMessage(s.Name(), models.MsgSrcApp, "status", "done", "")) } func (s *AoCState) ProcessDirectMessage(m models.BotMessage) bool { msgPts := strings.Fields(m.Text) if len(msgPts) < 2 || msgPts[0] != "!aoc" { return false } switch msgPts[1] { case "help": return s.DoHelpCmd(m) case "ping": return s.DoPingCmd(m) case "top": return s.DoTopCmd(m) } return false } func (s *AoCState) ProcessAdminDirectMessage(m models.BotMessage) bool { msgPts := strings.Fields(m.Text) if len(msgPts) < 2 || msgPts[0] != "!aoc" { return false } switch msgPts[1] { case "help": s.DoHelpAdminCmd(m) return true case "ping": s.DoPingCmd(m) return true case "top": s.DoTopCmd(m) return true case "session": s.DoSessionCmd(m) return true case "status": s.DoStatusCmd(m) return true } return false } func (s *AoCState) ProcessChannelMessage(m models.BotMessage) bool { msgPts := strings.Fields(m.Text) if len(msgPts) < 2 || msgPts[0] != "!aoc" { return false } switch msgPts[1] { case "top": return s.DoTopCmd(m) } return false } func (s *AoCState) DoHelpCmd(m models.BotMessage) bool { txt := fmt.Sprint(":christmas_tree: AoC Help :christmas_tree:\n", "• help - Print this message\n", "• ping - PONG! Check if the service is running\n", "• top [year] - Print the top 5 users for the passed year\n", " If no year is passed, the current year is used\n", " Passing 'all' considers all years\n", ) s.SendSlackMessage(txt, m.Target) return true } func (s *AoCState) DoHelpAdminCmd(m models.BotMessage) bool { txt := fmt.Sprint(":christmas_tree: AoC Help :christmas_tree:\n", "• help - Print this message\n", "• ping - PONG! Check if the service is running\n", "• top [year] - Print the top 5 users for the passed year\n", " If no year is passed, the current year is used\n", " Passing 'all' considers all years\n", "• session [token] - Print or set the session cookie\n", "• status - Print the status of the service\n", ) s.SendSlackMessage(txt, m.Target) return true } func (s *AoCState) DoPingCmd(m models.BotMessage) bool { s.SendSlackMessage(":christmas_tree: PONG :christmas_tree:", m.Target) return true } func (s *AoCState) DoSessionCmd(m models.BotMessage) bool { msgPts := strings.Fields(m.Text) if len(msgPts) == 3 && msgPts[1] == "session" { // Set the session cookie admin := s.model.GetSlackAdminDMId() if admin == "" { s.SendAdminIdError() return true } aocSession := msgPts[2] s.setAoCSessionCookie(strings.TrimSpace(aocSession)) s.sessionCookie = aocSession s.sessionNeedsUpdate = false s.SendSlackMessage(":christmas_tree: New Session: "+s.sessionCookie, admin) } else if len(msgPts) == 2 && msgPts[1] == "session" { // Print the session cookie admin := s.model.GetSlackAdminDMId() if admin == "" { s.SendAdminIdError() return true } // We only send the session cookie to the admin s.SendSlackMessage(":christmas_tree: session: "+s.sessionCookie, admin) } return true } func (s *AoCState) DoStatusCmd(m models.BotMessage) bool { admin := s.model.GetSlackAdminDMId() if admin == "" { s.SendAdminIdError() return true } txt := ":christmas_tree: Advent of Code Status :christmas_tree:" txt = fmt.Sprintf( "%s\nLast Update: %s\nNext Update In: %s", txt, s.lastUpdate.Format(time.RFC3339), s.lastUpdate.Add(time.Minute*15).Sub(time.Now()).Round(time.Second), ) s.SendSlackMessage(txt, m.Target) return false } func (s *AoCState) DoTopCmd(m models.BotMessage) bool { msgPts := strings.Fields(m.Text) var err error var yr int if len(msgPts) > 2 { yr, err = strconv.Atoi(msgPts[2]) if err != nil { if msgPts[2] == "all" { yr = -1 } } } else { yr = s.GetLatestYear() } var txt string if yr == -1 { var err error txt, err = s.DoTopForAll() if err != nil { txt = "Error calculating all-time tops" } } else { txt = s.DoTopForYear(yr) } s.SendSlackMessage(txt, m.Target) return true } func (s *AoCState) DoTopForAll() (string, error) { mbrMap := make(map[string]aoc.Member) for _, yr := range s.GetListOfAoCYears() { l, err := s.aoc.GetLeaderboard(yr) if err != nil { return "", err } for k, v := range l.Members { if m, ok := mbrMap[k]; ok { m.Stars = m.Stars + v.Stars m.LocalScore = m.LocalScore + v.LocalScore mbrMap[k] = m } else { mbrMap[k] = v } } } var mbrs []aoc.Member for _, v := range mbrMap { mbrs = append(mbrs, v) } if len(mbrs) == 0 { return "", errors.New("No member data") } sort.Sort(aoc.ByStarsThenScore(mbrs)) txt := ":christmas_tree: AoC All-Time Top Five! :christmas_tree:" var num int for k := 0; k < len(mbrs); k++ { v := mbrs[len(mbrs)-k-1] txt = fmt.Sprintf("%s\n%s (%d :star:, %d)", txt, v.Name, v.Stars, v.LocalScore) num++ if num >= 5 { break } } return txt, nil } func (s *AoCState) DoTopForYear(yr int) string { mbrs := s.GetMembers(yr) if len(mbrs) == 0 { return "No data for that year" } sort.Sort(aoc.ByStarsThenScore(mbrs)) txt := fmt.Sprintf(":christmas_tree: AoC Top Five for %d! :christmas_tree:", yr) var num int for k := 0; k < len(mbrs); k++ { v := mbrs[len(mbrs)-k-1] txt = fmt.Sprintf("%s\n%s (%d :star:, %d)", txt, v.Name, v.Stars, v.LocalScore) num++ if num >= 5 { break } } return txt } func (s *AoCState) GetMembers(yr int) []aoc.Member { var ret []aoc.Member l, err := s.aoc.GetLeaderboard(yr) if err != nil { return ret } for _, v := range l.Members { ret = append(ret, v) } return ret } func (s *AoCState) GetLatestYear() int { latestYear := time.Now().Year() if time.Now().Month() < 12 { latestYear-- } return latestYear } func (s *AoCState) GetListOfAoCYears() []int { var ret []int for k := s.GetLatestYear(); k > 2014; k-- { ret = append(ret, k) } return ret } func (s *AoCState) GetAoCBoard(yr int) (*aoc.Leaderboard, error) { return s.aoc.GetLeaderboard(yr) } func (s *AoCState) AoCSilentBoardCheckAndUpdate(yr int) { if s.AoCBoardNeedsUpdate(yr) { l, err := s.aoc.GetLeaderboard(yr) if err != nil { admin := s.model.GetSlackAdminDMId() if admin == "" { s.SendAdminIdError() return } if err.Error() == "Invalid Session Cookie" { s.sessionNeedsUpdate = true } s.SendSlackMessage(fmt.Sprintf(":warning: AoC Error processing leaderboard (%d) - %s", yr, err.Error()), admin) return } s.model.SendMessage(models.NewBotMessage(s.Name(), models.MsgSrcApp, models.MsgTpStatus, "", fmt.Sprintf("Received leaderboard (%d)", yr))) // Save the leaderboard to the db s.saveLeaderboard(l) } } func (s *AoCState) AoCBoardCheckAndUpdate(yr int) { stYr := strconv.Itoa(yr) channelId, _ := s.getChannelId() if s.AoCBoardNeedsUpdate(yr) { l, err := s.aoc.GetLeaderboard(yr) if err != nil { admin := s.model.GetSlackAdminDMId() if admin == "" { s.SendAdminIdError() return } if err.Error() == "Invalid Session Cookie" { s.sessionNeedsUpdate = true } s.SendSlackMessage(fmt.Sprintf(":warning: AoC Error processing leaderboard (%d) - %s", yr, err.Error()), admin) return } s.model.SendMessage(models.NewBotMessage(s.Name(), models.MsgSrcApp, models.MsgTpStatus, "", fmt.Sprintf("Received leaderboard (%d)", yr))) // Compare the new leaderboard to the saved one for _, v := range l.Members { mbr, err := s.getMember(l.Event, v.ID) if err != nil { continue } if mbr.Stars != v.Stars { plural := "star" if v.Stars > 1 { plural = "stars" } mbrAll := s.getMemberAllYears(v.ID) // Gather all stars from all leaderboards var totalStars int for k, m := range mbrAll { if k != stYr { totalStars += m.Stars } } // Add this new one into the total allYearsText := fmt.Sprintf(" (%d for all years)", totalStars+v.Stars) if yr == s.GetLatestYear() { s.SendSlackMessage(fmt.Sprintf(":christmas_tree: %s now has %d %s! :christmas_tree:%s", v.Name, v.Stars, plural, allYearsText), channelId) } else { s.SendSlackMessage(fmt.Sprintf(":christmas_tree: %s now has %d %s! (%d) :christmas_tree:%s", v.Name, v.Stars, plural, yr, allYearsText), channelId) } } } // Save the leaderboard to the db s.saveLeaderboard(l) } } func (s *AoCState) AoCBoardNeedsUpdate(yr int) bool { var freshDt time.Duration if yr == s.GetLatestYear() { freshDt, _ = time.ParseDuration("15m") // AoC asks that we not update more than once every 15 minutes } else { freshDt, _ = time.ParseDuration("1h") } l, err := s.aoc.GetCachedLeaderboard(yr) if err != nil { if err.Error() == "Invalid Year" { return false } // This board is not cached, we need to pull it return true } return time.Since(l.LastFetch) > freshDt } func (s *AoCState) AoCBoardIsNew(yr int) bool { l, err := s.aoc.GetCachedLeaderboard(yr) if err != nil || l == nil { if err.Error() == "Invalid Year" { return false } return true } return false } func (s *AoCState) RequestBoardId() { reader := bufio.NewReader(os.Stdin) fmt.Print("Advent of Code Board ID: ") boardId, _ := reader.ReadString('\n') s.setAoCBoardId(strings.TrimSpace(boardId)) } func (s *AoCState) RequestSessionCookie() { reader := bufio.NewReader(os.Stdin) fmt.Print("Advent of Code Session Cookie: ") aocSession, _ := reader.ReadString('\n') s.setAoCSessionCookie(strings.TrimSpace(aocSession)) s.sessionCookie = aocSession } // The channel that we post updates to func (s *AoCState) RequestChannelId() { reader := bufio.NewReader(os.Stdin) fmt.Print("Advent of Code Slack Channel ID: ") chn, _ := reader.ReadString('\n') s.setChannelId(strings.TrimSpace(chn)) } // Returns true if we have all AoC years saved to the DB func (s *AoCState) haveAllYearsCached() bool { for _, yr := range s.GetListOfAoCYears() { _, err := s.aoc.GetCachedLeaderboard(yr) if err != nil { return false } } return true } /* DB Functions */ func (s *AoCState) setAoCBoardId(brdId string) error { return s.model.SetString([]string{"aoc", "config", "board_id"}, brdId) } func (s *AoCState) getAoCBoardId() (string, error) { return s.model.GetString([]string{"aoc", "config", "board_id"}) } func (s *AoCState) setAoCSessionCookie(sess string) error { return s.model.SetString([]string{"aoc", "config", "session"}, sess) } func (s *AoCState) getAoCSessionCookie() (string, error) { return s.model.GetString([]string{"aoc", "config", "session"}) } func (s *AoCState) setChannelId(chn string) error { return s.model.SetString([]string{"aoc", "config", "channel_id"}, chn) } func (s *AoCState) getChannelId() (string, error) { return s.model.GetString([]string{"aoc", "config", "channel_id"}) } func (s *AoCState) saveLeaderboard(l *aoc.Leaderboard) error { err := s.model.SetInt([]string{"aoc", "leaderboards", l.Event, "owner_id"}, l.OwnerID) if err != nil { return err } err = s.model.SetString([]string{"aoc", "leaderboards", l.Event, "last_fetch"}, l.LastFetch.Format(time.RFC3339)) if err != nil { return err } for _, v := range l.Members { if err = s.saveMember(l.Event, &v); err != nil { s.model.SendMessage(models.NewErrorBotMessage(s.Name(), models.MsgSrcApp, fmt.Sprintf("Error Saving Member (%s)", v.Name))) } } return nil } func (s *AoCState) saveMember(event string, m *aoc.Member) error { strId := strconv.Itoa(m.ID) err := s.model.SetString([]string{"aoc", "leaderboards", event, "members", strId, "id"}, strId) if err != nil { return err } err = s.model.SetString([]string{"aoc", "leaderboards", event, "members", strId, "stars"}, strconv.Itoa(m.Stars)) if err != nil { return err } err = s.model.SetString([]string{"aoc", "leaderboards", event, "members", strId, "last_star_ts"}, m.LastStarTs.Format(time.RFC3339)) if err != nil { return err } err = s.model.SetString([]string{"aoc", "leaderboards", event, "members", strId, "name"}, m.Name) if err != nil { return err } err = s.model.SetString([]string{"aoc", "leaderboards", event, "members", strId, "local_score"}, strconv.Itoa(m.LocalScore)) if err != nil { return err } err = s.model.SetString([]string{"aoc", "leaderboards", event, "members", strId, "global_score"}, strconv.Itoa(m.GlobalScore)) if err != nil { return err } return nil } func (s *AoCState) getMemberAllYears(memberId int) map[string]*aoc.Member { ret := make(map[string]*aoc.Member) for _, yr := range s.GetListOfAoCYears() { stYr := strconv.Itoa(yr) m, err := s.getMember(stYr, memberId) if err == nil { ret[stYr] = m } } return ret } func (s *AoCState) getMember(event string, memberId int) (*aoc.Member, error) { var err error var wrk string mbr := new(aoc.Member) mbrPath := []string{"aoc", "leaderboards", event, "members", strconv.Itoa(memberId)} mbr.ID, err = s.model.GetInt(append(mbrPath, "id")) if err != nil { return nil, err } wrk, err = s.model.GetString(append(mbrPath, "stars")) if err != nil { return nil, err } mbr.Stars, err = strconv.Atoi(wrk) if err != nil { return nil, err } wrk, err = s.model.GetString(append(mbrPath, "last_star_ts")) if err != nil { return nil, err } mbr.LastStarTs, err = time.Parse(time.RFC3339, wrk) if err != nil { return nil, err } mbr.Name, err = s.model.GetString(append(mbrPath, "name")) if err != nil { return nil, err } wrk, err = s.model.GetString(append(mbrPath, "local_score")) if err != nil { return nil, err } mbr.LocalScore, err = strconv.Atoi(wrk) wrk, err = s.model.GetString(append(mbrPath, "global_score")) if err != nil { return nil, err } mbr.GlobalScore, err = strconv.Atoi(wrk) return mbr, nil } func (s *AoCState) SendAdminIdError() { s.model.SendMessage(models.NewBotMessage(s.Name(), models.MsgSrcApp, models.MsgTpError, models.MsgSrcApp, "No Admin ID Found")) } func (s *AoCState) SendSlackMessage(text, dest string) { s.model.SendMessage(models.NewBotMessage(s.Name(), models.MsgSrcSlack, models.MsgTpMessage, dest, text)) }