using System; using System.IO; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using CompatBot.Commands; using CompatBot.Commands.Converters; using CompatBot.Database; using CompatBot.Database.Providers; using CompatBot.EventHandlers; using CompatBot.ThumbScrapper; using CompatBot.Utils; using DSharpPlus; using DSharpPlus.CommandsNext; using DSharpPlus.Entities; using DSharpPlus.Interactivity; using Microsoft.Extensions.Configuration.UserSecrets; using Microsoft.Extensions.DependencyInjection; namespace CompatBot { internal static class Program { private static readonly SemaphoreSlim InstanceCheck = new SemaphoreSlim(0, 1); private static readonly SemaphoreSlim ShutdownCheck = new SemaphoreSlim(0, 1); internal const ulong InvalidChannelId = 13; internal static async Task Main(string[] args) { 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() != null) Console.WriteLine("Bot config path: " + Path.GetDirectoryName(Path.GetFullPath(Config.GoogleApiConfigPath))); return; } 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.RestartNoSaving(0); 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 StatsStorage.RestoreAsync().ConfigureAwait(false); Config.Log.Debug("Restored stats from persistent storage"); var backgroundTasks = Task.WhenAll( AmdDriverVersionProvider.RefreshAsync(), new PsnScraper().RunAsync(Config.Cts.Token), GameTdbScraper.RunAsync(Config.Cts.Token), new AppveyorClient.Client().GetBuildAsync(Guid.NewGuid().ToString(), Config.Cts.Token), StatsStorage.BackgroundSaveAsync() ); 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, }; 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(); commands.RegisterCommands(); commands.RegisterCommands(); commands.RegisterCommands(); commands.RegisterCommands(); commands.RegisterCommands(); commands.RegisterCommands(); commands.RegisterCommands(); commands.RegisterCommands(); commands.RegisterCommands(); commands.RegisterCommands(); commands.RegisterCommands(); commands.RegisterCommands(); commands.RegisterCommands(); commands.RegisterCommands(); commands.RegisterCommands(); commands.RegisterCommands(); commands.RegisterCommands(); commands.RegisterCommands(); commands.RegisterCommands(); commands.CommandErrored += UnknownCommandHandler.OnError; var interactivityConfig = new InteractivityConfiguration { }; client.UseInteractivity(interactivityConfig); client.Ready += async r => { Config.Log.Info("Bot is ready to serve!"); Config.Log.Info(""); Config.Log.Info($"Bot user id : {r.Client.CurrentUser.Id} ({r.Client.CurrentUser.Username})"); Config.Log.Info($"Bot admin id : {Config.BotAdminId} ({(await r.Client.GetUserAsync(Config.BotAdminId)).Username})"); Config.Log.Info(""); }; client.GuildAvailable += async gaArgs => { await BotStatusMonitor.RefreshAsync(gaArgs.Client).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(gaArgs.Client, gaArgs.Guild).ContinueWith(_ => Config.Log.Info($"Starbucks backlog checked in {gaArgs.Guild.Name}."), TaskScheduler.Default), DiscordInviteFilter.CheckBacklogAsync(gaArgs.Client, 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 += gaArgs => UsernameValidationMonitor.MonitorAsync(gaArgs.Client, true); client.GuildUnavailable += guArgs => { Config.Log.Warn($"{guArgs.Guild.Name} is unavailable"); return Task.CompletedTask; }; 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 += ProductCodeLookup.OnMessageCreated; client.MessageCreated += LogParsingHandler.OnMessageCreated; client.MessageCreated += LogAsTextMonitor.OnMessageCreated; client.MessageCreated += DiscordInviteFilter.OnMessageCreated; client.MessageCreated += PostLogHelpHandler.OnMessageCreated; client.MessageCreated += BotReactionsHandler.OnMessageCreated; client.MessageCreated += AppveyorLinksHandler.OnMessageCreated; client.MessageCreated += GithubLinksHandler.OnMessageCreated; client.MessageCreated += NewBuildsMonitor.OnMessageCreated; client.MessageCreated += TableFlipMonitor.OnMessageCreated; client.MessageCreated += IsTheGamePlayableHandler.OnMessageCreated; client.MessageCreated += EmpathySimulationHandler.OnMessageCreated; client.MessageUpdated += ContentFilterMonitor.OnMessageUpdated; client.MessageUpdated += DiscordInviteFilter.OnMessageUpdated; client.MessageUpdated += EmpathySimulationHandler.OnMessageUpdated; client.MessageDeleted += ThumbnailCacheMonitor.OnMessageDeleted; client.MessageDeleted += EmpathySimulationHandler.OnMessageDeleted; 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; client.DebugLogger.LogMessageReceived += (sender, eventArgs) => { Action logLevel = Config.Log.Info; if (eventArgs.Level == LogLevel.Debug) logLevel = Config.Log.Debug; else if (eventArgs.Level == LogLevel.Info) { //logLevel = Config.Log.Info; if (eventArgs.Message?.Contains("Session resumed") ?? false) Watchdog.DisconnectTimestamps.Clear(); } else if (eventArgs.Level == LogLevel.Warning) { logLevel = Config.Log.Warn; if (eventArgs.Message?.Contains("Dispatch:PRESENCES_REPLACE") ?? false) BotStatusMonitor.RefreshAsync(client).ConfigureAwait(false).GetAwaiter().GetResult(); } else if (eventArgs.Level == LogLevel.Error) logLevel = Config.Log.Error; else if (eventArgs.Level == LogLevel.Critical) { logLevel = Config.Log.Fatal; if ((eventArgs.Message?.Contains("Socket connection terminated") ?? false) || (eventArgs.Message?.Contains("heartbeats were skipped. Issuing reconnect.") ?? false)) Watchdog.DisconnectTimestamps.Enqueue(DateTime.UtcNow); } logLevel(eventArgs.Exception, eventArgs.Message); }; 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; if (SandboxDetector.Detect() == SandboxType.Docker) { 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); db.SaveChanges(); } } if (args.LastOrDefault() is string strCh && ulong.TryParse(strCh, out var chId)) channelId = chId; 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("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 ? "" : $"[{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) ); 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 { ShutdownCheck.Release(); if (singleInstanceCheckThread.IsAlive) singleInstanceCheckThread.Join(100); } if (!Config.inMemorySettings.ContainsKey("shutdown")) Sudo.Bot.Restart(InvalidChannelId); } } }