Example #1
0
        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);
        }
Example #2
0
            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);
                }
            }
Example #3
0
        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;
        }
Example #4
0
        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);
            }
        }
Example #5
0
        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);
        }