package discord import ( "fmt" "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 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(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 { 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 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 { log.Error().Msgf("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 { log.Error().Msgf("could not modify role %s for user %s", r, m.UserID) } } } } func leaveHandler(cfg *config.Config) func(s *discordgo.Session, m *discordgo.GuildMemberRemove) { return func(s *discordgo.Session, m *discordgo.GuildMemberRemove) { sendMessage(s, cfg.LeaveChannel, fmt.Sprintf("%s (%s) left the server. :sob:", m.Mention(), m.User.String()), true) } } func updateStatus(s *discordgo.Session) { ticker := time.NewTicker(time.Minute * 30) for { dj, err := newDadJoke() if err != nil { log.Warn().Msgf("could not get new dad joke: %v", err) } else if err := s.UpdateStatus(1, dj.Joke); err != nil { log.Warn().Msgf("could not update status: %v", err) } <-ticker.C } }