canopeas/discord/discord.go

363 lines
9.5 KiB
Go

package discord
import (
"fmt"
"git.jojodev.com/Minecraft/canopeas/discord/track"
"os"
"strings"
"time"
"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"
"github.com/dghubble/go-twitter/twitter"
"github.com/dghubble/oauth1"
)
// 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 tracker
t, err := db.LoadTracker()
if err != nil {
return nil, err
}
// 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 == "" {
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(joinHandler(cfg))
bot.AddHandler(leaveHandler(cfg))
bot.AddHandler(commandHandler(cfg, db, sapi, twitterClient))
bot.AddHandler(pingHandler(cfg, db))
bot.AddHandler(trackHandler(t))
bot.AddHandler(trackCommandHandler(t, 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)
// Create application commands
if _, err := bot.ApplicationCommandBulkOverwrite(bot.State.User.ID, "", []*discordgo.ApplicationCommand{
trackCommand,
}); err != nil {
log.Err(err).Msg("could not create application commands")
}
return bot, nil
}
func sendTyping(s *discordgo.Session, channelID string) {
if err := s.ChannelTyping(channelID); err != nil {
log.Error().Msgf("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 {
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
}
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) {
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, 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 {
log.Warn().Msgf("could not remove invocation for %s: %v", m.Content, err)
}
}
feedback, err := cmd.run(cmdInit)
if err != nil {
feedback = "Internal error"
log.Error().Msgf("error while running %s: %v", cmdArg, err)
}
if len(feedback) > 0 {
sendMessage(s, m.ChannelID, feedback, false)
}
}
}
func pingHandler(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 {
log.Error().Msgf("could not increment ping for %s: %v", cfg.FiredRole, err)
}
}
}
}
}
func trackHandler(t track.Tracker) func(s *discordgo.Session, m *discordgo.MessageCreate) {
return func(s *discordgo.Session, m *discordgo.MessageCreate) {
// Ignore bots
if m.Author.Bot {
return
}
trackers := t.Search(strings.ToLower(m.Content))
for _, tracker := range trackers {
// Ignore self-trackers
if tracker == m.Author.ID {
continue
}
sendMessage(s, tracker, fmt.Sprintf("%s\n>>>%s", m.Author.Mention(), m.Content), true)
sendMessage(s, tracker, fmt.Sprintf("https://discord.com/channels/%s/%s/%s", m.GuildID, m.ChannelID, m.ID), true)
}
}
}
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 {
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(1, dj.Joke); err != nil {
log.Warn().Msgf("could not update status: %v", err)
}
<-ticker.C
}
}
func joinHandler(cfg *config.Config) func(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
return func(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
sendMessage(s, cfg.Welcome.Channel, os.Expand(cfg.Welcome.Message, func(s string) string {
switch strings.ToLower(s) {
case "user":
return m.Mention()
default:
return s
}
}), false)
if cfg.Welcome.DM != "" {
dm, err := s.UserChannelCreate(m.User.ID)
if err != nil {
log.Err(err).Msgf("could not create DM with %s", m.User.Username)
return
}
sendMessage(s, dm.ID, os.Expand(cfg.Welcome.DM, func(s string) string {
switch strings.ToLower(s) {
case "user":
return m.Mention()
default:
return s
}
}), false)
}
}
}