Compare commits

...

30 Commits

Author SHA1 Message Date
jolheiser e37945c5d5
Add welcome handler 2 weeks ago
jolheiser 520cc5968c
Change module 2 weeks ago
Etzelia ac862d8d18 Ignore self in register (#13) 6 months ago
Etzelia 74e49546fe Add config for register URL and check for dupes (#12) 6 months ago
Etzelia 4fb7cc2928 Closures (#9) 7 months ago
Etzelia 8e83885118 Dad joke status (#8) 8 months ago
Etzelia cc3ceb6668 Make imgur album commands configurable (#7) 8 months ago
Etzelia 16e0383e39
Update modules 9 months ago
Etzelia 3ecd7f04e4
Quicker rate limit test 9 months ago
Etzelia c5066e84e2
Make reaction roles easier to implement 9 months ago
Etzelia c84c523e55
canopeas 9 months ago
Etzelia 326b370ca8
Merge branch 'broom' of birbmc.com:Etzelia/sedbot into main 10 months ago
Etzelia dec436805c
Clean up 11 months ago
Etzelia beb2b4a6aa Add ServerAPI, clean up help, Twitter (#31) 11 months ago
Etzelia 2dc22baa28 Birbs (#25) 1 year ago
Etzelia cf674102b1 Update gojang (#24) 1 year ago
Etzelia d86a1f86b0 Jupiter (#23) 1 year ago
Etzelia bed25234bb Add XKCD and FK (#22) 1 year ago
Etzelia 6a891d1ed1 Change default branch to main 1 year ago
Etzelia 1a6b6c174a Include username#discriminator in leave message (#21) 1 year ago
Etzelia d1600be5d1 Add leave messages (#19) 1 year ago
Etzelia a581094959 Update go-mcm and change Eq to Exact (#18) 1 year ago
Etzelia 8769b33bca Fix Drone Release (#16) 1 year ago
Etzelia 03260f8892 Set up main release (#15) 1 year ago
Etzelia 1b5257e6b9
Fix setting 1 year ago
Etzelia a9bbc0bf92
Set up main release 1 year ago
Etzelia e688ba4733 Report error in register command (#14) 1 year ago
Etzelia 944e417ddb Add Drone tests (#13) 1 year ago
Etzelia ff397f14e6 Add status command (#11) 1 year ago
Etzelia 1683cc6214 Add welcome command (#10) 1 year ago
  1. 42
      .drone.yml
  2. 8
      .gitignore
  3. 31
      Makefile
  4. 10
      README.md
  5. 135
      config/canopeas.example.toml
  6. 94
      config/config.go
  7. 29
      database/database.go
  8. 30
      database/ping.go
  9. 49
      database/unban.go
  10. 54
      discord/ban.go
  11. 47
      discord/birb.go
  12. 42
      discord/broadcast.go
  13. 8
      discord/clear.go
  14. 5
      discord/compliment.go
  15. 66
      discord/dad.go
  16. 169
      discord/discord.go
  17. 64
      discord/echo.go
  18. 5
      discord/fired.go
  19. 77
      discord/help.go
  20. 20
      discord/history.go
  21. 71
      discord/imgur.go
  22. 7
      discord/inspire.go
  23. 5
      discord/insult.go
  24. 45
      discord/register.go
  25. 51
      discord/status.go
  26. 34
      discord/tweet.go
  27. 41
      discord/unban.go
  28. 36
      discord/unbans.go
  29. 44
      discord/utils.go
  30. 6
      discord/utils_test.go
  31. 51
      discord/welcome.go
  32. 46
      discord/xkcd.go
  33. 29
      falseknees/client.go
  34. 130
      falseknees/falseknees.go
  35. 483
      falseknees/falseknees_test.go
  36. 21
      go.mod
  37. 82
      go.sum
  38. 47
      imgur/imgur.go
  39. 23
      inspiro/inspiro.go
  40. 17
      inspiro/inspiro_test.go
  41. 35
      main.go
  42. 35
      sedbot.example.go
  43. 61
      sedbot.example.toml

42
.drone.yml

@ -0,0 +1,42 @@
---
kind: pipeline
name: compliance
trigger:
event:
- pull_request
steps:
- name: build
pull: always
image: golang:1.16
commands:
- make test
- make build
- name: check
pull: always
image: golang:1.16
commands:
- make vet
---
kind: pipeline
name: release
trigger:
event:
- push
branch:
- main
steps:
- name: build
pull: always
image: golang:1.16
commands:
- make build
- name: gitea-release
pull: always
image: jolheiser/drone-gitea-main:latest
settings:
token:
from_secret: gitea_token
base: https://git.canopymc.net
files:
- "canopeas"

8
.gitignore

@ -1,8 +1,6 @@
# GoLand
.idea/
# Generated
config/config_default.go
# sedbot
sedbot*
# canopeas
canopeas*
!config/canopeas.example.toml

31
Makefile

@ -1,31 +0,0 @@
GO ?= go
.PHONY: fmt
fmt:
$(GO) fmt ./...
.PHONY: imp
imp:
imp -w
.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
.PHONY: check
check: generate imp fmt test vet build

10
README.md

@ -1,6 +1,6 @@
# SedBot
# canopeas
BirbMC Discord generi-bot.
Canopy Discord generi-bot.
## Commands
@ -20,12 +20,6 @@ BirbMC Discord generi-bot.
* `clear [<@user>] <number>` - Clear <number> messages (optionally only by @user)
## Building
```text
make build-all
```
## License
[MIT](LICENSE)

135
config/canopeas.example.toml

@ -0,0 +1,135 @@
# token is the Discord token for the bot
token = ""
# prefix is the bot command prefix
prefix = "!"
# db_path is the path to the database (default is next to binary)
db_path = "canopeas.db"
# mc_path is the path to the root directory of the minecraft server
mc_path = "/home/minecraft/server/"
# fired_role is to check how many time Carolyn has been fired
fired_role = "0"
# leave_channel is the channel to post leave messages to
leave_channel = "0"
# staff_roles are for staff commands
staff_roles = []
# meme_rate is the rate limit for memes
meme_rate = "0"
# Imgur Client ID
imgur_client_id = ""
# Welcome new users
[welcome]
# Channel ID of the welcome channel
channel = "0"
# Message to send in welcome channel
message = """\
**Hey ${user}, welcome to The Canopy 🎉 !**
If you are new, please read #rules and fill out an application here: https://canopymc.net/apply.
You will need to join our creative server at `creative.canopymc.net` to be verified.
**After** applying and joining the server, please run the `!register <MC username>` (without the brackets) command in this channel to join the Discord!
"""
# DM to send to user
dm = """\
**Hey ${user}, welcome to The Canopy 🎉 !**
If you are new, please read #rules and fill out an application here: https://canopymc.net/apply.
You will need to join our survival server at `creative.canopymc.net` to be verified.
After doing that please run the `!register <MC username>` (without the brackets) command in the welcome channel to join the Discord!
"""
[register]
# role is the role to assign to a user after registering
role = "0"
# url is the URL to show to new users
url = "https://google.com"
# welcome_channel is the channel to message to welcome the newly registered user
welcome_channel = "0"
# ServerAPI options
[serverapi]
# API endpoint
endpoint = ""
# Auth token
token = ""
# Twitter options
[twitter]
# Consumer Key
consumer_key = ""
# Consumer Secret
consumer_secret = ""
# Access Token
access_token = ""
# Access Secret
access_secret = ""
# Server options
[server]
# connection address
address = ""
# connection port
port = 25565
# MCM options
[mcm]
# the token for the MCM API
token = ""
# the base URL to the MCM API
url = ""
# insults
# <args>, your <target> looks like <comparison>, you <adjective> <noun>
[insult]
targets = []
comparisons = []
adjectives = []
nouns = []
# compliments
# <args>, I would <verb> my <noun> just to <minor thing>.
[compliment]
verbs = []
nouns = []
minor_things = []
[[albums]]
name = "jupiter"
aliases = ["jup", "jupjup"]
album_id = ""
help = "Images of Jupiter"
# echoes are any basic command -> message
[[echoes]]
name = "discord"
aliases = ["invite", "gib"]
message = "<https://birbmc.com/discord>"
help = "Get the invite link"
# message_roles are for messages that should toggle a role when a user selects it
[[message_roles]]
channel_id = "0"
message_id = "0"
[[message_roles.reactions]]
role_id = "0"
emoji = "👍"
[[message_roles.reactions]]
role_id = "0"
emoji = "👎"

94
config/config.go

@ -1,30 +1,53 @@
package config
import (
_ "embed"
"io/ioutil"
"os"
"github.com/BurntSushi/toml"
"github.com/pelletier/go-toml"
)
var defaultConfig = []byte("")
//go:embed canopeas.example.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"`
MCPath string `toml:"mc_path"`
StaffRoles []string `toml:"staff_roles"`
Echoes []Echo `toml:"echoes"`
MessageRoles []MessageRole `toml:"message_roles"`
RegisterRole string `toml:"register_role"`
RegisteredChannel string `toml:"registered_channel"`
FiredRole string `toml:"fired_role"`
MemeRate string `toml:"meme_rate"`
Insult struct {
Token string `toml:"token"`
Prefix string `toml:"prefix"`
MCM struct {
Token string `toml:"token"`
URL string `toml:"url"`
} `toml:"mcm"`
Server struct {
Address string `toml:"address"`
Port int `toml:"port"`
} `toml:"server"`
DBPath string `toml:"db_path"`
MCPath string `toml:"mc_path"`
ImgurClientID string `toml:"imgur_client_id"`
ServerAPI struct {
Endpoint string `toml:"endpoint"`
Token string `toml:"token"`
} `toml:"serverapi"`
Twitter struct {
ConsumerKey string `toml:"consumer_key"`
ConsumerSecret string `toml:"consumer_secret"`
AccessToken string `toml:"access_token"`
AccessSecret string `toml:"access_secret"`
} `toml:"twitter"`
StaffRoles []string `toml:"staff_roles"`
Echoes []Echo `toml:"echoes"`
Albums []Album `toml:"albums"`
MessageRoles []MessageRole `toml:"message_roles"`
Register struct {
URL string `toml:"url"`
Role string `toml:"role"`
WelcomeChannel string `toml:"welcome_channel"`
} `toml:"register"`
LeaveChannel string `toml:"leave_channel"`
FiredRole string `toml:"fired_role"`
MemeRate string `toml:"meme_rate"`
Insult struct {
Targets []string `toml:"targets"`
Comparisons []string `toml:"comparisons"`
Adjectives []string `toml:"adjectives"`
@ -35,13 +58,22 @@ type Config struct {
Nouns []string `toml:"nouns"`
MinorThings []string `toml:"minor_things"`
} `toml:"compliment"`
Welcome struct {
Channel string `toml:"channel"`
Message string `toml:"message"`
DM string `toml:"dm"`
} `toml:"welcome"`
}
type MessageRole struct {
ChannelID string `toml:"channel_id"`
MessageID string `toml:"message_id"`
RoleID string `toml:"role_id"`
Emoji string `toml:"emoji"`
ChannelID string `toml:"channel_id"`
MessageID string `toml:"message_id"`
Reactions []MessageReaction `toml:"reactions"`
}
type MessageReaction struct {
Emoji string `toml:"emoji"`
RoleID string `toml:"role_id"`
}
type Echo struct {
@ -51,11 +83,18 @@ type Echo struct {
Help string `toml:"help"`
}
type Album struct {
Name string `toml:"name"`
Aliases []string `toml:"aliases"`
AlbumID string `toml:"album_id"`
Help string `toml:"help"`
}
func Load(configPath string) (*Config, error) {
var err error
var configContent []byte
if len(configPath) == 0 {
configPath = "sedbot.toml"
configPath = "canopeas.toml"
}
configContent, err = ioutil.ReadFile(configPath)
@ -70,10 +109,15 @@ func Load(configPath string) (*Config, error) {
}
}
var cfg *Config
if err = toml.Unmarshal(configContent, &cfg); err != nil {
var cfg Config
tree, err := toml.LoadBytes(configContent)
if err != nil {
return nil, err
}
if err := tree.Unmarshal(&cfg); err != nil {
return nil, err
}
return cfg, nil
return &cfg, nil
}

29
database/database.go

@ -1,16 +1,16 @@
package database
import (
"strconv"
"go.etcd.io/bbolt"
)
var (
firedBucket = []byte("fired")
unbanBucket = []byte("unban")
buckets = [][]byte{
firedBucket,
unbanBucket,
}
)
@ -19,7 +19,7 @@ type Database struct {
}
func Load(dbPath string) (*Database, error) {
db, err := bbolt.Open(dbPath, 0600, bbolt.DefaultOptions)
db, err := bbolt.Open(dbPath, 0o600, bbolt.DefaultOptions)
if err != nil {
return nil, err
}
@ -35,26 +35,3 @@ func Load(dbPath string) (*Database, error) {
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)))
})
}

30
database/ping.go

@ -0,0 +1,30 @@
package database
import (
"strconv"
"go.etcd.io/bbolt"
)
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)))
})
}

49
database/unban.go

@ -0,0 +1,49 @@
package database
import (
"strconv"
"time"
"go.etcd.io/bbolt"
)
type UnbanRecord struct {
Username string
Expiration time.Time
}
func (u UnbanRecord) ExpirationString() string {
return u.Expiration.Format("01/02/2006 at 03:04:05 pm MST")
}
func (db *Database) ListUnbans() []UnbanRecord {
unbans := make([]UnbanRecord, 0)
_ = db.db.View(func(tx *bbolt.Tx) error {
return tx.Bucket(unbanBucket).ForEach(func(username, unix []byte) error {
unixNum, err := strconv.ParseInt(string(unix), 10, 64)
if err != nil {
return err
}
unbans = append(unbans, UnbanRecord{
Username: string(username),
Expiration: time.Unix(unixNum, 0),
})
return nil
})
})
return unbans
}
func (db *Database) AddUnban(record UnbanRecord) error {
usernameByte := []byte(record.Username)
return db.db.Update(func(tx *bbolt.Tx) error {
return tx.Bucket(unbanBucket).Put(usernameByte, []byte(strconv.FormatInt(record.Expiration.Unix(), 10)))
})
}
func (db *Database) RemoveUnban(username string) error {
usernameByte := []byte(username)
return db.db.Update(func(tx *bbolt.Tx) error {
return tx.Bucket(unbanBucket).Delete(usernameByte)
})
}

54
discord/ban.go

@ -0,0 +1,54 @@
package discord
import (
"fmt"
"net/http"
"strings"
"time"
"git.jojodev.com/Minecraft/go-serverapi"
)
func init() {
commands = append(commands, &command{
staffOnly: true,
name: "ban",
validate: func(cmd commandInit) bool {
return isStaff(cmd.message.Member.Roles, cmd.config.StaffRoles)
},
run: func(cmd commandInit) (string, error) {
args := strings.Fields(cmd.message.Content)
if len(args) < 2 {
return "This command requires an in-game username.", nil
}
target := args[1]
reason := "You have been banned. Appeal at https://birbmc.com/appeal"
if len(args) >= 3 {
reason = fmt.Sprintf("%s. Appeal at https://birbmc.com/appeal", strings.Join(args[2:], " "))
}
ban := serverapi.Ban{
Kick: serverapi.Kick{
Unban: serverapi.Unban{
Target: target,
},
Reason: reason,
},
Source: "Discord",
Created: time.Now().Unix(),
}
status, err := cmd.sapiClient.Ban(ban)
if err != nil {
return "", err
}
if status != http.StatusOK {
return fmt.Sprintf("ServerAPI returned status %d when trying to ban %s", status, target), nil
}
return fmt.Sprintf("%s was banned by %s", target, cmd.message.Author.Username), nil
},
help: "Ban a player",
})
}

47
discord/birb.go

@ -0,0 +1,47 @@
package discord
import (
"context"
"fmt"
"strconv"
"strings"
"git.jojodev.com/Minecraft/canopeas/falseknees"
)
func init() {
commands = append(commands, &command{
name: "birb",
validate: func(cmd commandInit) bool {
return true
},
run: func(cmd commandInit) (string, error) {
if !memeRateLimit.Try() {
return "", nil
}
client := falseknees.New()
var comic *falseknees.Comic
var err error
args := strings.Fields(cmd.message.Content)
if len(args) < 2 {
comic, err = client.Random(context.Background())
} else if strings.EqualFold(args[1], "new") {
comic, err = client.Current(context.Background())
} else {
comicNum, err := strconv.Atoi(args[1])
if err != nil {
return "", err
}
comic, err = client.Comic(context.Background(), comicNum)
}
if err != nil {
return "", err
}
return fmt.Sprintf("%d: %s\n%s", comic.Num, comic.Title, comic.Img), nil
},
help: "Get a FalseKnees comic",
})
}

42
discord/broadcast.go

@ -0,0 +1,42 @@
package discord
import (
"fmt"
"net/http"
"strings"
"git.jojodev.com/Minecraft/go-serverapi"
)
func init() {
commands = append(commands, &command{
staffOnly: true,
name: "broadcast",
validate: func(cmd commandInit) bool {
return isStaff(cmd.message.Member.Roles, cmd.config.StaffRoles)
},
run: func(cmd commandInit) (string, error) {
args := strings.Fields(cmd.message.Content)
if len(args) < 2 {
return "This command requires a message to broadcast", nil
}
message := strings.Join(args[1:], " ")
broadcast := serverapi.Broadcast{
From: cmd.message.Author.Username,
Message: message,
}
status, err := cmd.sapiClient.Broadcast(broadcast)
if err != nil {
return "", err
}
if status != http.StatusOK {
return fmt.Sprintf("ServerAPI returned status %d when trying to broadcast.", status), nil
}
return "Broadcast sent!", nil
},
help: "Send an in-game broadcast",
})
}

8
discord/clear.go

@ -8,7 +8,9 @@ import (
const clearMax = 20
func init() {
commands["clear"] = command{
commands = append(commands, &command{
name: "clear",
staffOnly: true,
validate: func(cmd commandInit) bool {
return isStaff(cmd.message.Member.Roles, cmd.config.StaffRoles)
},
@ -24,7 +26,7 @@ func init() {
if userID != "" {
limitArg = 2
if len(args) < 2 {
return "This command takes needs two arguments with a mention", nil
return "This command takes two arguments with a mention", nil
}
} else if len(args) < 1 {
return "This command takes one argument without a mention", nil
@ -64,5 +66,5 @@ func init() {
return "", cmd.session.ChannelMessagesBulkDelete(cmd.message.ChannelID, batch)
},
help: "Clear messages",
}
})
}

5
discord/compliment.go

@ -6,7 +6,8 @@ import (
)
func init() {
commands["compliment"] = command{
commands = append(commands, &command{
name: "compliment",
validate: func(cmd commandInit) bool {
return true
},
@ -38,5 +39,5 @@ func init() {
return "", nil
},
help: "Compliment someone!",
}
})
}

66
discord/dad.go

@ -2,8 +2,11 @@ package discord
import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"github.com/rs/zerolog/log"
)
const (
@ -18,7 +21,8 @@ type dadJoke struct {
}
func init() {
commands["dad"] = command{
commands = append(commands, &command{
name: "dad",
validate: func(cmd commandInit) bool {
return true
},
@ -27,38 +31,48 @@ func init() {
return "", nil
}
req, err := http.NewRequest(http.MethodGet, dadJokeAPI, nil)
dj, err := newDadJoke()
if err != nil {
return "", err
log.Warn().Msgf("error getting new dad joke: %v", err)
return dadJokeErr, nil
}
req.Header.Add("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
return dj.Joke, nil
},
help: "Get a random Dad joke",
})
}
if resp.StatusCode != http.StatusOK {
return dadJokeErr, nil
}
func newDadJoke() (*dadJoke, error) {
req, err := http.NewRequest(http.MethodGet, dadJokeAPI, nil)
if err != nil {
return nil, err
}
req.Header.Add("Accept", "application/json")
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
var dj dadJoke
if err := json.Unmarshal(body, &dj); err != nil {
return "", nil
}
if resp.StatusCode != http.StatusOK {
return nil, errors.New("non-ok status")
}
// Check status again, in case API returned an error
if dj.Status != http.StatusOK {
return dadJokeErr, nil
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return dj.Joke, nil
},
help: "Get a random Dad joke",
var dj *dadJoke
if err := json.Unmarshal(body, &dj); err != nil {
return nil, errors.New("could not unmarshal")
}
// Check status again, in case API returned an error
if dj.Status != http.StatusOK {
return nil, errors.New("API error")
}
return dj, nil
}

169
discord/discord.go

@ -1,34 +1,51 @@
package discord
import (
"fmt"
"os"
"strings"
"time"
"go.etztech.xyz/sedbot/config"
"go.etztech.xyz/sedbot/database"
"github.com/rs/zerolog/log"
"git.jojodev.com/Minecraft/go-serverapi"
"git.jojodev.com/Minecraft/canopeas/config"
"git.jojodev.com/Minecraft/canopeas/database"
"github.com/bwmarrin/discordgo"
"go.jolheiser.com/beaver"
"github.com/dghubble/go-twitter/twitter"
"github.com/dghubble/oauth1"
)
// Register commands to this map
var (
commands = make(map[string]command)
commands = make([]*command, 0)
commandMap = make(map[string]*command)
messageRoleMap = make(map[string]map[string]string)
memeRateLimit *rateLimit
embedColor = 0x007D96
)
type commandInit struct {
session *discordgo.Session
message *discordgo.Message
config *config.Config
database *database.Database
session *discordgo.Session
message *discordgo.Message
config *config.Config
database *database.Database
sapiClient *serverapi.Client
twitterClient *twitter.Client
}
type command struct {
staffOnly bool
deleteInvocation bool
child bool
// TODO Does this really need to exist separately?
validate func(cmd commandInit) bool
run func(cmd commandInit) (string, error)
name string
aliases []string
help string
}
@ -38,17 +55,55 @@ func Bot(cfg *config.Config, db *database.Database) (*discordgo.Session, error)
return nil, err
}
// Init rand
rand.Seed(time.Now().UnixNano())
// Init ServerAPI
sapi := serverapi.NewClient(cfg.ServerAPI.Endpoint, serverapi.WithToken(cfg.ServerAPI.Token))
// Init Twitter
twitterConfig := oauth1.NewConfig(cfg.Twitter.ConsumerKey, cfg.Twitter.ConsumerSecret)
twitterToken := oauth1.NewToken(cfg.Twitter.AccessToken, cfg.Twitter.AccessSecret)
twitterHttpClient := twitterConfig.Client(oauth1.NoContext, twitterToken)
twitterClient := twitter.NewClient(twitterHttpClient)
// Init Unban Schedule
sched := &unbanSchedule{
db: db,
sapi: sapi,
}
go sched.Run()
for _, messageRole := range cfg.MessageRoles {
if messageRoleMap[messageRole.MessageID] == nil {
messageRoleMap[messageRole.MessageID] = make(map[string]string)
}
_ = bot.MessageReactionAdd(messageRole.ChannelID, messageRole.MessageID, messageRole.Emoji)
messageRoleMap[messageRole.MessageID][messageRole.Emoji] = messageRole.RoleID
for _, reaction := range messageRole.Reactions {
_ = bot.MessageReactionAdd(messageRole.ChannelID, messageRole.MessageID, reaction.Emoji)
messageRoleMap[messageRole.MessageID][reaction.Emoji] = reaction.RoleID
}
}
// Init commandMap
if err := Album(cfg); err != nil {
return nil, err
}
Echo(cfg)
for _, c := range commands {
if c.name == "" {
log.Error().Msgf("command is missing a name: %s", c.help)
continue
}
commandMap[c.name] = c
for _, a := range c.aliases {
commandMap[a] = c
}
}
bot.AddHandler(readyHandler())
bot.AddHandler(commandHandler(cfg, db))
bot.AddHandler(joinHandler(cfg))
bot.AddHandler(leaveHandler(cfg))
bot.AddHandler(commandHandler(cfg, db, sapi, twitterClient))
bot.AddHandler(messageHandler(cfg, db))
bot.AddHandler(reactionAddHandler())
bot.AddHandler(reactionRemoveHandler())
@ -60,12 +115,15 @@ func Bot(cfg *config.Config, db *database.Database) (*discordgo.Session, error)
}
memeRateLimit = NewRateLimit(d)
// Intents
bot.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAll)
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)
log.Error().Msgf("could not send typing status: %v", err)
}
}
@ -81,16 +139,17 @@ func sendMessage(s *discordgo.Session, channelID, content string, scrub bool) *d
msg, err = s.ChannelMessageSend(channelID, content)
}
if err != nil {
beaver.Errorf("could not send message: %v", err)
log.Error().Msgf("could not send message: %v", err)
return nil
}
return msg
}
func sendEmbed(s *discordgo.Session, channelID string, embed *discordgo.MessageEmbed) *discordgo.Message {
embed.Color = embedColor
msg, err := s.ChannelMessageSendEmbed(channelID, embed)
if err != nil {
beaver.Errorf("could not send embed: %v", err)
log.Error().Msgf("could not send embed: %v", err)
return nil
}
return msg
@ -109,11 +168,14 @@ func isStaff(authorRoleIDs, staffRoleIDs []string) bool {
func readyHandler() func(s *discordgo.Session, m *discordgo.Ready) {
return func(s *discordgo.Session, r *discordgo.Ready) {
beaver.Infof("https://discord.com/api/oauth2/authorize?client_id=%s&permissions=0&redirect_uri=https://birbmc.com&scope=bot", r.User.ID)
log.Info().Msgf("https://discord.com/api/oauth2/authorize?client_id=%s&permissions=0&redirect_uri=https://birbmc.com&scope=bot", r.User.ID)
// Init status changer
go updateStatus(s)
}
}
func commandHandler(cfg *config.Config, db *database.Database) func(s *discordgo.Session, m *discordgo.MessageCreate) {
func commandHandler(cfg *config.Config, db *database.Database, sapi *serverapi.Client, twitterClient *twitter.Client) func(s *discordgo.Session, m *discordgo.MessageCreate) {
return func(s *discordgo.Session, m *discordgo.MessageCreate) {
// Ignore bots
if m.Author.Bot {
@ -133,25 +195,36 @@ func commandHandler(cfg *config.Config, db *database.Database) func(s *discordgo
cmdArg := strings.ToLower(args[0])
cmd, ok := commands[cmdArg]
cmd, ok := commandMap[cmdArg]
if !ok {
return
}
if cmd.staffOnly && !isStaff(m.Member.Roles, cfg.StaffRoles) {
return
}
cmdInit := commandInit{
session: s,
message: m.Message,
config: cfg,
database: db,
session: s,
message: m.Message,
config: cfg,
database: db,
sapiClient: sapi,
twitterClient: twitterClient,
}
if !cmd.validate(cmdInit) {
sendMessage(s, m.ChannelID, "You cannot run this command.", false)
return
}
if cmd.deleteInvocation {
if err := s.ChannelMessageDelete(m.Message.ChannelID, m.Message.ID); err != nil {
log.Warn().Msgf("could not remove invocation for %s: %v", m.Content, err)
}
}
feedback, err := cmd.run(cmdInit)
if err != nil {
feedback = "Internal error"
beaver.Errorf("error while running %s: %v", cmdArg, err)
log.Error().Msgf("error while running %s: %v", cmdArg, err)
}
if len(feedback) > 0 {
sendMessage(s, m.ChannelID, feedback, false)
@ -170,7 +243,7 @@ func messageHandler(cfg *config.Config, db *database.Database) func(s *discordgo
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)
log.Error().Msgf("could not increment ping for %s: %v", cfg.FiredRole, err)
}
}
}
@ -199,8 +272,56 @@ func reactionHandler(add bool, s *discordgo.Session, m *discordgo.MessageReactio
roleCmd = s.GuildMemberRoleRemove
}
if err := roleCmd(m.GuildID, m.UserID, r); err != nil {
beaver.Errorf("could not modify role %s for user %s", r, m.UserID)
log.Error().Msgf("could not modify role %s for user %s", r, m.UserID)
}
}
}
}
func leaveHandler(cfg *config.Config) func(s *discordgo.Session, m *discordgo.GuildMemberRemove) {
return func(s *discordgo.Session, m *discordgo.GuildMemberRemove) {
sendMessage(s, cfg.LeaveChannel, fmt.Sprintf("%s (%s) left the server. :sob:", m.Mention(), m.User.String()), true)
}
}
func updateStatus(s *discordgo.Session) {
ticker := time.NewTicker(time.Minute * 30)
for {
dj, err := newDadJoke()
if err != nil {
log.Warn().Msgf("could not get new dad joke: %v", err)
} else if err := s.UpdateStatus(1, dj.Joke); err != nil {
log.Warn().Msgf("could not update status: %v", err)
}
<-ticker.C
}
}
func joinHandler(cfg *config.Config) func(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
return func(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
sendMessage(s, cfg.Welcome.Channel, os.Expand(cfg.Welcome.Message, func(s string) string {
switch strings.ToLower(s) {
case "user":
return m.Mention()
default:
return s
}
}), false)
if cfg.Welcome.DM != "" {
dm, err := s.UserChannelCreate(m.User.ID)
if err != nil {
log.Err(err).Msgf("could not create DM with %s", m.User.Username)
return
}
sendMessage(s, dm.ID, os.Expand(cfg.Welcome.DM, func(s string) string {
switch strings.ToLower(s) {
case "user":
return m.Mention()
default:
return s
}
}), false)
}
}
}

64
discord/echo.go

@ -4,45 +4,59 @@ import (
"fmt"
"strings"
"go.etztech.xyz/sedbot/config"
"git.jojodev.com/Minecraft/canopeas/config"
"github.com/bwmarrin/discordgo"
)
func Echo(cfg *config.Config) {
echoes := make([]string, 0)
for _, e := range cfg.Echoes {
echo := e
commands[echo.Name] = command{
for _, echo := range cfg.Echoes {
e := echo // Closure
commands = append(commands, &command{
name: e.Name,
aliases: e.Aliases,
child: true,
validate: func(cmd commandInit) bool {
return true
},
run: func(cmd commandInit) (string, error) {
return echo.Message, nil
return e.Message, nil
},
help: echo.Help,
}
for _, a := range echo.Aliases {
alias := a
commands[alias] = command{
validate: func(cmd commandInit) bool {
return true
},
run: func(cmd commandInit) (string, error) {
return echo.Message, nil
},
help: echo.Help,
}
}
combined := append([]string{echo.Name}, echo.Aliases...)
echoes = append(echoes, fmt.Sprintf("**%s**: %s", strings.Join(combined, ", "), echo.Help))
help: e.Help,
})
}
commands["echoes"] = command{
commands = append(commands, &command{
deleteInvocation: true,
name: "echoes",
validate: func(cmd commandInit) bool {
return true
},
run: func(cmd commandInit) (string, error) {
return strings.Join(echoes, "\n"), nil
embed := &discordgo.MessageEmbed{
Title: "Echo Commands",
Fields: make([]*discordgo.MessageEmbedField, len(cfg.Echoes)),
}
for i, echo := range cfg.Echoes {
name := echo.Name
if len(echo.Aliases) > 0 {
name += fmt.Sprintf(" (%s)", strings.Join(echo.Aliases, ", "))
}
embed.Fields[i] = &discordgo.MessageEmbedField{
Name: name,
Value: echo.Help,
Inline: true,
}
}
channel, err := cmd.session.UserChannelCreate(cmd.message.Author.ID)
if err != nil {
return "", err
}
sendEmbed(cmd.session, channel.ID, embed)
return "", nil
},
help: "Get all dynamic messages",
}
})
}

5
discord/fired.go

@ -5,7 +5,8 @@ import (
)
func init() {
commands["fired"] = command{
commands = append(commands, &command{
name: "fired",
validate: func(cmd commandInit) bool {
return true
},
@ -14,5 +15,5 @@ func init() {
cmd.database.CheckPing(cmd.config.FiredRole)), nil
},
help: "Check how many times Carolyn has been fired.",
}
})
}

77
discord/help.go

@ -3,39 +3,86 @@ package discord
import (
"fmt"
"strings"
"github.com/bwmarrin/discordgo"
)
func init() {
commands["help"] = command{
commands = append(commands, &command{
deleteInvocation: true,
name: "help",
validate: func(cmd commandInit) bool {
return true
},
run: func(cmd commandInit) (string, error) {
args := strings.Fields(cmd.message.Content)[1:]
var resp string
var resp *discordgo.MessageEmbed
if len(args) == 1 {
resp = singleHelp(args[0])
resp = singleHelp(cmd, args[0])
} else {
resp = allHelp()
resp = allHelp(cmd)
}
channel, err := cmd.session.UserChannelCreate(cmd.message.Author.ID)
if err != nil {
return "", err
}
return resp, nil
sendEmbed(cmd.session, channel.ID, 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)
func singleHelp(cmd commandInit, arg string) *discordgo.MessageEmbed {
embed := &discordgo.MessageEmbed{
Title: "Unkown Command",
Color: 0x007D96,
}
c, ok := commandMap[arg]
if !ok {
return embed
}
if c.staffOnly && !isStaff(cmd.message.Member.Roles, cmd.config.StaffRoles) {
return embed
}
return "Unknown command"
embed.Title = c.name
embed.Description = c.help
return embed
}
func allHelp() string {
helps := make([]string, 0)
for n, c := range commands {
helps = append(helps, fmt.Sprintf("%s: %s", n, c.help))
func allHelp(cmd commandInit) *discordgo.MessageEmbed {
staff := isStaff(cmd.message.Member.Roles, cmd.config.StaffRoles)
embed := &discordgo.MessageEmbed{
Title: "SedBot Help",
Fields: make([]*discordgo.MessageEmbedField, 0),
}
if staff {
embed.Description = "Commands with an asterisk (*) are staff-only"
}
for _, c := range commands {
if c.child {
continue
}
cmdName := c.name
if len(c.aliases) > 0 {
cmdName += fmt.Sprintf(" (%s)", strings.Join(c.aliases, ", "))
}
if c.staffOnly {
cmdName = fmt.Sprintf("*%s", cmdName)
}
if !c.staffOnly || staff {
embed.Fields = append(embed.Fields, &discordgo.MessageEmbedField{
Name: cmdName,
Value: c.help,
Inline: true,
})
}
}
return strings.Join(helps, "\n")
return embed
}

20
discord/history.go

@ -5,14 +5,17 @@ import (
"strings"
"time"
"github.com/rs/zerolog/log"
"gitea.com/jolheiser/gojang"
"gitea.com/jolheiser/gojang/rate"
"github.com/bwmarrin/discordgo"
"go.jolheiser.com/beaver"
"go.jolheiser.com/gojang"
"go.jolheiser.com/gojang/rate"
)
func init() {
cmd := command{
commands = append(commands, &command{
name: "history",
aliases: []string{"names"},
validate: func(cmd commandInit) bool {
return true
},
@ -42,13 +45,13 @@ func init() {
if rate.IsRateLimitExceededError(err) {
return "Rate limited by Mojang, slow down!", nil
}
beaver.Errorf("Profile: %v", err)
log.Error().Msgf("Profile: %v", err)
return "Could not contact the Mojang API.", nil
}
names, err := client.UUIDToNameHistory(profile.UUID)
if err != nil {
beaver.Errorf("UUIDToNameHistory: %v", err)
log.Error().Msgf("UUIDToNameHistory: %v", err)
return "Could not contact the Mojang API.", nil
}
@ -79,8 +82,5 @@ func init() {
return "", nil
},
help: "Minecraft name history",
}
commands["history"] = cmd
commands["names"] = cmd
})
}

71
discord/imgur.go

@ -0,0 +1,71 @@
package discord
import (
"fmt"
"strings"
"git.jojodev.com/Minecraft/canopeas/config"
"git.jojodev.com/Minecraft/canopeas/imgur"
"github.com/bwmarrin/discordgo"
)
func Album(cfg *config.Config) error {
for _, a := range cfg.Albums {
images, err := imgur.Get(cfg.ImgurClientID, a.AlbumID)
if err != nil {
return err
}
commands = append(commands, &command{
name: a.Name,
aliases: a.Aliases,
child: true,
validate: func(cmd commandInit) bool {
return true
},
run: func(cmd commandInit) (string, error) {
if !memeRateLimit.Try() {
return "", nil
}
img := images[rand.Intn(len(images))-1]
return img.Link, nil
},
help: a.Help,
})
}
commands = append(commands, &command{
deleteInvocation: true,
name: "albums",
validate: func(cmd commandInit) bool {
return true
},
run: func(cmd commandInit) (string, error) {
embed := &discordgo.MessageEmbed{
Title: "Album Commands",
Fields: make([]*discordgo.MessageEmbedField, len(cfg.Albums)),
}
for i, a := range cfg.Albums {
name := a.Name
if len(a.Aliases) > 0 {
name += fmt.Sprintf(" (%s)", strings.Join(a.Aliases, ", "))
}
embed.Fields[i] = &discordgo.MessageEmbedField{
Name: name,
Value: a.Help,
Inline: true,
}
}
channel, err := cmd.session.UserChannelCreate(cmd.message.Author.ID)
if err != nil {
return "", err
}
sendEmbed(cmd.session, channel.ID, embed)
return "", nil
},
help: "Get all imgur albums",
})
return nil
}

7
discord/inspire.go