forked from Minecraft/canopeas
295 lines
7.7 KiB
Go
295 lines
7.7 KiB
Go
package discord
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.canopymc.net/Etzelia/canopeas/config"
|
|
"git.canopymc.net/Etzelia/canopeas/database"
|
|
|
|
"git.canopymc.net/Etzelia/go-serverapi"
|
|
"github.com/bwmarrin/discordgo"
|
|
"github.com/dghubble/go-twitter/twitter"
|
|
"github.com/dghubble/oauth1"
|
|
"go.jolheiser.com/beaver"
|
|
)
|
|
|
|
// Register commands to this map
|
|
var (
|
|
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
|
|
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
|
|
}
|
|
|
|
func Bot(cfg *config.Config, db *database.Database) (*discordgo.Session, error) {
|
|
bot, err := discordgo.New("Bot " + cfg.Token)
|
|
if err != nil {
|
|
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)
|
|
}
|
|
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 == "" {
|
|
beaver.Errorf("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(leaveHandler(cfg))
|
|
bot.AddHandler(commandHandler(cfg, db, sapi, twitterClient))
|
|
bot.AddHandler(messageHandler(cfg, db))
|
|
bot.AddHandler(reactionAddHandler())
|
|
bot.AddHandler(reactionRemoveHandler())
|
|
|
|
// Rate limits
|
|
d, err := time.ParseDuration(cfg.MemeRate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
return nil
|
|
}
|
|
return msg
|
|
}
|
|
|
|
func sendEmbed(s *discordgo.Session, channelID string, embed *discordgo.MessageEmbed) *discordgo.Message {
|
|
embed.Color = embedColor
|
|
msg, err := s.ChannelMessageSendEmbed(channelID, embed)
|
|
if err != nil {
|
|
beaver.Errorf("could not send embed: %v", err)
|
|
return nil
|
|
}
|
|
return msg
|
|
}
|
|
|
|
func isStaff(authorRoleIDs, staffRoleIDs []string) bool {
|
|
for _, aRole := range authorRoleIDs {
|
|
for _, sRole := range staffRoleIDs {
|
|
if aRole == sRole {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func 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)
|
|
|
|
// Init status changer
|
|
go updateStatus(s)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
return
|
|
}
|
|
|
|
// Check prefix
|
|
if !strings.HasPrefix(m.Content, cfg.Prefix) {
|
|
return
|
|
}
|
|
|
|
content := m.Content[1:]
|
|
args := strings.Fields(content)
|
|
if len(args) == 0 {
|
|
return
|
|
}
|
|
|
|
cmdArg := strings.ToLower(args[0])
|
|
|
|
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,
|
|
sapiClient: sapi,
|
|
twitterClient: twitterClient,
|
|
}
|
|
if !cmd.validate(cmdInit) {
|
|
sendMessage(s, m.ChannelID, "You cannot run this command.", false)
|
|
return
|
|
}
|
|
if cmd.deleteInvocation {
|
|
if err := s.ChannelMessageDelete(m.Message.ChannelID, m.Message.ID); err != nil {
|
|
beaver.Warnf("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)
|
|
}
|
|
if len(feedback) > 0 {
|
|
sendMessage(s, m.ChannelID, feedback, false)
|
|
}
|
|
}
|
|
}
|
|
|
|
func messageHandler(cfg *config.Config, db *database.Database) func(s *discordgo.Session, m *discordgo.MessageCreate) {
|
|
return func(s *discordgo.Session, m *discordgo.MessageCreate) {
|
|
// Ignore bots
|
|
if m.Author.Bot {
|
|
return
|
|
}
|
|
|
|
// [FIRED] increment
|
|
for _, role := range m.MentionRoles {
|
|
if cfg.FiredRole == role {
|
|
if err := db.IncrementPing(cfg.FiredRole); err != nil {
|
|
beaver.Errorf("could not increment ping for %s: %v", cfg.FiredRole, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func reactionAddHandler() func(s *discordgo.Session, m *discordgo.MessageReactionAdd) {
|
|
return func(s *discordgo.Session, m *discordgo.MessageReactionAdd) {
|
|
reactionHandler(true, s, m.MessageReaction)
|
|
}
|
|
}
|
|
|
|
func reactionRemoveHandler() func(s *discordgo.Session, m *discordgo.MessageReactionRemove) {
|
|
return func(s *discordgo.Session, m *discordgo.MessageReactionRemove) {
|
|
reactionHandler(false, s, m.MessageReaction)
|
|
}
|
|
}
|
|
|
|
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)
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
beaver.Warnf("could not get new dad joke: %v", err)
|
|
} else if err := s.UpdateStatus(1, dj.Joke); err != nil {
|
|
beaver.Warnf("could not update status: %v", err)
|
|
}
|
|
<-ticker.C
|
|
}
|
|
}
|