Initial Commit

This commit is contained in:
Brian Buller 2019-11-12 18:45:56 -06:00
commit ac3783e47b
11 changed files with 859 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
cmd/plugins/*
cmd/helperbot
cmd/*.db

11
buildplugins.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
cd plugins_src
for i in `ls`; do
echo "Building plugin: $i"
go build -buildmode=plugin $i
if [ $? -eq 0 ]; then
mv *.so ../cmd/plugins
fi
done
cd ..

91
cmd/app.go Normal file
View File

@ -0,0 +1,91 @@
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
type App struct {
DebugMode bool
running bool
m *BotModel
plugins []HelperPlugin
}
func NewApp() (*App, error) {
a := new(App)
if DebugMode {
fmt.Println("Running in Debug Mode. All messages will be sent to Admin DM")
}
a.DebugMode = DebugMode
err := a.initialize()
if err != nil {
return nil, err
}
a.running = true
go a.MonitorSlackMessages()
return a, nil
}
func (a *App) initialize() error {
var err error
if a.m, err = NewBotModel(); err != nil {
return err
}
reader := bufio.NewReader(os.Stdin)
// Load up the plugins
pluginDir := strings.TrimSpace(a.m.getPluginDir())
a.LoadPluginsFromDirectory(pluginDir)
if err != nil {
fmt.Println("Error loading plugins")
fmt.Println(err.Error())
os.Exit(1)
}
// Now initialize the Slack stuff
var slackToken, slackDMid string
slackToken, err = a.m.getSlackToken()
if err != nil || slackToken == "" {
fmt.Print("Slack API Token: ")
slackToken, _ = reader.ReadString('\n')
a.m.setSlackToken(strings.TrimSpace(slackToken))
}
slackDMid, err = a.m.getSlackAdminDMId()
if err != nil || slackDMid == "" {
fmt.Print("Slack Admin DM ID: ")
slackDMid, _ = reader.ReadString('\n')
a.m.setSlackAdminDMId(strings.TrimSpace(slackDMid))
}
if err = a.m.NewSlack(); err != nil {
return err
}
go a.watchMessageChannel()
return nil
}
func (a *App) watchMessageChannel() {
for a.running {
msg := <-a.m.messages
slackMsg := msg.GetMessage()
if slackMsg.Type == "control" && slackMsg.Name == "quit" {
a.running = false
break
} else if msg.GetDestination() == "slack" {
a.m.SendSlackChannelMessage(&slackMsg)
}
for _, v := range a.plugins {
v.State.ProcessMessage(msg)
}
}
close(a.m.messages)
}

58
cmd/helper_plugin.go Normal file
View File

@ -0,0 +1,58 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"plugin"
"git.bullercodeworks.com/brian/helperbot"
)
type HelperPlugin struct {
p *plugin.Plugin
State helperbot.PluginState
}
func (a *App) LoadPluginsFromDirectory(dir string) error {
fmt.Println("Loading Plugins (", dir, ")")
files, err := ioutil.ReadDir(dir)
if err != nil {
fmt.Println("Error loading plugins")
fmt.Println(err.Error())
os.Exit(1)
}
for _, f := range files {
p, err := plugin.Open(dir + f.Name())
if err != nil {
fmt.Println(fmt.Sprintf("Error loading plugin (%s)\n", f.Name()))
fmt.Println(err.Error())
os.Exit(1)
}
hp, err := NewHelperPlugin(p)
if err != nil {
fmt.Println(fmt.Sprintf("Error loading plugin (%s)\n", f.Name()))
fmt.Println(err.Error())
os.Exit(1)
}
hp.State.Initialize(a.m)
hp.State.Run()
a.plugins = append(a.plugins, *hp)
}
return nil
}
func NewHelperPlugin(p *plugin.Plugin) (*HelperPlugin, error) {
h := &HelperPlugin{
p: p,
}
// Parse the plugin's state
pluginStateSymbol, err := p.Lookup("State")
if err != nil {
return nil, err
}
h.State = pluginStateSymbol.(helperbot.PluginState)
return h, nil
}

14
cmd/helpers.go Normal file
View File

@ -0,0 +1,14 @@
package main
import (
"encoding/json"
slack "git.bullercodeworks.com/brian/go-slack"
)
func GetMessageJson(msg *slack.Message) string {
if mb, me := json.Marshal(msg); me == nil {
return string(mb)
}
return ""
}

49
cmd/main.go Normal file
View File

@ -0,0 +1,49 @@
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
slack "git.bullercodeworks.com/brian/go-slack"
)
var DebugMode = false
var a *App
func main() {
if len(os.Args) > 1 {
if os.Args[1] == "-debug" || os.Args[1] == "--debug" {
DebugMode = true
}
}
a, err := NewApp()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Monitor the Advent of Code Boards
//go m.MonitorAoCBoards()
// Monitor incoming Slack messages
//go m.MonitorSlackMessages()
// Set up a channel to intercept Ctrl+C for graceful shutdowns
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
// Save the changes when the app quits
fmt.Println("\nFinishing up...")
a.m.messages <- NewBotMessage("main", "main", slack.Message{Type: "control", Name: "quit"})
}()
for a.running {
time.Sleep(time.Second * 2)
}
fmt.Println("Model has stopped running")
fmt.Println("Done")
os.Exit(0)
}

21
cmd/message.go Normal file
View File

@ -0,0 +1,21 @@
package main
import slack "git.bullercodeworks.com/brian/go-slack"
// This message type is for communications over the messages channel
type BotMessage struct {
source string
dest string
message slack.Message
}
func NewBotMessage(src, dst string, msg slack.Message) BotMessage {
return BotMessage{
source: src,
dest: dst,
message: msg,
}
}
func (m BotMessage) GetSource() string { return m.source }
func (m BotMessage) GetDestination() string { return m.dest }
func (m BotMessage) GetMessage() slack.Message { return m.message }

101
cmd/model.go Normal file
View File

@ -0,0 +1,101 @@
package main
import (
"errors"
"fmt"
"os"
"strings"
slack "git.bullercodeworks.com/brian/go-slack"
"git.bullercodeworks.com/brian/helperbot"
"github.com/br0xen/boltease"
)
type BotModel struct {
db *boltease.DB
messages chan helperbot.Message
slack *slack.Slack
cache map[string][]byte
}
func NewBotModel() (*BotModel, error) {
var err error
m := new(BotModel)
m.cache = 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
}
m.db.MkBucketPath([]string{"slack", "users"})
m.db.MkBucketPath([]string{"slack", "channels"})
return m, nil
}
func (m *BotModel) SendMessage(src, dst string, msg slack.Message) {
m.messages <- NewBotMessage(src, dst, msg)
}
func (m *BotModel) getPluginDir() string {
ret, err := m.GetString([]string{"config", "plugin_dir"})
if err != nil || strings.TrimSpace(ret) == "" {
ret = "./plugins/"
if err = m.SetString([]string{"config", "plugin_dir"}, ret); err != nil {
fmt.Println("Error setting plugin directory")
fmt.Println(err.Error())
os.Exit(1)
}
}
fmt.Println("Plugin Dir: ", ret)
return ret
}
func (m *BotModel) GetBytes(path []string) ([]byte, error) {
var err error
var v []byte
var ok bool
joinedPath := strings.Join(path, "/")
if v, ok = m.cache[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]
v, err = m.db.GetBytes(path, key)
if err != nil {
return nil, err
}
m.cache[joinedPath] = v
}
}
return v, nil
}
func (m *BotModel) SetBytes(path []string, val []byte) error {
if len(path) > 1 {
joinedPath := strings.Join(path, "/")
path, key := path[:len(path)-1], path[len(path)-1]
err := m.db.SetBytes(path, key, val)
if err != nil {
return err
}
// Update the cache
m.cache[joinedPath] = val
return nil
}
return errors.New("Invalid path")
}
func (m *BotModel) GetString(path []string) (string, error) {
bts, err := m.GetBytes(path)
if err != nil {
return "", err
}
return string(bts), nil
}
func (m *BotModel) SetString(path []string, val string) error {
return m.SetBytes(path, []byte(val))
}

77
cmd/model_slack.go Normal file
View File

@ -0,0 +1,77 @@
package main
import (
slack "git.bullercodeworks.com/brian/go-slack"
)
/* DB Functions */
func (m *BotModel) setSlackToken(token string) error {
return m.SetBytes([]string{"slack", "config", "token"}, []byte(token))
}
func (m *BotModel) getSlackToken() (string, error) {
return m.GetString([]string{"slack", "config", "token"})
}
func (m *BotModel) setSlackAdminDMId(adminId string) error {
return m.SetString([]string{"slack", "config", "admin_dm_id"}, adminId)
}
func (m *BotModel) getSlackAdminDMId() (string, error) {
return m.GetString([]string{"slack", "config", "admin_dm_id"})
}
func (m *BotModel) setSlackChannelId(chanId string) error {
return m.SetString([]string{"slack", "config", "channel_id"}, chanId)
}
func (m *BotModel) getSlackChannelId() (string, error) {
return m.GetString([]string{"slack", "config", "channel_id"})
}
/* End DB Functions */
func (m *BotModel) NewSlack() error {
token, err := m.getSlackToken()
if err != nil {
return err
}
if m.slack, err = slack.CreateSlack(token); err != nil {
return err
}
return nil
}
func (a *App) MonitorSlackMessages() {
for a.running {
msg, err := a.m.slack.GetMessage()
if err == nil {
a.m.SendMessage("slack", "main", msg)
}
}
}
func (m *BotModel) SendSlackChannelMessage(msg *slack.Message) error {
if DebugMode {
return m.SendSlackAdminMessage(msg)
}
// Send message to slack channel
var err error
if err = m.slack.PostMessage(*msg); err != nil {
return err
}
return nil
}
func (m *BotModel) SendSlackAdminMessage(msg *slack.Message) error {
// Send message to slack admin
var err error
msg.Channel, err = m.getSlackAdminDMId()
if err != nil {
return err
}
if err = m.slack.PostMessage(*msg); err != nil {
return err
}
return nil
}

25
interfaces.go Normal file
View File

@ -0,0 +1,25 @@
package helperbot
import slack "git.bullercodeworks.com/brian/go-slack"
type Model interface {
SendMessage(src, dest string, message slack.Message)
GetBytes(path []string) ([]byte, error)
SetBytes(path []string, val []byte) error
GetString(path []string) (string, error)
SetString(path []string, val string) error
}
type Message interface {
GetSource() string
GetDestination() string
GetMessage() slack.Message
}
type PluginState interface {
Name() string
Initialize(Model) error
ProcessMessage(Message)
Run()
Exit()
}

409
plugins_src/plugin_aoc.go Normal file
View File

@ -0,0 +1,409 @@
package main
import (
"C"
)
import (
"bufio"
"errors"
"fmt"
"os"
"sort"
"strconv"
"strings"
"time"
aoc "git.bullercodeworks.com/brian/go-adventofcode"
slack "git.bullercodeworks.com/brian/go-slack"
"git.bullercodeworks.com/brian/helperbot"
)
/* Plugin State */
type AoCState struct {
model helperbot.Model
boardId string
sessionCookie string
aoc *aoc.AoC
}
var State AoCState
/* Plugin Interface Functions */
func (s *AoCState) Name() string { return "advent-of-code" }
func (s *AoCState) Initialize(m helperbot.Model) 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()
}
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 helperbot.Message) {
if m.GetSource() == "slack" {
slackMsg := m.GetMessage()
msgPts := strings.Fields(slackMsg.Text)
if len(msgPts) < 1 {
return
}
if msgPts[0] != "!aoc" || len(msgPts) < 2 {
return
}
msgPts = msgPts[1:]
yr, err := strconv.Atoi(msgPts[0])
if err != nil {
yr = s.GetLatestYear()
} else {
msgPts = msgPts[1:]
}
switch msgPts[0] {
case "ping":
s.model.SendMessage(s.Name(), "slack", slack.Message{
Type: "message",
Channel: slackMsg.Channel,
Text: ":christmas_tree: PONG :christmas_tree:",
})
case "top":
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.model.SendMessage(s.Name(), "slack", slack.Message{
Type: "message",
Channel: slackMsg.Channel,
Text: txt,
})
return
}
}
}
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() {
for {
channelId, err := s.getChannelId()
if err != nil {
// This plugin fails without a channel id
return
}
for _, yr := range s.GetListOfAoCYears() {
l, err := s.aoc.GetLeaderboard(yr)
if err != nil {
msg := slack.Message{
Type: "error",
Text: fmt.Sprintf("Error processing leaderboard (%d)", yr),
Time: time.Now(),
}
s.model.SendMessage(s.Name(), "main", msg)
} else {
msg := slack.Message{
Type: "success",
Text: fmt.Sprintf("Received leaderboard (%d)", yr),
Time: time.Now(),
}
s.model.SendMessage(s.Name(), "main", msg)
// 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 {
s.model.SendMessage(s.Name(), "slack", slack.Message{
Type: "message",
Channel: channelId,
Text: ":christmas_tree: " + v.Name + " now has " + strconv.Itoa(v.Stars) + " stars! :christmas_tree:",
})
}
}
// Save the leaderboard to the db
s.saveLeaderboard(l)
}
time.Sleep(time.Minute)
}
time.Sleep(time.Minute * 10)
}
s.model.SendMessage(s.Name(), "main", slack.Message{
Type: "status",
Text: "done",
Time: time.Now(),
})
}
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) 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))
}
// 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))
}
/* 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.SetString([]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(s.Name(), "main", slack.Message{
Type: "error",
Text: fmt.Sprintf("Error Saving Member (%s)", v.Name),
Time: time.Now(),
})
}
}
return nil
}
func (s *AoCState) saveMember(event string, m *aoc.Member) error {
err := s.model.SetString([]string{"aoc", "leaderboards", event, "members", m.ID, "id"}, m.ID)
if err != nil {
return err
}
err = s.model.SetString([]string{"aoc", "leaderboards", event, "members", m.ID, "stars"}, strconv.Itoa(m.Stars))
if err != nil {
return err
}
err = s.model.SetString([]string{"aoc", "leaderboards", event, "members", m.ID, "last_star_ts"}, m.LastStarTs.Format(time.RFC3339))
if err != nil {
return err
}
err = s.model.SetString([]string{"aoc", "leaderboards", event, "members", m.ID, "name"}, m.Name)
if err != nil {
return err
}
err = s.model.SetString([]string{"aoc", "leaderboards", event, "members", m.ID, "local_score"}, strconv.Itoa(m.LocalScore))
if err != nil {
return err
}
err = s.model.SetString([]string{"aoc", "leaderboards", event, "members", m.ID, "global_score"}, strconv.Itoa(m.GlobalScore))
if err != nil {
return err
}
return nil
}
func (s *AoCState) getMember(event string, memberId string) (*aoc.Member, error) {
var err error
var wrk string
mbr := new(aoc.Member)
mbrPath := []string{"aoc", "leaderboards", event, "members", memberId}
mbr.ID, err = s.model.GetString(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
}