tappd: add plugin

This commit is contained in:
Chris Sexton 2022-10-13 20:19:01 -04:00
parent 2457d6769e
commit 866b947f42
5 changed files with 367 additions and 5 deletions

View File

@ -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))

View File

@ -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

146
plugins/tappd/image.go Normal file
View File

@ -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
}

176
plugins/tappd/tappd.go Normal file
View File

@ -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)
}

31
plugins/tappd/web.go Normal file
View File

@ -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)
}