package main import ( "flag" "fmt" "io" "math/rand" "net/http" "net/url" "os" "os/signal" "strings" "syscall" "time" "github.com/bwmarrin/discordgo" "github.com/rudi9719/loggy" ) var ( startupTime time.Time setupToken = fmt.Sprintf("!setup %+v", rand.Intn(9999)+1000) rebootToken = fmt.Sprintf("!reboot %+v", rand.Intn(9999)+1000) bump = true config Config log = loggy.NewLogger(config.LogOpts) lastActiveChan string lastActiveTime time.Time token string configFile string setupMsg string dg *discordgo.Session lastPM = make(map[string]time.Time) introMsg = make(map[string]string) quotes = []string{"The hardest choices require the strongest wills.", "You're strong, but I could snap my fingers and you'd all cease to exist.", "Fun isn't something one considers when balancing the universe. But this... does put a smile on my face.", "Perfectly balanced, as all things should be.", "I am inevitable."} ) func init() { flag.StringVar(&token, "t", "", "Bot Token") flag.StringVar(&configFile, "c", "", "Config file") flag.Parse() } func main() { go runWeb() defer log.PanicSafe() if configFile == "" { configFile = "config.json" } else { loadConfig() } log = loggy.NewLogger(config.LogOpts) startupTime = time.Now() lastActiveTime = time.Now() lastActiveChan = config.AdminChannel if token == "" { log.LogPanic("No token provided. Please run: disgord-thanos -t ") } log.LogCritical("SetupToken: %+v\nRebootToken: %+v", setupToken, rebootToken) var err error dg, err = discordgo.New("Bot " + token) if err != nil { log.LogErrorType(err) log.LogPanic("Unable to create bot using token.") } dg.AddHandler(ready) dg.AddHandler(guildMemberRemove) dg.AddHandler(guildMemberAdd) dg.AddHandler(guildMemberBanned) dg.AddHandler(messageCreate) dg.AddHandler(readReaction) dg.AddHandler(guildMemberUpdate) dg.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAll) err = dg.Open() if err != nil { log.LogErrorType(err) log.LogPanic("Unable to open websocket.") } log.LogInfo("Thanos is now running. Press CTRL-C to exit.") go purgeTimer(dg) sc := make(chan os.Signal, 1) signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) <-sc saveConfig() dg.Close() } func exit(s *discordgo.Session) { s.Close() saveConfig() os.Exit(0) } func runPurge(s *discordgo.Session) { defer log.PanicSafe() if time.Since(config.BumpTime) > 2*time.Hour { bump = true } for uid, join := range config.Probations { if time.Since(join) > 2*time.Hour { delete(config.Probations, uid) s.ChannelMessageDelete(config.IntroChann, uid) } } for k, v := range config.Unverified { isUnverified := false m, err := s.GuildMember(config.GuildID, k) if err != nil { delete(config.Unverified, k) continue } for _, role := range m.Roles { if role == config.MonitorRole { isUnverified = true } } if isUnverified { if val, ok := lastPM[k]; ok && time.Since(val) < 5*time.Minute { continue } lastPM[k] = time.Now() pmChann, _ := s.UserChannelCreate(k) s.ChannelMessageSend(pmChann.ID, fmt.Sprintf("This is a reminder that you have not verified with me and will be removed in %+v. You may reply to this message for verification instructions.", time.Until(v.Add(1*time.Hour)))) if time.Since(v) > (time.Hour * 1) { s.ChannelMessageSend(config.AdminChannel, fmt.Sprintf("%+v was removed.", m.Mention())) s.GuildMemberDeleteWithReason(config.GuildID, k, fmt.Sprintf("Unverified user %+v.", v)) } } else { delete(config.Unverified, k) } } messages, _ := s.ChannelMessages(config.MonitorChann, 100, "", "", "") for _, message := range messages { found := false for user := range config.Unverified { if message.Author.ID == user { found = true } for _, mention := range message.Mentions { if mention.ID == user { found = true } } } if !found { s.ChannelMessageDelete(config.MonitorChann, message.ID) } } saveConfig() } func ready(s *discordgo.Session, event *discordgo.Ready) { // Set the playing status. s.UpdateGameStatus(0, "DreamDaddy v2.2") } func guildMemberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) { defer log.PanicSafe() for role := range m.Roles { if fmt.Sprintf("%+v", role) == config.MonitorRole { s.ChannelMessageSend(config.AdminChannel, "New unverified user detected.") s.ChannelMessageSend(config.MonitorChann, fmt.Sprintf("Welcome %+v, you may PM me your verification, or I will ban you in an hour!\nSay \"!rules\" in this channel, without quotes for the rules. You may private/direct message me for verification instructions.\n\nYou will not be able to read/see other channels or users until you verify.", m.User.Mention())) config.Unverified[m.User.ID] = time.Now() config.Probations[m.User.ID] = time.Now() saveConfig() } } } func guildMemberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd) { defer log.PanicSafe() config.Unverified[m.User.ID] = time.Now() config.Probations[m.User.ID] = time.Now() s.GuildMemberRoleAdd(config.GuildID, m.User.ID, config.MonitorRole) s.ChannelMessageSend(config.MonitorChann, fmt.Sprintf("Welcome %+v, you may PM me your verification, or I will ban you in an hour!\nSay \"!rules\" in this channel, without quotes for the rules. You may private/direct message me for verification instructions.\n\nYou will not be able to read/see other channels or users until you verify.", m.User.Mention())) saveConfig() } func guildMemberBanned(s *discordgo.Session, m *discordgo.GuildBanAdd) { defer log.PanicSafe() for uid := range config.Probations { if m.User.Email == uid { delete(config.Probations, uid) s.ChannelMessageDelete(config.IntroChann, uid) } } saveConfig() } func guildMemberRemove(s *discordgo.Session, m *discordgo.GuildMemberRemove) { defer log.PanicSafe() go runPurge(s) banned := false for uid, join := range config.Probations { if time.Since(join) < 2*time.Hour { if m.User.ID == uid { banned = true s.GuildBanCreateWithReason(config.GuildID, m.User.ID, fmt.Sprintf("Left within 2 hours of joining. %+v", time.Since(join)), 0) delete(config.Probations, uid) s.ChannelMessageDelete(config.IntroChann, uid) } } else { delete(config.Probations, uid) s.ChannelMessageDelete(config.IntroChann, uid) } } s.ChannelMessageSend(config.AdminChannel, fmt.Sprintf("%+v (@%+v) has left, ban: %+v", m.User.ID, m.User.Username, banned)) delete(config.Unverified, m.User.ID) for msg, v := range config.Verifications { if v.UserID == m.User.ID { delete(config.Verifications, msg) s.ChannelMessageDelete(config.IntroChann, m.User.ID) } } saveConfig() } func verifyMember(s *discordgo.Session, u discordgo.User) { defer log.PanicSafe() s.GuildMemberRoleAdd(config.GuildID, u.ID, config.VerifiedRole) s.GuildMemberRoleRemove(config.GuildID, u.ID, config.MonitorRole) st, _ := s.UserChannelCreate(u.ID) s.ChannelMessageSend(st.ID, "Your verification has been accepted, welcome!") id, _ := s.ChannelMessageSend(config.IntroChann, fmt.Sprintf("Welcome %+v please introduce yourself! :) feel free to check out <#710557387937022034> to tag your roles. Also please mute any channels you are not interested in!", u.Mention())) introMsg[id.ID] = u.ID } func rejectVerification(s *discordgo.Session, u discordgo.User) { defer log.PanicSafe() st, _ := s.UserChannelCreate(u.ID) if st != nil { s.ChannelMessageSend(st.ID, fmt.Sprintf("Your verification has been rejected. This means it did not clearly show your face, with your pinkie finger held to the corner of your mouth, or the photo looked edited/filtered. No filters will be accepted.\n\nPlease try again before %+v", time.Until(time.Now().Add(1*time.Hour)))) } config.Unverified[u.ID] = time.Now() } func requestAge(s *discordgo.Session, u discordgo.User) { defer log.PanicSafe() st, _ := s.UserChannelCreate(u.ID) s.ChannelMessageSend(st.ID, "What is your ASL? (Age/Sex/Language) Please note, this is NOT requesting your gender, but your biological sex. Gender is a social construct, sex is biology and in the context of pornographic images more important.") } func handlePM(s *discordgo.Session, m *discordgo.MessageCreate) { defer log.PanicSafe() if strings.Contains(m.Content, "Rule") || strings.Contains(m.Content, "rule") { s.ChannelMessageSend(m.ChannelID, "I specifically said to say \"!rules\" without quotes in the unverified channel for the rules.") } for _, uid := range config.Verifications { user := userFromID(s, uid.UserID) if m.Author.ID == user.ID { s.ChannelMessageSend(m.ChannelID, "Your verification is pending. An admin will respond to it when they are available.") s.ChannelMessageSend(config.AdminChannel, fmt.Sprintf("%+v said: %+v", m.Author.Mention(), m.Content)) return } } if len(m.Attachments) != 1 { s.ChannelMessageSend(m.ChannelID, "```I am a bot and this is an autoreply.\n\nUntil you send a verification, I will always say the following message:```\nYou may only send me your verification (and nothing else) to be passed to the admins (and no one else). Verification is a clear full face pic, with your pinky finger held to the corner of your mouth.") s.ChannelMessageSend(config.AdminChannel, fmt.Sprintf("%+v said: %+v", m.Author.Mention(), m.Content)) return } delete(config.Unverified, m.Author.ID) var v Verification v.Submitted = time.Now() v.UserID = m.Author.ID v.Username = m.Author.Username v.Photo = m.Attachments[0].ProxyURL v.Status = "Submitted" msg, _ := s.ChannelMessageSend(config.AdminChannel, fmt.Sprintf("%+v\n%+v", v.Username, v.Photo)) config.Verifications[msg.ID] = v s.MessageReactionAdd(config.AdminChannel, msg.ID, "👎") s.MessageReactionAdd(config.AdminChannel, msg.ID, "👍") s.MessageReactionAdd(config.AdminChannel, msg.ID, "👶") s.MessageReactionAdd(config.AdminChannel, msg.ID, "⛔") } func readReaction(s *discordgo.Session, m *discordgo.MessageReactionAdd) { defer log.PanicSafe() if m.ChannelID != config.AdminChannel || m.UserID == s.State.User.ID { return } admin, _ := s.GuildMember(config.GuildID, m.UserID) adminInteraction(s, admin.User.ID) verification, ok := config.Verifications[m.MessageID] if !ok { return } verification.Admin = admin.User.Username verification.Closed = time.Now() user := userFromID(s, verification.UserID) if user.ID == "" { s.ChannelMessageSend(config.AdminChannel, fmt.Sprintf("%+v, that user was not found, they might have left.", admin.Mention())) delete(config.Verifications, m.MessageID) return } if m.Emoji.Name == "👎" { rejectVerification(s, user) verification.Status = "Rejected" } else if m.Emoji.Name == "👍" { verifyMember(s, user) verification.Status = "Accepted" go storeVerification(verification) } else if m.Emoji.Name == "👶" { requestAge(s, user) log.LogInfo("%+v has requested ASL for user %+v.", admin.User.Username, user.Username) return } else if m.Emoji.Name == "⛔" { s.GuildBanCreateWithReason(config.GuildID, user.ID, fmt.Sprintf("Underage or too many failed verifications. %+v", admin.User.Username), 5) verification.Status = "Banned" } else { return } log.LogInfo("%+v", verification.prettyPrint()) delete(config.Verifications, m.MessageID) } func storeVerification(v Verification) { defer log.PanicSafe() fileURL, _ := url.Parse(v.Photo) path := fileURL.Path segments := strings.Split(path, "/") fileName := segments[len(segments)-1] file, _ := os.Create(fmt.Sprintf("./verifications/%s-%s-%s", v.UserID, v.Username, fileName)) client := http.Client{ CheckRedirect: func(r *http.Request, via []*http.Request) error { r.URL.Opaque = r.URL.Path return nil }, } resp, err := client.Get(v.Photo) if err != nil { log.LogError("Unable to download verification %s-%s-%s", v.UserID, v.Username, fileName) } defer resp.Body.Close() defer file.Close() _, err = io.Copy(file, resp.Body) if err != nil { log.LogError("Unable to store verification %s-%s-%s", v.UserID, v.Username, fileName) } } func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { defer log.PanicSafe() if m.Author.ID == s.State.User.ID || m.Author.Bot { return } if m.GuildID == "" { handlePM(s, m) return } if m.ChannelID == config.MonitorChann { if strings.Contains(m.Content, "erif") && !m.Author.Bot { s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("%+v send me a private message for verification.", m.Author.Mention())) } return } for role := range m.Member.Roles { if fmt.Sprintf("%+v", role) == config.AdminRole { adminInteraction(s, m.Author.ID) } } if m.ChannelID != config.AdminChannel { lastActiveChan = m.ChannelID lastActiveTime = time.Now() } if strings.HasPrefix(m.Content, "!d bump") { if time.Since(config.BumpTime) < 2*time.Hour { s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("Sorry, <@%+v> already claimed the bump. Better luck next time!", config.LastBumper)) return } config.LastBumper = m.Author.ID go bumpTimer(s) return } if time.Since(config.BumpTime) > 2*time.Hour { s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("%+v please say \"!d bump\" without the quotes to bump our server :)", m.Author.Mention())) } if m.ChannelID == config.AdminChannel { if strings.HasPrefix(m.Content, rebootToken) { exit(s) } if strings.HasPrefix(m.Content, "!quote") { quotes = append(quotes, strings.ReplaceAll(m.Content, "!quote", "")) } if strings.HasPrefix(m.Content, "!snap") || strings.HasPrefix(m.Content, "!purge") { go runPurge(s) s.ChannelMessageSend(config.AdminChannel, quotes[rand.Intn(len(quotes))]) } if strings.HasPrefix(m.Content, "!st") { go status(s) saveConfig() } } }