From 97e9d7d0c2312385bfd8dcbb85a1bc4bd7d263cc Mon Sep 17 00:00:00 2001 From: Georg Pfuetzenreuter Date: Sun, 22 Sep 2024 17:42:49 +0200 Subject: [PATCH 1/3] Implement Jeopardy cashout This adds integration between Watbot and the Limnoria Jeopardy plugin. If a game of Jeopardy ends, Watbot will parse the finishers message and pay a small share of the Jeopardy price money in the form of Watcoins. To avoid abuse, only Jeopardy finishing messages from authorized bots are considered. An IRC user is considered an authorized bot if the hostmask matches one of the configured bot hostmasks, and if the nickname is configured for "jeopardy" in the newly introduced bot games configuration. Signed-off-by: Georg Pfuetzenreuter Add sample message to Jeopardy logic Signed-off-by: Georg Pfuetzenreuter --- config.example.yaml | 6 +++++ main.go | 6 +++++ wat/bot.go | 62 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/config.example.yaml b/config.example.yaml index 5d3fbac..02dc9dd 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -6,6 +6,12 @@ watbot: name: watest nick: watest # nick is name by default user: watest # user is nick by default + bots: # optional, no default + games: # mapping of bot names to games + katyusha: + - jeopardy # currently jeopardy is the only integrated game + hosts: # hostmasks considered as valid bots + - bot.example.com admins: # optional, no default hosts: - admin.example.com diff --git a/main.go b/main.go index c242571..fb71444 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,10 @@ type watConfig struct { Pass string `yaml:"pass"` User string `yaml:"user"` Name string `yaml:"name"` + Bots struct { + Hosts []string `yaml:"hosts"` + Games wat.BotGameConfig `yaml:"games"` + } `yaml:"bots"` Admins struct { Hosts []string `yaml:"hosts"` } `yaml:"admins"` @@ -99,6 +103,8 @@ func main() { PermittedChannels: config.Channels.Permitted, IgnoredHosts: config.Ignores.Hosts, AdminHosts: config.Admins.Hosts, + BotHosts: config.Bots.Hosts, + BotGames: config.Bots.Games, } tcpConf := &tls.Config{ InsecureSkipVerify: !config.Server.TlsVerify, diff --git a/wat/bot.go b/wat/bot.go index a555de4..b361137 100644 --- a/wat/bot.go +++ b/wat/bot.go @@ -3,6 +3,7 @@ package wat import ( "crypto/tls" "fmt" + "strconv" "strings" "github.com/go-irc/irc" @@ -17,7 +18,16 @@ type WatBot struct { Nick string } +type BotGame struct { + Games []string + Name string +} + +type BotGameConfig map[string][]string + type WatConfig struct { + BotHosts []string + BotGames BotGameConfig AdminHosts []string IgnoredHosts []string AutoJoinChannels []string @@ -62,6 +72,20 @@ func (w *WatBot) Admin(m *irc.Message) bool { return w.Allowed(m.Prefix.Host, w.c.AdminHosts) } +func (w *WatBot) Bot(m *irc.Message) (bool, []string) { + isBot := w.Allowed(m.Prefix.Host, w.c.BotHosts) + var games []string + if isBot { + for b, g := range w.c.BotGames { + if b == m.Prefix.Name { + games = g + break + } + } + } + return isBot, games +} + func (w *WatBot) Allowed(c string, r []string) bool { for _, allowed := range r { if c == allowed { @@ -123,6 +147,44 @@ func (w *WatBot) Msg(m *irc.Message) { args = args[1:] } + // integration with games in other bots + isBot, games := w.Bot(m) + if isBot { + // Jeopardy + // parses a message "Top finishers: (nick1: 1300) (nick2: 1200)" from an authorized Jeopardy game bot + if args[0] == "Top" && args[1] == "finishers:" && w.Allowed("jeopardy", games) { + // hey, I avoided regex! + finisherPrizes := strings.Split(strings.Replace(strings.Replace(strings.Replace(strings.Replace(strings.Join(args[2:], " "), ") (", ";", -1), ": ", ":", -1), "(", "", 1), ")", "", 1), ";") + fmt.Printf("Processing Jeopardy: %s\n", finisherPrizes) + for _, pair := range finisherPrizes { + nameCoinPair := strings.Split(pair, ":") + coins, err := strconv.ParseUint(nameCoinPair[1], 10, 64) + if err != nil { + fmt.Printf("Invalid coins, cannot process pair for cashout: %s.\n", nameCoinPair) + continue + } + name := nameCoinPair[0] + // Jeopardy prizes are quite a lot of $$$, make it a bit more sane + coins = coins / 40 + // name = we assume the Jeopardy player name to match a Watbot player name + // host = we could use some WHO logic to find the host, but assuming nickname lookup to be sufficient here + // create = based on the above, maybe rather not create Watbot players based on only a nick? + // but it expects someone to have played with Watbot before to be eligible for Jeopardy cashout .. + player := w.Db.User(name, "", false) + if player.Nick == "" { + fmt.Printf("Player %s does not exist in Watbot, skipping cashout.\n", name) + continue + } else { + w.reply(m, fmt.Sprintf("smartass %s, gave u %d :)", player.Nick, coins)) + player.Coins += coins + w.Db.Update(player) + } + } + + return + } + } + // check if command char (or something weird) is present if args[0] != "wat" && args[0][0] != '#' { return From d06e724f067deb55b8009b62be8e300fe79d1961 Mon Sep 17 00:00:00 2001 From: Georg Pfuetzenreuter Date: Sun, 29 Sep 2024 14:35:25 +0200 Subject: [PATCH 2/3] Refactor integrations Move to a separate file for better code structure and to avoid huge branching inside Msg(). Signed-off-by: Georg Pfuetzenreuter --- wat/bot.go | 61 +++------------------------------- wat/integration.go | 83 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 57 deletions(-) create mode 100644 wat/integration.go diff --git a/wat/bot.go b/wat/bot.go index b361137..41c030f 100644 --- a/wat/bot.go +++ b/wat/bot.go @@ -3,7 +3,6 @@ package wat import ( "crypto/tls" "fmt" - "strconv" "strings" "github.com/go-irc/irc" @@ -14,17 +13,11 @@ type WatBot struct { conn *tls.Conn c *WatConfig game *WatGame + integration *WatIntegration Db *WatDb Nick string } -type BotGame struct { - Games []string - Name string -} - -type BotGameConfig map[string][]string - type WatConfig struct { BotHosts []string BotGames BotGameConfig @@ -38,6 +31,7 @@ func NewWatBot(config *irc.ClientConfig, watConfig *WatConfig, serverConn *tls.C wat := WatBot{conn: serverConn, Nick: config.Nick, c: watConfig} wat.Db = NewWatDb() wat.game = NewWatGame(&wat, wat.Db) + wat.integration = NewWatIntegration(&wat, wat.Db, &WatIntegrationConfig{BotHosts: watConfig.BotHosts, BotGames: watConfig.BotGames}) config.Handler = irc.HandlerFunc(wat.HandleIrcMsg) wat.client = irc.NewClient(wat.conn, *config) return &wat @@ -72,20 +66,6 @@ func (w *WatBot) Admin(m *irc.Message) bool { return w.Allowed(m.Prefix.Host, w.c.AdminHosts) } -func (w *WatBot) Bot(m *irc.Message) (bool, []string) { - isBot := w.Allowed(m.Prefix.Host, w.c.BotHosts) - var games []string - if isBot { - for b, g := range w.c.BotGames { - if b == m.Prefix.Name { - games = g - break - } - } - } - return isBot, games -} - func (w *WatBot) Allowed(c string, r []string) bool { for _, allowed := range r { if c == allowed { @@ -148,41 +128,8 @@ func (w *WatBot) Msg(m *irc.Message) { } // integration with games in other bots - isBot, games := w.Bot(m) - if isBot { - // Jeopardy - // parses a message "Top finishers: (nick1: 1300) (nick2: 1200)" from an authorized Jeopardy game bot - if args[0] == "Top" && args[1] == "finishers:" && w.Allowed("jeopardy", games) { - // hey, I avoided regex! - finisherPrizes := strings.Split(strings.Replace(strings.Replace(strings.Replace(strings.Replace(strings.Join(args[2:], " "), ") (", ";", -1), ": ", ":", -1), "(", "", 1), ")", "", 1), ";") - fmt.Printf("Processing Jeopardy: %s\n", finisherPrizes) - for _, pair := range finisherPrizes { - nameCoinPair := strings.Split(pair, ":") - coins, err := strconv.ParseUint(nameCoinPair[1], 10, 64) - if err != nil { - fmt.Printf("Invalid coins, cannot process pair for cashout: %s.\n", nameCoinPair) - continue - } - name := nameCoinPair[0] - // Jeopardy prizes are quite a lot of $$$, make it a bit more sane - coins = coins / 40 - // name = we assume the Jeopardy player name to match a Watbot player name - // host = we could use some WHO logic to find the host, but assuming nickname lookup to be sufficient here - // create = based on the above, maybe rather not create Watbot players based on only a nick? - // but it expects someone to have played with Watbot before to be eligible for Jeopardy cashout .. - player := w.Db.User(name, "", false) - if player.Nick == "" { - fmt.Printf("Player %s does not exist in Watbot, skipping cashout.\n", name) - continue - } else { - w.reply(m, fmt.Sprintf("smartass %s, gave u %d :)", player.Nick, coins)) - player.Coins += coins - w.Db.Update(player) - } - } - - return - } + if w.integration.HandleIntegration(m, args) { + return } // check if command char (or something weird) is present diff --git a/wat/integration.go b/wat/integration.go new file mode 100644 index 0000000..833b9a3 --- /dev/null +++ b/wat/integration.go @@ -0,0 +1,83 @@ +package wat + +import ( + "fmt" + "strconv" + "strings" + + "github.com/go-irc/irc" +) + +type BotGameConfig map[string][]string + +type WatIntegrationConfig struct { + BotHosts []string + BotGames BotGameConfig +} + +type WatIntegration struct { + bot *WatBot + db *WatDb + c *WatIntegrationConfig +} + +func NewWatIntegration(bot *WatBot, db *WatDb, c *WatIntegrationConfig) *WatIntegration { + return &WatIntegration{bot, db, c} +} + +func (w *WatIntegration) Bot(m *irc.Message) (bool, []string) { + isBot := w.bot.Allowed(m.Prefix.Host, w.c.BotHosts) + var games []string + if isBot { + for b, g := range w.c.BotGames { + if b == m.Prefix.Name { + games = g + break + } + } + } + return isBot, games +} + +func (w *WatIntegration) HandleIntegration(m *irc.Message, msgargs []string) bool { + isBot, games := w.Bot(m) + if isBot { + // handles a message "Top finishers: (nick1: 1300) (nick2: 1200)" from an authorized Jeopardy game bot + if msgargs[0] == "Top" && msgargs[1] == "finishers:" && w.bot.Allowed("jeopardy", games) { + w.Jeopardy(m, msgargs) + return true + } + } + // not an authorized bot or no integration matched the given message + return false +} + +func (w *WatIntegration) Jeopardy(m *irc.Message, msgargs []string) { + // hey, I avoided regex! + finisherPrizes := strings.Split(strings.Replace(strings.Replace(strings.Replace(strings.Replace(strings.Join(msgargs[2:], " "), ") (", ";", -1), ": ", ":", -1), "(", "", 1), ")", "", 1), ";") + fmt.Printf("Processing Jeopardy: %s\n", finisherPrizes) + for _, pair := range finisherPrizes { + nameCoinPair := strings.Split(pair, ":") + coins, err := strconv.ParseUint(nameCoinPair[1], 10, 64) + if err != nil { + fmt.Printf("Invalid coins, cannot process pair for cashout: %s.\n", nameCoinPair) + continue + } + name := nameCoinPair[0] + // Jeopardy prizes are quite a lot of $$$, make it a bit more sane + coins = coins / 40 + // name = we assume the Jeopardy player name to match a Watbot player name + // host = we could use some WHO logic to find the host, but assuming nickname lookup to be sufficient here + // create = based on the above, maybe rather not create Watbot players based on only a nick? + // but it expects someone to have played with Watbot before to be eligible for Jeopardy cashout .. + player := w.db.User(name, "", false) + if player.Nick == "" { + fmt.Printf("Player %s does not exist in Watbot, skipping cashout.\n", name) + continue + } else { + w.bot.reply(m, fmt.Sprintf("smartass %s, gave u %d :)", player.Nick, coins)) + player.Coins += coins + w.db.Update(player) + } + } +} From 89ed59a9c71e4b21e462a5c7746a8decc41dc487 Mon Sep 17 00:00:00 2001 From: Georg Pfuetzenreuter Date: Tue, 1 Oct 2024 20:55:33 +0200 Subject: [PATCH 3/3] Explain Jeopardy finishers parsing Elaborate as the convoluted logic can be difficult to understand. Signed-off-by: Georg Pfuetzenreuter --- wat/integration.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/wat/integration.go b/wat/integration.go index 833b9a3..b4ff8e9 100644 --- a/wat/integration.go +++ b/wat/integration.go @@ -54,9 +54,20 @@ func (w *WatIntegration) HandleIntegration(m *irc.Message, msgargs []string) boo func (w *WatIntegration) Jeopardy(m *irc.Message, msgargs []string) { // hey, I avoided regex! + // 1. Starts parsing an array of message arguments containing "Top finishers: (nick1: 1000) (nick2: 2000)", where + // the "($nick: $value)" pairs can contain arbitrary nicknames + integer values and can repeat one to any amount of times + // 2. Join the array on spaces to a string, but skip the first two elements to remove "Top" and "finishers:" + // 3. Replace ") (" in the string with ";" - the semicolon is chosen as a temporary delimiter because it does not conflict with any other characters in the message + // 4. Replace ": " in the string with ":" + // 5. Replace "(" in the string with "" (relevant for the first nick/value pair) + // 6. Replace ")" in the string with "" (relevant for the last nick/value pair) + // 7. Now, we have a string like "nick1:1000;nick2:2000" - split it back into an array on ";" + // 8. The result is an array like "[nick1:1000, nick2:2000]" finisherPrizes := strings.Split(strings.Replace(strings.Replace(strings.Replace(strings.Replace(strings.Join(msgargs[2:], " "), ") (", ";", -1), ": ", ":", -1), "(", "", 1), ")", "", 1), ";") fmt.Printf("Processing Jeopardy: %s\n", finisherPrizes) + // iterate over the "$nick:$value" string elements for _, pair := range finisherPrizes { + // turn the string element into an array, where the first entry is the nickname, and the second the value nameCoinPair := strings.Split(pair, ":") coins, err := strconv.ParseUint(nameCoinPair[1], 10, 64) if err != nil {