From a9937d9b8e89824e2d59bf22b8c8804e6811dcc3 Mon Sep 17 00:00:00 2001 From: Chris Sexton Date: Wed, 9 Sep 2020 13:21:39 -0400 Subject: [PATCH] discord: add discord functionality * added discord connector * modified user to support image avatars instead of URL avatars * modified meme to send IDs instead of names --- bot/interfaces.go | 4 + bot/msg/message.go | 1 + bot/user/users.go | 11 +- connectors/discord/discord.go | 140 +++++++++++++++++---- connectors/slackapp/slackApp.go | 7 +- main.go | 3 +- plugins/meme/meme.go | 207 +++++++++++++++++++------------- 7 files changed, 255 insertions(+), 118 deletions(-) diff --git a/bot/interfaces.go b/bot/interfaces.go index 8e38a81..af93915 100644 --- a/bot/interfaces.go +++ b/bot/interfaces.go @@ -29,11 +29,15 @@ const ( Help // SelfMessage triggers when the bot is sending a message SelfMessage + // Delete removes a message by ID + Delete ) type ImageAttachment struct { URL string AltTxt string + Width int + Height int } type Kind int diff --git a/bot/msg/message.go b/bot/msg/message.go index 6d3712c..9a13363 100644 --- a/bot/msg/message.go +++ b/bot/msg/message.go @@ -12,6 +12,7 @@ type Log Messages type Messages []Message type Message struct { + ID string User *user.User // With Slack, channel is the ID of a channel Channel string diff --git a/bot/user/users.go b/bot/user/users.go index 5a6e5f8..dd2efe3 100644 --- a/bot/user/users.go +++ b/bot/user/users.go @@ -2,14 +2,17 @@ package user +import "image" + // User type stores user history. This is a vehicle that will follow the user for the active // session type User struct { // Current nickname known - ID string - Name string - Admin bool - Icon string + ID string + Name string + Admin bool + Icon string + IconImg image.Image } func New(name string) User { diff --git a/connectors/discord/discord.go b/connectors/discord/discord.go index 10fc204..a6f3446 100644 --- a/connectors/discord/discord.go +++ b/connectors/discord/discord.go @@ -2,10 +2,8 @@ package discord import ( "errors" - "os" - "os/signal" - "syscall" - "time" + "fmt" + "strings" "github.com/velour/catbase/bot/msg" @@ -45,14 +43,81 @@ func (d Discord) Send(kind bot.Kind, args ...interface{}) (string, error) { switch kind { case bot.Message: - st, err := d.client.ChannelMessageSend(args[0].(string), args[1].(string)) + return d.sendMessage(args[0].(string), args[1].(string), false, args...) + case bot.Action: + return d.sendMessage(args[0].(string), args[1].(string), true, args...) + case bot.Edit: + st, err := d.client.ChannelMessageEdit(args[0].(string), args[1].(string), args[2].(string)) return st.ID, err + case bot.Reply: + original, err := d.client.ChannelMessage(args[0].(string), args[1].(string)) + msg := args[2].(string) + if err != nil { + log.Error().Err(err).Msg("could not get original") + } else { + msg = fmt.Sprintf("> %s\n%s", original, msg) + } + return d.sendMessage(args[0].(string), msg, false, args...) + case bot.Reaction: + msg := args[2].(msg.Message) + err := d.client.MessageReactionAdd(args[0].(string), msg.ID, args[1].(string)) + return args[1].(string), err + case bot.Delete: + ch := args[0].(string) + id := args[1].(string) + err := d.client.ChannelMessageDelete(ch, id) + if err != nil { + log.Error().Err(err).Msg("cannot delete message") + } + return id, err default: log.Error().Msgf("discord.Send: unknown kind, %+v", kind) return "", errors.New("unknown message type") } } +func (d *Discord) sendMessage(channel, message string, meMessage bool, args ...interface{}) (string, error) { + if meMessage && !strings.HasPrefix(message, "_") && !strings.HasSuffix(message, "_") { + message = "_" + message + "_" + } + + var embeds *discordgo.MessageEmbed + + for _, arg := range args { + switch a := arg.(type) { + case bot.ImageAttachment: + //embeds.URL = a.URL + embeds = &discordgo.MessageEmbed{} + embeds.Description = a.AltTxt + embeds.Image = &discordgo.MessageEmbedImage{ + URL: a.URL, + Width: a.Width, + Height: a.Height, + } + } + } + + data := &discordgo.MessageSend{ + Content: message, + Embed: embeds, + } + + log.Debug(). + Interface("data", data). + Interface("args", args). + Msg("sending message") + + st, err := d.client.ChannelMessageSendComplex(channel, data) + + //st, err := d.client.ChannelMessageSend(channel, message) + if err != nil { + log.Error().Err(err).Msg("Error sending message") + return "", err + } + + return st.ID, err +} + func (d *Discord) GetEmojiList() map[string]string { if d.emojiCache != nil { return d.emojiCache @@ -91,15 +156,19 @@ func (d *Discord) Profile(id string) (user.User, error) { log.Error().Err(err).Msg("Error getting user") return user.User{}, err } - return *convertUser(u), nil + return *d.convertUser(u), nil } -func convertUser(u *discordgo.User) *user.User { +func (d *Discord) convertUser(u *discordgo.User) *user.User { + img, err := d.client.UserAvatar(u.ID) + if err != nil { + log.Error().Err(err).Msg("error getting avatar") + } return &user.User{ - ID: u.ID, - Name: u.Username, - Admin: false, - Icon: u.Avatar, + ID: u.ID, + Name: u.Username, + Admin: false, + IconImg: img, } } @@ -119,14 +188,7 @@ func (d *Discord) Serve() error { d.client.AddHandler(d.messageCreate) - sc := make(chan os.Signal, 1) - signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) - <-sc - - log.Debug().Msg("closing discord connection") - - // Cleanly close down the Discord session. - return d.client.Close() + return nil } func (d *Discord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { @@ -135,13 +197,39 @@ func (d *Discord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate return } - msg := msg.Message{ - User: convertUser(m.Author), - Channel: m.ChannelID, - ChannelName: m.ChannelID, - Body: m.Content, - Time: time.Now(), + ch, err := s.Channel(m.ChannelID) + if err != nil { + log.Error().Err(err).Msg("error getting channel info") } - d.event(d, bot.Message, msg) + isCmd, text := bot.IsCmd(d.config, m.Content) + + tStamp, _ := m.Timestamp.Parse() + + msg := msg.Message{ + ID: m.ID, + User: d.convertUser(m.Author), + Channel: m.ChannelID, + ChannelName: ch.Name, + Body: text, + Command: isCmd, + Time: tStamp, + } + + log.Debug().Interface("m", m).Interface("msg", msg).Msg("message received") + + authorizedChannels := d.config.GetArray("channels", []string{}) + + if in(ch.Name, authorizedChannels) { + d.event(d, bot.Message, msg) + } +} + +func in(s string, lst []string) bool { + for _, i := range lst { + if s == i { + return true + } + } + return false } diff --git a/connectors/slackapp/slackApp.go b/connectors/slackapp/slackApp.go index 7093ffd..0ebc5c5 100644 --- a/connectors/slackapp/slackApp.go +++ b/connectors/slackapp/slackApp.go @@ -447,6 +447,7 @@ func (s *SlackApp) buildMessage(m *slackevents.MessageEvent) msg.Message { tstamp := slackTStoTime(m.TimeStamp) return msg.Message{ + ID: m.TimeStamp, User: &user.User{ ID: m.User, Name: name, @@ -651,8 +652,8 @@ func (s *SlackApp) reactionReceived(event *slackevents.ReactionAddedEvent) error return s.log(msg, channel) } -func (s *SlackApp) Profile(name string) (user.User, error) { - log.Debug().Msgf("Getting profile for %s", name) +func (s *SlackApp) Profile(identifier string) (user.User, error) { + log.Debug().Msgf("Getting profile for %s", identifier) users, err := s.api.GetUsers() if err != nil { @@ -660,7 +661,7 @@ func (s *SlackApp) Profile(name string) (user.User, error) { } for _, u := range users { - if u.Name == name { + if u.Name == identifier || u.ID == identifier { return user.User{ ID: u.ID, Name: stringForUser(&u), diff --git a/main.go b/main.go index dc0594c..03ef348 100644 --- a/main.go +++ b/main.go @@ -163,5 +163,6 @@ func main() { } addr := c.Get("HttpAddr", "127.0.0.1:1337") - log.Fatal().Err(http.ListenAndServe(addr, nil)) + log.Debug().Msgf("starting web service at %s", addr) + log.Fatal().Err(http.ListenAndServe(addr, nil)).Msg("bot killed") } diff --git a/plugins/meme/meme.go b/plugins/meme/meme.go index bf51df0..3d4c8ec 100644 --- a/plugins/meme/meme.go +++ b/plugins/meme/meme.go @@ -12,6 +12,7 @@ import ( "net/http" "net/url" "path" + "regexp" "sort" "strings" "time" @@ -67,7 +68,19 @@ func New(b bot.Bot) *MemePlugin { return mp } +var cmdMatch = regexp.MustCompile(`(?i)meme (.+)`) + func (p *MemePlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool { + if message.Command && cmdMatch.MatchString(message.Body) { + subs := cmdMatch.FindStringSubmatch(message.Body) + if len(subs) != 2 { + p.bot.Send(c, bot.Message, message.Channel, "Invalid meme request.") + return true + } + minusMeme := subs[1] + p.sendMeme(c, message.Channel, message.ChannelName, message.ID, message.User, minusMeme) + return true + } return false } @@ -202,6 +215,107 @@ func (p *MemePlugin) img(w http.ResponseWriter, r *http.Request) { p.images.cleanup() } +func (p *MemePlugin) bully(c bot.Connector, format, id string) image.Image { + bullyIcon := "" + + for _, bully := range p.c.GetArray("meme.bully", []string{}) { + if format == bully { + if u, err := c.Profile(bully); err == nil { + bullyIcon = u.Icon + } else { + log.Debug().Err(err).Msgf("could not get profile for %s", format) + } + formats := p.c.GetMap("meme.memes", defaultFormats) + format = randEntry(formats) + break + } + } + + if u, err := c.Profile(id); bullyIcon == "" && err == nil { + if u.IconImg != nil { + return u.IconImg + } + bullyIcon = u.Icon + } + + u, err := url.Parse(bullyIcon) + if err != nil { + log.Error().Err(err).Msg("error with bully URL") + } + bullyImg, err := DownloadTemplate(u) + if err != nil { + log.Error().Err(err).Msg("error downloading bully icon") + } + return bullyImg +} + +func (p *MemePlugin) sendMeme(c bot.Connector, channel, channelName, msgID string, from *user.User, text string) { + parts := strings.SplitN(text, " ", 2) + if len(parts) != 2 { + log.Debug().Msgf("Bad meme request: %s, %s", from, text) + p.bot.Send(c, bot.Message, channel, fmt.Sprintf("%s tried to send me a bad meme request.", from)) + return + } + isCmd, message := bot.IsCmd(p.c, parts[1]) + format := parts[0] + + log.Debug().Strs("parts", parts).Msgf("Meme:\n%+v", text) + + go func() { + top, bottom := "", message + parts = strings.Split(message, "\n") + if len(parts) > 1 { + top, bottom = parts[0], parts[1] + } + + if top == "_" { + message = bottom + } else if bottom == "_" { + message = top + } + + bullyImg := p.bully(c, format, from.ID) + + id, w, h, err := p.genMeme(format, top, bottom, bullyImg) + if err != nil { + msg := fmt.Sprintf("Hey %s, I couldn't download that image you asked for.", from) + p.bot.Send(c, bot.Message, channel, msg) + return + } + baseURL := p.c.Get("BaseURL", ``) + u, _ := url.Parse(baseURL) + u.Path = path.Join(u.Path, "meme", "img", id) + + log.Debug().Msgf("image is at %s", u.String()) + _, err = p.bot.Send(c, bot.Message, channel, "", bot.ImageAttachment{ + URL: u.String(), + AltTxt: fmt.Sprintf("%s: %s", from.Name, message), + Width: w, + Height: h, + }) + + if err == nil && msgID != "" { + p.bot.Send(c, bot.Delete, channel, msgID) + } + + m := msg.Message{ + User: &user.User{ + ID: from.ID, + Name: from.Name, + Admin: false, + }, + Channel: channel, + ChannelName: channelName, + Body: message, + Command: isCmd, + Time: time.Now(), + } + + p.bot.Receive(c, bot.Message, m) + }() + +} + func (p *MemePlugin) slashMeme(c bot.Connector) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { r.ParseForm() @@ -212,81 +326,15 @@ func (p *MemePlugin) slashMeme(c bot.Connector) http.HandlerFunc { text := r.PostForm.Get("text") log.Debug().Msgf("channel: %s", channel) - parts := strings.SplitN(text, " ", 2) - if len(parts) != 2 { - log.Debug().Msgf("Bad meme request: %s, %s", from, text) - p.bot.Send(c, bot.Message, channel, fmt.Sprintf("%s tried to send me a bad meme request.", from)) - return - } - isCmd, message := bot.IsCmd(p.c, parts[1]) - format := parts[0] - - bullyIcon := "" - - for _, bully := range p.c.GetArray("meme.bully", []string{}) { - if format == bully { - if u, err := c.Profile(bully); err == nil { - bullyIcon = u.Icon - } else { - log.Debug().Err(err).Msgf("could not get profile for %s", format) - } - formats := p.c.GetMap("meme.memes", defaultFormats) - format = randEntry(formats) - break - } + user := &user.User{ + ID: from, // HACK but should work fine + Name: from, } - if u, err := c.Profile(from); bullyIcon == "" && err == nil { - bullyIcon = u.Icon - } + p.sendMeme(c, channel, channelName, "", user, text) - log.Debug().Strs("parts", parts).Msgf("Meme:\n%+v", text) w.WriteHeader(200) w.Write(nil) - - go func() { - top, bottom := "", message - parts = strings.Split(message, "\n") - if len(parts) > 1 { - top, bottom = parts[0], parts[1] - } - - if top == "_" { - message = bottom - } else if bottom == "_" { - message = top - } - - id, err := p.genMeme(format, top, bottom, bullyIcon) - if err != nil { - msg := fmt.Sprintf("Hey %s, I couldn't download that image you asked for.", from) - p.bot.Send(c, bot.Message, channel, msg) - return - } - 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: fmt.Sprintf("%s: %s", from, message), - }) - m := msg.Message{ - User: &user.User{ - ID: from, - Name: from, - Admin: false, - }, - Channel: channel, - ChannelName: channelName, - Body: message, - Command: isCmd, - Time: time.Now(), - } - - p.bot.Receive(c, bot.Message, m) - }() } } @@ -322,7 +370,7 @@ var defaultFormats = map[string]string{ "raptor": "Philosoraptor.jpg", } -func (p *MemePlugin) genMeme(meme, top, bottom, bully string) (string, error) { +func (p *MemePlugin) genMeme(meme, top, bottom string, bully image.Image) (string, int, int, error) { fontSizes := []float64{48, 36, 24, 16, 12} fontSize := fontSizes[0] @@ -346,7 +394,7 @@ func (p *MemePlugin) genMeme(meme, top, bottom, bully string) (string, error) { img, err := DownloadTemplate(u) if err != nil { log.Debug().Msgf("failed to download image: %s", err) - return "", err + return "", 0, 0, err } r := img.Bounds() @@ -372,7 +420,7 @@ func (p *MemePlugin) genMeme(meme, top, bottom, bully string) (string, error) { h = r.Dy() log.Debug().Msgf("resized to %v, %v", w, h) - if bully != "" { + if bully != nil { img = p.applyBully(img, bully) } @@ -427,20 +475,11 @@ func (p *MemePlugin) genMeme(meme, top, bottom, bully string) (string, error) { log.Debug().Msgf("Saved to %s\n", path) - return path, nil + return path, w, h, nil } -func (p *MemePlugin) applyBully(img image.Image, bully string) image.Image { - log.Debug().Msgf("applying bully: %s", bully) +func (p *MemePlugin) applyBully(img, bullyImg image.Image) image.Image { dst := image.NewRGBA(img.Bounds()) - u, err := url.Parse(bully) - if err != nil { - return img - } - bullyImg, err := DownloadTemplate(u) - if err != nil { - return img - } scaleFactor := p.c.GetFloat64("meme.bullyScale", 0.1)