1package discord 2 3import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "log" 8 "regexp" 9 "strconv" 10 "strings" 11 12 "github.com/automuteus/utils/pkg/premium" 13 "github.com/denverquane/amongusdiscord/metrics" 14 15 "github.com/bwmarrin/discordgo" 16 "github.com/denverquane/amongusdiscord/discord/command" 17 "github.com/denverquane/amongusdiscord/storage" 18 "github.com/nicksnyder/go-i18n/v2/i18n" 19) 20 21const ( 22 MaxDebugMessageSize = 1980 23 detailedMapString = "detailed" 24 clearArgumentString = "clear" 25 trueString = "true" 26) 27 28var MatchIDRegex = regexp.MustCompile(`^[A-Z0-9]{8}:[0-9]+$`) 29 30// TODO cache/preconstruct these (no reason to make them fresh everytime help is called, except for the prefix...) 31func ConstructEmbedForCommand(prefix string, cmd command.Command, sett *storage.GuildSettings) *discordgo.MessageEmbed { 32 return &discordgo.MessageEmbed{ 33 URL: "", 34 Type: "", 35 Title: cmd.Emoji + " " + strings.Title(cmd.Command), 36 Description: sett.LocalizeMessage(cmd.Description, 37 map[string]interface{}{ 38 "CommandPrefix": sett.CommandPrefix, 39 }), 40 Timestamp: "", 41 Color: 15844367, // GOLD 42 Image: nil, 43 Thumbnail: nil, 44 Video: nil, 45 Provider: nil, 46 Author: nil, 47 Fields: []*discordgo.MessageEmbedField{ 48 { 49 Name: sett.LocalizeMessage(&i18n.Message{ 50 ID: "commands.ConstructEmbedForCommand.Fields.Example", 51 Other: "Example", 52 }), 53 Value: "`" + fmt.Sprintf("%s %s", prefix, cmd.Example) + "`", 54 Inline: false, 55 }, 56 { 57 Name: sett.LocalizeMessage(&i18n.Message{ 58 ID: "commands.ConstructEmbedForCommand.Fields.Arguments", 59 Other: "Arguments", 60 }), 61 Value: "`" + sett.LocalizeMessage(cmd.Arguments) + "`", 62 Inline: false, 63 }, 64 { 65 Name: sett.LocalizeMessage(&i18n.Message{ 66 ID: "commands.ConstructEmbedForCommand.Fields.Aliases", 67 Other: "Aliases", 68 }), 69 Value: strings.Join(cmd.Aliases, ", "), 70 Inline: false, 71 }, 72 }, 73 } 74} 75 76func (bot *Bot) HandleCommand(isAdmin, isPermissioned bool, sett *storage.GuildSettings, s *discordgo.Session, g *discordgo.Guild, m *discordgo.MessageCreate, args []string) { 77 prefix := sett.CommandPrefix 78 cmd := command.GetCommand(args[0]) 79 80 gsr := GameStateRequest{ 81 GuildID: m.GuildID, 82 TextChannel: m.ChannelID, 83 } 84 85 if cmd.CommandType != command.Null { 86 log.Print(fmt.Sprintf("\"%s\" command typed by User %s\n", cmd.Command, m.Author.ID)) 87 } 88 89 switch { 90 case cmd.IsAdmin && !isAdmin: 91 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 92 ID: "message_handlers.handleMessageCreate.noPerms", 93 Other: "User does not have the required permissions to execute this command!", 94 })) 95 case cmd.IsOperator && (!isPermissioned && !isAdmin): 96 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 97 ID: "message_handlers.handleMessageCreate.noPerms", 98 Other: "User does not have the required permissions to execute this command!", 99 })) 100 default: 101 metrics.RecordDiscordRequests(bot.RedisInterface.client, metrics.MessageCreateDelete, 2) 102 switch cmd.CommandType { 103 case command.Help: 104 if len(args[1:]) == 0 { 105 embed := helpResponse(isAdmin, isPermissioned, prefix, command.AllCommands, sett) 106 s.ChannelMessageSendEmbed(m.ChannelID, &embed) 107 } else { 108 cmd = command.GetCommand(args[1]) 109 if cmd.CommandType != command.Null { 110 embed := ConstructEmbedForCommand(prefix, cmd, sett) 111 s.ChannelMessageSendEmbed(m.ChannelID, embed) 112 } else { 113 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 114 ID: "commands.HandleCommand.Help.notFound", 115 Other: "I didn't recognize that command! View `help` for all available commands!", 116 })) 117 } 118 } 119 120 case command.New: 121 bot.handleNewGameMessage(s, m, g, sett) 122 123 case command.End: 124 log.Println("User typed end to end the current game") 125 126 dgs := bot.RedisInterface.GetReadOnlyDiscordGameState(gsr) 127 if v, ok := bot.EndGameChannels[dgs.ConnectCode]; ok { 128 v <- true 129 } 130 delete(bot.EndGameChannels, dgs.ConnectCode) 131 132 bot.applyToAll(dgs, false, false) 133 134 case command.Pause: 135 lock, dgs := bot.RedisInterface.GetDiscordGameStateAndLock(gsr) 136 if lock == nil { 137 break 138 } 139 dgs.Running = !dgs.Running 140 141 bot.RedisInterface.SetDiscordGameState(dgs, lock) 142 if !dgs.Running { 143 bot.applyToAll(dgs, false, false) 144 } 145 146 edited := dgs.Edit(s, bot.gameStateResponse(dgs, sett)) 147 if edited { 148 metrics.RecordDiscordRequests(bot.RedisInterface.client, metrics.MessageEdit, 1) 149 } 150 151 case command.Refresh: 152 bot.RefreshGameStateMessage(gsr, sett) 153 case command.Link: 154 if len(args[1:]) < 2 { 155 embed := ConstructEmbedForCommand(prefix, cmd, sett) 156 s.ChannelMessageSendEmbed(m.ChannelID, embed) 157 } else { 158 lock, dgs := bot.RedisInterface.GetDiscordGameStateAndLock(gsr) 159 if lock == nil { 160 break 161 } 162 bot.linkPlayer(s, dgs, args[1:]) 163 bot.RedisInterface.SetDiscordGameState(dgs, lock) 164 165 edited := dgs.Edit(s, bot.gameStateResponse(dgs, sett)) 166 if edited { 167 metrics.RecordDiscordRequests(bot.RedisInterface.client, metrics.MessageEdit, 1) 168 } 169 } 170 171 case command.Unlink: 172 if len(args[1:]) == 0 { 173 embed := ConstructEmbedForCommand(prefix, cmd, sett) 174 s.ChannelMessageSendEmbed(m.ChannelID, embed) 175 } else { 176 userID, err := extractUserIDFromMention(args[1]) 177 if err != nil { 178 log.Println(err) 179 } else { 180 log.Print(fmt.Sprintf("Removing player %s", userID)) 181 lock, dgs := bot.RedisInterface.GetDiscordGameStateAndLock(gsr) 182 if lock == nil { 183 break 184 } 185 dgs.ClearPlayerData(userID) 186 187 bot.RedisInterface.SetDiscordGameState(dgs, lock) 188 189 // update the state message to reflect the player leaving 190 edited := dgs.Edit(s, bot.gameStateResponse(dgs, sett)) 191 if edited { 192 metrics.RecordDiscordRequests(bot.RedisInterface.client, metrics.MessageEdit, 1) 193 } 194 } 195 } 196 case command.UnmuteAll: 197 dgs := bot.RedisInterface.GetReadOnlyDiscordGameState(gsr) 198 bot.applyToAll(dgs, false, false) 199 200 case command.Settings: 201 premStatus, days := bot.PostgresInterface.GetGuildPremiumStatus(m.GuildID) 202 isPrem := !premium.IsExpired(premStatus, days) 203 bot.HandleSettingsCommand(s, m, sett, args, isPrem) 204 205 case command.Map: 206 if len(args[1:]) == 0 { 207 embed := ConstructEmbedForCommand(prefix, cmd, sett) 208 s.ChannelMessageSendEmbed(m.ChannelID, embed) 209 } else { 210 mapVersion := args[len(args)-1] 211 212 var mapName string 213 switch mapVersion { 214 case "simple", detailedMapString: 215 mapName = strings.Join(args[1:len(args)-1], " ") 216 default: 217 mapName = strings.Join(args[1:], " ") 218 mapVersion = sett.GetMapVersion() 219 } 220 mapItem, err := NewMapItem(mapName) 221 if err != nil { 222 log.Println(err) 223 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 224 ID: "commands.HandleCommand.Map.notFound", 225 Other: "I don't have a map by that name!", 226 })) 227 break 228 } 229 switch mapVersion { 230 case "simple": 231 s.ChannelMessageSend(m.ChannelID, mapItem.MapImage.Simple) 232 case detailedMapString: 233 s.ChannelMessageSend(m.ChannelID, mapItem.MapImage.Detailed) 234 default: 235 log.Println("mapVersion has unexpected value for 'map' command") 236 } 237 } 238 239 case command.Cache: 240 if len(args[1:]) == 0 { 241 embed := ConstructEmbedForCommand(prefix, cmd, sett) 242 s.ChannelMessageSendEmbed(m.ChannelID, embed) 243 } else { 244 userID, err := extractUserIDFromMention(args[1]) 245 if err != nil { 246 log.Println(err) 247 s.ChannelMessageSend(m.ChannelID, "I couldn't find a user by that name or ID!") 248 break 249 } 250 if len(args[2:]) == 0 { 251 cached := bot.RedisInterface.GetUsernameOrUserIDMappings(m.GuildID, userID) 252 if len(cached) == 0 { 253 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 254 ID: "commands.HandleCommand.Cache.emptyCachedNames", 255 Other: "I don't have any cached player names stored for that user!", 256 })) 257 } else { 258 buf := bytes.NewBuffer([]byte(sett.LocalizeMessage(&i18n.Message{ 259 ID: "commands.HandleCommand.Cache.cachedNames", 260 Other: "Cached in-game names:", 261 }))) 262 buf.WriteString("\n```\n") 263 for n := range cached { 264 buf.WriteString(fmt.Sprintf("%s\n", n)) 265 } 266 buf.WriteString("```") 267 268 s.ChannelMessageSend(m.ChannelID, buf.String()) 269 } 270 } else if strings.ToLower(args[2]) == clearArgumentString || strings.ToLower(args[2]) == "c" { 271 err := bot.RedisInterface.DeleteLinksByUserID(m.GuildID, userID) 272 if err != nil { 273 log.Println(err) 274 } else { 275 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 276 ID: "commands.HandleCommand.Cache.Success", 277 Other: "Successfully deleted all cached names for that user!", 278 })) 279 } 280 } 281 } 282 283 case command.Privacy: 284 if m.Author != nil { 285 var arg = "" 286 if len(args[1:]) > 0 { 287 arg = args[1] 288 } 289 if arg == "" || (arg != "showme" && arg != "optin" && arg != "optout") { 290 embed := ConstructEmbedForCommand(prefix, cmd, sett) 291 s.ChannelMessageSendEmbed(m.ChannelID, embed) 292 } else { 293 embed := bot.privacyResponse(m.GuildID, m.Author.ID, arg, sett) 294 s.ChannelMessageSendEmbed(m.ChannelID, embed) 295 } 296 } 297 298 case command.Info: 299 embed := bot.infoResponse(m.GuildID, sett) 300 _, err := s.ChannelMessageSendEmbed(m.ChannelID, embed) 301 if err != nil { 302 log.Println(err) 303 } 304 305 case command.DebugState: 306 if m.Author != nil { 307 state := bot.RedisInterface.GetReadOnlyDiscordGameState(gsr) 308 if state != nil { 309 jBytes, err := json.MarshalIndent(state, "", " ") 310 if err != nil { 311 log.Println(err) 312 } else { 313 for i := 0; i < len(jBytes); i += MaxDebugMessageSize { 314 end := i + MaxDebugMessageSize 315 if end > len(jBytes) { 316 end = len(jBytes) 317 } 318 s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("```JSON\n%s\n```", jBytes[i:end])) 319 } 320 } 321 } 322 } 323 324 case command.ASCII: 325 if len(args[1:]) == 0 { 326 s.ChannelMessageSend(m.ChannelID, ASCIICrewmate) 327 } else { 328 id, err := extractUserIDFromMention(args[1]) 329 if id == "" || err != nil { 330 s.ChannelMessageSend(m.ChannelID, "I couldn't find a user by that name or ID!") 331 } else { 332 imposter := false 333 count := 1 334 if len(args[2:]) > 0 { 335 if args[2] == trueString || args[2] == "t" { 336 imposter = true 337 } 338 if len(args[3:]) > 0 { 339 if itCount, err := strconv.Atoi(args[3]); err == nil { 340 count = itCount 341 } 342 } 343 } 344 s.ChannelMessageSend(m.ChannelID, ASCIIStarfield(sett, args[1], imposter, count)) 345 } 346 } 347 348 case command.Stats: 349 premStatus, days := bot.PostgresInterface.GetGuildPremiumStatus(m.GuildID) 350 isPrem := !premium.IsExpired(premStatus, days) 351 if len(args[1:]) == 0 { 352 embed := ConstructEmbedForCommand(prefix, cmd, sett) 353 s.ChannelMessageSendEmbed(m.ChannelID, embed) 354 } else { 355 userID, err := extractUserIDFromMention(args[1]) 356 if userID == "" || err != nil { 357 arg := strings.ReplaceAll(args[1], "\"", "") 358 if arg == "g" || arg == "guild" || arg == "server" { 359 if len(args) > 2 && args[2] == "reset" { 360 if !isAdmin { 361 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 362 ID: "message_handlers.handleResetGuild.noPerms", 363 Other: "Only Admins are capable of resetting server stats", 364 })) 365 } else { 366 if len(args) == 3 { 367 _, err := s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 368 ID: "commands.StatsCommand.Reset.NoConfirm", 369 Other: "Please type `{{.CommandPrefix}} stats guild reset confirm` if you are 100% certain that you wish to **completely reset** your guild's stats!", 370 }, 371 map[string]interface{}{ 372 "CommandPrefix": prefix, 373 })) 374 if err != nil { 375 log.Println(err) 376 } 377 } else if args[3] == "confirm" { 378 err := bot.PostgresInterface.DeleteAllGamesForServer(m.GuildID) 379 if err != nil { 380 s.ChannelMessageSend(m.ChannelID, "Encountered the following error when deleting the server's stats: "+err.Error()) 381 } else { 382 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 383 ID: "commands.StatsCommand.Reset.Success", 384 Other: "Successfully reset your guild's stats!", 385 })) 386 } 387 } 388 } 389 } else { 390 _, err := s.ChannelMessageSendEmbed(m.ChannelID, bot.GuildStatsEmbed(m.GuildID, sett, isPrem)) 391 if err != nil { 392 log.Println(err) 393 } 394 } 395 } else { 396 arg = strings.ToUpper(arg) 397 log.Println(arg) 398 if MatchIDRegex.MatchString(arg) { 399 strs := strings.Split(arg, ":") 400 if len(strs) < 2 { 401 log.Println("Something very wrong with the regex for match/conn codes...") 402 } else { 403 s.ChannelMessageSendEmbed(m.ChannelID, bot.GameStatsEmbed(m.GuildID, strs[1], strs[0], sett, isPrem)) 404 } 405 } else { 406 s.ChannelMessageSend(m.ChannelID, "I didn't recognize that user, you mistyped 'guild', or didn't provide a valid Match ID") 407 } 408 } 409 } else { 410 if len(args) > 2 && args[2] == "reset" { 411 if !isAdmin { 412 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 413 ID: "message_handlers.handleResetGuild.noPerms", 414 Other: "Only Admins are capable of resetting server stats", 415 })) 416 } else { 417 if len(args) == 3 { 418 _, err := s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 419 ID: "commands.StatsCommand.ResetUser.NoConfirm", 420 Other: "Please type `{{.CommandPrefix}} stats `{{.User}}` reset confirm` if you are 100% certain that you wish to **completely reset** that user's stats!", 421 }, 422 map[string]interface{}{ 423 "CommandPrefix": prefix, 424 "User": args[1], 425 })) 426 if err != nil { 427 log.Println(err) 428 } 429 } else if args[3] == "confirm" { 430 err := bot.PostgresInterface.DeleteAllGamesForUser(userID) 431 if err != nil { 432 s.ChannelMessageSend(m.ChannelID, "Encountered the following error when deleting that user's stats: "+err.Error()) 433 } else { 434 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 435 ID: "commands.StatsCommand.ResetUser.Success", 436 Other: "Successfully reset {{.User}}'s stats!", 437 }, 438 map[string]interface{}{ 439 "User": args[1], 440 })) 441 } 442 } 443 } 444 } else { 445 s.ChannelMessageSendEmbed(m.ChannelID, bot.UserStatsEmbed(userID, m.GuildID, sett, isPrem)) 446 } 447 } 448 } 449 450 case command.Premium: 451 premStatus, days := bot.PostgresInterface.GetGuildPremiumStatus(m.GuildID) 452 if len(args[1:]) == 0 { 453 s.ChannelMessageSendEmbed(m.ChannelID, premiumEmbedResponse(m.GuildID, premStatus, days, sett)) 454 } else { 455 tier := premium.FreeTier 456 if !premium.IsExpired(premStatus, days) { 457 tier = premStatus 458 } 459 arg := strings.ToLower(args[1]) 460 if isAdmin { 461 if arg == "invite" || arg == "invites" || arg == "inv" { 462 _, err := s.ChannelMessageSendEmbed(m.ChannelID, premiumInvitesEmbed(tier, sett)) 463 if err != nil { 464 log.Println(err) 465 } 466 } else { 467 s.ChannelMessageSend(m.ChannelID, "Sorry, I didn't recognize that premium command or argument!") 468 } 469 } else { 470 s.ChannelMessageSend(m.ChannelID, "Viewing the premium invites is an Admin-only command") 471 } 472 } 473 474 default: 475 s.ChannelMessageSend(m.ChannelID, sett.LocalizeMessage(&i18n.Message{ 476 ID: "commands.HandleCommand.default", 477 Other: "Sorry, I didn't understand `{{.InvalidCommand}}`! Please see `{{.CommandPrefix}} help` for commands", 478 }, 479 map[string]interface{}{ 480 "CommandPrefix": prefix, 481 "InvalidCommand": args[0], 482 })) 483 } 484 } 485 486 deleteMessage(s, m.ChannelID, m.Message.ID) 487} 488