From 39d2d89759fbfaf409150543cd4d8e7fa93a84d7 Mon Sep 17 00:00:00 2001 From: Chris Sexton Date: Tue, 5 Oct 2021 18:52:59 -0400 Subject: [PATCH] initial commit --- api.go | 154 ++++++++++++++++++++++++++++++++++ beebot.go | 185 +++++++++++++++++++++++++++++++++++++++++ beebot.json | 73 ++++++++++++++++ cmd/beebot/main.go | 61 ++++++-------- config/config.go | 143 +++++++++++++++++++++++++++++++ filters.go | 70 ++++++++++++++++ go.mod | 30 ++++++- go.sum | 110 +++++++++++++++++++++++- schema.go | 24 ++++++ templates/config.html | 122 +++++++++++++++++++++++++++ templates/filters.html | 127 ++++++++++++++++++++++++++++ templates/index.html | 61 ++++++++++++++ templates/log.html | 91 ++++++++++++++++++++ web.go | 65 +++++++++++++++ 14 files changed, 1277 insertions(+), 39 deletions(-) create mode 100644 api.go create mode 100644 beebot.go create mode 100644 beebot.json create mode 100644 config/config.go create mode 100644 filters.go create mode 100644 schema.go create mode 100644 templates/config.html create mode 100644 templates/filters.html create mode 100644 templates/index.html create mode 100644 templates/log.html create mode 100644 web.go diff --git a/api.go b/api.go new file mode 100644 index 0000000..e338887 --- /dev/null +++ b/api.go @@ -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) +} diff --git a/beebot.go b/beebot.go new file mode 100644 index 0000000..9a8f7a1 --- /dev/null +++ b/beebot.go @@ -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 +} diff --git a/beebot.json b/beebot.json new file mode 100644 index 0000000..c467e78 --- /dev/null +++ b/beebot.json @@ -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"} diff --git a/cmd/beebot/main.go b/cmd/beebot/main.go index eee2bdc..cadfb8e 100644 --- a/cmd/beebot/main.go +++ b/cmd/beebot/main.go @@ -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 - - o, err = geddit.NewOAuthSession( - *clientID, - *clientSecret, - userAgent, - fmt.Sprintf("http://%s/cb", *baseAddr), - ) - if err != nil { - log.Fatal(1, err) + zerolog.SetGlobalLevel(zerolog.InfoLevel) + if *debug { + zerolog.SetGlobalLevel(zerolog.DebugLevel) } - err = o.LoginAuth(*userName, *password) + 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(2, err) + panic(err) } - me, err := o.AboutRedditor("_beebot_") + 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(3, err) + log.Fatal().Err(err).Msg("beebot died") } - 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() } diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..6816ac8 --- /dev/null +++ b/config/config.go @@ -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 +} diff --git a/filters.go b/filters.go new file mode 100644 index 0000000..bf76ed2 --- /dev/null +++ b/filters.go @@ -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 +} diff --git a/go.mod b/go.mod index 8e4b048..da56136 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index ea81fd5..ce2c32b 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/schema.go b/schema.go new file mode 100644 index 0000000..0096e66 --- /dev/null +++ b/schema.go @@ -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 +} diff --git a/templates/config.html b/templates/config.html new file mode 100644 index 0000000..8b4119e --- /dev/null +++ b/templates/config.html @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + Memes + + + +
+ + BeeBot + + {{ name }} + + + + {{ err }} + + + + + Note: Environmental configuration values will not be displayed in this list. Unset configuration options appear in the logs. + + + + + + + + + + Add + + + + + + + +
+ + + + + diff --git a/templates/filters.html b/templates/filters.html new file mode 100644 index 0000000..4eb3a02 --- /dev/null +++ b/templates/filters.html @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + Memes + + + +
+ + BeeBot + + {{ name }} + + + + {{ err }} + + + + + + + + + + + + + + + + + + + Add + + + + + + + +
+ + + + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..33c060c --- /dev/null +++ b/templates/index.html @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + Memes + + + +
+ + BeeBot + + {{ name }} + + + + {{ err }} + +
+ + + + diff --git a/templates/log.html b/templates/log.html new file mode 100644 index 0000000..7ceca50 --- /dev/null +++ b/templates/log.html @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + Memes + + + +
+ + BeeBot + + {{ name }} + + + + {{ err }} + + + +
+ + + + diff --git a/web.go b/web.go new file mode 100644 index 0000000..0f209af --- /dev/null +++ b/web.go @@ -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 +}