Helperbot V2

This commit is contained in:
2023-11-30 12:40:06 -06:00
parent 29ff2c7daf
commit de126011c4
32 changed files with 1271 additions and 899 deletions

36
models/interfaces.go Normal file
View File

@@ -0,0 +1,36 @@
package models
import (
"time"
"github.com/slack-go/slack"
)
type Model interface {
SendMessage(msg BotMessage)
GetSlackAdminDMId() string
GetSlackLatency() time.Duration
GetBytes(path []string) ([]byte, error)
SetBytes(path []string, val []byte) error
GetString(path []string) (string, error)
SetString(path []string, val string) error
GetInt(path []string) (int, error)
SetInt(path []string, val int) error
}
type Message interface {
GetSource() string
GetDestination() string
GetType() string
GetTarget() string
GetText() string
}
type PluginState interface {
Name() string
Initialize(Model) error
ProcessMessage(BotMessage) bool
ProcessRTMEvent(*slack.RTMEvent) bool
Run()
Exit()
}

89
models/message.go Normal file
View File

@@ -0,0 +1,89 @@
package models
import (
"fmt"
"github.com/slack-go/slack"
)
const (
MsgSrcApp = "app"
MsgSrcSlack = "slack"
MsgTpControl = "control"
MsgTpError = "error"
MsgTpMessage = "message"
MsgTextQuit = "quit"
)
type BotMessage struct {
Source string
Dest string
Type string
Target string
Text string
}
var QuitMessage = BotMessage{
Source: MsgSrcApp,
Dest: MsgSrcApp,
Type: MsgTpControl,
Target: MsgSrcApp,
Text: MsgTextQuit,
}
func NewBotMessage(src, dst, tp, tgt, msg string) BotMessage {
return BotMessage{
Source: src,
Dest: dst,
Type: tp,
Target: tgt,
Text: msg,
}
}
func NewErrorBotMessage(src, tgt, msg string) BotMessage {
return BotMessage{
Source: src,
Dest: MsgSrcApp,
Type: MsgTpError,
Target: tgt,
Text: msg,
}
}
func BotMessageFromSlack(msg slack.Message) BotMessage {
tgt := msg.Channel
if tgt == "" && msg.User != "" {
tgt = msg.User
}
return BotMessage{
Source: MsgSrcSlack,
Dest: MsgSrcApp,
Type: msg.Type,
Target: tgt,
Text: msg.Text,
}
}
func (b BotMessage) GetSource() string { return b.Source }
func (b BotMessage) GetDestination() string { return b.Dest }
func (b BotMessage) GetType() string { return b.Type }
func (b BotMessage) GetTarget() string { return b.Target }
func (b BotMessage) GetText() string { return b.Text }
func (b BotMessage) IsTo(dest string) bool { return b.Dest == dest }
func (b BotMessage) IsError() bool { return b.Type == MsgTpError }
func (b BotMessage) Is(msg BotMessage) bool {
return b.Matches(msg.Source, msg.Dest, msg.Type, msg.Target, msg.Text)
}
func (b BotMessage) Matches(src, dest, tp, tgt, msg string) bool {
return b.Source == src &&
b.Dest == dest &&
b.Type == tp &&
b.Target == tgt &&
b.Text == msg
}
func (b BotMessage) String() string {
return fmt.Sprintf("BotMessage: %s -> %s[%s]@%s :: %s", b.Source, b.Dest, b.Type, b.Target, b.Text)
}

92
models/model.go Normal file
View File

@@ -0,0 +1,92 @@
package models
import (
"fmt"
"time"
"git.bullercodeworks.com/brian/boltease"
"github.com/slack-go/slack"
)
type BotModel struct {
debug bool
db *boltease.DB
messages chan BotMessage
slackApiToken string
slackApi *slack.Client
slackRTM *slack.RTM
slackRTMLatency time.Duration
incomingSlackMessages chan *slack.MessageEvent
otherRTMEvents chan *slack.RTMEvent
messageWatchers []func(msg BotMessage) bool
rtmWatchers []func(event *slack.RTMEvent) bool
}
func NewBotModel(debug bool) (*BotModel, error) {
var err error
m := new(BotModel)
m.debug = debug
m.messages = make(chan BotMessage, 100)
m.messageWatchers = make([]func(msg BotMessage) bool, 1)
m.AddMessageWatcher(m.DebugMessageWatcher)
m.AddRTMWatcher(m.DebugRTMWatcher)
m.db, err = boltease.Create("helperbot.db", 0600, nil)
if err != nil {
return nil, err
}
if err = m.NewSlack(); err != nil {
return nil, err
}
go m.monitorSlackMessages()
return m, nil
}
func (m *BotModel) DebugMessageWatcher(msg BotMessage) bool {
if m.debug {
fmt.Printf("Received Message: %s\n", msg)
}
return false
}
func (m *BotModel) DebugRTMWatcher(event *slack.RTMEvent) bool {
if m.debug {
fmt.Printf("RTMEvent: %s\n", event)
}
return false
}
func (m *BotModel) SendMessage(msg BotMessage) {
fmt.Println("Sending:", msg)
m.messages <- msg
}
func (m *BotModel) AddMessageWatcher(watcher func(msg BotMessage) bool) {
m.messageWatchers = append(m.messageWatchers, watcher)
}
func (m *BotModel) AddRTMWatcher(watcher func(event *slack.RTMEvent) bool) {
m.rtmWatchers = append(m.rtmWatchers, watcher)
}
func (m *BotModel) ProcessMessageChannel() {
msg := <-m.messages
for _, v := range m.messageWatchers {
// Pass the message to the watcher
if v(msg) {
// if a watcher returns true, the message was consumed
break
}
}
}
func (m *BotModel) ProcessRTMChannel() {
msg := <-m.otherRTMEvents
for _, v := range m.rtmWatchers {
// Pass the event to the watcher
if v(msg) {
// if a watcher returns true, the message was consumed
break
}
}
}

62
models/model_bolt.go Normal file
View File

@@ -0,0 +1,62 @@
package models
import (
"errors"
"strconv"
)
func (m *BotModel) GetBytes(path []string) ([]byte, error) {
var err error
var v []byte
// 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
}
}
return v, nil
}
func (m *BotModel) SetBytes(path []string, val []byte) error {
if len(path) > 1 {
path, key := path[:len(path)-1], path[len(path)-1]
err := m.db.SetBytes(path, key, val)
if err != nil {
return err
}
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))
}
func (m *BotModel) GetInt(path []string) (int, error) {
bts, err := m.GetBytes(path)
if err != nil {
return 0, err
}
if len(bts) == 0 {
return 0, nil
}
return strconv.Atoi(string(bts))
}
func (m *BotModel) SetInt(path []string, val int) error {
return m.SetString(path, strconv.Itoa(val))
}

156
models/model_slack.go Normal file
View File

@@ -0,0 +1,156 @@
package models
import (
"bufio"
"fmt"
"os"
"strings"
"time"
"github.com/slack-go/slack"
"github.com/spf13/viper"
)
const (
keyPluginDir = "plugin_dir"
keySlackToken = "slack.token"
keySlackAdminDmId = "slack.admin_dm_id"
)
func (m *BotModel) GetPluginDir() string { return viper.GetString(keyPluginDir) }
/* Slack Config Functions */
func (m *BotModel) getSlackToken() string { return viper.GetString(keySlackToken) }
func (m *BotModel) setSlackToken(token string) error {
viper.Set(keySlackToken, token)
return viper.WriteConfig()
}
func (m *BotModel) GetSlackAdminDMId() string { return viper.GetString(keySlackAdminDmId) }
func (m *BotModel) setSlackAdminDMId(adminId string) error {
viper.Set(keySlackAdminDmId, adminId)
return viper.WriteConfig()
}
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) requestAdminDMId() {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Slack Admin DM ID: ")
dmId, _ := reader.ReadString('\n')
m.setSlackAdminDMId(strings.TrimSpace(dmId))
}
/* End Slack Config Functions */
func (m *BotModel) NewSlack() error {
var err error
m.slackApiToken = m.getSlackToken()
if m.slackApiToken == "" {
m.requestSlackToken()
}
slackDmId := m.GetSlackAdminDMId()
if slackDmId == "" {
m.requestAdminDMId()
}
m.incomingSlackMessages = make(chan *slack.MessageEvent, 50)
m.otherRTMEvents = make(chan *slack.RTMEvent, 50)
m.slackApi = slack.New(m.slackApiToken)
m.slackRTM = m.slackApi.NewRTM()
m.slackRTMLatency = time.Duration(0)
go m.slackRTM.ManageConnection()
go m.HandleRTMEvents()
return err
}
func (m *BotModel) GetChannelInfo(id string) (*slack.Channel, error) {
convInfo := &slack.GetConversationInfoInput{
ChannelID: id,
IncludeLocale: false,
IncludeNumMembers: false,
}
return m.slackApi.GetConversationInfo(convInfo)
}
func (m *BotModel) HandleRTMEvents() {
for msg := range m.slackRTM.IncomingEvents {
fmt.Printf("RTM Message: %v\n", msg)
switch ev := msg.Data.(type) {
case *slack.MessageEvent:
fmt.Println("RTM Message: Processing Message Event")
m.processMessageEvent(ev)
case *slack.LatencyReport:
fmt.Println("RTM Message: Latency Report")
m.otherRTMEvents <- &msg
case *slack.RTMError:
fmt.Printf("RTM ERROR: (%d) %s\n", ev.Code, ev.Msg)
m.otherRTMEvents <- &msg
default: // Ignore other events
}
}
}
func (m *BotModel) SendSlackAdminMessage(msg string) error {
// Send message to slack admin
dmId := m.GetSlackAdminDMId()
_, _, err := m.slackApi.PostMessage(
dmId,
slack.MsgOptionText(msg, false),
slack.MsgOptionAsUser(true),
)
return err
}
func (m *BotModel) SendSlackChannelMessage(msg string, cid string) error {
if m.debug {
return m.SendSlackAdminMessage(msg)
}
_, _, err := m.slackApi.PostMessage(
cid,
slack.MsgOptionText(msg, false),
slack.MsgOptionAsUser(true),
)
return err
}
func (m *BotModel) processMessageEvent(ev *slack.MessageEvent) {
m.getSlackUserName(ev.User)
m.getSlackChannelName(ev.Channel)
m.incomingSlackMessages <- ev
}
func (m *BotModel) getSlackUserName(id string) (string, error) {
user, err := m.slackApi.GetUserInfo(id)
if err != nil {
return "", err
}
return user.Profile.DisplayName, nil
}
func (m *BotModel) getSlackChannelName(id string) (string, error) {
c, err := m.GetChannelInfo(id)
if err != nil {
return "", err
}
return c.Name, nil
}
func (m *BotModel) GetSlackLatency() time.Duration {
return m.slackRTMLatency
}
func (m *BotModel) monitorSlackMessages() {
for msg := range m.incomingSlackMessages {
m.SendMessage(BotMessageFromSlack(slack.Message(*msg)))
}
}