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