├── .gitignore ├── .travis.yml ├── Procfile ├── chess.go ├── command.go ├── const.go ├── cringe.go ├── db.go ├── emoji.go ├── funny.go ├── genkey.go ├── giveaway.go ├── go.mod ├── go.sum ├── invoice.go ├── main.go ├── mocker.go ├── optout.go ├── rekt.go ├── replier.go ├── replies.go ├── role_reapplicator.go ├── rules.go ├── staff.go ├── stupid.go ├── utils.go ├── verify.go └── welcomer.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .env 3 | /ImpactBot 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.13.x 5 | 6 | script: 7 | - diff -u <(echo -n) <(gofmt -d ./) 8 | - go test -v ./... 9 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: ./bin/ImpactBot 2 | -------------------------------------------------------------------------------- /chess.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bwmarrin/discordgo" 7 | ) 8 | 9 | func chess(caller *discordgo.Member, msg *discordgo.Message, args []string) error { 10 | err := discord.GuildMemberRoleAdd(impactServer, caller.User.ID, Chess.ID) 11 | if err != nil { 12 | return err 13 | } 14 | discord.MessageReactionAdd(msg.ChannelID, msg.ID, check) 15 | 16 | return resp(msg.ChannelID, fmt.Sprintf("User has been given chess role!")) 17 | } 18 | 19 | func unchess(caller *discordgo.Member, msg *discordgo.Message, args []string) error { 20 | err := discord.GuildMemberRoleRemove(impactServer, caller.User.ID, Chess.ID) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | return discord.MessageReactionAdd(msg.ChannelID, msg.ID, check) 26 | } 27 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/bwmarrin/discordgo" 11 | ) 12 | 13 | var ( 14 | prefix string 15 | prefixPattern *regexp.Regexp 16 | ) 17 | 18 | type Command struct { 19 | Name string 20 | Aliases []string 21 | Description string 22 | Usage []string 23 | RoleNeeded *Role 24 | Handler func(caller *discordgo.Member, message *discordgo.Message, args []string) error 25 | } 26 | 27 | // List of commands 28 | var Commands = []Command{ 29 | { 30 | Name: "optout", 31 | Description: "Opt out of our terms and leave the server permanently", 32 | Usage: []string{"i am sure"}, 33 | Handler: optOutHandler, 34 | }, 35 | { 36 | Name: "tempmute", 37 | Aliases: []string{"tm"}, 38 | Description: "Mute someone temporarily, optionally from a specific channel", 39 | Usage: []string{"@user reason", "@user #channel reason", "#channel @user reason"}, 40 | RoleNeeded: &Support, 41 | Handler: muteHandler, 42 | }, 43 | { 44 | Name: "mute", 45 | Aliases: []string{"m"}, 46 | Description: "Mute someone permanently, optionally from a specific channel", 47 | Usage: []string{"@user reason", "@user #channel reason", "#channel @user reason"}, 48 | RoleNeeded: &Moderator, 49 | Handler: muteHandler, 50 | }, 51 | { 52 | Name: "unmute", 53 | Aliases: []string{"um"}, 54 | Description: "Unmute someone, either server-wide, from a specific channel, or remove all mutes", 55 | Usage: []string{"@user", "@user #channel", "@user all"}, 56 | RoleNeeded: &Moderator, 57 | Handler: unmuteHandler, 58 | }, 59 | { 60 | Name: "kick", 61 | Description: "Kick someone from the server", 62 | Usage: []string{"@user reason"}, 63 | RoleNeeded: &Moderator, 64 | Handler: rektHandler, 65 | }, 66 | { 67 | Name: "ban", 68 | Aliases: []string{"rm"}, 69 | Description: "Ban someone from the server", 70 | Usage: []string{"@user reason"}, 71 | RoleNeeded: &Moderator, 72 | Handler: rektHandler, 73 | }, 74 | { 75 | Name: "rules", 76 | Aliases: []string{"rule", "r"}, 77 | Description: "Display the rules or a specific rule. Optionally @mention a user to tag them in the response.", 78 | Usage: []string{ 79 | "", 80 | "", 81 | "", 82 | }, 83 | Handler: rulesHandler, 84 | }, 85 | { 86 | Name: "want", 87 | Description: "want a nick", 88 | Usage: []string{ 89 | "", 90 | }, 91 | RoleNeeded: &Support, 92 | Handler: wantHandler, 93 | }, 94 | { 95 | Name: "cringe", 96 | Aliases: []string{"c"}, 97 | Description: "Generates a random cringe image", 98 | Usage: []string{""}, 99 | Handler: handleCringe, 100 | }, 101 | { 102 | Name: "addcringe", 103 | Aliases: []string{"ac"}, 104 | Description: "Adds a cringe photo to the collection", 105 | Usage: []string{"", "url"}, 106 | RoleNeeded: &Support, 107 | Handler: handleAddCringe, 108 | }, 109 | { 110 | Name: "delcringe", 111 | Aliases: []string{"dc"}, 112 | Description: "Removes a cringe photo from the collection", 113 | Usage: []string{"", "url"}, 114 | RoleNeeded: &Moderator, 115 | Handler: handleDelCringe, 116 | }, 117 | { 118 | Name: "genkey", 119 | Aliases: []string{"gk"}, 120 | Description: "Generates an Impact premium key", 121 | Usage: []string{"", "role [...roles]"}, 122 | RoleNeeded: &SeniorMod, 123 | Handler: genkey, 124 | }, 125 | { 126 | Name: "giveaway", 127 | Description: "Gives you the giveaway role", 128 | Usage: []string{""}, 129 | Handler: giveaway, 130 | }, 131 | { 132 | Name: "ungiveaway", 133 | Description: "Removes the giveaway role", 134 | Usage: []string{""}, 135 | Handler: ungiveaway, 136 | }, 137 | { 138 | Name: "stupid", 139 | Description: "makes you so stupid impcat bot will ignore you", 140 | Usage: []string{""}, 141 | Handler: stupid, 142 | }, 143 | { 144 | Name: "unstupid", 145 | Description: "no more stupid", 146 | Usage: []string{""}, 147 | Handler: unstupid, 148 | }, 149 | { 150 | Name: "funny", 151 | Aliases: []string{"unfunny"}, 152 | Description: "didnt laugh", 153 | Usage: []string{""}, 154 | Handler: handleFunny, 155 | }, 156 | { 157 | Name: "chess", 158 | Description: "chess mode! gives you chess role", 159 | Usage: []string{""}, 160 | Handler: chess, 161 | }, 162 | { 163 | Name: "unchess", 164 | Description: "no more chess", 165 | Usage: []string{""}, 166 | Handler: unchess, 167 | }, 168 | } 169 | 170 | func init() { 171 | // Load prefix from the environment 172 | prefix = os.Getenv("IMPACT_PREFIX") 173 | if prefix == "" { 174 | prefix = "i!" 175 | } 176 | // Match case-insensitive & ignore whitespace around prefix 177 | prefixPattern = regexp.MustCompile(`(?i)^\s*` + regexp.QuoteMeta(prefix) + `\s*`) 178 | 179 | // Have to append helpCommand after initializing Commands to avoid an initialization loop 180 | Commands = append(Commands, helpCommand) 181 | } 182 | 183 | func onMessageSentCommandHandler(session *discordgo.Session, m *discordgo.MessageCreate) { 184 | msg := m.Message 185 | if msg == nil || msg.Author == nil || msg.Type != discordgo.MessageTypeDefault || msg.Author.ID == myselfID { 186 | return // wtf 187 | } 188 | if msg.GuildID != impactServer && msg.GuildID != "" { 189 | return // Only allow guild messages and DMs 190 | } 191 | 192 | content := msg.Content 193 | content = strings.Replace(content, ">", "> ", -1) 194 | content = strings.Replace(content, "<", " <", -1) 195 | 196 | if match := prefixPattern.FindString(content); match != "" { // bot woke 197 | args := strings.Fields(content[len(match):]) 198 | command := findCommand(strings.ToLower(args[0])) 199 | if command == nil { 200 | _ = resp(msg.ChannelID, fmt.Sprintf("Command \"%s\" not found! Try %shelp", args[0], prefix)) 201 | return 202 | } 203 | author, err := GetMember(msg.Author.ID) 204 | if err != nil { 205 | return 206 | } 207 | if command.RoleNeeded != nil && !IsUserAtLeast(author, *command.RoleNeeded) { 208 | _ = resp(msg.ChannelID, fmt.Sprintf("Command \"%s\" requires at least %s", command.Name, command.RoleNeeded.Name)) 209 | return 210 | } 211 | err = command.Handler(author, msg, args) 212 | if err != nil { 213 | _ = resp(msg.ChannelID, fmt.Sprintf("Command \"%s\" returned an error: %s", command.Name, err.Error())) 214 | return 215 | } 216 | 217 | } 218 | } 219 | 220 | func findCommand(command string) *Command { 221 | for _, it := range Commands { 222 | if command == it.Name { 223 | return &it 224 | } 225 | for _, alias := range it.Aliases { 226 | if command == alias { 227 | return &it 228 | } 229 | } 230 | } 231 | return nil 232 | } 233 | 234 | var helpCommand = Command{ 235 | Name: "help", 236 | Description: "Display this help message. Specify `all` to include commands you don't have permission to run. Specify a command's name or alias to see help for only that specific command.", 237 | Aliases: []string{"?"}, 238 | Usage: []string{ 239 | "", 240 | "all", 241 | "", 242 | }, 243 | Handler: func(caller *discordgo.Member, message *discordgo.Message, args []string) error { 244 | embed := discordgo.MessageEmbed{ 245 | Color: prettyembedcolor, 246 | Fields: []*discordgo.MessageEmbedField{}, 247 | } 248 | // all is true if the user asked for commands they don't have permission for 249 | all := len(args) == 2 && strings.ToLower(args[1]) == "all" 250 | if len(args) < 2 || all { 251 | // All commands 252 | embed.Title = "ImpactBot help" 253 | embed.Description = "Available commands:" 254 | if all { 255 | embed.Description = "All commands:" 256 | } 257 | for _, command := range Commands { 258 | // Include this command if the user has permission to run it or they asked for "all" commands 259 | if all || command.RoleNeeded == nil || IsUserAtLeast(caller, *command.RoleNeeded) { 260 | embed.Fields = append(embed.Fields, &discordgo.MessageEmbedField{ 261 | Name: command.Name, 262 | Value: command.helpText(), 263 | }) 264 | } 265 | } 266 | } else { 267 | // Specified commands 268 | command := findCommand(args[1]) 269 | if command == nil { 270 | return errors.New(fmt.Sprintf("Command \"%s\" not found! Try %shelp", args[1], prefix)) 271 | } 272 | embed.Title = fmt.Sprintf("Command `%s` usage", command.Name) 273 | embed.Description = command.helpText() 274 | embed.Footer = &discordgo.MessageEmbedFooter{ 275 | Text: fmt.Sprintf("See %shelp for alternative commands", prefix), 276 | } 277 | } 278 | _, err := discord.ChannelMessageSendEmbed(message.ChannelID, &embed) 279 | if err != nil { 280 | return err 281 | } 282 | 283 | return nil 284 | }, 285 | } 286 | 287 | func (c Command) helpText() string { 288 | var desc strings.Builder 289 | // Print aliases first 290 | if len(c.Aliases) > 0 { 291 | desc.WriteString("\n_Alias") 292 | if len(c.Aliases) > 1 { 293 | // plural meme 294 | desc.WriteString("es") 295 | } 296 | desc.WriteString(": ") 297 | for _, alias := range c.Aliases { 298 | desc.WriteString(fmt.Sprintf("**%s** ", alias)) 299 | } 300 | desc.WriteString("_\n") 301 | } 302 | // Then usages 303 | if len(c.Usage) > 0 { 304 | desc.WriteString("```\n") 305 | for _, usage := range c.Usage { 306 | desc.WriteString(fmt.Sprintf("%s%s %s\n", prefix, c.Name, usage)) 307 | } 308 | desc.WriteString("```") 309 | } 310 | // Then description 311 | desc.WriteString(c.Description) 312 | if !strings.HasSuffix(c.Description, ".") { 313 | desc.WriteString(".") 314 | } 315 | // Then, finally, required permissions 316 | if c.RoleNeeded != nil { 317 | desc.WriteString(fmt.Sprintf("\nRequires `%s` or higher", c.RoleNeeded.Name)) 318 | } 319 | 320 | return desc.String() 321 | } 322 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //noinspection GoUnusedConst 4 | const ( 5 | impactServer = "208753003996512258" 6 | brady = "205718273696858113" 7 | prettyembedcolor = 0xAD7EC2 8 | 9 | trash = "🗑" 10 | check = "✅" 11 | 12 | announcements = "378645175947362320" 13 | general = "208753003996512258" 14 | media = "514880901629607947" 15 | help = "222120655594848256" 16 | distro = "433342110461067266" 17 | politics = "745728642587295744" 18 | bot = "306182416329080833" 19 | donatorInfo = "613478149669388298" 20 | betterGeneral = "617140506069303306" 21 | betterHelp = "583453983427788830" 22 | na = "616409814779691114" 23 | oldguys = "293796603146272768" 24 | development = "280478531346104321" 25 | impactBotLog = "617549691730526209" 26 | testing = "617066818925756506" 27 | ) 28 | 29 | var ( 30 | HeadDev = Role{"209817890713632768", "Head Developer"} 31 | Developer = Role{"221655083748687873", "Developer"} 32 | SeniorMod = Role{"663065117738663938", "Senior Moderator"} 33 | Moderator = Role{"210377982731223040", "Moderator"} 34 | Support = Role{"245682967546953738", "Support"} 35 | Donator = Role{"210114021641289728", "Donator"} 36 | Verified = Role{"671048798654562354", "Verified"} 37 | InVoice = Role{"677329885680762904", "In Voice"} 38 | Stupid = Role{"743903534160019476", "Stupid"} 39 | Weeb = Role{"612744883467190275", "weeb"} 40 | Giveaway = Role{"698619050833477633", "Giveaway Ping"} 41 | Chess = Role{"816750980807786546", "Chess"} 42 | ) 43 | 44 | type Role struct { 45 | // Discord id 46 | ID string 47 | Name string 48 | } 49 | 50 | func RolesToIDs(roles []Role) []string { 51 | var ids []string 52 | for _, role := range roles { 53 | ids = append(ids, role.ID) 54 | } 55 | return ids 56 | } 57 | 58 | var muteRoles = map[string]string{ 59 | "": "630800201015361566", 60 | general: "352144990606196749", 61 | media: "669632558551793666", 62 | help: "230803433752363020", 63 | distro: "624971877424693288", 64 | politics: "745729048688328865", 65 | bot: "640263788985188362", 66 | betterGeneral: "669633342525800471", 67 | betterHelp: "669633463242194955", 68 | na: "669632725644214283", 69 | oldguys: "669632828371107881", 70 | development: "669632988686057472", 71 | } 72 | 73 | func (r Role) Mention() string { 74 | return "<@&" + r.ID + ">" 75 | } 76 | -------------------------------------------------------------------------------- /cringe.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | "github.com/bwmarrin/discordgo" 8 | ) 9 | 10 | // this file is the story of my life lol 11 | 12 | func handleCringe(_ *discordgo.Member, msg *discordgo.Message, _ []string) error { 13 | var rngCringe string 14 | err := DB.QueryRow("SELECT image FROM cringe ORDER BY RANDOM() LIMIT 1").Scan(&rngCringe) 15 | if err != nil { 16 | return err 17 | } 18 | reply := discordgo.MessageEmbed{ 19 | Title: ":camera_with_flash:", 20 | Image: &discordgo.MessageEmbedImage{ 21 | URL: rngCringe, 22 | }, 23 | Color: prettyembedcolor, 24 | } 25 | _, err = discord.ChannelMessageSendEmbed(msg.ChannelID, &reply) 26 | go cringeReact(msg.ChannelID, msg.ID) 27 | return err 28 | } 29 | 30 | func handleAddCringe(caller *discordgo.Member, msg *discordgo.Message, args []string) error { 31 | if caller.User.ID == "488400748296667147" { 32 | return fmt.Errorf("cringe vini does not get to add cringe because cringe vini himself is cringe") 33 | } 34 | if caller.User.ID == "162848980647018496" { 35 | return fmt.Errorf("berrely is too cringe to add cringe") 36 | } 37 | 38 | if len(args) < 2 { 39 | if len(msg.Attachments) > 0 { 40 | return cringe(msg.Attachments[0].URL, msg.ChannelID, msg.ID) 41 | } 42 | return fmt.Errorf("error : no attachments / links found to add") 43 | } 44 | _, err := url.ParseRequestURI(args[1]) 45 | if err != nil { 46 | return fmt.Errorf("invalid url scheme") 47 | } 48 | return cringe(args[1], msg.ChannelID, msg.ID) 49 | } 50 | 51 | func handleDelCringe(caller *discordgo.Member, msg *discordgo.Message, args []string) error { 52 | if len(args) < 2 { 53 | return fmt.Errorf("give url") 54 | } 55 | _, err := url.ParseRequestURI(args[1]) 56 | if err != nil { 57 | return fmt.Errorf("invalid url scheme") 58 | } 59 | var count int 60 | err = DB.QueryRow("SELECT COUNT(*) FROM cringe WHERE image = $1", args[1]).Scan(&count) 61 | if err != nil { 62 | return err 63 | } 64 | if count == 0 { 65 | return fmt.Errorf("that is not cringe") 66 | } 67 | _, err = DB.Exec("DELETE FROM cringe WHERE image = $1", args[1]) 68 | if err != nil { 69 | return err 70 | } 71 | return resp(msg.ChannelID, "cringe deleted successfully") 72 | } 73 | 74 | func cringe(url string, channelID string, messageID string) error { 75 | _, err := DB.Exec("INSERT INTO cringe(image) VALUES($1)", url) 76 | if err == nil { 77 | go cringeReact(channelID, messageID) 78 | } 79 | return err 80 | } 81 | 82 | func cringeReact(channelID string, messageID string) { 83 | _ = discord.MessageReactionAdd(channelID, messageID, "why_steve_a_pig:558474255776481291") 84 | _ = discord.MessageReactionAdd(channelID, messageID, "im_stuff:558474787031351339") 85 | _ = discord.MessageReactionAdd(channelID, messageID, "alex_omg_no:558475172022059009") 86 | _ = discord.MessageReactionAdd(channelID, messageID, "steve_your_sister_is_awesome:558475291454996510") 87 | } 88 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | _ "github.com/lib/pq" 10 | ) 11 | 12 | var DB *sql.DB 13 | 14 | func init() { 15 | url := os.Getenv("DATABASE_URL") 16 | if url == "" { 17 | fmt.Println("WARNING: No database url specified, not connecting to postgres!") 18 | return 19 | } 20 | var err error 21 | DB, err = sql.Open("postgres", url) 22 | if err != nil { 23 | // ok if there IS a url then we're expected to be able to connect 24 | // so if THAT fails, then that's a real error 25 | panic(err) 26 | } 27 | err = DB.Ping() 28 | if err != nil { 29 | // apparently this DOUBLE CHECKS that it's up? 30 | panic(err) 31 | } 32 | err = schema() 33 | if err != nil { 34 | // Failed to create table or something 35 | panic(err) 36 | } 37 | } 38 | 39 | func schema() (err error) { 40 | _, err = DB.Exec(` 41 | CREATE EXTENSION IF NOT EXISTS "pgcrypto"; 42 | `) 43 | if err != nil { 44 | log.Println("Unable to load pgcrypto extension") 45 | panic(err) 46 | } 47 | 48 | _, err = DB.Exec(` 49 | CREATE TABLE IF NOT EXISTS mutes ( 50 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 51 | discord_id TEXT NOT NULL, 52 | channel_id TEXT, 53 | expiration TIMESTAMP 54 | ); 55 | `) 56 | if err != nil { 57 | log.Println("Unable to create mutes table") 58 | panic(err) 59 | } 60 | 61 | _, err = DB.Exec(` 62 | CREATE TABLE IF NOT EXISTS cringe ( 63 | image TEXT NOT NULL 64 | ) 65 | `) 66 | if err != nil { 67 | log.Println("Unable to create cringe table") 68 | panic(err) 69 | } 70 | 71 | _, err = DB.Exec(` 72 | CREATE TABLE IF NOT EXISTS nicks ( 73 | id TEXT PRIMARY KEY, 74 | nick SERIAL 75 | ) 76 | `) 77 | if err != nil { 78 | log.Println("Unable to create nicks table") 79 | panic(err) 80 | } 81 | 82 | _, err = DB.Exec(` 83 | CREATE TABLE IF NOT EXISTS nicktrade ( 84 | id TEXT, 85 | desirednick INTEGER, 86 | 87 | UNIQUE(id, desirednick) 88 | ) 89 | `) 90 | if err != nil { 91 | log.Println("Unable to create nicktrade table") 92 | panic(err) 93 | } 94 | 95 | 96 | 97 | return 98 | } 99 | -------------------------------------------------------------------------------- /emoji.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/bwmarrin/discordgo" 5 | "log" 6 | emoji "github.com/tmdvs/Go-Emoji-Utils" 7 | "regexp" 8 | "strings" 9 | "unicode" 10 | ) 11 | 12 | var discordEmote = regexp.MustCompile(``) 13 | func emojiMsg(m *discordgo.Message){ 14 | if m.ChannelID != "808248247520985130" { 15 | return 16 | } 17 | 18 | stripped := strings.Map(func(r rune) rune { 19 | if unicode.IsSpace(r) { 20 | return -1 21 | } 22 | return r 23 | }, discordEmote.ReplaceAllString(emoji.RemoveAll(m.Content), "")) 24 | 25 | log.Println("Original message:", m.Content) 26 | log.Println("Stripped:", stripped) 27 | 28 | if stripped != "" { 29 | err := discord.GuildMemberDeleteWithReason(m.GuildID, m.Author.ID, "Sending a non emoji message in the emoji-only channel") 30 | if err != nil { 31 | return 32 | } 33 | resp(m.ChannelID, "User <@" + m.Author.ID + "> was kicked for sending https://discord.com/channels/208753003996512258/808248247520985130/"+m.ID+" because it has non emoji characters: `" + stripped + "`") 34 | } 35 | } 36 | 37 | 38 | func onMessageEdited(session *discordgo.Session, m *discordgo.MessageUpdate) { 39 | emojiMsg(m.Message) 40 | } 41 | 42 | -------------------------------------------------------------------------------- /funny.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/PuerkitoBio/goquery" 8 | "github.com/bwmarrin/discordgo" 9 | ) 10 | 11 | func handleFunny(_ *discordgo.Member, msg *discordgo.Message, _ []string) error { 12 | resp, err := http.Get("https://ifunny.co/feeds/shuffle") 13 | if err != nil { 14 | return err 15 | } 16 | doc, err := goquery.NewDocumentFromResponse(resp) 17 | if err != nil { 18 | return err 19 | } 20 | val, exists := doc.Find(".media__image").First().Attr("data-src") 21 | if !exists { 22 | return errors.New("we ran out of runny =(") 23 | } 24 | reply := &discordgo.MessageEmbed{ 25 | Title: ":rofl:", 26 | Image: &discordgo.MessageEmbedImage{ 27 | URL: val, 28 | }, 29 | Color: prettyembedcolor, 30 | } 31 | _, err = discord.ChannelMessageSendEmbed(msg.ChannelID, reply) 32 | return err 33 | } 34 | -------------------------------------------------------------------------------- /genkey.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "strings" 10 | 11 | "github.com/bwmarrin/discordgo" 12 | ) 13 | 14 | func genkey(caller *discordgo.Member, msg *discordgo.Message, args []string) error { 15 | // Strip command name from args and set default role 16 | args = args[1:] 17 | if len(args) < 1 { 18 | args = []string{"premium"} 19 | } 20 | 21 | // Serialise args to a role array 22 | // TODO ignore prefixing args? 23 | var roles strings.Builder 24 | for _, role := range args { 25 | if roles.Len() > 0 { 26 | roles.WriteString("&") 27 | } 28 | roles.WriteString("role=" + url.QueryEscape(role)) 29 | } 30 | 31 | // Default to premium 32 | if roles.Len() == 0 { 33 | return errors.New("No valid roles in list") 34 | } 35 | 36 | code, err := get("https://api.impactclient.net/v1/integration/impactbot/genkey?auth=" + os.Getenv("IMPACTBOT_AUTH_SECRET") + "&" + roles.String()) 37 | if err != nil { 38 | return err 39 | } 40 | return resp(msg.ChannelID, "Token is `"+code+"`\n[Click here to register an Impact Account](https://impactclient.net/register.html?token="+code+")") 41 | } 42 | 43 | func get(url string) (string, error) { 44 | resp, err := http.Get(url) 45 | if err != nil { 46 | return "", err 47 | } 48 | defer resp.Body.Close() 49 | data, err := ioutil.ReadAll(resp.Body) 50 | if err != nil { 51 | return "", err 52 | } 53 | if resp.StatusCode != http.StatusOK { 54 | return "", errors.New(string(data)) 55 | } 56 | return string(data), nil 57 | } 58 | -------------------------------------------------------------------------------- /giveaway.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/bwmarrin/discordgo" 5 | ) 6 | 7 | func giveaway(caller *discordgo.Member, msg *discordgo.Message, args []string) error { 8 | err := discord.GuildMemberRoleAdd(impactServer, caller.User.ID, Giveaway.ID) 9 | if err != nil { 10 | return err 11 | } 12 | return discord.MessageReactionAdd(msg.ChannelID, msg.ID, check) 13 | } 14 | 15 | func ungiveaway(caller *discordgo.Member, msg *discordgo.Message, args []string) error { 16 | err := discord.GuildMemberRoleRemove(impactServer, caller.User.ID, Giveaway.ID) 17 | if err != nil { 18 | return err 19 | } 20 | return discord.MessageReactionAdd(msg.ChannelID, msg.ID, check) 21 | } 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ImpactDevelopment/ImpactBot 2 | 3 | go 1.13 4 | 5 | // +heroku goVersion go1.13 6 | 7 | require ( 8 | github.com/PuerkitoBio/goquery v1.5.1 9 | github.com/bwmarrin/discordgo v0.23.2 10 | github.com/google/uuid v1.1.1 11 | github.com/gorilla/websocket v1.4.2 // indirect 12 | github.com/lib/pq v1.2.0 13 | github.com/stretchr/testify v1.4.0 // indirect 14 | github.com/subosito/gotenv v1.2.0 15 | github.com/tmdvs/Go-Emoji-Utils v1.1.0 16 | github.com/writeas/go-strip-markdown v2.0.1+incompatible 17 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect 18 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= 2 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 3 | github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= 4 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 5 | github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4= 6 | github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M= 7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 10 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= 12 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 13 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 14 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 15 | github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= 16 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 21 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 22 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 23 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 24 | github.com/tmdvs/Go-Emoji-Utils v1.1.0 h1:gtPix7HZPrd49+MNDcuRLvv4xVNxCE5wgjqyuvmbyYg= 25 | github.com/tmdvs/Go-Emoji-Utils v1.1.0/go.mod h1:J82i2WeGn+Kz+T3s5v9+i/OJlvevIVfGZ6qXgqiNWBc= 26 | github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw= 27 | github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE= 28 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= 29 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 30 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 31 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= 32 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 33 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 34 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 35 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 36 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= 37 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 38 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 39 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= 41 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 43 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 44 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 46 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 47 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 48 | -------------------------------------------------------------------------------- /invoice.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/bwmarrin/discordgo" 5 | ) 6 | 7 | func onVoiceStateUpdate(session *discordgo.Session, m *discordgo.VoiceStateUpdate) { 8 | if m.GuildID != impactServer { 9 | return 10 | } 11 | if m.ChannelID == "" || m.Deaf || m.SelfDeaf { 12 | _ = session.GuildMemberRoleRemove(impactServer, m.UserID, InVoice.ID) 13 | } else { 14 | _ = session.GuildMemberRoleAdd(impactServer, m.UserID, InVoice.ID) 15 | } 16 | } 17 | 18 | func checkDeservesInVoiceRole(userid string) bool { 19 | for _, guild := range discord.State.Guilds { 20 | for _, vs := range guild.VoiceStates { 21 | if vs.UserID == userid && !vs.Deaf && !vs.SelfDeaf { 22 | return true 23 | } 24 | } 25 | } 26 | return false 27 | } 28 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/bwmarrin/discordgo" 8 | "github.com/subosito/gotenv" 9 | ) 10 | 11 | var discord *discordgo.Session 12 | 13 | var myselfID string 14 | 15 | func init() { 16 | var err error 17 | 18 | // You can set environment variables in the git-ignored .env file for convenience while running locally 19 | err = gotenv.Load() 20 | if err == nil { 21 | println("Loaded .env file") 22 | } else if os.IsNotExist(err) { 23 | println("No .env file found") 24 | err = nil // Mutating state is bad mkay 25 | } else { 26 | panic(err) 27 | } 28 | 29 | token := os.Getenv("DISCORD_BOT_TOKEN") 30 | if token == "" { 31 | panic("Must set environment variable DISCORD_BOT_TOKEN") 32 | } 33 | log.Println("Establishing discord connection") 34 | discord, err = discordgo.New("Bot " + token) 35 | if err != nil { 36 | panic(err) 37 | } 38 | discord.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsGuildMembers | discordgo.IntentsAllWithoutPrivileged) 39 | user, err := discord.User("@me") 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | myselfID = user.ID 45 | log.Println("I am", myselfID) 46 | 47 | discord.AddHandler(onUserJoin) 48 | discord.AddHandler(onMessageSent) 49 | discord.AddHandler(onMessageReactedTo) 50 | discord.AddHandler(onReady) 51 | discord.AddHandler(onMessageSent2) 52 | discord.AddHandler(onMessageSentCommandHandler) 53 | discord.AddHandler(onUserJoin2) 54 | discord.AddHandler(onVoiceStateUpdate) 55 | discord.AddHandler(onUserJoin3) 56 | discord.AddHandler(onReady2) 57 | discord.AddHandler(onGuildMemberUpdate) 58 | discord.AddHandler(onMessageEdited) 59 | } 60 | 61 | func main() { 62 | err := discord.Open() 63 | if err != nil { 64 | panic(err) 65 | } 66 | log.Println("Connected to discord") 67 | forever := make(chan int) 68 | <-forever 69 | } 70 | 71 | func onReady(discord *discordgo.Session, ready *discordgo.Ready) { 72 | err := discord.UpdateStatusComplex(discordgo.UpdateStatusData{ 73 | IdleSince: nil, 74 | Activities: []*discordgo.Activity{{ 75 | Name: "the Impact Discord", 76 | Type: 3, 77 | }}, 78 | AFK: false, 79 | Status: "", 80 | }) 81 | if err != nil { 82 | log.Println("Error attempting to set my status") 83 | log.Println(err) 84 | } 85 | servers := discord.State.Guilds 86 | log.Printf("Impcat bot has started on %d servers:", len(servers)) 87 | for _, guild := range servers { 88 | log.Println("Server ID", guild.ID) 89 | fullGuild, err := discord.Guild(guild.ID) 90 | if err == nil { 91 | log.Println("Full server ID", fullGuild.ID, "Full name", fullGuild.Name) 92 | } 93 | } 94 | 95 | // Replace rules message 96 | updateRules() 97 | } 98 | -------------------------------------------------------------------------------- /mocker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/bwmarrin/discordgo" 9 | ) 10 | 11 | func canDMBot(userId string) bool { 12 | member, err := GetMember(userId) 13 | if err != nil || member == nil { 14 | return false 15 | } 16 | return !includes(member.Roles, muteRoles[help]) 17 | } 18 | 19 | // inform when someone DMs the bot because the messages are humorous 20 | func onMessageSent2(session *discordgo.Session, m *discordgo.MessageCreate) { 21 | msg := m.Message 22 | if msg == nil || msg.Author == nil || msg.Type != discordgo.MessageTypeDefault { 23 | return // wtf 24 | } 25 | author := msg.Author.ID 26 | 27 | // Don't talk to oneself 28 | if author == myselfID { 29 | return 30 | } 31 | 32 | if msg.GuildID != "" { 33 | return // DMs only! 34 | } 35 | 36 | if !canDMBot(author) { 37 | return 38 | } 39 | 40 | if match := prefixPattern.FindString(msg.Content); match != "" { 41 | return // this is a command 42 | } 43 | 44 | embed := &discordgo.MessageEmbed{ 45 | Author: &discordgo.MessageEmbedAuthor{}, 46 | Color: prettyembedcolor, 47 | Description: "", 48 | Fields: []*discordgo.MessageEmbedField{ 49 | { 50 | Name: ":rotating_light: :wheelchair: I have received a DM :wheelchair: :rotating_light:", 51 | Value: msg.Content, 52 | }, 53 | }, 54 | Footer: &discordgo.MessageEmbedFooter{ 55 | Text: "from @" + msg.Author.Username + "#" + msg.Author.Discriminator, 56 | }, 57 | } 58 | _, err := session.ChannelMessageSendEmbed(impactBotLog, embed) 59 | if err != nil { 60 | log.Println(err) 61 | } 62 | 63 | go func() { 64 | time.Sleep(1 * time.Second) 65 | ch, err := discord.UserChannelCreate(author) 66 | if err != nil { 67 | log.Println(err) 68 | return 69 | } 70 | err = discord.ChannelTyping(ch.ID) 71 | if err != nil { 72 | log.Println(err) 73 | } 74 | time.Sleep(2 * time.Second) 75 | 76 | _, err = discord.ChannelMessageSend(ch.ID, fmt.Sprintf("Hey, do you need some help? Checkout <#%s>!\n\nYour Discord User ID is `"+author+"`\n\n**Go to https://impactclient.net/discord.html?discord="+author+"** to verify your account and be able to talk on the server.", help)) 77 | if err != nil { 78 | log.Println(err) 79 | } 80 | }() 81 | } 82 | -------------------------------------------------------------------------------- /optout.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/bwmarrin/discordgo" 9 | ) 10 | 11 | func optOutHandler(caller *discordgo.Member, msg *discordgo.Message, args []string) (err error) { 12 | if len(args) < 2 || strings.ToLower(strings.Join(args[1:], " ")) != "i am sure" { 13 | return fmt.Errorf("You will be **kicked from this server** (or **banned** if you are a weeb) and **lose your roles** by continuing. Are you sure you want to opt out? `%s%s %s`", prefix, args[0], "i am sure") 14 | } 15 | 16 | if DB == nil { 17 | return errors.New("Unable to connect to database") 18 | } 19 | // Delete anything referencing them 20 | _, err = DB.Exec(`DELETE FROM mutes WHERE discord_id = $1`, caller.User.ID) 21 | if err != nil { 22 | return fmt.Errorf("We were unable to clear your info. Please contact a moderator.\nError: %s", err.Error()) 23 | } 24 | 25 | // Check if they're muted 26 | var muted bool 27 | for _, role := range muteRoles { 28 | if hasRole(caller, Role{ID: role}) { 29 | muted = true 30 | break 31 | } 32 | } 33 | 34 | // If muted ban, if not kick 35 | if muted { 36 | // Send them a DM before banning 37 | _ = SendDM(caller.User.ID, "We appreciate you opting out. We have deleted any data we had stored about you. You have been banned from the server to prevent bypassing our moderation system.") 38 | 39 | // We have to ban them or they could bypass a mute 40 | err = discord.GuildBanCreateWithReason(impactServer, caller.User.ID, "opted out of tos", 0) 41 | if err != nil { 42 | return fmt.Errorf("We were unable to ban you. Please contact a moderator.\nError: %s", err.Error()) 43 | } 44 | } else { 45 | // Send them a DM before kicking 46 | _ = SendDM(caller.User.ID, "Thank you for opting out. We have deleted any data we had stored about you and kicked you from the server.") 47 | 48 | err = discord.GuildMemberDeleteWithReason(impactServer, caller.User.ID, "opted out of tos") 49 | if err != nil { 50 | return fmt.Errorf("We were unable to kick you. Please contact a moderator.\nError: %s", err.Error()) 51 | } 52 | } 53 | 54 | return resp(msg.ChannelID, fmt.Sprintf("User @%s#%s has opted out and been banned to prevent mute bypassing", caller.User.Username, caller.User.Discriminator)) 55 | } 56 | -------------------------------------------------------------------------------- /rekt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "fmt" 7 | "regexp" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/bwmarrin/discordgo" 13 | "github.com/google/uuid" 14 | ) 15 | 16 | const ( 17 | RateLimit = 5 * time.Minute 18 | TempmuteDuration = 3 * time.Hour 19 | UnmuteInterval = 10 * time.Second 20 | ) 21 | 22 | var ratelimit = make(map[string]int64) 23 | var ratelimitLock sync.Mutex 24 | 25 | var mentionRegex = regexp.MustCompile(`^<(?P[#@])!?(?P\d+)>$`) 26 | 27 | func evalRatelimit(author string) bool { 28 | ratelimitLock.Lock() 29 | defer ratelimitLock.Unlock() 30 | 31 | until := ratelimit[author] 32 | if until < time.Now().UnixNano() { // defaults to 0 so this works properly 33 | rateLimitDuration := RateLimit 34 | if author == "162848980647018496" { 35 | // berrely is pee pee poo poo 36 | rateLimitDuration = 2 * time.Hour 37 | // can only tempmute every 2 hours instead of every 5 minutes 38 | } 39 | ratelimit[author] = time.Now().Add(rateLimitDuration).UnixNano() 40 | return true 41 | } 42 | 43 | return false 44 | } 45 | 46 | func resp(ch string, text string) error { 47 | embed := &discordgo.MessageEmbed{ 48 | Author: &discordgo.MessageEmbedAuthor{}, 49 | Color: prettyembedcolor, 50 | Description: text, 51 | Footer: &discordgo.MessageEmbedFooter{ 52 | Text: "♿ Impact Client ♿", 53 | }, 54 | Timestamp: time.Now().Format(time.RFC3339), 55 | } 56 | _, err := discord.ChannelMessageSendEmbed(ch, embed) 57 | return err 58 | } 59 | 60 | // Turns the first one or two args into users and/or channels and also returns whatever args weren't consumed 61 | func getUserAndChannelAndArgs(args []string) (user *discordgo.User, channel *discordgo.Channel, remainingArgs []string) { 62 | remainingArgs = args 63 | if len(args) < 1 { 64 | return 65 | } 66 | user, channel = getUserOrChannelForArg(args[0]) 67 | if user != nil || channel != nil { 68 | // Consume an arg 69 | remainingArgs = remainingArgs[1:] 70 | // Don't return since we want to try the second arg too 71 | } else { 72 | // No match on first arg so don't try to match second arg 73 | return 74 | } 75 | 76 | // getUserOrChannelForArg always has one nil arg, so if-else instead of if-elseif is fine 77 | if len(args) < 2 { 78 | return 79 | } 80 | if user == nil { 81 | user, _ = getUserOrChannelForArg(args[1]) 82 | if user != nil { 83 | // Consume an arg 84 | remainingArgs = remainingArgs[1:] 85 | } 86 | } else { 87 | _, channel = getUserOrChannelForArg(args[1]) 88 | if channel != nil { 89 | // Consume an arg 90 | remainingArgs = remainingArgs[1:] 91 | } 92 | } 93 | return 94 | } 95 | 96 | // Send a blocking api request if a match is found 97 | func getUserOrChannelForArg(arg string) (*discordgo.User, *discordgo.Channel) { 98 | match := findNamedMatches(mentionRegex, arg) 99 | id := match["ID"] 100 | if id == "" { 101 | return nil, nil 102 | } 103 | 104 | // Sends a blocking API request 105 | switch match["Type"] { 106 | case "#": 107 | { 108 | // Try to get from the "State" cache before falling back to the API 109 | channel, err := discord.State.Channel(id) 110 | if err != nil || channel == nil { 111 | channel, err = discord.Channel(id) 112 | } 113 | if err == nil { 114 | return nil, channel 115 | } 116 | } 117 | case "@": 118 | { 119 | // discordgo doesn't cache users, only guilds, channels & members 120 | user, err := discord.User(id) 121 | if err == nil { 122 | return user, nil 123 | } 124 | } 125 | } 126 | 127 | return nil, nil 128 | } 129 | 130 | // Gets the mute role associated with the given channel 131 | // Returns an error if no matching role exists 132 | func getMuteRoleForChannel(channel *discordgo.Channel) (role string, err error) { 133 | if channel == nil { 134 | role = muteRoles[""] 135 | return 136 | } 137 | var ok bool // Avoid shadowing role with := 138 | if role, ok = muteRoles[channel.ID]; !ok { 139 | err = fmt.Errorf("unable to find mute role for channel %s", channel.Mention()) 140 | } 141 | return 142 | } 143 | 144 | func muteHandler(caller *discordgo.Member, msg *discordgo.Message, args []string) error { 145 | user, channel, remainingArgs := getUserAndChannelAndArgs(args[1:]) 146 | if user == nil { 147 | return errors.New("First argument should mention user or channel") 148 | } 149 | if len(remainingArgs) < 1 { 150 | return errors.New("Give a reason") 151 | } 152 | 153 | target, err := GetMember(user.ID) 154 | if err != nil { 155 | return err 156 | } 157 | 158 | if !outranks(caller, target) { 159 | return fmt.Errorf("You don't outrank %s", target.User.Username) 160 | } 161 | 162 | // Support can tempmute, but only on users without roles 163 | if strings.ToLower(args[0]) == "tempmute" { 164 | 165 | if IsUserLowerThan(caller, Moderator) && !evalRatelimit(msg.Author.ID) { 166 | return errors.New("Too soon") 167 | } 168 | if IsUserLowerThan(caller, Moderator) { 169 | trustedRoles := append(RolesToIDs(staffRoles)) // TODO calculate this only once? 170 | for _, role := range target.Roles { 171 | if includes(trustedRoles, role) { 172 | return errors.New("They have trusted role(s)") 173 | } 174 | } 175 | } 176 | } 177 | 178 | muteRole, err := getMuteRoleForChannel(channel) 179 | if err != nil { 180 | return fmt.Errorf("Can't mute from %s yet", channel.Mention()) 181 | } 182 | 183 | // Reasons are important 184 | var reason strings.Builder 185 | reason.WriteString(args[0]) 186 | reason.WriteString(" has been issued to " + user.Username) 187 | if channel != nil { 188 | reason.WriteString(" from channel " + channel.Mention()) 189 | } 190 | reason.WriteString(" by @" + msg.Author.Username + "#" + msg.Author.Discriminator) 191 | reason.WriteString(" for reason: " + strings.Join(remainingArgs, " ")) 192 | providedReason := reason.String() 193 | 194 | // Direct message the user being muted 195 | DM, err := discord.UserChannelCreate(user.ID) // only creates it if it doesn"t already exist 196 | if err == nil { 197 | // if there is an error DMing them, we still want to ban them, they just won't know why 198 | err = resp(DM.ID, providedReason) 199 | if err != nil { 200 | fmt.Printf("Error direct messaging %s#%s: %s\n", user.Username, user.Discriminator, err.Error()) 201 | } 202 | } 203 | 204 | if DB == nil { 205 | return errors.New("I have no database, so I cannot ") 206 | } 207 | 208 | // Row values 209 | var ( 210 | id uuid.UUID 211 | userId = user.ID 212 | channelId sql.NullString 213 | ) 214 | if channel != nil { 215 | channelId = sql.NullString{ 216 | String: channel.ID, 217 | Valid: true, 218 | } 219 | } 220 | var expiration *time.Time 221 | if strings.ToLower(args[0]) == "tempmute" { 222 | exp := time.Now().Add(TempmuteDuration) // go doesn't let you do this in one line 223 | expiration = &exp 224 | } 225 | 226 | // Check if we have a matching mute already 227 | err = DB.QueryRow("SELECT id from mutes WHERE discord_id=$1 AND (channel_id=$2 OR ($2 IS NULL AND channel_id IS NULL))", userId, channelId).Scan(&id) 228 | 229 | if err == nil { 230 | // Update existing entry, but only if it was already a tempmute. Don't downgrade a mute to a tempmute. 231 | _, err = DB.Exec("UPDATE mutes SET expiration=$2 WHERE id=$1 AND expiration IS NOT NULL", id, expiration) 232 | if err != nil { 233 | return err 234 | } 235 | } else if errors.Is(err, sql.ErrNoRows) { 236 | // Insert new entry 237 | _, err = DB.Exec("INSERT INTO mutes (discord_id, channel_id, expiration) VALUES ($1, $2, $3)", userId, channelId, expiration) 238 | if err != nil { 239 | return err 240 | } 241 | } else { 242 | // Unknown error 243 | return err 244 | } 245 | err = discord.GuildMemberRoleAdd(msg.GuildID, user.ID, muteRole) 246 | if err != nil { 247 | return err 248 | } 249 | 250 | _ = resp(impactBotLog, providedReason) 251 | 252 | _ = resp(msg.ChannelID, providedReason) 253 | return nil 254 | } 255 | 256 | func unmuteHandler(caller *discordgo.Member, msg *discordgo.Message, args []string) (err error) { 257 | var unmuteAll bool 258 | user, channel, remainingArgs := getUserAndChannelAndArgs(args[1:]) 259 | if user == nil { 260 | return errors.New("First argument should mention user") 261 | } 262 | if channel == nil && len(remainingArgs) == 1 && "all" == strings.ToLower(remainingArgs[0]) { 263 | unmuteAll = true 264 | } else if len(remainingArgs) > 0 { 265 | return fmt.Errorf("Unexpected arguments \"%s\"", strings.Join(remainingArgs, " ")) 266 | } 267 | 268 | var target *discordgo.Member 269 | target, err = GetMember(user.ID) 270 | if err != nil { 271 | return 272 | } 273 | 274 | if outranks(target, caller) { 275 | return fmt.Errorf("You must be at least the same rank as %s", target.User.Username) 276 | } 277 | 278 | // produce a list of muted channels for the command output 279 | var fullMute bool 280 | var channels []string 281 | 282 | if unmuteAll { 283 | // Remove all mutes from DB 284 | _, err = DB.Exec("DELETE FROM mutes WHERE discord_id = $1", user.ID) 285 | if err != nil { 286 | return err 287 | } 288 | 289 | // Remove all mute roles 290 | var count uint 291 | for mutedChannel, muteRole := range muteRoles { 292 | if hasRole(target, Role{ID: muteRole}) { 293 | // Keep track of what was unmuted for the reply 294 | count++ 295 | if mutedChannel == "" { 296 | fullMute = true 297 | } else { 298 | channels = append(channels, mutedChannel) 299 | } 300 | 301 | // Remove the role 302 | err = discord.GuildMemberRoleRemove(msg.GuildID, user.ID, muteRole) 303 | } 304 | } 305 | 306 | // We didn't actually unmute anything! 307 | if count < 1 { 308 | return fmt.Errorf("No mutes found for @%s#%s", user.Username, user.Discriminator) 309 | } 310 | } else { 311 | // unmute specified channel (or server-wide for nil) 312 | muteRole, err := getMuteRoleForChannel(channel) 313 | if err != nil { 314 | if channel != nil { 315 | return fmt.Errorf("Can't unmute from %s yet", channel.Mention()) 316 | } else { 317 | return err // Unknown error 318 | } 319 | } 320 | 321 | // Check the user is actually muted... 322 | if !hasRole(target, Role{ID: muteRole}) { 323 | if channel == nil { 324 | return fmt.Errorf("%s isn't muted serverwide", user.Username) 325 | } else { 326 | return fmt.Errorf("%s isn't muted in %s", user.Username, channel.Mention()) 327 | } 328 | } 329 | 330 | // Update the database and keep track of what was unmuted (for the reply) 331 | if channel == nil { 332 | fullMute = true 333 | _, err = DB.Exec("DELETE FROM mutes WHERE discord_id = $1 AND channel_id IS NULL", user.ID) 334 | if err != nil { 335 | return err 336 | } 337 | } else { 338 | channels = append(channels, channel.ID) 339 | _, err = DB.Exec("DELETE FROM mutes WHERE discord_id = $1 AND channel_id = $2", user.ID, channel.ID) 340 | if err != nil { 341 | return err 342 | } 343 | } 344 | 345 | // Unmute them 346 | err = discord.GuildMemberRoleRemove(msg.GuildID, user.ID, muteRole) 347 | } 348 | 349 | // Construct a reply out of the unmuted channels slice 350 | var reply strings.Builder 351 | reply.WriteString("User " + user.Username + " has been ") 352 | if fullMute { 353 | reply.WriteString("unmuted serverwide") 354 | if len(channels) > 0 { 355 | reply.WriteString(" and ") 356 | } 357 | } 358 | if len(channels) > 0 { 359 | reply.WriteString("unmuted from ") 360 | for index, id := range channels { 361 | if index > 0 { 362 | reply.WriteString(", ") 363 | if index == len(channels)-1 { 364 | reply.WriteString("& ") 365 | } 366 | } 367 | reply.WriteString(fmt.Sprintf("<#%s>", id)) 368 | } 369 | } 370 | reply.WriteString(" by @" + msg.Author.Username + "#" + msg.Author.Discriminator) 371 | reply.WriteString("\n") 372 | 373 | // Direct message the user being unmuted 374 | DM, err := discord.UserChannelCreate(user.ID) 375 | if err == nil { 376 | err = resp(DM.ID, reply.String()) 377 | if err != nil { 378 | s := fmt.Sprintf("Error direct messaging %s#%s: %s", user.Username, user.Discriminator, err.Error()) 379 | fmt.Println(s) 380 | reply.WriteString(s + "\n") 381 | } 382 | } 383 | 384 | // Respond in chat & #impactbot-log 385 | _ = resp(impactBotLog, reply.String()) 386 | _ = resp(msg.ChannelID, reply.String()) 387 | 388 | return nil 389 | } 390 | 391 | // tbh should this be separate handlers?? 392 | // or maybe multiple handlers here is stupid, this is mostly a copy of mute handler :\ 393 | func rektHandler(caller *discordgo.Member, msg *discordgo.Message, args []string) error { 394 | user, channel, remainingArgs := getUserAndChannelAndArgs(args[1:]) 395 | if user == nil { 396 | return errors.New("First argument should mention user") 397 | } 398 | if channel != nil { 399 | return errors.New(args[0] + " does not support channel mentions") 400 | } 401 | if len(remainingArgs) < 1 { 402 | return errors.New("Give a reason") 403 | } 404 | 405 | target, err := GetMember(user.ID) 406 | if err != nil { 407 | return err 408 | } 409 | if !outranks(caller, target) { 410 | return fmt.Errorf("You don't outrank %s", target.User.Username) 411 | } 412 | 413 | // Reasons are important 414 | providedReason := args[0] + " has been issued to " + user.Username + " by @" + msg.Author.Username + "#" + msg.Author.Discriminator + " for reason: " + strings.Join(remainingArgs, " ") 415 | 416 | // Direct message the user being rekt 417 | DM, err := discord.UserChannelCreate(user.ID) // only creates it if it doesn"t already exist 418 | if err == nil { 419 | // if there is an error DMing them, we still want to ban them, they just won't know why 420 | err = resp(DM.ID, providedReason) 421 | if err != nil { 422 | fmt.Printf("Error direct messaging %s#%s: %s\n", user.Username, user.Discriminator, err.Error()) 423 | } 424 | } 425 | 426 | switch args[0] { 427 | case "ban": 428 | err = discord.GuildBanCreateWithReason(msg.GuildID, user.ID, providedReason, 0) 429 | case "kick": 430 | err = discord.GuildMemberDeleteWithReason(msg.GuildID, user.ID, providedReason) 431 | } 432 | 433 | if err != nil { 434 | return err 435 | } 436 | 437 | _ = resp(impactBotLog, providedReason) 438 | 439 | _ = resp(msg.ChannelID, providedReason) 440 | return nil 441 | } 442 | 443 | // unmuteCallback is called every UNMUTE_INTERVAL to unmute any expired temp mutes 444 | func unmuteCallback() { 445 | if DB == nil { 446 | return 447 | } 448 | 449 | // Get all expired rows 450 | now := time.Now() 451 | rows, err := DB.Query("SELECT id, discord_id, channel_id FROM mutes WHERE expiration < $1 AND expiration IS NOT NULL", now) 452 | if err != nil { 453 | if !errors.Is(err, sql.ErrNoRows) { 454 | fmt.Println("Error querying expired tempmutes", err) 455 | } 456 | return 457 | } 458 | 459 | for rows.Next() { 460 | var ( 461 | id uuid.UUID 462 | discordId string 463 | channelId sql.NullString 464 | ) 465 | 466 | err := rows.Scan(&id, &discordId, &channelId) 467 | if err != nil { 468 | fmt.Println("Error scanning tempmute entry", err) 469 | continue 470 | } 471 | 472 | // We're handling the user, so delete from db. 473 | // If we fail to unmute, at least we won't end up handling them forever 474 | _, err = DB.Exec("DELETE FROM mutes WHERE id = $1", id) 475 | if err != nil { 476 | fmt.Println("Error deleting tempmute entry", err) 477 | continue 478 | } 479 | 480 | // Get channel and mute role 481 | var channel *discordgo.Channel 482 | if channelId.Valid { 483 | channel, err = discord.Channel(channelId.String) 484 | if err != nil { 485 | fmt.Println("Error getting tempmute channel from channel id "+channelId.String, err) 486 | continue 487 | } 488 | } 489 | muteRole, err := getMuteRoleForChannel(channel) 490 | if err != nil { 491 | fmt.Println(fmt.Sprintf("Invalid mute role for channel id \"%s\"\n", channelId.String), err) 492 | continue 493 | } 494 | 495 | // Construct a message to the unmuted user 496 | var message strings.Builder 497 | message.WriteString(fmt.Sprintf("Your temp mute")) 498 | if channel != nil { 499 | message.WriteString(fmt.Sprintf(" from %s", channel.Mention())) 500 | } 501 | message.WriteString(" has expired!\n") 502 | 503 | { // Log the unmute 504 | var username = "user" 505 | if user, _ := discord.User(discordId); user != nil { 506 | username = fmt.Sprintf("@%s#%s", user.Username, user.Discriminator) 507 | } 508 | if channel == nil { 509 | fmt.Printf("Processing unmute for %s (%s) serverwide\n", username, discordId) 510 | } else { 511 | fmt.Printf("Processing unmute for %s (%s) from channel #%s (%s)\n", username, discordId, channel.Name, channel.ID) 512 | } 513 | } 514 | 515 | // Do the unmute 516 | err = discord.GuildMemberRoleRemove(impactServer, discordId, muteRole) 517 | if err != nil { 518 | fmt.Println("Could not remove mute role \""+muteRole+"\" from user \""+discordId+"\"", err) 519 | message.WriteString("But the bot failed to unmute you! Please show this message to a moderator.\n") 520 | } 521 | 522 | // DM message to user 523 | dm, err := discord.UserChannelCreate(discordId) 524 | if err != nil { 525 | continue // guess we can't let em know 526 | } 527 | _ = resp(dm.ID, message.String()) 528 | } 529 | } 530 | 531 | func init() { 532 | if DB == nil { 533 | fmt.Println("WARNING: No DB when initialising tempmutes callback, either rekt.go was initialised before db.go or there is no DB") 534 | } 535 | go func() { 536 | ticker := time.NewTicker(UnmuteInterval) 537 | for range ticker.C { 538 | unmuteCallback() 539 | } 540 | }() 541 | } 542 | 543 | func onUserJoin2(s *discordgo.Session, m *discordgo.GuildMemberAdd) { 544 | if m.GuildID != impactServer || m.User == nil { 545 | return 546 | } 547 | if DB == nil { 548 | return 549 | } 550 | 551 | // Get all nonexpired rows 552 | now := time.Now() 553 | rows, err := DB.Query("SELECT channel_id FROM mutes WHERE (expiration IS NULL OR expiration > $1) AND discord_id = $2", now, m.User.ID) 554 | if err != nil { 555 | if !errors.Is(err, sql.ErrNoRows) { 556 | fmt.Println("Error querying expired tempmutes", err) 557 | } 558 | return 559 | } 560 | 561 | for rows.Next() { 562 | var ( 563 | channelId sql.NullString 564 | ) 565 | 566 | err := rows.Scan(&channelId) 567 | if err != nil { 568 | fmt.Println("Error scanning tempmute entry", err) 569 | continue 570 | } 571 | 572 | // Get channel and mute role 573 | var channel *discordgo.Channel 574 | if channelId.Valid { 575 | channel, err = discord.Channel(channelId.String) 576 | if err != nil { 577 | fmt.Println("Error getting tempmute channel from channel id "+channelId.String, err) 578 | continue 579 | } 580 | } 581 | muteRole, err := getMuteRoleForChannel(channel) 582 | if err != nil { 583 | fmt.Println(fmt.Sprintf("Invalid mute role for channel id \"%s\"\n", channelId.String), err) 584 | continue 585 | } 586 | 587 | // Do the unmute 588 | err = discord.GuildMemberRoleAdd(impactServer, m.User.ID, muteRole) 589 | if err != nil { 590 | fmt.Println("Could not remove mute role \""+muteRole+"\" from user \""+m.User.ID+"\"", err) 591 | } 592 | } 593 | } 594 | -------------------------------------------------------------------------------- /replier.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | "sync" 7 | "time" 8 | 9 | "github.com/bwmarrin/discordgo" 10 | ) 11 | 12 | const replySenderTimeout = 30 * time.Second 13 | 14 | // a map from ID of a message I sent, to the ID of who is allowed to delete it (aka who sent the message that I was responding to) 15 | var messageSender = make(map[string]string) 16 | var messageSenderLock sync.Mutex 17 | 18 | func onMessageReactedTo(session *discordgo.Session, reaction *discordgo.MessageReactionAdd) { 19 | messageSenderLock.Lock() 20 | defer messageSenderLock.Unlock() 21 | 22 | // If the reaction isn't trash we don't care 23 | if reaction.Emoji.Name != trash { 24 | return 25 | } 26 | 27 | // Get the reply we sent 28 | reply, err := session.ChannelMessage(reaction.ChannelID, reaction.MessageID) 29 | if err != nil { 30 | return //wtf 31 | } 32 | 33 | // If we didn't send the reply or we added the reaction 34 | if reply.Author.ID != myselfID || reaction.UserID == myselfID { 35 | return 36 | } 37 | 38 | user, err := GetMember(reaction.UserID) 39 | if err != nil { 40 | return 41 | } 42 | 43 | // Filter approved users 44 | if !isMessageSender(reaction.UserID, reaction.MessageID) && !IsUserStaff(user) { 45 | return 46 | } 47 | 48 | // Delete the reply 49 | // sometimes errors since it was already trashcanned, dont spam logs with this error its too common 50 | go session.ChannelMessageDelete(reply.ChannelID, reply.ID) 51 | } 52 | 53 | func onMessageSent(session *discordgo.Session, m *discordgo.MessageCreate) { 54 | msg := m.Message 55 | if msg == nil || msg.Author == nil || (msg.Type != discordgo.MessageTypeDefault && msg.Type != discordgo.MessageTypeReply) { 56 | return // wtf 57 | } 58 | 59 | // Don't talk to oneself 60 | if msg.Author.ID == myselfID { 61 | return 62 | } 63 | 64 | author, err := GetMember(msg.Author.ID) 65 | if err != nil { 66 | return 67 | } 68 | 69 | if m.GuildID == impactServer { 70 | memberSanityCheck(author) 71 | emojiMsg(msg) 72 | } 73 | 74 | // Unless we're being spoken to 75 | if !triggeredManually(msg) { 76 | // Don't talk where we're not welcome 77 | whitelist := []string{general, help, bot, betterHelp, testing} 78 | if !includes(whitelist, msg.ChannelID) { 79 | return 80 | } 81 | 82 | // Ignore messages from ‘know-it-all’s 83 | if IsUserStaff(author) || hasRole(author, Stupid) { 84 | return 85 | } 86 | } 87 | 88 | // Phew, actually start doing stuff 89 | response := "" 90 | replyLoop: 91 | for _, reply := range replies { 92 | if includes(reply.excludeChannels, msg.ChannelID) { 93 | continue replyLoop 94 | } 95 | if hasRole(author, reply.excludeRoles...) { 96 | continue replyLoop 97 | } 98 | if len(reply.onlyChannels) > 0 && !includes(reply.onlyChannels, msg.ChannelID) { 99 | continue replyLoop 100 | } 101 | if len(reply.onlyRoles) > 0 { 102 | if !hasRole(author, reply.onlyRoles...) { 103 | continue replyLoop 104 | } 105 | } 106 | lower := strings.ToLower(msg.Content) 107 | if !reply.regex.MatchString(lower) { 108 | continue replyLoop 109 | } 110 | if reply.notRegex != nil && reply.notRegex.MatchString(lower) { 111 | continue replyLoop 112 | } 113 | 114 | // Not excluded, append to response 115 | response += reply.message + "\n" 116 | } 117 | if response == "" { 118 | return 119 | } 120 | response = strings.TrimSpace(response) 121 | messageSenderLock.Lock() 122 | defer messageSenderLock.Unlock() 123 | embed := &discordgo.MessageEmbed{ 124 | Author: &discordgo.MessageEmbedAuthor{}, 125 | Color: prettyembedcolor, 126 | Description: response, 127 | } 128 | reply, err := session.ChannelMessageSendEmbed(msg.ChannelID, embed) 129 | if err != nil { 130 | log.Println(err) 131 | return // if this failed, msg will be nil, so we cannot continue! 132 | } 133 | 134 | // Add a trashcan icon if the message wasn't triggered manually 135 | // Keep track of who is allowed to delete the message too 136 | if !triggeredManually(msg) { 137 | err = session.MessageReactionAdd(reply.ChannelID, reply.ID, trash) 138 | if err != nil { 139 | log.Println(err) 140 | } 141 | 142 | // Add the message to the sender map then delete it later 143 | messageSender[reply.ID] = msg.Author.ID 144 | go func() { 145 | time.Sleep(replySenderTimeout) 146 | messageSenderLock.Lock() 147 | defer messageSenderLock.Unlock() 148 | delete(messageSender, reply.ID) 149 | }() 150 | } 151 | } 152 | 153 | func triggeredManually(msg *discordgo.Message) bool { 154 | // TODO other methods of manual triggering, e.g. i!commands 155 | return mentionsMe(msg) 156 | } 157 | 158 | // Check the given user was the sender that triggered message 159 | func isMessageSender(user, message string) bool { 160 | author, ok := messageSender[message] 161 | return ok && user == author 162 | } 163 | -------------------------------------------------------------------------------- /replies.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | type Reply struct { 11 | pattern string 12 | unless string 13 | message string 14 | excludeChannels []string 15 | excludeRoles []Role 16 | onlyChannels []string 17 | onlyRoles []Role 18 | regex *regexp.Regexp 19 | notRegex *regexp.Regexp 20 | } 21 | 22 | type currencyinfo struct { 23 | Amount int64 `json:"premium_amount"` 24 | DisplayName string `json:"display_name"` 25 | Symbol string `json:"symbol"` 26 | } 27 | 28 | var nightlies = "https://impactclient.net/ImpactInstaller.?nightlies=true" 29 | var installer = "https://impactclient.net/download" 30 | 31 | var replies = []Reply{ 32 | { 33 | pattern: `faq|question|tutorial`, 34 | message: "[Setup/Install FAQ](https://github.com/impactdevelopment/impactclient/wiki/Setup-FAQ)\n[Usage FAQ](https://github.com/impactdevelopment/impactclient/wiki/Usage-FAQ)", 35 | }, 36 | { 37 | pattern: `defender|virus|mcafee|norton|trojan|\brat\b`, 38 | message: "[FAQ: My antivirus says the installer is a virus! Is it a virus?](https://github.com/ImpactDevelopment/ImpactIssues/wiki/Setup-FAQ#my-antivirus-says-the-installer-is-a-virus-is-it-a-virus)\n\n[Direct download link after AdFly](https://impactdevelopment.github.io/?brady-money-grubbing-completed=true)", 39 | }, 40 | { 41 | pattern: `tutorial|(impact|install|download).*(windows|linux|mac)`, 42 | message: "Tutorial videos for downloading and installing the client:\n[Windows](https://www.youtube.com/watch?v=9IV_NC377pg)\n[macOS](https://www.youtube.com/watch?v=BBO0v4eq95k)\n[GNU/Linux](https://www.youtube.com/watch?v=XPLvooJeQEI)\n", 43 | }, 44 | { 45 | pattern: `baritone\s*setting`, 46 | message: "[Baritone settings list and documentation](https://baritone.leijurv.com/baritone/api/Settings.html#field.detail)", 47 | }, 48 | { 49 | pattern: `screenshot`, 50 | message: "[How to take a screenshot in Minecraft](https://www.minecraft.net/en-us/article/screenshotting-guide)", 51 | }, 52 | { 53 | pattern: `use\s*baritone|baritone\s*(usage|command)|[^u]\.b|goal|goto|path`, 54 | message: "Please read the [Baritone usage guide](https://github.com/cabaletta/baritone/blob/master/USAGE.md)", 55 | }, 56 | { // Info for non-donators about donating 57 | pattern: `premium|donat|become\s*a?\s+don(at)?or|what\s*do\s*(you|i|u)\s*(get|unlock)|perks?`, 58 | // Unless it would be a better fit for another reply (e.g. not asking about premium) 59 | unless: `(installe?r?|mediafire|dire(c|k)+to?\s+(linko?|url|site|page)|ad\s?f\.?ly|(ad|u)\s?block|download|ERR_CONNECTION_ABORTED|evassmat|update|infect)`, 60 | message: "If you donate **** (or more), you will receive early access to upcoming releases through nightly builds when they are available (**now including 1.16.5 nightly builds!**), " + 61 | "1 premium mod (Ignite), a cape visible to other Impact users, a gold colored name in the Impact Discord Server, and access to #donator-help (with faster and nicer responses). " + 62 | "Go on the [website](https://impactclient.net/#donate) to donate. You will also need to [register](https://impactclient.net/register) your account and/or " + 63 | "[login](https://impactclient.net/account) to get access to all the promised features", 64 | excludeRoles: []Role{Donator}, 65 | }, 66 | { // Installer download for non-donators (not asking about premium) 67 | pattern: `installe?r?|mediafire|dire(c|k)+to?\s+(linko?|url|site|page)|ad\s?f\.?ly|(ad|u)\s?block|download|ERR_CONNECTION_ABORTED|evassmat|update|infect`, 68 | // Unless it would be a better match for another reply (e.g. asking about premium, optifine or forge) 69 | unless: `nightly|pre[- ]*release|beta|alpha|alfa|((download|get|where).*1[.]16)`, 70 | excludeRoles: []Role{Donator}, 71 | message: "Download the installer [here](" + installer + ")", 72 | }, 73 | { // Installer download for donators 74 | pattern: `installe?r?|mediafire|dire(c|k)+to?\s+(linko?|url|site|page)|ad\s?f\.?ly|(ad|u)\s?block|download|ERR_CONNECTION_ABORTED|evassmat|update|infect`, 75 | onlyRoles: []Role{Donator}, 76 | message: "You can download the normal installer [here](" + installer + ").\n" + 77 | "As a Donator, you can also install **nightly builds** of Impact: Download the Impact Nightly Installer [for Windows (.exe)](" + strings.Replace(nightlies, "", "exe", 1) + ") or [for other platforms (.jar)](" + strings.Replace(nightlies, "", "jar", 1) + ").\n" + 78 | "You can also access these links by logging into your [Impact Account dashboard](https://impactclient.net/account)", 79 | }, 80 | { // Install info for optifine 81 | pattern: `\b(install.*impact.*opti\s*fine)\b`, 82 | message: "Use the [installer](" + installer + ") to add OptiFine to Impact: [Instructions](https://github.com/ImpactDevelopment/ImpactIssues/wiki/Adding-OptiFine)", 83 | }, 84 | { // Install info for Forge 85 | pattern: `\b(install.*impact.*forge|support.*forge)\b`, 86 | message: "Use the [installer](https://impactclient.net/) to install Forge (1.12.2 only)\nStandalone Baritone supports Forge on various versions - Download from [GitHub](https://github.com/cabaletta/baritone/releases).", 87 | }, 88 | { // Install info for LiteLoader 89 | pattern: `lite\s*loader`, 90 | message: "[LiteLoader tutorial](https://github.com/ImpactDevelopment/ImpactIssues/wiki/Adding-LiteLoader)", 91 | }, 92 | { // Links to Impactt website 93 | pattern: `(web\s?)?(site|page)`, 94 | message: "[Impact Website](https://impactclient.net)", 95 | }, 96 | { // GitHub repo for issues 97 | pattern: `issue|bug|crash|error|suggest(ion)?s?|feature|enhancement`, 98 | message: "Use the [GitHub repo](https://github.com/ImpactDevelopment/ImpactIssues/issues) to report issues/suggestions!", 99 | }, 100 | { // Non-donator help redirect 101 | pattern: `help|support`, 102 | message: "Switch to the <#" + help + "> channel!", 103 | excludeRoles: []Role{Donator}, 104 | excludeChannels: []string{help, betterHelp}, 105 | }, 106 | { // Donator help redirect 107 | pattern: `help|support`, 108 | message: "Switch to the <#" + betterHelp + "> channel!", 109 | onlyRoles: []Role{Donator}, 110 | excludeChannels: []string{help, betterHelp}, 111 | }, 112 | { // What does Franky do???? 113 | pattern: `what(\sdoes|\sis|s|'s)?\s+franky`, 114 | message: "[It does exactly what you think it does.](https://youtu.be/_FzInOheiRw)", 115 | }, 116 | { // Information on macros 117 | pattern: `macros?`, 118 | message: "Macros are in-game chat commands, they can be accessed in-game by clicking on the Impact button, then Macros.", 119 | }, 120 | { // Links to the Impact changelogs 121 | pattern: `change(\s*logs?|s)`, 122 | message: "[Changelog](https://impactclient.net/changelog)", 123 | }, 124 | { // Notice about "hacking" 125 | pattern: `hack(s|ing|er|client)?`, 126 | message: "**Impact is not a hacked client**, it is designed as a utility mod (e.g. for anarchy servers).\nSupport will not be provided to users who utilise Impact on servers that do not allow it.\nPlease also note that the discussion of \"hacks\" in this Discord server is prohibited to comply with the [Discord Community Guidelines](https://discord.com/guidelines)", 127 | }, 128 | { // Weeb moment 129 | pattern: `dumb|retard|idiot`, 130 | message: "Like the " + Weeb.Mention() + "s?", 131 | onlyRoles: []Role{Weeb}, 132 | }, 133 | { // Info on using schematics 134 | pattern: `schematics?`, 135 | message: "Schematic file **MUST** be made in a 1.12.2 world or prior.\n1) Place the .schematic file into `.minecraft/schematics`.\n2) Ensure all the blocks are in your hotbar.\n3) Type `#build name.schematic`.", 136 | }, 137 | { // Info on using cracked launchers 138 | pattern: `((crack|cracked) (launcher|account|game|minecraft))|(terramining|shiginima|(t(-|)launcher))`, 139 | message: "Impact does not support cracked launchers. You can attempt to use the unstable Forge version, but no further support will be provided.", 140 | }, 141 | { // Link to the Impact wiki 142 | pattern: `\b(impact\s*wiki|(setup|use)\s*spam(mer)?|faq)\b`, 143 | message: "[Impact Wiki](https://github.com/ImpactDevelopment/ImpactIssues/wiki)", 144 | }, 145 | { // Downloads for JRE 146 | pattern: `java.*(download|runtime|environment)`, 147 | message: "[Downloads for Java Runtime Environment](https://www.java.com/download/)", 148 | }, 149 | { // How to use Impact's automine function 150 | pattern: `how.+(mine|auto\s*mine)`, 151 | message: "You can mine a specific type of block(s) by typing `#mine [number of blocks to mine] []` in chat.\nYou can find a list of block ID names [here](https://www.digminecraft.com/lists/)", 152 | }, 153 | { // Older versions of Impact 154 | pattern: `(impact.+(1\.8|1\.7))|((1\.8|1\.7).impact)`, 155 | message: "Impact for older versions is no longer availible to comply with Mojang's EULA.", 156 | }, 157 | { // Information on using Impact with modpacks 158 | pattern: `(modpack|\bftb\b|rlcraft|skyfactory|valhelsia|pixelmon|sevtech)`, 159 | message: "Impact is generally incompatible with modpacks and support will not be provided if you encounter bugs with them. It's likely your game will just crash on startup.", 160 | }, 161 | { 162 | pattern: `good\s*bot`, 163 | message: "tnyak yow *nuwzzwes yoww necky wecky*", 164 | }, 165 | { 166 | pattern: `((anti(-|\s*)(kb|knockback))|velocity)`, 167 | message: "**Velocity**, also known as **Anti-knockback**, is a module under \"Movement\" that prevents the player from taking knockback.", 168 | }, 169 | { 170 | pattern: `(gui|r(-|\s)shift|module|(open|close|show|hide)\s*impact)`, 171 | message: "To open or close the Impact GUI, press the `rshift` key, located below `enter`.", 172 | }, 173 | } 174 | 175 | func init() { 176 | // Fetch stripe info from Impact API, since we need to know how much users need to donate for premium 177 | amountPattern := regexp.MustCompile(``) 178 | amountList := "" 179 | currencies, err := getCurrencyInfo() 180 | if err != nil { 181 | println("Error getting Stripe info from Impact API: " + err.Error()) 182 | } else { 183 | amountList = getCurrencyAmountString(currencies) 184 | } 185 | 186 | // Compile all the regex patterns, and replace any template strings 187 | for i := range replies { 188 | replies[i].regex = regexp.MustCompile(replies[i].pattern) 189 | 190 | if replies[i].unless != "" { 191 | replies[i].notRegex = regexp.MustCompile(replies[i].unless) 192 | } 193 | 194 | if amountPattern.MatchString(replies[i].message) { 195 | replies[i].message = amountPattern.ReplaceAllLiteralString(replies[i].message, amountList) 196 | } 197 | } 198 | } 199 | 200 | func getCurrencyAmountString(currencies map[string]currencyinfo) string { 201 | const separator = ", " 202 | var currencyList = "" 203 | for id, currency := range currencies { 204 | if currencyList != "" { 205 | currencyList += separator 206 | } 207 | var amount float64 208 | amount = float64(currency.Amount) / 100 209 | currencyList += fmt.Sprintf("%s%0.2f %s", currency.Symbol, amount, strings.ToUpper(id)) 210 | } 211 | index := strings.LastIndex(currencyList, separator) 212 | if index > -1 { 213 | currencyList = currencyList[0:index] + " or " + currencyList[index+len(separator):] 214 | } 215 | 216 | return currencyList 217 | } 218 | 219 | func getCurrencyInfo() (map[string]currencyinfo, error) { 220 | str, err := get("https://api.impactclient.net/v1/stripe/info") 221 | if err != nil { 222 | return nil, err 223 | } 224 | var info struct { 225 | Currencies map[string]currencyinfo `json:"currencies"` 226 | } 227 | err = json.Unmarshal([]byte(str), &info) 228 | if err != nil { 229 | return nil, err 230 | } 231 | return info.Currencies, nil 232 | } 233 | -------------------------------------------------------------------------------- /role_reapplicator.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/bwmarrin/discordgo" 10 | ) 11 | 12 | // note: this is not for mutes. rekt does that 13 | // this is for roles determined by the central db 14 | func onUserJoin3(s *discordgo.Session, m *discordgo.GuildMemberAdd) { 15 | if m.GuildID != impactServer { 16 | return 17 | } 18 | if shouldGiveDonator(m.User.ID) { 19 | err := discord.GuildMemberRoleAdd(impactServer, m.User.ID, Donator.ID) 20 | if err != nil { 21 | log.Println(err) 22 | } 23 | } 24 | } 25 | 26 | func shouldGiveDonator(discordID string) bool { 27 | url := "https://api.impactclient.net/v1/integration/impactbot/checkdonator/" + discordID + "?auth=" + os.Getenv("IMPACTBOT_AUTH_SECRET") 28 | return isYes(url) 29 | } 30 | 31 | func isYes(url string) bool { 32 | resp, err := http.Get(url) 33 | if err != nil { 34 | log.Println(err) 35 | return false 36 | } 37 | defer resp.Body.Close() 38 | data, err := ioutil.ReadAll(resp.Body) 39 | if err != nil { 40 | log.Println(err) 41 | return false 42 | } 43 | return string(data) == "yes" 44 | } 45 | -------------------------------------------------------------------------------- /rules.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | stripmd "github.com/writeas/go-strip-markdown" 7 | "log" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/bwmarrin/discordgo" 13 | ) 14 | 15 | const rulesChannel = "667494326372139008" 16 | const rulesMessage = "667497572264312832" 17 | 18 | var rules = []string{ 19 | "Follow [Discord's ToS](https://discord.com/terms).", 20 | "Moderators have the final say. Do not argue with them.", 21 | "Use the correct channel for the topic you are discussing. _Ask questions in <#" + help + ">, report bugs on [GitHub](https://github.com/ImpactDevelopment/ImpactIssues/issues), etc._", 22 | "No trolling, unnecessary pinging / @-ing / DMing, spamming, advertising, non-English conversation, bullying, or blatant rudeness.", 23 | "No NSFW content. _This includes messages, images, gifs, videos, audio, links, usernames, nicknames, statuses, profile pictures, etc._", 24 | "Don't “ask to ask”. _This means no messages that just say something like “Can I ask a question?”, or “Can someone help me?”, or, worst of all, “hello??”. The answer is yes, in <#" + help + ">)._", 25 | "Channel specific rules or topics can be found in the channel description. _They may extend upon or overrule these blanket rules._", 26 | } 27 | 28 | var extraRules = []*discordgo.MessageEmbedField{ 29 | { 30 | Name: "Volunteers", 31 | Value: "All staff, including Support, Moderators, and Developers are volunteers. " + 32 | "They are under _no obligation_ to help you, but are likely to if you are polite.", 33 | }, 34 | { 35 | Name: "Why can't I speak‽", 36 | Value: "You need to verify yourself! Click the link at the top of my welcome DM to you, if you have it. Otherwise, click [here](https://impactclient.net/discord.html?member=1) and fill in your info manually.", 37 | }, 38 | { 39 | Name: "Terms", 40 | Value: "By using this discord you agree to our bots storing information about you, such as your discord id. " + 41 | "If you wish for this information to be removed from our servers you can run `i!optout` to delete any records we have about you and remove you from the server.", 42 | }, 43 | } 44 | 45 | func rulesHandler(caller *discordgo.Member, msg *discordgo.Message, args []string) error { 46 | reply := discordgo.MessageEmbed{ 47 | Color: prettyembedcolor, 48 | } 49 | 50 | // Separate @mentions from args 51 | var b strings.Builder 52 | if len(args) > 0 { 53 | for _, word := range args[1:] { 54 | if match, err := regexp.MatchString("^<@!?[0-9]+>$", word); err != nil { 55 | return err 56 | } else if !match { 57 | b.WriteString(word + " ") 58 | } 59 | } 60 | } 61 | 62 | // Try to parse an index or a search term 63 | search := strings.TrimSpace(b.String()) 64 | index, _ := strconv.Atoi(search) 65 | 66 | if search == "" { 67 | reply.Title = "Rules" 68 | reply.Description = buildRules() 69 | } else { 70 | // If search matches index, then they specified a rule number 71 | // otherwise they provided a search term 72 | if search == strconv.Itoa(index) { 73 | index-- // Rule numbers are one higher than index 74 | } else { 75 | var err error 76 | index, err = findRuleFromStrings(strings.TrimSpace(search)) 77 | if err != nil { 78 | return err 79 | } 80 | } 81 | 82 | if index >= len(rules) { 83 | return errors.New("There are only " + strconv.Itoa(len(rules)) + " rules, " + strconv.Itoa(index+1) + " is too high.") 84 | } 85 | if index < 0 { 86 | return errors.New("Rules are counted from 1, " + strconv.Itoa(index+1) + " is too low") 87 | } 88 | 89 | reply.Title = "Rule " + strconv.Itoa(index+1) 90 | reply.Description = rules[index] 91 | reply.Footer = &discordgo.MessageEmbedFooter{ 92 | Text: fmt.Sprintf("Run %s%s for more", prefix, args[0]), 93 | } 94 | } 95 | 96 | // Mention any users mentioned by the caller 97 | var mentions string 98 | for _, user := range msg.Mentions { 99 | mentions += user.Mention() + " " 100 | } 101 | 102 | _, err := discord.ChannelMessageSendComplex(msg.ChannelID, &discordgo.MessageSend{ 103 | Content: mentions, 104 | Embed: &reply, 105 | }) 106 | 107 | return err 108 | } 109 | 110 | func findRuleFromStrings(phrase ...string) (int, error) { 111 | for _, word := range phrase { 112 | for i, rule := range rules { 113 | if strings.Contains(strings.ToLower(stripmd.Strip(rule)), strings.ToLower(stripmd.Strip(word))) { 114 | return i, nil 115 | } 116 | } 117 | } 118 | return -1, errors.New("unable to find rule matching \"" + strings.Join(phrase, " ") + "\"") 119 | } 120 | 121 | func updateRules() { 122 | _, err := discord.ChannelMessageEditEmbed(rulesChannel, rulesMessage, &discordgo.MessageEmbed{ 123 | Color: prettyembedcolor, 124 | Thumbnail: &discordgo.MessageEmbedThumbnail{ 125 | URL: "https://cdn.discordapp.com/attachments/224684271913140224/571442198718185492/unknown.png", 126 | }, 127 | Fields: append(extraRules, &discordgo.MessageEmbedField{ 128 | Name: "Rules", 129 | Value: buildRules(), 130 | }), 131 | }) 132 | if err != nil { 133 | log.Println("Unable to edit rules message with id " + rulesMessage) 134 | } 135 | } 136 | 137 | func buildRules() string { 138 | var r strings.Builder 139 | for index, rule := range rules { 140 | r.WriteString(strconv.Itoa(index + 1)) 141 | r.WriteString(". ") 142 | r.WriteString(rule) 143 | r.WriteString("\n") 144 | } 145 | 146 | return r.String() 147 | } 148 | -------------------------------------------------------------------------------- /staff.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/bwmarrin/discordgo" 4 | 5 | // Higher (lower index) is better 6 | var staffRoles = []Role{ 7 | HeadDev, 8 | Developer, 9 | SeniorMod, 10 | Moderator, 11 | Support, 12 | } 13 | 14 | func (r Role) IsAtLeast(role Role) bool { 15 | for _, testRole := range staffRoles { 16 | switch testRole { 17 | case role: 18 | return true 19 | case r: 20 | return false 21 | } 22 | } 23 | return false 24 | } 25 | 26 | func (r Role) IsHigherThan(role Role) bool { 27 | for _, testRole := range staffRoles { 28 | switch testRole { 29 | case r: 30 | return false 31 | case role: 32 | return true 33 | } 34 | } 35 | return false 36 | } 37 | 38 | func GetRolesAtLeast(role Role) []Role { 39 | return staffRoles[:GetRank(role)+1] 40 | } 41 | 42 | func GetRolesHigherThan(role Role) []Role { 43 | return staffRoles[:GetRank(role)] 44 | } 45 | 46 | // Lower is better, -1 is not found 47 | func GetRank(role Role) int { 48 | for i, r := range staffRoles { 49 | if r == role { 50 | return i 51 | } 52 | } 53 | return -1 54 | } 55 | 56 | func IsUserStaff(user *discordgo.Member) bool { 57 | return hasRole(user, staffRoles...) 58 | } 59 | 60 | func IsUserAtLeast(user *discordgo.Member, role Role) bool { 61 | return hasRole(user, GetRolesAtLeast(role)...) 62 | } 63 | 64 | func IsUserHigherThan(user *discordgo.Member, role Role) bool { 65 | return hasRole(user, GetRolesHigherThan(role)...) 66 | } 67 | 68 | func IsUserLowerThan(user *discordgo.Member, role Role) bool { 69 | return !IsUserAtLeast(user, role) 70 | } 71 | 72 | func GetHighestStaffRole(user *discordgo.Member) int { 73 | for i, r := range staffRoles { 74 | if hasRole(user, r) { 75 | return i 76 | } 77 | } 78 | return -1 79 | } 80 | -------------------------------------------------------------------------------- /stupid.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/bwmarrin/discordgo" 5 | ) 6 | 7 | func stupid(caller *discordgo.Member, msg *discordgo.Message, args []string) error { 8 | err := discord.GuildMemberRoleAdd(impactServer, caller.User.ID, Stupid.ID) 9 | if err != nil { 10 | return err // fuckups get caught here 11 | } 12 | return discord.MessageReactionAdd(msg.ChannelID, msg.ID, check) 13 | } 14 | 15 | func unstupid(caller *discordgo.Member, msg *discordgo.Message, args []string) error { 16 | err := discord.GuildMemberRoleRemove(impactServer, caller.User.ID, Stupid.ID) 17 | if err != nil { 18 | return err // fuckups get caught here 19 | } 20 | return discord.MessageReactionAdd(msg.ChannelID, msg.ID, check) 21 | } 22 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/bwmarrin/discordgo" 7 | ) 8 | 9 | // True if user has ANY role passed in 10 | func hasRole(user *discordgo.Member, role ...Role) bool { 11 | for _, r := range role { 12 | if includes(user.Roles, r.ID) { 13 | return true 14 | } 15 | } 16 | return false 17 | } 18 | 19 | // True if user has ALL roles passed in 20 | func hasRoles(user *discordgo.Member, role ...Role) bool { 21 | for _, r := range role { 22 | if !includes(user.Roles, r.ID) { 23 | return false 24 | } 25 | } 26 | return true 27 | } 28 | 29 | func mentionsMe(msg *discordgo.Message) bool { 30 | for _, user := range msg.Mentions { 31 | if user != nil && user.ID == myselfID { 32 | return true 33 | } 34 | } 35 | return false 36 | } 37 | 38 | func includes(list []string, val string) bool { 39 | for _, x := range list { 40 | if x == val { 41 | return true 42 | } 43 | } 44 | return false 45 | } 46 | 47 | func SendDM(userID string, message string) error { 48 | ch, err := discord.UserChannelCreate(userID) // only creates it if it doesn"t already exist 49 | if err != nil { 50 | return err 51 | } 52 | _, err = discord.ChannelMessageSend(ch.ID, message) 53 | return err 54 | } 55 | 56 | func highestRole(user *discordgo.Member) *Role { 57 | for _, role := range staffRoles { 58 | if includes(user.Roles, role.ID) { 59 | return &role 60 | } 61 | } 62 | return nil 63 | } 64 | 65 | // true if user1 is higher than user2 66 | // also false if user1 is staff and user2 is not 67 | func outranks(user1, user2 *discordgo.Member) bool { 68 | role := highestRole(user2) 69 | if role == nil { 70 | return IsUserStaff(user1) 71 | } 72 | 73 | if user1.User.ID == "96711543202254848" { 74 | // pepsi is poo poo and outranks nobody 75 | return false 76 | } 77 | 78 | return IsUserHigherThan(user1, *role) 79 | } 80 | 81 | // Get a Member from the Impact Discord 82 | func GetMember(userID string) (member *discordgo.Member, err error) { 83 | member, err = discord.State.Member(impactServer, userID) 84 | if err != nil { 85 | member, err = discord.GuildMember(impactServer, userID) 86 | } 87 | return member, err 88 | } 89 | 90 | func findNamedMatches(r *regexp.Regexp, str string) map[string]string { 91 | matches := r.FindStringSubmatch(str) 92 | names := r.SubexpNames() 93 | subs := map[string]string{} 94 | 95 | for i, sub := range matches { 96 | if names[i] != "" { 97 | subs[names[i]] = sub 98 | } 99 | } 100 | 101 | return subs 102 | } 103 | -------------------------------------------------------------------------------- /verify.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/bwmarrin/discordgo" 10 | ) 11 | 12 | var staffIDs [5][]string 13 | 14 | var nicknameENFORCEMENT = make(map[string]string) 15 | 16 | func onReady2(discord *discordgo.Session, ready *discordgo.Ready) { 17 | go func() { 18 | prev := "" 19 | total := 0 20 | donatorCount := 0 21 | verifiedCount := 0 22 | for { 23 | log.Println("Fetching starting at", prev) 24 | st, err := discord.GuildMembers(impactServer, prev, 1000) 25 | if err != nil { 26 | log.Println(err) 27 | break 28 | } 29 | log.Println("Fetched", len(st), "more members, total so far is", total) 30 | if len(st) == 0 { 31 | log.Println("No more members!") 32 | break 33 | } 34 | prev = st[len(st)-1].User.ID 35 | for _, member := range st { 36 | total++ 37 | memberSanityCheck(member) 38 | if hasRole(member, Donator) { 39 | donatorCount++ 40 | } 41 | if hasRole(member, Verified) { 42 | verifiedCount++ 43 | } 44 | /*if IsUserStaff(member) { 45 | discord.GuildMemberNickname(impactServer, member.User.ID, "") 46 | }*/ 47 | if IsUserStaff(member) { 48 | staffIDs[GetHighestStaffRole(member)] = append(staffIDs[GetHighestStaffRole(member)], member.User.ID) 49 | } 50 | } 51 | } 52 | log.Println("Processed", total, "members") 53 | log.Println("There are", donatorCount, "donators") 54 | log.Println("There are", verifiedCount, "verified") 55 | if DB == nil { 56 | return 57 | } 58 | for i := 1; i < 5; i++ { 59 | for _, id := range staffIDs[i] { 60 | var num int 61 | err := DB.QueryRow("SELECT nick FROM nicks WHERE id = $1", id).Scan(&num) 62 | if err != nil { 63 | err = DB.QueryRow("INSERT INTO nicks(id) VALUES ($1) RETURNING (nick)", id).Scan(&num) 64 | if err != nil { 65 | panic(err) 66 | } 67 | } 68 | meme(num, id) 69 | } 70 | } 71 | }() 72 | } 73 | 74 | func meme(num int, id string) { 75 | str := strconv.Itoa(num) 76 | for len(str) < 2 { 77 | str = "0" + str 78 | } 79 | nick := "Based Entity " + str 80 | err := discord.GuildMemberNickname(impactServer, id, nick) 81 | if err != nil { 82 | log.Println(err) 83 | } 84 | nicknameENFORCEMENT[id] = nick 85 | } 86 | 87 | func onGuildMemberUpdate(discord *discordgo.Session, guildMemberUpdate *discordgo.GuildMemberUpdate) { 88 | memberSanityCheck(guildMemberUpdate.Member) 89 | } 90 | 91 | func memberSanityCheck(member *discordgo.Member) { 92 | if member.User.Bot { 93 | // Don't enforce roles/nicks on bots 94 | log.Printf("Skipping sanity check on bot user %s#%s (%s)\n", member.User.Username, member.User.Discriminator, member.Nick) 95 | return 96 | } 97 | if len(member.Roles) > 0 && !hasRole(member, Verified) { 98 | log.Println("Member", member.User.ID, "had roles not including verified") 99 | err := discord.GuildMemberRoleAdd(impactServer, member.User.ID, Verified.ID) 100 | if err != nil { 101 | log.Println(err) 102 | } 103 | } 104 | if !hasRole(member, Verified) && accountCreatedMoreThanSixMonthsAgo(member.User.ID) && joinedServerMoreThanOneMonthAgo(member) { 105 | log.Println("Member", member.User.ID, "has been in the server for a month, and has an account over 6 months old, but isn't verified") 106 | err := discord.GuildMemberRoleAdd(impactServer, member.User.ID, Verified.ID) 107 | if err != nil { 108 | log.Println(err) 109 | } 110 | } 111 | if hasRole(member, InVoice) && !checkDeservesInVoiceRole(member.User.ID) { 112 | log.Println("Member", member.User.ID, "had In Voice but isn't in voice") 113 | err := discord.GuildMemberRoleRemove(impactServer, member.User.ID, InVoice.ID) 114 | if err != nil { 115 | log.Println(err) 116 | } 117 | } 118 | } 119 | 120 | func wantHandler(caller *discordgo.Member, msg *discordgo.Message, args []string) error { 121 | reply := discordgo.MessageEmbed{ 122 | Color: prettyembedcolor, 123 | } 124 | 125 | switch len(args) { 126 | case 1: 127 | reply.Title = "no" 128 | reply.Description = "give number you want" 129 | case 2: 130 | want, err := strconv.Atoi(args[1]) 131 | if err != nil { 132 | return err 133 | } 134 | sentBy := msg.Author.ID 135 | 136 | var curr int 137 | err = DB.QueryRow("SELECT nick FROM nicks WHERE id = $1", sentBy).Scan(&curr) 138 | if err != nil { 139 | return err 140 | } 141 | if curr == want { 142 | return errors.New("No") 143 | } 144 | var already string 145 | err = DB.QueryRow("SELECT id FROM nicks WHERE nick = $1", want).Scan(&already) 146 | if err != nil { 147 | return err 148 | } 149 | // already held by already 150 | _, err = DB.Exec("INSERT INTO nicktrade (id, desirednick) VALUES ($1, $2) ON CONFLICT (id, desirednick) DO NOTHING", sentBy, want) 151 | if err != nil { 152 | return err 153 | } 154 | rows, err := DB.Query("SELECT nicktrade.desirednick AS desired, nicks.nick AS curr FROM nicks INNER JOIN nicktrade ON nicktrade.id = nicks.id") 155 | if err != nil { 156 | return err 157 | } 158 | defer rows.Close() 159 | edges := make(map[int][]int) 160 | for rows.Next() { 161 | var desired int 162 | var curr int 163 | err = rows.Scan(&desired, &curr) 164 | if err != nil { 165 | return err 166 | } 167 | edges[curr] = append(edges[curr], desired) 168 | } 169 | err = rows.Err() 170 | if err != nil { 171 | return err 172 | } 173 | path := DFS(edges, curr, curr) 174 | if len(path) < 2 { 175 | return errors.New("okay i added your request to the database but i cannot satisfy it at the moment") 176 | } 177 | path = path[:len(path)-1] 178 | reply.Title = "yes" 179 | reply.Description = "Based cycle BTW MY CODE IS SHIT SO THIS CLEARS ALL i!wants" 180 | IDs := make([]string, 0) 181 | for i := range path { 182 | oldNick := path[i] 183 | //newNick := path[(i+1)%len(path)] 184 | var person string 185 | err = DB.QueryRow("SELECT id FROM nicks WHERE nick = $1", oldNick).Scan(&person) 186 | if err != nil { 187 | return err 188 | } 189 | IDs = append(IDs, person) 190 | } 191 | for i := range path { 192 | //oldNick := path[i] 193 | newNick := path[(i+1)%len(path)] 194 | person := IDs[i] 195 | 196 | _, err := DB.Exec("UPDATE nicks SET nick = $1 WHERE id = $2", newNick, person) 197 | if err != nil { 198 | return err 199 | } 200 | 201 | meme(newNick, person) 202 | } 203 | //noinspection SqlWithoutWhere 204 | _, err = DB.Exec("DELETE FROM nicktrade") 205 | if err != nil { 206 | return err 207 | } 208 | default: 209 | return errors.New("incorrect number of arguments") 210 | } 211 | 212 | _, err := discord.ChannelMessageSendEmbed(msg.ChannelID, &reply) 213 | return err 214 | } 215 | 216 | func DFS(edges map[int][]int, start int, end int) []int { 217 | for _, str := range edges[start] { 218 | if str == end { 219 | return []int{start, end} 220 | } 221 | path := DFS(edges, str, end) 222 | if path != nil { 223 | return append([]int{start}, path...) 224 | } 225 | } 226 | return nil 227 | } 228 | 229 | func accountCreatedMoreThanSixMonthsAgo(discordID string) bool { 230 | u, err := strconv.ParseUint(discordID, 10, 64) 231 | if err != nil { 232 | return false 233 | } 234 | nowMS := uint64(time.Now().Unix()) * 1000 235 | createdAtMS := u/(1<<22) + 1420070400000 236 | ageMS := nowMS - createdAtMS 237 | ageDays := ageMS / 1000 / 86400 238 | return ageDays > 6*30 239 | } 240 | 241 | func joinedServerMoreThanOneMonthAgo(member *discordgo.Member) bool { 242 | joinedAt, err := member.JoinedAt.Parse() 243 | if err != nil { 244 | return false 245 | } 246 | duration := time.Now().Sub(joinedAt) 247 | return duration > 30*24*time.Hour 248 | } 249 | -------------------------------------------------------------------------------- /welcomer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/bwmarrin/discordgo" 8 | ) 9 | 10 | func onUserJoin(s *discordgo.Session, m *discordgo.GuildMemberAdd) { 11 | log.Println("On user join") 12 | if m.GuildID != impactServer { 13 | return 14 | } 15 | if m.User == nil { 16 | return 17 | } 18 | embed := &discordgo.MessageEmbed{ 19 | Author: &discordgo.MessageEmbedAuthor{}, 20 | Color: prettyembedcolor, 21 | Title: "Welcome to the Impact Discord!", 22 | Fields: append([]*discordgo.MessageEmbedField{ 23 | { 24 | Name: "Setup/Install FAQ", 25 | Value: "[Click here!](https://github.com/ImpactDevelopment/ImpactClient/wiki/Setup-FAQ)", 26 | Inline: true, 27 | }, 28 | { 29 | Name: "Usage FAQ", 30 | Value: "[Click here!](https://github.com/ImpactDevelopment/ImpactClient/wiki/Usage-FAQ)", 31 | Inline: true, 32 | }, 33 | { 34 | Name: "Rules", 35 | Value: "[Click here!](https://discordapp.com/channels/208753003996512258/667494326372139008/667497572264312832)", 36 | Inline: true, 37 | }, 38 | { 39 | Name: "Github Links", 40 | Value: "[Impact](https://github.com/ImpactDevelopment/ImpactClient), [Installer](https://github.com/ImpactDevelopment/Installer/), [Baritone](https://github.com/cabaletta/baritone)", 41 | Inline: true, 42 | }, 43 | { 44 | Name: "Tutorial videos for downloading and installing the client", 45 | Value: "[Windows](https://www.youtube.com/watch?v=QP6CN-1JYYE)\n[Mac OSX](https://www.youtube.com/watch?v=BBO0v4eq95k)\n[Linux](https://www.youtube.com/watch?v=XPLvooJeQEI)\n", 46 | }, 47 | }, extraRules...), 48 | Footer: &discordgo.MessageEmbedFooter{ 49 | Text: "♿ Impact Client ♿", 50 | }, 51 | Timestamp: time.Now().Format(time.RFC3339), 52 | Thumbnail: &discordgo.MessageEmbedThumbnail{ 53 | URL: "https://cdn.discordapp.com/attachments/224684271913140224/571442198718185492/unknown.png", 54 | }, 55 | } 56 | if hasRole(m.Member, Verified) { 57 | embed.Description = "You have been verified automatically! Check the useful links below or <#" + help + ">" 58 | } else { 59 | embed.Description = "**In order to prevent spam you will not be able to talk until you complete a captcha by clicking [this link](https://impactclient.net/discord.html?discord=" + m.User.ID + ")** to prove you're not a bot!\n\nCheck the useful links below. Please do not DM a staff member while waiting. Try to resolve the problem using the FAQ, or the help channel when you can speak." 60 | } 61 | ch, err := discord.UserChannelCreate(m.User.ID) 62 | if err != nil { 63 | log.Println(err) 64 | log.Println("Can't begin to send welcome message") 65 | return 66 | } 67 | _, err = discord.ChannelMessageSendEmbed(ch.ID, embed) 68 | if err != nil { 69 | log.Println(err) 70 | log.Println("Can't send welcome message") 71 | return 72 | } 73 | } 74 | --------------------------------------------------------------------------------