private static async Task AddOrUpdateThumbnailAsync(string contentId, string name, string url, CancellationToken cancellationToken) { var match = ContentIdMatcher.Match(contentId); if (!match.Success) { return; } var productCode = match.Groups["product_id"].Value; if (!ProductCodeLookup.ProductCode.IsMatch(productCode)) { return; } name = string.IsNullOrEmpty(name) ? null : name; using (var db = new ThumbnailDb()) { var savedItem = db.Thumbnail.FirstOrDefault(t => t.ProductCode == productCode); if (savedItem == null) { var newItem = new Thumbnail { ProductCode = productCode, ContentId = contentId, Name = name, Url = url, Timestamp = DateTime.UtcNow.Ticks, }; db.Thumbnail.Add(newItem); } else if (!string.IsNullOrEmpty(url)) { if (string.IsNullOrEmpty(savedItem.Url)) { savedItem.Url = url; } if (string.IsNullOrEmpty(savedItem.Name) && !string.IsNullOrEmpty(name)) { savedItem.Name = name; } if (!ScrapeStateProvider.IsFresh(savedItem.Timestamp)) { if (savedItem.Url != url) { savedItem.Url = url; savedItem.EmbeddableUrl = null; } if (name != null && savedItem.Name != name) { savedItem.Name = name; } } savedItem.ContentId = contentId; savedItem.Timestamp = DateTime.UtcNow.Ticks; } await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } }
public static async Task SetLastRunTimestampAsync(string locale, string containerId = null) { if (string.IsNullOrEmpty(locale)) { throw new ArgumentException(nameof(locale)); } var id = GetId(locale, containerId); using (var db = new ThumbnailDb()) { var timestamp = db.State.FirstOrDefault(s => s.Locale == id); if (timestamp == null) { db.State.Add(new State { Locale = id, Timestamp = DateTime.UtcNow.Ticks }); } else { timestamp.Timestamp = DateTime.UtcNow.Ticks; } await db.SaveChangesAsync().ConfigureAwait(false); } }
public static async Task OnMessageDeleted(MessageDeleteEventArgs args) { if (args.Channel.Id != Config.ThumbnailSpamId) { return; } if (string.IsNullOrEmpty(args.Message.Content)) { return; } if (!args.Message.Attachments.Any()) { return; } using var db = new ThumbnailDb(); var thumb = db.Thumbnail.FirstOrDefault(i => i.ContentId == args.Message.Content); if (thumb?.EmbeddableUrl is string url && !string.IsNullOrEmpty(url) && args.Message.Attachments.Any(a => a.Url == url)) { thumb.EmbeddableUrl = null; await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); } }
public static async Task <string> GetThumbnailUrlAsync(this DiscordClient client, string productCode) { productCode = productCode.ToUpperInvariant(); var tmdbInfo = await PsnClient.GetTitleMetaAsync(productCode, Config.Cts.Token).ConfigureAwait(false); if (tmdbInfo?.Icon.Url is string tmdbIconUrl) { return(tmdbIconUrl); } using (var db = new ThumbnailDb()) { var thumb = await db.Thumbnail.FirstOrDefaultAsync(t => t.ProductCode == productCode).ConfigureAwait(false); //todo: add search task if not found if (thumb?.EmbeddableUrl is string embeddableUrl && !string.IsNullOrEmpty(embeddableUrl)) { return(embeddableUrl); } if (string.IsNullOrEmpty(thumb?.Url) || !ScrapeStateProvider.IsFresh(thumb.Timestamp)) { var gameTdbCoverUrl = await GameTdbScraper.GetThumbAsync(productCode).ConfigureAwait(false); if (!string.IsNullOrEmpty(gameTdbCoverUrl)) { if (thumb == null) { var addResult = await db.Thumbnail.AddAsync(new Thumbnail { ProductCode = productCode, Url = gameTdbCoverUrl }).ConfigureAwait(false); thumb = addResult.Entity; } else { thumb.Url = gameTdbCoverUrl; } thumb.Timestamp = DateTime.UtcNow.Ticks; await db.SaveChangesAsync().ConfigureAwait(false); } } if (thumb?.Url is string url && !string.IsNullOrEmpty(url)) { var contentName = (thumb.ContentId ?? thumb.ProductCode); var embed = await GetEmbeddableUrlAsync(client, contentName, url).ConfigureAwait(false); if (embed.url != null) { thumb.EmbeddableUrl = embed.url; await db.SaveChangesAsync().ConfigureAwait(false); return(embed.url); } } } return(null); }
public static async Task <(string url, DiscordColor color)> GetThumbnailUrlWithColorAsync(DiscordClient client, string contentId, DiscordColor defaultColor, string url = null) { if (string.IsNullOrEmpty(contentId)) { throw new ArgumentException("ContentID can't be empty", nameof(contentId)); } contentId = contentId.ToUpperInvariant(); using var db = new ThumbnailDb(); var info = await db.Thumbnail.FirstOrDefaultAsync(ti => ti.ContentId == contentId, Config.Cts.Token).ConfigureAwait(false); info ??= new Thumbnail { Url = url }; if (info.Url == null) { return(null, defaultColor); } DiscordColor?analyzedColor = null; if (string.IsNullOrEmpty(info.EmbeddableUrl)) { var em = await GetEmbeddableUrlAsync(client, contentId, info.Url).ConfigureAwait(false); if (em.url is string eUrl) { info.EmbeddableUrl = eUrl; if (em.image is byte[] jpg) { Config.Log.Trace("Getting dominant color for " + eUrl); analyzedColor = ColorGetter.Analyze(jpg, defaultColor); var c = analyzedColor.Value.Value; if (c != defaultColor.Value) { info.EmbedColor = c; } } await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); } } if ((!info.EmbedColor.HasValue && !analyzedColor.HasValue) || (info.EmbedColor.HasValue && info.EmbedColor.Value == defaultColor.Value)) { var c = await GetImageColorAsync(info.EmbeddableUrl, defaultColor).ConfigureAwait(false); if (c.HasValue && c.Value.Value != defaultColor.Value) { info.EmbedColor = c.Value.Value; await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); } } var color = info.EmbedColor.HasValue ? new DiscordColor(info.EmbedColor.Value) : defaultColor; return(info.EmbeddableUrl, color); }
public static async Task <(string url, DiscordColor color)> GetThumbnailUrlWithColorAsync(DiscordClient client, string contentId, DiscordColor defaultColor, string url = null) { if (string.IsNullOrEmpty(contentId)) { throw new ArgumentException("ContentID can't be empty", nameof(contentId)); } contentId = contentId.ToUpperInvariant(); using (var db = new ThumbnailDb()) { var info = await db.TitleInfo.FirstOrDefaultAsync(ti => ti.ContentId == contentId, Config.Cts.Token).ConfigureAwait(false); if (info == null) { info = new TitleInfo { ContentId = contentId, ThumbnailUrl = url, Timestamp = DateTime.UtcNow.Ticks }; var thumb = await db.Thumbnail.FirstOrDefaultAsync(t => t.ContentId == contentId).ConfigureAwait(false); if (thumb?.EmbeddableUrl is string eUrl && thumb.Url is string thumbUrl && thumbUrl == url) { info.ThumbnailEmbeddableUrl = eUrl; } info = db.TitleInfo.Add(info).Entity; await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); } if (string.IsNullOrEmpty(info.ThumbnailEmbeddableUrl)) { var em = await GetEmbeddableUrlAsync(client, contentId, info.ThumbnailUrl).ConfigureAwait(false); if (em.url is string eUrl) { info.ThumbnailEmbeddableUrl = eUrl; if (em.image is byte[] jpg) { info.EmbedColor = ColorGetter.Analyze(jpg, defaultColor).Value; } await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); } } if (!info.EmbedColor.HasValue) { var c = await GetImageColorAsync(info.ThumbnailEmbeddableUrl, defaultColor).ConfigureAwait(false); if (c.HasValue) { info.EmbedColor = c.Value.Value; await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); } } var color = info.EmbedColor.HasValue ? new DiscordColor(info.EmbedColor.Value) : defaultColor; return(info.ThumbnailEmbeddableUrl, color); } }
public static bool IsFresh(string locale, DateTime dataTimestamp) { using var db = new ThumbnailDb(); var timestamp = string.IsNullOrEmpty(locale) ? db.State.OrderBy(s => s.Timestamp).FirstOrDefault() : db.State.FirstOrDefault(s => s.Locale == locale); if (timestamp?.Timestamp is long checkDate && checkDate > 0) { return(new DateTime(checkDate, DateTimeKind.Utc) > dataTimestamp); } return(false); }
public static async Task SaveAsync(TSyscallStats syscallInfo) { if (syscallInfo == null || syscallInfo.Count == 0) { return; } if (await Limiter.WaitAsync(1000, Config.Cts.Token)) { try { using var db = new ThumbnailDb(); foreach (var productCodeMap in syscallInfo) { var product = db.Thumbnail.AsNoTracking().FirstOrDefault(t => t.ProductCode == productCodeMap.Key) ?? db.Thumbnail.Add(new Thumbnail { ProductCode = productCodeMap.Key }).Entity; if (product.Id == 0) { await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); } foreach (var moduleMap in productCodeMap.Value) { foreach (var func in moduleMap.Value) { var syscall = db.SyscallInfo.AsNoTracking().FirstOrDefault(sci => sci.Module == moduleMap.Key.ToUtf8() && sci.Function == func.ToUtf8()) ?? db.SyscallInfo.Add(new SyscallInfo { Module = moduleMap.Key.ToUtf8(), Function = func.ToUtf8() }).Entity; if (syscall.Id == 0) { await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); } if (!db.SyscallToProductMap.Any(m => m.ProductId == product.Id && m.SyscallInfoId == syscall.Id)) { db.SyscallToProductMap.Add(new SyscallToProductMap { ProductId = product.Id, SyscallInfoId = syscall.Id }); } } } } await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); } finally { Limiter.Release(); } } }
public static async Task <string> GetTitleNameAsync(string productCode, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(productCode)) { return(null); } productCode = productCode.ToUpperInvariant(); using (var db = new ThumbnailDb()) { var thumb = await db.Thumbnail.FirstOrDefaultAsync( t => t.ProductCode == productCode, cancellationToken : cancellationToken ).ConfigureAwait(false); if (thumb?.Name is string title) { return(title); } var meta = await PsnClient.GetTitleMetaAsync(productCode, cancellationToken).ConfigureAwait(false); title = meta?.Name; try { if (!string.IsNullOrEmpty(title)) { if (thumb == null) { thumb = ( await db.Thumbnail.AddAsync(new Thumbnail { ProductCode = productCode, Name = title, }, cancellationToken).ConfigureAwait(false) ).Entity; } else { thumb.Name = title; } await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } } catch (Exception e) { Config.Log.Warn(e); } return(title); } }
public static string GetTitleName(string productCode) { if (string.IsNullOrEmpty(productCode)) { return(null); } using (var db = new ThumbnailDb()) { var thumb = db.Thumbnail.FirstOrDefault(t => t.ProductCode == productCode.ToUpperInvariant()); return(thumb?.Name); } }
public async Task Rescan(CommandContext ctx) { using var db = new ThumbnailDb(); var items = db.State.ToList(); foreach (var state in items) { state.Timestamp = 0; } await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); await ctx.ReactWithAsync(Config.Reactions.Success, "Reset state timestamps").ConfigureAwait(false); }
public static bool IsFresh(string locale, string?containerId = null) { var id = GetId(locale, containerId); using var db = new ThumbnailDb(); var timestamp = string.IsNullOrEmpty(id) ? db.State.OrderBy(s => s.Timestamp).FirstOrDefault() : db.State.FirstOrDefault(s => s.Locale == id); if (timestamp?.Timestamp is long checkDate && checkDate > 0) { return(IsFresh(new DateTime(checkDate, DateTimeKind.Utc))); } return(false); }
public static async Task SearchForGame(CommandContext ctx, string search, int maxResults) { var ch = await ctx.GetChannelForSpamAsync().ConfigureAwait(false); DiscordMessage msg = null; try { if (string.IsNullOrEmpty(search)) { var interact = ctx.Client.GetInteractivity(); msg = await msg.UpdateOrCreateMessageAsync(ch, "What game are you looking for?").ConfigureAwait(false); var response = await interact.WaitForMessageAsync(m => m.Author == ctx.User && m.Channel == ch).ConfigureAwait(false); await msg.DeleteAsync().ConfigureAwait(false); msg = null; if (string.IsNullOrEmpty(response.Result?.Content)) { await ctx.ReactWithAsync(Config.Reactions.Failure).ConfigureAwait(false); return; } search = response.Result.Content; } string titleId = null; var productIds = ProductCodeLookup.GetProductIds(search); if (productIds.Count > 0) { using var db = new ThumbnailDb(); var contentId = await db.Thumbnail.FirstOrDefaultAsync(t => t.ProductCode == productIds[0].ToUpperInvariant()).ConfigureAwait(false); if (contentId?.ContentId != null) { titleId = contentId.ContentId; } if (contentId?.Name != null) { search = contentId.Name; } } var alteredSearch = search.Trim(); if (alteredSearch.EndsWith("demo", StringComparison.InvariantCultureIgnoreCase)) { alteredSearch = alteredSearch[..^ 4].TrimEnd();
public async Task Fix(CommandContext ctx, [Description("Product ID to reset")] string productId) { var count = 0; using (var db = new ThumbnailDb()) { var items = db.Thumbnail.Where(i => i.ProductCode == productId && !string.IsNullOrEmpty(i.EmbeddableUrl)); foreach (var thumb in items) { thumb.EmbeddableUrl = null; } count = await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); } await ctx.RespondAsync($"Removed {count} cached links").ConfigureAwait(false); }
private static bool NeedLookup(string contentId) { using (var db = new ThumbnailDb()) if (db.Thumbnail.FirstOrDefault(t => t.ContentId == contentId) is Thumbnail thumbnail) { if (!string.IsNullOrEmpty(thumbnail.Url)) { if (ScrapeStateProvider.IsFresh(new DateTime(thumbnail.Timestamp, DateTimeKind.Utc))) { return(false); } } } return(true); }
public static async Task CleanAsync(CancellationToken cancellationToken) { using (var db = new ThumbnailDb()) { var latestTimestamp = db.State.OrderByDescending(s => s.Timestamp).FirstOrDefault()?.Timestamp; if (!latestTimestamp.HasValue) { return; } var cutOff = new DateTime(latestTimestamp.Value, DateTimeKind.Utc).Add(-CheckInterval); var oldItems = db.State.Where(s => s.Timestamp < cutOff.Ticks); db.State.RemoveRange(oldItems); await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } }
public async Task Rename(CommandContext ctx, [Description("Product code such as BLUS12345")] string productCode, [RemainingText, Description("New game title to save in the database")] string title) { productCode = productCode.ToUpperInvariant(); using var db = new ThumbnailDb(); var item = db.Thumbnail.FirstOrDefault(t => t.ProductCode == productCode); if (item == null) { await ctx.ReactWithAsync(Config.Reactions.Failure, $"Unknown product code {productCode}", true).ConfigureAwait(false); } else { item.Name = title; await db.SaveChangesAsync().ConfigureAwait(false); await ctx.ReactWithAsync(Config.Reactions.Success, "Title updated successfully").ConfigureAwait(false); } }
public async Task Fix(CommandContext ctx, [Description("Product ID to reset")] string productId) { var linksToRemove = new List <(string contentId, string link)>(); using (var db = new ThumbnailDb()) { var items = db.Thumbnail.Where(i => i.ProductCode == productId && !string.IsNullOrEmpty(i.EmbeddableUrl)); foreach (var thumb in items) { linksToRemove.Add((thumb.ContentId, thumb.EmbeddableUrl)); thumb.EmbeddableUrl = null; } await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); } await TryDeleteThumbnailCache(ctx, linksToRemove).ConfigureAwait(false); await ctx.RespondAsync($"Removed {linksToRemove.Count} cached links").ConfigureAwait(false); }
private void AppendSyscallsStats(DiscordEmbedBuilder embed) { using (var db = new ThumbnailDb()) { var syscallCount = db.SyscallInfo.Where(sci => sci.Function.StartsWith("sys_")).Distinct().Count(); var syscallModuleCount = db.SyscallInfo.Where(sci => sci.Function.StartsWith("sys_")).Select(sci => sci.Module).Distinct().Count(); var totalFuncCount = db.SyscallInfo.Select(sci => sci.Function).Distinct().Count(); var totalModuleCount = db.SyscallInfo.Select(sci => sci.Module).Distinct().Count(); var fwCallCount = totalFuncCount - syscallCount; var fwModuleCount = totalModuleCount - syscallModuleCount; var gameCount = db.SyscallToProductMap.Select(m => m.ProductId).Distinct().Count(); embed.AddField("SceCall Stats", $"Tracked game IDs: {gameCount}\n" + $"Tracked syscalls: {syscallCount} function{(syscallCount == 1 ? "" : "s")} in {syscallModuleCount} module{(syscallModuleCount == 1 ? "" : "s")}\n" + $"Tracked fw calls: {fwCallCount} function{(fwCallCount == 1 ? "" : "s")} in {fwModuleCount} module{(fwModuleCount == 1 ? "" : "s")}\n", true); } }
public async Task Add(CommandContext ctx, [Description("Product code such as BLUS12345")] string contentId, [RemainingText, Description("New game title to save in the database")] string title) { contentId = contentId.ToUpperInvariant(); var productCode = contentId; var productCodeMatch = ProductCodeLookup.ProductCode.Match(contentId); var contentIdMatch = PsnScraper.ContentIdMatcher.Match(contentId); if (contentIdMatch.Success) { productCode = contentIdMatch.Groups["product_id"].Value; } else if (productCodeMatch.Success) { productCode = productCodeMatch.Groups["letters"].Value + productCodeMatch.Groups["numbers"].Value; contentId = null; } else { await ctx.ReactWithAsync(Config.Reactions.Failure, "Invalid content id", true).ConfigureAwait(false); return; } using var db = new ThumbnailDb(); var item = db.Thumbnail.FirstOrDefault(t => t.ProductCode == productCode); if (item != null) { await ctx.ReactWithAsync(Config.Reactions.Failure, $"Product code {contentId} already exists", true).ConfigureAwait(false); } else { item = new Thumbnail { ProductCode = contentId, ContentId = contentId, Name = title, }; db.Add(item); await db.SaveChangesAsync().ConfigureAwait(false); await ctx.ReactWithAsync(Config.Reactions.Success, "Title added successfully").ConfigureAwait(false); } }
private static void AppendSyscallsStats(DiscordEmbedBuilder embed) { try { using var db = new ThumbnailDb(); var syscallCount = db.SyscallInfo.AsNoTracking().Where(sci => sci.Function.StartsWith("sys_") || sci.Function.StartsWith("_sys_")).Distinct().Count(); var totalFuncCount = db.SyscallInfo.AsNoTracking().Select(sci => sci.Function).Distinct().Count(); var fwCallCount = totalFuncCount - syscallCount; var gameCount = db.SyscallToProductMap.AsNoTracking().Select(m => m.ProductId).Distinct().Count(); embed.AddField("SceCall Stats", $"Tracked game IDs: {gameCount}\n" + $"Tracked syscalls: {syscallCount} function{(syscallCount == 1 ? "" : "s")}\n" + $"Tracked fw calls: {fwCallCount} function{(fwCallCount == 1 ? "" : "s")}\n", true); } catch (Exception e) { Config.Log.Warn(e); } }
private async Task ReturnSyscallsByGameAsync(CommandContext ctx, string productId) { productId = productId.ToUpperInvariant(); string title = null; using (var db = new ThumbnailDb()) { title = db.Thumbnail.FirstOrDefault(t => t.ProductCode == productId)?.Name; title = string.IsNullOrEmpty(title) ? productId : $"[{productId}] {title.Trim(40)}"; var sysInfoList = await db.SyscallToProductMap.AsNoTracking() .Where(m => m.Product.ProductCode == productId) .Select(m => m.SyscallInfo) .Distinct() .ToAsyncEnumerable() .OrderBy(sci => sci.Module) .ThenBy(sci => sci.Function) .ToList() .ConfigureAwait(false); if (ctx.User.Id == 216724245957312512UL) { sysInfoList = sysInfoList.Where(i => i.Function.StartsWith("sys_", StringComparison.InvariantCultureIgnoreCase)).ToList(); } if (sysInfoList.Any()) { var result = new StringBuilder($"List of syscalls used by `{title}`:```").AppendLine(); foreach (var sci in sysInfoList) { result.AppendLine($"{sci.Module}: {sci.Function}"); } await ctx.SendAutosplitMessageAsync(result.Append("```")).ConfigureAwait(false); } else { await ctx.RespondAsync($"No information available for `{title}`").ConfigureAwait(false); } } }
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); } }
private async Task ReturnSyscallsByGameAsync(CommandContext ctx, string productId) { productId = productId.ToUpperInvariant(); using var db = new ThumbnailDb(); var title = db.Thumbnail.FirstOrDefault(t => t.ProductCode == productId)?.Name; title = string.IsNullOrEmpty(title) ? productId : $"[{productId}] {title.Trim(40)}"; var sysInfoList = db.SyscallToProductMap.AsNoTracking() .Where(m => m.Product.ProductCode == productId) .Select(m => m.SyscallInfo) .Distinct() .AsEnumerable() .OrderBy(sci => sci.Function.TrimStart('_')) .ToList(); if (sysInfoList.Any()) { var result = new StringBuilder(); foreach (var sci in sysInfoList) { result.AppendLine(sci.Function); } using var memoryStream = Config.MemoryStreamManager.GetStream(); using var streamWriter = new StreamWriter(memoryStream, Encoding.UTF8); await streamWriter.WriteAsync(result).ConfigureAwait(false); await streamWriter.FlushAsync().ConfigureAwait(false); memoryStream.Seek(0, SeekOrigin.Begin); await ctx.RespondWithFileAsync($"{productId} syscalls.txt", memoryStream, $"List of syscalls used by `{title}`").ConfigureAwait(false); } else { await ctx.RespondAsync($"No information available for `{title}`").ConfigureAwait(false); } }
public async Task Rename(CommandContext ctx, [Description("Old function name")] string oldFunctionName, [Description("New function name")] string newFunctionName) { using var db = new ThumbnailDb(); var oldMatches = await db.SyscallInfo.Where(sci => sci.Function == oldFunctionName).ToListAsync().ConfigureAwait(false); if (oldMatches.Count == 0) { await ctx.RespondAsync($"Function `{oldFunctionName}` could not be found").ConfigureAwait(false); await Search(ctx, oldFunctionName).ConfigureAwait(false); return; } if (oldMatches.Count > 1) { await ctx.RespondAsync("More than one matching function was found, I can't handle this right now 😔").ConfigureAwait(false); await Search(ctx, oldFunctionName).ConfigureAwait(false); return; } var conflicts = await db.SyscallInfo.Where(sce => sce.Function == newFunctionName).AnyAsync().ConfigureAwait(false); if (conflicts) { await ctx.RespondAsync($"There is already a function `{newFunctionName}`").ConfigureAwait(false); await Search(ctx, newFunctionName).ConfigureAwait(false); return; } oldMatches[0].Function = newFunctionName; await db.SaveChangesAsync().ConfigureAwait(false); await ctx.RespondAsync($"Function `{oldFunctionName}` was successfully renamed to `{newFunctionName}`").ConfigureAwait(false); }
public static async Task <string> GetThumbnailUrlAsync(this DiscordClient client, string productCode) { productCode = productCode.ToUpperInvariant(); using (var db = new ThumbnailDb()) { var thumb = await db.Thumbnail.FirstOrDefaultAsync(t => t.ProductCode == productCode.ToUpperInvariant()).ConfigureAwait(false); //todo: add search task if not found if (thumb?.EmbeddableUrl is string embeddableUrl && !string.IsNullOrEmpty(embeddableUrl)) { return(embeddableUrl); } if (string.IsNullOrEmpty(thumb?.Url) || !ScrapeStateProvider.IsFresh(thumb.Timestamp)) { var gameTdbCoverUrl = await GameTdbScraper.GetThumbAsync(productCode).ConfigureAwait(false); if (!string.IsNullOrEmpty(gameTdbCoverUrl)) { if (thumb == null) { var addResult = await db.Thumbnail.AddAsync(new Thumbnail { ProductCode = productCode, Url = gameTdbCoverUrl }).ConfigureAwait(false); thumb = addResult.Entity; } else { thumb.Url = gameTdbCoverUrl; } thumb.Timestamp = DateTime.UtcNow.Ticks; await db.SaveChangesAsync().ConfigureAwait(false); } } if (thumb?.Url is string url && !string.IsNullOrEmpty(url)) { if (!string.IsNullOrEmpty(Path.GetExtension(url))) { thumb.EmbeddableUrl = url; await db.SaveChangesAsync().ConfigureAwait(false); return(url); } try { using (var imgStream = await HttpClient.GetStreamAsync(url).ConfigureAwait(false)) using (var memStream = new MemoryStream()) { await imgStream.CopyToAsync(memStream).ConfigureAwait(false); // minimum jpg size is 119 bytes, png is 67 bytes if (memStream.Length < 64) { return(null); } memStream.Seek(0, SeekOrigin.Begin); var spam = await client.GetChannelAsync(Config.ThumbnailSpamId).ConfigureAwait(false); //var message = await spam.SendFileAsync(memStream, (thumb.ContentId ?? thumb.ProductCode) + ".jpg").ConfigureAwait(false); var contentName = (thumb.ContentId ?? thumb.ProductCode); var message = await spam.SendFileAsync(contentName + ".jpg", memStream, contentName).ConfigureAwait(false); thumb.EmbeddableUrl = message.Attachments.First().Url; await db.SaveChangesAsync().ConfigureAwait(false); return(thumb.EmbeddableUrl); } } catch (Exception e) { Config.Log.Warn(e); } } } return(null); }
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 <(int funcs, int links)> FixInvalidFunctionNamesAsync() { var syscallStats = new TSyscallStats(); int funcs = 0, links = 0; using (var db = new ThumbnailDb()) { var funcsToFix = new List <SyscallInfo>(0); try { funcsToFix = await db.SyscallInfo.Where(sci => sci.Function.Contains('(')).ToListAsync().ConfigureAwait(false); funcs = funcsToFix.Count; if (funcs == 0) { return(0, 0); } foreach (var sci in funcsToFix) { var productIds = await db.SyscallToProductMap.AsNoTracking().Where(m => m.SyscallInfoId == sci.Id).Select(m => m.Product.ProductCode).Distinct().ToListAsync().ConfigureAwait(false); links += productIds.Count; foreach (var productId in productIds) { if (!syscallStats.TryGetValue(productId, out var scInfo)) { syscallStats[productId] = scInfo = new Dictionary <string, HashSet <string> >(); } if (!scInfo.TryGetValue(sci.Module, out var smInfo)) { scInfo[sci.Module] = smInfo = new HashSet <string>(); } smInfo.Add(sci.Function.Split('(', 2)[0]); } } } catch (Exception e) { Config.Log.Warn(e, "Failed to build fixed syscall mappings"); throw e; } await SaveAsync(syscallStats).ConfigureAwait(false); if (await Limiter.WaitAsync(1000, Config.Cts.Token)) { try { db.SyscallInfo.RemoveRange(funcsToFix); await db.SaveChangesAsync().ConfigureAwait(false); } catch (Exception e) { Config.Log.Warn(e, "Failed to remove broken syscall mappings"); throw e; } finally { Limiter.Release(); } } } return(funcs, links); }
public static async Task <(int funcs, int links)> FixDuplicatesAsync() { int funcs = 0, links = 0; using (var db = new ThumbnailDb()) { var duplicateFunctionNames = await db.SyscallInfo.Where(sci => db.SyscallInfo.Count(isci => isci.Function == sci.Function && isci.Module == sci.Module) > 1).Distinct().ToListAsync().ConfigureAwait(false); if (duplicateFunctionNames.Count == 0) { return(0, 0); } if (await Limiter.WaitAsync(1000, Config.Cts.Token)) { try { foreach (var dupFunc in duplicateFunctionNames) { var dups = db.SyscallInfo.Where(sci => sci.Function == dupFunc.Function && sci.Module == dupFunc.Module).ToList(); if (dups.Count < 2) { continue; } var mostCommonDup = dups.Select(dup => (dup, count: db.SyscallToProductMap.Count(scm => scm.SyscallInfoId == dup.Id))).OrderByDescending(stat => stat.count).First().dup; var dupsToRemove = dups.Where(df => df.Id != mostCommonDup.Id).ToList(); funcs += dupsToRemove.Count; foreach (var dupToRemove in dupsToRemove) { var mappings = db.SyscallToProductMap.Where(scm => scm.SyscallInfoId == dupToRemove.Id).ToList(); links += mappings.Count; foreach (var mapping in mappings) { if (!db.SyscallToProductMap.Any(scm => scm.ProductId == mapping.ProductId && scm.SyscallInfoId == mostCommonDup.Id)) { db.SyscallToProductMap.Add(new SyscallToProductMap { ProductId = mapping.ProductId, SyscallInfoId = mostCommonDup.Id }); } } } await db.SaveChangesAsync().ConfigureAwait(false); db.SyscallInfo.RemoveRange(dupsToRemove); await db.SaveChangesAsync().ConfigureAwait(false); } await db.SaveChangesAsync().ConfigureAwait(false); } catch (Exception e) { Config.Log.Warn(e, "Failed to remove duplicate syscall entries"); throw; } finally { Limiter.Release(); } } } return(funcs, links); }
public async Task Search(CommandContext ctx, [RemainingText, Description("Product ID, module, or function name. **Case sensitive**")] string search) { if (string.IsNullOrEmpty(search)) { await ctx.ReactWithAsync(Config.Reactions.Failure, "No meaningful search query provided").ConfigureAwait(false); return; } var productCodes = ProductCodeLookup.GetProductIds(search); if (productCodes.Any()) { await ReturnSyscallsByGameAsync(ctx, productCodes.First()).ConfigureAwait(false); return; } if (ctx.User.Id == 216724245957312512UL && !search.StartsWith("sys_", StringComparison.InvariantCultureIgnoreCase)) { await ctx.RespondAsync($"This is not a _syscall_, {ctx.User.Mention}").ConfigureAwait(false); return; } using var db = new ThumbnailDb(); if (db.SyscallInfo.Any(sci => sci.Module == search || sci.Function == search)) { var productInfoList = db.SyscallToProductMap.AsNoTracking() .Where(m => m.SyscallInfo.Module == search || m.SyscallInfo.Function == search) .Select(m => new { m.Product.ProductCode, Name = m.Product.Name.StripMarks() ?? "???" }) .Distinct() .ToList(); var groupedList = productInfoList .GroupBy(m => m.Name, m => m.ProductCode, StringComparer.InvariantCultureIgnoreCase) .OrderBy(g => g.Key, StringComparer.OrdinalIgnoreCase) .ToList(); if (groupedList.Any()) { var bigList = groupedList.Count >= Config.MaxSyscallResultLines; var result = new StringBuilder(); var fullList = bigList ? new StringBuilder() : null; result.AppendLine($"List of games using `{search}`:```"); var c = 0; foreach (var gi in groupedList) { var productIds = string.Join(", ", gi.Distinct().OrderBy(pc => pc).AsEnumerable()); if (c < Config.MaxSyscallResultLines) { result.AppendLine($"{gi.Key.Trim(60)} [{productIds}]"); } if (bigList) { fullList.AppendLine($"{gi.Key} [{productIds}]"); } c++; } await ctx.SendAutosplitMessageAsync(result.Append("```")).ConfigureAwait(false); if (bigList) { using var memoryStream = new MemoryStream((int)(fullList.Capacity * 1.25)); using var streamWriter = new StreamWriter(memoryStream, Encoding.UTF8); await streamWriter.WriteAsync(fullList).ConfigureAwait(false); await streamWriter.FlushAsync().ConfigureAwait(false); memoryStream.Seek(0, SeekOrigin.Begin); await ctx.RespondWithFileAsync($"{search}.txt", memoryStream, $"See attached file for full list of {groupedList.Count} entries").ConfigureAwait(false); } } else { await ctx.RespondAsync($"No games found that use `{search}`").ConfigureAwait(false); } } else { var result = new StringBuilder("Unknown entity name"); var modules = await db.SyscallInfo.Select(sci => sci.Module).Distinct().ToListAsync().ConfigureAwait(false); var substrModules = modules.Where(m => m.Contains(search, StringComparison.CurrentCultureIgnoreCase)); var fuzzyModules = modules .Select(m => (name: m, score: search.GetFuzzyCoefficientCached(m))) .Where(i => i.score > 0.6) .OrderByDescending(i => i.score) .Select(i => i.name) .ToList(); modules = substrModules .Concat(fuzzyModules) .Distinct() .OrderBy(m => m, StringComparer.OrdinalIgnoreCase) .ToList(); var modulesFound = modules.Any(); if (modulesFound) { result.AppendLine(", possible modules:```"); foreach (var m in modules) { result.AppendLine(m); } result.AppendLine("```"); } var functions = await db.SyscallInfo.Select(sci => sci.Function).Distinct().ToListAsync().ConfigureAwait(false); var substrFuncs = functions.Where(f => f.Contains(search, StringComparison.InvariantCultureIgnoreCase)); var fuzzyFuncs = functions .Select(f => (name: f, score: search.GetFuzzyCoefficientCached(f))) .Where(i => i.score > 0.6) .OrderByDescending(i => i.score) .Select(i => i.name) .ToList(); functions = substrFuncs .Concat(fuzzyFuncs) .Distinct() .OrderBy(f => f, StringComparer.OrdinalIgnoreCase) .ToList(); var functionsFound = functions.Any(); if (functionsFound) { if (modulesFound) { result.AppendLine("Possible functions:```"); } else { result.AppendLine(", possible functions:```"); } foreach (var f in functions) { result.AppendLine(f); } result.AppendLine("```"); } await ctx.SendAutosplitMessageAsync(result).ConfigureAwait(false); } }