// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors. package bot import ( "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" "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 // 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 } type EndPoint struct { Name, URL string } // 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), 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() http.HandleFunc("/", bot.serveRoot) 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) 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") } } // 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) } 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 } // 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")) if botnick == "" { log.Fatal(). Msgf(`You must run catbase -set nick -val `) } 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 { 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 } var users = map[string]*user.User{} func (b *bot) GetUser(nick string) *user.User { if _, ok := users[nick]; !ok { users[nick] = &user.User{ Name: nick, Admin: b.checkAdmin(nick), } } return users[nick] } func (b *bot) NewUser(nick string) *user.User { return &user.User{ Name: nick, Admin: b.checkAdmin(nick), } } 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}) } 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 }