public static async Task <(string productCode, TitleInfo info)> LookupGameAsync(DiscordChannel channel, DiscordMessage message, string gameTitle) { var lastBotMessages = await channel.GetMessagesBeforeAsync(message.Id, 20, DateTime.UtcNow.AddSeconds(-30)).ConfigureAwait(false); foreach (var msg in lastBotMessages) { if (BotReactionsHandler.NeedToSilence(msg).needToChill) { return(null, null); } } try { var requestBuilder = RequestBuilder.Start().SetSearch(gameTitle); var searchCompatListTask = Client.GetCompatResultAsync(requestBuilder, Config.Cts.Token); var localList = CompatList.GetLocalCompatResult(requestBuilder); var status = await searchCompatListTask.ConfigureAwait(false); status = status.Append(localList); if ((status.ReturnCode == 0 || status.ReturnCode == 2) && status.Results.Any()) { var sortedList = status.GetSortedList(); var bestMatch = sortedList.First(); var listWithStatus = sortedList .TakeWhile(i => Math.Abs(i.score - bestMatch.score) < double.Epsilon) .Where(i => !string.IsNullOrEmpty(i.info.Status) && i.info.Status != "Unknown") .ToList(); if (listWithStatus.Count > 0) { bestMatch = listWithStatus.First(); } var(code, info, score) = bestMatch; Config.Log.Debug($"Looked up \"{gameTitle}\", got \"{info?.Title}\" with score {score}"); if (score < Config.GameTitleMatchThreshold) { return(null, null); } if (!string.IsNullOrEmpty(info?.Title)) { StatsStorage.GameStatCache.TryGetValue(info.Title, out int stat); StatsStorage.GameStatCache.Set(info.Title, ++stat, StatsStorage.CacheTime); } return(code, info); } } catch (Exception e) { Config.Log.Warn(e); } return(null, null); }
public async Task ImportMc(CommandContext ctx) { if (await ImportLockObj.WaitAsync(0).ConfigureAwait(false)) { try { await CompatList.ImportMetacriticScoresAsync().ConfigureAwait(false); await using var db = new ThumbnailDb(); var linkedItems = await db.Thumbnail.CountAsync(i => i.MetacriticId != null).ConfigureAwait(false); await ctx.RespondAsync($"Importing Metacritic info was successful, linked {linkedItems} items").ConfigureAwait(false); } finally { ImportLockObj.Release(); } } else { await ctx.RespondAsync("Another import operation is already in progress").ConfigureAwait(false); } }
public static async Task OnMessageCreated(DiscordClient c, MessageCreateEventArgs args) { if (DefaultHandlerFilter.IsFluff(args?.Message)) { return; } #if !DEBUG if (!(args.Channel.Id == Config.BotGeneralChannelId || args.Channel.Name.Equals("help", StringComparison.InvariantCultureIgnoreCase))) { return; } if (CooldownBuckets.TryGetValue(args.Channel.Id, out var lastCheck) && DateTime.UtcNow - lastCheck < CooldownThreshold) { return; } if (args.Author.IsSmartlisted(c, args.Guild)) { return; } #endif var matches = GameNameStatusMention1.Matches(args.Message.Content); if (!matches.Any()) { return; } var gameTitle = matches.Select(m => m.Groups["game_title_1"].Value) .Concat(matches.Select(m => m.Groups["game_title_2"].Value)) .Concat(matches.Select(m => m.Groups["game_title_3"].Value)) .FirstOrDefault(t => !string.IsNullOrEmpty(t)); if (string.IsNullOrEmpty(gameTitle) || gameTitle.Length < 2) { return; } gameTitle = CompatList.FixGameTitleSearch(gameTitle); if (gameTitle.Length < 4) { return; } if (ProductCodeLookup.ProductCode.IsMatch(args.Message.Content)) { return; } var(_, info) = await LookupGameAsync(args.Channel, args.Message, gameTitle).ConfigureAwait(false); if (string.IsNullOrEmpty(info?.Status)) { return; } gameTitle = info.Title?.StripMarks(); if (string.IsNullOrEmpty(gameTitle)) { return; } var botSpamChannel = await c.GetChannelAsync(Config.BotSpamId).ConfigureAwait(false); var status = info.Status.ToLowerInvariant(); string msg; if (status == "unknown") { msg = $"{args.Message.Author.Mention} {gameTitle} status is {status}"; } else { if (status != "playable") { status += " (not playable)"; } msg = $"{args.Message.Author.Mention} {gameTitle} is {status}"; if (!string.IsNullOrEmpty(info.Date)) { msg += $" since {info.ToUpdated()}"; } } msg += $"\nfor more results please use compatibility list (<https://rpcs3.net/compatibility>) or `{Config.CommandPrefix}c` command in {botSpamChannel.Mention} (`!c {gameTitle.Sanitize()}`)"; await args.Channel.SendMessageAsync(msg).ConfigureAwait(false); CooldownBuckets[args.Channel.Id] = DateTime.UtcNow; }
internal static async Task Main(string[] args) { Config.TelemetryClient?.TrackEvent("startup"); Console.WriteLine("Confinement: " + SandboxDetector.Detect()); if (args.Length > 0 && args[0] == "--dry-run") { Console.WriteLine("Database path: " + Path.GetDirectoryName(Path.GetFullPath(DbImporter.GetDbPath("fake.db", Environment.SpecialFolder.ApplicationData)))); if (Assembly.GetEntryAssembly().GetCustomAttribute <UserSecretsIdAttribute>() != null) { Console.WriteLine("Bot config path: " + Path.GetDirectoryName(Path.GetFullPath(Config.GoogleApiConfigPath))); } return; } if (Process.GetCurrentProcess().Id == 0) { Config.Log.Info("Well, this was unexpected"); } var singleInstanceCheckThread = new Thread(() => { using var instanceLock = new Mutex(false, @"Global\RPCS3 Compatibility Bot"); if (instanceLock.WaitOne(1000)) { try { InstanceCheck.Release(); ShutdownCheck.Wait(); } finally { instanceLock.ReleaseMutex(); } } }); try { singleInstanceCheckThread.Start(); if (!await InstanceCheck.WaitAsync(1000).ConfigureAwait(false)) { Config.Log.Fatal("Another instance is already running."); return; } if (string.IsNullOrEmpty(Config.Token) || Config.Token.Length < 16) { Config.Log.Fatal("No token was specified."); return; } if (SandboxDetector.Detect() == SandboxType.Docker) { Config.Log.Info("Checking for updates..."); try { var(updated, stdout) = await Sudo.Bot.UpdateAsync().ConfigureAwait(false); if (!string.IsNullOrEmpty(stdout) && updated) { Config.Log.Debug(stdout); } if (updated) { Sudo.Bot.Restart(InvalidChannelId, "Restarted due to new bot updates not present in this Docker image"); return; } } catch (Exception e) { Config.Log.Error(e, "Failed to check for updates"); } } using (var db = new BotDb()) if (!await DbImporter.UpgradeAsync(db, Config.Cts.Token)) { return; } using (var db = new ThumbnailDb()) if (!await DbImporter.UpgradeAsync(db, Config.Cts.Token)) { return; } await SqlConfiguration.RestoreAsync().ConfigureAwait(false); Config.Log.Debug("Restored configuration variables from persistent storage"); await StatsStorage.RestoreAsync().ConfigureAwait(false); Config.Log.Debug("Restored stats from persistent storage"); var backgroundTasks = Task.WhenAll( AmdDriverVersionProvider.RefreshAsync(), #if !DEBUG new PsnScraper().RunAsync(Config.Cts.Token), GameTdbScraper.RunAsync(Config.Cts.Token), #endif StatsStorage.BackgroundSaveAsync(), CompatList.ImportCompatListAsync() ); try { if (!Directory.Exists(Config.IrdCachePath)) { Directory.CreateDirectory(Config.IrdCachePath); } } catch (Exception e) { Config.Log.Warn(e, $"Failed to create new folder {Config.IrdCachePath}: {e.Message}"); } var config = new DiscordConfiguration { Token = Config.Token, TokenType = TokenType.Bot, MessageCacheSize = Config.MessageCacheSize, LoggerFactory = Config.LoggerFactory, }; using var client = new DiscordClient(config); var commands = client.UseCommandsNext(new CommandsNextConfiguration { StringPrefixes = new[] { Config.CommandPrefix, Config.AutoRemoveCommandPrefix }, Services = new ServiceCollection().BuildServiceProvider(), }); commands.RegisterConverter(new TextOnlyDiscordChannelConverter()); commands.RegisterCommands <Misc>(); commands.RegisterCommands <CompatList>(); commands.RegisterCommands <Sudo>(); commands.RegisterCommands <CommandsManagement>(); commands.RegisterCommands <ContentFilters>(); commands.RegisterCommands <Warnings>(); commands.RegisterCommands <Explain>(); commands.RegisterCommands <Psn>(); commands.RegisterCommands <Invites>(); commands.RegisterCommands <Moderation>(); commands.RegisterCommands <Ird>(); commands.RegisterCommands <BotMath>(); commands.RegisterCommands <Pr>(); commands.RegisterCommands <Events>(); commands.RegisterCommands <E3>(); commands.RegisterCommands <Cyberpunk2077>(); commands.RegisterCommands <Rpcs3Ama>(); commands.RegisterCommands <BotStats>(); commands.RegisterCommands <Syscall>(); commands.RegisterCommands <ForcedNicknames>(); commands.RegisterCommands <Minesweeper>(); if (!string.IsNullOrEmpty(Config.AzureComputerVisionKey)) { commands.RegisterCommands <Vision>(); } commands.CommandErrored += UnknownCommandHandler.OnError; client.UseInteractivity(new InteractivityConfiguration()); client.Ready += async(c, _) => { var admin = await c.GetUserAsync(Config.BotAdminId).ConfigureAwait(false); Config.Log.Info("Bot is ready to serve!"); Config.Log.Info(""); Config.Log.Info($"Bot user id : {c.CurrentUser.Id} ({c.CurrentUser.Username})"); Config.Log.Info($"Bot admin id : {Config.BotAdminId} ({admin.Username ?? "???"}#{admin.Discriminator ?? "????"})"); Config.Log.Info(""); }; client.GuildAvailable += async(c, gaArgs) => { await BotStatusMonitor.RefreshAsync(c).ConfigureAwait(false); Watchdog.DisconnectTimestamps.Clear(); Watchdog.TimeSinceLastIncomingMessage.Restart(); if (gaArgs.Guild.Id != Config.BotGuildId) { #if DEBUG Config.Log.Warn($"Unknown discord server {gaArgs.Guild.Id} ({gaArgs.Guild.Name})"); #else Config.Log.Warn($"Unknown discord server {gaArgs.Guild.Id} ({gaArgs.Guild.Name}), leaving..."); await gaArgs.Guild.LeaveAsync().ConfigureAwait(false); #endif return; } Config.Log.Info($"Server {gaArgs.Guild.Name} is available now"); Config.Log.Info($"Checking moderation backlogs in {gaArgs.Guild.Name}..."); try { await Task.WhenAll( Starbucks.CheckBacklogAsync(c, gaArgs.Guild).ContinueWith(_ => Config.Log.Info($"Starbucks backlog checked in {gaArgs.Guild.Name}."), TaskScheduler.Default), DiscordInviteFilter.CheckBacklogAsync(c, gaArgs.Guild).ContinueWith(_ => Config.Log.Info($"Discord invites backlog checked in {gaArgs.Guild.Name}."), TaskScheduler.Default) ).ConfigureAwait(false); } catch (Exception e) { Config.Log.Warn(e, "Error running backlog tasks"); } Config.Log.Info($"All moderation backlogs checked in {gaArgs.Guild.Name}."); }; client.GuildAvailable += (c, _) => UsernameValidationMonitor.MonitorAsync(c, true); client.GuildUnavailable += (_, guArgs) => { Config.Log.Warn($"{guArgs.Guild.Name} is unavailable"); return(Task.CompletedTask); }; #if !DEBUG /* * client.GuildDownloadCompleted += async gdcArgs => * { * foreach (var guild in gdcArgs.Guilds) * await ModProvider.SyncRolesAsync(guild.Value).ConfigureAwait(false); * }; */ #endif client.MessageReactionAdded += Starbucks.Handler; client.MessageReactionAdded += ContentFilterMonitor.OnReaction; client.MessageCreated += (_, __) => { Watchdog.TimeSinceLastIncomingMessage.Restart(); return(Task.CompletedTask); }; client.MessageCreated += ContentFilterMonitor.OnMessageCreated; // should be first client.MessageCreated += GlobalMessageCache.OnMessageCreated; var mediaScreenshotMonitor = new MediaScreenshotMonitor(client); if (!string.IsNullOrEmpty(Config.AzureComputerVisionKey)) { client.MessageCreated += mediaScreenshotMonitor.OnMessageCreated; } client.MessageCreated += ProductCodeLookup.OnMessageCreated; client.MessageCreated += LogParsingHandler.OnMessageCreated; client.MessageCreated += LogAsTextMonitor.OnMessageCreated; client.MessageCreated += DiscordInviteFilter.OnMessageCreated; client.MessageCreated += PostLogHelpHandler.OnMessageCreated; client.MessageCreated += BotReactionsHandler.OnMessageCreated; client.MessageCreated += GithubLinksHandler.OnMessageCreated; client.MessageCreated += NewBuildsMonitor.OnMessageCreated; client.MessageCreated += TableFlipMonitor.OnMessageCreated; client.MessageCreated += IsTheGamePlayableHandler.OnMessageCreated; client.MessageCreated += EmpathySimulationHandler.OnMessageCreated; client.MessageUpdated += GlobalMessageCache.OnMessageUpdated; client.MessageUpdated += ContentFilterMonitor.OnMessageUpdated; client.MessageUpdated += DiscordInviteFilter.OnMessageUpdated; client.MessageUpdated += EmpathySimulationHandler.OnMessageUpdated; client.MessageDeleted += GlobalMessageCache.OnMessageDeleted; if (Config.DeletedMessagesLogChannelId > 0) { client.MessageDeleted += DeletedMessagesMonitor.OnMessageDeleted; } client.MessageDeleted += ThumbnailCacheMonitor.OnMessageDeleted; client.MessageDeleted += EmpathySimulationHandler.OnMessageDeleted; client.MessagesBulkDeleted += GlobalMessageCache.OnMessagesBulkDeleted; client.UserUpdated += UsernameSpoofMonitor.OnUserUpdated; client.UserUpdated += UsernameZalgoMonitor.OnUserUpdated; client.GuildMemberAdded += Greeter.OnMemberAdded; client.GuildMemberAdded += UsernameSpoofMonitor.OnMemberAdded; client.GuildMemberAdded += UsernameZalgoMonitor.OnMemberAdded; client.GuildMemberAdded += UsernameValidationMonitor.OnMemberAdded; client.GuildMemberUpdated += UsernameSpoofMonitor.OnMemberUpdated; client.GuildMemberUpdated += UsernameZalgoMonitor.OnMemberUpdated; client.GuildMemberUpdated += UsernameValidationMonitor.OnMemberUpdated; Watchdog.DisconnectTimestamps.Enqueue(DateTime.UtcNow); try { await client.ConnectAsync().ConfigureAwait(false); } catch (Exception e) { Config.Log.Error(e, "Failed to connect to Discord: " + e.Message); throw; } ulong? channelId = null; string restartMsg = null; using (var db = new BotDb()) { var chState = db.BotState.FirstOrDefault(k => k.Key == "bot-restart-channel"); if (chState != null) { if (ulong.TryParse(chState.Value, out var ch)) { channelId = ch; } db.BotState.Remove(chState); } var msgState = db.BotState.FirstOrDefault(i => i.Key == "bot-restart-msg"); if (msgState != null) { restartMsg = msgState.Value; db.BotState.Remove(msgState); } db.SaveChanges(); } if (string.IsNullOrEmpty(restartMsg)) { restartMsg = null; } if (channelId.HasValue) { Config.Log.Info($"Found channelId {channelId}"); DiscordChannel channel; if (channelId == InvalidChannelId) { channel = await client.GetChannelAsync(Config.ThumbnailSpamId).ConfigureAwait(false); await channel.SendMessageAsync(restartMsg ?? "Bot has suffered some catastrophic failure and was restarted").ConfigureAwait(false); } else { channel = await client.GetChannelAsync(channelId.Value).ConfigureAwait(false); await channel.SendMessageAsync("Bot is up and running").ConfigureAwait(false); } } else { Config.Log.Debug($"Args count: {args.Length}"); var pArgs = args.Select(a => a == Config.Token ? "<Token>" : $"[{a}]"); Config.Log.Debug("Args: " + string.Join(" ", pArgs)); } Config.Log.Debug("Running RPCS3 update check thread"); backgroundTasks = Task.WhenAll( backgroundTasks, NewBuildsMonitor.MonitorAsync(client), Watchdog.Watch(client), InviteWhitelistProvider.CleanupAsync(client), UsernameValidationMonitor.MonitorAsync(client), Psn.Check.MonitorFwUpdates(client, Config.Cts.Token), Watchdog.SendMetrics(client), Watchdog.CheckGCStats(), mediaScreenshotMonitor.ProcessWorkQueue() ); while (!Config.Cts.IsCancellationRequested) { if (client.Ping > 1000) { Config.Log.Warn($"High ping detected: {client.Ping}"); } await Task.Delay(TimeSpan.FromMinutes(1), Config.Cts.Token).ContinueWith(dt => { /* in case it was cancelled */ }, TaskScheduler.Default).ConfigureAwait(false); } await backgroundTasks.ConfigureAwait(false); } catch (Exception e) { if (!Config.inMemorySettings.ContainsKey("shutdown")) { Config.Log.Fatal(e, "Experienced catastrophic failure, attempting to restart..."); } } finally { Config.TelemetryClient?.Flush(); ShutdownCheck.Release(); if (singleInstanceCheckThread.IsAlive) { singleInstanceCheckThread.Join(100); } } if (!Config.inMemorySettings.ContainsKey("shutdown")) { Sudo.Bot.Restart(InvalidChannelId, null); } }
public static async Task OnError(CommandErrorEventArgs e) { if (e.Context.User.IsBotSafeCheck()) { return; } if (!(e.Exception is CommandNotFoundException cnfe)) { Config.Log.Error(e.Exception); return; } if (string.IsNullOrEmpty(cnfe.CommandName)) { return; } if (e.Context.Prefix != Config.CommandPrefix && e.Context.Prefix != Config.AutoRemoveCommandPrefix && (e.Context.Message.Content?.EndsWith("?") ?? false) && e.Context.CommandsNext.RegisteredCommands.TryGetValue("8ball", out var cmd)) { var updatedContext = e.Context.CommandsNext.CreateContext( e.Context.Message, e.Context.Prefix, cmd, e.Context.Message.Content.Substring(e.Context.Prefix.Length).Trim() ); try { await cmd.ExecuteAsync(updatedContext).ConfigureAwait(false); } catch { } return; } if (cnfe.CommandName.Length < 3) { return; } #if !DEBUG if (e.Context.User.IsSmartlisted(e.Context.Client, e.Context.Guild)) { return; } #endif var pos = e.Context.Message?.Content?.IndexOf(cnfe.CommandName) ?? -1; if (pos < 0) { return; } var gameTitle = e.Context.Message.Content.Substring(pos).TrimEager().Trim(40); if (string.IsNullOrEmpty(gameTitle) || char.IsPunctuation(gameTitle[0])) { return; } var term = gameTitle.ToLowerInvariant(); var(explanation, fuzzyMatch, score) = await Explain.LookupTerm(term).ConfigureAwait(false); if (score > 0.5 && explanation != null) { if (!string.IsNullOrEmpty(fuzzyMatch)) { var fuzzyNotice = $"Showing explanation for `{fuzzyMatch}`:"; #if DEBUG fuzzyNotice = $"Showing explanation for `{fuzzyMatch}` ({score:0.######}):"; #endif await e.Context.RespondAsync(fuzzyNotice).ConfigureAwait(false); } StatsStorage.ExplainStatCache.TryGetValue(explanation.Keyword, out int stat); StatsStorage.ExplainStatCache.Set(explanation.Keyword, ++stat, StatsStorage.CacheTime); await e.Context.Channel.SendMessageAsync(explanation.Text, explanation.Attachment, explanation.AttachmentFilename).ConfigureAwait(false); return; } gameTitle = CompatList.FixGameTitleSearch(gameTitle); var productCodes = ProductCodeLookup.GetProductIds(gameTitle); if (productCodes.Any()) { await ProductCodeLookup.LookupAndPostProductCodeEmbedAsync(e.Context.Client, e.Context.Message, productCodes).ConfigureAwait(false); return; } var(productCode, titleInfo) = await IsTheGamePlayableHandler.LookupGameAsync(e.Context.Channel, e.Context.Message, gameTitle).ConfigureAwait(false); if (titleInfo != null) { var thumbUrl = await e.Context.Client.GetThumbnailUrlAsync(productCode).ConfigureAwait(false); var embed = titleInfo.AsEmbed(productCode, thumbnailUrl: thumbUrl); await ProductCodeLookup.FixAfrikaAsync(e.Context.Client, e.Context.Message, embed).ConfigureAwait(false); await e.Context.Channel.SendMessageAsync(embed : embed).ConfigureAwait(false); return; } var ch = await e.Context.GetChannelForSpamAsync().ConfigureAwait(false); await ch.SendMessageAsync( $"I am not sure what you wanted me to do, please use one of the following commands:\n" + $"`{Config.CommandPrefix}c {term.Sanitize(replaceBackTicks: true)}` to check the game status\n" + $"`{Config.CommandPrefix}explain list` to look at the list of available explanations\n" + $"`{Config.CommandPrefix}help` to look at available bot commands\n" ).ConfigureAwait(false); }