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