From 866b947f428221049807d3ed7a1e0940ae04c51f Mon Sep 17 00:00:00 2001 From: Chris Sexton <3216719+chrissexton@users.noreply.github.com> Date: Thu, 13 Oct 2022 20:19:01 -0400 Subject: [PATCH] tappd: add plugin --- main.go | 2 + plugins/meme/meme.go | 17 ++-- plugins/tappd/image.go | 146 ++++++++++++++++++++++++++++++++++ plugins/tappd/tappd.go | 176 +++++++++++++++++++++++++++++++++++++++++ plugins/tappd/web.go | 31 ++++++++ 5 files changed, 367 insertions(+), 5 deletions(-) create mode 100644 plugins/tappd/image.go create mode 100644 plugins/tappd/tappd.go create mode 100644 plugins/tappd/web.go diff --git a/main.go b/main.go index 358e917..924ed9f 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "flag" "github.com/velour/catbase/plugins/pagecomment" "github.com/velour/catbase/plugins/talker" + "github.com/velour/catbase/plugins/tappd" "github.com/velour/catbase/plugins/topic" "io" "math/rand" @@ -144,6 +145,7 @@ func main() { b.AddPlugin(leftpad.New(b)) b.AddPlugin(dice.New(b)) b.AddPlugin(picker.New(b)) + b.AddPlugin(tappd.New(b)) b.AddPlugin(beers.New(b)) b.AddPlugin(remember.New(b)) b.AddPlugin(your.New(b)) diff --git a/plugins/meme/meme.go b/plugins/meme/meme.go index cc911a6..5865cc2 100644 --- a/plugins/meme/meme.go +++ b/plugins/meme/meme.go @@ -306,7 +306,15 @@ var defaultFormats = map[string]string{ "raptor": "https://imgflip.com/s/meme/Philosoraptor.jpg", } -func (p *MemePlugin) findFontSize(config []memeText, fontLocation string, w, h int, sizes []float64) float64 { +func FindFontSizeConfigs(configs []memeText, fontLocation string, w, h int, sizes []float64) float64 { + texts := []string{} + for _, c := range configs { + texts = append(texts, c.Text) + } + return FindFontSize(texts, fontLocation, w, h, sizes) +} + +func FindFontSize(config []string, fontLocation string, w, h int, sizes []float64) float64 { fontSize := 12.0 m := gg.NewContext(w, h) @@ -320,9 +328,9 @@ func (p *MemePlugin) findFontSize(config []memeText, fontLocation string, w, h i return fontSize } - w, _ := m.MeasureString(s.Text) + w, _ := m.MeasureString(s) if w > longestW { - longestStr = s.Text + longestStr = s longestW = w } } @@ -449,6 +457,7 @@ func (p *MemePlugin) genMeme(spec specification) ([]byte, error) { // Apply black stroke m.SetHexColor("#000") strokeSize := 6 + fontSize := FindFontSizeConfigs(spec.Configs, defaultFont, w, h, fontSizes) for dy := -strokeSize; dy <= strokeSize; dy++ { for dx := -strokeSize; dx <= strokeSize; dx++ { // give it rounded corners @@ -460,7 +469,6 @@ func (p *MemePlugin) genMeme(spec specification) ([]byte, error) { if fontLocation == "" { fontLocation = defaultFont } - fontSize := p.findFontSize(spec.Configs, fontLocation, w, h, fontSizes) m.LoadFontFace(fontLocation, fontSize) x := float64(w)*c.XPerc + float64(dx) y := float64(h)*c.YPerc + float64(dy) @@ -476,7 +484,6 @@ func (p *MemePlugin) genMeme(spec specification) ([]byte, error) { if fontLocation == "" { fontLocation = defaultFont } - fontSize := p.findFontSize(spec.Configs, fontLocation, w, h, fontSizes) m.LoadFontFace(fontLocation, fontSize) x := float64(w) * c.XPerc y := float64(h) * c.YPerc diff --git a/plugins/tappd/image.go b/plugins/tappd/image.go new file mode 100644 index 0000000..74de188 --- /dev/null +++ b/plugins/tappd/image.go @@ -0,0 +1,146 @@ +package tappd + +import ( + "bytes" + "github.com/fogleman/gg" + "github.com/nfnt/resize" + "github.com/rs/zerolog/log" + "github.com/velour/catbase/plugins/meme" + "image" + "image/png" + "net/http" + "net/url" + "path" +) + +func (p *Tappd) getImage(url string) (image.Image, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + log.Debug(). + Str("url", url). + Msgf("about to decode and crash") + img, _, err := image.Decode(resp.Body) + if err != nil { + return nil, err + } + + r := img.Bounds() + w := r.Dx() + h := r.Dy() + + maxSz := p.c.GetFloat64("maxImgSz", 750.0) + + if w > h { + scale := maxSz / float64(w) + w = int(float64(w) * scale) + h = int(float64(h) * scale) + } else { + scale := maxSz / float64(h) + w = int(float64(w) * scale) + h = int(float64(h) * scale) + } + + log.Debug().Msgf("trynig to resize to %v, %v", w, h) + img = resize.Resize(uint(w), uint(h), img, resize.Lanczos3) + r = img.Bounds() + w = r.Dx() + h = r.Dy() + log.Debug().Msgf("resized to %v, %v", w, h) + + return img, nil +} + +type textSpec struct { + text string + // percentage location of text center + x float64 + y float64 +} + +func defaultSpec() textSpec { + return textSpec{ + x: 0.5, + y: 0.9, + } +} + +func (p *Tappd) overlay(img image.Image, texts []textSpec) ([]byte, error) { + font := p.c.Get("meme.font", "impact.ttf") + fontSizes := []float64{48, 36, 24, 16, 12} + r := img.Bounds() + w := r.Dx() + h := r.Dy() + + txts := []string{} + for _, t := range texts { + txts = append(txts, t.text) + } + + fontSize := meme.FindFontSize(txts, font, w, h, fontSizes) + + m := gg.NewContext(w, h) + m.DrawImage(img, 0, 0) + for _, spec := range texts { + // write some stuff on the image here + if err := m.LoadFontFace(font, fontSize); err != nil { + return nil, err + } + + // 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)*spec.x + float64(dx) + y := float64(h)*spec.y + float64(dy) + m.DrawStringAnchored(spec.text, x, y, 0.5, 0.5) + } + } + + m.SetHexColor("#FFF") + x := float64(w) * spec.x + y := float64(h) * spec.y + m.DrawStringAnchored(spec.text, x, y, 0.5, 0.5) + } + i := bytes.Buffer{} + if err := png.Encode(&i, m.Image()); err != nil { + return nil, err + } + return i.Bytes(), nil +} + +func (p *Tappd) getAndOverlay(id, srcURL string, texts []textSpec) (imageInfo, error) { + baseURL := p.c.Get("BaseURL", ``) + u, _ := url.Parse(baseURL) + u.Path = path.Join(u.Path, "tappd", id) + img, err := p.getImage(srcURL) + if err != nil { + return imageInfo{}, err + } + data, err := p.overlay(img, texts) + if err != nil { + return imageInfo{}, err + } + bounds := img.Bounds() + info := imageInfo{ + ID: id, + SrcURL: srcURL, + BotURL: u.String(), + Img: img, + Repr: data, + W: bounds.Dx(), + H: bounds.Dy(), + } + p.imageMap[id] = info + log.Debug(). + Interface("BotURL", info.BotURL). + Str("ID", id). + Msgf("here's some info") + return info, nil +} diff --git a/plugins/tappd/tappd.go b/plugins/tappd/tappd.go new file mode 100644 index 0000000..97a0036 --- /dev/null +++ b/plugins/tappd/tappd.go @@ -0,0 +1,176 @@ +package tappd + +import ( + "fmt" + "github.com/bwmarrin/discordgo" + "github.com/rs/zerolog/log" + "github.com/velour/catbase/bot" + "github.com/velour/catbase/config" + "github.com/velour/catbase/connectors/discord" + "image" + "regexp" + "time" +) + +type Tappd struct { + b bot.Bot + c *config.Config + imageMap map[string]imageInfo +} + +type imageInfo struct { + ID string + SrcURL string + BotURL string + Img image.Image + Repr []byte + W int + H int +} + +func New(b bot.Bot) *Tappd { + t := &Tappd{ + b: b, + c: b.Config(), + imageMap: make(map[string]imageInfo), + } + t.register() + t.registerWeb() + t.mkDB() + return t +} + +func (p *Tappd) mkDB() error { + db := p.b.DB() + tx, err := db.Beginx() + if err != nil { + tx.Rollback() + return err + } + _, err = tx.Exec(`create table if not exists tappd ( + id integer primary key autoincrement, + who string, + channel string, + message string, + ts datetime + );`) + if err != nil { + tx.Rollback() + return err + } + err = tx.Commit() + if err != nil { + return err + } + return nil +} + +func (p *Tappd) log(who, channel, message string) error { + db := p.b.DB() + tx, err := db.Beginx() + if err != nil { + tx.Rollback() + return err + } + _, err = tx.Exec(`insert into tappd (who, channel, message, ts) values (?, ?, ? ,?)`, + who, channel, message, time.Now()) + if err != nil { + tx.Rollback() + return err + } + err = tx.Commit() + if err != nil { + return err + } + return nil +} + +func (p *Tappd) registerDiscord(d *discord.Discord) { + cmd := discordgo.ApplicationCommand{ + Name: "tap", + Description: "tappd a beer in", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionAttachment, + Name: "image", + Description: "Picture that beer, but on Discord", + Required: true, + }, + { + Type: discordgo.ApplicationCommandOptionString, + Name: "comment", + Description: "Comment on that beer", + Required: true, + }, + }, + } + if err := d.RegisterSlashCmd(cmd, p.tap); err != nil { + log.Error().Err(err).Msgf("could not register") + } +} + +func (p *Tappd) tap(s *discordgo.Session, i *discordgo.InteractionCreate) { + who := i.Interaction.Member.Nick + channel := i.Interaction.ChannelID + msg := fmt.Sprintf("%s checked in: %s", + i.Interaction.Member.Nick, + i.ApplicationCommandData().Options[1].StringValue()) + attachID := i.ApplicationCommandData().Options[0].Value.(string) + attach := i.ApplicationCommandData().Resolved.Attachments[attachID] + spec := defaultSpec() + spec.text = msg + info, err := p.getAndOverlay(attachID, attach.URL, []textSpec{spec}) + if err != nil { + log.Error().Err(err).Msgf("error with interaction") + err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Error getting the image", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + if err != nil { + log.Error().Err(err).Msgf("error with interaction") + } + return + } + embed := &discordgo.MessageEmbed{ + Description: msg, + Image: &discordgo.MessageEmbedImage{ + URL: info.BotURL, + Width: info.W, + Height: info.H, + }, + } + err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{embed}, + }, + }) + if err != nil { + log.Error().Err(err).Msgf("error with interaction") + return + } + err = p.log(who, channel, msg) + if err != nil { + log.Error().Err(err).Msgf("error recording tap") + } +} + +func (p *Tappd) register() { + ht := bot.HandlerTable{ + { + Kind: bot.Startup, IsCmd: false, + Regex: regexp.MustCompile(`.*`), + Handler: func(r bot.Request) bool { + switch conn := r.Conn.(type) { + case *discord.Discord: + p.registerDiscord(conn) + } + return false + }, + }, + } + p.b.RegisterTable(p, ht) +} diff --git a/plugins/tappd/web.go b/plugins/tappd/web.go new file mode 100644 index 0000000..d4d1fba --- /dev/null +++ b/plugins/tappd/web.go @@ -0,0 +1,31 @@ +package tappd + +import ( + "encoding/json" + "github.com/go-chi/chi/v5" + "github.com/rs/zerolog/log" + "net/http" +) + +func (p *Tappd) registerWeb() { + r := chi.NewRouter() + r.HandleFunc("/", p.serveImage) + p.b.RegisterWeb(r, "/tappd/{id}") +} + +func (p *Tappd) serveImage(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + imgData, ok := p.imageMap[id] + log.Debug(). + Str("id", id). + Str("SrcURL", imgData.SrcURL). + Bool("ok", ok). + Msgf("creating request") + if !ok { + w.WriteHeader(404) + out, _ := json.Marshal(struct{ err string }{"could not find ID"}) + w.Write(out) + return + } + w.Write(imgData.Repr) +}