1package discord 2 3import ( 4 "context" 5 "fmt" 6 "github.com/automuteus/galactus/broker" 7 "github.com/automuteus/utils/pkg/game" 8 "github.com/automuteus/utils/pkg/premium" 9 "github.com/automuteus/utils/pkg/task" 10 "github.com/bsm/redislock" 11 redis_common "github.com/denverquane/amongusdiscord/common" 12 "github.com/denverquane/amongusdiscord/discord/command" 13 "github.com/denverquane/amongusdiscord/metrics" 14 "log" 15 "os" 16 "strconv" 17 "strings" 18 "time" 19 20 "github.com/denverquane/amongusdiscord/storage" 21 22 "github.com/bwmarrin/discordgo" 23 "github.com/nicksnyder/go-i18n/v2/i18n" 24) 25 26const DefaultMaxActiveGames = 150 27 28const downloadURL = "https://capture.automute.us" 29 30func (bot *Bot) handleMessageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { 31 // IgnoreSpectator all messages created by the bot itself 32 if m.Author.ID == s.State.User.ID { 33 return 34 } 35 36 if redis_common.IsUserBanned(bot.RedisInterface.client, m.Author.ID) { 37 return 38 } 39 40 lock := bot.RedisInterface.LockSnowflake(m.ID) 41 // couldn't obtain lock; bail bail bail! 42 if lock == nil { 43 return 44 } 45 defer lock.Release(ctx) 46 47 g, err := s.State.Guild(m.GuildID) 48 if err != nil { 49 log.Println(err) 50 return 51 } 52 53 contents := m.Content 54 sett := bot.StorageInterface.GetGuildSettings(m.GuildID) 55 prefix := sett.GetCommandPrefix() 56 57 if strings.Contains(m.Content, "<@!"+s.State.User.ID+">") { 58 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 59 ID: "message_handlers.handleMessageCreate.respondPrefix", 60 Other: "I respond to the prefix {{.CommandPrefix}}", 61 }, 62 map[string]interface{}{ 63 "CommandPrefix": prefix, 64 })) 65 return 66 } 67 68 globalPrefix := os.Getenv("AUTOMUTEUS_GLOBAL_PREFIX") 69 if globalPrefix != "" && strings.HasPrefix(contents, globalPrefix) { 70 // if the global matches, then use that for future processing/control flow using the prefix 71 prefix = globalPrefix 72 } 73 74 if strings.HasPrefix(contents, prefix) { 75 if redis_common.IsUserRateLimitedGeneral(bot.RedisInterface.client, m.Author.ID) { 76 banned := redis_common.IncrementRateLimitExceed(bot.RedisInterface.client, m.Author.ID) 77 if banned { 78 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 79 ID: "message_handlers.softban", 80 Other: "I'm ignoring {{.User}} for the next 5 minutes, stop spamming", 81 }, 82 map[string]interface{}{ 83 "User": mentionByUserID(m.Author.ID), 84 })) 85 } else { 86 msg, err := s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 87 ID: "message_handlers.generalRatelimit", 88 Other: "{{.User}}, you're issuing commands too fast! Please slow down!", 89 }, 90 map[string]interface{}{ 91 "User": mentionByUserID(m.Author.ID), 92 })) 93 if err == nil { 94 go func() { 95 time.Sleep(time.Second * 3) 96 s.ChannelMessageDelete(m.ChannelID, msg.ID) 97 }() 98 } 99 } 100 101 return 102 } 103 redis_common.MarkUserRateLimit(bot.RedisInterface.client, m.Author.ID, "", 0) 104 105 oldLen := len(contents) 106 contents = strings.Replace(contents, prefix+" ", "", 1) 107 if len(contents) == oldLen { // didn't have a space 108 contents = strings.Replace(contents, prefix, "", 1) 109 } 110 111 isAdmin, isPermissioned := false, false 112 113 if g.OwnerID == m.Author.ID || (len(sett.AdminUserIDs) == 0 && len(sett.PermissionRoleIDs) == 0) { 114 // the guild owner should always have both permissions 115 // or if both permissions are still empty everyone get both 116 isAdmin = true 117 isPermissioned = true 118 } else { 119 // if we have no admins, then we MUST have mods as per the check above. 120 if len(sett.AdminUserIDs) == 0 { 121 // we have no admins, but we have mods, so make sure users fulfill that check 122 isAdmin = sett.HasRolePerms(m.Member) 123 } else { 124 // we have admins; make sure user is one 125 isAdmin = sett.HasAdminPerms(m.Author) 126 } 127 // even if we have admins, we can grant mod if the moderators role is empty; it is lesser permissions 128 isPermissioned = len(sett.PermissionRoleIDs) == 0 || sett.HasRolePerms(m.Member) 129 } 130 131 if len(contents) == 0 { 132 if len(prefix) <= 1 { 133 // prevent bot from spamming help message whenever the single character 134 // prefix is sent by mistake 135 return 136 } 137 embed := helpResponse(isAdmin, isPermissioned, prefix, command.AllCommands, sett) 138 s.ChannelMessageSendEmbed(m.ChannelID, &embed) 139 // delete the user's message 140 deleteMessage(s, m.ChannelID, m.ID) 141 } else { 142 args := strings.Split(contents, " ") 143 144 for i, v := range args { 145 args[i] = strings.ToLower(v) 146 } 147 148 bot.HandleCommand(isAdmin, isPermissioned, sett, s, g, m, args) 149 } 150 } 151} 152 153func (bot *Bot) handleReactionGameStartAdd(s *discordgo.Session, m *discordgo.MessageReactionAdd) { 154 // IgnoreSpectator all reactions created by the bot itself 155 if m.UserID == s.State.User.ID { 156 return 157 } 158 159 if redis_common.IsUserBanned(bot.RedisInterface.client, m.UserID) { 160 return 161 } 162 163 lock := bot.RedisInterface.LockSnowflake(m.MessageID + m.UserID + m.Emoji.ID) 164 // couldn't obtain lock; bail bail bail! 165 if lock == nil { 166 return 167 } 168 defer lock.Release(ctx) 169 170 g, err := s.State.Guild(m.GuildID) 171 if err != nil { 172 log.Println(err) 173 return 174 } 175 176 // TODO explicitly unmute/undeafen users that unlink. Current control flow won't do it (ala discord bots not being undeafened) 177 178 sett := bot.StorageInterface.GetGuildSettings(m.GuildID) 179 180 gsr := GameStateRequest{ 181 GuildID: m.GuildID, 182 TextChannel: m.ChannelID, 183 } 184 lock, dgs := bot.RedisInterface.GetDiscordGameStateAndLock(gsr) 185 if lock != nil && dgs != nil && dgs.Exists() { 186 // verify that the User is reacting to the state/status message 187 if dgs.IsReactionTo(m) { 188 if redis_common.IsUserRateLimitedGeneral(bot.RedisInterface.client, m.UserID) { 189 banned := redis_common.IncrementRateLimitExceed(bot.RedisInterface.client, m.UserID) 190 if banned { 191 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 192 ID: "message_handlers.softban", 193 Other: "I'm ignoring {{.User}} for the next 5 minutes, stop spamming", 194 }, 195 map[string]interface{}{ 196 "User": mentionByUserID(m.UserID), 197 })) 198 } else { 199 msg, err := s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 200 ID: "message_handlers.handleReactionGameStartAdd.generalRatelimit", 201 Other: "{{.User}}, you're reacting too fast! Please slow down!", 202 }, map[string]interface{}{ 203 "User": mentionByUserID(m.UserID), 204 })) 205 if err == nil { 206 go func() { 207 time.Sleep(time.Second * 3) 208 s.ChannelMessageDelete(m.ChannelID, msg.ID) 209 }() 210 } 211 } 212 return 213 } 214 redis_common.MarkUserRateLimit(bot.RedisInterface.client, m.UserID, "Reaction", redis_common.ReactionRateLimitDuration) 215 idMatched := false 216 if m.Emoji.Name == "▶️" { 217 metrics.RecordDiscordRequests(bot.RedisInterface.client, metrics.ReactionAdd, 14) 218 go removeReaction(bot.PrimarySession, m.ChannelID, m.MessageID, m.Emoji.Name, m.UserID) 219 go removeReaction(bot.PrimarySession, m.ChannelID, m.MessageID, m.Emoji.Name, "@me") 220 go dgs.AddAllReactions(bot.PrimarySession, bot.StatusEmojis[true]) 221 } else { 222 for color, e := range bot.StatusEmojis[true] { 223 if e.ID == m.Emoji.ID { 224 idMatched = true 225 log.Print(fmt.Sprintf("Player %s reacted with color %s\n", m.UserID, game.GetColorStringForInt(color))) 226 // the User doesn't exist in our userdata cache; add them 227 user, added := dgs.checkCacheAndAddUser(g, s, m.UserID) 228 if !added { 229 log.Println("No users found in Discord for UserID " + m.UserID) 230 idMatched = false 231 } else { 232 auData, found := dgs.AmongUsData.GetByColor(game.GetColorStringForInt(color)) 233 if found { 234 user.Link(auData) 235 dgs.UpdateUserData(m.UserID, user) 236 go bot.RedisInterface.AddUsernameLink(m.GuildID, m.UserID, auData.Name) 237 } else { 238 log.Println("I couldn't find any player data for that color; is your capture linked?") 239 idMatched = false 240 } 241 } 242 243 // then remove the player's reaction if we matched, or if we didn't 244 go s.MessageReactionRemove(m.ChannelID, m.MessageID, e.FormatForReaction(), m.UserID) 245 break 246 } 247 } 248 if !idMatched { 249 // log.Println(m.Emoji.Name) 250 if m.Emoji.Name == "❌" { 251 log.Println("Removing player " + m.UserID) 252 dgs.ClearPlayerData(m.UserID) 253 go s.MessageReactionRemove(m.ChannelID, m.MessageID, "❌", m.UserID) 254 idMatched = true 255 } 256 } 257 // make sure to update any voice changes if they occurred 258 if idMatched { 259 bot.handleTrackedMembers(bot.PrimarySession, sett, 0, NoPriority, gsr) 260 edited := dgs.Edit(s, bot.gameStateResponse(dgs, sett)) 261 if edited { 262 metrics.RecordDiscordRequests(bot.RedisInterface.client, metrics.MessageEdit, 1) 263 } 264 } 265 } 266 } 267 bot.RedisInterface.SetDiscordGameState(dgs, lock) 268 } 269} 270 271// voiceStateChange handles more edge-case behavior for users moving between voice channels, and catches when 272// relevant discord api requests are fully applied successfully. Otherwise, we can issue multiple requests for 273// the same mute/unmute, erroneously 274func (bot *Bot) handleVoiceStateChange(s *discordgo.Session, m *discordgo.VoiceStateUpdate) { 275 snowFlakeLock := bot.RedisInterface.LockSnowflake(m.ChannelID + m.UserID + m.SessionID) 276 // couldn't obtain lock; bail bail bail! 277 if snowFlakeLock == nil { 278 return 279 } 280 defer snowFlakeLock.Release(ctx) 281 282 prem, days := bot.PostgresInterface.GetGuildPremiumStatus(m.GuildID) 283 premTier := premium.FreeTier 284 if !premium.IsExpired(prem, days) { 285 premTier = prem 286 } 287 288 sett := bot.StorageInterface.GetGuildSettings(m.GuildID) 289 gsr := GameStateRequest{ 290 GuildID: m.GuildID, 291 VoiceChannel: m.ChannelID, 292 } 293 294 stateLock, dgs := bot.RedisInterface.GetDiscordGameStateAndLock(gsr) 295 if stateLock == nil { 296 return 297 } 298 defer stateLock.Release(ctx) 299 300 var voiceLock *redislock.Lock 301 if dgs.ConnectCode != "" { 302 voiceLock = bot.RedisInterface.LockVoiceChanges(dgs.ConnectCode, time.Second) 303 if voiceLock == nil { 304 return 305 } 306 } 307 308 g, err := s.State.Guild(dgs.GuildID) 309 310 if err != nil || g == nil { 311 return 312 } 313 314 // fetch the userData from our userData data cache 315 userData, err := dgs.GetUser(m.UserID) 316 if err != nil { 317 // the User doesn't exist in our userdata cache; add them 318 userData, _ = dgs.checkCacheAndAddUser(g, s, m.UserID) 319 } 320 321 tracked := m.ChannelID != "" && dgs.Tracking.ChannelID == m.ChannelID 322 323 auData, found := dgs.AmongUsData.GetByName(userData.InGameName) 324 325 var isAlive bool 326 327 // only actually tracked if we're in a tracked channel AND linked to a player 328 if !sett.GetMuteSpectator() { 329 tracked = tracked && found 330 isAlive = auData.IsAlive 331 } else { 332 if !found { 333 // we just assume the spectator is dead 334 isAlive = false 335 } else { 336 isAlive = auData.IsAlive 337 } 338 } 339 mute, deaf := sett.GetVoiceState(isAlive, tracked, dgs.AmongUsData.GetPhase()) 340 // check the userdata is linked here to not accidentally undeafen music bots, for example 341 if found && (userData.ShouldBeDeaf != deaf || userData.ShouldBeMute != mute) && (mute != m.Mute || deaf != m.Deaf) { 342 userData.SetShouldBeMuteDeaf(mute, deaf) 343 344 dgs.UpdateUserData(m.UserID, userData) 345 346 if dgs.Running { 347 uid, _ := strconv.ParseUint(m.UserID, 10, 64) 348 req := task.UserModifyRequest{ 349 Premium: premTier, 350 Users: []task.UserModify{ 351 { 352 UserID: uid, 353 Mute: mute, 354 Deaf: deaf, 355 }, 356 }, 357 } 358 mdsc := bot.GalactusClient.ModifyUsers(m.GuildID, dgs.ConnectCode, req, voiceLock) 359 if mdsc == nil { 360 log.Println("Nil response from modifyUsers, probably not good...") 361 } else { 362 go RecordDiscordRequestsByCounts(bot.RedisInterface.client, mdsc) 363 } 364 } 365 } 366 bot.RedisInterface.SetDiscordGameState(dgs, stateLock) 367} 368 369func (bot *Bot) handleNewGameMessage(s *discordgo.Session, m *discordgo.MessageCreate, g *discordgo.Guild, sett *storage.GuildSettings) { 370 lock, dgs := bot.RedisInterface.GetDiscordGameStateAndLock(GameStateRequest{ 371 GuildID: m.GuildID, 372 TextChannel: m.ChannelID, 373 }) 374 retries := 0 375 for lock == nil { 376 if retries > 10 { 377 log.Println("DEADLOCK in obtaining game state lock, upon calling new") 378 s.ChannelMessageSend(m.ChannelID, "I wasn't able to make a new game, maybe try in a different text channel?") 379 return 380 } 381 retries++ 382 lock, dgs = bot.RedisInterface.GetDiscordGameStateAndLock(GameStateRequest{ 383 GuildID: m.GuildID, 384 TextChannel: m.ChannelID, 385 }) 386 } 387 388 if redis_common.IsUserRateLimitedSpecific(bot.RedisInterface.client, m.Author.ID, "NewGame") { 389 banned := redis_common.IncrementRateLimitExceed(bot.RedisInterface.client, m.Author.ID) 390 if banned { 391 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 392 ID: "message_handlers.softban", 393 Other: "{{.User}} I'm ignoring your messages for the next 5 minutes, stop spamming", 394 }, map[string]interface{}{ 395 "User": mentionByUserID(m.Author.ID), 396 })) 397 } else { 398 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 399 ID: "message_handlers.handleNewGameMessage.specificRatelimit", 400 Other: "{{.User}} You're creating games too fast! Please slow down!", 401 }, map[string]interface{}{ 402 "User": mentionByUserID(m.Author.ID), 403 })) 404 } 405 lock.Release(context.Background()) 406 return 407 } 408 409 redis_common.MarkUserRateLimit(bot.RedisInterface.client, m.Author.ID, "NewGame", redis_common.NewGameRateLimitDuration) 410 411 channels, err := s.GuildChannels(m.GuildID) 412 if err != nil { 413 log.Println(err) 414 } 415 416 tracking := TrackingChannel{} 417 418 // loop over all the channels in the discord and cross-reference with the one that the .au new author is in 419 for _, channel := range channels { 420 if channel.Type == discordgo.ChannelTypeGuildVoice { 421 for _, v := range g.VoiceStates { 422 // if the User who typed au new is in a voice channel 423 if v.UserID == m.Author.ID { 424 // once we find the voice channel 425 if channel.ID == v.ChannelID { 426 tracking = TrackingChannel{ 427 ChannelID: channel.ID, 428 ChannelName: channel.Name, 429 } 430 break 431 } 432 } 433 } 434 } 435 } 436 if tracking.ChannelID == "" { 437 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 438 ID: "message_handlers.handleNewGameMessage.noChannel", 439 Other: "{{.User}}, please join a voice channel before starting a match!", 440 }, map[string]interface{}{ 441 "User": mentionByUserID(m.Author.ID), 442 })) 443 lock.Release(context.Background()) 444 return 445 } 446 447 // allow people with a previous game going to be able to make new games 448 if dgs.GameStateMsg.MessageID != "" { 449 if v, ok := bot.EndGameChannels[dgs.ConnectCode]; ok { 450 v <- true 451 } 452 delete(bot.EndGameChannels, dgs.ConnectCode) 453 454 dgs.Reset() 455 } else { 456 premStatus, days := bot.PostgresInterface.GetGuildPremiumStatus(m.GuildID) 457 premTier := premium.FreeTier 458 if !premium.IsExpired(premStatus, days) { 459 premTier = premStatus 460 } 461 462 // Premium users should always be allowed to start new games; only check the free guilds 463 if premTier == premium.FreeTier { 464 activeGames := broker.GetActiveGames(bot.RedisInterface.client, GameTimeoutSeconds) 465 act := os.Getenv("MAX_ACTIVE_GAMES") 466 num, err := strconv.ParseInt(act, 10, 64) 467 if err != nil { 468 num = DefaultMaxActiveGames 469 } 470 if activeGames > num { 471 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 472 ID: "message_handlers.handleNewGameMessage.lockout", 473 Other: "If I start any more games, Discord will lock me out, or throttle the games I'm running! \n" + 474 "Please try again in a few minutes, or consider AutoMuteUs Premium (`{{.CommandPrefix}} premium`)\n" + 475 "Current Games: {{.Games}}", 476 }, map[string]interface{}{ 477 "CommandPrefix": sett.CommandPrefix, 478 "Games": fmt.Sprintf("%d/%d", activeGames, num), 479 })) 480 lock.Release(context.Background()) 481 return 482 } 483 } 484 } 485 486 connectCode := generateConnectCode(m.GuildID) 487 488 dgs.ConnectCode = connectCode 489 490 bot.RedisInterface.RefreshActiveGame(m.GuildID, connectCode) 491 492 killChan := make(chan EndGameMessage) 493 494 go bot.SubscribeToGameByConnectCode(m.GuildID, connectCode, killChan) 495 496 dgs.Subscribed = true 497 498 bot.RedisInterface.SetDiscordGameState(dgs, lock) 499 500 bot.ChannelsMapLock.Lock() 501 bot.EndGameChannels[connectCode] = killChan 502 bot.ChannelsMapLock.Unlock() 503 504 hyperlink, minimalURL := formCaptureURL(bot.url, connectCode) 505 506 var embed = discordgo.MessageEmbed{ 507 URL: "", 508 Type: "", 509 Title: sett.LocalizeMessage(&i18n.Message{ 510 ID: "message_handlers.handleNewGameMessage.embed.Title", 511 Other: "You just started a game!", 512 }), 513 Description: sett.LocalizeMessage(&i18n.Message{ 514 ID: "message_handlers.handleNewGameMessage.embed.Description", 515 Other: "Click the following link to link your capture: \n <{{.hyperlink}}>\n\n" + 516 "Don't have the capture installed? Latest version [here]({{.downloadURL}})\n\nTo link your capture manually:", 517 }, 518 map[string]interface{}{ 519 "hyperlink": hyperlink, 520 "downloadURL": downloadURL, 521 }), 522 Timestamp: "", 523 Color: 3066993, // GREEN 524 Image: nil, 525 Thumbnail: nil, 526 Video: nil, 527 Provider: nil, 528 Author: nil, 529 Fields: []*discordgo.MessageEmbedField{ 530 { 531 Name: sett.LocalizeMessage(&i18n.Message{ 532 ID: "message_handlers.handleNewGameMessage.embed.Fields.URL", 533 Other: "URL", 534 }), 535 Value: minimalURL, 536 Inline: true, 537 }, 538 { 539 Name: sett.LocalizeMessage(&i18n.Message{ 540 ID: "message_handlers.handleNewGameMessage.embed.Fields.Code", 541 Other: "Code", 542 }), 543 Value: connectCode, 544 Inline: true, 545 }, 546 }, 547 } 548 549 log.Println("Generated URL for connection: " + hyperlink) 550 551 sendMessageDM(s, m.Author.ID, &embed) 552 553 bot.handleGameStartMessage(s, m, sett, tracking, g, connectCode) 554} 555 556func (bot *Bot) handleGameStartMessage(s *discordgo.Session, m *discordgo.MessageCreate, sett *storage.GuildSettings, channel TrackingChannel, g *discordgo.Guild, connCode string) { 557 lock, dgs := bot.RedisInterface.GetDiscordGameStateAndLock(GameStateRequest{ 558 GuildID: m.GuildID, 559 TextChannel: m.ChannelID, 560 ConnectCode: connCode, 561 }) 562 if lock == nil { 563 log.Println("Couldn't obtain lock for DGS on game start...") 564 return 565 } 566 dgs.AmongUsData.SetRoomRegionMap("", "", game.EMPTYMAP) 567 568 dgs.clearGameTracking(s) 569 570 dgs.Running = true 571 572 if channel.ChannelName != "" { 573 dgs.Tracking = TrackingChannel{ 574 ChannelID: channel.ChannelID, 575 ChannelName: channel.ChannelName, 576 } 577 for _, v := range g.VoiceStates { 578 if v.ChannelID == channel.ChannelID { 579 dgs.checkCacheAndAddUser(g, s, v.UserID) 580 } 581 } 582 } 583 584 dgs.CreateMessage(s, bot.gameStateResponse(dgs, sett), m.ChannelID, m.Author.ID) 585 586 bot.RedisInterface.SetDiscordGameState(dgs, lock) 587 588 // log.Println("Added self game state message") 589 // +18 emojis, 1 for X 590 metrics.RecordDiscordRequests(bot.RedisInterface.client, metrics.ReactionAdd, 19) 591 592 go dgs.AddAllReactions(bot.PrimarySession, bot.StatusEmojis[true]) 593} 594