mirror of
https://github.com/velour/catbase.git
synced 2025-04-04 20:21:42 +00:00
All vue pages now request `/nav` to get a JSON array of navigation instead of relying on the Go template to have the nav built in. This cleans up all of the crufty `{{ "{{ thing }}" }}` that was making it hard to wriet vue. This also paves the way to using the new Go resource embedding so that the pages don't need to be wrapped in Go files.
338 lines
8.9 KiB
Go
338 lines
8.9 KiB
Go
// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
|
|
|
|
package bot
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"net/http"
|
|
"reflect"
|
|
"regexp"
|
|
"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"
|
|
"github.com/velour/catbase/config"
|
|
)
|
|
|
|
// bot type provides storage for bot-wide information, configs, and database connections
|
|
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]Plugin
|
|
pluginOrdering []string
|
|
|
|
// channel -> plugin
|
|
pluginBlacklist map[string]bool
|
|
|
|
// plugin, this is bot-wide
|
|
pluginWhitelist map[string]bool
|
|
|
|
// Users holds information about all of our friends
|
|
users []user.User
|
|
// Represents the bot
|
|
me user.User
|
|
|
|
config *config.Config
|
|
|
|
conn Connector
|
|
|
|
logIn chan msg.Message
|
|
logOut chan msg.Messages
|
|
|
|
version string
|
|
|
|
// The entries to the bot's HTTP interface
|
|
httpEndPoints []EndPoint
|
|
|
|
// filters registered by plugins
|
|
filters map[string]func(string) string
|
|
|
|
callbacks CallbackMap
|
|
|
|
password string
|
|
passwordCreated time.Time
|
|
|
|
quiet bool
|
|
}
|
|
|
|
type EndPoint struct {
|
|
Name string `json:"name"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
// Variable represents a $var replacement
|
|
type Variable struct {
|
|
Variable, Value string
|
|
}
|
|
|
|
// 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)
|
|
|
|
msglog.RunNew(logIn, logOut)
|
|
|
|
users := []user.User{
|
|
{
|
|
Name: config.Get("Nick", "bot"),
|
|
},
|
|
}
|
|
|
|
bot := &bot{
|
|
config: config,
|
|
plugins: make(map[string]Plugin),
|
|
pluginOrdering: make([]string, 0),
|
|
pluginBlacklist: make(map[string]bool),
|
|
pluginWhitelist: 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()
|
|
bot.RefreshPluginWhitelist()
|
|
|
|
http.HandleFunc("/", bot.serveRoot)
|
|
http.HandleFunc("/nav", bot.serveNav)
|
|
|
|
connector.RegisterEvent(bot.Receive)
|
|
|
|
return bot
|
|
}
|
|
|
|
// DefaultConnector is the main connector used for the bot
|
|
// If more than one connector is on, some users may not see all messages if this is used.
|
|
// Usage should be limited to out-of-band communications such as timed messages.
|
|
func (b *bot) DefaultConnector() Connector {
|
|
return b.conn
|
|
}
|
|
|
|
// WhoAmI returns the bot's current registered name
|
|
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) DB() *sqlx.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() {
|
|
if _, err := b.DB().Exec(`create table if not exists variables (
|
|
id integer primary key,
|
|
name string,
|
|
value string
|
|
);`); 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 blacklist table")
|
|
}
|
|
if _, err := b.DB().Exec(`create table if not exists pluginWhitelist (
|
|
name string primary key
|
|
);`); err != nil {
|
|
log.Fatal().Err(err).Msgf("Initial DB migration create whitelist table")
|
|
}
|
|
}
|
|
|
|
// Adds a constructed handler to the bots handlers list
|
|
func (b *bot) AddPlugin(h Plugin) {
|
|
name := reflect.TypeOf(h).String()
|
|
b.plugins[name] = h
|
|
b.pluginOrdering = append(b.pluginOrdering, name)
|
|
}
|
|
|
|
// Who returns users for a channel the bot is in
|
|
// Check the particular connector for channel values
|
|
func (b *bot) Who(channel string) []user.User {
|
|
names := b.conn.Who(channel)
|
|
users := []user.User{}
|
|
for _, n := range names {
|
|
users = append(users, user.New(n))
|
|
}
|
|
return users
|
|
}
|
|
|
|
var suffixRegex *regexp.Regexp
|
|
|
|
// IsCmd checks if message is a command and returns its curtailed version
|
|
func IsCmd(c *config.Config, message string) (bool, string) {
|
|
cmdcs := c.GetArray("CommandChar", []string{"!"})
|
|
botnick := strings.ToLower(c.Get("Nick", "bot"))
|
|
r := fmt.Sprintf(`(?i)\s*\W*\s*?%s\W*$`, botnick)
|
|
if suffixRegex == nil {
|
|
suffixRegex = regexp.MustCompile(r)
|
|
}
|
|
if botnick == "" {
|
|
log.Fatal().
|
|
Msgf(`You must run catbase -set nick -val <your bot nick>`)
|
|
}
|
|
iscmd := false
|
|
lowerMessage := strings.ToLower(message)
|
|
|
|
if strings.HasPrefix(lowerMessage, botnick) &&
|
|
len(lowerMessage) > len(botnick) &&
|
|
(lowerMessage[len(botnick)] == ',' || lowerMessage[len(botnick)] == ':') {
|
|
|
|
iscmd = true
|
|
message = message[len(botnick):]
|
|
|
|
// trim off the customary addressing punctuation
|
|
if message[0] == ':' || message[0] == ',' {
|
|
message = message[1:]
|
|
}
|
|
} else if suffixRegex.MatchString(message) {
|
|
iscmd = true
|
|
message = suffixRegex.ReplaceAllString(message, "")
|
|
} else {
|
|
for _, cmdc := range cmdcs {
|
|
if strings.HasPrefix(lowerMessage, cmdc) && len(cmdc) > 0 {
|
|
iscmd = true
|
|
message = message[len(cmdc):]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// trim off any whitespace left on the message
|
|
message = strings.TrimSpace(message)
|
|
|
|
return iscmd, message
|
|
}
|
|
|
|
func (b *bot) CheckAdmin(nick string) bool {
|
|
for _, u := range b.Config().GetArray("Admins", []string{}) {
|
|
if nick == u {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Register a text filter which every outgoing message is passed through
|
|
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})
|
|
}
|
|
|
|
// GetPassword returns a random password generated for the bot
|
|
// Passwords expire in 24h and are used for the web interface
|
|
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
|
|
}
|
|
|
|
// SetQuiet is called to silence the bot from sending channel messages
|
|
func (b *bot) SetQuiet(status bool) {
|
|
b.quiet = status
|
|
}
|
|
|
|
// RefreshPluginBlacklist loads data for which plugins are disabled for particular channels
|
|
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
|
|
}
|
|
|
|
// RefreshPluginWhitelist loads data for which plugins are enabled
|
|
func (b *bot) RefreshPluginWhitelist() error {
|
|
whitelistItems := []struct {
|
|
Name string
|
|
}{
|
|
{Name: "admin"}, // we must always ensure admin is on!
|
|
}
|
|
if err := b.DB().Select(&whitelistItems, `select name from pluginWhitelist`); err != nil {
|
|
return fmt.Errorf("%w", err)
|
|
}
|
|
b.pluginWhitelist = make(map[string]bool)
|
|
for _, i := range whitelistItems {
|
|
b.pluginWhitelist[i.Name] = true
|
|
}
|
|
log.Debug().Interface("whitelist", b.pluginWhitelist).Msgf("Refreshed plugin whitelist")
|
|
return nil
|
|
}
|
|
|
|
// GetPluginNames returns an ordered list of plugins loaded (used for blacklisting)
|
|
func (b *bot) GetPluginNames() []string {
|
|
names := []string{}
|
|
for _, name := range b.pluginOrdering {
|
|
names = append(names, pluginNameStem(name))
|
|
}
|
|
return names
|
|
}
|
|
|
|
func (b *bot) GetWhitelist() []string {
|
|
list := []string{}
|
|
for k := range b.pluginWhitelist {
|
|
list = append(list, k)
|
|
}
|
|
return list
|
|
}
|
|
|
|
func (b *bot) onBlacklist(channel, plugin string) bool {
|
|
return b.pluginBlacklist[channel+plugin]
|
|
}
|
|
|
|
func (b *bot) onWhitelist(plugin string) bool {
|
|
return b.pluginWhitelist[plugin]
|
|
}
|
|
|
|
func pluginNameStem(name string) string {
|
|
return strings.Split(strings.TrimPrefix(name, "*"), ".")[0]
|
|
}
|