catbase/bot/bot.go

277 lines
6.4 KiB
Go
Raw Normal View History

2016-01-17 18:00:44 +00:00
// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
package bot
import (
"database/sql"
2013-06-01 17:39:17 +00:00
"html/template"
"log"
2013-06-01 17:10:15 +00:00
"net/http"
"strings"
"time"
2016-03-19 18:02:46 +00:00
"github.com/jmoiron/sqlx"
2016-01-17 18:00:44 +00:00
"github.com/velour/catbase/config"
_ "github.com/mattn/go-sqlite3"
)
// 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
// Represents the bot
Me User
Config *config.Config
2016-03-10 18:37:07 +00:00
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.
2016-03-19 18:02:46 +00:00
DB *sqlx.DB
DBVersion int64
logIn chan Message
logOut chan Messages
Version string
2013-06-01 17:10:15 +00:00
// The entries to the bot's HTTP interface
2013-06-01 17:39:17 +00:00
httpEndPoints map[string]string
}
// Log provides a slice of messages in order
type Log Messages
type Messages []Message
type Logger struct {
in <-chan Message
out chan<- Messages
entries Messages
}
func NewLogger(in chan Message, out chan Messages) *Logger {
return &Logger{in, out, make(Messages, 0)}
}
func RunNewLogger(in chan Message, out chan Messages) {
logger := NewLogger(in, out)
go logger.Run()
}
func (l *Logger) sendEntries() {
l.out <- l.entries
}
func (l *Logger) Run() {
var msg Message
for {
select {
case msg = <-l.in:
l.entries = append(l.entries, msg)
case l.out <- l.entries:
go l.sendEntries()
}
}
}
type Message struct {
User *User
Channel, Body string
2012-08-25 04:49:48 +00:00
Raw string
Command bool
2012-08-25 04:49:48 +00:00
Action bool
Time time.Time
2013-04-21 20:49:00 +00:00
Host string
}
type Variable struct {
Variable, Value string
}
// NewBot creates a Bot for a given connection and set of handlers.
2016-03-10 18:37:07 +00:00
func NewBot(config *config.Config, connector Connector) *Bot {
2016-03-19 18:02:46 +00:00
sqlDB, err := sqlx.Open("sqlite3", config.DB.File)
if err != nil {
log.Fatal(err)
}
logIn := make(chan Message)
logOut := make(chan Messages)
RunNewLogger(logIn, logOut)
users := []User{
User{
Name: config.Nick,
},
}
2013-06-01 17:10:15 +00:00
bot := &Bot{
Config: config,
Plugins: make(map[string]Handler),
PluginOrdering: make([]string, 0),
2016-03-10 18:37:07 +00:00
Conn: connector,
Users: users,
Me: users[0],
DB: sqlDB,
logIn: logIn,
logOut: logOut,
Version: config.Version,
2013-06-01 17:39:17 +00:00
httpEndPoints: make(map[string]string),
}
2013-06-01 17:10:15 +00:00
bot.migrateDB()
2013-06-01 17:10:15 +00:00
http.HandleFunc("/", bot.serveRoot)
2013-06-01 17:29:12 +00:00
if config.HttpAddr == "" {
config.HttpAddr = "127.0.0.1:1337"
}
go http.ListenAndServe(config.HttpAddr, nil)
2013-06-01 17:10:15 +00:00
2016-03-11 02:11:52 +00:00
connector.RegisterMessageReceived(bot.MsgReceived)
connector.RegisterEventReceived(bot.EventReceived)
2016-03-10 18:37:07 +00:00
2013-06-01 17:10:15 +00:00
return bot
}
// 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 b.DBVersion == 1 {
if _, err := b.DB.Exec(`create table if not exists variables (
id integer primary key,
name string,
perms string,
type string
);`); err != nil {
log.Fatal("Initial DB migration create variables table: ", err)
}
if _, err := b.DB.Exec(`create table if not exists 'values' (
id integer primary key,
varId integer,
value string
);`); err != nil {
log.Fatal("Initial DB migration create values 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)
2013-06-01 17:10:15 +00:00
if entry := h.RegisterWeb(); entry != nil {
2013-06-01 17:39:17 +00:00
b.httpEndPoints[name] = *entry
2013-06-01 17:10:15 +00:00
}
}
func (b *Bot) Who(channel string) []User {
out := []User{}
for _, u := range b.Users {
if u.Name != b.Config.Nick {
out = append(out, u)
}
}
return out
}
2013-06-01 17:39:17 +00:00
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">
2014-04-21 01:12:02 +00:00
<meta name="viewport" content="width=device-width, initial-scale=1">
2013-06-01 17:39:17 +00:00
</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>
`
2013-06-01 17:10:15 +00:00
func (b *Bot) serveRoot(w http.ResponseWriter, r *http.Request) {
2013-06-01 17:39:17 +00:00
context := make(map[string]interface{})
context["EndPoints"] = b.httpEndPoints
t, err := template.New("rootIndex").Parse(rootIndex)
if err != nil {
log.Println(err)
2013-06-01 17:10:15 +00:00
}
2013-06-01 17:39:17 +00:00
t.Execute(w, context)
2013-06-01 17:10:15 +00:00
}
2016-03-11 02:11:52 +00:00
// Checks if message is a command and returns its curtailed version
func IsCmd(c *config.Config, message string) (bool, string) {
cmdc := c.CommandChar
botnick := strings.ToLower(c.Nick)
iscmd := false
lowerMessage := strings.ToLower(message)
if strings.HasPrefix(lowerMessage, cmdc) && len(cmdc) > 0 {
iscmd = true
message = message[len(cmdc):]
// } else if match, _ := regexp.MatchString(rex, lowerMessage); match {
} else 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:]
}
}
// trim off any whitespace left on the message
message = strings.TrimSpace(message)
return iscmd, message
}