From b0ad5ff844f1a9eb56dfb057d9cf2670e6703e3d Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Thu, 2 Dec 2021 07:41:48 -0600 Subject: [PATCH] Accidentally ignored helperbot cmd source --- .gitignore | 4 - cmd/helperbot/app.go | 105 +++++++++++++++ cmd/helperbot/helper_plugin.go | 64 +++++++++ cmd/helperbot/helperbot.service | 16 +++ cmd/helperbot/helpers.go | 14 ++ cmd/helperbot/main.go | 47 +++++++ cmd/helperbot/message.go | 21 +++ cmd/helperbot/model.go | 111 ++++++++++++++++ cmd/helperbot/model_slack.go | 228 ++++++++++++++++++++++++++++++++ 9 files changed, 606 insertions(+), 4 deletions(-) create mode 100644 cmd/helperbot/app.go create mode 100644 cmd/helperbot/helper_plugin.go create mode 100755 cmd/helperbot/helperbot.service create mode 100644 cmd/helperbot/helpers.go create mode 100644 cmd/helperbot/main.go create mode 100644 cmd/helperbot/message.go create mode 100644 cmd/helperbot/model.go create mode 100644 cmd/helperbot/model_slack.go diff --git a/.gitignore b/.gitignore index baf4c98..70ecfa4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,3 @@ -cmd/plugins/* -cmd/helperbot -cmd/*.db -cmd/cmd helperbot.tgz build/ diff --git a/cmd/helperbot/app.go b/cmd/helperbot/app.go new file mode 100644 index 0000000..ef8dec0 --- /dev/null +++ b/cmd/helperbot/app.go @@ -0,0 +1,105 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/nlopes/slack" +) + +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 + } + bt, bterr := a.m.GetString([]string{"config", "plugin_dir"}) + if bterr == nil { + fmt.Println(bt) + } + + 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 string + // var 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)) + } + + 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 + slackMsg := msg.GetMessage() + if slackMsg.Type == "control" && slackMsg.Text == "quit" { + a.running = false + break + } else if msg.GetDestination() == "error" { + fmt.Printf("ERROR: %s: %s\n", msg.GetSource(), msg.GetMessage().Text) + } else if msg.GetDestination() == "slack" { + a.m.SendSlackChannelMessage(&slackMsg) + } + + for _, v := range a.plugins { + v.State.ProcessMessage(msg) + } + } +} + +func (a *App) watchRTMEventChannel() { + for a.running { + msg := <-a.m.OtherRTMEvents + for _, v := range a.plugins { + v.State.ProcessRTMEvent(*msg) + } + } +} diff --git a/cmd/helperbot/helper_plugin.go b/cmd/helperbot/helper_plugin.go new file mode 100644 index 0000000..8ccfa8d --- /dev/null +++ b/cmd/helperbot/helper_plugin.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "plugin" + "strings" + + "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 { + if !strings.HasSuffix(f.Name(), ".so") { + fmt.Printf("Skipping file (%s)\n", f.Name()) + continue + } + 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() + fmt.Printf("Plugin Loaded (%s)\n", f.Name()) + 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 +} diff --git a/cmd/helperbot/helperbot.service b/cmd/helperbot/helperbot.service new file mode 100755 index 0000000..e0d9865 --- /dev/null +++ b/cmd/helperbot/helperbot.service @@ -0,0 +1,16 @@ +[Unit] +Description= +After=syslog.target +After=network.target + +[Service] +Type=simple +User=brbuller +Group=brbuller +WorkingDirectory=/home/brbuller/helperbot/ +ExecStart=/home/brbuller/helperbot/helperbot +Restart=always +Environment="HOME=/home/brbuller/helperbot","USER=brbuller" + +[Install] +WantedBy=multi-user.target diff --git a/cmd/helperbot/helpers.go b/cmd/helperbot/helpers.go new file mode 100644 index 0000000..8584f3c --- /dev/null +++ b/cmd/helperbot/helpers.go @@ -0,0 +1,14 @@ +package main + +import ( + "encoding/json" + + "github.com/nlopes/slack" +) + +func GetMessageJson(msg *slack.Message) string { + if mb, me := json.Marshal(msg); me == nil { + return string(mb) + } + return "" +} diff --git a/cmd/helperbot/main.go b/cmd/helperbot/main.go new file mode 100644 index 0000000..c0880fd --- /dev/null +++ b/cmd/helperbot/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "github.com/nlopes/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) + } + + // 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...") + msg := slack.Message{} + msg.Type = "control" + msg.Text = "quit" + a.m.messages <- NewBotMessage("main", "main", msg) + }() + for a.running { + time.Sleep(time.Second * 2) + } + fmt.Println("Model has stopped running") + fmt.Println("Done") + os.Exit(0) +} diff --git a/cmd/helperbot/message.go b/cmd/helperbot/message.go new file mode 100644 index 0000000..761b166 --- /dev/null +++ b/cmd/helperbot/message.go @@ -0,0 +1,21 @@ +package main + +import "github.com/nlopes/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 } diff --git a/cmd/helperbot/model.go b/cmd/helperbot/model.go new file mode 100644 index 0000000..0e82cf3 --- /dev/null +++ b/cmd/helperbot/model.go @@ -0,0 +1,111 @@ +package main + +import ( + "errors" + "fmt" + "os" + "strings" + "time" + + "git.bullercodeworks.com/brian/helperbot" + "github.com/br0xen/boltease" + "github.com/nlopes/slack" +) + +type BotModel struct { + db *boltease.DB + + messages chan helperbot.Message + + slackApiToken string + slackApi *slack.Client + slackRTM *slack.RTM + slackRTMLatency time.Duration + slackIdToFriendly map[string]string + IncomingSlackMessages chan *slack.MessageEvent + OtherRTMEvents chan *slack.RTMEvent + + dataCache map[string][]byte +} + +func NewBotModel() (*BotModel, error) { + var err error + m := new(BotModel) + 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 +} + +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.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] + v, err = m.db.GetBytes(path, key) + if err != nil { + return nil, err + } + m.dataCache[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.dataCache[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 + } + if len(bts) == 0 { + return "", nil + } + return string(bts), nil +} + +func (m *BotModel) SetString(path []string, val string) error { + return m.SetBytes(path, []byte(val)) +} diff --git a/cmd/helperbot/model_slack.go b/cmd/helperbot/model_slack.go new file mode 100644 index 0000000..dd54e79 --- /dev/null +++ b/cmd/helperbot/model_slack.go @@ -0,0 +1,228 @@ +package main + +import ( + "bufio" + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/nlopes/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"}) +} + +/* End DB Functions */ + +func (m *BotModel) NewSlack() error { + var err error + m.slackApiToken, err = m.getSlackToken() + if err != nil { + if strings.HasPrefix(err.Error(), "Couldn't find") { + m.RequestSlackToken() + } else { + return err + } + } + var slackDMid string + slackDMid, err = m.GetSlackAdminDMId() + if err != nil || slackDMid == "" { + m.RequestAdminDMId() + } + 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 (m *BotModel) RequestAdminDMId() { + reader := bufio.NewReader(os.Stdin) + fmt.Print("Slack Admin DM ID: ") + dmId, _ := reader.ReadString('\n') + m.setSlackAdminDMId(strings.TrimSpace(dmId)) +} + +func (m *BotModel) RequestSlackToken() { + reader := bufio.NewReader(os.Stdin) + fmt.Print("Slack Token: ") + token, _ := reader.ReadString('\n') + m.slackApiToken = strings.TrimSpace(token) + m.setSlackToken(m.slackApiToken) +} + +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 + } + } +} + +func (m *BotModel) SendSlackChannelMessage(msg *slack.Message) error { + if DebugMode { + return m.SendSlackAdminMessage(msg) + } + // Send message to slack channel + m.PostSlackMessage(msg) + 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 + } + 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 +}