diff --git a/config/config.go b/config/config.go index 5a43af7..c75d72a 100644 --- a/config/config.go +++ b/config/config.go @@ -4,6 +4,7 @@ package config import ( "database/sql" + "encoding/json" "fmt" "os" "regexp" @@ -94,6 +95,19 @@ func (c *Config) GetString(key, fallback string) string { return configValue } +func (c *Config) GetMap(key string, fallback map[string]string) map[string]string { + content := c.Get(key, "") + if content == "" { + return fallback + } + vals := map[string]string{} + err := json.Unmarshal([]byte(content), &vals) + if err != nil { + return fallback + } + return vals +} + // GetArray returns the string slice config value for a string key // It will first look in the env vars for the key with ;; separated values // Look, I'm too lazy to do parsing to ensure that a comma is what the user meant diff --git a/go.mod b/go.mod index 1c97c36..970f7ee 100644 --- a/go.mod +++ b/go.mod @@ -19,12 +19,15 @@ require ( github.com/chrissexton/sentiment v0.0.0-20190927141846-d69c422ba035 github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc // indirect github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad // indirect + github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90 github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gocolly/colly v1.2.0 github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 // indirect github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82 // indirect github.com/gonum/internal v0.0.0-20181124074243-f884aa714029 // indirect + github.com/google/uuid v1.1.1 + github.com/gorilla/mux v1.7.4 github.com/gorilla/websocket v1.4.1 // indirect github.com/james-bowman/nlp v0.0.0-20191016091239-d9dbfaff30c6 github.com/james-bowman/sparse v0.0.0-20190423065201-80c6877364c7 // indirect diff --git a/go.sum b/go.sum index dc90243..521b1ac 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,7 @@ github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc h1:tP7tkU+vI github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc/go.mod h1:ORH5Qp2bskd9NzSfKqAF7tKfONsEkCarTE5ESr/RVBw= github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad h1:Qk76DOWdOp+GlyDKBAG3Klr9cn7N+LcYc82AZ2S7+cA= github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad/go.mod h1:mPKfmRa823oBIgl2r20LeMSpTAteW5j7FLkc0vjmzyQ= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90 h1:WXb3TSNmHp2vHoCroCIB1foO/yQ36swABL8aOVeDpgg= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 h1:GOfMz6cRgTJ9jWV0qAezv642OhPnKEG7gtUjJSdStHE= github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17/go.mod h1:HfkOCN6fkKKaPSAeNq/er3xObxTW4VLeY6UUK895gLQ= @@ -53,6 +54,7 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI= github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE= github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -62,6 +64,10 @@ github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82 h1:EvokxLQsaaQjcWVWSV github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82/go.mod h1:PxC8OnwL11+aosOB5+iEPoV3picfs8tUpkVd0pDo+Kg= github.com/gonum/internal v0.0.0-20181124074243-f884aa714029 h1:8jtTdc+Nfj9AR+0soOeia9UZSvYBvETVHZrugUowJ7M= github.com/gonum/internal v0.0.0-20181124074243-f884aa714029/go.mod h1:Pu4dmpkhSyOzRwuXkOgAvijx4o+4YMUJJo9OvPYMkks= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= @@ -127,6 +133,7 @@ golang.org/x/exp v0.0.0-20191014171548-69215a2ee97e h1:ewBcnrlKhy0GKnQ31tXkOC/G7 golang.org/x/exp v0.0.0-20191014171548-69215a2ee97e/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= diff --git a/main.go b/main.go index 6ade223..4be3246 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "github.com/velour/catbase/plugins/achievements" "github.com/velour/catbase/plugins/aoc" + "github.com/velour/catbase/plugins/meme" "github.com/velour/catbase/plugins/twitter" "github.com/rs/zerolog" @@ -139,6 +140,7 @@ func main() { b.AddPlugin(impossible.New(b)) b.AddPlugin(cli.New(b)) b.AddPlugin(aoc.New(b)) + b.AddPlugin(meme.New(b)) b.AddPlugin(achievements.New(b)) // catches anything left, will always return true b.AddPlugin(fact.New(b)) diff --git a/plugins/meme/meme.go b/plugins/meme/meme.go new file mode 100644 index 0000000..1e11516 --- /dev/null +++ b/plugins/meme/meme.go @@ -0,0 +1,163 @@ +package meme + +import ( + "bytes" + "fmt" + "image" + "image/png" + "net/http" + "net/url" + "path" + "strings" + + "github.com/fogleman/gg" + "github.com/google/uuid" + "github.com/rs/zerolog/log" + + "github.com/velour/catbase/bot" + "github.com/velour/catbase/bot/msg" + "github.com/velour/catbase/config" +) + +type MemePlugin struct { + bot bot.Bot + c *config.Config + + images map[string][]byte +} + +func New(b bot.Bot) *MemePlugin { + mp := &MemePlugin{ + bot: b, + c: b.Config(), + images: make(map[string][]byte), + } + + b.Register(mp, bot.Message, mp.message) + b.Register(mp, bot.Help, mp.help) + mp.registerWeb(b.DefaultConnector()) + + return mp +} + +func (p *MemePlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool { + return false +} + +func (p *MemePlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool { + formats := p.c.GetMap("meme.memes", defaultFormats) + msg := "Use `/meme [format] [text]` to create a meme.\nI know the following formats:" + for k := range formats { + msg += "\n" + k + } + p.bot.Send(c, bot.Message, message.Channel, msg) + return true +} + +func (p *MemePlugin) registerWeb(c bot.Connector) { + http.HandleFunc("/slash/meme", func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + log.Debug().Msgf("Meme:\n%+v", r.PostForm.Get("text")) + channel := r.PostForm.Get("channel_id") + log.Debug().Msgf("channel: %s", channel) + + parts := strings.SplitN(r.PostForm.Get("text"), " ", 2) + log.Debug().Strs("parts", parts).Msgf("Meme:\n%+v", r.PostForm.Get("text")) + w.WriteHeader(200) + + id := p.genMeme(parts[0], parts[1]) + baseURL := p.c.Get("BaseURL", `https://catbase.velour.ninja`) + u, _ := url.Parse(baseURL) + u.Path = path.Join(u.Path, "meme", "img", id) + + log.Debug().Msgf("image is at %s", u.String()) + p.bot.Send(c, bot.Message, channel, "", bot.ImageAttachment{ + URL: u.String(), + AltTxt: parts[1], + }) + w.Write(nil) + }) + + http.HandleFunc("/meme/img/", func(w http.ResponseWriter, r *http.Request) { + _, file := path.Split(r.URL.Path) + id := file + img := p.images[id] + w.Write(img) + }) +} + +func DownloadTemplate(file string) image.Image { + url := fmt.Sprintf("https://imgflip.com/s/meme/%s", file) + res, err := http.Get(url) + if err != nil { + log.Error().Msgf("%s template from %s failed because of %v", file, url, err) + } + defer res.Body.Close() + image, _, err := image.Decode(res.Body) + if err != nil { + log.Error().Msgf("Could not decode %s because of %v", file, err) + } + return image +} + +var defaultFormats = map[string]string{ + "fry": "Futurama-Fry.jpg", + "aliens": "Ancient-Aliens.jpg", + "doge": "Doge.jpg", + "simply": "One-Does-Not-Simply.jpg", + "wonka": "Creepy-Condescending-Wonka.jpg", + "grumpy": "Grumpy-Cat.jpg", + "raptor": "Philosoraptor.jpg", +} + +func (p *MemePlugin) genMeme(meme, text string) string { + const fontSize = 36 + + formats := p.c.GetMap("meme.memes", defaultFormats) + + path := uuid.New().String() + + imgName, ok := formats[meme] + if !ok { + imgName = meme + } + img := DownloadTemplate(imgName) + r := img.Bounds() + w := r.Dx() + h := r.Dy() + + m := gg.NewContext(w, h) + m.DrawImage(img, 0, 0) + fontLocation := p.c.Get("meme.font", "impact.ttf") + err := m.LoadFontFace(fontLocation, fontSize) // problem + if err != nil { + log.Error().Err(err).Msg("could not load font") + } + + // Apply black stroke + m.SetHexColor("#000") + strokeSize := 6 + for dy := -strokeSize; dy <= strokeSize; dy++ { + for dx := -strokeSize; dx <= strokeSize; dx++ { + // give it rounded corners + if dx*dx+dy*dy >= strokeSize*strokeSize { + continue + } + x := float64(w/2 + dx) + y := float64(h - fontSize + dy) + m.DrawStringAnchored(text, x, y, 0.5, 0.5) + } + } + + // Apply white fill + m.SetHexColor("#FFF") + m.DrawStringAnchored(text, float64(w)/2, float64(h)-fontSize, 0.5, 0.5) + + i := bytes.Buffer{} + png.Encode(&i, m.Image()) + p.images[path] = i.Bytes() + + log.Debug().Msgf("Saved to %s\n", path) + + return path +}