commit 6091f30610f275eccb280e20b82ac608bfbf1d8c Author: Etzelia Date: Mon Jul 12 16:27:30 2021 -0500 Initial commit Signed-off-by: Etzelia diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a8950c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# GoLand +.idea/ + +# Binaries +stonebot* \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4b87d7f --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2021 Etzelia + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6077f95 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# stonebot + +ROCKS + +## License + +[MIT](LICENSE) \ No newline at end of file diff --git a/assets/assets.go b/assets/assets.go new file mode 100644 index 0000000..b8ede6d --- /dev/null +++ b/assets/assets.go @@ -0,0 +1,6 @@ +package assets + +import _ "embed" + +//go:embed rocks.gif +var RocksGif []byte diff --git a/assets/rocks.gif b/assets/rocks.gif new file mode 100644 index 0000000..bcbbf98 Binary files /dev/null and b/assets/rocks.gif differ diff --git a/database/database.go b/database/database.go new file mode 100644 index 0000000..42a8519 --- /dev/null +++ b/database/database.go @@ -0,0 +1,69 @@ +package database + +import ( + "errors" + "os" + "strconv" + + "go.etcd.io/bbolt" +) + +var ( + stonelandBucket = []byte("stoneland") + stonesKey = []byte("stones") +) + +type Database struct { + db *bbolt.DB +} + +func New(dbPath string) (*Database, error) { + db, err := bbolt.Open(dbPath, os.ModePerm, bbolt.DefaultOptions) + if err != nil { + return nil, err + } + return &Database{ + db: db, + }, db.Update(func(tx *bbolt.Tx) error { + _, err := tx.CreateBucketIfNotExists(stonelandBucket) + return err + }) +} + +func (db *Database) Close() error { + return db.db.Close() +} + +func (db *Database) Rock(num int) error { + return db.db.Update(func(tx *bbolt.Tx) error { + bucket := tx.Bucket(stonelandBucket) + if bucket == nil { + return errors.New("bucket for stones was not created") + } + stones, err := db.rocks(tx) + if err != nil { + return err + } + stones += num + return bucket.Put(stonesKey, []byte(strconv.Itoa(stones))) + }) +} + +func (db *Database) Rocks() (count int, err error) { + return count, db.db.View(func(tx *bbolt.Tx) error { + count, err = db.rocks(tx) + return err + }) +} + +func (db *Database) rocks(tx *bbolt.Tx) (int, error) { + bucket := tx.Bucket(stonelandBucket) + if bucket == nil { + return -1, errors.New("bucket for stones was not created") + } + stones := bucket.Get(stonesKey) + if stones == nil { + return 0, nil + } + return strconv.Atoi(string(stones)) +} diff --git a/discord/discord.go b/discord/discord.go new file mode 100644 index 0000000..8a1e0bb --- /dev/null +++ b/discord/discord.go @@ -0,0 +1,88 @@ +package discord + +import ( + "bytes" + "fmt" + "strings" + + "git.canopymc.net/Etzelia/stonebot/assets" + "git.canopymc.net/Etzelia/stonebot/database" + + "github.com/bwmarrin/discordgo" + "go.jolheiser.com/beaver" +) + +func New(token, prefix string, db *database.Database) (*discordgo.Session, error) { + bot, err := discordgo.New("Bot " + token) + if err != nil { + return nil, err + } + bot.Identify.Intents = discordgo.IntentsAllWithoutPrivileged + + bot.AddHandler(CommandHandler(prefix, db)) + bot.AddHandler(MessageHandler(prefix, db)) + + return bot, nil +} + +func CommandHandler(prefix string, db *database.Database) func(session *discordgo.Session, message *discordgo.MessageCreate) { + return func(session *discordgo.Session, message *discordgo.MessageCreate) { + // Ignore non-prefix + if !strings.HasPrefix(message.Content, prefix) { + return + } + + // Ignore bots + if message.Author.Bot { + return + } + + args := strings.Fields(message.Content[1:]) + switch strings.ToLower(args[0]) { + case "rocks", "stones": + rocks, err := db.Rocks() + if err != nil { + beaver.Errorf("could not count rocks: %v", err) + return + } + if _, err := session.ChannelMessageSend(message.ChannelID, fmt.Sprintf("%d ROCKS", rocks)); err != nil { + beaver.Errorf("could not send message: %v", err) + return + } + } + } +} + +func MessageHandler(prefix string, db *database.Database) func(session *discordgo.Session, message *discordgo.MessageCreate) { + return func(session *discordgo.Session, message *discordgo.MessageCreate) { + // Ignore prefix + if strings.HasPrefix(message.Content, prefix) { + return + } + + // Ignore bots + if message.Author.Bot { + return + } + + rocks := strings.Count(message.Content, "rock") + rocks += strings.Count(message.Content, "stone") + if err := db.Rock(rocks); err != nil { + beaver.Errorf("could not add rocks: %v", err) + } + + if rocks > 0 { + if _, err := session.ChannelMessageSendComplex(message.ChannelID, &discordgo.MessageSend{ + Files: []*discordgo.File{ + { + Name: "rocks.gif", + ContentType: "multipart/form-data", + Reader: bytes.NewReader(assets.RocksGif), + }, + }, + }); err != nil { + beaver.Errorf("could not send rocks gif: %v", err) + } + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ec5f954 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module git.canopymc.net/Etzelia/stonebot + +go 1.16 + +require ( + github.com/bwmarrin/discordgo v0.23.2 + github.com/gorilla/websocket v1.4.2 // indirect + github.com/peterbourgon/ff/v3 v3.1.0 + go.etcd.io/bbolt v1.3.6 + go.jolheiser.com/beaver v1.1.2 + golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect + golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1939488 --- /dev/null +++ b/go.sum @@ -0,0 +1,32 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4= +github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M= +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/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= +github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/peterbourgon/ff/v3 v3.1.0 h1:5JAeDK5j/zhKFjyHEZQXwXBoDijERaos10RE+xamOsY= +github.com/peterbourgon/ff/v3 v3.1.0/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE= +go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.jolheiser.com/beaver v1.1.2 h1:X8voMSTy+8QUFzHlvG89EUVZY/xPQe6fQLVjUPjTvWY= +go.jolheiser.com/beaver v1.1.2/go.mod h1:7X4F5+XOGSC3LejTShoBdqtRCnPWcnRgmYGmG3EKW8g= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 h1:5jaG59Zhd+8ZXe8C+lgiAGqkOaZBruqrWclLkgAww34= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..51d3d77 --- /dev/null +++ b/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "flag" + "os" + "os/signal" + "syscall" + + "git.canopymc.net/Etzelia/stonebot/database" + "git.canopymc.net/Etzelia/stonebot/discord" + + "github.com/peterbourgon/ff/v3" + "github.com/peterbourgon/ff/v3/fftoml" + "go.jolheiser.com/beaver" + "go.jolheiser.com/beaver/color" +) + +func main() { + fs := flag.NewFlagSet("stonebot", flag.ExitOnError) + fs.String("config", "stonebot.toml", "Path to config file") + verboseFlag := fs.Bool("verbose", false, "Verbose logging") + tokenFlag := fs.String("token", "", "Discord bot token") + prefixFlag := fs.String("prefix", "!", "Discord bot prefix") + dbFlag := fs.String("db", "stonebot.db", "Path to DB") + if err := ff.Parse(fs, os.Args[1:], + ff.WithEnvVarPrefix("STONEBOT"), + ff.WithConfigFileFlag("config"), + ff.WithConfigFileParser(fftoml.Parser), + ff.WithAllowMissingConfigFile(true), + ); err != nil { + beaver.Fatal(err) + } + + beaver.Console.Format = beaver.FormatOptions{ + TimePrefix: true, + StackPrefix: true, + StackLimit: 15, + LevelPrefix: true, + LevelColor: true, + } + color.Fatal = color.Error + + if *tokenFlag == "" { + beaver.Fatal("discord token is required") + } + + if *verboseFlag { + beaver.Console.Level = beaver.DEBUG + } + + db, err := database.New(*dbFlag) + if err != nil { + beaver.Fatalf("could not connect to database: %v", err) + } + defer db.Close() + + bot, err := discord.New(*tokenFlag, *prefixFlag, db) + if err != nil { + beaver.Fatalf("could not run bot: %v", err) + } + defer bot.Close() + if err := bot.Open(); err != nil { + beaver.Fatalf("could not open bot connection: %v", err) + } + + beaver.Info("Bot is now running. Press CTRL-C to exit.") + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc +}