From 95a757ee759acd367bd34d1f063d204ffdb4ee13 Mon Sep 17 00:00:00 2001 From: Ichaboc Date: Thu, 27 Aug 2020 16:12:49 -0400 Subject: [PATCH] Initial Commit --- .gitignore | 3 + config.go | 200 +++++++++++++++++++++++++++++ main.go | 363 +++++++++++++++++++++++++++++++++++++++++++++++++++++ types.go | 29 +++++ 4 files changed, 595 insertions(+) create mode 100644 .gitignore create mode 100644 config.go create mode 100644 main.go create mode 100644 types.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36931fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +disgord-thanos +config.json +start.sh diff --git a/config.go b/config.go new file mode 100644 index 0000000..ad68c52 --- /dev/null +++ b/config.go @@ -0,0 +1,200 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "time" + "os" + + "github.com/bwmarrin/discordgo" +) + +func status(s *discordgo.Session) { + defer log.PanicSafe() + monChan, _ := s.Channel(config.MonitorChann) + + roles, _ := s.GuildRoles(config.GuildID) + var monRole discordgo.Role + var veriRole discordgo.Role + for _, role := range roles { + if role.ID == config.MonitorRole { + monRole = *role + } + if role.ID == config.VerifiedRole { + veriRole = *role + } + } + status := fmt.Sprintf("Uptime: %+v\n", time.Since(startupTime)) + status += fmt.Sprintf("Monitor role: %+v\n", monRole.Mention()) + status += fmt.Sprintf("Monitor chann: %+v\n", monChan.Mention()) + status += fmt.Sprintf("Verified role: %+v\n", veriRole.Mention()) + status += fmt.Sprintf("Last bump: %+v\n", time.Since(config.BumpTime)) + status += fmt.Sprintf("Last bumper: <@%+v>\n", config.LastBumper) + status += fmt.Sprintf("Bump needed: %+v\n", bump) + if len(config.Unverified) > 0 { + status += "Unverified users:\n```" + for k, v := range config.Unverified { + uvUser := userFromID(s, k) + status += fmt.Sprintf("\n%+v will be removed in %+v", uvUser.Username, time.Until(v.Add(1*time.Hour))) + } + status += "```" + } else { + status += "There are no unverified users.\n" + } + if len(config.Verifications) > 0 { + status += "Pending verifications:\n" + status += "```" + for _, v := range config.Verifications { + status += fmt.Sprintf("%+v has submitted a verification.", v.Username) + } + status += "```" + } else { + status += "There are no pending verifications." + } + if len(config.Probations) > 0 { + status += "\nThe following users are on probation: \n```" + for uid, join := range config.Probations { + probationUser := userFromID(s, uid) + status += fmt.Sprintf("%+v for another %+v\n", probationUser.Username, time.Until(join.Add(2*time.Hour))) + } + status += "```" + } + s.ChannelMessageSend(config.AdminChannel, status) + statistics := "```" + for k, v := range config.Stats { + adminUser, err := s.GuildMember(config.GuildID, k) + if err == nil { + statistics += fmt.Sprintf("\n%+v: %+v", adminUser.User.Username, v+1) + } else { + log.LogErrorType(err) + } + } + statistics += "\n```" + log.LogInfo(fmt.Sprintf("Private statistics: %+v", statistics)) + go runPurge(s) + return +} + +func loadConfig() { + var c Config + confFile, _ := ioutil.ReadFile(configFile) + err := json.Unmarshal([]byte(confFile), &c) + if err != nil { + log.LogErrorType(err) + return + } + config = c + + if time.Since(config.BumpTime) < (2 * time.Hour) { + bump = false + } else { + bump = true + } + + if config.Stats == nil { + config.Stats = make(map[string]int) + } + if config.Unverified == nil { + config.Unverified = make(map[string]time.Time) + } + if config.Verifications == nil { + config.Verifications = make(map[string]Verification) + } + if config.Probations == nil { + config.Probations = make(map[string]time.Time) + } + + log.LogInfo("Setup completed using config file.") +} + +func saveConfig() { + defer log.PanicSafe() + file, err := json.Marshal(config) + if err != nil { + log.LogErrorType(err) + } + err = ioutil.WriteFile(configFile, file, 0600) + if err != nil { + log.LogErrorType(err) + } +} + +func setAdminChannel(s *discordgo.Session, m *discordgo.MessageCreate) { + config.AdminChannel = m.ChannelID + if len(m.MentionRoles) != 1 { + s.ChannelMessageSend(config.AdminChannel, "Invalid verified role") + return + } + config.VerifiedRole = m.MentionRoles[0] + s.ChannelMessageDelete(m.ChannelID, m.ID) + msg, _ := s.ChannelMessageSend(config.AdminChannel, "Run !setup in the monitored channel to finish setup.") + setupMsg = msg.ID +} +func setMonitorChann(s *discordgo.Session, m *discordgo.MessageCreate) { + config.MonitorChann = m.ChannelID + if len(m.MentionRoles) != 1 { + s.ChannelMessageSend(config.AdminChannel, "Invalid monitor role") + return + } + config.MonitorRole = m.MentionRoles[0] + s.ChannelMessageDelete(m.ChannelID, m.ID) + s.ChannelMessageSend(config.AdminChannel, "Setup completed. Run !status for status.") + s.ChannelMessageDelete(config.AdminChannel, setupMsg) + go purgeTimer(s) + saveConfig() +} + +func bumpTimer(s *discordgo.Session) { + if !bump { + return + } + bump = false + config.BumpTime = time.Now() + time.Sleep(2 * time.Hour) + if time.Since(lastActiveTime) < (5*time.Minute) && lastActiveChan != config.AdminChannel { + s.ChannelMessageSend(lastActiveChan, "!d bump is ready, please use it. (say \"!d bump\" without the quotes)") + } + s.ChannelMessageSend(config.AdminChannel, "!d bump is ready.") + bump = true +} +func purgeTimer(s *discordgo.Session) { + for { + runPurge(s) + saveConfig() + if time.Since(lastActiveTime) > 4 * time.Hour && time.Since(startupTime) > 12 * time.Hour { + saveConfig() + os.Exit(0) + } + time.Sleep(20 * time.Minute) + } +} + +func (v Verification) prettyPrint() string { + ret := "" + ret += fmt.Sprintf("```%+v has marked %+v's verification as %+v\n", v.Admin, v.Username, v.Status) + ret += fmt.Sprintf("Submitted: %+v\nClosed: %+v\n", v.Submitted, v.Closed) + ret += fmt.Sprintf("Turnaround time: %+v```", time.Since(v.Submitted)) + ret += fmt.Sprintf("\n%+v", v.Photo) + return ret +} + +func userFromID(s *discordgo.Session, i string) discordgo.User { + u, err := s.GuildMember(config.GuildID, i) + if err != nil { + log.LogErrorType(err) + return discordgo.User{} + } + return *u.User +} + +func adminInteraction(s *discordgo.Session, m string) { + admin, _ := s.GuildMember(config.GuildID, m) + counter, ok := config.Stats[admin.User.ID] + if !ok { + config.Stats[admin.User.ID] = 0 + } else { + config.Stats[admin.User.ID] = counter + 1 + } + +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..4ac2c5d --- /dev/null +++ b/main.go @@ -0,0 +1,363 @@ +package main + +import ( + "flag" + "fmt" + "math/rand" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/bwmarrin/discordgo" + "github.com/rudi9719/loggy" +) + +var ( + // Logging Setup + logOpts = loggy.LogOpts{ + UseStdout: true, + Level: 5, + KBTeam: "nightmarehaus.logs.fetiche", + KBChann: "general", + ProgName: "disgord-thanos", + } + log = loggy.NewLogger(logOpts) + startupTime time.Time + setupToken = "!setup " + rebootToken = "!reboot " + bump = true + config Config + lastActiveChan string + lastActiveTime time.Time + token string + configFile string + setupMsg string + lastPM = make(map[string]time.Time) + 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() { + defer log.PanicSafe() + startupTime = time.Now() + lastActiveTime = time.Now() + lastActiveChan = "627620309754839070" + if token == "" { + log.LogPanic("No token provided. Please run: disgord-thanos -t ") + } + defer log.PanicSafe() + if configFile == "" { + configFile = "config.json" + setupToken = fmt.Sprintf("!setup %+v", rand.Intn(9999)+1000) + rebootToken = fmt.Sprintf("!reboot %+v", rand.Intn(9999)+1000) + log.LogCritical(fmt.Sprintf("SetupToken: %+v\nRebootToken: %+v", setupToken, rebootToken)) + } else { + loadConfig() + } + + 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) + + 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) + } + } + 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.UpdateStatus(0, "DreamDaddy v0.5") +} + +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())) +} + +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) + } + } +} + +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) + } + } else { + delete(config.Probations, uid) + } + } + s.ChannelMessageSend(config.AdminChannel, fmt.Sprintf("%+v (@%+v) has left, ban: %+v", m.User.ID, m.User.Username, banned)) +} + +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!") + s.ChannelMessageSend("627620309754839070", fmt.Sprintf("Welcome %+v please introduce yourself! :)", u.Mention())) +} + +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)") + +} + +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 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) + msg, _ := s.ChannelMessageSend(config.AdminChannel, fmt.Sprintf("%+v\n%+v", m.Author.Username, m.Attachments[0].ProxyURL)) + 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" + 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" + } else if m.Emoji.Name == "👶" { + requestAge(s, user) + log.LogInfo(fmt.Sprintf("%+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(fmt.Sprintf("%+v", verification.prettyPrint())) + delete(config.Verifications, m.MessageID) +} + +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 && time.Since(config.BumpTime) > 2 * time.Hour && !strings.Contains(m.Content, "!d bump") { + s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("%+v please say \"!d bump\" without the quotes to bump our server :)", m.Author.Mention())) + } + 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 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())) + } + } + 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 config.AdminChannel == "" { + if !strings.HasPrefix(m.Content, setupToken) { + return + } + config.GuildID = m.GuildID + setAdminChannel(s, m) + return + } else if config.MonitorChann == "" { + if !strings.HasPrefix(m.Content, setupToken) { + return + } + setMonitorChann(s, m) + return + } + 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", "")) + pmChann, _ := s.UserChannelCreate(m.Author.ID) + s.ChannelMessageSend(pmChann.ID, fmt.Sprintf("Your quote was added.\n %+v", quotes)) + return + } + if strings.HasPrefix(m.Content, "!snap") || strings.HasPrefix(m.Content, "!purge") { + go runPurge(s) + s.ChannelMessageSend(config.AdminChannel, quotes[rand.Intn(len(quotes))]) + return + } + if strings.HasPrefix(m.Content, "!st") { + go status(s) + saveConfig() + } + } +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..0abcaa7 --- /dev/null +++ b/types.go @@ -0,0 +1,29 @@ +package main + +import "time" + +// Config struct used for bot +type Config struct { + GuildID string + AdminChannel string + AdminRole string + MonitorRole string + MonitorChann string + VerifiedRole string + BumpTime time.Time + LastBumper string + Stats map[string]int + Unverified map[string]time.Time + Verifications map[string]Verification + Probations map[string]time.Time +} + +type Verification struct { + UserID string + Username string + Photo string + Submitted time.Time + Status string + Admin string + Closed time.Time +}