Compare commits
38 Commits
rate-insul
...
main
Author | SHA1 | Date |
---|---|---|
jolheiser | 75bd31e00f | |
jolheiser | 986473e1df | |
jolheiser | 449f99cf69 | |
jolheiser | a15bb13ba5 | |
Etzelia | ac862d8d18 | |
Etzelia | 74e49546fe | |
Etzelia | 4fb7cc2928 | |
Etzelia | 8e83885118 | |
Etzelia | cc3ceb6668 | |
Etzelia | 16e0383e39 | |
Etzelia | 3ecd7f04e4 | |
Etzelia | c5066e84e2 | |
Etzelia | c84c523e55 | |
Etzelia | 326b370ca8 | |
Etzelia | dec436805c | |
Etzelia | beb2b4a6aa | |
Etzelia | 2dc22baa28 | |
Etzelia | cf674102b1 | |
Etzelia | d86a1f86b0 | |
Etzelia | bed25234bb | |
Etzelia | 6a891d1ed1 | |
Etzelia | 1a6b6c174a | |
Etzelia | d1600be5d1 | |
Etzelia | a581094959 | |
Etzelia | 8769b33bca | |
Etzelia | 03260f8892 | |
Etzelia | 1b5257e6b9 | |
Etzelia | a9bbc0bf92 | |
Etzelia | e688ba4733 | |
Etzelia | 944e417ddb | |
Etzelia | ff397f14e6 | |
Etzelia | 1683cc6214 | |
Etzelia | 37c6afa8c3 | |
Etzelia | 0f40d84b68 | |
Etzelia | f204ee6e37 | |
Etzelia | 66a418261e | |
Etzelia | 7d6b11ee2f | |
Etzelia | 053921be43 |
|
@ -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"
|
|
@ -1,8 +1,6 @@
|
|||
# GoLand
|
||||
.idea/
|
||||
|
||||
# Generated
|
||||
config/config_default.go
|
||||
|
||||
# sedbot
|
||||
sedbot*
|
||||
# canopeas
|
||||
canopeas*
|
||||
!config/canopeas.example.toml
|
31
Makefile
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
|
12
README.md
12
README.md
|
@ -1,6 +1,6 @@
|
|||
# SedBot
|
||||
# canopeas
|
||||
|
||||
BirbMC Discord generi-bot.
|
||||
Canopy Discord generi-bot.
|
||||
|
||||
## Commands
|
||||
|
||||
|
@ -13,17 +13,13 @@ BirbMC Discord generi-bot.
|
|||
* `inspire` - Get a random "inspirational" message from [InspiroBot](https://inspirobot.me)
|
||||
* `dad` - Get a random dad joke from [icanhazdadjoke](https://icanhazdadjoke.com)
|
||||
* `insult` - The fan favorite returns
|
||||
* `compliment` - The other fan favorite returns
|
||||
* `names <in-game name> [<01/02/2006>]` - Minecraft name history (optionally at a specific time)
|
||||
|
||||
### Moderation
|
||||
|
||||
* `clear [<@user>] <number>` - Clear <number> messages (optionally only by @user)
|
||||
|
||||
## Building
|
||||
|
||||
```text
|
||||
make build-all
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
|
@ -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 = "👎"
|
102
config/config.go
102
config/config.go
|
@ -1,41 +1,79 @@
|
|||
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"`
|
||||
|
||||
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 {
|
||||
Targets []string `toml:"target"`
|
||||
Comparisons []string `toml:"comparison"`
|
||||
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"`
|
||||
Nouns []string `toml:"nouns"`
|
||||
} `toml:"insult"`
|
||||
Compliment struct {
|
||||
Verbs []string `toml:"verbs"`
|
||||
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 {
|
||||
|
@ -45,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)
|
||||
|
@ -64,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
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
if err := tree.Unmarshal(&cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
|
|
@ -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)))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)))
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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",
|
||||
})
|
||||
}
|
|
@ -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",
|
||||
})
|
||||
}
|
|
@ -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,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",
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands = append(commands, &command{
|
||||
name: "compliment",
|
||||
validate: func(cmd commandInit) bool {
|
||||
return true
|
||||
},
|
||||
run: func(cmd commandInit) (string, error) {
|
||||
if !memeRateLimit.Try() {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
fields := strings.Fields(cmd.message.Content)
|
||||
|
||||
var target string
|
||||
if len(fields) > 1 {
|
||||
target = strings.Join(fields[1:], " ")
|
||||
} else if cmd.message.Member.Nick != "" {
|
||||
target = cmd.message.Member.Nick
|
||||
} else {
|
||||
target = cmd.message.Author.Username
|
||||
}
|
||||
|
||||
compliment := fmt.Sprintf("%s, I would %s my %s just to %s.",
|
||||
target,
|
||||
random(cmd.config.Compliment.Verbs),
|
||||
random(cmd.config.Compliment.Nouns),
|
||||
random(cmd.config.Compliment.MinorThings),
|
||||
)
|
||||
|
||||
sendMessage(cmd.session, cmd.message.ChannelID, compliment, true)
|
||||
|
||||
return "", nil
|
||||
},
|
||||
help: "Compliment someone!",
|
||||
})
|
||||
}
|
|
@ -2,8 +2,11 @@ package discord
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -18,47 +21,58 @@ type dadJoke struct {
|
|||
}
|
||||
|
||||
func init() {
|
||||
commands["dad"] = command{
|
||||
commands = append(commands, &command{
|
||||
name: "dad",
|
||||
validate: func(cmd commandInit) bool {
|
||||
return true
|
||||
},
|
||||
run: func(cmd commandInit) (string, error) {
|
||||
if !memeRateLimit.Try() {
|
||||
return "Woah, slow down!", nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, dadJokeAPI, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Add("Accept", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return dadJokeErr, nil
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var dj dadJoke
|
||||
if err := json.Unmarshal(body, &dj); err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Check status again, in case API returned an error
|
||||
if dj.Status != http.StatusOK {
|
||||
dj, err := newDadJoke()
|
||||
if err != nil {
|
||||
log.Warn().Msgf("error getting new dad joke: %v", err)
|
||||
return dadJokeErr, nil
|
||||
}
|
||||
|
||||
return dj.Joke, nil
|
||||
},
|
||||
help: "Get a random Dad joke",
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func newDadJoke() (*dadJoke, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, dadJokeAPI, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("Accept", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("non-ok status")
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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,19 +115,41 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(s *discordgo.Session, channelID, content string) *discordgo.Message {
|
||||
msg, err := s.ChannelMessageSend(channelID, content)
|
||||
func sendMessage(s *discordgo.Session, channelID, content string, scrub bool) *discordgo.Message {
|
||||
var msg *discordgo.Message
|
||||
var err error
|
||||
if scrub {
|
||||
msg, err = s.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{
|
||||
Content: content,
|
||||
AllowedMentions: &discordgo.MessageAllowedMentions{},
|
||||
})
|
||||
} else {
|
||||
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 {
|
||||
log.Error().Msgf("could not send embed: %v", err)
|
||||
return nil
|
||||
}
|
||||
return msg
|
||||
|
@ -91,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 {
|
||||
|
@ -115,28 +195,39 @@ 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.")
|
||||
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)
|
||||
sendMessage(s, m.ChannelID, feedback, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -152,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -174,15 +265,63 @@ func reactionRemoveHandler() func(s *discordgo.Session, m *discordgo.MessageReac
|
|||
func reactionHandler(add bool, s *discordgo.Session, m *discordgo.MessageReaction) {
|
||||
if _, ok := messageRoleMap[m.MessageID]; ok {
|
||||
if r, ok := messageRoleMap[m.MessageID][m.Emoji.APIName()]; ok {
|
||||
var roleCmd func(guildID, userID, roleID string) (err error)
|
||||
var roleCmd func(guildID, userID, roleID string, opts ...discordgo.RequestOption) (err error)
|
||||
if add {
|
||||
roleCmd = s.GuildMemberRoleAdd
|
||||
} else {
|
||||
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.UpdateGameStatus(0, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,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.",
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
return resp, nil
|
||||
|
||||
channel, err := cmd.session.UserChannelCreate(cmd.message.Author.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
return "Unknown command"
|
||||
c, ok := commandMap[arg]
|
||||
if !ok {
|
||||
return embed
|
||||
}
|
||||
|
||||
if c.staffOnly && !isStaff(cmd.message.Member.Roles, cmd.config.StaffRoles) {
|
||||
return embed
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
return strings.Join(helps, "\n")
|
||||
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 embed
|
||||
}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"gitea.com/jolheiser/gojang"
|
||||
"gitea.com/jolheiser/gojang/rate"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands = append(commands, &command{
|
||||
name: "history",
|
||||
aliases: []string{"names"},
|
||||
validate: func(cmd commandInit) bool {
|
||||
return true
|
||||
},
|
||||
run: func(cmd commandInit) (string, error) {
|
||||
args := strings.Fields(cmd.message.Content)
|
||||
if len(args) < 2 {
|
||||
return "You must give this command a Minecraft username", nil
|
||||
}
|
||||
|
||||
sendTyping(cmd.session, cmd.message.ChannelID)
|
||||
|
||||
at := time.Now()
|
||||
if len(args) > 2 {
|
||||
t, err := time.Parse("01/02/2006", args[2])
|
||||
if err != nil {
|
||||
return `Could not parse the time. Use the format 01/30/2006`, nil
|
||||
}
|
||||
at = t
|
||||
}
|
||||
|
||||
client := gojang.New(time.Second * 5)
|
||||
profile, err := client.Profile(args[1], at)
|
||||
if err != nil {
|
||||
if gojang.IsPlayerNotFoundError(err) {
|
||||
return "Could not find a player with that username at that time.", nil
|
||||
}
|
||||
if rate.IsRateLimitExceededError(err) {
|
||||
return "Rate limited by Mojang, slow down!", nil
|
||||
}
|
||||
log.Error().Msgf("Profile: %v", err)
|
||||
return "Could not contact the Mojang API.", nil
|
||||
}
|
||||
|
||||
names, err := client.UUIDToNameHistory(profile.UUID)
|
||||
if err != nil {
|
||||
log.Error().Msgf("UUIDToNameHistory: %v", err)
|
||||
return "Could not contact the Mojang API.", nil
|
||||
}
|
||||
|
||||
var history string
|
||||
for _, name := range names {
|
||||
cleaned := strings.NewReplacer("_", "\\_", "*", "\\*").Replace(name.Name)
|
||||
history += fmt.Sprintf("\n%s", cleaned)
|
||||
if name.ChangedToAt == 0 {
|
||||
history += " (original)"
|
||||
} else {
|
||||
history += fmt.Sprintf(" (%s)", name.ChangedToAtTime().Format("01/02/2006"))
|
||||
}
|
||||
}
|
||||
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Color: 0x7ed321,
|
||||
Thumbnail: &discordgo.MessageEmbedThumbnail{
|
||||
URL: fmt.Sprintf("https://minotar.net/helm/%s/100.png", names.Current().Name),
|
||||
},
|
||||
Fields: []*discordgo.MessageEmbedField{
|
||||
{
|
||||
Name: fmt.Sprintf("%s's Name History", names.Current().Name),
|
||||
Value: history,
|
||||
},
|
||||
},
|
||||
}
|
||||
sendEmbed(cmd.session, cmd.message.ChannelID, embed)
|
||||
return "", nil
|
||||
},
|
||||
help: "Minecraft name history",
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,15 +1,16 @@
|
|||
package discord
|
||||
|
||||
import "go.etztech.xyz/inspiro"
|
||||
import "git.jojodev.com/Minecraft/canopeas/inspiro"
|
||||
|
||||
func init() {
|
||||
commands["inspire"] = command{
|
||||
commands = append(commands, &command{
|
||||
name: "inspire",
|
||||
validate: func(cmd commandInit) bool {
|
||||
return true
|
||||
},
|
||||
run: func(cmd commandInit) (string, error) {
|
||||
if !memeRateLimit.Try() {
|
||||
return "Woah, slow down!", nil
|
||||
return "", nil
|
||||
}
|
||||
|
||||
sendTyping(cmd.session, cmd.message.ChannelID)
|
||||
|
@ -22,5 +23,5 @@ func init() {
|
|||
return img, nil
|
||||
},
|
||||
help: "Get inspired!",
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,40 +2,43 @@ package discord
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
r "math/rand"
|
||||
"time"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands["insult"] = command{
|
||||
commands = append(commands, &command{
|
||||
name: "insult",
|
||||
validate: func(cmd commandInit) bool {
|
||||
return true
|
||||
},
|
||||
run: func(cmd commandInit) (string, error) {
|
||||
if !memeRateLimit.Try() {
|
||||
return "Woah, slow down!", nil
|
||||
return "", nil
|
||||
}
|
||||
|
||||
args, err := cmd.message.ContentWithMoreMentionsReplaced(cmd.session)
|
||||
if err != nil {
|
||||
return "", err
|
||||
fields := strings.Fields(cmd.message.Content)
|
||||
|
||||
var target string
|
||||
if len(fields) > 1 {
|
||||
target = strings.Join(fields[1:], " ")
|
||||
} else if cmd.message.Member.Nick != "" {
|
||||
target = cmd.message.Member.Nick
|
||||
} else {
|
||||
target = cmd.message.Author.Username
|
||||
}
|
||||
|
||||
insult := fmt.Sprintf("%s, your %s looks like %s, you %s %s.",
|
||||
args,
|
||||
target,
|
||||
random(cmd.config.Insult.Targets),
|
||||
random(cmd.config.Insult.Comparisons),
|
||||
random(cmd.config.Insult.Adjectives),
|
||||
random(cmd.config.Insult.Nouns),
|
||||
)
|
||||
return insult, nil
|
||||
|
||||
sendMessage(cmd.session, cmd.message.ChannelID, insult, true)
|
||||
|
||||
return "", nil
|
||||
},
|
||||
help: "Insult someone!",
|
||||
}
|
||||
}
|
||||
|
||||
var rand = r.New(r.NewSource(time.Now().Unix()))
|
||||
|
||||
func random(list []string) string {
|
||||
idx := rand.Intn(len(list) - 1)
|
||||
return list[idx]
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"go.etztech.xyz/go-mcm"
|
||||
"go.etztech.xyz/go-mcm/model/django"
|
||||
"git.jojodev.com/Minecraft/go-mcm"
|
||||
"git.jojodev.com/Minecraft/go-mcm/model/django"
|
||||
|
||||
"git.jojodev.com/Minecraft/canopeas/config"
|
||||
)
|
||||
|
||||
const bannedPlayersFile = "banned-players.json"
|
||||
|
||||
func init() {
|
||||
commands["register"] = command{
|
||||
commands = append(commands, &command{
|
||||
name: "register",
|
||||
validate: func(cmd commandInit) bool {
|
||||
return len(cmd.message.Member.Roles) == 0
|
||||
},
|
||||
|
@ -19,58 +27,116 @@ func init() {
|
|||
return "You must give this command your application username", nil
|
||||
}
|
||||
|
||||
username := args[1]
|
||||
|
||||
sendTyping(cmd.session, cmd.message.ChannelID)
|
||||
|
||||
manager := mcm.NewMCM(cmd.config.MCMToken, cmd.config.MCMURL)
|
||||
manager := mcm.NewMCM(cmd.config.MCM.Token, cmd.config.MCM.URL)
|
||||
models := manager.NewModel()
|
||||
|
||||
players, err := models.Player(models.NewDjangoBuilder().IExact(django.PlayerUsername, args[1]))
|
||||
players, err := models.Player(models.NewDjangoBuilder().IExact(django.PlayerUsername, username))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
apps, err := models.Application(models.NewDjangoBuilder().IExact(django.ApplicationUsername, args[1]))
|
||||
apps, err := models.Application(models.NewDjangoBuilder().IExact(django.ApplicationUsername, username))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var nickname string
|
||||
var accepted *bool
|
||||
if len(players) > 0 {
|
||||
nickname = players[0].Username
|
||||
player := players[0]
|
||||
|
||||
// Check for a ban
|
||||
ban, err := findBan(cmd.config, player.UUID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ban != nil {
|
||||
return fmt.Sprintf("You are currently banned: **%s**", ban.Reason), nil
|
||||
}
|
||||
|
||||
nickname = player.Username
|
||||
if len(apps) == 0 {
|
||||
apps, err = models.Application(models.NewDjangoBuilder().Eq(django.ApplicationID, players[0].ApplicationID))
|
||||
if len(apps) == 0 {
|
||||
apps, err = models.Application(models.NewDjangoBuilder().Exact(django.ApplicationID, player.ApplicationID))
|
||||
if err != nil {
|
||||
return "Something went wrong, please contact staff", nil
|
||||
}
|
||||
if len(apps) == 0 {
|
||||
return "No application found for that player", nil
|
||||
}
|
||||
}
|
||||
} else if len(apps) > 0 {
|
||||
if apps[0].Accepted != nil && *apps[0].Accepted {
|
||||
return "Please join the server and then re-try this command", nil
|
||||
}
|
||||
} else {
|
||||
}
|
||||
if len(apps) == 0 {
|
||||
return "No player or applications found for that username", nil
|
||||
}
|
||||
accepted = apps[0].Accepted
|
||||
|
||||
accepted := apps[0].Accepted
|
||||
if accepted == nil {
|
||||
return "Your application is still being reviewed, hang tight", nil
|
||||
} else if !*accepted {
|
||||
return "Your application was denied, good luck finding a new server", nil
|
||||
}
|
||||
|
||||
// Accepted
|
||||
if err := cmd.session.GuildMemberNickname(cmd.message.GuildID, cmd.message.Author.ID, nickname); err != nil {
|
||||
return "", err
|
||||
if nickname != "" {
|
||||
// Accepted, check for dupe user
|
||||
guild, err := cmd.session.State.Guild(cmd.message.GuildID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, member := range guild.Members {
|
||||
nick := member.Nick
|
||||
if nick == "" {
|
||||
nick = member.User.Username
|
||||
}
|
||||
if strings.EqualFold(nickname, nick) && cmd.message.Author.ID != member.User.ID {
|
||||
return "A member with that name already exists in this Discord. Please contact staff.", nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := cmd.session.GuildMemberNickname(cmd.message.GuildID, cmd.message.Author.ID, nickname); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
}
|
||||
if err := cmd.session.GuildMemberRoleAdd(cmd.message.GuildID, cmd.message.Author.ID, cmd.config.RegisterRole); err != nil {
|
||||
if err := cmd.session.GuildMemberRoleAdd(cmd.message.GuildID, cmd.message.Author.ID, cmd.config.Register.Role); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Don't return feedback because this goes in a different channel
|
||||
sendMessage(cmd.session, cmd.config.RegisteredChannel, fmt.Sprintf("Welcome, **%s**!", cmd.message.Author.Mention()))
|
||||
sendMessage(cmd.session, cmd.config.Register.WelcomeChannel, fmt.Sprintf("Welcome, **%s**!", cmd.message.Author.Mention()), false)
|
||||
return "", nil
|
||||
},
|
||||
help: "Register yourself with the Discord",
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type Ban struct {
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Created string `json:"created"`
|
||||
Source string `json:"source"`
|
||||
Expires string `json:"expires"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
func findBan(cfg *config.Config, uuid string) (*Ban, error) {
|
||||
banData, err := ioutil.ReadFile(filepath.Join(cfg.MCPath, bannedPlayersFile))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var bans []*Ban
|
||||
if err := json.Unmarshal(banData, &bans); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ban := range bans {
|
||||
if uuid == ban.UUID {
|
||||
return ban, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.com/jolheiser/gojang/query"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands = append(commands, &command{
|
||||
name: "status",
|
||||
aliases: []string{"version", "online"},
|
||||
validate: func(cmd commandInit) bool {
|
||||
return true
|
||||
},
|
||||
run: func(cmd commandInit) (string, error) {
|
||||
server := query.NewServer(cmd.config.Server.Address, cmd.config.Server.Port)
|
||||
|
||||
q, err := server.Query(query.DefaultTimeout, query.DefaultDeadline)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: fmt.Sprintf("Server Status for `%s`", cmd.config.Server.Address),
|
||||
Description: q.MOTD,
|
||||
Fields: []*discordgo.MessageEmbedField{
|
||||
{
|
||||
Name: "Version",
|
||||
Value: fmt.Sprintf("%s %s", q.ServerMod.Name, q.Version),
|
||||
Inline: true,
|
||||
},
|
||||
{
|
||||
Name: "~~Players~~ Birbs Online",
|
||||
Value: fmt.Sprintf("%d / %d", q.CurrentPlayers, q.MaxPlayers),
|
||||
Inline: true,
|
||||
},
|
||||
{
|
||||
Name: "~~Players~~ Birbs",
|
||||
Value: strings.Join(q.Players, ", "),
|
||||
},
|
||||
},
|
||||
}
|
||||
sendEmbed(cmd.session, cmd.message.ChannelID, embed)
|
||||
return "", nil
|
||||
},
|
||||
help: "Get the server status",
|
||||
})
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands = append(commands, &command{
|
||||
staffOnly: true,
|
||||
name: "tweet",
|
||||
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 tweet", nil
|
||||
}
|
||||
|
||||
message := strings.Join(args[1:], " ")
|
||||
tweet, resp, err := cmd.twitterClient.Statuses.Update(message, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.StatusCode%100 != 2 {
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://twitter.com/%d/status/%d", tweet.User.ID, tweet.ID), nil
|
||||
},
|
||||
help: "Send a tweet from the BirbMC Twitter",
|
||||
})
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.jojodev.com/Minecraft/canopeas/database"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands = append(commands, &command{
|
||||
staffOnly: true,
|
||||
name: "unban",
|
||||
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) < 3 {
|
||||
return "This command requires an in-game username and number of days (use 0 days to unban on next schedule).", nil
|
||||
}
|
||||
days, err := strconv.Atoi(args[2])
|
||||
if err != nil {
|
||||
return "number of days must be an integer", nil
|
||||
}
|
||||
target := args[1]
|
||||
expiration := time.Now().Add(time.Hour * 24 * time.Duration(days))
|
||||
record := database.UnbanRecord{
|
||||
Username: target,
|
||||
Expiration: expiration,
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s will be unbanned on %s", target, record.ExpirationString()),
|
||||
cmd.database.AddUnban(record)
|
||||
},
|
||||
help: "Unban a player",
|
||||
})
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands = append(commands, &command{
|
||||
staffOnly: true,
|
||||
name: "unbans",
|
||||
validate: func(cmd commandInit) bool {
|
||||
return isStaff(cmd.message.Member.Roles, cmd.config.StaffRoles)
|
||||
},
|
||||
run: func(cmd commandInit) (string, error) {
|
||||
unbans := cmd.database.ListUnbans()
|
||||
if len(unbans) == 0 {
|
||||
return "There are no pending unbans", nil
|
||||
}
|
||||
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Fields: make([]*discordgo.MessageEmbedField, len(unbans)),
|
||||
}
|
||||
for i, record := range cmd.database.ListUnbans() {
|
||||
embed.Fields[i] = &discordgo.MessageEmbedField{
|
||||
Name: record.Username,
|
||||
Value: record.ExpirationString(),
|
||||
Inline: true,
|
||||
}
|
||||
}
|
||||
|
||||
sendEmbed(cmd.session, cmd.message.ChannelID, embed)
|
||||
return "", nil
|
||||
},
|
||||
help: "Check the unban scheduler",
|
||||
})
|
||||
}
|
|
@ -1,6 +1,16 @@
|
|||
package discord
|
||||
|
||||
import "time"
|
||||
import (
|
||||
r "math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"git.jojodev.com/Minecraft/go-serverapi"
|
||||
|
||||
"git.jojodev.com/Minecraft/canopeas/database"
|
||||
)
|
||||
|
||||
type rateLimit struct {
|
||||
rate time.Duration
|
||||
|
@ -22,3 +32,53 @@ func (r *rateLimit) Try() bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var rand = r.New(r.NewSource(time.Now().Unix()))
|
||||
|
||||
func random(list []string) string {
|
||||
size := len(list)
|
||||
if size == 0 {
|
||||
return ""
|
||||
} else if size == 1 {
|
||||
return list[0]
|
||||
}
|
||||
idx := rand.Intn(size)
|
||||
return list[idx]
|
||||
}
|
||||
|
||||
type unbanSchedule struct {
|
||||
db *database.Database
|
||||
sapi *serverapi.Client
|
||||
}
|
||||
|
||||
func (u *unbanSchedule) Run() {
|
||||
u.check()
|
||||
ticker := time.NewTicker(time.Minute * 5)
|
||||
for {
|
||||
<-ticker.C
|
||||
u.check()
|
||||
}
|
||||
}
|
||||
|
||||
func (u *unbanSchedule) check() {
|
||||
log.Debug().Msg("Running unban schedule")
|
||||
now := time.Now()
|
||||
for _, record := range u.db.ListUnbans() {
|
||||
if now.After(record.Expiration) {
|
||||
log.Info().Msgf("Unbanning %s", record.Username)
|
||||
unban := serverapi.Unban{
|
||||
Target: record.Username,
|
||||
}
|
||||
status, err := u.sapi.Unban(unban)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("")
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
log.Error().Msgf("ServerAPI returned status %d when trying to ban %s", status, record.Username)
|
||||
}
|
||||
if err := u.db.RemoveUnban(record.Username); err != nil {
|
||||
log.Error().Msgf("could not remove unban for %s in database", record.Username)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ func TestRateLimit(t *testing.T) {
|
|||
}
|
||||
|
||||
now()
|
||||
limit := NewRateLimit(time.Second * 2)
|
||||
limit := NewRateLimit(time.Second)
|
||||
|
||||
if ok := limit.Try(); !ok {
|
||||
now()
|
||||
|
@ -25,7 +25,7 @@ func TestRateLimit(t *testing.T) {
|
|||
t.FailNow()
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
|
||||
if ok := limit.Try(); ok {
|
||||
now()
|
||||
|
@ -33,7 +33,7 @@ func TestRateLimit(t *testing.T) {
|
|||
t.FailNow()
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
|
||||
if ok := limit.Try(); !ok {
|
||||
now()
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands = append(commands, &command{
|
||||
deleteInvocation: true,
|
||||
name: "welcome",
|
||||
validate: func(cmd commandInit) bool {
|
||||
return isStaff(cmd.message.Member.Roles, cmd.config.StaffRoles)
|
||||
},
|
||||
run: func(cmd commandInit) (string, error) {
|
||||
orphans := make([]*discordgo.Member, 0)
|
||||
|
||||
members, err := cmd.session.GuildMembers(cmd.message.GuildID, "", 1000)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, member := range members {
|
||||
if len(member.Roles) == 0 {
|
||||
orphans = append(orphans, member)
|
||||
}
|
||||
}
|
||||
|
||||
resp := "There are no members without roles!"
|
||||
if len(orphans) > 0 {
|
||||
resp = "```\n"
|
||||
for _, orphan := range orphans {
|
||||
resp += fmt.Sprintf("<@%s>\n", orphan.User.ID)
|
||||
}
|
||||
resp += "```"
|
||||
}
|
||||
|
||||
channel, err := cmd.session.UserChannelCreate(cmd.message.Author.ID)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if _, err := cmd.session.ChannelMessageSend(channel.ID, resp); err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
},
|
||||
help: "Get a list of people with no roles",
|
||||
})
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.com/jolheiser/xkcd"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands = append(commands, &command{
|
||||
name: "xkcd",
|
||||
validate: func(cmd commandInit) bool {
|
||||
return true
|
||||
},
|
||||
run: func(cmd commandInit) (string, error) {
|
||||
if !memeRateLimit.Try() {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
client := xkcd.New()
|
||||
|
||||
var comic *xkcd.Comic
|
||||
var err error
|
||||
args := strings.Fields(cmd.message.Content)
|
||||
if len(args) < 2 {
|
||||
comic, err = client.Current(context.Background())
|
||||
} else {
|
||||
comicNum, err := strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
return "", errors.New("this command only accepts a number or no arguments")
|
||||
}
|
||||
comic, err = client.Comic(context.Background(), comicNum)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d: %s\n%s\n%s", comic.Num, comic.SafeTitle, comic.Alt, comic.Img), nil
|
||||
},
|
||||
help: "Get an xkcd comic",
|
||||
})
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package falseknees
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Client is a FalseKnees client
|
||||
type Client struct {
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
// New returns a new Client
|
||||
func New(opts ...ClientOption) *Client {
|
||||
c := &Client{
|
||||
http: http.DefaultClient,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// ClientOption is options for a Client
|
||||
type ClientOption func(*Client)
|
||||
|
||||
// WithHTTP is a ClientOption for using a different http.Client
|
||||
func WithHTTP(client *http.Client) ClientOption {
|
||||
return func(c *Client) {
|
||||
c.http = client
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
package falseknees
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
updateInterval = time.Minute * 30
|
||||
baseURL = "https://www.falseknees.com/"
|
||||
currentRe = regexp.MustCompile(`window\.location\.href.+"(.+)\.html"`)
|
||||
imageRe = regexp.MustCompile(`src="(imgs.+\.png)".+title="(.+)"`)
|
||||
|
||||
current int
|
||||
lastUpdate time.Time
|
||||
)
|
||||
|
||||
// Comic is a FalseKnees comic
|
||||
type Comic struct {
|
||||
Num int
|
||||
Title string
|
||||
Img string
|
||||
}
|
||||
|
||||
// Comic returns a specific Comic
|
||||
func (c *Client) Comic(ctx context.Context, num int) (*Comic, error) {
|
||||
return c.comic(ctx, num)
|
||||
}
|
||||
|
||||
// Current returns the current Comic
|
||||
func (c *Client) Current(ctx context.Context) (*Comic, error) {
|
||||
if err := c.updateCurrent(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Comic(ctx, current)
|
||||
}
|
||||
|
||||
// Random returns a random Comic
|
||||
func (c *Client) Random(ctx context.Context) (*Comic, error) {
|
||||
if err := c.updateCurrent(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
return c.Comic(ctx, rand.Intn(current)+1)
|
||||
}
|
||||
|
||||
func (c *Client) updateCurrent(ctx context.Context) error {
|
||||
now := time.Now()
|
||||
if !lastUpdate.IsZero() && lastUpdate.After(now.Add(-updateInterval)) {
|
||||
return nil
|
||||
}
|
||||
lastUpdate = now
|
||||
|
||||
u := fmt.Sprintf("%sindex.html", baseURL)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("could not get page for index: %s", resp.Status)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
match := currentRe.FindStringSubmatch(string(body))
|
||||
if len(match) == 0 {
|
||||
return errors.New("could not find current comic")
|
||||
}
|
||||
|
||||
curr, err := strconv.Atoi(match[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
current = curr
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) comic(ctx context.Context, num int) (*Comic, error) {
|
||||
u := fmt.Sprintf("%s%d.html", baseURL, num)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("could not get page for comic %d: %s", num, resp.Status)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
match := imageRe.FindStringSubmatch(string(body))
|
||||
if len(match) == 0 {
|
||||
return nil, fmt.Errorf("could not find comic #%d", num)
|
||||
}
|
||||
|
||||
return &Comic{
|
||||
Num: num,
|
||||
Title: match[2],
|
||||
Img: fmt.Sprintf("%s%s", baseURL, match[1]),
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,483 @@
|
|||
package falseknees
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
server *httptest.Server
|
||||
client *Client
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/index.html" {
|
||||
_, _ = w.Write(indexHTML)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/389.html" {
|
||||
_, _ = w.Write(currentHTML)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/252.html" {
|
||||
_, _ = w.Write(bunnyHTML)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
server = httptest.NewServer(http.HandlerFunc(handler))
|
||||
baseURL = server.URL + "/"
|
||||
currentComic.Img = fmt.Sprintf("%simgs/389.png", baseURL)
|
||||
bunnyComic.Img = fmt.Sprintf("%simgs/252.png", baseURL)
|
||||
client = New()
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestCurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
comic, err := client.Current(context.Background())
|
||||
if err != nil {
|
||||
t.Logf("could not get current comic: %v\n", err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if *comic != currentComic {
|
||||
t.Log("comic does not match test data")
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestComic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
comic, err := client.Comic(context.Background(), 252)
|
||||
if err != nil {
|
||||
t.Logf("could not get comic 252: %v\n", err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if *comic != bunnyComic {
|
||||
t.Log("comic does not match test data")
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
currentComic = Comic{
|
||||
Num: 389,
|
||||
Title: "that's the good stuff",
|
||||
}
|
||||
bunnyComic = Comic{
|
||||
Num: 252,
|
||||
Title: "Spring is the fucking greatest shit",
|
||||
}
|
||||
|
||||
indexHTML = []byte(`<!DOCTYPE html
|
||||
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<head>
|
||||
<meta name="description"
|
||||
content="False Knees is a webcomic written by Joshua Barkman. All silly nonsense is my own." />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
|
||||
<meta http-equiv="refresh" content="0; URL=389.html" />
|
||||
<script type="text/javascript">
|
||||
window.location.href = "389.html"
|
||||
</script>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="imgs/favicon.png" sizes="96x96">
|
||||
<!-- Facebook Meta tags -->
|
||||
<meta property="og:title" content="False Knees" />
|
||||
<meta property="og:type" content="blog" />
|
||||
<meta property="og:url" content="http://www.falseknees.com/index.html" />
|
||||
<meta property="og:image" content="http://www.falseknees.com/imgs/389.png" />
|
||||
<meta property="og:site_name" content="False Knees" />
|
||||
<meta property="fb:admins" content="1646220005" />
|
||||
|
||||
<link rel="image_src" href="imgs/389.png" />
|
||||
<link type="text/css" rel="stylesheet" href="stylesheet.css" />
|
||||
<title>Page Redirection</title>
|
||||
|
||||
<!-- Google Analytics -->
|
||||
<script type="text/javascript">
|
||||
var _gaq = _gaq || [];
|
||||
var pluginUrl =
|
||||
'//www.google-analytics.com/plugins/ga/inpage_linkid.js';
|
||||
_gaq.push(['_require', 'inpage_linkid', pluginUrl]);
|
||||
_gaq.push(['_setAccount', 'UA-37345913-1']);
|
||||
_gaq.push(['_setDomainName', 'falseknees.com']);
|
||||
_gaq.push(['_trackPageview']);
|
||||
|
||||
(function() {
|
||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="fb-root"></div>
|
||||
<script>
|
||||
(function(d, s, id) {
|
||||
var js, fjs = d.getElementsByTagName(s)[0];
|
||||
if (d.getElementById(id)) return;
|
||||
js = d.createElement(s); js.id = id;
|
||||
js.src = "//connect.facebook.net/en_GB/sdk.js#xfbml=1&version=v2.0";
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
}(document, 'script', 'facebook-jssdk'));
|
||||
</script>
|
||||
|
||||
<!-- Title Image -->
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td>
|
||||
<div id="title">
|
||||
<a href="index.html"><img src="imgs/falseknees.png" width="800" /></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- New Button Placement -->
|
||||
<div align="center">
|
||||
<p>
|
||||
<table>
|
||||
<tr>
|
||||
<td><a href="about.html"
|
||||
title=""><img src="imgs/aboutoff.png" height="50" alt="About" onmouseover="this.src='imgs/abouton.png'" onmouseout="this.src='imgs/aboutoff.png'" /></a>
|
||||
</td>
|
||||
<td><a title=""><img src="imgs/stara.png" height="50" alt="" /></a></td>
|
||||
<td><a href="https://false-knees.myshopify.com/"
|
||||
title=""><img src="imgs/store.png" height="50" alt="Store" onmouseover="this.src='imgs/storeon.png'" onmouseout="this.src='imgs/store.png'" /></a>
|
||||
</td>
|
||||
<td><a title=""><img src="imgs/starb.png" height="50" alt="" /></a></td>
|
||||
<td><a href="https://www.patreon.com/falseknees?ty=h"><img src="imgs/patron.png" height="50" onmouseover="this.src='imgs/patroff.png'" onmouseout="this.src='imgs/patron.png'"/></a>
|
||||
</td>
|
||||
<td><a title=""><img src="imgs/starc.png" height="50" alt="" /></a></td>
|
||||
<td><a href="book.html"
|
||||
title=""><img src="imgs/book.png" height="40" alt="About" onmouseover="this.src='imgs/bookon.png'" onmouseout="this.src='imgs/book.png'" /></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Comic -->
|
||||
<div>
|
||||
<img src="imgs/389.png" width="600" title="that's the good stuff" />
|
||||
</div>
|
||||
|
||||
<!-- Descriptive Text Box -->
|
||||
<!-- Descriptive Text Box -->
|
||||
<div align="center">
|
||||
</div>
|
||||
|
||||
<!-- Navigation Buttons -->
|
||||
<div align="center">
|
||||
<p>
|
||||
<table>
|
||||
<tr>
|
||||
<td><a href="1.html"
|
||||
title=""><img src="imgs/first.png" height="60" alt="First" onmouseover="this.src='imgs/firston.png'" onmouseout="this.src='imgs/first.png'" /></a>
|
||||
</td>
|
||||
<td><a href="388.html"
|
||||
title=""><img src="imgs/previous.png" height="60" alt="Previous" onmouseover="this.src='imgs/previouson.png'" onmouseout="this.src='imgs/previous.png'" /></a>
|
||||
</td>
|
||||
<td><a href="archive.html"
|
||||
title=""><img src="imgs/archive.png" height="60" alt="Archive" onmouseover="this.src='imgs/archive2.png'" onmouseout="this.src='imgs/archive.png'" /></a>
|
||||
</td>
|
||||
<td><a href="index.html"
|
||||
title=""><img src="imgs/next.png" height="60" alt="Next" onmouseover="this.src='imgs/nexton.png'" onmouseout="this.src='imgs/next.png'" /></a>
|
||||
</td>
|
||||
<td><a href="index.html"
|
||||
title=""><img src="imgs/last.png" height="60" alt="Last" onmouseover="this.src='imgs/laston.png'" onmouseout="this.src='imgs/last.png'" /></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Social Media -->
|
||||
<table class="social" align="center">
|
||||
<tr>
|
||||
<td><a href="http://falseknees.tumblr.com/"><img src="imgs/TumblrButton.png" width="60" onmouseover="this.src='imgs/TumblrButtonOn.png'" onmouseout="this.src='imgs/TumblrButton.png'"/></a>
|
||||
</td>
|
||||
<td><a href="https://instagram.com/FalseKnees"><img src="imgs/instagram.png" width="60" onmouseover="this.src='imgs/instagramon.png'" onmouseout="this.src='imgs/instagram.png'"/></a>
|
||||
</td>
|
||||
<td><a href="https://www.facebook.com/FalseKnees?ref=hl"><img src="imgs/FacebookButton.png" width="60" onmouseover="this.src='imgs/FacebookButtonOn.png'" onmouseout="this.src='imgs/FacebookButton.png'"/></a>
|
||||
</td>
|
||||
<td><a href="http://www.webtoons.com/en/challenge/false-knees/list?title_no=79544"><img src="imgs/webtooff.png" width="60" onmouseover="this.src='imgs/webtoon.png'" onmouseout="this.src='imgs/webtooff.png'"/></a>
|
||||
</td>
|
||||
<td><a href="https://tapas.io/series/FalseKnees"><img src="imgs/tap.png" width="60" onmouseover="this.src='imgs/tapon.png'" onmouseout="this.src='imgs/tap.png'"/></a>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Copyright -->
|
||||
<div>
|
||||
<p>False Knees © 2013-whenever Joshua Barkman</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>`)
|
||||
|
||||
currentHTML = []byte(`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta name="description" content="False Knees is a webcomic written by Joshua Barkman. All silly nonsense is my own." />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="imgs/favicon.png" sizes="96x96">
|
||||
<!-- Facebook Meta tags -->
|
||||
<meta property="og:title" content="False Knees" />
|
||||
<meta property="og:type" content="blog" />
|
||||
<meta property="og:url" content="http://www.falseknees.com/389.html" />
|
||||
<meta property="og:image" content="http://www.falseknees.com/imgs/389.png" />
|
||||
<meta property="og:site_name" content="False Knees" />
|
||||
<meta property="fb:admins" content="1646220005" />
|
||||
|
||||
<link rel="image_src" href="imgs/389.png" />
|
||||
<link type="text/css" rel="stylesheet" href="stylesheet.css" />
|
||||
<title>False Knees</title>
|
||||
|
||||
<!-- Google Analytics -->
|
||||
<script type="text/javascript">
|
||||
|
||||
var _gaq = _gaq || [];
|
||||
var pluginUrl =
|
||||
'//www.google-analytics.com/plugins/ga/inpage_linkid.js';
|
||||
_gaq.push(['_require', 'inpage_linkid', pluginUrl]);
|
||||
_gaq.push(['_setAccount', 'UA-37345913-1']);
|
||||
_gaq.push(['_setDomainName', 'falseknees.com']);
|
||||
_gaq.push(['_trackPageview']);
|
||||
|
||||
(function() {
|
||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="fb-root"></div>
|
||||
<script>(function(d, s, id) {
|
||||
var js, fjs = d.getElementsByTagName(s)[0];
|
||||
if (d.getElementById(id)) return;
|
||||
js = d.createElement(s); js.id = id;
|
||||
js.src = "//connect.facebook.net/en_GB/sdk.js#xfbml=1&version=v2.0";
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
}(document, 'script', 'facebook-jssdk'));</script>
|
||||
|
||||
<!-- Title Image -->
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td><div id="title">
|
||||
<a href="index.html"><img src="imgs/falseknees.png" width="800" /></a>
|
||||
</div></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- New Button Placement -->
|
||||
<div align="center">
|
||||
<p>
|
||||
<table>
|
||||
<tr>
|
||||
<td><a href="about.html" title=""><img src="imgs/aboutoff.png" height="50" alt="About" onmouseover="this.src='imgs/abouton.png'" onmouseout="this.src='imgs/aboutoff.png'" /></a></td>
|
||||
<td><a title=""><img src="imgs/stara.png" height="50" alt="" /></a></td>
|
||||
<td><a href="https://false-knees.myshopify.com/" title=""><img src="imgs/store.png" height="50" alt="Store" onmouseover="this.src='imgs/storeon.png'" onmouseout="this.src='imgs/store.png'" /></a></td>
|
||||
<td><a title=""><img src="imgs/starb.png" height="50" alt="" /></a></td>
|
||||
<td><a href="https://www.patreon.com/falseknees?ty=h"><img src="imgs/patron.png" height="50" onmouseover="this.src='imgs/patroff.png'" onmouseout="this.src='imgs/patron.png'"/></a></td>
|
||||
<td><a title=""><img src="imgs/starc.png" height="50" alt="" /></a></td>
|
||||
<td><a href="book.html" title=""><img src="imgs/book.png" height="40" alt="About" onmouseover="this.src='imgs/bookon.png'" onmouseout="this.src='imgs/book.png'" /></a></td>
|
||||
</tr>
|
||||
</table></p>
|
||||
</div>
|
||||
|
||||
<!-- Comic -->
|
||||
<div>
|
||||
<img src="imgs/389.png" width="600" title="that's the good stuff" />
|
||||
</div>
|
||||
|
||||
<!-- Descriptive Text Box -->
|
||||
<!-- Descriptive Text Box -->
|
||||
<div align="center">
|
||||
</div>
|
||||
|
||||
<!-- Navigation Buttons -->
|
||||
<div align="center">
|
||||
<p>
|
||||
<table>
|
||||
<tr>
|
||||
<td><a href="1.html" title=""><img src="imgs/first.png" height="60" alt="First" onmouseover="this.src='imgs/firston.png'" onmouseout="this.src='imgs/first.png'" /></a></td>
|
||||
<td><a href="388.html" title=""><img src="imgs/previous.png" height="60" alt="Previous" onmouseover="this.src='imgs/previouson.png'" onmouseout="this.src='imgs/previous.png'" /></a></td>
|
||||
<td><a href="archive.html" title=""><img src="imgs/archive.png" height="60" alt="Archive" onmouseover="this.src='imgs/archive2.png'" onmouseout="this.src='imgs/archive.png'" /></a></td>
|
||||
<td><a href="389.html" title=""><img src="imgs/next.png" height="60" alt="Next" onmouseover="this.src='imgs/nexton.png'" onmouseout="this.src='imgs/next.png'" /></a></td>
|
||||
<td><a href="index.html" title=""><img src="imgs/last.png" height="60" alt="Last" onmouseover="this.src='imgs/laston.png'" onmouseout="this.src='imgs/last.png'" /></a></td>
|
||||
</tr>
|
||||
</table></p>
|
||||
</div>
|
||||
|
||||
<!-- Social Media -->
|
||||
<table class="social" align="center">
|
||||
<tr>
|
||||
<td><a href="http://falseknees.tumblr.com/"><img src="imgs/TumblrButton.png" width="60" onmouseover="this.src='imgs/TumblrButtonOn.png'" onmouseout="this.src='imgs/TumblrButton.png'"/></a>
|
||||
</td>
|
||||
<td><a href="https://instagram.com/FalseKnees"><img src="imgs/instagram.png" width="60" onmouseover="this.src='imgs/instagramon.png'" onmouseout="this.src='imgs/instagram.png'"/></a>
|
||||
</td>
|
||||
<td><a href="https://www.facebook.com/FalseKnees?ref=hl"><img src="imgs/FacebookButton.png" width="60" onmouseover="this.src='imgs/FacebookButtonOn.png'" onmouseout="this.src='imgs/FacebookButton.png'"/></a>
|
||||
</td>
|
||||
<td><a href="http://www.webtoons.com/en/challenge/false-knees/list?title_no=79544"><img src="imgs/webtooff.png" width="60" onmouseover="this.src='imgs/webtoon.png'" onmouseout="this.src='imgs/webtooff.png'"/></a>
|
||||
</td>
|
||||
<td><a href="https://tapas.io/series/FalseKnees"><img src="imgs/tap.png" width="60" onmouseover="this.src='imgs/tapon.png'" onmouseout="this.src='imgs/tap.png'"/></a>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Copyright -->
|
||||
<div>
|
||||
<p>False Knees © 2013-whenever Joshua Barkman</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
|
||||
bunnyHTML = []byte(`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta name="description" content="False Knees is a webcomic written by Joshua Barkman. All silly nonsense is my own." />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="imgs/favicon.png" sizes="96x96">
|
||||
<!-- Facebook Meta tags -->
|
||||
<meta property="og:title" content="False Knees" />
|
||||
<meta property="og:type" content="blog" />
|
||||
<meta property="og:url" content="http://www.falseknees.com/252.html" />
|
||||
<meta property="og:image" content="http://www.falseknees.com/imgs/252.png" />
|
||||
<meta property="og:site_name" content="False Knees" />
|
||||
<meta property="fb:admins" content="1646220005" />
|
||||
|
||||
<link rel="image_src" href="imgs/252.png" />
|
||||
<link type="text/css" rel="stylesheet" href="stylesheet.css" />
|
||||
<title>False Knees</title>
|
||||
|
||||
<!-- Google Analytics -->
|
||||
<script type="text/javascript">
|
||||
|
||||
var _gaq = _gaq || [];
|
||||
var pluginUrl =
|
||||
'//www.google-analytics.com/plugins/ga/inpage_linkid.js';
|
||||
_gaq.push(['_require', 'inpage_linkid', pluginUrl]);
|
||||
_gaq.push(['_setAccount', 'UA-37345913-1']);
|
||||
_gaq.push(['_setDomainName', 'falseknees.com']);
|
||||
_gaq.push(['_trackPageview']);
|
||||
|
||||
(function() {
|
||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="fb-root"></div>
|
||||
<script>(function(d, s, id) {
|
||||
var js, fjs = d.getElementsByTagName(s)[0];
|
||||
if (d.getElementById(id)) return;
|
||||
js = d.createElement(s); js.id = id;
|
||||
js.src = "//connect.facebook.net/en_GB/sdk.js#xfbml=1&version=v2.0";
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
}(document, 'script', 'facebook-jssdk'));</script>
|
||||
|
||||
<!-- Title Image -->
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td><div id="title">
|
||||
<a href="index.html"><img src="imgs/falseknees.png" width="800" /></a>
|
||||
</div></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- New Button Placement -->
|
||||
<div align="center">
|
||||
<p>
|
||||
<table>
|
||||
<tr>
|
||||
<td><a href="about.html" title=""><img src="imgs/aboutoff.png" height="50" alt="About" onmouseover="this.src='imgs/abouton.png'" onmouseout="this.src='imgs/aboutoff.png'" /></a></td>
|
||||
<td><a title=""><img src="imgs/stara.png" height="50" alt="" /></a></td>
|
||||
<td><a href="https://false-knees.myshopify.com/collections/comic-prints" title=""><img src="imgs/store.png" height="50" alt="Store" onmouseover="this.src='imgs/storeon.png'" onmouseout="this.src='imgs/store.png'" /></a></td>
|
||||
<td><a title=""><img src="imgs/starb.png" height="50" alt="" /></a></td>
|
||||
<td><a href="https://www.patreon.com/falseknees?ty=h"><img src="imgs/patron.png" height="50" onmouseover="this.src='imgs/patroff.png'" onmouseout="this.src='imgs/patron.png'"/></a></td>
|
||||
<td><a title=""><img src="imgs/starc.png" height="50" alt="" /></a></td>
|
||||
<td><a href="book.html" title=""><img src="imgs/book.png" height="40" alt="About" onmouseover="this.src='imgs/bookon.png'" onmouseout="this.src='imgs/book.png'" /></a></td>
|
||||
</tr>
|
||||
</table></p>
|
||||
</div>
|
||||
|
||||
<!-- Comic -->
|
||||
<div>
|
||||
<img src="imgs/252.png" width="600" title="Spring is the fucking greatest shit" />
|
||||
</div>
|
||||
|
||||
<!-- Descriptive Text Box -->
|
||||
<!-- Descriptive Text Box -->
|
||||
<div align="center">
|
||||
</div>
|
||||
|
||||
<!-- Navigation Buttons -->
|
||||
<div align="center">
|
||||
<p>
|
||||
<table>
|
||||
<tr>
|
||||
<td><a href="1.html" title=""><img src="imgs/first.png" height="60" alt="First" onmouseover="this.src='imgs/firston.png'" onmouseout="this.src='imgs/first.png'" /></a></td>
|
||||
<td><a href="251.html" title=""><img src="imgs/previous.png" height="60" alt="Previous" onmouseover="this.src='imgs/previouson.png'" onmouseout="this.src='imgs/previous.png'" /></a></td>
|
||||
<td><a href="archive.html" title=""><img src="imgs/archive.png" height="60" alt="Archive" onmouseover="this.src='imgs/archive2.png'" onmouseout="this.src='imgs/archive.png'" /></a></td>
|
||||
<td><a href="253.html" title=""><img src="imgs/next.png" height="60" alt="Next" onmouseover="this.src='imgs/nexton.png'" onmouseout="this.src='imgs/next.png'" /></a></td>
|
||||
<td><a href="index.html" title=""><img src="imgs/last.png" height="60" alt="Last" onmouseover="this.src='imgs/laston.png'" onmouseout="this.src='imgs/last.png'" /></a></td>
|
||||
</tr>
|
||||
</table></p>
|
||||
</div>
|
||||
|
||||
<!-- Social Media -->
|
||||
<table class="social" align="center">
|
||||
<tr>
|
||||
<td><a href="http://falseknees.tumblr.com/"><img src="imgs/TumblrButton.png" width="60" onmouseover="this.src='imgs/TumblrButtonOn.png'" onmouseout="this.src='imgs/TumblrButton.png'"/></a>
|
||||
</td>
|
||||
<td><a href="https://instagram.com/FalseKnees"><img src="imgs/instagram.png" width="60" onmouseover="this.src='imgs/instagramon.png'" onmouseout="this.src='imgs/instagram.png'"/></a>
|
||||
</td>
|
||||
<td><a href="https://www.facebook.com/FalseKnees?ref=hl"><img src="imgs/FacebookButton.png" width="60" onmouseover="this.src='imgs/FacebookButtonOn.png'" onmouseout="this.src='imgs/FacebookButton.png'"/></a>
|
||||
</td>
|
||||
<td><a href="http://www.webtoons.com/en/challenge/false-knees/list?title_no=79544"><img src="imgs/webtooff.png" width="60" onmouseover="this.src='imgs/webtoon.png'" onmouseout="this.src='imgs/webtooff.png'"/></a>
|
||||
</td>
|
||||
<td><a href="https://tapas.io/series/FalseKnees"><img src="imgs/tap.png" width="60" onmouseover="this.src='imgs/tapon.png'" onmouseout="this.src='imgs/tap.png'"/></a>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Copyright -->
|
||||
<div>
|
||||
<p>False Knees © 2013-whenever Joshua Barkman</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
)
|
21
go.mod
21
go.mod
|
@ -1,13 +1,18 @@
|
|||
module go.etztech.xyz/sedbot
|
||||
module git.jojodev.com/Minecraft/canopeas
|
||||
|
||||
go 1.14
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/bwmarrin/discordgo v0.20.3
|
||||
github.com/urfave/cli/v2 v2.2.0 // indirect
|
||||
git.jojodev.com/Minecraft/go-mcm v0.0.1
|
||||
git.jojodev.com/Minecraft/go-serverapi v0.0.1
|
||||
gitea.com/jolheiser/gojang v0.0.7
|
||||
gitea.com/jolheiser/xkcd v0.0.2
|
||||
github.com/bwmarrin/discordgo v0.27.1
|
||||
github.com/dghubble/go-twitter v0.0.0-20201011215211-4b180d0cc78d
|
||||
github.com/dghubble/oauth1 v0.7.0
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml v1.8.1
|
||||
github.com/rs/zerolog v1.26.1
|
||||
go.etcd.io/bbolt v1.3.4
|
||||
go.etztech.xyz/go-mcm v1.3.0
|
||||
go.etztech.xyz/inspiro v0.0.0-20200606185551-edfdf9da2359
|
||||
go.jolheiser.com/beaver v1.0.2
|
||||
golang.org/x/crypto v0.8.0 // indirect
|
||||
)
|
||||
|
|
122
go.sum
122
go.sum
|
@ -1,34 +1,96 @@
|
|||
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=
|
||||
git.jojodev.com/Minecraft/go-mcm v0.0.1 h1:3nfjCz3wA4l44QWYMRC7DLwU4GneWC5e1CRub7Tt1S0=
|
||||
git.jojodev.com/Minecraft/go-mcm v0.0.1/go.mod h1:0VAA1b5ZgM1leeYOqMhBnEfdonZccdMdeQrsF+50s04=
|
||||
git.jojodev.com/Minecraft/go-serverapi v0.0.1 h1:sn594cScthq0W/ntLVhWfMfNcPXG9IyyqMatcEjcYIQ=
|
||||
git.jojodev.com/Minecraft/go-serverapi v0.0.1/go.mod h1:a3e4OnMNJnd2OzwRG62Fe/k70SelNDoAxYW9SYmkDf4=
|
||||
gitea.com/jolheiser/gojang v0.0.7 h1:Q4cG7QYiKQsJtUWgXXiolAH9DCLRoaQ4olaO9OV628U=
|
||||
gitea.com/jolheiser/gojang v0.0.7/go.mod h1:r9kj2wv/21Da7VpWz+qmxLexH85o2BAM4NMxeYgQlcY=
|
||||
gitea.com/jolheiser/xkcd v0.0.2 h1:HJP83YwSKxSYcoNfpb1ZpAfBvkUAnN+YgeukraXtfrc=
|
||||
gitea.com/jolheiser/xkcd v0.0.2/go.mod h1:aDa2vX54wLaX8Ra5CGN2GWBX13UWAGJKGGddzHl/hks=
|
||||
github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
|
||||
github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY=
|
||||
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
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/dghubble/go-twitter v0.0.0-20201011215211-4b180d0cc78d h1:sBKr0A8iQ1qAOozedZ8Aox+Jpv+TeP1Qv7dcQyW8V+M=
|
||||
github.com/dghubble/go-twitter v0.0.0-20201011215211-4b180d0cc78d/go.mod h1:xfg4uS5LEzOj8PgZV7SQYRHbG7jPUnelEiaAVJxmhJE=
|
||||
github.com/dghubble/oauth1 v0.7.0 h1:AlpZdbRiJM4XGHIlQ8BuJ/wlpGwFEJNnB4Mc+78tA/w=
|
||||
github.com/dghubble/oauth1 v0.7.0/go.mod h1:8pFdfPkv/jr8mkChVbNVuJ0suiHe278BtWI4Tk1ujxk=
|
||||
github.com/dghubble/sling v1.3.0 h1:pZHjCJq4zJvc6qVQ5wN1jo5oNZlNE0+8T/h0XeXBUKU=
|
||||
github.com/dghubble/sling v1.3.0/go.mod h1:XXShWaBWKzNLhu2OxikSNFrlsvowtz4kyRuXUG7oQKY=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
|
||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
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=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
|
||||
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
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/go-mcm v1.3.0 h1:ULhLEHRJZYh+hIgHfp7IuqpOn6H7kkrVxnnfzyZLQM0=
|
||||
go.etztech.xyz/go-mcm v1.3.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=
|
||||
go.jolheiser.com/gql v0.0.1 h1:y3LGHcJUZI9otTCcMn8TVdF3aEzNX0FW6m0YUamlLto=
|
||||
go.jolheiser.com/gql v0.0.1/go.mod h1:74eYqVRIxsOFxtVl0RYGKNyYQgJYQaxOCgar7LP71Hw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-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=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package imgur
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Images []*Image `json:"data"`
|
||||
Success bool `json:"success"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Datetime int `json:"datetime"`
|
||||
Type string `json:"type"`
|
||||
Animated bool `json:"animated"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Size int `json:"size"`
|
||||
Views int `json:"views"`
|
||||
Bandwidth int `json:"bandwidth"`
|
||||
Deletehash string `json:"deletehash"`
|
||||
Section string `json:"section"`
|
||||
Link string `json:"link"`
|
||||
}
|
||||
|
||||
func Get(clientID, albumID string) ([]*Image, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://api.imgur.com/3/album/%s/images", albumID), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Client-ID %s", clientID))
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var imgurResp Response
|
||||
return imgurResp.Images, json.NewDecoder(resp.Body).Decode(&imgurResp)
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package inspiro
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const api = "https://inspirobot.me/api?generate=true"
|
||||
|
||||
func Generate() (string, error) {
|
||||
resp, err := http.Get(api)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return string(body), nil
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package inspiro
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
if _, err := Generate(); err != nil {
|
||||
t.Logf("could not generate image: %v", err)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
35
main.go
35
main.go
|
@ -1,5 +1,3 @@
|
|||
//go:generate go run sedbot.example.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
@ -8,54 +6,49 @@ import (
|
|||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"go.etztech.xyz/sedbot/config"
|
||||
"go.etztech.xyz/sedbot/database"
|
||||
"go.etztech.xyz/sedbot/discord"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"go.jolheiser.com/beaver"
|
||||
"git.jojodev.com/Minecraft/canopeas/config"
|
||||
"git.jojodev.com/Minecraft/canopeas/database"
|
||||
"git.jojodev.com/Minecraft/canopeas/discord"
|
||||
)
|
||||
|
||||
var configFlag string
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&configFlag, "config", "sedbot.toml", "Set config path")
|
||||
flag.Parse()
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
|
||||
beaver.Console.Format = beaver.FormatOptions{
|
||||
TimePrefix: true,
|
||||
StackPrefix: true,
|
||||
StackLimit: 15,
|
||||
LevelPrefix: true,
|
||||
LevelColor: true,
|
||||
}
|
||||
flag.StringVar(&configFlag, "config", "canopeas.toml", "Set config path")
|
||||
flag.Parse()
|
||||
|
||||
cfg, err := config.Load(configFlag)
|
||||
if err != nil {
|
||||
beaver.Fatalf("could not load config: %v", err)
|
||||
log.Fatal().Msgf("could not load config: %v", err)
|
||||
}
|
||||
|
||||
db, err := database.Load(cfg.DBPath)
|
||||
if err != nil {
|
||||
beaver.Fatalf("could not load database: %v", err)
|
||||
log.Fatal().Msgf("could not load database: %v", err)
|
||||
}
|
||||
|
||||
bot, err := discord.Bot(cfg, db)
|
||||
if err != nil {
|
||||
beaver.Fatalf("could not start Discord bot: %v", err)
|
||||
log.Fatal().Msgf("could not start Discord bot: %v", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := bot.Open(); err != nil {
|
||||
beaver.Errorf("error running Discord bot: %v", err)
|
||||
log.Error().Msgf("error running Discord bot: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
beaver.Info("Bot is now running. Press CTRL-C to exit.")
|
||||
log.Info().Msg("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)
|
||||
log.Error().Msgf("error closing Discord bot: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
// +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)
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
# 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 the MCM API
|
||||
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 = []
|
||||
|
||||
# meme_rate is the rate limit for memes
|
||||
meme_rate = "3s"
|
||||
|
||||
# insults
|
||||
# <args>, your <target> looks like <comparison>, you <adjective> <noun>
|
||||
[insult]
|
||||
targets = []
|
||||
comparisons = []
|
||||
adjectives = []
|
||||
nouns = []
|
||||
|
||||
# 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"
|
||||
role_id = "0"
|
||||
emoji = "thumbsup"
|
Loading…
Reference in New Issue