2016-01-17 18:00:44 +00:00
|
|
|
// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
|
2013-12-10 23:37:07 +00:00
|
|
|
|
2012-08-17 20:38:15 +00:00
|
|
|
package bot
|
|
|
|
|
2012-10-11 20:28:00 +00:00
|
|
|
import (
|
2016-01-15 06:12:26 +00:00
|
|
|
"database/sql"
|
2013-06-01 17:39:17 +00:00
|
|
|
"html/template"
|
|
|
|
"log"
|
2013-06-01 17:10:15 +00:00
|
|
|
"net/http"
|
2016-03-29 16:34:04 +00:00
|
|
|
"regexp"
|
2012-10-11 20:28:00 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
2014-04-20 19:08:24 +00:00
|
|
|
|
2016-03-19 18:02:46 +00:00
|
|
|
"github.com/jmoiron/sqlx"
|
2016-03-29 16:34:04 +00:00
|
|
|
"github.com/mattn/go-sqlite3"
|
2016-01-17 18:00:44 +00:00
|
|
|
"github.com/velour/catbase/config"
|
2012-10-11 20:28:00 +00:00
|
|
|
)
|
2012-08-17 20:38:15 +00:00
|
|
|
|
2016-03-30 14:00:20 +00:00
|
|
|
// bot type provides storage for bot-wide information, configs, and database connections
|
|
|
|
type bot struct {
|
2012-08-17 22:09:29 +00:00
|
|
|
// 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
|
2016-03-30 14:00:20 +00:00
|
|
|
plugins map[string]Handler
|
|
|
|
pluginOrdering []string
|
2012-08-17 22:09:29 +00:00
|
|
|
|
|
|
|
// Users holds information about all of our friends
|
2016-03-30 14:00:20 +00:00
|
|
|
users []User
|
2013-05-08 00:08:18 +00:00
|
|
|
// Represents the bot
|
2016-03-30 14:00:20 +00:00
|
|
|
me User
|
2012-08-17 22:09:29 +00:00
|
|
|
|
2016-03-30 14:00:20 +00:00
|
|
|
config *config.Config
|
2012-08-17 22:09:29 +00:00
|
|
|
|
2016-03-30 14:00:20 +00:00
|
|
|
conn Connector
|
2016-03-10 18:37:07 +00:00
|
|
|
|
2016-01-15 06:12:26 +00:00
|
|
|
// SQL DB
|
2016-01-17 17:20:16 +00:00
|
|
|
// 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-30 14:00:20 +00:00
|
|
|
db *sqlx.DB
|
|
|
|
dbVersion int64
|
2012-08-26 23:23:51 +00:00
|
|
|
|
2012-10-11 20:28:00 +00:00
|
|
|
logIn chan Message
|
|
|
|
logOut chan Messages
|
|
|
|
|
2016-03-30 14:00:20 +00:00
|
|
|
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
|
2012-08-17 20:38:15 +00:00
|
|
|
}
|
|
|
|
|
2012-10-11 20:28:00 +00:00
|
|
|
// 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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-08-17 22:09:29 +00:00
|
|
|
type Message struct {
|
|
|
|
User *User
|
|
|
|
Channel, Body string
|
2012-08-25 04:49:48 +00:00
|
|
|
Raw string
|
2012-08-18 01:39:26 +00:00
|
|
|
Command bool
|
2012-08-25 04:49:48 +00:00
|
|
|
Action bool
|
2012-10-11 20:28:00 +00:00
|
|
|
Time time.Time
|
2013-04-21 20:49:00 +00:00
|
|
|
Host string
|
2012-08-17 22:09:29 +00:00
|
|
|
}
|
|
|
|
|
2012-08-26 23:23:51 +00:00
|
|
|
type Variable struct {
|
|
|
|
Variable, Value string
|
|
|
|
}
|
|
|
|
|
2016-03-29 16:34:04 +00:00
|
|
|
func init() {
|
|
|
|
regex := func(re, s string) (bool, error) {
|
|
|
|
return regexp.MatchString(re, s)
|
|
|
|
}
|
|
|
|
sql.Register("sqlite3_custom",
|
|
|
|
&sqlite3.SQLiteDriver{
|
|
|
|
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
|
|
|
|
return conn.RegisterFunc("REGEXP", regex, true)
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2016-03-30 14:00:20 +00:00
|
|
|
// Newbot creates a bot for a given connection and set of handlers.
|
|
|
|
func New(config *config.Config, connector Connector) Bot {
|
2016-03-29 16:34:04 +00:00
|
|
|
sqlDB, err := sqlx.Open("sqlite3_custom", config.DB.File)
|
2012-08-17 21:37:49 +00:00
|
|
|
if err != nil {
|
2016-01-15 06:12:26 +00:00
|
|
|
log.Fatal(err)
|
2012-08-17 21:37:49 +00:00
|
|
|
}
|
|
|
|
|
2012-10-11 20:28:00 +00:00
|
|
|
logIn := make(chan Message)
|
|
|
|
logOut := make(chan Messages)
|
|
|
|
|
|
|
|
RunNewLogger(logIn, logOut)
|
|
|
|
|
2013-05-07 23:05:40 +00:00
|
|
|
users := []User{
|
|
|
|
User{
|
2013-06-02 01:59:55 +00:00
|
|
|
Name: config.Nick,
|
2013-05-07 23:05:40 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2016-03-30 14:00:20 +00:00
|
|
|
bot := &bot{
|
|
|
|
config: config,
|
|
|
|
plugins: make(map[string]Handler),
|
|
|
|
pluginOrdering: make([]string, 0),
|
|
|
|
conn: connector,
|
|
|
|
users: users,
|
|
|
|
me: users[0],
|
|
|
|
db: sqlDB,
|
2012-10-11 20:28:00 +00:00
|
|
|
logIn: logIn,
|
|
|
|
logOut: logOut,
|
2016-03-30 14:00:20 +00:00
|
|
|
version: config.Version,
|
2013-06-01 17:39:17 +00:00
|
|
|
httpEndPoints: make(map[string]string),
|
2012-08-17 20:38:15 +00:00
|
|
|
}
|
2013-06-01 17:10:15 +00:00
|
|
|
|
2016-01-15 06:12:26 +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
|
2012-08-17 20:38:15 +00:00
|
|
|
}
|
|
|
|
|
2016-03-30 14:00:20 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2016-01-15 06:12:26 +00:00
|
|
|
// 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.
|
2016-03-30 14:00:20 +00:00
|
|
|
func (b *bot) migrateDB() {
|
|
|
|
_, err := b.db.Exec(`create table if not exists version (version integer);`)
|
2016-01-15 06:12:26 +00:00
|
|
|
if err != nil {
|
2016-01-15 13:59:26 +00:00
|
|
|
log.Fatal("Initial DB migration create version table: ", err)
|
2016-01-15 06:12:26 +00:00
|
|
|
}
|
2016-01-15 13:59:26 +00:00
|
|
|
var version sql.NullInt64
|
2016-03-30 14:00:20 +00:00
|
|
|
err = b.db.QueryRow("select max(version) from version").Scan(&version)
|
2016-01-15 13:59:26 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal("Initial DB migration get version: ", err)
|
|
|
|
}
|
|
|
|
if version.Valid {
|
2016-03-30 14:00:20 +00:00
|
|
|
b.dbVersion = version.Int64
|
|
|
|
log.Printf("Database version: %v\n", b.dbVersion)
|
2016-01-15 13:59:26 +00:00
|
|
|
} else {
|
2016-01-15 06:12:26 +00:00
|
|
|
log.Printf("No versions, we're the first!.")
|
2016-03-30 14:00:20 +00:00
|
|
|
_, err := b.db.Exec(`insert into version (version) values (1)`)
|
2016-01-15 06:12:26 +00:00
|
|
|
if err != nil {
|
2016-01-15 13:59:26 +00:00
|
|
|
log.Fatal("Initial DB migration insert: ", err)
|
2016-01-15 06:12:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-30 14:00:20 +00:00
|
|
|
if b.dbVersion == 1 {
|
|
|
|
if _, err := b.db.Exec(`create table if not exists variables (
|
2016-01-15 06:12:26 +00:00
|
|
|
id integer primary key,
|
|
|
|
name string,
|
|
|
|
perms string,
|
|
|
|
type string
|
|
|
|
);`); err != nil {
|
2016-01-15 13:59:26 +00:00
|
|
|
log.Fatal("Initial DB migration create variables table: ", err)
|
2016-01-15 06:12:26 +00:00
|
|
|
}
|
2016-03-30 14:00:20 +00:00
|
|
|
if _, err := b.db.Exec(`create table if not exists 'values' (
|
2016-01-15 06:12:26 +00:00
|
|
|
id integer primary key,
|
|
|
|
varId integer,
|
|
|
|
value string
|
|
|
|
);`); err != nil {
|
2016-01-15 13:59:26 +00:00
|
|
|
log.Fatal("Initial DB migration create values table: ", err)
|
2016-01-15 06:12:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-08-17 20:38:15 +00:00
|
|
|
// Adds a constructed handler to the bots handlers list
|
2016-03-30 14:00:20 +00:00
|
|
|
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
|
|
|
}
|
2012-08-17 21:37:49 +00:00
|
|
|
}
|
2012-08-26 20:15:50 +00:00
|
|
|
|
2016-03-30 14:00:20 +00:00
|
|
|
func (b *bot) Who(channel string) []User {
|
2014-04-21 01:06:42 +00:00
|
|
|
out := []User{}
|
2016-03-30 14:00:20 +00:00
|
|
|
for _, u := range b.users {
|
|
|
|
if u.Name != b.Config().Nick {
|
2014-04-21 01:06:42 +00:00
|
|
|
out = append(out, u)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return out
|
2013-06-02 01:59:55 +00:00
|
|
|
}
|
|
|
|
|
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>
|
|
|
|
`
|
|
|
|
|
2016-03-30 14:00:20 +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
|
|
|
|
}
|