From 053921be43380b29694d9f3fae49e0453a52af51 Mon Sep 17 00:00:00 2001 From: Etzelia Date: Thu, 2 Jul 2020 22:00:22 +0200 Subject: [PATCH] Add rate limit, insult, and name history (#3) Update README Signed-off-by: Etzelia Refactor, squash bugs, and add name history Signed-off-by: Etzelia Add rate limit and insult Signed-off-by: Etzelia Reviewed-on: https://git.etztech.xyz/Etzelia/sedbot/pulls/3 --- README.md | 7 +++- config/config.go | 7 ++++ discord/dad.go | 4 ++ discord/discord.go | 19 +++++++++ discord/discord_test.go | 10 +++++ discord/echo.go | 2 +- discord/history.go | 90 +++++++++++++++++++++++++++++++++++++++++ discord/inspire.go | 4 ++ discord/insult.go | 59 +++++++++++++++++++++++++++ discord/utils.go | 26 ++++++++++++ discord/utils_test.go | 43 ++++++++++++++++++++ go.mod | 1 + go.sum | 2 + sedbot.example.toml | 11 +++++ 14 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 discord/discord_test.go create mode 100644 discord/history.go create mode 100644 discord/insult.go create mode 100644 discord/utils.go create mode 100644 discord/utils_test.go diff --git a/README.md b/README.md index f5efed3..892649f 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,13 @@ BirbMC Discord generi-bot. ### Public * `register ` - Register to the Discord -* `links` - Get a list of dynamic links - * `` - Get a specific link +* `echoes` - Get a list of dynamic echo messages + * `` - Get a specific echo message * `fired` - Check how many times Carolyn has been fired * `inspire` - Get a random "inspirational" message from [InspiroBot](https://inspirobot.me) +* `dad` - Get a random dad joke from [icanhazdadjoke](https://icanhazdadjoke.com) +* `insult` - The fan favorite returns +* `names [<01/02/2006>]` - Minecraft name history (optionally at a specific time) ### Moderation diff --git a/config/config.go b/config/config.go index 6b59878..8ea5ca6 100644 --- a/config/config.go +++ b/config/config.go @@ -22,6 +22,13 @@ type Config struct { RegisterRole string `toml:"register_role"` RegisteredChannel string `toml:"registered_channel"` FiredRole string `toml:"fired_role"` + MemeRate string `toml:"meme_rate"` + Insult struct { + Targets []string `toml:"targets"` + Comparisons []string `toml:"comparisons"` + Adjectives []string `toml:"adjectives"` + Nouns []string `toml:"nouns"` + } `toml:"insult"` } type MessageRole struct { diff --git a/discord/dad.go b/discord/dad.go index 84a6fb3..10d999e 100644 --- a/discord/dad.go +++ b/discord/dad.go @@ -23,6 +23,10 @@ func init() { return true }, run: func(cmd commandInit) (string, error) { + if !memeRateLimit.Try() { + return "Woah, slow down!", nil + } + req, err := http.NewRequest(http.MethodGet, dadJokeAPI, nil) if err != nil { return "", err diff --git a/discord/discord.go b/discord/discord.go index 8e4fed6..3398e88 100644 --- a/discord/discord.go +++ b/discord/discord.go @@ -2,6 +2,7 @@ package discord import ( "strings" + "time" "go.etztech.xyz/sedbot/config" "go.etztech.xyz/sedbot/database" @@ -14,6 +15,8 @@ import ( var ( commands = make(map[string]command) messageRoleMap = make(map[string]map[string]string) + + memeRateLimit *rateLimit ) type commandInit struct { @@ -50,6 +53,13 @@ func Bot(cfg *config.Config, db *database.Database) (*discordgo.Session, error) bot.AddHandler(reactionAddHandler()) bot.AddHandler(reactionRemoveHandler()) + // Rate limits + d, err := time.ParseDuration(cfg.MemeRate) + if err != nil { + return nil, err + } + memeRateLimit = NewRateLimit(d) + return bot, nil } @@ -68,6 +78,15 @@ func sendMessage(s *discordgo.Session, channelID, content string) *discordgo.Mes return msg } +func sendEmbed(s *discordgo.Session, channelID string, embed *discordgo.MessageEmbed) *discordgo.Message { + msg, err := s.ChannelMessageSendEmbed(channelID, embed) + if err != nil { + beaver.Errorf("could not send embed: %v", err) + return nil + } + return msg +} + func isStaff(authorRoleIDs, staffRoleIDs []string) bool { for _, aRole := range authorRoleIDs { for _, sRole := range staffRoleIDs { diff --git a/discord/discord_test.go b/discord/discord_test.go new file mode 100644 index 0000000..498ef79 --- /dev/null +++ b/discord/discord_test.go @@ -0,0 +1,10 @@ +package discord + +import ( + "os" + "testing" +) + +func TestMain(m *testing.M) { + os.Exit(m.Run()) +} diff --git a/discord/echo.go b/discord/echo.go index f249772..b450a02 100644 --- a/discord/echo.go +++ b/discord/echo.go @@ -33,7 +33,7 @@ func Echo(cfg *config.Config) { } } combined := append([]string{echo.Name}, echo.Aliases...) - echoes = append(echoes, fmt.Sprintf("%s: %s", strings.Join(combined, ", "), echo.Help)) + echoes = append(echoes, fmt.Sprintf("**%s**: %s", strings.Join(combined, ", "), echo.Help)) } commands["echoes"] = command{ diff --git a/discord/history.go b/discord/history.go new file mode 100644 index 0000000..367f4e6 --- /dev/null +++ b/discord/history.go @@ -0,0 +1,90 @@ +package discord + +import ( + "fmt" + "strings" + "time" + + "github.com/bwmarrin/discordgo" + "go.jolheiser.com/beaver" + "go.jolheiser.com/gojang" + "go.jolheiser.com/gojang/rate" +) + +func init() { + cmd := command{ + validate: func(cmd commandInit) bool { + return true + }, + run: func(cmd commandInit) (string, error) { + if !memeRateLimit.Try() { + return "Woah, slow down!", nil + } + + args := strings.Fields(cmd.message.Content) + if len(args) < 2 { + return "You must give this command a Minecraft username", nil + } + + sendTyping(cmd.session, cmd.message.ChannelID) + + at := time.Now() + if len(args) > 2 { + t, err := time.Parse("01/02/2006", args[2]) + if err != nil { + return `Could not parse the time. Use the format 01/30/2006`, nil + } + at = t + } + + client := gojang.New(time.Second * 5) + profile, err := client.Profile(args[1], at) + if err != nil { + if gojang.IsPlayerNotFoundError(err) { + return "Could not find a player with that username at that time.", nil + } + if rate.IsRateLimitExceededError(err) { + return "Rate limited by Mojang, slow down!", nil + } + beaver.Errorf("Profile: %v", err) + return "Could not contact the Mojang API.", nil + } + + names, err := client.UUIDToNameHistory(profile.UUID) + if err != nil { + beaver.Errorf("UUIDToNameHistory: %v", err) + return "Could not contact the Mojang API.", nil + } + + var history string + for _, name := range names { + cleaned := strings.NewReplacer("_", "\\_", "*", "\\*").Replace(name.Name) + history += fmt.Sprintf("\n%s", cleaned) + if name.ChangedToAt == 0 { + history += " (original)" + } else { + history += fmt.Sprintf(" (%s)", name.ChangedToAtTime().Format("01/02/2006")) + } + } + + embed := &discordgo.MessageEmbed{ + Color: 0x7ed321, + Thumbnail: &discordgo.MessageEmbedThumbnail{ + URL: fmt.Sprintf("https://minotar.net/helm/%s/100.png", names.Current().Name), + }, + Fields: []*discordgo.MessageEmbedField{ + { + Name: fmt.Sprintf("%s's Name History", names.Current().Name), + Value: history, + }, + }, + } + sendEmbed(cmd.session, cmd.message.ChannelID, embed) + return "", nil + }, + help: "Minecraft name history", + } + + commands["history"] = cmd + commands["names"] = cmd +} diff --git a/discord/inspire.go b/discord/inspire.go index 0772bcd..d412128 100644 --- a/discord/inspire.go +++ b/discord/inspire.go @@ -8,6 +8,10 @@ func init() { return true }, run: func(cmd commandInit) (string, error) { + if !memeRateLimit.Try() { + return "Woah, slow down!", nil + } + sendTyping(cmd.session, cmd.message.ChannelID) img, err := inspiro.Generate() diff --git a/discord/insult.go b/discord/insult.go new file mode 100644 index 0000000..d541468 --- /dev/null +++ b/discord/insult.go @@ -0,0 +1,59 @@ +package discord + +import ( + "fmt" + r "math/rand" + "strings" + "time" +) + +func init() { + commands["insult"] = command{ + validate: func(cmd commandInit) bool { + return true + }, + run: func(cmd commandInit) (string, error) { + if !memeRateLimit.Try() { + return "Woah, slow down!", nil + } + + args, err := cmd.message.ContentWithMoreMentionsReplaced(cmd.session) + fields := strings.Fields(args) + + var target string + if len(fields) > 1 { + target = strings.Join(fields[1:], " ") + } else if cmd.message.Member.Nick != "" { + target = cmd.message.Member.Nick + } else { + target = cmd.message.Author.Username + } + + if err != nil { + return "", err + } + insult := fmt.Sprintf("%s, your %s looks like %s, you %s %s.", + target, + random(cmd.config.Insult.Targets), + random(cmd.config.Insult.Comparisons), + random(cmd.config.Insult.Adjectives), + random(cmd.config.Insult.Nouns), + ) + return insult, nil + }, + help: "Insult someone!", + } +} + +var rand = r.New(r.NewSource(time.Now().Unix())) + +func random(list []string) string { + size := len(list) + if size == 0 { + return "" + } else if size == 1 { + return list[0] + } + idx := rand.Intn(size) + return list[idx] +} diff --git a/discord/utils.go b/discord/utils.go new file mode 100644 index 0000000..c6582b2 --- /dev/null +++ b/discord/utils.go @@ -0,0 +1,26 @@ +package discord + +import ( + "time" +) + +type rateLimit struct { + rate time.Duration + next time.Time +} + +func NewRateLimit(rate time.Duration) *rateLimit { + return &rateLimit{ + rate: rate, + next: time.Now(), + } +} + +func (r *rateLimit) Try() bool { + now := time.Now() + if now.After(r.next) { + r.next = now.Add(r.rate) + return true + } + return false +} diff --git a/discord/utils_test.go b/discord/utils_test.go new file mode 100644 index 0000000..f0d2e04 --- /dev/null +++ b/discord/utils_test.go @@ -0,0 +1,43 @@ +package discord + +import ( + "testing" + "time" +) + +func TestRateLimit(t *testing.T) { + now := func() { + t.Logf("Time: %s", time.Now().Format("15:04:05")) + } + + now() + limit := NewRateLimit(time.Second * 2) + + if ok := limit.Try(); !ok { + now() + t.Log("First try: Rate limit should pass.") + t.FailNow() + } + + if ok := limit.Try(); ok { + now() + t.Log("Second try: Rate limit should fail.") + t.FailNow() + } + + time.Sleep(time.Second) + + if ok := limit.Try(); ok { + now() + t.Log("Third try: Rate limit should fail.") + t.FailNow() + } + + time.Sleep(time.Second) + + if ok := limit.Try(); !ok { + now() + t.Log("Fourth try: Rate limit should pass.") + t.FailNow() + } +} diff --git a/go.mod b/go.mod index efc7e41..75c4437 100644 --- a/go.mod +++ b/go.mod @@ -10,4 +10,5 @@ require ( go.etztech.xyz/go-mcm v1.3.0 go.etztech.xyz/inspiro v0.0.0-20200606185551-edfdf9da2359 go.jolheiser.com/beaver v1.0.2 + go.jolheiser.com/gojang v0.0.1 ) diff --git a/go.sum b/go.sum index 6c0dd8b..b849b9e 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,8 @@ go.etztech.xyz/inspiro v0.0.0-20200606185551-edfdf9da2359 h1:j/ZeoAj185wHfCSYD52 go.etztech.xyz/inspiro v0.0.0-20200606185551-edfdf9da2359/go.mod h1:+fC1WzJm/oS4UEgqr1jPouWerxBys52lTTDA94/5bf8= go.jolheiser.com/beaver v1.0.2 h1:KA2D6iO8MQhZi1nZYi/Chak/f1Cxfrs6b1XO623+Khk= go.jolheiser.com/beaver v1.0.2/go.mod h1:7X4F5+XOGSC3LejTShoBdqtRCnPWcnRgmYGmG3EKW8g= +go.jolheiser.com/gojang v0.0.1 h1:hK4ELqfY+FFNjf/juU0nszxV/fbdlNl1guyJRS3LETs= +go.jolheiser.com/gojang v0.0.1/go.mod h1:hUBULFDoampNM97E1IaYUhkLBJ30sb7iGsoFOdDU76I= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= diff --git a/sedbot.example.toml b/sedbot.example.toml index cc02ab7..ad890ba 100644 --- a/sedbot.example.toml +++ b/sedbot.example.toml @@ -25,6 +25,17 @@ registered_channel = "0" # staff_roles are for staff commands staff_roles = [] +# meme_rate is the rate limit for memes +meme_rate = "0" + +# insults +# , your looks like , you +[insult] +targets = [] +comparisons = [] +adjectives = [] +nouns = [] + # echoes are any basic command -> message [[echoes]] name = "discord"