initial commit
This commit is contained in:
parent
99d709217b
commit
39d2d89759
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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"}
|
|
@ -3,9 +3,14 @@ package main
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"os"
|
||||||
|
|
||||||
|
beebot "code.chrissexton.org/cws/BeeBot"
|
||||||
"github.com/jzelinskie/geddit"
|
"github.com/jzelinskie/geddit"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = 1.0
|
const version = 1.0
|
||||||
|
@ -13,51 +18,39 @@ const version = 1.0
|
||||||
var userAgent = fmt.Sprintf("BeeBot:%.2f (by u/phlyingpenguin)", version)
|
var userAgent = fmt.Sprintf("BeeBot:%.2f (by u/phlyingpenguin)", version)
|
||||||
var scopes = "identity read edit"
|
var scopes = "identity read edit"
|
||||||
|
|
||||||
var clientID = flag.String("id", "", "Client ID")
|
var debug = flag.Bool("debug", false, "Turn debug printing on")
|
||||||
var clientSecret = flag.String("secret", "", "Client Secret")
|
var dbFilePath = flag.String("db", "beebot.db", "Database file path")
|
||||||
var baseAddr = flag.String("url", "127.0.0.1:9595", "Base address")
|
var logFilePath = flag.String("log", "beebot.json", "Log file path")
|
||||||
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 o *geddit.OAuthSession
|
var o *geddit.OAuthSession
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
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(
|
consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
|
||||||
*clientID,
|
logFile, err := os.OpenFile(*logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
*clientSecret,
|
|
||||||
userAgent,
|
|
||||||
fmt.Sprintf("http://%s/cb", *baseAddr),
|
|
||||||
)
|
|
||||||
if err != nil {
|
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 {
|
if err != nil {
|
||||||
log.Fatal(2, err)
|
log.Fatal().Err(err).Msg("beebot died")
|
||||||
}
|
}
|
||||||
|
|
||||||
me, err := o.AboutRedditor("_beebot_")
|
// b.Run()
|
||||||
if err != nil {
|
b.ServeWeb()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
30
go.mod
|
@ -1,11 +1,37 @@
|
||||||
module code.chrissexton.org/cws/BeeBot
|
module code.chrissexton.org/cws/BeeBot
|
||||||
|
|
||||||
go 1.13
|
go 1.17
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6 // indirect
|
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/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
|
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
|
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
110
go.sum
|
@ -1,22 +1,126 @@
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
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 h1:KXlsf+qt/X5ttPGEjR0tPH1xaWWoKBEg9Q1THAj2h3I=
|
||||||
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
|
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 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
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 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
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 h1:5SmVkj0GZ8GU4eUF6JOhwZj4GeCxPphxTdZK07R5Q1U=
|
||||||
github.com/jzelinskie/geddit v0.0.0-20190913104144-95ef6806b073/go.mod h1:KiUhpHWSO6xCSPYKhRXa1LDLtbxZKaFH4NINTP3Lm2Q=
|
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-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-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-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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
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 h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
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-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-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.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 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
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=
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue