diff --git a/.gitignore b/.gitignore index ba65788..dffcaf7 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,45 @@ vendor .vscode/ *.code-workspace *config.lua +modd.conf + + +# Created by https://www.gitignore.io/api/macos +# Edit at https://www.gitignore.io/?templates=macos + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# End of https://www.gitignore.io/api/macos + +util/*/files +util/*/files +run.sh +.idea +logs +util/files diff --git a/README.md b/README.md index 96d357f..bf5fede 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # CatBase +[![Build Status](https://travis-ci.com/velour/catbase.svg?branch=master)](https://travis-ci.com/velour/catbase) + CatBase is a bot that trolls our little corner of the IRC world and keeps our friends laughing from time to time. Sometimes he makes us angry too. He is crafted as a clone of XKCD's Bucket bot, which learns from things he's told and regurgitates his knowledge to the various channels that he lives in. I've found in many such projects that randomness can often make bots feel much more alive than they are, so CatBase is a big experiment in how great randomness is. ## Getting Help diff --git a/bot/bot.go b/bot/bot.go index 777229c..4d0c1a0 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -3,13 +3,15 @@ package bot import ( - "database/sql" - "html/template" - "log" + "fmt" + "math/rand" "net/http" + "reflect" "strings" + "time" "github.com/jmoiron/sqlx" + "github.com/rs/zerolog/log" "github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msglog" "github.com/velour/catbase/bot/user" @@ -20,7 +22,7 @@ import ( type bot struct { // Each plugin must be registered in our plugins handler. To come: a map so that this // will allow plugins to respond to specific kinds of events - plugins map[string]Handler + plugins map[string]Plugin pluginOrdering []string // Users holds information about all of our friends @@ -32,30 +34,33 @@ type bot struct { conn Connector - // SQL DB - // TODO: I think it'd be nice to use https://github.com/jmoiron/sqlx so that - // the select/update/etc statements could be simplified with struct - // marshalling. - db *sqlx.DB - dbVersion int64 - logIn chan msg.Message logOut chan msg.Messages version string // The entries to the bot's HTTP interface - httpEndPoints map[string]string + httpEndPoints []EndPoint // filters registered by plugins filters map[string]func(string) string + + callbacks CallbackMap + + password string + passwordCreated time.Time } +type EndPoint struct { + Name, URL string +} + +// Variable represents a $var replacement type Variable struct { Variable, Value string } -// Newbot creates a bot for a given connection and set of handlers. +// New creates a bot for a given connection and set of handlers. func New(config *config.Config, connector Connector) Bot { logIn := make(chan msg.Message) logOut := make(chan msg.Messages) @@ -63,94 +68,69 @@ func New(config *config.Config, connector Connector) Bot { msglog.RunNew(logIn, logOut) users := []user.User{ - user.User{ - Name: config.Nick, + { + Name: config.Get("Nick", "bot"), }, } bot := &bot{ config: config, - plugins: make(map[string]Handler), + plugins: make(map[string]Plugin), pluginOrdering: make([]string, 0), conn: connector, users: users, me: users[0], - db: config.DBConn, logIn: logIn, logOut: logOut, - version: config.Version, - httpEndPoints: make(map[string]string), + httpEndPoints: make([]EndPoint, 0), filters: make(map[string]func(string) string), + callbacks: make(CallbackMap), } bot.migrateDB() http.HandleFunc("/", bot.serveRoot) - if config.HttpAddr == "" { - config.HttpAddr = "127.0.0.1:1337" - } - go http.ListenAndServe(config.HttpAddr, nil) - connector.RegisterMessageReceived(bot.MsgReceived) - connector.RegisterEventReceived(bot.EventReceived) - connector.RegisterReplyMessageReceived(bot.ReplyMsgReceived) + connector.RegisterEvent(bot.Receive) return bot } +func (b *bot) DefaultConnector() Connector { + return b.conn +} + +func (b *bot) WhoAmI() string { + return b.me.Name +} + // Config gets the configuration that the bot is using func (b *bot) Config() *config.Config { return b.config } -func (b *bot) DBVersion() int64 { - return b.dbVersion -} - func (b *bot) DB() *sqlx.DB { - return b.db + return b.config.DB } // Create any tables if necessary based on version of DB // Plugins should create their own tables, these are only for official bot stuff // Note: This does not return an error. Database issues are all fatal at this stage. func (b *bot) migrateDB() { - _, err := b.db.Exec(`create table if not exists version (version integer);`) - if err != nil { - log.Fatal("Initial DB migration create version table: ", err) - } - var version sql.NullInt64 - err = b.db.QueryRow("select max(version) from version").Scan(&version) - if err != nil { - log.Fatal("Initial DB migration get version: ", err) - } - if version.Valid { - b.dbVersion = version.Int64 - log.Printf("Database version: %v\n", b.dbVersion) - } else { - log.Printf("No versions, we're the first!.") - _, err := b.db.Exec(`insert into version (version) values (1)`) - if err != nil { - log.Fatal("Initial DB migration insert: ", err) - } - } - - if _, err := b.db.Exec(`create table if not exists variables ( + if _, err := b.DB().Exec(`create table if not exists variables ( id integer primary key, name string, value string );`); err != nil { - log.Fatal("Initial DB migration create variables table: ", err) + log.Fatal().Err(err).Msgf("Initial DB migration create variables table") } } // Adds a constructed handler to the bots handlers list -func (b *bot) AddHandler(name string, h Handler) { +func (b *bot) AddPlugin(h Plugin) { + name := reflect.TypeOf(h).String() b.plugins[name] = h b.pluginOrdering = append(b.pluginOrdering, name) - if entry := h.RegisterWeb(); entry != nil { - b.httpEndPoints[name] = *entry - } } func (b *bot) Who(channel string) []user.User { @@ -162,50 +142,14 @@ func (b *bot) Who(channel string) []user.User { return users } -var rootIndex string = ` - - - - Factoids - - - - {{if .EndPoints}} -
- - - - - - - - - {{range $key, $value := .EndPoints}} - - - - {{end}} - -
Plugin
{{$key}}
-
- {{end}} - -` - -func (b *bot) serveRoot(w http.ResponseWriter, r *http.Request) { - context := make(map[string]interface{}) - context["EndPoints"] = b.httpEndPoints - t, err := template.New("rootIndex").Parse(rootIndex) - if err != nil { - log.Println(err) - } - t.Execute(w, context) -} - -// Checks if message is a command and returns its curtailed version +// IsCmd checks if message is a command and returns its curtailed version func IsCmd(c *config.Config, message string) (bool, string) { - cmdcs := c.CommandChar - botnick := strings.ToLower(c.Nick) + cmdcs := c.GetArray("CommandChar", []string{"!"}) + botnick := strings.ToLower(c.Get("Nick", "bot")) + if botnick == "" { + log.Fatal(). + Msgf(`You must run catbase -set nick -val `) + } iscmd := false lowerMessage := strings.ToLower(message) @@ -237,7 +181,7 @@ func IsCmd(c *config.Config, message string) (bool, string) { } func (b *bot) CheckAdmin(nick string) bool { - for _, u := range b.Config().Admins { + for _, u := range b.Config().GetArray("Admins", []string{}) { if nick == u { return true } @@ -265,7 +209,7 @@ func (b *bot) NewUser(nick string) *user.User { } func (b *bot) checkAdmin(nick string) bool { - for _, u := range b.Config().Admins { + for _, u := range b.Config().GetArray("Admins", []string{}) { if nick == u { return true } @@ -277,3 +221,31 @@ func (b *bot) checkAdmin(nick string) bool { func (b *bot) RegisterFilter(name string, f func(string) string) { b.filters[name] = f } + +// Register a callback +func (b *bot) Register(p Plugin, kind Kind, cb Callback) { + t := reflect.TypeOf(p).String() + if _, ok := b.callbacks[t]; !ok { + b.callbacks[t] = make(map[Kind][]Callback) + } + if _, ok := b.callbacks[t][kind]; !ok { + b.callbacks[t][kind] = []Callback{} + } + b.callbacks[t][kind] = append(b.callbacks[t][kind], cb) +} + +func (b *bot) RegisterWeb(root, name string) { + b.httpEndPoints = append(b.httpEndPoints, EndPoint{name, root}) +} + +func (b *bot) GetPassword() string { + if b.passwordCreated.Before(time.Now().Add(-24 * time.Hour)) { + adjs := b.config.GetArray("bot.passwordAdjectives", []string{"very"}) + nouns := b.config.GetArray("bot.passwordNouns", []string{"noun"}) + verbs := b.config.GetArray("bot.passwordVerbs", []string{"do"}) + a, n, v := adjs[rand.Intn(len(adjs))], nouns[rand.Intn(len(nouns))], verbs[rand.Intn(len(verbs))] + b.passwordCreated = time.Now() + b.password = fmt.Sprintf("%s-%s-%s", a, n, v) + } + return b.password +} diff --git a/bot/handlers.go b/bot/handlers.go index cd01bb0..0a1a3e9 100644 --- a/bot/handlers.go +++ b/bot/handlers.go @@ -6,86 +6,55 @@ import ( "database/sql" "errors" "fmt" - "log" "math/rand" + "reflect" "regexp" "strconv" "strings" "time" + "github.com/rs/zerolog/log" "github.com/velour/catbase/bot/msg" ) -// Handles incomming PRIVMSG requests -func (b *bot) MsgReceived(msg msg.Message) { - log.Println("Received message: ", msg) +func (b *bot) Receive(conn Connector, kind Kind, msg msg.Message, args ...interface{}) bool { + log.Debug(). + Interface("msg", msg). + Msg("Received event") // msg := b.buildMessage(client, inMsg) // do need to look up user and fix it - if strings.HasPrefix(msg.Body, "help ") && msg.Command { + if kind == Message && strings.HasPrefix(msg.Body, "help") && msg.Command { parts := strings.Fields(strings.ToLower(msg.Body)) - b.checkHelp(msg.Channel, parts) + b.checkHelp(conn, msg.Channel, parts) + log.Debug().Msg("Handled a help, returning") goto RET } for _, name := range b.pluginOrdering { - p := b.plugins[name] - if p.Message(msg) { - break + if b.runCallback(conn, b.plugins[name], kind, msg, args...) { + goto RET } } RET: b.logIn <- msg - return + return true } -// Handle incoming events -func (b *bot) EventReceived(msg msg.Message) { - log.Println("Received event: ", msg) - //msg := b.buildMessage(conn, inMsg) - for _, name := range b.pluginOrdering { - p := b.plugins[name] - if p.Event(msg.Body, msg) { // TODO: could get rid of msg.Body - break +func (b *bot) runCallback(conn Connector, plugin Plugin, evt Kind, message msg.Message, args ...interface{}) bool { + t := reflect.TypeOf(plugin).String() + for _, cb := range b.callbacks[t][evt] { + if cb(conn, evt, message, args...) { + return true } } + return false } -// Handle incoming replys -func (b *bot) ReplyMsgReceived(msg msg.Message, identifier string) { - log.Println("Received message: ", msg) - - for _, name := range b.pluginOrdering { - p := b.plugins[name] - if p.ReplyMessage(msg, identifier) { - break - } - } -} - -func (b *bot) SendMessage(channel, message string) string { - return b.conn.SendMessage(channel, message) -} - -func (b *bot) SendAction(channel, message string) string { - return b.conn.SendAction(channel, message) -} - -func (b *bot) ReplyToMessageIdentifier(channel, message, identifier string) (string, bool) { - return b.conn.ReplyToMessageIdentifier(channel, message, identifier) -} - -func (b *bot) ReplyToMessage(channel, message string, replyTo msg.Message) (string, bool) { - return b.conn.ReplyToMessage(channel, message, replyTo) -} - -func (b *bot) React(channel, reaction string, message msg.Message) bool { - return b.conn.React(channel, reaction, message) -} - -func (b *bot) Edit(channel, newMessage, identifier string) bool { - return b.conn.Edit(channel, newMessage, identifier) +// Send a message to the connection +func (b *bot) Send(conn Connector, kind Kind, args ...interface{}) (string, error) { + return conn.Send(kind, args...) } func (b *bot) GetEmojiList() map[string]string { @@ -93,31 +62,38 @@ func (b *bot) GetEmojiList() map[string]string { } // Checks to see if the user is asking for help, returns true if so and handles the situation. -func (b *bot) checkHelp(channel string, parts []string) { +func (b *bot) checkHelp(conn Connector, channel string, parts []string) { if len(parts) == 1 { // just print out a list of help topics topics := "Help topics: about variables" - for name, _ := range b.plugins { + for name := range b.plugins { + name = strings.Split(strings.TrimPrefix(name, "*"), ".")[0] topics = fmt.Sprintf("%s, %s", topics, name) } - b.SendMessage(channel, topics) + b.Send(conn, Message, channel, topics) } else { // trigger the proper plugin's help response if parts[1] == "about" { - b.Help(channel, parts) + b.Help(conn, channel, parts) return } if parts[1] == "variables" { - b.listVars(channel, parts) + b.listVars(conn, channel, parts) return } - plugin := b.plugins[parts[1]] - if plugin != nil { - plugin.Help(channel, parts) - } else { - msg := fmt.Sprintf("I'm sorry, I don't know what %s is!", parts[1]) - b.SendMessage(channel, msg) + for name, plugin := range b.plugins { + if strings.HasPrefix(name, "*"+parts[1]) { + if b.runCallback(conn, plugin, Help, msg.Message{Channel: channel}, channel, parts) { + return + } else { + msg := fmt.Sprintf("I'm sorry, I don't know how to help you with %s.", parts[1]) + b.Send(conn, Message, channel, msg) + return + } + } } + msg := fmt.Sprintf("I'm sorry, I don't know what %s is!", strings.Join(parts, " ")) + b.Send(conn, Message, channel, msg) } } @@ -192,38 +168,38 @@ func (b *bot) Filter(message msg.Message, input string) string { func (b *bot) getVar(varName string) (string, error) { var text string - err := b.db.Get(&text, `select value from variables where name=? order by random() limit 1`, varName) + err := b.DB().Get(&text, `select value from variables where name=? order by random() limit 1`, varName) switch { case err == sql.ErrNoRows: return "", fmt.Errorf("No factoid found") case err != nil: - log.Fatal("getVar error: ", err) + log.Fatal().Err(err).Msg("getVar error") } return text, nil } -func (b *bot) listVars(channel string, parts []string) { +func (b *bot) listVars(conn Connector, channel string, parts []string) { var variables []string - err := b.db.Select(&variables, `select name from variables group by name`) + err := b.DB().Select(&variables, `select name from variables group by name`) if err != nil { - log.Fatal(err) + log.Fatal().Err(err) } msg := "I know: $who, $someone, $digit, $nonzero" if len(variables) > 0 { msg += ", " + strings.Join(variables, ", ") } - b.SendMessage(channel, msg) + b.Send(conn, Message, channel, msg) } -func (b *bot) Help(channel string, parts []string) { +func (b *bot) Help(conn Connector, channel string, parts []string) { msg := fmt.Sprintf("Hi, I'm based on godeepintir version %s. I'm written in Go, and you "+ "can find my source code on the internet here: "+ "http://github.com/velour/catbase", b.version) - b.SendMessage(channel, msg) + b.Send(conn, Message, channel, msg) } // Send our own musings to the plugins -func (b *bot) selfSaid(channel, message string, action bool) { +func (b *bot) selfSaid(conn Connector, channel, message string, action bool) { msg := msg.Message{ User: &b.me, // hack Channel: channel, @@ -236,9 +212,8 @@ func (b *bot) selfSaid(channel, message string, action bool) { } for _, name := range b.pluginOrdering { - p := b.plugins[name] - if p.BotMessage(msg) { - break + if b.runCallback(conn, b.plugins[name], SelfMessage, msg) { + return } } } diff --git a/bot/interfaces.go b/bot/interfaces.go index 2780d29..d9f5b62 100644 --- a/bot/interfaces.go +++ b/bot/interfaces.go @@ -9,51 +9,80 @@ import ( "github.com/velour/catbase/config" ) +const ( + _ = iota + + // Message any standard chat + Message + // Reply something containing a message reference + Reply + // Action any /me action + Action + // Reaction Icon reaction if service supports it + Reaction + // Edit message ref'd new message to replace + Edit + // Not sure what event is + Event + // Help is used when the bot help system is triggered + Help + // SelfMessage triggers when the bot is sending a message + SelfMessage +) + +type ImageAttachment struct { + URL string + AltTxt string +} + +type Kind int +type Callback func(Connector, Kind, msg.Message, ...interface{}) bool +type CallbackMap map[string]map[Kind][]Callback + +// Bot interface serves to allow mocking of the actual bot type Bot interface { + // Config allows access to the bot's configuration system Config() *config.Config - DBVersion() int64 + // DB gives access to the current database DB() *sqlx.DB + // Who lists users in a particular channel Who(string) []user.User - AddHandler(string, Handler) - SendMessage(string, string) string - SendAction(string, string) string - ReplyToMessageIdentifier(string, string, string) (string, bool) - ReplyToMessage(string, string, msg.Message) (string, bool) - React(string, string, msg.Message) bool - Edit(string, string, string) bool - MsgReceived(msg.Message) - ReplyMsgReceived(msg.Message, string) - EventReceived(msg.Message) + // WhoAmI gives a nick for the bot + WhoAmI() string + // AddPlugin registers a new plugin handler + AddPlugin(Plugin) + // First arg should be one of bot.Message/Reply/Action/etc + Send(Connector, Kind, ...interface{}) (string, error) + // First arg should be one of bot.Message/Reply/Action/etc + Receive(Connector, Kind, msg.Message, ...interface{}) bool + // Register a callback + Register(Plugin, Kind, Callback) + Filter(msg.Message, string) string LastMessage(string) (msg.Message, error) + CheckAdmin(string) bool GetEmojiList() map[string]string RegisterFilter(string, func(string) string) + RegisterWeb(string, string) + DefaultConnector() Connector + GetWebNavigation() []EndPoint + GetPassword() string } +// Connector represents a server connection to a chat service type Connector interface { - RegisterEventReceived(func(message msg.Message)) - RegisterMessageReceived(func(message msg.Message)) - RegisterReplyMessageReceived(func(msg.Message, string)) + RegisterEvent(Callback) + + Send(Kind, ...interface{}) (string, error) - SendMessage(channel, message string) string - SendAction(channel, message string) string - ReplyToMessageIdentifier(string, string, string) (string, bool) - ReplyToMessage(string, string, msg.Message) (string, bool) - React(string, string, msg.Message) bool - Edit(string, string, string) bool GetEmojiList() map[string]string Serve() error Who(string) []string } -// Interface used for compatibility with the Plugin interface -type Handler interface { - Message(message msg.Message) bool - Event(kind string, message msg.Message) bool - ReplyMessage(msg.Message, string) bool - BotMessage(message msg.Message) bool - Help(channel string, parts []string) - RegisterWeb() *string +// Plugin interface used for compatibility with the Plugin interface +// Uhh it turned empty, but we're still using it to ID plugins +type Plugin interface { } diff --git a/bot/mock.go b/bot/mock.go index 7d6c6e6..6d7fb70 100644 --- a/bot/mock.go +++ b/bot/mock.go @@ -4,11 +4,12 @@ package bot import ( "fmt" - "log" + "net/http" "strconv" "strings" "github.com/jmoiron/sqlx" + "github.com/rs/zerolog/log" "github.com/stretchr/testify/mock" "github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/user" @@ -19,85 +20,94 @@ type MockBot struct { mock.Mock db *sqlx.DB - Cfg config.Config + Cfg *config.Config - Messages []string - Actions []string + Messages []string + Actions []string + Reactions []string } -func (mb *MockBot) Config() *config.Config { return &mb.Cfg } -func (mb *MockBot) DBVersion() int64 { return 1 } -func (mb *MockBot) DB() *sqlx.DB { return mb.db } -func (mb *MockBot) Conn() Connector { return nil } -func (mb *MockBot) Who(string) []user.User { return []user.User{} } -func (mb *MockBot) AddHandler(name string, f Handler) {} -func (mb *MockBot) SendMessage(ch string, msg string) string { - mb.Messages = append(mb.Messages, msg) - return fmt.Sprintf("m-%d", len(mb.Actions)-1) +func (mb *MockBot) Config() *config.Config { return mb.Cfg } +func (mb *MockBot) DB() *sqlx.DB { return mb.Cfg.DB } +func (mb *MockBot) Who(string) []user.User { return []user.User{} } +func (mb *MockBot) WhoAmI() string { return "tester" } +func (mb *MockBot) DefaultConnector() Connector { return nil } +func (mb *MockBot) GetPassword() string { return "12345" } +func (mb *MockBot) Send(c Connector, kind Kind, args ...interface{}) (string, error) { + switch kind { + case Message: + mb.Messages = append(mb.Messages, args[1].(string)) + return fmt.Sprintf("m-%d", len(mb.Actions)-1), nil + case Action: + mb.Actions = append(mb.Actions, args[1].(string)) + return fmt.Sprintf("a-%d", len(mb.Actions)-1), nil + case Edit: + ch, m, id := args[0].(string), args[1].(string), args[2].(string) + return mb.edit(c, ch, m, id) + case Reaction: + ch, re, msg := args[0].(string), args[1].(string), args[2].(msg.Message) + return mb.react(c, ch, re, msg) + } + return "ERR", fmt.Errorf("Mesasge type unhandled") } -func (mb *MockBot) SendAction(ch string, msg string) string { - mb.Actions = append(mb.Actions, msg) - return fmt.Sprintf("a-%d", len(mb.Actions)-1) +func (mb *MockBot) AddPlugin(f Plugin) {} +func (mb *MockBot) Register(p Plugin, kind Kind, cb Callback) {} +func (mb *MockBot) RegisterWeb(_, _ string) {} +func (mb *MockBot) GetWebNavigation() []EndPoint { return nil } +func (mb *MockBot) Receive(c Connector, kind Kind, msg msg.Message, args ...interface{}) bool { + return false } -func (mb *MockBot) ReplyToMessageIdentifier(channel, message, identifier string) (string, bool) { - return "", false -} -func (mb *MockBot) ReplyToMessage(channel, message string, replyTo msg.Message) (string, bool) { - return "", false -} -func (mb *MockBot) MsgReceived(msg msg.Message) {} -func (mb *MockBot) EventReceived(msg msg.Message) {} -func (mb *MockBot) Filter(msg msg.Message, s string) string { return "" } +func (mb *MockBot) Filter(msg msg.Message, s string) string { return s } func (mb *MockBot) LastMessage(ch string) (msg.Message, error) { return msg.Message{}, nil } func (mb *MockBot) CheckAdmin(nick string) bool { return false } -func (mb *MockBot) React(channel, reaction string, message msg.Message) bool { return false } +func (mb *MockBot) react(c Connector, channel, reaction string, message msg.Message) (string, error) { + mb.Reactions = append(mb.Reactions, reaction) + return "", nil +} -func (mb *MockBot) Edit(channel, newMessage, identifier string) bool { +func (mb *MockBot) edit(c Connector, channel, newMessage, identifier string) (string, error) { isMessage := identifier[0] == 'm' if !isMessage && identifier[0] != 'a' { - log.Printf("failed to parse identifier: %s", identifier) - return false + err := fmt.Errorf("failed to parse identifier: %s", identifier) + log.Error().Err(err) + return "", err } index, err := strconv.Atoi(strings.Split(identifier, "-")[1]) if err != nil { - log.Printf("failed to parse identifier: %s", identifier) - return false + err := fmt.Errorf("failed to parse identifier: %s", identifier) + log.Error().Err(err) + return "", err } if isMessage { if index < len(mb.Messages) { mb.Messages[index] = newMessage } else { - return false + return "", fmt.Errorf("No message") } } else { if index < len(mb.Actions) { mb.Actions[index] = newMessage } else { - return false + return "", fmt.Errorf("No action") } } - return true -} - -func (mb *MockBot) ReplyMsgReceived(msg.Message, string) { - + return "", nil } func (mb *MockBot) GetEmojiList() map[string]string { return make(map[string]string) } func (mb *MockBot) RegisterFilter(s string, f func(string) string) {} func NewMockBot() *MockBot { - db, err := sqlx.Open("sqlite3_custom", ":memory:") - if err != nil { - log.Fatal("Failed to open database:", err) - } + cfg := config.ReadConfig("file::memory:?mode=memory&cache=shared") b := MockBot{ - db: db, + Cfg: cfg, Messages: make([]string, 0), Actions: make([]string, 0), } + // If any plugin registered a route, we need to reset those before any new test + http.DefaultServeMux = new(http.ServeMux) return &b } diff --git a/bot/msg/message.go b/bot/msg/message.go index 78a40f0..6d3712c 100644 --- a/bot/msg/message.go +++ b/bot/msg/message.go @@ -12,9 +12,14 @@ type Log Messages type Messages []Message type Message struct { - User *user.User - Channel, Body string - Raw string + User *user.User + // With Slack, channel is the ID of a channel + Channel string + // With slack, channelName is the nice name of a channel + ChannelName string + Body string + IsIM bool + Raw interface{} Command bool Action bool Time time.Time diff --git a/bot/web.go b/bot/web.go new file mode 100644 index 0000000..b8e52ed --- /dev/null +++ b/bot/web.go @@ -0,0 +1,73 @@ +package bot + +import ( + "html/template" + "net/http" + "strings" +) + +func (b *bot) serveRoot(w http.ResponseWriter, r *http.Request) { + context := make(map[string]interface{}) + context["Nav"] = b.GetWebNavigation() + t := template.Must(template.New("rootIndex").Parse(rootIndex)) + t.Execute(w, context) +} + +// GetWebNavigation returns a list of bootstrap-vue links +// The parent