Compare commits

...

26 Commits

Author SHA1 Message Date
67ef6fec1b Merge pull request 'Prevent dice overflow' (#27) from overflow into master
Reviewed-on: #27
2024-10-12 20:05:22 +02:00
930839ab58
Prevent dice overflow
rand.Int() would panic when the max value is <= 0, which happens when
big.NewInt() was fed with a too large number. Avoid this by validating
the big.NewInt() return beforehand. Add error handling to all callers to
both gracefully return to IRC and to log an error message.
Rename the shadowed "max" variable whilst at it to avoid confusion.

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
2024-10-10 18:43:46 +02:00
91b0e21b7a Merge pull request 'Configurable database path' (#26) from config into master
Reviewed-on: #26
2024-10-10 16:56:19 +02:00
dfe7deff72
Configurable database path
Allow the database file to reside in a user defined location instead of
requiring it to be in the working directory.

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
2024-10-10 00:04:16 +02:00
e300f71370 Merge pull request 'Lower nickname in Jeopardy cashout' (#25) from jeopardylower into master
Reviewed-on: #25
2024-10-10 00:00:50 +02:00
5ce4d4bfe4
Lower nickname in Jeopardy cashout
Align with GetTarget() which is used in games to have consistent
handling of nicknames regardless of their casing.

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
2024-10-09 23:36:45 +02:00
c4ff70da70 Merge pull request 'Improve Jeopardy cashout message' (#24) from jeopardymsg into master
Reviewed-on: #24
2024-10-03 19:55:50 +02:00
7ec49a9769
Improve Jeopardy cashout message
Print only a single message instead of one per winner to reduce chat
clutter.
Skip cashout to users who won less than the possible cashout value as
limited by the division value to avoid congratulating someone who only
gets 0.
Abort should a regression cause the logic to process an empty set of
finishers to prevent unexpected behavior.

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
2024-10-03 19:43:39 +02:00
b6187e0077 Merge pull request 'Change host lookup to be optional' (#23) from dbhost into master
Reviewed-on: #23
2024-10-03 18:00:59 +02:00
accf16c92a
Change host lookup to be optional
Some parts of the logic do not make use of the host column in the
players table, allow the field to be empty and do not query for an empty
value (which might return bogus entries) if no value is passed to the
lookup function.
This additionally avoids the need for the hardcoded initial host when
configuring the bot player.

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
2024-10-02 22:30:06 +02:00
394d6ca2fa Merge pull request 'Strip formatting codes' (#19) from format into master
Reviewed-on: #19
2024-10-02 21:44:49 +02:00
d99a0d84bf
Strip formatting codes
To allow for correct parsing of messages containing formatting codes and
to avoid exploitation of unparseable messages, strip all formatting
codes from a message when entering the parsing chain.

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
2024-10-02 21:40:30 +02:00
a5cc5e0317 Merge pull request 'Implement Jeopardy cashout' (#18) from jeopardy into master
Reviewed-on: #18
2024-10-02 20:07:10 +02:00
89ed59a9c7
Explain Jeopardy finishers parsing
Elaborate as the convoluted logic can be difficult to understand.

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
2024-10-02 20:03:56 +02:00
d06e724f06
Refactor integrations
Move to a separate file for better code structure and to avoid huge
branching inside Msg().

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
2024-10-02 20:03:56 +02:00
97e9d7d0c2
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>

Add sample message to Jeopardy logic

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
2024-09-29 14:36:23 +02:00
a475bc2f42 Merge pull request 'Implement configuration file + automatic channel joining' (#14) from config into master
Reviewed-on: #14
2024-09-28 19:43:10 +02:00
0a90b6e483
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-28 19:33:18 +02:00
cc322e87e6
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-28 19:33:18 +02:00
bde9a8defb
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-28 19:33:17 +02:00
f4a9607770 Merge pull request 'Remove count script' (#16) from count into master
Reviewed-on: #16
2024-09-24 23:12:50 +02:00
dbaf6f046a
Remove count script
Unable to find a purpose.

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
2024-09-24 21:59:25 +02:00
331d38694e Merge pull request 'Remove and ignore binary' (#15) from gitignore into master
Reviewed-on: #15
2024-09-24 21:13:01 +02:00
bd7d3abf24
Remove and ignore binary
Build artifacts shouldn't be tracked in version control.

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
2024-09-21 16:22:04 +02:00
be1b15b6f9 Merge pull request 'Add myself as admin' (#12) from admin into master
Reviewed-on: #12
2024-09-21 07:23:34 +02:00
84696672cd
Add myself as admin
For some administrative operations, such as joining channels, it would
be useful if I could "imp"ersonate the bot.

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
2024-09-21 01:09:28 +02:00
12 changed files with 437 additions and 58 deletions

3
.gitignore vendored
View File

@ -8,6 +8,9 @@
*.so *.so
*.dylib *.dylib
# Normal binary, `go build`
watbot
# Test binary, build with `go test -c` # Test binary, build with `go test -c`
*.test *.test

27
config.example.yaml Normal file
View File

@ -0,0 +1,27 @@
watbot:
database: wat.db # wat.db (in the working directory) is the default
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'

2
count
View File

@ -1,2 +0,0 @@
wc -l osrc/watbot_*.py
wc -l *.go wat/*.go

6
go.mod
View File

@ -3,8 +3,12 @@ module git.circuitco.de/self/watbot
go 1.15 go 1.15
require ( require (
github.com/creasty/defaults v1.8.0
github.com/ergochat/irc-go v0.4.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
) )

28
go.sum
View File

@ -1,4 +1,10 @@
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/ergochat/irc-go v0.4.0 h1:0YibCKfAAtwxQdNjLQd9xpIEPisLcJ45f8FNsMHAuZc=
github.com/ergochat/irc-go v0.4.0/go.mod h1:2vi7KNpIPWnReB5hmLpl92eMywQvuIeIIGdt/FQCph0=
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 +13,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=

125
main.go
View File

@ -1,40 +1,121 @@
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 {
Database string `default:"wat.db" yaml:"database"`
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{ DatabasePath: config.Database,
"#lucy", AutoJoinChannels: config.Channels.Join,
"#sweden", PermittedChannels: config.Channels.Permitted,
}, IgnoredHosts: config.Ignores.Hosts,
IgnoredHosts: []string{ AdminHosts: config.Admins.Hosts,
"tripsit/user/creatonez", BotHosts: config.Bots.Hosts,
}, 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

@ -6,6 +6,7 @@ import (
"strings" "strings"
"github.com/go-irc/irc" "github.com/go-irc/irc"
"github.com/ergochat/irc-go/ircfmt"
) )
type WatBot struct { type WatBot struct {
@ -13,19 +14,26 @@ type WatBot struct {
conn *tls.Conn conn *tls.Conn
c *WatConfig c *WatConfig
game *WatGame game *WatGame
integration *WatIntegration
Db *WatDb Db *WatDb
Nick string Nick string
} }
type WatConfig struct { type WatConfig struct {
PermittedChannels []string DatabasePath 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 {
wat := WatBot{conn: serverConn, Nick: config.Nick, c: watConfig} wat := WatBot{conn: serverConn, Nick: config.Nick, c: watConfig}
wat.Db = NewWatDb() wat.Db = NewWatDb(watConfig.DatabasePath)
wat.game = NewWatGame(&wat, wat.Db) 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) config.Handler = irc.HandlerFunc(wat.HandleIrcMsg)
wat.client = irc.NewClient(wat.conn, *config) wat.client = irc.NewClient(wat.conn, *config)
return &wat return &wat
@ -35,17 +43,29 @@ 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 {
return m.Prefix.Host == "mph.monster" return w.Allowed(m.Prefix.Host, w.c.AdminHosts)
} }
func (w *WatBot) Allowed(c string, r []string) bool { func (w *WatBot) Allowed(c string, r []string) bool {
@ -67,7 +87,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
@ -86,7 +106,7 @@ func (w *WatBot) Msg(m *irc.Message) {
} }
// fieldsfunc allows you to obtain rune separated fields/args // fieldsfunc allows you to obtain rune separated fields/args
args := strings.FieldsFunc(m.Params[1], func(c rune) bool { return c == ' ' }) args := strings.FieldsFunc(ircfmt.Strip(m.Params[1]), func(c rune) bool { return c == ' ' })
if len(args) == 0 { if len(args) == 0 {
return return
@ -109,6 +129,11 @@ func (w *WatBot) Msg(m *irc.Message) {
args = args[1:] args = args[1:]
} }
// integration with games in other bots
if w.integration.HandleIntegration(m, args) {
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

View File

@ -52,10 +52,10 @@ type WatDb struct {
db *gorm.DB db *gorm.DB
} }
func NewWatDb() *WatDb { func NewWatDb(dbpath string) *WatDb {
w := WatDb{} w := WatDb{}
var err error var err error
w.db, err = gorm.Open(sqlite.Open("wat.db"), &gorm.Config{}) w.db, err = gorm.Open(sqlite.Open(dbpath), &gorm.Config{})
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -65,8 +65,12 @@ func NewWatDb() *WatDb {
func (w *WatDb) User(nick, host string, create bool) Player { func (w *WatDb) User(nick, host string, create bool) Player {
var player Player var player Player
query := "nick = ?"
if host != "" {
query = query + " or host = ?"
}
// Try and get a user // Try and get a user
if err := w.db.First(&player, "nick = ? or host = ?", nick, host).Error; err != nil && create { if err := w.db.First(&player, query, nick, host).Error; err != nil && create {
fmt.Printf("Creating user: %s\n", err.Error()) fmt.Printf("Creating user: %s\n", err.Error())
// No user, make another // No user, make another
player.Nick = nick player.Nick = nick

View File

@ -28,7 +28,7 @@ var unconscious = "wat, your hands fumble and fail you. try resting, weakling."
func NewWatGame(bot *WatBot, db *WatDb) *WatGame { func NewWatGame(bot *WatBot, db *WatDb) *WatGame {
g := WatGame{bot, db, Player{}, nil, nil, nil, nil, map[string]int{}} g := WatGame{bot, db, Player{}, nil, nil, nil, nil, map[string]int{}}
g.me = g.db.User(bot.Nick, "amia8t89xfp8y.liberta.casa", true) g.me = g.db.User(bot.Nick, "", true)
g.commands = map[string](func(*Player, []string) string){ g.commands = map[string](func(*Player, []string) string){
//"wat": g.megaWat, //"wat": g.megaWat,
"steroid": g.Steroid, "steroid": g.Steroid,
@ -121,9 +121,17 @@ func (g *WatGame) help() string {
return ret return ret
} }
func (g *WatGame) RandInt(max int64) uint64 { func (g *WatGame) RandInt(maxx int64) (uint64, error) {
i, _ := rand.Int(rand.Reader, big.NewInt(max)) bi := big.NewInt(maxx)
return i.Uint64() // prevent panic of rand.Int on big numbers
if bi.BitLen() < 2 {
return 0, fmt.Errorf("overflow, got %d", bi)
}
i, err := rand.Int(rand.Reader, bi)
if err != nil {
return 0, err
}
return i.Uint64(), nil
} }
func (g *WatGame) Heal(player *Player, fields []string) string { func (g *WatGame) Heal(player *Player, fields []string) string {
@ -166,8 +174,11 @@ func (g *WatGame) Dice(player *Player, fields []string) string {
roll = i roll = i
} }
} }
answer := g.RandInt(int64(roll)) + 1 answer, err := g.RandInt(int64(roll))
return fmt.Sprintf("1d%d - %d", roll, answer) if e := handleError(err); e != "" {
return e
}
return fmt.Sprintf("1d%d - %d", roll + 1, answer)
} }
type PositiveError struct{} type PositiveError struct{}
@ -206,12 +217,20 @@ func (g *WatGame) Roll(player *Player, fields []string) string {
} }
lotteryNum := int64(-1) lotteryNum := int64(-1)
if dieSize > 100 { if dieSize > 100 {
lotteryNum = int64(g.RandInt(dieSize)) + 1 lotteryNumRand, randErr := g.RandInt(dieSize)
if e := handleError(randErr); e != "" {
return e
}
lotteryNum = int64(lotteryNumRand) + 1
} }
if amount > player.Coins { if amount > player.Coins {
return "wat? brokeass" return "wat? brokeass"
} }
n := int64(g.RandInt(dieSize)) + 1 nRand, randErr := g.RandInt(dieSize)
if e := handleError(randErr); e != "" {
return e
}
n := int64(nRand) + 1
ret := fmt.Sprintf("%s rolls the %d sided die... %d! ", player.Nick, dieSize, n) ret := fmt.Sprintf("%s rolls the %d sided die... %d! ", player.Nick, dieSize, n)
if n == lotteryNum { if n == lotteryNum {
player.Coins += player.Coins player.Coins += player.Coins
@ -255,11 +274,18 @@ func (g *WatGame) Punch(player *Player, fields []string) string {
if !target.Conscious() { if !target.Conscious() {
return "wat? you're punching someone who is already unconscious. u crazy?" return "wat? you're punching someone who is already unconscious. u crazy?"
} }
chance := g.RandInt(6) + 1 chance, randErr := g.RandInt(6)
dmg := g.RandInt(6) + 1 if e := handleError(randErr); e != "" {
return e
}
dmg, randErr := g.RandInt(6)
if e := handleError(randErr); e != "" {
return e
}
dmg = dmg + 1
ret := fmt.Sprintf("%s rolls a d6... %s ", player.Nick, player.Nick) ret := fmt.Sprintf("%s rolls a d6... %s ", player.Nick, player.Nick)
dmg += uint64(player.Level(player.Anarchy)) dmg += uint64(player.Level(player.Anarchy))
if chance > 3 { if chance + 1 > 3 {
ret += fmt.Sprintf("hits %s for %d points of damage! ", target.Nick, dmg) ret += fmt.Sprintf("hits %s for %d points of damage! ", target.Nick, dmg)
target.Health -= int64(dmg) target.Health -= int64(dmg)
g.db.Update(target) g.db.Update(target)
@ -300,7 +326,11 @@ func (g *WatGame) Frame(player *Player, fields []string) string {
if target.Coins < amount { if target.Coins < amount {
return fmt.Sprintf("wat? %s is too poor for this.", target.Nick) return fmt.Sprintf("wat? %s is too poor for this.", target.Nick)
} }
n := g.RandInt(6) + 1 n, randErr := g.RandInt(6)
if e := handleError(randErr); e != "" {
return e
}
n = n + 1
ret := fmt.Sprintf("%s rolls a d6 to frame %s with %d %s: It's a %d! (<3 wins). ", player.Nick, target.Nick, amount, currency, n) ret := fmt.Sprintf("%s rolls a d6 to frame %s with %d %s: It's a %d! (<3 wins). ", player.Nick, target.Nick, amount, currency, n)
if n < 3 { if n < 3 {
ret += fmt.Sprintf("You frame %s for a minor crime. They pay me %d.", target.Nick, amount) ret += fmt.Sprintf("You frame %s for a minor crime. They pay me %d.", target.Nick, amount)
@ -335,7 +365,11 @@ func (g *WatGame) Steal(player *Player, fields []string) string {
if target.Coins < amount { if target.Coins < amount {
return fmt.Sprintf("wat? %s is poor and doesn't have that much to steal. (%d %s)", target.Nick, target.Coins, currency) return fmt.Sprintf("wat? %s is poor and doesn't have that much to steal. (%d %s)", target.Nick, target.Coins, currency)
} }
n := g.RandInt(6) + 1 n, randErr := g.RandInt(6)
if e := handleError(randErr); e != "" {
return e
}
n = n + 1
ret := fmt.Sprintf("%s is trying to steal %d %s from %s... ", player.Nick, amount, currency, target.Nick) ret := fmt.Sprintf("%s is trying to steal %d %s from %s... ", player.Nick, amount, currency, target.Nick)
if n < 3 { if n < 3 {
ret += "You did it! Sneaky bastard!" ret += "You did it! Sneaky bastard!"
@ -380,7 +414,11 @@ func (g *WatGame) Leech(player *Player, fields []string) string {
if err != "" { if err != "" {
return err return err
} }
r := g.RandInt(10) + 1 r, randErr := g.RandInt(10)
if e := handleError(randErr); e != "" {
return e
}
r = r + 1
reply := fmt.Sprintf("You muster your wealth and feed it to %s. ", g.bot.Nick) reply := fmt.Sprintf("You muster your wealth and feed it to %s. ", g.bot.Nick)
hpDown := amount / divisor hpDown := amount / divisor
player.Coins -= amount player.Coins -= amount
@ -408,7 +446,11 @@ func (g *WatGame) Rest(player *Player, fields []string) string {
} else if delta < minRest { } else if delta < minRest {
ret = fmt.Sprintf("wat were you thinking, sleeping at a time like this (%d until next rest)", minRest-delta) ret = fmt.Sprintf("wat were you thinking, sleeping at a time like this (%d until next rest)", minRest-delta)
} else { } else {
value := g.RandInt(10) + 1 value, randErr := g.RandInt(10)
if e := handleError(randErr); e != "" {
return e
}
value = value + 1
if player.Health < -5 { if player.Health < -5 {
player.Health = 1 player.Health = 1
ret = fmt.Sprintf("wow ur beat up. i pity u, ur health is now 1.") ret = fmt.Sprintf("wow ur beat up. i pity u, ur health is now 1.")
@ -435,8 +477,15 @@ func (g *WatGame) Bench(player *Player, fields []string) string {
if !g.CanAct(player, Action_Lift, minTime) { if !g.CanAct(player, Action_Lift, minTime) {
return "you're tired. no more lifting for now." return "you're tired. no more lifting for now."
} }
weight := g.RandInt(370) + 50 weight, randErr := g.RandInt(370)
reps := g.RandInt(10) if e := handleError(randErr); e != "" {
return e
}
weight = weight + 50
reps, randErr := g.RandInt(10)
if e := handleError(randErr); e != "" {
return e
}
value := int64(0) value := int64(0)
reply := fmt.Sprintf("%s benches %dwatts for %d reps, ", player.Nick, weight, reps) reply := fmt.Sprintf("%s benches %dwatts for %d reps, ", player.Nick, weight, reps)
if weight < 150 { if weight < 150 {
@ -452,7 +501,10 @@ func (g *WatGame) Bench(player *Player, fields []string) string {
} }
if g.roid[player.Nick] != 0 { if g.roid[player.Nick] != 0 {
delete(g.roid, player.Nick) delete(g.roid, player.Nick)
success := g.RandInt(2) success, randErr := g.RandInt(2)
if e := handleError(randErr); e != "" {
return e
}
if success != 0 { if success != 0 {
player.Health = 0 player.Health = 0
player.Anarchy -= 10 player.Anarchy -= 10
@ -475,7 +527,10 @@ func (g *WatGame) Riot(player *Player, fields []string) string {
if !g.CanAct(player, Action_Riot, int64((48 * time.Hour).Seconds())) { if !g.CanAct(player, Action_Riot, int64((48 * time.Hour).Seconds())) {
return "Planning a riot takes time and the right circumstances. Be prepared. (nothing happens)" return "Planning a riot takes time and the right circumstances. Be prepared. (nothing happens)"
} }
r := g.RandInt(100) r, randErr := g.RandInt(100)
if e := handleError(randErr); e != "" {
return e
}
reply := "" reply := ""
if r > 40 { if r > 40 {
player.Anarchy += 3 player.Anarchy += 3
@ -621,9 +676,21 @@ func PrintTwo(nick string, value uint64) string {
} }
func (g *WatGame) megaWat(player *Player, _ []string) string { func (g *WatGame) megaWat(player *Player, _ []string) string {
mega := g.RandInt(1000000) + 1 mega, randErr := g.RandInt(1000000)
kilo := g.RandInt(1000) + 1 if e := handleError(randErr); e != "" {
ten := g.RandInt(100) + 1 return e + " mega fail"
}
mega = mega + 1
kilo, randErr := g.RandInt(1000)
if e := handleError(randErr); e != "" {
return e + " kilo fail"
}
kilo = kilo + 1
ten, randErr := g.RandInt(100)
if e := handleError(randErr); e != "" {
return e + " ten fail"
}
ten = ten + 1
reply := "" reply := ""
if mega == 23 { if mega == 23 {
player.Coins += 1000000 player.Coins += 1000000

126
wat/integration.go Normal file
View File

@ -0,0 +1,126 @@
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!
// 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)
var msg string
var many bool
fiprcount := len(finisherPrizes)
cashoutcount := 0
// only a single winner
if fiprcount == 1 {
msg = "smartass %s :) gave u %d"
many = false
// multiple winners
} else if fiprcount > 1 {
msg = "gang of smartasses :) gave %s %d"
many = true
// no winners (should never get here)
} else {
fmt.Printf("Empty finishers, aborting Jeopardy processing")
return
}
// 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 {
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
if coins == 0 {
continue
}
cashoutcount += 1
// 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(strings.ToLower(name), "", false)
if player.Nick == "" {
fmt.Printf("Player %s does not exist in Watbot, skipping cashout.\n", name)
continue
}
// fill previous format placeholders
msg = fmt.Sprintf(msg, player.Nick, coins)
if many {
// append additional ones for filling in the next loop iteration
msg = msg + ", %s %d"
}
player.Coins += coins
w.db.Update(player)
}
if many {
// remove format placeholders from last loop iteration
msg = strings.Replace(msg, ", %s %d", ".", 1)
}
if cashoutcount > 0 {
w.bot.reply(m, msg)
}
}

22
wat/utils.go Normal file
View File

@ -0,0 +1,22 @@
package wat
import (
"fmt"
"runtime"
)
func handleError(err error) string {
if err != nil {
pc, _, _, ok := runtime.Caller(1)
details := runtime.FuncForPC(pc)
var cFun string
if ok && details != nil {
cFun = details.Name()
} else {
cFun = "???"
}
fmt.Printf("caught error in %s: %v\n", cFun, err)
return "u wat"
}
return ""
}

BIN
watbot

Binary file not shown.