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
This commit is contained in:
Chris Sexton 2020-09-09 13:21:39 -04:00
parent dba38310e4
commit a9937d9b8e
7 changed files with 255 additions and 118 deletions

View File

@ -29,11 +29,15 @@ const (
Help Help
// SelfMessage triggers when the bot is sending a message // SelfMessage triggers when the bot is sending a message
SelfMessage SelfMessage
// Delete removes a message by ID
Delete
) )
type ImageAttachment struct { type ImageAttachment struct {
URL string URL string
AltTxt string AltTxt string
Width int
Height int
} }
type Kind int type Kind int

View File

@ -12,6 +12,7 @@ type Log Messages
type Messages []Message type Messages []Message
type Message struct { type Message struct {
ID string
User *user.User User *user.User
// With Slack, channel is the ID of a channel // With Slack, channel is the ID of a channel
Channel string Channel string

View File

@ -2,6 +2,8 @@
package user package user
import "image"
// User type stores user history. This is a vehicle that will follow the user for the active // User type stores user history. This is a vehicle that will follow the user for the active
// session // session
type User struct { type User struct {
@ -10,6 +12,7 @@ type User struct {
Name string Name string
Admin bool Admin bool
Icon string Icon string
IconImg image.Image
} }
func New(name string) User { func New(name string) User {

View File

@ -2,10 +2,8 @@ package discord
import ( import (
"errors" "errors"
"os" "fmt"
"os/signal" "strings"
"syscall"
"time"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
@ -45,14 +43,81 @@ func (d Discord) Send(kind bot.Kind, args ...interface{}) (string, error) {
switch kind { switch kind {
case bot.Message: 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 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: default:
log.Error().Msgf("discord.Send: unknown kind, %+v", kind) log.Error().Msgf("discord.Send: unknown kind, %+v", kind)
return "", errors.New("unknown message type") 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 { func (d *Discord) GetEmojiList() map[string]string {
if d.emojiCache != nil { if d.emojiCache != nil {
return d.emojiCache return d.emojiCache
@ -91,15 +156,19 @@ func (d *Discord) Profile(id string) (user.User, error) {
log.Error().Err(err).Msg("Error getting user") log.Error().Err(err).Msg("Error getting user")
return user.User{}, err 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{ return &user.User{
ID: u.ID, ID: u.ID,
Name: u.Username, Name: u.Username,
Admin: false, Admin: false,
Icon: u.Avatar, IconImg: img,
} }
} }
@ -119,14 +188,7 @@ func (d *Discord) Serve() error {
d.client.AddHandler(d.messageCreate) d.client.AddHandler(d.messageCreate)
sc := make(chan os.Signal, 1) return nil
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()
} }
func (d *Discord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { 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 return
} }
msg := msg.Message{ ch, err := s.Channel(m.ChannelID)
User: convertUser(m.Author), if err != nil {
Channel: m.ChannelID, log.Error().Err(err).Msg("error getting channel info")
ChannelName: m.ChannelID,
Body: m.Content,
Time: time.Now(),
} }
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) d.event(d, bot.Message, msg)
}
}
func in(s string, lst []string) bool {
for _, i := range lst {
if s == i {
return true
}
}
return false
} }

View File

@ -447,6 +447,7 @@ func (s *SlackApp) buildMessage(m *slackevents.MessageEvent) msg.Message {
tstamp := slackTStoTime(m.TimeStamp) tstamp := slackTStoTime(m.TimeStamp)
return msg.Message{ return msg.Message{
ID: m.TimeStamp,
User: &user.User{ User: &user.User{
ID: m.User, ID: m.User,
Name: name, Name: name,
@ -651,8 +652,8 @@ func (s *SlackApp) reactionReceived(event *slackevents.ReactionAddedEvent) error
return s.log(msg, channel) return s.log(msg, channel)
} }
func (s *SlackApp) Profile(name string) (user.User, error) { func (s *SlackApp) Profile(identifier string) (user.User, error) {
log.Debug().Msgf("Getting profile for %s", name) log.Debug().Msgf("Getting profile for %s", identifier)
users, err := s.api.GetUsers() users, err := s.api.GetUsers()
if err != nil { if err != nil {
@ -660,7 +661,7 @@ func (s *SlackApp) Profile(name string) (user.User, error) {
} }
for _, u := range users { for _, u := range users {
if u.Name == name { if u.Name == identifier || u.ID == identifier {
return user.User{ return user.User{
ID: u.ID, ID: u.ID,
Name: stringForUser(&u), Name: stringForUser(&u),

View File

@ -163,5 +163,6 @@ func main() {
} }
addr := c.Get("HttpAddr", "127.0.0.1:1337") 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")
} }

View File

@ -12,6 +12,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
"regexp"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -67,7 +68,19 @@ func New(b bot.Bot) *MemePlugin {
return mp return mp
} }
var cmdMatch = regexp.MustCompile(`(?i)meme (.+)`)
func (p *MemePlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool { 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 return false
} }
@ -202,25 +215,7 @@ func (p *MemePlugin) img(w http.ResponseWriter, r *http.Request) {
p.images.cleanup() p.images.cleanup()
} }
func (p *MemePlugin) slashMeme(c bot.Connector) http.HandlerFunc { func (p *MemePlugin) bully(c bot.Connector, format, id string) image.Image {
return func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
log.Debug().Msgf("Meme:\n%+v", r.PostForm.Get("text"))
channel := r.PostForm.Get("channel_id")
channelName := r.PostForm.Get("channel_name")
from := r.PostForm.Get("user_name")
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 := "" bullyIcon := ""
for _, bully := range p.c.GetArray("meme.bully", []string{}) { for _, bully := range p.c.GetArray("meme.bully", []string{}) {
@ -236,13 +231,35 @@ func (p *MemePlugin) slashMeme(c bot.Connector) http.HandlerFunc {
} }
} }
if u, err := c.Profile(from); bullyIcon == "" && err == nil { if u, err := c.Profile(id); bullyIcon == "" && err == nil {
if u.IconImg != nil {
return u.IconImg
}
bullyIcon = u.Icon 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) log.Debug().Strs("parts", parts).Msgf("Meme:\n%+v", text)
w.WriteHeader(200)
w.Write(nil)
go func() { go func() {
top, bottom := "", message top, bottom := "", message
@ -257,25 +274,34 @@ func (p *MemePlugin) slashMeme(c bot.Connector) http.HandlerFunc {
message = top message = top
} }
id, err := p.genMeme(format, top, bottom, bullyIcon) bullyImg := p.bully(c, format, from.ID)
id, w, h, err := p.genMeme(format, top, bottom, bullyImg)
if err != nil { if err != nil {
msg := fmt.Sprintf("Hey %s, I couldn't download that image you asked for.", from) msg := fmt.Sprintf("Hey %s, I couldn't download that image you asked for.", from)
p.bot.Send(c, bot.Message, channel, msg) p.bot.Send(c, bot.Message, channel, msg)
return return
} }
baseURL := p.c.Get("BaseURL", `https://catbase.velour.ninja`) baseURL := p.c.Get("BaseURL", ``)
u, _ := url.Parse(baseURL) u, _ := url.Parse(baseURL)
u.Path = path.Join(u.Path, "meme", "img", id) u.Path = path.Join(u.Path, "meme", "img", id)
log.Debug().Msgf("image is at %s", u.String()) log.Debug().Msgf("image is at %s", u.String())
p.bot.Send(c, bot.Message, channel, "", bot.ImageAttachment{ _, err = p.bot.Send(c, bot.Message, channel, "", bot.ImageAttachment{
URL: u.String(), URL: u.String(),
AltTxt: fmt.Sprintf("%s: %s", from, message), 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{ m := msg.Message{
User: &user.User{ User: &user.User{
ID: from, ID: from.ID,
Name: from, Name: from.Name,
Admin: false, Admin: false,
}, },
Channel: channel, Channel: channel,
@ -287,6 +313,28 @@ func (p *MemePlugin) slashMeme(c bot.Connector) http.HandlerFunc {
p.bot.Receive(c, bot.Message, m) 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()
log.Debug().Msgf("Meme:\n%+v", r.PostForm.Get("text"))
channel := r.PostForm.Get("channel_id")
channelName := r.PostForm.Get("channel_name")
from := r.PostForm.Get("user_name")
text := r.PostForm.Get("text")
log.Debug().Msgf("channel: %s", channel)
user := &user.User{
ID: from, // HACK but should work fine
Name: from,
}
p.sendMeme(c, channel, channelName, "", user, text)
w.WriteHeader(200)
w.Write(nil)
} }
} }
@ -322,7 +370,7 @@ var defaultFormats = map[string]string{
"raptor": "Philosoraptor.jpg", "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} fontSizes := []float64{48, 36, 24, 16, 12}
fontSize := fontSizes[0] fontSize := fontSizes[0]
@ -346,7 +394,7 @@ func (p *MemePlugin) genMeme(meme, top, bottom, bully string) (string, error) {
img, err := DownloadTemplate(u) img, err := DownloadTemplate(u)
if err != nil { if err != nil {
log.Debug().Msgf("failed to download image: %s", err) log.Debug().Msgf("failed to download image: %s", err)
return "", err return "", 0, 0, err
} }
r := img.Bounds() r := img.Bounds()
@ -372,7 +420,7 @@ func (p *MemePlugin) genMeme(meme, top, bottom, bully string) (string, error) {
h = r.Dy() h = r.Dy()
log.Debug().Msgf("resized to %v, %v", w, h) log.Debug().Msgf("resized to %v, %v", w, h)
if bully != "" { if bully != nil {
img = p.applyBully(img, bully) 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) 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 { func (p *MemePlugin) applyBully(img, bullyImg image.Image) image.Image {
log.Debug().Msgf("applying bully: %s", bully)
dst := image.NewRGBA(img.Bounds()) 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) scaleFactor := p.c.GetFloat64("meme.bullyScale", 0.1)