Compare commits

...

1 Commits

Author SHA1 Message Date
Chris Sexton 39d2d89759 initial commit 2021-10-05 18:52:59 -04:00
14 changed files with 1277 additions and 39 deletions

154
api.go Normal file
View File

@ -0,0 +1,154 @@
package beebot
import (
"encoding/json"
"io/ioutil"
"net/http"
"os"
"github.com/go-chi/chi"
"github.com/rs/zerolog/log"
)
func (b *BeeBot) apiEndpoints() http.Handler {
r := chi.NewRouter()
r.Route("/nav", func(r chi.Router) {
r.Get("/", b.getNav)
})
r.Route("/config", func(r chi.Router) {
r.Get("/", b.getConfig)
r.Post("/", b.setConfig)
r.Delete("/", b.deleteConfig)
})
r.Route("/filters", func(r chi.Router) {
r.Get("/", b.getFilters)
r.Post("/", b.postFilters)
r.Put("/{name}", b.putFilters)
r.Delete("/", b.deleteFilters)
})
r.Route("/log", func(r chi.Router) {
r.Get("/", b.getLog)
})
return r
}
type configEntry struct {
Key string `json:"key"`
Value string `json:"value"`
}
func (b *BeeBot) getNav(w http.ResponseWriter, r *http.Request) {
j, _ := json.Marshal(b.nav)
w.Write(j)
}
func (b *BeeBot) getConfig(w http.ResponseWriter, r *http.Request) {
entries := []configEntry{}
err := b.db.Select(&entries, `select key, value from config`)
if err != nil {
log.Error().Err(err).Msg("Could not get configuration entries")
w.WriteHeader(500)
j, _ := json.Marshal(err)
w.Write(j)
}
j, _ := json.Marshal(entries)
w.Write(j)
}
func (b *BeeBot) setConfig(w http.ResponseWriter, r *http.Request) {
config := configEntry{}
body, _ := ioutil.ReadAll(r.Body)
err := json.Unmarshal(body, &config)
if err != nil {
log.Error().Err(err).Msg("Could not get configuration entries")
w.WriteHeader(400)
j, _ := json.Marshal(err)
w.Write(j)
}
err = b.c.Set(config.Key, config.Value)
if err != nil {
log.Error().Err(err).Msg("Could not set configuration entry")
w.WriteHeader(400)
j, _ := json.Marshal(err)
w.Write(j)
}
}
func (b *BeeBot) deleteConfig(w http.ResponseWriter, r *http.Request) {
config := configEntry{}
body, _ := ioutil.ReadAll(r.Body)
err := json.Unmarshal(body, &config)
if err != nil {
log.Error().Err(err).Msg("Could not get configuration entries")
w.WriteHeader(400)
j, _ := json.Marshal(err)
w.Write(j)
}
log.Info().Msgf("Deleting config: %s", config.Key)
err = b.c.Unset(config.Key)
if err != nil {
log.Error().Err(err).Msg("Could not unset configuration entry")
w.WriteHeader(400)
j, _ := json.Marshal(err)
w.Write(j)
}
resp, _ := json.Marshal(struct {
Status string `json:"status"`
}{"ok"})
w.Write(resp)
}
func (b *BeeBot) getFilters(w http.ResponseWriter, r *http.Request) {
filters, err := b.AllFilters()
if err != nil {
log.Error().Err(err).Msg("Could not get filters")
}
j, _ := json.Marshal(filters)
w.Write(j)
}
func (b *BeeBot) postFilters(w http.ResponseWriter, r *http.Request) {
filter := Filter{}
body, _ := ioutil.ReadAll(r.Body)
err := json.Unmarshal(body, &filter)
filter.populate(b.db)
if err != nil {
log.Error().Err(err).Msg("Could not read filter entry")
w.WriteHeader(400)
j, _ := json.Marshal(err)
w.Write(j)
}
err = filter.Save()
if err != nil {
log.Error().Err(err).Msg("Could not save filter")
w.WriteHeader(400)
j, _ := json.Marshal(err)
w.Write(j)
}
out, err := json.Marshal(filter)
if err != nil {
log.Error().Err(err).Msg("Could not marshal filter output")
w.WriteHeader(500)
j, _ := json.Marshal(err)
w.Write(j)
}
w.Write(out)
}
func (b *BeeBot) putFilters(w http.ResponseWriter, r *http.Request) {
}
func (b *BeeBot) deleteFilters(w http.ResponseWriter, r *http.Request) {
}
func (b *BeeBot) getLog(w http.ResponseWriter, r *http.Request) {
f, _ := os.Open(b.logPath)
logs, err := ioutil.ReadAll(f)
if err != nil {
log.Error().Err(err).Msg("Could not open logs")
w.WriteHeader(500)
j, _ := json.Marshal(err)
w.Write(j)
}
w.Write(logs)
}

185
beebot.go Normal file
View File

@ -0,0 +1,185 @@
package beebot
import (
"fmt"
"time"
"code.chrissexton.org/cws/BeeBot/config"
"github.com/jmoiron/sqlx"
"github.com/jpillora/backoff"
"github.com/jzelinskie/geddit"
"github.com/rs/zerolog/log"
)
// DefaultAddr is the HTTP service address
const DefaultAddr = "127.0.0.1:9595"
// BeeBot represents our bot
type BeeBot struct {
reddit string
logPath string
nav map[string]string
debug bool
db *sqlx.DB
c *config.Config
*geddit.OAuthSession
}
// New creates a BeeBot instance, its database, and connects to reddit
func New(dbFilePath, logFilePath string, debug bool) (*BeeBot, error) {
db, err := sqlx.Connect("sqlite", dbFilePath)
if err != nil {
return nil, err
}
c := config.New(db)
clientID := c.Get("clientid", "")
clientSecret := c.Get("clientsecret", "")
userAgent := c.Get("userAgent", "BeeBot")
baseAddr := c.Get("baseaddr", DefaultAddr)
userName := c.Get("username", "")
password := c.Get("password", "")
reddit := c.Get("reddit", "")
o, err := geddit.NewOAuthSession(
clientID,
clientSecret,
userAgent,
fmt.Sprintf("http://%s/cb", baseAddr),
)
if err != nil {
return nil, err
}
if err = o.LoginAuth(userName, password); err != nil {
return nil, err
}
b := &BeeBot{
reddit: reddit,
logPath: logFilePath,
nav: make(map[string]string),
debug: debug,
db: db,
c: c,
OAuthSession: o,
}
b.setupDB()
return b, nil
}
// Serve starts a polling service with exponential backoff
func (b *BeeBot) Serve(dur time.Duration, done chan (bool)) {
backoff := &backoff.Backoff{
Min: time.Duration(b.c.GetInt("backoff.min", 100)) * time.Millisecond,
Max: time.Duration(b.c.GetInt("backoff.max", 10)) * time.Second,
Factor: b.c.GetFloat64("backoff.factor", 2),
Jitter: b.c.GetInt("backoff.jitter", 1) == 1,
}
for {
timer := time.NewTimer(backoff.Duration())
select {
case <-done:
timer.Stop()
return
case <-timer.C:
if err := b.Run(); err == nil {
backoff.Reset()
}
}
}
}
// Run triggers a single query and Filter of the reddit
func (b *BeeBot) Run() error {
offenders := map[string]map[string]bool{}
filters, err := b.AllFilters()
if handleErr(err, "Could not get list of filters") {
return err
}
for _, f := range filters {
offenders[f.Name] = map[string]bool{}
tmpOffenders := []string{}
err := b.db.Select(&tmpOffenders, "select offender from offenders where type=?", f.Name)
if handleErr(err, "could not get %s offenders", f.Name) {
return err
}
for _, o := range tmpOffenders {
offenders[f.Name][o] = true
}
}
comments, err := b.SubredditComments(b.reddit)
if handleErr(err, "could not get subreddit comments for %s", b.reddit) {
return err
}
for _, c := range comments {
for _, f := range filters {
if f.regex.MatchString(c.Body) {
if _, ok := offenders[f.Name][c.Author]; ok {
log.Debug().Msgf("Skipping offender %s", c.Author)
} else {
offenders[f.Name][c.Author] = true
_, err = b.db.Exec(`insert into offenders (offender, type) values (?, ?)`, c.Author, f.Name)
if handleErr(err, "could not insert raisin offenders") {
return err
}
}
}
}
}
log.Debug().Msgf("Processed %d comments", len(comments))
subOpts := geddit.ListingOptions{
Limit: 10,
}
posts, err := b.SubredditSubmissions(b.reddit, geddit.NewSubmissions, subOpts)
for _, p := range posts {
for _, f := range filters {
if f.regex.MatchString(p.Title) || f.regex.MatchString(p.Selftext) {
if _, ok := offenders[f.Name][p.Author]; ok {
log.Debug().Msgf("Skipping offender %s", p.Author)
} else {
offenders[f.Name][p.Author] = true
_, err = b.db.Exec(`insert into offenders (offender, type) values (?, ?)`, p.Author, f.Name)
if handleErr(err, "could not insert raisin offenders") {
return err
}
}
}
}
}
log.Debug().Msgf("Processed %d posts", len(posts))
return nil
}
func in(val string, from []string) bool {
for _, e := range from {
if e == val {
return true
}
}
return false
}
func handleErr(err error, message string, extras ...interface{}) bool {
if err != nil {
log.Error().
Err(err).
Msgf(message, extras...)
return true
}
return false
}

73
beebot.json Normal file
View File

@ -0,0 +1,73 @@
{"level":"info","time":"2021-09-27T11:33:13-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T11:33:13-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"debug","time":"2021-09-27T11:33:13-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key baseaddr is empty"}
{"level":"debug","time":"2021-09-27T11:33:14-04:00","caller":"/home/cws/src/BeeBot/beebot.go:151","message":"Processed 1 comments"}
{"level":"debug","time":"2021-09-27T11:33:15-04:00","caller":"/home/cws/src/BeeBot/beebot.go:172","message":"Processed 2 posts"}
{"level":"info","time":"2021-09-27T11:38:23-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T11:38:23-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"debug","time":"2021-09-27T11:38:23-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key baseaddr is empty"}
{"level":"debug","time":"2021-09-27T11:38:24-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key baseaddr is empty"}
{"level":"info","time":"2021-09-27T11:39:31-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T11:39:31-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"info","time":"2021-09-27T11:41:02-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T11:41:02-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"info","time":"2021-09-27T11:41:56-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T11:41:56-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"info","time":"2021-09-27T11:42:37-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T11:42:37-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"info","time":"2021-09-27T11:43:33-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T11:43:33-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"info","time":"2021-09-27T12:05:12-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T12:05:12-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"info","time":"2021-09-27T12:12:01-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T12:12:01-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"info","time":"2021-09-27T12:12:29-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T12:12:29-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"info","time":"2021-09-27T12:24:40-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T12:24:40-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"debug","time":"2021-09-27T12:24:41-04:00","caller":"/home/cws/src/BeeBot/web.go:55","message":"using live mode"}
{"level":"error","error":"open templates/templates/index.html: no such file or directory","time":"2021-09-27T12:24:43-04:00","caller":"/home/cws/src/BeeBot/web.go:47","message":"Could not read template"}
{"level":"info","time":"2021-09-27T12:25:24-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"error","error":"open templates/index.html: file does not exist","time":"2021-09-27T12:25:27-04:00","caller":"/home/cws/src/BeeBot/web.go:47","message":"Could not read template"}
{"level":"info","time":"2021-09-27T12:25:54-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"info","time":"2021-09-27T12:26:09-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T12:26:09-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"debug","time":"2021-09-27T12:26:10-04:00","caller":"/home/cws/src/BeeBot/web.go:55","message":"using live mode"}
{"level":"info","time":"2021-09-27T12:26:32-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T12:26:32-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"debug","time":"2021-09-27T12:26:32-04:00","caller":"/home/cws/src/BeeBot/web.go:54","message":"using live mode"}
{"level":"error","error":"open templates/log.html: no such file or directory","time":"2021-09-27T12:26:51-04:00","caller":"/home/cws/src/BeeBot/web.go:46","message":"Could not read template"}
{"level":"info","time":"2021-09-27T15:40:20-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T15:40:20-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"debug","time":"2021-09-27T15:40:21-04:00","caller":"/home/cws/src/BeeBot/web.go:54","message":"using live mode"}
{"level":"error","error":"open templates/filters.html: no such file or directory","time":"2021-09-27T16:03:48-04:00","caller":"/home/cws/src/BeeBot/web.go:46","message":"Could not read template"}
{"level":"error","error":"unexpected end of JSON input","time":"2021-09-27T16:18:40-04:00","caller":"/home/cws/src/BeeBot/api.go:63","message":"Could not get configuration entries"}
{"level":"error","error":"unexpected end of JSON input","time":"2021-09-27T16:19:26-04:00","caller":"/home/cws/src/BeeBot/api.go:63","message":"Could not get configuration entries"}
{"level":"error","error":"unexpected end of JSON input","time":"2021-09-27T16:20:27-04:00","caller":"/home/cws/src/BeeBot/api.go:63","message":"Could not get configuration entries"}
{"level":"info","time":"2021-09-27T16:21:45-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T16:21:45-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"debug","time":"2021-09-27T16:21:46-04:00","caller":"/home/cws/src/BeeBot/web.go:54","message":"using live mode"}
{"level":"info","time":"2021-09-27T16:28:19-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T16:28:19-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"debug","time":"2021-09-27T16:28:20-04:00","caller":"/home/cws/src/BeeBot/web.go:54","message":"using live mode"}
{"level":"error","error":"unexpected end of JSON input","time":"2021-09-27T16:28:24-04:00","caller":"/home/cws/src/BeeBot/api.go:82","message":"Could not get configuration entries"}
{"level":"error","error":"unexpected end of JSON input","time":"2021-09-27T16:28:32-04:00","caller":"/home/cws/src/BeeBot/api.go:82","message":"Could not get configuration entries"}
{"level":"error","error":"unexpected end of JSON input","time":"2021-09-27T16:28:34-04:00","caller":"/home/cws/src/BeeBot/api.go:82","message":"Could not get configuration entries"}
{"level":"error","error":"json: cannot unmarshal object into Go value of type string","time":"2021-09-27T16:29:02-04:00","caller":"/home/cws/src/BeeBot/api.go:82","message":"Could not get configuration entries"}
{"level":"info","time":"2021-09-27T16:29:37-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T16:29:37-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"debug","time":"2021-09-27T16:29:38-04:00","caller":"/home/cws/src/BeeBot/web.go:54","message":"using live mode"}
{"level":"info","time":"2021-09-27T16:31:47-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T16:31:47-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"debug","time":"2021-09-27T16:31:48-04:00","caller":"/home/cws/src/BeeBot/web.go:54","message":"using live mode"}
{"level":"info","time":"2021-09-27T16:37:11-04:00","caller":"/home/cws/src/BeeBot/cmd/beebot/main.go:47","message":"BeeBot v1.00"}
{"level":"debug","time":"2021-09-27T16:37:11-04:00","caller":"/home/cws/src/BeeBot/config/config.go:85","message":"WARN: Key useragent is empty"}
{"level":"debug","time":"2021-09-27T16:37:12-04:00","caller":"/home/cws/src/BeeBot/web.go:54","message":"using live mode"}
{"level":"info","time":"2021-09-27T16:37:13-04:00","caller":"/home/cws/src/BeeBot/api.go:87","message":"Deleting config: "}
{"level":"info","time":"2021-09-27T16:38:29-04:00","caller":"/home/cws/src/BeeBot/api.go:87","message":"Deleting config: "}
{"level":"info","time":"2021-09-27T16:38:52-04:00","caller":"/home/cws/src/BeeBot/api.go:87","message":"Deleting config: "}
{"level":"info","time":"2021-09-27T16:39:25-04:00","caller":"/home/cws/src/BeeBot/api.go:87","message":"Deleting config: "}
{"level":"error","error":"json: cannot unmarshal object into Go struct field configEntry.key of type string","time":"2021-09-27T16:39:52-04:00","caller":"/home/cws/src/BeeBot/api.go:82","message":"Could not get configuration entries"}
{"level":"info","time":"2021-09-27T16:39:52-04:00","caller":"/home/cws/src/BeeBot/api.go:87","message":"Deleting config: "}
{"level":"info","time":"2021-09-27T16:41:08-04:00","caller":"/home/cws/src/BeeBot/api.go:87","message":"Deleting config: key2"}
{"level":"info","time":"2021-09-27T16:41:10-04:00","caller":"/home/cws/src/BeeBot/api.go:87","message":"Deleting config: key"}

View File

@ -3,9 +3,14 @@ package main
import (
"flag"
"fmt"
"log"
"os"
beebot "code.chrissexton.org/cws/BeeBot"
"github.com/jzelinskie/geddit"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
_ "modernc.org/sqlite"
)
const version = 1.0
@ -13,51 +18,39 @@ const version = 1.0
var userAgent = fmt.Sprintf("BeeBot:%.2f (by u/phlyingpenguin)", version)
var scopes = "identity read edit"
var clientID = flag.String("id", "", "Client ID")
var clientSecret = flag.String("secret", "", "Client Secret")
var baseAddr = flag.String("url", "127.0.0.1:9595", "Base address")
var userName = flag.String("user", "_BeeBot_", "Login name")
var password = flag.String("password", "nope", "Login password")
var reddit = flag.String("reddit", "MeadTest", "Default reddit")
var debug = flag.Bool("debug", false, "Turn debug printing on")
var dbFilePath = flag.String("db", "beebot.db", "Database file path")
var logFilePath = flag.String("log", "beebot.json", "Log file path")
var o *geddit.OAuthSession
func main() {
flag.Parse()
log.Printf("BeeBot v%.2f", version)
var err error
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if *debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
o, err = geddit.NewOAuthSession(
*clientID,
*clientSecret,
userAgent,
fmt.Sprintf("http://%s/cb", *baseAddr),
)
consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
logFile, err := os.OpenFile(*logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(1, err)
panic(err)
}
err = o.LoginAuth(*userName, *password)
multi := zerolog.MultiLevelWriter(consoleWriter, logFile)
log.Logger = zerolog.New(multi).
With().Timestamp().Caller().Stack().
Logger()
log.Info().Msgf("BeeBot v%.2f", version)
b, err := beebot.New(*dbFilePath, *logFilePath, *debug)
if err != nil {
log.Fatal(2, err)
log.Fatal().Err(err).Msg("beebot died")
}
me, err := o.AboutRedditor("_beebot_")
if err != nil {
log.Fatal(3, err)
}
log.Printf("%+v", me)
cap, err := o.NewCaptcha()
if err != nil {
log.Fatal(4, err)
}
post := geddit.NewTextSubmission(*reddit, "Monthly challenge", "Hey this is a monthly challenge", false, cap)
sub, err := o.Submit(post)
if err != nil {
log.Fatal(5, err)
}
// b.Run()
b.ServeWeb()
}

143
config/config.go Normal file
View File

@ -0,0 +1,143 @@
// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
package config
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log"
)
// Config stores any system-wide startup information that cannot be easily configured via
// the database
type Config struct {
*sqlx.DB
}
// GetFloat64 returns the config value for a string key
// It will first look in the env vars for the key
// It will check the db for the key if an env DNE
// Finally, it will return a zero value if the key does not exist
// It will attempt to convert the value to a float64 if it exists
func (c *Config) GetFloat64(key string, fallback float64) float64 {
f, err := strconv.ParseFloat(c.GetString(key, fmt.Sprintf("%f", fallback)), 64)
if err != nil {
return 0.0
}
return f
}
// GetInt64 returns the config value for a string key
// It will first look in the env vars for the key
// It will check the db for the key if an env DNE
// Finally, it will return a zero value if the key does not exist
// It will attempt to convert the value to an int if it exists
func (c *Config) GetInt64(key string, fallback int64) int64 {
i, err := strconv.ParseInt(c.GetString(key, strconv.FormatInt(fallback, 10)), 10, 64)
if err != nil {
return 0
}
return i
}
// GetInt returns the config value for a string key
// It will first look in the env vars for the key
// It will check the db for the key if an env DNE
// Finally, it will return a zero value if the key does not exist
// It will attempt to convert the value to an int if it exists
func (c *Config) GetInt(key string, fallback int) int {
i, err := strconv.Atoi(c.GetString(key, strconv.Itoa(fallback)))
if err != nil {
return 0
}
return i
}
// Get is a shortcut for GetString
func (c *Config) Get(key, fallback string) string {
return c.GetString(key, fallback)
}
func envkey(key string) string {
key = strings.ToUpper(key)
key = strings.Replace(key, ".", "", -1)
return key
}
// GetString returns the config value for a string key
// It will first look in the env vars for the key
// It will check the db for the key if an env DNE
// Finally, it will return a zero value if the key does not exist
// It will convert the value to a string if it exists
func (c *Config) GetString(key, fallback string) string {
key = strings.ToLower(key)
if v, found := os.LookupEnv(envkey(key)); found {
return v
}
var configValue string
q := `select value from config where key=?`
err := c.DB.Get(&configValue, q, key)
if err != nil {
log.Debug().Msgf("WARN: Key %s is empty", key)
return fallback
}
return configValue
}
// Unset removes config values from the database
func (c *Config) Unset(key string) error {
q := `delete from config where key=?`
tx, err := c.Begin()
if err != nil {
return err
}
_, err = tx.Exec(q, key)
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
// Set changes the value for a configuration in the database
// Note, this is always a string. Use the SetArray for an array helper
func (c *Config) Set(key, value string) error {
key = strings.ToLower(key)
value = strings.Trim(value, "`")
q := `insert into config (key,value) values (?, ?)
on conflict(key) do update set value=?;`
tx, err := c.Begin()
if err != nil {
return err
}
_, err = tx.Exec(q, key, value, value)
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
// New loads a configuration from the specified database
func New(db *sqlx.DB) *Config {
c := Config{}
c.DB = db
c.MustExec(`create table if not exists config (
key string,
value string,
primary key (key)
);`)
return &c
}

70
filters.go Normal file
View File

@ -0,0 +1,70 @@
package beebot
import (
"regexp"
"text/template"
"github.com/jmoiron/sqlx"
)
// Filter represents a comment response for the bot
type Filter struct {
ID int64
Name string
RegexStr string `db:"regex"`
Template string
regex *regexp.Regexp
db *sqlx.DB
}
// NewFilter creates and saves a Filter
func (b *BeeBot) NewFilter(name, regex, tpl string) (*Filter, error) {
// Verify the template
_, err := template.New("tmp").Parse(tpl)
if err != nil {
return nil, err
}
_, err = regexp.Compile(regex)
if err != nil {
return nil, err
}
f := &Filter{
Name: name,
RegexStr: regex,
Template: tpl,
}
f.populate(b.db)
return f, nil
}
func (f *Filter) populate(db *sqlx.DB) {
f.regex = regexp.MustCompile(f.RegexStr)
f.db = db
}
// Save commits a template to the database
func (f *Filter) Save() error {
q := `insert into filters (name, regexstr, template) values (?, ? ,?)
on conflict(name) do update set regexstr=?, template=?;`
res, err := f.db.Exec(q, f.Name, f.RegexStr, f.Template,
f.RegexStr, f.Template)
if err != nil {
return err
}
f.ID, err = res.LastInsertId()
return err
}
// AllFilters returns every filter known
func (b *BeeBot) AllFilters() ([]Filter, error) {
filters := []Filter{}
err := b.db.Select(&filters, `select * from Filters`)
if handleErr(err, "could not get Filter list") {
return nil, err
}
for _, f := range filters {
f.populate(b.db)
}
return filters, nil
}

30
go.mod
View File

@ -1,11 +1,37 @@
module code.chrissexton.org/cws/BeeBot
go 1.13
go 1.17
require (
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6 // indirect
github.com/go-chi/chi v1.5.4
github.com/google/go-querystring v1.0.0 // indirect
github.com/jmoiron/sqlx v1.3.4
github.com/jpillora/backoff v1.0.0
github.com/jzelinskie/geddit v0.0.0-20190913104144-95ef6806b073
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
github.com/rs/zerolog v1.25.0
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
modernc.org/sqlite v1.13.1
)
require (
github.com/golang/protobuf v1.2.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b // indirect
golang.org/x/tools v0.1.5 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.4.0 // indirect
lukechampine.com/uint128 v1.1.1 // indirect
modernc.org/cc/v3 v3.34.0 // indirect
modernc.org/ccgo/v3 v3.11.2 // indirect
modernc.org/libc v1.11.3 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.0.5 // indirect
modernc.org/opt v0.1.1 // indirect
modernc.org/strutil v1.1.1 // indirect
modernc.org/token v1.0.0 // indirect
)

110
go.sum
View File

@ -1,22 +1,126 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6 h1:KXlsf+qt/X5ttPGEjR0tPH1xaWWoKBEg9Q1THAj2h3I=
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.5.3 h1:x95R7cp+rSeeqAMI2knLtQ0DKlaBhv2NrtrOvafPHRo=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w=
github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/jzelinskie/geddit v0.0.0-20190913104144-95ef6806b073 h1:5SmVkj0GZ8GU4eUF6JOhwZj4GeCxPphxTdZK07R5Q1U=
github.com/jzelinskie/geddit v0.0.0-20190913104144-95ef6806b073/go.mod h1:KiUhpHWSO6xCSPYKhRXa1LDLtbxZKaFH4NINTP3Lm2Q=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.25.0 h1:Rj7XygbUHKUlDPcVdoLyR91fJBsduXj5fRxyqIQj/II=
github.com/rs/zerolog v1.25.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b h1:S7hKs0Flbq0bbc9xgYt4stIEG1zNDFqyrPwAX2Wj/sE=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.34.0 h1:dFhZc/HKR3qp92sYQxKRRaDMz+sr1bwcFD+m7LSCrAs=
modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=
modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=
modernc.org/ccgo/v3 v3.11.2 h1:gqa8PQ2v7SjrhHCgxUO5dzoAJWSLAveJqZTNkPCN0kc=
modernc.org/ccgo/v3 v3.11.2/go.mod h1:6kii3AptTDI+nUrM9RFBoIEUEisSWCbdczD9ZwQH2FE=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=
modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=
modernc.org/libc v1.11.3 h1:q//spBhqp23lC/if8/o8hlyET57P8mCZqrqftzT2WmY=
modernc.org/libc v1.11.3/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14=
modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.13.1 h1:s/qk6VTTVyQIyhVNWa50whBBcI3+2oREbx85t227iOo=
modernc.org/sqlite v1.13.1/go.mod h1:2qO/6jZJrcQaxFUHxOwa6Q6WfiGSsiVj6GXX0Ker+Jg=
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/tcl v1.5.9 h1:DZMfR+RDJRhcrmMEMTJgVIX+Wf5qhfVX0llI0rsc20w=
modernc.org/tcl v1.5.9/go.mod h1:bcwjvBJ2u0exY6K35eAmxXBBij5kXb1dHlAWmfhqThE=
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.1.2 h1:IjjzDsIFbl0wuF2KfwvdyUAJVwxD4iwZ6akLNiDoClM=
modernc.org/z v1.1.2/go.mod h1:sj9T1AGBG0dm6SCVzldPOHWrif6XBpooJtbttMn1+Js=

24
schema.go Normal file
View File

@ -0,0 +1,24 @@
package beebot
func (b *BeeBot) setupDB() error {
if _, err := b.db.Exec(`create table if not exists filters (
id integer primary key autoincrement,
name text unique,
regex text,
template text
)`); err != nil {
return err
}
if _, err := b.db.Exec(`create table if not exists offenders (
id integer primary key autoincrement,
offender text,
filter_name text,
foreign key(filter_name) references filters(name)
)`); err != nil {
return err
}
return nil
}

122
templates/config.html Normal file
View File

@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Load required Bootstrap and BootstrapVue CSS -->
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<!-- Load polyfills to support older browsers -->
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CMutationObserver"></script>
<!-- Load Vue followed by BootstrapVue -->
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="https://unpkg.com/vue-router"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<meta charset="UTF-8">
<title>Memes</title>
</head>
<body>
<div id="app">
<b-navbar>
<b-navbar-brand>BeeBot</b-navbar-brand>
<b-navbar-nav>
<b-nav-item v-for="(url, name) in nav" :href="url" :active="name === 'Config'">{{ name }}</b-nav-item>
</b-navbar-nav>
</b-navbar>
<b-alert
dismissable
variant="error"
v-if="err"
@dismissed="err = ''">
{{ err }}
</b-alert>
<b-form @submit="add">
<b-container>
<b-row>
<b>Note:</b> Environmental configuration values will not be displayed in this list. Unset configuration options appear in the logs.
</b-row>
<b-row>
<b-col cols="5">
<b-input placeholder="Key..." v-model="newConfig.key"></b-input>
</b-col>
<b-col cols="5">
<b-input placeholder="Value..." v-model="newConfig.value"></b-input>
</b-col>
<b-col cols="2">
<b-button type="submit">Add</b-button>
</b-col>
</b-row>
</b-container>
</b-form>
<b-table
:items="config"
:fields="fields">
<template v-slot:cell(rm)="data">
<b-button @click="rm(data.item.key)">X</b-button>
</template>
</table>
</div>
<script>
var router = new VueRouter({
mode: 'history',
routes: [],
});
var app = new Vue({
el: '#app',
router,
data: {
err: '',
nav: [],
config: [],
newConfig: {key: '', value: ''},
fields: [
{key: 'key'},
{key: 'value'},
{key: 'rm', sortable: false}
],
},
mounted() {
axios.get('/api/v1/nav')
.catch(err => this.err = err)
.then(resp => {
this.nav = resp.data;
})
this.refresh()
},
methods: {
refresh: function() {
axios.get('/api/v1/config')
.catch(err => console.log(err))
.then(resp => {
this.config = resp.data
})
},
add: function(evt) {
if (evt) {
evt.preventDefault();
evt.stopPropagation();
}
axios.post('/api/v1/config', this.newConfig)
.catch(err => console.log(err))
.then(resp => {
this.newConfig = {}
this.refresh()
})
},
rm: function(key) {
axios.delete('/api/v1/config/', {data: {key: key}})
.catch(err => console.log(err))
.then(resp => {
this.newConfig = {}
this.refresh()
})
}
}
})
</script>
</body>
</html>

127
templates/filters.html Normal file
View File

@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Load required Bootstrap and BootstrapVue CSS -->
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<!-- Load polyfills to support older browsers -->
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CMutationObserver"></script>
<!-- Load Vue followed by BootstrapVue -->
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="https://unpkg.com/vue-router"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<meta charset="UTF-8">
<title>Memes</title>
</head>
<body>
<div id="app">
<b-navbar>
<b-navbar-brand>BeeBot</b-navbar-brand>
<b-navbar-nav>
<b-nav-item v-for="(url, name) in nav" :href="url" :active="name === 'Filters'">{{ name }}</b-nav-item>
</b-navbar-nav>
</b-navbar>
<b-alert
dismissable
variant="error"
v-if="err"
@dismissed="err = ''">
{{ err }}
</b-alert>
<b-form @submit="add">
<b-container>
<b-row>
<b-col cols="5">
<b-input placeholder="Key..." v-model="newfilters.name"></b-input>
</b-col>
<b-col cols="5">
<b-input placeholder="Value..." v-model="newfilters.regex"></b-input>
</b-col>
</b-row>
<b-row>
<b-col cols="10">
<b-input placeholder="Template..." v-model="newfilters.template"></b-input>
</b-col>
</b-row>
<b-row>
<b-col cols="2">
<b-button type="submit">Add</b-button>
</b-col>
</b-row>
</b-container>
</b-form>
<b-table
:items="filters"
:fields="fields">
<template v-slot:cell(rm)="data">
<b-button @click="rm(data.item.key)">X</b-button>
</template>
</table>
</div>
<script>
var router = new VueRouter({
mode: 'history',
routes: [],
});
var app = new Vue({
el: '#app',
router,
data: {
err: '',
nav: [],
filters: [],
newfilters: {key: '', value: ''},
fields: [
{key: 'key'},
{key: 'value'},
{key: 'rm', sortable: false}
],
},
mounted() {
axios.get('/api/v1/nav')
.catch(err => this.err = err)
.then(resp => {
this.nav = resp.data;
})
this.refresh()
},
methods: {
refresh: function() {
axios.get('/api/v1/filters')
.catch(err => console.log(err))
.then(resp => {
this.filters = resp.data
})
},
add: function(evt) {
if (evt) {
evt.preventDefault();
evt.stopPropagation();
}
axios.post('/api/v1/filters', this.newfilters)
.catch(err => console.log(err))
.then(resp => {
this.newfilters = {}
this.refresh()
})
},
rm: function(key) {
axios.delete('/api/v1/filters', {data: {key: key}})
.catch(err => console.log(err))
.then(resp => {
this.newfilters = {}
this.refresh()
})
}
}
})
</script>
</body>
</html>

61
templates/index.html Normal file
View File

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Load required Bootstrap and BootstrapVue CSS -->
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<!-- Load polyfills to support older browsers -->
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CMutationObserver"></script>
<!-- Load Vue followed by BootstrapVue -->
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="https://unpkg.com/vue-router"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<meta charset="UTF-8">
<title>Memes</title>
</head>
<body>
<div id="app">
<b-navbar>
<b-navbar-brand>BeeBot</b-navbar-brand>
<b-navbar-nav>
<b-nav-item v-for="(url, name) in nav" :href="url" :active="name === 'Index'">{{ name }}</b-nav-item>
</b-navbar-nav>
</b-navbar>
<b-alert
dismissable
variant="error"
v-if="err"
@dismissed="err = ''">
{{ err }}
</b-alert>
</div>
<script>
var router = new VueRouter({
mode: 'history',
routes: []
});
var app = new Vue({
el: '#app',
router,
data: {
err: '',
nav: [],
},
mounted() {
axios.get('/api/v1/nav')
.then(resp => {
this.nav = resp.data;
})
.catch(err => console.log(err))
},
methods: {
}
})
</script>
</body>
</html>

91
templates/log.html Normal file
View File

@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Load required Bootstrap and BootstrapVue CSS -->
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<!-- Load polyfills to support older browsers -->
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CMutationObserver"></script>
<!-- Load Vue followed by BootstrapVue -->
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="https://unpkg.com/vue-router"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<meta charset="UTF-8">
<title>Memes</title>
</head>
<body>
<div id="app">
<b-navbar>
<b-navbar-brand>BeeBot</b-navbar-brand>
<b-navbar-nav>
<b-nav-item v-for="(url, name) in nav" :href="url" :active="name === 'Log'">{{ name }}</b-nav-item>
</b-navbar-nav>
</b-navbar>
<b-alert
dismissable
variant="error"
v-if="err"
@dismissed="err = ''">
{{ err }}
</b-alert>
<b-table
:items="log"
:fields="fields">
</table>
</div>
<script>
var router = new VueRouter({
mode: 'history',
routes: [],
});
var app = new Vue({
el: '#app',
router,
data: {
err: '',
nav: [],
log: [],
fields: [
{key: 'level'},
{
key: 'time',
sortable: true,
sortDirection: 'asc'
},
{key: 'caller'},
{key: 'message'},
],
},
mounted() {
axios.get('/api/v1/nav')
.catch(err => this.err = err)
.then(resp => {
this.nav = resp.data;
})
this.refresh()
},
methods: {
refresh: function() {
axios.get('/api/v1/log')
.catch(err => this.err = err)
.then(resp => {
let data = resp.data.split("\n");
for (let d of data) {
try {
this.log.push(JSON.parse(d))
} catch (e) {
console.log(e)
}
}
})
}
}
})
</script>
</body>
</html>

65
web.go Normal file
View File

@ -0,0 +1,65 @@
package beebot
import (
"embed"
"io/fs"
"net/http"
"os"
"github.com/go-chi/chi"
"github.com/rs/zerolog/log"
)
//go:embed templates/*.html
var embeddedFS embed.FS
var files fs.FS
// ServeWeb configures and starts the webserver
func (b *BeeBot) ServeWeb() {
router := chi.NewRouter()
files = getFileSystem(b.debug)
api := b.apiEndpoints()
router.Mount("/api/v1", api)
router.HandleFunc("/", staticPage("index.html"))
router.HandleFunc("/filters", staticPage("filters.html"))
router.HandleFunc("/config", staticPage("config.html"))
router.HandleFunc("/log", staticPage("log.html"))
b.nav["Filters"] = "/filters"
b.nav["Config"] = "/config"
b.nav["Log"] = "/log"
// Don't want to block for this (later)
baseAddr := b.c.Get("baseaddr", DefaultAddr)
log.Fatal().
Err(http.ListenAndServe(baseAddr, router)).
Msg("HTTP server")
}
func staticPage(templatePath string) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
index, err := fs.ReadFile(files, templatePath)
if err != nil {
log.Error().Err(err).Msg("Could not read template")
}
w.Write(index)
}
}
func getFileSystem(useOS bool) fs.FS {
if useOS {
log.Print("using live mode")
return os.DirFS("templates")
}
log.Print("using embed mode")
fsys, err := fs.Sub(embeddedFS, "templates")
if err != nil {
log.Error().Err(err).Msg("Could not load file templates")
}
return fsys
}