Initial Commit

Signed-off-by: Etzelia <etzelia@hotmail.com>
rate-insult
Etzelia 2020-06-09 08:04:44 -05:00
commit 9fc6b4e99c
No known key found for this signature in database
GPG Key ID: 708511AE7ABC5314
17 changed files with 747 additions and 0 deletions

8
.gitignore vendored 100644
View File

@ -0,0 +1,8 @@
# GoLand
.idea/
# Generated
config/config_default.go
# sedbot
sedbot*

24
Makefile 100644
View File

@ -0,0 +1,24 @@
GO ?= go
.PHONY: fmt
fmt:
$(GO) fmt ./...
.PHONY: generate
generate:
$(GO) generate ./...
.PHONY: test
test:
$(GO) test -race ./...
.PHONY: vet
vet:
$(GO) vet ./...
.PHONY: build
build:
$(GO) build
.PHONY: build-all
build-all: generate build

23
README.md 100644
View File

@ -0,0 +1,23 @@
# SedBot
BirbMC Discord generi-bot.
## Commands
### Public
* `register <in-game name>` - Register to the Discord
* `links` - Get a list of dynamic links
* `<link>` - Get a specific link
* `fired` - Check how many times Carolyn has been fired
* `inspire` - Get a random "inspirational" message from [InspiroBot](https://inspirobot.me)
### Moderation
* `clear [<@user>] <number>` - Clear <number> messages (optionally only by @user)
## Building
```text
make build-all
```

64
config/config.go 100644
View File

@ -0,0 +1,64 @@
package config
import (
"io/ioutil"
"os"
"github.com/BurntSushi/toml"
)
var defaultConfig = []byte("")
type Config struct {
Token string `toml:"token"`
Prefix string `toml:"prefix"`
MCMToken string `toml:"mcm_token"`
MCMURL string `toml:"mcm_url"`
DBPath string `toml:"db_path"`
StaffRoles []string `toml:"staff_roles"`
Links []Link `toml:"links"`
MessageRoles []MessageRole `toml:"message_roles"`
RegisterRole string `toml:"register_role"`
RegisteredChannel string `toml:"registered_channel"`
FiredRole string `toml:"fired_role"`
}
type MessageRole struct {
MessageID string `toml:"message_id"`
RoleID string `toml:"role_id"`
Emoji string `toml:"emoji"`
}
type Link struct {
Name string `toml:"name"`
Aliases []string `toml:"aliases"`
URL string `toml:"url"`
}
func Load(configPath string) (*Config, error) {
var err error
var configContent []byte
if len(configPath) == 0 {
configPath = "sedbot.toml"
}
configContent, err = ioutil.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
if err = ioutil.WriteFile(configPath, defaultConfig, os.ModePerm); err != nil {
return nil, err
}
configContent = defaultConfig
} else {
return nil, err
}
}
var cfg *Config
if err = toml.Unmarshal(configContent, &cfg); err != nil {
return nil, err
}
return cfg, nil
}

View File

@ -0,0 +1,60 @@
package database
import (
"strconv"
"go.etcd.io/bbolt"
)
var (
firedBucket = []byte("fired")
buckets = [][]byte{
firedBucket,
}
)
type Database struct {
db *bbolt.DB
}
func Load(dbPath string) (*Database, error) {
db, err := bbolt.Open(dbPath, 0600, bbolt.DefaultOptions)
if err != nil {
return nil, err
}
for _, b := range buckets {
if err := db.Update(func(tx *bbolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(b)
return err
}); err != nil {
return nil, err
}
}
return &Database{
db: db,
}, nil
}
func (db *Database) CheckPing(roleID string) int {
roleIDByte := []byte(roleID)
var idx int
_ = db.db.View(func(tx *bbolt.Tx) error {
num := tx.Bucket(firedBucket).Get(roleIDByte)
if num != nil {
if i, err := strconv.Atoi(string(num)); err == nil {
idx = i
}
}
return nil
})
return idx
}
func (db *Database) IncrementPing(roleID string) error {
roleIDByte := []byte(roleID)
return db.db.Update(func(tx *bbolt.Tx) error {
idx := db.CheckPing(roleID)
return tx.Bucket(firedBucket).Put(roleIDByte, []byte(strconv.Itoa(idx+1)))
})
}

69
discord/clear.go 100644
View File

@ -0,0 +1,69 @@
package discord
import (
"errors"
"strconv"
"strings"
)
const clearMax = 20
func init() {
commands["clear"] = command{
validate: func(cmd commandInit) bool {
return isStaff(cmd.message.Member.Roles, cmd.config.StaffRoles)
},
run: func(cmd commandInit) error {
args := strings.Fields(cmd.message.Content)
var userID string
if len(cmd.message.Mentions) > 0 {
userID = cmd.message.Mentions[0].ID
}
limitArg := 1
if userID != "" {
limitArg = 2
if len(args) < 2 {
return errors.New("this command takes needs two arguments with a mention")
}
} else if len(args) < 1 {
return errors.New("this command takes one argument without a mention")
}
limit, err := strconv.Atoi(args[limitArg])
if err != nil {
return err
}
if limit < 0 {
limit = 0
} else if limit > clearMax {
limit = 20
}
batch := []string{cmd.message.ID}
var deleted int
messages, err := cmd.session.ChannelMessages(cmd.message.ChannelID, 100, cmd.message.ID, "", "")
for _, msg := range messages {
if deleted == limit {
break
}
deleteMessage := true
if userID != "" {
deleteMessage = msg.Author.ID == userID
}
if deleteMessage {
batch = append(batch, msg.ID)
deleted++
}
}
return cmd.session.ChannelMessagesBulkDelete(cmd.message.ChannelID, batch)
},
help: "Clear messages",
}
}

134
discord/discord.go 100644
View File

@ -0,0 +1,134 @@
package discord
import (
"strings"
"go.etztech.xyz/sedbot/config"
"go.etztech.xyz/sedbot/database"
"github.com/bwmarrin/discordgo"
"go.jolheiser.com/beaver"
)
// Register commands to this map
var commands = make(map[string]command)
type commandInit struct {
session *discordgo.Session
message *discordgo.Message
config *config.Config
database *database.Database
}
type command struct {
validate func(cmd commandInit) bool
run func(cmd commandInit) error
help string
}
func Bot(cfg *config.Config, db *database.Database) (*discordgo.Session, error) {
bot, err := discordgo.New("Bot " + cfg.Token)
if err != nil {
return nil, err
}
Links(cfg)
bot.AddHandler(commandHandler(cfg, db))
bot.AddHandler(messageHandler(cfg, db))
beaver.Info("https://discord.com/api/oauth2/authorize?client_id=718905104643784825&permissions=0&redirect_uri=https%3A%2F%2Fbirbmc.com&scope=bot")
return bot, nil
}
func sendTyping(s *discordgo.Session, channelID string) {
if err := s.ChannelTyping(channelID); err != nil {
beaver.Errorf("could not send typing status: %v", err)
}
}
func sendMessage(s *discordgo.Session, channelID, content string) *discordgo.Message {
msg, err := s.ChannelMessageSend(channelID, content)
if err != nil {
beaver.Errorf("could not send message: %v", err)
return nil
}
return msg
}
func isStaff(authorRoleIDs, staffRoleIDs []string) bool {
for _, aRole := range authorRoleIDs {
for _, sRole := range staffRoleIDs {
if aRole == sRole {
return true
}
}
}
return false
}
func commandHandler(cfg *config.Config, db *database.Database) func(s *discordgo.Session, m *discordgo.MessageCreate) {
return func(s *discordgo.Session, m *discordgo.MessageCreate) {
// Ignore bots
if m.Author.Bot {
return
}
// Check prefix
if !strings.HasPrefix(m.Content, cfg.Prefix) {
return
}
content := m.Content[1:]
args := strings.Fields(content)
if len(args) == 0 {
return
}
cmdArg := strings.ToLower(args[0])
isHelp := strings.EqualFold(cmdArg, "help")
cmd, ok := commands[cmdArg]
if !ok && !isHelp {
return
}
if isHelp {
sendMessage(s, m.ChannelID, cmd.help)
return
}
cmdInit := commandInit{
session: s,
message: m.Message,
config: cfg,
database: db,
}
if !cmd.validate(cmdInit) {
sendMessage(s, m.ChannelID, "You cannot run this command.")
return
}
if err := cmd.run(cmdInit); err != nil {
sendMessage(s, m.ChannelID, err.Error())
}
}
}
func messageHandler(cfg *config.Config, db *database.Database) func(s *discordgo.Session, m *discordgo.MessageCreate) {
return func(s *discordgo.Session, m *discordgo.MessageCreate) {
// Ignore bots
if m.Author.Bot {
return
}
// [FIRED] increment
for _, role := range m.MentionRoles {
if cfg.FiredRole == role {
if err := db.IncrementPing(cfg.FiredRole); err != nil {
beaver.Errorf("could not increment ping for %s: %v", cfg.FiredRole, err)
}
}
}
}
}

19
discord/fired.go 100644
View File

@ -0,0 +1,19 @@
package discord
import (
"fmt"
)
func init() {
commands["fired"] = command{
validate: func(cmd commandInit) bool {
return true
},
run: func(cmd commandInit) error {
sendMessage(cmd.session, cmd.message.ChannelID, fmt.Sprintf("Carolyn has been fired **%d** times!",
cmd.database.CheckPing(cmd.config.FiredRole)))
return nil
},
help: "Check how many times Carolyn has been fired.",
}
}

42
discord/help.go 100644
View File

@ -0,0 +1,42 @@
package discord
import (
"fmt"
"strings"
)
func init() {
commands["help"] = command{
validate: func(cmd commandInit) bool {
return true
},
run: func(cmd commandInit) error {
args := strings.Fields(cmd.message.Content)[1:]
var resp string
if len(args) == 1 {
resp = singleHelp(args[0])
} else {
resp = allHelp()
}
sendMessage(cmd.session, cmd.message.ChannelID, resp)
return nil
},
help: "HELP! HEEEEEEEEEELP!",
}
}
func singleHelp(cmd string) string {
if c, ok := commands[cmd]; ok {
return fmt.Sprintf("%s: %s", cmd, c.help)
}
return "Unknown command"
}
func allHelp() string {
helps := make([]string, 0)
for n, c := range commands {
helps = append(helps, fmt.Sprintf("%s: %s", n, c.help))
}
return strings.Join(helps, "\n")
}

23
discord/inspire.go 100644
View File

@ -0,0 +1,23 @@
package discord
import "go.etztech.xyz/inspiro"
func init() {
commands["inspire"] = command{
validate: func(cmd commandInit) bool {
return true
},
run: func(cmd commandInit) error {
sendTyping(cmd.session, cmd.message.ChannelID)
img, err := inspiro.Generate()
if err != nil {
return err
}
sendMessage(cmd.session, cmd.message.ChannelID, img)
return nil
},
help: "Get inspired!",
}
}

49
discord/links.go 100644
View File

@ -0,0 +1,49 @@
package discord
import (
"fmt"
"strings"
"go.etztech.xyz/sedbot/config"
)
func Links(cfg *config.Config) {
links := make([]string, 0)
for _, link := range cfg.Links {
commands[link.Name] = command{
validate: func(cmd commandInit) bool {
return true
},
run: func(cmd commandInit) error {
sendMessage(cmd.session, cmd.message.ChannelID, fmt.Sprintf("<%s>", link.URL))
return nil
},
help: fmt.Sprintf("Returns the link for %s", link.Name),
}
links = append(links, fmt.Sprintf("%s -> %s", link.Name, link.URL))
for _, alias := range link.Aliases {
commands[alias] = command{
validate: func(cmd commandInit) bool {
return true
},
run: func(cmd commandInit) error {
sendMessage(cmd.session, cmd.message.ChannelID, fmt.Sprintf("<%s>", link.URL))
return nil
},
help: fmt.Sprintf("Returns the link for %s", alias),
}
links = append(links, fmt.Sprintf("%s -> %s", alias, link.URL))
}
}
commands["links"] = command{
validate: func(cmd commandInit) bool {
return true
},
run: func(cmd commandInit) error {
sendMessage(cmd.session, cmd.message.ChannelID, strings.Join(links, "\n"))
return nil
},
help: "Get all dynamic links",
}
}

View File

@ -0,0 +1,53 @@
package discord
import (
"errors"
"fmt"
"strings"
"go.etztech.xyz/go-mcm"
"go.etztech.xyz/go-mcm/model/django"
)
func init() {
commands["register"] = command{
validate: func(cmd commandInit) bool {
return len(cmd.message.Member.Roles) == 0
},
run: func(cmd commandInit) error {
args := strings.Fields(cmd.message.Content)
if len(args) < 2 {
return errors.New("you must give this command your application username")
}
sendTyping(cmd.session, cmd.message.ChannelID)
manager := mcm.NewMCM(cmd.config.MCMToken, cmd.config.MCMURL)
models := manager.NewModel()
apps, err := models.Application(models.NewDjangoBuilder().IExact(django.ApplicationUsername, args[1]))
if err != nil {
return err
}
if len(apps) == 0 {
return errors.New("no application found with that username")
}
if !apps[0].Accepted {
return errors.New("sorry, your application was denied")
}
// Accepted
if err := cmd.session.GuildMemberNickname(cmd.message.GuildID, cmd.message.Author.ID, apps[0].Username); err != nil {
return err
}
if err := cmd.session.GuildMemberRoleAdd(cmd.message.GuildID, cmd.message.Author.ID, cmd.config.RegisterRole); err != nil {
return err
}
sendMessage(cmd.session, cmd.config.RegisteredChannel, fmt.Sprintf("Welcome, **%s**!", cmd.message.Author.Mention()))
return nil
},
help: "Register yourself with the Discord",
}
}

13
go.mod 100644
View File

@ -0,0 +1,13 @@
module go.etztech.xyz/sedbot
go 1.14
require (
github.com/BurntSushi/toml v0.3.1
github.com/bwmarrin/discordgo v0.20.3
github.com/urfave/cli/v2 v2.2.0 // indirect
go.etcd.io/bbolt v1.3.4
go.etztech.xyz/go-mcm v1.2.0
go.etztech.xyz/inspiro v0.0.0-20200606185551-edfdf9da2359
go.jolheiser.com/beaver v1.0.2
)

32
go.sum 100644
View File

@ -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.20.3 h1:AxjcHGbyBFSC0a3Zx5nDQwbOjU7xai5dXjRnZ0YB7nU=
github.com/bwmarrin/discordgo v0.20.3/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etztech.xyz/go-mcm v1.2.0 h1:QEOv6oAjy9ICP3AOcwItNT88wBjOZHK7UKseZEcZh0k=
go.etztech.xyz/go-mcm v1.2.0/go.mod h1:Hz2YULB3sN/aQA8cPSm2d6LM3E3qTMspzfRCIAD/1dc=
go.etztech.xyz/inspiro v0.0.0-20200606185551-edfdf9da2359 h1:j/ZeoAj185wHfCSYD52Kt/69i3Bzk1MXN4Qh1yP6+P4=
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=
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=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

61
main.go 100644
View File

@ -0,0 +1,61 @@
//go:generate go run sedbot.example.go
package main
import (
"flag"
"os"
"os/signal"
"syscall"
"go.etztech.xyz/sedbot/config"
"go.etztech.xyz/sedbot/database"
"go.etztech.xyz/sedbot/discord"
"go.jolheiser.com/beaver"
)
var configFlag string
func main() {
flag.StringVar(&configFlag, "config", "sedbot.toml", "Set config path")
flag.Parse()
beaver.Console.Format = beaver.FormatOptions{
TimePrefix: true,
StackPrefix: true,
StackLimit: 15,
LevelPrefix: true,
LevelColor: true,
}
cfg, err := config.Load(configFlag)
if err != nil {
beaver.Fatalf("could not load config: %v", err)
}
db, err := database.Load(cfg.DBPath)
if err != nil {
beaver.Fatalf("could not load database: %v", err)
}
bot, err := discord.Bot(cfg, db)
if err != nil {
beaver.Fatalf("could not start Discord bot: %v", err)
}
go func() {
if err := bot.Open(); err != nil {
beaver.Errorf("error running Discord bot: %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
if err := bot.Close(); err != nil {
beaver.Errorf("error closing Discord bot: %v", err)
}
}

35
sedbot.example.go 100644
View File

@ -0,0 +1,35 @@
// +build ignore
package main
import (
"fmt"
"io/ioutil"
"os"
)
const (
exampleFile = "sedbot.example.toml"
writeFile = "config/config_default.go"
tmpl = `package config
func init() {
defaultConfig = []byte(` + "`" + `%s` + "`" + `)
}
`
)
func main() {
bytes, err := ioutil.ReadFile(exampleFile)
if err != nil {
fmt.Printf("Could not read from %s. Are you in the root directory of the project?", exampleFile)
os.Exit(1)
}
data := fmt.Sprintf(tmpl, string(bytes))
if err := ioutil.WriteFile(writeFile, []byte(data), os.ModePerm); err != nil {
fmt.Printf("Could not write to %s.", writeFile)
os.Exit(1)
}
}

View File

@ -0,0 +1,38 @@
# token is the Discord token for the bot
token = ""
# prefix is the bot command prefix
prefix = "!"
# mcm_token is the token for the MCM API
mcm_token = ""
# mcm_url is the base URL to MCM
mcm_url = ""
# db_path is the path to the database (default is next to binary)
db_path = "sedbot.db"
# fired_role is to check how many time Carolyn has been fired
fired_role = "0"
# register_role is the role to assign to a user after registering
register_role = "0"
# registered_channel is the channel to message to welcome the newly registered user
registered_channel = "0"
# staff_roles are for staff commands
staff_roles = []
# links are any basic link -> URL commands
[[links]]
name = "discord"
aliases = ["invite", "gib"]
url = "https://birbmc.com/discord"
# message_roles are for messages that should toggle a role when a user selects it
[[message_roles]]
message_id = "0"
role_id = "0"
emoji = "thumbsup"