diff --git a/bot/bot.go b/bot/bot.go index 4cae7b4..491704a 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -26,6 +26,9 @@ type bot struct { plugins map[string]Plugin pluginOrdering []string + // channel -> plugin + pluginBlacklist map[string]bool + // Users holds information about all of our friends users []user.User // Represents the bot @@ -77,21 +80,24 @@ func New(config *config.Config, connector Connector) Bot { } bot := &bot{ - config: config, - plugins: make(map[string]Plugin), - pluginOrdering: make([]string, 0), - conn: connector, - users: users, - me: users[0], - logIn: logIn, - logOut: logOut, - httpEndPoints: make([]EndPoint, 0), - filters: make(map[string]func(string) string), - callbacks: make(CallbackMap), + config: config, + plugins: make(map[string]Plugin), + pluginOrdering: make([]string, 0), + pluginBlacklist: make(map[string]bool), + conn: connector, + users: users, + me: users[0], + logIn: logIn, + logOut: logOut, + httpEndPoints: make([]EndPoint, 0), + filters: make(map[string]func(string) string), + callbacks: make(CallbackMap), } bot.migrateDB() + bot.RefreshPluginBlacklist() + http.HandleFunc("/", bot.serveRoot) connector.RegisterEvent(bot.Receive) @@ -127,6 +133,13 @@ func (b *bot) migrateDB() { );`); err != nil { log.Fatal().Err(err).Msgf("Initial DB migration create variables table") } + if _, err := b.DB().Exec(`create table if not exists pluginBlacklist ( + channel string, + name string, + primary key (channel, name) + );`); err != nil { + log.Fatal().Err(err).Msgf("Initial DB migration create variables table") + } } // Adds a constructed handler to the bots handlers list @@ -265,3 +278,35 @@ func (b *bot) GetPassword() string { func (b *bot) SetQuiet(status bool) { b.quiet = status } + +func (b *bot) RefreshPluginBlacklist() error { + blacklistItems := []struct { + Channel string + Name string + }{} + if err := b.DB().Select(&blacklistItems, `select channel, name from pluginBlacklist`); err != nil { + return fmt.Errorf("%w", err) + } + b.pluginBlacklist = make(map[string]bool) + for _, i := range blacklistItems { + b.pluginBlacklist[i.Channel+i.Name] = true + } + log.Debug().Interface("blacklist", b.pluginBlacklist).Msgf("Refreshed plugin blacklist") + return nil +} + +func (b *bot) GetPluginNames() []string { + names := []string{} + for _, name := range b.pluginOrdering { + names = append(names, pluginNameStem(name)) + } + return names +} + +func (b *bot) onBlacklist(channel, plugin string) bool { + return b.pluginBlacklist[channel+plugin] +} + +func pluginNameStem(name string) string { + return strings.Split(strings.TrimPrefix(name, "*"), ".")[0] +} diff --git a/bot/handlers.go b/bot/handlers.go index c5baf21..0508f97 100644 --- a/bot/handlers.go +++ b/bot/handlers.go @@ -31,7 +31,11 @@ func (b *bot) Receive(conn Connector, kind Kind, msg msg.Message, args ...interf goto RET } + log.Debug().Msgf("checking blacklist %v", b.pluginBlacklist) for _, name := range b.pluginOrdering { + if b.onBlacklist(msg.Channel, pluginNameStem(name)) { + continue + } if b.runCallback(conn, b.plugins[name], kind, msg, args...) { goto RET } @@ -70,7 +74,7 @@ func (b *bot) checkHelp(conn Connector, channel string, parts []string) { // just print out a list of help topics topics := "Help topics: about variables" for name := range b.plugins { - name = strings.Split(strings.TrimPrefix(name, "*"), ".")[0] + name = pluginNameStem(name) topics = fmt.Sprintf("%s, %s", topics, name) } b.Send(conn, Message, channel, topics) diff --git a/bot/interfaces.go b/bot/interfaces.go index 22b4200..1a7d629 100644 --- a/bot/interfaces.go +++ b/bot/interfaces.go @@ -4,6 +4,7 @@ package bot import ( "github.com/jmoiron/sqlx" + "github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/user" "github.com/velour/catbase/config" @@ -69,6 +70,8 @@ type Bot interface { GetWebNavigation() []EndPoint GetPassword() string SetQuiet(bool) + GetPluginNames() []string + RefreshPluginBlacklist() error } // Connector represents a server connection to a chat service diff --git a/bot/mock.go b/bot/mock.go index a72fe18..7179482 100644 --- a/bot/mock.go +++ b/bot/mock.go @@ -112,3 +112,6 @@ func NewMockBot() *MockBot { http.DefaultServeMux = new(http.ServeMux) return &b } + +func (mb *MockBot) GetPluginNames() []string { return nil } +func (mb *MockBot) RefreshPluginBlacklist() error { return nil } diff --git a/plugins/admin/admin.go b/plugins/admin/admin.go index eeb2d5a..7c304e0 100644 --- a/plugins/admin/admin.go +++ b/plugins/admin/admin.go @@ -8,6 +8,7 @@ import ( "html/template" "net/http" "os" + "regexp" "strings" "time" @@ -51,6 +52,9 @@ var forbiddenKeys = map[string]bool{ "meme.memes": true, } +var addBlacklist = regexp.MustCompile(`(?i)disable plugin (.*)`) +var rmBlacklist = regexp.MustCompile(`(?i)enable plugin (.*)`) + // Message responds to the bot hook on recieving messages. // This function returns true if the plugin responds in a meaningful way to the users message. // Otherwise, the function returns false and the bot continues execution of other plugins. @@ -103,6 +107,30 @@ func (p *AdminPlugin) message(conn bot.Connector, k bot.Kind, message msg.Messag return true } + if addBlacklist.MatchString(body) { + submatches := addBlacklist.FindStringSubmatch(message.Body) + plugin := submatches[1] + if err := p.addBlacklist(message.Channel, plugin); err != nil { + p.bot.Send(conn, bot.Message, message.Channel, fmt.Sprintf("I couldn't add that item: %s", err)) + log.Error().Err(err).Msgf("error adding blacklist item") + return true + } + p.bot.Send(conn, bot.Message, message.Channel, fmt.Sprintf("%s disabled. Use `!enable plugin %s` to re-enable it.", plugin, plugin)) + return true + } + + if rmBlacklist.MatchString(body) { + submatches := rmBlacklist.FindStringSubmatch(message.Body) + plugin := submatches[1] + if err := p.rmBlacklist(message.Channel, plugin); err != nil { + p.bot.Send(conn, bot.Message, message.Channel, fmt.Sprintf("I couldn't remove that item: %s", err)) + log.Error().Err(err).Msgf("error removing blacklist item") + return true + } + p.bot.Send(conn, bot.Message, message.Channel, fmt.Sprintf("%s enabled. Use `!disable plugin %s` to disable it.", plugin, plugin)) + return true + } + if strings.ToLower(body) == "password" { p.bot.Send(conn, bot.Message, message.Channel, p.bot.GetPassword()) return true @@ -254,3 +282,32 @@ func (p *AdminPlugin) handleWebAPI(w http.ResponseWriter, r *http.Request) { j, _ := json.Marshal(configEntries) fmt.Fprintf(w, "%s", j) } + +func (p *AdminPlugin) addBlacklist(channel, plugin string) error { + if plugin == "admin" { + return fmt.Errorf("you cannot disable the admin plugin") + } + return p.modBlacklist(`insert or replace into pluginBlacklist values (?, ?)`, channel, plugin) +} + +func (p *AdminPlugin) rmBlacklist(channel, plugin string) error { + return p.modBlacklist(`delete from pluginBlacklist where channel=? and name=?`, channel, plugin) +} + +func (p *AdminPlugin) modBlacklist(query, channel, plugin string) error { + plugins := p.bot.GetPluginNames() + for _, pp := range plugins { + if pp == plugin { + if _, err := p.db.Exec(query, channel, plugin); err != nil { + return fmt.Errorf("%w", err) + } + err := p.bot.RefreshPluginBlacklist() + if err != nil { + return fmt.Errorf("%w", err) + } + return nil + } + } + err := fmt.Errorf("unknown plugin named '%s'", plugin) + return err +}