From feb42b8293bd8ae4a9b23aa375dea66a15c579ca Mon Sep 17 00:00:00 2001 From: Chris Sexton <3216719+chrissexton@users.noreply.github.com> Date: Wed, 8 Jun 2022 20:57:24 +0000 Subject: [PATCH] emojy: allow creation and removal of emojy - includes a web interface for uploading new images - includes a web interface for viewing 'possible' images --- bot/handlers.go | 4 +- bot/interfaces.go | 10 ++- bot/mock.go | 4 +- connectors/discord/discord.go | 30 ++++++- connectors/irc/irc.go | 10 ++- connectors/slackapp/slackApp.go | 13 +++- go.mod | 1 + go.sum | 3 + plugins/cli/cli.go | 10 ++- plugins/emojifyme/emojifyme.go | 2 +- plugins/emojy/emojy.go | 133 ++++++++++++++++++++++++++++---- plugins/emojy/index.html | 101 ++++++++++++++++++++---- plugins/emojy/web.go | 87 +++++++++++++++++++++ 13 files changed, 362 insertions(+), 46 deletions(-) diff --git a/bot/handlers.go b/bot/handlers.go index 5b740f4..2e202de 100644 --- a/bot/handlers.go +++ b/bot/handlers.go @@ -91,8 +91,8 @@ func (b *bot) Send(conn Connector, kind Kind, args ...any) (string, error) { return conn.Send(kind, args...) } -func (b *bot) GetEmojiList() map[string]string { - return b.conn.GetEmojiList() +func (b *bot) GetEmojiList(force bool) map[string]string { + return b.conn.GetEmojiList(force) } // Checks to see if the user is asking for help, returns true if so and handles the situation. diff --git a/bot/interfaces.go b/bot/interfaces.go index f7e14d6..6234d11 100644 --- a/bot/interfaces.go +++ b/bot/interfaces.go @@ -129,7 +129,7 @@ type Bot interface { CheckAdmin(string) bool // GetEmojiList returns known emoji - GetEmojiList() map[string]string + GetEmojiList(bool) map[string]string // RegisterFilter creates a filter function for message processing RegisterFilter(string, func(string) string) @@ -186,7 +186,7 @@ type Connector interface { Send(Kind, ...any) (string, error) // GetEmojiList returns a connector's known custom emoji - GetEmojiList() map[string]string + GetEmojiList(bool) map[string]string // Serve starts a connector's connection routine Serve() error @@ -203,6 +203,12 @@ type Connector interface { // Emojy translates emojy to/from services Emojy(string) string + // UploadEmojy creates a new emojy on the server related to the file at path + UploadEmojy(emojy, path string) error + + // DeleteEmojy removes the specified emojy from the server + DeleteEmojy(emojy string) error + // GetChannelName returns the human-friendly name for an ID (if possible) GetChannelName(id string) string diff --git a/bot/mock.go b/bot/mock.go index 5b2b470..1993589 100644 --- a/bot/mock.go +++ b/bot/mock.go @@ -103,7 +103,9 @@ func (mb *MockBot) edit(c Connector, channel, newMessage, identifier string) (st return "", nil } -func (mb *MockBot) GetEmojiList() map[string]string { return make(map[string]string) } +func (mb *MockBot) GetEmojiList(bool) map[string]string { return make(map[string]string) } +func (mb *MockBot) DeleteEmojy(name string) error { return nil } +func (mb *MockBot) UploadEmojy(emojy, path string) error { return nil } func (mb *MockBot) RegisterFilter(s string, f func(string) string) {} func NewMockBot() *MockBot { diff --git a/connectors/discord/discord.go b/connectors/discord/discord.go index 748212c..61d4efe 100644 --- a/connectors/discord/discord.go +++ b/connectors/discord/discord.go @@ -134,8 +134,8 @@ func (d *Discord) sendMessage(channel, message string, meMessage bool, args ...a return st.ID, err } -func (d *Discord) GetEmojiList() map[string]string { - if d.emojiCache != nil { +func (d *Discord) GetEmojiList(force bool) map[string]string { + if d.emojiCache != nil && !force { return d.emojiCache } guildID := d.config.Get("discord.guildid", "") @@ -155,6 +155,16 @@ func (d *Discord) GetEmojiList() map[string]string { return emojis } +func (d *Discord) GetEmojySnowflake(name string) string { + list := d.GetEmojiList(true) + for k, v := range list { + if name == v { + return k + } + } + return name +} + // Who gets the users in the guild // Note that the channel ID does not matter in this particular case func (d *Discord) Who(id string) []string { @@ -298,6 +308,22 @@ func (d *Discord) Emojy(name string) string { return name } +func (d *Discord) UploadEmojy(emojy, path string) error { + guildID := d.config.Get("discord.guildid", "") + defaultRoles := d.config.GetArray("discord.emojyRoles", []string{}) + _, err := d.client.GuildEmojiCreate(guildID, emojy, path, defaultRoles) + if err != nil { + return err + } + return nil +} + +func (d *Discord) DeleteEmojy(emojy string) error { + guildID := d.config.Get("discord.guildid", "") + emojyID := d.GetEmojySnowflake(emojy) + return d.client.GuildEmojiDelete(guildID, emojyID) +} + func (d *Discord) URLFormat(title, url string) string { return fmt.Sprintf("%s (%s)", title, url) } diff --git a/connectors/irc/irc.go b/connectors/irc/irc.go index c9cb408..d850690 100644 --- a/connectors/irc/irc.go +++ b/connectors/irc/irc.go @@ -129,7 +129,7 @@ func (i *Irc) sendAction(channel, message string, args ...any) (string, error) { return i.sendMessage(channel, message, args...) } -func (i *Irc) GetEmojiList() map[string]string { +func (i *Irc) GetEmojiList(force bool) map[string]string { //we're not going to do anything because it's IRC return make(map[string]string) } @@ -334,6 +334,14 @@ func (i Irc) Emojy(name string) string { return name } +func (i Irc) UploadEmojy(emojy, path string) error { + return fmt.Errorf("unimplemented") +} + +func (d Irc) DeleteEmojy(emojy string) error { + return fmt.Errorf("unimplemented") +} + // GetChannelName returns the channel ID for a human-friendly name (if possible) func (i Irc) GetChannelID(name string) string { return name diff --git a/connectors/slackapp/slackApp.go b/connectors/slackapp/slackApp.go index 7d57c07..13327eb 100644 --- a/connectors/slackapp/slackApp.go +++ b/connectors/slackapp/slackApp.go @@ -396,7 +396,10 @@ func (s *SlackApp) edit(channel, newMessage, identifier string) (string, error) return ts, err } -func (s *SlackApp) GetEmojiList() map[string]string { +func (s *SlackApp) GetEmojiList(force bool) map[string]string { + if force { + s.populateEmojiList() + } return s.emoji } @@ -727,6 +730,14 @@ func (s *SlackApp) Emojy(name string) string { return name } +func (s *SlackApp) UploadEmojy(emojy, path string) error { + return fmt.Errorf("unimplemented") +} + +func (s *SlackApp) DeleteEmojy(emojy string) error { + return fmt.Errorf("unimplemented") +} + func (s *SlackApp) URLFormat(title, url string) string { return fmt.Sprintf("<%s|%s>", url, title) } diff --git a/go.mod b/go.mod index c4ebfe6..f225ea4 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/chrissexton/sentiment v0.0.0-20190927141846-d69c422ba035 github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90 github.com/forPelevin/gomoji v1.1.4 + github.com/gabriel-vasile/mimetype v1.4.0 github.com/go-chi/chi/v5 v5.0.7 github.com/gocolly/colly v1.2.0 github.com/google/uuid v1.3.0 diff --git a/go.sum b/go.sum index e02a0d8..b061468 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90 h1:WXb3TSNmHp2vHoCro github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/forPelevin/gomoji v1.1.4 h1:mlxsZQgTO7v1qnpUUoS8kk0Lf/rEvxZYgYxuVUX7edg= github.com/forPelevin/gomoji v1.1.4/go.mod h1:ypB7Kz3Fsp+LVR7KoT7mEFOioYBuTuAtaAT4RGl+ASY= +github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro= +github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= 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= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= @@ -197,6 +199,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0= diff --git a/plugins/cli/cli.go b/plugins/cli/cli.go index d52c982..c3d7454 100644 --- a/plugins/cli/cli.go +++ b/plugins/cli/cli.go @@ -122,13 +122,15 @@ func (p *CliPlugin) Send(kind bot.Kind, args ...any) (string, error) { p.counter++ return id, nil } -func (p *CliPlugin) GetEmojiList() map[string]string { return nil } -func (p *CliPlugin) Serve() error { return nil } -func (p *CliPlugin) Who(s string) []string { return nil } +func (p *CliPlugin) GetEmojiList(bool) map[string]string { return nil } +func (p *CliPlugin) Serve() error { return nil } +func (p *CliPlugin) Who(s string) []string { return nil } func (p *CliPlugin) Profile(name string) (user.User, error) { return user.User{}, fmt.Errorf("unimplemented") } -func (p *CliPlugin) Emojy(name string) string { return name } +func (p *CliPlugin) Emojy(name string) string { return name } +func (p *CliPlugin) DeleteEmojy(name string) error { return nil } +func (p *CliPlugin) UploadEmojy(emojy, path string) error { return nil } func (p *CliPlugin) URLFormat(title, url string) string { return fmt.Sprintf("%s (%s)", title, url) } diff --git a/plugins/emojifyme/emojifyme.go b/plugins/emojifyme/emojifyme.go index 1d1567b..d7ea9a4 100644 --- a/plugins/emojifyme/emojifyme.go +++ b/plugins/emojifyme/emojifyme.go @@ -63,7 +63,7 @@ func (p *EmojifyMePlugin) message(r bot.Request) bool { message := r.Msg if !p.GotBotEmoji { p.GotBotEmoji = true - emojiMap := p.Bot.GetEmojiList() + emojiMap := p.Bot.GetEmojiList(false) for e := range emojiMap { p.Emoji[e] = e } diff --git a/plugins/emojy/emojy.go b/plugins/emojy/emojy.go index 3080bcc..fc94512 100644 --- a/plugins/emojy/emojy.go +++ b/plugins/emojy/emojy.go @@ -1,16 +1,24 @@ package emojy import ( + "bytes" + "encoding/base64" + "fmt" + "github.com/fogleman/gg" + "github.com/gabriel-vasile/mimetype" + "math" + "os" + "path" + "path/filepath" + "regexp" + "strings" + "time" + "github.com/forPelevin/gomoji" "github.com/jmoiron/sqlx" "github.com/rs/zerolog/log" "github.com/velour/catbase/bot" "github.com/velour/catbase/config" - "os" - "path" - "regexp" - "strings" - "time" ) type EmojyPlugin struct { @@ -19,6 +27,8 @@ type EmojyPlugin struct { db *sqlx.DB } +const maxLen = 32 + func New(b bot.Bot) *EmojyPlugin { log.Debug().Msgf("emojy.New") p := &EmojyPlugin{ @@ -64,6 +74,48 @@ func (p *EmojyPlugin) register() { return false }, }, + { + Kind: bot.Message, IsCmd: true, + Regex: regexp.MustCompile(`(?i)^addemojy (?P.+)$`), + Handler: func(r bot.Request) bool { + name := sanitizeName(r.Values["name"]) + 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 + } + 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 + } + list := r.Conn.GetEmojiList(true) + for k, v := range list { + if v == name { + p.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("added emojy: %s <:%s:%s>", name, name, k)) + break + } + } + return true + }, + }, + { + Kind: bot.Message, IsCmd: true, + Regex: regexp.MustCompile(`(?i)^rmemojy (?P.+)$`), + Handler: func(r bot.Request) bool { + name := sanitizeName(r.Values["name"]) + onServerList := invertEmojyList(p.b.GetEmojiList(false)) + if _, ok := onServerList[name]; !ok { + p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "Emoji does not exist") + return true + } + if err := r.Conn.DeleteEmojy(name); err != nil { + p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "error "+err.Error()) + return true + } + p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "removed emojy "+name) + return true + }, + }, } p.b.RegisterTable(p, ht) } @@ -100,7 +152,7 @@ func invertEmojyList(emojy map[string]string) map[string]string { func (p *EmojyPlugin) allCounts() (map[string][]EmojyCount, error) { out := map[string][]EmojyCount{} - onServerList := invertEmojyList(p.b.DefaultConnector().GetEmojiList()) + onServerList := invertEmojyList(p.b.GetEmojiList(true)) q := `select emojy, count(observed) as count from emojyLog group by emojy order by count desc` result := []EmojyCount{} err := p.db.Select(&result, q) @@ -111,8 +163,8 @@ func (p *EmojyPlugin) allCounts() (map[string][]EmojyCount, error) { _, e.OnServer = onServerList[e.Emojy] if isEmoji(e.Emojy) { out["emoji"] = append(out["emoji"], e) - } else if ok, fname, _ := p.isKnownEmojy(e.Emojy); ok { - e.URL = fname + } else if ok, _, eURL, _ := p.isKnownEmojy(e.Emojy); ok { + e.URL = eURL out["emojy"] = append(out["emojy"], e) } else { out["unknown"] = append(out["unknown"], e) @@ -121,25 +173,74 @@ func (p *EmojyPlugin) allCounts() (map[string][]EmojyCount, error) { return out, nil } -func (p *EmojyPlugin) isKnownEmojy(name string) (bool, string, error) { +func (p *EmojyPlugin) allFiles() (map[string]string, map[string]string, error) { + files := map[string]string{} + urls := map[string]string{} emojyPath := p.c.Get("emojy.path", "emojy") baseURL := p.c.Get("emojy.baseURL", "/emojy/file") entries, err := os.ReadDir(emojyPath) if err != nil { - return false, "", err + return nil, nil, err } for _, e := range entries { - if !e.IsDir() && - (strings.HasPrefix(e.Name(), name) || - strings.HasPrefix(e.Name(), strings.Replace(name, "-", "_", -1)) || - strings.HasPrefix(e.Name(), strings.Trim(name, "-_"))) { + if !e.IsDir() { + baseName := strings.TrimSuffix(e.Name(), filepath.Ext(e.Name())) url := path.Join(baseURL, e.Name()) - return true, url, nil + files[baseName] = path.Join(emojyPath, e.Name()) + urls[baseName] = url } } - return false, "", nil + return files, urls, nil +} + +func (p *EmojyPlugin) isKnownEmojy(name string) (bool, string, string, error) { + allFiles, allURLs, err := p.allFiles() + if err != nil { + return false, "", "", err + } + if _, ok := allURLs[name]; !ok { + return false, "", "", nil + } + return true, allFiles[name], allURLs[name], nil } 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) + 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) + if err != nil { + return err + } + ctx := gg.NewContextForImage(i) + max := math.Max(float64(ctx.Width()), float64(ctx.Height())) + ctx.Scale(maxEmojySz/max, maxEmojySz/max) + w := bytes.NewBuffer([]byte{}) + err = ctx.EncodePNG(w) + if err != nil { + return err + } + mtype := mimetype.Detect(w.Bytes()) + base64Img := "data:" + mtype.String() + ";base64," + base64.StdEncoding.EncodeToString(w.Bytes()) + if err := c.UploadEmojy(name, base64Img); err != nil { + return err + } + return nil +} + +func sanitizeName(name string) string { + name = strings.ReplaceAll(name, "-", "_") + nameLen := len(name) + if nameLen > maxLen { + nameLen = maxLen + } + return name[:nameLen] +} diff --git a/plugins/emojy/index.html b/plugins/emojy/index.html index ed3b164..ce6e71a 100644 --- a/plugins/emojy/index.html +++ b/plugins/emojy/index.html @@ -2,8 +2,8 @@ - - + + @@ -22,18 +22,39 @@ Emojys - {{ item.name }} + {{ item.name + }} + - - {{ err }} - -