Compare commits

...

4 Commits

Author SHA1 Message Date
b7f2ad6dcd
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 <mail@georg-pfuetzenreuter.net>
2024-09-22 17:46:37 +02:00
9e9347ade7
Implement automatic channel joining
Avoid the need for an administrator to join the bot to channels by
implementing a configuration option allowing the passing of channels
the bot should always join to by itself upon startup.

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
2024-09-21 18:46:51 +02:00
1f41d9ac17
Replace redundant logic in Admin()
The Allowed() function already implements this loop, use it to reduce
redundant code.

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
2024-09-21 17:49:39 +02:00
22b23c755e
Implement configuration file
Abstract settings which commonly differ between instances to a YAML
based configuration file to allow for easy administration without the
need for modifying the source code.

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
2024-09-21 16:06:39 +02:00
5 changed files with 230 additions and 33 deletions

26
config.example.yaml Normal file
View File

@ -0,0 +1,26 @@
watbot:
server:
host: irc.casa # mandatory, no default
port: 6697
tls_verify: true
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
ignores: # optional, no default
hosts:
- annoying.example.com
channels: # optional, no default
join:
- crantest # channels without a prefix character will be prefixed with "#"
permitted:
- '#lucy'

5
go.mod
View File

@ -3,8 +3,11 @@ module git.circuitco.de/self/watbot
go 1.15 go 1.15
require ( require (
github.com/creasty/defaults v1.8.0
github.com/go-irc/irc v2.1.0+incompatible github.com/go-irc/irc v2.1.0+incompatible
github.com/namsral/flag v1.7.4-pre github.com/stretchr/testify v1.9.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/sqlite v1.1.4 gorm.io/driver/sqlite v1.1.4
gorm.io/gorm v1.20.11 gorm.io/gorm v1.20.11
) )

26
go.sum
View File

@ -1,4 +1,8 @@
github.com/go-irc/irc v1.3.0 h1:IMD+d/+EzY51ecMLOz73r/NXTZrEp8khrePxRCvX71M= github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk=
github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-irc/irc v2.1.0+incompatible h1:pg7pMVq5OYQbqTxceByD/EN8VIsba7DtKn49rsCnG8Y= github.com/go-irc/irc v2.1.0+incompatible h1:pg7pMVq5OYQbqTxceByD/EN8VIsba7DtKn49rsCnG8Y=
github.com/go-irc/irc v2.1.0+incompatible/go.mod h1:jJILTRy8s/qOvusiKifAEfhQMVwft1ZwQaVJnnzmyX4= github.com/go-irc/irc v2.1.0+incompatible/go.mod h1:jJILTRy8s/qOvusiKifAEfhQMVwft1ZwQaVJnnzmyX4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@ -7,8 +11,24 @@ github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ= github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/namsral/flag v1.7.4-pre h1:b2ScHhoCUkbsq0d2C15Mv+VU8bl8hAXV8arnWiOHNZs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/namsral/flag v1.7.4-pre/go.mod h1:OXldTctbM6SWH1K899kPZcf65KxJiD7MsceFUpB5yDo= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM= gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=

123
main.go
View File

@ -1,40 +1,119 @@
package main package main
import "fmt" import (
import "crypto/tls" "crypto/tls"
"errors"
"fmt"
"log"
"os"
import "github.com/go-irc/irc" "flag"
import "github.com/namsral/flag" "github.com/go-irc/irc"
import "git.circuitco.de/self/watbot/wat" "git.circuitco.de/self/watbot/wat"
"github.com/creasty/defaults"
"gopkg.in/yaml.v3"
)
type Config struct {
Watbot watConfig `yaml:"watbot"`
}
type watConfig struct {
Nick string `yaml:"nick"`
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"`
Channels struct {
Join []string `yaml:"join"`
Permitted []string `yaml:"permitted"`
} `yaml:"channels"`
Ignores struct {
Hosts []string `yaml:"hosts"`
} `yaml:"ignores"`
Server struct {
Host string `yaml:"host"`
Port int `default:"6697" yaml:"port"`
TlsVerify bool `default:"true" yaml:"tls_verify"`
} `yaml:"server"`
}
func readConfig(configPath string) (*watConfig, error) {
allConfig := Config{}
buffer, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("Could not read configuration file: %s", err)
}
err = yaml.Unmarshal(buffer, &allConfig)
if err != nil {
return nil, fmt.Errorf("Could not parse configuration file: %s", err)
}
config := &allConfig.Watbot
defaults.Set(config)
if config.Server.Host == "" {
return nil, errors.New("Shall I play wattery to know where to connect to?")
}
if config.Name != "" && config.Nick == "" {
config.Nick = config.Name
}
if config.Nick != "" && config.User == "" {
config.User = config.Nick
}
if config.Name == "" || config.Nick == "" || config.User == "" {
return nil, errors.New("Don't know who I am.")
}
return config, nil
}
func main() { func main() {
pass := flag.String("pass", "", "password") var configPathArg string
flag.StringVar(&configPathArg, "config", "config.yaml", "Path to configuration file")
flag.Parse() flag.Parse()
fmt.Printf("PASS len %d\n", len(*pass)) log.Println("Starting with configuration:", configPathArg)
config := irc.ClientConfig{
Nick: "watt", config, err := readConfig(configPathArg)
Pass: *pass, if err != nil {
User: "wat", log.Fatalln(err)
Name: "wat", os.Exit(1)
}
ircConfig := irc.ClientConfig{
Nick: config.Nick,
Pass: config.Pass,
User: config.User,
Name: config.Name,
} }
watConfig := wat.WatConfig{ watConfig := wat.WatConfig{
PermittedChannels: []string{ AutoJoinChannels: config.Channels.Join,
"#lucy", PermittedChannels: config.Channels.Permitted,
"#sweden", IgnoredHosts: config.Ignores.Hosts,
}, AdminHosts: config.Admins.Hosts,
IgnoredHosts: []string{ BotHosts: config.Bots.Hosts,
"tripsit/user/creatonez", BotGames: config.Bots.Games,
},
} }
tcpConf := &tls.Config{ tcpConf := &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: !config.Server.TlsVerify,
} }
conn, err := tls.Dial("tcp", "127.0.0.1:6697", tcpConf) conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port), tcpConf)
if err != nil { if err != nil {
fmt.Println("err " + err.Error()) fmt.Println("err " + err.Error())
return return
} }
wwat := wat.NewWatBot(&config, &watConfig, conn) wwat := wat.NewWatBot(&ircConfig, &watConfig, conn)
wwat.Run() wwat.Run()
} }

View File

@ -3,6 +3,7 @@ package wat
import ( import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"strconv"
"strings" "strings"
"github.com/go-irc/irc" "github.com/go-irc/irc"
@ -17,9 +18,20 @@ type WatBot struct {
Nick string Nick string
} }
type BotGame struct {
Games []string
Name string
}
type BotGameConfig map[string][]string
type WatConfig struct { type WatConfig struct {
PermittedChannels []string BotHosts []string
BotGames BotGameConfig
AdminHosts []string
IgnoredHosts []string IgnoredHosts []string
AutoJoinChannels []string
PermittedChannels []string
} }
func NewWatBot(config *irc.ClientConfig, watConfig *WatConfig, serverConn *tls.Conn) *WatBot { func NewWatBot(config *irc.ClientConfig, watConfig *WatConfig, serverConn *tls.Conn) *WatBot {
@ -35,23 +47,43 @@ func CleanNick(nick string) string {
return string(nick[0]) + "\u200c" + nick[1:] return string(nick[0]) + "\u200c" + nick[1:]
} }
func PrefixChannel(channel string) string {
// there could theoretically be other channel prefixes ..
if channel[0] != '#' && channel[0] != '!' {
channel = "#" + channel
}
return channel
}
func (w *WatBot) HandleIrcMsg(c *irc.Client, m *irc.Message) { func (w *WatBot) HandleIrcMsg(c *irc.Client, m *irc.Message) {
switch cmd := m.Command; cmd { switch cmd := m.Command; cmd {
case "PING": case "PING":
w.write("PONG", m.Params[0]) w.write("PONG", m.Params[0])
case "PRIVMSG": case "PRIVMSG":
w.Msg(m) w.Msg(m)
case "001":
for _, channel := range w.c.AutoJoinChannels {
w.write("JOIN", PrefixChannel(channel))
}
} }
} }
func (w *WatBot) Admin(m *irc.Message) bool { func (w *WatBot) Admin(m *irc.Message) bool {
admins := [2]string{"mph.monster", "cranberry.juice"} return w.Allowed(m.Prefix.Host, w.c.AdminHosts)
for _, admin := range admins { }
if m.Prefix.Host == admin {
return true 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 false return isBot, games
} }
func (w *WatBot) Allowed(c string, r []string) bool { func (w *WatBot) Allowed(c string, r []string) bool {
@ -73,7 +105,7 @@ func (w *WatBot) CanRespond(m *irc.Message) bool {
// if !strings.Contains(m.Prefix.Host, "") { // if !strings.Contains(m.Prefix.Host, "") {
// return false // return false
// } // }
if !w.Allowed(m.Params[0], w.c.PermittedChannels) { if !w.Allowed(PrefixChannel(m.Params[0]), w.c.PermittedChannels) {
return false return false
} }
return true return true
@ -115,6 +147,43 @@ func (w *WatBot) Msg(m *irc.Message) {
args = args[1:] args = args[1:]
} }
// integration with games in other bots
isBot, games := w.Bot(m)
if isBot {
// Jeopardy
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 // check if command char (or something weird) is present
if args[0] != "wat" && args[0][0] != '#' { if args[0] != "wat" && args[0][0] != '#' {
return return