catbase/bot/bot.go

270 lines
6.2 KiB
Go

// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
package bot
import (
"database/sql"
"html/template"
"log"
"net/http"
"strings"
"github.com/jmoiron/sqlx"
"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]Handler
pluginOrdering []string
// Users holds information about all of our friends
users []user.User
// Represents the bot
me user.User
config *config.Config
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
}
type Variable struct {
Variable, Value string
}
// Newbot 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{
user.User{
Name: config.Nick,
},
}
bot := &bot{
config: config,
plugins: make(map[string]Handler),
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),
}
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)
return bot
}
// 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
}
// 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 (
id integer primary key,
name string,
value string
);`); err != nil {
log.Fatal("Initial DB migration create variables table: ", err)
}
}
// Adds a constructed handler to the bots handlers list
func (b *bot) AddHandler(name string, h Handler) {
b.plugins[strings.ToLower(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 {
names := b.conn.Who(channel)
users := []user.User{}
for _, n := range names {
users = append(users, user.New(n))
}
return users
}
var rootIndex string = `
<!DOCTYPE html>
<html>
<head>
<title>Factoids</title>
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.1.0/pure-min.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
{{if .EndPoints}}
<div style="padding-top: 1em;">
<table class="pure-table">
<thead>
<tr>
<th>Plugin</th>
</tr>
</thead>
<tbody>
{{range $key, $value := .EndPoints}}
<tr>
<td><a href="{{$value}}">{{$key}}</a></td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
</html>
`
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
func IsCmd(c *config.Config, message string) (bool, string) {
cmdcs := c.CommandChar
botnick := strings.ToLower(c.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 {
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().Admins {
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().Admins {
if nick == u {
return true
}
}
return false
}