diff --git a/connectors/discord/discord.go b/connectors/discord/discord.go index 9e77267..01e1a12 100644 --- a/connectors/discord/discord.go +++ b/connectors/discord/discord.go @@ -29,6 +29,7 @@ type Discord struct { uidCache map[string]string registeredCmds []*discordgo.ApplicationCommand + cmdHandlers map[string]CmdHandler } func New(config *config.Config) *Discord { @@ -37,10 +38,17 @@ func New(config *config.Config) *Discord { log.Fatal().Err(err).Msg("Could not connect to Discord") } d := &Discord{ - config: config, - client: client, - uidCache: map[string]string{}, + config: config, + client: client, + uidCache: map[string]string{}, + registeredCmds: []*discordgo.ApplicationCommand{}, + cmdHandlers: map[string]CmdHandler{}, } + d.client.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { + if h, ok := d.cmdHandlers[i.ApplicationCommandData().Name]; ok { + h(s, i) + } + }) return d } func (d *Discord) GetRouter() (http.Handler, string) { @@ -393,9 +401,12 @@ func (d *Discord) SetRole(userID, roleID string) error { return d.client.GuildMemberRoleAdd(guildID, userID, roleID) } -func (d *Discord) RegisterSlashCmd(c discordgo.ApplicationCommand) error { +type CmdHandler func(s *discordgo.Session, i *discordgo.InteractionCreate) + +func (d *Discord) RegisterSlashCmd(c discordgo.ApplicationCommand, handler CmdHandler) error { guildID := d.config.Get("discord.guildid", "") cmd, err := d.client.ApplicationCommandCreate(d.client.State.User.ID, guildID, &c) + d.cmdHandlers[c.Name] = handler if err != nil { return err } diff --git a/main.go b/main.go index b3b3e35..fab30ce 100644 --- a/main.go +++ b/main.go @@ -180,6 +180,7 @@ func main() { log.Fatal().Err(err) } + log.Debug().Msgf("Sending bot.Startup message") b.Receive(client, bot.Startup, msg.Message{}) b.ListenAndServe() diff --git a/plugins/counter/counter.go b/plugins/counter/counter.go index 0b535c8..42ee952 100644 --- a/plugins/counter/counter.go +++ b/plugins/counter/counter.go @@ -256,14 +256,14 @@ func (i *Item) Delete() error { return err } -func (p *CounterPlugin) migrate(r bot.Request) bool { +func (p *CounterPlugin) migrate(r bot.Request) (retVal bool) { db := p.db nicks := []string{} err := db.Select(&nicks, `select distinct nick from counter where userid is null`) if err != nil { log.Error().Err(err).Msg("could not get nick list") - return false + return } log.Debug().Msgf("Migrating %d nicks to IDs", len(nicks)) @@ -284,7 +284,7 @@ func (p *CounterPlugin) migrate(r bot.Request) bool { if err := tx.Commit(); err != nil { log.Error().Err(err).Msg("Could not migrate users") } - return false + return } func setupDB(b bot.Bot) error { diff --git a/plugins/cowboy/cowboy.go b/plugins/cowboy/cowboy.go index dfc5233..3231862 100644 --- a/plugins/cowboy/cowboy.go +++ b/plugins/cowboy/cowboy.go @@ -2,6 +2,8 @@ package cowboy import ( "fmt" + "github.com/bwmarrin/discordgo" + "github.com/velour/catbase/plugins/emojy" "regexp" "github.com/velour/catbase/connectors/discord" @@ -19,6 +21,8 @@ type Cowboy struct { baseEmojyURL string } +var defaultOverlays = map[string]string{"hat": "hat"} + func New(b bot.Bot) *Cowboy { emojyPath := b.Config().Get("emojy.path", "emojy") baseURL := b.Config().Get("emojy.baseURL", "/emojy/file") @@ -30,15 +34,24 @@ func New(b bot.Bot) *Cowboy { } c.register() c.registerWeb() - switch conn := b.DefaultConnector().(type) { - case *discord.Discord: - c.registerCmds(conn) - } return &c } func (p *Cowboy) register() { tbl := bot.HandlerTable{ + { + Kind: bot.Startup, IsCmd: false, + Regex: regexp.MustCompile(`.*`), + Handler: func(r bot.Request) bool { + log.Debug().Msgf("Got bot.Startup") + switch conn := r.Conn.(type) { + case *discord.Discord: + log.Debug().Msg("Found a discord connection") + p.registerCmds(conn) + } + return false + }, + }, { Kind: bot.Message, IsCmd: true, Regex: regexp.MustCompile(`(?i)^:cowboy_clear_cache:$`), @@ -63,7 +76,9 @@ func (p *Cowboy) register() { func (p *Cowboy) makeCowboy(r bot.Request) { what := r.Values["what"] // This'll add the image to the cowboy_cache before discord tries to access it over http - i, err := cowboy(p.c, p.emojyPath, p.baseEmojyURL, what) + overlays := p.c.GetMap("cowboy.overlays", defaultOverlays) + hat := overlays["hat"] + i, err := cowboy(p.emojyPath, p.baseEmojyURL, hat, what) if err != nil { log.Error().Err(err).Msg(":cowboy_fail:") p.b.Send(r.Conn, bot.Ephemeral, r.Msg.Channel, r.Msg.User.ID, "Hey cowboy, that image wasn't there.") @@ -82,5 +97,95 @@ func (p *Cowboy) makeCowboy(r bot.Request) { } func (p *Cowboy) registerCmds(d *discord.Discord) { - //d.RegisterSlashCmd() + log.Debug().Msg("About to register some startup commands") + cmd := discordgo.ApplicationCommand{ + Name: "cowboy", + Description: "cowboy-ify an emojy", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "emojy", + Description: "which emojy you want cowboied", + Required: true, + }, + }, + } + overlays := p.c.GetMap("cowboy.overlays", defaultOverlays) + hat := overlays["hat"] + if err := d.RegisterSlashCmd(cmd, p.mkOverlayCB(hat)); err != nil { + log.Error().Err(err).Msg("could not register cowboy command") + } + cmd = discordgo.ApplicationCommand{ + Name: "overlay", + Description: "overlay-ify an emojy", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "overlay", + Description: "which overlay you want", + Required: true, + }, + { + Type: discordgo.ApplicationCommandOptionString, + Name: "emojy", + Description: "which emojy you want overlaid", + Required: true, + }, + }, + } + if err := d.RegisterSlashCmd(cmd, p.mkOverlayCB("")); err != nil { + log.Error().Err(err).Msg("could not register cowboy command") + } +} + +func (p *Cowboy) mkOverlayCB(overlay string) func(s *discordgo.Session, i *discordgo.InteractionCreate) { + return func(s *discordgo.Session, i *discordgo.InteractionCreate) { + log.Debug().Msg("got a cowboy command") + + lastEmojy := p.c.Get("cowboy.lastEmojy", "rust") + emojyPlugin := emojy.NewAPI(p.b) + + name := i.ApplicationCommandData().Options[0].StringValue() + if overlay == "" { + overlay = name + name = i.ApplicationCommandData().Options[1].StringValue() + } + msg := fmt.Sprintf("You asked for %s overlaid by %s", name, overlay) + + newEmojy, err := cowboy(p.emojyPath, p.baseEmojyURL, overlay, name) + if err != nil { + msg = err.Error() + goto resp + } + + err = emojyPlugin.RmEmojy(p.b.DefaultConnector(), lastEmojy) + if err != nil { + msg = err.Error() + goto resp + } + + // Look, I don't love it as a workaround either + if overlay == "hat" { + overlay = "cowboy" + } + name = emojy.SanitizeName(overlay + "_" + name) + err = emojyPlugin.UploadEmojyImage(p.b.DefaultConnector(), name, newEmojy) + if err != nil { + msg = err.Error() + goto resp + } + + p.c.Set("cowboy.lastEmojy", name) + + msg = fmt.Sprintf("You replaced %s with a new emojy %s, pardner!", lastEmojy, name) + + resp: + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: msg, + Flags: uint64(discordgo.MessageFlagsEphemeral), + }, + }) + } } diff --git a/plugins/cowboy/cowboy_image.go b/plugins/cowboy/cowboy_image.go index d803585..6ce353a 100644 --- a/plugins/cowboy/cowboy_image.go +++ b/plugins/cowboy/cowboy_image.go @@ -13,7 +13,6 @@ import ( "github.com/nfnt/resize" "github.com/rs/zerolog/log" - "github.com/velour/catbase/config" "github.com/velour/catbase/plugins/emojy" ) @@ -42,10 +41,17 @@ func getEmojy(emojyPath, baseEmojyURL, name string) (image.Image, error) { return img, nil } -func getCowboyHat(c *config.Config, emojyPath string) (image.Image, error) { - p := path.Join(emojyPath, c.Get("cowboy.hatname", "hat.png")) - p = path.Clean(p) - f, err := os.Open(p) +func getCowboyHat(emojyPath, overlay string) (image.Image, error) { + emojies, _, err := emojy.AllFiles(emojyPath, "") + if err != nil { + return nil, err + } + overlay, ok := emojies[overlay] + if !ok { + return nil, fmt.Errorf("could not find overlay %s", overlay) + } + overlay = path.Clean(overlay) + f, err := os.Open(overlay) if err != nil { return nil, err } @@ -56,8 +62,8 @@ func getCowboyHat(c *config.Config, emojyPath string) (image.Image, error) { return img, nil } -func cowboyifyImage(c *config.Config, emojyPath string, input image.Image) (image.Image, error) { - hat, err := getCowboyHat(c, emojyPath) +func cowboyifyImage(emojyPath, overlay string, input image.Image) (image.Image, error) { + hat, err := getCowboyHat(emojyPath, overlay) if err != nil { return nil, err } @@ -72,19 +78,19 @@ func cowboyifyImage(c *config.Config, emojyPath string, input image.Image) (imag return dst, nil } -func cowboy(c *config.Config, emojyPath, baseEmojyURL, name string) (image.Image, error) { +func cowboy(emojyPath, baseEmojyURL, overlay, name string) (image.Image, error) { cowboyMutex.Lock() defer cowboyMutex.Unlock() if img, ok := cowboyCache[name]; ok { log.Debug().Msgf(":cowboy_using_cached_image: %s", name) return img, nil } - log.Debug().Msgf(":cowboy_generating_image: %s", name) + log.Debug().Msgf(":cowboy_generating_image: %s with overlay %s", name, overlay) emjy, err := getEmojy(emojyPath, baseEmojyURL, name) if err != nil { return nil, err } - img, err := cowboyifyImage(c, emojyPath, emjy) + img, err := cowboyifyImage(emojyPath, overlay, emjy) if err != nil { return nil, err } diff --git a/plugins/cowboy/web.go b/plugins/cowboy/web.go index c5625ad..02618ed 100644 --- a/plugins/cowboy/web.go +++ b/plugins/cowboy/web.go @@ -9,15 +9,22 @@ import ( func (p *Cowboy) registerWeb() { r := chi.NewRouter() - r.HandleFunc("/img/{what}", p.handleImage) + r.HandleFunc("/img/{overlay}/{what}", p.handleImage) p.b.RegisterWeb(r, "/cowboy") } func (p *Cowboy) handleImage(w http.ResponseWriter, r *http.Request) { what := chi.URLParam(r, "what") - img, err := encode(cowboy(p.c, p.emojyPath, p.baseEmojyURL, what)) + overlay := chi.URLParam(r, "overlay") + overlays := p.c.GetMap("cowboy.overlays", defaultOverlays) + overlayPath, ok := overlays[overlay] + if !ok { + w.WriteHeader(http.StatusNotFound) + return + } + img, err := encode(cowboy(p.emojyPath, p.baseEmojyURL, overlayPath, what)) if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Error: %s", err) return } diff --git a/plugins/emojy/emojy.go b/plugins/emojy/emojy.go index f18f9d2..7ee2e76 100644 --- a/plugins/emojy/emojy.go +++ b/plugins/emojy/emojy.go @@ -4,6 +4,8 @@ import ( "bytes" "encoding/base64" "fmt" + "image" + "image/draw" "math" "os" "path" @@ -34,7 +36,14 @@ type EmojyPlugin struct { const maxLen = 32 func New(b bot.Bot) *EmojyPlugin { - log.Debug().Msgf("emojy.New") + p := NewAPI(b) + p.register() + p.registerWeb() + return p +} + +// NewAPI creates a version used only for API purposes (no callbacks registered) +func NewAPI(b bot.Bot) *EmojyPlugin { emojyPath := b.Config().Get("emojy.path", "emojy") baseURL := b.Config().Get("emojy.baseURL", "/emojy/file") p := &EmojyPlugin{ @@ -45,8 +54,6 @@ func New(b bot.Bot) *EmojyPlugin { baseURL: baseURL, } p.setupDB() - p.register() - p.registerWeb() return p } @@ -86,10 +93,10 @@ func (p *EmojyPlugin) register() { Kind: bot.Message, IsCmd: true, Regex: regexp.MustCompile(`(?i)^swapemojy (?P.+) (?P.+)$`), Handler: func(r bot.Request) bool { - old := sanitizeName(r.Values["old"]) - new := sanitizeName(r.Values["new"]) - p.rmEmojy(r, old) - p.addEmojy(r, new) + old := SanitizeName(r.Values["old"]) + new := SanitizeName(r.Values["new"]) + p.rmEmojyHandler(r, old) + p.addEmojyHandler(r, new) return true }, }, @@ -97,46 +104,57 @@ func (p *EmojyPlugin) register() { Kind: bot.Message, IsCmd: true, Regex: regexp.MustCompile(`(?i)^addemojy (?P.+)$`), Handler: func(r bot.Request) bool { - name := sanitizeName(r.Values["name"]) - return p.addEmojy(r, name) + name := SanitizeName(r.Values["name"]) + return p.addEmojyHandler(r, name) }, }, { Kind: bot.Message, IsCmd: true, Regex: regexp.MustCompile(`(?i)^rmemojy (?P.+)$`), Handler: func(r bot.Request) bool { - name := sanitizeName(r.Values["name"]) - return p.rmEmojy(r, name) + name := SanitizeName(r.Values["name"]) + return p.rmEmojyHandler(r, name) }, }, } p.b.RegisterTable(p, ht) } -func (p *EmojyPlugin) rmEmojy(r bot.Request, name string) bool { +func (p *EmojyPlugin) RmEmojy(c bot.Connector, name string) error { onServerList := invertEmojyList(p.b.GetEmojiList(false)) + // Call a non-existent emojy a successful remove if _, ok := onServerList[name]; !ok { - p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "Emoji does not exist") - return true + return fmt.Errorf("could not find emojy %s", name) } - if err := r.Conn.DeleteEmojy(name); err != nil { - p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "error "+err.Error()) + if err := c.DeleteEmojy(name); err != nil { + return err + } + return nil +} + +func (p *EmojyPlugin) rmEmojyHandler(r bot.Request, name string) bool { + err := p.RmEmojy(r.Conn, name) + if err != nil { + p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "Emoji does not exist") return true } p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "removed emojy "+name) return true } -func (p *EmojyPlugin) addEmojy(r bot.Request, name string) bool { +func (p *EmojyPlugin) AddEmojy(c bot.Connector, name string) error { onServerList := invertEmojyList(p.b.GetEmojiList(false)) if _, ok := onServerList[name]; ok { - p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "Emoji already exists") - return true + return fmt.Errorf("emojy already exists") } - if err := p.uploadEmojy(r.Conn, name); err != nil { - p.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("error adding emojy: %v", err)) - return true + if err := p.UploadEmojyInCache(c, name); err != nil { + return err } + return nil +} + +func (p *EmojyPlugin) addEmojyHandler(r bot.Request, name string) bool { + p.AddEmojy(r.Conn, name) list := r.Conn.GetEmojiList(true) for k, v := range list { if v == name { @@ -233,23 +251,44 @@ func isEmoji(in string) bool { return gomoji.ContainsEmoji(in) } -func (p *EmojyPlugin) uploadEmojy(c bot.Connector, name string) error { - maxEmojySz := p.c.GetFloat64("emoji.maxsize", 128.0) +func (p *EmojyPlugin) UploadEmojyInCache(c bot.Connector, name string) error { ok, fname, _, err := p.isKnownEmojy(name) if !ok || err != nil { u := p.c.Get("baseurl", "") u = u + "/emojy" return fmt.Errorf("error getting emojy, the known emojy list can be found at: %s", u) } - i, err := gg.LoadImage(fname) + f, err := os.Open(fname) if err != nil { return err } - ctx := gg.NewContextForImage(i) + img, _, err := image.Decode(f) + if err != nil { + return err + } + return p.UploadEmojyImage(c, name, img) +} + +func imageToRGBA(src image.Image) *image.RGBA { + // No conversion needed if image is an *image.RGBA. + if dst, ok := src.(*image.RGBA); ok { + return dst + } + + // Use the image/draw package to convert to *image.RGBA. + b := src.Bounds() + dst := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy())) + draw.Draw(dst, dst.Bounds(), src, b.Min, draw.Src) + return dst +} + +func (p *EmojyPlugin) UploadEmojyImage(c bot.Connector, name string, data image.Image) error { + maxEmojySz := p.c.GetFloat64("emoji.maxsize", 128.0) + ctx := gg.NewContextForRGBA(imageToRGBA(data)) max := math.Max(float64(ctx.Width()), float64(ctx.Height())) ctx.Scale(maxEmojySz/max, maxEmojySz/max) w := bytes.NewBuffer([]byte{}) - err = ctx.EncodePNG(w) + err := ctx.EncodePNG(w) if err != nil { return err } @@ -261,7 +300,7 @@ func (p *EmojyPlugin) uploadEmojy(c bot.Connector, name string) error { return nil } -func sanitizeName(name string) string { +func SanitizeName(name string) string { name = strings.ReplaceAll(name, "-", "_") nameLen := len(name) if nameLen > maxLen {